From 439284060f0c4ed2eceedc96e50baabde7bac63b Mon Sep 17 00:00:00 2001 From: llblab Date: Mon, 24 Aug 2026 23:24:39 +0400 Subject: [PATCH] 0.7.24: scale block-paced Actors and close Asset Conversion trust boundaries --- .agents/skills/README.md | 2 +- .../alignment/ledgers/ambiguities.jsonl | 6 + .../skills/alignment/ledgers/dead_ends.jsonl | 9 + .../alignment/ledgers/hallucinations.jsonl | 38 + .../rules/actors-identity-rules.json | 6 +- .../audit-protocol-coherence-regressions.sh | 118 +- .agents/skills/alignment/scripts/auditor.sh | 4 +- .../skills/architecture-experiments/SKILL.md | 336 + .../scripts/validate-record-normalization.sh | 74 + .../templates/EXP-NNNN.md | 141 + .../tracks/actors/EXP-0001.md | 173 + .../tracks/actors/EXP-0002.md | 259 + .../tracks/actors/EXP-0003.md | 168 + .../tracks/actors/EXP-0005.md | 215 + .../tracks/actors/EXP-0006.md | 186 + .../tracks/actors/EXP-0008.md | 160 + .../p128-crossing_placed_maximum_unit.rs | 99 + ...8-crossing_placed_non_tail_emptied_unit.rs | 99 + ...8-crossing_placed_non_tail_trimmed_unit.rs | 99 + .../p128-crossing_tail_refill_probe.rs | 52 + .../p256-crossing_placed_maximum_unit.rs | 99 + ...6-crossing_placed_non_tail_emptied_unit.rs | 99 + ...6-crossing_placed_non_tail_trimmed_unit.rs | 99 + .../p256-crossing_tail_refill_probe.rs | 52 + .../p64-crossing_placed_maximum_unit.rs | 99 + ...4-crossing_placed_non_tail_emptied_unit.rs | 99 + ...4-crossing_placed_non_tail_trimmed_unit.rs | 99 + .../p64-crossing_tail_refill_probe.rs | 52 + .../tracks/actors/EXP-0009.md | 179 + .../crossing_coalesced_cohort_preflight.rs | 75 + .../crossing_fire_cohort_preflight.rs | 75 + .../EXP-0009/crossing_placed_maximum_unit.rs | 99 + .../crossing_placed_non_tail_emptied_unit.rs | 99 + .../crossing_placed_non_tail_trimmed_unit.rs | 99 + .../crossing_rearm_cohort_preflight.rs | 64 + .../crossing_skip_cohort_preflight.rs | 60 + .../crossing_terminal_cohort_preflight.rs | 75 + ...nal-crossing_coalesced_cohort_preflight.rs | 73 + .../final-crossing_fire_cohort_preflight.rs | 73 + .../final-crossing_placed_maximum_unit.rs | 97 + ...l-crossing_placed_non_tail_emptied_unit.rs | 97 + ...l-crossing_placed_non_tail_trimmed_unit.rs | 97 + .../final-crossing_rearm_cohort_preflight.rs | 68 + .../final-crossing_skip_cohort_preflight.rs | 68 + ...inal-crossing_terminal_cohort_preflight.rs | 73 + .../h0-crossing_coalesced_cohort_preflight.rs | 73 + .../h0-crossing_fire_cohort_preflight.rs | 73 + .../h0-crossing_placed_maximum_unit.rs | 97 + ...0-crossing_placed_non_tail_emptied_unit.rs | 97 + ...0-crossing_placed_non_tail_trimmed_unit.rs | 97 + .../h0-crossing_rearm_cohort_preflight.rs | 68 + .../h0-crossing_skip_cohort_preflight.rs | 68 + .../h0-crossing_terminal_cohort_preflight.rs | 73 + ...4-crossing_placed_non_tail_emptied_unit.rs | 97 + ...4-crossing_placed_non_tail_trimmed_unit.rs | 97 + .../tracks/actors/EXP-0010.md | 198 + .../tracks/actors/EXP-0013.md | 173 + .../tracks/actors/EXP-0016.md | 193 + .../tracks/actors/EXP-0017.md | 165 + .../tracks/actors/EXP-0018.md | 174 + .../tracks/actors/EXP-0019.md | 238 + .../tracks/actors/EXP-0020.md | 163 + .../tracks/actors/EXP-0021.md | 162 + .../tracks/actors/EXP-0022.md | 157 + .../tracks/actors/EXP-0023.md | 252 + .../tracks/actors/EXP-0024.md | 184 + .../tracks/actors/experiments.md | 92 + .../tracks/adapters/experiments.md | 52 + .../tracks/router/experiments.md | 51 + .agents/skills/benchmarking/SKILL.md | 90 - .../evidence/runtime-weight-delta-ledger.md | 275 +- .../references/runtime-threat-boundaries.md | 6 + .../scripts/generate-weight-delta-ledger.mjs | 2 +- AGENTS.md | 9 +- BACKLOG.md | 500 +- CHANGELOG.md | 11 + docs/README.md | 3 + docs/actors-control-plane.contract.en.md | 4 +- ...-performance-assurance.specification.en.md | 284 + ...actors-resource-policy.specification.en.md | 242 + docs/actors.integration.en.md | 100 +- docs/asset-conversion.integration.en.md | 104 + docs/core.architecture.en.md | 2 +- docs/oracle.integration.en.md | 8 +- scripts/README.md | 11 +- scripts/_common.sh | 17 +- scripts/actors-assurance.sh | 89 +- scripts/audit-asset-conversion-boundaries.sh | 60 + scripts/benchmarks.sh | 83 +- scripts/export-papi-metadata.sh | 2 +- scripts/validate-local.sh | 7 +- template/Cargo.lock | 39 +- template/Cargo.toml | 10 +- template/pallets/actors/Cargo.toml | 1 + template/pallets/actors/README.md | 8 +- .../pallets/actors/docs/architecture.en.md | 245 +- template/pallets/actors/docs/embedding.md | 46 +- .../pallets/actors/docs/specification.en.md | 2778 +++++---- .../actors/embedding-runtime/README.md | 4 +- .../actors/embedding-runtime/src/lib.rs | 277 +- template/pallets/actors/src/adapters.rs | 158 +- template/pallets/actors/src/benchmarking.rs | 5383 ++++++++++++++++- template/pallets/actors/src/crossing.rs | 461 +- template/pallets/actors/src/execution.rs | 1013 +++- template/pallets/actors/src/lib.rs | 3734 ++++++++++-- template/pallets/actors/src/mock.rs | 157 +- template/pallets/actors/src/reactions.rs | 484 +- template/pallets/actors/src/scheduler.rs | 2589 ++++++-- template/pallets/actors/src/subscriptions.rs | 18 + template/pallets/actors/src/tests.rs | 422 +- template/pallets/actors/src/tests/core.rs | 252 +- template/pallets/actors/src/tests/crossing.rs | 671 +- .../pallets/actors/src/tests/execution.rs | 936 ++- .../actors/src/tests/fees_and_funding.rs | 1966 ++++-- .../pallets/actors/src/tests/lifecycle.rs | 181 +- .../pallets/actors/src/tests/market_tasks.rs | 27 +- .../pallets/actors/src/tests/observations.rs | 965 ++- .../pallets/actors/src/tests/scheduling.rs | 1178 +++- .../actors/src/tests/storage_and_api.rs | 1644 ++++- template/pallets/actors/src/tests/wakeups.rs | 172 +- template/pallets/actors/src/types.rs | 6 + template/pallets/actors/src/types/contract.rs | 396 +- template/pallets/actors/src/types/fault.rs | 31 + .../pallets/actors/src/types/lifecycle.rs | 430 +- .../pallets/actors/src/types/observation.rs | 48 +- template/pallets/actors/src/types/plan.rs | 11 + template/pallets/actors/src/types/resource.rs | 1523 +++++ .../pallets/actors/src/types/scheduler.rs | 42 +- template/pallets/actors/src/weights.rs | 498 +- .../governance/docs/architecture.en.md | 4 +- .../pallets/oracle/docs/architecture.en.md | 2 +- template/pallets/oracle/docs/embedding.md | 2 +- .../pallets/oracle/docs/specification.en.md | 8 +- template/pallets/oracle/src/lib.rs | 25 +- template/pallets/oracle/src/mock.rs | 8 + template/pallets/oracle/src/tests.rs | 20 +- .../pallets/router/docs/architecture.en.md | 12 +- template/pallets/router/docs/embedding.md | 6 +- .../pallets/router/docs/specification.en.md | 11 +- .../router/embedding-runtime/src/lib.rs | 1 + template/pallets/router/src/benchmarking.rs | 11 + template/pallets/router/src/lib.rs | 57 +- template/pallets/router/src/mock.rs | 1 + template/pallets/router/src/tests.rs | 37 +- template/pallets/router/src/types.rs | 23 +- template/pallets/router/src/weights.rs | 18 +- template/pallets/session-rotation/Cargo.toml | 46 + template/pallets/session-rotation/README.md | 14 + .../session-rotation/docs/architecture.en.md | 17 + .../session-rotation/docs/embedding.md | 14 + .../session-rotation/docs/specification.en.md | 18 + .../session-rotation/src/benchmarking.rs | 25 + template/pallets/session-rotation/src/lib.rs | 130 + template/pallets/tmc/src/lib.rs | 9 +- template/pallets/tmc/src/tests.rs | 10 + template/primitives/src/assets.rs | 110 +- template/runtime/Cargo.toml | 8 + .../runtime/examples/actor_cost_vectors.rs | 379 ++ .../runtime/examples/trigger_bond_vectors.rs | 85 - .../fixtures/maximum-context-basic.scale | Bin 0 -> 13669 bytes template/runtime/src/apis.rs | 163 +- template/runtime/src/benchmarks.rs | 1 + template/runtime/src/configs/actor_config.rs | 1447 ++++- .../src/configs/address_event_ingress.rs | 7 +- template/runtime/src/configs/assets_config.rs | 181 +- .../runtime/src/configs/deos_router_config.rs | 102 +- .../runtime/src/configs/governance_config.rs | 31 +- template/runtime/src/configs/mod.rs | 174 +- template/runtime/src/configs/oracle_config.rs | 19 +- template/runtime/src/configs/pool_index.rs | 119 - .../runtime/src/configs/resource_meter.rs | 160 + .../runtime/src/configs/staking_config.rs | 10 +- template/runtime/src/configs/xcm_config.rs | 16 +- template/runtime/src/lib.rs | 25 +- .../src/tests/actors_integration_tests.rs | 3158 ++++++++-- .../asset_conversion_integration_tests.rs | 57 +- template/runtime/src/tests/common.rs | 44 +- .../tests/deos_router_integration_tests.rs | 423 +- .../src/tests/dispatchability_matrix_tests.rs | 32 +- .../src/tests/fee_integration_tests.rs | 62 +- .../src/tests/governance_integration_tests.rs | 50 +- .../src/tests/oracle_integration_tests.rs | 29 +- .../src/tests/tmctol_integration_tests.rs | 46 +- template/runtime/src/tmctol_read_model.rs | 4 +- template/runtime/src/weights/mod.rs | 1 + .../runtime/src/weights/pallet_deos_actors.rs | 4785 +++++++++++---- .../runtime/src/weights/pallet_deos_router.rs | 192 +- .../src/weights/pallet_session_rotation.rs | 73 + template/runtime/src/weights/pallet_tmc.rs | 26 +- .../vendor/pallet-asset-conversion/Cargo.lock | 4042 +++++++++++++ .../vendor/pallet-asset-conversion/Cargo.toml | 231 + .../pallet-asset-conversion/Cargo.toml.orig | 61 + .../pallet-asset-conversion/DEOS-PATCH.md | 28 + .../vendor/pallet-asset-conversion/README.md | 25 + .../src/benchmarking.rs | 439 ++ .../vendor/pallet-asset-conversion/src/lib.rs | 1527 +++++ .../pallet-asset-conversion/src/liquidity.rs | 138 + .../pallet-asset-conversion/src/mock.rs | 178 + .../pallet-asset-conversion/src/swap.rs | 261 + .../pallet-asset-conversion/src/tests.rs | 2824 +++++++++ .../pallet-asset-conversion/src/types.rs | 171 + .../pallet-asset-conversion/src/weights.rs | 356 ++ web-client/.papi/descriptors/package.json | 2 +- web-client/.papi/metadata/deos.scale | Bin 276938 -> 287834 bytes web-client/.prettierignore | 1 + web-client/docs/architecture.en.md | 16 +- web-client/package-lock.json | 6 +- web-client/package.json | 6 +- .../scripts/check-actors-normative-drift.mjs | 29 +- .../scripts/generate-actors-abi-manifest.mjs | 15 +- .../scripts/test-actors-abi-manifest.mjs | 4 +- web-client/scripts/test-actors-analysis.mjs | 26 + web-client/scripts/test-actors-authoring.mjs | 29 +- .../scripts/test-actors-client-storage.mjs | 37 + .../scripts/test-actors-cost-projection.mjs | 345 ++ .../scripts/test-actors-eligibility.mjs | 52 +- .../scripts/test-actors-matching-wasm.mjs | 28 +- .../scripts/test-actors-materialization.mjs | 138 + .../scripts/test-actors-normative-drift.mjs | 27 +- .../test-actors-reactive-authoring.mjs | 11 +- .../scripts/test-actors-risk-warnings.mjs | 19 +- web-client/scripts/test-actors-simulation.mjs | 12 +- .../test-actors-trigger-bond-vectors.mjs | 58 - web-client/scripts/upgrade-state-evidence.mjs | 10 +- .../src/lib/adapters/blockchain/actor-cost.ts | 44 + .../blockchain/actor-materialization.ts | 182 + .../lib/adapters/blockchain/actor-resource.ts | 85 + .../adapters/blockchain/actor-simulation.ts | 4 +- .../src/lib/adapters/blockchain/index.ts | 56 +- .../lib/adapters/blockchain/observations.ts | 2 +- web-client/src/lib/adapters/contract.ts | 13 +- .../automation/AutomationStepEditor.svelte | 1 - .../automation/AutomationTriggerEditor.svelte | 31 +- .../lib/automation/actors-abi-manifest.json | 2692 +++++++-- .../lib/automation/actors-cost-vectors.json | 1 + .../actors-fee-envelope-vectors.json | 2 +- .../actors-protocol-bounds.generated.ts | 9 +- .../lib/automation/actors-protocol-bounds.ts | 1 + .../actors-trigger-bond-vectors.json | 27 - web-client/src/lib/automation/analysis.ts | 45 +- web-client/src/lib/automation/authoring.ts | 46 +- web-client/src/lib/automation/cost-vectors.ts | 346 ++ web-client/src/lib/automation/cost.ts | 313 + web-client/src/lib/automation/eligibility.ts | 23 +- .../ingress-runtime-evidence.generated.ts | 2 +- .../src/lib/automation/matching-wasm.ts | 32 +- .../src/lib/automation/materialization.ts | 56 + web-client/src/lib/automation/resource.ts | 59 + .../src/lib/automation/risk-warnings.ts | 6 +- .../automation/runtime-simulation-codec.ts | 7 +- web-client/src/lib/automation/simulation.ts | 10 +- .../lib/automation/trigger-bond-vectors.ts | 37 - .../observation/ObservationInspector.svelte | 120 + .../observation/runtime-evidence.generated.ts | 20 +- .../src/lib/widgets/AutomationWidget.svelte | 174 +- wiki/glossary/core-terms.en.md | 24 + wiki/glossary/core-terms.ru.md | 24 + wiki/overview/actor-system.en.md | 14 +- wiki/overview/actor-system.ru.md | 22 +- wiki/overview/router.en.md | 17 +- wiki/overview/router.ru.md | 17 +- 261 files changed, 58804 insertions(+), 7680 deletions(-) create mode 100644 .agents/skills/architecture-experiments/SKILL.md create mode 100755 .agents/skills/architecture-experiments/scripts/validate-record-normalization.sh create mode 100644 .agents/skills/architecture-experiments/templates/EXP-NNNN.md create mode 100644 .agents/skills/architecture-experiments/tracks/actors/EXP-0001.md create mode 100644 .agents/skills/architecture-experiments/tracks/actors/EXP-0002.md create mode 100644 .agents/skills/architecture-experiments/tracks/actors/EXP-0003.md create mode 100644 .agents/skills/architecture-experiments/tracks/actors/EXP-0005.md create mode 100644 .agents/skills/architecture-experiments/tracks/actors/EXP-0006.md create mode 100644 .agents/skills/architecture-experiments/tracks/actors/EXP-0008.md create mode 100644 .agents/skills/architecture-experiments/tracks/actors/EXP-0008/p128-crossing_placed_maximum_unit.rs create mode 100644 .agents/skills/architecture-experiments/tracks/actors/EXP-0008/p128-crossing_placed_non_tail_emptied_unit.rs create mode 100644 .agents/skills/architecture-experiments/tracks/actors/EXP-0008/p128-crossing_placed_non_tail_trimmed_unit.rs create mode 100644 .agents/skills/architecture-experiments/tracks/actors/EXP-0008/p128-crossing_tail_refill_probe.rs create mode 100644 .agents/skills/architecture-experiments/tracks/actors/EXP-0008/p256-crossing_placed_maximum_unit.rs create mode 100644 .agents/skills/architecture-experiments/tracks/actors/EXP-0008/p256-crossing_placed_non_tail_emptied_unit.rs create mode 100644 .agents/skills/architecture-experiments/tracks/actors/EXP-0008/p256-crossing_placed_non_tail_trimmed_unit.rs create mode 100644 .agents/skills/architecture-experiments/tracks/actors/EXP-0008/p256-crossing_tail_refill_probe.rs create mode 100644 .agents/skills/architecture-experiments/tracks/actors/EXP-0008/p64-crossing_placed_maximum_unit.rs create mode 100644 .agents/skills/architecture-experiments/tracks/actors/EXP-0008/p64-crossing_placed_non_tail_emptied_unit.rs create mode 100644 .agents/skills/architecture-experiments/tracks/actors/EXP-0008/p64-crossing_placed_non_tail_trimmed_unit.rs create mode 100644 .agents/skills/architecture-experiments/tracks/actors/EXP-0008/p64-crossing_tail_refill_probe.rs create mode 100644 .agents/skills/architecture-experiments/tracks/actors/EXP-0009.md create mode 100644 .agents/skills/architecture-experiments/tracks/actors/EXP-0009/crossing_coalesced_cohort_preflight.rs create mode 100644 .agents/skills/architecture-experiments/tracks/actors/EXP-0009/crossing_fire_cohort_preflight.rs create mode 100644 .agents/skills/architecture-experiments/tracks/actors/EXP-0009/crossing_placed_maximum_unit.rs create mode 100644 .agents/skills/architecture-experiments/tracks/actors/EXP-0009/crossing_placed_non_tail_emptied_unit.rs create mode 100644 .agents/skills/architecture-experiments/tracks/actors/EXP-0009/crossing_placed_non_tail_trimmed_unit.rs create mode 100644 .agents/skills/architecture-experiments/tracks/actors/EXP-0009/crossing_rearm_cohort_preflight.rs create mode 100644 .agents/skills/architecture-experiments/tracks/actors/EXP-0009/crossing_skip_cohort_preflight.rs create mode 100644 .agents/skills/architecture-experiments/tracks/actors/EXP-0009/crossing_terminal_cohort_preflight.rs create mode 100644 .agents/skills/architecture-experiments/tracks/actors/EXP-0009/final-crossing_coalesced_cohort_preflight.rs create mode 100644 .agents/skills/architecture-experiments/tracks/actors/EXP-0009/final-crossing_fire_cohort_preflight.rs create mode 100644 .agents/skills/architecture-experiments/tracks/actors/EXP-0009/final-crossing_placed_maximum_unit.rs create mode 100644 .agents/skills/architecture-experiments/tracks/actors/EXP-0009/final-crossing_placed_non_tail_emptied_unit.rs create mode 100644 .agents/skills/architecture-experiments/tracks/actors/EXP-0009/final-crossing_placed_non_tail_trimmed_unit.rs create mode 100644 .agents/skills/architecture-experiments/tracks/actors/EXP-0009/final-crossing_rearm_cohort_preflight.rs create mode 100644 .agents/skills/architecture-experiments/tracks/actors/EXP-0009/final-crossing_skip_cohort_preflight.rs create mode 100644 .agents/skills/architecture-experiments/tracks/actors/EXP-0009/final-crossing_terminal_cohort_preflight.rs create mode 100644 .agents/skills/architecture-experiments/tracks/actors/EXP-0009/h0-crossing_coalesced_cohort_preflight.rs create mode 100644 .agents/skills/architecture-experiments/tracks/actors/EXP-0009/h0-crossing_fire_cohort_preflight.rs create mode 100644 .agents/skills/architecture-experiments/tracks/actors/EXP-0009/h0-crossing_placed_maximum_unit.rs create mode 100644 .agents/skills/architecture-experiments/tracks/actors/EXP-0009/h0-crossing_placed_non_tail_emptied_unit.rs create mode 100644 .agents/skills/architecture-experiments/tracks/actors/EXP-0009/h0-crossing_placed_non_tail_trimmed_unit.rs create mode 100644 .agents/skills/architecture-experiments/tracks/actors/EXP-0009/h0-crossing_rearm_cohort_preflight.rs create mode 100644 .agents/skills/architecture-experiments/tracks/actors/EXP-0009/h0-crossing_skip_cohort_preflight.rs create mode 100644 .agents/skills/architecture-experiments/tracks/actors/EXP-0009/h0-crossing_terminal_cohort_preflight.rs create mode 100644 .agents/skills/architecture-experiments/tracks/actors/EXP-0009/n64-crossing_placed_non_tail_emptied_unit.rs create mode 100644 .agents/skills/architecture-experiments/tracks/actors/EXP-0009/n64-crossing_placed_non_tail_trimmed_unit.rs create mode 100644 .agents/skills/architecture-experiments/tracks/actors/EXP-0010.md create mode 100644 .agents/skills/architecture-experiments/tracks/actors/EXP-0013.md create mode 100644 .agents/skills/architecture-experiments/tracks/actors/EXP-0016.md create mode 100644 .agents/skills/architecture-experiments/tracks/actors/EXP-0017.md create mode 100644 .agents/skills/architecture-experiments/tracks/actors/EXP-0018.md create mode 100644 .agents/skills/architecture-experiments/tracks/actors/EXP-0019.md create mode 100644 .agents/skills/architecture-experiments/tracks/actors/EXP-0020.md create mode 100644 .agents/skills/architecture-experiments/tracks/actors/EXP-0021.md create mode 100644 .agents/skills/architecture-experiments/tracks/actors/EXP-0022.md create mode 100644 .agents/skills/architecture-experiments/tracks/actors/EXP-0023.md create mode 100644 .agents/skills/architecture-experiments/tracks/actors/EXP-0024.md create mode 100644 .agents/skills/architecture-experiments/tracks/actors/experiments.md create mode 100644 .agents/skills/architecture-experiments/tracks/adapters/experiments.md create mode 100644 .agents/skills/architecture-experiments/tracks/router/experiments.md delete mode 100644 .agents/skills/benchmarking/SKILL.md create mode 100644 docs/actors-performance-assurance.specification.en.md create mode 100644 docs/actors-resource-policy.specification.en.md create mode 100644 docs/asset-conversion.integration.en.md create mode 100755 scripts/audit-asset-conversion-boundaries.sh create mode 100644 template/pallets/actors/src/types/fault.rs create mode 100644 template/pallets/actors/src/types/plan.rs create mode 100644 template/pallets/actors/src/types/resource.rs create mode 100644 template/pallets/session-rotation/Cargo.toml create mode 100644 template/pallets/session-rotation/README.md create mode 100644 template/pallets/session-rotation/docs/architecture.en.md create mode 100644 template/pallets/session-rotation/docs/embedding.md create mode 100644 template/pallets/session-rotation/docs/specification.en.md create mode 100644 template/pallets/session-rotation/src/benchmarking.rs create mode 100644 template/pallets/session-rotation/src/lib.rs create mode 100644 template/runtime/examples/actor_cost_vectors.rs delete mode 100644 template/runtime/examples/trigger_bond_vectors.rs create mode 100644 template/runtime/fixtures/maximum-context-basic.scale delete mode 100644 template/runtime/src/configs/pool_index.rs create mode 100644 template/runtime/src/configs/resource_meter.rs create mode 100644 template/runtime/src/weights/pallet_session_rotation.rs create mode 100644 template/vendor/pallet-asset-conversion/Cargo.lock create mode 100644 template/vendor/pallet-asset-conversion/Cargo.toml create mode 100644 template/vendor/pallet-asset-conversion/Cargo.toml.orig create mode 100644 template/vendor/pallet-asset-conversion/DEOS-PATCH.md create mode 100644 template/vendor/pallet-asset-conversion/README.md create mode 100644 template/vendor/pallet-asset-conversion/src/benchmarking.rs create mode 100644 template/vendor/pallet-asset-conversion/src/lib.rs create mode 100644 template/vendor/pallet-asset-conversion/src/liquidity.rs create mode 100644 template/vendor/pallet-asset-conversion/src/mock.rs create mode 100644 template/vendor/pallet-asset-conversion/src/swap.rs create mode 100644 template/vendor/pallet-asset-conversion/src/tests.rs create mode 100644 template/vendor/pallet-asset-conversion/src/types.rs create mode 100644 template/vendor/pallet-asset-conversion/src/weights.rs create mode 100644 web-client/scripts/test-actors-client-storage.mjs create mode 100644 web-client/scripts/test-actors-cost-projection.mjs create mode 100644 web-client/scripts/test-actors-materialization.mjs delete mode 100644 web-client/scripts/test-actors-trigger-bond-vectors.mjs create mode 100644 web-client/src/lib/adapters/blockchain/actor-cost.ts create mode 100644 web-client/src/lib/adapters/blockchain/actor-materialization.ts create mode 100644 web-client/src/lib/adapters/blockchain/actor-resource.ts create mode 100644 web-client/src/lib/automation/actors-cost-vectors.json delete mode 100644 web-client/src/lib/automation/actors-trigger-bond-vectors.json create mode 100644 web-client/src/lib/automation/cost-vectors.ts create mode 100644 web-client/src/lib/automation/cost.ts create mode 100644 web-client/src/lib/automation/materialization.ts create mode 100644 web-client/src/lib/automation/resource.ts delete mode 100644 web-client/src/lib/automation/trigger-bond-vectors.ts diff --git a/.agents/skills/README.md b/.agents/skills/README.md index d19752d1..8bb5ff65 100644 --- a/.agents/skills/README.md +++ b/.agents/skills/README.md @@ -5,7 +5,7 @@ Project skills form a small Domain DAG for agent-facing workflow ownership. They | Skill | Owns | Explicitly excludes | Public route | | --- | --- | --- | --- | | `alignment` | Changed-scope DEOS audits, completion judgement, and durable failure memory | Project CI/release validation; subsystem implementation policy | `alignment/SKILL.md`; `alignment/scripts/completion-gate.sh` | -| `benchmarking` | Benchmark design, evidence classification, interpretation, and integration | Runtime semantics; shared command execution; release publication | `benchmarking/SKILL.md`; project-owned benchmark/build scripts | +| `architecture-experiments` | Physical architecture hypotheses, controlled benchmark design, production-Weight evidence, durable rejected alternatives, artifact-bound lineage, invalidation, and optimization gradients | Protocol semantics; benchmark command execution; open-work and architecture truth | `architecture-experiments/SKILL.md` | | `domain-dag` | Independent generic ownership/DAG review and validator | DEOS subsystem policy; project acceptance | `domain-dag/SKILL.md`; `domain-dag/scripts/validate-domain-dag.sh` | | `release-assurance` | Agent-led dependency provenance, Weight delta, threat-boundary, artifact-identity, and attestation review | Project validation, CI, runtime semantics, publication, signing, and history mutation | `release-assurance/SKILL.md` and its private evidence scripts | | `wiki-sync` | Independent generated Wiki projection, provenance, trust, localization, and consolidation | Source specification ownership; browser implementation; project acceptance | `wiki-sync/SKILL.md` and its private scripts | diff --git a/.agents/skills/alignment/ledgers/ambiguities.jsonl b/.agents/skills/alignment/ledgers/ambiguities.jsonl index f60315b5..90b2e626 100644 --- a/.agents/skills/alignment/ledgers/ambiguities.jsonl +++ b/.agents/skills/alignment/ledgers/ambiguities.jsonl @@ -4,3 +4,9 @@ {"timestamp":"2026-08-10T16:09:48Z","title":"Prelaunch contraction delta conflict","summary":"The original exact-zero count notation conflicted with the mandatory removal of pre-genesis compatibility shadows.","scope":"release/staking-compatibility","status":"resolved","files":"BACKLOG.md,template/pallets/staking/src/lib.rs","tags":"release-scope,compatibility,storage,dispatch","remedy":"Interpret closure counts as no-growth constraints and require count deltas at or below zero; removal of unsupported dispatch, storage, and runtime API surfaces remains admissible.","related":"BACKLOG.md Scope Constraints and CLEAN-3"} {"timestamp":"2026-08-11T00:12:27Z","title":"Reference upgrade authorization had no executable proposal ingress","summary":"The urgent runtime-upgrade path could authorize an existing L1RootAction proposal through unanimous VETO Pass, but proposal creation remained AdminOnly under EnsureRoot with no declared live Root ingress.","scope":"release/runtime-upgrade","status":"resolved","files":"BACKLOG.md,template/pallets/governance/src/lib.rs,template/runtime/src/configs/governance_config.rs","tags":"upgrade,governance,authority,live-evidence","remedy":"Use PrimaryEligibleSigned on the existing proposal call for protocol L1RootAction only; derive eligibility from nonzero primary-track staking power, retain fee/cap/rollback semantics, and leave Root-equivalent execution inside the bounded payload executor. VETO remains protection-only.","related":"BACKLOG.md AUTH-1 and NET-5"} {"timestamp":"2026-08-11T01:13:48Z","title":"Exact baseline cannot use candidate governance ingress","summary":"The apparent causality blocker came from treating pre-1.0 source tags as deployed runtime lineage, although no downstream product network may launch before DEOS 1.0.","scope":"release/runtime-upgrade","status":"resolved","files":"AGENTS.md,BACKLOG.md,template/runtime/src/lib.rs","tags":"upgrade,governance,authority,baseline,causality,pre-genesis","remedy":"Treat every 0.x line as fresh-genesis source with breaking changes and no migrations; keep runtime version 1 and require live upgrade evidence only after a downstream production genesis at 1.0 or later.","related":"AGENTS.md Pre-1.0 Launch Boundary"} +{"timestamp":"2026-08-29T06:07:27Z","title":"Parachain validation fixed-overhead owner","summary":"Upstream cumulus set_validation_data declares zero dispatch Weight and explicitly leaves its non-message overhead unbenchmarked, while only selected hook/message work is registered dynamically; additionally, the SDK DMP reserve is not directly consumed by the reference runtime's EnqueueWithOrigin adapter, so DEOS replaced its fictitious 25% value with the measured finite owner and derived a 512-message cap from the relay 32 MiB allocation and sanitized 64 KiB minimum but must still enforce it in Prepass check_inherents, so DEOS cannot yet claim a conservative FixedBlockWeight from FRAME counters or nominal reserve constants alone.","scope":"actors/resource-integration","status":"open","files":"docs/actors-resource-policy.specification.en.md,template/runtime/src/configs/mod.rs","tags":"resource-accounting,parachain-inherent,upstream-gap","remedy":"Introduce measured bounded DEOS ownership for validation-context overhead, classify each active DMP/XCMP path, enforce the declared 512-message DMP cap plus finite XCMP full/hashed bounds pre-execution through the Prepass check_inherents owner, remove or replace unbound legacy reserves, then combine the resulting maxima component-wise with Timestamp and all other fixed hooks before activating the resource budget.","related":"cumulus-pallet-parachain-system-0.29.0/src/lib.rs:set_validation_data"} +{"timestamp":"2026-08-29T06:29:37Z","title":"Message Queue hook phase misclassification","summary":"Pallet order initially suggested Message Queue consumed its 35% service envelope during on_idle before Actors, but runtime configuration binds that envelope to ServiceWeight while IdleMaxServiceWeight is disabled; Message Queue therefore runs in on_initialize, and XCMP Queue on_idle owns only bounded lazy migration.","scope":"actors/resource-policy","status":"resolved","files":"docs/actors-resource-policy.specification.en.md,template/runtime/src/lib.rs,template/runtime/src/configs/mod.rs","tags":"economic-zipper,on-idle,xcm,message-queue","remedy":"Classify Message Queue on_initialize and the bounded XCMP lazy-migration reserve under FixedBlockWeight; retain the simple ExternalPhase to FreshDrain transition and do not invent an ExternalIdleService resource domain.","related":"docs/actors-resource-policy.specification.en.md Sections 1, 2, and 6"} +{"timestamp":"2026-08-29T08:05:47Z","title":"Session rotation consumes the full block Weight","summary":"Upstream pallet_session::on_initialize returns RuntimeBlockWeights::max_block whenever the six-hour PeriodicSessions boundary rotates, so a static FixedBlockWeight that honestly covers every block leaves zero schedulable Actor/User capacity.","scope":"actors/resource-policy","status":"open","files":"docs/actors-resource-policy.specification.en.md,template/runtime/src/configs/mod.rs,template/runtime/src/tests/actors_integration_tests.rs","tags":"session,weight,fixed-envelope,economic-zipper","remedy":"Move periodic rotation into a small benchmarked DEOS pallet that calls public pallet_session::rotate_session, bind upstream ShouldEndSession to a never-ending adapter while retaining periodic NextSessionRotation, and use the generated bounded hook owner; retaining the full charge is rejected because it leaves zero Actor Control for the required Prepass.","related":"pallet-session-49.0.0/src/lib.rs:on_initialize"} +{"timestamp":"2026-08-29T08:51:50Z","title":"Session rotation consumes the full block Weight","summary":"Resolved by moving periodic rotation to pallet-deos-session-rotation, disabling upstream ShouldEndSession rotation, retaining PeriodicSessions prediction, and binding a 120-identity runtime benchmark owner.","scope":"actors/resource-policy","status":"resolved","files":"template/pallets/session-rotation,template/runtime/src/configs/mod.rs,template/runtime/src/weights/pallet_session_rotation.rs,docs/actors-resource-policy.specification.en.md","tags":"session,weight,fixed-envelope,economic-zipper","remedy":"Retain generated session rotation Weight in FixedBlockWeight and regenerate whenever session geometry or manager composition changes.","related":"BACKLOG.md Implementation / Session Rotation Weight"} +{"timestamp":"2026-08-29T09:28:23Z","title":"Maximum context benchmark needs a no-std relay-proof fixture","summary":"Benchmarking the complete set_validation_data path cannot call its private enqueue leaves or construct BasicParachainInherentData from defaults; the measured callback must retain a valid relay proof and maximum abridged message data prepared outside the measured block.","scope":"actors/resource-policy","status":"open","files":"template/pallets/actors/src/benchmarking.rs,template/runtime/src/configs/actor_config.rs,template/runtime/src/apis.rs","tags":"benchmark,context-inherent,xcmp,weight","remedy":"Use an associated benchmark fixture type on the Actors BenchmarkHelper and gate no-std cumulus-test-relay-sproof-builder under runtime-benchmarks; prepare the valid BasicParachainInherentData plus InboundMessagesData outside #[block], dispatch set_validation_data inside it, and verify registered-versus-unregistered component ownership before binding Weight.","related":"BACKLOG.md Evidence / Context Inherent Geometry"} +{"timestamp":"2026-08-29T10:12:46Z","title":"Maximum context benchmark needs a no-std relay-proof fixture","summary":"Resolved with a validated static SCALE BasicParachainInherentData proof keyed to benchmark ParaId 100, exact included_para_head, and maximum DMP/HRMP MQC heads; setup reconstructs abridged messages outside measurement and the Wasm benchmark executes set_validation_data successfully.","scope":"actors/resource-policy","status":"resolved","files":"template/runtime/fixtures/maximum-context-basic.scale,template/runtime/src/configs/actor_config.rs,template/pallets/actors/src/benchmarking.rs","tags":"benchmark,context-inherent,xcmp,weight","remedy":"Retain fixture hash 720062c34020ad38b5c56e7772befd24a38775a7b3dff547c4b5b28dac97ae17 and regenerate whenever context bounds, ParaId benchmark genesis, MQC construction, proof topology, or message allocation changes.","related":"BACKLOG.md Evidence / Context Inherent Geometry"} diff --git a/.agents/skills/alignment/ledgers/dead_ends.jsonl b/.agents/skills/alignment/ledgers/dead_ends.jsonl index 1ae9094b..103b656d 100644 --- a/.agents/skills/alignment/ledgers/dead_ends.jsonl +++ b/.agents/skills/alignment/ledgers/dead_ends.jsonl @@ -8,3 +8,12 @@ {"timestamp":"2026-08-21T07:48:46Z","title":"Truncating sub-account derivation for generic custody","summary":"Replacing hashed-seed AccountId decoding with PalletId::into_sub_account_truncating can collapse distinct tagged or owner-specific custody identities for narrow generic AccountId types.","scope":"template/pallets/actors,template/pallets/staking","status":"resolved","files":"template/pallets/actors/src/lib.rs,template/pallets/staking/src/pool.rs","tags":"account-identity,generic-runtime,panic-hardening","remedy":"Preserve canonical identities and remove decoding panics through explicit host-supplied custody accounts or a fallible API; validate narrow-AccountId mocks and concrete runtime addresses before retaining a derivation change.","related":""} {"timestamp":"2026-08-22T03:10:33Z","title":"Recursive PAPI generation during npm install","summary":"Running papi generate from postinstall is self-recursive because descriptor generation invokes npm install; a clean CI host can retain incomplete descriptor types even when a warm local worktree passes.","scope":"web-client/generated-descriptors","status":"resolved","files":"web-client/package.json,web-client/README.md","tags":"papi,postinstall,clean-ci,generated-types","remedy":"Keep dependency installation free of descriptor generation; make clean validation prepare Svelte state, then generate descriptors exactly once before type checking.","related":"GitHub Actions run 32547604017"} {"timestamp":"2026-08-22T04:09:55Z","title":"Post-merge-only heavy validation","summary":"A pull-request gate that runs only the fast profile can approve changes to client or heavy validation that first fail after merge on main, turning main into the discovery environment.","scope":"github-actions/release-validation","status":"resolved","files":".github/workflows/ci.yml,scripts/validate-local.sh","tags":"ci-topology,pr-gate,main-failure,release","remedy":"Run the heavy profile under the required pull-request validation-gate; retain main heavy and tag full as repeated evidence rather than first coverage.","related":"GitHub Actions runs 32547507574,32550146969"} +{"timestamp":"2026-08-28T13:25:34Z","title":"Global request_activation deferral cannot implement NextBlock provenance","summary":"Forcing every Idle request_activation under NextBlock to a block-N+1 wakeup produced 242 package test failures and changed temporal, direct-control, observation, queue-capacity, scheduler-meter, and continuation fixtures. request_activation is a shared placement owner, not a certified cause boundary; it cannot distinguish external causes from previously caused temporal or continuation service.","scope":"actors-fresh-step-zero","status":"rejected","files":"template/pallets/actors/src/scheduler.rs,template/pallets/actors/src/tests,template/runtime/src/tests/actors_integration_tests.rs","tags":"causality,provenance,activation","remedy":"Carry typed cause provenance at occurrence materialization and preserve a separate prepass cutoff; do not infer cause timing from the generic activation owner or actor state.","related":"actors/EXP-0018"} +{"timestamp":"2026-08-28T16:23:52Z","title":"Phase-only deferred-cause floor","summary":"Applying N+1 from worker time whenever provenance is Deferred repeatedly postpones Observation fanout first processed after its source block and cannot distinguish already-aged work.","scope":"template/pallets/actors causal timing","status":"rejected","files":"template/pallets/actors/src/reactions.rs,template/pallets/actors/src/scheduler.rs","tags":"actors,causality,provenance","remedy":"Persist the exact cause block with each latest/active dirty revision and Crossing transition; apply the floor only while service occurs in that source block.","related":""} +{"timestamp":"2026-08-28T18:33:57Z","title":"W2 over-capacity sustained-Fresh geometry","summary":"An EXP-0018 profile with 10,000 continuously Running Actors plus additional Fresh Actors exceeds the production MaxActiveActors=10,000 boundary and cannot represent an admitted workload.","scope":"actors-fresh-step-zero","status":"rejected","files":".agents/skills/architecture-experiments/tracks/actors/EXP-0018.md,template/runtime/src/configs/actor_config.rs","tags":"actors,experiment,capacity,liveness","remedy":"Define 1,000/10,000 as total active populations partitioned between Running Actors and a bounded reusable Fresh-ingress pool; never exceed the configured active cap.","related":"actors/EXP-0018"} +{"timestamp":"2026-08-28T21:40:03Z","title":"Maximum W2 setup through measured runtime blocks","summary":"Constructing 6,656 Running and 3,329 Fresh Actors, then opening the Running set through ordinary runtime blocks, failed to finish by block 7,000 after 2,479 seconds with effectful Steps; replacing effects with skipped Steps still timed out after 3,600 seconds. This mixes population setup with the liveness interval and is not a bounded comparative harness.","scope":"actors-fresh-step-zero","status":"rejected","files":"template/runtime/src/tests/actors_integration_tests.rs,.agents/skills/architecture-experiments/tracks/actors/EXP-0018.md","tags":"actors,experiment,liveness,harness","remedy":"Materialize a validated prebuilt Running/Fresh state artifact outside measured blocks, attest its storage/config identity, and measure only bounded post-snapshot ingress/service windows. Do not retain an hour-scale ignored unit test.","related":"actors/EXP-0018"} +{"timestamp":"2026-08-29T06:05:38Z","title":"Average initialization ratio as fixed resource envelope","summary":"FRAME's 10% average-initialization hint is not a conservative FixedBlockWeight bound; parachain validation registers dynamic inbound DMP/XCMP work and the runtime configures independent 25% reserves for each message class.","scope":"actors/resource-integration","status":"rejected","files":"template/runtime/src/lib.rs,template/runtime/src/configs/mod.rs,docs/actors-resource-policy.specification.en.md","tags":"resource-accounting,parachain-inherent,weight","remedy":"Derive and enforce a component-wise worst-case fixed/context envelope covering base/finalization, Timestamp, validation overhead, and both inbound-message reserves before configuring the schedulable budget.","related":""} +{"timestamp":"2026-08-29T09:48:12Z","title":"No-std relay proof builder and incomplete static proof fixture","summary":"cumulus-test-relay-sproof-builder advertises default-features=false but fails wasm32v1-none compilation because its source lacks no-std prelude/alloc imports. A generated static BasicParachainInherentData proof compiled, but benchmark dispatch trapped because the proof omitted the included parachain head required by set_validation_data.","scope":"actors/resource-policy","status":"open","files":"template/runtime/src/configs/actor_config.rs,template/pallets/actors/src/benchmarking.rs","tags":"benchmark,context-inherent,wasm,relay-proof","remedy":"Generate a complete static proof fixture containing included_para_head plus the exact DMP/HRMP MQC heads, validate it by dispatching set_validation_data in a std fixture test before embedding, then retry the Wasm benchmark; do not bind or retain a benchmark whose fixture has not passed the actual dispatch.","related":"BACKLOG.md Evidence / Context Inherent Geometry"} +{"timestamp":"2026-08-29T10:12:46Z","title":"No-std relay proof builder and incomplete static proof fixture","summary":"Resolved by generating the proof under std, validating the actual maximum set_validation_data dispatch, embedding only the 13,669-byte SCALE Basic fixture, and reconstructing message data in Wasm benchmark setup; benchmark completed at 50 steps and 20 repeats.","scope":"actors/resource-policy","status":"resolved","files":"template/runtime/fixtures/maximum-context-basic.scale,template/runtime/src/tests/actors_integration_tests.rs","tags":"benchmark,context-inherent,wasm,relay-proof","remedy":"Do not depend on the broken no-std proof-builder path; keep the static fixture dispatch test as the falsification owner.","related":"maximum_context_inherent benchmark"} +{"timestamp":"2026-08-29T15:18:00Z","title":"Prepass stale cleanup cannot reuse the current single-meter base pass","summary":"Resolved by adding an independent scan-control meter bounded by the reserved Actor Control maximum; saturated prepass cleanup can now leave a stale suffix without letting subsequent FIFO scans consume ActorBase effect capacity or retain reservation authority.","scope":"actors/mandatory-prepass","status":"resolved","files":"template/pallets/actors/src/lib.rs,template/pallets/actors/src/scheduler.rs","tags":"resource-algebra,prepass,stale-cleanup,domain-meter","remedy":"Keep FIFO scan consumption dual-metered against both total pass capacity and Actor Control; do not substitute total-minus-effect reconciliation as the admission guard.","related":"BACKLOG.md I5"} +{"timestamp":"2026-08-30T08:18:00Z","title":"100-Steps-per-block planning witness is incompatible with generated production Weight","summary":"Complete branch-exact Actor control and effect Weight disproves the historical target of 10,000 one-Step cycles in 100 blocks; the accepted 10,000-Active-Actor scheduler requires a measured 1,300-block first-traversal horizon.","scope":"actors/production-throughput","status":"resolved","files":"BACKLOG.md,template/runtime/src/tests/actors_integration_tests.rs","tags":"actors,throughput,weight,completion-honesty","remedy":"Use starvation-free first traversal under generated two-dimensional Weight as release evidence, report committed Steps and distinct Actors separately, and never present the 1,300-block evidence horizon as an SLA.","related":"stress_10k_actors_queue_scheduler"} diff --git a/.agents/skills/alignment/ledgers/hallucinations.jsonl b/.agents/skills/alignment/ledgers/hallucinations.jsonl index 5d6e5cf6..e827c7dd 100644 --- a/.agents/skills/alignment/ledgers/hallucinations.jsonl +++ b/.agents/skills/alignment/ledgers/hallucinations.jsonl @@ -25,3 +25,41 @@ {"timestamp":"2026-08-24T02:25:26Z","severity":"FATAL","rule":"O(N) Iteration Anti-Pattern","files":"/home/llb/Repos/deos/template/pallets/actors/src/tests/crossing.rs,/home/llb/Repos/deos/template/pallets/actors/src/tests/execution.rs,/home/llb/Repos/deos/template/pallets/actors/src/tests/fees_and_funding.rs,/home/llb/Repos/deos/template/pallets/actors/src/tests/lifecycle.rs,/home/llb/Repos/deos/template/pallets/actors/src/tests/observations.rs,/home/llb/Repos/deos/template/pallets/actors/src/tests/scheduling.rs,/home/llb/Repos/deos/template/pallets/actors/src/tests/storage_and_api.rs,/home/llb/Repos/deos/template/pallets/actors/src/tests/wakeups.rs"} {"timestamp":"2026-08-24T02:25:26Z","severity":"FATAL","rule":"Unchecked Actors Arithmetic","files":"/home/llb/Repos/deos/template/pallets/actors/src/tests/core.rs,/home/llb/Repos/deos/template/pallets/actors/src/tests/crossing.rs,/home/llb/Repos/deos/template/pallets/actors/src/tests/execution.rs,/home/llb/Repos/deos/template/pallets/actors/src/tests/lifecycle.rs,/home/llb/Repos/deos/template/pallets/actors/src/tests/observations.rs"} {"timestamp":"2026-08-24T09:45:22Z","severity":"FATAL","rule":"O(N) Iteration Anti-Pattern","files":"/home/llb/Repos/deos/template/pallets/actors/src/crossing.rs"} +{"timestamp":"2026-08-25T04:00:36Z","severity":"FATAL","rule":"Unowned Actors Pallet Panic Surface","files":"/home/llb/Repos/deos/template/pallets/actors/src/types/contract.rs"} +{"timestamp":"2026-08-25T04:00:37Z","severity":"FATAL","rule":"Unowned Runtime Panic Surface","files":"/home/llb/Repos/deos/template/pallets/actors/src/types/contract.rs"} +{"timestamp":"2026-08-25T04:22:13Z","severity":"FATAL","rule":"O(N) Iteration Anti-Pattern","files":"/home/llb/Repos/deos/template/pallets/actors/src/types/contract.rs"} +{"timestamp":"2026-08-25T06:55:53Z","title":"Benchmarking before Experiment Prepared gate","summary":"EXP-0003 B0/C1 current-load benchmarks ran while the record remained Proposed and before the complete lifecycle harness and candidate identities were frozen.","scope":"architecture-experiments/actors","status":"resolved","files":".agents/skills/architecture-experiments/tracks/actors/EXP-0003.md,template/pallets/actors/src/benchmarking.rs","tags":"experiment-lifecycle,evidence-classification","remedy":"Treat outputs as non-authoritative rehearsal, finish and freeze the complete B0/C1 harness, transition to Prepared, then rerun; assert status before benchmark commands.","related":""} +{"timestamp":"2026-08-25T09:08:30Z","severity":"FATAL","rule":"O(N) Iteration Anti-Pattern","files":"/home/llb/Repos/deos/template/pallets/actors/src/lib.rs"} +{"timestamp":"2026-08-25T13:18:59Z","severity":"FATAL","rule":"Unowned Actors Pallet Panic Surface","files":"/home/llb/Repos/deos/template/pallets/actors/src/lib.rs"} +{"timestamp":"2026-08-25T13:19:00Z","severity":"FATAL","rule":"Unowned Runtime Panic Surface","files":"/home/llb/Repos/deos/template/pallets/actors/src/lib.rs"} +{"timestamp":"2026-08-26T16:14:43Z","severity":"FATAL","rule":"O(N) Iteration Anti-Pattern","files":"/home/llb/Repos/deos/template/pallets/actors/src/scheduler.rs"} +{"timestamp":"2026-08-26T21:35:06Z","title":"Runtime ceiling advanced without every generated benchmark range","summary":"The P32 binding left C6 geometry benchmarks at the prior 0..=2 range, compared the maximum-Opening selector against four predicates even though four Opening predicates produce eight evaluation units, and declared 40 funding entries without populating them in the atomic benchmark. The owner was unreachable and its setup omitted one claimed maximum dimension.","scope":"actors-production-weight","status":"resolved","files":"template/pallets/actors/src/benchmarking.rs,template/runtime/src/configs/actor_config.rs,template/runtime/src/weights/pallet_deos_actors.rs","tags":"benchmark-range,configured-bound,p32,weight-soundness,unreachable-owner","remedy":"Whenever a runtime bound advances, audit every physical benchmark range and prove each special owner is selected by a realizable context whose benchmark setup and postconditions materialize every claimed maximum dimension.","related":"EXP-0010"} +{"timestamp":"2026-08-26T22:17:06Z","title":"Subtracting separately generated Weight owners is not a valid atomic split","summary":"A measured full-cycle Running branch had lower estimated ProofSize than the sum of separately generated queue scan/probe/consume owners because proof keys overlap. Component-wise subtraction therefore cannot derive an inner atomic owner even when RefTime subtraction appears valid.","scope":"actors-production-weight","status":"resolved","files":"template/pallets/actors/src/benchmarking.rs,template/runtime/src/configs/actor_config.rs","tags":"weight-algebra,proof-size,atomic-owner,benchmark-boundary","remedy":"Benchmark each non-overlapping ownership boundary directly. The remaining Running/Suspended and nonmaximum-Opening owners must measure the inner plan/evaluate/commit/post-placement atom after separately owned queue discovery/state loading, never derive it by subtracting independent generated estimates.","related":"I4 Control Weight Port"} +{"timestamp":"2026-08-26T21:55:03Z","severity":"FATAL","rule":"O(N) Iteration Anti-Pattern","files":"/home/llb/Repos/deos/template/pallets/actors/src/lib.rs"} +{"timestamp":"2026-08-27T00:08:00Z","title":"Mixed predicate evaluation units are not a monotonic benchmark axis","summary":"A single 0..=8 Suspended-head benchmark axis changed physical predicate composition at unit five: current predicates decreased while frozen Opening predicates increased. RefTime peaked near the composition boundary and then fell, causing generated linear regression to discard the axis and underprice realizable intermediate branches.","scope":"actors-production-weight","status":"resolved","files":"template/pallets/actors/src/benchmarking.rs,template/runtime/src/weights/pallet_deos_actors.rs","tags":"benchmark-axis,non-monotonic,opening-predicate,weight-soundness","remedy":"Keep the monotonic current-only 0..=4 owner and benchmark Opening-heavy branches under a separate direct owner materializing the worst realizable one-Opening/three-current composition; do not encode changing physical compositions as one linear parameter.","related":"I4 Control Weight Port"} +{"timestamp":"2026-08-27T01:53:34Z","severity":"FATAL","rule":"Unowned Runtime Panic Surface","files":"/home/llb/Repos/deos/template/pallets/actors/src/execution.rs"} +{"timestamp":"2026-08-27T03:42:29Z","severity":"FATAL","rule":"Unowned Actors Pallet Panic Surface","files":"/home/llb/Repos/deos/template/pallets/actors/src/lib.rs"} +{"timestamp":"2026-08-27T03:42:29Z","severity":"FATAL","rule":"Unowned Runtime Panic Surface","files":"/home/llb/Repos/deos/template/pallets/actors/src/lib.rs"} +{"timestamp":"2026-08-27T04:59:47Z","severity":"FATAL","rule":"O(N) Iteration Anti-Pattern","files":"/home/llb/Repos/deos/template/pallets/actors/src/lib.rs"} +{"timestamp":"2026-08-28T00:00:00Z","title":"Persistent funding wait solves the wrong User process-lifetime boundary","summary":"Replacing current-Step apoptosis with ActorFundingWait prevented immediate death but retained nonviable process state indefinitely and added wait authority, ingress wake, and scheduler reinsertion. It also failed the stronger invariant that an economically admitted multi-Step Cycle must finish without reclassification between Steps.","scope":"actors-user-lifecycle","status":"resolved","files":"template/pallets/actors/docs/specification.en.md,docs/actors-resource-policy.specification.en.md,.agents/skills/architecture-experiments/tracks/actors/EXP-0019.md","tags":"apoptosis,cycle-admission,funding-wait,pipeline-liveness,custody","remedy":"Evaluate User viability only while Idle. Derive a fixed-size full-Cycle economic envelope at Contract mutation, reserve retry-inclusive Cycle plus minimal-cleanup capacity at Opening, carry it through Q1, and perform custody-neutral minimal apoptosis only at a nonviable boundary.","related":"actors/EXP-0019"} +{"timestamp":"2026-08-27T05:48:52Z","severity":"FATAL","rule":"O(N) Iteration Anti-Pattern","files":"/home/llb/Repos/deos/template/pallets/actors/src/lib.rs"} +{"timestamp":"2026-08-27T06:41:19Z","title":"Whole-pipeline control liveness does not require prepaying future Action effects","summary":"The first EXP-0019 correction combined Actor control and Task-effect maxima into one retry-inclusive Cycle balance reserve. That conflated deterministic machine service with uncertain transaction-like Actions, created a persistent run fee ledger, and charged authority for effects that might never be invoked.","scope":"actors-fee-lifecycle","status":"resolved","files":"template/pallets/actors/docs/specification.en.md,.agents/skills/architecture-experiments/tracks/actors/EXP-0019.md","tags":"fee-boundary,pipeline-machine,action-effect,run-ledger","remedy":"Charge Trigger plus complete bounded Pipeline Machine control upfront at ready Opening, charge each invoked Action from current valid-actual effect evidence, treat unfunded non-invocation as FundingUnavailable, and retain no run machine balance ledger.","related":"actors/EXP-0019"} +{"timestamp":"2026-08-27T18:06:07Z","severity":"FATAL","rule":"O(N) Iteration Anti-Pattern","files":"/home/llb/Repos/deos/template/pallets/actors/src/lib.rs"} +{"timestamp":"2026-08-27T19:11:28Z","severity":"FATAL","rule":"O(N) Iteration Anti-Pattern","files":"/home/llb/Repos/deos/template/runtime/examples/actor_cost_vectors.rs"} +{"timestamp":"2026-08-28T12:58:05Z","severity":"FATAL","rule":"O(N) Iteration Anti-Pattern","files":"/home/llb/Repos/deos/template/pallets/actors/src/lib.rs"} +{"timestamp":"2026-08-28T12:58:24Z","severity":"FATAL","rule":"O(N) Iteration Anti-Pattern","files":"/home/llb/Repos/deos/template/pallets/actors/src/lib.rs"} +{"timestamp":"2026-08-28T13:10:51Z","title":"Configured NextBlock timing is not implemented by the current Actors cutoff","summary":"EXP-0018 and architecture prose described T+1 as the production baseline, but external AddressEvent/Manual readiness is queued in block N and execute_cycle snapshots NextQueueTicket only during the same block on_idle pass; runtime tests explicitly execute repeated same-block transfers. A generated I4 Weight/Wasm identity therefore does not establish the specified NextBlock causal baseline.","scope":"actors-fresh-step-zero","status":"open","files":"template/pallets/actors/src/scheduler.rs,template/pallets/actors/src/lib.rs,template/runtime/src/tests/actors_integration_tests.rs,.agents/skills/architecture-experiments/tracks/actors/EXP-0018.md","tags":"causality,cutoff,experiment-baseline","remedy":"Implement and test the prepass cutoff/NextBlock source contract before constructing or measuring SameBlockExternal; bind timing and provenance into runtime configuration and admission identity.","related":"BACKLOG.md I8/I12 and actors/EXP-0018"} +{"timestamp":"2026-08-29T05:31:42Z","severity":"FATAL","rule":"Unowned Runtime Panic Surface","files":"/home/llb/Repos/deos/template/pallets/actors/src/types/resource.rs"} +{"timestamp":"2026-08-29T08:58:10Z","severity":"FATAL","rule":"O(N) Iteration Anti-Pattern","files":"/home/llb/Repos/deos/template/runtime/src/apis.rs"} +{"timestamp":"2026-08-29T08:58:12Z","severity":"FATAL","rule":"Unowned Reference Runtime Panic Surface","files":"/home/llb/Repos/deos/template/runtime/src/apis.rs"} +{"timestamp":"2026-08-29T08:58:12Z","severity":"FATAL","rule":"Unowned Runtime Panic Surface","files":"/home/llb/Repos/deos/template/runtime/src/apis.rs"} +{"timestamp":"2026-08-29T11:01:34Z","severity":"FATAL","rule":"O(N) Iteration Anti-Pattern","files":"/home/llb/Repos/deos/template/runtime/src/configs/mod.rs"} +{"timestamp":"2026-08-29T11:56:47Z","severity":"FATAL","rule":"Unowned Reference Runtime Panic Surface","files":"/home/llb/Repos/deos/template/runtime/src/configs/resource_meter.rs"} +{"timestamp":"2026-08-29T11:56:48Z","severity":"FATAL","rule":"Unowned Runtime Panic Surface","files":"/home/llb/Repos/deos/template/runtime/src/configs/resource_meter.rs"} +{"timestamp":"2026-08-29T12:17:09Z","severity":"FATAL","rule":"Unowned Runtime Panic Surface","files":"/home/llb/Repos/deos/template/pallets/actors/src/types/resource.rs"} +{"timestamp":"2026-08-30T05:24:25Z","severity":"FATAL","rule":"Unowned Runtime Panic Surface","files":"/home/llb/Repos/deos/template/pallets/actors/src/types/resource.rs"} +{"timestamp":"2026-08-30T20:46:28Z","severity":"FATAL","rule":"O(N) Iteration Anti-Pattern","files":"/home/llb/Repos/deos/template/primitives/src/assets.rs,/home/llb/Repos/deos/template/runtime/src/configs/deos_router_config.rs"} +{"timestamp":"2026-08-30T20:46:32Z","severity":"FATAL","rule":"Unowned Production Pallet Panic Surface","files":"/home/llb/Repos/deos/template/pallets/router/src/lib.rs"} +{"timestamp":"2026-08-30T20:46:32Z","severity":"FATAL","rule":"Unowned Runtime Panic Surface","files":"/home/llb/Repos/deos/template/pallets/router/src/lib.rs,/home/llb/Repos/deos/template/vendor/pallet-asset-conversion/src/lib.rs"} +{"timestamp":"2026-08-30T20:48:50Z","severity":"FATAL","rule":"O(N) Iteration Anti-Pattern","files":"/home/llb/Repos/deos/template/runtime/src/configs/deos_router_config.rs"} +{"timestamp":"2026-08-30T20:49:13Z","severity":"FATAL","rule":"O(N) Iteration Anti-Pattern","files":"/home/llb/Repos/deos/template/runtime/src/configs/deos_router_config.rs"} diff --git a/.agents/skills/alignment/rules/actors-identity-rules.json b/.agents/skills/alignment/rules/actors-identity-rules.json index f78700c1..e1bbdea4 100644 --- a/.agents/skills/alignment/rules/actors-identity-rules.json +++ b/.agents/skills/alignment/rules/actors-identity-rules.json @@ -49,6 +49,7 @@ "template/pallets/actors/src/lib\\.rs:.*contract\\.steps\\.iter", "template/pallets/actors/src/lib\\.rs:.*legs\\.iter", "template/pallets/actors/src/lib\\.rs:.*ActorIdentities::::iter", + "template/pallets/actors/src/lib\\.rs:.*ActorStateHolds::::iter", "template/pallets/actors/src/lib\\.rs:.*SystemSovereigns::::iter", "template/pallets/actors/src/lib\\.rs:.*SovereignIndex::::iter", "template/pallets/actors/src/types\\.rs", @@ -81,7 +82,8 @@ "admitted cursor-local retry counter remains below its u32 bound", "suspension requires bounded RetryLater", "admitted mutable RetryLater plan has a valid unresolved cursor", - "terminal Continuation can be cleared atomically", + "terminal Actor run can be cleared atomically", + "fresh terminal cycle nonce was checked before Opening", "failure bound \\(MaxConsecutiveFailures\\) is precluded by admission", "fresh productive execution snapshot satisfies terminal preconditions", "handled above", @@ -95,7 +97,7 @@ "one predicate fits", "one clause fits" ], - "rationale": "These exact execution assertions are mechanically precluded by the canonical actor loader, admission bounds, exhaustive StepOutcome control mapping, integrity checks, or transactional finalization; the four compact fit messages belong to the file-local test module. Any differently worded site is unowned and fails the full-tree audit.", + "rationale": "These exact execution assertions are mechanically precluded by the canonical actor loader, admission bounds, exhaustive StepOutcome control mapping, checked Opening nonce derivation, integrity checks, or transactional finalization; the four compact fit messages belong to the file-local test module. Any differently worded site is unowned and fails the full-tree audit.", "expiry": "Replace an entry when its assertion becomes typed failure or when its preclusion owner changes." }, { diff --git a/.agents/skills/alignment/scripts/audit-protocol-coherence-regressions.sh b/.agents/skills/alignment/scripts/audit-protocol-coherence-regressions.sh index 57a77a62..8956f072 100755 --- a/.agents/skills/alignment/scripts/audit-protocol-coherence-regressions.sh +++ b/.agents/skills/alignment/scripts/audit-protocol-coherence-regressions.sh @@ -363,8 +363,9 @@ main() { reject_pattern 'get_preimage\(hash\)' \ "Witness preparation regressed to loading the generic preimage value from storage" \ "$runtime_configs/governance_config.rs" - reject_pattern 'DispatchClass::|Pays::' \ - "Custom public call changed dispatch class or payment semantics outside the runtime dispatchability matrix" \ + reject_pattern_except 'DispatchClass::' \ + 'pallets/actors/src/lib\.rs:[0-9]+:[[:space:]]+DispatchClass::Mandatory,$' \ + "Custom public call changed dispatch class outside the runtime dispatchability matrix" \ "$TEMPLATE_DIR/pallets/actors/src/lib.rs" \ "$TEMPLATE_DIR/pallets/asset-registry/src/lib.rs" \ "$TEMPLATE_DIR/pallets/governance/src/lib.rs" \ @@ -372,6 +373,26 @@ main() { "$TEMPLATE_DIR/pallets/router/src/lib.rs" \ "$TEMPLATE_DIR/pallets/staking/src/lib.rs" \ "$TEMPLATE_DIR/pallets/tmc/src/lib.rs" + reject_pattern 'Pays::' \ + "Custom public call changed payment semantics outside the Actors-funded Manual occurrence boundary" \ + "$TEMPLATE_DIR/pallets/asset-registry/src/lib.rs" \ + "$TEMPLATE_DIR/pallets/governance/src/lib.rs" \ + "$TEMPLATE_DIR/pallets/oracle/src/lib.rs" \ + "$TEMPLATE_DIR/pallets/router/src/lib.rs" \ + "$TEMPLATE_DIR/pallets/staking/src/lib.rs" \ + "$TEMPLATE_DIR/pallets/tmc/src/lib.rs" + local actor_pays_count + actor_pays_count="$(rg -N -o 'Pays::' "$TEMPLATE_DIR/pallets/actors/src/lib.rs" | wc -l | tr -d '[:space:]')" + [[ "$actor_pays_count" == "5" ]] || { + log_error "Actors public-call payment surface diverged from the audited Manual and mandatory-prepass boundaries" + exit 1 + } + require_anchor 'Call::actor_prepass' \ + "$TEMPLATE_DIR/pallets/actors/src/lib.rs" "Mandatory Actor Prepass inherent lost its canonical call owner" + require_anchor 'actor_type == ActorType::User && trigger_processed' \ + "$TEMPLATE_DIR/pallets/actors/src/lib.rs" "Manual User occurrence no longer owns the sole Actors Pays::No success path" + require_anchor 'actor_funded_success' \ + "$TEMPLATE_DIR/runtime/src/tests/dispatchability_matrix_tests.rs" "Actor-funded success payment is missing from the dispatchability matrix" require_anchor 'every_custom_runtime_call_family_fits_its_dispatch_envelope_at_maximum_input' \ "$TEMPLATE_DIR/runtime/src/tests/dispatchability_matrix_tests.rs" "Custom-call dispatchability matrix is missing" local custom_call_count expected_custom_call_count @@ -383,6 +404,7 @@ main() { "$TEMPLATE_DIR/pallets/router/src/lib.rs" \ "$TEMPLATE_DIR/pallets/staking/src/lib.rs" \ "$TEMPLATE_DIR/pallets/tmc/src/lib.rs" | wc -l | tr -d '[:space:]')" + custom_call_count="$((custom_call_count - 1))" # Payload-free Mandatory inherent is not a signed dispatchability-matrix family. expected_custom_call_count="$(awk \ '$1 == "const" && $2 == "EXPECTED_CUSTOM_CALL_FAMILIES:" { gsub(/;/, "", $5); print $5 }' \ "$TEMPLATE_DIR/runtime/src/tests/dispatchability_matrix_tests.rs")" @@ -648,12 +670,12 @@ main() { "$actors_tests" "Actors cursor-local unsuccessful-attempt reset evidence is missing" require_anchor 'retry_target_uses_only_cursor_local_count_and_last_attempt_block' \ "$actors_tests" "Actors retry timing no longer proves its minimal stored inputs" - reject_pattern '(?:pub\s+attempt:\s*u32|ContinuationState\.attempt|\bcontinuation\.attempt|\battempt:\s*u32)' \ + reject_pattern '(?:pub\s+attempt:\s*u32|ActorRunState\.attempt|\brun_state\.attempt|\battempt:\s*u32)' \ "Removed Actors cycle-global attempt ordinal reintroduced" \ "$TEMPLATE_DIR/pallets/actors/src" "$TEMPLATE_DIR/pallets/actors/docs" \ "$PROJECT_ROOT/docs/actors-control-plane.contract.en.md" \ "$PROJECT_ROOT/web-client/src" "$PROJECT_ROOT/web-client/docs" - require_anchor 'continuation_attempts_have_unique_chain_coordinates_without_the_stored_ordinal' \ + require_anchor 'run_attempts_have_unique_chain_coordinates_without_the_stored_ordinal' \ "$actors_tests" "Actors attempt-identity contraction proof is missing" require_anchor 'pub enum ActorClassificationError' \ "$actors_lifecycle_types" \ @@ -669,7 +691,7 @@ main() { "Actors simulation classification wrapper is missing" require_anchor 'eligibility_projection_rejects_partial_active_partitions' "$actors_tests" \ "Actors shared eligibility/simulation classification evidence is missing" - reject_pattern 'SimulationError::(?:ActorInvariant|ContinuationInvariant|ComputationOverflow)' \ + reject_pattern 'SimulationError::(?:ActorInvariant|RunInvariant|ComputationOverflow)' \ "Duplicated classification mapping reintroduced into simulation" \ "$TEMPLATE_DIR/pallets/actors/src" "$TEMPLATE_DIR/pallets/actors/docs" require_anchor 'public_api_error_signatures_use_shared_typed_cores' "$actors_tests" \ @@ -708,7 +730,7 @@ main() { require_anchor 'cumulative_outcomes: attempt.outcomes' \ "$TEMPLATE_DIR/pallets/actors/src/execution.rs" \ "Actors simulation reconstructed counters outside production finalization" - require_anchor 'continuation_simulation_preserves_retry_position_and_committed_state' \ + require_anchor 'run_simulation_preserves_retry_position_and_committed_state' \ "$actors_tests" "Actors shared failure-cause simulation evidence is missing" reject_pattern '\b(?:SimulationStepOutcome|SimulationStatus)\b' \ "Retired simulation-only Actors outcome vocabulary reintroduced" \ @@ -722,7 +744,7 @@ main() { "$TEMPLATE_DIR/pallets/actors" "$PROJECT_ROOT/docs" "$PROJECT_ROOT/web-client/src" require_anchor 'opening_and_current_predicates_observe_distinct_step_state' "$actors_tests" \ "Actors explicit Opening/Current timing evidence is missing" - require_anchor 'opening_predicate_result_is_reused_by_continuation' "$actors_tests" \ + require_anchor 'opening_predicate_result_is_reused_by_run_state' "$actors_tests" \ "Actors frozen Opening truth evidence is missing" require_anchor 'unavailable_observation_skips_without_incrementing_failures' "$actors_tests" \ "Actors false-precondition skip evidence is missing" @@ -767,10 +789,84 @@ main() { reject_pattern 'predicate_set_evaluation\(step\.preconditions\.evaluation_units\(\)\)' \ "Actors dual-phase predicate Weight bypasses benchmark-domain chunking" \ "$TEMPLATE_DIR/pallets/actors/src" - require_anchor 'cycle_closes_with_fee_budget_exhausted_when_unfunded' "$actors_tests" \ - "Actors fee-budget close-cause evidence is missing" - require_anchor 'balance_exhausted_takes_precedence_over_fee_budget_exhausted' "$actors_tests" \ - "Actors balance-versus-fee close-cause precedence evidence is missing" + reject_pattern 'BalanceExhausted|FeeBudgetExhausted' \ + "Actors active runtime/client surfaces revived retired mid-pipeline economic close reasons" \ + "$TEMPLATE_DIR/pallets/actors/src" \ + "$TEMPLATE_DIR/runtime/src" \ + "$PROJECT_ROOT/web-client/src/lib/automation/eligibility.ts" + require_anchor 'underfunded_manual_occurrence_creates_no_fee_readiness_or_apoptosis' "$actors_tests" \ + "Actors underfunded Manual occurrence preservation evidence is missing" + require_anchor 'manual_trigger_collection_failure_rolls_back_readiness_and_fee_movement' "$actors_tests" \ + "Actors Manual occurrence collection rollback evidence is missing" + require_anchor 'address_event_charges_occurrence_before_pipeline_opening' "$actors_tests" \ + "Actors AddressEvent occurrence charging evidence is missing" + require_anchor 'underfunded_address_event_advances_without_fee_readiness_or_apoptosis' "$actors_tests" \ + "Actors automatic AddressEvent underfunding evidence is missing" + require_anchor 'address_event_collection_failure_preserves_source_progress_without_readiness' "$actors_tests" \ + "Actors automatic AddressEvent collection-failure progression evidence is missing" + require_anchor 'address_event_trigger_occurrence' \ + "$TEMPLATE_DIR/runtime/src/weights/pallet_deos_actors.rs" \ + "Actors generated AddressEvent Trigger Weight owner is missing" + require_anchor 'observation_change_charges_occurrence_before_pipeline_opening' \ + "$TEMPLATE_DIR/pallets/actors/src/tests/observations.rs" \ + "Actors ObservationChange occurrence charging evidence is missing" + require_anchor 'underfunded_observation_change_advances_without_fee_readiness_or_apoptosis' \ + "$TEMPLATE_DIR/pallets/actors/src/tests/observations.rs" \ + "Actors automatic ObservationChange underfunding evidence is missing" + require_anchor 'observation_change_trigger_occurrence' \ + "$TEMPLATE_DIR/runtime/src/weights/pallet_deos_actors.rs" \ + "Actors generated ObservationChange Trigger Weight owner is missing" + require_anchor 'observation_crossing_fire_charges_before_readiness' \ + "$TEMPLATE_DIR/pallets/actors/src/tests/crossing.rs" \ + "Actors ObservationCrossing fire occurrence charging evidence is missing" + require_anchor 'underfunded_crossing_fire_advances_without_fee_readiness_or_apoptosis' \ + "$TEMPLATE_DIR/pallets/actors/src/tests/crossing.rs" \ + "Actors automatic ObservationCrossing underfunding evidence is missing" + require_anchor 'crossing_fire_collection_failure_advances_without_readiness' \ + "$TEMPLATE_DIR/pallets/actors/src/tests/crossing.rs" \ + "Actors automatic ObservationCrossing collection-failure progression evidence is missing" + require_anchor 'crossing_batch_falls_back_to_scalar_progress_for_an_underfunded_member' \ + "$TEMPLATE_DIR/pallets/actors/src/tests/crossing.rs" \ + "Actors ObservationCrossing batch underfunding progress evidence is missing" + require_anchor 'observation_crossing_trigger_occurrence' \ + "$TEMPLATE_DIR/runtime/src/weights/pallet_deos_actors.rs" \ + "Actors generated ObservationCrossing Trigger Weight owner is missing" + require_anchor 'at_time_occurrence_charges_once_consumes_deadline_and_latches_readiness' \ + "$TEMPLATE_DIR/pallets/actors/src/tests/scheduling.rs" \ + "Actors AtTime occurrence charging evidence is missing" + require_anchor 'busy_at_time_occurrence_charges_and_preserves_independent_run_service' \ + "$TEMPLATE_DIR/pallets/actors/src/tests/scheduling.rs" \ + "Actors busy AtTime occurrence evidence is missing" + require_anchor 'underfunded_at_time_occurrence_selects_prepaid_custody_neutral_apoptosis' \ + "$TEMPLATE_DIR/pallets/actors/src/tests/scheduling.rs" \ + "Actors automatic AtTime underfunded-apoptosis evidence is missing" + require_anchor 'at_time_trigger_occurrence' \ + "$TEMPLATE_DIR/runtime/src/weights/pallet_deos_actors.rs" \ + "Actors generated AtTime Trigger Weight owner is missing" + require_anchor 'cadenced_latch_disables_detection_until_pipeline_opening' \ + "$TEMPLATE_DIR/pallets/actors/src/tests/scheduling.rs" \ + "Actors Cadenced useful-latch disable/re-arm evidence is missing" + require_anchor 'busy_cadenced_occurrence_charges_and_preserves_independent_run_service' \ + "$TEMPLATE_DIR/pallets/actors/src/tests/scheduling.rs" \ + "Actors busy Cadenced occurrence evidence is missing" + require_anchor 'underfunded_cadenced_occurrence_advances_without_fee_readiness_or_apoptosis' \ + "$TEMPLATE_DIR/pallets/actors/src/tests/scheduling.rs" \ + "Actors automatic Cadenced underfunding evidence is missing" + require_anchor 'pipeline_and_trigger_temporal_memberships_coexist_and_drain_independently' \ + "$TEMPLATE_DIR/pallets/actors/src/tests/wakeups.rs" \ + "Actors independent Pipeline/Trigger temporal topology evidence is missing" + require_anchor 'cadenced_trigger_occurrence' \ + "$TEMPLATE_DIR/runtime/src/weights/pallet_deos_actors.rs" \ + "Actors generated Cadenced Trigger Weight owner is missing" + reject_pattern 'Cadenced never reads or sets `pending_signal`|including while Running/Suspended or already pending|setting or coalescing readiness' \ + "Actors documentation revived paid latched activity or bypassed canonical cadence readiness" \ + "$TEMPLATE_DIR/pallets/actors/docs" "$PROJECT_ROOT/docs" + reject_pattern '\bFresh (?:cohort|opportunity|opening|Active|activation|cycle)\b' \ + "Actors specification revived ambiguous Fresh scheduler vocabulary" \ + "$TEMPLATE_DIR/pallets/actors/docs/specification.en.md" + require_anchor 'observation_fanout_blocked_page' \ + "$TEMPLATE_DIR/pallets/actors/src/lib.rs" \ + "Actors ordinary fanout admission omits the measured blocked-fallback owner" require_anchor '`Attempt identity proof`' \ "$TEMPLATE_DIR/pallets/actors/docs/architecture.en.md" \ "Actors attempt-identity proof is missing from the implementation map" diff --git a/.agents/skills/alignment/scripts/auditor.sh b/.agents/skills/alignment/scripts/auditor.sh index 2d02affe..266178f0 100755 --- a/.agents/skills/alignment/scripts/auditor.sh +++ b/.agents/skills/alignment/scripts/auditor.sh @@ -334,7 +334,7 @@ run_audit() { actors_execution_panic_allowlist="$(inventory_patterns actors-execution-panic-owner-allowlist)" actors_pallet_panic_allowlist="$(inventory_patterns actors-pallet-panic-owner-allowlist)" runtime_panic_allowlist="$(inventory_patterns runtime-panic-owner-allowlist "$PROJECT_ROOT/.agents/skills/alignment/rules/runtime-panic-rules.json")" - check_rule "\\.iter\\(\\)|for .* in .*::iter\\(\\)" "Potential unbounded state iteration detected. DEOS prefers bounded/O(1) mechanics." "tests.rs|/tests/|benchmarking.rs|mock.rs|deos-bypass: bounded-iter|legs\\.iter\\(|normalized_transfers\\.iter\\(|tracked\\.iter\\(|${bounded_iteration_allowlist}" "O(N) Iteration Anti-Pattern" "template/pallets/staking/docs/architecture.en.md" "FATAL" "hallucination" "The violation is a concrete false move against the O(1) contract" + check_rule "\\.iter\\(\\)|for .* in .*::iter\\(\\)" "Potential unbounded state iteration detected. DEOS prefers bounded/O(1) mechanics." "tests.rs|/tests/|/vendor/|benchmarking.rs|mock.rs|deos-bypass: bounded-iter|legs\\.iter\\(|normalized_transfers\\.iter\\(|tracked\\.iter\\(|${bounded_iteration_allowlist}" "O(N) Iteration Anti-Pattern" "template/pallets/staking/docs/architecture.en.md" "FATAL" "hallucination" "The violation is a concrete false move against the O(1) contract" check_rule "StorageValue<.*, Vec<|StorageMap<.*, Vec<" "Unbounded Vec found in storage. Use BoundedVec for consensus state." "tests.rs|/tests/|mock.rs|deos-bypass: vec" "Unbounded Vector Anti-Pattern" "docs/read-model.contract.en.md" "WARNING" "hallucination" "Consensus-state unboundedness is a direct architecture falsehood" check_rule "sudo_|ensure_root" "Admin/root policy detected. DEOS prefers mechanism over policy for core flows." "tests.rs|/tests/|mock.rs|pallet-governance|deos-bypass: admin" "Sudo Root Policy Anti-Pattern" "docs/manifesto.en.md" "FATAL" "boundary-drift" "This usually signals a mechanism-to-policy drift across a constitutional boundary" check_rule "pub fn claim.*\\(origin" "Traditional claim extrinsic detected. Prefer token-driven balance ingress where possible." "tests.rs|/tests/|mock.rs|staking|deos-bypass: claim" "Empty Extrinsic Anti-Pattern" "docs/core.architecture.en.md" "WARNING" "boundary-drift" "This usually means token-driven ingress is drifting back into user-pulled extrinsic control" @@ -353,7 +353,7 @@ run_audit() { fi check_rule "panic!|unreachable!|\\.expect\\(|\\.unwrap\\(" "Unowned production pallet panic surface detected. Return a typed failure or mark a mechanically precluded same-line owner." "tests.rs|mock.rs|benchmarking.rs|deos-bypass: panic-owner" "Unowned Production Pallet Panic Surface" "BACKLOG.md" "FATAL" "hallucination" "Production pallet panic assumptions require an explicit owner and falsifiable preclusion evidence" "/template/pallets/(asset-registry|governance|oracle|router|staking|tmc)/src/" if [[ "$MODE" == "changed" ]]; then - check_rule "panic!|unreachable!|\\.expect\\(|\\.unwrap\\(" "New unowned Rust panic surface detected. Return a typed failure, prove the site mechanically precluded, or mark the same line with 'deos-bypass: panic-owner' and its owner." "tests.rs|/tests/|mock.rs|benchmarking.rs|/examples/|genesis_config_presets.rs|embedding-runtime|deos-bypass: panic-owner" "Unowned Runtime Panic Surface" "BACKLOG.md" "FATAL" "hallucination" "Reachable panic assumptions require an explicit owner and falsifiable preclusion evidence" + check_rule "panic!|unreachable!|\\.expect\\(|\\.unwrap\\(" "New unowned Rust panic surface detected. Return a typed failure, prove the site mechanically precluded, or mark the same line with 'deos-bypass: panic-owner' and its owner." "tests.rs|/tests/|/vendor/|mock.rs|benchmarking.rs|/examples/|genesis_config_presets.rs|embedding-runtime|deos-bypass: panic-owner" "Unowned Runtime Panic Surface" "BACKLOG.md" "FATAL" "hallucination" "Reachable panic assumptions require an explicit owner and falsifiable preclusion evidence" fi } diff --git a/.agents/skills/architecture-experiments/SKILL.md b/.agents/skills/architecture-experiments/SKILL.md new file mode 100644 index 00000000..d47da1e9 --- /dev/null +++ b/.agents/skills/architecture-experiments/SKILL.md @@ -0,0 +1,336 @@ +--- +name: architecture-experiments +description: Preserves evidence-driven physical architecture experiments, candidate decisions, rejected alternatives, exact baselines, cross-release lineage, and the next optimization gradient without allowing benchmarks to redefine semantics. +--- + +# Architecture Experiments + +Use this skill when implementation work must choose among physical architectures, measured geometry, resource allocations, lowering strategies, or other benchmark-sensitive mechanisms. It makes optimization cumulative across releases by preserving what was tried, against which baseline, with which artifacts, why a candidate won or lost, when evidence became stale, and what experiment should follow. + +Experiments are decision instruments, not output. Open one only when a real implementation choice can materially affect a declared project target. Seek the highest decision-relevant performance among designs that preserve explicit functionality, safety, boundedness, and operability constraints. Do not create candidates or benchmarks to exercise the method, fill a portfolio, or accumulate evidence. Measure the smallest comparison that can select or reject a design, stop when the decision is supported, and delete candidate code that does not win. + +## Ownership Boundary + +This skill owns: + +- Falsifiable architecture hypotheses and candidate boundaries. +- Exact baseline and controlled-comparison contracts. +- Durable track-local Experiment Records, status transitions, relations, and track indexes. +- First-class experiment tracks that partition stable physical research domains, baselines, gradients, and cross-track evidence flow. +- Multidimensional interpretation, Pareto classification, architectural decisions, rejected alternatives, invalidation, and next-gradient selection. +- Benchmark design, measurement hygiene, evidence classification, production-Weight handoff, Experimental Closure, and Architecture Provenance judgement. + +It does not own: + +- Protocol semantics or specification acceptance. +- Benchmark command implementations, generated Weight files, tests, release publication, or architecture-document truth. +- Open-work state, which remains in `BACKLOG.md`. + +Root scripts and pallet harnesses mechanically execute measurements; this Skill is the single policy owner for why, what, and how to measure, interpret, decide, and retain architectural evidence. Routine regression checks may use the same method without creating an Experiment Record; open one when a material regression changes assumptions, needs architectural diagnosis, or introduces a candidate. + +Experiment evidence is partitioned under [`tracks`](./tracks/). Each `tracks//experiments.md` owns its charter and canonical local index, and sibling `EXP-NNNN.md` files own evidence. This co-locates method, track direction, rejected alternatives, and cross-release lineage without diffusing history into project documentation. The canonical record template is [`templates/EXP-NNNN.md`](./templates/EXP-NNNN.md). Copy it to `tracks//EXP-NNNN.md`; canonical identity is `/EXP-NNNN`, while the Markdown title carries the semantic name. Keep compact measurements, observations, interpretation, and decisions directly in that record by default. When justified raw artifacts would make inline Markdown materially worse, place them in the record's sibling directory `tracks//EXP-NNNN/`. Track qualification is mandatory because numeric IDs may repeat across tracks. + +Project documentation may cite Experiment IDs as compact provenance, but it describes only accepted project truth and never owns raw measurements, candidate history, or experiment relations. `BACKLOG.md` owns remaining work. Deleting this Skill intentionally deletes its private experimental-memory capability but must not affect builds, tests, CI, release validation, or runtime behavior. + +## Canonical Development Order + +Experiments preserve this mandatory order: + +```text +Specification +→ Implementation + ├ candidate construction + ├ controlled measurement + ├ evidence recording + ├ candidate comparison + └ implementation convergence +→ Tests +→ Implementation Correction +→ Domain Architecture +``` + +Specification owns semantic architecture: behavior, ordering, atomicity, fairness, failure, causality, ownership, and safety. Experiment Records own why one conforming physical implementation was selected. Architecture documents own current accepted implementation truth after tests and correction converge. + +An experiment MUST NOT silently redefine semantics. If evidence shows the specification is defective: + +```text +finding → mark affected evidence invalidated +→ explicitly reopen Specification in BACKLOG.md +→ record the semantic conflict and rationale +→ change and accept Specification +→ repeat Implementation → Tests → Correction → Architecture +``` + +A faster candidate that changes externally observable semantics, deterministic behavior, FIFO, causal speed, atomicity, rollback, ownership, economic behavior, correctness, or production Weight soundness is not an optimization candidate. Reject it from the comparison and route it as a semantic proposal. + +## Record Identity and Layout + +Use track-local monotonic IDs `EXP-NNNN`, zero-padded to four digits. The canonical identity is `/EXP-NNNN`; the numeric suffix may repeat across tracks. Within one track IDs never encode a release, are never reused, and remain stable across renames, supersession, or invalidation. Allocate the next ID from the highest ID in that track's `experiments.md`; concurrent branches touching the same track must resolve collisions before merge. + +```text +.agents/skills/architecture-experiments/ +├ SKILL.md +├ scripts/ +│ └ validate-record-normalization.sh +├ templates/ +│ └ EXP-NNNN.md +└ tracks/ + ├ actors/ + │ ├ experiments.md + │ ├ EXP-NNNN.md + │ └ EXP-NNNN/ + │ └ raw-artifact.ext + ├ adapters/ + │ ├ experiments.md + │ └ EXP-NNNN.md + └ router/ + ├ experiments.md + └ EXP-NNNN.md +``` + +Every experiment record is copied from `templates/EXP-NNNN.md`; its track-qualified path is stable identity. A Proposed question MAY exist only as a row in that track's `experiments.md`; create the record before transitioning to Prepared. Do not create empty track or artifact directories, semantic filename aliases, or a global evidence directory keyed only by unqualified numeric ID. Keep compact evidence in the record. CSV and TSV are tabular evidence, not standalone artifacts: convert their rows into Markdown tables inside `EXP-NNNN.md`, then delete the temporary delimited file. Never create or retain an `EXP-NNNN/` directory for CSV, TSV, or another delimited-table encoding. Create the sibling `EXP-NNNN/` artifact directory only for non-tabular raw evidence whose fidelity, machine consumption, or reviewability prevents faithful inline retention; artifact filenames describe their workload or candidate without repeating the parent ID. + +Each track index is concise navigation plus its stable charter, not experiment evidence. Each prepared-or-later `EXP-NNNN.md` follows the canonical template and links any ID-prefixed sibling artifact. Rejected, Superseded, Invalidated, and Inconclusive records remain permanent and discoverable after candidate code is deleted. + +### Record Normalization + +[`templates/EXP-NNNN.md`](./templates/EXP-NNNN.md) is the executable normalization source for metadata field order and second-level section order. Every track record must match that shape exactly; record-specific third-level subsections remain permitted. The record's `Primary track` value must be a relative Markdown link whose label is the containing `` and whose target is sibling `experiments.md`. + +Run `./.agents/skills/architecture-experiments/scripts/validate-record-normalization.sh` after creating, moving, or restructuring any Experiment Record or changing the template. The validator discovers every `tracks//EXP-NNNN.md`, derives the canonical shape from the template, and fails on metadata, section, primary-track drift, or any retained CSV/TSV under `tracks/`. It is private Skill-method validation and must not become a dependency of project validation or the completion gate. + +## Experiment Tracks + +A track is a stable physical research domain that can carry multiple experiment lineages across releases. It is not a release phase, backlog, folder label, or second experiment index. Create a track only when it has a distinct scope owner, invariant boundary, baseline lineage, reusable question portfolio, and entry/exit rule. Otherwise assign the question to an existing track. A portfolio names possible decision gradients only; it never authorizes an experiment without current implementation pressure. + +Each track owns one `tracks//experiments.md` charter/index and sibling Experiment Records. The charter/index owns only: + +- Scope and explicit exclusions. +- Governing invariants shared by its experiments. +- Accepted physical baseline references. +- Research question families and next-gradient portfolio. +- Directional cross-track evidence dependencies. +- Entry, transfer, dormancy, and retirement conditions. +- Links to related Experiment IDs. + +A track MUST NOT duplicate experiment status, measurements, interpretation, decisions, or open-work state. Those remain in the owning track index, Experiment Records, and `BACKLOG.md` respectively. Every Experiment Record belongs to its containing primary track and MAY link related tracks. The primary track owns the decision question; related tracks receive or provide evidence without becoming co-owners. + +Cross-track dependencies must be directional and acyclic. When a proposed experiment would create a cycle, split the question at the evidence boundary or choose the track that owns the changed physical mechanism. Update `experiments.md` when its track boundary, accepted baseline, portfolio, dependency, lifecycle, experiment status, or relation changes. Moving an experiment between tracks changes canonical identity and is allowed only before a record reaches Prepared; otherwise supersede it with a related record in the receiving track. + +## Lifecycle + +| Status | Meaning | Permitted next states | +| --- | --- | --- | +| Proposed | A decision-relevant hypothesis exists; candidate or controls are not ready | Prepared, Rejected, Superseded | +| Prepared | Baseline, candidates, controls, workloads, criteria, commands, and artifact plan are ready | Measuring, Invalidated | +| Measuring | Controlled execution has begun but required samples or profiles are incomplete | Measured, Inconclusive, Invalidated | +| Measured | Raw or normalized evidence exists; no interpretation is accepted yet | Interpreted, Inconclusive, Invalidated | +| Interpreted | Measurement, uncertainty, Pareto shape, and validity have been analyzed separately from decision | Accepted, Rejected, Inconclusive, Invalidated | +| Accepted | Candidate becomes or materially informs the implementation baseline | Superseded, Invalidated | +| Rejected | Evidence is sufficient not to select the candidate | Superseded, Invalidated | +| Inconclusive | Evidence cannot distinguish candidates or support the decision | Proposed, Superseded, Invalidated | +| Superseded | Later stronger evidence replaces the old decision while preserving its historical validity | Invalidated | +| Invalidated | Changed assumptions mean the evidence no longer supports its prior current claim | Terminal; create or relate a replacement experiment | + +Status describes evidence maturity, not code completion. Never jump from Measured to Accepted without an explicit Interpretation. Never rewrite an old decision to imitate later knowledge; append relations and transition it to Superseded or Invalidated with rationale. + +## When to Open an Experiment + +Open one when all are true: + +- A conforming physical choice could materially change a release target, resource bound, lifecycle dispatchability, state footprint, correctness simplicity, or scaling dependency. +- At least two candidates or one candidate plus an exact baseline can be controlled comparably. +- The result can change an implementation decision. +- The smallest falsifying workload and materiality threshold can be stated. + +Do not open one for: + +- A normative semantic choice. +- Routine regression tests, ordinary profiling with no decision, or generation of already-selected production Weight. +- A cosmetic refactor with no measurable architecture question. +- An idea already rejected under still-valid equivalent conditions. +- Measurement whose result cannot change the implementation. + +Before opening, search the index by affected domain, mechanism, question, candidate, and relations. Read every linked Accepted, Rejected, Superseded, or Invalidated predecessor that shares the mechanism. Either refine prior evidence or explain which assumption makes repetition necessary. + +## Experiment Protocol + +### 1. Lock the Decision Question + +State one decision, the owning implementation phase, governing specification sections, and semantic invariants candidates may not change. Separate physical variables from semantic constants. + +Declare materiality before measurement. Use a release target, maximum RefTime/ProofSize envelope, lifecycle dispatchability, throughput or latency bound, eliminated read/write/scaling dependency, state-hold reduction, or justified minimum percentage. A tiny numeric win without architectural significance is not material by default. + +### 2. Establish the Exact Baseline + +Name: + +- Baseline Experiment ID when one exists. +- Exact architecture and source commit/tree. +- Runtime configuration and relevant constants. +- Benchmark runtime Wasm hash and final production runtime Wasm hash as separate fields when applicable. +- Generated Weight identity, Rust toolchain, benchmark CLI/configuration, database backend, and workload. +- State population and warm/cold cache assumptions. + +Never write “faster than before.” If exact identity is unavailable, narrow the claim or keep the experiment Proposed/Prepared. + +### 3. Prepare Minimal Candidates + +Construct the smallest candidate implementations capable of falsifying the hypothesis. Preserve one semantic workload and all governing invariants. Keep candidate-only code isolated enough to delete after decision without deleting the record. + +List controlled variables and changed variables separately. If multiple variables differ, say so and do not attribute the result to one variable. Use the same toolchain, commands, repeats, state, runtime constants, Weight implementation, database configuration, and cache assumptions unless the changed variable explicitly requires otherwise. + +### 4. Measure by Evidence Class + +Classify every evidence source: + +| Evidence | Authority | +| --- | --- | +| Synthetic microbenchmark | Isolated mechanism behavior; no runtime or release throughput claim | +| Pallet benchmark | Bounded pallet branch, storage model, and generated Weight inputs under benchmark setup | +| Native stress benchmark | Noisy native host/runtime latency, throughput, memory, or queue behavior in the declared environment | +| Integration benchmark | Cross-component behavior and contention for the measured composition | +| Production-Wasm benchmark | Production runtime execution and Weight evidence for measured cases | +| Full-runtime block profile | Composition, contention, throughput, and block-budget evidence | +| Exact release-tree validation profile | Strongest tree-bound release conclusion within recorded conditions | + +Production evidence has stronger authority than exploratory evidence. Never let weaker evidence silently override stronger evidence or project a microbenchmark into production truth. + +Match statistical method to evidence class. For noisy wall-clock work, declare warmup, repeated-run count and rationale, distribution or relevant percentiles, variance, and predeclared outlier handling; do not hide tails behind an average. Keep setup deterministic and outside the measured region, isolate competing processes, record host/cache conditions, random seed, and contamination. For deterministic/model-generated FRAME Weight, use the required steps/repeats and generated model review rather than irrelevant statistical ceremony. + +Record relevant dimensions independently: RefTime, ProofSize, database reads/writes, encoded persistent state, lifecycle Weight, create/update/close cost, state hold, throughput, latency, queue pressure, fragmentation, memory, Wasm size, node wall-clock behavior, and TryRuntime cost. Omit irrelevant metrics explicitly rather than fabricating values. + +For parameter geometry, choose candidates from measured boundaries and information value. Avoid blind sweeps; use one only when geometry is unknown, bounded sweep cost is justified, and the result can materially change architecture. + +### 5. Execute and Integrate Runtime Benchmarks + +For DEOS FRAME work, read the changed call path, benchmark, `WeightInfo`, runtime binding, governing specification, and open experiment before execution. Define bounded components, worst-case state, and postconditions; split branches when proof, reads/writes, cleanup, or failure topology differs. Construct the smallest state that reaches the real worst case, keep setup outside the measured block, assert the intended branch, and use measured ProofSize mode for storage-sensitive paths. + +Use [`scripts/benchmarks.sh`](../../../scripts/benchmarks.sh) and its `--help` as the command owner. Run `--check` first, then one focused extrinsic or coherent same-runtime matrix. `--skip-build` is valid only while reusing the same freshly built benchmark runtime; rebuild after source, features, runtime configuration, or toolchain changes. Temporary focused output may support review but never replaces the complete generated pallet file. + +For accepted production Weight: + +- Keep RefTime and ProofSize separate and model independent stop conditions. +- Review maximum branch ownership, parameterized geometry, storage annotations, and generated database reads/writes against implementation reality. +- Distinguish minimum execution time, actual observed work, declared/charged Weight, and generated model; none substitutes for another. +- Verify benchmark name, `WeightInfo`, generic fallback, runtime implementation, and production binding agree; no placeholder runtime Weight remains. +- Rebuild production Wasm through [`scripts/03-build-runtime.sh`](../../../scripts/03-build-runtime.sh), retain benchmark-runtime and post-generation/final production Wasm identities separately, and run focused compile/check, formatting, Clippy, tests, and changed-scope completion. + +A host timing, count ceiling, diagnostic run, or `Weight::MAX` test does not establish ordinary-block capacity. Production claims require runtime-bound generated methods plus production-Wasm or stronger composition evidence. + +### 6. Preserve Evidence + +Keep every normalized or raw tabular dataset directly in the Markdown record as a compact table. CSV, TSV, and equivalent delimited-table files are temporary interchange only: integrate their complete decision-relevant rows into `EXP-NNNN.md` and delete them before the experiment checkpoint. Size or machine consumption does not justify a separate tabular artifact; split or summarize the inline table without discarding decision-relevant evidence. Retain a separate raw log, plot, binary, trace, or other non-tabular artifact only when fidelity or reviewability genuinely requires another file. Store such artifacts under the sibling track-qualified `tracks//EXP-NNNN/` directory and link specific files from the record; never use a global `evidence/EXP-NNNN/` path because numeric IDs are track-local. Retain full raw output only when small, uniquely valuable, or required to review/reproduce the decision. Large output may remain external or ephemeral only when the record preserves exact commands, hashes, parameters, environment, and sufficient normalized measurements. + +Do not claim reproducibility when an essential artifact or condition was discarded. Never use one ambiguous `artifact_hash`; identify source tree, benchmark Wasm, production Wasm, generated Weight, and raw-output digest separately. + +### 7. Separate Result, Interpretation, and Decision + +- `Result`: What was measured, including uncertainty and deltas. +- `Interpretation`: What the evidence implies, which dimension binds, limitations, confounders, and Pareto relation. +- `Decision`: Which candidate is selected or why none is selected, against declared criteria. + +A lower RefTime does not imply acceptance. Classify candidates as Pareto-improving, Pareto-dominated, a tradeoff, or a binding-dimension winner. Do not collapse dimensions into an arbitrary scalar unless the governing resource policy defines and justifies that objective. + +Negative outcomes are valid: no material difference, regression, inconclusive, invalid experiment, rejected candidate, or falsified hypothesis. Record each rejected alternative with evidence and reason. + +### 8. Update Baseline and Lineage + +For Accepted decisions, name the new physical baseline and relate the record with `replaces`, `refines`, or `validates`. For later contrary evidence, use `supersedes`, `contradicts`, or `invalidates`. Relations are bidirectionally discoverable through index rows and record links. + +Update the primary track's accepted baseline only after an Accepted decision changes that track's physical baseline. Preserve `Affected domain` and `Physical mechanism` inside the record as finer evidence labels; they do not replace track ownership. Release remains an index column rather than a release-local archive. + +### 9. Derive the Next Gradient + +Do not brainstorm from zero. Ask in order: + +1. Which target is failing or furthest from its envelope? +2. Which resource dimension binds? +3. Which measured component dominates that dimension? +4. Which physical architecture owns that component? +5. Which conforming change could reduce it? +6. What is the smallest experiment that can falsify the hypothesis? + +Record: + +```text +target → measured gap → binding dimension → dominant contributor +→ owning mechanism → next hypothesis → smallest falsifier +``` + +Also record eliminated hypotheses, remaining plausible hypotheses, and the successor Experiment ID when allocated. + +### 10. Reconcile Project Truth + +- Keep open work only in `BACKLOG.md`; link the Experiment ID rather than duplicating results. +- Keep current implementation truth in code and tests. +- After implementation, tests, and correction converge, cite significant accepted Experiment IDs from architecture prose without copying full tables. +- Keep completed causal history in Experiment Records, not the changelog. +- Update the owning track's `experiments.md` in the same change as every record status or decision transition. + +## Validity and Invalidation + +Review related records when any dependency changes materially: + +- Storage layout or state population geometry. +- Database backend or cache assumptions. +- MaxBlockWeight, ProofSize limit, Actor resource policy, or relevant runtime constants. +- Compiler/toolchain, benchmark method, or Wasm build. +- Task semantics, host adapter, underlying pallet Weight, or production Weight implementation. +- Workload, fairness, queueing, causality, or lifecycle contract. + +Use Superseded when stronger evidence replaces a still-historically-valid decision. Use Invalidated when changed assumptions remove support for the old claim. State whether qualitative insight remains useful; never delete the record. + +## Closure Gates + +### Experimental Closure Gate + +Implementation alternatives are converged enough to enter the main Test phase only when: + +- Every architecture-affecting alternative is Accepted, Rejected, Inconclusive, or explicitly deferred in `BACKLOG.md` with rationale. +- Every accepted benchmark-sensitive physical choice has an Experiment Record. +- Every rejected candidate remains indexed and discoverable. +- Baseline, artifacts, workloads, measurements, interpretation, decision, and validity are explicit. +- No decision exists only in chat, temporary output, commit messages, or developer memory. + +### Architecture Provenance Gate + +Domain Architecture may close only when every significant physical decision is traceable to at least one normative specification section, Accepted Experiment Record, production benchmark, or correctness/security invariant. Architecture states current truth and cites provenance compactly; it does not become an experiment log. + +## Stop Rules + +Stop the experiment loop when any applies: + +- The acceptance target is met. +- No candidate has a plausible decision-relevant advantage. +- Remaining delta is below declared materiality. +- The bottleneck moved to another architecture domain. +- Further progress requires semantic change and therefore Specification reopening. +- Evidence is insufficient; mark Inconclusive and state the exact missing evidence. +- The task contract or user says stop. + +Do not optimize indefinitely because another microbenchmark improvement is imaginable. Performance never outranks correctness, deterministic semantics, atomicity, FIFO, causal speed, ownership, rollback, runtime safety, or production Weight soundness. + +## Track Registry + +Track indexes are the canonical navigation surfaces for experiment status and lineage. + +| Track | Scope | Index | +| --- | --- | --- | +| Actors | Actor storage, scheduling, control, lifecycle, and executor topology | [tracks/actors/experiments.md](./tracks/actors/experiments.md) | +| Adapters | Runtime adapter boundaries, lowering, and effect-resource evidence | [tracks/adapters/experiments.md](./tracks/adapters/experiments.md) | +| Router | Route search, quote, proof, and execution topology | [tracks/router/experiments.md](./tracks/router/experiments.md) | + +Add a track only after its boundary, invariants, portfolio, dependencies, and entry/exit conditions are concrete. Create `tracks//experiments.md` first; do not reserve empty directories. Track-local indexes allocate IDs, own statuses and relations, and preserve rejected or invalidated records. + +## Handoff + +Report: + +- Experiment ID, status transition, decision question, and mechanism. +- Exact baseline and candidate artifact identities. +- Evidence class, workloads, controlled/changed variables, and validation. +- Result, Interpretation, and Decision as separate statements. +- Pareto class, binding dimension, rejected alternatives, and validity scope. +- New baseline or reason none changed. +- Next gradient or stop condition. +- Index, backlog, and eventual architecture provenance updates. diff --git a/.agents/skills/architecture-experiments/scripts/validate-record-normalization.sh b/.agents/skills/architecture-experiments/scripts/validate-record-normalization.sh new file mode 100755 index 00000000..7eea7ca5 --- /dev/null +++ b/.agents/skills/architecture-experiments/scripts/validate-record-normalization.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +set -euo pipefail + +skill_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" +template="$skill_dir/templates/EXP-NNNN.md" + +if [[ ! -f "$template" ]]; then + printf 'error: canonical template not found: %s\n' "$template" >&2 + exit 1 +fi + +mapfile -t records < <(find "$skill_dir/tracks" -mindepth 2 -maxdepth 2 -type f -name 'EXP-[0-9][0-9][0-9][0-9].md' -print | sort) +mapfile -t delimited_tables < <(find "$skill_dir/tracks" -type f \( -name '*.csv' -o -name '*.tsv' \) -print | sort) +if [[ "${#delimited_tables[@]}" -gt 0 ]]; then + printf 'Experiment Record normalization failed: CSV/TSV evidence must be integrated as Markdown tables in the owning record:\n' >&2 + printf -- '- %s\n' "${delimited_tables[@]}" >&2 + exit 1 +fi + +node --input-type=module - "$template" "${records[@]}" <<'NODE' +import fs from 'node:fs'; +import path from 'node:path'; + +const [template, ...records] = process.argv.slice(2); +const failures = []; +const expectedFields = extractFields(fs.readFileSync(template, 'utf8'), template); +const expectedSections = extractSections(fs.readFileSync(template, 'utf8')); + +for (const record of records) { + const source = fs.readFileSync(record, 'utf8'); + const fields = extractFields(source, record); + const sections = extractSections(source); + const track = path.basename(path.dirname(record)); + const primaryTrack = fieldValue(source, 'Primary track'); + + compare(record, 'metadata fields', expectedFields, fields); + compare(record, 'second-level sections', expectedSections, sections); + + if (primaryTrack !== `[${track}](./experiments.md)`) { + failures.push(`${record}: Primary track must be [${track}](./experiments.md), found: ${primaryTrack}`); + } +} + +if (failures.length > 0) { + console.error('Experiment Record normalization failed:'); + for (const failure of failures) console.error(`- ${failure}`); + process.exit(1); +} + +console.log(`Experiment Record normalization passed: ${records.length} record(s), ${expectedFields.length} metadata field(s), ${expectedSections.length} section(s)`); + +function extractFields(source, file) { + const table = source.match(/^\| Field \| Value \|\n\| --- \| --- \|\n((?:\|.*\n)+)/m); + if (!table) { + failures.push(`${file}: missing canonical metadata table immediately under the title`); + return []; + } + return [...table[1].matchAll(/^\| ([^|]+) \| .* \|$/gm)].map((match) => match[1].trim()); +} + +function extractSections(source) { + return [...source.matchAll(/^## ([^#].*)$/gm)].map((match) => match[1].trim()); +} + +function fieldValue(source, name) { + const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + return source.match(new RegExp(`^\\| ${escaped} \\| (.*) \\|$`, 'm'))?.[1].trim() ?? ''; +} + +function compare(file, label, expected, actual) { + if (JSON.stringify(expected) === JSON.stringify(actual)) return; + failures.push(`${file}: ${label} differ from template\n expected: ${expected.join(' | ')}\n actual: ${actual.join(' | ')}`); +} +NODE diff --git a/.agents/skills/architecture-experiments/templates/EXP-NNNN.md b/.agents/skills/architecture-experiments/templates/EXP-NNNN.md new file mode 100644 index 00000000..a6ab1b18 --- /dev/null +++ b/.agents/skills/architecture-experiments/templates/EXP-NNNN.md @@ -0,0 +1,141 @@ +# EXP-NNNN — Short Decision Title + +| Field | Value | +| --- | --- | +| Status | Proposed / Prepared / Measuring / Measured / Interpreted / Accepted / Rejected / Inconclusive / Superseded / Invalidated | +| Architecture release / experiment campaign | Measured X.Y.Z baseline / campaign X.Y.Z | +| Date | YYYY-MM-DD or Not recorded | +| Affected domain | Domain | +| Physical mechanism | Stable cross-release mechanism name | +| Primary track | Replace with a link to sibling `./experiments.md` | +| Related tracks | None or links to `..//experiments.md` | +| Baseline experiment | EXP-NNNN or None | +| Successor experiment | EXP-NNNN or None | + +## Decision Question + +State the single implementation choice this evidence can decide. + +## Governing Specification + +- `Exact section`: Path and heading. +- `Invariant`: Candidate-invariant semantic rule. +- `Reopen trigger`: Finding that would require Specification reopening rather than candidate selection. + +## Context and Hypothesis + +- `Current architecture`: Exact physical behavior at the baseline. +- `Reason`: Measured gap or decision pressure. +- `Hypothesis`: Falsifiable candidate claim. +- `Materiality`: Minimum decision-relevant change or release envelope. + +## Baseline and Candidates + +| ID | Architecture | Source tree | Runtime/config identity | +| --- | --- | --- | --- | +| Baseline | Exact baseline | Commit and tree | Relevant constants | +| A | Candidate A | Commit and tree | Relevant constants | + +## Controls + +- `Controlled variables`: Toolchain, Wasm build, Weight implementation, database, command, repeats, state, workload, cache assumptions, and constants held equal. +- `Changed variables`: Exact intended differences. +- `Confounders`: Known uncontrolled differences; state None when none are known. + +## Benchmark Class, Workloads, and Criteria + +- `Benchmark class`: Microbenchmark / pallet / native stress / integration / production-Wasm / full-runtime block / release-tree validation. + +| Workload | State/population geometry | Hot/cold assumptions | Purpose | +| --- | --- | --- | --- | +| W1 | Exact bounded setup | Declared | Decision witness | + +- `Metrics`: Relevant dimensions; omit only with rationale. +- `Statistical method`: Warmup, samples, repeats, variance/distribution, percentiles, and outlier policy where applicable; explain when deterministic Weight methodology makes them irrelevant. Integrate every CSV/TSV-style dataset below as Markdown rather than retaining a delimited artifact. +- `Acceptance criteria`: Candidate-selection rule declared before measurement. +- `Rejection criteria`: Semantic, correctness, multidimensional, or materiality failure. + +## Environment and Artifact Identity + +| Artifact | Identity | +| --- | --- | +| Source commit | Hash | +| Source tree | Tree hash | +| Rust toolchain | Exact version | +| Benchmark runtime Wasm | Hash or Not applicable | +| Post-generation production runtime Wasm | Hash or Not measured | +| Final release production runtime Wasm | Hash or Not measured | +| Generated Weight | File/hash/method identity or Not applicable | +| Database/backend | Exact configuration | +| Benchmark command/config | Exact command and parameters | +| Raw evidence | Inline tables below; repository path/digest only for justified non-tabular artifacts, or Not retained with reason | + +## Measurements + +| Candidate | Workload | RefTime | ProofSize | Reads | Writes | Other decision metrics | +| --- | --- | ---: | ---: | ---: | ---: | --- | +| Baseline | W1 | Not measured | Not measured | Not measured | Not measured | Not measured | + +## Derived Metrics + +| Candidate | Workload | RefTime delta | ProofSize delta | Throughput / latency | Marginal or amortized cost | Pareto class | +| --- | --- | ---: | ---: | --- | --- | --- | +| A | W1 | Not derived | Not derived | Not derived | Not derived | Not interpreted | + +## Statistical / Variance Notes + +Report warmup, distribution, variance, tails, outliers, contamination, and sample adequacy where applicable. For deterministic generated Weight, record model/repeat review instead. + +## Result + +Measurement only. Report observations, uncertainty, and negative results without selecting a candidate. + +## Interpretation + +Explain binding dimension, dominant contributor, Pareto relation, confounders, and what the evidence does or does not imply. + +## Decision + +State Accepted, Rejected, Inconclusive, Superseded, or Invalidated; identify the selected/new baseline or explain why none changed. Never merge this section with Result. + +## Rejected Alternatives and Tradeoffs + +| Candidate | Decision | Evidence-backed reason | +| --- | --- | --- | +| Candidate | Pending | Pending measurement | + +## Validity + +- `Establishes`: Exact supported claim. +- `Does not establish`: Explicit non-claims. +- `Known limitations`: Missing profiles, uncertainty, or scope restrictions. +- `Invalidation triggers`: Layout, runtime, toolchain, policy, adapter, Weight, or methodology changes that require review. +- `Residual qualitative insight`: Useful intuition if numeric authority later becomes invalid. + +## Follow-Up / Next Gradient + +```text +target → measured gap → binding dimension → dominant contributor +→ owning mechanism → next hypothesis → smallest falsifier +``` + +- `Eliminated hypotheses`: None yet. +- `Remaining hypotheses`: Candidate list. +- `Stop condition`: Exact target/materiality boundary. + +## Architecture Impact + +- `Affected physical truth`: Domain/mechanism. +- `Architecture update`: Required only after implementation, tests, and correction converge. +- `Provenance citation`: Suggested compact `EXP-NNNN` reference. + +## Relations + +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: Governing specification and baseline IDs. +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0001.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0001.md new file mode 100644 index 00000000..fcf2fc3d --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0001.md @@ -0,0 +1,173 @@ +# EXP-0001 — Current Actor Throughput and Service Stability Baseline + +| Field | Value | +| --- | --- | +| Status | Rejected | +| Architecture release / experiment campaign | DEOS 0.7.23 baseline / 0.7.24 optimization campaign | +| Date | 2026-08-25 | +| Affected domain | DEOS Actors runtime composition | +| Physical mechanism | FIFO scheduler, cadence wakeups, queue service, one-Step StopCycle path | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | None | +| Successor experiment | EXP-0013 | + +## Decision Question + +Does the current architecture sustain the 0.7.24 target of 100 completed one-Step Actor cycles per block while retaining bounded, uniform, starvation-free service as active population rises to 10,000? + +## Governing Specification + +- `Target`: `BACKLOG.md` → Quantitative Release Contract and S13 Reactive Transfer 10k profile. +- `Preserved invariants`: `template/pallets/actors/docs/specification.en.md` requires User/System class-neutral strict FIFO, temporal readiness, cutoff, bounded work, and Weight-metered execution. +- `Reopen trigger`: Any proposed speedup that changes FIFO, deterministic service, causal timing, atomicity, or economic effect semantics is a semantic proposal, not a candidate optimization. + +## Context and Hypothesis + +- `Current architecture`: Active Actors hold hot scheduler state. Cadence-1 obligations enter bounded wakeup buckets, materialize into one monotonic paged FIFO, and execute through `on_initialize` plus `on_idle`. Queue service loads canonical Actor state, validates the live ticket and same-block guard, reserves branch Weight, consumes the paged head transactionally, executes one cycle, and schedules next work. Runtime configuration sets `MaxExecutionsPerBlock = 1,000`, queue page size `64`, and queue capacity `10,000`; Weight remains the primary admission bound. +- `Workload`: Mutable System Actors, each with one cadence-1 Step whose task is `StopCycle`, no precondition, persistent contract, and 10,000 native units. Genesis System Actors are removed. Every synthetic block runs Actors `on_initialize` then `on_idle(Weight::MAX)`. +- `Hypothesis`: The current physical scheduler can reach at least 100 completed cycles/block with stable service in this cheaper-than-Transfer one-Step profile. +- `Materiality`: The release target is 100 completed cycles/block. Stability requires zero failed Steps, no starvation, and cycle-count spread at most 4. + +## Baseline and Candidates + +| ID | Architecture | Source tree | Runtime/config identity | +| --- | --- | --- | --- | +| B0 | Released 0.7.23 FIFO/wakeup/paged scheduler | Tag commit `de498bf5a2822e48b1a08fc3f1951d2790c160ce`; release tree `88af295a9ec21d024b678d273500d23d0cd908d0`; template subtree `00f8044e913b978c4fe80c3ee77002226bb57269` | Native `deos-runtime` release test; `MaxExecutionsPerBlock=1000`; queue page `64`; production Weight file hash below | + +B0 is tested against the release envelope rather than another implementation. Rejecting the hypothesis establishes an optimization baseline; it does not reject the current implementation's correctness. + +## Controls + +- `Controlled variables`: Same source tree, release binary, toolchain, runtime constants, actor shape, initial balance, block counts, host, and warm cache across five measured repetitions. +- `Changed variables`: Active Actor population and corresponding bounded horizon: `48/96`, `100/150`, `1,000/252`, `10,000/418` actors/blocks. +- `Confounders`: Native in-memory test execution is not production Wasm or a real block producer. CPU frequency scaling and unrelated host load were not pinned. Setup time is included in elapsed wall clock but not in cycle counts. + +## Benchmark Class, Workloads, and Criteria + +- `Benchmark class`: Native integration stress benchmark plus runtime queue-occupancy profile. + +| Workload | State/population geometry | Hot/cold assumptions | Purpose | +| --- | --- | --- | --- | +| Wall-clock matrix | 48/100/1,000/10,000 Actors over 96/150/252/418 blocks | Warm native process after compilation | Measure deterministic cycle rate and noisy native host cost | +| Occupancy profile | 10,000 Actors over 418 blocks | Native in-memory runtime | Observe fairness and queue/wakeup frontier pressure | + +- `Metrics`: Completed cycles/block, native milliseconds/block, sample SD/CV, nonce spread, maximum queue, wakeup backlog, and wakeup buckets. +- `Statistical method`: Five warm wall-clock samples with min/mean/max, sample SD, CV, and no outlier removal; deterministic cycle counts must match. Occupancy runs once because it reports deterministic state counters, not host timing. +- `Acceptance criteria`: At least 100 completed cycles/block, zero failed assertions, no starvation, and cycle-count spread at most 4. +- `Rejection criteria`: Cycle rate below 100 or any correctness/fairness failure. + +## Environment and Artifact Identity + +| Artifact | Identity | +| --- | --- | +| Measured checkout | Campaign commit `36c1929d31e5caf422417a9c0d1d2f57ef9caebf`; tree `06a2adaab2f6124d5a9f7508d7b9fd432352e528`; template `00f8044e913b978c4fe80c3ee77002226bb57269` | +| Rust toolchain | `rustc 1.98.0 (88d9e12ae 2026-08-18)`, LLVM 22.1.8; `cargo 1.98.0 (797e8a9bc 2026-08-05)` | +| Host | Linux `7.1.9-200.fc44.x86_64`; AMD Ryzen 7 4800H, 8 cores/16 threads; frequency scaling enabled | +| Native benchmark binary | `template/target/release/deps/deos_runtime-c8acc5f75da755e3`; SHA-256 `a5161fe3fc7fb4313fbd95b25b3c74ed46de8bb0348356ee11f005d41b3ee82b` | +| Generated Weight | `template/runtime/src/weights/pallet_deos_actors.rs`; SHA-256 `26afb6f472d0471082fc2f92c1c8f1639a2c4d9a10c7dffede4dae9f6e8c6b01` | +| Benchmark/production Wasm | Not measured; no Wasm throughput claim | +| Wall-clock command | `cd template && cargo test --release -p deos-runtime --locked profile_scheduler_wallclock_matrix -- --ignored --nocapture`; five runs | +| Occupancy command | `cd template && cargo test --release -p deos-runtime --locked profile_scheduler_queue_wakeup_occupancy_10k -- --ignored --nocapture`; one run | +| Raw evidence | Normalized evidence retained below; separate logs omitted because tables, commands, binary, and environment preserve decision-relevant outputs | + +## Measurements + +| Actors | Blocks | Cycles | Cycles/block | ms/block min | ms/block mean | ms/block max | Sample SD | CV | +| ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| 48 | 96 | 3,935 | 40.9896 | 3.7094 | 3.8393 | 4.0214 | 0.1204 | 3.14% | +| 100 | 150 | 6,129 | 40.8600 | 3.8398 | 3.9737 | 4.0857 | 0.0886 | 2.23% | +| 1,000 | 252 | 9,980 | 39.6032 | 4.8052 | 4.8677 | 4.9504 | 0.0612 | 1.26% | +| 10,000 | 418 | 13,476 | 32.2392 | 9.8566 | 10.0084 | 10.2192 | 0.1538 | 1.54% | + +### Wall-Clock Samples + +| Run | 48 Actors | 100 Actors | 1,000 Actors | 10,000 Actors | +| ---: | ---: | ---: | ---: | ---: | +| 1 | 3.7534 | 3.9617 | 4.8095 | 9.8903 | +| 2 | 3.7094 | 3.8398 | 4.8052 | 9.8566 | +| 3 | 3.8514 | 4.0857 | 4.8795 | 10.2192 | +| 4 | 4.0214 | 3.9798 | 4.9504 | 9.9622 | +| 5 | 3.8610 | 4.0014 | 4.8941 | 10.1137 | + +Values are native milliseconds per synthetic block. Cycle counts were identical in every run: `3,935`, `6,129`, `9,980`, and `13,476` respectively. + +### Occupancy Observation + +| Actors | Blocks | Minimum nonce | Maximum nonce | Spread | Maximum queue | Maximum wakeup backlog | Maximum wakeup buckets | +| ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| 10,000 | 418 | 1 | 2 | 1 | 9,985 | 9,906 | 102 | + +Both tests passed with zero failed assertions. + +## Derived Metrics + +| Population | Cycles/block | Gap to target | Rate delta from 48 Actors | Pareto class | +| ---: | ---: | ---: | ---: | --- | +| 48 | 40.9896 | -59.0104 | Baseline | Not applicable; no candidate | +| 100 | 40.8600 | -59.1400 | -0.32% | Not applicable; no candidate | +| 1,000 | 39.6032 | -60.3968 | -3.38% | Not applicable; no candidate | +| 10,000 | 32.2392 | -67.7608 | -21.35% | Not applicable; no candidate | + +## Statistical / Variance Notes + +- Five native wall-clock samples were retained without outlier removal. CV ranges from 1.26% to 3.14%. +- Cycle counts were identical across runs, while host timing remains diagnostic because CPU scaling and unrelated host load were not pinned. +- The occupancy profile is deterministic behavioral evidence, not a production timing sample. + +## Result + +The current baseline completes about 41 cycles/block at 48–100 Actors, 39.60 at 1,000, and 32.24 at 10,000. The 10,000-Actor rate is 67.76% below the 100 cycles/block release target. Service remained starvation-free over 418 blocks and substantially inside the allowed fairness spread, but the queue and wakeup frontier approached full population occupancy. Native wall-clock variability was low across five samples, while mean host cost rose from 3.84 to 10.01 ms/block. + +## Interpretation + +- `Measurement`: The deterministic cycle counter falsifies the target hypothesis even for a no-effect StopCycle workload; the more expensive reactive Transfer target cannot be inferred to perform better. +- `Stability`: Relative actor service is uniform at 10,000 population (spread 1), and measured cycle counts are exactly repeatable. This is strong behavioral evidence for bounded fairness, not production timing evidence. +- `Scaling`: Throughput loses about 21.4% from the 48-Actor rate to the 10,000-Actor rate while queue/wakeup occupancy becomes dense. Population-sensitive control/materialization and queue pressure are plausible contributors. +- `Binding dimension`: Not established. This native profile combines wakeup, materialization, queue, canonical-state, execution, and rescheduling work and does not decompose RefTime, ProofSize, reads, or writes. +- `Pareto class`: Not applicable; no alternative architecture was measured. + +## Decision + +Reject the hypothesis that the current physical baseline already meets 100 completed cycles/block. Preserve this tree as the exact pre-optimization baseline. Do not change semantics or select a candidate from this experiment alone. + +## Rejected Alternatives and Tradeoffs + +| Candidate or claim | Decision | Evidence-backed reason | +| --- | --- | --- | +| Treat `MaxExecutionsPerBlock=1,000` as throughput | Rejected | The count is only a safety ceiling; measured service is 32.24–40.99 cycles/block. | +| Claim production readiness from native wall clock | Rejected | No production-Wasm, block-author, database, RefTime, or ProofSize profile was executed. | +| Optimize fairness before throughput | Deferred | Fairness spread is already 1 at 10,000; target throughput is the material gap. | + +## Validity + +- `Establishes`: DEOS 0.7.23 native deterministic throughput and fairness for cadence-1 System Actors executing one StopCycle Step under the recorded horizons. +- `Does not establish`: Reactive detection throughput, Transfer/economic effect throughput, production-Wasm capacity, six-second block safety, User contention, RefTime/ProofSize attribution, or database behavior. +- `Invalidation triggers`: Scheduler/wakeup/FIFO/hot-state layout, Weight implementation, runtime limits, Step semantics, workload harness, compiler, or benchmark methodology changes. +- `Residual insight`: Preserve exact cycle-rate and occupancy geometry as the comparison point even after numeric authority becomes stale. + +## Follow-Up / Next Gradient + +```text +target 100 cycles/block → measured 32.24 at 10k → gap 67.76% +→ binding resource unknown → combined one-Step control path dominates tested work +→ Actors hot-state/Step proof/FIFO-wakeup owners +→ decompose one-Step production Weight and ProofSize +→ focused production benchmark with storage annotations and external-operation control +``` + +- `Eliminated hypotheses`: Current baseline already meets target; fairness imbalance is the primary observed failure; the count ceiling predicts throughput. +- `Next experiment`: EXP-0013, one-Step Actor Weight/ProofSize/read/write decomposition, followed by EXP-0014 external-operation differential if effect/control ownership remains ambiguous. +- `Stop condition`: Stop this baseline thread now; it has falsified its hypothesis. Resume only through a candidate experiment with production evidence. + +## Architecture Impact + +- None. Current architecture remains unchanged; future accepted physical changes should cite this baseline as causal provenance. + +## Relations + +- `Baseline of`: EXP-0013 and later throughput candidates. +- `Depends on`: Accepted 0.7.23 Actors architecture and the separate 0.7.24 release target. +- `Confirms`: Current strict-FIFO fairness remains bounded in the measured 10,000-Actor profile. +- `Contradicts`: Any claim that the configured execution ceiling is current measured throughput. +- `Invalidates`: None. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0002.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0002.md new file mode 100644 index 00000000..730f0ee2 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0002.md @@ -0,0 +1,259 @@ +# EXP-0002 — Actor Contract Production Ceiling + +| Field | Value | +| --- | --- | +| Status | Invalidated | +| Architecture release / experiment campaign | DEOS 0.7.24 baseline / 0.7.24 topology-scaling campaign | +| Date | 2026-08-25 | +| Affected domain | DEOS Actors contract storage, lifecycle, and current-Step execution | +| Physical mechanism | Runtime `MaxContractSteps` ceiling and its dependent bounded state geometry | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0001](./EXP-0001.md) | +| Successor experiment | [EXP-0010](./EXP-0010.md) | + +## Decision Question + +Can the accepted `MaxContractSteps = 32` production profile replace the current 8-Step runtime baseline while maximum create/update/close and current-Step execution remain component-wise dispatchable, semantically identical short Contracts pay no global-ceiling tax, and maximum retained state remains bounded; if not, does controlled 16/8 evidence locate a physical blocker for another conforming candidate or demonstrate a genuine semantic infeasibility that requires explicit Specification reopening? + +## Governing Specification + +- `Exact sections`: [`template/pallets/actors/docs/specification.en.md` §1.4 Block-Paced Actor Cycles, §3.1 Complexity, Identity, and Admission, §4.0 Task Resource Boundary, §4.4 Atomicity, §10 Runtime Configuration, and §11 Conformance](../../../../../template/pallets/actors/docs/specification.en.md); [`docs/actors-resource-policy.specification.en.md` §1 Resource Domain, §2 Resource Algebra, §4 Admission and Actual Weight, and §5 Maximum Step Fit and FIFO Fragmentation](../../../../../docs/actors-resource-policy.specification.en.md); [`docs/actors-performance-assurance.specification.en.md` §1 Evidence Contract and §4 Contract Complexity Matrix](../../../../../docs/actors-performance-assurance.specification.en.md). +- `Backlog contract`: [`BACKLOG.md` No Ceiling Tax, Quantitative Release Contract, and I1 Max Steps](../../../../../BACKLOG.md) fix production at 32 unless Specification is explicitly reopened; 8 and 16 are comparative profiles only. +- `Invariant — No Ceiling Tax`: Raising the global ceiling MUST NOT add cold-body reads, unused Predicate work, full-pipeline orchestration or fees, maximum-pipeline ProofSize, maximum state hold, or unread fragments to an otherwise identical one-, two-, or three-Step Contract. +- `Invariant — lifecycle dispatchability`: Maximum admitted create, activate, update, deactivate, cancel, and close geometry MUST fit the owning runtime dispatch envelope component-wise; maximum close MUST remove all bounded body/run/topology authority atomically without an unbounded prefix scan or orphan. +- `Invariant — component-wise Weight`: RefTime and ProofSize are independent stop conditions. Reads, writes, persistent bytes, state hold, loaded fragments, and Wasm size remain separate evidence and MUST NOT be collapsed into one score. +- `Invariant — semantics parity`: Candidate ceilings change no FIFO order, Actor class treatment, causal delay, one-Step-per-block pacing, Task behavior, fee origin, rollback boundary, semantic Contract identity, run-state ownership, or current-Step outcome. +- `Reopen trigger`: Reopen Specification only for a demonstrated semantic defect or infeasibility that remains after relevant conforming physical candidates are rejected. One P32 implementation exceeding an envelope rejects that physical candidate; it does not by itself invalidate the normative ceiling. P16/B8 may locate a threshold but cannot authorize a lower production ceiling. + +## Context and Hypothesis + +- `Current architecture`: The current baseline binds the admitted semantic Contract to one C6 head with inline Step 0, authority-bound tail chunks of at most four Steps, an admission certificate carrying one resource envelope per Step, and a block-paced executor that loads only the current fragment after Opening. The DEOS runtime still binds `(MaxContractSteps, MaxFundingTrackedAssets, MaxOpeningSnapshotEntries, MaxOpeningPredicateResults) = (8, 10, 16, 32)`. +- `Current runtime bounds`: `MaxBlockWeight = Weight(2,000,000,000,000 RefTime, 5,000,000 ProofSize)`, Normal dispatch is 50%, minimum `on_idle` reserve is 50%, `MaxExecutionsPerBlock = 1,000`, and both Active Actor and queue capacity are 10,000. Actor service applies the accepted dual-meter policy and current-Step resource envelopes. +- `Current implementation gap`: The accepted 0.7.24 specification fixes 32 while production remains at 8 pending this experiment. Preparation found that lifecycle benchmarks still authored one-Step bodies and the Contract-level idle-fit gate still summed a whole cycle despite current-Step execution. The harness now builds maximum C6 geometry for production create/activate/deactivate/update/close owners, asserts exact tail cardinality or complete removal, and admits the component-wise maximum individual current-Step envelope rather than a suffix sum. +- `Reason`: I1 cannot bind 32 from a count constant. It needs controlled production-Wasm and generated Weight evidence that separates short-Contract invariance from maximum lifecycle/state growth and one current-Step fit. +- `Hypothesis`: With only the ceiling tuple changed, P32 remains bounded and dispatchable; short Contract execution and state footprints are invariant across B8/P16/P32 except for an explicitly identified fixed runtime-owned cause; maximum retained geometry grows no faster than the admitted Step/body/snapshot bounds. +- `Materiality`: A semantic mismatch, one added cold-body read/write/fragment for a matched short Contract, any short-Contract persistent-byte/state-hold increase caused only by the global ceiling, any ProofSize increase caused by an unread maximum, or any component-wise dispatchability failure is material regardless of percentage. For otherwise structurally identical generated RefTime models, candidate medians must remain within ±5% of B8; this predeclared bound conservatively exceeds the largest `3.36%` preparation delta between 10/5 diagnostics and 50/20 B8 lifecycle generation. Five 50/20 short-path samples per profile are retained with no outlier removal. Smaller differences remain reportable and cannot excuse a structural tax. For maximum geometry, superlinear growth above the exact bound-derived cardinality plus one fixed header is material. + +## Baseline and Candidates + +| ID | Architecture | Source tree | Runtime/config identity | +| --- | --- | --- | --- | +| B8 | Current C6 hot-head/tail-chunk geometry and block-paced current-Step execution | HEAD `d2e1ab4f171f9dadec5bfb260e14e4b36bb7dabf`; HEAD tree `a038f887542f04133371dfe3f38942393b7efdcc`; candidate manifest `4c461bd9be9d655b061949b542391b950f9e47f879a6d467a0e210ecbdff335a`; runtime config `8356c3743291987a571851ef8583e2d083d2d7a1aa81cfce2aaaa28f12dc70ae` | `(steps, funding, opening surfaces, opening predicates) = (8, 10, 16, 32)` | +| P16 | Same physical implementation and benchmark harness; comparative bound tuple only | Candidate manifest `3158a9960f2ebe67f462971c17897b1e77ed259141e362b082001faf42694ae6`; runtime config `ab89eb08e74c3738a0e695e143bf454e15b9f38429225c920425304b8e9bd3f4` | `(16, 20, 32, 64)`; comparative only | +| P32 | Same physical implementation and benchmark harness; accepted production bound tuple only | Candidate manifest `ee8a89b8019b08b2fc394996d34dff429a972964a460a29816e0d8b0ce70b367`; runtime config `ba0abf193b28c5a461fa8cf546c631f87495e09d8c58de37f5b468e279f4c983` | `(32, 40, 64, 128)`; only profile eligible for production acceptance under the current Specification | + +The B8 `template/` manifest digest is computed over the path-sorted `sha256sum` lines for every tracked or untracked non-ignored file returned by `git ls-files -co --exclude-standard -- template`. It therefore identifies the current dirty template worktree without staging or writing Git objects. Candidate manifests must use the same procedure. + +P16 and P32 change the dependent bounds required by the accepted relations, not only the Step count. `MaxOpeningSnapshotEntries = 2 × MaxContractSteps` and `MaxOpeningPredicateResults = 4 × MaxContractSteps`; the comparative funding bounds use the linear `10/20/40` sequence anchored by the current 8-Step value and accepted 32-Step reference value. Any further source difference makes the affected comparison multivariable and MUST be declared rather than attributed to the ceiling. + +## Controls + +- `Controlled variables`: One parent source tree; identical pallet/runtime implementation; identical temporary experiment-only benchmark/test harness; Rust `1.94.1`; Cargo `1.94.1`; `frame-omni-bencher` binary `0.22.0` and benchmark CLI `58.0.0`; compiled Wasm execution; 4,096 heap pages; 50 benchmark steps; 20 repeats; database cache 1,024; deterministic accounts/assets/feeds; identical Task, Trigger, Predicate, funding, run, and topology fixtures; identical maximum block Weight and dispatch ratios; cold build per profile; warm reuse only within the same freshly built profile; identical generated template and normalization. +- `Changed variables`: Only `(MaxContractSteps, MaxFundingTrackedAssets, MaxOpeningSnapshotEntries, MaxOpeningPredicateResults)` as B8 `(8,10,16,32)`, P16 `(16,20,32,64)`, or P32 `(32,40,64,128)`. +- `Confounders`: C6 geometry, Running persistence, one-current-Step execution, loaded-state reuse, and the dual-meter runtime path now exist. Actual current-Step fee settlement and unified atomic control Weight remain open but are held identical across profiles; this experiment may establish ceiling geometry and dispatchability without claiming those independent I3/I4 closures. Isolated candidate trees and final runtime bindings do not yet exist and remain gates before Measuring. +- `Cache assumptions`: Each profile begins with a fresh benchmark-runtime build. Focused extrinsics for that unchanged profile MAY use `--skip-build`; any source, feature, bound, toolchain, or runtime-config change requires a rebuild. No wall-clock host claim is accepted from cache-sensitive runs. + +## Benchmark Class, Workloads, and Criteria + +- `Benchmark classes`: Pallet benchmark for branch/storage/Weight models; production-Wasm benchmark for the candidate runtime; deterministic package/runtime tests for semantic parity and dispatchability; encoded-state profile for retained bytes/hold/fragment counts; exact candidate-tree validation profile after generated Weight and final production Wasm exist. + +| Workload | State/population geometry | Hot/cold assumptions | Purpose | +| --- | --- | --- | --- | +| W-S1 | One-Step Manual User Contract with the same compact unconditional `StopCycle` Step under B8/P16/P32 | Warm benchmark process; only current Step may load | Smallest No Ceiling Tax falsifier | +| W-S2 | Two-Step Contract with identical compact Steps under B8/P16/P32; measure each current cursor separately | Opening once; no unread successor body | Detect suffix or maximum-pipeline charging | +| W-S3 | Three-Step Contract with one maximum Current Predicate Step and otherwise compact Steps; measure each cursor | Only owning current Predicate set loads | Required short-Contract complexity matrix | +| W-C | Active Mutable User creation at the maximum profile Step, Predicate, Opening-surface, funding-tracked, hold, and worst production lifecycle topology geometry | Setup outside measured call | Maximum create dispatchability and state footprint | +| W-L | Maximum-profile Dormant activation, Active deactivation, and open-run cancellation as separate measured branches | Maximum body/run/topology relevant to each branch | Remaining maximum lifecycle dispatchability and atomic state deltas | +| W-U | Existing maximum-profile Active Mutable User with maximum persisted run/funding geometry; replace with a disjoint maximum-profile Contract/topology | Old/new full lifecycle geometry is intentional | Maximum update, cancellation, hold delta, and atomic replacement | +| W-X | Maximum-profile Active Mutable User with maximum run state and worst bounded cleanup topology | Full bounded body/run/topology deletion is intentional | Maximum close dispatchability and orphan-free cleanup | +| W-E0 | W-S1 current-Step execution at a live FIFO head | No cold or later Step read | Short execution ceiling tax and control/effect split | +| W-Emax | Maximum-profile Contract at first, middle, and last cursor; maximum current Predicate/task shape; one committed Step only | Opening geometry measured separately; later Steps unread | Maximum current-Step fit, cursor invariance, and no full-suffix execution | +| W-P32 | One executable 32-Step Cycle across a congestion-free fixture | Exactly one committed Step per eligible block | Semantic parity: 32 distinct committed-Step blocks and no earlier completion | + +- `Metrics`: Generated RefTime and ProofSize; measured and estimated proof where available; database reads/writes; loaded body fragments and payloads; canonical/retained bytes; fragment count; state hold; run footprint; lifecycle declared Weight versus normal dispatch envelope; current-Step control Weight versus Actor Control limit; Task-effect Weight versus Actor base turn; final production Wasm bytes/hash; events, cursor, ticket, eligibility, nonce, outcome, and cleanup postconditions. +- `Omitted metrics`: Native wall-clock throughput, noisy host latency, and full-block population throughput are not decision authorities here. EXP-0001 owns the old native throughput baseline; EXP-0013 owns one-Step Weight/ProofSize decomposition. +- `Statistical method`: FRAME generated models use 50 steps and 20 repeats. Record minimum execution time, coefficients, standard errors, measured/estimated ProofSize, and database terms; do not invent percentile statistics for deterministic model output. Any auxiliary wall-clock observation is diagnostic only and, if retained, requires five warm runs with min/median/max, sample standard deviation, no outlier removal, and declared host contamination. +- `Acceptance criteria`: P32 passes only if every semantic parity test passes; W-C/W-L/W-U/W-X declared Weight fits the normal dispatch envelope component-wise; W-Emax control/effect envelopes fit their accepted component-wise Actor envelopes; W-S1/W-S2/W-S3 add no structural ceiling tax and stay within the predeclared RefTime equivalence bound; maximum retained state is bounded by exact admitted cardinality with no superlinear or orphan topology; generated Weight, runtime bindings, and final production Wasm agree on P32. +- `Rejection criteria`: Reject P32 on any semantic change, same-block successor, rollback/fee/FIFO divergence, lifecycle or current-Step component overflow, stale/unread maximum ProofSize, added short-body read/write/fragment/hold, superlinear persistent state, incomplete cleanup, generated/runtime binding disagreement, or untrustworthy actual Weight. P16/B8 cannot be selected for production without Specification reopening even if numerically better. +- `Pareto rule`: Compare dimensions independently. P32 is Pareto-acceptable only when its required maximum-capability cost does not introduce a short-Contract regression and every binding envelope passes. A RefTime win cannot compensate for a ProofSize, state, lifecycle, or semantics failure. + +## Environment and Artifact Identity + +| Artifact | Identity | +| --- | --- | +| Source commit | `d2e1ab4f171f9dadec5bfb260e14e4b36bb7dabf` | +| Source tree | HEAD tree `a038f887542f04133371dfe3f38942393b7efdcc`; frozen B8 candidate `template/` manifest `4c461bd9be9d655b061949b542391b950f9e47f879a6d467a0e210ecbdff335a` | +| Rust toolchain | `rustc 1.94.1 (e408947bf 2026-03-25)`; `cargo 1.94.1 (29ea6fb6a 2026-03-24)`; `template/rust-toolchain.toml` SHA-256 `65c209bb7f6bb23e50e17220955aa8f818e55973436ea7dc93e1cb7ec5afe37e` | +| Benchmark runtime Wasm | Controlled B8 `252910bf8f6d4dce91b312834e5ab30db10e8ec9b77087336b9917eade1fba89` (5,977,397 bytes); P16 `bee75b22e4cdd7fc7a2c1d1ca6042bff9ef11ddfd78e1337ef21b39fca6492c4` (5,976,212 bytes); P32 `8307fedfdbe1997361bab8b66d0624e5c8a50ba1aca3e02c75321a7e166d9791` (5,975,484 bytes) | +| Post-generation production runtime Wasm | Prepared B8 production Wasm SHA-256 `0687b53b5252d57ab5e1cf983cecb678913a5b1cfb0746565130327476f40744`; exact B8 preparation baseline, not yet candidate comparison evidence | +| Final release production runtime Wasm | B8 baseline `0687b53b5252d57ab5e1cf983cecb678913a5b1cfb0746565130327476f40744`; P16 candidate `efc20334cd204a1a17ba56039fce09504563f76905a096ee072c6570c729b0b2`; P32 candidate `11ad12bc68261795ba45c614401c9c8e0dfb61e920395f360fd30f126e0fdb77` | +| Generated Weight | B8 `a3c8dceebf3d1b85e1410e1e7cccaf766f4666779546a5690c6fdc7e84f64be3`; P16 `e644e77c53481f7bb578d640c04fd71c17dbab93d4739fafc5b1c002479096e1`; P32 `fbc5a7b5b84ad6fc02d25aa1b27fb64a3224071892d463670a46fe37ad828816`; CLI 58.0.0, 50 steps, 20 repeats, compiled Wasm, DB cache 1,024 | +| Database/backend | FRAME benchmark externalities under `frame-omni-bencher`; chain `None` in the existing generated header; cache 1,024 | +| Benchmark command/config | [`scripts/benchmarks.sh`](../../../../../scripts/benchmarks.sh) with 50 steps, 20 repeats, 4,096 heap pages; script SHA-256 `5d4f46c5954196b0f5301193e60953b32e10735aab05aed3440aa1bd0644d4fd` | +| Raw evidence | No EXP-0002 raw evidence exists. Existing normalized B8 descriptors are identified below; candidate logs/artifacts are planned, not generated. | + +The existing benchmark Wasm and Weight file are exact artifacts at their current paths, and the Weight header names that Wasm path. Their byte-for-byte source equivalence to the later current dirty `template/` manifest is not established. They MUST NOT be reused as candidate evidence after any source/profile change. + +### B8 Preparation Diagnostic + +The first maximum-body `create_user_actor` diagnostic failed before measurement with `ContractStepsExceedOnIdleBudget`. This exposed a stale whole-cycle sum in Contract-level admission, despite the production scheduler already admitting only one current-Step resource envelope. Replacing that sum with the component-wise maximum individual Step envelope plus bounded scheduler and terminal cleanup restored maximum B8 admission. This is an implementation correction and preparation result, not candidate measurement. + +The following 10-step/5-repeat focused outputs prove that maximum B8 C6 lifecycle setup executes and touches exactly two tail chunks. They are diagnostic only; the production comparison still requires 50 steps and 20 repeats. + +| Branch | RefTime | ProofSize | Reads | Writes | Output SHA-256 | +| --- | ---: | ---: | ---: | ---: | --- | +| `create_user_actor` | 341,879,000 | 81,886 | 57 | 50 | `bfc209a0f2b0031397d8378a698bf5454795b0b586030dc781dad7d1abf5abeb` | +| `update_contract` | 651,071,000 | 81,886 | 57 | 52 | `56a5d2189046350d0fed22c74aff8b233c70058546747d9459f0cc8ccfbc10c5` | +| `close_actor` | 459,354,000 | 81,886 | 54 | 53 | `87aa8c8b6ed226da4346ae15e8a9618753fe43dc9320577b19948cdd425c33d0` | + +The subsequent production-quality B8 generation established the prepared lifecycle baseline below. These values are exact baseline preparation evidence but remain outside the candidate Measurements until the Prepared gate freezes all profiles and commands. + +| Branch | RefTime | ProofSize | Reads | Writes | +| --- | ---: | ---: | ---: | ---: | +| `create_user_actor` | 330,774,000 | 81,886 | 57 | 50 | +| `activate_actor` | 291,522,000 | 81,886 | 50 | 44 | +| `deactivate_actor` | 406,902,000 | 81,886 | 49 | 46 | +| `update_contract` | 637,871,000 | 81,886 | 57 | 52 | +| `close_actor` | 446,433,000 | 81,886 | 54 | 53 | + +Profile-generic harness preparation passed maximum block-paced semantics for 8, 16, and 32 Steps. Actual short geometry remained exactly `head=120`, `certificate=190`, `tail_chunks=0`, and `tail_bytes=0`; maximum geometry grew linearly as `(certificate, chunks, tail bytes) = (267, 2, 231), (355, 4, 473), (531, 8, 957)`. State-log SHA-256 values are B8 `a3509fc5e96b0f18050d3605a305ce840431e60e64503f7e1914ca42ad8a7ac3`, P16 `9203b84ddd6f7dab12c6e5b5257b45b934ccd6b4473569ba18f68e895faf31ba`, and P32 `813a534f8deda5bb01596cd7c07568a7b57318be1f6bd894ac67a63e2f04e92d`. These runs validate the harness and candidate construction only; controlled benchmark interpretation begins after Prepared. + +### Canonical Commands and Artifact Plan + +The isolated candidates live under `/tmp/deos-exp-0002/candidates/{B8,P16,P32}` and differ only in the declared four-value profile tuple. Preparation runs of the profile-generic semantic and state probes passed under all three profiles; their outputs are harness validation, not Measurements. + +The frozen matrix uses existing production benchmark owners rather than experiment-named production code. `scheduler_actor_state_probe`, `current_step_plan_opening_head`, `current_step_plan_running_tail`, and `scheduler_paged_execute_cheap` own current service and short-path comparison. Complete generation owns maximum create/activate/deactivate/update/close geometry. The semantic profile proves maximum one-Step-per-block progression; the state profile reports actual short/max C6 bytes; the project dispatchability matrix proves runtime-call envelopes. + +Run in each exact candidate checkout, replacing only `EXP_PROFILE`: + +```bash +set -euo pipefail +EXP_PROFILE=B8 +ROOT="/tmp/deos-exp-0002/candidates/${EXP_PROFILE}" +OUT="/tmp/deos-exp-0002/${EXP_PROFILE}" +mkdir -p "${OUT}" +cd "${ROOT}" +./scripts/benchmarks.sh --check +cd template +cargo test --locked -p deos-runtime exp_0002_ceiling_semantics -- --nocapture +cargo test --locked -p deos-runtime every_custom_runtime_call_family_fits_its_dispatch_envelope_at_maximum_input -- --nocapture +cargo test --release --locked -p deos-runtime profile_exp_0002_state_footprint -- --ignored --nocapture 2>&1 | tee "${OUT}/state.log" +cd .. +for sample in 1 2 3 4 5; do + reuse=() + if [ "${sample}" -gt 1 ]; then reuse=(--skip-build); fi + ./scripts/benchmarks.sh "${reuse[@]}" --steps 50 --repeat 20 --extrinsic scheduler_paged_execute_cheap --output "${OUT}/execute-short-${sample}.rs" pallet_deos_actors + done +for branch in scheduler_actor_state_probe current_step_plan_opening_head current_step_plan_running_tail; do + ./scripts/benchmarks.sh --skip-build --steps 50 --repeat 20 --extrinsic "${branch}" --output "${OUT}/${branch}.rs" pallet_deos_actors +done +sha256sum template/target/benchmarks/release/wbuild/deos-runtime/deos_runtime.compact.compressed.wasm "${OUT}/"*.rs "${OUT}/state.log" +``` + +After focused evidence is complete for one unchanged candidate, generate its complete candidate Weight file, rebuild production Wasm, and capture post-generation identities: + +```bash +./scripts/benchmarks.sh --steps 50 --repeat 20 pallet_deos_actors +./scripts/03-build-runtime.sh +sha256sum template/runtime/src/weights/pallet_deos_actors.rs template/target/release/wbuild/deos-runtime/deos_runtime.compact.compressed.wasm +cargo fmt --all -- --check +cargo test --locked -p pallet-deos-actors +cargo test --locked -p deos-runtime every_custom_runtime_call_family_fits_its_dispatch_envelope_at_maximum_input -- --nocapture +cargo clippy --workspace --all-targets -- -D warnings +cd .. +./.agents/skills/alignment/scripts/completion-gate.sh +``` + +- `Artifact plan`: Append normalized candidate measurements, model errors, footprint values, semantic postconditions, hashes, and command outcomes directly to this record. Keep `/tmp/deos-exp-0002//` raw logs and focused generated files through interpretation; record every digest here. Add an `EXP-0002-*` sibling only if raw size, machine consumption, or fidelity makes inline evidence materially worse, and state that justification before creation. +- `Production handoff`: Candidate full Weight files remain isolated until Interpretation accepts P32. Only then may the accepted P32 generated file and final production Wasm enter project truth under the ordinary implementation/test/correction sequence. + +## Measurements + +| Candidate | Workload | RefTime | ProofSize | Reads | Writes | Other decision metrics | +| --- | --- | ---: | ---: | ---: | ---: | --- | +| B8 | W-S1 short FIFO execution | Median `289,384,987` at `n=1`; samples `288,375,274 / 448,398,084 / 289,384,987 / 292,778,667 / 287,725,580` | `7,065` | `12` | `7` | Measured storage model; benchmark Wasm `252910bf`, 5,977,397 bytes | +| P16 | W-S1 short FIFO execution | Median `290,507,676` at `n=1`; samples `293,963,083 / 290,507,676 / 294,909,713 / 278,232,304 / 276,524,330` | `7,065` | `12` | `7` | `+0.39%` median; structurally identical model; benchmark Wasm `bee75b22`, 5,976,212 bytes | +| P32 | W-S1 short FIFO execution | Median `284,797,443` at `n=1`; samples `289,043,615 / 283,219,058 / 287,486,305 / 284,797,443 / 280,974,701` | `7,065` | `12` | `7` | `-1.59%` median; structurally identical model; benchmark Wasm `8307fedf`, 5,975,484 bytes | +| B8 | W-C/W-L/W-U/W-X | Not measured | Not measured | Not measured | Not measured | State/hold/dispatch fit not measured | +| P16 | W-C/W-L/W-U/W-X | Create `363,809,000`; activate `359,549,000`; deactivate `441,544,000`; update `709,809,000`; close `501,539,000` | All `81,886` | `59 / 52 / 51 / 59 / 56` | `52 / 46 / 48 / 54 / 55` | Runtime dispatchability matrix passed; Weight `e644e77c`; post-generation manifest `a37821e2`; production Wasm `efc20334` | +| P32 | W-C/W-L/W-U/W-X | Create `416,960,000`; activate `498,605,000`; deactivate `540,790,000`; update `853,824,000`; close `584,582,000` | All `81,886` | `63 / 56 / 55 / 63 / 60` | `56 / 50 / 52 / 58 / 59` | Runtime dispatchability matrix passed; Weight `fbc5a7b5`; post-generation manifest `d2726232`; production Wasm `11ad12bc` | +| B8 | W-E0/W-Emax | Opening head `43,302,000`; maximum tail `57,361,625` | Opening `5,211`; maximum tail `6,257` | Retained in focused files | Retained in focused files | Maximum state probe `48,960,000 / 5,670`; W-P32 not representable | +| P16 | W-E0/W-Emax | Opening head `42,813,000`; maximum tail `58,364,553` | Opening `5,211`; maximum tail `6,884` | Retained in focused files | Retained in focused files | Maximum state probe `50,077,000 / 5,670`; W-P32 not representable | +| P32 | W-E0/W-Emax/W-P32 | Opening head `42,744,000`; maximum tail `63,852,677`; corrected atomic maximum Opening `1,559,647,000` | Opening `5,211`; maximum tail `8,137`; atomic maximum Opening `179,078` | Atomic branch `148` | Atomic branch `9` | Maximum state probe `55,874,000 / 6,162`; 32-block semantic profile passed; conservative control fit passed | + +## Derived Metrics + +| Candidate | Workload | RefTime delta | ProofSize delta | Throughput / latency | Marginal or amortized cost | Pareto class | +| --- | --- | ---: | ---: | --- | --- | --- | +| P16 | W-S1 short FIFO execution | `+0.39%` median | Exact match | Not in scope | No added read, write, measured proof, or actual short state | Pareto-equivalent short path; comparative only | +| P32 | Full measured matrix | W-S1 `-1.59%`; maximum costs grow with exact geometry | W-S1 FIFO exact, but fixed observation fanout unit `300,510 → 331,998` | Fixed 20% fanout envelope falls from 3 to 2 pages per block | Adds 24-Step capability but taxes short observation subscribers through maximum encoded run/funding/admission bounds | Invalidated physical candidate | + +## Statistical / Variance Notes + +The first B8 benchmark-runtime build stopped before measurement with `Disk quota exceeded` because generated debug/release targets from all three candidates coexisted on the 32 GiB `/tmp` filesystem. Only generated candidate targets were deleted; source manifests and preparation logs were unchanged. The successful retry began with 24 GiB free, and later profiles ran sequentially. The runtime later cleared `/tmp` between P32 and P16 lifecycle generations; focused raw files became ephemeral after their hashes and normalized values were recorded here. P16 was reconstructed byte-for-byte at manifest `3158a996…` before full generation. Exact commands, hashes, normalized samples/models, state values, and artifact identities remain sufficient to reproduce the decision, but this record does not claim retained raw-file availability. B8 and P32 each retain all five frozen 50/20 W-S1 samples with no outlier removal. B8 sample 2 has an anomalous `305,413,392` intercept despite a `142,618,000` minimum and ordinary `142,984,692` slope; the predeclared median keeps it visible without allowing it to dominate. P16 is `0.39%` above B8 and P32 is `1.59%` below B8, both inside the ±5% equivalence threshold. Structural short-path proof/read/write identity is exact across all profiles. Production-quality lifecycle generation, maximum semantic profiles, runtime dispatchability, generated Weight identity, and candidate production Wasm now exist for all profiles. No interpretation is accepted in Measured status. + +## Result + +B8/P16/P32 short FIFO execution has identical ProofSize/read/write models and invariant actual short C6 bytes; P16 and P32 median RefTime deltas are `+0.39%` and `-1.59%`. Maximum retained-state and lifecycle costs grow with exact bounded geometry, and the corrected P32 atomic maximum-Opening control branch fits at `Weight(1,559,647,000, 179,078)`. Final handoff exposed an omitted short-path surface: the generated dense observation-fanout page increased from B8 `Weight(21,152,536,000, 300,510)` to P32 `Weight(21,261,200,000, 331,998)`, reducing service under the fixed 20% block envelope from three pages to two. Production was reverted to regenerated B8 authority. + +## Interpretation + +The initial matrix made P32 appear capability-dominant because actual short C6 bytes and short FIFO ProofSize/reads/writes were invariant, both candidate RefTime medians satisfied the ±5% equivalence bound, and maximum geometry grew linearly. The first production preflight exposed overlapping maximum-control ProofSize; an atomic maximum-Opening owner corrected that defect, and exact P32 regeneration then fit the conservative control envelope at `Weight(1,559,647,000, 179,078)`. Final generated Weight nevertheless falsified the broader No Ceiling Tax claim: dense observation fanout over identical one-Step subscribers inherited larger maximum encoded admission/funding/run bounds, increasing unit ProofSize `300,510 → 331,998` and reducing service under the unchanged 20% block envelope from three pages to two. The FIFO benchmark had omitted this reactive activation branch. P32 is therefore not a conforming physical candidate even though its maximum lifecycle and control branches fit. P16 remains comparative only; selecting it cannot satisfy the normative 32-Step ceiling. A successor topology must separate compact activation authority from extended execution payloads before fresh measurement. + +## Decision + +Invalidated. The corrected P32 control branch, 32-block semantics, and lifecycle dispatchability pass, but P32 fails the predeclared No Ceiling Tax criterion outside the originally measured short FIFO path. Maximum encoded admission, funding, and run bounds raise the fixed Weight of an otherwise identical one-Step observation subscriber page and reduce bounded fanout service from three pages to two under unchanged runtime policy. This is a physical storage/service-authority defect, not evidence against the normative 32-Step ceiling. Production remains on regenerated B8; a successor must compact or tier activation authority before P32 is remeasured. + +## Rejected Alternatives and Tradeoffs + +| Candidate or shortcut | Decision | Evidence-backed reason | +| --- | --- | --- | +| Treat configured Step count as dispatchability evidence | Rejected before measurement | A count proves neither RefTime nor ProofSize fit, lifecycle cleanup, retained state, or production-Wasm behavior. | +| Select P16/P8 without Specification reopening | Rejected before measurement | The accepted semantic reference fixes 32; comparative profiles diagnose a blocker only. | +| Use the current monolithic `ActorContract` maximum proof as No Ceiling Tax proof | Rejected before measurement | It charges an 8,735-byte maximum in current descriptors and cannot establish lazy current-Step geometry. | +| Reuse one benchmark Wasm across profile/source changes | Rejected before measurement | The Skill and harness require a fresh build after source, bound, feature, runtime-config, or toolchain changes. | +| Collapse RefTime, ProofSize, state, and lifecycle into one score | Rejected before measurement | The governing resource policy requires component-wise independent authority. | + +## Validity + +- `Establishes`: The original FIFO-only short-path matrix was insufficient; final production acceptance must include reactive activation/fanout Weight whose fixed proof can inherit maximum encoded state bounds. It also establishes exact corrected P32 atomic control fit. +- `Does not establish`: A conforming P32 storage topology, final production P32 authority, executable 20/80 accounting, actual current-Step fee settlement, full-block throughput, or release-tree equivalence. +- `Known limitations`: Focused raw files were ephemeral after their hashes and normalized values were retained. Actual current-Step fee settlement and unified atomic control Weight remain independent open work and were held constant rather than claimed complete here. +- `Invalidation triggers`: Any change to Contract/body/run storage, Step representation, lifecycle cleanup, current-Step executor, Task adapters/effect Weight, `MaxBlockWeight`, dispatch ratios, Actor resource policy, profile tuple, benchmark harness/method, database/cache mode, generated Weight template, Rust/SDK/bencher toolchain, Wasm builder, or production Weight binding requires baseline review and fresh affected candidates. +- `Residual qualitative insight`: Separate short-Contract invariance from maximum-capability cost; a higher protocol ceiling is acceptable only when unused capacity is physically absent from the short Actor's proof, state, and execution path. + +## Follow-Up / Next Gradient + +```text +production ceiling 32 → FIFO short path and lifecycle pass +→ atomic maximum-Opening P32 proof 179,078 and control/effect fit +→ observation fanout fixed proof 300,510 → 331,998 +→ fixed-envelope service 3 → 2 pages +→ compact/tier activation authority before a fresh P32 candidate +``` + +- `Smallest falsifier`: Exact P32 generated maximum control still exceeds the configured Actor Control limit component-wise after overlap correction and explicit fixed-envelope binding. +- `Eliminated hypotheses`: A configured count ceiling alone proves throughput or dispatchability; P16 is an automatic conforming fallback; RefTime can compensate for ProofSize/state failure; C6 imposes short-Contract ceiling tax; maximum lifecycle dispatchability binds before control. +- `Remaining hypotheses`: Splitting compact activation authority from extended admission resources, funding, and run payloads can keep short subscriber activation proof invariant while preserving fail-closed exact tickets and maximum P32 execution. +- `Stop condition`: Stop immediately on semantic divergence or untrustworthy Weight and route the defect to its owner. A physical P32 failure rejects that candidate and routes geometry correction to the owning experiment; reopen Specification only after evidence demonstrates semantic defect or infeasibility beyond rejected physical candidates. Accept P32 only after every predeclared criterion passes. +- `Blockers to Prepared`: Not applicable; this candidate is Invalidated. +- `Measurement ordering`: Prepare a successor physical geometry, prove compact activation authority and unchanged FIFO semantics under B8 first, then rerun the complete B8/P32 matrix including observation fanout before any production handoff. + +## Architecture Impact + +- `Affected physical truth`: Runtime Step ceiling, dependent snapshot/funding bounds, lifecycle maximum geometry, current-Step proof, state hold, and generated Weight. +- `Architecture update`: None from this invalidated candidate. Update package/integration architecture only after implementation, tests, correction, an Accepted successor P32 Weight, and final production Wasm converge. +- `Provenance citation`: Cite EXP-0002 only as invalidation evidence for the rejected C6 P32 physical candidate, never as P32 production authority. + +## Relations + +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: The Step-ceiling and maximum-body boundary left outside [EXP-0001](./EXP-0001.md). +- `Contradicts`: Any claim that current `MaxContractSteps = 8`, an existing benchmark Wasm, or a count ceiling proves the accepted P32 contract. +- `Depends on`: Accepted 0.7.24 Actors specification, resource policy, performance assurance, I1 Max Steps, selected I2 body geometry for final ceiling comparison, the owning I3 current-Step path for execution acceptance, and the exact B8 source/artifact baseline above. +- `Baseline of`: [EXP-0010](./EXP-0010.md) compact observation activation authority. +- `Confirms`: C6 FIFO short-path ceiling invariance, linear maximum state growth, lifecycle dispatchability through P32, and corrected maximum-Opening control fit. +- `Invalidates`: The C6 P32 production candidate because fixed broad-fanout ProofSize inherits unused maximum execution payload. +- `Coordinates with EXP-0013`: EXP-0013 owns decomposition of the current one-Step Actor ProofSize/Weight and is the direct successor of EXP-0001. EXP-0002 consumes that decomposition when distinguishing a global-ceiling tax from unrelated one-Step control cost; neither experiment supersedes the other. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0003.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0003.md new file mode 100644 index 00000000..e1f0992f --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0003.md @@ -0,0 +1,168 @@ +# EXP-0003 — Actor Contract Body Geometry + +| Field | Value | +| --- | --- | +| Status | Rejected | +| Architecture release / experiment campaign | DEOS 0.7.24 baseline / 0.7.24 topology-scaling campaign | +| Date | 2026-08-25 | +| Affected domain | DEOS Actors Contract storage, lifecycle, and current-Step loading | +| Physical mechanism | Hot header plus bounded ordered Step-body representation | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | None | +| Successor experiment | EXP-0016 | + +## Decision Question + +Which conforming Contract body geometry—one Step cell, two-Step chunk, four-Step chunk, or compact Step header plus external payload—best minimizes current-Step ProofSize and lifecycle Weight without making short Contracts pay for unused ceiling capacity? + +## Governing Specification + +- `Exact section`: `template/pallets/actors/docs/specification.en.md` → `Hot/Cold Physical Contract`, `Complete Projection`, and `Active Execution State Geometry`. +- `Preserved invariants`: One semantic ordered Contract; exact bounded reconstruction; activation without cold body reads; execution loads only the current Step representation; stale/missing/overlapping fragments fail closed; atomic create/update/close; no mutable semantic mirror. +- `Reopen trigger`: Specification reopens only for demonstrated semantic defect or infeasibility beyond the relevant physical candidates. A losing candidate is rejected without changing semantics. + +## Context and Hypothesis + +- `Current architecture`: One `ActorContracts[actor_id]` value stores authored header fields and the complete bounded Step vector. Activation, scheduling, execution, lifecycle, and projection share that monolithic value. +- `Reason`: The 32-Step target and no-ceiling-tax contract require evidence that current-Step reads and short-Contract state do not inherit maximum body geometry. +- `Hypothesis`: One bounded fragmented representation can materially reduce current-Step ProofSize while retaining dispatchable maximum lifecycle operations and exact projection. +- `Materiality`: ProofSize, reads/writes, encoded state bytes, loaded-fragment count, and correctness postconditions are exact: any delta is material. A generated RefTime delta is material when its absolute relative delta exceeds `max(2%, 3 × pooled relative model standard error)` for the matched branch. A runtime-Wasm byte delta above 1% is material. Selection seeks the highest measured current-Step and lifecycle performance among candidates that preserve every hard invariant and production envelope; no arbitrary improvement quota may reject a real Pareto improvement. + +## Baseline and Candidates + +| ID | Architecture | Source tree | Runtime/config identity | +| --- | --- | --- | --- | +| B0 | Monolithic `ActorContract` with inline bounded Step vector | Isolated tree pending; preparatory dirty template manifest `e72934987931886d6fb60531e1fb0a21acfaed2636986309d623661a146511d1` at HEAD `d2e1ab4f` | 8-Step runtime baseline; generated Weight `31df0c32`; candidate-bound Wasm pending | +| C1 | One actor/semantic/body/index-bound Step per storage cell plus hot header | Shared controlled candidate tree manifest `45c436268d0cff5fa9133943c0fe0f04d50b9130511659045206f1e6b14b3f50` | Benchmark-only stores/harness in matching runtime-benchmark Wasm; not production binding | +| C2 | Two-Step contiguous chunks | Not constructed | Pending | +| C3 | Four-Step contiguous chunks | Not constructed | Pending | +| C4 | Compact Step header plus external payload | Not constructed; Step-shape question remains owned by EXP-0004 | Pending | +| C5 | Hot header with uniquely owned inline Step 0 plus lazy tail fragments | Earned successor candidate; not constructed in EXP-0003 | Must preserve exact projection and later-cursor loading | + +## Controls + +- `Controlled variables`: Semantic Contract bytes, Actor class/mutability, Trigger, funding/completion policy, Step workloads, 8-Step production baseline, database/cache mode, toolchain, runtime configuration, benchmark method, and correctness postconditions. +- `Changed variables`: Physical body fragment/chunk/payload geometry and its bounded locators only. +- `Confounders`: Step representation changes belong to EXP-0004 and must not be mixed into this comparison unless explicitly factored as a shared candidate dimension. + +## Benchmark Class, Workloads, and Criteria + +- `Benchmark class`: FRAME lifecycle/current-Step benchmarks, retained-state encoding, matching production Wasm, and exact reconstruction/fault tests. +- `Workloads`: Short 1-Step, representative compact/mixed Contracts, maximum 8-Step baseline, and comparative 16/32-Step geometry where candidate construction permits. +- `Metrics`: Component-wise RefTime and ProofSize; database reads/writes; encoded persistent bytes; current-Step proof/read set; create/update/close/activate/projection Weight; Wasm size; exact semantic equivalence. +- `Statistical method`: Identical 50-step/20-repeat generated benchmarks across isolated candidate trees; retain model coefficient standard errors and remove no samples. Treat RefTime as equivalent only within `max(2%, 3 × pooled relative model standard error)`. Exact dimensions have no noise band. +- `Acceptance criteria`: Exact semantic and reconstruction parity; no cold unreached-body proof in matched one-Step execution; every maximum lifecycle call fits its production dispatch envelope component-wise; no unbounded scan; and selection from the conforming nondominated set by the release bottleneck. If no dimension is binding, prefer lower current-Step ProofSize, then lifecycle RefTime, persistent bytes, Wasm size, and implementation simplicity. A tradeoff is accepted only when its sacrificed dimension stays inside a predeclared bound and the gain improves the declared release target. +- `Rejection criteria`: Any semantic divergence, unbounded scan, stale fragment authority, non-dispatchable maximum lifecycle path, or undeclared component-wise regression. + +## Environment and Artifact Identity + +| Artifact | Identity | +| --- | --- | +| Preparatory repository | HEAD `d2e1ab4f171f9dadec5bfb260e14e4b36bb7dabf`; committed tree `a038f887542f04133371dfe3f38942393b7efdcc`; dirty template manifest `e72934987931886d6fb60531e1fb0a21acfaed2636986309d623661a146511d1` | +| Manifest method | Path-sorted tracked/untracked template content hash; preparation identity only, not an isolated candidate tree | +| Toolchain | `rustc 1.98.0 (88d9e12ae 2026-08-18)`; `cargo 1.98.0 (797e8a9bc 2026-08-05)`; `Cargo.lock` SHA-256 `0c2eb29358ad007d45b1ddeb09c4e0f279c9630901c2838dc6c83c74126a1bed` | +| Benchmark harness | `scripts/benchmarks.sh` SHA-256 `f932b293093c751bd3ab6f9870b0ac45014eef2193133bd909f54869f9383818`; 50 steps, 20 repeats required | +| Existing generated Weight | `template/runtime/src/weights/pallet_deos_actors.rs` SHA-256 `31df0c32b1ee726e1f9531b679c2a8462bed8c038aa9558b3747334d2ee14e94`; preparatory only, not EXP-0003 measurement | +| Controlled B0/C1 source | Shared dirty template manifest `45c436268d0cff5fa9133943c0fe0f04d50b9130511659045206f1e6b14b3f50` at HEAD `d2e1ab4f`; B0 and C1 coexist only under `runtime-benchmarks`, so source/toolchain/runtime controls are identical | +| Harness | Frozen pairs: `create`, `update`, `close`, `load_current`, `reconstruct`; ignored `profile_exp_0003_state_footprint`; all use 1..=8 inert ordered StopCycle Steps and exact candidate postconditions | +| Rehearsal evidence | Stale pre-Prepared C1 authority and partial harness only; Wasm `247cab80`, outputs/logs retained above solely as invalid preparation evidence | +| Matching benchmark Wasm | SHA-256 `23c83b9f4fb00631fcdbd89f83eb381e8b5d6a8e95506f4c11e3335e06d5c016` built once from manifest `45c43626` | + +The candidate manifest and canonical measurement commands are frozen as: + +```bash +git ls-files -co --exclude-standard -z template | sort -z | xargs -0 sha256sum | sha256sum +out=/tmp/deos-exp-0003-prepared +rm -rf "$out" && mkdir -p "$out" +skip=() +for branch in create update close load_current reconstruct; do + DEOS_VERBOSE=1 ./scripts/benchmarks.sh "${skip[@]}" --steps 50 --repeat 20 \ + --extrinsic "exp_0003_b0_${branch}" --output "$out/b0-${branch}.rs" pallet_deos_actors \ + 2>&1 | tee "$out/b0-${branch}.log" + skip=(--skip-build) + DEOS_VERBOSE=1 ./scripts/benchmarks.sh --skip-build --steps 50 --repeat 20 \ + --extrinsic "exp_0003_c1_${branch}" --output "$out/c1-${branch}.rs" pallet_deos_actors \ + 2>&1 | tee "$out/c1-${branch}.log" +done +cd template +/home/llb/.rustup/toolchains/1.94.1-x86_64-unknown-linux-gnu/bin/cargo test -p pallet-deos-actors profile_exp_0003_state_footprint --locked -- --ignored --nocapture | tee "$out/state-footprint.log" +cd .. +sha256sum "$out"/* template/target/benchmarks/release/wbuild/deos-runtime/deos_runtime.compact.compressed.wasm +``` + +The first B0 command builds the matching benchmark Wasm; every later command uses `--skip-build`. Any source/manifest drift invalidates the matrix before Measuring. + +## Measurements + +The complete Prepared matrix ran once from manifest `45c43626` and matching benchmark Wasm `23c83b9f`. Models exclude database Weight additions shown separately. + +| Branch | B0 RefTime model | C1 RefTime model | B0 / C1 estimated ProofSize | B0 / C1 reads | B0 / C1 writes | +| --- | ---: | ---: | ---: | ---: | ---: | +| Create | `3,376,126 + 19,794n` | `3,877,731 + 1,562,395n` | 0 / 0 | 0 / 0 | 1 / `1+n` | +| Update | `3,515,967 + 23,874n` | `3,971,947 + 1,560,379n` | 0 / 0 | 0 / 0 | 1 / `1+n` | +| Close | `10,995,751 + 63,251n` | `10,425,184 + 1,383,580n` | `4,011+3n` / 4,076 | 1 / 1 | 1 / `1+n` | +| Current load | `10,654,793 + 62,456n` | `13,905,264 + 387,281n` | `4,011+3n` / `4,158+30n` | 1 / 2 | 0 / 0 | +| Reconstruction | `12,859,862 + 143,786n` | `13,194,933 + 4,068,717n` | `4,011+3n` / `4,086+2,574n` | 1 / `1+n` | 0 / 0 | + +| Steps | B0 encoded bytes | C1 encoded bytes | C1 header | C1 maximum cell | +| ---: | ---: | ---: | ---: | ---: | +| 1 | 13 | 188 | 109 | 79 | +| 8 | 34 | 741 | 109 | 79 | + +All ten normalized outputs, raw logs, state profile, matching Wasm, and digest manifest are retained under `/tmp/deos-exp-0003-prepared/`. + +## Derived Metrics + +At `n=8`, before database Weight additions, C1 changes RefTime versus B0 by: create `+363.35%`, update `+343.89%`, close `+86.87%`, current load `+52.44%`, and reconstruction `+226.51%`. C1 persistent bytes increase by `+1,346.15%` at one Step and `+2,079.41%` at eight Steps. C1 current load also adds one database read and raises estimated proof from 4,035 to 4,398 bytes at eight Steps. + +## Statistical / Variance Notes + +The valid matrix used 50 steps and 20 repeats with no sample removal and one matching Wasm. Generated slope standard errors are retained in normalized outputs; all are far below the 2% materiality floor. The earlier pre-Prepared rehearsal remains excluded. + +## Result + +C1 is component-wise worse than B0 at the current 1..=8 profile across every measured decision branch and retained-state footprint. No candidate decision is made at Measured status; causal interpretation and candidate pruning follow separately. + +## Interpretation + +- `C1 current profile`: C1 is strictly dominated by B0 at 1..=8 Steps. It adds a header read, one write per Step for lifecycle mutation, one read per Step for reconstruction, and 14.46×/21.79× retained bytes at one/eight Steps. +- `No Ceiling Tax`: B0 remains semantically unsuitable for the 32-Step target because its one monolithic maximum proof grows with the global body ceiling. C1 proves that naive fragmentation is not an acceptable cure; repeating body authority per Step converts ceiling tax into read, write, reconstruction, and persistent-state tax. +- `C2/C3`: Pure tail chunks can amortize lifecycle writes and authority bytes, but they retain the second current-path read and increase the loaded tail fragment above C1. Because C1 already loses current load and B0 lifecycle is constant, C2/C3 are not earned as the next candidate before a design removes the one-Step extra read. +- `C4`: External payload separation is a Step-shape question. It cannot repair C1's duplicated body authority or header-plus-cell read by itself and remains deferred to EXP-0004. +- `C5 gradient`: Inline exactly Step 0 in the hot header as its unique body owner, with lazy authority-bound tail fragments only when `step_count > 1`. A one-Step Actor then needs one storage read and no tail record, while later cursors retain lazy loading. This directly targets the 100 one-Step throughput objective and no-ceiling-tax invariant. + +## Decision + +Reject C1 as a production geometry for the current profile. Do not build C2/C3 merely to soften a design whose one-Step path is already dominated. Preserve B0 temporarily, remove C1 instrumentation after its successor is prepared, and route one earned C5 A/B candidate to a successor Experiment Record before implementation selection. + +## Rejected Alternatives and Tradeoffs + +| Candidate or shortcut | Decision | Evidence-backed reason | +| --- | --- | --- | +| C1 one-cell body | Rejected for production | Loses all measured branches and state footprint at 1..=8 Steps. | +| Build C2/C3 immediately | Rejected as next move | Fewer tail records cannot remove the extra header+fragment read for Step 0. | +| Keep B0 as 32-Step production geometry | Rejected as final design | Monolithic maximum proof violates the accepted no-ceiling-tax physical contract. | +| Move payloads first | Deferred to EXP-0004 | Payload shape does not resolve duplicated body authority or the extra Step-0 read. | + +## Validity + +- `Establishes`: C1's component-wise regression at 1..=8 Steps, rejection of naive per-Step fragmentation, and the earned inline-Step-0 successor gradient. +- `Does not establish`: C5 performance, tail fragment geometry, 16/32-Step dispatchability, Step-payload shape, or production acceptance. +- `Invalidation triggers`: Contract semantics, candidate family, workload geometry, benchmark method, runtime limits, Weight owners, toolchain, or Wasm builder changes. + +## Follow-Up / Next Gradient + +- Open one successor record for C5: hot header with uniquely owned inline Step 0 and lazy tail fragments. +- Freeze B0/C5 one-Step, later-cursor, lifecycle, reconstruction, and footprint controls before measuring; add a chunked tail only if measured tail lifecycle pressure earns it. +- Remove every C1 `Exp0003*` store/function and temporary profile after the successor harness can preserve the B0 baseline independently. Project build, tests, CI, release validation, and runtime behavior must not depend on this Skill or its private experiment lifecycle. + +## Architecture Impact + +None from rejected C1. `Exp0003*` stores and benchmark functions are temporary `runtime-benchmarks`-only candidate instrumentation, not project architecture or acceptance dependencies. Delete them after selection; retain only the accepted generic production implementation and ordinary project-owned regression/benchmark evidence. Architecture documents remain unchanged until implementation/tests converge. + +## Relations + +- `Gates`: I2 Physical Geometry Prototype and Selected Geometry. +- `Related`: EXP-0002 consumes the selected body geometry for 8/16/32 ceiling comparison. +- `Successor`: EXP-0016 owns the inline-Step-0 plus lazy-tail geometry. EXP-0004 remains conditional Step-representation work after body ownership converges. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0005.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0005.md new file mode 100644 index 00000000..068d172b --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0005.md @@ -0,0 +1,215 @@ +# EXP-0005 — Current Execution Authority Geometry + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Architecture release / experiment campaign | DEOS 0.7.24 optimization campaign | +| Date | 2026-08-30 | +| Affected domain | Actor hot state, current Step, run state, and FIFO service | +| Physical mechanism | Current execution authority representation | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | EXP-0013 | +| Successor experiment | None | + +## Decision Question + +Which single-owner representation minimizes per-block current-Step control proof and mutation cost without duplicating Step semantics or weakening lifecycle authority? + +## Governing Specification + +- `Exact section`: `template/pallets/actors/docs/specification.en.md` → `§3.3`, `§3.5`, `§5.1`, `§6.1`, `§8.2-§8.3`, `§9.3`, and `§13.1`. +- `Invariant`: Preserve strict FIFO, Q1, N+1 eligibility, atomic Step effects, durable prefixes, transactional replacement/cancel/close, and one canonical semantic Step owner. +- `Reopen trigger`: Any candidate requiring stale execution caches, duplicate Step truth, relaxed update invalidation, or a changed queue order reopens Specification and is not an optimization candidate. + +## Context and Hypothesis + +- `Current architecture`: ActorHot, Contract head/tail, split Run head/payload, admission/activation authority, and FIFO ticket are separately proven during service. +- `Reason`: Equal-thirds 10,000-Actor profiles remain Actor-machine constrained at roughly 14.7 committed distinct Actors per saturated block before authority experiments. +- `Hypothesis`: Co-locating frequently co-read authority or transferring current-Step ownership into a commitment-bound RunFrame can materially reduce RefTime/ProofSize per Q1 commit. +- `Materiality`: At least 20% lower complete control Weight or at least 20% more distinct Actors per saturated block, with no larger binding state-hold/lifecycle regression. + +## Baseline and Candidates + +| ID | Architecture | Source tree | Runtime/config identity | +| --- | --- | --- | --- | +| A0 | Separate Hot, Contract, admission certificate, Run head/payload, and queue authority | HEAD `ab62e396`; prepared worktree digest `3c064304` | Q1, C6, P128/P64, MaxSteps32; allocation now bounded by EXP-0022 equal thirds | +| A1 | Merge ActorHot and optional RunHead; keep RunPayload separate and Contract Step canonical | Focused candidate measured; rejected before integration | Complete-path feasibility bound cannot approach 20% materiality | +| A2 | Fold AdmissionCertificate into ContractHead as one Contract-owned authority value | Integrated, measured, rejected, and reverted | Only +3.2% production throughput versus 20% threshold | +| A3 | Sole-owner Current RunFrame materialized at Opening and replaced per successor | Rejected structurally before construction | Duplicates the reusable authored Step or requires destructive Contract movement/restoration | + +## Controls + +- `Controlled variables`: Trigger/task workloads, FIFO/Q1, runtime ratio, Contract geometry, toolchain, generated method, population, database, and cache assumptions. +- `Changed variables`: Physical key/value placement and current execution authority ownership only. +- `Confounders`: Candidate layouts and exact benchmark owner decomposition require preparation before measurement. + +## Benchmark Class, Workloads, and Criteria + +- `Benchmark class`: Focused pallet benchmark, production-Wasm benchmark, state-footprint probe, runtime integration, and 10k native stress. + +| Workload | State/population geometry | Hot/cold assumptions | Purpose | +| --- | --- | --- | --- | +| W1 | One-Step Idle→Opening→commit | Cold Actor keys | Dominant cheap-cycle path | +| W2 | 32-Step Running successor and completion | Current frame hot only within operation | Q1 persistence cost | +| W3 | Temporary retry and delayed FIFO head | Run authority persisted across blocks | Repeated-current-Step benefit | +| W4 | Replacement/cancel/close at every run phase | Maximum bounded Contract/run state | Invalidation and rollback cost | +| W5 | 10,000 Transfer Actors | Setup outside measured blocks | End-to-end throughput | + +- `Metrics`: `CommittedStepsPerBlock`; `DistinctActorsProgressedPerBlock`; component-wise `ActorMachineCostPerCommittedStep`; component-wise `UsefulActionWeight / ActorMachineWeight`; reads/writes, encoded bytes, state hold, current-Step keys touched, replacement/cancel/close Weight, first traversal, and service-gap distribution. +- `Statistical method`: Generated Weight at 50 steps/20 repeats for finalists; focused probes may reject candidates earlier; deterministic semantic/state-root tests precede performance interpretation. +- `Acceptance criteria`: Semantic equivalence and a material Pareto improvement in at least one primary metric without unacceptable regression in another, lifecycle/state hold, strict FIFO, Q1, or service-gap fairness. `100 Steps/block` is a stress direction, not the objective function or a promise. +- `Rejection criteria`: Duplicate semantic ownership, stale execution, increased binding ProofSize, unbounded invalidation, rollback drift, or sub-material gain. + +## Environment and Artifact Identity + +| Artifact | Identity | +| --- | --- | +| Source commit | `ab62e39612aee895ad41bb45c4bf085318b6cf8a` | +| Source tree | Dirty-worktree diff digest `e739c2d32a546a76269d481b95faf8b803b0ee131e6523fca0338f71e76bc8ba` | +| Rust toolchain | `rustc 1.98.0 (88d9e12ae 2026-08-18)` | +| Benchmark runtime Wasm | A1 combined `32bb96f7`; A1 isolated `38d3dfaa`; A2 focused `d259331f2d6a78c47e195b780ff3caf93e2d2b67effe7451d237d7fb847f07e2` | +| Post-generation production runtime Wasm | Retained A0 plus compact Pipeline fee event `2617e8186cc304ce9ee4a07b263e324a226f615068cdf6b068267cf3a16fbc69` | +| Final release production runtime Wasm | Pending | +| Generated Weight | Retained A0 regenerated at 50/20 after compact Pipeline fee event; SHA-256 `5199425030b4d2c0523a2a21134a8994ab29d5f70ff1303b42cbf2ff82598db3` | +| Database/backend | FRAME compiled Wasm, production benchmark backend, and native release tests | +| Benchmark command/config | `frame-omni-bencher v1 benchmark pallet` over candidate Wasm, focused A0/A1 owners, 50 steps/20 repeats; final generation remains project script only | +| Raw evidence | Inline authority, focused-probe, and integrated tables under Measurements; temporary TSV digests `d391ced1`, `6d377bc1`, `b7aae82a`, and `8d34a2cf`; integrated generated Weight `c77c06e6` | + +## Measurements + +| Candidate | Workload | RefTime | ProofSize | Reads | Writes | Other decision metrics | +| --- | --- | ---: | ---: | ---: | ---: | --- | +| A0 | W1 probe | `245,890,000` with runtime DB | `5,998` | 7 | 0 | Reads identity/hot/Contract/funding/run head/run payload/certificate | +| A0 | W1 cheap complete marginal | `972,536,265` with runtime DB | `3,361` | 11 | 5 | Generated `scheduler_paged_execute_cheap_mixed` marginal; owners are non-additive | +| A0 | W2 run progress | `580,528,000` with runtime DB | `5,775` | 8 | 3 | Running successor path | +| A0 | W2 run completion | `675,499,000` with runtime DB | `7,276` | 8 | 4 | Running terminal path | +| A1 | Focused read/write probe | `12,500,000` versus A0 `17,530,000` | Measured `719` versus `763`; estimated `2,784` versus `5,282` | 1 versus 2 | 1 versus 2 | Same candidate Wasm and setup; complete lifecycle integration pending | +| A1 | Hot-only probe | `11,590,000` versus A0 `11,520,000` | Measured `494` versus `526`; estimated `2,784` versus `2,583` | 1 versus 1 | 1 versus 1 | Time neutral; conservative proof +201 bytes | +| A1 | RunHead-only probe | `13,130,000` versus A0 `11,870,000` | Measured `719` versus `630`; estimated `2,784` versus `2,699` | 1 versus 1 | 1 versus 1 | Isolated time +10.6%, measured proof +14.1% | +| A2 | Combined Contract+certificate probe | `12,500,000` versus A0 `22,900,000` | Measured `779` versus `1,056`; estimated `4,930` versus `7,429` | 1 versus 2 | 1 versus 2 | Contract authority remains sole semantic owner | +| A2 | Contract-only probe | `12,850,000` versus A0 `13,680,000` | Measured `779` versus `696`; estimated `4,930` versus `4,746` | 1 versus 1 | 1 versus 1 | Time -6.1%; conservative proof +184 bytes | +| A3 | W1-W4 | Not measured | Not measured | Not measured | Not measured | No conforming candidate layout exists | + +### Authority Baseline + +| Owner | RefTime with runtime DB | Proof | Reads | Writes | Storage scope | Additive | +| --- | ---: | ---: | ---: | ---: | --- | --- | +| Scheduler state probe | 245,890,000 | 5,998 | 7 | 0 | Identity, Hot, Contract head, funding, run head/payload, certificate | No | +| Cycle orchestration | 480,664,000 | 5,736 | 5 | 3 | Certificate, Contract head, funding, Hot, identity | No | +| Step orchestration n=1 | 481,773,768 | 5,736 | 5 | 3 | Certificate, Contract head, funding, Hot, identity | No | +| Run progress | 580,528,000 | 5,775 | 8 | 3 | Identity, Hot, Contract head, funding, run head, tail, hold, payload | No | +| Run complete | 675,499,000 | 7,276 | 8 | 4 | Identity, Hot, Contract head, funding, run head/payload, certificate, hold | No | +| Cheap mixed marginal | 972,536,265 | 3,361 | 11 | 5 | Complete measured cheap Actor marginal | No | +| Hot plus run head | Not applicable | 5,282 | 2 | Variable | Separate maximum proofs `2,583 + 2,699` | Yes | +| Merged Hot/run head | Not applicable | 2,807 | 1 | Variable | Approximate maximum `332 + 2,475` | Estimate | +| Contract plus certificate | Not applicable | 7,429 | 2 | Variable | Separate maximum proofs `4,746 + 2,683` | Yes | +| Merged Contract/certificate | Not applicable | 4,954 | 1 | Variable | Approximate maximum `2,479 + 2,475` | Estimate | + +### A1 Focused Probe + +| Candidate | Time ps | Measured proof | Estimated proof | Reads | Writes | Raw JSON SHA-256 | Benchmark Wasm SHA-256 | +| --- | ---: | ---: | ---: | ---: | ---: | --- | --- | +| A0 separate Hot/run head | 17,530,000 | 763 | 5,282 | 2 | 2 | `40db650677b5213d3fcf73ae6df9caf1b567126e6ecc099def513a02faaa3b9c` | `32bb96f7457f7fde82155bb5d2027daaf8891b1049e35e00c1236673db8949c9` | +| A1 merged process head | 12,500,000 | 719 | 2,784 | 1 | 1 | `47c33f00108da69445ca6d23a83fc96e9e1d4a018b180ac1ed8ba95aa8f0f59a` | `32bb96f7457f7fde82155bb5d2027daaf8891b1049e35e00c1236673db8949c9` | +| A0 Hot only | 11,520,000 | 526 | 2,583 | 1 | 1 | `e62f86511bd9dd37ae7f50300b7ee7cc2881ec0314f25c01b8cc6ccfd24502f4` | `38d3dfaae601b6a6a0cd125eff4a8d87b3d195cb6c9797211558e45e04a5c14f` | +| A1 merged Hot only | 11,590,000 | 494 | 2,784 | 1 | 1 | `9d5b4357f6c1aa4dc7f1255928e523dd128d81bfa30cce473cd1cd0c312ebad1` | `38d3dfaae601b6a6a0cd125eff4a8d87b3d195cb6c9797211558e45e04a5c14f` | +| A0 run head only | 11,870,000 | 630 | 2,699 | 1 | 1 | `c37fe55490040692c1173098c44be2d64d4faa9e39c1c5eea729882e8a829c5c` | `38d3dfaae601b6a6a0cd125eff4a8d87b3d195cb6c9797211558e45e04a5c14f` | +| A1 merged run head only | 13,130,000 | 719 | 2,784 | 1 | 1 | `2eb2bd99e2085477d1161b7bf75491bf67b938ef9173244a0db33e4ae5b786d9` | `38d3dfaae601b6a6a0cd125eff4a8d87b3d195cb6c9797211558e45e04a5c14f` | + +### A2 Focused Probe + +| Candidate | Time ps | Measured proof | Estimated proof | Reads | Writes | Raw JSON SHA-256 | Benchmark Wasm SHA-256 | +| --- | ---: | ---: | ---: | ---: | ---: | --- | --- | +| A0 Contract plus certificate | 22,900,000 | 1,056 | 7,429 | 2 | 2 | `c0762ee589d28efea8535a97468ad9dbcdadac997108327b173d7c9debc08794` | `d259331f2d6a78c47e195b780ff3caf93e2d2b67effe7451d237d7fb847f07e2` | +| A2 merged Contract authority | 12,500,000 | 779 | 4,930 | 1 | 1 | `548c095562ecd5d4460597a522e543e930fa7166e4358878c2b86f2463f4adcf` | `d259331f2d6a78c47e195b780ff3caf93e2d2b67effe7451d237d7fb847f07e2` | +| A0 Contract only | 13,680,000 | 696 | 4,746 | 1 | 1 | `2efb404b468940f651a9febc7fafa64ec853a1e3777bb646aaaa209dde589ba4` | `d259331f2d6a78c47e195b780ff3caf93e2d2b67effe7451d237d7fb847f07e2` | +| A2 merged Contract only | 12,850,000 | 779 | 4,930 | 1 | 1 | `4597358be8aeba4a8896e0a3872655078aca7c68109c621135344a267900f991` | `d259331f2d6a78c47e195b780ff3caf93e2d2b67effe7451d237d7fb847f07e2` | + +### A2 Integrated Profile + +| Candidate | Allocation | Control RefTime / proof | Actor base RefTime / proof | User base RefTime / proof | W1 | W2 | W3 | Failed Steps | +| --- | --- | --- | --- | --- | ---: | ---: | ---: | ---: | +| A0 separate authority | Equal thirds | `303,126,763,576 / 676,150` | `303,126,763,576 / 676,150` | `303,126,763,576 / 676,150` | 679 | 679 | 679 | 0 | +| A2 merged authority | Equal thirds | `306,167,841,909 / 676,150` | `306,167,841,909 / 676,150` | `306,167,841,910 / 676,150` | 658 | 658 | 658 | 0 | + +## Derived Metrics + +| Candidate | Workload | RefTime delta | ProofSize delta | Throughput / latency | Marginal or amortized cost | Pareto class | +| --- | --- | ---: | ---: | --- | --- | --- | +| A1 | Focused probe | `-28.7%` | Measured `-5.8%`; estimated `-47.3%` | Complete-path throughput not measured | Removes one read and one write | Micro-winner only | +| A1 | Equal-thirds feasibility bound | At most about `130.0M` RefTime saved against about `20.8B` effective saturated control per committed Actor | At most `2,475` structural bytes against about `45.9K` effective proof per committed Actor | Optimistic ceiling below `1%` RefTime and about `5.4%` proof | Assumes every isolated saving survives composition | Reject before storage rewrite | +| A2 | Combined probe | `-45.4%` | Measured `-26.2%`; estimated `-33.6%` | Complete-path throughput not measured | Removes one read and one write | Strongest focused candidate; advance first | +| A2 | Contract-only probe | `-6.1%` | Measured `+11.9%`; estimated `+3.9%` | Complete-path throughput not measured | Same operations | Bounded proof tradeoff | +| A2 | Integrated W1/W2 | Control envelope `+1.0%` from regenerated block normalization | Proof envelope unchanged | Blocks `679→658`; throughput `+3.2%` | Zero semantic failures; user dispatch retained | Sub-material | +| A2 | Integrated W3 | Same | Same | Blocks `679→658`; throughput `+3.2%` | Zero failed Steps | Sub-material | +| A3 | W1/W5 one-Step | No possible read removal | No possible proof removal | No primary-metric gain | Adds RunFrame materialization if retained | Dominated | +| A3 | W2/W3 Running/retry | Potential current-Step read avoidance | Potential fragment-proof avoidance | Unmeasured | Requires a persistent Step copy or destructive reusable-Contract rewrite | Non-conforming | + +## Statistical / Variance Notes + +Focused probes use matched candidate Wasm and 50/20 generation. Integrated A2 uses deterministic equal-thirds W1-W3 profiles. A1 and A3 stop before integration because optimistic feasibility or semantic ownership already fails the declared gate. + +## Result + +A2 was the strongest focused candidate, but complete generated Weight disproves material production impact. Integrated equal-thirds W1/W2/W3 all improve only from block 679 to 658, about 3.2% throughput, below the declared 20% threshold. Zero failures and retained user dispatch establish semantic viability but do not justify production churn; the integrated candidate must be reverted. + +A1 combined-owner measurement confirms a local structural win, but equal-thirds production geometry bounds its possible complete-path effect far below materiality. At about 14.7 committed Actors/block, each committed Actor consumes roughly `20.8B / 45.9K` of the saturated Control envelope. Even granting the full isolated read+write/time saving and the full structural proof reduction on every commit yields below 1% RefTime and about 5.4% proof, while RunHead-only access already regresses 10.6% time and 14.1% measured proof. An integrated storage rewrite cannot clear the 20% gate. + +A3 has no conforming physical layout. A reusable Contract must retain its authored Step for future Cycles. Materializing that Step in a persistent RunFrame duplicates semantic execution authority, forbidden by §3.5 and §13.1. Moving it destructively from the Contract requires bounded restoration on completion/cancel/close and breaks ordinary one-Step Opening without removing its Contract read. A3 is therefore dominated for W1/W5 and non-conforming for W2/W3. + +## Interpretation + +A2 is rejected as sub-material, reverted to the separate owner baseline, and retained as evidence only. A1 is rejected before integration because an optimistic composition bound is still far below the declared gate and its RunHead-only path regresses. A3 is rejected because every persistent form duplicates Step truth and every destructive form breaks reusable Contract ownership. The separate A0 authority geometry remains the local Pareto baseline. + +## Decision + +Retain A0. No candidate materially improves `CommittedStepsPerBlock`, `DistinctActorsProgressedPerBlock`, `ActorMachineCostPerCommittedStep`, or component-wise `UsefulActionWeight / ActorMachineWeight` without a larger proof/lifecycle tradeoff or semantic-authority violation. Close EXP-0005 and move the next independent gradient to EXP-0021 Contract ceiling. + +## Rejected Alternatives and Tradeoffs + +| Candidate | Decision | Evidence-backed reason | +| --- | --- | --- | +| Whole-Contract duplicate cache | Excluded | Duplicates semantic truth and enlarges bounded writes/state hold | +| Step payload copied into FIFO page | Excluded | Amplifies every queue-page proof and duplicates Actor-local authority | +| Off-chain execution cache | Excluded | Cannot own consensus correctness | +| A1 merged Hot+RunHead | Rejected | Optimistic complete-path bound remains below 1% RefTime/about 5.4% proof and RunHead-only regresses | +| A2 merged Contract+certificate | Rejected and reverted | Integrated throughput gain is only 3.2% against the declared 20% gate | +| A3 persistent Current RunFrame | Rejected | Duplicates authored Step authority or requires destructive Contract restoration | + +## Validity + +- `Establishes`: A0 is the local Pareto authority geometry for the declared equal-thirds workloads and current storage/Weight model. +- `Does not establish`: Permission to change FIFO, Q1, or Actor semantics. +- `Known limitations`: Structural proof estimates exclude trie/model interaction and require focused generated evidence. +- `Invalidation triggers`: Contract/run layout, queue topology, Weight method, or semantic lifecycle changes. +- `Residual qualitative insight`: Co-location wins only when avoided proofs exceed larger writes and invalidation cost. + +## Follow-Up / Next Gradient + +```text +about 14.7 committed distinct Actors/block → decompose authority proofs +→ reject A1 by complete-path bound → integrate/revert sub-material A2 +→ reject non-conforming A3 → retain A0 → advance EXP-0021 +``` + +- `Eliminated hypotheses`: A1, A2, A3, whole-Contract, FIFO-payload, and off-chain caches. +- `Remaining hypotheses`: None in current execution-authority geometry. +- `Stop condition`: Met; retain A0. + +## Architecture Impact + +- `Affected physical truth`: Hot/run/Contract storage topology and execution loading. +- `Architecture update`: None; rejected A2 was reverted and A0 remains implementation truth. +- `Provenance citation`: `actors/EXP-0005`. + +## Relations + +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: EXP-0013 loaded-state reuse. +- `Contradicts`: None. +- `Depends on`: Actors specification, EXP-0013, and EXP-0016. +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0006.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0006.md new file mode 100644 index 00000000..3e3cdf5e --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0006.md @@ -0,0 +1,186 @@ +# EXP-0006 — Actor Control Allocation Sweep + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Architecture release / experiment campaign | DEOS 0.7.24 optimization campaign | +| Date | 2026-08-30 | +| Affected domain | Actor Control and Shared Economic block allocation | +| Physical mechanism | Component-wise block resource partition | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | EXP-0013 | +| Successor experiment | None | + +## Decision Question + +Does `20/80`, `25/75`, or `30/70` maximize useful Actor progress while retaining explicit user and Actor effect capacity? + +## Governing Specification + +- `Exact section`: `docs/actors-resource-policy.specification.en.md` → `2. Resource Algebra` and `3. Economic Zipper`. +- `Invariant`: Allocation remains component-wise, work-conserving, class-neutral, and split equally between Actor/User base turns inside Shared Economic. +- `Reopen trigger`: Selecting a candidate that removes guaranteed user service, changes FIFO/Q1, or treats RefTime and ProofSize as fungible requires Specification reopening. + +## Context and Hypothesis + +- `Current architecture`: Production reserves 20% of schedulable Weight for Actor Control and 80% for Shared Economic, split into equal 40/40 base turns. +- `Reason`: The accepted effectful 10,000-Actor first traversal averages about 7.7 distinct Actors per block and Actor Control binds before cheap Transfer effects. +- `Hypothesis`: 25% or 30% Control materially increases committed Steps without making user or expensive Actor effects the new unacceptable bottleneck. +- `Materiality`: Select a non-baseline candidate only for at least 20% more distinct Actors per saturated block or a Pareto improvement in 10k traversal with explicit retained user throughput. + +## Baseline and Candidates + +| ID | Architecture | Source tree | Runtime/config identity | +| --- | --- | --- | --- | +| A0 | 20% Control / 80% Shared; 40/40 base turns | HEAD `ab62e396`; dirty-worktree digest `e739c2d3` | Production P128/P64, Q1, C6, MaxSteps32 | +| A1 | 25% Control / 75% Shared; 37.5/37.5 base turns | Candidate worktree digest `9cdbc741`; feature `exp-actor-control-25` | Otherwise identical to A0 | +| A2 | 30% Control / 70% Shared; 35/35 base turns | Candidate worktree digest `9cdbc741`; feature `exp-actor-control-30` | Otherwise identical to A0 | + +## Controls + +- `Controlled variables`: Source geometry, generated Weight owners, toolchain, database, Q1, FIFO, actor population, Contract/task mix, Trigger mix, setup state, block count, and test commands. +- `Changed variables`: Actor Control percentage and derived Shared Economic/base-turn limits only. +- `Confounders`: Candidate runtime Wasms differ by constants; full Weight regeneration is unnecessary unless candidate code or benchmarked branch geometry changes. + +## Benchmark Class, Workloads, and Criteria + +- `Benchmark class`: Deterministic resource algebra, full-runtime block, and native release stress. + +| Workload | State/population geometry | Hot/cold assumptions | Purpose | +| --- | --- | --- | --- | +| W1 | 10,000 Manual/Cadenced one-Step Transfer Actors | Setup outside measured blocks; active state cold per block | Actor-only first traversal | +| W2 | W1 plus one signed user dispatch every block | Same as W1 | User-base-turn preservation | +| W3 | Mixed 9,500 Transfer / 400 SwapOut / 100 other demand | Production task state | Detect effect bottleneck migration | +| W4 | Empty, partial, saturated Actor and user demand | Canonical block phases | Borrowing and fragmentation | + +- `Metrics`: Committed Steps/block, distinct Actors/block, first-traversal blocks, user dispatches, RefTime/ProofSize by domain, first exhausted component, failures, state roots, and effect dispositions. +- `Statistical method`: Deterministic fixed populations and 1,300-block horizons; production generated Weight supplies conservative bounds; repeat each candidate profile three times and require identical semantic counters. +- `Acceptance criteria`: Semantic equality, every complete branch component-wise admitted, user base turn preserved, and material throughput improvement without an unacceptable effect-capacity regression. +- `Rejection criteria`: Lost progress, FIFO drift, user starvation, task-disposition drift, block overrun, or improvement below materiality. + +## Environment and Artifact Identity + +| Artifact | Identity | +| --- | --- | +| Source commit | `ab62e39612aee895ad41bb45c4bf085318b6cf8a` | +| Source tree | Dirty-worktree diff digest `e739c2d32a546a76269d481b95faf8b803b0ee131e6523fca0338f71e76bc8ba` | +| Rust toolchain | `rustc 1.98.0 (88d9e12ae 2026-08-18)` | +| Benchmark runtime Wasm | Pending candidate builds | +| Post-generation production runtime Wasm | A0 `9cf6cd5966d044aafe1563b43dbe49b2ff896c4cf557feac53eb61284c076fcd`; accepted A2 `595165fb8778871b2852c95d08b3a1f562ce42788af15a3c16ff67fe80db73d9` | +| Final release production runtime Wasm | Pending exact release tree | +| Generated Weight | A0 `54098ec60bb3b283f3dd226f23f82d4d4025a93bb8763fa1fa8ea7c82292602d` | +| Database/backend | FRAME compiled Wasm and native release tests on the recorded development host | +| Benchmark command/config | `SKIP_WASM_BUILD=1 cargo test --release --locked -p deos-runtime [optional candidate feature] transfer_10k_first_traversal_with_continuous_user_dispatch --lib -- --ignored --nocapture` | +| Raw evidence | Inline W1/W2/W3 sweep tables under Measurements; temporary TSV digests `1135fdd6`, `b84d486a`, and `16efca51` | + +## Measurements + +| Candidate | Workload | RefTime | ProofSize | Reads | Writes | Other decision metrics | +| --- | --- | ---: | ---: | ---: | ---: | --- | +| A0 | W1 | Control `181,876,058,145`; Shared `727,504,232,583` | Control `405,690`; Shared `1,622,760` | Unchanged generated owners | Unchanged generated owners | Actor-only first traversal completes at block 1,000 | +| A1 | W1 | Control `227,345,072,682`; Shared `682,035,218,046` | Control `507,112`; Shared `1,521,338` | Unchanged generated owners | Unchanged generated owners | Actor-only first traversal completes at block 867 | +| A2 | W1 | Control `272,814,087,218`; Shared `636,566,203,510` | Control `608,535`; Shared `1,419,915` | Unchanged generated owners | Unchanged generated owners | Actor-only first traversal completes at block 746 | +| A0 | W2 | Control `181,876,058,145`; Shared `727,504,232,583` | Control `405,690`; Shared `1,622,760` | Unchanged generated owners | Unchanged generated owners | Continuous user dispatch; first traversal completes at block 1,000 | +| A1 | W2 | Control `227,345,072,682`; Shared `682,035,218,046` | Control `507,112`; Shared `1,521,338` | Unchanged generated owners | Unchanged generated owners | First traversal completes at block 867 | +| A2 | W2 | Control `272,814,087,218`; Shared `636,566,203,510` | Control `608,535`; Shared `1,419,915` | Unchanged generated owners | Unchanged generated owners | First traversal completes at block 746 | +| A0 | W3 | Production bounded | Production bounded | Unchanged generated owners | Unchanged generated owners | 9,500 Transfer/400 SwapOut/100 control plus continuous user dispatch; block 993; zero failed Steps | +| A1 | W3 | Production bounded | Production bounded | Unchanged generated owners | Unchanged generated owners | Block 865; zero failed Steps | +| A2 | W3 | Production bounded | Production bounded | Unchanged generated owners | Unchanged generated owners | Block 765; zero failed Steps | +| A0-A2 | W4 | Exact component partitions | Exact component partitions | Not applicable | Not applicable | Saturated base turns and forward borrowing consume each candidate Shared envelope exactly | + +### W1 Transfer Sweep + +| Candidate | Control ratio | Completion block | Failed Steps | Transfer Actors | Continuous user dispatch | +| --- | --- | ---: | ---: | ---: | --- | +| A0 | `20/80` | 1,000 | 0 | 10,000 | No | +| A1 | `25/75` | 867 | 0 | 10,000 | No | +| A2 | `30/70` | 746 | 0 | 10,000 | No | + +### W2 Resource Partition Sweep + +| Candidate | Control ratio | Actor Control RefTime / proof | Shared RefTime / proof | Actor base RefTime / proof | User base RefTime / proof | Completion block | +| --- | --- | --- | --- | --- | --- | ---: | +| A0 | `20/80` | `181,876,058,145 / 405,690` | `727,504,232,583 / 1,622,760` | `363,752,116,291 / 811,380` | `363,752,116,292 / 811,380` | 1,000 | +| A1 | `25/75` | `227,345,072,682 / 507,112` | `682,035,218,046 / 1,521,338` | `341,017,609,023 / 760,669` | `341,017,609,023 / 760,669` | 867 | +| A2 | `30/70` | `272,814,087,218 / 608,535` | `636,566,203,510 / 1,419,915` | `318,283,101,755 / 709,957` | `318,283,101,755 / 709,958` | 746 | + +### W3 Mixed Economic Sweep + +| Candidate | Control ratio | Completion block | Failed Steps | Transfer | SwapOut | Control | Continuous user dispatch | +| --- | --- | ---: | ---: | ---: | ---: | ---: | --- | +| A0 | `20/80` | 993 | 0 | 9,500 | 400 | 100 | Yes | +| A1 | `25/75` | 865 | 0 | 9,500 | 400 | 100 | Yes | +| A2 | `30/70` | 765 | 0 | 9,500 | 400 | 100 | Yes | + +## Derived Metrics + +| Candidate | Workload | RefTime delta | ProofSize delta | Throughput / latency | Marginal or amortized cost | Pareto class | +| --- | --- | ---: | ---: | --- | --- | --- | +| A1 | W2 | +25% Control envelope | +25% Control envelope | Traversal blocks `-13.3%`; about `+15.4%` traversal throughput | 5 percentage points moved from Shared; each base turn `-6.25%` | Below declared 20% throughput materiality on W2 | +| A2 | W2 | +50% Control envelope | +50% Control envelope | Traversal blocks `-25.4%`; about `+34.0%` traversal throughput | 10 percentage points moved from Shared; each base turn `-12.5%` | Material on W2 | +| A1 | W3 | Allocation-only | Allocation-only | Traversal blocks `-12.9%`; about `+14.8%` throughput | Shared/base reduction as above | Below materiality | +| A2 | W3 | Allocation-only | Allocation-only | Traversal blocks `-23.0%`; about `+29.8%` throughput | Shared/base reduction as above | Material; no observed effect or user failure | + +## Statistical / Variance Notes + +Deterministic semantic counters must match exactly. Wall-clock duration is diagnostic only; consensus decisions use generated two-dimensional Weight and fixed workload counters. + +## Result + +W1, W2, and W3 completed deterministically for A0/A1/A2, and W4 resource algebra passes every candidate. W1 exactly matches W2 at 1,000/867/746 completion blocks, proving that one continuous user dispatch does not mask attribution. A2 finishes W3 228 blocks earlier than A0; all 400 SwapOut Actors reach successful zero-failure dispositions. + +## Interpretation + +Measurement is complete. W1 and W2 are identical, so the continuous user dispatch is non-binding. A1 misses the predeclared materiality threshold in all measured traversals. A2 yields about 34.0% more W1/W2 throughput and 29.8% more W3 throughput; 400 SwapOut effects do not make 70% Shared Economic binding. The measured cost is explicit: each guaranteed Shared base turn shrinks 12.5% from the A0 value. A2 is the only candidate that clears materiality in every throughput workload while preserving exact user-turn algebra and zero failed mixed effects. + +## Decision + +Accept A2 as the DEOS reference allocation: 30% Actor Control, 70% Shared Economic, and equal 35% Actor/User base turns. Reject A1 as sub-material. Delete candidate-only feature branches after binding A2 as the fixed production default; retain the generic checked ratio constructor as host/resource-algebra capability. + +## Rejected Alternatives and Tradeoffs + +| Candidate | Decision | Evidence-backed reason | +| --- | --- | --- | +| A0 20/80 | Rejected | Correct and safe, but materially slower in every saturated traversal | +| A1 25/75 | Rejected | Improves throughput only 14.8–15.4%, below the declared 20% threshold | +| A2 30/70 | Accepted | Improves throughput 29.8–34.0%, preserves continuous user dispatch, zero failed mixed Steps, and exact borrowing | +| Above 30% Control | Excluded | Would reduce each Shared base turn below 35% before nearby candidates establish need | +| Dynamic per-block ratio | Excluded | Adds policy/state complexity and changes the fixed resource contract | + +## Validity + +- `Establishes`: A2 is the best declared fixed allocation for the measured production workloads. +- `Does not establish`: A universal optimal ratio or execution SLA. +- `Known limitations`: User demand uses one admitted call per block rather than saturating every user base turn; W4 proves the exact guaranteed envelope algebra. +- `Invalidation triggers`: Changed Weight owners, block limits, FIFO/Q1 semantics, workload geometry, or resource algebra. +- `Residual qualitative insight`: Control gains must be priced against both Actor effects and user work. + +## Follow-Up / Next Gradient + +```text +100 Steps/block aspiration → A0 about 7.7 distinct Actors/block +→ Actor Control appears binding → ratio is the owning policy +→ test 25/75 and 30/70 → optimize authority only if ratio remains insufficient +``` + +- `Eliminated hypotheses`: A1 is sub-material; A0 is slower; ratios above 30% and dynamic ratios remain unearned. +- `Remaining hypotheses`: Execution-authority geometry can reduce the dominant control unit further under A2. +- `Stop condition`: Met by accepting A2 after W1-W4. + +## Architecture Impact + +- `Affected physical truth`: Runtime block resource constants and derived base turns. +- `Architecture update`: Bind 30/70 in the runtime, resource specification, integration docs, and production tests. +- `Provenance citation`: `actors/EXP-0006`. + +## Relations + +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: EXP-0013 production throughput pressure. +- `Contradicts`: None. +- `Depends on`: Actors resource policy, EXP-0013, and current production Weight. +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0008.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0008.md new file mode 100644 index 00000000..17efd045 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0008.md @@ -0,0 +1,160 @@ +# EXP-0008 — Crossing Membership Page Geometry + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Architecture release / experiment campaign | DEOS 0.7.24 | +| Date | 2026-08-29 | +| Affected domain | DEOS Actors ObservationCrossing membership topology | +| Physical mechanism | Sparse Crossing member pages and contiguous cohort snapshots | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | EXP-0010 | +| Successor experiment | EXP-0009 | + +## Decision Question + +Which production `ObservationPageSize` among 64, 128, and 256 minimizes complete Crossing materialization cost while retaining bounded mutation, exact non-tail compaction, and at least 10,000 admitted feed memberships? + +## Governing Specification + +- `Exact section`: `template/pallets/actors/docs/specification.en.md` → `3.5 ObservationCrossing`, `15.2.9 Production Bounds`; `docs/actors-performance-assurance.specification.en.md` → Crossing production evidence. +- `Invariant`: Pages preserve generation-checked membership authority, exact tail/non-tail compaction, revision order, scalar fallback, bounded ProofSize, class-neutral FIFO, and current-state Trigger latching. +- `Reopen trigger`: Any candidate requires changing Crossing semantics, FIFO order, the 10,000-member bound, or one-causal-hop timing rather than physical page geometry. + +## Context and Hypothesis + +- `Current architecture`: Production uses 64-entry Observation pages, a four-candidate Crossing cohort, sparse radix leaves, contiguous-prefix snapshots, and exact physical-tail refill for non-tail removal. +- `Reason`: The release requires a generated cohort of at least 128 candidates when the complete branch fits and explicitly requires measured `64/128/256` page geometry. +- `Hypothesis`: A 128- or 256-entry page can reduce page traversal and refill reads enough to support a larger contiguous cohort, but may lose on maximum encoded-value ProofSize and mutation cost. +- `Materiality`: Select only a candidate that admits 10,000 memberships, keeps every complete branch inside Actor Control and the Crossing worker envelope component-wise, and improves the downstream EXP-0009 cohort frontier. + +## Baseline and Candidates + +| ID | Architecture | Source tree | Runtime/config identity | +| --- | --- | --- | --- | +| P64 | Current contiguous page and tail-refill topology | HEAD `cbc5a2b9ed32f385c6d3cb19c1e673ff476b1e73`; shared candidate source below | Default feature set; `ObservationPageSize=64`, `MaxCrossingActorsPerBlock=4`, 10,000 total / 9,000 User memberships | +| P128 | Same topology with 128-entry pages | Same feature-gated source | `exp-crossing-page-128`; only `ObservationPageSize=128` changes | +| P256 | Same topology with 256-entry pages | Same feature-gated source | `exp-crossing-page-256`; only `ObservationPageSize=256` changes | + +## Controls + +- `Controlled variables`: Rust/Cargo toolchain, compiled-Wasm benchmark method, database/cache, 50 steps, 20 repeats, Crossing semantics, radix topology, transition bounds, membership population, queue state, Trigger mix, cohort size, worker envelope, and all non-page runtime constants. +- `Changed variables`: `ObservationPageSize` and resulting SCALE page bound only. +- `Confounders`: The production cohort remains four and cannot decide the final ≥128 cohort; EXP-0009 owns cohort selection after this experiment selects page geometry. Candidate benchmark/production Wasm identities do not exist until measurement builds. + +## Benchmark Class, Workloads, and Criteria + +- `Benchmark class`: FRAME pallet generated Weight under compiled Wasm plus deterministic package/runtime integration profiles. + +| Workload | State/population geometry | Hot/cold assumptions | Purpose | +| --- | --- | --- | --- | +| W1 Tail fire | Full source tail page; homogeneous fire candidates | Maximum encoded page cold | Compare contiguous fire and page rewrite | +| W2 Non-tail fire | Full source page plus full physical tail refill | Both pages cold | Expose dominant refill ProofSize and writes | +| W3 Rearm/coalesce/close | Full pages across each terminal branch | Branch-required Actor state cold | Prevent optimization for placed-only behavior | +| W4 Scalar/mixed fallback | First mismatch at positions 1, midpoint, and final slot | Required pages cold | Preserve exact fallback and bounded classification | +| W5 Capacity | 10,000 total / 9,000 User memberships | Deterministic prefunded setup outside measurement | Prove admission and sparse-page topology | + +- `Metrics`: RefTime, ProofSize, reads, writes, encoded page bytes, candidates admitted per complete branch, pages touched, worker-envelope fit, and runtime Wasm bytes. +- `Statistical method`: Generate Weight with 50 steps and 20 repeats against each matching benchmark Wasm; retain raw model output and review both Weight components. Deterministic correctness profiles use exact state/event roots and no timing-based exclusion. +- `Acceptance criteria`: Select the Pareto candidate that enables the highest conforming EXP-0009 cohort; ties prefer smaller maximum encoded page value and lower lifecycle mutation Weight. Every branch and 10,000-member profile must pass. +- `Rejection criteria`: Any invariant regression, component-wise envelope overflow, maximum-extrinsic failure, capacity loss, stale locator, partial mutation, or candidate that cannot improve the downstream cohort frontier. + +## Environment and Artifact Identity + +| Artifact | Identity | +| --- | --- | +| Source commit | `cbc5a2b9ed32f385c6d3cb19c1e673ff476b1e73` | +| Source tree | Shared feature-gated source: `runtime/Cargo.toml` SHA-256 `0c5b19a7ccf7f06086457e588929f36936951010018d8952870525aceb30fee0`; `actor_config.rs` SHA-256 `3802d5f1add2329ca9971a41eb059d3113bfc9d209ddea29d8495d9b29cc8553` | +| Rust toolchain | `rustc 1.94.1 (e408947bf 2026-03-25)`; `cargo 1.94.1 (29ea6fb6a 2026-03-24)`; toolchain SHA-256 `65c209bb7f6bb23e50e17220955aa8f818e55973436ea7dc93e1cb7ec5afe37e` | +| Benchmark runtime Wasm | P64 `9dbeb7e85724dd0950966098b0a25f9f3a9a54f9f4570b892d2466a024896aa9` (6,087,493 bytes); P128 `b267768d55c7cab83d9d861ba75a75ae2a298f6b246f48cb3c9be3714b21246c` (6,088,735 bytes); P256 `acd226cbf3b697a86c65b20cfa0b415a73b9983e3d305aeb968d1736bcd3c1ce` (6,088,826 bytes) | +| Post-generation production runtime Wasm | Selected split-page composition `9cf6cd5966d044aafe1563b43dbe49b2ff896c4cf557feac53eb61284c076fcd` (P128 Crossing, P64 broad fanout; canonical production profile, about 1.4 MiB) | +| Final release production runtime Wasm | Pending exact release tree | +| Generated Weight | `template/runtime/src/weights/pallet_deos_actors.rs`; candidate hashes pending | +| Database/backend | FRAME compiled Wasm, database cache 1024 | +| Benchmark command/config | `./scripts/benchmarks.sh` focused Actors owners, 50 steps / 20 repeats; exact invocations pending preparation | +| Raw evidence | Focused generated owners retained under sibling [`EXP-0008/`](./EXP-0008/); current path-sorted SHA-256 manifest digest `f4d177996a6ec16145c65135c2c092eba47e11123ddb73399958d36fcfe099e0`; candidate Wasms were rebuilt sequentially from shared source | + +## Measurements + +| Candidate | Workload | RefTime | ProofSize | Reads | Writes | Other decision metrics | +| --- | --- | ---: | ---: | ---: | ---: | --- | +| P64 | Tail refill probe | 17,181,000 | 4,561 | 1 | 0 | Current page proof baseline | +| P128 | Tail refill probe | 18,928,000 | 5,585 | 1 | 0 | +1,242 Wasm bytes versus P64 | +| P256 | Tail refill probe | 19,625,000 | 7,633 | 1 | 0 | +1,333 Wasm bytes versus P64 | +| P64 | Placed maximum | 1,001,051,000 | 81,886 | 89 | 121 | 64-entry maximum branch | +| P128 | Placed maximum | 1,136,336,000 | 81,886 | 89 | 185 | 128-entry maximum branch | +| P256 | Placed maximum | 1,398,176,000 | 81,886 | 89 | 313 | 256-entry maximum branch | +| P64 | Non-tail emptied / trimmed | 794,248,000 / 791,174,000 | 81,886 | 87 | 123 / 125 | Exact refill variants | +| P128 | Non-tail emptied / trimmed | 944,967,000 / 960,123,000 | 81,886 | 87 | 187 / 189 | Exact refill variants | +| P256 | Non-tail emptied / trimmed | 1,241,029,000 / 1,244,871,000 | 81,886 | 87 | 315 / 317 | Exact refill variants | + +## Derived Metrics + +| Candidate | Workload | RefTime delta | ProofSize delta | Throughput / latency | Marginal or amortized cost | Pareto class | +| --- | --- | ---: | ---: | --- | --- | --- | +| P128 | Tail / maximum / emptied / trimmed | +10.17% / +13.51% / +18.98% / +21.35% | +22.45% tail; unchanged 81,886 branch cap | Enables one-page 128-candidate snapshot geometry | +64 writes on each maximum mutation branch | Dominated by P64 at common work; target-enabling candidate | +| P256 | Tail / maximum / emptied / trimmed | +14.23% / +39.67% / +56.25% / +57.34% | +67.35% tail; unchanged 81,886 branch cap | Enables one-page 256-candidate snapshot geometry | +192 writes on each maximum mutation branch | Dominated by P128 for the required 128 frontier | + +## Statistical / Variance Notes + +All twelve focused owners used generated Weight with 50 steps and 20 repeats against the matching candidate Wasm. No samples or owners were discarded. The current fixed ProofSize cap of the complete mutation branches masks page-value growth there, while the isolated tail probe exposes the expected 4,561 → 5,585 → 7,633 proof progression. + +## Result + +P64 is cheapest at common work. P128 pays 10.17–21.35% RefTime and 64 additional maximum-branch writes to make a one-page 128-candidate snapshot representable. P256 pays 14.23–57.34% RefTime and 192 additional writes, while its extra 256-entry frontier is not required by the release target. + +## Interpretation + +EXP-0009 confirms that P128 is the smallest page supporting the selected C128 tail/preflight frontier. H0 plus the N64 non-tail cap keeps complete ProofSize inside Actor Control without requiring P256 or weakening exact refill. + +## Decision + +Accept P128 as the Crossing membership baseline with EXP-0009's C128 tail/preflight and N64 non-tail geometry. Full production regeneration exposed that sharing P128 with broad ObservationChange made one fanout page exceed Actor Control; physical ownership is therefore split, retaining P64 broad fanout independently. Full Actors Weight, metadata/ABI, storage version 15, and canonical post-generation Wasm evidence have landed. + +## Rejected Alternatives and Tradeoffs + +| Candidate | Decision | Evidence-backed reason | +| --- | --- | --- | +| Pages below 64 | Excluded | Cannot improve the required ≥128 contiguous cohort frontier and increases traversal pressure | +| P64 | Rejected for target | Cheapest common work but cannot represent the selected one-page C128 frontier | +| P128 | Accepted | Smallest target-enabling page; complete H0-N64 evidence passes in EXP-0009 | +| P256 | Rejected | Adds page ProofSize and mutation writes without a required frontier | +| Pages above 256 | Deferred | No release requirement or measured pressure justifies the larger maximum encoded value | + +## Validity + +- `Establishes`: The predeclared candidate set, controls, workloads, and decision rule. +- `Does not establish`: A winning page size, ≥128 cohort fit, or production throughput. +- `Known limitations`: Candidate branch Weight remains unmeasured; matching benchmark Wasm identities and compile checks do not establish dispatchability. +- `Invalidation triggers`: Crossing storage topology, latching authority, radix traversal, worker envelope, resource policy, membership bounds, or benchmark methodology changes. +- `Residual qualitative insight`: Larger pages trade fewer topology reads for larger cold proofs and mutations; neither dimension may be inferred from capacity alone. + +## Follow-Up / Next Gradient + +```text +four-candidate production ceiling → page/refill pressure +→ compare 64/128/256 under common cohort geometry +→ select page geometry → EXP-0009 cohort scaling +``` + +- `Eliminated hypotheses`: None yet. +- `Remaining hypotheses`: None inside the measured P64/P128/P256 geometry; reopen only if a stronger production target invalidates the accepted frontier. +- `Stop condition`: One candidate is selected with complete branch, capacity, Weight, Wasm, and exact-root evidence, or all candidates fail the downstream frontier and EXP-0009 reports the binding owner. + +## Architecture Impact + +- `Affected physical truth`: Crossing member page encoding, page traversal, contiguous snapshots, tail refill, and generated branch Weight. +- `Architecture update`: Update package and DEOS integration architecture only after selected implementation, tests, correction, and generation converge. +- `Provenance citation`: `Actors EXP-0008`. + +## Relations + +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: EXP-0010 compact Observation activation authority. +- `Contradicts`: None. +- `Depends on`: EXP-0010, EXP-0020, Q1, P32, strict FIFO, and the fixed resource policy. +- `Baseline of`: EXP-0009. +- `Confirms`: None yet. +- `Invalidates`: None. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0008/p128-crossing_placed_maximum_unit.rs b/.agents/skills/architecture-experiments/tracks/actors/EXP-0008/p128-crossing_placed_maximum_unit.rs new file mode 100644 index 00000000..fa261b08 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0008/p128-crossing_placed_maximum_unit.rs @@ -0,0 +1,99 @@ + +//! Autogenerated weights for `pallet_deos_actors` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 58.0.0 +//! DATE: 2026-08-29, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `fedora`, CPU: `AMD Ryzen 7 4800H with Radeon Graphics` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 + +// Executed Command: +// frame-omni-bencher +// v1 +// benchmark +// pallet +// --runtime +// /home/llb/Repos/deos/template/target/benchmarks/release/wbuild/deos-runtime/deos_runtime.compact.compressed.wasm +// --pallet +// pallet_deos_actors +// --extrinsic +// crossing_placed_maximum_unit +// --steps +// 50 +// --repeat +// 20 +// --heap-pages +// 4096 +// --output +// /tmp/deos-exp-0008/weights/p128-crossing_placed_maximum_unit.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions for `pallet_deos_actors`. +pub struct WeightInfo(PhantomData); +impl pallet_deos_actors::WeightInfo for WeightInfo { + /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:1) + /// Proof: `Actors::CrossingPendingFeedListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingTransitionQueues` (r:1 w:0) + /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(3171), added: 5646, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingRangeCursors` (r:1 w:1) + /// Proof: `Actors::CrossingRangeCursors` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingLeafStates` (r:2 w:2) + /// Proof: `Actors::CrossingLeafStates` (`max_values`: None, `max_size`: Some(62), added: 2537, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingMemberPages` (r:2 w:2) + /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(2120), added: 4595, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingMemberships` (r:8 w:128) + /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:4 w:4) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorHot` (r:4 w:4) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:4 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:4 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorIdentities` (r:4 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorFunding` (r:4 w:0) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:4 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) + /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + /// Storage: `Actors::NextQueueTicket` (r:1 w:1) + /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueHead` (r:1 w:0) + /// Proof: `Actors::QueueHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueTail` (r:1 w:1) + /// Proof: `Actors::QueueTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueOccupancy` (r:1 w:1) + /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueuePages` (r:1 w:1) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:5 w:5) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingFeedMembershipCount` (r:1 w:1) + /// Proof: `Actors::CrossingFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingUserFeedMembershipCount` (r:1 w:1) + /// Proof: `Actors::CrossingUserFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingRadixNodes` (r:32 w:32) + /// Proof: `Actors::CrossingRadixNodes` (`max_values`: None, `max_size`: Some(53), added: 2528, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingPendingFeeds` (r:1 w:0) + /// Proof: `Actors::CrossingPendingFeeds` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) + fn crossing_placed_maximum_unit() -> Weight { + // Proof Size summary in bytes: + // Measured: `17455` + // Estimated: `81886` + // Minimum execution time: 1_108_399_000 picoseconds. + Weight::from_parts(1_136_336_000, 0) + .saturating_add(Weight::from_parts(0, 81886)) + .saturating_add(T::DbWeight::get().reads(89)) + .saturating_add(T::DbWeight::get().writes(185)) + } +} diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0008/p128-crossing_placed_non_tail_emptied_unit.rs b/.agents/skills/architecture-experiments/tracks/actors/EXP-0008/p128-crossing_placed_non_tail_emptied_unit.rs new file mode 100644 index 00000000..00546177 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0008/p128-crossing_placed_non_tail_emptied_unit.rs @@ -0,0 +1,99 @@ + +//! Autogenerated weights for `pallet_deos_actors` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 58.0.0 +//! DATE: 2026-08-29, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `fedora`, CPU: `AMD Ryzen 7 4800H with Radeon Graphics` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 + +// Executed Command: +// frame-omni-bencher +// v1 +// benchmark +// pallet +// --runtime +// /home/llb/Repos/deos/template/target/benchmarks/release/wbuild/deos-runtime/deos_runtime.compact.compressed.wasm +// --pallet +// pallet_deos_actors +// --extrinsic +// crossing_placed_non_tail_emptied_unit +// --steps +// 50 +// --repeat +// 20 +// --heap-pages +// 4096 +// --output +// /tmp/deos-exp-0008/weights/p128-crossing_placed_non_tail_emptied_unit.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions for `pallet_deos_actors`. +pub struct WeightInfo(PhantomData); +impl pallet_deos_actors::WeightInfo for WeightInfo { + /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:1) + /// Proof: `Actors::CrossingPendingFeedListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingTransitionQueues` (r:1 w:0) + /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(3171), added: 5646, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingRangeCursors` (r:1 w:1) + /// Proof: `Actors::CrossingRangeCursors` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingLeafStates` (r:2 w:2) + /// Proof: `Actors::CrossingLeafStates` (`max_values`: None, `max_size`: Some(62), added: 2537, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingMemberPages` (r:3 w:3) + /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(2120), added: 4595, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingMemberships` (r:8 w:132) + /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:4 w:4) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorHot` (r:4 w:4) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:4 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:4 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorIdentities` (r:4 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorFunding` (r:4 w:0) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:4 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) + /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + /// Storage: `Actors::NextQueueTicket` (r:1 w:1) + /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueHead` (r:1 w:0) + /// Proof: `Actors::QueueHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueTail` (r:1 w:1) + /// Proof: `Actors::QueueTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueOccupancy` (r:1 w:1) + /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueuePages` (r:1 w:1) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingFeedMembershipCount` (r:1 w:1) + /// Proof: `Actors::CrossingFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingUserFeedMembershipCount` (r:1 w:1) + /// Proof: `Actors::CrossingUserFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingRadixNodes` (r:32 w:32) + /// Proof: `Actors::CrossingRadixNodes` (`max_values`: None, `max_size`: Some(53), added: 2528, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingPendingFeeds` (r:1 w:0) + /// Proof: `Actors::CrossingPendingFeeds` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) + fn crossing_placed_non_tail_emptied_unit() -> Weight { + // Proof Size summary in bytes: + // Measured: `16754` + // Estimated: `81886` + // Minimum execution time: 906_624_000 picoseconds. + Weight::from_parts(944_967_000, 0) + .saturating_add(Weight::from_parts(0, 81886)) + .saturating_add(T::DbWeight::get().reads(87)) + .saturating_add(T::DbWeight::get().writes(187)) + } +} diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0008/p128-crossing_placed_non_tail_trimmed_unit.rs b/.agents/skills/architecture-experiments/tracks/actors/EXP-0008/p128-crossing_placed_non_tail_trimmed_unit.rs new file mode 100644 index 00000000..fd780f57 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0008/p128-crossing_placed_non_tail_trimmed_unit.rs @@ -0,0 +1,99 @@ + +//! Autogenerated weights for `pallet_deos_actors` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 58.0.0 +//! DATE: 2026-08-29, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `fedora`, CPU: `AMD Ryzen 7 4800H with Radeon Graphics` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 + +// Executed Command: +// frame-omni-bencher +// v1 +// benchmark +// pallet +// --runtime +// /home/llb/Repos/deos/template/target/benchmarks/release/wbuild/deos-runtime/deos_runtime.compact.compressed.wasm +// --pallet +// pallet_deos_actors +// --extrinsic +// crossing_placed_non_tail_trimmed_unit +// --steps +// 50 +// --repeat +// 20 +// --heap-pages +// 4096 +// --output +// /tmp/deos-exp-0008/weights/p128-crossing_placed_non_tail_trimmed_unit.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions for `pallet_deos_actors`. +pub struct WeightInfo(PhantomData); +impl pallet_deos_actors::WeightInfo for WeightInfo { + /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:1) + /// Proof: `Actors::CrossingPendingFeedListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingTransitionQueues` (r:1 w:0) + /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(3171), added: 5646, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingRangeCursors` (r:1 w:1) + /// Proof: `Actors::CrossingRangeCursors` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingLeafStates` (r:2 w:2) + /// Proof: `Actors::CrossingLeafStates` (`max_values`: None, `max_size`: Some(62), added: 2537, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingMemberPages` (r:3 w:3) + /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(2120), added: 4595, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingMemberships` (r:8 w:134) + /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:4 w:4) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorHot` (r:4 w:4) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:4 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:4 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorIdentities` (r:4 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorFunding` (r:4 w:0) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:4 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) + /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + /// Storage: `Actors::NextQueueTicket` (r:1 w:1) + /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueHead` (r:1 w:0) + /// Proof: `Actors::QueueHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueTail` (r:1 w:1) + /// Proof: `Actors::QueueTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueOccupancy` (r:1 w:1) + /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueuePages` (r:1 w:1) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingFeedMembershipCount` (r:1 w:1) + /// Proof: `Actors::CrossingFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingUserFeedMembershipCount` (r:1 w:1) + /// Proof: `Actors::CrossingUserFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingRadixNodes` (r:32 w:32) + /// Proof: `Actors::CrossingRadixNodes` (`max_values`: None, `max_size`: Some(53), added: 2528, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingPendingFeeds` (r:1 w:0) + /// Proof: `Actors::CrossingPendingFeeds` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) + fn crossing_placed_non_tail_trimmed_unit() -> Weight { + // Proof Size summary in bytes: + // Measured: `17108` + // Estimated: `81886` + // Minimum execution time: 926_320_000 picoseconds. + Weight::from_parts(960_123_000, 0) + .saturating_add(Weight::from_parts(0, 81886)) + .saturating_add(T::DbWeight::get().reads(87)) + .saturating_add(T::DbWeight::get().writes(189)) + } +} diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0008/p128-crossing_tail_refill_probe.rs b/.agents/skills/architecture-experiments/tracks/actors/EXP-0008/p128-crossing_tail_refill_probe.rs new file mode 100644 index 00000000..bdde3179 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0008/p128-crossing_tail_refill_probe.rs @@ -0,0 +1,52 @@ + +//! Autogenerated weights for `pallet_deos_actors` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 58.0.0 +//! DATE: 2026-08-29, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `fedora`, CPU: `AMD Ryzen 7 4800H with Radeon Graphics` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 + +// Executed Command: +// frame-omni-bencher +// v1 +// benchmark +// pallet +// --runtime +// /home/llb/Repos/deos/template/target/benchmarks/release/wbuild/deos-runtime/deos_runtime.compact.compressed.wasm +// --pallet +// pallet_deos_actors +// --extrinsic +// crossing_tail_refill_probe +// --steps +// 50 +// --repeat +// 20 +// --heap-pages +// 4096 +// --output +// /tmp/deos-exp-0008/weights/p128-crossing_tail_refill_probe.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions for `pallet_deos_actors`. +pub struct WeightInfo(PhantomData); +impl pallet_deos_actors::WeightInfo for WeightInfo { + /// Storage: `Actors::CrossingMemberPages` (r:1 w:0) + /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(2120), added: 4595, mode: `MaxEncodedLen`) + fn crossing_tail_refill_probe() -> Weight { + // Proof Size summary in bytes: + // Measured: `771` + // Estimated: `5585` + // Minimum execution time: 16_972_000 picoseconds. + Weight::from_parts(18_928_000, 0) + .saturating_add(Weight::from_parts(0, 5585)) + .saturating_add(T::DbWeight::get().reads(1)) + } +} diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0008/p256-crossing_placed_maximum_unit.rs b/.agents/skills/architecture-experiments/tracks/actors/EXP-0008/p256-crossing_placed_maximum_unit.rs new file mode 100644 index 00000000..742f3510 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0008/p256-crossing_placed_maximum_unit.rs @@ -0,0 +1,99 @@ + +//! Autogenerated weights for `pallet_deos_actors` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 58.0.0 +//! DATE: 2026-08-29, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `fedora`, CPU: `AMD Ryzen 7 4800H with Radeon Graphics` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 + +// Executed Command: +// frame-omni-bencher +// v1 +// benchmark +// pallet +// --runtime +// /home/llb/Repos/deos/template/target/benchmarks/release/wbuild/deos-runtime/deos_runtime.compact.compressed.wasm +// --pallet +// pallet_deos_actors +// --extrinsic +// crossing_placed_maximum_unit +// --steps +// 50 +// --repeat +// 20 +// --heap-pages +// 4096 +// --output +// /tmp/deos-exp-0008/weights/p256-crossing_placed_maximum_unit.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions for `pallet_deos_actors`. +pub struct WeightInfo(PhantomData); +impl pallet_deos_actors::WeightInfo for WeightInfo { + /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:1) + /// Proof: `Actors::CrossingPendingFeedListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingTransitionQueues` (r:1 w:0) + /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(3171), added: 5646, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingRangeCursors` (r:1 w:1) + /// Proof: `Actors::CrossingRangeCursors` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingLeafStates` (r:2 w:2) + /// Proof: `Actors::CrossingLeafStates` (`max_values`: None, `max_size`: Some(62), added: 2537, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingMemberPages` (r:2 w:2) + /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(4168), added: 6643, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingMemberships` (r:8 w:256) + /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:4 w:4) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorHot` (r:4 w:4) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:4 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:4 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorIdentities` (r:4 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorFunding` (r:4 w:0) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:4 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) + /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + /// Storage: `Actors::NextQueueTicket` (r:1 w:1) + /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueHead` (r:1 w:0) + /// Proof: `Actors::QueueHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueTail` (r:1 w:1) + /// Proof: `Actors::QueueTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueOccupancy` (r:1 w:1) + /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueuePages` (r:1 w:1) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:5 w:5) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingFeedMembershipCount` (r:1 w:1) + /// Proof: `Actors::CrossingFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingUserFeedMembershipCount` (r:1 w:1) + /// Proof: `Actors::CrossingUserFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingRadixNodes` (r:32 w:32) + /// Proof: `Actors::CrossingRadixNodes` (`max_values`: None, `max_size`: Some(53), added: 2528, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingPendingFeeds` (r:1 w:0) + /// Proof: `Actors::CrossingPendingFeeds` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) + fn crossing_placed_maximum_unit() -> Weight { + // Proof Size summary in bytes: + // Measured: `23372` + // Estimated: `81886` + // Minimum execution time: 1_365_699_000 picoseconds. + Weight::from_parts(1_398_176_000, 0) + .saturating_add(Weight::from_parts(0, 81886)) + .saturating_add(T::DbWeight::get().reads(89)) + .saturating_add(T::DbWeight::get().writes(313)) + } +} diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0008/p256-crossing_placed_non_tail_emptied_unit.rs b/.agents/skills/architecture-experiments/tracks/actors/EXP-0008/p256-crossing_placed_non_tail_emptied_unit.rs new file mode 100644 index 00000000..a0f1dd6b --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0008/p256-crossing_placed_non_tail_emptied_unit.rs @@ -0,0 +1,99 @@ + +//! Autogenerated weights for `pallet_deos_actors` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 58.0.0 +//! DATE: 2026-08-29, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `fedora`, CPU: `AMD Ryzen 7 4800H with Radeon Graphics` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 + +// Executed Command: +// frame-omni-bencher +// v1 +// benchmark +// pallet +// --runtime +// /home/llb/Repos/deos/template/target/benchmarks/release/wbuild/deos-runtime/deos_runtime.compact.compressed.wasm +// --pallet +// pallet_deos_actors +// --extrinsic +// crossing_placed_non_tail_emptied_unit +// --steps +// 50 +// --repeat +// 20 +// --heap-pages +// 4096 +// --output +// /tmp/deos-exp-0008/weights/p256-crossing_placed_non_tail_emptied_unit.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions for `pallet_deos_actors`. +pub struct WeightInfo(PhantomData); +impl pallet_deos_actors::WeightInfo for WeightInfo { + /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:1) + /// Proof: `Actors::CrossingPendingFeedListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingTransitionQueues` (r:1 w:0) + /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(3171), added: 5646, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingRangeCursors` (r:1 w:1) + /// Proof: `Actors::CrossingRangeCursors` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingLeafStates` (r:2 w:2) + /// Proof: `Actors::CrossingLeafStates` (`max_values`: None, `max_size`: Some(62), added: 2537, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingMemberPages` (r:3 w:3) + /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(4168), added: 6643, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingMemberships` (r:8 w:260) + /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:4 w:4) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorHot` (r:4 w:4) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:4 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:4 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorIdentities` (r:4 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorFunding` (r:4 w:0) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:4 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) + /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + /// Storage: `Actors::NextQueueTicket` (r:1 w:1) + /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueHead` (r:1 w:0) + /// Proof: `Actors::QueueHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueTail` (r:1 w:1) + /// Proof: `Actors::QueueTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueOccupancy` (r:1 w:1) + /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueuePages` (r:1 w:1) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingFeedMembershipCount` (r:1 w:1) + /// Proof: `Actors::CrossingFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingUserFeedMembershipCount` (r:1 w:1) + /// Proof: `Actors::CrossingUserFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingRadixNodes` (r:32 w:32) + /// Proof: `Actors::CrossingRadixNodes` (`max_values`: None, `max_size`: Some(53), added: 2528, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingPendingFeeds` (r:1 w:0) + /// Proof: `Actors::CrossingPendingFeeds` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) + fn crossing_placed_non_tail_emptied_unit() -> Weight { + // Proof Size summary in bytes: + // Measured: `22814` + // Estimated: `81886` + // Minimum execution time: 1_212_185_000 picoseconds. + Weight::from_parts(1_241_029_000, 0) + .saturating_add(Weight::from_parts(0, 81886)) + .saturating_add(T::DbWeight::get().reads(87)) + .saturating_add(T::DbWeight::get().writes(315)) + } +} diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0008/p256-crossing_placed_non_tail_trimmed_unit.rs b/.agents/skills/architecture-experiments/tracks/actors/EXP-0008/p256-crossing_placed_non_tail_trimmed_unit.rs new file mode 100644 index 00000000..552a5879 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0008/p256-crossing_placed_non_tail_trimmed_unit.rs @@ -0,0 +1,99 @@ + +//! Autogenerated weights for `pallet_deos_actors` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 58.0.0 +//! DATE: 2026-08-29, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `fedora`, CPU: `AMD Ryzen 7 4800H with Radeon Graphics` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 + +// Executed Command: +// frame-omni-bencher +// v1 +// benchmark +// pallet +// --runtime +// /home/llb/Repos/deos/template/target/benchmarks/release/wbuild/deos-runtime/deos_runtime.compact.compressed.wasm +// --pallet +// pallet_deos_actors +// --extrinsic +// crossing_placed_non_tail_trimmed_unit +// --steps +// 50 +// --repeat +// 20 +// --heap-pages +// 4096 +// --output +// /tmp/deos-exp-0008/weights/p256-crossing_placed_non_tail_trimmed_unit.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions for `pallet_deos_actors`. +pub struct WeightInfo(PhantomData); +impl pallet_deos_actors::WeightInfo for WeightInfo { + /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:1) + /// Proof: `Actors::CrossingPendingFeedListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingTransitionQueues` (r:1 w:0) + /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(3171), added: 5646, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingRangeCursors` (r:1 w:1) + /// Proof: `Actors::CrossingRangeCursors` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingLeafStates` (r:2 w:2) + /// Proof: `Actors::CrossingLeafStates` (`max_values`: None, `max_size`: Some(62), added: 2537, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingMemberPages` (r:3 w:3) + /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(4168), added: 6643, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingMemberships` (r:8 w:262) + /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:4 w:4) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorHot` (r:4 w:4) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:4 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:4 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorIdentities` (r:4 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorFunding` (r:4 w:0) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:4 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) + /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + /// Storage: `Actors::NextQueueTicket` (r:1 w:1) + /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueHead` (r:1 w:0) + /// Proof: `Actors::QueueHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueTail` (r:1 w:1) + /// Proof: `Actors::QueueTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueOccupancy` (r:1 w:1) + /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueuePages` (r:1 w:1) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingFeedMembershipCount` (r:1 w:1) + /// Proof: `Actors::CrossingFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingUserFeedMembershipCount` (r:1 w:1) + /// Proof: `Actors::CrossingUserFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingRadixNodes` (r:32 w:32) + /// Proof: `Actors::CrossingRadixNodes` (`max_values`: None, `max_size`: Some(53), added: 2528, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingPendingFeeds` (r:1 w:0) + /// Proof: `Actors::CrossingPendingFeeds` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) + fn crossing_placed_non_tail_trimmed_unit() -> Weight { + // Proof Size summary in bytes: + // Measured: `22812` + // Estimated: `81886` + // Minimum execution time: 1_213_512_000 picoseconds. + Weight::from_parts(1_244_871_000, 0) + .saturating_add(Weight::from_parts(0, 81886)) + .saturating_add(T::DbWeight::get().reads(87)) + .saturating_add(T::DbWeight::get().writes(317)) + } +} diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0008/p256-crossing_tail_refill_probe.rs b/.agents/skills/architecture-experiments/tracks/actors/EXP-0008/p256-crossing_tail_refill_probe.rs new file mode 100644 index 00000000..5e875fc0 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0008/p256-crossing_tail_refill_probe.rs @@ -0,0 +1,52 @@ + +//! Autogenerated weights for `pallet_deos_actors` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 58.0.0 +//! DATE: 2026-08-29, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `fedora`, CPU: `AMD Ryzen 7 4800H with Radeon Graphics` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 + +// Executed Command: +// frame-omni-bencher +// v1 +// benchmark +// pallet +// --runtime +// /home/llb/Repos/deos/template/target/benchmarks/release/wbuild/deos-runtime/deos_runtime.compact.compressed.wasm +// --pallet +// pallet_deos_actors +// --extrinsic +// crossing_tail_refill_probe +// --steps +// 50 +// --repeat +// 20 +// --heap-pages +// 4096 +// --output +// /tmp/deos-exp-0008/weights/p256-crossing_tail_refill_probe.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions for `pallet_deos_actors`. +pub struct WeightInfo(PhantomData); +impl pallet_deos_actors::WeightInfo for WeightInfo { + /// Storage: `Actors::CrossingMemberPages` (r:1 w:0) + /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(4168), added: 6643, mode: `MaxEncodedLen`) + fn crossing_tail_refill_probe() -> Weight { + // Proof Size summary in bytes: + // Measured: `771` + // Estimated: `7633` + // Minimum execution time: 18_019_000 picoseconds. + Weight::from_parts(19_625_000, 0) + .saturating_add(Weight::from_parts(0, 7633)) + .saturating_add(T::DbWeight::get().reads(1)) + } +} diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0008/p64-crossing_placed_maximum_unit.rs b/.agents/skills/architecture-experiments/tracks/actors/EXP-0008/p64-crossing_placed_maximum_unit.rs new file mode 100644 index 00000000..cf178177 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0008/p64-crossing_placed_maximum_unit.rs @@ -0,0 +1,99 @@ + +//! Autogenerated weights for `pallet_deos_actors` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 58.0.0 +//! DATE: 2026-08-29, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `fedora`, CPU: `AMD Ryzen 7 4800H with Radeon Graphics` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 + +// Executed Command: +// frame-omni-bencher +// v1 +// benchmark +// pallet +// --runtime +// /home/llb/Repos/deos/template/target/benchmarks/release/wbuild/deos-runtime/deos_runtime.compact.compressed.wasm +// --pallet +// pallet_deos_actors +// --extrinsic +// crossing_placed_maximum_unit +// --steps +// 50 +// --repeat +// 20 +// --heap-pages +// 4096 +// --output +// /tmp/deos-exp-0008/weights/p64-crossing_placed_maximum_unit.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions for `pallet_deos_actors`. +pub struct WeightInfo(PhantomData); +impl pallet_deos_actors::WeightInfo for WeightInfo { + /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:1) + /// Proof: `Actors::CrossingPendingFeedListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingTransitionQueues` (r:1 w:0) + /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(3171), added: 5646, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingRangeCursors` (r:1 w:1) + /// Proof: `Actors::CrossingRangeCursors` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingLeafStates` (r:2 w:2) + /// Proof: `Actors::CrossingLeafStates` (`max_values`: None, `max_size`: Some(62), added: 2537, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingMemberPages` (r:2 w:2) + /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(1096), added: 3571, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingMemberships` (r:8 w:64) + /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:4 w:4) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorHot` (r:4 w:4) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:4 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:4 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorIdentities` (r:4 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorFunding` (r:4 w:0) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:4 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) + /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + /// Storage: `Actors::NextQueueTicket` (r:1 w:1) + /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueHead` (r:1 w:0) + /// Proof: `Actors::QueueHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueTail` (r:1 w:1) + /// Proof: `Actors::QueueTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueOccupancy` (r:1 w:1) + /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueuePages` (r:1 w:1) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:5 w:5) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingFeedMembershipCount` (r:1 w:1) + /// Proof: `Actors::CrossingFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingUserFeedMembershipCount` (r:1 w:1) + /// Proof: `Actors::CrossingUserFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingRadixNodes` (r:32 w:32) + /// Proof: `Actors::CrossingRadixNodes` (`max_values`: None, `max_size`: Some(53), added: 2528, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingPendingFeeds` (r:1 w:0) + /// Proof: `Actors::CrossingPendingFeeds` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) + fn crossing_placed_maximum_unit() -> Weight { + // Proof Size summary in bytes: + // Measured: `13537` + // Estimated: `81886` + // Minimum execution time: 973_882_000 picoseconds. + Weight::from_parts(1_001_051_000, 0) + .saturating_add(Weight::from_parts(0, 81886)) + .saturating_add(T::DbWeight::get().reads(89)) + .saturating_add(T::DbWeight::get().writes(121)) + } +} diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0008/p64-crossing_placed_non_tail_emptied_unit.rs b/.agents/skills/architecture-experiments/tracks/actors/EXP-0008/p64-crossing_placed_non_tail_emptied_unit.rs new file mode 100644 index 00000000..14c8fb9d --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0008/p64-crossing_placed_non_tail_emptied_unit.rs @@ -0,0 +1,99 @@ + +//! Autogenerated weights for `pallet_deos_actors` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 58.0.0 +//! DATE: 2026-08-29, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `fedora`, CPU: `AMD Ryzen 7 4800H with Radeon Graphics` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 + +// Executed Command: +// frame-omni-bencher +// v1 +// benchmark +// pallet +// --runtime +// /home/llb/Repos/deos/template/target/benchmarks/release/wbuild/deos-runtime/deos_runtime.compact.compressed.wasm +// --pallet +// pallet_deos_actors +// --extrinsic +// crossing_placed_non_tail_emptied_unit +// --steps +// 50 +// --repeat +// 20 +// --heap-pages +// 4096 +// --output +// /tmp/deos-exp-0008/weights/p64-crossing_placed_non_tail_emptied_unit.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions for `pallet_deos_actors`. +pub struct WeightInfo(PhantomData); +impl pallet_deos_actors::WeightInfo for WeightInfo { + /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:1) + /// Proof: `Actors::CrossingPendingFeedListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingTransitionQueues` (r:1 w:0) + /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(3171), added: 5646, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingRangeCursors` (r:1 w:1) + /// Proof: `Actors::CrossingRangeCursors` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingLeafStates` (r:2 w:2) + /// Proof: `Actors::CrossingLeafStates` (`max_values`: None, `max_size`: Some(62), added: 2537, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingMemberPages` (r:3 w:3) + /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(1096), added: 3571, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingMemberships` (r:8 w:68) + /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:4 w:4) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorHot` (r:4 w:4) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:4 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:4 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorIdentities` (r:4 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorFunding` (r:4 w:0) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:4 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) + /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + /// Storage: `Actors::NextQueueTicket` (r:1 w:1) + /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueHead` (r:1 w:0) + /// Proof: `Actors::QueueHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueTail` (r:1 w:1) + /// Proof: `Actors::QueueTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueOccupancy` (r:1 w:1) + /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueuePages` (r:1 w:1) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingFeedMembershipCount` (r:1 w:1) + /// Proof: `Actors::CrossingFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingUserFeedMembershipCount` (r:1 w:1) + /// Proof: `Actors::CrossingUserFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingRadixNodes` (r:32 w:32) + /// Proof: `Actors::CrossingRadixNodes` (`max_values`: None, `max_size`: Some(53), added: 2528, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingPendingFeeds` (r:1 w:0) + /// Proof: `Actors::CrossingPendingFeeds` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) + fn crossing_placed_non_tail_emptied_unit() -> Weight { + // Proof Size summary in bytes: + // Measured: `13056` + // Estimated: `81886` + // Minimum execution time: 761_282_000 picoseconds. + Weight::from_parts(794_248_000, 0) + .saturating_add(Weight::from_parts(0, 81886)) + .saturating_add(T::DbWeight::get().reads(87)) + .saturating_add(T::DbWeight::get().writes(123)) + } +} diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0008/p64-crossing_placed_non_tail_trimmed_unit.rs b/.agents/skills/architecture-experiments/tracks/actors/EXP-0008/p64-crossing_placed_non_tail_trimmed_unit.rs new file mode 100644 index 00000000..9c62be47 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0008/p64-crossing_placed_non_tail_trimmed_unit.rs @@ -0,0 +1,99 @@ + +//! Autogenerated weights for `pallet_deos_actors` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 58.0.0 +//! DATE: 2026-08-29, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `fedora`, CPU: `AMD Ryzen 7 4800H with Radeon Graphics` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 + +// Executed Command: +// frame-omni-bencher +// v1 +// benchmark +// pallet +// --runtime +// /home/llb/Repos/deos/template/target/benchmarks/release/wbuild/deos-runtime/deos_runtime.compact.compressed.wasm +// --pallet +// pallet_deos_actors +// --extrinsic +// crossing_placed_non_tail_trimmed_unit +// --steps +// 50 +// --repeat +// 20 +// --heap-pages +// 4096 +// --output +// /tmp/deos-exp-0008/weights/p64-crossing_placed_non_tail_trimmed_unit.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions for `pallet_deos_actors`. +pub struct WeightInfo(PhantomData); +impl pallet_deos_actors::WeightInfo for WeightInfo { + /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:1) + /// Proof: `Actors::CrossingPendingFeedListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingTransitionQueues` (r:1 w:0) + /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(3171), added: 5646, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingRangeCursors` (r:1 w:1) + /// Proof: `Actors::CrossingRangeCursors` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingLeafStates` (r:2 w:2) + /// Proof: `Actors::CrossingLeafStates` (`max_values`: None, `max_size`: Some(62), added: 2537, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingMemberPages` (r:3 w:3) + /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(1096), added: 3571, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingMemberships` (r:8 w:70) + /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:4 w:4) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorHot` (r:4 w:4) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:4 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:4 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorIdentities` (r:4 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorFunding` (r:4 w:0) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:4 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) + /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + /// Storage: `Actors::NextQueueTicket` (r:1 w:1) + /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueHead` (r:1 w:0) + /// Proof: `Actors::QueueHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueTail` (r:1 w:1) + /// Proof: `Actors::QueueTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueOccupancy` (r:1 w:1) + /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueuePages` (r:1 w:1) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingFeedMembershipCount` (r:1 w:1) + /// Proof: `Actors::CrossingFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingUserFeedMembershipCount` (r:1 w:1) + /// Proof: `Actors::CrossingUserFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingRadixNodes` (r:32 w:32) + /// Proof: `Actors::CrossingRadixNodes` (`max_values`: None, `max_size`: Some(53), added: 2528, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingPendingFeeds` (r:1 w:0) + /// Proof: `Actors::CrossingPendingFeeds` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) + fn crossing_placed_non_tail_trimmed_unit() -> Weight { + // Proof Size summary in bytes: + // Measured: `13315` + // Estimated: `81886` + // Minimum execution time: 758_628_000 picoseconds. + Weight::from_parts(791_174_000, 0) + .saturating_add(Weight::from_parts(0, 81886)) + .saturating_add(T::DbWeight::get().reads(87)) + .saturating_add(T::DbWeight::get().writes(125)) + } +} diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0008/p64-crossing_tail_refill_probe.rs b/.agents/skills/architecture-experiments/tracks/actors/EXP-0008/p64-crossing_tail_refill_probe.rs new file mode 100644 index 00000000..0d80c2f8 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0008/p64-crossing_tail_refill_probe.rs @@ -0,0 +1,52 @@ + +//! Autogenerated weights for `pallet_deos_actors` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 58.0.0 +//! DATE: 2026-08-29, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `fedora`, CPU: `AMD Ryzen 7 4800H with Radeon Graphics` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 + +// Executed Command: +// frame-omni-bencher +// v1 +// benchmark +// pallet +// --runtime +// /home/llb/Repos/deos/template/target/benchmarks/release/wbuild/deos-runtime/deos_runtime.compact.compressed.wasm +// --pallet +// pallet_deos_actors +// --extrinsic +// crossing_tail_refill_probe +// --steps +// 50 +// --repeat +// 20 +// --heap-pages +// 4096 +// --output +// /tmp/deos-exp-0008/weights/p64-crossing_tail_refill_probe.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions for `pallet_deos_actors`. +pub struct WeightInfo(PhantomData); +impl pallet_deos_actors::WeightInfo for WeightInfo { + /// Storage: `Actors::CrossingMemberPages` (r:1 w:0) + /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(1096), added: 3571, mode: `MaxEncodedLen`) + fn crossing_tail_refill_probe() -> Weight { + // Proof Size summary in bytes: + // Measured: `771` + // Estimated: `4561` + // Minimum execution time: 15_086_000 picoseconds. + Weight::from_parts(17_181_000, 0) + .saturating_add(Weight::from_parts(0, 4561)) + .saturating_add(T::DbWeight::get().reads(1)) + } +} diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0009.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0009.md new file mode 100644 index 00000000..ce769deb --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0009.md @@ -0,0 +1,179 @@ +# EXP-0009 — Crossing Production Cohort + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Architecture release / experiment campaign | DEOS 0.7.24 | +| Date | 2026-08-29 | +| Affected domain | DEOS Actors ObservationCrossing materialization | +| Physical mechanism | Contiguous causal cohort classification and grouped FIFO/rearm mutation | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | EXP-0008 | +| Successor experiment | None | + +## Decision Question + +Does a P128 page with a generated 128-candidate Crossing cohort fit every complete branch inside the component-wise Actor Control and worker envelopes while preserving exact state and 10,000-membership capacity? + +## Governing Specification + +- `Exact section`: `template/pallets/actors/docs/specification.en.md` → `3.5 ObservationCrossing`, `15.2.9 Production Bounds`; `docs/actors-resource-policy.specification.en.md` → Actor Control allocation; `docs/actors-performance-assurance.specification.en.md` → Crossing production evidence. +- `Invariant`: One Trigger instance may materialize one bounded contiguous initial cohort without changing strict FIFO, Q1, N+1 eligibility, class neutrality, useful-transition latching, exact non-tail refill, or fault isolation. +- `Reopen trigger`: Meeting the target requires priority, same-block execution, relaxed rollback, multiple committed Steps, unbounded traversal, or changed Trigger semantics. + +## Context and Hypothesis + +- `Current architecture`: Production P64 admits four homogeneous candidates per generated cohort and falls back to scalar service on mixed authority. EXP-0008 shows P128 is the smallest page capable of one-page 128-candidate snapshots; P256 is materially more expensive for the required frontier. +- `Reason`: The release requires at least 128 generated candidates when the complete branch fits and at least 10,000 admitted feed memberships. +- `Hypothesis`: Existing contiguous snapshot, batch preflight, grouped queue append, and exact tail-refill topology scales parametrically to 128 without semantic redesign; generated slopes expose whether Actor Control, ProofSize, writes, or queue admission binds. +- `Materiality`: C128 must fit every reachable complete branch component-wise and retain positive Actor execution floor. A preflight-only win is insufficient. + +## Baseline and Candidates + +| ID | Architecture | Source tree | Runtime/config identity | +| --- | --- | --- | --- | +| C4-P64 | Current production cohort | HEAD `cbc5a2b9ed32f385c6d3cb19c1e673ff476b1e73`; current dirty source | `ObservationPageSize=64`, `MaxCrossingActorsPerBlock=4` | +| C128-P128-W10 | Same topology with target cohort and current worker ceiling | Shared feature-gated source | `exp-crossing-cohort-128` implies P128 and sets cohort/benchmark range to 128; Crossing ceiling remains 10% | +| C128-P128-W14 | Same cohort with a wider local Crossing ceiling | Rejected before build | A 14% local ceiling is `280,000,000,000 / 700,000`, exceeding Actor Control `181,083,640,945 / 405,690` | +| C128-P128-H0 | Idle fast path over compiled detector authority without repeated Contract-head reads | Measured in feature-gated source | All preflights and tail C128 fit; non-tail double-locator proof fails | +| C128-P128-H0-N64 | Branch-sensitive exact non-tail cap | Measured in feature-gated source | Keep C128 tail/preflight; clamp non-tail source plus physical-tail refill to 64 candidates | + +## Controls + +- `Controlled variables`: Toolchain, compiled-Wasm benchmark method, database/cache, 50 steps, 20 repeats, radix and transition topology, Trigger semantics, actor class mix, contracts, queue state, feed population, worker envelope, and all non-page/cohort runtime constants. +- `Changed variables`: Observation page bound 64 → 128, cohort bound 4 → 128, matching benchmark component range 4 → 128, and for H0 only the physical owner of paired Crossing routing plus Idle activation inputs. +- `Confounders`: Page and cohort bounds move together because P64 cannot represent a 128-entry contiguous page cohort. EXP-0008 independently measured the page-only cost, allowing the cohort increment to be interpreted against P128 page evidence. + +## Benchmark Class, Workloads, and Criteria + +- `Benchmark class`: FRAME generated Weight under compiled Wasm, deterministic package/runtime integration profiles, and bounded native 10,000-membership setup outside measured blocks. + +| Workload | State/population geometry | Hot/cold assumptions | Purpose | +| --- | --- | --- | --- | +| W1 Fire preflight | Homogeneous 1..128 candidates | Required pages and Actor heads cold | Measure classification slope and maximum authority | +| W2 Coalesced/rearm/terminal/skip preflight | Homogeneous 1..128 per branch | Branch-required state cold | Prevent placed-only optimization | +| W3 Placed commit | 128 queueable candidates at tail | Queue pages and candidate state cold | Prove grouped FIFO and complete mutation fit | +| W4 Non-tail commit | 128-source prefix plus physical-tail refill | Source/tail pages cold | Prove exact compaction and locator repair | +| W5 Mixed/fault boundary | Mismatch/corruption at first, middle, final candidate | Required authority cold | Preserve scalar fallback and rollback | +| W5A H0 authority | Idle homogeneous cohort plus Running/Suspended/mismatched authority at first/middle/final positions | Contract heads remain cold and unread on retained Idle prefix | Prove exact fast-path boundary and generic fallback | +| W6 Capacity | 10,000 total / 9,000 User memberships | Prefunded deterministic setup outside measurement | Prove admission and convergence without loss | + +- `Metrics`: Parameterized RefTime/ProofSize, reads, writes, maximum branch Weight, Actor Control fit, worker-envelope fit, queue placements, exact roots, faults, Wasm bytes, and completion blocks. +- `Statistical method`: Generate candidate owners at 50 steps/20 repeats against the matching benchmark Wasm and inspect intercept/slope in both dimensions. Deterministic state tests compare exact roots and event/order evidence; no samples are discarded. +- `Acceptance criteria`: C128 fits every complete branch component-wise, preserves positive Actor execution floor and exact semantics, admits 10,000 memberships, and produces sound parameterized Weight through 128. +- `Rejection criteria`: Any envelope overflow, missing actual owner, invalid generated slope, queue/capacity failure, stale locator, partial mutation, semantic regression, or inability to complete the declared maximum profile. + +## Environment and Artifact Identity + +| Artifact | Identity | +| --- | --- | +| Source commit | `cbc5a2b9ed32f385c6d3cb19c1e673ff476b1e73` | +| Source tree | Feature-gated candidate in `template/{pallets/actors,runtime}`; exact hashes pending candidate build | +| Rust toolchain | `rustc 1.94.1 (e408947bf 2026-03-25)`; `cargo 1.94.1 (29ea6fb6a 2026-03-24)` | +| Benchmark runtime Wasm | Corrected complete-fixture C128-P128 `0e4c8bd54ed2fa93655e00e16c8ee347338e11b2eab4609debfa5fa8c84b3407` (6,089,773 bytes); earlier preflight-only identity remains recorded in measurement history | +| Post-generation production runtime Wasm | `9cf6cd5966d044aafe1563b43dbe49b2ff896c4cf557feac53eb61284c076fcd` (P128 Crossing/P64 broad split-page production profile, about 1.4 MiB) | +| Final release production runtime Wasm | Pending exact release tree | +| Generated Weight | Full `pallet_deos_actors` production file regenerated at 50 steps / 20 repeats after candidate selection | +| Database/backend | FRAME compiled Wasm, database cache 1024 | +| Benchmark command/config | Matching `runtime-benchmarks,exp-crossing-cohort-128` build; 50 steps / 20 repeats / 4,096 heap pages | +| Raw evidence | Baseline/pre-H0, intermediate H0/N64, and eight final H0-N64 owners retained under sibling [`EXP-0009/`](./EXP-0009/); current path-sorted manifest digest `c31bb6436266d0f65a79648811e7f5dc82e20cd569a1de8a4303b672a79dadec` | + +## Measurements + +| Candidate | Workload | RefTime | ProofSize | Reads | Writes | Other decision metrics | +| --- | --- | ---: | ---: | ---: | ---: | --- | +| C4-P64 | W1-W6 | Existing production evidence | Existing production evidence | Existing production evidence | Existing production evidence | Four-candidate baseline | +| C128-P128 | Fire preflight at 128 | 5,442,139,736 + DB | 608,981 | 1,026 | 0 | Generated slope 41,988,537 / 4,746 / 8 reads per candidate | +| C128-P128 | Coalesced preflight at 128 | 5,465,575,688 + DB | 608,981 | 1,026 | 0 | Generated slope 42,144,896 / 4,746 / 8 reads per candidate | +| C128-P128 | Terminal preflight at 128 | 5,480,760,592 + DB | 608,981 | 1,026 | 0 | Binding measured preflight RefTime | +| C128-P128 | Rearm preflight at 128 | 2,733,628,713 + DB | 608,478 | 640 | 0 | Generated slope 21,243,593 / 4,746 / 5 reads per candidate | +| C128-P128 | Skip preflight at 128 | 1,214,577,552 + DB | 331,614 | 384 | 0 | Generated slope 9,345,934 / 2,583 / 3 reads per candidate | +| C128-P128-W10 | Tail placed commit at 128 | 12,505,842,000 + DB | 608,478 | 1,110 | 466 | Complete grouped placement succeeds but exceeds 10% ProofSize ceiling | +| C128-P128-W10 | Non-tail emptied commit at 128 | 10,498,152,000 + DB | 653,534 | 1,204 | 560 | Binding ProofSize exceeds 500,000 ceiling by 153,534 | +| C128-P128-W10 | Non-tail trimmed commit at 126 | 10,530,838,000 + DB | 643,338 | 1,186 | 556 | Maximum reachable trimmed branch; 128 empties a 128-entry tail | +| C128-P128-W10 | W5-W6 | Not measured | Not measured | Not measured | Not measured | Mixed/fault and 10,000-member exact-root evidence pending | +| C128-P128-W14 | Envelope fit | Rejected | Rejected | Rejected | Rejected | Local widening exceeds authoritative Actor Control in both dimensions | +| C128-P128-H0 | Paired routing authority | Behavioral test passed | Feature-gated page field | Existing insertion/move writes | Existing page writes | Fire member stores rearm threshold; moved member stores fire threshold | +| C128-P128-H0 | Compact Idle authority | Behavioral test passed | Contract head not loaded by helper | Identity/hot/authority/certificate reads | No writes | Rejects non-Idle, run-head presence, wrong Trigger state, feed mismatch, or commitment mismatch | +| C128-P128-H0 final | Fire / coalesced / terminal preflight at 128 | 3,104,325,008 / 3,113,569,584 / 3,045,425,392 + DB | 346,965 | 898 | 0 | All fit Actor Control ProofSize 405,690 | +| C128-P128-H0 final | Rearm / skip preflight at 128 | 3,013,248,016 / 2,922,230,768 + DB | 346,462 | 896 | 0 | All five preflight families fit | +| C128-P128-H0 final | Tail placed commit at 128 | 10,124,286,000 + DB | 346,462 | 982 | 466 | Tail complete branch fits ProofSize | +| C128-P128-H0 | Non-tail emptied at 128 / trimmed at 126 | 7,503,240,000 / 7,225,756,000 + DB | 653,534 / 643,338 | 1,076 / 1,060 | 560 / 556 | Fails Actor Control ProofSize; reverse locators bind | +| C128-P128-H0-N64 final | Non-tail emptied / trimmed at 64 | 4,079,705,000 / 4,079,006,000 + DB | 327,262 | 563 | 367 / 369 | Both fit Actor Control ProofSize 405,690 | +| C128-P128-H0-N64 | 10,000-member herd release profile | Passed | N/A | N/A | N/A | Zero-match, 8-match, maximum herd, queue/deferred uniqueness, latched rearm, and repeated-fire convergence | +| C128-P128-H0-N64 | Maximum mixed wakeup/Crossing/fanout release profile | Passed | N/A | N/A | N/A | Breaker-held materialization and post-release execution complete without loss | +| C128-P128-H0-N64 | First/middle/final authority mismatch | Passed | N/A | N/A | 0 | First mismatch admits one exact generic scalar; later mismatch admits only preceding compact prefix; preflight root unchanged | +| C128-P128-H0-N64 | Final candidate evidence | Passed | Passed | Passed | Passed | Selection accepted; production promotion remains an implementation task | +| C128-P128-H0 | W5A | Not measured | Not measured | Not measured | Not measured | Require zero Contract-head reads for retained Idle prefix and exact fallback otherwise | + +## Derived Metrics + +| Candidate | Workload | RefTime delta | ProofSize delta | Throughput / latency | Marginal or amortized cost | Pareto class | +| --- | --- | ---: | ---: | --- | --- | --- | +| C128-P128 | W1-W6 | Not derived | Not derived | Not derived | Not derived | Not interpreted | + +## Statistical / Variance Notes + +Five preflight and three complete mutation owners used 50 steps/20 repeats against matching candidate Wasms. Terminal preflight is the maximum preflight at 5,480,760,592 RefTime plus 1,026 database reads and 608,981 ProofSize. Tail and non-tail emptied commits execute 128 candidates. A first trimmed fixture requested 128 moves from a 130-member tail, which creates a third page and correctly fails authority revalidation; the corrected maximum reachable trimmed branch moves 126 from a full 128-entry physical tail and leaves two entries. + +## Result + +H0 makes all five 128-candidate preflight families and the 128-candidate tail commit fit Actor Control ProofSize. Non-tail remains invalid: exact source-plus-tail refill validation reads 256 `CrossingMemberships` reverse locators, whose `2,549`-byte proof contribution dominates at 653,534. The tail branch reads 128 locators and fits at 346,462. Stale compiled authority falls back to generic classification; complete non-tail and maximum profiles remain pending. + +## Interpretation + +ProofSize remains binding. H0 validates header-free classification and tail C128; N64 preserves exact source-plus-tail bijection checks at `327,262` ProofSize, 78,428 below Actor Control. The branch-sensitive cap changes only how much contiguous authority one transaction admits: tail remains 128, non-tail remains strict source order at 64, and no locator validation, ordering, semantics, or global allocation is weakened. + +## Decision + +Accept P128 Crossing pages with H0 C128 tail/preflight and exact N64 non-tail. Reject W10/W14, P256, uniform non-tail C128, and shared P128 broad-fanout ownership. Crossing and broad ObservationChange now own independent P128/P64 page bounds; complete regenerated Actors Weight fits Actor Control, with synchronized metadata/ABI and canonical production Wasm evidence. Exact release-tree attestation remains release work. + +## Rejected Alternatives and Tradeoffs + +| Candidate | Decision | Evidence-backed reason | +| --- | --- | --- | +| C64-P64 | Diagnostic only | Cannot satisfy the explicit ≥128 production target | +| C128-P128-W10 | Rejected | Complete non-tail ProofSize 653,534 exceeds the 500,000 local worker ceiling | +| C128-P128-W14 | Rejected | The proposed `280,000,000,000 / 700,000` local ceiling exceeds Actor Control `181,083,640,945 / 405,690` | +| C128-P128-H0 | Partially accepted | Header-free preflight and tail C128 fit; non-tail C128 remains rejected | +| C128-P128-H0-N64 | Provisionally accepted | Non-tail emptied/trimmed fit at `327,262` ProofSize while preserving exact 64 + 64 reverse-locator validation | +| C256-P256 | Deferred | EXP-0008 shows materially higher page/mutation cost without release need | + +## Validity + +- `Establishes`: A compile-coherent C128-P128 candidate and predeclared complete evidence contract. +- `Does not establish`: Envelope fit, production selection, or 10,000-member throughput. +- `Known limitations`: Generated complete branches, two maximum release profiles, and first/middle/final mismatch boundaries pass. Production Weight regeneration remains pending. A first Running/Suspended or commitment-mismatched candidate receives one exact generic scalar fallback; a later mismatch truncates before that candidate and commits only the preceding compact prefix. +- `Invalidation triggers`: Page selection, cohort semantics, FIFO batching, latching, queue geometry, resource policy, worker envelope, or benchmark method changes. +- `Residual qualitative insight`: The decision is controlled by both complete grouped mutation and repeated Contract-head proof; page-local routing plus compact Idle placement can amortize the Trigger cause without caching Step semantics. + +## Follow-Up / Next Gradient + +```text +P128 target-enabling page → C128 W10 ProofSize failure +→ global Actor Control rejects W14 → isolate Contract-head slope +→ H0 page-local paired routing + compact Idle placement +→ exact fallback/rollback → generated envelope → 10,000-member profile +``` + +- `Eliminated hypotheses`: P64 alone can represent a one-page 128-candidate cohort. +- `Remaining hypotheses`: C128 tail plus N64 exact non-tail service fits Actor Control and preserves throughput better than weakening reverse-locator validation. +- `Stop condition`: Accept C128-P128 with complete generated and exact-root evidence, or reject it with one identified binding owner and a narrower conforming successor. + +## Architecture Impact + +- `Affected physical truth`: Crossing cohort bounds, contiguous snapshots, grouped preflight/commit, queue append, non-tail refill, and generated Weight. +- `Architecture update`: Update package and DEOS integration architecture only after implementation, tests, correction, and complete generation converge. +- `Provenance citation`: `Actors EXP-0009`. + +## Relations + +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: EXP-0010 compact activation and EXP-0008 page geometry. +- `Contradicts`: None. +- `Depends on`: EXP-0008, EXP-0020, Q1, strict FIFO, P32, and fixed resource policy. +- `Baseline of`: Crossing production implementation and T8 acceptance. +- `Confirms`: None yet. +- `Invalidates`: None. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/crossing_coalesced_cohort_preflight.rs b/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/crossing_coalesced_cohort_preflight.rs new file mode 100644 index 00000000..bf7caddd --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/crossing_coalesced_cohort_preflight.rs @@ -0,0 +1,75 @@ + +//! Autogenerated weights for `pallet_deos_actors` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 58.0.0 +//! DATE: 2026-08-29, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `fedora`, CPU: `AMD Ryzen 7 4800H with Radeon Graphics` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 + +// Executed Command: +// frame-omni-bencher +// v1 +// benchmark +// pallet +// --runtime +// /home/llb/Repos/deos/template/target/benchmarks/release/wbuild/deos-runtime/deos_runtime.compact.compressed.wasm +// --pallet +// pallet_deos_actors +// --extrinsic +// crossing_coalesced_cohort_preflight +// --steps +// 50 +// --repeat +// 20 +// --heap-pages +// 4096 +// --output +// /tmp/deos-exp-0009/weights/crossing_coalesced_cohort_preflight.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions for `pallet_deos_actors`. +pub struct WeightInfo(PhantomData); +impl pallet_deos_actors::WeightInfo for WeightInfo { + /// Storage: `Actors::CrossingMemberships` (r:128 w:0) + /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:128 w:0) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorHot` (r:128 w:0) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:128 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:128 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorIdentities` (r:128 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorFunding` (r:128 w:0) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:128 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) + /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + /// Storage: `Actors::NextQueueTicket` (r:1 w:0) + /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// The range of component `c` is `[1, 128]`. + fn crossing_coalesced_cohort_preflight(c: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1687 + c * (767 ±0)` + // Estimated: `1493 + c * (4746 ±0)` + // Minimum execution time: 69_563_000 picoseconds. + Weight::from_parts(71_029_000, 0) + .saturating_add(Weight::from_parts(0, 1493)) + // Standard Error: 40_168 + .saturating_add(Weight::from_parts(42_144_896, 0).saturating_mul(c.into())) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().reads((8_u64).saturating_mul(c.into()))) + .saturating_add(Weight::from_parts(0, 4746).saturating_mul(c.into())) + } +} diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/crossing_fire_cohort_preflight.rs b/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/crossing_fire_cohort_preflight.rs new file mode 100644 index 00000000..0c584b48 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/crossing_fire_cohort_preflight.rs @@ -0,0 +1,75 @@ + +//! Autogenerated weights for `pallet_deos_actors` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 58.0.0 +//! DATE: 2026-08-29, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `fedora`, CPU: `AMD Ryzen 7 4800H with Radeon Graphics` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 + +// Executed Command: +// frame-omni-bencher +// v1 +// benchmark +// pallet +// --runtime +// /home/llb/Repos/deos/template/target/benchmarks/release/wbuild/deos-runtime/deos_runtime.compact.compressed.wasm +// --pallet +// pallet_deos_actors +// --extrinsic +// crossing_fire_cohort_preflight +// --steps +// 50 +// --repeat +// 20 +// --heap-pages +// 4096 +// --output +// /tmp/deos-exp-0009/weights/crossing_fire_cohort_preflight.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions for `pallet_deos_actors`. +pub struct WeightInfo(PhantomData); +impl pallet_deos_actors::WeightInfo for WeightInfo { + /// Storage: `Actors::CrossingMemberships` (r:128 w:0) + /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:128 w:0) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorHot` (r:128 w:0) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:128 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:128 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorIdentities` (r:128 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorFunding` (r:128 w:0) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:128 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) + /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + /// Storage: `Actors::NextQueueTicket` (r:1 w:0) + /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// The range of component `c` is `[1, 128]`. + fn crossing_fire_cohort_preflight(c: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1591 + c * (757 ±0)` + // Estimated: `1493 + c * (4746 ±0)` + // Minimum execution time: 67_049_000 picoseconds. + Weight::from_parts(67_607_000, 0) + .saturating_add(Weight::from_parts(0, 1493)) + // Standard Error: 37_794 + .saturating_add(Weight::from_parts(41_988_537, 0).saturating_mul(c.into())) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().reads((8_u64).saturating_mul(c.into()))) + .saturating_add(Weight::from_parts(0, 4746).saturating_mul(c.into())) + } +} diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/crossing_placed_maximum_unit.rs b/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/crossing_placed_maximum_unit.rs new file mode 100644 index 00000000..d5b54bf3 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/crossing_placed_maximum_unit.rs @@ -0,0 +1,99 @@ + +//! Autogenerated weights for `pallet_deos_actors` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 58.0.0 +//! DATE: 2026-08-29, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `fedora`, CPU: `AMD Ryzen 7 4800H with Radeon Graphics` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 + +// Executed Command: +// frame-omni-bencher +// v1 +// benchmark +// pallet +// --runtime +// /home/llb/Repos/deos/template/target/benchmarks/release/wbuild/deos-runtime/deos_runtime.compact.compressed.wasm +// --pallet +// pallet_deos_actors +// --extrinsic +// crossing_placed_maximum_unit +// --steps +// 50 +// --repeat +// 20 +// --heap-pages +// 4096 +// --output +// /tmp/deos-exp-0009-complete/crossing_placed_maximum_unit.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions for `pallet_deos_actors`. +pub struct WeightInfo(PhantomData); +impl pallet_deos_actors::WeightInfo for WeightInfo { + /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:1) + /// Proof: `Actors::CrossingPendingFeedListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingTransitionQueues` (r:1 w:0) + /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(3171), added: 5646, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingRangeCursors` (r:1 w:1) + /// Proof: `Actors::CrossingRangeCursors` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingLeafStates` (r:2 w:2) + /// Proof: `Actors::CrossingLeafStates` (`max_values`: None, `max_size`: Some(62), added: 2537, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingMemberPages` (r:2 w:2) + /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(2120), added: 4595, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingMemberships` (r:128 w:128) + /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:128 w:128) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorHot` (r:128 w:128) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:128 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:128 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorIdentities` (r:128 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorFunding` (r:128 w:0) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:128 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) + /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + /// Storage: `Actors::NextQueueTicket` (r:1 w:1) + /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueHead` (r:1 w:0) + /// Proof: `Actors::QueueHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueTail` (r:1 w:1) + /// Proof: `Actors::QueueTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueOccupancy` (r:1 w:1) + /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueuePages` (r:2 w:2) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:5 w:5) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingFeedMembershipCount` (r:1 w:1) + /// Proof: `Actors::CrossingFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingRadixNodes` (r:64 w:64) + /// Proof: `Actors::CrossingRadixNodes` (`max_values`: None, `max_size`: Some(53), added: 2528, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingUserFeedMembershipCount` (r:1 w:1) + /// Proof: `Actors::CrossingUserFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingPendingFeeds` (r:1 w:0) + /// Proof: `Actors::CrossingPendingFeeds` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) + fn crossing_placed_maximum_unit() -> Weight { + // Proof Size summary in bytes: + // Measured: `100038` + // Estimated: `608478` + // Minimum execution time: 12_299_877_000 picoseconds. + Weight::from_parts(12_505_842_000, 0) + .saturating_add(Weight::from_parts(0, 608478)) + .saturating_add(T::DbWeight::get().reads(1110)) + .saturating_add(T::DbWeight::get().writes(466)) + } +} diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/crossing_placed_non_tail_emptied_unit.rs b/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/crossing_placed_non_tail_emptied_unit.rs new file mode 100644 index 00000000..83a32499 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/crossing_placed_non_tail_emptied_unit.rs @@ -0,0 +1,99 @@ + +//! Autogenerated weights for `pallet_deos_actors` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 58.0.0 +//! DATE: 2026-08-29, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `fedora`, CPU: `AMD Ryzen 7 4800H with Radeon Graphics` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 + +// Executed Command: +// frame-omni-bencher +// v1 +// benchmark +// pallet +// --runtime +// /home/llb/Repos/deos/template/target/benchmarks/release/wbuild/deos-runtime/deos_runtime.compact.compressed.wasm +// --pallet +// pallet_deos_actors +// --extrinsic +// crossing_placed_non_tail_emptied_unit +// --steps +// 50 +// --repeat +// 20 +// --heap-pages +// 4096 +// --output +// /tmp/deos-exp-0009-complete/crossing_placed_non_tail_emptied_unit.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions for `pallet_deos_actors`. +pub struct WeightInfo(PhantomData); +impl pallet_deos_actors::WeightInfo for WeightInfo { + /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:1) + /// Proof: `Actors::CrossingPendingFeedListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingTransitionQueues` (r:1 w:0) + /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(3171), added: 5646, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingRangeCursors` (r:1 w:1) + /// Proof: `Actors::CrossingRangeCursors` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingLeafStates` (r:2 w:2) + /// Proof: `Actors::CrossingLeafStates` (`max_values`: None, `max_size`: Some(62), added: 2537, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingMemberPages` (r:3 w:3) + /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(2120), added: 4595, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingMemberships` (r:256 w:256) + /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:128 w:128) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorHot` (r:128 w:128) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:128 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:128 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorIdentities` (r:128 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorFunding` (r:128 w:0) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:128 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) + /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + /// Storage: `Actors::NextQueueTicket` (r:1 w:1) + /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueHead` (r:1 w:0) + /// Proof: `Actors::QueueHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueTail` (r:1 w:1) + /// Proof: `Actors::QueueTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueOccupancy` (r:1 w:1) + /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueuePages` (r:2 w:2) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingFeedMembershipCount` (r:1 w:1) + /// Proof: `Actors::CrossingFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingUserFeedMembershipCount` (r:1 w:1) + /// Proof: `Actors::CrossingUserFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingRadixNodes` (r:32 w:32) + /// Proof: `Actors::CrossingRadixNodes` (`max_values`: None, `max_size`: Some(53), added: 2528, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingPendingFeeds` (r:1 w:0) + /// Proof: `Actors::CrossingPendingFeeds` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) + fn crossing_placed_non_tail_emptied_unit() -> Weight { + // Proof Size summary in bytes: + // Measured: `128608` + // Estimated: `653534` + // Minimum execution time: 10_292_117_000 picoseconds. + Weight::from_parts(10_498_152_000, 0) + .saturating_add(Weight::from_parts(0, 653534)) + .saturating_add(T::DbWeight::get().reads(1204)) + .saturating_add(T::DbWeight::get().writes(560)) + } +} diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/crossing_placed_non_tail_trimmed_unit.rs b/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/crossing_placed_non_tail_trimmed_unit.rs new file mode 100644 index 00000000..fa52d3c7 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/crossing_placed_non_tail_trimmed_unit.rs @@ -0,0 +1,99 @@ + +//! Autogenerated weights for `pallet_deos_actors` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 58.0.0 +//! DATE: 2026-08-29, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `fedora`, CPU: `AMD Ryzen 7 4800H with Radeon Graphics` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 + +// Executed Command: +// frame-omni-bencher +// v1 +// benchmark +// pallet +// --runtime +// /home/llb/Repos/deos/template/target/benchmarks/release/wbuild/deos-runtime/deos_runtime.compact.compressed.wasm +// --pallet +// pallet_deos_actors +// --extrinsic +// crossing_placed_non_tail_trimmed_unit +// --steps +// 50 +// --repeat +// 20 +// --heap-pages +// 4096 +// --output +// /tmp/deos-exp-0009-complete/crossing_placed_non_tail_trimmed_unit.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions for `pallet_deos_actors`. +pub struct WeightInfo(PhantomData); +impl pallet_deos_actors::WeightInfo for WeightInfo { + /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:1) + /// Proof: `Actors::CrossingPendingFeedListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingTransitionQueues` (r:1 w:0) + /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(3171), added: 5646, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingRangeCursors` (r:1 w:1) + /// Proof: `Actors::CrossingRangeCursors` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingLeafStates` (r:2 w:2) + /// Proof: `Actors::CrossingLeafStates` (`max_values`: None, `max_size`: Some(62), added: 2537, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingMemberPages` (r:3 w:3) + /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(2120), added: 4595, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingMemberships` (r:252 w:256) + /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:126 w:126) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorHot` (r:126 w:126) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:126 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:126 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorIdentities` (r:126 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorFunding` (r:126 w:0) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:126 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) + /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + /// Storage: `Actors::NextQueueTicket` (r:1 w:1) + /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueHead` (r:1 w:0) + /// Proof: `Actors::QueueHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueTail` (r:1 w:1) + /// Proof: `Actors::QueueTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueOccupancy` (r:1 w:1) + /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueuePages` (r:2 w:2) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingFeedMembershipCount` (r:1 w:1) + /// Proof: `Actors::CrossingFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingUserFeedMembershipCount` (r:1 w:1) + /// Proof: `Actors::CrossingUserFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingRadixNodes` (r:32 w:32) + /// Proof: `Actors::CrossingRadixNodes` (`max_values`: None, `max_size`: Some(53), added: 2528, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingPendingFeeds` (r:1 w:0) + /// Proof: `Actors::CrossingPendingFeeds` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) + fn crossing_placed_non_tail_trimmed_unit() -> Weight { + // Proof Size summary in bytes: + // Measured: `127489` + // Estimated: `643338` + // Minimum execution time: 10_159_347_000 picoseconds. + Weight::from_parts(10_530_838_000, 0) + .saturating_add(Weight::from_parts(0, 643338)) + .saturating_add(T::DbWeight::get().reads(1186)) + .saturating_add(T::DbWeight::get().writes(556)) + } +} diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/crossing_rearm_cohort_preflight.rs b/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/crossing_rearm_cohort_preflight.rs new file mode 100644 index 00000000..c56a8d6d --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/crossing_rearm_cohort_preflight.rs @@ -0,0 +1,64 @@ + +//! Autogenerated weights for `pallet_deos_actors` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 58.0.0 +//! DATE: 2026-08-29, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `fedora`, CPU: `AMD Ryzen 7 4800H with Radeon Graphics` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 + +// Executed Command: +// frame-omni-bencher +// v1 +// benchmark +// pallet +// --runtime +// /home/llb/Repos/deos/template/target/benchmarks/release/wbuild/deos-runtime/deos_runtime.compact.compressed.wasm +// --pallet +// pallet_deos_actors +// --extrinsic +// crossing_rearm_cohort_preflight +// --steps +// 50 +// --repeat +// 20 +// --heap-pages +// 4096 +// --output +// /tmp/deos-exp-0009/weights/crossing_rearm_cohort_preflight.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions for `pallet_deos_actors`. +pub struct WeightInfo(PhantomData); +impl pallet_deos_actors::WeightInfo for WeightInfo { + /// Storage: `Actors::CrossingMemberships` (r:128 w:0) + /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:128 w:0) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorHot` (r:128 w:0) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:128 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:128 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) + /// The range of component `c` is `[1, 128]`. + fn crossing_rearm_cohort_preflight(c: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1052 + c * (626 ±0)` + // Estimated: `990 + c * (4746 ±0)` + // Minimum execution time: 37_646_000 picoseconds. + Weight::from_parts(14_448_809, 0) + .saturating_add(Weight::from_parts(0, 990)) + // Standard Error: 29_915 + .saturating_add(Weight::from_parts(21_243_593, 0).saturating_mul(c.into())) + .saturating_add(T::DbWeight::get().reads((5_u64).saturating_mul(c.into()))) + .saturating_add(Weight::from_parts(0, 4746).saturating_mul(c.into())) + } +} diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/crossing_skip_cohort_preflight.rs b/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/crossing_skip_cohort_preflight.rs new file mode 100644 index 00000000..05113d40 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/crossing_skip_cohort_preflight.rs @@ -0,0 +1,60 @@ + +//! Autogenerated weights for `pallet_deos_actors` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 58.0.0 +//! DATE: 2026-08-29, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `fedora`, CPU: `AMD Ryzen 7 4800H with Radeon Graphics` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 + +// Executed Command: +// frame-omni-bencher +// v1 +// benchmark +// pallet +// --runtime +// /home/llb/Repos/deos/template/target/benchmarks/release/wbuild/deos-runtime/deos_runtime.compact.compressed.wasm +// --pallet +// pallet_deos_actors +// --extrinsic +// crossing_skip_cohort_preflight +// --steps +// 50 +// --repeat +// 20 +// --heap-pages +// 4096 +// --output +// /tmp/deos-exp-0009/weights/crossing_skip_cohort_preflight.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions for `pallet_deos_actors`. +pub struct WeightInfo(PhantomData); +impl pallet_deos_actors::WeightInfo for WeightInfo { + /// Storage: `Actors::CrossingMemberships` (r:128 w:0) + /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:128 w:0) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorHot` (r:128 w:0) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// The range of component `c` is `[1, 128]`. + fn crossing_skip_cohort_preflight(c: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `754 + c * (131 ±0)` + // Estimated: `990 + c * (2583 ±0)` + // Minimum execution time: 17_949_000 picoseconds. + Weight::from_parts(18_298_000, 0) + .saturating_add(Weight::from_parts(0, 990)) + // Standard Error: 10_759 + .saturating_add(Weight::from_parts(9_345_934, 0).saturating_mul(c.into())) + .saturating_add(T::DbWeight::get().reads((3_u64).saturating_mul(c.into()))) + .saturating_add(Weight::from_parts(0, 2583).saturating_mul(c.into())) + } +} diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/crossing_terminal_cohort_preflight.rs b/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/crossing_terminal_cohort_preflight.rs new file mode 100644 index 00000000..086843ae --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/crossing_terminal_cohort_preflight.rs @@ -0,0 +1,75 @@ + +//! Autogenerated weights for `pallet_deos_actors` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 58.0.0 +//! DATE: 2026-08-29, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `fedora`, CPU: `AMD Ryzen 7 4800H with Radeon Graphics` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 + +// Executed Command: +// frame-omni-bencher +// v1 +// benchmark +// pallet +// --runtime +// /home/llb/Repos/deos/template/target/benchmarks/release/wbuild/deos-runtime/deos_runtime.compact.compressed.wasm +// --pallet +// pallet_deos_actors +// --extrinsic +// crossing_terminal_cohort_preflight +// --steps +// 50 +// --repeat +// 20 +// --heap-pages +// 4096 +// --output +// /tmp/deos-exp-0009/weights/crossing_terminal_cohort_preflight.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions for `pallet_deos_actors`. +pub struct WeightInfo(PhantomData); +impl pallet_deos_actors::WeightInfo for WeightInfo { + /// Storage: `Actors::CrossingMemberships` (r:128 w:0) + /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:128 w:0) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorHot` (r:128 w:0) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:128 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:128 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorIdentities` (r:128 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorFunding` (r:128 w:0) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:128 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) + /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + /// Storage: `Actors::NextQueueTicket` (r:1 w:0) + /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// The range of component `c` is `[1, 128]`. + fn crossing_terminal_cohort_preflight(c: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1617 + c * (757 ±0)` + // Estimated: `1493 + c * (4746 ±0)` + // Minimum execution time: 66_770_000 picoseconds. + Weight::from_parts(67_258_000, 0) + .saturating_add(Weight::from_parts(0, 1493)) + // Standard Error: 42_260 + .saturating_add(Weight::from_parts(42_292_989, 0).saturating_mul(c.into())) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().reads((8_u64).saturating_mul(c.into()))) + .saturating_add(Weight::from_parts(0, 4746).saturating_mul(c.into())) + } +} diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/final-crossing_coalesced_cohort_preflight.rs b/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/final-crossing_coalesced_cohort_preflight.rs new file mode 100644 index 00000000..a90189e7 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/final-crossing_coalesced_cohort_preflight.rs @@ -0,0 +1,73 @@ + +//! Autogenerated weights for `pallet_deos_actors` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 58.0.0 +//! DATE: 2026-08-29, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `fedora`, CPU: `AMD Ryzen 7 4800H with Radeon Graphics` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 + +// Executed Command: +// frame-omni-bencher +// v1 +// benchmark +// pallet +// --runtime +// /home/llb/Repos/deos/template/target/benchmarks/release/wbuild/deos-runtime/deos_runtime.compact.compressed.wasm +// --pallet +// pallet_deos_actors +// --extrinsic +// crossing_coalesced_cohort_preflight +// --steps +// 50 +// --repeat +// 20 +// --heap-pages +// 4096 +// --output +// /tmp/deos-exp-0009-final/crossing_coalesced_cohort_preflight.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions for `pallet_deos_actors`. +pub struct WeightInfo(PhantomData); +impl pallet_deos_actors::WeightInfo for WeightInfo { + /// Storage: `Actors::CrossingMemberships` (r:128 w:0) + /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:128 w:0) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorIdentities` (r:128 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorHot` (r:128 w:0) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:128 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorActivationAuthority` (r:128 w:0) + /// Proof: `Actors::ActorActivationAuthority` (`max_values`: None, `max_size`: Some(159), added: 2634, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:128 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) + /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) + /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + /// Storage: `Actors::NextQueueTicket` (r:1 w:0) + /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// The range of component `c` is `[1, 128]`. + fn crossing_coalesced_cohort_preflight(c: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1511 + c * (608 ±0)` + // Estimated: `1493 + c * (2699 ±0)` + // Minimum execution time: 44_281_000 picoseconds. + Weight::from_parts(45_118_000, 0) + .saturating_add(Weight::from_parts(0, 1493)) + // Standard Error: 25_405 + .saturating_add(Weight::from_parts(23_972_278, 0).saturating_mul(c.into())) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().reads((7_u64).saturating_mul(c.into()))) + .saturating_add(Weight::from_parts(0, 2699).saturating_mul(c.into())) + } +} diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/final-crossing_fire_cohort_preflight.rs b/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/final-crossing_fire_cohort_preflight.rs new file mode 100644 index 00000000..1c23ce87 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/final-crossing_fire_cohort_preflight.rs @@ -0,0 +1,73 @@ + +//! Autogenerated weights for `pallet_deos_actors` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 58.0.0 +//! DATE: 2026-08-29, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `fedora`, CPU: `AMD Ryzen 7 4800H with Radeon Graphics` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 + +// Executed Command: +// frame-omni-bencher +// v1 +// benchmark +// pallet +// --runtime +// /home/llb/Repos/deos/template/target/benchmarks/release/wbuild/deos-runtime/deos_runtime.compact.compressed.wasm +// --pallet +// pallet_deos_actors +// --extrinsic +// crossing_fire_cohort_preflight +// --steps +// 50 +// --repeat +// 20 +// --heap-pages +// 4096 +// --output +// /tmp/deos-exp-0009-final/crossing_fire_cohort_preflight.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions for `pallet_deos_actors`. +pub struct WeightInfo(PhantomData); +impl pallet_deos_actors::WeightInfo for WeightInfo { + /// Storage: `Actors::CrossingMemberships` (r:128 w:0) + /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:128 w:0) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorIdentities` (r:128 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorHot` (r:128 w:0) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:128 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorActivationAuthority` (r:128 w:0) + /// Proof: `Actors::ActorActivationAuthority` (`max_values`: None, `max_size`: Some(159), added: 2634, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:128 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) + /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) + /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + /// Storage: `Actors::NextQueueTicket` (r:1 w:0) + /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// The range of component `c` is `[1, 128]`. + fn crossing_fire_cohort_preflight(c: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1416 + c * (598 ±0)` + // Estimated: `1493 + c * (2699 ±0)` + // Minimum execution time: 41_836_000 picoseconds. + Weight::from_parts(42_394_000, 0) + .saturating_add(Weight::from_parts(0, 1493)) + // Standard Error: 35_665 + .saturating_add(Weight::from_parts(23_921_336, 0).saturating_mul(c.into())) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().reads((7_u64).saturating_mul(c.into()))) + .saturating_add(Weight::from_parts(0, 2699).saturating_mul(c.into())) + } +} diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/final-crossing_placed_maximum_unit.rs b/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/final-crossing_placed_maximum_unit.rs new file mode 100644 index 00000000..7014a4ef --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/final-crossing_placed_maximum_unit.rs @@ -0,0 +1,97 @@ + +//! Autogenerated weights for `pallet_deos_actors` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 58.0.0 +//! DATE: 2026-08-29, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `fedora`, CPU: `AMD Ryzen 7 4800H with Radeon Graphics` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 + +// Executed Command: +// frame-omni-bencher +// v1 +// benchmark +// pallet +// --runtime +// /home/llb/Repos/deos/template/target/benchmarks/release/wbuild/deos-runtime/deos_runtime.compact.compressed.wasm +// --pallet +// pallet_deos_actors +// --extrinsic +// crossing_placed_maximum_unit +// --steps +// 50 +// --repeat +// 20 +// --heap-pages +// 4096 +// --output +// /tmp/deos-exp-0009-final/crossing_placed_maximum_unit.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions for `pallet_deos_actors`. +pub struct WeightInfo(PhantomData); +impl pallet_deos_actors::WeightInfo for WeightInfo { + /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:1) + /// Proof: `Actors::CrossingPendingFeedListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingTransitionQueues` (r:1 w:0) + /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(3171), added: 5646, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingRangeCursors` (r:1 w:1) + /// Proof: `Actors::CrossingRangeCursors` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingLeafStates` (r:2 w:2) + /// Proof: `Actors::CrossingLeafStates` (`max_values`: None, `max_size`: Some(62), added: 2537, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingMemberPages` (r:2 w:2) + /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(8264), added: 10739, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingMemberships` (r:128 w:128) + /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:128 w:128) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorIdentities` (r:128 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorHot` (r:128 w:128) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:128 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorActivationAuthority` (r:128 w:0) + /// Proof: `Actors::ActorActivationAuthority` (`max_values`: None, `max_size`: Some(159), added: 2634, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:128 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) + /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) + /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + /// Storage: `Actors::NextQueueTicket` (r:1 w:1) + /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueHead` (r:1 w:0) + /// Proof: `Actors::QueueHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueTail` (r:1 w:1) + /// Proof: `Actors::QueueTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueOccupancy` (r:1 w:1) + /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueuePages` (r:2 w:2) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:5 w:5) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingFeedMembershipCount` (r:1 w:1) + /// Proof: `Actors::CrossingFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingRadixNodes` (r:64 w:64) + /// Proof: `Actors::CrossingRadixNodes` (`max_values`: None, `max_size`: Some(53), added: 2528, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingUserFeedMembershipCount` (r:1 w:1) + /// Proof: `Actors::CrossingUserFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingPendingFeeds` (r:1 w:0) + /// Proof: `Actors::CrossingPendingFeeds` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) + fn crossing_placed_maximum_unit() -> Weight { + // Proof Size summary in bytes: + // Measured: `90773` + // Estimated: `346462` + // Minimum execution time: 9_859_653_000 picoseconds. + Weight::from_parts(10_124_286_000, 0) + .saturating_add(Weight::from_parts(0, 346462)) + .saturating_add(T::DbWeight::get().reads(982)) + .saturating_add(T::DbWeight::get().writes(466)) + } +} diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/final-crossing_placed_non_tail_emptied_unit.rs b/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/final-crossing_placed_non_tail_emptied_unit.rs new file mode 100644 index 00000000..a41ce299 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/final-crossing_placed_non_tail_emptied_unit.rs @@ -0,0 +1,97 @@ + +//! Autogenerated weights for `pallet_deos_actors` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 58.0.0 +//! DATE: 2026-08-29, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `fedora`, CPU: `AMD Ryzen 7 4800H with Radeon Graphics` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 + +// Executed Command: +// frame-omni-bencher +// v1 +// benchmark +// pallet +// --runtime +// /home/llb/Repos/deos/template/target/benchmarks/release/wbuild/deos-runtime/deos_runtime.compact.compressed.wasm +// --pallet +// pallet_deos_actors +// --extrinsic +// crossing_placed_non_tail_emptied_unit +// --steps +// 50 +// --repeat +// 20 +// --heap-pages +// 4096 +// --output +// /tmp/deos-exp-0009-final/crossing_placed_non_tail_emptied_unit.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions for `pallet_deos_actors`. +pub struct WeightInfo(PhantomData); +impl pallet_deos_actors::WeightInfo for WeightInfo { + /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:1) + /// Proof: `Actors::CrossingPendingFeedListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingTransitionQueues` (r:1 w:0) + /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(3171), added: 5646, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingRangeCursors` (r:1 w:1) + /// Proof: `Actors::CrossingRangeCursors` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingLeafStates` (r:2 w:2) + /// Proof: `Actors::CrossingLeafStates` (`max_values`: None, `max_size`: Some(62), added: 2537, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingMemberPages` (r:3 w:3) + /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(8264), added: 10739, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingMemberships` (r:128 w:192) + /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:64 w:64) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorIdentities` (r:64 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorHot` (r:64 w:64) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:64 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorActivationAuthority` (r:64 w:0) + /// Proof: `Actors::ActorActivationAuthority` (`max_values`: None, `max_size`: Some(159), added: 2634, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:64 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) + /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) + /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + /// Storage: `Actors::NextQueueTicket` (r:1 w:1) + /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueHead` (r:1 w:0) + /// Proof: `Actors::QueueHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueTail` (r:1 w:1) + /// Proof: `Actors::QueueTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueOccupancy` (r:1 w:1) + /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueuePages` (r:1 w:1) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingFeedMembershipCount` (r:1 w:1) + /// Proof: `Actors::CrossingFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingUserFeedMembershipCount` (r:1 w:1) + /// Proof: `Actors::CrossingUserFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingRadixNodes` (r:32 w:32) + /// Proof: `Actors::CrossingRadixNodes` (`max_values`: None, `max_size`: Some(53), added: 2528, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingPendingFeeds` (r:1 w:0) + /// Proof: `Actors::CrossingPendingFeeds` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) + fn crossing_placed_non_tail_emptied_unit() -> Weight { + // Proof Size summary in bytes: + // Measured: `76353` + // Estimated: `327262` + // Minimum execution time: 3_966_490_000 picoseconds. + Weight::from_parts(4_079_705_000, 0) + .saturating_add(Weight::from_parts(0, 327262)) + .saturating_add(T::DbWeight::get().reads(563)) + .saturating_add(T::DbWeight::get().writes(367)) + } +} diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/final-crossing_placed_non_tail_trimmed_unit.rs b/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/final-crossing_placed_non_tail_trimmed_unit.rs new file mode 100644 index 00000000..a0446f35 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/final-crossing_placed_non_tail_trimmed_unit.rs @@ -0,0 +1,97 @@ + +//! Autogenerated weights for `pallet_deos_actors` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 58.0.0 +//! DATE: 2026-08-29, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `fedora`, CPU: `AMD Ryzen 7 4800H with Radeon Graphics` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 + +// Executed Command: +// frame-omni-bencher +// v1 +// benchmark +// pallet +// --runtime +// /home/llb/Repos/deos/template/target/benchmarks/release/wbuild/deos-runtime/deos_runtime.compact.compressed.wasm +// --pallet +// pallet_deos_actors +// --extrinsic +// crossing_placed_non_tail_trimmed_unit +// --steps +// 50 +// --repeat +// 20 +// --heap-pages +// 4096 +// --output +// /tmp/deos-exp-0009-final/crossing_placed_non_tail_trimmed_unit.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions for `pallet_deos_actors`. +pub struct WeightInfo(PhantomData); +impl pallet_deos_actors::WeightInfo for WeightInfo { + /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:1) + /// Proof: `Actors::CrossingPendingFeedListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingTransitionQueues` (r:1 w:0) + /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(3171), added: 5646, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingRangeCursors` (r:1 w:1) + /// Proof: `Actors::CrossingRangeCursors` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingLeafStates` (r:2 w:2) + /// Proof: `Actors::CrossingLeafStates` (`max_values`: None, `max_size`: Some(62), added: 2537, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingMemberPages` (r:3 w:3) + /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(8264), added: 10739, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingMemberships` (r:128 w:194) + /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:64 w:64) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorIdentities` (r:64 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorHot` (r:64 w:64) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:64 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorActivationAuthority` (r:64 w:0) + /// Proof: `Actors::ActorActivationAuthority` (`max_values`: None, `max_size`: Some(159), added: 2634, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:64 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) + /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) + /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + /// Storage: `Actors::NextQueueTicket` (r:1 w:1) + /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueHead` (r:1 w:0) + /// Proof: `Actors::QueueHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueTail` (r:1 w:1) + /// Proof: `Actors::QueueTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueOccupancy` (r:1 w:1) + /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueuePages` (r:1 w:1) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingFeedMembershipCount` (r:1 w:1) + /// Proof: `Actors::CrossingFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingUserFeedMembershipCount` (r:1 w:1) + /// Proof: `Actors::CrossingUserFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingRadixNodes` (r:32 w:32) + /// Proof: `Actors::CrossingRadixNodes` (`max_values`: None, `max_size`: Some(53), added: 2528, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingPendingFeeds` (r:1 w:0) + /// Proof: `Actors::CrossingPendingFeeds` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) + fn crossing_placed_non_tail_trimmed_unit() -> Weight { + // Proof Size summary in bytes: + // Measured: `76648` + // Estimated: `327262` + // Minimum execution time: 3_987_653_000 picoseconds. + Weight::from_parts(4_079_006_000, 0) + .saturating_add(Weight::from_parts(0, 327262)) + .saturating_add(T::DbWeight::get().reads(563)) + .saturating_add(T::DbWeight::get().writes(369)) + } +} diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/final-crossing_rearm_cohort_preflight.rs b/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/final-crossing_rearm_cohort_preflight.rs new file mode 100644 index 00000000..90762aaf --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/final-crossing_rearm_cohort_preflight.rs @@ -0,0 +1,68 @@ + +//! Autogenerated weights for `pallet_deos_actors` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 58.0.0 +//! DATE: 2026-08-29, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `fedora`, CPU: `AMD Ryzen 7 4800H with Radeon Graphics` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 + +// Executed Command: +// frame-omni-bencher +// v1 +// benchmark +// pallet +// --runtime +// /home/llb/Repos/deos/template/target/benchmarks/release/wbuild/deos-runtime/deos_runtime.compact.compressed.wasm +// --pallet +// pallet_deos_actors +// --extrinsic +// crossing_rearm_cohort_preflight +// --steps +// 50 +// --repeat +// 20 +// --heap-pages +// 4096 +// --output +// /tmp/deos-exp-0009-final/crossing_rearm_cohort_preflight.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions for `pallet_deos_actors`. +pub struct WeightInfo(PhantomData); +impl pallet_deos_actors::WeightInfo for WeightInfo { + /// Storage: `Actors::CrossingMemberships` (r:128 w:0) + /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:128 w:0) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorIdentities` (r:128 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorHot` (r:128 w:0) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:128 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorActivationAuthority` (r:128 w:0) + /// Proof: `Actors::ActorActivationAuthority` (`max_values`: None, `max_size`: Some(159), added: 2634, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:128 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) + /// The range of component `c` is `[1, 128]`. + fn crossing_rearm_cohort_preflight(c: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1484 + c * (608 ±0)` + // Estimated: `990 + c * (2699 ±0)` + // Minimum execution time: 41_905_000 picoseconds. + Weight::from_parts(42_394_000, 0) + .saturating_add(Weight::from_parts(0, 990)) + // Standard Error: 38_955 + .saturating_add(Weight::from_parts(23_209_797, 0).saturating_mul(c.into())) + .saturating_add(T::DbWeight::get().reads((7_u64).saturating_mul(c.into()))) + .saturating_add(Weight::from_parts(0, 2699).saturating_mul(c.into())) + } +} diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/final-crossing_skip_cohort_preflight.rs b/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/final-crossing_skip_cohort_preflight.rs new file mode 100644 index 00000000..81c7a748 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/final-crossing_skip_cohort_preflight.rs @@ -0,0 +1,68 @@ + +//! Autogenerated weights for `pallet_deos_actors` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 58.0.0 +//! DATE: 2026-08-29, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `fedora`, CPU: `AMD Ryzen 7 4800H with Radeon Graphics` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 + +// Executed Command: +// frame-omni-bencher +// v1 +// benchmark +// pallet +// --runtime +// /home/llb/Repos/deos/template/target/benchmarks/release/wbuild/deos-runtime/deos_runtime.compact.compressed.wasm +// --pallet +// pallet_deos_actors +// --extrinsic +// crossing_skip_cohort_preflight +// --steps +// 50 +// --repeat +// 20 +// --heap-pages +// 4096 +// --output +// /tmp/deos-exp-0009-final/crossing_skip_cohort_preflight.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions for `pallet_deos_actors`. +pub struct WeightInfo(PhantomData); +impl pallet_deos_actors::WeightInfo for WeightInfo { + /// Storage: `Actors::CrossingMemberships` (r:128 w:0) + /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:128 w:0) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorIdentities` (r:128 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorHot` (r:128 w:0) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:128 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorActivationAuthority` (r:128 w:0) + /// Proof: `Actors::ActorActivationAuthority` (`max_values`: None, `max_size`: Some(159), added: 2634, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:128 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) + /// The range of component `c` is `[1, 128]`. + fn crossing_skip_cohort_preflight(c: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1389 + c * (598 ±0)` + // Estimated: `990 + c * (2699 ±0)` + // Minimum execution time: 37_645_000 picoseconds. + Weight::from_parts(38_134_000, 0) + .saturating_add(Weight::from_parts(0, 990)) + // Standard Error: 29_781 + .saturating_add(Weight::from_parts(22_532_006, 0).saturating_mul(c.into())) + .saturating_add(T::DbWeight::get().reads((7_u64).saturating_mul(c.into()))) + .saturating_add(Weight::from_parts(0, 2699).saturating_mul(c.into())) + } +} diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/final-crossing_terminal_cohort_preflight.rs b/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/final-crossing_terminal_cohort_preflight.rs new file mode 100644 index 00000000..9998d94a --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/final-crossing_terminal_cohort_preflight.rs @@ -0,0 +1,73 @@ + +//! Autogenerated weights for `pallet_deos_actors` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 58.0.0 +//! DATE: 2026-08-29, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `fedora`, CPU: `AMD Ryzen 7 4800H with Radeon Graphics` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 + +// Executed Command: +// frame-omni-bencher +// v1 +// benchmark +// pallet +// --runtime +// /home/llb/Repos/deos/template/target/benchmarks/release/wbuild/deos-runtime/deos_runtime.compact.compressed.wasm +// --pallet +// pallet_deos_actors +// --extrinsic +// crossing_terminal_cohort_preflight +// --steps +// 50 +// --repeat +// 20 +// --heap-pages +// 4096 +// --output +// /tmp/deos-exp-0009-final/crossing_terminal_cohort_preflight.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions for `pallet_deos_actors`. +pub struct WeightInfo(PhantomData); +impl pallet_deos_actors::WeightInfo for WeightInfo { + /// Storage: `Actors::CrossingMemberships` (r:128 w:0) + /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:128 w:0) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorIdentities` (r:128 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorHot` (r:128 w:0) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:128 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorActivationAuthority` (r:128 w:0) + /// Proof: `Actors::ActorActivationAuthority` (`max_values`: None, `max_size`: Some(159), added: 2634, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:128 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) + /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) + /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + /// Storage: `Actors::NextQueueTicket` (r:1 w:0) + /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// The range of component `c` is `[1, 128]`. + fn crossing_terminal_cohort_preflight(c: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1442 + c * (598 ±0)` + // Estimated: `1493 + c * (2699 ±0)` + // Minimum execution time: 42_744_000 picoseconds. + Weight::from_parts(43_302_000, 0) + .saturating_add(Weight::from_parts(0, 1493)) + // Standard Error: 24_052 + .saturating_add(Weight::from_parts(23_454_089, 0).saturating_mul(c.into())) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().reads((7_u64).saturating_mul(c.into()))) + .saturating_add(Weight::from_parts(0, 2699).saturating_mul(c.into())) + } +} diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/h0-crossing_coalesced_cohort_preflight.rs b/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/h0-crossing_coalesced_cohort_preflight.rs new file mode 100644 index 00000000..fd41ef3e --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/h0-crossing_coalesced_cohort_preflight.rs @@ -0,0 +1,73 @@ + +//! Autogenerated weights for `pallet_deos_actors` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 58.0.0 +//! DATE: 2026-08-29, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `fedora`, CPU: `AMD Ryzen 7 4800H with Radeon Graphics` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 + +// Executed Command: +// frame-omni-bencher +// v1 +// benchmark +// pallet +// --runtime +// /home/llb/Repos/deos/template/target/benchmarks/release/wbuild/deos-runtime/deos_runtime.compact.compressed.wasm +// --pallet +// pallet_deos_actors +// --extrinsic +// crossing_coalesced_cohort_preflight +// --steps +// 50 +// --repeat +// 20 +// --heap-pages +// 4096 +// --output +// /tmp/deos-exp-0009-h0-all/crossing_coalesced_cohort_preflight.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions for `pallet_deos_actors`. +pub struct WeightInfo(PhantomData); +impl pallet_deos_actors::WeightInfo for WeightInfo { + /// Storage: `Actors::CrossingMemberships` (r:128 w:0) + /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:128 w:0) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorIdentities` (r:128 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorHot` (r:128 w:0) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:128 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorActivationAuthority` (r:128 w:0) + /// Proof: `Actors::ActorActivationAuthority` (`max_values`: None, `max_size`: Some(159), added: 2634, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:128 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) + /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) + /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + /// Storage: `Actors::NextQueueTicket` (r:1 w:0) + /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// The range of component `c` is `[1, 128]`. + fn crossing_coalesced_cohort_preflight(c: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1511 + c * (608 ±0)` + // Estimated: `1493 + c * (2699 ±0)` + // Minimum execution time: 44_909_000 picoseconds. + Weight::from_parts(46_166_000, 0) + .saturating_add(Weight::from_parts(0, 1493)) + // Standard Error: 32_120 + .saturating_add(Weight::from_parts(24_327_565, 0).saturating_mul(c.into())) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().reads((7_u64).saturating_mul(c.into()))) + .saturating_add(Weight::from_parts(0, 2699).saturating_mul(c.into())) + } +} diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/h0-crossing_fire_cohort_preflight.rs b/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/h0-crossing_fire_cohort_preflight.rs new file mode 100644 index 00000000..2d69db45 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/h0-crossing_fire_cohort_preflight.rs @@ -0,0 +1,73 @@ + +//! Autogenerated weights for `pallet_deos_actors` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 58.0.0 +//! DATE: 2026-08-29, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `fedora`, CPU: `AMD Ryzen 7 4800H with Radeon Graphics` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 + +// Executed Command: +// frame-omni-bencher +// v1 +// benchmark +// pallet +// --runtime +// /home/llb/Repos/deos/template/target/benchmarks/release/wbuild/deos-runtime/deos_runtime.compact.compressed.wasm +// --pallet +// pallet_deos_actors +// --extrinsic +// crossing_fire_cohort_preflight +// --steps +// 50 +// --repeat +// 20 +// --heap-pages +// 4096 +// --output +// /tmp/deos-exp-0009-h0-all/crossing_fire_cohort_preflight.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions for `pallet_deos_actors`. +pub struct WeightInfo(PhantomData); +impl pallet_deos_actors::WeightInfo for WeightInfo { + /// Storage: `Actors::CrossingMemberships` (r:128 w:0) + /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:128 w:0) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorIdentities` (r:128 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorHot` (r:128 w:0) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:128 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorActivationAuthority` (r:128 w:0) + /// Proof: `Actors::ActorActivationAuthority` (`max_values`: None, `max_size`: Some(159), added: 2634, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:128 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) + /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) + /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + /// Storage: `Actors::NextQueueTicket` (r:1 w:0) + /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// The range of component `c` is `[1, 128]`. + fn crossing_fire_cohort_preflight(c: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1416 + c * (598 ±0)` + // Estimated: `1493 + c * (2699 ±0)` + // Minimum execution time: 42_743_000 picoseconds. + Weight::from_parts(43_302_000, 0) + .saturating_add(Weight::from_parts(0, 1493)) + // Standard Error: 37_484 + .saturating_add(Weight::from_parts(24_053_370, 0).saturating_mul(c.into())) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().reads((7_u64).saturating_mul(c.into()))) + .saturating_add(Weight::from_parts(0, 2699).saturating_mul(c.into())) + } +} diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/h0-crossing_placed_maximum_unit.rs b/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/h0-crossing_placed_maximum_unit.rs new file mode 100644 index 00000000..5b1cbd6e --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/h0-crossing_placed_maximum_unit.rs @@ -0,0 +1,97 @@ + +//! Autogenerated weights for `pallet_deos_actors` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 58.0.0 +//! DATE: 2026-08-29, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `fedora`, CPU: `AMD Ryzen 7 4800H with Radeon Graphics` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 + +// Executed Command: +// frame-omni-bencher +// v1 +// benchmark +// pallet +// --runtime +// /home/llb/Repos/deos/template/target/benchmarks/release/wbuild/deos-runtime/deos_runtime.compact.compressed.wasm +// --pallet +// pallet_deos_actors +// --extrinsic +// crossing_placed_maximum_unit +// --steps +// 50 +// --repeat +// 20 +// --heap-pages +// 4096 +// --output +// /tmp/deos-exp-0009-h0-all/crossing_placed_maximum_unit.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions for `pallet_deos_actors`. +pub struct WeightInfo(PhantomData); +impl pallet_deos_actors::WeightInfo for WeightInfo { + /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:1) + /// Proof: `Actors::CrossingPendingFeedListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingTransitionQueues` (r:1 w:0) + /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(3171), added: 5646, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingRangeCursors` (r:1 w:1) + /// Proof: `Actors::CrossingRangeCursors` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingLeafStates` (r:2 w:2) + /// Proof: `Actors::CrossingLeafStates` (`max_values`: None, `max_size`: Some(62), added: 2537, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingMemberPages` (r:2 w:2) + /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(8264), added: 10739, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingMemberships` (r:128 w:128) + /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:128 w:128) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorIdentities` (r:128 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorHot` (r:128 w:128) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:128 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorActivationAuthority` (r:128 w:0) + /// Proof: `Actors::ActorActivationAuthority` (`max_values`: None, `max_size`: Some(159), added: 2634, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:128 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) + /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) + /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + /// Storage: `Actors::NextQueueTicket` (r:1 w:1) + /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueHead` (r:1 w:0) + /// Proof: `Actors::QueueHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueTail` (r:1 w:1) + /// Proof: `Actors::QueueTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueOccupancy` (r:1 w:1) + /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueuePages` (r:2 w:2) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:5 w:5) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingFeedMembershipCount` (r:1 w:1) + /// Proof: `Actors::CrossingFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingRadixNodes` (r:64 w:64) + /// Proof: `Actors::CrossingRadixNodes` (`max_values`: None, `max_size`: Some(53), added: 2528, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingUserFeedMembershipCount` (r:1 w:1) + /// Proof: `Actors::CrossingUserFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingPendingFeeds` (r:1 w:0) + /// Proof: `Actors::CrossingPendingFeeds` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) + fn crossing_placed_maximum_unit() -> Weight { + // Proof Size summary in bytes: + // Measured: `90773` + // Estimated: `346462` + // Minimum execution time: 9_680_786_000 picoseconds. + Weight::from_parts(9_856_650_000, 0) + .saturating_add(Weight::from_parts(0, 346462)) + .saturating_add(T::DbWeight::get().reads(982)) + .saturating_add(T::DbWeight::get().writes(466)) + } +} diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/h0-crossing_placed_non_tail_emptied_unit.rs b/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/h0-crossing_placed_non_tail_emptied_unit.rs new file mode 100644 index 00000000..8ab8831b --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/h0-crossing_placed_non_tail_emptied_unit.rs @@ -0,0 +1,97 @@ + +//! Autogenerated weights for `pallet_deos_actors` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 58.0.0 +//! DATE: 2026-08-29, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `fedora`, CPU: `AMD Ryzen 7 4800H with Radeon Graphics` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 + +// Executed Command: +// frame-omni-bencher +// v1 +// benchmark +// pallet +// --runtime +// /home/llb/Repos/deos/template/target/benchmarks/release/wbuild/deos-runtime/deos_runtime.compact.compressed.wasm +// --pallet +// pallet_deos_actors +// --extrinsic +// crossing_placed_non_tail_emptied_unit +// --steps +// 50 +// --repeat +// 20 +// --heap-pages +// 4096 +// --output +// /tmp/deos-exp-0009-h0-all/crossing_placed_non_tail_emptied_unit.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions for `pallet_deos_actors`. +pub struct WeightInfo(PhantomData); +impl pallet_deos_actors::WeightInfo for WeightInfo { + /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:1) + /// Proof: `Actors::CrossingPendingFeedListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingTransitionQueues` (r:1 w:0) + /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(3171), added: 5646, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingRangeCursors` (r:1 w:1) + /// Proof: `Actors::CrossingRangeCursors` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingLeafStates` (r:2 w:2) + /// Proof: `Actors::CrossingLeafStates` (`max_values`: None, `max_size`: Some(62), added: 2537, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingMemberPages` (r:3 w:3) + /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(8264), added: 10739, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingMemberships` (r:256 w:256) + /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:128 w:128) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorIdentities` (r:128 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorHot` (r:128 w:128) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:128 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorActivationAuthority` (r:128 w:0) + /// Proof: `Actors::ActorActivationAuthority` (`max_values`: None, `max_size`: Some(159), added: 2634, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:128 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) + /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) + /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + /// Storage: `Actors::NextQueueTicket` (r:1 w:1) + /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueHead` (r:1 w:0) + /// Proof: `Actors::QueueHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueTail` (r:1 w:1) + /// Proof: `Actors::QueueTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueOccupancy` (r:1 w:1) + /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueuePages` (r:2 w:2) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingFeedMembershipCount` (r:1 w:1) + /// Proof: `Actors::CrossingFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingUserFeedMembershipCount` (r:1 w:1) + /// Proof: `Actors::CrossingUserFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingRadixNodes` (r:32 w:32) + /// Proof: `Actors::CrossingRadixNodes` (`max_values`: None, `max_size`: Some(53), added: 2528, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingPendingFeeds` (r:1 w:0) + /// Proof: `Actors::CrossingPendingFeeds` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) + fn crossing_placed_non_tail_emptied_unit() -> Weight { + // Proof Size summary in bytes: + // Measured: `122391` + // Estimated: `653534` + // Minimum execution time: 7_153_189_000 picoseconds. + Weight::from_parts(7_503_240_000, 0) + .saturating_add(Weight::from_parts(0, 653534)) + .saturating_add(T::DbWeight::get().reads(1076)) + .saturating_add(T::DbWeight::get().writes(560)) + } +} diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/h0-crossing_placed_non_tail_trimmed_unit.rs b/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/h0-crossing_placed_non_tail_trimmed_unit.rs new file mode 100644 index 00000000..4aed4393 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/h0-crossing_placed_non_tail_trimmed_unit.rs @@ -0,0 +1,97 @@ + +//! Autogenerated weights for `pallet_deos_actors` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 58.0.0 +//! DATE: 2026-08-29, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `fedora`, CPU: `AMD Ryzen 7 4800H with Radeon Graphics` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 + +// Executed Command: +// frame-omni-bencher +// v1 +// benchmark +// pallet +// --runtime +// /home/llb/Repos/deos/template/target/benchmarks/release/wbuild/deos-runtime/deos_runtime.compact.compressed.wasm +// --pallet +// pallet_deos_actors +// --extrinsic +// crossing_placed_non_tail_trimmed_unit +// --steps +// 50 +// --repeat +// 20 +// --heap-pages +// 4096 +// --output +// /tmp/deos-exp-0009-h0-all/crossing_placed_non_tail_trimmed_unit.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions for `pallet_deos_actors`. +pub struct WeightInfo(PhantomData); +impl pallet_deos_actors::WeightInfo for WeightInfo { + /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:1) + /// Proof: `Actors::CrossingPendingFeedListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingTransitionQueues` (r:1 w:0) + /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(3171), added: 5646, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingRangeCursors` (r:1 w:1) + /// Proof: `Actors::CrossingRangeCursors` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingLeafStates` (r:2 w:2) + /// Proof: `Actors::CrossingLeafStates` (`max_values`: None, `max_size`: Some(62), added: 2537, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingMemberPages` (r:3 w:3) + /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(8264), added: 10739, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingMemberships` (r:252 w:256) + /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:126 w:126) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorIdentities` (r:126 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorHot` (r:126 w:126) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:126 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorActivationAuthority` (r:126 w:0) + /// Proof: `Actors::ActorActivationAuthority` (`max_values`: None, `max_size`: Some(159), added: 2634, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:126 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) + /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) + /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + /// Storage: `Actors::NextQueueTicket` (r:1 w:1) + /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueHead` (r:1 w:0) + /// Proof: `Actors::QueueHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueTail` (r:1 w:1) + /// Proof: `Actors::QueueTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueOccupancy` (r:1 w:1) + /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueuePages` (r:2 w:2) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingFeedMembershipCount` (r:1 w:1) + /// Proof: `Actors::CrossingFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingUserFeedMembershipCount` (r:1 w:1) + /// Proof: `Actors::CrossingUserFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingRadixNodes` (r:32 w:32) + /// Proof: `Actors::CrossingRadixNodes` (`max_values`: None, `max_size`: Some(53), added: 2528, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingPendingFeeds` (r:1 w:0) + /// Proof: `Actors::CrossingPendingFeeds` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) + fn crossing_placed_non_tail_trimmed_unit() -> Weight { + // Proof Size summary in bytes: + // Measured: `121448` + // Estimated: `643338` + // Minimum execution time: 7_102_065_000 picoseconds. + Weight::from_parts(7_225_756_000, 0) + .saturating_add(Weight::from_parts(0, 643338)) + .saturating_add(T::DbWeight::get().reads(1060)) + .saturating_add(T::DbWeight::get().writes(556)) + } +} diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/h0-crossing_rearm_cohort_preflight.rs b/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/h0-crossing_rearm_cohort_preflight.rs new file mode 100644 index 00000000..39f34b24 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/h0-crossing_rearm_cohort_preflight.rs @@ -0,0 +1,68 @@ + +//! Autogenerated weights for `pallet_deos_actors` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 58.0.0 +//! DATE: 2026-08-29, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `fedora`, CPU: `AMD Ryzen 7 4800H with Radeon Graphics` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 + +// Executed Command: +// frame-omni-bencher +// v1 +// benchmark +// pallet +// --runtime +// /home/llb/Repos/deos/template/target/benchmarks/release/wbuild/deos-runtime/deos_runtime.compact.compressed.wasm +// --pallet +// pallet_deos_actors +// --extrinsic +// crossing_rearm_cohort_preflight +// --steps +// 50 +// --repeat +// 20 +// --heap-pages +// 4096 +// --output +// /tmp/deos-exp-0009-h0-all/crossing_rearm_cohort_preflight.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions for `pallet_deos_actors`. +pub struct WeightInfo(PhantomData); +impl pallet_deos_actors::WeightInfo for WeightInfo { + /// Storage: `Actors::CrossingMemberships` (r:128 w:0) + /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:128 w:0) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorIdentities` (r:128 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorHot` (r:128 w:0) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:128 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorActivationAuthority` (r:128 w:0) + /// Proof: `Actors::ActorActivationAuthority` (`max_values`: None, `max_size`: Some(159), added: 2634, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:128 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) + /// The range of component `c` is `[1, 128]`. + fn crossing_rearm_cohort_preflight(c: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1484 + c * (608 ±0)` + // Estimated: `990 + c * (2699 ±0)` + // Minimum execution time: 41_346_000 picoseconds. + Weight::from_parts(41_835_000, 0) + .saturating_add(Weight::from_parts(0, 990)) + // Standard Error: 28_064 + .saturating_add(Weight::from_parts(22_919_252, 0).saturating_mul(c.into())) + .saturating_add(T::DbWeight::get().reads((7_u64).saturating_mul(c.into()))) + .saturating_add(Weight::from_parts(0, 2699).saturating_mul(c.into())) + } +} diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/h0-crossing_skip_cohort_preflight.rs b/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/h0-crossing_skip_cohort_preflight.rs new file mode 100644 index 00000000..e7094a36 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/h0-crossing_skip_cohort_preflight.rs @@ -0,0 +1,68 @@ + +//! Autogenerated weights for `pallet_deos_actors` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 58.0.0 +//! DATE: 2026-08-29, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `fedora`, CPU: `AMD Ryzen 7 4800H with Radeon Graphics` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 + +// Executed Command: +// frame-omni-bencher +// v1 +// benchmark +// pallet +// --runtime +// /home/llb/Repos/deos/template/target/benchmarks/release/wbuild/deos-runtime/deos_runtime.compact.compressed.wasm +// --pallet +// pallet_deos_actors +// --extrinsic +// crossing_skip_cohort_preflight +// --steps +// 50 +// --repeat +// 20 +// --heap-pages +// 4096 +// --output +// /tmp/deos-exp-0009-h0-all/crossing_skip_cohort_preflight.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions for `pallet_deos_actors`. +pub struct WeightInfo(PhantomData); +impl pallet_deos_actors::WeightInfo for WeightInfo { + /// Storage: `Actors::CrossingMemberships` (r:128 w:0) + /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:128 w:0) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorIdentities` (r:128 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorHot` (r:128 w:0) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:128 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorActivationAuthority` (r:128 w:0) + /// Proof: `Actors::ActorActivationAuthority` (`max_values`: None, `max_size`: Some(159), added: 2634, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:128 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) + /// The range of component `c` is `[1, 128]`. + fn crossing_skip_cohort_preflight(c: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1389 + c * (598 ±0)` + // Estimated: `990 + c * (2699 ±0)` + // Minimum execution time: 37_854_000 picoseconds. + Weight::from_parts(38_553_000, 0) + .saturating_add(Weight::from_parts(0, 990)) + // Standard Error: 28_205 + .saturating_add(Weight::from_parts(22_604_727, 0).saturating_mul(c.into())) + .saturating_add(T::DbWeight::get().reads((7_u64).saturating_mul(c.into()))) + .saturating_add(Weight::from_parts(0, 2699).saturating_mul(c.into())) + } +} diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/h0-crossing_terminal_cohort_preflight.rs b/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/h0-crossing_terminal_cohort_preflight.rs new file mode 100644 index 00000000..f05b27d0 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/h0-crossing_terminal_cohort_preflight.rs @@ -0,0 +1,73 @@ + +//! Autogenerated weights for `pallet_deos_actors` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 58.0.0 +//! DATE: 2026-08-29, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `fedora`, CPU: `AMD Ryzen 7 4800H with Radeon Graphics` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 + +// Executed Command: +// frame-omni-bencher +// v1 +// benchmark +// pallet +// --runtime +// /home/llb/Repos/deos/template/target/benchmarks/release/wbuild/deos-runtime/deos_runtime.compact.compressed.wasm +// --pallet +// pallet_deos_actors +// --extrinsic +// crossing_terminal_cohort_preflight +// --steps +// 50 +// --repeat +// 20 +// --heap-pages +// 4096 +// --output +// /tmp/deos-exp-0009-h0-all/crossing_terminal_cohort_preflight.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions for `pallet_deos_actors`. +pub struct WeightInfo(PhantomData); +impl pallet_deos_actors::WeightInfo for WeightInfo { + /// Storage: `Actors::CrossingMemberships` (r:128 w:0) + /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:128 w:0) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorIdentities` (r:128 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorHot` (r:128 w:0) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:128 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorActivationAuthority` (r:128 w:0) + /// Proof: `Actors::ActorActivationAuthority` (`max_values`: None, `max_size`: Some(159), added: 2634, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:128 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) + /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) + /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + /// Storage: `Actors::NextQueueTicket` (r:1 w:0) + /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// The range of component `c` is `[1, 128]`. + fn crossing_terminal_cohort_preflight(c: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1442 + c * (598 ±0)` + // Estimated: `1493 + c * (2699 ±0)` + // Minimum execution time: 42_255_000 picoseconds. + Weight::from_parts(42_813_000, 0) + .saturating_add(Weight::from_parts(0, 1493)) + // Standard Error: 31_740 + .saturating_add(Weight::from_parts(23_962_103, 0).saturating_mul(c.into())) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().reads((7_u64).saturating_mul(c.into()))) + .saturating_add(Weight::from_parts(0, 2699).saturating_mul(c.into())) + } +} diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/n64-crossing_placed_non_tail_emptied_unit.rs b/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/n64-crossing_placed_non_tail_emptied_unit.rs new file mode 100644 index 00000000..d3854387 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/n64-crossing_placed_non_tail_emptied_unit.rs @@ -0,0 +1,97 @@ + +//! Autogenerated weights for `pallet_deos_actors` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 58.0.0 +//! DATE: 2026-08-29, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `fedora`, CPU: `AMD Ryzen 7 4800H with Radeon Graphics` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 + +// Executed Command: +// frame-omni-bencher +// v1 +// benchmark +// pallet +// --runtime +// /home/llb/Repos/deos/template/target/benchmarks/release/wbuild/deos-runtime/deos_runtime.compact.compressed.wasm +// --pallet +// pallet_deos_actors +// --extrinsic +// crossing_placed_non_tail_emptied_unit +// --steps +// 50 +// --repeat +// 20 +// --heap-pages +// 4096 +// --output +// /tmp/deos-exp-0009-n64/crossing_placed_non_tail_emptied_unit.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions for `pallet_deos_actors`. +pub struct WeightInfo(PhantomData); +impl pallet_deos_actors::WeightInfo for WeightInfo { + /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:1) + /// Proof: `Actors::CrossingPendingFeedListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingTransitionQueues` (r:1 w:0) + /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(3171), added: 5646, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingRangeCursors` (r:1 w:1) + /// Proof: `Actors::CrossingRangeCursors` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingLeafStates` (r:2 w:2) + /// Proof: `Actors::CrossingLeafStates` (`max_values`: None, `max_size`: Some(62), added: 2537, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingMemberPages` (r:3 w:3) + /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(8264), added: 10739, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingMemberships` (r:128 w:192) + /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:64 w:64) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorIdentities` (r:64 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorHot` (r:64 w:64) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:64 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorActivationAuthority` (r:64 w:0) + /// Proof: `Actors::ActorActivationAuthority` (`max_values`: None, `max_size`: Some(159), added: 2634, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:64 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) + /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) + /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + /// Storage: `Actors::NextQueueTicket` (r:1 w:1) + /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueHead` (r:1 w:0) + /// Proof: `Actors::QueueHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueTail` (r:1 w:1) + /// Proof: `Actors::QueueTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueOccupancy` (r:1 w:1) + /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueuePages` (r:1 w:1) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingFeedMembershipCount` (r:1 w:1) + /// Proof: `Actors::CrossingFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingUserFeedMembershipCount` (r:1 w:1) + /// Proof: `Actors::CrossingUserFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingRadixNodes` (r:32 w:32) + /// Proof: `Actors::CrossingRadixNodes` (`max_values`: None, `max_size`: Some(53), added: 2528, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingPendingFeeds` (r:1 w:0) + /// Proof: `Actors::CrossingPendingFeeds` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) + fn crossing_placed_non_tail_emptied_unit() -> Weight { + // Proof Size summary in bytes: + // Measured: `76353` + // Estimated: `327262` + // Minimum execution time: 3_941_347_000 picoseconds. + Weight::from_parts(4_070_555_000, 0) + .saturating_add(Weight::from_parts(0, 327262)) + .saturating_add(T::DbWeight::get().reads(563)) + .saturating_add(T::DbWeight::get().writes(367)) + } +} diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/n64-crossing_placed_non_tail_trimmed_unit.rs b/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/n64-crossing_placed_non_tail_trimmed_unit.rs new file mode 100644 index 00000000..f3176e0e --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0009/n64-crossing_placed_non_tail_trimmed_unit.rs @@ -0,0 +1,97 @@ + +//! Autogenerated weights for `pallet_deos_actors` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 58.0.0 +//! DATE: 2026-08-29, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `fedora`, CPU: `AMD Ryzen 7 4800H with Radeon Graphics` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 + +// Executed Command: +// frame-omni-bencher +// v1 +// benchmark +// pallet +// --runtime +// /home/llb/Repos/deos/template/target/benchmarks/release/wbuild/deos-runtime/deos_runtime.compact.compressed.wasm +// --pallet +// pallet_deos_actors +// --extrinsic +// crossing_placed_non_tail_trimmed_unit +// --steps +// 50 +// --repeat +// 20 +// --heap-pages +// 4096 +// --output +// /tmp/deos-exp-0009-n64/crossing_placed_non_tail_trimmed_unit.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions for `pallet_deos_actors`. +pub struct WeightInfo(PhantomData); +impl pallet_deos_actors::WeightInfo for WeightInfo { + /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:1) + /// Proof: `Actors::CrossingPendingFeedListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingTransitionQueues` (r:1 w:0) + /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(3171), added: 5646, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingRangeCursors` (r:1 w:1) + /// Proof: `Actors::CrossingRangeCursors` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingLeafStates` (r:2 w:2) + /// Proof: `Actors::CrossingLeafStates` (`max_values`: None, `max_size`: Some(62), added: 2537, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingMemberPages` (r:3 w:3) + /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(8264), added: 10739, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingMemberships` (r:128 w:194) + /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:64 w:64) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorIdentities` (r:64 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorHot` (r:64 w:64) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:64 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorActivationAuthority` (r:64 w:0) + /// Proof: `Actors::ActorActivationAuthority` (`max_values`: None, `max_size`: Some(159), added: 2634, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:64 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) + /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) + /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + /// Storage: `Actors::NextQueueTicket` (r:1 w:1) + /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueHead` (r:1 w:0) + /// Proof: `Actors::QueueHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueTail` (r:1 w:1) + /// Proof: `Actors::QueueTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueOccupancy` (r:1 w:1) + /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueuePages` (r:1 w:1) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingFeedMembershipCount` (r:1 w:1) + /// Proof: `Actors::CrossingFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingUserFeedMembershipCount` (r:1 w:1) + /// Proof: `Actors::CrossingUserFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingRadixNodes` (r:32 w:32) + /// Proof: `Actors::CrossingRadixNodes` (`max_values`: None, `max_size`: Some(53), added: 2528, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingPendingFeeds` (r:1 w:0) + /// Proof: `Actors::CrossingPendingFeeds` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) + fn crossing_placed_non_tail_trimmed_unit() -> Weight { + // Proof Size summary in bytes: + // Measured: `76648` + // Estimated: `327262` + // Minimum execution time: 3_961_881_000 picoseconds. + Weight::from_parts(4_059_241_000, 0) + .saturating_add(Weight::from_parts(0, 327262)) + .saturating_add(T::DbWeight::get().reads(563)) + .saturating_add(T::DbWeight::get().writes(369)) + } +} diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0010.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0010.md new file mode 100644 index 00000000..07a15420 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0010.md @@ -0,0 +1,198 @@ +# EXP-0010 — Compact Observation Activation Authority + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Architecture release / experiment campaign | DEOS 0.7.24 baseline / 0.7.24 reactive-scaling campaign | +| Date | 2026-08-26 | +| Affected domain | DEOS Actors broad observation fanout, activation classification, and Contract/run storage proof | +| Physical mechanism | Compact activation authority and bounded fanout branch Weight | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0002](./EXP-0002.md) | +| Successor experiment | None | + +## Decision Question + +Which conforming activation-authority geometry lets broad observation fanout latch and request canonical future placement without loading or statically charging maximum Contract Step resources, funding payload, or run payload, while preserving exact terminal classification, FIFO/wakeup placement, fault isolation, and production Weight soundness for coalesced, placed, and terminal branches? + +## Governing Specification + +- `Exact sections`: [`template/pallets/actors/docs/specification.en.md` §1.5 Causal Readiness, §2 Trigger Matrix and Unified Placement, §5.2 Observation Topology, and §5.4 Mandatory Prepass](../../../../../template/pallets/actors/docs/specification.en.md); [`docs/actors-resource-policy.specification.en.md` §1 Resource Domain and §4 Admission and Actual Weight](../../../../../docs/actors-resource-policy.specification.en.md); [`docs/actors-performance-assurance.specification.en.md` §1 Evidence Contract and §4 Contract Complexity Matrix](../../../../../docs/actors-performance-assurance.specification.en.md). +- `Invariant — one activation contract`: Fanout sets the sole `pending_signal` latch and requests the same canonical placement semantics as every external readiness source; it may not create a second ready queue, scheduler, priority, or off-chain authority. +- `Invariant — exact terminal behavior`: Window expiry, cycle-nonce exhaustion, pause, cooldown, Running/Suspended placement, stale membership, and capacity failure remain fail-closed and transactionally equivalent to generic activation. +- `Invariant — No Ceiling Tax`: Raising B8 to P32 may not increase fixed fanout ProofSize, reads, writes, or retained short-Actor bytes solely because unused maximum admission/funding/run capacity exists. +- `Reopen trigger`: Reopen Specification only if every bounded single-placement geometry requires maximum execution payload to classify readiness correctly; a losing physical projection does not justify semantic relaxation. + +## Context and Hypothesis + +- `Current architecture`: Broad fanout visits one occupied subscriber page and invokes generic `request_activation`. Generic activation reconstructs admitted Contract state and loads `ActorFunding` plus `ActorRunState` before classification. A one-Step Idle subscriber does not read tail chunks, but generated fixed Weight still inherits maximum encoded admission, funding, and run bounds. +- `Measured gap`: EXP-0002 final P32 handoff raised dense-page ProofSize from B8 `300,510` to P32 `331,998`, reducing service under the unchanged 20% block envelope from three pages to two even though the subscriber Contract remained one Step. +- `Reason`: The normative 32-Step ceiling cannot bind while fanout service capacity is coupled to unused execution payload maxima. +- `Hypothesis`: A compact authority containing only the immutable Contract identity and readiness/terminal fields needed before placement can make B8/P32 fanout Weight structurally identical; extended admission resources, funding maps, run snapshots, and Step payload remain execution-owned and load only after placement reaches service. +- `Materiality`: Any B8/P32 difference in fixed fanout ProofSize/read/write topology is material. A candidate also fails if it adds one short FIFO storage key/read, weakens authority validation, delays terminal substitution beyond the canonical activation contract, or exceeds ±5% B8 RefTime without a strictly lower binding ProofSize dimension. + +## Baseline and Candidates + +| ID | Architecture | Source tree | Runtime/config identity | +| --- | --- | --- | --- | +| C6-B | Generic activation reconstructs current admitted state and loads funding/run authority | HEAD `d2e1ab4f171f9dadec5bfb260e14e4b36bb7dabf`; tree `a038f887542f04133371dfe3f38942393b7efdcc`; template manifest `9497317092f071f7397ad8f805096af3823341ddd220b8b253cbb622dae19fbf` | Production B8 `(8,10,16,32)`; Wasm `649aa5a729aa30fd858bbad7db239afb71824ba613ab51911bcc02ce2d24280a` | +| A | Fixed C6-bound activation projection; fragment-local Step resources; mutable `ActorRunHead` plus immutable snapshot payload; durable intra-page branch cursor; contiguous ordinary/scalar-terminal admission | Interpreted B8 manifest `72f5f686a3760e31df24e08382065fab5f69fd3941def1636f515c2e5c9e08cf`; controlled B8/P32 matrix retained | B8 and P32 profiles | +| B | Compact readiness/terminal projection colocated in `ActorHot`, leaving semantic Contract fields under C6 ownership | Rejected before source preparation | Would widen every FIFO/hot-state proof | +| C | Canonical C6 head split into header authority and unique Step-0 payload so generic activation reads only the header | Rejected before source preparation | Would add one key/read to every current-Step service path | + +Candidate A's fixed projection contains semantic Contract ID, body commitment, admission identity, observation feed, cooldown, schedule window, and optional terminal cycle nonce. The three C6 authority IDs bind projection identity without widening `ActorIdentity` or `ActorHot`; lifecycle transactions derive/write/delete the projection with Contract geometry, and TryRuntime proves exact field and authority equality. + +Preparation dependency review found that projection-only activation is insufficient: exact generic classification also owns current-Step User fee viability, dynamic native balance, Running/Suspended cursor and retry coherence, retry-attempt terminal substitution, and current eligibility. Candidate A therefore tiers existing authority rather than caching a lossy summary. C6 head/tail fragments co-locate each Step's admitted resource envelope; the admission certificate retains compact runtime/body identity only. `ActorRunHead` owns mutable scalar authority, cursor, eligibility, retry, outcomes, and suspension; immutable Opening/funding snapshots move to `ActorRunPayload`, written once at Opening and removed at termination. Ordinary fanout loads identity/hot, activation projection, one current fragment, compact run head when non-Idle, and the dynamic native account only for User viability. It does not load funding state, admission-resource vectors, unreached Steps, or run payload. + +Fanout requires projection presence and later execution revalidates its C6 IDs before semantic mutation, classifies window/cycle terminal state from the projection plus `ActorIdentity`/`ActorHot`, and never reads Step resources, funding, or run payload for ordinary activation. Running/Suspended actors must already carry canonical queue/wakeup placement and only latch/coalesce. Idle actors receive the same computed ready-FIFO or temporal-wakeup placement as generic activation. A durable subscriber-page position permits a compactly preflighted contiguous ordinary prefix to commit without charging a following terminal actor; a terminal actor executes under a separate scalar cleanup owner, and progress advances only after that branch commits. No second readiness queue or scheduler is introduced. + +A candidate is conforming only if every duplicated field remains an explicitly derived projection with C6 as semantic source, one generation owner, transactional drift prevention, and fail-closed stale-generation tests. + +## Controls + +- `Controlled variables`: C6 tail chunks; Step and Task representation; Actor IDs and page size 64; subscriber page occupancy; trigger/feed; one-Step short Contract; lifecycle state; queue/wakeup capacities; 20% fanout envelope; block Weight; database backend; benchmark tool; 50 steps/20 repeats; B8/P32 tuple relation; production Weight template; Rust/SDK line. +- `Changed variables`: Candidate A adds one fixed activation-projection key, co-locates Step resources with existing C6 fragments, splits mutable `ActorRunHead` from immutable `ActorRunPayload`, adds one bounded dirty-page position, and introduces compact ordinary cohort plus disjoint scalar terminal owners; `ActorIdentity`, `ActorHot`, authored Contract semantics, and funding state remain unchanged. +- `Confounders`: Generated models remain branch-specific rather than one monolithic fanout call. Queue, wakeup, coalesced, branch probe, terminal, exact fault record, and fault clear have disjoint owners; deterministic tests hold branch selection, rollback, retry pacing, and terminal precedence constant. + +## Benchmark Class, Workloads, and Criteria + +- `Benchmark classes`: Pallet production-Wasm benchmark, deterministic semantic integration test, encoded-state profile, generated-Weight comparison, and exact-tree runtime validation. + +| Workload | State/population geometry | Hot/cold assumptions | Purpose | +| --- | --- | --- | --- | +| W-FP | One full page of identical one-Step Idle `ObservationChange` subscribers requiring ready FIFO placement | Contract payload cold; compact activation authority hot | Primary B8/P32 ceiling-tax falsifier | +| W-FW | One full page requiring future cooldown/window wakeup placement | No Step/funding/run payload read | Temporal placement parity | +| W-FC | One full page already carrying canonical queue or wakeup placement | Hot coalescing only | Redundant placement elimination | +| W-FT | Bounded terminal candidate at window/cycle boundary | Full cleanup only in its declared terminal owner | Terminal substitution and Weight completeness | +| W-FX | Stale/corrupt membership and saturated queue/wakeup topology | Fail closed with exact page/source fault | Fault and retry parity | +| W-S1 | One-Step FIFO execution and actual retained short state | Current Step hot, no unread tail | Candidate overhead guard | +| W-MAX | Maximum P32 Contract/run/funding subscriber and lifecycle cleanup | Extended payload intentionally reachable | Maximum branch soundness | + +- `Metrics`: Generated RefTime/ProofSize, reads/writes, actual proof, page service under fixed envelope, encoded short and maximum bytes, loaded keys, lifecycle writes, Wasm hash/size, exact activation outcomes, queue/wakeup topology, terminal cleanup, and fault state. +- `Statistical method`: Generated Weight uses 50 steps and 20 repeats. Retain five focused 50/20 samples for W-FP per profile with no outlier removal; compare medians only after storage/read/write identity is established. Deterministic semantic and encoded-state tests use exact equality. +- `Acceptance criteria`: B8/P32 W-FP ProofSize/reads/writes are identical; fixed service remains at least three pages; W-S1 gains no storage read/key and stays within ±5% RefTime/ProofSize; all activation outcomes and terminal cleanup match the generic baseline; W-MAX fits component-wise; one candidate is Pareto-dominant or wins the binding ProofSize dimension without semantic loss. +- `Rejection criteria`: Any stale authority acceptance, second scheduler/queue, same-block execution, terminal delay, short FIFO read/key increase, B8/P32 fanout structural difference, fixed service below three pages, unbounded scan, or component-wise Weight failure. + +## Environment and Artifact Identity + +| Artifact | Identity | +| --- | --- | +| Source commit | `d2e1ab4f171f9dadec5bfb260e14e4b36bb7dabf` | +| Source tree | `a038f887542f04133371dfe3f38942393b7efdcc` | +| Rust toolchain | `rustc 1.98.0 (88d9e12ae 2026-08-18)`; Cargo `1.98.0 (797e8a9bc 2026-08-05)` | +| Benchmark runtime Wasm | Measured B8 `4aa4adba01042320db2145ef2cd797dd2fa36fc3c2cae647cf7b00066a5343d0` (5,987,320 bytes); P32 `c1786b0d7a3e110ce0698f51f1b66afd1fdc26b26ec96b755741140a0a8fca90` (5,988,477 bytes) | +| Post-generation production runtime Wasm | Integrated P32 `20df93fe148f7e8317c8e3650453b9aad19bd754d8d86c3f109c02105edc5983` (1,412,191 bytes); compact-certificate B8 `32c331ed`; historical pre-compaction candidate `48993d21`; production baseline `649aa5a7` | +| Final release production runtime Wasm | Not measured | +| Generated Weight | Integrated P32 Actors/Router/TMC `e83493decf683b130ca9c15be97e7bf8b33c9e72c571693b75b9783dd831f6a1` / `22483b5ea28ffe7e79fdea3ba92f13dc62df71ff162918bd3d99dfc3eed8ceef` / `851d61b5f1e421630701b3c5cea63886b23d548dd1ad7d880eac5a077d08ab0`; compact-certificate B8 Actors `996a4415`; historical pre-compaction candidate `0627f3e3` | +| Integrated P32 metadata/control plane | Metadata `6dd2cbe937a0f06380776220a4fda38e881400830b1a3f7948aeaae91a23f7f0`; ABI `ae481a6c79f8dbbfed0d5e35738caac2467adb1266f358377acd9bdc9351c4bf`; bounds `23ab8ac0`; observation evidence `273c4430` | +| Database/backend | Wasm benchmark state machine; database cache 1,024 | +| Benchmark command/config | Candidate `cargo build --release --locked -p deos-runtime --features runtime-benchmarks`; benchmark `observation_fanout_{base,branch_probe,page,wakeup_page,coalesced_page,terminal}`, `record_observation_fanout_worker_fault`, and `clear_observation_fanout_worker_fault` with `frame-omni-bencher v1 benchmark pallet --runtime --pallet pallet_deos_actors --extrinsic --steps 50 --repeat 20 --heap-pages 4096 --output `; semantic control `cargo test -p pallet-deos-actors --features runtime-benchmarks --locked` | +| Raw evidence | Candidate `/tmp/deos-exp-0010-A/repo`; post-compaction handoff `/tmp/deos-exp-0010-P32/repo`; interpreted B8 `72f5f686`; eight branch/control outputs/profile; five retained post-compaction W-S1/W-FP outputs/profile; P32 lifecycle/metadata/ABI/matching-Wasm tree; digests retained | + +## Measurements + +| Candidate | Workload | RefTime | ProofSize | Reads | Writes | Other decision metrics | +| --- | --- | ---: | ---: | ---: | ---: | --- | +| C6-B B8 | W-FP | `21,152,536,000` | `300,510` | Generated page model retained in baseline Weight | Generated page model retained in baseline Weight | Three pages under fixed envelope | +| C6-B P32 | W-FP | `21,261,200,000` | `331,998` | Generated page model retained in invalidated Weight evidence | Generated page model retained in invalidated Weight evidence | Two pages; EXP-0002 invalidation witness | +| Integrated B8 | Complete post-compaction generation | Fanout base/probe/queue/wakeup/coalesced/terminal `6,286,000 / 12,991,000 / 1,510,482,000 / 1,925,136,000 / 1,821,350,000 / 216,511,000` | Ordinary `302,558`; terminal `5,702` | Exact generated owners | Exact generated owners | Certificate maximum `208` bytes; benchmark Wasm `e9696b5b`; dependent Router/TMC Weight-renewed production Wasm `32c331ed` | +| Integrated P32 | Complete post-compaction generation | Fanout base/probe/queue/wakeup/coalesced/terminal `6,216,000 / 13,060,000 / 1,506,361,000 / 1,899,225,000 / 1,816,601,000 / 210,714,000` | Ordinary `302,558`; terminal `5,702` | Exact generated owners | Exact generated owners | Fault record/clear `20,394,000 / 13,899,000`; certificate maximum `208` bytes; benchmark Wasm `7626c71f`; production Wasm `20df93fe` | +| A B8 / P32 | W-FB base probe | `6,216,000` / `6,216,000` | Estimated `1,629`; measured `393` | `2` | `0` | Enlarged exact fault value; exact physical identity | +| A B8 / P32 | W-FQ queue page | `1,499,167,000` / `1,484,640,000` | Estimated `302,558`; measured `33,875` | `332` | `72` | One append plan for 64 Actors; exact physical identity | +| A B8 / P32 | W-FW wakeup page | `1,899,015,000` / `1,915,149,000` | Estimated `302,558`; measured `33,113` | `328` | `71` | One same-key wakeup cohort; exact physical identity; ordinary RefTime maximum | +| A B8 / P32 | W-FC coalesced page | `1,830,988,000` / `1,795,718,000` | Estimated `302,558`; measured `33,338` | `325` | `2` | Already-live page; exact physical identity | +| A B8 / P32 | W-BP branch probe | `12,921,000` / `12,921,000` | Estimated `3,569`; measured `679` | `2` | `0` | Retry-block-aware branch admission owner | +| A B8 / P32 | W-FT terminal | `210,505,000` / `211,832,000` | Estimated `5,702`; measured `3,221` | `25` | `24` | Scalar cleanup remains proof-identical despite wider cold maxima | +| A B8 / P32 | W-FF fault snapshot + record | `20,603,000` / `20,883,000` | Estimated `4,106`; measured `709` | `3` | `1` | Exact slot/C6 snapshot; profile-identical storage | +| A B8 / P32 | W-FX fault clear | `13,969,000` / `14,248,000` | Estimated `1,629`; measured `454` | `1` | `1` | Enlarged fault value; profile-identical storage | +| A B8 | W-S1 short FIFO execution | Median `278,834,455` at `n=1`; samples `287,029,790 / 278,834,455 / 279,472,283 / 277,450,176 / 277,270,825` | `7,076` | `12` | `7` | No new key/read | +| A P32 | W-S1 short FIFO execution | Median `277,568,642` at `n=1`; samples `277,451,031 / 277,568,642 / 288,877,622 / 279,976,263 / 277,384,743` | `7,076` | `12` | `7` | Exact B8 storage model | +| A B8 / P32 | W-FP five-sample queue page | Medians `1,502,310,000 / 1,477,237,000`; B8 `1,491,695,000 / 1,502,310,000 / 1,503,987,000 / 1,502,519,000 / 1,484,430,000`; P32 `1,482,195,000 / 1,477,237,000 / 1,466,271,000 / 1,476,679,000 / 1,493,859,000` | `302,558` | `332` | `72` | No outlier removal; exact profile structure | +| A B8 / P32 | W-MAX lifecycle | Create `335,174,000 / 420,312,000`; activate `295,224,000 / 493,646,000`; deactivate `421,220,000 / 572,079,000`; update `631,865,000 / 857,805,000`; close `482,821,000 / 629,699,000` | All `81,886` | `57/63`; `50/56`; `50/56`; `58/64`; `55/61` | `51/57`; `45/51`; `48/54`; `54/60`; `55/61` | Dispatch envelope test passes at P32 | +| Integrated B8 / P32 | Renewed W-S1 | Medians `279,839,923 / 278,171,778`; B8 `278,946,627 / 286,422,064 / 280,687,673 / 279,839,923 / 276,930,408`; P32 `277,466,141 / 278,907,747 / 280,946,377 / 277,960,636 / 278,171,778` | `7,064` | `12` | `7` | P32 median `-0.60%`; no outlier removal | +| Integrated B8 / P32 | Renewed W-FP | Medians `1,520,189,000 / 1,475,420,000`; B8 `1,520,189,000 / 1,532,342,000 / 1,503,357,000 / 1,520,888,000 / 1,518,025,000`; P32 `1,475,420,000 / 1,498,679,000 / 1,467,808,000 / 1,481,567,000 / 1,474,443,000` | `302,558` | `332` | `72` | P32 median `-2.94%`; no outlier removal | +| Integrated P32 | Renewed maximum lifecycle | Create `411,372,000`; activate `482,821,000`; deactivate `550,917,000`; update `832,242,000`; close `607,559,000` | All `81,886` | `63 / 56 / 56 / 64 / 61` | `57 / 51 / 54 / 60 / 61` | Maximum Opening `1,396,639,000 / 179,341`; dispatchability passes | +| A P32 | W-MAX retained geometry | Not applicable | Actual bytes | Not applicable | Not applicable | Short head/certificate/tails `131/190/0`; max `131/531/1,562` across 8 tails | +| A | Semantic/control matrix | Not applicable | Not applicable | Not applicable | Not applicable | 760 Actors tests; profile semantics and runtime dispatchability pass | + +## Derived Metrics + +| Candidate | Workload | RefTime delta | ProofSize delta | Throughput / latency | Marginal or amortized cost | Pareto class | +| --- | --- | ---: | ---: | --- | --- | --- | +| C6-B P32 | W-FP | `+0.51%` | `+10.48%` | Fixed service `3 → 2` pages | Unused maximum encoded bounds tax every short page | Dominated / invalidated baseline geometry | +| A P32 vs A B8 | W-FQ / W-FW / W-FC | `-0.97%` / `+0.85%` / `-1.93%` | Exact estimated/read/write match for each branch | Fixed service remains 3 pages under the wakeup RefTime maximum | All ordinary branches share ceiling-invariant proof without cold authority | Structurally equivalent ordinary branches; not decision-complete | +| A P32 vs A B8 | W-BP / W-FT / W-FF / W-FX | `0.00%` / `+0.63%` / `+1.36%` / `+2.00%` | Exact estimated/read/write match | Blocked probe, scalar terminal, and fault control owners | Capacity retry and cold authority do not price ordinary proof | Disjoint control structure passes focused diagnostic | +| A B8 vs C6-B B8 | W-S1 | `-3.65%` | `+0.16%` | Exact `12 / 7` read/write identity | Split run head adds 11 measured proof bytes without a new key/read | Inside predeclared ±5% guard | +| A P32 vs A B8 | W-S1 / W-FP | `-0.45%` / `-1.67%` median | Exact `7,076 / 302,558` | Exact profile read/write identity | No ceiling-dependent short/FIFO proof | Ceiling-invariant candidate structure | +| A B8 / P32 vs C6-B B8 / P32 | W-FP | RefTime `-92.90% / -93.05%` | Proof `+0.68% / -8.87%` | Fixed service `3 / 2 → 3 / 3` | Cohort admission removes repeated generic activation | Wins binding P32 proof/service dimension | +| A B8 vs historical C6-B B8 | Lifecycle | Create `+1.33%`; activate `+1.27%`; deactivate `+3.52%`; update `-0.94%`; close `+8.15%` | Exact lifecycle estimate `81,886` | One derived projection participates in lifecycle mutation | Close has the largest candidate overhead | Non-binding cold-path tradeoff; all P32 maxima fit | + +## Statistical / Variance Notes + +All generated models use 50 steps/20 repeats. W-S1 and W-FP retain five samples/profile with no outlier removal. W-S1 B8 contains one high first sample and P32 one high third sample; medians are reported exactly as predeclared. Every branch has exact estimated ProofSize/read/write identity across profiles. Exact B8/P32 benchmark Wasm, generated outputs, state log, and B8 post-generation Wasm are retained. Interpretation is intentionally deferred. + +## Result + +Required raw measurement and production handoff are complete for B8 and P32: renewed five-sample W-S1 and W-FP, all eight fanout/control owners, maximum lifecycle and Opening, compact certificate geometry, semantic parity, dispatchability, production Weight, metadata, ABI, generated control-plane evidence, and matching Wasm. The DEOS runtime now binds P32. + +## Interpretation + +The binding dimension is P32 ordinary fanout ProofSize because it determines discrete service under the fixed component-wise 20% envelope. Including the separately measured enlarged-fault base, branch probe, ordinary maximum, and reserved fault Weight admits three pages (`3 × 310,233 + 1,629 = 932,328` ProofSize) and rejects a fourth; candidate A therefore restores P32 service `2 → 3` without changing the envelope. Exact B8/P32 proof/read/write identity across every ordinary branch shows this is structural rather than host-latency noise. + +Candidate A is not globally Pareto-dominant: B8 W-FP proof rises `0.68%`, short C6 state gains 11 bytes, and historical B8 close RefTime rises `8.15%` because lifecycle owns one derived projection. These costs are non-binding: B8 service remains three pages, W-S1 has no new key/read and remains within ±5%, and maximum P32 lifecycle Weight is only `857,805,000 / 81,886` against the component-wise dispatch envelope. In exchange, P32 W-FP proof falls `8.87%`, RefTime falls about `93%` against generic activation, and fixed service returns to three pages. + +Semantic validity is supported by exact generic/compact classification parity, canonical queue/wakeup post-state equality, durable retry pacing, rollback-bound fault identity, stale-topology closure, and profile semantics. Production compaction removed admission-resource duplication; renewed P32 Weight, metadata, ABI, control-plane evidence, tests, and matching Wasm converge. Under the predeclared binding-dimension criterion, candidate A is accepted and shipped as the P32 runtime geometry. + +## Decision + +Accepted and integrated. Candidate A is the physical compact Observation activation baseline because it wins the binding P32 ProofSize/service dimension while preserving semantic parity, short-FIFO topology, three-page B8/P32 service, and maximum fit. Its B8 lifecycle/byte overhead remains an explicit non-binding tradeoff. Candidates B and C remain rejected. Complete renewed handoff evidence authorizes and now backs the production P32 binding. + +## Rejected Alternatives and Tradeoffs + +| Candidate | Decision | Evidence-backed reason | +| --- | --- | --- | +| Raise `ObservationFanoutWeightLimit` | Rejected before measurement | Changes held runtime resource policy, masks maximum-payload coupling, and spends more Actor Control instead of removing ceiling tax. | +| Mark the existing benchmark `pov_mode = Measured` only | Rejected before measurement | Ordinary one-Step setup would underprice reachable maximum/terminal activation branches without changing physical reads. | +| Add a second readiness queue | Rejected before measurement | Violates one scheduler, one FIFO/wakeup substrate, and canonical placement authority. | +| Colocate the projection in `ActorHot` | Rejected before source preparation | Every scheduler and current-Step probe reads `ActorHot`; widening it spends fixed proof outside fanout and is dominated by a fanout-owned key. | +| Split C6 header/Step 0 | Rejected before source preparation | Adds one storage key/read to every current-Step service path and violates the W-S1 topology guard by construction. | + +## Validity + +- `Establishes`: Candidate A restores three-page P32 service with ceiling-invariant ordinary branch structure, preserves short-FIFO topology and semantic parity in the measured matrix, and fits maximum P32 lifecycle dispatch component-wise. +- `Does not establish`: Release readiness, live-chain throughput, or fanout throughput beyond the fixed reference envelope. +- `Known limitations`: The complete generated handoff establishes the P32 physical/runtime contract only; actual-current branch fee settlement and broader atomic control ownership remain separately tracked work. +- `Invalidation triggers`: Changes to activation semantics, ActorHot/Contract/admission/funding/run layout, page size, fanout envelope, queue/wakeup topology, Weight template, toolchain, or benchmark fixtures require baseline review. +- `Residual qualitative insight`: A fixed page Weight can remain ceiling-invariant only when every reachable activation branch either uses compact bounded authority or has a separately admitted owner before mutation. + +## Follow-Up / Next Gradient + +```text +P32 ceiling → fanout proof 300,510 → 331,998 → service 3 → 2 +→ generic activation loads maximum execution authority +→ compact branch-complete activation authority +→ B8/P32 fixed-proof identity +``` + +- `Eliminated hypotheses`: The FIFO-only short benchmark covers every readiness path; a larger fanout budget removes ceiling tax; measured-PoV annotation alone proves maximum branch safety. +- `Remaining hypotheses`: None inside the accepted ceiling/activation geometry; declared invalidation triggers reopen measurement. +- `Stop condition`: Satisfied by candidate A. Reopen only through the declared invalidation triggers; production handoff remains ordinary implementation and validation work. + +## Architecture Impact + +- `Affected physical truth`: Activation classification authority, broad fanout page Weight, hot/cold Contract boundary, and reactive service capacity. +- `Architecture update`: Package and DEOS integration architecture record the shipped compact activation geometry, split run state, fragment-local resources, and P32 service envelope. +- `Provenance citation`: EXP-0010 is the physical decision and renewed production-handoff evidence source. + +## Relations + +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: [EXP-0002](./EXP-0002.md) omitted reactive short-path evidence. +- `Contradicts`: None. +- `Depends on`: [EXP-0016](./EXP-0016.md) C6 geometry and [EXP-0013](./EXP-0013.md) loaded-state reuse. +- `Baseline of`: Shipped compact Observation activation and the production P32 ceiling. +- `Confirms`: EXP-0002's final invalidation witness. +- `Invalidates`: None. +- `Semantic status under EXP-0019`: Partially superseded. Compact activation, ordinary fanout, no-ceiling-tax, and queue/wakeup parity remain accepted. The insolvent-User mixed branch and permissionless insolvency-cleanup interpretation are historical only; preserve their raw evidence and rerun only changed branch maxima. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0013.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0013.md new file mode 100644 index 00000000..44db595f --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0013.md @@ -0,0 +1,173 @@ +# EXP-0013 — One-Step Actor Resource Decomposition + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Architecture release / experiment campaign | DEOS 0.7.24 baseline / 0.7.24 topology-scaling campaign | +| Date | 2026-08-25 | +| Affected domain | DEOS Actors one-Step scheduler and execution path | +| Physical mechanism | FIFO service, canonical state loading, queue consumption, and Step orchestration | +| Primary track | [actors](./experiments.md) | +| Related tracks | [adapters](../adapters/experiments.md) | +| Baseline experiment | [EXP-0001](./EXP-0001.md) | +| Successor experiment | EXP-0016 implementation closure, then EXP-0002 | + +## Decision Question + +Which independently owned RefTime, ProofSize, database, and repeated-load components bind current one-Step Actor throughput, and therefore which physical change should be optimized first? + +## Governing Specification + +- `Exact sections`: `template/pallets/actors/docs/specification.en.md` → `Active Execution State Geometry`, `One-Step Commit Boundary`, and `One Service Contract`. +- `Preserved invariants`: Strict FIFO, class-neutral service, causal next-block readiness, one-Step atomicity, exact Weight ownership, bounded work, and no cold unreached-body proof. +- `Reopen trigger`: Only a semantic defect or infeasibility beyond relevant physical designs; a costly implementation path is not a semantic defect. + +## Context and Hypothesis + +- `Current architecture`: FIFO probing, paged-head consumption, and execution each reach canonical Actor partitions. The monolithic `ActorContract` maximum is 8,735 bytes, while scheduler execution benchmarks report combined per-Actor models. +- `Reason`: EXP-0001 measured only 32.24–40.99 completed cheap cycles/block against the 100-Step target. Building body geometries before identifying the binding resource risks optimizing a non-binding dimension. +- `Hypothesis`: Repeated canonical-state loading and monolithic Contract decoding materially contribute to one-Step RefTime/ProofSize; queue mutation and Step orchestration may independently bind. +- `Materiality`: Exact reads/writes and ProofSize deltas are material. RefTime attribution is material above `max(2%, 3 × pooled relative model standard error)`. The first implementation target must address the largest causally isolated conforming component, not merely the easiest benchmark. + +## Baseline and Candidates + +| ID | Architecture | Source tree | Runtime/config identity | +| --- | --- | --- | --- | +| B0 | Historical FIFO probe + loaded paged consume + suffix executor | Dirty template manifest `8fc77022b30059614f17716cf48e6a3a938634b040bf2a68a998550ae232a74e` at HEAD `d2e1ab4f`; benchmark Wasm `a4161bda` | Historical 8-Step diagnostic baseline | +| D1 | Existing isolated owners plus unchanged historical end-to-end coefficient | Same source/Wasm invocation set as B0 | Identical historical runtime/config | +| B1 | Converged C6 one-Step path reloads canonical state for planning and again at commit | Template manifest `5dc3631c24b75c69c1c1b647f99a6d50eb87e94410548c6363ec806bd06df7eb`; benchmark Wasm `7efed56e` | Current 8-Step runtime; 50 steps × 20 repeats; cache 1,024 | +| R1 | Reuse one canonical loaded state and admission certificate through ticket, plan, admission, FIFO consume, and execution | Template manifest `b01b5d141b9810890d7fdf468ecd448820e16bc05bc1a3c25accca357492d82e`; benchmark Wasm `2e51c471` | Only scheduler loaded-state reuse differs from B1 | + +B0/D1 identified the question. B1/R1 is the controlled production-Wasm decision pair after C6 and one-Step plan/commit converged. + +## Controls + +- `Controlled variables`: One cheap one-Step Contract, Actor state, queue page position, runtime limits, production Weight owners, toolchain, database/cache mode, and semantic postconditions. +- `Changed variables`: B1 reloads canonical identity/hot/C6 Contract/funding/run/admission owners during plan preparation and reloads the complete current-Step plan at commit. R1 returns the admission certificate with the first canonical load, constructs the plan from that decoded state, and carries it into the transaction; paged head consumption remains the commit-time ticket/topology revalidation. +- `Confounders`: FRAME storage annotations report unchanged distinct keys even when R1 removes repeated logical loading; therefore RefTime is the binding comparison and unchanged ProofSize/reads are not evidence of no benefit. + +## Benchmark Class, Workloads, and Criteria + +- `Benchmark class`: Production-Wasm FRAME microbenchmarks plus one unchanged end-to-end FIFO one-Step benchmark. +- `Workloads`: One ready StopCycle Actor at preserved-page and deleted-page boundaries; one Transfer control with effect Weight reported separately; warm repeated populations for end-to-end coefficient stability. +- `Metrics`: Component-wise RefTime/ProofSize, standard error, reads/writes, loaded partition/fragment count, encoded bytes, and end-to-end coefficient. +- `Statistical method`: 50 steps and 20 repeats with no removed samples; independently measured components must reconcile against end-to-end within the declared overlap model. +- `Acceptance criteria`: Every isolated owner has identical semantic postconditions; overlap is explicit; the largest material causal owner is identified; the proposed first optimization addresses that owner without weakening invariants. +- `Rejection criteria`: Non-matching Wasm/source, additive treatment of overlapping models, hidden effect Weight, or inability to preserve identical state/postconditions. + +## Environment and Artifact Identity + +| Artifact | Identity | +| --- | --- | +| Existing generated Weight | `template/runtime/src/weights/pallet_deos_actors.rs`; SHA-256 `31df0c32b1ee726e1f9531b679c2a8462bed8c038aa9558b3747334d2ee14e94` | +| Existing benchmark harness | `scripts/benchmarks.sh`; SHA-256 `f932b293093c751bd3ab6f9870b0ac45014eef2193133bd909f54869f9383818` | +| Toolchain | `rustc 1.98.0 (88d9e12ae 2026-08-18)`; `cargo 1.98.0 (797e8a9bc 2026-08-05)` | +| Exact measured source | HEAD `d2e1ab4f171f9dadec5bfb260e14e4b36bb7dabf`; dirty template manifest `8fc77022b30059614f17716cf48e6a3a938634b040bf2a68a998550ae232a74e` | +| Matching benchmark Wasm | SHA-256 `a4161bdae0f0b6b2ef589911410d86260b9041a1c6518c639558f3d5eac2f3eb` | +| D1 method | Existing production benchmark owners, 50 steps × 20 repeats, compiled Wasm, cache 1,024; no source change between historical runs | +| Historical normalized outputs | State `5f376ae3`; cycle `0dbb570a`; consume `91aca838`; end-to-end `5dc46d68` | +| Historical raw logs | State `7d9b8ab`; cycle `7bd20233`; consume `3828968b`; end-to-end `0e6c9f04`; retained at `/tmp/deos-exp-0013/` | +| B1 focused output | `baseline.rs` SHA-256 `767051c28100b8c0ae49016b3235e113a9c65351cf37ac77ed194574ac5e1f2e`; Wasm SHA-256 `7efed56e6df9fc863236379ffbc9cb9b76cd38f646b41819e1592a29a1bdd21f` | +| R1 focused output | `candidate.rs` SHA-256 `74fa4655fae27afc823377fa0c469cd7ea0c7485320e1b542ea28352cb868c30`; retained Wasm SHA-256 `2e51c471d49b5f251cc6278dc094034d014119c9246c08c040288b38987abac1` | +| Controlled artifacts | `/tmp/deos-exp-0013-loaded-reuse/`; source manifests and artifact digests retained in `SOURCE-MANIFESTS` and `SHA256SUMS` | +| Initial production handoff | Complete generated Actors Weight SHA-256 `708d9c542b7c3a41be0a9d651b259bc58a1a739ccc58c87601656d56ad92d9eb`; benchmark Wasm SHA-256 `b3599ab03252973923c18979c4b5098703d54f9cb8a0ef15edc3bab1507662d8` | +| R2 implementation-correction candidate | HEAD `5c02608e`; tracked-tree manifest `7941d02f`; dirty diff `ec5889d`; focused output `47862f4a`; matching benchmark Wasm `f457927f`; retained at `/tmp/deos-i4-atomic/opening-progress-inner-v4.rs` | +| R2 completion correction | Pre-correction output `bc15998e`; corrected minimal/maximal outputs `283263f8` / `068a3cfa`; matching benchmark Wasm `11ea3e66`; retained under `/tmp/deos-i4-atomic/` | +| R2 retry correction | Pre-correction minimal output `b5ee17fd`; corrected minimal/maximal outputs `f8741fa6` / `66299064`; matching benchmark Wasm `c45b7007`; retained under `/tmp/deos-i4-atomic/` | +| R2 non-closing failure owners | Minimal/maximal outputs `3c3a9fc6` / `cca2032f`; matching benchmark Wasm `76400312`; retained under `/tmp/deos-i4-atomic/` | +| R2 terminal-close assurance | Pre-correction/corrected minimal outputs `eb655025` / `5908cfb7`; maximum output `816c43a0`; matching benchmark Wasm `fe3ff004`; retained under `/tmp/deos-i4-atomic/` | + +## Measurements + +| Measured owner | RefTime model | Measured / estimated ProofSize | Reads | Writes | Scope | +| --- | ---: | ---: | ---: | ---: | --- | +| `scheduler_actor_state_probe` | 38,134,000 | 1,988 / 12,200 | 5 | 0 | Loads all canonical Actor partitions | +| `cycle_orchestration` | 52,172,000 | 1,279 / 12,200 | 5 | 3 | One preloaded-view StopCycle execution and finalization | +| `scheduler_paged_consume_preserve_page` | 49,099,000 | 1,419 / 4,884 | 9 | 3 | Generic non-loaded consume; diagnostic only, not the production loaded branch | +| Historical `scheduler_paged_execute_cheap(n)` | 124,250,000 + 107,270,178 × n | 1,482 + 275 × n / 3,726 + 2,750 × n | 6 + 5 × n | 4 + 3 × n | Pre-C6 diagnostic cohort | +| B1 converged reload baseline | 175,514,000 + 167,553,076 × n | 1,687 + 678 × n / 3,911 + 3,154 × n | 6 + 6 × n | 4 + 3 × n | Exact current one-Step baseline | +| R1 loaded-state reuse | 147,437,000 + 137,612,859 × n | 1,687 + 678 × n / 3,911 + 3,154 × n | 6 + 6 × n | 4 + 3 × n | Focused candidate reuses one decoded canonical load | +| R1 production generation | 147,717,000 + 137,894,997 × n | 1,687 + 678 × n / 3,911 + 3,154 × n | 6 + 6 × n | 4 + 3 × n | Complete Actors generation confirms the focused model | + +### Post-Acceptance Loaded Placement and Persistence Correction + +The direct P32 maximum-geometry fresh-Opening progress owner exposed two residual reloads after R1: successor placement reconstructed service state, and run persistence reloaded identity, hot state, admission, and the next tail fragment despite carrying a fully reconstructed execution view inside the same atomic attempt. Production now patches the post-Step placement view from the canonical hot owner, reuses that hot owner for queue preflight, and persists the already validated run against the transaction-revalidated FIFO ticket and immutable loaded Contract authority. + +The focused 50-step/20-repeat maximum-geometry model changed from transient v3 output `4fcc1207` to R2 output `47862f4a`: + +| Model | RefTime | Estimated ProofSize | Reads | Writes | +| --- | ---: | ---: | ---: | ---: | +| v3 loaded placement, reload persistence | `218,607,089 + 135,052,469t` | `8,998 + 21,386t` | `15 + 16t` | 8 | +| R2 loaded placement and persistence | `178,698,884 + 127,790,332t` | `7,833 + 21,350t` | `13 + 16t` | 8 | + +At P32 `t = 8`, R2 changes RefTime `1,299,026,841 → 1,201,021,540` (`-7.54%`), ProofSize `180,086 → 178,633` (`-0.81%`), and reads `143 → 141`, with writes unchanged. The v3 matching Wasm was not retained, so this is strong focused implementation-correction evidence rather than a new controlled architecture decision. R1 remains the accepted causal decision; R2 narrows its reuse boundary without changing semantic ownership. + +The fresh-Opening completion benchmark then exposed a second equivalent reconstruction: ordinary non-closing completion reloaded the full Active Contract only to test close policy already present in the loaded execution view. Before correction, the minimal owner was `100,420,049 + 6,883,407t` RefTime, `6,370 + 2,669t` ProofSize, `6 + t` reads, and three writes. Selecting the close condition from loaded immutable policy and reloading complete Active state only when a close is actually selected changed the model to `69,687,645 + 524,747t` RefTime, fixed `5,696` ProofSize, three reads, and three writes. At P32 `t = 8`, this is `155,487,305 → 73,885,621` RefTime (`-52.48%`), `27,722 → 5,696` ProofSize (`-79.45%`), and `14 → 3` reads. A separate maximum-realizable completion owner preserves dependency reads without charging them to the minimal path. + +Fresh-Opening retry exposed the same pattern in wakeup placement. The first direct minimal owner still reloaded current-Step service state through the wakeup helper and measured `150,819,172 + 892,814t` RefTime, `6,476 + 24t` ProofSize, ten reads, and eight writes. Passing the transaction-loaded hot authority through successor scheduling changed the model to `105,824,662 + 467,902t` RefTime, `5,359 + 24t` ProofSize, seven reads, and eight writes. At P32 `t = 8`, this is `157,961,684 → 109,567,878` RefTime (`-30.64%`), `6,668 → 5,551` ProofSize (`-16.75%`), and `10 → 7` reads. The maximum-realizable retry owner remains branch-local at `1,098,763,101` RefTime, `177,918` ProofSize, 137 reads, and eight writes for P32; its dependency reads do not tax the minimal path. + +Fresh-Opening non-closing failure uses a pre-effect invalid split to isolate control work from Task effect Weight. The minimal owner is `61,705,088 + 152,378t` RefTime with fixed `5,633` ProofSize and `3/3` reads/writes; at P32 this is `62,924,112`. The maximum-realizable owner is `102,259,467 + 118,565,318t` RefTime, `7,160 + 21,381t` ProofSize, `6 + 16t` reads, and three writes; at P32 this is `1,050,782,011` RefTime, `178,208` ProofSize, 134 reads, and three writes. Separating these branches prevents Opening dependency work from becoming a failure-path ceiling tax. + +Terminal-close assurance showed that `finalize_actor` redundantly reconstructed the same Active Contract immediately after execution had obtained exact current close authority. A loaded finalizer changed the minimal P32 full-branch RefTime `283,813,297 → 244,982,247` (`-13.68%`) while estimated ProofSize and storage keys remained effectively unchanged at `28,329`, 21 reads, and 20 writes. Maximum-realizable P32 close is `1,405,800,875` RefTime, `185,618` ProofSize, 152 reads, and 20 writes. These full-branch models are assurance, not Step control owners: scheduler admission already reserves `close_actor` separately, terminal cleanup charges no Actors fee, and the logical completed/failed outcome selects its direct Step owner. Substituting the full close model as actual control would double-reserve cleanup and violate fee ownership. + +## Derived Metrics + +- Historical D1 attribution remains non-additive: standalone state probe was 35.55% and cycle orchestration 48.64% of its then-current marginal coefficient. +- Against exact B1, R1 reduces the end-to-end marginal RefTime coefficient by `29,940,217`, or `17.87%`, and the constant by `28,077,000`, or `16.00%`. +- R1 leaves measured/estimated ProofSize models and generated distinct-key read/write counts unchanged. It is a RefTime-only Pareto improvement with no measured component regression. +- B1 pooled coefficient standard error is approximately `217,930`; the measured marginal delta is more than 137 times that error and far above the predeclared 2% floor. +- Unchanged key counts confirm that the win comes from decoded loaded-state reuse and removal of repeated logical access rather than a storage-ownership or proof contract change. + +## Statistical / Variance Notes + +Historical D1 used one matching benchmark Wasm. The controlled B1/R1 pair used separate exact source-bound Wasm artifacts because the changed production path necessarily changes Wasm, with identical toolchain, runtime constants, benchmark command, 50 steps, 20 repeats, cache 1,024, state setup, and semantic postconditions. B1 slope standard error is `145,483`; R1 is `162,214`; no samples were removed. + +## Result + +Historical D1 narrowed the target without causal attribution. The exact controlled B1/R1 pair now establishes that carrying one decoded canonical state and admission certificate through ticket construction, current-Step planning, admission, FIFO consumption, and execution reduces the cheap-Step marginal from `167,553,076` to `137,612,859` RefTime (`17.87%`). ProofSize remains `3,154` estimated per Actor and generated storage counts remain six reads/three writes per Actor. + +## Interpretation + +- `Supported`: Repeated canonical decoding/loading in the converged scheduler was a material RefTime owner; R1 removes that overlap without changing semantic state, storage keys, ProofSize, FIFO order, or writes. +- `Not supported`: Generated read counts cannot reveal repeated logical loading, and the result does not prove full C6 body reconstruction is optimal or that other standalone Weight owners are additive. +- `Residual`: Scheduler still reconstructs the bounded full C6 Contract before building the current-Step plan. EXP-0016 hot-header/current-fragment conversion remains the next distinct mechanism rather than part of this accepted reuse decision. + +## Decision + +Accept R1 as the production loaded-state reuse baseline. Its `17.87%` marginal RefTime reduction is material, component-wise non-regressing, and preserves identical one-Step semantics and storage ownership. Keep one canonical loaded-state/admission result through planning and execution; do not restore independent plan or commit-time Contract reloads. FIFO head consumption remains the atomic ticket/topology revalidation immediately before effect execution. + +## Rejected Alternatives and Tradeoffs + +| Shortcut | Decision | Evidence-backed reason | +| --- | --- | --- | +| Build every EXP-0003 geometry first | Rejected as first move | It spends implementation effort before proving body geometry binds the throughput target. | +| Sum existing Weight owners | Rejected | Their reads and orchestration overlap. | +| Optimize ProofSize only | Rejected | EXP-0001 misses a completed-Step rate and current diagnostics do not identify the binding Weight component. | +| Treat profiling as a release feature | Rejected | D1 exists only to select the production design and must remain minimal. | + +## Validity + +- `Establishes`: Exact B1/R1 source and Wasm identities, a `17.87%` marginal RefTime win, unchanged ProofSize/storage-key models, identical semantic postconditions, and R1 as the selected production reuse topology. +- `Does not establish`: Additive owner attribution, the benefit of replacing full Contract reconstruction with hot-header/current-fragment classification, 32-Step dispatchability, or external-operation overhead. +- `Later topology`: The subsequent EXP-0016 hot-head/current-fragment service conversion retains R1 and measured `135,919,720` marginal RefTime in its focused run. The R2 implementation correction extends loaded-state reuse through successor placement and run persistence. Both supersede earlier production Weight/Wasm handoff identities, not the causal B1/R1 decision. +- `Invalidation triggers`: Removal of loaded-state reuse, a changed semantic workload, canonical owner replacement that defeats the reuse boundary, runtime limits, toolchain, or production-Wasm method changes. + +## Follow-Up / Next Gradient + +- Preserve B1/R1 normalized outputs, source manifests, Wasm identities, and artifact hashes recorded above. +- Promote R1 through complete production Weight generation and matching-Wasm validation. +- Keep full C6 reconstruction removal in accepted EXP-0016 implementation work; measure it only if the converged hot-header path changes a declared target or exposes a new physical choice. +- Proceed to EXP-0002 only after production Weight and the exact current-Step path converge sufficiently for controlled 8/16/32 profiles. + +## Architecture Impact + +R1 is production architecture: scheduler service reuses the canonical state and admission certificate already loaded for classification to construct one `CurrentStepPlanOf`, then carries that plan across admission into the atomic FIFO-consume/execution transaction. R2 extends that same authority through successor placement and run persistence while FIFO head consumption remains the atomic ticket/topology revalidation. The implementation architecture may cite this record after full Weight and validation converge. + +## Relations + +- `Baseline`: EXP-0001 throughput and fairness result. +- `Refines`: EXP-0016 C6 implementation by reusing its canonical loaded owners without changing geometry. +- `Constrains`: EXP-0002 final 8/16/32 acceptance requires the selected R1 current-Step path. +- `Motivates`: Dormant EXP-0017 asks whether the accepted reuse principle could ever earn Q2/Q4 reopening; active EXP-0018 asks whether the retained Q1 Step-0 boundary can become a low-latency Fresh path. This record supplies neither result. +- `Supersedes`: The prior reload-heavy one-Step implementation represented by B1; historical B0/D1 attribution remains valid only for its recorded topology. +- `Semantic status under EXP-0019`: Partially superseded. Loaded-state reuse and every non-closing direct owner remain accepted. Terminal-close assurance remains historical lifecycle-cleanup evidence but no longer establishes apoptosis reservation, fee ownership, or hot scheduler necessity; preserve all artifacts and rerun only changed owner composition. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0016.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0016.md new file mode 100644 index 00000000..da3b455e --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0016.md @@ -0,0 +1,193 @@ +# EXP-0016 — Inline Step 0 with Lazy Tail Geometry + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Architecture release / experiment campaign | DEOS 0.7.24 baseline / 0.7.24 topology-scaling campaign | +| Date | 2026-08-25 | +| Affected domain | DEOS Actors Contract storage, one-Step loading, and bounded lifecycle | +| Physical mechanism | Hot header with uniquely owned inline Step 0 plus lazy authority-bound tail fragments | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0003](./EXP-0003.md) | +| Successor experiment | None | + +## Decision Question + +Does a hot Contract header that uniquely owns Step 0 and stores only Steps 1..N in lazy tail fragments preserve the one-read one-Step path while satisfying no-ceiling-tax, later-cursor loading, bounded lifecycle, exact reconstruction, and production dispatchability better than monolithic B0? + +## Governing Specification + +- `Exact sections`: `template/pallets/actors/docs/specification.en.md` → `Hot/Cold Physical Contract`, `Complete Projection`, `One-Step Commit Boundary`, and `Active Execution State Geometry`. +- `Preserved invariants`: One semantic ordered Contract; no duplicate authored Step owner; exact semantic/body commitments; activation without tail reads; execution loads only header plus the current tail fragment when cursor is nonzero; atomic lifecycle; stale authority fails closed; bounded reconstruction; no ceiling tax. +- `Reopen trigger`: Only demonstrated semantic defect or infeasibility beyond relevant physical designs. Candidate performance failure rejects the candidate, not the specification. + +## Context and Hypothesis + +- `Evidence`: EXP-0003 rejected one-cell C1 because header-plus-cell loading added a read and C1 lost every 1..=8 lifecycle/current-load/reconstruction branch while increasing retained bytes 14.46× at one Step and 21.79× at eight Steps. +- `Design pressure`: The release target is 100 completed one-Step Actor Steps/block, while the accepted 32-Step ceiling forbids monolithic maximum-body proof on short Contracts. +- `Hypothesis`: Uniquely inlining Step 0 removes C1's extra one-Step read and tail record. Lazy tail authority preserves ceiling independence for short Contracts and bounds later-cursor reads. +- `Materiality`: Reads/writes, ProofSize, loaded fragments, encoded bytes, and correctness are exact. RefTime uses `max(2%, 3 × pooled relative model standard error)`. The winner must improve the binding one-Step path without exceeding predeclared lifecycle/dispatchability bounds. + +## Baseline and Candidates + +| ID | Architecture | Source tree | Runtime/config identity | +| --- | --- | --- | --- | +| B0 | Monolithic authored header plus all inline Steps | EXP-0003 manifest `45c43626`; accepted normalized B0 outputs | 8-Step baseline; matching EXP-0003 Wasm `23c83b9f` | +| C5 | Hot header uniquely owns Step 0; Steps 1..N use one-cell fragments | Shared B0/C5 manifest `4cc1dd508cc2589c8634cf78611127224e8ed5a27d997f9bfe9d1697887a8b9e` | Measured; not selected because tail cardinality is material | +| C6 | Hot header uniquely owns Step 0; Steps 1..N use contiguous chunks of at most four Steps | Shared B0/C6 manifest `4aee3998ebe909be875050ed1372c243a79fbeabb7a3a0a5473fb22c918235b9` | Complete focused harness compiles; matching Wasm pending | +| C2 | Two-Step tail chunks | Not earned unless C6 leaves a material unresolved gradient | Not constructed | + +Rejected C1 remains evidence in EXP-0003 and is not a candidate here. + +## Controls + +- `Controlled variables`: Canonical authored Contract, semantic/body hashes, admission identity, Actor class/mutability, one/eight-Step workloads, toolchain, runtime, database/cache mode, benchmark method, and postconditions. +- `Changed variables`: Unique physical ownership of Step 0 and the presence/geometry of tail fragments only. +- `Confounders`: Step payload separation belongs to EXP-0004. Normal Running/I3 execution is not yet available; storage/current-loader evidence cannot establish final end-to-end throughput. + +## Benchmark Class, Workloads, and Criteria + +- `Benchmark class`: Matching production benchmark-Wasm storage/lifecycle/current-loader/reconstruction comparisons plus deterministic retained-state profile and semantic fault tests. +- `Workloads`: One-Step Contract; maximum current 8-Step Contract; Step 0 load; first/middle/last tail load; create/update/close; full reconstruction; missing/stale/wrong-actor/wrong-index tail authority. +- `Metrics`: Component-wise RefTime/ProofSize; reads/writes; encoded persistent bytes; loaded fragments; maximum call dispatch fit; exact semantic/body equality; Wasm size. +- `Statistical method`: 50 steps, 20 repeats, no sample removal, one matching Wasm per controlled matrix. +- `Acceptance criteria`: One-Step current load uses one storage read, no tail record, and no higher estimated ProofSize or material RefTime than matched B0; tail current load is bounded independently of global ceiling; exact reconstruction/fault tests pass; persistent growth is linear only in authored tail Steps; and the conservative sum of existing full lifecycle Weight plus C5 micro-storage Weight remains inside each production dispatch envelope component-wise before production integration. +- `Rejection criteria`: Extra one-Step read/tail state, maximum-ceiling proof on one-Step Contracts, duplicate authored Step ownership, stale fragment acceptance, unbounded cleanup, non-dispatchable lifecycle, or a material one-Step regression without a release-target benefit. + +## Environment and Artifact Identity + +- `Repository`: C5 evidence remains frozen at manifest `4cc1dd50`; the shared B0/C6 harness is frozen at manifest `4aee3998ebe909be875050ed1372c243a79fbeabb7a3a0a5473fb22c918235b9` at HEAD `d2e1ab4f`. +- `Baseline`: EXP-0003 B0 normalized outputs and raw hashes remain authoritative; rerun B0 only if source/runtime/harness controls drift. +- `Toolchain`: `rustc 1.98.0 (88d9e12ae 2026-08-18)`; `cargo 1.98.0 (797e8a9bc 2026-08-05)`; benchmark script SHA-256 `f932b293093c751bd3ab6f9870b0ac45014eef2193133bd909f54869f9383818`. +- `Harness`: Frozen B0/C6 pairs for create, close, reconstruction, and tail load; deterministic B0/C5/C6 retained-state profile; exact unique ownership/wrong-actor/wrong-index tests. C6 reuses the unchanged C5 head and does not duplicate its measured Step-0 load. +- `Matching benchmark Wasm`: SHA-256 `c9c09e3f4647ba2664a53e8cd5f57e28c2b0506a6483458fb8dd3811c5eba89c`, built once from manifest `4cc1dd50`. +- `Artifacts`: Twelve normalized outputs/logs, authority test, state profile, matching Wasm, and digest manifest retained at `/tmp/deos-exp-0016-prepared/`. +- `Temporary-code boundary`: Candidate stores/functions existed only under `runtime-benchmarks`; after selection, rejected C5 code and every experiment-named project symbol/profile were deleted, while the selected comparison was promoted to ordinary project-owned monolithic/chunked benchmarks and a generic geometry profile. + +### Canonical Commands + +```bash +out=/tmp/deos-exp-0016-prepared +rm -rf "$out" && mkdir -p "$out" +skip=() +for branch in create update close reconstruct; do + DEOS_VERBOSE=1 ./scripts/benchmarks.sh "${skip[@]}" --steps 50 --repeat 20 --extrinsic "exp_0016_b0_${branch}" --output "$out/b0-${branch}.rs" pallet_deos_actors 2>&1 | tee "$out/b0-${branch}.log" + skip=(--skip-build) + DEOS_VERBOSE=1 ./scripts/benchmarks.sh --skip-build --steps 50 --repeat 20 --extrinsic "exp_0016_c5_${branch}" --output "$out/c5-${branch}.rs" pallet_deos_actors 2>&1 | tee "$out/c5-${branch}.log" +done +for branch in load_first load_tail; do + DEOS_VERBOSE=1 ./scripts/benchmarks.sh --skip-build --steps 50 --repeat 20 --extrinsic "exp_0016_b0_${branch}" --output "$out/b0-${branch}.rs" pallet_deos_actors 2>&1 | tee "$out/b0-${branch}.log" + DEOS_VERBOSE=1 ./scripts/benchmarks.sh --skip-build --steps 50 --repeat 20 --extrinsic "exp_0016_c5_${branch}" --output "$out/c5-${branch}.rs" pallet_deos_actors 2>&1 | tee "$out/c5-${branch}.log" +done +cd template +/home/llb/.rustup/toolchains/1.94.1-x86_64-unknown-linux-gnu/bin/cargo test -p pallet-deos-actors inline_first_head_and_lazy_tail_have_unique_ordered_ownership --locked -- --nocapture | tee "$out/authority-test.log" +/home/llb/.rustup/toolchains/1.94.1-x86_64-unknown-linux-gnu/bin/cargo test -p pallet-deos-actors profile_exp_0016_state_footprint --locked -- --ignored --nocapture | tee "$out/state-footprint.log" +cd .. +sha256sum "$out"/* template/target/benchmarks/release/wbuild/deos-runtime/deos_runtime.compact.compressed.wasm | tee "$out/SHA256SUMS" +``` + +The commands above own the completed C5 matrix. The frozen C6 continuation uses a new artifact directory and matching Wasm: + +```bash +out=/tmp/deos-exp-0016-c6 +rm -rf "$out" && mkdir -p "$out" +skip=() +for branch in create close reconstruct load_tail; do + DEOS_VERBOSE=1 ./scripts/benchmarks.sh "${skip[@]}" --steps 50 --repeat 20 --extrinsic "exp_0016_b0_${branch}" --output "$out/b0-${branch}.rs" pallet_deos_actors 2>&1 | tee "$out/b0-${branch}.log" + skip=(--skip-build) + DEOS_VERBOSE=1 ./scripts/benchmarks.sh --skip-build --steps 50 --repeat 20 --extrinsic "exp_0016_c6_${branch}" --output "$out/c6-${branch}.rs" pallet_deos_actors 2>&1 | tee "$out/c6-${branch}.log" +done +cd template +cargo test -p pallet-deos-actors step_chunk_binds_authority_and_contiguous_first_index --locked -- --nocapture | tee "$out/authority-test.log" +cargo test -p pallet-deos-actors profile_exp_0016_state_footprint --locked -- --ignored --nocapture | tee "$out/state-footprint.log" +cd .. +sha256sum "$out"/* template/target/benchmarks/release/wbuild/deos-runtime/deos_runtime.compact.compressed.wasm | tee "$out/SHA256SUMS" +``` + +The first C6 B0 command builds the matching Wasm; all later C6 commands reuse it. Source-manifest drift invalidates the matrix before Measuring. + +## Measurements + +The complete Prepared matrix ran once from manifest `4cc1dd50` and matching benchmark Wasm `c9c09e3f`. Models exclude database Weight additions shown separately. + +| Branch | B0 RefTime model | C5 RefTime model | B0 / C5 estimated ProofSize | B0 / C5 reads | B0 / C5 writes | +| --- | ---: | ---: | ---: | ---: | ---: | +| Create | `3,437,088 + 20,912n` | `2,054,416 + 1,595,834n` | 0 / 0 | 0 / 0 | 1 / `n` | +| Update | `3,510,450 + 36,944n` | `2,238,763 + 1,635,990n` | 0 / 0 | 0 / 0 | 1 / `n` | +| Close | `10,775,070 + 77,715n` | `8,703,429 + 1,425,034n` | `4,011+3n` / `4,023+3n` | 1 / 1 | 1 / `n` | +| Reconstruction | `12,681,461 + 146,503n` | `7,884,592 + 4,188,636n` | `4,011+3n` / `4,147+1,596n` | 1 / `n` | 0 / 0 | +| Step 0 load | 10,477,000 | 9,499,000 | 4,014 / 4,008 | 1 / 1 | 0 / 0 | +| Tail load | `10,542,122 + 75,279n` | `13,173,133 + 274,644n` | `4,011+3n` / `4,083+34n` | 1 / 2 | 0 / 0 | + +| Steps | B0 encoded bytes | C5 encoded bytes | C5 head | C5 maximum tail cell | +| ---: | ---: | ---: | ---: | ---: | +| 1 | 13 | 112 | 112 | 0 | +| 8 | 34 | 665 | 112 | 79 | + +Authority/reconstruction tests passed, including wrong-actor and wrong-index rejection. The digest manifest under `/tmp/deos-exp-0016-prepared/` owns exact C5 output/log identities. + +The C6 continuation ran once from manifest `4aee3998` and matching Wasm `5396a887`; artifacts and digests are retained at `/tmp/deos-exp-0016-c6/`. + +| Branch | Matching B0 RefTime model | C6 RefTime model | B0 / C6 maximum observed keys | +| --- | ---: | ---: | ---: | +| Create | `3,442,306 + 21,223n` | `3,790,623 + 516,213n` | 1 / 3 writes | +| Close | `10,862,615 + 64,366n` | `9,809,773 + 443,918n` | 1 read + 1 write / 1 read + 3 writes | +| Reconstruction | `12,430,683 + 146,961n` | `10,978,182 + 1,773,645n` | 1 / 3 reads | +| Tail load | `10,666,349 + 68,359n` | `14,143,766 + 175,876n` | 1 / 2 reads | + +At eight Steps, C6 uses a 112-byte head and two chunks for 287 total encoded bytes, versus C5's 665 bytes and B0's 34 bytes. One Step remains head-only at 112 bytes. The generated linear database formulas undercount C6's step-function chunk cardinality, so they are non-authoritative; exact observed maximum keys above and measured RefTime remain valid decision evidence. Production benchmarking must parameterize chunk count or charge a worst-case constant. + +## Derived Metrics + +C5 Step-0 load improves RefTime by `9.33%`, reduces estimated ProofSize by 6 bytes, retains one read, and creates no tail record. At `n=8`, C5 changes RefTime versus B0 by create `+311.20%`, update `+302.70%`, close `+76.40%`, reconstruction `+198.80%`, and tail load `+37.92%`. C5 retained bytes increase by `+761.54%` at one Step and `+1,855.88%` at eight Steps. C5 tail load adds one read. + +At `n=8`, C6 changes RefTime versus matching B0 by create `+119.27%`, close `+17.43%`, reconstruction `+84.97%`, and tail load `+38.68%`. Versus C5, C6 reduces retained bytes `56.84%`, create RefTime `46.56%`, close `33.54%`, and reconstruction `39.20%`; its tail-load RefTime differs by less than the 2% materiality floor. C6 preserves C5's measured Step-0 result because the head is byte-identical and no tail key exists for one-Step Contracts. + +## Statistical / Variance Notes + +Both valid matrices used 50 steps and 20 repeats with no sample removal and one matching Wasm per matrix. Generated RefTime slope errors remain below the 2% materiality floor. C6 tail-load differs from C5 by less than that floor. Chunk-count step functions make generated linear DB/proof coefficients non-authoritative; exact raw maximum key counts, encoded bytes, and measured RefTime remain valid. No outlier was removed. + +## Result + +C6 preserves the accepted one-read, no-tail one-Step path and materially dominates C5 in lifecycle RefTime and retained bytes without a material tail-load RefTime loss. C6 still costs more than monolithic B0 on tail-sensitive paths, but B0 fails the 32-Step no-ceiling-tax direction that motivated body separation. Four-Step chunks bound later-cursor decoding independently of the global ceiling and reduce the 32-Step tail-key ceiling from 31 C5 cells to eight chunks. + +## Interpretation + +The Pareto comparison selects C6 among conforming candidates. B0 remains faster and smaller at the current eight-Step profile but cannot satisfy the accepted scalable physical contract without loading the whole body. C5 proves inline Step 0 but repeats authority and trie work per Step. C6 retains the binding one-Step improvement while sharply reducing C5's cardinality costs. A two-Step candidate is not earned because it would increase keys and authority repetition while C6 introduces no material tail-load RefTime regression against C5. An unbounded tail remains non-conforming because its decode/proof exposure grows with the global ceiling. + +## Decision + +Accepted. C6 is the selected I2 production geometry: one uniquely owned inline Step 0 and authority-bound contiguous tail chunks of at most four Steps. Experiment-named stores/functions and rejected C5 code have been removed. Production implementation must parameterize Weight by exact chunk count or charge worst-case constants and validate lifecycle/current-Step behavior before architecture documentation changes. + +## Rejected Alternatives and Tradeoffs + +| Alternative | Decision | Evidence-backed reason | +| --- | --- | --- | +| Repeat C1 | Rejected | EXP-0003 established broad current/lifecycle/state regression. | +| Build two/four-Step chunks first | Rejected as first move | Tail chunking cannot remove the one-Step extra read unless Step 0 moves into the header. | +| Keep all Steps in the hot header | Rejected as final geometry | Reintroduces global-ceiling proof tax. | +| Externalize payloads now | Deferred to EXP-0004 | Payload shape is independent of unique Step-0 ownership. | + +## Validity + +- `Establishes`: C6 as the selected physical body geometry and C5 as a rejected one-cell-tail candidate. +- `Does not establish`: Final production Weight, 16/32-Step dispatchability, I3 end-to-end throughput, or completed production integration. +- `Invalidation triggers`: Semantic/body framing, Step representation, runtime ceiling/profile, lifecycle contract, current-Step loader, benchmark method, toolchain, or Wasm change. + +## Follow-Up / Next Gradient + +- Implement accepted C6 under generic production storage/type owners. +- `Post-selection correction`: Project-owned chunked create/close/reconstruction benchmarks now use exact chunk-count component `c`; tail loading uses exact in-chunk Step count `s`. A 10-step/5-repeat matching-Wasm smoke at `/tmp/deos-chunk-count-smoke/` proved create `writes = 1 + c`, close `writes = 1 + c`, reconstruction `reads = 1 + c`, and tail load `reads = 2`. + +## Architecture Impact + +No architecture-document edit yet. The interpreted selection constrains I2 implementation; architecture updates wait for production implementation, tests, correction, Weight, and matching Wasm convergence. + +## Relations + +- `Predecessor`: EXP-0003 rejects naive C1 and supplies B0/C1 evidence. +- `Constrains`: EXP-0002 ceiling comparison and I2 Selected Geometry. +- `Conditional relation`: EXP-0004 only if payload/Step-shape proof remains binding after body ownership converges. +- `Motivates`: Dormant EXP-0017 preserves the Q2/Q4 reopen question, while active EXP-0018 uses accepted inline Step 0 for Fresh timing research. This record supplies neither quantum nor T0 execution evidence. +- `Semantic status under EXP-0019`: Current Category A evidence. C6 Chunk4, inline Step 0, lazy tails, authority, and retained-state conclusions do not depend on User economic apoptosis. Historical close measurements remain physical geometry evidence but must be renewed only because A1 adds/removes lifecycle partitions. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0017.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0017.md new file mode 100644 index 00000000..bfd73831 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0017.md @@ -0,0 +1,165 @@ +# EXP-0017 — Service Quantum Reopen Gate + +| Field | Value | +| --- | --- | +| Status | Proposed | +| Architecture release / experiment campaign | DEOS 0.7.24 topology-scaling campaign | +| Date | 2026-08-27 | +| Affected domain | DEOS Actors service quantum, current-Step execution, FIFO breadth, and causal cohort lifetime | +| Physical mechanism | Deferred actor-major consecutive-Step service over accepted C6 Chunk4 storage | +| Primary track | [actors](./experiments.md) | +| Related tracks | [adapters](../adapters/experiments.md) | +| Baseline experiment | [EXP-0013](./EXP-0013.md) and [EXP-0016](./EXP-0016.md) | +| Successor experiment | [EXP-0018](./EXP-0018.md) | + +## Decision Question + +After the converged Q1 path is fully optimized and measured, does remaining context/persistence overhead still bind strongly enough to justify reopening Specification for a controlled Q2/Q4 Pareto comparison over accepted C6 Chunk4 storage? + +## Governing Specification + +- `Exact sections`: `template/pallets/actors/docs/specification.en.md` → `1.4 Block-Paced Actor Cycles`, `4.1 Execution Model`, `4.2 Snapshots and Fresh Opening`, and `5.1 FIFO and Temporal Readiness`; `docs/actors-performance-assurance.specification.en.md` → `Independent Throughput Axes`, `Required Cost, Latency, and Fairness Metrics`, and `Contract Complexity Matrix`. +- `Invariant`: Chunk is physical Contract fragmentation, Batch/Service Quantum is the maximum consecutive Steps in one Actor turn, and Cohort is shared causal origin. Candidates preserve deterministic actor-major FIFO order, User/System neutrality, per-Step atomic effects and fees, bounded work, no Task-family ranking, no task-shape affinity, and no Actor-produced same-block reaction cascade. +- `Reopen trigger`: The authoritative Specification fixes Q1 and next-block continuation. Q2/Q4 require explicit Specification reopening and may re-enter only if converged Q1 still misses its committed-Step target and controlled decomposition proves context/persistence overhead remains binding after accepted reuse, fragment, accounting, and scheduler optimizations. T0 belongs to separate Fresh Step-0 causality research. + +## Context and Hypothesis + +- `Current architecture`: EXP-0016 accepted one inline Step 0 plus authority-bound tail chunks of at most four Steps while retaining Q1 execution. EXP-0013 accepted carrying one validated decoded state and admission certificate through one service transition, reducing the measured marginal cheap-Actor RefTime `167,553,076 → 137,612,859` (`-17.87%`); later loaded-state corrections removed additional redundant reconstruction without changing Q1 semantics. +- `Reason`: Repeated fragment loading, control setup, run persistence, and scheduler placement may dominate cheap multi-Step pipelines. C6's physical locality earns a controlled service-quantum question but does not imply that BatchMax equals ChunkSize. +- `Hypothesis`: Up-to-Q2 or up-to-Q4 actor-major service may amortize Actor-machine cost per committed Step and shorten short-pipeline completion while retaining a useful distinct-Actor breadth frontier. The null result is that Q1 remains preferable because next-block Current semantics, population breadth, proof/write pressure, or head-of-line behavior dominate. +- `Materiality`: A candidate must be Pareto-relevant across Steps/block and Actors/block and materially reduce Actor-machine RefTime or pipeline latency without violating component-wise ProofSize/block limits, deterministic service, accepted temporal semantics, or healthy service gaps. No scalar score may combine the two throughput axes or the two Weight dimensions. + +## Baseline and Candidates + +| ID | Architecture | Source tree | Runtime/config identity | +| --- | --- | --- | --- | +| Q1 | Accepted C6 Chunk4; at most one committed Step per Actor per block; successor eligible at N+1 | Provisional local HEAD `90c32d85010c713b95aa3a440e6ffed954814a4b`, tree `75fc490b35e13bc31fe43b9d71b990e619b8afba`; the dirty worktree is not measurement authority and must be replaced by a converged I4 evidence tree before Prepared | P32 `(32,40,64,128)`; production Wasm `20df93fe148f7e8317c8e3650453b9aad19bd754d8d86c3f109c02105edc5983` is pre-I4 and not measurement authority | +| Q2 | C6 Chunk4; actor-major execution of up to two consecutive independently atomic Steps | Dormant; no candidate code | Specification reopening required | +| Q4 | C6 Chunk4; actor-major execution of up to four consecutive independently atomic Steps | Dormant; no candidate code | Specification reopening required | +| T0 | Same-block Fresh Step-0 service with one-generation causal cutoff | Transferred to successor EXP-0018 after separate Specification acceptance | T+1 remains authoritative | + +## Controls + +- `Controlled variables`: Accepted C6 Chunk4 geometry, P32 authored bounds, Contract and Task bytes, Actor class mix, deterministic FIFO activation order, shared-cause Trigger revision, per-Step predicates and error policy, market fixtures, block Weight/resource policy, database/backend, toolchain, benchmark commands, seeds, cache/warmup protocol, prefunding, recipients, production Weight binding, and T+1 causality except in the separately admitted T0 comparison. +- `Changed variables`: BatchMax Q1/Q2/Q4 only; T0/T+1 is a separate crossed dimension and must never be folded into a quantum comparison. ChunkSize remains four unless measured boundary evidence earns a minimal Chunk2 cross-check. +- `Confounders`: Q2/Q4 alter temporal observation spacing unless the specification defines them as valid semantics. Action-heavy workloads may bind economic Weight rather than Actor-machine overhead. Cohort divergence changes eligible work and must be reported rather than normalized away. The current dirty baseline and pre-I4 Wasm prevent Prepared status. + +## Benchmark Class, Workloads, and Criteria + +- `Benchmark class`: Controlled pallet/production-Wasm branch measurement for marginal costs, deterministic native or integration stress for populations and cohort divergence, and full-runtime block profiles for end-to-end throughput, latency, fairness, head-of-line loss, and resource composition. + +| Workload | State/population geometry | Hot/cold assumptions | Purpose | +| --- | --- | --- | --- | +| W1 control-only | Pipeline lengths `1,2,4,8,16,32`; boundary populations `10,100,1,000,10,000` where feasible | C6 Step 0 hot; required tail chunk cold once per service turn | Isolate scheduler, fragment, RunFrame, fee, and persistence amortization | +| W2 Burn-like | Same representative lengths/populations with deterministic cheap effect | Canonical effect owner; no fabricated priority | Measure automation tax around a cheap useful effect | +| W3 Transfer | Same representative lengths/populations and stable recipients | Real ledger consequences and ingress ownership | Compare machine/action ratio and write pressure | +| W4 Swap | Deterministic bounded liquidity, typed success/failure, current market predicates | Real route/effect Weight; no stale shared quote | Measure market-heavy fit, divergence, and head-of-line capacity | +| W5 liquidity | Bounded add/remove-liquidity fixture | Real reserve/accounting writes | Measure heavy action without ranking by cost | +| W6 mixed population | `40%` one-Step, `25%` two-Step, `20%` four-Step, `10%` eight-Step, `5%` sixteen/thirty-two-Step boundary sample | Deterministic activation order | Locate Steps/block versus Actors/block Pareto frontier under heterogeneous demand | +| W7 divergent pipelines | Controlled Current false, temporary failure, funding unavailable, early completion, and retry at later cursors | Divergence seed and branch counts fixed | Measure early yield, service gap, cohort half-life, and wasted capacity | +| W8 shared-cause cohorts | One Trigger revision activates `10,100,1,000,10,000` short Actors | Cohort exists only through shared causal origin | Test whether Q2/Q4 expands the cohort-friendly short-Contract class without dynamic re-cohorting | +| W9 causal timing | T+1 baseline; T0 only if separately admitted | Exact trigger block, cutoff, first service, and Actor-produced causes recorded | Measure trigger-to-first-Step latency and verify causal firewall | + +- `Metrics`: CommittedStepsPerBlock; DistinctActorsProgressedPerBlock; ActorMachine RefTime/ProofSize per committed Step; reads/writes per committed Step; useful Action Weight versus Actor-machine Weight; trigger-to-first-Step and trigger-to-completion latency; continuously runnable service-gap distribution; head-of-line unused capacity; completion/outcome counts; cohort materialization/execution cost; cohort half-life/divergence; per-block control/effect usage; Wasm size; exact faults and ordering. +- `Statistical method`: Production FRAME Weight uses 50 steps/20 repeats, generated component review, and no outlier removal. Native/integration profiles use deterministic seeds, declared warmup, at least five retained runs when affordable, median plus range/p95, and no outlier removal unless contamination is recorded before interpretation. Full-runtime profiles retain every measured block rather than averaging away service gaps or head-of-line tails. +- `Acceptance criteria`: Select only a specification-conforming candidate on the useful two-axis Pareto frontier. It must materially improve Steps/block, per-Step machine cost, or completion latency while retaining substantial distinct-Actor breadth and bounded service gaps; fit RefTime and ProofSize independently; preserve exact actor-major order and every Step outcome; and add no Task/class/fee ranking or dynamic re-cohorting. Q1 wins if Q2/Q4 are semantically inadmissible, Pareto-dominated, or add complexity without material frontier gain. +- `Rejection criteria`: Semantic violation; same-block Current reinterpretation without authority; Actor-produced same-block cascade; FIFO reorder; non-atomic Step fees/effects; class or Task priority; component-wise block overrun; cold-body ceiling tax; unacceptable breadth/service-gap regression; hidden divergence; or a tiny throughput gain that does not justify consensus complexity. + +## Environment and Artifact Identity + +| Artifact | Identity | +| --- | --- | +| Source commit | Provisional `90c32d85010c713b95aa3a440e6ffed954814a4b`; bind converged post-I4 source before Prepared | +| Source tree | Provisional HEAD tree `75fc490b35e13bc31fe43b9d71b990e619b8afba`; dirty worktree excluded pending converged I4 manifest | +| Rust toolchain | `rustc 1.98.0 (88d9e12ae 2026-08-18)`; `cargo 1.98.0 (797e8a9bc 2026-08-05)` | +| Benchmark runtime Wasm | Current focused assurance Wasm SHA-256 `fe3ff0042fba99362474b0c767e6b7aaae61ec82b5c07c45437e33d02295f088`; not a quantum baseline | +| Post-generation production runtime Wasm | Pending converged I4 evidence | +| Final release production runtime Wasm | Not measured | +| Generated Weight | Current Actors file SHA-256 `0da809ee23f68c058495bd5f967ad1f21ce62dec47760198b635e1943ce462b1`; pending complete regeneration | +| Database/backend | Pending Prepared controls; use the same production benchmark backend for all candidates | +| Benchmark command/config | Pending project-owned harness after semantic acceptance; use `scripts/benchmarks.sh` for FRAME owners and retain exact full-runtime profile commands | +| Raw evidence | Planned `/tmp/deos-exp-0017/` manifest plus compact normalized record; retain digests, commands, candidate source manifests, Weight/Wasm identities, seeds, and selected raw distributions | + +## Measurements + +| Candidate | Workload | RefTime | ProofSize | Reads | Writes | Other decision metrics | +| --- | --- | ---: | ---: | ---: | ---: | --- | +| Q1 | W1-W9 | Not measured | Not measured | Not measured | Not measured | Steps/block, Actors/block, latency, gaps, and cohort half-life pending | +| Q2 | W1-W9 | Deferred | Deferred | Deferred | Deferred | Reopen condition not met | +| Q4 | W1-W9 | Deferred | Deferred | Deferred | Deferred | Reopen condition not met | +| T0 | W9 | Semantically gated | Semantically gated | Semantically gated | Semantically gated | T+1 remains authoritative | + +## Derived Metrics + +| Candidate | Workload | RefTime delta | ProofSize delta | Throughput / latency | Marginal or amortized cost | Pareto class | +| --- | --- | ---: | ---: | --- | --- | --- | +| Q1 | W1-W9 | Baseline pending | Baseline pending | Pending | Pending | Not interpreted | +| Q2 | W1-W9 | Not derived | Not derived | Not derived | Not derived | Not admissible yet | +| Q4 | W1-W9 | Not derived | Not derived | Not derived | Not derived | Not admissible yet | +| T0 | W9 | Not derived | Not derived | Not derived | Not derived | Separate semantic dimension | + +## Statistical / Variance Notes + +No quantum samples exist. EXP-0013 and EXP-0016 establish causal motivation and accepted storage/reuse baselines only; neither measures Q2/Q4 service. Their numeric results must not be projected into batch throughput. The full matrix may use representative boundary points instead of every Cartesian combination when cost is unreasonable, but every omission must preserve pipeline, population, action, divergence, and cohort boundaries sufficient to decide the frontier. + +## Result + +No quantum measurement result. The converged design direction retains Q1 and narrows active work to making the one-Step boundary cheap, load-elastic continuation liveness, Fresh Step-0 latency, and shared-cause Fresh cohorts. Q2/Q4 are preserved as reopenable provenance rather than active candidates. + +## Interpretation + +The C6 chunk size and Q1 service quantum remain independent. EXP-0013 shows repeated reconstruction is expensive, but the preferred next act is to delete that boundary cost rather than hide it behind a larger quantum. Q1 retains maximum scheduler granularity, population breadth, next-block Current semantics, and load degradation through service gap. Cohorts remain shared-cause Fresh materialization only and dissolve after Step 0 unless the one-Step Actor completes. + +## Decision + +Proposed and explicitly dormant. No physical baseline changes; Q1/T+1 remains authoritative. Do not transition to Prepared unless the recorded Q1 failure/decomposition reopen condition is met. Fresh T0/T+1 work transfers to EXP-0018 rather than reviving this quantum matrix. + +## Rejected Alternatives and Tradeoffs + +| Candidate | Decision | Evidence-backed reason | +| --- | --- | --- | +| Infer `BatchMax = 4` from C6 `ChunkSize = 4` | Rejected | EXP-0016 measured storage geometry and explicitly retained Q1; equality has no execution evidence | +| Cohort by shared Task type | Rejected | It manufactures scheduler affinity from implementation similarity, can reorder economic effects, and has no shared causal authority | +| Dynamic runtime re-cohorting after divergence | Rejected | Adds consensus work and policy complexity as independent Current state diverges; measure cohort half-life without repairing it | +| General Q2/Q4 as default 0.7.24 direction | Deferred | The converged hypothesis prefers deleting Q1 machine overhead while retaining scheduling granularity; no controlled Q1 failure has earned Specification reopening | +| T0 under current specification | Gated | It violates accepted causal next-block readiness; a separately specified one-generation firewall is required | +| Chunk2 search | Deferred | C6 is accepted; reopen chunk geometry only if a measured Q boundary creates a specific fragment-alignment confounder | + +## Validity + +- `Establishes`: The exact research boundary, semantic gate, controlled candidate axes, required workloads/metrics, baseline lineage, and artifact plan for a future Q1/Q2/Q4 comparison. +- `Does not establish`: That Q2/Q4 are valid, faster, fair, dispatchable, or preferable; that Chunk4 implies Q4; that same-block T0 is safe; or that 100 Steps/block equals 100 Actors/block. +- `Known limitations`: I4 production evidence is not regenerated; no quantum harness or sample exists; the deferred matrix has no numeric comparison and supports no Q2/Q4 performance claim. +- `Invalidation triggers`: Service/fairness/causality specification changes beyond the declared candidates; C6 replacement; resource-policy or block-Weight changes; executor/fee/RunFrame topology changes; toolchain/backend drift; or an I4 baseline that removes the hypothesized amortizable owner. +- `Residual qualitative insight`: Keep Chunk, Service Quantum, and Cohort independently tunable and report Steps/block and Actors/block independently even if Q1 remains final. + +## Follow-Up / Next Gradient + +```text +100 cheap committed Steps/block target → no Q2/Q4 evidence +→ unknown RefTime/ProofSize/breadth frontier → repeated fragment/control/run persistence +→ Actor service quantum → bounded up-to-Q hypothesis +→ Specification reopening, then smallest Q1/Q2/Q4 short-pipeline falsifier +``` + +- `Eliminated hypotheses`: Chunk4 alone justifies Q4; Task similarity justifies cohort affinity; one scalar throughput score can select the design. +- `Remaining hypotheses`: Q1 can become a tiny process-state transition; Fresh T0 may reduce first-reaction latency with a one-generation firewall; Q2/Q4 become relevant only if Q1 context/persistence overhead remains binding after convergence. +- `Stop condition`: Stop when one conforming candidate is Pareto-relevant and materially justified across representative cheap/effectful, mixed, divergent, and shared-cause profiles, or when Specification retains Q1/T+1 and thereby rejects Q2/Q4/T0 without candidate construction. + +## Architecture Impact + +- `Affected physical truth`: Actor service quantum, fragment reuse within a turn, run persistence frequency, FIFO population breadth, and causal cohort lifetime. +- `Architecture update`: None while Proposed. Update only after Specification acceptance, candidate convergence, tests, production Weight, and implementation correction. +- `Provenance citation`: Cite `actors/EXP-0017` only after an Accepted decision changes or validates the production service baseline. + +## Relations + +- `Replaces`: None. +- `Supersedes`: None; [EXP-0018](./EXP-0018.md) supersedes this record as the active default research direction while leaving its explicit reopen gate intact. +- `Refines`: [EXP-0013](./EXP-0013.md) loaded-state reuse and [EXP-0016](./EXP-0016.md) C6 locality as motivation, not as quantum evidence. +- `Contradicts`: None. +- `Depends on`: A future explicit Q>1 Specification reopening after the recorded trigger; converged Q1 evidence; [EXP-0013](./EXP-0013.md); [EXP-0016](./EXP-0016.md). +- `Baseline of`: None. +- `Confirms`: Chunk, Service Quantum, and Cohort are independent axes. +- `Invalidates`: None. +- `Semantic status under EXP-0019`: Current Category A dormant question. Any future reopen uses the post-apoptosis Q1 baseline; no pre-deletion rerun is warranted now. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0018.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0018.md new file mode 100644 index 00000000..08d9d231 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0018.md @@ -0,0 +1,174 @@ +# EXP-0018 — Fresh Step-0 Causal Timing + +| Field | Value | +| --- | --- | +| Status | Rejected | +| Architecture release / experiment campaign | DEOS 0.7.24 topology-scaling campaign | +| Date | 2026-08-27 | +| Affected domain | DEOS Actors Fresh Cycle readiness, first-Step latency, causal cutoff, and continuation liveness | +| Physical mechanism | Bounded Fresh Step-0 service over the Q1 scheduler and shared-cause causal cohorts | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0013](./EXP-0013.md), [EXP-0016](./EXP-0016.md), and [EXP-0017](./EXP-0017.md) | +| Successor experiment | None | + +## Decision Question + +Can one bounded same-block Fresh Step-0 opportunity reduce Trigger-to-first-Step latency relative to T+1 while preserving Q1, deterministic economic ordering, one causal generation per block, bounded block work, strict continuation liveness, and no repeatable Fresh priority? + +## Governing Specification + +- `Exact section`: `template/pallets/actors/docs/specification.en.md` → `1.4 Block-Paced Actor Cycles`, `1.5 Causal Readiness`, `4.1 Execution Model`, and `5.1 FIFO and Temporal Readiness`; `docs/actors-resource-policy.specification.en.md` → `6.1 Control Open and Execution Cutoff`; `docs/actors-performance-assurance.specification.en.md` → `3.4 Fresh Step-0 Timing` and `4.4 Mixed-Length Shared-Trigger Step 0`. +- `Invariant`: Q1; at most one committed Step/Actor/block; one shared Trigger instance defines causal discovery independently of `0..=MaxContractSteps`; bounded Fresh opportunity; actor-produced causes cannot execute in the source block; deterministic order; class/Task/fee/Weight-neutral continuation service; upfront Trigger/Pipeline Machine charge plus current-Action effect fee; no dynamic re-cohorting after first reaction; no unreachable-tail physical tax. +- `Reopen trigger`: Reopen only if measured production evidence proves that protocol-fixed next-block eligibility fails a required economic contract and a bounded alternative preserves FIFO, Q1, non-recursion, and available-Weight honesty. + +## Context and Hypothesis + +- `Current architecture`: Q1 execution, C6 optional inline Step 0, compact causal materialization, loaded-state reuse, typed cause phase/source block, and one protocol-fixed current-block prepass cutoff are implemented. Readiness observed in N cannot execute before N+1; later execution remains governed by FIFO and available `on_idle` Weight. The rejected timing configuration and candidate branches are physically absent. +- `Reason`: First economic reaction is more latency-sensitive than later continuation, but a controlled T+1 versus T0 comparison requires both timing architectures. The implementation must first converge to the specified T+1 prepass cutoff and source provenance; otherwise the experiment would mislabel existing same-block behavior as its baseline. +- `Hypothesis`: A small deterministic Fresh grant over one predeclared external/pre-Actor causal generation can materially reduce first-Step latency without starving Running Actors; unused Fresh/continuation grants remain work-conserving. The null is that phase/cutoff overhead, herd ProofSize, economic-order complexity, or continuation pressure makes T+1 preferable. +- `Materiality`: T0 must materially improve Trigger-to-first-Step latency or staleness under low/normal pressure, preserve a nonzero continuation floor and bounded p99/max service gap under sustained Fresh pressure, fit both Weight dimensions, and add no recursive generation or scheduler ranking. + +## Baseline and Candidates + +| ID | Architecture | Source tree | Runtime/config identity | +| --- | --- | --- | --- | +| C0 | Current permissive cutoff: external readiness may join the same block's `on_idle` FIFO cutoff without typed timing/provenance authority | HEAD `aaeb0ab`; exact formatted I4 worktree tree `22e94aec0c334b0adbbb03a3cbd443a118cdbaa1` | P32 `(32,40,64,128)`; production Wasm `364ba474a5775285bc53d214b1f06a613e2d3975bfcc3f175edf859ed94c6d01`; invalid comparison baseline | +| T+1 | Specification-authoritative Q1: every Trigger cause in N becomes service-eligible no earlier than N+1 | Fixed-timing cleanup tree `12452492a68aa8904dc670454a8780ff90766ca4` before identity annotation | Protocol-fixed cutoff; Weight `6a1d68bc`; production Wasm `674022f0` | +| T0 | Best-effort same-block Fresh Step 0 after all ordinary external extrinsics; actor-produced and other Deferred causes receive one N+1 floor | Dedicated runtime feature `exp-0018-same-block-external` in candidate tree `f4efc274a636ae5037185b0f065fad0aa5431b2d` before identity annotation; warning-free isolated build Wasm `405e46c889b7cd749637aad883d730ab3804c0b0cf0c69d813a9b7058d3046b2` | Exact hook-order harness plus runtime binding test; production default remains `NextBlock` | + +## Controls + +- `Controlled variables`: Q1, C6 Chunk4 with optional inline Step 0, byte-identical nonempty Step-0 semantics/resources, actor/population order, shared Trigger instance/revision, Action fixtures, User/System mix, funded per-length activation quotes, resource policy, Running backlog, deterministic seeds, runtime/toolchain/backend, block demand, and generated Weight method. Mixed-length profiles vary zero-Step absence or unreachable suffixes. Observation provenance has one bounded owner per revision: signed `Oracle::publish` is `ExternalPhase`, public/internal `publish_from` fails closed to `Deferred`, dirty-feed state retains separate latest and active-fanout provenance, and every queued Crossing transition carries its own provenance. Each of those revision owners must also retain the exact cause block: phase without source time cannot distinguish same-block work from an older deferred revision first processed by a later fanout turn. Temporal detection is always `Deferred` because its occurrence is materialized by Actor control after the pre-external cutoff. +- `Changed variables`: Earliest Fresh Step-0 eligibility and the minimum bounded control topology needed to enforce its causal generation only. Fresh grant size is a declared candidate parameter, never Actor-authored priority. +- `Confounders`: Trigger sources occupy different block phases; comparing unlike publication/extrinsic ordering is invalid. Fresh cohort materialization improvements must not be attributed to T0. Direct package tests that call `execute_cycle` without `on_initialize` do not establish either timing profile: timing conformance requires the complete block hook order, while lower-level queue tests need an explicit physical-cutoff fixture rather than an implicit runtime profile. The I4 baseline is final for candidate construction, but the large formatted worktree still requires its normal repository checkpoint before a commit identity can replace the exact tree identity. + +## Benchmark Class, Workloads, and Criteria + +- `Benchmark class`: Production-Wasm branch Weight for cutoff/materialization/service owners plus deterministic full-runtime block profiles for latency, ordering, herd behavior, and Running liveness. + +| Workload | State/population geometry | Hot/cold assumptions | Purpose | +| --- | --- | --- | --- | +| W1 low Fresh | Fresh populations `10/100`; no Running pressure | Inline Step 0 hot; no tail read | Minimum latency and overhead | +| W2 sustained Fresh | Total active populations `10/100/1,000/10,000`, partitioned into a continuously runnable Running set plus a bounded reusable Fresh-ingress pool; the 10,000 case never exceeds `MaxActiveActors` | Current fragments only | Prove Fresh pressure cannot starve continuation without specifying an impossible over-capacity population | +| W3 mixed-length shared-cause herd | One Trigger instance activates `10/100/1,000/10,000` Fresh Actors equally mixed across lengths `0/1/4/8/32` | Zero-Step has no body; nonempty Step 0 is byte-identical; tails remain cold | Measure cohort materialization, zero-Step completion, Step-0 service/dissolution, and No Ceiling Tax | +| W4 post-Step-0 continuation | The W3 multi-Step Actors after their first commit | Each Actor follows independent Q1 current-fragment loading | Separate initial cohort work from inter-Step service gaps and completion | +| W5 actions | Stop/control-only, Burn-like, Transfer, Swap, liquidity-heavy | Canonical effect owners | Separate Actor-machine overhead from useful execution | +| W6 economic order | Trigger source before/after declared phase with ordinary extrinsics around Fresh service | Exact state visibility retained | Prove replay and observation ordering | +| W7 causal firewall | Fresh Step 0 produces causes targeting other Actors | Source identities/generations recorded | Prove Actor-created generation waits until N+1 | + +- `Metrics`: Committed Steps/block; distinct Actors/block; Actor-machine RefTime/ProofSize/reads/writes per Step; Trigger-to-first-Step; inter-Step p50/p95/p99/max service gap; completion latency; trigger staleness; Fresh and Running grants/borrowing; head-of-line unused Weight; cohort materialization/dissolution; action Weight; exact generation/order/fault counts; loaded fragments and first-reaction deltas by total Contract length. +- `Statistical method`: FRAME Weight uses 50 steps/20 repeats and no sample removal. Full-runtime profiles retain every block under deterministic seeds; report median, p95, p99, max, and no outlier removal. Exact ordering/firewall evidence is pass/fail. +- `Acceptance criteria`: T0 lies on a useful latency/liveness Pareto frontier, preserves all invariants, and justifies its added consensus topology. Mixed `0/1/4/8/32` Contracts share length-blind causal discovery and are funded for their own activation quotes; first reaction loads no tail and has no machine RefTime/ProofSize/I/O term proportional to unreachable fragments. Same-block execution remains best-effort; no SLA claim. T+1 wins if T0 adds material overhead/complexity, weakens continuation liveness, or cannot define one deterministic economic order. +- `Rejection criteria`: Actor-generated same-block cascade; repeated Fresh privilege; Running starvation; class/Task/fee ranking; unbounded generation work; nondeterministic extrinsic visibility; Weight/proof overrun; stale Trigger authority; lost/duplicate Step; or immaterial latency gain. + +## Environment and Artifact Identity + +| Artifact | Identity | +| --- | --- | +| Source commit | Baseline HEAD `aaeb0ab`; exact post-I4 behavior includes the formatted worktree identified separately | +| Source tree | Permissive C0 I4 tree `22e94aec0c334b0adbbb03a3cbd443a118cdbaa1`; binding-only correction tree `7a007a17910bfd0ec4025257c44edf072cfc1122`; cutoff-authority tree `2dea97b1ba7590c65fe84ccf4ee55d3a62c87933`; pre-provenance T+1 evidence tree `dbf9009be86006d9f2dc0ffab27c01c5c07c2463`; provenance-complete T+1 evidence tree `7fc73c25ab32227fef2682c262797e92fafa4a0a`; fixed-timing rejection, physical cleanup, and context-reconciliation tree before this identity annotation `9fe556b4e45fd8b7dc474f648b3e4c459db73865`, each from an isolated temporary Git index over all tracked changes | +| Rust toolchain | `rustc 1.98.0 (88d9e12ae 2026-08-18)`; `cargo 1.98.0 (797e8a9bc 2026-08-05)` | +| Benchmark runtime Wasm | Fixed-timing generation `12e502e2699b0c47016efe2665ce7401eaac458417322747aaea242b0a31b239` | +| Post-generation production runtime Wasm | Fixed-timing worktree `674022f044810c5032d717b1f26aa4fd3f209cdcdb6a4497ec7df2053795a249` (`1,439,506` bytes) | +| T0 candidate runtime Wasm | Isolated locked build with `WASM_BUILD_WORKSPACE_HINT`: `405e46c889b7cd749637aad883d730ab3804c0b0cf0c69d813a9b7058d3046b2` (`1,518,147` bytes); candidate-only artifact under `/tmp/deos-exp-0018/t0-target` | +| Generated Weight | Fixed-timing Actors file `6a1d68bcc4b6db46ae0891bc064cfc42e8a5a7167e8d8773206e72c6a301c589` | +| Database/backend | FRAME benchmark compiled Wasm, cache 1024, worst-case map size 1,000,000 on `fedora` AMD Ryzen 7 4800H host | +| Benchmark command/config | T0 runtime: `cd template && WASM_BUILD_WORKSPACE_HINT="$PWD" CARGO_TARGET_DIR=/tmp/deos-exp-0018/t0-target cargo build --locked --release -p deos-runtime --features exp-0018-same-block-external`; T+1: `cargo test --manifest-path template/Cargo.toml -p pallet-deos-actors tests::scheduling::hook_order_fixture_exposes_external_ticket_after_prepass_cutoff -- --exact`; T0: same command shape for `tests::scheduling::exp_0018_same_block_external_hook_profile_executes_external_step_zero -- --ignored --exact`; FRAME owners use `scripts/benchmarks.sh` | +| Raw evidence | fixed-timing metadata `1b74078e209cc4053db9df625debc0667e3cefb4619ed290a01ca3af13548683`; fixed-timing benchmark Wasm `12e502e2699b0c47016efe2665ce7401eaac458417322747aaea242b0a31b239`; planned `/tmp/deos-exp-0018/{next-block,same-block-external}/state.scale` plus `manifest.json`, with source/config/Wasm/state-version/storage-root/payload digests, exact Actor partition, queue/wakeup/run-state counts, generation command, and bounded measurement outputs | + +## Measurements + +| Candidate | Workload | RefTime | ProofSize | Reads | Writes | Other decision metrics | +| --- | --- | ---: | ---: | ---: | ---: | --- | +| T+1 | Pre-external cutoff capture | `7,543,000` | `1,493` | `1` | `1` | Provenance-complete 50-step/20-repeat generation; matching block-tagged cutoff is consumed by `NextBlock` Actor Drain | +| T+1 | W1 low Fresh `10/100` | Same generated owners | Same generated owners | Same generated owners | Same generated owners | First commit N+1; completion block `ceil(population/3)+1` under the deterministic package profile | +| T0 | W1 low Fresh `10/100` | Same generated owners | Same generated owners | Same generated owners | Same generated owners | Three source-block commits; completion block `ceil(population/3)`; exact profile test passes | +| T+1/T0 | W2 Running `10/100` + `4/34` one-shot Fresh Actors, one ingress/block | Same generated owners | Same generated owners | Same generated owners | Same generated owners | Every eight-Step Running cursor advances under both timing profiles; no starvation through one complete population service bound | +| T+1/T0 | W2 total population `1,000`: 666 Running + 334 reusable Fresh | Same generated owners | Same generated owners | Same generated owners | Same generated owners | Two complete Fresh-pool rotations (668 consecutive ingresses); every Running cursor advances and every pool member recycles before reuse in both profiles | +| T+1/T0 | W2 total population `10,000` | Not measured | Not measured | Not measured | Not measured | Gated on a deterministic prebuilt-state runtime harness: constructing 10,000 Actors inside the measured test would mix setup/admission cost with scheduler liveness and cannot compare the candidates honestly | +| T+1/T0 | W3 reduced herds `12/100`, lengths `0/1/4/8` | Same generated owners | Same generated owners | Same generated owners | Same generated owners | One ExternalPhase Observation revision; T0 starts exactly the first three FIFO Actors in N, T+1 starts none, and both drain every mixed-length member once in exact FIFO order | +| T+1/T0 | W4 post-Step-0 continuation: 12 Actors × 8 Steps | Same generated owners | Same generated owners | Same generated owners | Same generated owners | Every Actor has exactly eight Action attempts at an exact four-block inter-Step gap; T0 completes at block 32 versus T+1 block 33 | +| T+1/T0 | W5 control/Burn/Transfer/Swap/liquidity | Same generated owners | Same generated owners | Same generated owners | Same generated owners | StopCycle is observed at Cycle start without an Action fee; each effect family emits its Action fee; every T0 first attempt/start is exactly one block earlier than T+1 | +| T+1/T0 | W6 opening economic order | Same generated owners | Same generated owners | Same generated owners | Same generated owners | T0 snapshots all value present before source-block Actor Drain and transfers 39 after minimum preservation; a post-Drain mint is not retroactive. T+1 snapshots that later value in N+1 and transfers 79 | +| T0 | W7 Actor-produced AddressEvent cause | Same generated owners | Same generated owners | Same generated owners | Same generated owners | External transfer starts Actor A in N; A's transfer latches Actor B as Deferred, B remains cycle nonce 0 through N, and B starts at N+1 | +| T+1/T0 | W3 runtime herd `10`, lengths `0/1/4/8/32` | Same generated owners | Same generated owners | Same generated owners | Same generated owners | Dedicated default/candidate runtime tests admit maximum-length tails, preserve exact subscriber/FIFO start order, and differ only in source-block eligibility | +| T+1/T0 | W3 runtime herd `1,000`, lengths `0/1/4/8/32` | Same generated owners | Same generated owners | Same generated owners | Same generated owners | Separate default/candidate runs drain all 1,000 first starts once in exact subscriber/FIFO order; each completes in under 8 seconds on the recorded host | +| T+1/T0 | W3 maximum-admissible reference-preset herd `9,985`, lengths `0/1/4/8/32` | Same generated owners | Same generated owners | Same generated owners | Same generated owners | Existing genesis identities leave exactly 9,985 identity/sovereign slots; default and candidate each drain the full herd once in exact FIFO order in about 193 seconds | +| T+1/T0 | W2 prebuilt-state maximum population | Not measured | Not measured | Not measured | Not measured | Ordinary setup-through-service harness rejected: effectful setup failed by block 7,000 after 2,479 s and skipped-Step setup timed out after 3,600 s; requires an attested prebuilt state artifact | + +## Derived Metrics + +| Candidate | Workload | RefTime delta | ProofSize delta | Throughput / latency | Marginal or amortized cost | Pareto class | +| --- | --- | ---: | ---: | --- | --- | --- | +| T+1 | W1 | Baseline | Baseline | First commit latency 1 block; population completion `ceil(n/3)+1` | Shared generated owner | Admissible baseline | +| T0 | W1 | No implementation/Weight-owner delta | No implementation/Weight-owner delta | First commit latency 0 blocks; population completion exactly 1 block earlier | Same three-Actor service capacity is shifted into N | Provisional latency win; liveness not yet interpreted | + +## Statistical / Variance Notes + +W1 and the initial W2 profile are exact deterministic hook assertions rather than statistical samples. W1 populations 10 and 100 preserve three committed Actors per service block, with T0 shifting the identical sequence one block earlier. W2 first opens 10 and 100 eight-Step Running Actors, injects one new Fresh Actor per block through `ceil(population/3)` blocks, and requires every retained cursor to advance under both profiles. The in-cap 1,000-Actor profile partitions 666 Running and 334 reusable Fresh Actors, sustains one ingress/block for 668 blocks, proves every pool member is Idle before reuse, and requires every Running cursor to advance. W7 closes the candidate causal-firewall gate in the dedicated runtime profile. An ordinary external transfer starts Actor A in source block N; A's authored transfer creates an AddressEvent for Actor B, but internal protocol provenance is `Deferred`, so B remains pending at cycle nonce 0 through N and starts only at N+1. No same-block Actor cascade is admitted. W6 fixes economic visibility at the declared Actor Drain boundary. Starting from 10 units, a 30-unit post-Trigger/pre-Drain mint is visible to T0 opening, which transfers 39 while preserving one unit; a 40-unit post-Drain mutation cannot retroactively change that receipt. T+1 opens in the following block and therefore snapshots the later total, transferring 79. This is an intentional ordering difference, not nondeterminism: both profiles consume exact state at their configured opening phase. W5 executes one control-only StopCycle plus canonical Burn, Transfer, Swap, and AddLiquidity attempts from one shared ExternalPhase cause. StopCycle remains a control result with no invented Action fee, all four effect families emit their ordinary Action-fee evidence, and each T0 start/attempt is exactly one block earlier than its T+1 counterpart without Task-family timing rank. W4 isolates continuation after a shared ExternalPhase herd: twelve eight-Step Actors preserve an exact four-block gap between all adjacent Action attempts under both profiles, while T0 shifts the complete 96-attempt sequence one block earlier (completion block 32 versus 33). This establishes that the candidate changes Fresh eligibility without changing Q1 continuation cadence in the bounded profile. The 1,000-Actor runtime W3 profile repeats the exact maximum-tail mix at production configuration scale. Separate default/candidate runs each drain all first starts exactly once in subscriber/FIFO order in under eight seconds on the recorded host. A guarded generator was implemented but never run and was physically removed after rejection. It requires the exact environment consent `DEOS_EXP_0018_GENERATE_SNAPSHOT=GENERATE_MAXIMUM_W2`, is ignored by default, reports progress every 500 setup blocks, fails closed after 20,000 blocks, verifies the complete partition before commit, and atomically writes only the matching profile path. The recorded generator/loader commands are intentionally obsolete and no snapshot artifact was produced. The removed draft bundle carried exact Running and Idle-Fresh Actor-id inventories and profile-scoped paths; none became retained evidence. Smoke round trips use separate `roundtrip-*` directories and cannot overwrite retained expensive artifacts. The snapshot proof atomically publishes a deterministic profile-scoped bundle path, reloads it from disk, rejects trailing bytes, recomputes the raw-storage digest, decodes trie root/state version from the manifest, restores externalities, and removes the test artifact. Its reusable partition verifier checks exact Running/Idle-Fresh cardinality, active/identity/System-sovereign counts, FIFO occupancy, total live wakeup entries, profile authority, every retained certificate commitment, every Running RunState, and every Idle-Fresh lifecycle/latch/queue/wakeup condition. The snapshot proof also SCALE-round-trips a versioned manifest containing timing, configured-bounds commitment, encoded state version/root, payload digest, Actor/identity/System-sovereign counts, Running/Idle-Fresh partition counts, and queue occupancy. After restore it rejects profile/config drift by comparing runtime timing and current authority, verifies the retained Actor certificate carries the same configured-bounds commitment, checks exact topology counts, and requires the Fresh fixture to remain unlatched. The project runtime has a profile-independent round-trip proof for the artifact substrate: it commits a seeded Actor-state overlay, SCALE-encodes and decodes the raw trie payload, binds a nonzero BLAKE2 payload digest, reconstructs `TestExternalities` from the exact trie root/state version, and observes the same Actor identity/count under both default and candidate builds. The prebuilt-state artifact must be profile-specific rather than shared. `FreshStepZeroTiming` participates in admission configured-bounds identity, so a T+1-created Actor certificate cannot be relabeled as a T0 candidate state. Each profile generator must commit its completed setup overlay, export `TestExternalities::into_raw_snapshot`, bind the trie root and SCALE payload digest, and reload through `from_raw_snapshot` at the recorded state version. Before measurement, the loader must verify its runtime feature/config identity, all Actor admission certificates, exact 6,656 Running / 3,329 Idle-Fresh partition, FIFO/wakeup/run-state counts, no pending Fresh member, and pallet try-state; either artifact failing those checks invalidates comparison. The maximum-population W2 attempt confirms why setup must be outside the measured interval. A 6,656 Running / 3,329 reusable-Fresh partition did not finish opening by block 7,000 after 2,479 seconds with effectful Steps; replacing effects with deterministically skipped Steps still exceeded a 3,600-second timeout. That hour-scale unit-test topology was removed. The rejected candidate has no remaining snapshot gate. The reference-preset boundary is identity-limited before an added 10,000-Actor herd: existing genesis identities and System sovereigns leave exactly 9,985 admissible experimental slots. Default and candidate maximum-herd runs each create and drain all 9,985 mixed-length Actors exactly once in FIFO order in about 193 seconds on the recorded host. This replaces the impossible added-10,000 geometry with the exact maximum admitted by the shipped preset. The runtime W3 profile adds the specification's maximum 32-Step tail to an exact `0/1/4/8/32` mix, with two Actors per length. Separate production-default and candidate-feature runs preserve exact subscriber/FIFO first-start order; only the candidate admits a nonempty source-block prefix. The reduced W3 profile uses the package's admitted `0/1/4/8` lengths and populations 12/100. It proves shared-revision causal discovery and FIFO first reaction are independent of those lengths: T0 starts exactly three source-block cycles, T+1 starts none, and every Actor starts exactly once in subscriber/FIFO order. Length 32 and larger populations remain runtime-profile work. The 10,000 production-runtime profile remains gated: the package mock intentionally bounds its physical FIFO at 1,024, while constructing the full population through runtime admission inside a timing test would contaminate liveness with setup cost. It requires a deterministic prebuilt-state fixture whose setup is excluded from measured blocks. EXP-0013 and EXP-0016 support loaded reuse and inline Step-0 locality only; neither establishes herd behavior or sustained-pressure liveness. Do not compare Trigger sources with different economic ordering or omit continuously runnable Running pressure. + +## Result + +The candidate lowered best-effort first-service latency by one block without changing FIFO order, Q1 continuation gaps, Task-family rank, or the actor-produced-cause firewall in the measured profiles. It did not guarantee immediate service for a herd, and maximum-population setup-through-service proved unsuitable as a bounded comparative harness. The selected fixed-timing implementation captures one block-tagged prepass cutoff and makes readiness observed in N eligible no earlier than N+1. Final evidence binds Actors Weight `6a1d68bc`, benchmark Wasm `12e502e2`, production Wasm `674022f0`, metadata `1b74078e`, and cleanup tree `9fe556b4`. + +## Interpretation + +The one-block best-case gain does not justify a consensus timing branch near release because the branch cannot provide a herd-level execution guarantee. A protocol-fixed eligibility floor is simpler, auditable, class-neutral, and compatible with strict FIFO and available-Weight execution. Initial cohortability remains independent of Contract length, and every surviving multi-Step Actor continues under ordinary Q1 service. + +## Decision + +Rejected. Production retains one protocol-fixed next-block eligibility floor; N+1 is not an execution SLA. `FreshStepZeroTiming`, `SameBlockExternal`, the candidate feature, disabled candidate tests, and snapshot generator/loader infrastructure are physically removed. Useful fixed-timing runtime herd tests remain as explicit ignored stress profiles. Reopening requires measured failure of a required economic contract and a bounded alternative that preserves FIFO, Q1, non-recursion, and Weight honesty. + +## Rejected Alternatives and Tradeoffs + +| Candidate | Decision | Evidence-backed reason | +| --- | --- | --- | +| Global `request_activation` N+1 deferral | Rejected | 242 package failures showed that generic activation is shared by external occurrences, prior temporal causes, observation placement, capacity recovery, and continuation control; timing must enter through typed cause provenance plus a separate cutoff | +| Observation provenance inferred during deferred fanout | Rejected | Fanout may run blocks after publication and may coexist with a newer dirty revision; current block, worker phase, or latest feed state cannot reconstruct the source revision's cause. The oracle publication boundary and revision-owned dirty/Crossing records must carry it explicitly. | +| Phase-only `Deferred` N+1 floor | Rejected | A fanout turn first processing an older revision would derive N+1 from worker time and postpone it repeatedly. Latest/active dirty revisions and Crossing transitions must retain the source block, and the floor applies only when service still occurs in that block. | +| Permanent Fresh priority | Rejected | Repeated triggers could manufacture privilege and starve Running Actors | +| Actor-generated same-block recursion | Rejected | Violates one-generation causal firewall and bounded block work | +| Nonzero/one-Step-only cohort eligibility | Rejected | Contract length is not causal discovery authority; optional Step 0 and cold tails remain outside materialization | +| Dynamic later-Step re-cohorting | Rejected | Divergent state creates authority/ordering cost without earned 0.7.24 evidence | +| Task-shape cohorts | Rejected | Shared implementation shape is not shared causal authority | +| Weight/fee/class scheduler ranking | Rejected | Cost determines fit/fee, not deterministic rank | +| T0 guarantee | Rejected | Same-block service is best-effort and cannot outrank block validity or liveness | + +## Validity + +- `Establishes`: The bounded Fresh timing question, Q1/non-priority invariants, workload matrix, metrics, causal-firewall gate, artifact plan, exact permissive C0 identity, and the missing T+1 baseline prerequisite. +- `Does not establish`: T0 safety, benefit, throughput, or acceptance; exact Fresh/Running grant; live-chain latency; or any Q2/Q4 claim. +- `Known limitations`: No live-chain latency distribution was measured. The rejected candidate artifacts and bounded tests are diagnostic history only; no T0 runtime/configuration surface remains supported. Tree `2dea97b1` establishes only configuration/admission binding and the original separately metered cutoff authority. The T+1 evidence tree `dbf9009b` consumes that authority for `NextBlock` and carries complete regenerated local evidence. Observation, Crossing, and temporal provenance closure is complete in evidence tree `7fc73c25`; the minimal T0 candidate and deterministic hook-profile harness are now concrete, while comparative W1-W7 measurements remain pending. +- `Invalidation triggers`: Q1/FIFO/causality change, Step-0 storage change, resource-policy phase change, Trigger topology change, block policy, toolchain/backend, or production Weight/Wasm drift. +- `Residual qualitative insight`: First-Step latency, inter-Step gap, and completion latency are distinct; shared-cause cohorts admit mixed Contract lengths and normally end after Step 0. The `N+1` floor is eligibility, not a separate successor queue: an ordinary successor must reserve its canonical FIFO position when its source Step commits so later readiness cannot overtake it. A block-bound staging page outside FIFO cannot recover that interleaved order without becoming a second scheduler. + +## Follow-Up / Next Gradient + +```text +low-latency causal reaction target → T+1 baseline latency +→ unknown T0 ordering/firewall cost → Fresh cutoff/materialization/service +→ specify one generation → smallest low-pressure + sustained-Fresh falsifier +``` + +- `Eliminated hypotheses`: Generic Fresh priority, recursive Actor reaction, task-shape cohorts, and dynamic re-cohorting. +- `Remaining hypotheses`: None for same-block execution. Future Q1 cost work must preserve the fixed next-block eligibility floor and cannot reopen T0 without a new specification decision. +- `Stop condition`: Accept one exact timing architecture or reject T0 when no deterministic bounded ordering preserves a nonzero continuation floor. + +## Architecture Impact + +- `Affected physical truth`: Fresh readiness phase/cutoff, one-generation authority, Step-0 service, and Fresh/Running capacity geometry. +- `Architecture update`: None while Proposed; update only after Specification, implementation, tests, Weight, and correction converge. +- `Provenance citation`: Cite `actors/EXP-0018` only after an Accepted decision. + +## Relations + +- `Replaces`: None. +- `Supersedes`: EXP-0017 as the active default research direction, without invalidating its deferred quantum question. +- `Refines`: EXP-0013 one-Step cost and EXP-0016 inline Step-0 locality into a Fresh latency question. +- `Contradicts`: None. +- `Depends on`: Accepted Fresh causal-firewall Specification; converged post-EXP-0019 I4 baseline; EXP-0013; EXP-0016. +- `Baseline of`: None. +- `Confirms`: Q1 and Chunk4 are independent; causal cohorts derive from Trigger origin. +- `Invalidates`: None. +- `Semantic status under EXP-0019`: Current Category A proposal, but no candidate may be constructed against the pre-deletion User lifecycle baseline. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0019.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0019.md new file mode 100644 index 00000000..9e34fc44 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0019.md @@ -0,0 +1,238 @@ +# EXP-0019 — Minimal User Apoptosis + +| Field | Value | +| --- | --- | +| Status | Invalidated | +| Architecture release / experiment campaign | DEOS 0.7.24 | +| Date | 2026-08-28 | +| Affected domain | DEOS Actors User lifecycle, scheduler admission, custody, and state economics | +| Physical mechanism | Four independent fee boundaries, busy-period Trigger charging/coalescence, Pipeline Machine pricing strategy, Pipeline-admission-only minimal process GC, and zero-Step lifecycle geometry | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | Historical pre-deletion 0.7.24 local semantic baseline plus EXP-0013 | +| Successor experiment | EXP-0020 | + +## Decision Question + +Which Pipeline Machine pricing strategy best preserves one paid whole-pipeline control quantum over Q1 while each processed Trigger occurrence is charged independently before readiness coalescence, Actions remain pay-as-attempted, and insufficient paid-readiness consumption reclaims only process state—including a zero-Step immutable temporal case? + +## Governing Specification + +- `Exact sections`: `template/pallets/actors/docs/specification.en.md` → `1.4 Block-Paced Actor Cycles`, `2.2 Class and Mutability`, `2.3 Sovereign Accounts and Identifiers`, `2.4 Lifecycle, Execution Funding, and Terminal Order`, `4.3 State Holds and Fees`, and `6.4 Weight and Fee Derivation`; `docs/actors-resource-policy.specification.en.md` → `4. Admission and Actual Weight`; `docs/actors-performance-assurance.specification.en.md` → `3.5 Minimal User Apoptosis`. +- `Invariant`: Creation, each processed Trigger occurrence, each admitted Pipeline, and each invoked Action attempt are four separate fee owners. Trigger charging precedes setting/coalescing `pending_signal` and remains active while Running/Suspended; Pipeline charging occurs only when Idle service consumes readiness. Production favors one upfront bounded machine charge; future Action fees are never prepaid. Running/Suspended control cannot die economically, Action `FundingUnavailable` follows authored policy, and `CycleAdmissionInsufficient` exists only at Idle Pipeline admission. Apoptosis deletes process state/memberships only; sovereign custody and exact-slot derivation survive. Zero Steps is valid; User Immutable rejects owner destruction. +- `Reopen trigger`: Reopen Specification only if one upfront bounded machine charge cannot represent generated control geometry, current-attempt Action fees cannot remain transaction-local, zero-Step service cannot retain strict FIFO/terminal authority, or minimal cleanup cannot remain custody-neutral. System lifecycle evidence does not reopen User semantics by analogy. + +## Context and Hypothesis + +- `Current architecture`: A0 classifies User fee-native shortfall at each current Step as `BalanceExhausted` or `FeeBudgetExhausted`, admitting autonomous close cleanup. Close itself is already balance-neutral and deterministic sovereign custody already survives process deletion. +- `Reason`: Current-Step death can destroy a process after earlier pipeline effects commit, while a persistent funding wait retains unbounded-lifetime state. Prepaying future Action effects instead over-couples machine liveness to uncertain economic execution and creates a run balance ledger. Generated bounded control geometry permits one machine charge while each Action remains transaction-like. +- `Hypothesis`: P0 one upfront Pipeline Machine charge removes Running accounting touches and state versus P1 hold/refund and P2 per-Step charging, while preserving paid retry/continuation control. Trigger-family pricing can remain fixed-size and independently charge every processed occurrence, including already-pending busy Actors, without opening duplicate Pipelines; zero-Step temporal service can complete with no Action fee. +- `Materiality`: A1 must contain no economic close inside Running/Suspended, funding-wait state, or future Action-fee reserve. Acceptance requires exact Action fee/failure behavior, custody-root identity, exact-slot recovery, zero-Step immutable temporal completion, and either material P0 hot-path/state/Wasm simplification or evidence strong enough to select another candidate through Specification reopening. + +## Baseline and Candidates + +| ID | Architecture | Source tree | Runtime/config identity | +| --- | --- | --- | --- | +| A0 | Historical User economic apoptosis: current-Step shortfall selects mandatory close and permissionless cleanup | Commit `31927940`, tree `7de2acf5`; exact identities retained inline in this record | Q1, C6 Chunk4, P32, 20/80; benchmark Wasm `fe3ff004` | +| A1-P0 | Each processed occurrence charges Trigger service before coalescing; later Idle consumption charges one upfront bounded Pipeline Machine/cleanup amount; no run fee ledger; Actions pay actual effects per attempt; Pipeline-admission-only apoptosis; zero-Step support | Live production implementation plus disposable matched P0 benchmark feature at common comparator source `2d97f16a` | Production-leading; same Q1/C6/P32/20-80 controls | +| A1-P1 | Same Trigger boundary plus maximum Pipeline Machine hold with valid-actual settlement/refund at terminal completion; cumulative-actual and refundable-remainder authorities remain separate representations | Disposable FRAME candidates at common comparator source `2d97f16a`; synthetic predecessor model digest `7e26c2cd` | Experimental comparator; one reservation plus 56-byte raw key/64-byte value authority and one 19-byte Holds value while carried | +| A1-P2 | Same Trigger boundary plus hold-backed per-attempt machine settlement after readiness consumption; Actions remain independently per attempt | Disposable FRAME candidate at common comparator source `2d97f16a`; synthetic predecessor model digest `7e26c2cd` | Experimental comparator; same maximum reservation, one authority entry, and per-attempt hold/collection settlement | + +## Controls + +- `Controlled variables`: Q1 service, C6 authority, P32 bounds, Action semantics/effect Weight, current-attempt Action-fee settlement, block resources, database, toolchain, workload state, FIFO order, custody, and System service order. +- `Changed variables`: Viability moves from current Step to Idle consumption of paid readiness; Trigger occurrence charging separates temporally and economically from Pipeline Opening; temporal Trigger membership remains live while a Pipeline is busy; P0/P1/P2 vary machine accounting only; zero-Step C6 head/Opening is admitted; minimal Pipeline-admission-failure cleanup replaces economic close composition; funding-wait and lifecycle Transfer privilege remain absent; User Immutable owner close is rejected. +- `Semantic comparator boundary`: A1-P0 is the accepted production target. A1-P1/P2 intentionally vary externally charged machine economics and therefore provide comparative evidence only; neither can become production baseline without explicit Specification reopening. +- `Confounders`: A1-P0 production Weight, metadata, and pre-format production Wasm are converged. The W1/W3 physical comparison uses disposable benchmark-only Cargo features whose linked Wasm identities necessarily differ from production/default builds; native metadata/schema/behavior remain equal, and no comparator Wasm is production authority. Generated coefficients can favor a candidate on isolated branches despite added topology, so omitted coefficients and negative deltas are model outcomes rather than proof of zero work. A0 sweep owners and focused close assurance use exact benchmark Wasm `fe3ff004`; unrelated T0 Fresh timing work does not enter the comparison. + +## Benchmark Class, Workloads, and Criteria + +- `Benchmark class`: Generated pallet Weight plus deterministic pallet/runtime state-machine and semantic-surface comparison. + +| Workload | State/population geometry | Hot/cold assumptions | Purpose | +| --- | --- | --- | --- | +| W1 Machine strategy | User pipelines `0/1/4/8/32`; no/max Precondition geometry/reads; no/max RetryLater; short/long service gaps | Identical control semantics; P0/P1/P2 accounting differs | Compare machine RefTime/ProofSize/I/O/touches/state/Wasm and charged-vs-real work | +| W2 Trigger/Pipeline separation | Each Trigger family across Idle immediate Opening, busy occurrence, repeated busy coalescence, delayed Opening, paid occurrence followed by Pipeline insufficiency, and zero-Step AtTime one-shot | Separate source/ordinary transaction, per-occurrence materialization, latch, and later Opening accounting | Derive defensible Trigger owners and prove no combined charge or duplicate Pipeline | +| W3 Running pipeline | Admitted `1/4/8/32` runs at every cursor | Current C6 fragment only | Prove P0 has no balance viability, machine accounting, or suffix reconstruction | +| W4 Action failure/retry | Success, typed failure, max retries, and fee-native underfunding | Action effects and actual evidence controlled | Prove invoked effects pay, unfunded non-invoked effects do not, and no apoptosis occurs | +| W5 Pipeline-admission apoptosis | Identical sufficient/insufficient paid pending Idle Users plus Running/Suspended controls | Custody prepopulated across assets/receipts; prior Trigger fee committed | Measure minimal process GC, Trigger non-refund, and absence of mid-run owner or adapter invocation | +| W6 zero-Step temporal | Immutable AtTime one-shot with `steps = []` and auto-close nonce 1 | No Step/body/snapshot/Action | Separate Creation/Trigger/Machine/Action/cleanup dimensions and custody survival | +| W7 exact-slot recovery | Closed slot with residual custody, fresh Mutable recovery Contract | Deterministic sovereign account | Prove custody continuity without rescue subsystem | +| W8 no-wait surface/System controls | Metadata/state counts plus representative System paths | No `ActorFundingWait`, run machine ledger, or ingress wake | Prove semantic compression and System non-regression | + +- `Metrics`: Component-wise RefTime/ProofSize, reads/writes, accounting touches, touched keys/bytes, separately incremental Trigger-occurrence and Pipeline-Opening cost, latch/fee/Cycle transitions, Trigger family cost, Opening cost by Steps/Predicates/reads/retries, charged-versus-real machine work, Action maximum/actual charges, Running cost by cursor/gap, minimal cleanup Weight, Wasm/run-state bytes, lifecycle/API counts, custody roots, and exact-slot identity. +- `Statistical method`: Generated Weight uses production steps/repeats with matching benchmark Wasm and raw model review. Deterministic semantic counts use exact source/metadata manifests. Comparative native profiles retain all declared samples with no outlier deletion. +- `Acceptance criteria`: W1 bounds every strategy and P0 materially reduces accounting/state complexity unless another comparator earns reopening; W2 prices every successfully processed occurrence independently, charges busy/already-pending occurrences, coalesces them into one later Pipeline, reports separate Trigger/Pipeline deltas, and preserves prior Trigger charges when Pipeline admission fails; W3 has no P0 machine accounting after Opening; W4 charges every invoked Action actual effect and treats underfunding normally; W5 closes only insufficient paid-readiness consumption and preserves custody; W6 completes with zero Action fee and rejects owner close; W7 reattaches exactly; W8 has zero wait/run-ledger topology and unchanged System order. +- `Rejection criteria`: Any combined Trigger/Pipeline charge, free successfully processed busy occurrence, duplicate Pipeline from coalesced readiness, Trigger refund on later Pipeline insufficiency, future Action-fee prepayment, economic close inside Running/Suspended, unpaid invoked effect, P0 machine hold/refund/per-Step touch, persistent funding wait, custody mutation, owner destruction of Immutable, invalid zero-Step authority, unbounded Opening, failed recovery, or System drift. + +## Environment and Artifact Identity + +| Artifact | Identity | +| --- | --- | +| Source commit | A0 `31927940f2b37892b61af40a862b78d95f8fac76`; A1 worktree baseline `14dc211e15c4913bc979fd5324559f7d43c334d0`; W5 closure `aaeb0ab` | +| Source tree | A0 `7de2acf598d45dfcae6cb53fb8f4dc15a928ddae`; Actors subtree `9b12273902a6d44a79beaa3963adc87922845348`; W5 closure tree `5f73bc32b21822741540819ac7abaf92a3b7e947` | +| Rust toolchain | `rustc 1.94.1 (e408947bf 2026-03-25)`; repository-pinned Cargo toolchain | +| Benchmark runtime Wasm | A0 `fe3ff0042fba99362474b0c767e6b7aaae61ec82b5c07c45437e33d02295f088` (`6,029,247` bytes); A1 production generation `6001d5acfda304c5956b3368a58b0b15d2f99396dc7f94b07679dc3233da7528` (`6,056,056` bytes); W1/W3 common-source P0 `020f3501` (`6,068,362`), P1 cumulative `244c952a` (`6,074,058`), P1 remaining `59d2f406` (`6,074,701`), and P2 `62eb26d1` (`6,074,405`); W2 Manual categorical profile `cabe23f5ccd165d6466a9a8976dff97ea5fd218ab177939c7be42a64565ee56c` (`6,063,294` bytes) and AddressEvent categorical profile `0a1435337a41b6e1f6bc2e1828f20bab9da63954e54109ab31c5ef380777c1c2` (`6,062,241` bytes); W5 closure `51f6cf3720354846a3f807411f924db1c19b5dff71caa384b7f5675d8d66b3d9` | +| Post-generation production runtime Wasm | A0 pre-I4 `20df93fe148f7e8317c8e3650453b9aad19bd754d8d86c3f109c02105edc5983` (`1,412,191` bytes); A1 `d7ec0fe0ac29adc6f9c1447cf9745154a7d956480fdcdae190bd1f1b315386a9` (`1,437,083` bytes) | +| Final release production runtime Wasm | formatted-tree production `364ba474a5775285bc53d214b1f06a613e2d3975bfcc3f175edf859ed94c6d01`; final release-tree confirmation remains external | +| Generated Weight | A0 production `0da809ee`; A1 complete P32 `b255b4d2ce64d5f74e2ffcd36eb3e2f0e3f296bf83a1cbd70739f94c1961040e`; W5 complete-generation closure `afdb03bbe0268e63dc8c0609ec18187050157f58b7b14809c4a37c17052c836e` | +| Database/backend | FRAME benchmark compiled Wasm, cache 1024, worst-case map size 1,000,000 on recorded `fedora` AMD Ryzen 7 4800H host | +| Benchmark command/config | Disposable `/tmp/deos-exp-0019-frame/run-exp0019-w1-w3-four-comparators.sh` built and identity-verified all four Wasms, then invoked `./scripts/benchmarks.sh --candidate FEATURE --skip-build --steps 50 --repeat 20 --extrinsic NAME --output FILE pallet_deos_actors` sequentially for eight owners each; no concurrent bencher | +| Raw evidence | A0 exact identities and focused-output provenance retained inline; P1/P2 synthetic model digest `7e26c2cd8e95358f4e99c6f711bcdca8e854998110ca6a9f8b75a972ee29707a`; normalized model `d9384e48`; common-source external bundle `/tmp/deos-exp-0019-frame/evidence/exp-0019-w1-w3-four-comparator-common-source` with 32-output manifest `78a8d3a060a2f7abf8114f0cb6978e6931466be3912a6727f90712ce3755e82c`, normalized analysis `15f862da3b032d814b04f04aa369d0b906d3131f347e71d6ea347c34ef347f30`, report `20e7dcd7b0c4f65769bf9dd6bbb3c5b6ee47acc5402c4be50690aa41ecc885e2`, and profile manifest `cd7f4270c7562d81d2b511c488042e3d7a3e89f34480fd4bde47638d3c0da58a`; W2 Manual outputs `/tmp/exp-0019-w2-manual-{idle,pending,suspended}.rs` with hashes `4e0df3d1`, `9d778740`, and `d1fed92b`; AddressEvent outputs `/tmp/exp-0019-w2-address-{idle,pending,suspended}.rs` with hashes `2172c7b0`, `e6233d2e`, and `cedefa7a` | + +## Measurements + +| Candidate | Workload | RefTime | ProofSize | Reads | Writes | Other decision metrics | +| --- | --- | ---: | ---: | ---: | ---: | --- | +| A0 | permissionless sweep probe | `55,524,000` base | `5,702` | `8` | `0` | Active-survival probe; changed classification surface | +| A0 | permissionless sweep-many insolvency cleanup | `22,133,025 + 118,726,481n` | `1,489 + 5,206n` | `3 + 13n` | `2 + 11n` | `n ∈ [1,5]`; direct apoptosis-dependent owner | +| A0 | P32 minimal opening-close assurance | `244,982,247` | `28,329` | `21` | `20` | Mixed lifecycle cleanup, not apoptosis-only | +| A0 | P32 maximum opening-close assurance | `1,405,800,875` | `185,618` | `152` | `20` | Mixed lifecycle cleanup, not apoptosis-only | +| A1-P0 | W5 Pipeline-admission apoptosis | `161,616,000` | `5,736` | `15` | `15` | Maximum Contract; no Crossing, ObservationChange, temporal, or Run topology; scheduler-only owner | +| A1-P0 | W8 final surface | Not applicable | Not applicable | Not applicable | Not applicable | 1,438,669-byte formatted-tree production Wasm; 2,673-byte maximum encoded Run; 3 lifecycle states; 39 events; 79 errors; 3 runtime API traits/5 methods; 149 Weight owners; 667 listed package tests; zero wait/ledger/wake source or client files | +| A0 | semantic surface | Not applicable | Not applicable | Not applicable | Not applicable | 41 reason occurrences across 11 source/client files; 27 test occurrences; five benchmark/sweep occurrences | +| A1-P0 generated | Manual Trigger occurrence | `162,034,000` | `9,635` | `14` | `7` | One User occurrence; source dispatch and readiness placement | +| A1-P0 W2 diagnostic | Manual first Idle occurrence | `162,524,000` | `8,365` | `14` | `7` | Exact categorical profile; creates first canonical queue placement | +| A1-P0 W2 diagnostic | Manual repeated pending occurrence | `120,967,000` | `8,206` | `9` | `2` | Exact categorical profile; charges and coalesces without a second placement | +| A1-P0 W2 diagnostic | Manual busy-Suspended occurrence | `175,235,000` | `8,519` | `13` | `7` | Exact categorical profile; reads retained Run authority and latches future readiness | +| A1-P0 generated | AddressEvent Trigger occurrence | `172,930,000` | `8,367` | `14` | `7` | One matching User occurrence; authoritative ingress remains separately owned | +| A1-P0 W2 diagnostic | AddressEvent first Idle occurrence | `169,368,000` | `8,367` | `14` | `7` | Exact categorical profile; filter match creates first canonical queue placement | +| A1-P0 W2 diagnostic | AddressEvent repeated pending occurrence | `134,936,000` | `8,208` | `9` | `2` | Exact categorical profile; charges and coalesces without a second placement | +| A1-P0 W2 diagnostic | AddressEvent busy-Suspended occurrence | `191,299,000` | `8,521` | `13` | `7` | Exact categorical profile; filter match reads retained Run authority and latches future readiness | +| A1-P0 generated | ObservationChange Trigger occurrence | `115,170,000` | `8,296` | `13` | `7` | One affected User occurrence; publisher ingress/fanout traversal excluded | +| A1-P0 generated | ObservationCrossing fire occurrence | `503,983,000` | `164,107` | `89` | `80` | One affected User fire from retained threshold through phase movement, collection, and placement; Oracle publication excluded | +| A1-P0 generated | AtTime Trigger occurrence | `221,610,000` | `8,552` | `20` | `9` | One due affected User after temporal traversal, including one-shot consumption, collection, and readiness materialization | +| A1-P0 generated | Cadenced Trigger occurrence | `221,052,000` | `8,505` | `22` | `12` | One due affected User after temporal traversal, including next-deadline placement, collection, and readiness materialization | +| A1-P0 generated | zero-Step Pipeline Machine | `44,211,000` | `4,952` | `5` | `3` | Inner persistent Opening after FIFO consumption; no Run, Action, or cleanup | +| A1-P0 generated | Maximum admitted full Opening | `522,631,000` | `27,824` | `30` | `15` | Maximum Contract/funding geometry; parameterized inner owners retain maximum predicate/snapshot/heavy-Task branches | +| A1-P0 generated | Pipeline cleanup | `736,628,000` | `81,886` | `64` | `63` | Exact generated `close_actor` owner; includes state-hold release without custody movement | +| A1-P0 generated | Blocked broad-fanout fallback | `116,528,430,000` | `304,734` | `527` | `136` | Required production owner restored to complete generation rather than diagnostic exclusion | +| A1-P0 matched comparator | W1/W3 eight-owner common matrix | `1,664,971,000..3,307,501,066` | `7,708..50,489` | `19..36` | `9..21` | No machine authority/hold; 57-byte Opening fee event; Wasm `6,068,362` bytes | +| A1-P1 cumulative | W1/W3 eight-owner common matrix | `2,003,531,000..3,463,514,296` | `6,464..48,178` | `21..37` | `10..23` | 56-byte raw key/64-byte authority value plus 19-byte Holds value; 25-byte reservation and 106-byte settlement events; Wasm `+5,696` bytes vs matched P0 | +| A1-P1 remaining | W1/W3 eight-owner common matrix | `1,991,588,000..3,456,343,518` | `6,464..48,178` | `21..37` | `10..23` | Exact collection/refund parity with cumulative representation; same retained state; Wasm `+6,339` bytes vs matched P0 | +| A1-P2 held | W1/W3 eight-owner common matrix | `2,011,284,000..3,491,440,460` | `8,687..48,178` | `21..39` | `12..23` | Same authority/hold entry with per-attempt hold and fee settlement; Wasm `+6,043` bytes vs matched P0 | +| A1-P2 negative control | W1 no-hold liveness | Not a performance result | Not a performance result | Model/test witness | Model/test witness | Capacity passes at Opening, an intervening fee-native Action debit makes a later no-hold attempt unaffordable; the hold-backed fixture rejects that debit and preserves liveness | + +P1 cannot derive terminal valid actual from current Run state because actual control Weight exists per attempt while the persisted head retains outcomes rather than Weight history. It therefore needs cumulative actual, remaining reservation, or equivalent durable authority. Production `Balance = u128` and FRAME `Weight = { ref_time: u64, proof_size: u64 }` are possible 16-byte representations, but no lower-bound proof excludes a more compact encoding or reuse of the reservation authority. Ordinary semantic Run state is controlled and common to all strategies. + +| Steps | P0 upfront maximum fee | P0 carry after Opening | P1 minimum extra topology | P2 minimum attempt settlements | +| ---: | ---: | ---: | --- | ---: | +| 0 | `72,846,712` | `0` bytes | Reservation and terminal settlement/refund in the same zero-Step service; no persisted accumulator | `1` | +| 1 | `84,925,675` | `0` bytes | Reservation and terminal settlement/refund in the same one-Step service; no persisted accumulator | `1` | +| 4 | `129,967,161` | `0` bytes | One held maximum plus a 56-byte raw key/64-byte authority value and one 19-byte Holds value across `3` nonterminal carries | `4` | +| 8 | `189,946,521` | `0` bytes | Same retained authority/hold geometry across `7` nonterminal carries | `8` | +| 32 | `549,822,681` | `0` bytes | Same retained authority/hold geometry across `31` nonterminal carries | `32` | + +For the current plain-Transfer, no-Precondition, no-retry production P0 vector, unused cleanup capacity is `69,093,024`: `94.85%`, `81.36%`, `53.16%`, `36.37%`, and `12.57%` of the respective totals above. The common-source runtime profiles now measure settlement directly: each strategy emitted 339 exact attempt rows grouped into 31 independent Pipelines over `0/1/4/8/32`, `p=0/4`, every cursor, retry attempts `1..10`, and `g=1/1000`. P0 event fields prove one-time Opening collection and zero later recollection; P1 representations are exact collection/refund equivalents; P2 reaches the same total through per-attempt collection. + +| Profile workload | P0 upfront collected | Valid-actual machine | P1/P2 collected | P1/P2 refund | Machine utilization | +| --- | ---: | ---: | ---: | ---: | ---: | +| zero-Step | `72,846,712` | `3,753,688` | `3,753,688` | `69,093,024` | `100.00%` | +| one-Step, `p=0` | `84,925,675` | `3,771,397` | `3,771,397` | `81,154,278` | `23.82%` | +| four-Step carry/terminal, `p=0` | `129,967,161` | `33,115,912` | `33,115,912` | `96,851,249` | `54.40%` | +| 32-Step, `p=4`, `g=1000` | `590,585,241` | `340,698,035` | `340,698,035` | `249,887,206` | `65.33%` | +| four-Step retry exhaustion, `p=4`, `g=1000` | `728,787,594` | `99,734,335` | `168,827,359` | `559,960,235` | `15.12%` | + +Persistent completion refunds all cleanup capacity under P1/P2; retry-exhaustion close collects the generated `69,093,024` cleanup upper because no narrower cleanup-actual owner exists. Every strategy retains the same maximum admission reservation to preserve whole-pipeline liveness. + +Current W1-W8 evidence is partial: + +| Workload | Established | Missing decision evidence | +| --- | --- | --- | +| W1 | Common-source P0/P1 cumulative/P1 remaining/P2 FRAME matrix across `0/1/4/8/32`, `p=0/4`, retry `1..10`, short/long gaps, RefTime, ProofSize, I/O, event bytes, raw authority/Holds bytes, Wasm, and 1,356 charged-versus-real attempt rows; no-hold liveness counterexample | No material gap identified | +| W2 | Six generated Trigger owners, semantic busy/coalesced/underfunded family tests, and executed exact Manual and AddressEvent Idle-first/repeated-pending/busy-Suspended categorical profiles without widening production `WeightInfo` | Add separate ObservationChange/ObservationCrossing/AtTime/Cadenced and later-Opening measurements for delayed, insufficient, and zero-Step AtTime cases | +| W3 | Matched Running carry/terminal owners over every declared cursor representative and `g=1/1000`; P0 has no authority or settlement after Opening | No material gap identified | +| W4 | `ActionFeeCharged` behavioral tests for invoked success/failure/retry/rollback and non-invoked `FundingUnavailable` | No material gap identified | +| W5 | Exact-capacity/minus-one, collector rollback, breaker, custody, no-mid-Pipeline-death behavior, and generated `pipeline_admission_apoptosis` at `161,616,000 / 5,736`, 15 reads/writes | No material gap; scheduler admission uses the narrow owner while economic cleanup capacity remains conservatively bound to `close_actor` | +| W6 | Generated zero-Step machine owner and immutable AtTime/no-Run/no-Action/nonce/auto-close/custody/simulation tests | No material gap identified | +| W7 | Package and production-runtime exact-slot tests | No material gap identified | +| W8 | Final formatted-tree counts, absence audit, metadata/ABI projections, System exemption vector, and class-neutral scheduler tests | No material gap identified | + +## Derived Metrics + +| Candidate | Workload | RefTime delta | ProofSize delta | Throughput / latency | Marginal or amortized cost | Pareto class | +| --- | --- | ---: | ---: | --- | --- | --- | +| A1-P0 | W1 machine accounting | Baseline | Baseline | One 57-byte fee event at Opening; zero later machine-economic state/transitions | Charges the complete certified machine/cleanup bound once | Physical/state simplicity winner; economic overcharge measured | +| A1-P1 cumulative / remaining | W1 machine accounting | Branch-dependent: representative maximum deltas `-286,206,554..+558,173,520` | Branch-dependent: `-5,515..+2,766` | Opening hold, one 56/64-byte authority entry, 25-byte reservation, 106-byte attempt events, terminal settlement | Refunds unused machine/cleanup capacity; representations are economically equivalent | Tradeoff; not globally Pareto-dominant because state/Wasm/event complexity rises while isolated generated branches vary | +| A1-P2 | W1 machine accounting | Representative maximum deltas `+167,968,183..+593,879,839` except shared terminal-model variation | Representative maximum deltas `-2,812..+2,766` | Same authority plus hold/fee writes per attempt and terminal release | Collects valid actual incrementally | Attempt/carry topology is dominated by P1; terminal model coefficients overlap, so no global numeric Pareto claim | + +## Statistical / Variance Notes + +A0 focused sweep owners were generated at 50 steps/20 repeats against one retained matching benchmark Wasm with no sample removal. The sweep-many linear model reports standard error `72,294` on its per-Actor RefTime coefficient. A1-P0 production owners were generated at 50 steps/20 repeats against benchmark Wasm `6001d5ac`; the corrected reachable Opening fixture and restored blocked-fanout owner passed focused runs before the complete rerun. + +The common W1/W3 comparison ran all 32 owner/comparator combinations sequentially at 50 steps/20 repeats on one source with no sample removal or concurrent bencher. Generated omitted coefficients mean model invariance only, not measured zero work; negative candidate deltas remain branch-model outcomes rather than global superiority. Deterministic profiles use all 1,356 emitted rows with no outlier deletion. P2 cannot remove maximum reservation or per-attempt settlement while preserving liveness. P1 authority is a 56-byte raw key plus 64-byte value and one 19-byte Holds value under production AccountId32 geometry. + +## Result + +The exact A0 changed boundary remains captured. A1-P0 has complete generated P32 production Weight, runtime-bound vectors, metadata, benchmark Wasm, and pre-format production Wasm. The common-source disposable FRAME matrix completes W1/W3 with 32 generated models, exact comparator Wasm deltas, raw authority/Holds bytes, complete strategy/ordinary event classification, and 1,356 charged-versus-real rows. P1 cumulative and remaining are economically equivalent; P2 reaches the same valid-actual total with additional per-attempt hold/collection topology. P0 carries no machine state after Opening but charges the entire admitted maximum and cleanup capacity. W1, W3, W4, W6, and W7 are complete. W2 now has exact Manual categorical measurements: first Idle placement is `162,524,000 / 8,365 / 14r/7w`, repeated pending coalescence is `120,967,000 / 8,206 / 9r/2w`, and busy-Suspended latching is `175,235,000 / 8,519 / 13r/7w`. AddressEvent respectively measures `169,368,000 / 8,367 / 14r/7w`, `134,936,000 / 8,208 / 9r/2w`, and `191,299,000 / 8,521 / 13r/7w`. Remaining family/Opening profiles belong to the replacement Trigger-latching experiment. W5 and W8 are complete, including the narrow apoptosis owner and final formatted-tree surface counts. + +## Interpretation + +P0 is the physical/state simplicity winner: one Opening collection, no machine authority, no machine hold, no later accounting, and the smallest comparator Wasm. P1 buys material User refunds with one retained authority/hold and one settlement event per attempt; cumulative and remaining representations are economically equivalent, while generated physical coefficients vary by branch. P2 adds per-attempt hold and fee-ledger writes without changing total valid-actual economics; its attempt/carry topology is dominated by P1, while terminal model coefficients overlap and prevent a global numeric Pareto claim. No P0 semantic failure or Specification reopen trigger is observed. This invalidated integrated record cannot issue a new final decision, but accepted EXP-0020 explicitly retains the inherited P0 Pipeline Machine semantics after W5 and exact-tree closure. + +## Decision + +Invalidated. The accepted Trigger-latching contract changed the useful occurrence from every successfully processed source event to only `pending_signal: false -> true`; redundant latched-period activity now performs no Actor-specific evaluation or fee collection. W1/W3/W4/W5/W6/W7 Pipeline Machine, Action, apoptosis, custody, and zero-Step evidence remains reusable input for EXP-0020, but W2 paid-coalescence candidates and any integrated final decision no longer apply. P0 remains the production-leading machine strategy only as inherited evidence, not as a decision of this invalidated record. + +## Rejected Alternatives and Tradeoffs + +| Candidate | Decision | Evidence-backed reason | +| --- | --- | --- | +| Current-Step apoptosis | Rejected semantically | Can kill a process between durable Steps of one admitted pipeline | +| Persistent `ActorFundingWait` | Rejected semantically | Retains nonviable process state and adds wait records, ingress wake, reactivation authority, and scheduler reinsertion | +| No User apoptosis | Rejected semantically | Gives abandoned unfunded process state indefinite lifetime without a release-owned resource justification | +| Auto-drain custody during close | Rejected semantically | Runtime cannot infer asset inventory or User exit policy; close remains lifecycle-only | +| Lifecycle source-exhausting Transfer | Rejected semantically | `Transfer(AllAvailable)` is ordinary authored behavior and cannot consume ledger/Cycle reserves through close privilege | +| Prepay future Action effects | Rejected semantically | Couples uncertain economic execution to machine admission and charges effects that may never be invoked | +| Recompute machine solvency every Step | Rejected semantically | Violates paid whole-pipeline authority and taxes the Q1 hot path | +| Owner-closeable User Immutable | Rejected semantically | Breaks true temporal-vault/timelock authority by permitting premature destruction | + +## Validity + +- `Establishes`: Exact A0 identity, accepted semantic target, complete generated A1-P0 P32 production Weight, runtime-bound vectors, pre-format production identities, completed W1/W3/W4/W6/W7 behavior/evidence, common-source P0/P1/P2 RefTime/ProofSize/I/O/Wasm models, raw authority/Holds bytes, exact charged-versus-real geometry, P1 representation equivalence, P2 attempt-scaling cost, and one exact no-hold liveness counterexample. +- `Does not establish`: Trigger-latching W2 semantics, a new integrated decision after invalidation, or release readiness; accepted EXP-0020 owns the replacement Trigger decision and explicitly inherits P0 Pipeline Machine semantics. +- `Known limitations`: The candidate code and 32 raw benchmark outputs remain disposable external evidence under `/tmp`; this record retains exact commands, hashes, identities, representative normalized ranges, state bytes, and charged economics. Comparator Wasm deltas include Cargo-feature/link-layout identity and are not production Wasm deltas. Generated omitted coefficients are not measured zero cost. The later W5 owner narrows scheduler admission only; economic cleanup capacity intentionally remains conservative. Visible client cost presentation remains open outside this invalidated experiment. +- `Invalidation triggers`: Trigger charging/coalescence changed to useful-transition latching, invalidating the integrated decision question and W2 evidence on 2026-08-28. Future changes to Q1, C6 authority, fee payer, deterministic sovereign derivation, state-hold ownership, or System lifecycle require reclassification of inherited evidence. +- `Residual qualitative insight`: One bounded upfront machine charge can own control liveness while Q1 and Action fees remain current-attempt atomic and custody remains process-independent. + +Existing evidence classification: + +| Record | Category | Semantic status | Still establishes | No longer establishes / required rerun | +| --- | --- | --- | --- | --- | +| EXP-0001 | A | Historical unaffected | Released throughput miss | No A1 authority; no rerun | +| EXP-0002 | A | Invalidated decision, unaffected reason | P32 tuple alone caused fanout ceiling tax | No apoptosis claim; no rerun | +| EXP-0003 | A | Rejected geometry evidence current | Naive fragmentation losses and inline-Step-0 pressure | No apoptosis claim; no rerun | +| EXP-0010 | B | Partially superseded | Compact activation, no-ceiling-tax, queue/wakeup parity, ordinary fanout | Insolvent-User mixed branch and permissionless insolvency cleanup become historical; rerun only changed fanout branch maxima | +| EXP-0013 | B | Partially superseded | Loaded-state reuse and non-closing direct control owners | Terminal-close assurance cannot justify apoptosis reservation/hot ownership; retain as lifecycle-cleanup evidence and rerun changed owner composition | +| EXP-0016 | A | Current | C6 Chunk4 geometry, inline Step 0, lazy tails, lifecycle geometry comparison | Close numbers are historical physical inputs, not economic-death authority; rerun close only because A1 state partitions change | +| EXP-0017 | A | Current but dormant | Q2/Q4 reopen gate | Future baseline must be post-A1; no immediate rerun | +| EXP-0018 | A | Current proposal | Fresh timing question and controls | Candidate construction must use post-A1 baseline; no pre-A1 measurement | + +## Follow-Up / Next Gradient + +```text +current-Step death → admitted-pipeline safety defect → split Trigger occurrence from Pipeline admission and Action effects +→ compare P0/P1/P2 → price Trigger families under busy coalescence → Pipeline-admission GC + zero-Step fixture +→ generated production evidence +``` + +- `Eliminated hypotheses`: Current-Step death is safe inside a pipeline; persistent funding wait is the preferred replacement; User Immutable requires protocol-owned custody disposal; close must drain custody. +- `Remaining hypotheses`: P1's measured User refund does not justify reopening against P0's physical simplicity; any future reopen requires a new valid experiment because Trigger-latching semantics invalidated this integrated comparison. +- `Stop condition`: W1-W8 satisfy the assurance contract, interpretation selects P0 or explicitly reopens strategy, no wait/run-ledger or mid-pipeline death owner remains, and the exact formatted tree plus Weight/metadata/ABI/Wasm identities converge. + +## Architecture Impact + +- `Affected physical truth`: Trigger pricing/collection, busy-period occurrence materialization, independent trigger-temporal versus Pipeline service placement, Pipeline Machine projection/charge, removal of run budget authority, current-Action settlement, zero-Step C6/Opening, Pipeline-admission classification, minimal cleanup, Immutable close authority, APIs/benchmarks/tests. +- `Architecture update`: Package and integration architecture update only after A1 implementation/tests/correction/generated evidence converge. +- `Provenance citation`: `Actors EXP-0019`. + +## Relations + +- `Replaces`: User economic-apoptosis semantic baseline, not its historical evidence. +- `Supersedes`: EXP-0013 terminal-close assurance only as justification for hot-path apoptosis reservation; no physical measurement is deleted. +- `Refines`: EXP-0010 mixed terminal branch and EXP-0016 lifecycle interpretation. +- `Contradicts`: Historical current-Step apoptosis and the unaccepted persistent funding-wait replacement; it retains only Idle boundary apoptosis. +- `Depends on`: Q1, C6 generalized to optional Step 0, generated Trigger/control maxima, valid-actual Action-effect settlement, deterministic sovereign custody, and exact-slot recreation. +- `Baseline of`: Future post-apoptosis I4 evidence, EXP-0018 preparation, and any reopened Q1 optimization. +- `Confirms`: Close is already balance-neutral and custody identity is process-independent. +- `Invalidates`: Only apoptosis-dependent scheduler, sweep, reservation, variant, setup, and mixed-branch conclusions. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0020.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0020.md new file mode 100644 index 00000000..b1d377fb --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0020.md @@ -0,0 +1,163 @@ +# EXP-0020 — Trigger Latch Disable and Re-arm Topology + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Architecture release / experiment campaign | DEOS 0.7.24 | +| Date | 2026-08-28 | +| Affected domain | DEOS Actors Trigger detection, readiness, fees, and Pipeline Opening | +| Physical mechanism | Useful-transition Trigger latching, detector disablement, and authoritative-state re-arm | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | EXP-0019 | +| Successor experiment | None | + +## Decision Question + +Which bounded detector disable/re-arm topology enforces zero Actor-specific Trigger evaluation, fee, activation, and causal-history accumulation while `pending_signal == true` without taxing dormant Actors or weakening current-state re-arm? + +## Governing Specification + +- `Exact section`: `template/pallets/actors/docs/specification.en.md` → `1.2 Declarative Trigger Topology`, `1.4 Block-Paced Actor Cycles`, `3.2 Triggers and Timing`, `4.3 State Holds and Fees`, and `7.1 Events`; `docs/actors-performance-assurance.specification.en.md` → `3.5 Minimal User Apoptosis`. +- `Invariant`: Only `pending_signal: false -> true` is a useful Actor-specific Trigger occurrence and charge. While latched, redundant sources do no Actor-specific Trigger evaluation, fee collection, activation, or history accumulation. Opening re-arms from current authoritative state and preserves at most one active plus one deferred Pipeline. +- `Reopen trigger`: Reopen Specification if a family cannot re-arm from current authoritative state without replay, if suppressing latched work would lose source truth required outside Actors, or if one-shot AtTime cannot remain consumed independently of latch clearing. + +## Context and Hypothesis + +- `Baseline architecture`: The invalidated EXP-0019 baseline kept detector authority active while latched, evaluated repeated family occurrences, charged each successful User occurrence, and reported paid coalescence. Pipeline Machine P0, Action, apoptosis, custody, and zero-Step evidence remains reusable. +- `Reason`: Paid coalescence permits source spam to impose repeated Actor-specific computation and fees while producing no additional readiness or Pipeline. +- `Hypothesis`: Eagerly removing indexed detector memberships on useful latching and reinstalling them from current authoritative state during Opening eliminates latched-period Actor work. Manual and AddressEvent can reject from one hot latch probe because they have no detector membership. A resident generation-disabled membership may reduce re-arm writes only if workers can reject it without Actor-state loading and without increasing source-frontier cost. +- `Materiality`: Acceptance requires zero Actor-specific fee/event/activation for redundant latched sources, no Actor-state load by indexed detector workers for disabled Actors, bounded atomic disable/re-arm, current-state Crossing/cadence behavior, no dormant-population tax, and no regression to inherited P0 Pipeline/Action semantics. + +## Baseline and Candidates + +| ID | Architecture | Source tree | Runtime/config identity | +| --- | --- | --- | --- | +| B0 | EXP-0019 paid coalescence with live detector memberships | Commit `20ddaa0e`; tree `88cf684c` plus current specification diff | Q1, C6 Chunk4, P32, 20/80 | +| A1 | Direct-family early rejection plus Cadenced eager detach/current-time re-arm; eager detach variants for ObservationChange and Crossing rejected | Tree `677dae5d` | Same runtime constants | +| A2 | Retain indexed entries behind detector-local disabled authority; skip before Actor-state load and rederive authoritative Crossing phase/revision during Opening | Tree `677dae5d` | Same runtime constants | + +## Controls + +- `Controlled variables`: Trigger semantics, Q1/C6/P32 geometry, Pipeline Machine P0, Action settlement, FIFO order, source publication, authoritative Oracle/timestamp state, toolchain, runtime constants, benchmark method, database, state populations, and workloads. +- `Changed variables`: Indexed membership removal/reinstallation versus membership-local disabled generation; direct families share the same hot-latch rejection boundary. +- `Confounders`: Current implementation and generated Weight still encode invalidated paid coalescence. Exact candidate source, Wasm, and Weight identities become available only after implementation. + +## Benchmark Class, Workloads, and Criteria + +- `Benchmark class`: Pallet benchmark, production-Wasm benchmark, deterministic semantic tests, and bounded fanout/herd profiles. + +| Workload | State/population geometry | Hot/cold assumptions | Purpose | +| --- | --- | --- | --- | +| W1 Direct latch | Manual and AddressEvent across unlatched, Running-latch, Suspended-latch, and repeated latched sources | Hot state available; no detector index | Prove one useful charge and zero redundant Actor-specific evaluation/fee | +| W2 ObservationChange | `1/64/10,000` subscribers with mixed latched/unlatched pages | Current feed revision authoritative | Compare detach versus membership-local disabled rejection and re-subscription cost | +| W3 Crossing | Armed and WaitingForRearm, then arbitrary latched-period reversals | Current value/revision authoritative; no replay | Prove membership disablement and current-state phase derivation | +| W4 Temporal | AtTime one-shot and Cadenced across short/long latched intervals | Current timestamp authoritative | Prove one-shot consumption and cadence re-arm strictly after current tick | +| W5 Opening | Zero/one/32-Step Opening with each indexed family latched | Current Contract fragment only | Measure re-arm RefTime, ProofSize, reads/writes, and atomic rollback | +| W6 DoS/herd | Repeated `10/100/1,000/10,000` source events against fully latched cohorts | Source publication remains independently charged | Prove no Actor-specific scaling with latched identity population | + +- `Metrics`: Component-wise RefTime/ProofSize, reads/writes, actor-state loads, detector entries visited, fee/event/activation counts, disable/re-arm writes, source-frontier cost, Opening delta, persistent bytes, Wasm bytes, and exact authoritative phase/deadline. +- `Statistical method`: Generated Weight uses 50 steps/20 repeats and matching benchmark Wasm with raw model review. Deterministic tests assert exact state/event/fee roots. Native herd profiles use declared fixed populations and all samples without outlier deletion. +- `Acceptance criteria`: A candidate satisfies every semantic invariant, eliminates Actor-state loads and Actor-specific charges for indexed latched identities, adds no population-proportional idle work, and is Pareto-superior on the binding source-frontier or Opening dimension without unbounded state. +- `Rejection criteria`: Any redundant Actor-specific evaluation/fee/event/activation, replay of latched causal history, stale Crossing phase, cadence catch-up, recreated AtTime opportunity, duplicate deferred Pipeline, unbounded scan, source-truth loss, or inherited Pipeline/Action regression. + +## Environment and Artifact Identity + +| Artifact | Identity | +| --- | --- | +| Source commit | `95ac8fe5cb5b684664ed7cc8a6135ee33947719f` | +| Source tree | `677dae5dc6b759c66dadebf0620eced9c9374477` | +| Rust toolchain | `rustc 1.98.0 (88d9e12ae 2026-08-18)` observed; repository-pinned Cargo toolchain governs execution | +| Benchmark runtime Wasm | `4b8ff81c509e808f0696e421b9f0ba06e5a2d77826ca78c3c0ef51154b6c6243` (`template/target/benchmarks/release/wbuild/deos-runtime/deos_runtime.compact.compressed.wasm`) | +| Post-generation production runtime Wasm | `394e2a75d591dacf9da858a382f2b0f7a53af71a6a1c8f47cd3fa024e8985ec0` (`template/target/release/wbuild/deos-runtime/deos_runtime.compact.compressed.wasm`) | +| Final release production runtime Wasm | Pending | +| Generated Weight | `929d81519cad94a8b79c5084fdc37afc92dfd7352cbee04fff270e9698eaf852` (`template/runtime/src/weights/pallet_deos_actors.rs`) | +| Runtime metadata | `56c992d2b4eeeb9473ada561d9855881cd01cec1211306b87b30717f6ab87800` (`web-client/.papi/metadata/deos.scale`) | +| Database/backend | FRAME compiled Wasm, cache 1024, worst-case map size 1,000,000 on recorded Fedora Ryzen 7 4800H host | +| Benchmark command/config | `./scripts/benchmarks.sh --check`, then focused candidate owners at `--steps 50 --repeat 20`, followed by complete Actors generation | +| Raw evidence | Complete Actors generation succeeded at 50 steps/20 repeats after focused repair verification of six changed semantic fixtures; deterministic family, race, rollback, density, and client-identity suites pass | + +## Measurements + +| Candidate | Workload | RefTime | ProofSize | Reads | Writes | Other decision metrics | +| --- | --- | ---: | ---: | ---: | ---: | --- | +| B0 | W1-W6 | Invalidated for latch decision | Invalidated | Invalidated | Invalidated | EXP-0019 preserves historical paid-coalescence evidence | +| A1 | W1 direct latch semantics | Not measured | Not measured | Not measured | Not measured | Manual and AddressEvent repeated-pending tests prove one fee/event, stable ticket, and one later Pipeline | +| A1 | W2 eager ObservationChange detach | Not measured | Not measured | Not measured | Not measured | Rejected: four dirty-fanout mutation/restart/model tests fail because subscription removal mutates or clears the authoritative pass being consumed | +| A1 | W3 Crossing useful-latch semantics | Not measured | Not measured | Not measured | Not measured | Running Pipeline with an unset latch charges and creates one deferred Pipeline; further crossings while latched advance phase without fee/event | +| A1 | W3 eager Crossing removal | Not measured | Not measured | Not measured | Not measured | Rejected: six reversal, generated-rearm, busy, pause/breaker, tail-refill, and lifecycle regressions fail because removal compacts the authoritative range traversal | +| A1 | W4 Cadenced detach/re-arm | Not measured | Not measured | Not measured | Not measured | Useful latch removes tick membership; Opening re-arms after current tick; re-arm-capacity failure rolls back; full pallet suite `663 passed`, `4 ignored` | +| A2 | W1-W6 complete generated envelope | Production generated | Production generated | Branch-exact generated bounds | Branch-exact generated bounds | Disabled indexed service performs one detector-authority probe and source-cursor progress without Actor-state load, fee, event, activation, or history mutation | + +## Derived Metrics + +| Candidate | Workload | RefTime delta | ProofSize delta | Throughput / latency | Marginal or amortized cost | Pareto class | +| --- | --- | ---: | ---: | --- | --- | --- | +| A1 | Direct/Cadenced | Baseline-invalidated | Baseline-invalidated | No redundant direct work; no Cadenced catch-up | One early latch probe or one Opening re-arm | Accepted for non-indexed/temporal families | +| A2 | Indexed disabled service | Baseline-invalidated | Baseline-invalidated | Retains bounded source traversal but removes canonical Actor activation work | One detector-local probe per visited member; no latched fee/event writes | Accepted for indexed families | + +## Statistical / Variance Notes + +The complete 50-step/20-repeat model exists under the recorded benchmark Wasm and Weight identities. B0 cannot supply a valid numerical control because its paid-coalescence semantics are rejected. Decision therefore uses generated boundedness plus exact branch traces: A2 retains unavoidable bounded source-entry traversal but replaces latched Actor-state/classification/fee/activation work with one detector-local authority probe and cursor progress. No timing samples were discarded. + +## Result + +A1 direct-family implementation rejects an already-pending occurrence before capacity, fee collection, activation, and event emission. AddressEvent gates source/asset Trigger filters on an unset latch while preserving independent funding accounting. ObservationChange returns from compact hot-latch state before classification, fee, activation, or event. Eagerly removing its subscription at latch was tested and deleted: four dirty-fanout mutation/restart/model regressions failed because removal mutates or clears the authoritative pass being consumed. Crossing distinguishes an active Pipeline with no deferred readiness from an already-latched deferred Pipeline: the former is useful and charged, while later transitions take detector-local skip without phase/revision, fee, event, or activation. Cadenced detection removes its future tick membership after a useful latch; Opening atomically installs the first deadline after current time before Pipeline charging, and capacity failure rolls back. AtTime remains consumed. The event field is removed and the full pallet suite passes. + +## Interpretation + +Manual, AddressEvent, ObservationChange, and Cadenced semantic evidence supports the useful latch. Cadenced establishes eager temporal detach plus current-state Opening re-arm with exact rollback, eliminating A2 for temporal topology. ObservationChange eager detach is rejected under the current authoritative dirty-pass topology; A2 now owns membership-local disabled authority that preserves pass structure while avoiding Actor-state loading. Crossing eager removal is likewise rejected because current-page compaction invalidates range cursor and cohort authorities. ObservationChange A2 retains subscription topology but checks one detector-local disabled authority before Actor-state loading, sets it atomically with useful readiness, and clears it before Opening charge. Crossing shares that authority: classification and scalar service skip disabled members before Actor-state loading, latched transitions do not update phase/revision, and Opening atomically relocates membership from current provider state before Pipeline charging. + +## Decision + +Accept the hybrid A1/A2 topology. Manual and AddressEvent use early direct latch rejection; Cadenced uses eager temporal detach and current-time Opening re-arm; AtTime remains consumed. ObservationChange and ObservationCrossing retain stable physical memberships behind `IndexedTriggerDetectionDisabled`, skip before Actor-state load, and re-enable atomically at Opening. A1 eager removal is rejected for indexed families because it mutates active source traversal. + +## Rejected Alternatives and Tradeoffs + +| Candidate | Decision | Evidence-backed reason | +| --- | --- | --- | +| B0 paid coalescence | Rejected semantically | Redundant latched sources perform Actor-specific work and charge without useful readiness | +| Universal Trigger index | Rejected semantically | Adds a generic topology and dormant tax where specialized family paths already own bounded detection | +| Latch check after full Actor evaluation | Rejected semantically | Violates the required early suppression boundary and preserves fee-DoS computation | +| A1 eager detach for direct/Cadenced | Accepted provisionally | Direct families have no detector membership; Cadenced detaches and re-arms atomically from current time | +| A1 eager ObservationChange detach | Rejected | Subscription removal mutates/clears the authoritative dirty pass and fails four existing race/model regressions | +| A1 eager Crossing detach | Rejected | Current-page compaction invalidates range cursor/cohort authority and fails six reversal, rearm, busy, and lifecycle regressions | +| A2 ObservationChange disabled generation | Accepted provisionally | One detector-local authority map preserves page/pass topology, skips Actor-state loading while latched, and clears atomically before Opening charge | +| A2 Crossing disabled generation | Accepted provisionally | Detector-local authority skips before Actor-state/classification, preserves range/cohort topology, discards latched transitions, and Opening reconstructs phase/revision from current provider state | + +## Validity + +- `Establishes`: Useful-only charging, topology-preserving indexed disablement, authoritative re-arm, complete generated Weight, exact source/Wasm/metadata identity, and deterministic family regressions. +- `Does not establish`: Final release-tree identity, downstream launch readiness, or an empirical wall-clock throughput claim. +- `Known limitations`: Latched indexed identities still incur one detector-local membership probe when their retained source page/range is visited; this is bounded source traversal, not Actor-state evaluation. +- `Invalidation triggers`: Trigger semantics, authoritative Oracle/timestamp ownership, Q1, FIFO, P32, Pipeline Machine strategy, or detector source topology changes. +- `Residual qualitative insight`: Removing useless detector authority at the latch boundary is preferable to repeatedly proving at the Actor body that readiness already exists. + +## Follow-Up / Next Gradient + +```text +paid coalescence → fee-DoS risk → useful-transition latch +→ indexed detector authority remains live → compare eager detach with disabled generation +→ smallest family falsifiers → production Weight and herd evidence +``` + +- `Eliminated hypotheses`: Repeated paid coalescence is acceptable; a late Actor-body check provides sufficient DoS protection; a universal index is required. +- `Remaining hypotheses`: A dedicated generated disabled-skip owner may tighten conservative shared branch envelopes, but is not required for correctness or dispatchability. +- `Stop condition`: Met for the topology decision; final release identity remains owned by release assurance. + +## Architecture Impact + +- `Affected physical truth`: Trigger detector membership lifecycle, hot-latch admission, Pipeline Opening re-arm, events, fees, Weight owners, and public quote behavior. +- `Architecture update`: Package and DEOS integration architecture update only after implementation, tests, correction, and generated evidence converge. +- `Provenance citation`: `Actors EXP-0020`. + +## Relations + +- `Replaces`: EXP-0019 paid-coalescence Trigger boundary only. +- `Supersedes`: None. +- `Refines`: EXP-0010 compact Observation activation and EXP-0016 C6 Opening geometry. +- `Contradicts`: EXP-0019 W2 paid redundant occurrence assumptions. +- `Depends on`: Accepted Trigger-latching specification, EXP-0019 inherited P0 Pipeline/Action/apoptosis evidence, Q1, C6, P32, strict FIFO, current Oracle value/revision, and current timestamp. +- `Baseline of`: Trigger-latch implementation and renewed I4/EXP-0018 evidence. +- `Confirms`: One Boolean readiness authority is sufficient for at most one deferred Pipeline. +- `Invalidates`: Paid-coalescence benchmarks, events, tests, and generated Trigger owners for redundant latched branches. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0021.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0021.md new file mode 100644 index 00000000..d697dcdb --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0021.md @@ -0,0 +1,162 @@ +# EXP-0021 — Contract Step Ceiling Sweep + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Architecture release / experiment campaign | DEOS 0.7.24 optimization campaign | +| Date | 2026-08-30 | +| Affected domain | Actor Contract maximum geometry | +| Physical mechanism | Maximum admitted Step count | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | EXP-0002 | +| Successor experiment | None | + +## Decision Question + +Do ceilings of 24 or 16 Steps produce material physical savings over 32 without an unjustified loss of Contract expressiveness? + +## Governing Specification + +- `Exact section`: `template/pallets/actors/docs/specification.en.md` → Contract bounds and `1.4 Block-Paced Actor Cycles`. +- `Invariant`: Preserve No Ceiling Tax, Q1, semantic identity, Opening/Current behavior, complete lifecycle dispatchability, and identical one-Step behavior. +- `Reopen trigger`: Any proposal to change Step semantics, execution quantum, or downstream policy rather than only the admitted ceiling requires Specification reopening. + +## Context and Hypothesis + +- `Current architecture`: MaxContractSteps is 32 with inline Step 0 and C6 lazy tail chunks of four Steps. +- `Reason`: A lower product ceiling may reduce maximum Contract, Opening, replacement, close, metadata, and state-hold geometry. +- `Hypothesis`: 24 or 16 materially reduces maximum lifecycle/storage envelopes but does not materially improve one-Step throughput because C6 already avoids ceiling tax. +- `Materiality`: Lower the ceiling only for at least 15% reduction in one binding maximum lifecycle or persistent-state metric, with statistically unchanged one-Step complete Weight. + +## Baseline and Candidates + +| ID | Architecture | Source tree | Runtime/config identity | +| --- | --- | --- | --- | +| A0 | MaxContractSteps 32; at most eight tail chunks including inline Step 0 geometry | HEAD `ab62e396`; worktree digest `e739c2d3` | Q1, C6 Chunk4 | +| A1 | MaxContractSteps 24; at most six tail chunks | Candidate constant build pending | Otherwise A0 | +| A2 | MaxContractSteps 16; at most four tail chunks | Candidate constant build pending | Otherwise A0 | + +## Controls + +- `Controlled variables`: Source, Q1/FIFO, resource ratio, tasks/predicates, Contract chunking, toolchain, Weight method, database, populations, and commands. +- `Changed variables`: MaxContractSteps and bounds derived directly from it. +- `Confounders`: Generated metadata/Wasm identities differ by candidate; authoring/product expressiveness is qualitative and reported separately. + +## Benchmark Class, Workloads, and Criteria + +- `Benchmark class`: Focused pallet benchmark, state-footprint probe, runtime integration, metadata comparison, and native 10k stress. + +| Workload | State/population geometry | Hot/cold assumptions | Purpose | +| --- | --- | --- | --- | +| W1 | One-Step Actor at each ceiling | Cold current authority | Verify No Ceiling Tax | +| W2 | Maximum 16/24/32-Step Contract | Full admitted tail | Persistent bytes/state hold | +| W3 | Maximum Opening, replacement, cancellation, and close | Maximum candidate geometry | Lifecycle envelope | +| W4 | Full sequential maximum cycle | One Step per block | Q1 dispatchability | +| W5 | 10,000 one-Step Transfer Actors | Setup outside measured blocks | Detect unintended throughput change | + +- `Metrics`: RefTime, ProofSize, reads/writes, encoded bytes, tail chunks, state hold, Wasm/metadata bytes, lifecycle Weight, and one-Step Steps/Actors per block. +- `Statistical method`: Deterministic candidate builds and state fixtures; focused generated owners at 50/20 only where ceiling changes a benchmark component; exact semantic counters. +- `Acceptance criteria`: No one-Step regression, complete candidate lifecycle fit, and material maximum-geometry reduction sufficient to justify lost expressiveness. +- `Rejection criteria`: Ceiling tax, lifecycle failure, sub-material physical gain, or downstream required Contracts exceeding the candidate. + +## Environment and Artifact Identity + +| Artifact | Identity | +| --- | --- | +| Source commit | `ab62e39612aee895ad41bb45c4bf085318b6cf8a` | +| Source tree | Closed experiment diff digest `8d4e83fcc9b71fbe881db4af943dc4b9f02e2fe1b086eaa878904025c827a130` | +| Rust toolchain | `rustc 1.98.0 (88d9e12ae 2026-08-18)` | +| Benchmark runtime Wasm | C24 `e5f59a68`, 1,533,975 bytes; C16 `859b65d9`, 1,533,973 bytes | +| Post-generation production runtime Wasm | A0 `9cf6cd5966d044aafe1563b43dbe49b2ff896c4cf557feac53eb61284c076fcd` | +| Final release production runtime Wasm | Pending | +| Generated Weight | A0 SHA-256 `54098ec60bb3b283f3dd226f23f82d4d4025a93bb8763fa1fa8ea7c82292602d` | +| Database/backend | FRAME compiled Wasm and native release fixtures | +| Benchmark command/config | Candidate constant builds plus focused 50/20 owners; exact wrapper pending | +| Raw evidence | Inline ceiling-sweep table under Measurements; temporary TSV SHA-256 `d42842d5f841aff8bed27aafcd49514e1b90d11e610d767da10c51bb47487da7` | + +## Measurements + +| Candidate | Workload | RefTime | ProofSize | Reads | Writes | Other decision metrics | +| --- | --- | ---: | ---: | ---: | ---: | --- | +| A0 | Maximum create | `1,346,956,658` | `24,294` | 11 | 10 | 32 Steps; eight tail chunks | +| A0 | Maximum close | `1,432,196,986` | `25,917` | 10 | 11 | Current production authority | +| A1 | Maximum create | `1,086,346,306` | `19,344` | 9 | 8 | 24 Steps; six tail chunks | +| A1 | Maximum close | `1,168,421,228` | `20,577` | 8 | 9 | Runtime Actors `182 passed, 14 ignored` | +| A2 | Maximum create | `825,735,954` | `14,394` | 7 | 6 | 16 Steps; four tail chunks | +| A2 | Maximum close | `904,645,470` | `15,237` | 6 | 7 | Runtime Actors `182 passed, 14 ignored` | + +### Complete Ceiling Sweep + +| Candidate | Max Steps | Tail chunks | Opening snapshots | Predicate results | Create RefTime / proof | Close RefTime / proof | Reconstruct RefTime / proof | Runtime tests | Wasm bytes | Wasm SHA-256 | +| --- | ---: | ---: | ---: | ---: | --- | --- | --- | --- | ---: | --- | +| A0 | 32 | 8 | 64 | 128 | `1,346,956,658 / 24,294` | `1,432,196,986 / 25,917` | `317,080,973 / 25,917` | Baseline | 1,534,264 | `8e8f0acd` | +| A1 | 24 | 6 | 48 | 96 | `1,086,346,306 / 19,344` | `1,168,421,228 / 20,577` | `256,143,193 / 20,577` | `182 passed, 14 ignored` | 1,533,975 | `e5f59a68` | +| A2 | 16 | 4 | 32 | 64 | `825,735,954 / 14,394` | `904,645,470 / 15,237` | `195,205,413 / 15,237` | `182 passed, 14 ignored` | 1,533,973 | `859b65d9` | + +## Derived Metrics + +| Candidate | Workload | RefTime delta | ProofSize delta | Throughput / latency | Marginal or amortized cost | Pareto class | +| --- | --- | ---: | ---: | --- | --- | --- | +| A1 | Maximum create / close | `-19.3% / -18.4%` | `-20.4% / -20.6%` | One-Step geometry unchanged | Eight Steps removed; dependent bounds `64/128→48/96` | Maximum-only improvement | +| A2 | Maximum create / close | `-38.7% / -36.8%` | `-40.7% / -41.2%` | One-Step geometry unchanged | Sixteen Steps removed; dependent bounds `64/128→32/64` | Larger maximum-only tradeoff | + +## Statistical / Variance Notes + +Semantic and encoded-size evidence is deterministic. Wall-clock time is diagnostic only. Generated models are reviewed component-wise when benchmark ranges change. + +## Result + +Both lower ceilings materially reduce maximum geometry and pass the focused runtime Actors suite when the dependent Opening snapshot and predicate-result bounds move in lockstep. C24 reduces maximum create/close RefTime by 18–19% and proof by about 20%; C16 reduces them by 37–41%. Compressed runtime Wasm changes by only 289–291 bytes, and C6 keeps one-Step geometry unchanged. + +## Interpretation + +The savings apply only to authored maximum geometry. Current C6/tiered authority already removes unused-ceiling tax from ordinary one-Step execution and fanout, while the 32-Step maximum remains component-wise dispatchable inside the one-third Control ceiling. No downstream Contract corpus demonstrates that 24 or 16 is sufficient as a reusable framework ceiling. Maximum-only savings therefore do not justify reducing protocol expressiveness. + +## Decision + +Retain `MaxContractSteps = 32`. Reject 24 and 16 as materially cheaper maximum profiles but inferior framework contracts without downstream sufficiency evidence. Preserve lockstep `MaxOpeningSnapshotEntries = 64` and `MaxOpeningPredicateResults = 128`. + +## Rejected Alternatives and Tradeoffs + +| Candidate | Decision | Evidence-backed reason | +| --- | --- | --- | +| Ceiling below 16 | Excluded | Expressiveness loss is large before 16 proves insufficiently efficient | +| Variable per-Actor protocol ceiling | Excluded | Does not shrink runtime maximum geometry and adds policy complexity | + +## Validity + +- `Establishes`: Comparative physical impact of 32/24/16 under C6. +- `Does not establish`: Universal application-level sufficiency of a ceiling. +- `Known limitations`: Candidate evidence uses the existing generated linear geometry models rather than regenerating identical formulas under each ceiling; runtime candidate builds and focused integration suites validate bound lockstep and dispatch behavior. +- `Invalidation triggers`: Changed chunk size, Contract layout, Q1, metadata representation, or lifecycle owners. +- `Residual qualitative insight`: A ceiling reduction is a storage/lifecycle choice, not presumed throughput optimization. + +## Follow-Up / Next Gradient + +```text +32-Step expressiveness → suspected maximum-geometry cost +→ compare 24/16 with identical C6 → test one-Step null hypothesis +→ choose only a material Pareto tradeoff +``` + +- `Eliminated hypotheses`: Variable per-Actor maximum. +- `Remaining hypotheses`: None under current C6/tiered authority; reopen only if maximum geometry becomes binding or a concrete downstream corpus proves a lower sufficient ceiling. +- `Stop condition`: Retain one ceiling after W1-W5 and downstream corpus review. + +## Architecture Impact + +- `Affected physical truth`: Runtime bounds, Contract/storage geometry, metadata, holds, and lifecycle envelopes. +- `Architecture update`: Specification/runtime/docs only after acceptance and complete regeneration. +- `Provenance citation`: `actors/EXP-0021`. + +## Relations + +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: EXP-0002 under the final C6/resource baseline. +- `Contradicts`: None. +- `Depends on`: EXP-0002, EXP-0016, and current Actors specification. +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0022.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0022.md new file mode 100644 index 00000000..f7396054 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0022.md @@ -0,0 +1,157 @@ +# EXP-0022 — Equal-Thirds Resource Allocation + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Architecture release / experiment campaign | DEOS 0.7.24 optimization campaign | +| Date | 2026-08-30 | +| Affected domain | Actor Control and Shared Economic block allocation | +| Physical mechanism | Equal-thirds component-wise resource partition | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | EXP-0006 | +| Successor experiment | None | + +## Decision Question + +Does exact one-third Actor Control with the Shared remainder split equally between Actor effects and user dispatch materially improve the accepted 30/35/35 baseline without unacceptable economic-capacity loss? + +## Governing Specification + +- `Exact section`: `docs/actors-resource-policy.specification.en.md` → `2. Resource Algebra` and `3. Economic Zipper`. +- `Invariant`: Preserve component-wise accounting, strict FIFO, Q1, class neutrality, work conservation, deterministic remainder ownership, and at least approximately one third each for Control, Actor effects, and user dispatch. +- `Reopen trigger`: Actor Control above one third is excluded; all further throughput gains must optimize execution inside the one-third envelope. + +## Context and Hypothesis + +- `Current architecture`: EXP-0006 selected fixed 30% Control and equal 35%/35% Shared base turns. +- `Reason`: One third is the declared maximum Control share and provides a symmetric DEOS resource boundary. +- `Hypothesis`: Raising Control from 30% to floor one third yields a smaller but measurable traversal gain while retained one-third effect/user turns remain sufficient. +- `Materiality`: Require at least 5% more traversal throughput than 30/35/35 with zero semantic failures and exact base-turn preservation. + +## Baseline and Candidates + +| ID | Architecture | Source tree | Runtime/config identity | +| --- | --- | --- | --- | +| A0 | 30% Control, 70% Shared, 35%/35% base turns | Accepted EXP-0006 production baseline | Wasm `595165fb`; Q1/C6/P128/P64 | +| A1 | floor 1/3 Control; Shared remainder split floor/remainder | Candidate feature `exp-equal-thirds` | Otherwise identical to A0 | + +## Controls + +- `Controlled variables`: Source geometry, generated Weight, tasks, populations, Trigger mix, FIFO/Q1, toolchain, setup, block horizon, and commands. +- `Changed variables`: Fixed ratio only. +- `Confounders`: Candidate Wasm differs by the ratio constant; wall-clock timing is diagnostic. + +## Benchmark Class, Workloads, and Criteria + +- `Benchmark class`: Deterministic resource algebra and native release stress. + +| Workload | State/population geometry | Hot/cold assumptions | Purpose | +| --- | --- | --- | --- | +| W1 | 10,000 Manual/Cadenced Transfer Actors | Setup outside measured blocks | Actor-only attribution | +| W2 | W1 plus one signed user dispatch per block | Same as W1 | Retained user admission | +| W3 | 9,500 Transfer/400 SwapOut/100 control plus user dispatch | Production effects | Shared-capacity regression | +| W4 | Saturated base turns and borrowing | Pure resource state | Exact thirds/remainders | + +- `Metrics`: Completion block, committed/distinct progress, failed Steps, user admissions, RefTime/ProofSize envelopes, and exact remainder ownership. +- `Statistical method`: Deterministic fixed profiles and generated Weight; semantic counters must match exactly. +- `Acceptance criteria`: At least 5% throughput gain in W1-W3, zero semantic failures, user dispatch retained, and W4 exact. +- `Rejection criteria`: Sub-material gain, effect/user failure, FIFO drift, or any Control share above one third. + +## Environment and Artifact Identity + +| Artifact | Identity | +| --- | --- | +| Source commit | `ab62e39612aee895ad41bb45c4bf085318b6cf8a` | +| Source tree | Candidate worktree digest `7219acfb7be2c42724a16e9a48cea53a75fc810a33b4f8dac054caef97b050ad` | +| Rust toolchain | `rustc 1.98.0 (88d9e12ae 2026-08-18)` | +| Benchmark runtime Wasm | Not applicable; Weight owners unchanged | +| Post-generation production runtime Wasm | A0 `595165fb8778871b2852c95d08b3a1f562ce42788af15a3c16ff67fe80db73d9` | +| Final release production runtime Wasm | Accepted equal-thirds tree `d495e79af02fdb0e4afebd1f661bed304c437eac66a85c3e49199c1917a807ab` | +| Generated Weight | SHA-256 `54098ec60bb3b283f3dd226f23f82d4d4025a93bb8763fa1fa8ea7c82292602d` | +| Database/backend | Native release tests on recorded development host | +| Benchmark command/config | Candidate feature over retained W1/W2/W3 ignored profiles | +| Raw evidence | Inline equal-thirds sweep under Measurements; temporary TSV SHA-256 `3f7c919822a1911a0faf11923e805d7022e6c80dfe5e93556a14814e20ed9171` | + +## Measurements + +| Candidate | Workload | RefTime | ProofSize | Reads | Writes | Other decision metrics | +| --- | --- | ---: | ---: | ---: | ---: | --- | +| A0 | W1/W2 | Control `272,814,087,218`; Shared `636,566,203,510` | Control `608,535`; Shared `1,419,915` | Unchanged | Unchanged | Completion block 746 | +| A0 | W3 | Same allocation | Same allocation | Unchanged | Unchanged | Completion block 765; zero failed Steps | +| A1 | W1/W2 | Control `303,126,763,576`; each base `303,126,763,576` | Each domain `676,150` | Unchanged | Unchanged | Completion block 679; continuous user dispatch retained | +| A1 | W3 | Same exact thirds | Same exact thirds | Unchanged | Unchanged | Completion block 679; zero failed Steps | +| A0-A1 | W4 | Exact checked partitions | Exact checked partitions | Not applicable | Not applicable | Saturation and forward borrowing pass for both | + +### Complete Allocation Sweep + +| Candidate | Allocation | Actor Control RefTime / proof | Actor base RefTime / proof | User base RefTime / proof | W1 | W2 | W3 | Failed Steps | +| --- | --- | --- | --- | --- | ---: | ---: | ---: | ---: | +| A0 | `30/35/35` | `272,814,087,218 / 608,535` | `318,283,101,755 / 709,957` | `318,283,101,755 / 709,958` | 746 | 746 | 765 | 0 | +| A1 | Equal thirds | `303,126,763,576 / 676,150` | `303,126,763,576 / 676,150` | `303,126,763,576 / 676,150` | 679 | 679 | 679 | 0 | + +## Derived Metrics + +| Candidate | Workload | RefTime delta | ProofSize delta | Throughput / latency | Marginal or amortized cost | Pareto class | +| --- | --- | ---: | ---: | --- | --- | --- | +| A1 | W1/W2 | Control `+11.1%`; each base `-4.76%` | Same proportional envelope change | Blocks `-9.0%`; throughput about `+9.9%` | One-third ceiling reached | Material Pareto tradeoff | +| A1 | W3 | Same allocation delta | Same allocation delta | Blocks `-11.2%`; throughput about `+12.7%` | Zero failed mixed effects | Material winner | + +## Statistical / Variance Notes + +Deterministic counters decide. Wall-clock duration and narrative symmetry do not select the candidate. + +## Result + +A1 completes W1/W2 at block 679 versus A0 block 746 and W3 at block 679 versus A0 block 765. Every continuous user dispatch remains admitted and W3 has zero failed Steps. W4 exact partition and borrowing tests pass. + +## Interpretation + +A1 clears the 5% materiality threshold in every throughput workload. The gain is 9.9% for Transfer and 12.7% for mixed effects while each guaranteed effect/user base turn falls only 4.76%. Equal thirds also establishes the declared hard Control ceiling; remaining throughput pressure transfers to execution-authority geometry. + +## Decision + +Accept A1 as the production resource partition. Actor Control is floor one third of schedulable Weight; Shared Economic owns the remainder; Actor and user base turns split that remainder floor/remainder. No later optimization may increase Actor Control above one third. + +## Rejected Alternatives and Tradeoffs + +| Candidate | Decision | Evidence-backed reason | +| --- | --- | --- | +| Control above one third | Excluded | Violates the declared minimum thirds for Actor effects and user dispatch | +| Dynamic ratio | Excluded | Adds consensus policy/state complexity without earned need | + +## Validity + +- `Establishes`: Comparative behavior of 30/35/35 and exact one thirds for declared profiles. +- `Does not establish`: A throughput SLA or permission to exceed one-third Control. +- `Known limitations`: Profiles admit one continuous user call rather than saturating all user capacity; exact W4 algebra owns the guaranteed envelope. +- `Invalidation triggers`: Changed Weight, authority geometry, block limits, or workloads. +- `Residual qualitative insight`: Equal thirds is a hard allocation boundary, not a substitute for authority optimization. + +## Follow-Up / Next Gradient + +```text +accepted 30/35/35 → one-third ceiling proposed → run retained W1-W4 +→ accept or reject → continue EXP-0005 inside fixed ceiling +``` + +- `Eliminated hypotheses`: Ratios above one third and retention of 30/35/35. +- `Remaining hypotheses`: EXP-0005 authority optimization can improve service inside the fixed thirds. +- `Stop condition`: Met by accepting A1. + +## Architecture Impact + +- `Affected physical truth`: Reference runtime resource ratio and derived base turns. +- `Architecture update`: Bind exact thirds in runtime, specifications, tests, and integration docs. +- `Provenance citation`: `actors/EXP-0022`. + +## Relations + +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: EXP-0006. +- `Contradicts`: None. +- `Depends on`: EXP-0006 and resource policy. +- `Baseline of`: Future authority work inside the selected ceiling. +- `Confirms`: None. +- `Invalidates`: None. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0023.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0023.md new file mode 100644 index 00000000..c3743a9a --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0023.md @@ -0,0 +1,252 @@ +# EXP-0023 — Step-Centric Memory Topology + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Architecture release / experiment campaign | DEOS 0.7.24 optimization campaign | +| Date | 2026-08-30 | +| Affected domain | Ordinary Running Q1 execution, run state, Contract-fragment access, FIFO, and successor persistence | +| Physical mechanism | Step-centric hot-path entry and optional paged/vectorized storage I/O | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | EXP-0005 | +| Successor experiment | None | + +## Decision Question + +Can ordinary Q1 continuation become materially cheaper when the current Step obligation, rather than a reconstructed Actor-centric view, is the physical hot-path entry while the Actor remains the semantic and control-plane entity? + +## Governing Specification + +- `Exact section`: `template/pallets/actors/docs/specification.en.md` → `§3.3`, `§3.5`, `§5.1`, `§5.3`, `§6.1`, `§8.2-§8.3`, and `§13.1`. +- `Invariant`: Preserve strict FIFO, Q1, N+1 causal eligibility, one live Pipeline plus one deferred latch, C6 Contract geometry, Pipeline Machine prepayment, Action pay-as-executed, atomic Task effects, no mid-Pipeline apoptosis, persistent sovereign custody, and exact-slot recovery. +- `Reopen trigger`: Any candidate that changes semantic order, batches execution, duplicates authored Contract semantics, weakens stale-authority rejection, introduces a second scheduler, or commits more than one Step per Actor per block is not a physical optimization candidate. + +## Context and Hypothesis + +- `Current architecture`: FIFO resolves ActorId and loads Actor identity/hot/Contract/funding/run partitions before current-Step execution and successor placement. +- `Reason`: EXP-0005 showed isolated key co-location does not materially improve complete production throughput; the larger remaining gradient may be the breadth of the Actor-centric hot path and repeated physical I/O. +- `Hypothesis`: A compact current-Step service handle plus stable run authority, direct current Contract-fragment access, situational snapshot loading, and optional page-level I/O amortization can reduce Actor-machine cost without changing Actor semantics. +- `Materiality`: At least 20% improvement in one primary Pareto metric without unacceptable regression in another, lifecycle/state hold, service-gap fairness, exact Task equivalence, or bounded worst-case ProofSize. + +## Baseline and Candidates + +| ID | Architecture | Source tree | Runtime/config identity | +| --- | --- | --- | --- | +| A0 | Current Actor-centric Q1 path | Current accepted equal-thirds tree | Q1, C6, P128/P64, MaxSteps32, one-third Control ceiling | +| A1 | Existing compact Step ticket + specialized Running loader + direct current tail chunk + situational snapshots + exact head-only hold delta | Prepared physical schema | No authored Step/header duplication; fallback to generic path at boundaries | +| A2 | A1 plus paged/vectorized Ready/run/successor I/O while execution remains sequential FIFO | Not constructed | One Step transaction at a time; grouped physical I/O only | + +No additional candidate is admitted unless measurements expose one specific independent gradient. + +## Controls + +- `Controlled variables`: Actor semantics, Trigger semantics, Contract semantics, fees, lifecycle, custody, Q1, FIFO order, N+1, resource allocation, generated Task Weight, workload data, and population. +- `Changed variables`: Hot-path key/value placement, Step-service handle, Contract-fragment addressing, situational-state placement, and page-level physical I/O. +- `Confounders`: Cache warmth, trie-path overlap, page occupancy, shared Contract chunks, successor density, transactional rollback implementation, and benchmark noise. + +## Benchmark Class, Workloads, and Criteria + +- `Benchmark class`: Focused complete-branch pallet benchmark, deterministic native release profile, state-footprint probe, and generated production Weight for finalists. + +| Workload | State/population geometry | Hot/cold assumptions | Purpose | +| --- | --- | --- | --- | +| W1 | Middle Running Step with successor | Current token/run hot; Actor control cold | Primary continuation cost | +| W2 | Middle Step with no snapshot dependency | Snapshot partitions cold | Situational loading benefit | +| W3 | Middle Step with one exact Opening/funding dependency | One required fragment loaded | Selective snapshot correctness | +| W4 | Temporary retry and delayed eligibility | Run authority persists across blocks | Retry/temporal path | +| W5 | Terminal Step, cancel, replacement, deactivate, close | Lifecycle keys cold | Invalidation and cleanup cost | +| W6 | Ready FIFO prefixes `1/4/8/16/32/64` | Declared page occupancy | A2 amortization curve | +| W7 | 10,000 Transfer plus mixed SwapOut/control profiles | Setup outside measured blocks | Production Pareto surface | + +- `Metrics`: `CommittedStepsPerBlock`; `DistinctActorsProgressedPerBlock`; component-wise `ActorMachineCostPerCommittedStep`; component-wise `UsefulActionWeight / ActorMachineWeight`; service-gap distribution; RefTime/ProofSize per committed Step; reads/writes; distinct trie keys; encoded bytes read/written; queue, run-state, Contract-fragment, snapshot, and successor costs. +- `Statistical method`: Focused probes reject candidates early. Final generated Weight uses 50 steps/20 repeats. Prefix profiles report each fixed point; deterministic native profiles retain exact counters and do not infer from wall-clock duration. +- `Acceptance criteria`: Material Pareto improvement in ordinary Running Q1 with exact semantic/state-root equivalence, class-neutral FIFO, one transaction per Step, bounded page work, and no lifecycle/state-hold displacement. +- `Rejection criteria`: Sub-material complete-path gain; duplicated semantic authority; execution batching; hidden affinity; stale token acceptance; increased binding ProofSize; unbounded page touch; rollback drift; or lifecycle regression. + +## Environment and Artifact Identity + +| Artifact | Identity | +| --- | --- | +| Source commit | `ab62e39612aee895ad41bb45c4bf085318b6cf8a` | +| Source tree | Closed experiment worktree digest `ab3f28dde2043f275d99f3e1975ce3684c4b6cd6f44217fe93e2c87948778172` | +| Rust toolchain | `rustc 1.98.0 (88d9e12ae 2026-08-18)` | +| Benchmark runtime Wasm | Matched A0/A1 probe `254ac498bb82aa26841d9ae4658bcb91c9b16c9926c52acbb30b8e43b7053868` | +| Post-generation production runtime Wasm | A0 `2617e8186cc304ce9ee4a07b263e324a226f615068cdf6b068267cf3a16fbc69` | +| Final release production runtime Wasm | Pending | +| Generated Weight | A0 SHA-256 `5199425030b4d2c0523a2a21134a8994ab29d5f70ff1303b42cbf2ff82598db3` | +| Database/backend | FRAME compiled Wasm benchmark backend and native release tests | +| Benchmark command/config | Focused candidate probes first; project benchmark script at 50/20 only for survivors | +| Raw evidence | Inline owner-ledger and candidate tables under Measurements; temporary TSV digests `4d705313`, `371e46d4`, `539ea377`, `7de6f3cd`, `6e728433`, `c7f1efa1`, `59180555`, `54a11dcf`, and `47a973a2` | + +## Measurements + +| Candidate | Workload | RefTime | ProofSize | Reads | Writes | Other decision metrics | +| --- | --- | ---: | ---: | ---: | ---: | --- | +| A0 | W1 legacy setup run progress | `581,715,000` with runtime DB | `5,775` | 8 | 3 | Setup removed prior payload and therefore modeled write-overlay proof | +| A0 | W1 persisted-payload run progress | `538,634,000` with runtime DB | `8,567` | 9 | 2 | Honest existing maximum payload: lower write cost but +48.3% estimated proof | +| A0 | W4 persisted-payload suspension | `514,332,000` with runtime DB | `8,376` | 8 | 2 | State-hold reconciliation reloads broad run payload | +| A0 | W4 retry | `205,385,000` with runtime DB | `5,589` | 2 | 1 | Run head plus broad RunPayload | +| A0 | W7 cheap mixed marginal | `971,773,183` with runtime DB | `3,361` | 11 | 5 | Generated paged-execution marginal; owners non-additive | +| A1 | W1 focused physical owner set | `463,623,000` with runtime DB versus matched A0 `981,175,000` | `5,273` versus `11,183` estimated; `1,808` versus `5,243` measured | 5 versus 15 | 3 versus 4 | Lower bound excludes complete semantic validation/effect; same candidate Wasm/setup | +| A1 | W2 integrated exact run-component hold | `807,425,000` versus matched A0 `981,175,000` | `8,591` versus `11,183` estimated | 10 versus 15 | 4 versus 4 | Exact H0 run component; removes detector/body rescan, not RunPayload | +| A1 | W3 corrected production route | `2,164,798,000` versus A0 `2,346,304,000` | Both `14,773` estimated / `6,358` measured | 21 versus 25 | Both 12 | Production persist + Queue reconciliation; four detector reads removed | +| A1 | W1 complete payload elision | `1,996,202,000` versus A0 `2,346,304,000` | `12,225` versus `14,773` estimated; `3,810` versus `6,358` measured | 20 versus 25 | Both 12 | Residual reader removed from pre-cutoff head classification | +| A2 | W1-W7 | Not measured | Not measured | Not measured | Not measured | Not measured | + +### A0 Middle-Step Owner Ledger + +| Scope | Owner | Access | Maximum bytes | Estimated proof | RefTime with DB | Reads | Writes | Note | +| --- | --- | --- | ---: | ---: | ---: | ---: | ---: | --- | +| Run progress | ActorIdentities | Read | 110 | 2,585 | — | 1 | 0 | Cold control candidate | +| Run progress | ActorHot | Read/write | 108 | 2,583 | — | 1 | 1 | Trigger/latch and cycle state | +| Run progress | ActorContractHead | Read | 2,271 | 4,746 | — | 1 | 0 | Potential cursor>0 avoidance | +| Run progress | ActorFunding | Read | 1,066 | 3,541 | — | 1 | 0 | Situational funding candidate | +| Run progress | ActorRunHead | Read/write | 224 | 2,699 | — | 1 | 1 | Stable hot run authority | +| Run progress | ActorAdmissionCertificate | Read | 208 | 2,683 | — | 1 | 0 | Commitment candidate, not duplicate truth | +| Run progress | ActorContractTailChunk | Read | 4,070 | 6,545 | — | 1 | 0 | Exact current middle-Step fragment | +| Run progress | ActorStateHolds | Read | 152 | 2,627 | — | 1 | 0 | State-hold validation candidate | +| Run progress | ActorRunPayload | Write | 2,533 | 5,008 | — | 0 | 1 | Broad snapshot/outcome rewrite | +| Run progress | Total | Mixed | — | 5,775 | 581,715,000 | 8 | 3 | Proof owners overlap | +| Cheap mixed marginal | Total | Mixed | — | 3,361 | 971,773,183 | 11 | 5 | Complete owners non-additive | +| Run retry | Run head + payload | Mixed | — | 5,589 | 205,385,000 | 2 | 1 | Retry authority | + +### Persisted-Payload Baseline + +| Path | Setup | RefTime with DB | Measured proof | Estimated proof | Reads | Writes | Raw JSON SHA-256 | Benchmark Wasm SHA-256 | +| --- | --- | ---: | ---: | ---: | ---: | ---: | --- | --- | +| Run progress | Existing maximum payload | 538,634,000 | 5,102 | 8,567 | 9 | 2 | `b15c228793665070938cf7e6b802321d4c939b3f3aca508652f17c35a2b7ed6b` | `71dd1b6f16a5ecef40602e8fafb1ef06076dbf7242bc5bcc72a7abb162d9780a` | +| Run suspend | Existing maximum payload | 514,332,000 | 4,911 | 8,376 | 8 | 2 | `2d0ac2a173e26d840b8d02f51c29a6393da1bd308073febe59a4b05db82f1a98` | `71dd1b6f16a5ecef40602e8fafb1ef06076dbf7242bc5bcc72a7abb162d9780a` | + +### A1 Focused and Integrated Hold + +| Probe | Candidate | RefTime with DB | Measured proof | Estimated proof | Reads | Writes | Raw JSON SHA-256 | Benchmark Wasm SHA-256 | +| --- | --- | ---: | ---: | ---: | ---: | ---: | --- | --- | +| Focused | A0 matched User progress | 981,175,000 | 5,243 | 11,183 | 15 | 4 | `7e2f0607711372a00727d809a6aa4868460e504699027a27507c99c18dc537bb` | `254ac498bb82aa26841d9ae4658bcb91c9b16c9926c52acbb30b8e43b7053868` | +| Focused | A1 physical hot-owner lower bound | 463,623,000 | 1,808 | 5,273 | 5 | 3 | `bdc80217c4951194f8e29c8c1a5869d1eca748306bf8496e62d4f6c973f5a19d` | `254ac498bb82aa26841d9ae4658bcb91c9b16c9926c52acbb30b8e43b7053868` | +| Integrated hold | A0 generic full reconciliation | 981,175,000 | 5,243 | 11,183 | 15 | 4 | `7e2f0607711372a00727d809a6aa4868460e504699027a27507c99c18dc537bb` | `254ac498bb82aa26841d9ae4658bcb91c9b16c9926c52acbb30b8e43b7053868` | +| Integrated hold | A1 exact run component | 807,425,000 | 5,126 | 8,591 | 10 | 4 | `c00834ff56a261e35374b466b87323390c553cb28fe49685b10029f18eb21abb` | `3ad5d1409faa694ccdec7f0edc1b7e373a867e1c31ed171c244d5a7511670e33` | + +### Complete Route Probes + +| Probe | Candidate | RefTime with DB | Measured proof | Estimated proof | Reads | Writes | Raw JSON SHA-256 | Benchmark Wasm SHA-256 | +| --- | --- | ---: | ---: | ---: | ---: | ---: | --- | --- | +| Scheduler A/B | A0 full hold reconciliation | 2,346,304,000 | 6,358 | 14,773 | 25 | 12 | `b3eab3e13b2eca1ddd63258d4d9f6f0b095bd93e22fd083fd17d8075d6fb64fd` | `4e466b68a1b3c79a4f8190c7543fa9b822021a50558849b85bc4714ace9685b9` | +| Scheduler A/B | A1 exact run component | 2,343,789,000 | 6,358 | 14,773 | 25 | 12 | `b610859cfe5780075b9aeb50e7a3b8df054ab2067f029aba294ceb766d54f9bd` | `af0b8ef91cd7fc8ce4f2b005c67a081f5dd1e9831782055d2dc415d050a13121` | +| Production route | A0 full reconciliation | 2,346,304,000 | 6,358 | 14,773 | 25 | 12 | `b3eab3e13b2eca1ddd63258d4d9f6f0b095bd93e22fd083fd17d8075d6fb64fd` | `4e466b68a1b3c79a4f8190c7543fa9b822021a50558849b85bc4714ace9685b9` | +| Production route | A1 run + Queue reconciliation | 2,164,798,000 | 6,358 | 14,773 | 21 | 12 | `1dddc4ad2b8acd7da3bccd6d00249b24fcada10ba34f75adc3eba6ff72932d3d` | `a04f21e47aeccd04823e444bd157824d10fff8d6567bd0f418c76ca7dd803482` | + +### Owner Necessity + +| Owner | A0 reads | Added proof | Middle-Step requirement | Removable without duplicate truth | +| --- | ---: | --- | --- | --- | +| QueuePages | 2 | 8,645 | Strict FIFO consume and successor enqueue | No | +| ActorIdentity | 1 | Unknown | Class, owner, sovereign, cycle authority | No | +| ActorHot | 1 | Unknown | Ticket, lifecycle, cycle coherence | No | +| ActorContractHead | 1 | 4,746 | Step count, schedule, completion, funding, Trigger coherence | No | +| ActorFunding | 1 | Unknown | Funding authority and Opening clear contract | Branch only | +| ActorRunHead | 1 | Unknown | Cycle, cursor, retry, outcome, commitments | No | +| ActorRunPayload | 1 | 5,008 | Frozen Opening, Predicate, and funding snapshots | Branch only | +| ActorAdmissionCertificate | 1 | 2,683 | Contract and Run authority coherence | No | +| ActorContractTailChunk | 1 | 6,545 | Current authored Step and resources | No | +| ActorStateHolds | 1 | Unknown | Exact User retained-geometry hold | No | +| Detector topology | 4 | Bounded | Full hold reconciliation only | Branch only | + +### Payload Owner and Complete Candidate + +| Probe | Candidate | RefTime with DB | Measured proof | Estimated proof | Reads | Writes | Raw JSON SHA-256 | Benchmark Wasm SHA-256 | +| --- | --- | ---: | ---: | ---: | ---: | ---: | --- | --- | +| Payload owner | A0 with RunPayload | 463,700,000 | 5,299 | 8,764 | 15 | 0 | `1bdd72e0a4ac592476b9aa14990dbdaa7f7eafd5e7b5f9497058a26d1a790c7d` | `1c70ee06ef8892eb3a31fa6282e9f1d7489c9abd7d57c7202324c184993521fa` | +| Payload owner | A1 without RunPayload | 420,471,000 | 2,751 | 6,216 | 14 | 0 | `848c2cc9985ea39451de1965a4233601bc3ce9665f6f968ae591b0c008ed801a` | `1c70ee06ef8892eb3a31fa6282e9f1d7489c9abd7d57c7202324c184993521fa` | +| Complete scheduler | A0 generic | 2,346,304,000 | 6,358 | 14,773 | 25 | 12 | `b3eab3e13b2eca1ddd63258d4d9f6f0b095bd93e22fd083fd17d8075d6fb64fd` | `4e466b68a1b3c79a4f8190c7543fa9b822021a50558849b85bc4714ace9685b9` | +| Complete scheduler | A1 payload elided | 1,996,202,000 | 3,810 | 12,225 | 20 | 12 | `7aaea99c2d0982235da86cd3fad8227ec044bcb1fa686bc3d6aa465a37c47218` | `ea87c74ac65e3c4e39f0de8e700ac28dc3135b353d787a7796b407b35d8bccdf` | + +## Derived Metrics + +| Candidate | Workload | RefTime delta | ProofSize delta | Throughput / latency | Marginal or amortized cost | Pareto class | +| --- | --- | ---: | ---: | --- | --- | --- | +| A1 | W1 focused feasibility | `-52.8%` | Estimated `-52.8%`; measured `-65.5%` | Complete branch not measured | Reads `15→5`; writes `4→3` | Clears focused materiality | +| A1 | W2 isolated hold reconciliation | `-17.7%` | Estimated `-23.2%`; measured `-2.2%` | Test/benchmark persistence owner | Reads `15→10`; writes unchanged | Route feasibility only | +| A1 | W3 route-discovery scheduler | `-0.1%` | No change | One User middle Step | No I/O change | Superseded by corrected route | +| A1 | W3 corrected production route | `-7.7%` | No change | One User middle Step | Reads `25→21`; writes unchanged | Sub-material | +| A1 | W1 complete payload elision | `-14.9%` | Estimated `-17.2%`; measured `-40.1%` | One User middle Step | Reads `25→20`; writes unchanged | Misses 20% RefTime and estimated-proof gates; reject | +| A1 | W1 matched payload owner probe | `420,471,000` versus A0 `463,700,000` (`-9.3%`) | Estimated `6,216` versus `8,764` (`-29.1%`); measured `2,751` versus `5,299` (`-48.1%`) | 14 versus 15 | Both 0 | Differs only by RunPayload read; clears focused proof gate | +| A2 | W1-W7 | Not derived | Not derived | Not derived | Not derived | Not interpreted | + +## Statistical / Variance Notes + +The first decision surface is one ordinary middle Step with successor, not create/Opening or full first traversal. The legacy run-progress benchmark removed the prior payload and hid its read proof behind a write overlay; candidate comparisons use the corrected persisted-payload setup. Prefix scaling is accepted only as storage-I/O amortization evidence and never as permission to batch execution. + +## Result + +A1 clears the focused gate on a matched User middle-Step setup: the candidate physical owner set lowers RefTime and estimated proof by 52.8%, measured proof by 65.5%, reads from 15 to 5, and writes from 4 to 3. This is a lower-bound owner probe rather than a complete semantic branch, so it authorizes only the smallest integrated Running in-chunk-successor implementation. + +Prepared A0 ledger and corrected the focused persistence setup so the maximum RunPayload exists before the measured Q1 transition. The correction exposes a larger binding gradient: progress ProofSize rises from the legacy `5,775` overlay model to an honest `8,567` because state-hold reconciliation reloads the existing broad payload. + +The minimal A1 adds no persistent cache or authored fields. The existing `ActorStepTicket` remains the compact handle. For a Running cursor inside a loaded tail chunk with an in-chunk successor, run authority validates the chunk directly; Contract head, admission certificate, and unrelated funding/snapshot partitions remain cold. Head-only progress reconciles the exact User hold by applying the checked encoded-length delta between old/new RunHead to the retained run-hold component; Opening, payload mutation, terminal, replacement, cancel, close, and any uncertain branch use the generic full reconciliation path. H0 Cycle-local hold policy therefore remains unchanged. + +## Interpretation + +The corrected production candidate changed both `persist_loaded_run_state` and final Queue-placement reconciliation. It removed ActivationAuthority, ObservationFeeds, ObservationSubscriptionSlot, and CrossingMembership reads from the complete middle-Step branch, but the remaining queue, Contract, RunPayload, action, and hold owners dominate proof. End-to-end RefTime improved only 7.7%, while measured and estimated proof were unchanged. Exact run-component reconciliation is therefore sub-material even on the correct route. + +The owner-necessity ledger leaves one plausible A1 survivor: situational RunPayload elision. A Step with no Opening predicate and no amount resolution that depends on frozen Opening/funding state can execute from RunHead plus the authored Step, preserve the untouched payload, and persist only the new RunHead. ContractHead cannot be removed without duplicating authored schedule/completion/funding authority. + +A matched actor+queue storage-owner A/B now isolates only the RunPayload read. Elision reduces estimated proof by 29.1%, measured proof by 48.1%, and RefTime by 9.3%. Trie overlap lowers the earlier arithmetic maximum, but focused ProofSize still clears the 20% gate. This authorizes the complete semantic branch implementation. + +The first retained implementation slice adds an exhaustive semantic dependency classifier owned by `AmountResolution`, `Task`, and `Step`. `Fixed`, `PercentageOfCurrent`, and `AllAvailable` remain snapshot-independent; `PercentageAtOpening`, `PercentageOfLastFunding`, and any Opening predicate require frozen cycle payload. Exhaustive Task matching makes future variants fail compilation until classified. + +The second retained slice makes current-Step execution consume the already validated `CurrentStepPlan.run` for Running cursor and state inputs instead of reloading `ActorRunStateStore`; Suspended still uses authoritative `begin_run_attempt`, and plan-less simulation falls back to storage. This does not yet eliminate the scheduler's initial payload read, but establishes one validated run input for the compact projection. + +The complete payload-elision candidate included explicit commitment provenance, compact Running projection, head-only persistence, checked hold delta, RunHead-based successor placement, and safe pre-cutoff projection. It passed the complete pallet suite before measurement and was then fully reverted after missing the materiality gate. The resulting A1 improved RefTime by 14.9% and estimated proof by 17.2%; measured proof improved 40.1%. No candidate-specific storage, plan field, loader, persistence branch, hold path, or scheduler path remains. + +## Decision + +Reject A1 and fully revert compact projection, payload-elision provenance, head-only persistence, hold-delta, and projection-specific scheduler changes. Retain only independently useful semantic dependency classification and validated-plan reuse if they remain behavior-neutral after cleanup. Do not advance A2: vectorization would add physical complexity atop a Step-centric layout that failed the complete-path materiality gate. + +## Rejected Alternatives and Tradeoffs + +| Candidate | Decision | Evidence-backed reason | +| --- | --- | --- | +| Authored Step copied into run/FIFO | Excluded | Duplicates canonical Contract semantics | +| Multi-Step execution batch | Excluded | Violates Q1 and per-Step transaction ownership | +| Task/class/Contract affinity | Excluded | Violates class-neutral strict FIFO | +| Generic replacement scheduler | Excluded | Creates a second order owner | +| External block-bound successor pages | Excluded | A successor committed before later Manual, Trigger, or wakeup readiness must reserve its canonical FIFO position at source; delayed page materialization either permits overtaking, inserts ahead of FIFO, or becomes a second order owner | +| Grouped transient successor flush | Excluded | Independent committed-Step durability requires each successor's canonical position to survive immediately; delayed cross-Step flush creates a shared failure domain, while per-Step flush restores scalar mutation | +| Off-chain Step authority | Excluded | Cannot own consensus correctness | + +## Validity + +- `Establishes`: When complete, comparative physical cost of Actor-centric versus Step-centric ordinary Q1 for declared workloads. +- `Does not establish`: Permission to alter Actor semantics, priority, fee ownership, lifecycle, custody, or the one-third Control ceiling. +- `Known limitations`: The retained classifier and validated-plan reuse are behavior-neutral and do not claim the rejected A1 cost reduction; corrected benchmark setup awaits the next ordinary Weight generation. +- `Invalidation triggers`: Contract/run/FIFO geometry, generated Weight, resource ratio, page bounds, or host transaction model changes. +- `Residual qualitative insight`: Step-centricity is useful only if it removes complete hot-path owners rather than renaming Actor-centric reconstruction. Ordinary exact-`N+1` placement already bypasses generic wakeup materialization and appends directly to FIFO after cutoff. Future page-I/O research must remain inside canonical FIFO ownership; a separate due-block successor page cannot encode interleaved global admission order. + +## Follow-Up / Next Gradient + +```text +A0 retained → A1 rejected below complete-path materiality +→ A2 not admitted → proceed to independent EXP-0021 Contract ceiling +``` + +- `Eliminated hypotheses`: Semantic redesign, execution batching, duplicate Step payload, priority, and second scheduler. +- `Remaining hypotheses`: None inside this Step-centric topology family; the independent Contract ceiling and Active-lifetime hold experiments remain separate. +- `Stop condition`: Accept one material Pareto winner or retain A0 after both candidates fail. + +## Architecture Impact + +- `Affected physical truth`: Ready token, run-state tiers, Contract-fragment addressing, situational snapshot partitions, successor topology, and page I/O. +- `Architecture update`: Required only after accepted implementation and complete lifecycle/rollback evidence. +- `Provenance citation`: `actors/EXP-0023`. + +## Relations + +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: EXP-0005 authority-geometry result. +- `Contradicts`: None. +- `Depends on`: EXP-0005, EXP-0013, EXP-0017, EXP-0022, Actors specification, and resource policy. +- `Baseline of`: Potential canonical FIFO-internal physical-topology experiments; external successor staging is closed. +- `Confirms`: None. +- `Invalidates`: None. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0024.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0024.md new file mode 100644 index 00000000..6dc46421 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0024.md @@ -0,0 +1,184 @@ +# EXP-0024 — Active-Lifetime Run-State Hold + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Architecture release / experiment campaign | 0.7.24 topology scaling | +| Date | 2026-08-30 | +| Affected domain | Actors economics and lifecycle | +| Physical mechanism | User Actor state-hold reservation horizon | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0023](./EXP-0023.md) | +| Successor experiment | None | + +## Decision Question + +Should a User Actor reserve run-state hold only while a Cycle exists, or reserve its maximum admitted run-state footprint for the Actor's active installed lifetime? + +## Governing Specification + +- `Exact sections`: `template/pallets/actors/docs/specification.en.md` → `2.4 Lifecycle, Execution Funding, and Terminal Order`, `4.3 State Holds and Fees`, and `6.4 Weight and Fee Derivation`; `docs/actors-resource-policy.specification.en.md` → `4 Admission and Actual Weight`. +- `Invariant`: State hold is owner-funded, component-exact, fail-closed, and independent of Pipeline Machine and Action fees. System Actors remain exempt. Trigger latching, strict FIFO, Q1, one active plus one deferred Pipeline, custody neutrality, and atomic Step effects do not change. +- `Reopen trigger`: Reopen Specification only if autonomous execution after installation cannot coexist with exact bounded hold authority, or if either reservation horizon requires custody transfer, recurring rent, or class-sensitive scheduling. + +## Context and Hypothesis + +- `Current architecture`: H0 derives the User Actor hold from current encoded state. Opening inserts `ActorRunStateStore`, then `reconcile_actor_state_hold` acquires the run component from the owner; suspension and each committed Step reconcile actual run bytes, and completion/cancellation releases the component. +- `Reason`: H0 minimizes locked capital but makes autonomous Cycle start depend on owner free balance at Opening and adds hold reconciliation to Opening, progress, suspension, completion, and cancellation. The Trigger and Pipeline fees can be admitted while the subsequent run hold still fails. +- `Hypothesis`: H1 can reserve one conservative maximum run component when an Actor becomes actively installed, eliminate Cycle-local run hold mutations, and allow later autonomous execution after the owner spends all otherwise-free balance, at acceptable lifecycle Weight and capital cost. +- `Materiality`: H1 must remove run-component hold/release mutations from every Cycle transition, preserve component-wise Weight and all semantics, and prove owner-zero-free-balance autonomy. Its permanently locked capital must be explicit and bounded; sub-material Weight reduction without autonomy or state-owner simplification does not justify H1. + +## Baseline and Candidates + +| ID | Architecture | Source tree | Runtime/config identity | +| --- | --- | --- | --- | +| H0 | Cycle-local exact encoded run hold | `aa339f7f`; dirty baseline `1d1fa30c` | Storage v15; P32; C6; current `ActorStateHoldBase/PerByte` | +| H1 | Active-installation maximum admitted run hold | Candidate diff `344600d2` | Same semantics/config; reservation horizon only | + +## Controls + +- `Controlled variables`: Contract, Actor class, schedule, funding, owner balance before reservation, run payload, FIFO position, Q1, database, toolchain, generated-Weight method, runtime resource split, and Task effect. +- `Changed variables`: Run hold target and acquisition/release points only. H0 tracks actual `ActorRunStateStore`; H1 reserves a conservative maximum while the User Actor has installed hot/Contract authority and releases it on deactivation, replacement transition as required, or close. +- `Confounders`: H1's conservative maximum must be derived from admitted runtime types rather than a hand-maintained byte constant. Existing broad `reconcile_actor_state_hold` calls may retain unrelated funding/detector reconciliation even after run mutations disappear. + +## Benchmark Class, Workloads, and Criteria + +- `Benchmark class`: Pallet benchmark, runtime integration, state-accounting test, and production-Wasm comparison. + +| Workload | State/population geometry | Hot/cold assumptions | Purpose | +| --- | --- | --- | --- | +| W1 | Maximum User Actor creation/activation | Cold lifecycle; no run exists | Reservation acquisition and creation/activation Weight | +| W2 | Maximum Opening to first committed Step | Maximum admitted frozen snapshot/run payload | Cycle-start hold mutation and autonomous execution | +| W3 | Running progress, suspension, resume, completion, cancellation | Persisted maximum run payload | Per-transition hold reads/writes and release behavior | +| W4 | Update/replacement and deactivation with Idle/Running/Suspended states | Maximum installed Contract | Reservation continuity, adjustment, rollback, and release | +| W5 | Close after each lifecycle state | Maximum topology | Exact final release and custody neutrality | +| W6 | Owner retains only held balance after installation | Trigger and sovereign funding remain valid | Autonomous execution without otherwise-free owner balance | +| W7 | 10,000 active one-Step User Actors, Idle and Running profiles | Setup outside measured blocks | Aggregate permanently locked capital and state-footprint truth | + +- `Metrics`: RefTime, estimated/measured ProofSize, reads/writes, encoded bytes, hold amount by component, owner free/held balance, Opening/progress/suspension/completion/cancel lifecycle success, rollback state root, Steps/Actors per block, and Wasm size. +- `Statistical method`: Use deterministic state fixtures and exact balance/hold assertions. Generate affected benchmark owners at 50 steps × 20 repeats for any finalist. Auxiliary focused probes may reject H1 before full generation but cannot authorize production. +- `Acceptance criteria`: H1 preserves semantics and atomicity, proves W6, removes Cycle-local run hold mutations, remains component-wise within lifecycle/control envelopes, and presents an explicit acceptable 10k locked-capital cost. No stale maximum, under-reservation, double release, or owner-liquidity dependency is allowed. +- `Rejection criteria`: Any execution under-reservation, hold drift, owner/custody mutation, System Actor charge, changed FIFO/Q1 behavior, inability to update/deactivate/close atomically, or locked-capital cost disproportionate to measured control/autonomy benefit. + +## Environment and Artifact Identity + +| Artifact | Identity | +| --- | --- | +| Source commit | `aa339f7f9103d700442d1aa3ecd2256cf4eb6668` | +| Source tree | Accepted H1 dirty-worktree digest `54010aaf911c0c61d7eaa55e45b3781a0d3ca7e437641b1d35cdf4c48db70329` | +| Rust toolchain | `rustc 1.98.0 (88d9e12ae 2026-08-18)` | +| Benchmark runtime Wasm | Pending candidate build | +| Post-generation production runtime Wasm | SHA-256 `8e036f9a74ef6b1003013586b9d1df7ebfb9c3703c56a9f8e060b7d12309d2bf`, 1,453,612 bytes | +| Final release production runtime Wasm | Pending exact release-tree freeze | +| Generated Weight | Full 50×20 production generation SHA-256 `6eae484ccff0b8a708e2a294c6407aa132153f34bed33b26cae13e44cf4848ef` | +| Database/backend | FRAME benchmark native RocksDB defaults | +| Benchmark command/config | `./scripts/benchmarks.sh --steps 50 --repeat 20 ... pallet_deos_actors` for affected owners | +| Raw evidence | Inline focused and capital tables under Measurements; source TSV digests retained there as provenance | + +## Measurements + +| Candidate | Workload | RefTime | ProofSize | Reads | Writes | Other decision metrics | +| --- | --- | ---: | ---: | ---: | ---: | --- | +| H0 | W7 hold | Not applicable | Not applicable | Not measured | Not measured | `0..0.001805` native units per Actor; `0..18.05` at 10k according to active maximum runs | +| H1 | W7 hold | Not applicable | Not applicable | Not measured | Not measured | `805` maximum run bytes; `0.001805` native units per active Actor; `18.05` at 10k | +| H1 | W6 autonomy | Not applicable | Not applicable | Not measured | Not measured | Opening persists after owner spends otherwise-free balance; focused test passes | +| H0 | W1 create User | `493,925,000` | `81,886` | 65 | 60 | Production generated baseline | +| H1 | W1 create User | `535,342,000` | `81,886` | 65 | 60 | Full production generation; one-time active reservation | +| H0 | W2 maximum Opening | `614,753,000` | `27,824` | 30 | 15 | Cycle-local run-hold acquisition | +| H1 | W2 maximum Opening | `602,530,000` | `27,824` | 30 | 15 | Full production generation; Active hold already present | +| H1 | Focused semantics | Not measured | Not measured | Not measured | Not measured | Pallet suite `700 passed, 5 ignored` | + +### Focused Weight Evidence + +The temporary TSV had SHA-256 `7037fa23c679fb4b8f2e5f27b4e0052fb6e931317b159523058e87c341276ac7`; its complete rows are retained here. + +| Workload | Candidate | RefTime | RefTime slope | Proof | Proof slope | Reads | Read slope | Writes | Write slope | Raw artifact identity | +| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | --- | +| Create User | H0 | 493,925,000 | 0 | 81,886 | 0 | 65 | 0 | 60 | 0 | Production Weight | +| Create User | H1 focused | 535,063,000 | 0 | 81,886 | 0 | 65 | 0 | 60 | 0 | `1595b1e5` | +| Maximum Opening | H0 | 614,753,000 | 0 | 27,824 | 0 | 30 | 0 | 15 | 0 | Production Weight | +| Maximum Opening | H1 focused | 553,640,000 | 0 | 27,824 | 0 | 30 | 0 | 15 | 0 | `463e5318` | +| Mixed scheduler | H0 | 359,339,000 | 196,773,183 | 4,772 | 3,361 | 5 | 11 | 4 | 5 | Production Weight | +| Mixed scheduler | H1 focused | 370,654,000 | 207,110,508 | 4,772 | 3,361 | 5 | 11 | 4 | 5 | `91584b19` | + +### Capital and Reservation Evidence + +The temporary TSV had SHA-256 `ebd4e87aa55b37c922ec02e536bc1df0294588d7e9da409b9c446ff6f0a8e330`; its complete rows are retained here. + +| Candidate | Reservation horizon | Maximum run bytes | Hold per Actor | Hold at 10k | Cycle target changes | Focused tests | +| --- | --- | ---: | --- | --- | --- | --- | +| H0 | Cycle-local exact | 805 | `0.001805` only while maximum run exists | `0..18.05` dynamic | Yes | Baseline | +| H1 | Active installed maximum | 805 | `0.001805` | `18.05` | No | `700 passed, 5 ignored` | + +## Derived Metrics + +| Candidate | Workload | RefTime delta | ProofSize delta | Throughput / latency | Marginal or amortized cost | Pareto class | +| --- | --- | ---: | ---: | --- | --- | --- | +| H1 | W1 create User | `+8.4%` | Exact match | One-time lifecycle cost | `0.001805` units per Active User Actor | Tradeoff | +| H1 | W2 maximum Opening | `-2.0%` | Exact match | Autonomous Opening | No Cycle-local run reservation | Pareto improvement after installation | +| H1 | Mixed execution slope | `-2.2%` | Exact match | Terminal User close remains included | Exact I/O unchanged | Pareto improvement | +| H1 | W7 10k Active | Not applicable | Not applicable | No scheduler priority change | `18.05` native units held | Bounded capital cost | + +## Statistical / Variance Notes + +Preliminary exact encoded-size and balance evidence is deterministic. H1 uses `MaxEncodedLen` rather than a maintained byte constant. Generated Weight model quality and complete runtime evidence remain pending. + +## Result + +H1 reserves the type-derived `805`-byte maximum run footprint whenever User Actor hot authority is installed. At production prices this locks `0.001805` native units per Active User Actor, or `18.05` units at 10,000. The owner-zero-free-balance fixture opens and persists a run because no new owner hold is needed. Full production generation shows User creation RefTime rising `8.4%`, maximum Opening falling `2.0%`, and the mixed execution slope falling `2.2%`; estimated ProofSize and exact I/O are unchanged. The earlier isolated mixed sample was noisy and is superseded by the complete 50×20 generation. Terminal User close still releases all Active hold authority under either horizon. + +## Interpretation + +H1 moves one bounded cost to installation, where owner authorization and balance admission are explicit, and removes the later owner-balance dependency from autonomous Opening. The `18.05`-unit 10k cost is bounded and small relative to the runtime population commitment. H0 is capital-minimal but can consume a paid useful Trigger and Pipeline admission path only to encounter unavailable run hold. H1 preserves fee, custody, FIFO, Q1, and class-neutral semantics while making installed capacity truthful. + +## Decision + +Accept H1 active-lifetime maximum run-state hold. Reserve the type-derived maximum when User Active authority is installed, preserve it through Opening/progress/suspension/completion/cancellation, and release it on deactivation or Close. Retain exact holds for all other components. + +## Rejected Alternatives and Tradeoffs + +| Candidate | Decision | Evidence-backed reason | +| --- | --- | --- | +| Reserve from Actor sovereign custody | Excluded | Violates owner-funded state-hold and custody neutrality | +| Acquire maximum hold lazily at first Trigger | Excluded | Retains autonomous owner-balance dependency and moves the same failure later | +| Charge recurring run rent | Excluded | Actor lifetime has no recurring rent | +| Hand-maintained maximum byte constant | Excluded | Can become stale and under-reserve after type/layout changes | + +## Validity + +- `Establishes`: H1 owner-balance autonomy, exact `805`-byte/`0.001805`-unit reservation, `18.05`-unit 10k cost, and focused create/Opening Weight tradeoff. +- `Does not establish`: Final release-tree identity or unrelated funding/detector hold optimization; the post-generation Weight/Wasm identities are established. +- `Known limitations`: Final release Wasm and metadata identities remain part of prerelease convergence; complete 50×20 generated Weight now owns production branch authority. +- `Invalidation triggers`: Run layout, hold pricing, lifecycle topology, admission payload, runtime balance semantics, benchmark method, or production Weight changes. +- `Residual qualitative insight`: Reservation horizon couples protocol autonomy to capital efficiency even when storage bytes are unchanged. + +## Follow-Up / Next Gradient + +```text +Cycle-local run hold → owner-balance liveness dependency +→ active-lifetime maximum reservation → autonomous Opening +→ measure lifecycle Weight and 10k locked capital +→ retain the component-wise Pareto winner +``` + +- `Eliminated hypotheses`: H0's lower idle capital dominates autonomous liveness; H1 requires stale byte constants; Cycle-local reservation is necessary for exact accounting. +- `Remaining hypotheses`: None for reservation horizon; ordinary production regeneration may correct branch models without reopening H1 semantics. +- `Stop condition`: Accept one reservation horizon only after exact hold accounting, owner-zero-free-balance autonomy, lifecycle rollback, generated Weight, and 10k capital evidence converge. + +## Architecture Impact + +- `Affected physical truth`: User Actor hold derivation, lifecycle reservation points, runtime cost quotes, autonomous Opening, and generated lifecycle/execution Weight. +- `Architecture update`: Update package and integration architecture only after accepted implementation and tests converge. +- `Provenance citation`: Cite EXP-0024 for the selected run-state hold reservation horizon. + +## Relations + +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: State-hold pressure left by [EXP-0023](./EXP-0023.md). +- `Contradicts`: None. +- `Depends on`: Accepted Actor state-hold semantics, C6 run topology, P32, strict FIFO/Q1, and equal-thirds resource policy. +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. diff --git a/.agents/skills/architecture-experiments/tracks/actors/experiments.md b/.agents/skills/architecture-experiments/tracks/actors/experiments.md new file mode 100644 index 00000000..bdff7568 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/experiments.md @@ -0,0 +1,92 @@ +# Actors Experiment Track + +| Field | Value | +| --- | --- | +| Track ID | actors | +| Status | Active | +| Scope owner | DEOS Actors physical execution, storage, scheduling, and control topology | +| Depends on tracks | None | + +## Scope and Boundary + +- `Owns`: Actor Contract and Step representation, hot/cold state, run state, FIFO and temporal topology, detector/fanout geometry, block service, resource allocation, and executor lowering. +- `Excludes`: Normative Actor semantics, adapter-internal execution architecture, Router-internal route selection, open work, and experiment results. + +## Governing Invariants + +- Preserve deterministic semantics, strict FIFO, one causal hop per block, atomic Step effects, User/System neutrality, bounded state, component-wise Weight, No Ceiling Tax, and production Weight soundness. +- Governing contracts remain in `template/pallets/actors/docs/specification.en.md`, `docs/actors-resource-policy.specification.en.md`, and `docs/actors-performance-assurance.specification.en.md`. + +## Accepted Physical Baseline + +- The 0.7.24 Actors baseline combines [EXP-0016](./EXP-0016.md) C6 Contract geometry, [EXP-0013](./EXP-0013.md) loaded-state reuse through one-Step planning and commit, and [EXP-0010](./EXP-0010.md) compact Observation activation. Complete renewed evidence binds the P32 runtime profile; [EXP-0001](./EXP-0001.md) preserves the rejected 0.7.23 throughput hypothesis as historical evidence. + +## Research Portfolio + +- Contract ceiling, body geometry, Step representation, and minimal active state. +- Actor control allocation, Economic Zipper, one-Step proof/overhead, and specification-gated service-quantum Pareto research. +- Crossing, observation fanout, cadence/wakeup, and aggregate FIFO geometry. +- Generic versus specialized executor lowering after structural convergence. + +## Cross-Track Dependencies + +- Actors may consume bounded effect-Weight and failure evidence from the [Adapters track](../adapters/experiments.md) and route envelopes from the [Router track](../router/experiments.md). +- Router does not depend on Actor queue geometry. Cyclic questions must be split at their evidence boundary. + +## Entry and Exit Conditions + +- `Entry`: The decision changes Actor-owned storage, scheduling, control allocation, lifecycle geometry, or execution lowering without changing semantics. +- `Exit`: Adapter-internal or route-internal choices transfer to their owning track; this track becomes Dormant when no decision-relevant Actor question remains. + +## Experiment Index + +IDs are immutable monotonic identities within this track, not execution-order numbers. Status and decision navigation is canonical here; evidence remains in each record. Current logical order belongs to `Current Decision Critical Path` because evidence may reorder, eliminate, merge, or defer Proposed questions without changing historical identities. + +| ID | Release | Status | Question | Baseline | Result / decision | Successor | +| --- | --- | --- | --- | --- | --- | --- | +| [EXP-0001](./EXP-0001.md) | 0.7.23 | Rejected | Does released 0.7.23 meet 100 one-Step-like cycles/block with stable service? | Tag `de498bf`; template `00f8044e` | 32.24–40.99 cycles/block; target missed | EXP-0013 | +| [EXP-0002](./EXP-0002.md) | 0.7.24 | Invalidated | Can 32 replace the 8-Step runtime without lifecycle/state failure or short-Contract ceiling tax? | C6 B8 manifest `4c461bd9`; P16/P32 tuple-only candidates | P32 control fits, but observation fanout proof `300,510 → 331,998` cuts fixed service `3 → 2` pages | Compact activation authority | +| [EXP-0003](./EXP-0003.md) | 0.7.24 | Rejected | Which Contract body geometry minimizes current-Step proof and lifecycle cost without ceiling tax? | Shared B0/C1 manifest `45c43626`; Wasm `23c83b9f` | Reject C1; naive fragmentation loses every branch; inline Step 0 is earned | EXP-0016 | +| EXP-0004 | 0.7.24 | Proposed | Which Step representation wins? | Current representation pending | Not measured | Pending | +| [EXP-0005](./EXP-0005.md) | 0.7.24 | Accepted | Which single-owner current execution authority minimizes Q1 control cost? | Final C6 Hot/Contract/Run/FIFO with accepted EXP-0022 equal thirds | Retain A0; reject A1 bound, reverted A2 +3.2%, and non-conforming A3 | Advance EXP-0021 | +| [EXP-0006](./EXP-0006.md) | 0.7.24 | Accepted | Which fixed 20/80, 25/75, or 30/70 allocation wins? | Production 20/80 and effectful 10k profiles | Select 30/70: W1/W2 +34.0%, W3 +29.8%, zero mixed failures | EXP-0005 continues from 30/70 | +| EXP-0007 | 0.7.24 | Proposed | Is symmetric base-turn allocation work-conserving? | Current service pending | Not measured | Pending | +| [EXP-0008](./EXP-0008.md) | 0.7.24 | Accepted | Which Crossing membership page geometry wins? | Focused candidates plus full split-page production regeneration | P128 Crossing is independent from P64 broad fanout | EXP-0009 | +| [EXP-0009](./EXP-0009.md) | 0.7.24 | Accepted | Does C128-P128 fit every complete Crossing branch? | Full split-page production Weight plus H0 C128/N64 evidence | P128/C128/N64 Crossing with independent P64 fanout is shipped | Shipped baseline | +| [EXP-0010](./EXP-0010.md) | 0.7.24 | Accepted | Which compact activation authority removes P32 fanout ceiling tax without semantic or short-FIFO loss? | EXP-0002 B8/P32 fanout proof `300,510 / 331,998` | Candidate A restores P32 service 2 → 3; complete renewed Weight/metadata/ABI/Wasm evidence binds production P32 | Shipped P32 baseline | +| EXP-0011 | 0.7.24 | Proposed | Which cadence/wakeup cohort serves bounded herds? | Current temporal topology pending | Not measured | Pending | +| EXP-0012 | 0.7.24 | Proposed | Which canonical FIFO-internal geometry reduces queue work? | Current direct FIFO | External block-bound successor staging excluded by interleaved-order proof; no candidate measured | Dormant pending measured canonical-FIFO pressure | +| [EXP-0013](./EXP-0013.md) | 0.7.24 | Accepted | Which causal owners bind one-Step Weight and throughput? | Exact B1 manifest `5dc3631c`; Wasm `7efed56e` | Reuse accepted: marginal RefTime `167,553,076 → 137,612,859` (`-17.87%`), ProofSize unchanged | EXP-0016 closure, then EXP-0002 | +| EXP-0014 | 0.7.24 | Proposed | What is Actor overhead over an equivalent external operation? | External baseline pending | Not measured | Pending | +| EXP-0015 | 0.7.24 | Proposed | Does specialized lowering beat the generic executor? | Structural baseline unavailable | Conditional | Pending | +| [EXP-0016](./EXP-0016.md) | 0.7.24 | Accepted | Does inline Step 0 plus a lazy tail beat monolithic B0 without ceiling tax? | Shared B0/C5 manifest `4cc1dd50`; Wasm `c9c09e3f` | C6 preserves Step-0 win and cuts C5 state 56.84% with material lifecycle gains | C6 selected for generic production implementation | +| [EXP-0017](./EXP-0017.md) | 0.7.24 | Proposed | Under what explicit Q1 failure may Q2/Q4 service quantum research reopen over C6 Chunk4? | EXP-0013 reuse + EXP-0016 C6; converged I4 identity pending | Explicitly dormant; Q1 remains authoritative and Q2/Q4 reopen only after measured binding context/persistence overhead | EXP-0018 owns Fresh timing | +| [EXP-0018](./EXP-0018.md) | 0.7.24 | Rejected | Can bounded same-block Fresh Step-0 service improve causal latency without recursion or continuation starvation? | fixed-timing cleanup tree `9fe556b4`; Weight `6a1d68bc`; benchmark Wasm `12e502e2`; production Wasm `674022f0`; metadata `1b74078e` | T0 lowers best-effort latency but adds a consensus timing branch without guaranteeing herd execution; T+1 remains FIFO eligibility, not an N+1 SLA | Retain `NextBlock`; delete candidate/snapshot surfaces | +| [EXP-0019](./EXP-0019.md) | 0.7.24 | Invalidated | Which paid-coalescence Trigger/Pipeline Machine strategy preserves Q1 control while Actions remain pay-as-attempted and activation failure cleans process only? | A0 `31927940`/`fe3ff004`; W5 source `aaeb0ab`, complete Weight `afdb03bb`; formatted W8 Wasm `364ba474`, 3/39/79 lifecycle/event/error variants | Trigger latching invalidated paid redundancy; W5/W8 close reusable P0 Pipeline/Action evidence inherited by EXP-0020 | EXP-0020 | +| [EXP-0020](./EXP-0020.md) | 0.7.24 | Accepted | Which detector disable/re-arm topology enforces useful-transition Trigger charging while retaining inherited P0 Pipeline Machine evidence? | Source `95ac8fe5`; tree `677dae5d`; Weight `929d8151`; metadata `56c992d2` | Hybrid A1 direct/Cadenced detach plus A2 indexed disabled authority; eager indexed removal rejected by traversal races | Final release identity only | +| [EXP-0021](./EXP-0021.md) | 0.7.24 | Accepted | Do 24 or 16 Steps materially beat the 32-Step ceiling under final C6? | 32-Step C6/tiered production baseline | Lower ceilings save 18–41% only at maximum geometry | Retain 32-Step framework expressiveness | +| [EXP-0022](./EXP-0022.md) | 0.7.24 | Accepted | Does exact one-third Control beat accepted 30/35/35? | EXP-0006 accepted 30/70 | Select equal thirds: W1/W2 +9.9%, W3 +12.7%, zero failures | EXP-0005 continues inside hard ceiling | +| [EXP-0023](./EXP-0023.md) | 0.7.24 | Accepted | Can a Step-centric hot path materially reduce ordinary Running Q1 machine cost? | A0 Actor-centric topology retained | Complete A1: -14.9% RefTime, -17.2% estimated proof | A1 reverted; A2 not admitted | +| [EXP-0024](./EXP-0024.md) | 0.7.24 | Accepted | Should run-state hold be Cycle-local or reserved for the active installed lifetime? | H0 Cycle-local exact hold | Select H1: 0.001805 units/Actor, autonomous Opening, create +8.4%, Opening -2.0% | Full production Weight regenerated | + +## Current Decision Critical Path + +- `Production baseline`: C6 fragment-local resources, split run head/payload state, loaded-state reuse through execution, successor placement, and run persistence, compact Observation activation, exact FIFO/wakeup authority, and the complete pre-I4 P32 Weight/metadata/ABI/Wasm handoff are converged; I4 must renew the generated handoff. +- `User lifecycle semantic baseline`: Trigger latching now charges only useful `pending_signal: false -> true` transitions, suppresses redundant Actor-specific evaluation/fees while latched, and re-arms detectors from current authoritative state after Opening. EXP-0019 is invalidated only for its paid-coalescence assumptions; its Pipeline Machine/Action/apoptosis/custody evidence transfers to EXP-0020. +- `One-Step economics and control`: Valid-actual Task-effect evidence, atomic Step control, upfront Pipeline Machine charging, and current-attempt Action charging remain implemented and regenerated for P0. EXP-0020 accepts useful-only Trigger charging through direct/Cadenced detach and indexed disabled authority; only final release identity remains external to the experiment. +- `Service quantum`: EXP-0017 is Proposed but explicitly dormant. Q1 remains authoritative; Q2/Q4 reopen only if converged Q1 misses the committed-Step target and controlled decomposition proves context/persistence overhead still binds after accepted optimizations. Chunk4 remains independent and no Task-shape cohort is allowed. +- `Fresh Step 0`: EXP-0018 rejected same-block execution and selected one protocol-fixed next-block eligibility floor. Readiness observed in N cannot execute before N+1, receives no class/market/provenance priority, and may execute later under strict FIFO and available `on_idle` Weight. +- `Step representation`: Keep EXP-0004 Proposed; open it only if an exact production profile identifies inline Task or Predicate payload proof as a material binding contributor. Do not add payload keys/reads from portfolio pressure alone. +- `Measured residual pressure`: EXP-0005 and EXP-0023 retained Actor-centric execution authority, EXP-0022 selected equal thirds, and EXP-0021 retained the 32-Step ceiling. EXP-0024 now isolates the remaining active-lifetime run-state hold decision. Keep EXP-0007 and EXP-0014 dormant unless this experiment exposes a separate allocation or external-overhead decision. Exact cumulative outcomes remain semantic because completion policy, cancellation, suspension, simulation, and boundary events consume them; they are not removable telemetry under the accepted specification. +- `Runnable topology boundary`: EXP-0012 is restricted to canonical FIFO-internal geometry. Ordinary exact-`N+1` successors already append directly to canonical FIFO. Never stage them in a separate block-bound page: it cannot preserve interleaved order with later Manual, Trigger, or wakeup admission unless it becomes a second ordering authority, expands staging to every readiness source, inserts ahead of the FIFO head, or reserves the canonical FIFO position at source. Only source-time canonical reservation conforms, and that is the retained owner. +- `Research injection reconciliation`: Current-Step block-Weight admission remains Q1-local. A ready User activation charges Trigger plus complete Pipeline Machine control upfront from one fixed hot projection; future Actions pay valid actual effects per attempt. No mid-pipeline economic death, funding-wait/run-ledger topology, lifetime rent, or lifecycle Transfer privilege exists; activation-only apoptosis preserves custody and exact-slot recovery. Zero-Step temporal service, cause-time Triggers, causal mixed-length first reaction, class-neutral FIFO, and the EXP-0006-selected 30/70 control/effect split remain canonical. Shared Contract-code deduplication and `PercentageOfLastFunding` removal remain unearned without measured binding pressure or real-family usage evidence. +- `Topology residual pressure`: The explicit ≥128 Crossing production-cohort target opens EXP-0008 for controlled P64/P128/P256 page evidence before EXP-0009 selects cohort geometry. Open EXP-0010 through EXP-0012 only when measured fanout, cadence/wakeup, or FIFO targets require a physical choice not already resolved by the shipped bounded topology. +- `Executor lowering`: Open EXP-0015 last and only if structural convergence leaves a material generic-versus-specialized lowering decision. + +This is a decision path, not a promise to execute every portfolio question. A Proposed row may be rejected, superseded, merged, or left unopened when stronger evidence removes its decision pressure. The release objective is the highest-performing conforming final design, not experiment-count completion. + +## Maintenance + +- Allocate the next ID from the highest ID in this track index; IDs may repeat in another track because the canonical identity is `/EXP-NNNN`. +- Create `EXP-NNNN.md` before Proposed becomes Prepared, and update this row with every lifecycle or relation change. +- Keep measurements, interpretation, decisions, and artifacts in the record rather than this index. diff --git a/.agents/skills/architecture-experiments/tracks/adapters/experiments.md b/.agents/skills/architecture-experiments/tracks/adapters/experiments.md new file mode 100644 index 00000000..3590fabb --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/adapters/experiments.md @@ -0,0 +1,52 @@ +# Adapters Experiment Track + +| Field | Value | +| --- | --- | +| Track ID | adapters | +| Status | Active | +| Scope owner | Runtime adapter physical boundaries, lowering, and effect-resource evidence | +| Depends on tracks | [router](../router/experiments.md) | + +## Scope and Boundary + +- `Owns`: Host adapter interface geometry, effect-Weight ports, actual-versus-maximum accounting, failure-class plumbing, generic lowering, and portability costs. +- `Excludes`: Protocol semantics, Actor queue/control topology, Router route-selection mechanics, open work, and experiment results. + +## Governing Invariants + +- Preserve host-defined semantics, transactional effects, typed failure classes, bounded resource ownership, exact non-overlapping Weight attribution, generic pallet portability, and fail-closed unsupported paths. + +## Accepted Physical Baseline + +- None established by an Accepted Experiment Record. Current code and production Weight remain implementation evidence only. + +## Research Portfolio + +- Effect-resource envelope representation and adapter-returned actual Weight. +- Generic versus task-family lowering and interface-surface pressure. +- Cross-pallet proof ownership, portability, and failure-path geometry. + +## Cross-Track Dependencies + +- Adapters consumes Router-owned route and execution envelopes when exposing swap effects. +- [Actors](../actors/experiments.md) may consume adapter evidence but does not determine adapter internals. +- Dependency direction is `Router → Adapters → Actors`; shared questions receive one primary owner instead of duplicate experiments. + +## Entry and Exit Conditions + +- `Entry`: The decision changes a runtime adapter boundary or effect-resource realization while preserving caller semantics. +- `Exit`: Actor scheduling questions transfer to Actors; route selection and quote topology transfer to Router; this track becomes Dormant when adapter geometry has no decision pressure. + +## Experiment Index + +No experiment is allocated. Add the first record only when a decision-relevant hypothesis, comparable baseline/candidate, materiality threshold, and smallest falsifier are known. + +| ID | Release | Status | Question | Baseline | Result / decision | Successor | +| --- | --- | --- | --- | --- | --- | --- | +| None | — | — | No experiment opened | — | — | — | + +## Maintenance + +- Allocate `EXP-0001` when the first real Adapters experiment is opened; IDs are monotonic within this track. +- Create `EXP-NNNN.md` before Proposed becomes Prepared, and update this index with every lifecycle or relation change. +- Keep measurements, interpretation, decisions, and artifacts in the record rather than this index. diff --git a/.agents/skills/architecture-experiments/tracks/router/experiments.md b/.agents/skills/architecture-experiments/tracks/router/experiments.md new file mode 100644 index 00000000..39a7f9b5 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/router/experiments.md @@ -0,0 +1,51 @@ +# Router Experiment Track + +| Field | Value | +| --- | --- | +| Track ID | router | +| Status | Active | +| Scope owner | DEOS Router physical route, quote, and execution architecture | +| Depends on tracks | None | + +## Scope and Boundary + +- `Owns`: Candidate enumeration geometry, bounded Native-anchored routing, quote/execution representation, route proof topology, and Router lifecycle/throughput tradeoffs. +- `Excludes`: Maximum-recipient-output semantics, downstream adapter wrappers, Actor scheduling, open work, and experiment results. + +## Governing Invariants + +- Preserve maximum recipient output among viable candidates, deterministic bounded search, fee-burning behavior, typed quote metadata, transactional execution, reserve safety, and production Weight soundness. + +## Accepted Physical Baseline + +- None established by an Accepted Experiment Record. Current Router implementation and generated Weight are implementation evidence only. + +## Research Portfolio + +- Direct versus bounded Native-anchored route geometry. +- Candidate quote representation and proof/read sharing. +- Quote-to-execution commitment, failure branches, and throughput limits. + +## Cross-Track Dependencies + +- Router produces bounded route/effect evidence that may be consumed by adapter and [Actors](../actors/experiments.md) research. +- Router experiments must not depend on adapter wrappers or Actor queue topology for correctness. + +## Entry and Exit Conditions + +- `Entry`: The decision changes Router-owned physical search, quote, route, or execution geometry without changing routing semantics. +- `Exit`: Wrapper/lowering questions transfer to adapter research; Actor service composition transfers to Actors; this track becomes Dormant when no Router decision remains. + +## Experiment Index + +No Router experiment has been opened or measured. The portfolio above is directional only and does not allocate IDs. + +| ID | Release | Status | Question | Baseline | Result / decision | Successor | +| --- | --- | --- | --- | --- | --- | --- | +| None | — | — | No experiment opened | — | — | — | + +## Maintenance + +- Allocate `EXP-0001` only when the first real Router hypothesis has a comparable baseline/candidate, materiality threshold, and smallest falsifier. +- Create `EXP-NNNN.md` before Proposed becomes Prepared, and update this index with every lifecycle or relation change. +- Keep measurements, interpretation, decisions, and artifacts in the record rather than this index. diff --git a/.agents/skills/benchmarking/SKILL.md b/.agents/skills/benchmarking/SKILL.md deleted file mode 100644 index bda659b7..00000000 --- a/.agents/skills/benchmarking/SKILL.md +++ /dev/null @@ -1,90 +0,0 @@ ---- -name: benchmarking -description: Selects, designs, interprets, and integrates benchmarks without duplicating shared scripts or overstating evidence; the current DEOS route covers FRAME runtime measurement. ---- - -# Benchmarking - -Use this skill when a DEOS change needs benchmark design, focused measurement, parameter comparison, generated-weight integration, or production evidence. The current executable route covers FRAME runtime benchmarking; frontend benchmarking remains out of scope until the project adopts an explicit measured need and shared command surface. - -## Ownership Boundary - -This skill owns benchmark methodology and agent judgment: - -- identify the bounded unit of work and relevant worst-case state; -- select compile, diagnostic, comparative, or production evidence; -- interpret RefTime, ProofSize, reads, and writes independently; -- route accepted numbers to generated weights and architecture evidence; -- reject claims stronger than the executed evidence. - -It does not own runtime semantics, pallet implementation, Actors scheduler stress policy, release publication, or command implementation. Shared execution remains in [`scripts/benchmarks.sh`](../../../scripts/benchmarks.sh), production Wasm construction remains in [`scripts/03-build-runtime.sh`](../../../scripts/03-build-runtime.sh), and each script's `--help` remains the sole flag/usage reference. - -## Single-Owner Map - -| Concern | Truth owner | -| --- | --- | -| Command syntax and prerequisites | Root script and its `--help` | -| Benchmark selection and interpretation | This skill | -| Worst-case setup and postconditions | Owning pallet `benchmarking.rs` | -| Accepted production numbers | Runtime-generated `weights/*.rs` | -| Architectural meaning and hashes | Owning architecture document | -| Remaining evidence | `BACKLOG.md` | - -Do not copy script flag inventories, generated numbers, benchmark setup code, or subsystem policy into this skill. Link to the owner and retain only reusable decision rules. - -## Evidence Classes - -| Class | Purpose | Permitted conclusion | -| --- | --- | --- | -| Compile | Verify benchmark registration and buildability | The benchmark surface compiles | -| Diagnostic | Falsify setup or compare a local code path cheaply | Relative/local behavior only | -| Comparative | Compare candidates under identical steps, repeats, runtime, and host | Candidate selection for measured cases | -| Production | Accept generated weights from the production benchmark runtime and rebuild production Wasm | Runtime weight/evidence acceptance within measured bounds | - -Unavailable optional comparative evidence may yield a narrower result. Missing production evidence fails closed when weights, runtime admission, or release claims depend on it. - -## Route - -1. Read the changed call path, existing benchmark, `WeightInfo`, runtime binding, owning architecture section, and open backlog gate. -2. Define one operation with explicit bounded components and postconditions. Split cases when storage topology changes, such as preserving versus deleting a page. -3. Run the benchmark compilation route through `scripts/benchmarks.sh --check`. -4. Use one focused extrinsic and a temporary output for diagnostic evidence. A focused output may supply a reviewed method, but it must never replace the complete generated pallet file. -5. Run a same-environment comparative matrix only when selecting a parameter or representation. -6. Accept production weights only after production-quality measurement, method/storage annotation review, runtime binding, and focused compile/check validation. -7. Rebuild production Wasm through `scripts/03-build-runtime.sh` when accepted runtime weights or runtime code change. -8. Record only accepted numbers, tool/runtime conditions, hashes, limits, and claim boundaries in the owning architecture document; narrow `BACKLOG.md` to what remains. -9. Run the changed-scope completion route. Escalate to the owning delivery/release gate when capacity, fairness, liveness, admission, or release acceptance changed. - -Use `--skip-build` only while reusing the same freshly built benchmark runtime for a coherent matrix. Rebuild after source, features, runtime configuration, or toolchain changes. - -## Benchmark Design - -- Measure work proportional to the bounded state actually processed, not total global state by convenience. -- Construct the smallest state that reaches the real worst-case branch; do not inflate unrelated actors, assets, pages, or history. -- Separate operations when they have different reads, writes, proof topology, cleanup, or failure semantics. -- Keep setup outside the measured block and assert the intended branch and resulting state inside or after it. -- Use measured ProofSize mode for storage-sensitive paths and inspect generated storage annotations against implementation reality. -- Model independent RefTime-stop and ProofSize-stop behavior when the production caller admits work in both dimensions. -- Treat page size as I/O granularity unless verified runtime budgeting establishes a throughput bound. - -## Interpretation Contract - -- RefTime, ProofSize, reads, and writes remain separate evidence dimensions. -- Minimum execution time is not the charged weight and does not override the generated model. -- Host timing supports controlled comparison; it does not establish reference-block throughput. -- A count ceiling, diagnostic benchmark, or `Weight::MAX` test does not prove ordinary-block capacity. -- Fixed benchmark cases prove only their measured topology. State assumptions and avoid extrapolation beyond them. -- Production claims require the runtime-bound method and production-Wasm evidence, not a temporary generated file alone. - -## Integration Checks - -Before handoff, verify: - -- benchmark name, `WeightInfo` declaration, generic fallback, and runtime implementation agree; -- generated read/write counts match the storage annotations and measured branch; -- no placeholder runtime weight remains; -- formatting, benchmark compilation, scoped Clippy/checks, and relevant tests pass through root scripts; -- architecture prose distinguishes measurement, inference, configured ceiling, and throughput claim; -- recorded weights and Wasm hashes match current files. - -Stop rather than generalize when the next benchmark lacks a defined production caller, bounded component, acceptance decision, or truthful claim it could change. diff --git a/.agents/skills/release-assurance/evidence/runtime-weight-delta-ledger.md b/.agents/skills/release-assurance/evidence/runtime-weight-delta-ledger.md index bd9ed547..e467c577 100644 --- a/.agents/skills/release-assurance/evidence/runtime-weight-delta-ledger.md +++ b/.agents/skills/release-assurance/evidence/runtime-weight-delta-ledger.md @@ -12,105 +12,160 @@ Interpretation codes classify changed paths only: `I` identity guard; `C` correc | Pallet | Weight method | RefTime: v0.7.22 → 0.7.23 candidate | Base delta | ProofSize: v0.7.22 → 0.7.23 candidate | Reads: v0.7.22 → 0.7.23 candidate | Writes: v0.7.22 → 0.7.23 candidate | Code | | --- | --- | ---: | ---: | ---: | ---: | ---: | --- | -| Actors | `create_user_actor` | `193,393,000 → 261,839,000` | +35.39% | `12,200 → 81,886` | `25 → 54` | `19 → 47` | C | -| Actors | `create_user_actor_at_slot` | `145,691,000 → 258,137,000` | +77.18% | `12,200 → 81,886` | `16 → 54` | `10 → 47` | C | -| Actors | `create_system_actor` | `103,087,000 → 210,295,000` | +104.00% | `12,200 → 81,886` | `16 → 53` | `11 → 47` | C | -| Actors | `create_system_actor_at_sovereign_id` | `91,563,000 → 199,540,000` | +117.93% | `12,200 → 81,886` | `14 → 51` | `9 → 45` | C | -| Actors | `create_user_actor_crossing_new_page` | `— → 254,785,000` | new | `— → 12,200` | `— → 22` | `— → 15` | C | -| Actors | `create_dormant_system_actor` | `67,817,000 → 65,861,000` | -2.88% | `12,200 → 12,200` | `12 → 12` | `7 → 7` | C | -| Actors | `activate_actor` | `124,249,000 → 190,181,000` | +53.06% | `12,200 → 81,886` | `19 → 47` | `14 → 41` | C | -| Actors | `deactivate_actor` | `103,018,000 → 312,475,000` | +203.32% | `12,200 → 81,886` | `9 → 46` | `6 → 43` | C | -| Actors | `pause_actor` | `62,020,000 → 59,506,000` | -4.05% | `12,200 → 12,200` | `7 → 7` | `2 → 2` | C | -| Actors | `resume_actor` | `61,112,000 → 59,506,000` | -2.63% | `12,200 → 12,200` | `7 → 7` | `2 → 2` | C | -| Actors | `manual_trigger` | `84,719,000 → 82,764,000` | -2.31% | `12,200 → 12,200` | `12 → 12` | `5 → 5` | C | -| Actors | `close_actor` | `210,365,000 → 355,707,000` | +69.09% | `12,200 → 81,886` | `24 → 51` | `23 → 50` | C | -| Actors | `update_contract` | `274,480,000 → 457,049,000` | +66.51% | `12,200 → 81,886` | `25 → 54` | `22 → 49` | C | -| Actors | `clear_crossing_worker_fault` | `— → 14,248,000` | new | `— → 1,529` | `— → 1` | `— → 1` | C | -| Actors | `clear_observation_fanout_worker_fault` | `— → 13,828,000` | new | `— → 1,516` | `— → 1` | `— → 1` | C | -| Actors | `clear_wakeup_worker_fault` | `— → 12,991,000` | new | `— → 1,503` | `— → 1` | `— → 1` | C | -| Actors | `set_active_actor_limit` | `9,289,000 → 9,429,000` | +1.51% | `1,489 → 1,489` | `2 → 2` | `0 → 0` | C | -| Actors | `permissionless_sweep` | `44,350,000 → 42,465,000` | -4.25% | `12,200 → 12,200` | `7 → 7` | `0 → 0` | C | -| Actors | `permissionless_sweep_many` | `23,891,147 + 66,286,454·n → 20,599,986 + 87,291,781·n` | -13.78% | `1,489 + 11,210·n → 1,489 + 11,210·n` | `3 + 11·n → 3 + 12·n` | `2 + 7·n → 2 + 8·n` | C | -| Actors | `fee_collection` | `43,791,000 → 43,022,000` | -1.76% | `3,593 → 3,593` | `1 → 1` | `1 → 1` | O | -| Actors | `task_transfer` | `232,854,000 → 226,429,000` | -2.76% | `12,200 → 12,200` | `18 → 18` | `8 → 8` | C | -| Actors | `task_burn` | `19,067,000 → 18,997,000` | -0.37% | `3,593 → 3,593` | `1 → 1` | `1 → 1` | C | -| Actors | `task_mint` | `197,794,000 → 191,089,000` | -3.39% | `12,200 → 12,200` | `16 → 16` | `6 → 6` | C | -| Actors | `predicate_set_evaluation` | `7,612,000 + 6,444,212·c → 7,543,000 + 6,543,854·c` | -0.91% | `3,675 + 674·c → 3,675 + 674·c` | `1 + 1·c → 1 + 1·c` | `0 → 0` | C | -| Actors | `task_stop_cycle` | `4,260,000 → 4,330,000` | +1.64% | `0 → 0` | `0 → 0` | `0 → 0` | C | -| Actors | `task_split_transfer` | `80,834,694 + 154,149,994·l → 78,936,673 + 152,943,219·l` | -2.35% | `8,040 + 11,210·l → 8,040 + 11,210·l` | `10 + 7·l → 10 + 7·l` | `4 + 3·l → 4 + 3·l` | C | -| Actors | `xcm_asset_deposit` | `218,117,000 → 215,464,000` | -1.22% | `12,200 → 12,200` | `19 → 19` | `8 → 8` | C | -| Actors | `task_add_liquidity` | `271,337,000 → 266,588,000` | -1.75% | `34,255 → 34,255` | `16 → 16` | `15 → 15` | C | -| Actors | `task_donate_liquidity` | `164,618,000 → 161,755,000` | -1.74% | `14,035 → 14,035` | `9 → 9` | `8 → 8` | C | -| Actors | `task_remove_liquidity` | `155,050,000 → 151,767,000` | -2.12% | `8,817 → 8,817` | `8 → 8` | `6 → 6` | C | -| Actors | `task_stake` | `85,906,000 → 84,649,000` | -1.46% | `8,817 → 8,817` | `7 → 7` | `7 → 7` | C | -| Actors | `task_unstake` | `101,691,000 → 100,503,000` | -1.17% | `8,817 → 8,817` | `7 → 7` | `7 → 7` | C | -| Actors | `task_dex_exact_in` | `509,640,000 → 502,376,000` | -1.43% | `19,253 → 19,253` | `40 → 40` | `17 → 17` | O | -| Actors | `task_dex_exact_out` | `502,865,000 → 495,532,000` | -1.46% | `19,253 → 19,253` | `39 → 39` | `17 → 17` | O | -| Actors | `scheduler_on_idle_base` | `14,737,000 → 14,667,000` | -0.47% | `1,543 → 1,543` | `6 → 6` | `1 → 1` | C | -| Actors | `materialization_coordinator_base` | `— → 25,283,000` | new | `— → 5,982` | `— → 10` | `— → 1` | C | -| Actors | `scheduler_actor_state_probe` | `37,715,000 → 36,109,000` | -4.26% | `12,200 → 12,200` | `5 → 5` | `0 → 0` | M | -| Actors | `cycle_orchestration` | `50,426,000 → 48,750,000` | -3.32% | `12,200 → 12,200` | `5 → 5` | `3 → 3` | C | -| Actors | `step_orchestration` | `49,961,778 + 174,432·n → 48,660,474 + 183,073·n` | -2.60% | `12,200 → 12,200` | `5 → 5` | `3 → 3` | C | -| Actors | `scheduler_paged_append_existing_page` | `75,639,000 → 74,172,000` | -1.94% | `7,938 → 7,979` | `10 → 10` | `5 → 5` | C | -| Actors | `scheduler_paged_append_new_page` | `73,335,000 → 72,008,000` | -1.81% | `10,283 → 10,319` | `11 → 11` | `5 → 5` | C | -| Actors | `scheduler_wakeup_append_existing_page` | `78,642,000 → 76,268,000` | -3.02% | `6,694 → 6,736` | `8 → 8` | `3 → 3` | C | -| Actors | `scheduler_wakeup_append_new_page` | `81,995,000 → 78,573,000` | -4.17% | `6,833 → 6,880` | `8 → 8` | `4 → 4` | C | -| Actors | `scheduler_wakeup_replace_exact` | `99,455,000 → 97,151,000` | -2.32% | `7,492 → 7,521` | `10 → 10` | `7 → 7` | C | -| Actors | `scheduler_wakeup_invalidate_middle_page` | `90,097,000 → 91,843,000` | +1.94% | `12,495 → 12,536` | `10 → 10` | `5 → 5` | C | -| Actors | `scheduler_wakeup_drain_partial_page` | `354,451,000 → 354,939,000` | +0.14% | `47,750 → 48,075` | `83 → 83` | `18 → 18` | C | -| Actors | `scheduler_wakeup_drain_full_page` | `648,137,000 → 658,684,000` | +1.63% | `90,034 → 90,587` | `164 → 164` | `36 → 36` | C | -| Actors | `scheduler_wakeup_drain_dense_boundary` | `673,421,000 → 686,551,000` | +1.95% | `92,825 → 93,394` | `170 → 170` | `38 → 38` | C | -| Actors | `scheduler_wakeup_drain_stale_page` | `569,704,000 → 602,530,000` | +5.76% | `89,426 → 89,979` | `164 → 164` | `4 → 4` | C | -| Actors | `scheduler_wakeup_cursor_insert` | `346,837,000 → 347,047,000` | +0.06% | `42,733 → 42,733` | `25 → 25` | `25 → 25` | C | -| Actors | `scheduler_wakeup_cursor_pop_min` | `452,230,000 → 448,249,000` | -0.88% | `55,259 → 55,259` | `34 → 34` | `26 → 26` | C | -| Actors | `scheduler_wakeup_cursor_remove_exact` | `422,267,000 → 419,753,000` | -0.60% | `54,726 → 54,726` | `33 → 33` | `25 → 25` | C | -| Actors | `scheduler_wakeup_cursor_worker_partial` | `112,517,000 → 116,428,000` | +3.48% | `7,498 → 7,524` | `14 → 15` | `8 → 8` | C | -| Actors | `scheduler_wakeup_cursor_worker_remove` | `548,262,000 → 535,621,000` | -2.31% | `56,434 → 56,463` | `48 → 49` | `33 → 33` | C | -| Actors | `scheduler_wakeup_cursor_worker_future` | `20,184,000 → 25,283,000` | +25.26% | `6,523 → 6,562` | `5 → 6` | `0 → 0` | C | -| Actors | `scheduler_paged_consume_preserve_page` | `48,540,000 → 47,702,000` | -1.73% | `4,868 → 4,893` | `9 → 9` | `3 → 3` | C | -| Actors | `scheduler_paged_consume_delete_page` | `50,007,000 → 48,750,000` | -2.51% | `4,846 → 4,875` | `9 → 9` | `5 → 5` | C | -| Actors | `scheduler_paged_tombstone_drain` | `43,931,000 + 9,481,801·n → 41,975,000 + 9,491,017·n` | -4.45% | `4,049 + 2,492·n → 4,096 + 2,492·n` | `5 + 5·n → 5 + 5·n` | `4 → 4` | C | -| Actors | `scheduler_paged_mixed_scan` | `47,004,000 + 40,911,571·n → 46,096,000 + 39,857,795·n` | -1.93% | `4,857 + 2,608·n → 4,846 + 2,616·n` | `5 + 5·n → 5 + 5·n` | `3 + 1·n → 3 + 1·n` | C | -| Actors | `scheduler_paged_execute_cheap` | `116,986,000 + 94,182,240·n → 115,379,000 + 93,865,117·n` | -1.37% | `3,716 + 2,733·n → 3,735 + 2,750·n` | `6 + 5·n → 6 + 5·n` | `4 + 3·n → 4 + 3·n` | C | -| Actors | `scheduler_paged_execute_cheap_mixed` | `244,449,000 + 120,287,892·n → 248,569,000 + 119,818,614·n` | +1.69% | `4,918 + 2,798·n → 4,729 + 2,829·n` | `6 + 6·n → 6 + 6·n` | `4 + 4·n → 4 + 4·n` | C | -| Actors | `continuation_suspend` | `39,972,061 + 41,737·s → 38,981,444 + 36,264·s` | -2.48% | `4,728 → 4,757` | `5 → 5` | `2 → 2` | C | -| Actors | `continuation_complete` | `40,928,000 → 39,601,000` | -3.24% | `5,154 → 5,183` | `5 → 5` | `2 → 2` | C | -| Actors | `continuation_cancel` | `177,888,000 → 175,514,000` | -1.33% | `12,200 → 12,200` | `16 → 16` | `11 → 11` | C | -| Actors | `continuation_suffix_admission` | `1,432,532 + 854·n → 1,432,976 + 494·n` | +0.03% | `0 → 0` | `0 → 0` | `0 → 0` | C | -| Actors | `observation_change_ingress` | `33,384,000 → 32,896,000` | -1.46% | `6,128 → 6,128` | `5 → 5` | `4 → 4` | C | -| Actors | `observation_fanout_base` | `6,146,000 → 6,705,000` | +9.10% | `1,543 → 1,543` | `1 → 2` | `0 → 0` | C | -| Actors | `observation_fanout_page` | `2,644,513,000 → 2,651,008,000` | +0.25% | `718,430 → 718,430` | `332 → 332` | `72 → 72` | C | -| Actors | `crossing_worker_base` | `6,146,000 → 6,705,000` | +9.10% | `1,543 → 1,543` | `1 → 2` | `0 → 0` | C | -| Actors | `crossing_work_probe` | `— → 46,305,000` | new | `— → 12,200` | `— → 8` | `— → 0` | C | -| Actors | `crossing_search_probe` | `— → 127,253,000` | new | `— → 81,886` | `— → 33` | `— → 0` | C | -| Actors | `crossing_fire_probe` | `— → 72,427,000` | new | `— → 12,200` | `— → 14` | `— → 0` | C | -| Actors | `crossing_tail_refill_probe` | `— → 17,530,000` | new | `— → 4,561` | `— → 1` | `— → 0` | C | -| Actors | `crossing_fire_pair_probe` | `— → 241,375,000` | new | `— → 23,410` | `— → 25` | `— → 0` | C | -| Actors | `crossing_fire_cohort_preflight` | `— → 21,192,911 + 33,046,169·c` | new | `— → 1,493 + 11,210·c` | `— → 2 + 7·c` | `— → 0` | C | -| Actors | `crossing_coalesced_cohort_preflight` | `— → 23,064,782 + 33,354,579·c` | new | `— → 1,493 + 11,210·c` | `— → 2 + 7·c` | `— → 0` | C | -| Actors | `crossing_terminal_cohort_preflight` | `— → 21,646,700 + 33,085,920·c` | new | `— → 1,493 + 11,210·c` | `— → 2 + 7·c` | `— → 0` | C | -| Actors | `crossing_skip_cohort_preflight` | `— → 6,986,537 + 8,569,450·c` | new | `— → 990 + 2,561·c` | `— → 0 + 2·c` | `— → 0` | C | -| Actors | `crossing_rearm_cohort_preflight` | `— → 12,893,611 + 11,773,740·c` | new | `— → 990 + 11,210·c` | `— → 0 + 3·c` | `— → 0` | C | -| Actors | `crossing_rearm_pair_probe` | `— → 58,249,000` | new | `— → 23,410` | `— → 11` | `— → 0` | C | -| Actors | `crossing_skip_pair_probe` | `— → 46,515,000` | new | `— → 6,112` | `— → 9` | `— → 0` | C | -| Actors | `crossing_transition_unit` | `31,988,000 → 31,918,000` | -0.22% | `6,060 → 6,060` | `5 → 5` | `2 → 2` | C | -| Actors | `crossing_leaf_unit` | `425,689,000 → 439,029,000` | +3.13% | `162,782 → 162,782` | `85 → 87` | `77 → 78` | C | -| Actors | `crossing_page_unit` | `426,457,000 → 440,426,000` | +3.28% | `162,782 → 162,782` | `85 → 87` | `77 → 78` | C | -| Actors | `crossing_rearm_unit` | `— → 397,683,000` | new | `— → 162,782` | `— → 78` | `— → 74` | C | -| Actors | `crossing_rearm_pair_unit` | `— → 430,439,000` | new | `— → 162,782` | `— → 82` | `— → 76` | C | -| Actors | `crossing_coalesced_unit` | `— → 419,403,000` | new | `— → 162,782` | `— → 82` | `— → 74` | C | -| Actors | `crossing_coalesced_pair_unit` | `— → 478,630,000` | new | `— → 162,782` | `— → 89` | `— → 76` | C | -| Actors | `crossing_placed_unit` | `— → 439,379,000` | new | `— → 162,782` | `— → 87` | `— → 78` | C | -| Actors | `crossing_placed_pair_unit` | `— → 510,967,000` | new | `— → 162,782` | `— → 94` | `— → 80` | C | -| Actors | `crossing_placed_maximum_unit` | `— → 712,602,000` | new | `— → 162,782` | `— → 108` | `— → 84` | C | -| Actors | `crossing_placed_non_tail_emptied_unit` | `— → 601,343,000` | new | `— → 81,886` | `— → 78` | `— → 117` | C | -| Actors | `crossing_placed_non_tail_trimmed_unit` | `— → 603,089,000` | new | `— → 81,886` | `— → 78` | `— → 119` | C | -| Actors | `crossing_skip_unit` | `— → 165,038,000` | new | `— → 81,886` | `— → 40` | `— → 2` | C | -| Actors | `crossing_skip_pair_unit` | `— → 70,960,000` | new | `— → 6,112` | `— → 10` | `— → 2` | C | -| Actors | `crossing_actor_unit` | `594,359,000 → 631,585,000` | +6.26% | `162,782 → 162,782` | `91 → 94` | `83 → 85` | C | -| Actors | `transaction_extension_ingress_base` | `13,479,000 → 13,619,000` | +1.04% | `6,052 → 6,052` | `2 → 2` | `0 → 0` | C | -| Actors | `transaction_extension_ingress_notify` | `159,939,000 → 153,584,000` | -3.97% | `12,200 → 12,200` | `10 → 10` | `6 → 6` | C | -| Actors | `funding_snapshot_open` | `13,339,290 + 125,929·a → 11,872,324 + 105,543·a` | -11.00% | `3,751 → 3,767` | `1 → 1` | `1 → 1` | C | +| Actors | `create_user_actor` | `193,393,000 → 535,342,000` | +176.82% | `12,200 → 81,886` | `25 → 65` | `19 → 60` | C | +| Actors | `create_user_actor_at_slot` | `145,691,000 → 537,716,000` | +269.08% | `12,200 → 81,886` | `16 → 65` | `10 → 60` | C | +| Actors | `create_system_actor` | `103,087,000 → 595,895,000` | +478.05% | `12,200 → 81,886` | `16 → 63` | `11 → 58` | C | +| Actors | `create_system_actor_at_sovereign_id` | `91,563,000 → 561,113,000` | +512.82% | `12,200 → 81,886` | `14 → 61` | `9 → 56` | C | +| Actors | `create_user_actor_crossing_new_page` | `— → 562,790,000` | new | `— → 53,350` | `— → 33` | `— → 28` | C | +| Actors | `create_dormant_system_actor` | `67,817,000 → 73,334,000` | +8.14% | `12,200 → 5,736` | `12 → 13` | `7 → 7` | C | +| Actors | `activate_actor` | `124,249,000 → 561,114,000` | +351.60% | `12,200 → 81,886` | `19 → 57` | `14 → 52` | C | +| Actors | `deactivate_actor` | `103,018,000 → 594,010,000` | +476.61% | `12,200 → 81,886` | `9 → 57` | `6 → 55` | C | +| Actors | `pause_actor` | `62,020,000 → 75,290,000` | +21.40% | `12,200 → 5,736` | `7 → 7` | `2 → 2` | C | +| Actors | `resume_actor` | `61,112,000 → 71,658,000` | +17.26% | `12,200 → 5,736` | `7 → 7` | `2 → 2` | C | +| Actors | `manual_trigger` | `84,719,000 → 162,733,000` | +92.09% | `12,200 → 9,635` | `12 → 14` | `5 → 7` | C | +| Actors | `address_event_trigger_occurrence` | `— → 178,307,000` | new | `— → 8,367` | `— → 14` | `— → 7` | C | +| Actors | `pipeline_admission_apoptosis` | `— → 147,996,000` | new | `— → 5,736` | `— → 15` | `— → 15` | C | +| Actors | `close_actor` | `210,365,000 → 742,564,000` | +252.99% | `12,200 → 81,886` | `24 → 64` | `23 → 64` | C | +| Actors | `update_contract` | `274,480,000 → 1,055,528,000` | +284.56% | `12,200 → 81,886` | `25 → 67` | `22 → 63` | C | +| Actors | `set_global_circuit_breaker` | `7,124,000 → 7,194,000` | +0.98% | `0 → 0` | `0 → 0` | `1 → 1` | C | +| Actors | `record_crossing_worker_fault` | `— → 11,454,000` | new | `— → 1,529` | `— → 1` | `— → 1` | C | +| Actors | `record_observation_fanout_worker_fault` | `— → 22,000,000` | new | `— → 4,106` | `— → 3` | `— → 1` | C | +| Actors | `record_wakeup_worker_fault` | `— → 10,337,000` | new | `— → 1,503` | `— → 1` | `— → 1` | C | +| Actors | `clear_crossing_worker_fault` | `— → 14,388,000` | new | `— → 1,529` | `— → 1` | `— → 1` | C | +| Actors | `clear_observation_fanout_worker_fault` | `— → 14,178,000` | new | `— → 1,629` | `— → 1` | `— → 1` | C | +| Actors | `clear_wakeup_worker_fault` | `— → 12,781,000` | new | `— → 1,503` | `— → 1` | `— → 1` | C | +| Actors | `set_active_actor_limit` | `9,289,000 → 10,127,000` | +9.02% | `1,489 → 1,489` | `2 → 2` | `0 → 0` | C | +| Actors | `permissionless_sweep` | `44,350,000 → 50,776,000` | +14.49% | `12,200 → 5,736` | `7 → 7` | `0 → 0` | C | +| Actors | `permissionless_sweep_many` | `23,891,147 + 66,286,454·n → 24,823,652 + 141,011,511·n` | +3.90% | `1,489 + 11,210·n → 1,489 + 4,746·n` | `3 + 11·n → 3 + 14·n` | `2 + 7·n → 2 + 14·n` | C | +| Actors | `fee_collection` | `43,791,000 → 44,070,000` | +0.64% | `3,593 → 3,593` | `1 → 1` | `1 → 1` | O | +| Actors | `task_transfer` | `232,854,000 → 350,609,000` | +50.57% | `12,200 → 18,280` | `18 → 21` | `8 → 8` | C | +| Actors | `task_burn` | `19,067,000 → 19,277,000` | +1.10% | `3,593 → 3,593` | `1 → 1` | `1 → 1` | C | +| Actors | `task_mint` | `197,794,000 → 298,576,000` | +50.95% | `12,200 → 18,280` | `16 → 18` | `6 → 6` | C | +| Actors | `predicate_set_evaluation` | `7,612,000 + 6,444,212·c → 7,612,000 + 6,549,402·c` | 0.00% | `3,675 + 674·c → 3,675 + 674·c` | `1 + 1·c → 1 + 1·c` | `0 → 0` | C | +| Actors | `task_stop_cycle` | `4,260,000 → 4,261,000` | +0.02% | `0 → 0` | `0 → 0` | `0 → 0` | C | +| Actors | `task_split_transfer` | `80,834,694 + 154,149,994·l → 159,307,195 + 200,071,385·l` | +97.08% | `8,040 + 11,210·l → 18,280 + 4,746·l` | `10 + 7·l → 11 + 9·l` | `4 + 3·l → 4 + 3·l` | C | +| Actors | `xcm_asset_deposit` | `218,117,000 → 335,314,000` | +53.73% | `12,200 → 18,280` | `19 → 21` | `8 → 8` | C | +| Actors | `task_add_liquidity` | `271,337,000 → 273,224,000` | +0.70% | `34,255 → 34,255` | `16 → 16` | `15 → 15` | C | +| Actors | `task_donate_liquidity` | `164,618,000 → 165,317,000` | +0.42% | `14,035 → 14,035` | `9 → 9` | `8 → 8` | C | +| Actors | `task_remove_liquidity` | `155,050,000 → 158,472,000` | +2.21% | `8,817 → 8,817` | `8 → 8` | `6 → 6` | C | +| Actors | `task_stake` | `85,906,000 → 87,722,000` | +2.11% | `8,817 → 8,817` | `7 → 7` | `7 → 7` | C | +| Actors | `task_unstake` | `101,691,000 → 102,389,000` | +0.69% | `8,817 → 8,817` | `7 → 7` | `7 → 7` | C | +| Actors | `task_dex_exact_in` | `509,640,000 → 577,736,000` | +13.36% | `19,253 → 19,253` | `40 → 41` | `17 → 17` | O | +| Actors | `task_dex_exact_out` | `502,865,000 → 563,419,000` | +12.04% | `19,253 → 19,253` | `39 → 40` | `17 → 17` | O | +| Actors | `scheduler_on_initialize_cutoff` | `— → 10,895,000` | new | `— → 1,560` | `— → 2` | `— → 2` | C | +| Actors | `scheduler_on_idle_base` | `14,737,000 → 17,042,000` | +15.64% | `1,543 → 1,560` | `6 → 7` | `1 → 2` | C | +| Actors | `materialization_coordinator_base` | `— → 26,889,000` | new | `— → 5,982` | `— → 10` | `— → 1` | C | +| Actors | `contract_geometry_create` | `— → 30,032,003 + 5,254,905·c` | new | `— → 4,494 + 2,475·c` | `— → 3 + 1·c` | `— → 2 + 1·c` | C | +| Actors | `contract_geometry_close` | `— → 28,059,784 + 7,044,916·c` | new | `— → 4,558 + 2,669·c` | `— → 2 + 1·c` | `— → 3 + 1·c` | C | +| Actors | `contract_geometry_reconstruct` | `— → 23,780,966 + 5,510,951·c` | new | `— → 4,557 + 2,670·c` | `— → 2 + 1·c` | `— → 0` | C | +| Actors | `current_step_load_head` | `— → 20,952,000` | new | `— → 4,513` | `— → 2` | `— → 0` | C | +| Actors | `current_step_load_tail` | `— → 26,725,030 + 109,947·s` | new | `— → 4,729 + 14·s` | `— → 3` | `— → 0` | C | +| Actors | `current_step_plan_opening_head` | `— → 44,280,000` | new | `— → 5,223` | `— → 6` | `— → 0` | C | +| Actors | `current_step_plan_suspended_head` | `— → 71,868,000` | new | `— → 8,058` | `— → 7` | `— → 0` | C | +| Actors | `current_step_plan_running_tail` | `— → 77,337,414` | new | `— → 8,238 + 14·s` | `— → 8` | `— → 0` | C | +| Actors | `opening_snapshot_capture` | `— → 2,119,881 + 9,168,258·e` | new | `— → 4,373 + 2,260·e` | `— → 1 + 2·e` | `— → 0` | C | +| Actors | `opening_predicate_capture` | `— → 2,893,842 + 7,596,115·p` | new | `— → 4,235 + 2,525·p` | `— → 1 + 2·p` | `— → 0` | C | +| Actors | `scheduler_actor_state_probe` | `37,715,000 → 69,213,000` | +83.52% | `12,200 → 5,998` | `5 → 7` | `0 → 0` | M | +| Actors | `cycle_orchestration` | `50,426,000 → 54,686,000` | +8.45% | `12,200 → 5,736` | `5 → 5` | `3 → 3` | C | +| Actors | `step_orchestration` | `49,961,778 + 174,432·n → 55,114,428 + 53,460·n` | +10.31% | `12,200 → 5,736` | `5 → 5` | `3 → 3` | C | +| Actors | `scheduler_paged_append_existing_page` | `75,639,000 → 122,783,000` | +62.33% | `7,938 → 14,048` | `10 → 11` | `5 → 5` | C | +| Actors | `scheduler_paged_append_new_page` | `73,335,000 → 117,265,000` | +59.90% | `10,283 → 16,435` | `11 → 12` | `5 → 5` | C | +| Actors | `scheduler_wakeup_append_existing_page` | `78,642,000 → 89,119,000` | +13.32% | `6,694 → 7,566` | `8 → 9` | `3 → 3` | C | +| Actors | `scheduler_wakeup_append_new_page` | `81,995,000 → 104,065,000` | +26.92% | `6,833 → 7,739` | `8 → 9` | `4 → 4` | C | +| Actors | `scheduler_wakeup_replace_exact` | `99,455,000 → 105,811,000` | +6.39% | `7,492 → 8,001` | `10 → 11` | `7 → 7` | C | +| Actors | `scheduler_wakeup_invalidate_middle_page` | `90,097,000 → 127,812,000` | +41.86% | `12,495 → 13,538` | `10 → 11` | `5 → 5` | C | +| Actors | `scheduler_wakeup_drain_partial_page` | `354,451,000 → 460,611,000` | +29.95% | `47,750 → 53,971` | `83 → 99` | `18 → 18` | C | +| Actors | `scheduler_wakeup_drain_full_page` | `648,137,000 → 838,458,000` | +29.36% | `90,034 → 101,637` | `164 → 196` | `36 → 36` | C | +| Actors | `scheduler_wakeup_drain_dense_boundary` | `673,421,000 → 884,763,000` | +31.38% | `92,825 → 104,784` | `170 → 203` | `38 → 38` | C | +| Actors | `scheduler_wakeup_drain_stale_page` | `569,704,000 → 771,689,000` | +35.45% | `89,426 → 101,029` | `164 → 196` | `4 → 4` | C | +| Actors | `scheduler_wakeup_cursor_insert` | `346,837,000 → 350,120,000` | +0.95% | `42,733 → 42,706` | `25 → 25` | `25 → 25` | C | +| Actors | `scheduler_wakeup_cursor_pop_min` | `452,230,000 → 455,372,000` | +0.69% | `55,259 → 55,232` | `34 → 34` | `26 → 26` | C | +| Actors | `scheduler_wakeup_cursor_remove_exact` | `422,267,000 → 439,518,000` | +4.09% | `54,726 → 54,699` | `33 → 33` | `25 → 25` | C | +| Actors | `scheduler_wakeup_cursor_worker_partial` | `112,517,000 → 129,837,000` | +15.39% | `7,498 → 7,976` | `14 → 16` | `8 → 8` | C | +| Actors | `at_time_trigger_occurrence` | `— → 213,647,000` | new | `— → 8,552` | `— → 20` | `— → 9` | C | +| Actors | `cadenced_trigger_occurrence` | `— → 243,471,000` | new | `— → 8,505` | `— → 22` | `— → 11` | C | +| Actors | `scheduler_wakeup_cursor_worker_remove` | `548,262,000 → 583,324,000` | +6.40% | `56,434 → 56,912` | `48 → 50` | `33 → 33` | C | +| Actors | `scheduler_wakeup_cursor_worker_future` | `20,184,000 → 25,422,000` | +25.95% | `6,523 → 6,608` | `5 → 6` | `0 → 0` | C | +| Actors | `scheduler_paged_consume_preserve_page` | `48,540,000 → 58,179,000` | +19.86% | `4,868 → 5,528` | `9 → 10` | `3 → 3` | C | +| Actors | `scheduler_paged_consume_delete_page` | `50,007,000 → 60,553,000` | +21.09% | `4,846 → 5,428` | `9 → 10` | `5 → 5` | C | +| Actors | `scheduler_paged_tombstone_drain` | `43,931,000 + 9,481,801·n → 34,433,000 + 4,050,747·n` | -21.62% | `4,049 + 2,492·n → 3,778 + 2,572·n` | `5 + 5·n → 5 + 2·n` | `4 → 4` | C | +| Actors | `scheduler_paged_mixed_scan` | `47,004,000 + 40,911,571·n → 37,785,000 + 49,051,039·n` | -19.61% | `4,857 + 2,608·n → 5,121 + 2,866·n` | `5 + 5·n → 3 + 4·n` | `3 + 1·n → 3 + 1·n` | C | +| Actors | `scheduler_inner_zero_step_complete` | `— → 65,302,000` | new | `— → 5,537` | `— → 7` | `— → 3` | C | +| Actors | `scheduler_paged_execute_opening_max` | `— → 602,530,000` | new | `— → 27,824` | `— → 30` | `— → 15` | C | +| Actors | `scheduler_inner_opening_close_min` | `— → 166,255,626 + 14,639,590·t` | new | `— → 6,975 + 2,670·t` | `— → 14 + 1·t` | `— → 13 + 1·t` | C | +| Actors | `scheduler_inner_opening_failed_min` | `— → 90,667,173 + 6,290,776·t` | new | `— → 6,729 + 2,670·t` | `— → 6 + 1·t` | `— → 3` | C | +| Actors | `scheduler_inner_opening_retry_min` | `— → 144,484,373 + 6,879,240·t` | new | `— → 6,780 + 2,669·t` | `— → 11 + 1·t` | `— → 8` | C | +| Actors | `scheduler_inner_opening_failed_max` | `— → 134,958,006 + 133,022,701·t` | new | `— → 8,347 + 22,129·t` | `— → 9 + 17·t` | `— → 3` | C | +| Actors | `scheduler_inner_opening_retry_max` | `— → 175,843,602 + 135,539,780·t` | new | `— → 8,179 + 22,125·t` | `— → 13 + 17·t` | `— → 8` | C | +| Actors | `scheduler_inner_opening_complete_min` | `— → 90,437,834 + 6,413,765·t` | new | `— → 6,694 + 2,670·t` | `— → 6 + 1·t` | `— → 3` | C | +| Actors | `scheduler_inner_opening_progress_min` | `— → 160,687,415 + 6,143,902·t` | new | `— → 7,045 + 2,670·t` | `— → 13 + 1·t` | `— → 8` | C | +| Actors | `scheduler_inner_opening_close_max` | `— → 222,786,976 + 158,797,600·t` | new | `— → 8,593 + 22,129·t` | `— → 17 + 17·t` | `— → 13 + 1·t` | C | +| Actors | `scheduler_inner_opening_complete_max` | `— → 129,491,462 + 133,519,432·t` | new | `— → 8,312 + 22,129·t` | `— → 9 + 17·t` | `— → 3` | C | +| Actors | `scheduler_inner_opening_progress_max` | `— → 207,036,514 + 137,351,321·t` | new | `— → 8,640 + 22,090·t` | `— → 15 + 17·t` | `— → 8` | C | +| Actors | `scheduler_inner_running_complete` | `— → 101,569,308 + 2,184,815·s + 10,818,953·p` | new | `— → 7,016 + 2,718·p + 49·s` | `— → 9 + 2·p` | `— → 4` | C | +| Actors | `scheduler_inner_running_progress` | `— → 122,324,329 + 1,383,909·s + 9,153,353·p` | new | `— → 6,762 + 2,693·p + 20·s` | `— → 13 + 2·p` | `— → 6` | C | +| Actors | `scheduler_inner_suspended_tail_retry` | `— → 130,930,125 + 1,548,440·s + 9,050,714·p` | new | `— → 6,244 + 2,693·p + 12·s` | `— → 9 + 2·p` | `— → 6` | C | +| Actors | `scheduler_inner_suspended_tail_complete` | `— → 140,693,328 + 1,409,965·s + 10,195,287·p` | new | `— → 7,294 + 2,718·p + 26·s` | `— → 10 + 2·p` | `— → 4` | C | +| Actors | `scheduler_inner_suspended_tail_progress` | `— → 147,572,410 + 2,290,885·s + 9,300,962·p` | new | `— → 6,764 + 2,693·p + 20·s` | `— → 13 + 2·p` | `— → 6` | C | +| Actors | `scheduler_inner_suspended_head_retry` | `— → 104,441,245 + 629,188·n + 77,615·r + 606,658·f + 8,633,349·p` | new | `— → 5,133 + 21·f + 22·n + 2,693·p + 2·r` | `— → 9 + 2·p` | `— → 6` | C | +| Actors | `scheduler_inner_suspended_head_complete` | `— → 106,659,615 + 563,608·n + 56,231·r + 653,361·f + 8,866,318·p` | new | `— → 6,038 + 21·f + 22·n + 2,716·p + 2·r` | `— → 9 + 2·p` | `— → 4` | C | +| Actors | `scheduler_inner_suspended_head_progress` | `— → 118,701,874 + 788,283·n + 89,582·r + 711,606·f + 8,289,022·p` | new | `— → 5,721 + 21·f + 22·n + 2,693·p + 2·r` | `— → 12 + 2·p` | `— → 6` | C | +| Actors | `scheduler_inner_suspended_head_opening_retry` | `— → 124,942,472 + 757,648·n + 123,257·r + 603,701·f` | new | `— → 11,197 + 21·f + 22·n + 2·r` | `— → 15` | `— → 6` | C | +| Actors | `scheduler_inner_suspended_head_opening_complete` | `— → 141,660,101 + 560,255·n + 45,927·r + 527,155·f` | new | `— → 11,985 + 21·f + 22·n + 2·r` | `— → 14` | `— → 4` | C | +| Actors | `scheduler_inner_suspended_head_opening_progress` | `— → 140,648,325 + 798,378·n + 102,327·r + 840,487·f` | new | `— → 11,578 + 21·f + 22·n + 2·r` | `— → 17` | `— → 6` | C | +| Actors | `scheduler_paged_execute_cheap` | `116,986,000 + 94,182,240·n → 136,961,000 + 126,316,005·n` | +17.07% | `3,716 + 2,733·n → 4,210 + 3,171·n` | `6 + 5·n → 6 + 7·n` | `4 + 3·n → 4 + 3·n` | C | +| Actors | `scheduler_paged_execute_cheap_mixed` | `244,449,000 + 120,287,892·n → 368,768,000 + 192,451,310·n` | +50.86% | `4,918 + 2,798·n → 4,772 + 3,361·n` | `6 + 6·n → 5 + 11·n` | `4 + 4·n → 4 + 5·n` | C | +| Actors | `run_progress` | `— → 106,160,000` | new | `— → 8,567` | `— → 9` | `— → 2` | C | +| Actors | `run_suspend` | `— → 104,554,000` | new | `— → 8,376` | `— → 8` | `— → 2` | C | +| Actors | `run_retry` | `— → 53,639,000` | new | `— → 5,589` | `— → 2` | `— → 1` | C | +| Actors | `run_complete` | `— → 77,246,000` | new | `— → 7,276` | `— → 8` | `— → 4` | C | +| Actors | `run_cancel` | `— → 301,510,000` | new | `— → 9,635` | `— → 19` | `— → 12` | C | +| Actors | `run_suffix_admission` | `— → 1,433,649 + 596·n` | new | `— → 0` | `— → 0` | `— → 0` | C | +| Actors | `observation_change_trigger_occurrence` | `— → 120,199,000` | new | `— → 8,296` | `— → 14` | `— → 8` | C | +| Actors | `observation_change_ingress` | `33,384,000 → 36,178,000` | +8.37% | `6,128 → 6,184` | `5 → 5` | `4 → 4` | C | +| Actors | `observation_fanout_base` | `6,146,000 → 6,565,000` | +6.82% | `1,543 → 1,629` | `1 → 2` | `0 → 0` | C | +| Actors | `observation_fanout_branch_probe` | `— → 13,759,000` | new | `— → 3,587` | `— → 2` | `— → 0` | C | +| Actors | `observation_fanout_page` | `2,644,513,000 → 4,738,388,000` | +79.18% | `718,430 → 304,734` | `332 → 461` | `72 → 201` | C | +| Actors | `observation_fanout_wakeup_page` | `— → 5,132,089,000` | new | `— → 304,734` | `— → 457` | `— → 200` | C | +| Actors | `observation_fanout_coalesced_page` | `— → 2,008,178,000` | new | `— → 304,734` | `— → 389` | `— → 2` | C | +| Actors | `observation_fanout_terminal` | `— → 239,000,000` | new | `— → 5,736` | `— → 27` | `— → 25` | C | +| Actors | `observation_fanout_blocked_page` | `— → 121,065,020,000` | new | `— → 304,734` | `— → 591` | `— → 200` | C | +| Actors | `crossing_worker_base` | `6,146,000 → 7,543,000` | +22.73% | `1,543 → 1,543` | `1 → 2` | `0 → 0` | C | +| Actors | `crossing_work_probe` | `— → 65,373,000` | new | `— → 11,729` | `— → 12` | `— → 0` | C | +| Actors | `observation_crossing_trigger_occurrence` | `— → 530,453,000` | new | `— → 164,204` | `— → 90` | `— → 81` | C | +| Actors | `crossing_search_probe` | `— → 130,256,000` | new | `— → 81,886` | `— → 33` | `— → 0` | C | +| Actors | `crossing_fire_probe` | `— → 70,052,000` | new | `— → 11,729` | `— → 14` | `— → 0` | C | +| Actors | `crossing_fire_pair_probe` | `— → 219,095,000` | new | `— → 11,729` | `— → 25` | `— → 0` | C | +| Actors | `crossing_tail_refill_probe` | `— → 21,162,000` | new | `— → 11,729` | `— → 1` | `— → 0` | C | +| Actors | `crossing_fire_cohort_preflight` | `— → 43,303,000 + 25,902,326·c` | new | `— → 1,493 + 2,699·c` | `— → 2 + 7·c` | `— → 0` | C | +| Actors | `crossing_coalesced_cohort_preflight` | `— → 47,283,000 + 25,665,150·c` | new | `— → 1,493 + 2,699·c` | `— → 2 + 7·c` | `— → 0` | C | +| Actors | `crossing_terminal_cohort_preflight` | `— → 2,884,470 + 25,208,667·c` | new | `— → 1,493 + 2,699·c` | `— → 2 + 7·c` | `— → 0` | C | +| Actors | `crossing_skip_cohort_preflight` | `— → 39,531,000 + 23,675,640·c` | new | `— → 990 + 2,699·c` | `— → 0 + 7·c` | `— → 0` | C | +| Actors | `crossing_rearm_cohort_preflight` | `— → 41,836,000 + 23,957,644·c` | new | `— → 990 + 2,699·c` | `— → 0 + 7·c` | `— → 0` | C | +| Actors | `crossing_rearm_pair_probe` | `— → 96,523,000` | new | `— → 11,729` | `— → 19` | `— → 0` | C | +| Actors | `crossing_skip_pair_probe` | `— → 92,960,000` | new | `— → 11,729` | `— → 19` | `— → 0` | C | +| Actors | `crossing_transition_unit` | `31,988,000 → 34,781,000` | +8.73% | `6,060 → 6,636` | `5 → 5` | `2 → 2` | C | +| Actors | `crossing_leaf_unit` | `425,689,000 → 583,394,000` | +37.05% | `162,782 → 162,782` | `85 → 90` | `77 → 81` | C | +| Actors | `crossing_page_unit` | `426,457,000 → 580,251,000` | +36.06% | `162,782 → 162,782` | `85 → 90` | `77 → 81` | C | +| Actors | `crossing_rearm_unit` | `— → 430,229,000` | new | `— → 162,782` | `— → 80` | `— → 74` | C | +| Actors | `crossing_rearm_pair_unit` | `— → 483,729,000` | new | `— → 162,782` | `— → 86` | `— → 76` | C | +| Actors | `crossing_coalesced_unit` | `— → 454,674,000` | new | `— → 162,782` | `— → 84` | `— → 74` | C | +| Actors | `crossing_coalesced_pair_unit` | `— → 546,238,000` | new | `— → 162,782` | `— → 92` | `— → 76` | C | +| Actors | `crossing_placed_unit` | `— → 574,663,000` | new | `— → 162,782` | `— → 90` | `— → 81` | C | +| Actors | `crossing_placed_pair_unit` | `— → 611,261,000` | new | `— → 162,782` | `— → 97` | `— → 85` | C | +| Actors | `crossing_placed_maximum_unit` | `— → 10,442,486,000` | new | `— → 346,462` | `— → 982` | `— → 466` | C | +| Actors | `crossing_placed_non_tail_emptied_unit` | `— → 4,321,498,000` | new | `— → 327,262` | `— → 563` | `— → 367` | C | +| Actors | `crossing_placed_non_tail_trimmed_unit` | `— → 4,300,965,000` | new | `— → 327,262` | `— → 563` | `— → 369` | C | +| Actors | `crossing_skip_unit` | `— → 175,584,000` | new | `— → 81,886` | `— → 41` | `— → 2` | C | +| Actors | `crossing_skip_pair_unit` | `— → 80,388,000` | new | `— → 11,729` | `— → 12` | `— → 2` | C | +| Actors | `crossing_actor_unit` | `594,359,000 → 757,091,000` | +27.38% | `162,782 → 162,782` | `91 → 97` | `83 → 91` | C | +| Actors | `transaction_extension_ingress_base` | `13,479,000 → 13,759,000` | +2.08% | `6,052 → 6,052` | `2 → 2` | `0 → 0` | C | +| Actors | `transaction_extension_ingress_notify` | `159,939,000 → 303,955,000` | +90.04% | `12,200 → 5,998` | `10 → 13` | `6 → 6` | C | +| Actors | `funding_snapshot_open` | `13,339,290 + 125,929·a → 13,311,775 + 127,773·a` | -0.21% | `3,751 → 4,531` | `1 → 1` | `1 → 1` | C | +| Actors | `maximum_context_inherent` | `— → 196,901,501,000` | new | `— → 3,517` | `— → 13` | `— → 23` | C | +| Actors | `block_resource_finalize` | `— → 8,590,000` | new | `— → 1,560` | `— → 1` | `— → 2` | C | +| Actors | `block_resource_meter_extension` | `— → 10,826,000` | new | `— → 1,560` | `— → 1` | `— → 1` | C | +| Actors | `maximum_xcm_version_discovery` | `— → 456,839,000` | new | `— → 248,490` | `— → 102` | `— → 1` | C | | Oracle | `register_feed_existing_producer` | `132,142,000 → 146,111,000` | +10.57% | `20,532 → 20,532` | `3 → 3` | `3 → 3` | C | | Oracle | `register_feed_new_producer` | `194,720,000 → 215,185,000` | +10.51% | `44,394 → 44,394` | `4 → 4` | `4 → 4` | C | | Oracle | `pause_feed` | `15,365,000 → 15,366,000` | +0.01% | `3,551 → 3,551` | `1 → 1` | `1 → 1` | C | @@ -124,11 +179,13 @@ Interpretation codes classify changed paths only: `I` identity guard; `C` correc | Oracle | `publish_ema_changed_combined` | `— → 58,667,000` | new | `— → 6,060` | `— → 10` | `— → 7` | C | | Oracle | `publish_ema_changed_secondary_capacity` | `— → 45,188,000` | new | `— → 6,060` | `— → 8` | `— → 0` | C | | Oracle | `publish_ema_refresh` | `21,931,000 → 22,280,000` | +1.59% | `3,551 → 3,551` | `2 → 2` | `1 → 1` | C | -| Router | `direct_xyk_exact_input` | `294,176,000 → 343,205,000` | +16.67% | `12,200 → 12,200` | `25 → 30` | `12 → 18` | C | -| Router | `direct_mint_exact_input` | `315,129,000 → 321,275,000` | +1.95% | `23,410 → 23,410` | `33 → 33` | `14 → 14` | C | -| Router | `native_anchored_exact_input` | `435,468,000 → 500,770,000` | +15.00% | `19,253 → 19,253` | `36 → 44` | `17 → 27` | C | -| Router | `direct_xyk_exact_output` | `164,898,000 → 332,170,000` | +101.44% | `6,208 → 12,200` | `10 → 29` | `5 → 18` | C | -| Router | `native_anchored_exact_output` | `302,348,000 → 506,427,000` | +67.50% | `16,644 → 19,253` | `21 → 43` | `10 → 27` | C | +| Router | `direct_xyk_exact_input` | `294,176,000 → 389,092,000` | +32.27% | `12,200 → 9,635` | `25 → 31` | `12 → 18` | C | +| Router | `direct_mint_exact_input` | `315,129,000 → 390,349,000` | +23.87% | `23,410 → 21,862` | `33 → 34` | `14 → 14` | C | +| Router | `native_anchored_exact_input` | `435,468,000 → 562,930,000` | +29.27% | `19,253 → 19,253` | `36 → 45` | `17 → 27` | C | +| Router | `direct_xyk_exact_output` | `164,898,000 → 387,486,000` | +134.99% | `6,208 → 9,635` | `10 → 30` | `5 → 18` | C | +| Router | `native_anchored_exact_output` | `302,348,000 → 556,714,000` | +84.13% | `16,644 → 19,253` | `21 → 44` | `10 → 27` | C | +| Router | `create_pool` | `— → 144,923,000` | new | `— → 34,255` | `— → 13` | `— → 10` | C | +| TMC | `mint_with_distribution` | `164,130,000 → 164,758,000` | +0.38% | `12,200 → 6,208` | `11 → 11` | `4 → 4` | C | ## Interpretation @@ -136,7 +193,11 @@ Every listed dimension requires review against the owning implementation and ben ## Retired Weight Owners -- None. +- Actors `continuation_suspend` +- Actors `continuation_retry` +- Actors `continuation_complete` +- Actors `continuation_cancel` +- Actors `continuation_suffix_admission` Any retired owner requires implementation review before release acceptance; absence from the candidate alone does not prove safe replacement. @@ -144,7 +205,7 @@ Any retired owner requires implementation review before release acceptance; abse - Regenerate: `./.agents/skills/release-assurance/scripts/weight-delta-ledger.sh` - Verify freshness: `./.agents/skills/release-assurance/scripts/weight-delta-ledger.sh --check` -- Reproduce production weights through `./scripts/benchmarks.sh` and the owning Benchmarking Skill; focused outputs do not replace complete generated pallet files. +- Reproduce production weights through `./scripts/benchmarks.sh` and the owning Architecture Experiments Skill; focused outputs do not replace complete generated pallet files. -Candidate weight source identity: `7643a3d9d80ef9239dcdc55e291b836f51bcba4338f86f1c41848cbc069ea3ce`. +Candidate weight source identity: `e3615c4896619eabba068ba980988810f7c2ec6ebaa893f3539a9919a1bfa3c9`. diff --git a/.agents/skills/release-assurance/references/runtime-threat-boundaries.md b/.agents/skills/release-assurance/references/runtime-threat-boundaries.md index b04fcc57..40a325d3 100644 --- a/.agents/skills/release-assurance/references/runtime-threat-boundaries.md +++ b/.agents/skills/release-assurance/references/runtime-threat-boundaries.md @@ -13,10 +13,13 @@ The protected outcome is deterministic bounded protocol behavior under malformed | Runtime origin | Root and explicitly typed governance adapters | Ordinary signed accounts and arbitrary runtime calls | Reject implicit privilege; test every privileged origin against a signed caller. Signed Oracle publication remains feed-local and grants no general administration. | | Consensus storage | Canonical bounded partitions and indexes | Partial, corrupt, stale, or contradictory partition combinations | Load through one canonical classifier, return typed failure or conservative absence, and reconcile complete bounded topology in try-state. | | Scheduler service | One FIFO, wakeup owner, ticket allocator, and typed block/tick clocks | Queue pressure, delayed timestamps, heavy heads, stale entries, and index exhaustion | Preserve order and committed progress, bound every scan and retry, reselect mixed clocks per unit, and terminate unrecoverable index exhaustion explicitly. | +| Actor block phase | Required context plus one payload-free Mandatory Actor Prepass and fixed FIFO cutoff | Missing, duplicate, stale, or post-extrinsic prepass; current-block readiness; phase replay | Reject invalid ordering before mutation, keep Prepass as the sole pre-external Actor owner, retain `N -> N+1-or-later`, and require one finalizable phase with no outstanding reservation. | +| Actor resource meter | Generated component-wise Actor Control and Task-effect maxima with checked actual settlement | RefTime/ProofSize fragmentation, stale state, overflow, over-reported actual Weight, or cross-domain borrowing | Reserve maxima before semantic mutation, settle only valid actual Weight, keep Actor Control at or below one third, and halt fail-closed rather than publishing inconsistent usage. | | Actor adapter call | Typed task input, task-owned spend/output bound, and measured host adapter | DEX, staking, asset, Oracle, or hook rejection after partial work | Keep task-local effects transactional; preserve committed plan prefixes; classify temporary and permanent failures without compensation or whole-plan rollback. | | Certified movement | Producer-declared movement protocol and preflight owner | Reordered notify, post-dispatch failure, XCM holding failure, or duplicate delivery | Admit only the closed certified protocol inventory; bind consequence, rollback, and Weight ownership; never infer movement by scanning events. | | Arithmetic | Checked or widened authoritative calculation | Overflow, narrowing loss, denominator failure, terminal horizons, and adversarial reserve magnitudes | Use checked operations, `U256` where products can exceed native widths, and exact narrowing. Saturation is limited to explicit semantic floors, conservative Weight caps, or bounded telemetry. | | Price reference | Fresh typed Oracle observation, then a direct-pool reserve fallback | Stale or missing feeds, manipulated pools, self-certifying execution quotes, and MEV | Keep the execution quote out of its own reference path, reject excessive deviation before mutation, and make no external fair-price or ordering guarantee. | +| Asset Conversion lifecycle | Canonical physical pair locator, Router call filter, transactional lifecycle, and patched full-balance reserve engine | Semantic aliases, same-ledger pools, raw fee bypass, partial Pool/LP/Oracle topology, protected-balance omission, and dependency substitution | Reject aliases at the lowest pool boundary; expose swaps and creation only through Router; commit complete topology atomically; validate both topology directions; and pin one reviewed upstream implementation. | | Governance service | Chronological bounded epoch buckets and typed payload authority | Delayed clocks, maximum-density buckets, malformed tallies, and unsupported payload execution | Drain one persisted phase at a time under measured caps, advance epochs only after every family drains, fail arithmetic transactionally, and deny unsupported authority. | | Governance custody | Checked aggregate source custody and maximum retained horizon | Concurrent proposals, replacement votes, double counting, premature unlock, and source-ledger drift | Reuse only actually custodied power, admit only free increments, extend horizons monotonically, and reconcile aggregate custody against host ledgers. | | Runtime custody identity | Host-owned infallible account mappings | Narrow-account truncation, tagged subaccount collision, actor-id aliasing, and signed ownership | Preserve stable host addresses, prove non-aliasing across custody roles, and keep System Actor control under a non-signable pallet account. | @@ -32,12 +35,15 @@ The protected outcome is deterministic bounded protocol behavior under malformed | --- | --- | | A signer acquires Root-equivalent Actors, Governance, Router, Staking, TMC, Asset Registry, asset-force, preimage, XCM, XCMP, collator, or upgrade authority. | Runtime authority inventory in `template/runtime/src/tests/integrity_tests.rs` and typed payload coverage in `governance_integration_tests.rs`. | | A malformed Actor partition executes, consumes placement, reports ready, or accepts Continuation mutation as though active. | Canonical-loader, partition-mask, scheduler, projection, and try-state tests under `template/pallets/actors/src/tests/`. | +| A block omits, duplicates, reorders, or replays Mandatory Actor Prepass; executes current-block readiness; exceeds Actor Control; or settles inconsistent actual Weight. | Actors package phase/resource tests and runtime production full-block, transaction-extension, prepass-ordering, cutoff, component-fragmentation, and finalization integration tests. | | Continuation cancellation re-primes a retained signal behind its old physical wakeup, creating two conflicting pointer claims and a permanent due-head stall. | Exact wakeup invalidation tests in the Actors package and independent DEX embedding fixture, plus the measured `continuation_cancel` branch. | | A failed task, certified movement, XCM deposit, Router call, pool index, TMC distribution, staking operation, or Governance terminal action retains a partial root. | Package and runtime exact-root rollback tests owned by the affected subsystem. | | Delayed Governance epoch work skips chronology, drops a same-epoch suffix, advances early, or exceeds its measured per-block family cap. | Governance epoch-service unit tests, runtime maximum-composition assertions, and generated Governance weights. | | Concurrent Governance proposals count more voting power than source custody or release it before the maximum horizon. | Governance aggregate-custody tests and runtime host-ledger reconciliation. | | Native `Here` enters the foreign registry, one XCM program exceeds the holding bound, or teleport/arbitrary call execution becomes reachable. | Asset Registry reserved-location tests and runtime XCM identity, holding, filter, ingress, and rollback tests. | | A Router System swap certifies itself, accepts an out-of-bound reference deviation, or mutates before reference rejection. | Router package tests and runtime Actors/Router integration tests. | +| A semantic alias creates a same-ledger pool, a raw XYK call bypasses Router fees, or failed creation leaves any Pool, LP binding, asset, account, event, or Oracle feed. | Shared-primitives canonicality tests, runtime call-filter tests, exact state-root lifecycle rollback tests, topology-corruption tests, and `scripts/audit-asset-conversion-boundaries.sh`. | +| Quote, liquidity, and execution disagree because protected pool balances are omitted, or more than one Asset Conversion implementation is reachable. | Vendored upstream reserve regressions, runtime quote/execution and removal parity tests, Cargo tree uniqueness review, and `template/vendor/pallet-asset-conversion/DEOS-PATCH.md`. | | An unknown runtime eligibility, close reason, execution phase, or typed failure reaches browser display as a valid state. | `web-client/scripts/test-actors-eligibility.mjs` and generated Actors ABI freshness checks. | | A production path reaches a package fallback Weight, exceeds RefTime or ProofSize admission, or changes database cardinality without ledger evidence. | Runtime Weight tests, production generated files, `scripts/benchmarks.sh`, and this Skill's Weight delta evidence. | | A reachable panic or unreviewed dependency/tool substitution enters the candidate tree. | Alignment panic audit, this Skill's dependency-provenance review, project bootstrap identity checks, and the completion gate. | diff --git a/.agents/skills/release-assurance/scripts/generate-weight-delta-ledger.mjs b/.agents/skills/release-assurance/scripts/generate-weight-delta-ledger.mjs index 25558a68..3d44a217 100755 --- a/.agents/skills/release-assurance/scripts/generate-weight-delta-ledger.mjs +++ b/.agents/skills/release-assurance/scripts/generate-weight-delta-ledger.mjs @@ -180,7 +180,7 @@ lines.push( '', '- Regenerate: `./.agents/skills/release-assurance/scripts/weight-delta-ledger.sh`', '- Verify freshness: `./.agents/skills/release-assurance/scripts/weight-delta-ledger.sh --check`', - '- Reproduce production weights through `./scripts/benchmarks.sh` and the owning Benchmarking Skill; focused outputs do not replace complete generated pallet files.', + '- Reproduce production weights through `./scripts/benchmarks.sh` and the owning Architecture Experiments Skill; focused outputs do not replace complete generated pallet files.', '', `Candidate weight source identity: \`${createHash('sha256').update(sourceHashes.join('\n')).digest('hex')}\`.`, '', diff --git a/AGENTS.md b/AGENTS.md index e96e991c..60db9b9d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -100,7 +100,12 @@ - `Actors Progress Preservation`: Keep `RetryLater` Mutable-only and Temporary-only, with one sparse scalar-cursor Continuation on the canonical FIFO/wakeup substrate; preserve committed prefixes without compensation, whole-plan rollback, duplicate scheduler state, or off-chain correctness dependencies. `template/pallets/actors/docs/specification.en.md` owns the full contract. - `Actors Authority Simplicity`: Keep consensus authority at immutable `ActorType::{User, System}` until a concrete shipped need proves two types insufficient; every actor receives bounded service through one undifferentiated paged FIFO under one scheduler, one global ticket allocator/cutoff, one actor-local ticket, and one wakeup/Continuation/lifecycle owner, where actor class never changes service order, not an Actor-id whitelist, owner-authored priority, a generic policy object, or pre-extrinsic execution. - `Actors Market Loss Bounds`: Every market task carries its owning spend/output bound; generic Actors must not impose an asset-agnostic raw-balance System cap. Every System swap additionally obeys the typed reference-deviation guard, and temporary market rejection uses one capped retry/wakeup path rather than accepting an adverse fill. -- `Actors Observation Reactions`: Derive duplicate-free subscriptions from admitted typed trigger sources into actor-owned reusable slot-addressed pages; DEOS Oracle change context may mark bounded dirty state only, deferred fanout may set the existing pending latch, and the existing scheduler alone executes actors. +- `Actors Observation Reactions`: Derive duplicate-free subscriptions from admitted typed trigger sources into actor-owned reusable slot-addressed pages; DEOS Oracle change context may mark bounded dirty state only, deferred fanout may set the existing pending latch, and the existing scheduler alone executes actors. One shared Trigger instance defines initial causal cohortability independently of Contract length; mixed-length Actors share Step-0 materialization/reaction without loading unreachable tails, then every surviving Actor continues independently under Q1. +- `Actor Fee Boundaries`: Keep Creation Fee, useful-readiness Trigger Fee, per-admitted-cycle complete bounded Pipeline Machine Fee, and pay-as-attempted Action execution fee as four independent owners. A Trigger charges its generated detection/matching/materialization owner only for `pending_signal: false -> true`; while latched, redundant occurrences perform no Actor-specific Trigger evaluation or fee collection and create no additional activation. Later consumption charges complete bounded Pipeline control—including retry, continuation, RunFrame, completion, and minimal-apoptosis work—but never future Action effects. Actor lifetime creates no recurring rent. +- `Trigger Latching`: Once `pending_signal` becomes true, disable or remove the Actor from family-specific detector topology where practical until Pipeline Opening consumes the latch; otherwise reject redundant work before Actor-specific evaluation. Re-arm from current authoritative state, intentionally discard latched-period causal history, and preserve at most one active plus one deferred Pipeline. +- `User Cycle Admission`: Trigger processing and Pipeline admission are separate transitions. One paid `false -> true` Trigger transition may latch readiness while a Pipeline remains Running/Suspended; only Idle consumption tests and charges complete Pipeline Machine plus cleanup capacity and opens one cycle. Running/Suspended Q1 Steps reuse paid machine authority, while each Action attempt independently admits and pays its actual effect. Insufficient Pipeline admission selects custody-neutral minimal apoptosis; the committed useful-readiness Trigger fee remains final. +- `Actor Close and Custody`: Minimal apoptosis and every other close delete process semantics, release lifecycle metadata/slot/state resources, and perform no economic unwind or custody transfer. Deterministic sovereign custody survives close and exact-slot recreation; exit/recovery uses ordinary authored Steps, and `Transfer(AllAvailable)` receives no lifecycle source-exhaustion privilege. +- `Zero-Step and Immutable`: Admit `0..=MaxContractSteps`; a zero-Step activation is a bounded FIFO-ordered Opening/completion control cycle with no Action fee, and `AtTime { after_ticks }` supplies first-class one-shot temporal readiness. User Immutable rejects owner close and every premature mutation/destruction path, while mandatory authored/temporal terminal semantics and insufficient-activation minimal apoptosis remain independently effective. - `Token-Driven Coordination`: Prefer asset movement and runtime hooks over privileged signed calls when token ingress itself defines the event. - `Bounded Consensus State`: Every storage collection, iteration, history surface, retry path, and projection must have a defensible bound. - `Read-Model Honesty`: Public data must be classified as bounded authoritative on-chain truth or externally indexed/materialized truth; canonical UX must not hide an indexer dependency. @@ -162,7 +167,7 @@ - `Liquidity Slippage`: Derive Liquidity Actor swap tolerance from current reserve depth and clamp it between explicit runtime bounds. - `Fee Collection`: Keep DEOS Router trading fees on the Burn Actor path; collect 100% of transaction, Actor-execution, governance-opening, and XCM-execution fees into the Fee Sink System Actor independently of actor execution liveness, and name the generic Actors boundary by collection rather than trading-route semantics. Keep collection ledger-only and let one 120-tick cadence under the 500 ms consensus clock own allocation, so no fee source fabricates a privileged or parallel queue signal. - `Fee Allocation Phases`: Each Fee Sink cycle processes 10% of its current spendable native buffer only when the processed amount can fund at least one ED to every configured allocation leg. While collators remain permissioned, the processed amount splits 50/50 to staking ingress and liquidity provisioning; a future equal-thirds security/staking/liquidity plan requires permissionless collators and an explicit bounded security-reward contract, with the unprocessed buffer and indivisible remainder retained for later cycles. -- `Native Flow Anchors`: The DEOS runtime endows System Actor, custody, and staking-ingress accounts admitted for arbitrarily small native flows with one persistent free-balance ED, preserves that anchor through spend resolution, and converts only newly received value. Generic Actors follows exact host-ledger consequences and does not promise provider-only or reserved-only zero-free sub-ED ingress. +- `Native Flow Anchors`: The DEOS runtime endows System Actor, custody, and staking-ingress accounts admitted for arbitrarily small native flows with one persistent free-balance ED, preserves that anchor through authored spend resolution, and converts only newly received value. Actor close remains balance-neutral and grants no source-exhaustion exception. Generic Actors follows exact host-ledger consequences and does not promise provider-only or reserved-only zero-free sub-ED ingress. - `Collator Reward Gate`: Treat `CollatorRewardPot` as an unresolved design placeholder, not an accepted pallet or storage topology, until eligibility, contribution accounting, settlement cadence, custody, payout, leftovers, and failure behavior have explicit owners and bounds. ## 8. Engineering and Validation diff --git a/BACKLOG.md b/BACKLOG.md index 8d3bcb92..c5999e48 100644 --- a/BACKLOG.md +++ b/BACKLOG.md @@ -4,6 +4,502 @@ > > Pre-`1.0` boundary: no DEOS network will launch before `1.0`. The `0.7.x` line remains fresh-genesis and may change storage, metadata, runtime APIs, validation topology, and release mechanics without deployed-lineage migration or live-network ceremony. -## DEOS 0.7.24 — High-Frequency Actor Efficiency +--- -- [ ] `Actors / Differential Overhead`: Establish paired production benchmarks for an identical bounded Router swap invoked manually and through a one-step price-reactive Actor; report only marginal detection, canonical-probe, materialization/FIFO, interpreter, fee-settlement, completion, and retry overhead after subtracting shared Router/Oracle/AMM work. Use the evidence to minimize generic orchestration cost, amortize shared feed/market reads across bounded cohorts, and evaluate a compiled `ObservationCrossing → bounded SwapOut → Complete/Retry` execution family without weakening FIFO fairness, cutoff, loss bounds, atomicity, or retry semantics. +# DEOS 0.7.24 — Topology Scaling + +> **Release framing:** `Resource-Proportional Actors + Economic Zipper + Block-Paced Execution + Active-Frontier Scaling` +> +> **Dependency:** start from the accepted `v0.7.23` Reactive Topology Closure. Preserve canonical Trigger-state ownership, generation-checked physical authority, transactional topology transitions, strict FIFO, branch-exact Weight, repairable worker faults, checked arithmetic, economic ownership for User Actors, and the fresh-genesis boundary. +> +> **Primary objective:** permit locally rich Actor Contracts with a target ceiling of 32 Steps without making small Actors pay for unused complexity, while proving bounded, starvation-free first traversal of the 10,000-Active-Actor production population under generated two-dimensional Weight. +> +> **Canonical delivery order:** +> `Specification → Implementation → Tests → Implementation Correction → Domain Architecture` +> +> Architecture documents record behavior that has already survived implementation and tests. They are not used to retroactively legitimize code. + +--- + +# Asset Conversion Closure + + +--- + +# Release Laws + +## Semantic Responsibility Law + +```text +Actor boundaries follow semantic responsibility. +Runtime bounds follow physical cost. +``` + +Step count does not decide how many Actors should exist. One Actor may remain locally complex when doing so preserves a coherent: + +```text +responsibility +custody boundary +failure boundary +activation semantics +observability surface +``` + +## Physical Decomposition Law + +```text +Contract headers +Step cells/pages +run state +execution tickets +detector indexes +admission certificates +``` + +are physical topology belonging to one Actor Contract. They are not separate authored Contracts. + +## No Ceiling Tax + +```text +cost(one-step Actor, MaxContractSteps = 8) +≈ +cost(one-step Actor, MaxContractSteps = 32) +``` + +Increasing the protocol ceiling must not add the following to a short Actor: + +```text +cold body reads +unused predicate work +maximum-pipeline orchestration +maximum-pipeline fees +maximum-pipeline ProofSize +``` + +## Block-Paced Execution + +```text +one Actor Cycle +→ at most one committed Step per block +``` + +A successor Step receives: + +```text +eligible_at >= current_block + 1 +``` + +It may execute later because of FIFO pressure or congestion, but never earlier. Physical ChunkSize remains independent of the fixed Q1 ServiceQuantum, and causal CohortSize derives only from one shared Trigger cause. + +## Causal Speed Limit + +Under the authoritative fixed timing model, readiness created by an operation in block `N` may cause an Actor Step no earlier than block `N + 1`; this is an eligibility floor rather than an execution SLA, and ordinary FIFO plus available `on_idle` Weight may delay service further. + +```text +A effect in N +→ readiness B +→ B Step no earlier than N + 1 +``` + +Therefore: + +```text +maximum endogenous propagation += one causal hop per block +``` + +## Step Atomicity + +The following commit atomically: + +```text +preconditions ++ amount resolution ++ one Task effect ++ StepOutcome ++ fee settlement ++ cursor transition +``` + +The full pipeline is explicitly non-atomic. Effects of previously committed Steps remain durable if a later Step fails, is cancelled, or the Actor is closed. + +## Dual-Resource Law + +```text +1/3 Actor Control Plane +2/3 Shared Economic Execution +``` + +The Actor Control Plane pays for: + +```text +detection +materialization +hot-state loading +lazy Step loading +preconditions +amount resolution +FIFO management +run-state bookkeeping +fee bookkeeping +retry/completion +fault handling +``` + +Shared Economic Execution pays for: + +```text +ordinary user extrinsics +Actor Transfer +Actor Swap +Actor Liquidity +Actor Stake/Unstake +Actor Mint/Burn +other Task effects +``` + +## Economic Zipper + +Under continuous demand from both sides, Shared Economic Execution is split symmetrically: + +```text +1/3 Actor base turn +1/3 user base turn +``` + +The split is work-conserving: + +```text +unused Actor capacity → users +unused user capacity → Actors +``` + +Fairness is measured by `Weight`, not by call count. + +## Class-Neutral Service + +```text +User Actor +System Actor +``` + +share: + +```text +one Actor FIFO +one ActorControlMeter +one SharedEconomicMeter +one Step service protocol +``` + +Actor class never influences ordering or resource preference. System Actor fee exemption remains an economic policy, not a scheduler priority. + +## Active-Frontier Law + +```text +IdleTax ≈ fixed bounded work +EventCost ≈ affected discovery frontier +ControlCost ≈ pending materialization frontier +EffectCost ≈ executed Step effects +``` + +Consensus work must not scale with total dormant identity population. + +--- + +# Quantitative Release Contract + +The earlier 100-Steps-per-block planning witness is not a production promise: complete generated control/effect Weight disproves it for the accepted topology. Release acceptance instead requires a starvation-free first traversal of the 10,000-Active-Actor population within the measured 1,300-block horizon, with no claim that the horizon is an execution SLA. + +Every production profile reports independently: + +```text +S = committed Actor Steps / block +A = distinct Actors progressed / block +``` + +For the exact one-Step Q1 witness, `S = A`; mixed or specification-admitted multi-Step profiles MUST preserve both axes and their Pareto frontier rather than relabeling either as the other. + +The production target is: + +```text +MaxContractSteps = 32 +``` + +Production `MaxContractSteps` is fixed at `32`. The `8` and `16` profiles exist only for comparative benchmarks; any proposal to reduce the production ceiling must reopen Phase S and amend the owning specification before implementation or release work continues. + +--- + +# Canonical Work Protocol + +## Stage Gates + +No later stage begins until the previous stage is closed: + +```text +S0 Normative specification accepted +I0 Implementation complete +T0 Tests implemented and executed +F0 Implementation corrected against test evidence +A0 Architecture records final accepted mechanism +``` + +## Reopening Rule + +If a test proves that the specification itself is defective: + +```text +test failure +→ specification change record +→ reopen Specification +→ invalidate affected Implementation/Test/Fix tasks +→ repeat the chain +``` + +A test or architecture document must never be silently changed to match accidental implementation behavior. + +## Correction Rule + +During `Implementation Correction`: + +- implementation is corrected against the accepted specification; +- every consensus-relevant defect gets a minimized regression; +- production Weight is regenerated after final code changes; +- architecture documents remain untouched until the implementation converges. + +--- + +# PHASE I — IMPLEMENTATION + +Implementation follows the accepted canonical Actors, resource-policy, and performance-assurance specifications. A verified specification defect reopens specification convergence before affected implementation continues. Architecture-affecting alternatives use the track-local `/EXP-NNNN` inventories routed by `.agents/skills/architecture-experiments/SKILL.md`; the backlog tracks remaining work, while the Skill-owned Experiment Records retain candidates, measurements, rejected alternatives, decisions, and lineage. + +Before Phase T begins, the Experimental Closure Gate requires every architecture-affecting alternative to be Accepted, Rejected, Inconclusive, or explicitly deferred here with rationale; every accepted benchmark-sensitive choice must have durable evidence and every rejected candidate must remain discoverable. + +--- + +## I4A — Q1 Freshness and Load-Elastic Service Research + + +`Deferred architecture`: Q1 hot-path decomposition and general Q2/Q4 service remain dormant in EXP-0017. Reopen only if final block-resource evidence misses the committed-Step target and controlled decomposition proves current-fragment control/persistence overhead remains binding. Dynamic re-cohorting, Task-type cohorts, Weight/fee-aware ranking, and User/System priority scheduling remain outside 0.7.24. + +--- + +## I5 — Mandatory Actor Prepass Inherent + +The versioned payload-free Mandatory Actor Prepass is implemented with required Timestamp/parachain context, duplicate and ordering rejection, one-way phase/finalization guards, bounded stale cleanup and materialization, immutable FIFO cutoff, Actor base service, and valid actual Weight. + +--- + +## I6 — Per-Block Resource Meters + + +--- + +## I7 — Actor Drain + +The resource-bounded Actor Drain is implemented against the fixed prepass cutoff with strict FIFO, remaining Actor Control and Shared Economic capacity, generated actual Weight, and bounded telemetry. + +--- + +## I9 — ObservationCrossing Scaling + +The shipped coordinator, rotated grants, sparse radix authority, contiguous cohort snapshot, hot-header classification, grouped queue/rearm writes, non-tail refill, locator repair, terminal handling, scalar fallback, bounded faults, and class-neutral service are the retained baseline. + +--- + +## I15 — Benchmark and Assurance Harness + + +--- + +# PHASE T — TESTS + +Tests are written only after the corresponding implementation is complete. + +--- + +## T1 — Block Phase and Economic Zipper Tests + +--- + +## T2 — Block-Paced and Causal Execution Tests + +The retained suites already prove one committed Step per Actor/block, durable prefixes, exact Continue/Retry/Stop/Abort/cancellation behavior, fixed N+1 eligibility, actor-produced causal chains, useful-transition latching, mixed-length initial cohorts, cohort dissolution, and congestion-delayed FIFO progress. + +--- + +## T4 — Opening, Current, and Contract Geometry Tests + +The retained suites already prove immutable Opening balance/observation/predicate/funding snapshots, live Current evaluation, prior-effect and previous-outcome visibility, retry semantics, effect-before-evaluation admission, false-predicate accounting, hot/current-fragment-only execution, C6 commitment/integrity, exact rollback, 32-Step lifecycle dispatchability, ceiling invariance, semantic identity, and runtime admission identity. + +--- + +## T6 — Task Effect Equivalence Tests + +The retained parity matrix covers every Task family, canonical host-state/effect-Weight ownership, Actor-only control overhead, typed Temporary/Permanent policy, rollback, and exactly-once induced consequences. + +--- + +## T7 — FIFO Tests + +The retained suites already prove monotonic tickets, one live ticket, temporal-to-FIFO materialization, separate deferred-cycle readiness, tombstone cleanup, saturation, strict heavy/corrupt heads, index exhaustion, exact-slot recreation, and class-neutral order. + +--- + +## T8 — Detector Scaling Tests + +The retained package/runtime suites already cover aggregate Crossing branches, scalar fallback, exact rollback, occupied broad pages, revision resumption, sparse block/tick wakeups, no catch-up, Manual timing, AddressEvent filtering, useful-transition latching, and producer rollback. + +--- + +## T9 — Economics Tests + +The retained suites cover 32-Step state holds, reserve/release rollback, useful Trigger and complete Pipeline charging, actual Action fees, false-predicate non-invocation, no ceiling fee, protected minima, and System fee exemption without priority. + +--- + +## T10 — Runtime API Tests + +Existing tests retain bounded runtime projections, fail-closed variants, current materialization faults, and generated metadata freshness. + +--- + +## T11 — Model and Property Tests + +The retained pure and state-machine models cover lifecycle, signals, success, Continue, Retry, suspension, retained readiness, updates, close, breaker, exact events/fees/cursors, TryRuntime, and minimized regressions. + +--- + +## T12 — Mandatory Scale Profiles + + +--- + +# PHASE F — IMPLEMENTATION CORRECTION + +Phase F owns only concrete defects exposed by Phase T. Add one minimized, independently closable correction item per verified failure; return specification defects to Phase S and remove each implementation item immediately after its regression and owning validation pass. Speculative fix inventories, evergreen review rules, and duplicate release checklists do not belong in the backlog. + +--- + +# PHASE A — DOMAIN ARCHITECTURE + +Architecture documents are updated only after a frozen green implementation exists. + +If architecture writing reveals a new semantic inconsistency, Phase A stops and the affected domain returns to Phase S. + +Before Phase A closes, the Architecture Provenance Gate requires every significant physical choice to cite a normative specification, Accepted Experiment Record, production benchmark, or correctness/security invariant without copying the research log into architecture prose. + +--- + +## A1 — Actors Domain Architecture + +**Owner:** `template/pallets/actors/docs/architecture.en.md` + +The current document already owns the retained Actor state, C6 geometry, identity, run, Q1, snapshots, latch, FIFO, Weight, rollback, storage, source, generated-owner, TryRuntime, and non-goal truth. + + +--- + +## A2 — Runtime Resource Architecture + +**Owner:** `docs/actors.integration.en.md` + +Record: + +```text +context inherents +→ Actor Prepass +→ user dispatch +→ Actor Drain +``` + + +--- + +## A3 — Oracle Domain Architecture + +**Owner:** `template/pallets/oracle/docs/architecture.en.md` + +The current Oracle document already owns publication atomicity, provenance, backpressure, fault, Weight, and no-cascade truth. + + +--- + +## A4 — Router and Economic Effect Architecture + +**Owners:** `template/pallets/router/docs/architecture.en.md` and affected pallet architecture docs. + +The current package and integration documents already own canonical economic effects, atomicity, sequential market state, induced consequences, and production Weight equivalence. + + +--- + +## A5 — Actors Embedding Architecture + +**Owner:** `template/pallets/actors/docs/embedding.md` + +Record host ports: + +```text +TaskEffectWeight +ActorControlWeight +Actor state holds +mandatory prepass integration +shared economic accounting +current context requirements +``` + + +--- + +# Non-Goals + +- No literal alternation `User call → Actor call → User call` based on node-local arrival order. +- No shared consensus queue combining mempool entries and Actor tickets. +- No class priority for System Actors. +- No same-block execution of multiple Steps from one Actor Cycle. +- No splitting one Task across blocks. +- No whole-pipeline atomicity. +- No generic priority queue or bypass around a heavy FIFO head. +- No universal Trigger index. +- No authored page/chunk/batch/slice geometry. +- No second semantic execution engine. +- No arbitrary runtime-dispatch Task. +- No promise of 10,000 maximum-cost 32-Step Cycles in ten minutes. +- No production promise of one million Active Actors. +- No network-wide physiology graph; that belongs to `0.7.25`. +- No live-network migration or upgrade ceremony. + +--- + +# Canonical Delivery Sequence + +The release work must proceed in this exact order: + +```text +1. Converge the canonical Actors, resource-policy, and performance-assurance specifications. +2. Reconcile the backlog against accepted specification and retained implementation truth. +3. Implement mandatory Actor Prepass, component-wise resource meters, Economic Zipper, and Actor Drain. +4. Scale Crossing and broad fanout, then converge named runtime projections. +5. Run final semantic, resource, model, API, and scale acceptance cohorts. +6. Generate production Weight/Wasm/metadata evidence and correct only verified defects. +7. Freeze the accepted tree and update package, integration, embedding, and Wiki architecture from shipped truth. +8. Run final release validation on that exact documented tree. +``` + +--- + +# Release Thesis + +`0.7.24` is not merely a performance release. It establishes a new physical model for DEOS Actors: + +```text +rich Contracts ++ lazy local state ++ one-Step causal clock ++ symmetric economic contention ++ bounded control physiology ++ active-frontier scaling +``` + +Only after these laws are implemented, falsified, corrected, and frozen in architecture should `0.7.25 — Network Physiology` analyze cascades, feedback loops, dependency topology, effect topology, circulation, and systemic externalities. diff --git a/CHANGELOG.md b/CHANGELOG.md index dff9aaae..825cc733 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ > > Each release keeps at most 8 outcome records of at most 512 characters. +## 0.7.24: Topology Scaling and Block-Paced Execution + +- `Actors / Block-Paced Execution`: Added the mandatory payload-free Actor Prepass, fixed prepass cutoff, explicit block phases, one committed Step per Actor per block, and `N -> N+1-or-later` causal eligibility while preserving strict class-neutral FIFO and progress-preserving Continuation. +- `Runtime / Economic Zipper`: Partitioned schedulable Weight component-wise into a one-third Actor Control ceiling and equal Actor-effect/user base turns over Shared Economic capacity, with work-conserving idle borrowing, maximum reservation, valid-actual reclaim, independent RefTime/ProofSize fragmentation, three Mandatory bases including Actor Prepass, fail-closed reconciliation, and reserved-envelope telemetry that is not mislabeled as actual usage. +- `Actors / Active Frontier`: Bounded production identity and Active Actor capacity at 10,000 without dormant scans; separated hot scheduling/run authority from cold Contract bodies and Step chunks; retained the 32-Step ceiling after measured C24/C16 sweeps; and added exact active-lifetime User RunState holds. +- `Actors / Trigger Scaling`: Retained useful-transition-only charging and latching, authoritative re-arm, P128 Crossing membership and tail/preflight, N64 non-tail work, independent P64 broad fanout, bounded family rotation, and one active plus one deferred Pipeline without stale or duplicate semantic authority. +- `Actors / Evidence and Assurance`: Added fee-free prepaid apoptosis for underfunded one-shot AtTime and unfundable retries, with per-attempt maximum liability and actual-cost settlement; regenerated Weight, vectors, metadata, descriptors, ABI and bounds; completed mixed 10,000-Actor traversal at block 679 with zero failures; and passed package/runtime, heavy scaling, Clippy, no-std, TryRuntime, embedding, identity, provenance and threat gates. +- `Asset Conversion / Trust Closure`: Separated semantic and physical asset identity; rejected ledger aliases at the pool boundary; made Router the only public XYK and atomic permissionless pool-lifecycle surface; removed post-dispatch LP repair; integrated full-balance reserves; separated fee domains; and proved pool/LP/Oracle rollback and bidirectional integrity. +- `Architecture / Experiments`: Closed Step-centric memory, Contract-ceiling, allocation, and active-lifetime hold experiments; retained direct FIFO/A0 where gains were sub-material; excluded external successor staging by interleaved-order proof; embedded all CSV/TSV evidence in owning records; and made retained delimited evidence fail validation. +- `Documentation / Wiki`: Synchronized Actors, Oracle, Router, embedding, resource-policy, and integration truth; projected Economic Zipper, block pacing, Hot/Cold Contract, active frontier, and state holds into strict bilingual OKF pages; and passed structural, graph, localization, and independent semantic-parity review. + ## 0.7.23: Reactive Topology Closure and Cohort Throughput - `Actors / Declarative Trigger Topology`: Moved Crossing phase/revision and cadence into canonical `ActorHot` Trigger state, reduced locators to generation-checked physical authority, and routed create/activate/replace/deactivate/close/genesis through one transactional transition compiler with exact no-op, full family matrix, rollback, fresh-genesis storage version 9, and refundable family-based User bonds while System Actors remain exempt. diff --git a/docs/README.md b/docs/README.md index 974ef28f..48f955c6 100644 --- a/docs/README.md +++ b/docs/README.md @@ -20,6 +20,8 @@ The simulator is the executable authority for TMCTOL formulas, thresholds, and i ## DEOS Actors - [DEOS Actors Specification](../template/pallets/actors/docs/specification.en.md) +- [DEOS Actors Resource Policy Specification](./actors-resource-policy.specification.en.md) +- [DEOS Actors Performance Assurance Specification](./actors-performance-assurance.specification.en.md) - [Package Architecture](../template/pallets/actors/docs/architecture.en.md) - [Embedding Guide](../template/pallets/actors/docs/embedding.md) - [DEOS Actors Integration](./actors.integration.en.md) @@ -37,6 +39,7 @@ The simulator is the executable authority for TMCTOL formulas, thresholds, and i - [Specification](../template/pallets/router/docs/specification.en.md) - [Package Architecture](../template/pallets/router/docs/architecture.en.md) - [Embedding Guide](../template/pallets/router/docs/embedding.md) +- [Asset Conversion Integration](./asset-conversion.integration.en.md) ## DEOS Staking diff --git a/docs/actors-control-plane.contract.en.md b/docs/actors-control-plane.contract.en.md index e9b45736..b9167c42 100644 --- a/docs/actors-control-plane.contract.en.md +++ b/docs/actors-control-plane.contract.en.md @@ -97,7 +97,7 @@ Version history is materialized truth. An indexer may correlate artifacts with a Every forecast records the canonical `contractId`, block hash or state snapshot, metadata hash, and runtime API or local model version used. Results are advisory and become stale when any dependency changes. -Weight forecasts must preserve RefTime and ProofSize separately. Fee forecasts identify evaluation, execution upper bound, fee conversion, and lifecycle overhead rather than returning one unexplained number. Amount resolution identifies live balances, opening snapshots, minimum-balance constraints, fee reservation, and adapter quotes used by each step. +Weight forecasts must preserve RefTime and ProofSize separately. Fee forecasts identify Creation, Trigger-family, complete Pipeline Machine, cleanup, current Action maximum/actual, state-hold, and conversion provenance rather than returning one unexplained number. Amount resolution identifies live balances, opening snapshots, minimum-balance constraints, only the current-attempt Action-fee reservation, and adapter quotes used by each Step. Local simulation cannot claim runtime truth unless it executes the matching runtime Wasm against the identified state snapshot. Heuristic or adapter-local projections must carry a narrower provenance label and may not authorize submission automatically. @@ -163,7 +163,7 @@ The falsification corpus covers price → swap → price, fee funding → downst The first runtime provider simulates one attempt of an existing active actor whose stored `ActorContract`, `ActorType`, and `Mutability` exactly match the validated artifact. It supports an idle actor's next fresh cycle and a suspended actor's next Continuation attempt. It does not simulate creation, dormant activation, a proposed replacement Actor Contract, scheduler throughput, queue position, or future block timing. -The request carries `actor_id`, the exact decoded Actor Contract, actor type, mutability, and mode `FreshCurrentPlan | CurrentContinuation`. `FreshCurrentPlan` requires idle run state and starts at cursor `0` with the next cycle nonce. `CurrentContinuation` requires suspended run state and reuses the stored nonce, unresolved cursor, opening snapshot, and cumulative outcomes while preserving the stored retry position; chain block/event coordinates identify the simulated attempt without a stored ordinal. +The request carries `actor_id`, the exact decoded Actor Contract, actor type, mutability, and mode `FreshCurrentPlan | CurrentRun`. `FreshCurrentPlan` requires idle run state and starts at cursor `0` with the next cycle nonce. `CurrentRun` requires suspended run state and reuses the stored nonce, unresolved cursor, opening snapshot, and cumulative outcomes while preserving the stored retry position; chain block/event coordinates identify the simulated attempt without a stored ordinal. The runtime API runs only after normal liveness, lifecycle, window, nonce, fee-budget, and Continuation invariants pass. A mismatch or unavailable prerequisite returns a bounded typed error and performs no task. The API remains bounded by the actor's admitted plan, configured maximum steps, existing task weights, and the same adapter calls as production execution; it must not inspect an unbounded event or storage history. diff --git a/docs/actors-performance-assurance.specification.en.md b/docs/actors-performance-assurance.specification.en.md new file mode 100644 index 00000000..4165288e --- /dev/null +++ b/docs/actors-performance-assurance.specification.en.md @@ -0,0 +1,284 @@ +# DEOS Actors Performance Assurance Specification + +- **Scope**: Project-owned production performance and stress acceptance +- **Target**: `pre-1.0.0` +- **Status**: Normative + +This document defines the workloads and evidence required to accept DEOS Actors scaling claims. It does not select physical architecture, generate production Weight, replace pallet correctness tests, or make release attestation decisions. Runtime code, generated Weight, project validation, Experiment Records, and release assurance retain their respective ownership. + +--- + +## 1. Evidence Contract + +Every target run MUST record: + +```text +exact source tree +runtime and production-Wasm identity +production Weight identity +runtime constants and Actor ceiling +state population and deterministic seed +start and final block +per-block Actor Control, Actor effect, and user-dispatch Weight +RefTime and ProofSize independently +completed, skipped, failed, suspended, closed, and pending counts +FIFO, detector, wakeup, body, and resource faults +``` + +Setup, genesis construction, prefunding, and deterministic population generation are outside the measured block interval. The measured interval begins with the named trigger operation or first baseline operation. A profile passes only against runtime-bound production Weight and production-Wasm or stronger full-runtime block evidence. Native wall-clock or synthetic microbenchmarks may diagnose a candidate but cannot establish these targets. + +All populations, market state, authored bounds, transaction demand, and seeds MUST be deterministic and retained by the project harness. Weight fairness and deltas are component-wise; no scalar score may combine RefTime and ProofSize. Database reads/writes, persistent bytes, block count, and faults remain separately reported. + +Architecture-affecting candidate comparisons use Experiment Records. The accepted project harness and generated Weight remain project truth; release assurance may review their exact-tree identity but is not a project validation dependency. + +Maximum-context evidence MUST use the runtime-admitted DMP message, HRMP message, and HRMP channel bounds together, preserve the full/hashed split produced by the configured PoV allocator, execute the actual context inherent in benchmark Wasm, and record the fixture and benchmark-Wasm identities. A registered SDK composition may own the fixed envelope only after component-wise dominance over the complete measured path is proved without additive double accounting. + +### 1.1 Independent Throughput Axes + +Every throughput profile MUST report two independent primary axes: + +```text +S = committed Actor Steps / measured block +A = distinct Actors that committed at least one Step / measured block +``` + +Report per-block values and the measured-interval distribution for both axes. Never omit one by inferring it from the other. Under authoritative Q1, one Actor commits at most one Step per block, so `S == A` is a conformance expectation for committed service; any divergence requires an explicit counted cause or is a defect. Both axes remain named independently so future derived fast paths or a separately reopened service contract cannot silently change metric meaning. + +The historical target remains: + +```text +10,000 completed one-Step User Actor Cycles +/ 100 eligible production blocks += 100 cheap committed Actor Steps/block +``` + +For that exact Q1 one-Step witness, the target also entails 100 distinct Actors progressed/block. It is not a universal promise of 100 distinct Actors/block for multi-Step, mixed, market-heavy, divergent, or differently paced workloads. + +Candidate comparisons MUST expose the two-axis Pareto frontier. A candidate is Pareto-dominated when another conforming candidate provides at least as much `S` and `A` under the same workload and is strictly better on one. No arbitrary scalar score may collapse `S`, `A`, RefTime, ProofSize, latency, or fairness into one rank. + +### 1.2 Required Cost, Latency, and Fairness Metrics + +For each candidate and workload report at minimum: + +```text +ActorMachineRefTimePerCommittedStep +ActorMachineProofSizePerCommittedStep +DBReadsPerCommittedStep +DBWritesPerCommittedStep +TriggerToFirstStepLatencyBlocks +TriggerToCompletionLatencyBlocks +ServiceGapBlocks +HeadOfLineUnusedRefTime +HeadOfLineUnusedProofSize +UsefulActionWeight +ActorMachineWeight +``` + +`ServiceGapBlocks` is the number of blocks between committed progress opportunities for a continuously runnable Actor. Report the full deterministic sequence for bounded profiles or at least mean, median, p95, p99, maximum, and the affected Actor count for larger profiles. Under homogeneous Q1 pressure compare observed gaps with `ceil(runnable_actor_population / sustainable_committed_step_capacity_per_block)` and explain fragmentation, control, effect, or eligibility deviations. `HeadOfLineUnusedRefTime` and `HeadOfLineUnusedProofSize` are the otherwise available component-wise capacities stranded when the strict live FIFO head cannot fit; do not convert them into one scalar. + +Actor-machine and action ownership remain separate: + +```text +ActorMachineWeight = + scheduler and causal materialization + + canonical state and current-fragment loading + + predicates, resolution, admission, and fee bookkeeping + + run/continuation persistence + + lifecycle, placement, and Actor events + +UsefulActionWeight = + canonical committed or typed-failed economic Task-effect Weight +``` + +Report each Weight component independently and MAY additionally report `UsefulActionWeight / ActorMachineWeight` only as two labeled component ratios when the denominator is nonzero. A heavy economic action is neither a scheduler regression nor a priority claim. A lower machine ratio cannot hide higher absolute ProofSize, reads/writes, latency, failure, or head-of-line loss. + +For shared-cause profiles additionally report causal cohort materialization cost, ordered Fresh candidates admitted, Step-0 commits/completions, multi-Step Actors entering independent Running state, dissolution cost, and effect on continuation service gaps. The cohort normally ends after Step 0; one-Step Actors may complete within it. Do not spend consensus work tracking later behavioral alignment or rebuilding cohorts after divergence. Cohort membership derives only from one shared Trigger instance; shared Task shape and Contract length create no scheduling affinity or eligibility filter. Any candidate requiring `steps.len() == 1` or equivalent total-length classification is nonconforming. + +### 1.3 Required Workload Classes + +A service-throughput claim MUST cover enough distinct action classes to distinguish Actor-machine amortization from useful economic work. The minimum representative set is: + +- Control-only or `StopCycle`-like cheap Step. +- Burn-like cheap effect. +- Transfer. +- Swap or another bounded market action. +- Liquidity-heavy action. + +Each class uses its canonical effect owner and reports typed outcomes separately from runtime service. The scheduler MUST NOT rank by Task family, declared Weight, fee, or Actor class to improve packing. Workload-class results are separate evidence points and MUST NOT be averaged into one synthetic Actor. + +## 2. Ten-Thousand Actor Targets + +### 2.1 Reactive Transfer + +Construct 10,000 prefunded one-Step User Actors on one feed: + +```text +ObservationCrossing +→ Transfer +→ completed Cycle +``` + +One accepted changed publication in source block `N` causes the qualifying crossing. Every Actor MUST reach a completed one-Step Cycle within the 100 eligible production blocks `N + 1 ..= N + 100`. The profile fails on a lost signal, duplicate Step, FIFO violation, block Weight overrun, unresolved worker fault, or incomplete Actor. Report `S` and `A` per block even though both count the same committed population in this exact Q1 one-Step profile. + +The Transfer destination and amount geometry MUST be identical across the population and executable throughout the run. Setup MUST isolate scheduler/control throughput from avoidable recipient-creation or insufficient-funding failures while retaining the real certified ingress and ledger consequences of the canonical Transfer operation. + +### 2.2 Contended Transfer + +Repeat the Reactive Transfer profile while valid ordinary external extrinsics continuously consume the complete User base turn in every measured block. Actors MUST still complete all 10,000 Cycles within blocks `N + 1 ..= N + 100` using their guaranteed Actor base turn. + +The external workload MUST be deterministic, independently successful, and sufficient in both Weight dimensions to demonstrate base-turn contention without exceeding runtime validity. The report MUST show `S`, `A`, Actor/user consumption, borrowing, service gaps, and component-wise head fragmentation independently. + +### 2.3 Reactive SwapOut + +Construct 10,000 prefunded one-Step User Actors: + +```text +ObservationCrossing +→ bounded SwapOut +→ committed typed Step disposition +``` + +One qualifying publication in block `N` causes readiness. Every Actor MUST commit one typed Step disposition within blocks `N + 1 ..= N + 100`. The market fixture MUST use deterministic bounded liquidity and authored input protection; it MUST NOT fabricate 10,000 stale shared quotes or promise that every market operation succeeds economically. + +Report runtime service separately from economic outcome: + +- Runtime service: The Step committed one canonical `StepOutcome` without duplicate, loss, FIFO violation, or block overrun. +- Economic success: The Task returned the successful SwapOut outcome. +- Economic non-success: Precondition skip, funding outcome, typed Temporary/Permanent failure, suspension, or terminal policy disposition. + +The throughput target concerns committed typed dispositions. Economic success rate is a separate measured result and MUST NOT be relabeled as scheduler failure or guaranteed market execution. Report `S`, `A`, action Weight, and Actor-machine Weight independently so market cost cannot be mistaken for scheduler policy or overhead. + +## 3. Baselines and Differential Overhead + +### 3.1 Manual Baseline + +Measure the same one-Step Transfer and SwapOut Contracts, populations, balances, Tasks, and block policy through Manual readiness without observation detector work. Under the production `NextBlock` timing baseline, Manual signals remain causal next-block obligations; setup MAY batch deterministic signal extrinsics only when their transaction Weight is reported separately from Actor Control and effect execution. + +The baseline isolates detector/materialization work from current-Step control and Task effect work. It does not replace the reactive acceptance targets. + +### 3.2 External Baseline + +Measure the equivalent external Transfer and SwapOut operations through their canonical public mechanisms under the same runtime, market state, recipients, authored bounds, Shared Economic budget, and production Weight owners. External transaction validation/payment overhead MUST be reported separately rather than subtracted as if it were Actor control. + +### 3.3 Actor Control Differential + +For each equivalent effect branch, report: + +```text +Actor control overhead = + Actor Step total Weight + - canonical equivalent Task-effect Weight +``` + +The subtraction is checked independently for RefTime and ProofSize. The canonical adapter effect Weight is the subtraction owner; external extrinsic envelope overhead is not. A negative component indicates mismatched branches or accounting error and invalidates the comparison. + +Also report database reads/writes and proof contributors so a lower RefTime cannot hide increased ProofSize or persistent topology cost. + +### 3.4 Step-0 Timing + +Assure the protocol-fixed next-block eligibility floor using identical authored block extrinsics, Trigger causes, FIFO backlog, runtime constants, Contracts, balances, and action fixtures. + +Report separately: + +- Trigger-to-first-Step latency and Trigger-to-completion latency. +- Readiness materialization/control Weight and Step-0 machine/effect Weight. +- Prepass and Drain `S`/`A`, ProofSize, reads/writes, cutoff, and stranded capacity. +- Exact state visible when Step 0 becomes eligible no earlier than `N + 1`. +- Actor-produced causes and proof that their first target Step is no earlier than block `N + 1`. +- Newly eligible populations `10/100/1,000/10,000`, low and sustained ingress, and a large shared-cause herd. +- Continuously runnable Running populations `10/100/1,000/10,000` under sustained ingress, with mean/p50/p95/p99/max inter-Step service gap and base-pass progress. +- One-Step completion versus multi-Step transition into independent Running service. + +The implementation fails assurance on lost or duplicate readiness, bypass of an older FIFO head, current-block Actor recursion, repeatable privilege, Running base-pass loss, component-wise block overrun, an unbounded materialization/cutoff path, or any execution before the source block's `N + 1` eligibility floor. + +### 3.5 Minimal User Apoptosis + +Compare the retained historical current-Step economic-close baseline with Pipeline-admission-only minimal apoptosis and the four fee boundaries under identical Q1 Contract geometry, Task effects, block resources, and toolchain. Rerun only workloads whose Trigger pricing, Pipeline Machine strategy, Action-fee path, zero-Step geometry, cleanup, custody, projection, or benchmark setup crosses the changed dependency. + +Pipeline Machine strategy MUST compare: + +- `P0 UpfrontBounded`: One complete worst-case machine charge at Opening, no hold/refund/run ledger. +- `P1 HoldAndSettle`: Maximum machine hold with valid-actual settlement/refund. +- `P2 PerStepMachine`: Current-Step machine charging during Q1 service. + +Measure `0/1/4/8/32` Steps across no/max Precondition geometry and reads, no/max RetryLater control paths, long inter-Step gaps, and identical Action effects. Report RefTime, ProofSize, reads/writes, accounting touches, persistent bytes, Wasm bytes, charged-versus-real machine work, failure surface, and runtime/API complexity. P0 is the production-leading hypothesis; P1/P2 cannot be accepted from lower charged cost alone when accounting/state complexity is not materially justified. + +Additionally report: + +- Trigger occurrence service by `Manual`, `AddressEvent`, `ObservationChange`, `ObservationCrossing`, `AtTime`, and `Cadenced`, separating source publication/ordinary transaction work from the disjoint User Trigger Fee. +- Independent Trigger and Pipeline deltas for: Idle useful occurrence with immediate Opening; one busy `false -> true` occurrence during an active Pipeline; redundant latched-period sources that perform no Actor-specific evaluation or charge; delayed Opening after retained `pending_signal`; paid useful Trigger followed by unavailable Pipeline Machine capacity; and a zero-Step immutable AtTime one-shot. +- For every separation fixture, component-wise incremental RefTime, ProofSize, database reads/writes, accounting touches, fee movement, latch transition, detector disable/re-arm, and Cycle transition attributable separately to useful occurrence materialization and Pipeline Opening. +- Running/Suspended current-Step cost at every cursor, proving no balance viability read, machine charge, machine hold, or suffix reconstruction after Opening. +- Success, typed failure, retry, and `FundingUnavailable` Action paths: every invoked Action pays valid actual effect Weight; an unfunded non-invoked Action pays zero and follows prepaid control policy without apoptosis. +- Pipeline-admission-insufficient minimal apoptosis Weight, ProofSize, reads/writes, touched keys, event count, custody-root invariance, non-refund of committed Trigger fees, and absence of any Task/economic adapter. +- Exact-slot custody recovery, User Immutable early-close rejection, ordinary `Transfer(AllAvailable)` source preservation, and no close-time adapter invocation. +- A zero-Step immutable AtTime one-shot fixture, reporting Creation, Trigger occurrence, delayed Pipeline Machine Opening, Action, completion, and cleanup dimensions independently; Action cost MUST be zero. +- Wasm size, run-state bytes, canonical lifecycle-state count, public event/error/API variants, Weight-owner count, and mandatory test-matrix count. `ActorFundingWait`, reactivation, and population-scale sleeping-state counts MUST remain zero. + +The target fails on any combined Trigger/Pipeline charge, Actor-specific evaluation or fee for redundant latched-period source activity, duplicate deferred Pipeline, failure to re-arm from current authoritative state after latch consumption, refunded prior useful Trigger fee on Pipeline insufficiency, economic close between admitted Steps, future Action-fee prepayment, unpaid invoked Action effect, machine accounting inside Running/Suspended under P0, persistent funding wait, custody mutation during apoptosis, owner destruction of User Immutable, inability to recover by exact-slot recreation, invalid zero-Step authority, unbounded Opening work, or changed System service order. + +## 4. Contract Complexity Matrix + +### 4.1 Short Contracts Under Global Ceilings + +Benchmark semantically identical one-, two-, and three-Step Contracts under `MaxContractSteps = 8`, `16`, and `32`. For each current Step report control/effect Weight, ProofSize, loaded fragments, state hold, create/update/close Weight, and persisted run footprint. + +Raising the global ceiling MUST NOT add cold body reads, unused Predicate work, maximum-pipeline ProofSize, or a fee for unauthored machine geometry to the identical short Contract. Any fixed difference requires an explicit runtime-owned cause and production evidence. + +### 4.2 Maximum Contract Pacing + +A 32-Step Actor with every Step executable MUST use exactly 32 distinct committed-Step blocks, with at most one committed Step in each block, and MUST NOT complete before its thirty-second eligible execution block. Consecutive execution blocks are required only in a fixture that guarantees no FIFO or resource congestion. The profile records every cursor, ticket, source block, successor eligibility, execution block, outcome, and final nonce. Congestion may make completion later but never earlier. + +### 4.3 Mixed Complexity + +Construct: + +```text +9,500 one-Step Actors +400 three-Step Actors +100 thirty-two-Step Actors +``` + +Activate all through one deterministic profile while preserving strict FIFO and block pacing. Report `S`, `A`, service gaps, progress, and completion separately by complexity class. Every admitted Actor must make eventual progress under recurring conforming capacity; short Actors receive no class priority, and maximum Contracts cannot create cold-body or whole-cycle admission cost for unrelated short Actors. + +No ten-minute completion promise applies to all Steps in this mixed workload unless measured evidence independently establishes one. + +### 4.4 Mixed-Length Shared-Trigger Step 0 + +Construct one shared Trigger instance whose affected causal cohort contains an equal deterministic mix of Contracts with lengths `0`, `1`, `4`, `8`, and `32`. Nonempty members use the same semantic Step-0 Predicate/Task/resource shape; only zero-Step absence or unreachable suffix length/content differs. Run populations `10/100/1,000/10,000` under both T+1 and any specification-admitted T0 candidate. + +Report cohort discovery/materialization, first Opening/Step-0 control/effect Weight, ProofSize, reads/writes, loaded fragments, first-reaction latency, zero-Step completions, Step-0 commits, one-Step completions, and multi-Step transitions into independent Q1 Running service. After Step 0, report continuations separately by Contract length; they are not cohort work. + +Initial causal candidate discovery MUST be length-blind; fixtures fund every member for its own activation quote. First reaction MUST load no tail fragment and incur no RefTime/ProofSize/read/write term proportional to unreachable Steps or tail chunks. Pipeline Machine charging consumes one fixed-size generation-bound hot-header projection; the amount intentionally follows authored bounded control geometry while projection read/encoding topology stays constant. Any cold-tail read, suffix reconstruction, or one-Step-only cohort filter fails No Ceiling Tax. + +## 5. Active-Frontier Stress Profiles + +### 5.1 Cadence Herd + +Construct 100,000 Actors with identical or tightly clustered due ticks. Measure empty-time probes, due-page materialization, aggregate FIFO placement, partial resumption, block/tick ordering, and total convergence. Service MUST remain bounded per block, preserve temporal order, coalesce missed cadence without catch-up Cycles, and expose no unbounded scan or duplicate ticket. + +### 5.2 Observation Herd + +Construct 100,000 `ObservationChange` subscriptions or `ObservationCrossing` memberships on one hot feed, subject to the tested profile's explicit configured capacity. Measure publication ingress separately from deferred page/threshold discovery, candidate materialization, FIFO placement, faults, and convergence. + +Publication work MUST remain cardinality-independent. Deferred work MAY span blocks but MUST preserve revision order, exact membership authority, causal delay, and bounded per-block control. + +### 5.3 Million Dormant + +Construct 1,000,000 Dormant identities in the stress state generator without claiming that the production runtime admits one million Active Actors. A tiny fixed active frontier is then exercised while all other identities remain Dormant. + +Ordinary idle consensus work MUST use the same bounded probe/read topology as the zero- or small-dormant baseline. RefTime, ProofSize, and database reads/writes MAY differ only through a declared fixed-width encoding or database effect independent of dormant population; no loop, page walk, or identity-proportional probe is allowed. Activation of one selected identity may scale only with that identity's bounded lifecycle work. + +TryRuntime or offline integrity cost for constructing/checking the full stress state MUST be reported separately from consensus idle Weight. + +## 6. Claim Boundary + +Passing these profiles supports only the exact populations, Tasks, runtime constants, state geometry, production Weight, and source tree measured. It does not establish: + +- 10,000 maximum-cost 32-Step Cycles in ten minutes. +- One million Active Actors. +- Exact execution-block SLAs for individual Actors. +- Market success, fair price, MEV protection, or permanent liquidity. +- Equivalent throughput after runtime bounds, Weight, storage geometry, Tasks, or block policy change. + +Changed assumptions require rerunning the affected project profile and invalidating or superseding architecture evidence whose baseline no longer applies. diff --git a/docs/actors-resource-policy.specification.en.md b/docs/actors-resource-policy.specification.en.md new file mode 100644 index 00000000..ef7ac70b --- /dev/null +++ b/docs/actors-resource-policy.specification.en.md @@ -0,0 +1,242 @@ +# DEOS Actors Resource Policy Specification + +- **Scope**: DEOS reference-runtime block resource allocation +- **Target**: `pre-1.0.0` +- **Status**: Normative + +RFC 2119/RFC 8174 key words are normative when uppercase. This document defines the host resource policy under which the DEOS reference runtime composes portable Actors with ordinary economic dispatch. The Actors package specification owns Actor semantics, FIFO authority, and Task behavior; this document owns only reference-runtime block phases and multidimensional resource allocation. + +--- + +## 1. Resource Domain + +`Weight` is an ordered pair: + +```text +Weight = (RefTime, ProofSize) +``` + +Every comparison, subtraction, ratio, reservation, release, and limit in this document MUST be applied independently to both components. A call or Actor Step fits an envelope only when both components fit. Neither component MAY be converted into, borrowed from, or compensated by the other, and the runtime MUST NOT reduce Weight to a scalar fairness score. + +Let: + +```text +MaxBlockWeight +FixedBlockWeight +SchedulableBlockWeight +``` + +be component-wise Weight values. `MaxBlockWeight` is the FRAME maximum block Weight. `FixedBlockWeight` is the conservative runtime-owned envelope for block/context work that belongs to neither Actor Control nor Shared Economic Execution. It includes block initialization/finalization, required context-establishing inherents, and runtime-declared non-economic maintenance reserves, including Timestamp, bounded session rotation, parachain validation context, Message Queue `on_initialize` service, and XCMP Queue lazy-migration `on_idle`. It excludes the mandatory Actor Prepass because the prepass charges its Actor-specific work to Actor Control and its Task effects to Shared Economic Execution. + +The runtime MUST configure: + +```text +FixedBlockWeight <= MaxBlockWeight +SchedulableBlockWeight = MaxBlockWeight - FixedBlockWeight +``` + +with checked component-wise subtraction. The configured fixed envelope MUST cover the maximum admitted fixed/context path; unused fixed capacity is not silently reclassified as schedulable capacity within the same block. Increasing fixed work therefore requires explicit recomputation of the schedulable envelope. + +Every variable-size context inherent component contributing to `FixedBlockWeight` MUST have a runtime-declared finite admission bound whose generated Weight is valid at that bound. Benchmark component ranges, relay-side expectations, block-length limits, nominal reserves, and post-dispatch overweight rejection are not admission authority. The Actor Prepass `check_inherents` owner MUST inspect the shared parachain inherent data and reject full or hashed DMP/XCMP geometry beyond those bounds before execution; the node provider MUST construct within the same limits. Direct prepass dispatch still verifies established canonical context and finalization still requires the completed phase protocol, but neither may claim to reconstruct discarded inherent payload geometry from post-dispatch storage. + +A maintenance branch that is unreachable in the active fresh-genesis topology is not fixed work merely because upstream code contains it. Its absence MUST be mechanically proved; any runtime upgrade or configuration that can activate it MUST recompute and admit its maximum before activation. The runtime MUST benchmark the complete maximum admitted context path, including validation, metadata traversal, full/hashed handling, and outer bookkeeping. Existing SDK leaves may remain the fixed owner only when their component-wise registered composition dominates that complete measured path. The measured owner is then a falsification floor rather than an additive duplicate; failed dominance requires a replacement fixed owner before the geometry remains admissible. + +## 2. Resource Algebra + +The DEOS reference policy is: + +```text +ActorControlLimit = floor(SchedulableBlockWeight / 3) +SharedEconomicLimit = SchedulableBlockWeight - ActorControlLimit +ActorBaseTurn = floor(50% * SharedEconomicLimit) +UserBaseTurn = SharedEconomicLimit - ActorBaseTurn +``` + +The subtraction definitions assign every indivisible remainder exactly once and ensure: + +```text +ActorControlLimit + SharedEconomicLimit = SchedulableBlockWeight +ActorBaseTurn + UserBaseTurn = SharedEconomicLimit +``` + +`ActorControlLimit` pays only for Actor-specific detection, materialization, canonical hot-state and current-Step loading, precondition and amount evaluation, FIFO and temporal topology, run-state bookkeeping, Actor fee bookkeeping, retry/completion, and repairable fault handling. Materialization fairness does not require all family minima in one block: when their sum does not fit, the persistent cursor admits one fitting family quantum and rotates future first service without borrowing Shared Economic capacity. + +`SharedEconomicLimit` pays for ordinary external economic dispatch and Actor Task effects. An Actor Task effect MUST use the same production Weight owner as its equivalent external mechanism. Actor class and fee exemption do not change the meter charged. + +The runtime MUST preserve all of the following component-wise: + +```text +actor_control_used <= ActorControlLimit + +actor_effect_used ++ user_dispatch_used +<= SharedEconomicLimit + +fixed_weight ++ actor_control_used ++ actor_effect_used ++ user_dispatch_used +<= MaxBlockWeight +``` + +`fixed_weight` is the configured fixed/context envelope reserved at admission and reported as `fixed_reserved`; it is not presented as measured actual work. The runtime MUST preserve this complete envelope for every block, and observing less conditional fixed work does not enlarge the current block's schedulable limits. A future `fixed_actual` surface requires one authoritative runtime owner that measures every included hook, inherent, and context component. + +Any overflow, underflow, inconsistent reservation, impossible release, or disagreement that would make authoritative accounting uncertain MUST fail closed before the affected economic effect. Already committed earlier extrinsics or Actor Steps remain durable. Optional Actor work MUST halt for the block when safe accounting cannot be recovered. + +## 3. Economic Zipper + +The selected DEOS reference allocation is exact thirds: Actor Control receives floor one third, Shared Economic receives the remainder, and that remainder is split floor/remainder between Actor and user base turns. One third is the maximum permissible Actor Control share; further throughput work MUST optimize inside it. The Shared Economic envelope has symmetric approximately one-third turns under continuous demand: + +```text +Actor base turn = ActorBaseTurn +User base turn = UserBaseTurn +``` + +The split is by multidimensional Weight, never by call count, Actor count, ticket count, fee, Actor class, or node-local arrival time. + +Actors MAY consume up to `ActorBaseTurn` during the pre-user Actor base pass. Ordinary external extrinsics then MAY consume every part of `SharedEconomicLimit` not already consumed by Actor effects. This includes the complete User base turn and any Actor base-turn capacity Actors left unused. After external dispatch, Actor Drain MAY consume the remaining Shared Economic capacity, including unused User base-turn capacity. + +Work conservation changes available capacity, not ordering authority: + +- The runtime MUST NOT construct a consensus queue that merges node-local transaction-pool arrival order with Actor tickets. +- Block authors retain ordinary external-extrinsic ordering, subject to runtime validity and resource checks. +- Actor effects retain canonical on-chain strict FIFO ordering. +- User and System Actors share the same Actor FIFO and resource rules; Actor class MUST NOT affect ordering, admission, or allocation. +- Borrowing unused capacity MUST NOT reserve future work, bypass the current live Actor FIFO head, or admit a current-block readiness obligation across the execution cutoff. + +`UserBaseTurn` is the minimum Shared Economic capacity left for valid ordinary external dispatch when Actors fully consume `ActorBaseTurn`, except for unavoidable dispatch granularity: a user call that does not fit the remaining two-dimensional envelope is not partially admitted. Actor Drain begins only after the external-extrinsic phase and therefore cannot take capacity from a valid user call already admitted by the block author. + +## 4. Admission and Actual Weight + +Every ordinary extrinsic and Actor Step effect MUST reserve its declared maximum Weight before semantic mutation. Actor Step block admission MUST also reserve its maximum current Actor Control envelope before evaluating Current predicates or invoking an effect. Trigger and Pipeline economic admission follow the canonical fee boundaries in the Actors specification: only a useful `pending_signal: false -> true` transition performs Actor-specific Trigger work and charges its generated family owner, while later Idle readiness consumption separately charges complete bounded Pipeline Machine/cleanup service before Opening. This prepays economic machine service but does not reserve one-block Weight or future Action effects. Running/Suspended Steps consume paid machine authority without a control fee or economic-close classification. + +Each Action-bearing Task attempt reserves only its current maximum effect fee while preserving the ledger minimum, then replaces it with valid actual effect Weight. An unfunded non-invoked Action yields `FundingUnavailable`, consumes prepaid Actor Control only, and follows authored policy. An underfunded Trigger occurrence creates no readiness and never invokes apoptosis. An Idle User that cannot fund Pipeline Machine/cleanup plus ledger minimum while consuming paid readiness selects a separately generated minimal-apoptosis Actor Control owner. It consumes no Shared Economic Task envelope, performs no Opening/Task/custody mutation, refunds no prior Trigger fee, and may allow strict FIFO to continue only after process cleanup commits atomically. + +After dispatch, the runtime MUST replace each maximum reservation with valid actual post-dispatch Weight: + +```text +used_after = used_before + actual +``` + +where `actual <= reserved` component-wise. The difference becomes available only within the same owning envelope: + +- Released Actor Control capacity remains Actor Control capacity. +- Released Actor effect capacity remains Shared Economic capacity. +- Released user-dispatch capacity remains Shared Economic capacity. + +An absent, malformed, greater-than-reserved, or otherwise untrustworthy actual Weight MUST fail closed according to the owning dispatch transaction. Reclaim MUST NOT erase Weight already registered with FRAME, move Weight between Actor Control and Shared Economic domains, or violate the block-total invariant. + +A failed external extrinsic is charged its valid actual post-dispatch Weight. A committed unsuccessful Actor Step is charged its actual Actor Control work and any actual Task effect work. A rejected or rolled-back Actor Step MUST preserve the transactional semantics defined by the Actors specification while retaining whatever outer FRAME accounting is required for work already performed. + +## 5. Maximum Step Fit and FIFO Fragmentation + +Every admitted Actor Step MUST declare a maximum control envelope and maximum Task-effect envelope. The effect envelope MUST fit `ActorBaseTurn`, and the control envelope MUST fit `ActorControlLimit`, both component-wise. A runtime configuration that admits a Step violating either condition is invalid. + +Strict FIFO forbids bypassing a live head merely because a later Step is smaller. The Actor base pass or Actor Drain MUST stop when the live head cannot fit either the remaining Actor Control capacity or the remaining Shared Economic capacity available to that pass. + +Head fragmentation is acceptable only when all of the following hold: + +- The stop is caused by the exact live head failing a component-wise fit check. +- No later ticket executes around that head. +- The runtime records the remaining Actor Control and Shared Economic Weight and the head's declared envelopes for measurement. +- In at least one blocking component, the stranded remainder is strictly less than the corresponding required head component. +- The required head component is no greater than the configured maximum admitted single-Step component. + +This is the maximum semantic fragmentation bound. No stronger bound is valid for the non-blocking Weight component: a ProofSize-heavy head may strand substantial RefTime and vice versa. Production acceptance MUST report both components and MUST NOT disguise such stranding through a scalar percentage. If measured fragmentation prevents the release throughput contract, the implementation or admitted Step geometry must change; FIFO bypass is not an allowed correction. + +## 6. Canonical Block Phase Protocol + +Every block MUST have the following semantic order: + +```text +1. Context-establishing inherents +2. Mandatory Actor Prepass inherent +3. Signed and ordinary external extrinsics +4. Actor Drain during on_idle +5. Finalization +``` + +Timestamp, parachain validation data, and every other required context-establishing inherent remain mandatory fixed/context work outside the Economic Zipper. They MUST execute before the Actor Prepass. The prepass MUST verify from canonical current-block runtime state that Timestamp and every runtime-declared required parachain context owner have been established; absence, stale-block context, or impossible ordering makes the prepass invalid. + +Each block MUST contain exactly one Actor Prepass inherent, including a block with no Actor work. The call carries no author-selected scheduling, ticket, budget, or payload data. Its presence establishes the consensus phase boundary. A duplicate prepass, a prepass before required context, or any signed or ordinary external extrinsic before the prepass makes the block invalid. An external extrinsic submitted after the prepass remains subject to ordinary runtime validity and the Shared Economic meter. + +The runtime MUST retain one current-block phase marker: + +```text +ContextIncomplete → PrepassExecuting → ExternalPhase → FreshDrain → Finalizable +``` + +Transitions are one-way. External extrinsic validity requires `ExternalPhase`; Actor Drain sets `Finalizable` on completion even when no work exists. Finalization MUST fail closed unless the marker proves exactly one completed prepass and this progression. The marker is transient block protocol state, not historical telemetry. + +### 6.1 Control Open and Execution Cutoff + +The prepass first performs control-open work under `ActorControlLimit`. Control-open MAY service only obligations whose causal lower bound permits service at the start of the current block. Current Timestamp MAY identify a cadence boundary during prepass, but readiness caused by observing that boundary remains next-block. + +After control-open, the runtime freezes `prepass_execution_cutoff` at the greatest canonical Actor FIFO ticket then admitted for base-pass execution, or the explicit empty value. Cutoff capture is mandatory Actor-specific control work and consumes its generated owner from `ActorControlLimit`; it is excluded from `FixedBlockWeight` and Shared Economic capacity. The base pass uses only tickets at or below that cutoff. + +This cutoff remains the Actor Drain cutoff and excludes every current-block cause. It is an upper bound, not permission to ignore ticket eligibility, actor/run authority, strict FIFO, the circuit breaker, or either resource meter. Readiness observed in block `N` becomes eligible no earlier than `N + 1`; later execution remains best-effort under FIFO and available `on_idle` Weight. + +### 6.2 Actor Base Pass and External Phase + +After freezing the cutoff, the prepass executes the Actor base pass against the canonical FIFO. It MUST stop when any of the following first prevents the exact live head from proceeding: + +- The Actor base turn lacks the head's Task-effect envelope. +- The remaining Actor Control meter lacks the head's control envelope. +- The live head is above the execution cutoff or is otherwise not temporally eligible. +- The strict-FIFO, breaker, fault, or fail-closed accounting contract requires a stop. + +A ready Idle User head lacking complete activation admission is not a fragmentation stop when the remaining Actor Control meter fits minimal apoptosis. Cleanup consumes the live head and process topology atomically, preserves custody, and then permits FIFO progress. A Running or Suspended User is never economically classified at the head. If required cleanup control does not fit, the pass stops without mutation. + +An empty or immediately stopped base pass is valid; the mandatory prepass still commits its phase marker and actual control Weight. + +After prepass completion, the block author MAY include signed and ordinary external extrinsics in any order accepted by runtime validity. Their consensus result MUST depend only on block contents and canonical state, never on node-local transaction-pool arrival timing. External dispatch consumes the Shared Economic capacity remaining after Actor base-pass effects and MAY borrow all Actor base-turn capacity left unused. Readiness observed during the current block remains beyond its fixed execution cutoff and cannot execute before the next block. + + +### 6.3 Actor Drain + +During `on_idle`, Actor Drain MAY consume Shared Economic capacity left after external dispatch and Actor Control capacity left after all prior Actor work. It continues from the exact canonical FIFO head and uses the current block's fixed `prepass_execution_cutoff`. + +Actor Drain MUST NOT: + +- Execute a ticket greater than the active cutoff. +- Execute current-block Actor-produced, cadence, retry, successor, retained-signal, or incompletely materialized readiness. +- Give a Fresh ticket priority over an older live head, reserve effect capacity for Fresh work, grant a second Step to one Actor, or grant class preference. +- Reopen either control stage, recompute a frozen cutoff, reserve capacity from a future block, or exceed the actual block remainder supplied to `on_idle`. + +The prior-generation base pass is the nonzero Running-continuation floor. Newly eligible work uses only remaining ordinary capacity and may borrow no protected future amount. When no eligible cutoff-bounded head fits, Drain returns its exact actual Weight and stops. Finalization then reconciles complete block meters under Section 2. + +### 6.4 Runtime and Node Boundary + +The runtime owns the Actor Prepass inherent identifier, payload-free call, required-presence rule, ordering checks, phase state, control-open algorithms, fixed cutoff, certified causal provenance, Actor base pass, Actor Drain, actual Weight, duplicate rejection, and final reconciliation. These are consensus rules and MUST NOT depend on node policy. + +The node-side inherent provider owns only deterministic insertion of the runtime-declared empty Actor Prepass inherent data item into every authored block after supplying required context inherent data. It MUST NOT select Actors, tickets, budgets, or execution counts. Failure to construct the required data is a block-authoring failure, not permission to omit the prepass. + +The runtime `inherent_extrinsics` path MUST derive exactly one canonical prepass extrinsic from that data. The runtime `check_inherents` path is the primary import-validation owner for missing, duplicate, malformed, noncanonical, or over-bound context/prepass inherent data and extrinsics. Runtime extrinsic application MUST independently reject duplicate or misordered prepass execution and external-before-prepass dispatch, while finalization MUST reject absence. These defenses ensure that direct block construction cannot bypass the import-time check. + +Node-local signal arrival is not inherent input. Actor signals enter only through their canonical runtime operations and acquire on-chain causal and FIFO authority there. + +This protocol does not redefine FRAME dispatch classes or create an Operational reserve; introducing a concrete Operational call requires a separately measured block-weight policy. + +## 7. Economic Binding + +The Actors specification exclusively owns creation, state-hold, Trigger, Pipeline Machine, Action, lifecycle, settlement, and projection economics. This resource policy neither transfers value nor redefines those boundaries. Fee or hold exemption changes value movement only: it MUST NOT change Actor Control or Shared Economic accounting, FIFO order, block admission, actual-Weight evidence, or the protected ledger minimum. Economic prepayment does not reserve future block Weight, and reclaimed block Weight does not imply an economic refund. + +## 8. Conformance + +A conforming runtime MUST provide generated or executable evidence that falsifies at least: + +- Independent RefTime and ProofSize exhaustion. +- Full Actor and user base-turn contention. +- Actor-empty and user-empty work conservation. +- Partial demand on either side. +- Maximum single-Step fit. +- Strict-head fragmentation without bypass. +- Fixed T+1 eligibility, cutoff immutability, no current-block or Actor-produced same-block cause, and Running progress under sustained ingress pressure. +- Pre-dispatch reservation and post-dispatch actual reclaim. +- Failed external dispatch and committed unsuccessful Actor Step accounting. +- Arithmetic or meter corruption fail-closed behavior. +- Agreement between internal resource totals and FRAME-registered block Weight. +- User/System Actor resource neutrality for executable Steps; User-only boundary admission and minimal apoptosis consume Actor Control but never scheduler priority. +- Idle nonviable-head minimal cleanup without effect reservation, custody mutation, mid-run classification, or later-head bypass before cleanup commits. + +Telemetry and runtime API projections MAY expose finalized counters, but they are read-only observations and MUST NOT become resource authority. diff --git a/docs/actors.integration.en.md b/docs/actors.integration.en.md index 68ec0e56..4b9e3024 100644 --- a/docs/actors.integration.en.md +++ b/docs/actors.integration.en.md @@ -21,17 +21,17 @@ The portable actor contract and crate implementation remain in [`template/pallet ## Temporal Binding -The six-second DEOS slot binds block cooldown and window horizons through `ActorMaxExecutionDelayBlocks = 52_596_000`, exactly `ceil(10 × 365.25 days / 6 seconds)`. Cadence uses consensus timestamp through `ActorCadenceTickMillis = 500` and its independent `ActorMaxCadenceDelayTicks = 631_152_000`, exactly `ceil(10 × 365.25 days / 500 milliseconds)`. These typed horizons are never converted or reused across clocks; retry backoff remains separately protocol-capped. +The six-second DEOS slot binds block cooldown and window horizons through `ActorMaxExecutionDelayBlocks = 52_596_000`, exactly `ceil(10 × 365.25 days / 6 seconds)`. AtTime and Cadenced use consensus timestamp through `ActorCadenceTickMillis = 500` and independent `ActorMaxTemporalDelayTicks = 631_152_000`, exactly `ceil(10 × 365.25 days / 500 milliseconds)`. These typed horizons are never converted or reused across clocks; retry backoff remains separately protocol-capped. -## User Trigger-State Bonds +## Actor-State Holds and Capacity -The DEOS runtime reserves a refundable native bond from the User owner for every Active Trigger: one ED for `Manual`; two ED for `AddressEvent`, `ObservationChange`, and `Cadenced`; and five ED for `ObservationCrossing`. `ActorFunding.trigger_state_bond` stores the exact amount. Active creation and Dormant activation reserve before detector topology commits, semantic replacement adjusts the difference atomically, and deactivation or any terminal close releases it before canonical partition removal. System Actors remain exempt under host-owned hard capacity. This custody is separate from the non-refundable creation fee, sovereign protected execution balance, attempt fees, and task funding; current page, radix, subscriber, or queue occupancy never changes the amount. Runtime integration proves Manual-to-Cadenced adjustment, deactivation and explicit-close refund, fail-before-mutation on insufficient reserve, and exact-root rollback after an injected post-reserve failure; apoptosis and worker closure share the same `finalize_actor` release owner. A two-User Crossing fire profile depletes the first sovereign, closes it during materialization, releases exactly one Crossing bond, removes its physical membership, preserves the moved peer locator/cursor path, and continues the peer without a worker fault. +The DEOS runtime maintains no Trigger-family bond, rent, or fee reserve. It binds `pallet-balances` as `StateHoldCurrency`, `RuntimeHoldReason::Actors(ActorState)` as the dedicated reason, one ED as each present component's fixed base, and one `MICRO_UNIT` per retained SCALE byte. `ActorStateHolds` separates identity, C6 header, actual cold chunks, detector topology, funding, and run capacity per User Actor; the run component reserves the type-derived maximum for the Active installed lifetime, costs `0.001805` native units per Actor under current DEOS pricing, and prevents owner-balance dependency at autonomous Opening. Dormant Actors release Active components, and System Actors are hold-exempt host capacity. Lifecycle, ingress, execution, cancellation, deactivation, and close reconcile owner hold deltas transactionally, and close never touches sovereign custody. ## Namespace and Sovereign Accounts The runtime binds package `pallet-deos-actors`, Rust crate `pallet_deos_actors`, and `ActorsPalletId = *b"actors00"`. The pallet account is `PalletId(*b"actors00").into_account_truncating()` under `AccountId32` and SS58 prefix `42`. -User actors derive sovereign accounts from `(PalletId, owner, owner_slot)`. System actors derive them from `(PalletId, "system", sovereign_id)`; `ActorClass::System { sovereign_id }` carries that custody locator independently from the actor-id key. Fresh creation assigns the new actor id as a new locator. `SystemSovereigns` retains every allocated locator as `Vacant | Occupied(actor_id)`; governance may attach a fresh identity to a vacant locator without changing its account or residual balances. `ActorCreated.actor_class` carries `ActorClass::User { owner_slot }` or `ActorClass::System { sovereign_id }`; no separate event field duplicates either value. +User actors derive sovereign accounts from `(PalletId, owner, owner_slot)`. Close releases the slot without moving native or registered-asset custody; the same owner may install a fresh Mutable recovery Contract at that exact slot, receiving a fresh actor id and nonce while reusing and accessing the same sovereign account. No rescue subsystem or custody transfer is involved. System actors derive accounts from `(PalletId, "system", sovereign_id)`; `ActorClass::System { sovereign_id }` carries that custody locator independently from the actor-id key. Fresh creation assigns the new actor id as a new locator. `SystemSovereigns` retains every allocated locator as `Vacant | Occupied(actor_id)`; governance may attach a fresh identity to a vacant locator without changing its account or residual balances. `ActorCreated.actor_class` carries `ActorClass::User { owner_slot }` or `ActorClass::System { sovereign_id }`; no separate event field duplicates either value. The complete DEOS deterministic System account map follows. @@ -211,13 +211,15 @@ Every certified producer path that can credit an Actors sovereign account routes Movement to a non-Actors recipient and a task transfer to the actor's own sovereign remain explicit exclusions: `resolve_actor` returns `None` for non-sovereign recipients and the pallet's recipient validation rejects self-transfers with `SelfTransferNotAllowed`. -Fee collection is intentionally excluded from certified AddressEvent ingress. Actor, transaction-payment, governance-opening, and XCM fee collectors credit the Fee Sink ledger without creating trigger or funding state; its single cadence owns allocation. `ACTORS_ADDRESS_EVENT_PRODUCER_INVENTORY`, generated ingress evidence, paired-executive tests, and the embedding fixture continue to cover paths that actually signal an actor. +Fee-collector ledger movements are intentionally excluded from certified AddressEvent ingress. Actors now charges generated Manual, matching AddressEvent, affected-Actor ObservationChange, fired-Actor ObservationCrossing, due AtTime, and due Cadenced occurrence owners, but that collector credit—like transaction-payment, governance-opening, and XCM fee credits—reaches the Fee Sink ledger without recursively creating trigger or funding state; its single cadence owns allocation. `ACTORS_ADDRESS_EVENT_PRODUCER_INVENTORY`, generated ingress evidence, paired-executive tests, and the embedding fixture continue to cover paths that actually signal an actor. ## Fee Composition -The package-owned `attempt_fee_envelope` derives each task and complete-plan reserve from runtime-generated task weights, `WeightToFee`, and configured step fees. `settle_attempt_fee_step` releases the selected reservation before charging evaluation-only or attempted-step fees. DEOS runtime tests consume that exact envelope rather than reconstructing fee arithmetic. +Manual, AddressEvent, ObservationChange, ObservationCrossing fire, AtTime, and Cadenced are the implemented Trigger-fee families. Each family performs Actor-specific Trigger work and charges its generated occurrence owner only for a useful `pending_signal: false -> true` transition. Redundant latched activity performs no Actor-specific evaluation, fee, event, activation, or causal-history accumulation. Source-owned movement, funding accumulation, and authoritative Observation state may continue independently. Opening re-arms stateful Trigger families from current authority; AtTime remains one-shot consumed; Cadenced resumes from the first deadline strictly after the current authoritative tick. Underfunding or collector failure creates no readiness or apoptosis. A homogeneous Crossing batch whose collection cannot complete rolls back its aggregate attempt and advances through the already-admitted scalar owner, preventing a free retry loop without charging the publisher. The package-owned `PipelineMachineEnvelope` binds complete bounded control/cleanup pricing in the certified Contract head. When Idle paid readiness is consumable, the scheduler charges that total before Opening; one-unit shortfall selects `CycleAdmissionInsufficient` process cleanup without refunding prior Trigger fees. Running/Suspended service performs no machine affordability or collection. Current-Step resource admission still validates post-dispatch control/effect evidence component-wise, but `StepFeeBreakdown` reserves and settles only the current Action effect. Non-invoked effects, false predicates, skipped resolution, `FundingUnavailable`, and `StopCycle` produce no Action collection. -Package-generated `actors-fee-envelope-vectors.json` constrains browser forecast fee policy across User/System suffixes, reservation release, rollback pricing, and the direct User fee-native floor. One selected fee charges per executed or failed task attempt; skipped steps charge evaluation only. +Package-generated `actors-fee-envelope-vectors.json` constrains the browser's Action-only suffix reservation and protected-floor behavior. Runtime-generated `actors-cost-vectors.json` binds metadata and Actors Weight hashes to separate `ActorCostApi` owners across Manual `0/1/4/8/32` geometry, every Trigger family at one Step, dormant User absence, and explicit System exemption. `runtime/examples/actor_cost_vectors.rs`, `automation/cost-vectors.ts`, Actors assurance, and full regeneration own generation, fail-closed parsing, freshness, and drift evidence. Visible presentation remains open. + +Runtime Pipeline projection binds the generated zero-Step owner (`44,211,000 / 4,952`, five reads and three writes), every C6 Step-control branch across authored retry counts, and exact generated `close_actor` cleanup (`736,628,000 / 81,886`, 64 reads and 63 writes) rather than the broader lifecycle maximum. Runtime settlement charges a Pipeline once per admitted Cycle and charges every invoked success or typed adapter failure from valid actual effect Weight. Each committed Action-bearing attempt appends `ActionFeeCharged` with `(actor_id, cycle_nonce, step_index)`, actual effect Weight, and the exact User charge or System zero after semantic boundary events; non-invocation and `StopCycle` emit no Action receipt. Pipeline, Action, collection, or evidence failure rolls back queue consumption, Step state, effects, placement, close, fees, and events within the owning current-Step transaction. `TmctolFeeCollector` transfers the complete charge into Fee Sink System Actor `1` through a ledger-only movement, matching transaction-payment and XCM-trader collection without fabricating AddressEvent readiness. One 120-tick cadence under the 500 ms consensus clock owns a stable 60-second allocation period. Its plan processes 10% of the current spendable Native buffer only when each configured split leg receives at least one ED, allocates the processed amount under the current phase policy, and retains the unprocessed buffer plus indivisible remainder. Runtime regressions prove collection-only custody, threshold skips, timestamp readiness, and no early execution. @@ -236,49 +238,61 @@ Pure lifecycle cleanup charges no execution fee and runs no plan. User fee admis | `MaxQueueEntriesScannedPerBlock` | 10,000 physical inspections | | `MaxExecutionsPerBlock` | 1,000 defense-in-depth attempt ceiling | | Crossing worker | 10% of maximum block Weight; admits one complete generated worst-case unit | -| Wakeup worker | 14% contribution to the shared materialization envelope | +| Wakeup worker | 14% contribution to the shared materialization envelope; block-keyed Pipeline service and tick-keyed Trigger detection use independent Actor pointers | | ObservationChange fanout | 20% contribution to the shared materialization envelope | | `MaxCrossingMembersPerFeed` | 10,000 total memberships | | `MaxUserCrossingMembersPerFeed` | 9,000 User memberships; remaining 1,000 positions are System-only | | `MaxCrossingActorsPerBlock` | Four candidates through two reachable homogeneous pair cohorts | -| `MaxContractSteps` | Configurable shared `1..=255` bound; DEOS baseline is 8 for both classes | +| `MaxContractSteps` | Configured maximum remains within `0..=255`; each User or System Contract admits `0..=32` Steps under the DEOS production binding | | `MaxRetryAttempts` | 10 cursor-local unsuccessful attempts | | `MaxConsecutiveFailures` | 10 | | `MaxAutoCloseNonceHorizon` | 10,000 | -Runtime block policy assigns 50% to dispatch and 50% guaranteed `on_idle` headroom. No dedicated Operational reserve exists while no concrete critical Operational call consumes one. `ActorOnIdleReserve` binds directly to `1,000,000,000,000 / 2,500,000`. +The fixed context owner uses measured DMP plus the smallest component-wise XCMP residual and outer enqueue bookkeeping whose sum dominates the complete maximum-context benchmark; it does not retain the superseded 25% fictitious handler reserve. + +The production resource algebra partitions the Normal block envelope component-wise into Actor Control `304,686,077,576 / 676,150` and Shared Economic `609,372,155,152 / 1,352,300`. Shared Economic grants equal base turns of `304,686,077,576 / 676,150` to Actor effects and signed user dispatch. Either side may borrow unused capacity from the other; neither may consume the other's guaranteed base while both are saturated. RefTime and ProofSize fragment independently and unused capacity in one component cannot compensate for exhaustion in the other. + +The block sequence is `context inherents -> Mandatory Actor Prepass -> signed user dispatch -> Actor Drain -> on_finalize`. The node supplies the payload-free versioned prepass inherent exactly once after Timestamp and parachain context; runtime phase guards reject absence, duplication, staleness, or ordering after signed dispatch. Prepass freezes one FIFO cutoff and reaches `ExternalPhase`; Drain preserves that cutoff through `FreshDrain`, then generated finalization reaches `Finalizable`. `on_finalize` consumes only a matching current-block marker with zero outstanding reservations. + +Prepass and Drain reserve maximum Actor Control before mutation and settle generated actual control afterward. Actor Actions reserve Shared Economic maxima from the Actor base turn and reclaim valid actual Weight transactionally. `BlockResourceMeterExtension` performs the equivalent maximum reservation and valid-actual reclaim for signed calls from the user base turn. Internal accounting and FRAME block Weight are reconciled fail-closed; uncertainty, overflow, stale state, and inconsistent post-dispatch evidence halt resource service rather than fabricate capacity. + +`ActorResourceApi` exposes the configured two-dimensional budget, optional current authoritative block state, and latest bounded finalized non-authoritative snapshot as named SCALE projections. `ActorEligibilityApi` v6 replaces tuple-shaped fault/capacity output with `MaterializationFaults` and `CrossingCapacity`; clients do not reconstruct domain usage, phase, semantic faults, or capacity dimensions from events and tuple positions. + +Generated `BlockResourceMeterExtension` execution (`10,896,000 / 1,560`, one read and one write) reserves each signed extrinsic's complete declared maximum from Shared Economic state during prepare, settles valid `PostDispatchInfo` actual Weight after dispatch, and rejects missing, stale, over-capacity, or inconsistent state with distinct transaction-invalidity evidence. Runtime block policy assigns 50% to dispatch and 50% guaranteed `on_idle` headroom. No dedicated Operational reserve exists while no concrete critical Operational call consumes one. `ActorOnIdleReserve` binds directly to `1,000,000,000,000 / 2,500,000`. + +Resource acceptance is layered rather than implemented as a second execution engine. `types::resource::tests` exhausts empty, partial, saturated, borrowing, component-fragmented, settlement, and corruption algebra under the protocol-fixed equal-thirds budget selected by `actors/EXP-0022`. `production_full_block_resource_harness_records_actor_and_user_contention` composes the real mandatory prepass, one Actor effect, one signed external dispatch, Actor Drain, and finalized telemetry in one runtime block. Wakeups, Crossing, and broad fanout contribute `14%`, `10%`, and `20%` of maximum block Weight to one `44%` shared materialization envelope. Generated maximum-unit minima for all three families fit together with positive RefTime and ProofSize remainder. A persisted cursor rotates the first family, reserves every later minimum, lends the remainder to the first family, and may return trailing unused reservations through one counter-preserving second grant. The `25,283,000 / 5,982` coordinator owner includes ten reads and one write for cursor state and bounded remaining-work classification. Fixed scheduler base, coordinator, mandatory cleanup, and the shared envelope are subtracted once; the residual is the exact Actor execution floor. -The runtime stores one refundable Trigger-state bond in `ActorFunding` for every Active User: one ED for Manual, two for AddressEvent, two for ObservationChange, five for ObservationCrossing, and two for Cadenced. The same runtime policy reserves before topology commit, adjusts on replacement, releases on deactivation or every terminal close, reports the benchmark maximum, and quotes prospective amounts through `ActorEligibilityApi` version `4`. System Actors are exempt. Fresh-genesis Actors storage version `9` includes canonical Trigger runtime state, exact User Crossing counts, bond state, and the materialization-family cursor; the pre-`1.0` reference line defines no deployed-state migration. +`ActorFunding` stores bounded tracked-asset funding only; it carries no Trigger-family amount or machine-fee ledger. Fresh-genesis Actors storage version `15` includes C6 fragment-local resources, compact activation authority, the control-only `PipelineMachineEnvelope`, split run head/payload state, exact User Crossing counts, the materialization-family cursor, one transient block-resource state, and one latest finalized non-authoritative telemetry snapshot; the pre-`1.0` reference line defines no deployed-state migration. System and User actors share one paged FIFO with a single global ticket order; no actor class reserves an execution share, weight slice, or service right (spec 8.1.8). Housekeeping, observation fanout, wakeup work, cleanup, and admission bookkeeping remain outside the actor-service pass; within that pass, strict head-of-line order and the shared cutoff alone bound service. -Creation, activation, fresh custody reattachment, and plan replacement reject any actor whose scheduler, complete attempt, and pure cleanup do not fit the guaranteed two-dimensional envelope. +Creation, activation, fresh custody reattachment, and plan replacement reject any Actor Contract whose generated scheduler, complete bounded Pipeline Machine, and pure cleanup resources do not fit the guaranteed two-dimensional envelope. User sovereign service funding is not required until a ready Opening. ## Reactive Delivery Evidence -The runtime binds `MaxObservationFanoutPagesPerBlock = 64` with a `400,000,000,000 / 1,000,000` fanout contribution and `MaxWakeupsPerBlock = 512` with a `280,000,000,000 / 700,000` wakeup contribution to the shared envelope. The complete five-partition actor probe makes production ProofSize admit one worst-case fanout unit after base admission. +The runtime binds `MaxObservationFanoutPagesPerBlock = 64` with a `400,000,000,000 / 1,000,000` fanout contribution and `MaxWakeupsPerBlock = 512` with a `280,000,000,000 / 700,000` wakeup contribution to the shared envelope. Compact activation authority keeps ordinary fanout independent of unused maximum Contract resources. Base plus branch probe, the component-wise queue/wakeup/coalesced/blocked maximum, and reserved fault-record Weight admit two complete ordinary pages before RefTime binds. | Production topology after publication | Fanout service units | Blocks at one unit | | --- | ---: | ---: | -| One feed with 10,000 subscribers | 157 | 157 | +| One feed with 10,000 subscribers | 157 | 79 | | One sparse subscriber at a historical high page id | 1 | 1 | -| Four dirty feeds with 10,000 subscribers each | 628 | 628 | -| One revision restart after quiescence | At most 314 | 314 | +| Four dirty feeds with 10,000 subscribers each | 628 | 314 | +| One revision restart after quiescence | At most 314 | 157 | | Persistently saturated queue | Unbounded | Unbounded | -A fanout service unit is one occupied-page attempt or one cursorless restart/cleanup transition; final-page completion may clear or restart within its page unit. The finite rows require stable active topology, no newer selected-feed revision, available configured RefTime/ProofSize budget, eventual queue capacity, and same-finalized-block runtime/code/metadata/constants matching generated client evidence. Mismatch withholds the rows while retaining factual chain topology. Estimates end at fanout completion, not queue admission, Predicate evaluation, or actor attempt. +An ordinary fanout service unit is one exact subscriber-page turn. Capacity pressure retains the exact page and subscriber position for next-block retry; terminal cleanup uses a separate scalar turn. The finite rows require stable active topology, no newer selected-feed revision, available configured RefTime/ProofSize budget, eventual queue or wakeup capacity, and same-finalized-block runtime/code/metadata/constants matching generated client evidence. Estimates end at fanout completion, not Actor execution. -Production fanout base is `31,076,000 / 1,543` (`6,076,000` benchmark RefTime plus one DbWeight read); the completing dense unit is `17,373,176,000 / 718,430` (`1,898,176,000` benchmark RefTime plus 331 reads and 72 writes). ProofSize therefore remains the active one-unit limit under the configured budget. +Production P32 fanout base is `56,565,000 / 1,629`; the branch probe is `63,759,000 / 3,587`; and the component-wise ordinary maximum is `155,840,020,000 / 304,734`. Reserved fault record is `197,000,000 / 4,106`. One admitted ordinary page owns `156,100,779,000 / 312,427` after the base. Two pages plus base consume `312,258,123,000 / 626,483`; a third exceeds the `400,000,000,000` RefTime envelope, so RefTime binds before ProofSize. -The accepted Actors production run used `frame-omni-bencher 0.22.0`, production Wasm, 50 steps, and 20 repeats. `template/runtime/src/weights/pallet_deos_actors.rs` owns the complete current scheduler, wakeup, ingress, task, predicate, and orchestration coefficients. Those values price measured bounded topology only; they imply no throughput promise. +The current Actors production generation used `frame-omni-bencher 0.22.0`, benchmark Wasm, 50 steps, and 20 repeats. `template/runtime/src/weights/pallet_deos_actors.rs` owns the complete current scheduler, wakeup, ingress, task, predicate, and orchestration coefficients. Those values price measured bounded topology only; they imply no throughput promise. ## Generated Evidence and Artifacts `scripts/actors-assurance.sh` owns freshness checks for Actors semantic, fee-envelope, ABI, observation, ingress, weight, and metadata evidence. Production Wasm, metadata, descriptors, and generated client evidence remain owned by their build/export commands and checked directly by the `full` validation profile. -`template/runtime/src/weights/pallet_deos_actors.rs` owns complete generated methods and storage annotations. Architecture records only load-bearing admission values; benchmark-host timing never becomes a chain-throughput claim. +`template/runtime/src/weights/pallet_deos_actors.rs` owns complete generated methods and storage annotations. The converged handoff binds generated Actors Weight `ac206ec06b3f2c2789da23540ca1ae87d343c8d2196f77b1c12c43569c0d3b9e`, production Wasm `484d7f9aa9eb4d767bb7ecaefce05d6c50c7d25b9b381454872e50ee35272fb6`, and metadata `27984891721c42acbce79d4e458e9b40dd6b9a046228438a072f4f2c1bd0f74e`. `scripts/actors-assurance.sh` reports and preserves those identities while running exact named heavy profiles. Architecture records only load-bearing admission values; benchmark-host timing never becomes a chain-throughput claim. ### Generated Event Trace Corpus @@ -286,8 +300,8 @@ These non-normative traces project the package event-order fixtures; Section 8 o | Scenario | Ordered event trace | Falsification anchor | | --- | --- | --- | -| Fresh transfer cycle | `CycleStarted -> TransferExecuted -> CycleSummary(Completed)` | Fresh simulation and package cycle-order tests | -| Temporary retry attempt | `CycleContinued -> StepFailed(Temporary) -> CycleSuspended(Temporary)` | `continuation_*` and retry-bound package tests | +| Fresh transfer cycle | `CycleStarted -> TransferExecuted -> CycleSummary(Completed) -> ActionFeeCharged` | Fresh simulation and package cycle-order tests | +| Temporary retry attempt | `CycleContinued -> StepFailed(Temporary) -> CycleSuspended(Temporary) -> ActionFeeCharged` | `continuation_*` and retry-bound package tests | | Cancel on semantic update | `CycleCancelled -> CycleSummary(Cancelled) -> ContractUpdated` | Semantic replacement cancellation tests | | Close with Continuation | `CycleCancelled(Closing) -> CycleSummary(Cancelled) -> ActorClosed` | Continuation close-order tests | | Expiry during suspension | `CycleCancelled(Closing(WindowExpired)) -> CycleSummary(Cancelled) -> ActorClosed(WindowExpired)` | Window-expiry Continuation tests | @@ -296,13 +310,17 @@ The corpus intentionally omits block numbers, balances, and exhaustive step fiel ## Control Plane and Read Surfaces -Canonical active projection joins `ActorIdentities`, `ActorHot`, and `ActorContract` at one finalized block; funding details require the separately bounded `ActorFunding` value. Dormant identity, queue/wakeup membership, active-dirty topology, and bounded simulation results remain canonical-chain truth. +Canonical active projection joins `ActorIdentities`, `ActorHot`, certified C6 Contract fragments, compact admission identity, bounded funding, and optional split run head/payload at one finalized block. Dormant identity, queue/wakeup membership, active-dirty topology, and bounded simulation results remain canonical-chain truth. The runtime simulation API executes the same package evaluator and finalizer used by scheduler service. Its bounded records carry canonical `StepOutcome` values, including concrete failure cause plus retry disposition, and its status is the shared `AttemptDisposition`; DEOS adds no adapter-side simulation model. -Version 4 `ActorEligibilityApi` reports `NotRegistered`, `Dormant`, or the canonical Active classification at one finalized block, preserving terminal reason plus exact retry/block/timestamp-tick payloads; Crossing phase, installation revision, pending/processing revisions, latch, and placement are semantic fields. Its companion `materialization_faults` method projects only the bounded current Crossing, broad-fanout, and wakeup faults without pages, radix geometry, or fault history; `crossing_capacity` returns the runtime's 9,000 User / 10,000 total per-feed policy and exact current semantic counts; `trigger_state_bond` quotes prospective User custody from the exact runtime policy consumed by dispatch. The browser therefore never reimplements cadence, cooldown, schedule window, retry backoff, breaker, latch, or detector topology logic. It is canonical-chain truth at the queried block and never promises service, because queue position and available Weight decide actual admission. +Version 6 `ActorEligibilityApi` reports `NotRegistered`, `Dormant`, or the canonical Active classification at one finalized block, preserving terminal reason plus exact retry/block/timestamp-tick payloads; Crossing phase, installation revision, pending/processing revisions, latch, and placement are semantic fields. Its companion `materialization_faults` method projects only the bounded current Crossing, broad-fanout, and wakeup faults without pages, radix geometry, or fault history, while `crossing_capacity` returns the runtime's 9,000 User / 10,000 total per-feed policy and exact current semantic counts. The browser therefore never reimplements cadence, cooldown, schedule window, retry backoff, breaker, latch, or detector topology logic. It is canonical-chain truth at the queried block and never promises service, because queue position and available Weight decide actual admission. + +`ActorCostApi` binds the DEOS `Balance` and returns one bounded canonical quote. User Creation Fee, current family-specific Trigger Weight/fee, upfront Pipeline Machine/cleanup amounts, current maximum Action-effect Weight/fee, and geometry-bound state hold remain separately named. Trigger provenance hashes the six generated occurrence owners; Pipeline provenance carries the admitted runtime/Weight identities; state-hold provenance exposes DEOS pricing of one ED per present component plus one `MICRO_UNIT` per retained SCALE byte. System Actors report zero Actors fees and explicit hold exemption without resource privilege. + +`automation/cost.ts` projects `ActorCostApi` without recombining owners and maps committed `ActionFeeCharged` receipts by exact Actor/Cycle/Step coordinates; `adapters/blockchain/actor-cost.ts` invokes the typed API at one caller-supplied finalized hash and returns explicit unavailability instead of local fee inference. `automation/cost-vectors.ts` fail-closes malformed identities, totals, strategies, family/geometry coverage, System exemption, and dormant semantics before generated runtime vectors enter browser validation. -The browser Trigger editor rejects invalid hysteresis, discloses no-retrofire/rearm semantics, distinguishes typed User-capacity versus total-capacity atomic failures, directs authors to same-block `crossing_capacity`, warns that broad-fanout service scales with subscribed pages, explains latched-fire coalescing and FIFO backpressure, and shows the runtime-generated refundable bond vector in native base units and labels the finalized runtime quote as authoritative. The vector binds metadata and production Actors Weight identities, covers every Trigger family, and is freshness-checked by project validation; it contains no browser-owned bond formula. The browser's authoring, artifact, matching-Wasm, simulation, observation, and governance-composition surfaces live under `web-client/src/lib/automation/` and `web-client/src/lib/observation/`. They bind metadata and runtime identity rather than recreating pallet semantics. +The browser Trigger editor rejects invalid hysteresis, discloses no-retrofire/rearm semantics, distinguishes typed User-capacity versus total-capacity atomic failures, directs authors to same-block `crossing_capacity`, warns that broad-fanout service scales with subscribed pages, and explains latched-fire coalescing plus FIFO backpressure. It exposes no Trigger-family bond quote because dispatch owns no such economic surface. The browser's authoring, artifact, matching-Wasm, simulation, observation, and governance-composition surfaces live under `web-client/src/lib/automation/` and `web-client/src/lib/observation/`. They bind metadata and runtime identity rather than recreating pallet semantics. Unbounded history, archive search, forecasting records, governance preparation history, and longitudinal telemetry remain materialized-provider work under [`actors-control-plane.contract.en.md`](./actors-control-plane.contract.en.md) and [`read-model.contract.en.md`](./read-model.contract.en.md). @@ -328,19 +346,7 @@ Each task executes inside one task-owned storage transaction. A rejected task re `scripts/actors-assurance.sh` owns package portability, external embedding, runtime integration, scheduler fairness, dense/sparse liveness, 10,000-actor queue stress, Crossing relevance, and occupancy proof commands. Optimized-profile regressions prove that the 10,000-actor fairness/occupancy matrix serves every actor with nonce spread at most four, that 10,000 wakeups due at one block drain completely, and that a 10,000-member Crossing feed performs no placement on a zero-match transition before activating only an eight-actor crossed cohort with unique tickets. The same profile then converges all 10,000 memberships on one threshold, crosses and oscillates the maximum 625-page herd in exactly 10,002 atomic service units per transition, and proves exactly one unchanged live queue or deferred wakeup placement per actor under the 1,024-ticket package fixture. A 17-leaf range spaced across 113 binary orders preserves its suffix cursor after the mock Weight cap admits three leaves and converges in seven worker passes. These are service-unit and configured-fixture measurements, not user-facing time promises. A standard 512-actor mixed-clock regression advances timestamp by 99 ticks at once and proves per-unit clock round-robin, within-clock FIFO, one execution and one canonical placement per actor, bounded completion, and no cadence catch-up burst. -Production-Wasm runs at 50 steps and 20 repeats measure validated-context carry through live-head consumption. The current System cheap-cycle coefficient is `94,182,240 / 2,733` RefTime/ProofSize per actor with five reads and three writes; the alternating System/User coefficient is `120,287,892 / 2,798` with six reads and four writes. Repository toolchain authorities and canonical validation profiles own environment and profile selection; this integration document does not duplicate run timestamps or version pins. - -| Production Weight owner | RefTime / ProofSize / DB | Evidence boundary | -| --- | --- | --- | -| FIFO cheap actor slope | `94,182,240 / 2,733 / 5r 3w` | Loaded live-head service; generated linear coefficient | -| Partial wakeup-page drain | `354,939,000 / 48,075 / 83r 18w` | One maximum 32-entry page unit | -| Broad ObservationChange page | `2,651,008,000 / 718,430 / 332r 72w` | Remains subscriber-proportional; canonical identity joins add one read | -| Crossing no-match transition | `31,918,000 / 6,060 / 5r 2w` | Fixed radix transition setup; zero Actor partition reads | -| Crossing terminal actor | `631,585,000 / 162,782 / 94r 85w` | One matched actor closing through bond-aware scheduler exhaustion and exact source cleanup | -| Crossing same-threshold page | `440,426,000 / 162,782 / 87r 78w` | One bounded membership-page worker unit; herd completion spans units | -| Crossing placed fire | `439,379,000 / 162,782 / 87r 78w` | One admitted candidate through canonical latch and FIFO placement | - -These are generated coefficients before the runtime database schedule is added, except that each row states its generated read/write count explicitly. The rows establish bounded unit cost and read ownership, not block throughput or user-facing latency. +Benchmark-Wasm generation at 50 steps and 20 repeats owns every branch coefficient and exact database term. The current System cheap-cycle slope is `126,316,005 / 3,171` with seven reads and three writes; the alternating System/User slope is `192,451,310 / 3,361` with 11 reads and five writes. The mixed `9,500` Transfer, `400` SwapOut, and `100` control-Actor profile completes starvation-free first traversal in block `679` under continuous signed user dispatch, with zero failed Steps inside the measured `1,301`-block horizon. This is a reproducible production profile, not an execution SLA; congestion and fragmented component capacity may delay any individual Actor. ## Reactive Capacity Ledger @@ -348,14 +354,16 @@ The current production policy has a `44%` shared materialization envelope, exact | Crossing path | Generated owner RefTime / ProofSize | Candidate ceiling | 10,000-candidate reference drain | | --- | ---: | ---: | ---: | -| No-match transition | `31,918,000 / 6,060` transition plus `46,305,000 / 12,200` common probe when candidate classification occurs | 0 | Not a candidate drain | -| Post-installation skip pair | `46,515,000 / 6,112` probe; `66,210,000 / 6,112` execution | 4/block | 2,500 blocks | -| Rearm pair | `58,249,000 / 23,410` probe; `423,874,000 / 162,782` execution | 4/block | 2,500 blocks | -| Coalesced fire pair | `108,884,000 / 23,410` fire-pair probe; `472,973,000 / 162,782` execution | 4/block | 2,500 blocks | -| Placed fire pair | `108,884,000 / 23,410` fire-pair probe; `519,976,000 / 162,782` execution | 4/block | 2,500 blocks | -| Terminal fire | `72,427,000 / 12,200` fire probe; `631,585,000 / 162,782` cleanup execution | 4/block component cap | 2,500 blocks | -| One same-threshold page | Exact two-candidate homogeneous cohorts under `440,426,000 / 162,782` page ownership | 4/block | 2,500 blocks | -| Maximum homogeneous herd | Placed-pair path under the shared envelope | 4/block | 2,500 blocks, or 4h10m at six-second blocks | +| No-match transition | `32,895,000 / 6,060` transition; staged probes remain separate generated owners | 0 | Not a candidate drain | +| Post-installation skip pair | `52,522,000 / 6,156` probe; `74,033,000 / 6,156` execution | 4/block | 2,500 blocks | +| Rearm pair | `86,815,000 / 10,482` probe; `476,185,000 / 162,782` execution | 4/block | 2,500 blocks | +| Coalesced fire pair | `334,335,000 / 10,482` fire-pair probe; `657,217,000 / 162,782` execution | 4/block | 2,500 blocks | +| Placed fire pair | `334,335,000 / 10,482` fire-pair probe; `652,398,000 / 162,782` execution | 4/block | 2,500 blocks | +| Maximum placed batch | `998,886,000 / 81,886` execution after staged probes | 4/block | 2,500 blocks | +| Non-tail placed batch | Component-wise maximum of `795,994,000 / 81,886` emptied-tail and `801,232,000 / 81,886` trimmed-tail execution | 4/block | 2,500 blocks | +| Terminal fire | Generated fire probe; `693,745,000 / 162,782` cleanup execution | 4/block component cap | 2,500 blocks | +| One same-threshold page | Funded scalar owner `560,485,000 / 162,782`; homogeneous batches select their dedicated owner | 4/block | 2,500 blocks | +| Maximum homogeneous herd | Maximum placed-batch path under the shared envelope | 4/block | 2,500 blocks, or 4h10m at six-second blocks | The ledger separates zero-match transition setup, sparse occupied-leaf search, source-page traversal, rearm, coalesced fire, placed fire, and terminal cleanup. The current search owner still couples a newly found sparse threshold to its first candidate; larger candidate-count cohorts and measured sparse-search amortization remain open, so these values must be reselected before final release evidence. diff --git a/docs/asset-conversion.integration.en.md b/docs/asset-conversion.integration.en.md new file mode 100644 index 00000000..f82f786d --- /dev/null +++ b/docs/asset-conversion.integration.en.md @@ -0,0 +1,104 @@ +# Asset Conversion Integration + +## Status and Ownership + +This document owns only concrete DEOS composition of semantic asset identity, physical ledgers, the upstream XYK engine, public Router control, pool lifecycle, LP reverse identity, Oracle topology, and fresh-genesis validation. Reusable Router semantics belong to `template/pallets/router/docs/specification.en.md`; shipped package behavior belongs to `template/pallets/router/docs/architecture.en.md`. + +## Identity Boundary + +`AssetKind` is semantic/domain identity. `LedgerAssetKey` is physical ledger identity: + +```text +ledger_key(Native) = Native +ledger_key(Local(x)) = Assets(x) +ledger_key(Foreign(x)) = Assets(x) +``` + +Canonical admission permits `Local(id)` only outside the `TYPE_FOREIGN` namespace and `Foreign(id)` only inside it. The implicit `u32 -> AssetKind` conversion is removed. Market pairs require canonical endpoints and distinct ledger keys. The shared primitive is consumed by DEOS Router, TMC, the runtime pool locator, Oracle pool admission, and topology integrity. + +## Underlying XYK Engine + +`pallet-asset-conversion` remains the underlying pool, liquidity, quote, reserve, and swap engine. Asset Conversion `Pools` is authoritative pool existence. DEOS does not infer existence from a quote, `PoolEmpty`, an Oracle feed, or an LP binding. + +The runtime carries the minimal full-balance correction from Polkadot SDK PR `#12408`, merge commit `408895c27a5aff4bac99e956df5426983566f8cb`. Provenance, exact source digests, removal condition, and review expiry are recorded in `template/vendor/pallet-asset-conversion/DEOS-PATCH.md`. Cargo resolves exactly one implementation. + +Quote, liquidity withdrawal, and execution reserve calculations use full physical pool balances. Withdrawability remains a separate execution constraint; full reserve truth is not a promise that every unit can be withdrawn under `Preserve`. + +## Canonical Pool Identity + +`DeosPoolLocator` is the lowest common Asset Conversion boundary. Every pool call, quote, reserve read, liquidity mutation, swap, and pallet-to-pallet consumer passes through it. It rejects noncanonical endpoints and same-ledger aliases before deriving storage or pool-account identity. + +```text +valid_pool_pair(a, b) + = canonical(a) + and canonical(b) + and ledger_key(a) != ledger_key(b) +``` + +Router LP bindings and Oracle topology consume this configured canonical pool identity rather than independently inventing pair order. + +## Public Swap Boundary + +DEOS Router is the only public XYK execution surface. Runtime `BaseCallFilter` denies raw Asset Conversion exact-input and exact-output swaps. The allow-list is explicit, so new upstream calls fail closed until reviewed. + +```text +user XYK intent + -> DEOS Router + -> Router fee + -> Asset Conversion execution +``` + +Router exposes signed exact-input call `0` and exact-output call `2`. Exact-output `max_amount_in` includes the Router fee and routed XYK input. User fee conservation is transactional: sender input equals Router fee plus routed input, and the Burn Actor receives exactly the Router fee. + +## Pool Lifecycle + +Signed Router call `3` is the permissionless DEOS pool lifecycle. It owns one transaction: + +```text +canonical pair validation +-> canonical pool identity +-> expected LP identity +-> LP namespace/capacity/collision preflight +-> Oracle topology preflight +-> underlying pool creation +-> actual LP identity verification +-> LP reverse binding +-> forward and reverse Oracle feeds +-> commit +``` + +Any failure restores the complete state root. Raw public Asset Conversion creation is filtered. Production runtime code contains one direct underlying `create_pool` call inside this lifecycle, enforced by `scripts/audit-asset-conversion-boundaries.sh`. `add_liquidity` never creates or repairs topology. + +`PoolIndexExtension` is removed from the signed transaction format. Because the 0.x line is fresh-genesis, `transaction_version` remains `1`; metadata and signed-extension evidence must nevertheless be regenerated from the changed format. + +## Fee Domains + +The runtime configures independent parameters: + +| Domain | Parameter | Launch value | Recipient/effect | +| --- | --- | ---: | --- | +| XYK swap | `XykLpFee` | `0%` | No implicit LP swap revenue | +| Liquidity withdrawal | `LiquidityWithdrawalFee` | `0%` | Independent withdrawal math | +| DEOS Router | `RouterFee` | `0.5%` | Burn Actor fee flow | + +Changing one domain must not alter another domain's quote or withdrawal contract. + +## Failure Semantics + +Noncanonical identity, physical alias, invalid path, absent pool, LP mismatch, and topology corruption are Permanent. Empty or temporarily insufficient liquidity, output withdrawability, and market-dependent slippage may be RetryLater. Unknown Asset Conversion failures fail closed as Permanent. + +`PoolEmpty` describes liquidity state only. It does not establish pool registration; `Pools` remains authoritative. + +## Genesis Boundary + +Raw Asset Conversion genesis pools are unsupported because upstream genesis creation cannot atomically establish DEOS LP reverse identity and Oracle topology. Runtime genesis validation fails hard if an earlier builder creates any pool or orphan LP binding. Every DEOS profile starts with empty pool topology; permissionless complete pools are created after genesis through the canonical lifecycle. + +This narrower fresh-genesis baseline excludes one-sided liquidity, same-ledger pairs, duplicate pairs, LP collisions, incomplete reverse identity, and incomplete Oracle topology without migration or repair semantics. + +## Integrity and Evidence + +Runtime `try_state` validates both topology directions, exact LP identity, LP asset existence, canonical pair storage, physical-pair uniqueness, required forward/reverse Oracle feeds, and exact pool/index cardinality. + +Adversarial tests cover semantic aliases, raw swap and creation bypass, unrelated non-sufficient assets, protected balances, LP capacity, LP collision, LP mismatch, post-pool failure, Oracle capacity, missing/orphan bindings, missing feeds, and injected physical-alias pools. Every lifecycle rejection proves no partial mutation; post-mutation cases compare the exact state root. + +Production canonical creation Weight generated at 50 steps × 20 repeats is `144,923,000 / 34,255` with 13 reads and 10 writes. Final release closure regenerates metadata, signed-extension evidence, Router/runtime Weight, Wasm, and exact-tree provenance together. diff --git a/docs/core.architecture.en.md b/docs/core.architecture.en.md index c6b150dc..b45cba47 100644 --- a/docs/core.architecture.en.md +++ b/docs/core.architecture.en.md @@ -176,7 +176,7 @@ Block N: - `Budget-capped`: Every housekeeping, queue, cycle, and close unit starts only after two-dimensional admission against the remaining `on_idle` budget. - `Deterministic FIFO fairness`: Monotonic tickets preserve bounded FIFO carry-over without class-weight knobs or queue reconstruction; measured stress profiles, rather than a System/User alternation claim, own the current fairness SLO. - `Starvation observability`: After the fixed hook base is admitted, a live FIFO head stalled by Weight, fee collection, or an invariant advances sparse `IdleStarvationState`; detection and recovery remain observability-only and never dispatch emergency work in `on_initialize`. -- `Deferred service`: Weight blockage and rolled-back fee-infrastructure failure retain the same FIFO head for later service; User fee-budget insufficiency remains terminal (`FeeBudgetExhausted`). +- `Deferred service`: Weight blockage and rolled-back fee-infrastructure failure retain the same FIFO head for later service; only Idle consumption of paid readiness tests complete User Pipeline capacity, and shortfall selects `CycleAdmissionInsufficient` without refunding prior Trigger fees. ### 4.2 Resilience: Backpressure via Cooldown diff --git a/docs/oracle.integration.en.md b/docs/oracle.integration.en.md index 0fd737b6..c7585899 100644 --- a/docs/oracle.integration.en.md +++ b/docs/oracle.integration.en.md @@ -13,7 +13,7 @@ The reusable package contract and implementation remain in [`template/pallets/or | DEOS feed identity, meaning, and provenance | `template/primitives/src/oracle.rs` | | Runtime bounds, origins, pool-feed identity, and Actors hook | `template/runtime/src/configs/oracle_config.rs` | | Canonical LP pair plus directional-feed registration | `template/runtime/src/configs/assets_config.rs` | -| Pool-index and feed-admission weight envelope | `template/runtime/src/configs/pool_index.rs` | +| Atomic pool/LP/feed lifecycle and admission Weight | `template/runtime/src/configs/assets_config.rs`, `template/runtime/src/weights/pallet_deos_router.rs` | | DEOS Router production and consumption | `template/runtime/src/configs/deos_router_config.rs`, `template/pallets/router/src/lib.rs` | | Runtime-generated DEOS Oracle weights | `template/runtime/src/weights/pallet_oracle.rs` | | Pallet index and metadata composition | `template/runtime/src/lib.rs` | @@ -34,9 +34,9 @@ The DEOS runtime mounts DEOS Oracle at pallet index `52`. It bounds global feeds Canonical pool indexing admits one EMA feed at scale `12` for each ordered direction. Both use the DEOS Router pallet account as producer, pre-execution-reserve provenance, zero rejection, and Active lifecycle. Repeated indexing succeeds only when the complete immutable configuration matches. -Two feeds per pool bound Router admission to `500` complete directional pairs. Before pool creation or liquidity mutation dispatches, `PoolIndexExtension` prevalidates the predicted or existing LP token, bounded reverse-index capacity and collisions, both feed identities, and producer capacity. This ordering is required because a transaction-extension post-dispatch error does not roll back successful call state. +Two feeds per pool bound Router admission to `500` complete directional pairs. The permissionless DEOS pool lifecycle prevalidates the expected LP token, bounded reverse-index capacity and collisions, both feed identities, and producer capacity before underlying creation. Pool, actual LP verification, reverse binding, and both feeds share one transactional rollback owner; liquidity mutation never repairs topology. -The pool-index call charges two worst-case DEOS Oracle registration envelopes plus 13 reads and one write for bounded preflight and LP-index work. No pool admission path performs an unbounded feed scan. +Canonical pool creation is measured as one Router call at `144,923,000 / 34,255` with 13 reads and 10 writes. No pool admission path performs an unbounded feed scan. `PoolIndexExtension` is removed from the signed transaction format. ## Router Production and Consumption @@ -60,7 +60,7 @@ Direct publication propagates the exact Actors dispatch error, including `DirtyO These failures are fail-closed availability signals, not deferred work. Operators must repair or clear the bounded dirty topology or restore capacity before retrying the producer operation. A later retry re-enters the same atomic path; no rejected observation revision or notification obligation survives for replay. -The DEOS Oracle dispatch envelope adds the runtime-declared Actors ingress weight to the maximum DEOS Oracle publication branch. Independently metered fanout and actor execution never enter publication weight. +The DEOS Oracle dispatch envelope adds the runtime-declared Actors ingress weight to the maximum DEOS Oracle publication branch. Changed publication may mark only bounded dirty authority: broad `ObservationChange` uses independent P64 subscriber pages, while ordered Crossing membership and preflight use P128 candidate geometry with P64 non-tail continuation. Publication performs none of that deferred page traversal. The shared materialization coordinator meters broad fanout, Crossing, and wakeups against their `20%`, `10%`, and `14%` contributions inside one `44%` Actor Control envelope; generated family minima and rotated lending preserve a positive Actor execution remainder. Independently metered fanout and actor execution never enter publication weight. ## Canonical and Materialized Read Surfaces diff --git a/scripts/README.md b/scripts/README.md index b6612cba..4cc18649 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -79,10 +79,13 @@ Each numbered command is independently callable by a human or CI from any workin Run the local bootstrap chain: tools -> runtime build -> chain spec -> Zombienet, using Polkadot and Omni Node binaries already available in `PATH` or `./bin`. Start the web client directly from `web-client` with `npm run dev`. - [validate-local.sh](./validate-local.sh) - Prepare pinned repository dependencies and directly run the selected `fast`, `heavy`, or `full` validation profile. Fast owns simulator tests and workspace CI; heavy adds client, Actors, and benchmark checks; full additionally prepares the checksum-verified binary bundle, then builds the production runtime and regenerates metadata and client artifacts with zero worktree drift. GitHub Actions runs heavy validation only through the required pull-request `validation-gate`; local full validation owns pre-`1.0` release acceptance. No Skill audit, cache, hidden authority, network run, or release action is involved. + Prepare pinned repository dependencies and directly run the selected `fast`, `heavy`, or `full` validation profile. Fast owns simulator tests and workspace CI; heavy adds client, Actors, and benchmark checks; full additionally prepares the checksum-verified binary bundle, then builds the production runtime and regenerates metadata and client artifacts with zero worktree drift. GitHub Actions runs heavy validation through the required pull-request `validation-gate` with `SKIP_WASM_BUILD=1`; this explicitly omits only Wasm identity while retaining Actors source, Weight, metadata, behavioral, and heavy-profile assurance. Local full validation owns pre-`1.0` release acceptance. No Skill audit, cache, hidden authority, network run, or release action is involved. - [actors-assurance.sh](./actors-assurance.sh) - Shared current-tree Actors proof contract for semantic-manifest and fee-envelope-vector freshness, cross-language semantics, scheduler stress, capacity, and independent-runtime embedding. Historical transition replay is release history rather than a routine validation dependency. + Shared current-tree Actors proof contract for semantic-manifest and fee-envelope-vector freshness, cross-language semantics, scheduler stress, capacity, and independent-runtime embedding. By default it reports and preserves exact source, production Weight, production Wasm, and metadata identities. A caller that intentionally cannot build Wasm may set `REQUIRE_WASM_IDENTITY=0`; the script reports that narrower evidence boundary while retaining every non-Wasm check. Historical transition replay is release history rather than a routine validation dependency. + +- [audit-asset-conversion-boundaries.sh](./audit-asset-conversion-boundaries.sh) + Read-only fail-closed audit requiring exactly one production direct Asset Conversion pool-creation owner inside the atomic DEOS lifecycle and prohibiting LP-binding repair outside that owner. It accepts no arguments except `--help` and may run from any working directory. - [try-runtime-local.sh](./try-runtime-local.sh) Build `deos-runtime` with `try-runtime` and optionally execute live dry-runs against the local parachain RPC. @@ -94,7 +97,7 @@ Before creating a release tag, run `./scripts/validate-local.sh full` against th Commands executed through the shared script harness use compact output by default: successful test, build, lint, documentation, metadata, and benchmark steps print only their label, duration, and result. A failed step prints the last 80 lines and retains its complete output in a temporary log whose path appears in the error. Set `DEOS_VERBOSE=1` to restore live full output, or set `DEOS_FAILURE_TAIL_LINES=N` to change the failure excerpt without enabling verbose mode. - [benchmarks.sh](./benchmarks.sh) - Run pallet benchmarking flows and weight generation helpers. Supports `--extra` for Actors diagnostics, `--extrinsic NAME --output FILE` for focused evidence that must not replace complete production weights, and `--skip-build` when reusing a freshly built benchmark runtime. The [`benchmarking` skill](../.agents/skills/benchmarking/SKILL.md) owns case selection, evidence interpretation, weight handoff, and claim boundaries without duplicating this command surface. + Run pallet benchmarking flows and weight generation helpers. `--check` compiles benchmark surfaces and rejects generated Weight files that name Actors storage absent from the current pallet; `--extra` enables Actors diagnostics, `--extrinsic NAME --output FILE` produces focused evidence that must not replace complete production weights, and `--skip-build` reuses a freshly built benchmark runtime. The [`architecture-experiments` skill](../.agents/skills/architecture-experiments/SKILL.md) owns case selection, controlled comparison, evidence interpretation, decision lineage, Weight handoff, and claim boundaries without duplicating this command surface. - [ci-local.sh](./ci-local.sh) Reproduce local CI or select one compact check with `--only`; narrow Cargo work further with `--package NAME`, `--test-filter NAME`, and explicit feature mode. Apply Rust formatting with `--only format --fix`. Agents should prefer this entrypoint over raw Cargo commands. @@ -105,7 +108,7 @@ Commands executed through the shared script harness use compact output by defaul Idempotently prepare the composite live-chain state needed for local wallet, swap, and native-staking UI testing: verify genesis prerequisites, fund Alice, and create or top up the Native/foreign and `NTVE/stNTVE` pools. This is a named admin workflow rather than an atomic numbered leaf because it coordinates several state checks and transactions. - [export-papi-metadata.sh](./export-papi-metadata.sh) - Export native runtime metadata through the committed `deos-runtime` metadata example, regenerate PAPI descriptors, and project observation-inspector runtime evidence from the exact metadata, compressed runtime-code Wasm, runtime constants, production Actors weights, and descriptor identity. This replaces ad hoc metadata export and independently maintained inspector constants. + Export native runtime metadata through the committed `deos-runtime` metadata example in an isolated Cargo target, regenerate PAPI descriptors, and project observation-inspector runtime evidence from the exact metadata, compressed production runtime-code Wasm, runtime constants, production Actors weights, and descriptor identity. Isolation prevents metadata compilation from replacing the production Wasm artifact. This replaces ad hoc metadata export and independently maintained inspector constants. - [bootstrap-native-staking-local.sh](./bootstrap-native-staking-local.sh) Consolidated native staking bootstrap helper. `check` reads canonical `NativeSecurityMode` plus live readiness for the `NTVE/stNTVE` pool and Native Staking Liquidity Actor skeleton; `prepare-calls` emits plan-only Root/governance or signed-operator call data for staking registration, pool creation, and liquidity seeding without synthesizing mode-inactive LP-security actions. It never signs or submits transactions. The staking specification and package architecture own readiness sequencing and authority boundaries; this command remains the single operator surface. diff --git a/scripts/_common.sh b/scripts/_common.sh index d21a744c..efad2e3d 100644 --- a/scripts/_common.sh +++ b/scripts/_common.sh @@ -74,14 +74,19 @@ activate_pinned_node() { return 0 fi local nvm_dir="${NVM_DIR:-$HOME/.nvm}" - if [[ ! -s "$nvm_dir/nvm.sh" ]]; then - log_error "Node $expected is required and nvm is unavailable at $nvm_dir/nvm.sh" + if [[ -s "$nvm_dir/nvm.sh" ]]; then + # shellcheck disable=SC1090 + source "$nvm_dir/nvm.sh" + nvm install "$expected" + nvm use "$expected" >/dev/null + elif command -v fnm >/dev/null 2>&1; then + eval "$(fnm env --shell bash)" + fnm install "$expected" + fnm use "$expected" >/dev/null + else + log_error "Node $expected is required and neither nvm nor fnm is available" exit 1 fi - # shellcheck disable=SC1090 - source "$nvm_dir/nvm.sh" - nvm install "$expected" - nvm use "$expected" >/dev/null actual="$(node --version)" actual="${actual#v}" [[ "$actual" == "$expected" ]] || { log_error "Unable to activate Node $expected"; exit 1; } diff --git a/scripts/actors-assurance.sh b/scripts/actors-assurance.sh index f2f357bb..969c51b1 100755 --- a/scripts/actors-assurance.sh +++ b/scripts/actors-assurance.sh @@ -6,6 +6,11 @@ source "$(dirname "${BASH_SOURCE[0]}")/_common.sh" CARGO_PROFILE="${CARGO_PROFILE:-release}" INCLUDE_OCCUPANCY_PROFILE="${INCLUDE_OCCUPANCY_PROFILE:-1}" QUICK_MODE="${QUICK_MODE:-0}" +REQUIRE_WASM_IDENTITY="${REQUIRE_WASM_IDENTITY:-1}" +EVIDENCE_SOURCE_IDENTITY="" +EVIDENCE_WEIGHT_IDENTITY="" +EVIDENCE_WASM_IDENTITY="" +EVIDENCE_METADATA_IDENTITY="" REQUIRED_HEAVY_PROFILES=( "scheduler_stress_fifo_over_capacity_fairness_matrix" @@ -13,11 +18,15 @@ REQUIRED_HEAVY_PROFILES=( "scheduler_stress_fifo_sparse_topology_long_run_liveness" "stress_10k_actors_queue_scheduler" "checkpoint_a_s6_dense_10k_wakeups_converge_without_drops" + "transfer_10k_manual_and_reactive_first_traversal" + "transfer_10k_first_traversal_with_continuous_user_dispatch" + "mixed_9500_transfer_400_swapout_100_control_first_traversal" ) PACKAGE_HEAVY_PROFILES=( "crossing_scale_10k_zero_match_small_cohort_and_maximum_herd" "breaker_materializes_maximum_mixed_wakeup_crossing_and_broad_fanout_without_execution_loss" "crossing_mixed_dense_sparse_directional_lifecycle_profile" + "maximum_dormant_identity_population_adds_no_idle_scan" ) OCCUPANCY_HEAVY_PROFILE="profile_scheduler_queue_wakeup_occupancy_10k" DIAGNOSTIC_HEAVY_PROFILES=("profile_scheduler_wallclock_matrix") @@ -39,6 +48,7 @@ Environment: CARGO_PROFILE=release|dev INCLUDE_OCCUPANCY_PROFILE=0|1 QUICK_MODE=0|1 + REQUIRE_WASM_IDENTITY=0|1 Require and preserve production Wasm (default: 1) DEOS_VERBOSE=0|1 DEOS_FAILURE_TAIL_LINES=N EOF @@ -71,10 +81,83 @@ check_prerequisites() { phase_banner "Step 1: Prerequisites" require_directory "$TEMPLATE_DIR" "Template directory" hydrate_local_tool_paths - require_commands cargo npm + require_commands cargo npm git sha256sum + [[ -f "$TEMPLATE_DIR/runtime/src/weights/pallet_deos_actors.rs" ]] || { + log_error "Actors production Weight not found" + return 1 + } + if [[ "$REQUIRE_WASM_IDENTITY" == "1" ]]; then + [[ -f "$TEMPLATE_DIR/target/release/wbuild/deos-runtime/deos_runtime.compact.compressed.wasm" ]] || { + log_error "Production runtime Wasm not found" + return 1 + } + else + log_warning "Production Wasm identity disabled by the caller; source, Weight, metadata, and behavioral assurance remain required" + fi + [[ -f "$PROJECT_ROOT/web-client/.papi/metadata/deos.scale" ]] || { + log_error "Runtime metadata not found" + return 1 + } log_success "Release gate prerequisites checked" } +worktree_evidence_identity() { + { + git -C "$PROJECT_ROOT" diff --binary --no-ext-diff + while IFS= read -r -d '' path; do + sha256sum "$PROJECT_ROOT/$path" + done < <(git -C "$PROJECT_ROOT" ls-files --others --exclude-standard -z | sort -z) + } | sha256sum | awk '{print $1}' +} + +capture_evidence_identity() { + EVIDENCE_SOURCE_IDENTITY="$(worktree_evidence_identity)" + EVIDENCE_WEIGHT_IDENTITY="$(sha256sum "$TEMPLATE_DIR/runtime/src/weights/pallet_deos_actors.rs" | awk '{print $1}')" + if [[ "$REQUIRE_WASM_IDENTITY" == "1" ]]; then + EVIDENCE_WASM_IDENTITY="$(sha256sum "$TEMPLATE_DIR/target/release/wbuild/deos-runtime/deos_runtime.compact.compressed.wasm" | awk '{print $1}')" + else + EVIDENCE_WASM_IDENTITY="not-required" + fi + EVIDENCE_METADATA_IDENTITY="$(sha256sum "$PROJECT_ROOT/web-client/.papi/metadata/deos.scale" | awk '{print $1}')" + log_info "Evidence source: $EVIDENCE_SOURCE_IDENTITY" + log_info "Evidence Weight: $EVIDENCE_WEIGHT_IDENTITY" + log_info "Evidence Wasm: $EVIDENCE_WASM_IDENTITY" + log_info "Evidence metadata: $EVIDENCE_METADATA_IDENTITY" +} + +verify_evidence_identity_unchanged() { + local source_identity weight_identity wasm_identity metadata_identity + source_identity="$(worktree_evidence_identity)" + weight_identity="$(sha256sum "$TEMPLATE_DIR/runtime/src/weights/pallet_deos_actors.rs" | awk '{print $1}')" + if [[ "$REQUIRE_WASM_IDENTITY" == "1" ]]; then + wasm_identity="$(sha256sum "$TEMPLATE_DIR/target/release/wbuild/deos-runtime/deos_runtime.compact.compressed.wasm" | awk '{print $1}')" + else + wasm_identity="not-required" + fi + metadata_identity="$(sha256sum "$PROJECT_ROOT/web-client/.papi/metadata/deos.scale" | awk '{print $1}')" + if [[ "$source_identity" != "$EVIDENCE_SOURCE_IDENTITY" ]]; then + log_error "Actors assurance changed the source identity" + return 1 + fi + if [[ "$weight_identity" != "$EVIDENCE_WEIGHT_IDENTITY" ]]; then + log_error "Actors assurance changed the Weight identity" + return 1 + fi + if [[ "$wasm_identity" != "$EVIDENCE_WASM_IDENTITY" ]]; then + log_error "Actors assurance changed the Wasm identity" + return 1 + fi + if [[ "$metadata_identity" != "$EVIDENCE_METADATA_IDENTITY" ]]; then + log_error "Actors assurance changed the metadata identity" + return 1 + fi + if [[ "$REQUIRE_WASM_IDENTITY" == "1" ]]; then + log_success "Actors assurance retained exact source, Weight, Wasm, and metadata identities" + else + log_success "Actors assurance retained exact source, Weight, and metadata identities; caller explicitly omitted Wasm identity" + fi +} + required_heavy_profiles() { printf '%s\n' "${REQUIRED_HEAVY_PROFILES[@]}" if [[ "$INCLUDE_OCCUPANCY_PROFILE" == "1" ]]; then @@ -118,7 +201,7 @@ verify_heavy_profiles_resolve_exactly_once() { run_gate() { run_shell_step "Actors gate: fee-envelope vector freshness" "" "cd \"$TEMPLATE_DIR\" && cargo run -q --locked -p pallet-deos-actors --example fee_envelope_vectors -- --check ../web-client/src/lib/automation/actors-fee-envelope-vectors.json" - run_shell_step "Actors gate: Trigger bond vector freshness" "" "cd \"$TEMPLATE_DIR\" && cargo run -q --locked -p deos-runtime --example trigger_bond_vectors -- --check ../web-client/src/lib/automation/actors-trigger-bond-vectors.json" + run_shell_step "Actors gate: runtime cost vector freshness" "" "cd \"$TEMPLATE_DIR\" && cargo run -q --locked -p deos-runtime --example actor_cost_vectors -- --check ../web-client/src/lib/automation/actors-cost-vectors.json" run_shell_step "Actors gate: ABI manifest drift" "" "cd \"$PROJECT_ROOT/web-client\" && npm run check:actors-abi" run_shell_step "Actors gate: normative surface drift" "" "cd \"$PROJECT_ROOT/web-client\" && npm run check:actors-normative-drift" run_shell_step "Actors gate: observation runtime evidence drift" "" "cd \"$PROJECT_ROOT/web-client\" && npm run check:observation-evidence" @@ -187,8 +270,10 @@ main() { parse_args "$@" phase_banner "DEOS Actors assurance" check_prerequisites + capture_evidence_identity log_info "Profile: $CARGO_PROFILE | quick: $QUICK_MODE | occupancy: $INCLUDE_OCCUPANCY_PROFILE" run_gate + verify_evidence_identity_unchanged phase_banner "Summary" log_success "Actors scheduler assurance completed successfully" } diff --git a/scripts/audit-asset-conversion-boundaries.sh b/scripts/audit-asset-conversion-boundaries.sh new file mode 100755 index 00000000..73b625e0 --- /dev/null +++ b/scripts/audit-asset-conversion-boundaries.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=scripts/_common.sh +source "$SCRIPT_DIR/_common.sh" + +usage() { + cat <<'USAGE' +Usage: audit-asset-conversion-boundaries.sh [--help] + +Fail closed unless production runtime code has exactly one direct +AssetConversion::create_pool owner (the atomic DEOS lifecycle) and no external LP-binding repair. + +Inputs: repository source tree. +Outputs: concise audit result. +Side effects: none. +USAGE +} + +parse_args() { + if [[ $# -gt 1 ]]; then + usage >&2 + exit 2 + fi + case "${1:-}" in + "") ;; + -h|--help) usage; exit 0 ;; + *) usage >&2; exit 2 ;; + esac +} + +check_prerequisites() { + require_commands rg +} + +main() { + local runtime="$PROJECT_ROOT/template/runtime/src" + local creation_matches repair_matches + creation_matches="$(rg -n 'AssetConversion::create_pool' "$runtime" \ + --glob '*.rs' --glob '!**/tests/**' --glob '!**/tests.rs' || true)" + if [[ "$(printf '%s\n' "$creation_matches" | grep -c . || true)" -ne 1 ]] \ + || [[ "$creation_matches" != *"configs/assets_config.rs:"* ]]; then + printf '%s\n' 'Unowned direct Asset Conversion pool creation detected:' >&2 + printf '%s\n' "$creation_matches" >&2 + exit 1 + fi + repair_matches="$(rg -n 'register_pool_lp_pair' "$runtime" \ + --glob '*.rs' --glob '!**/configs/assets_config.rs' --glob '!**/tests/**' --glob '!**/tests.rs' || true)" + if [[ -n "$repair_matches" ]]; then + printf '%s\n' 'LP-binding mutation exists outside the lifecycle owner:' >&2 + printf '%s\n' "$repair_matches" >&2 + exit 1 + fi + printf '%s\n' 'Asset Conversion boundary audit passed: one lifecycle creation owner, no external LP repair.' +} + +parse_args "$@" +check_prerequisites +main diff --git a/scripts/benchmarks.sh b/scripts/benchmarks.sh index 46a05ab0..75f8be0f 100755 --- a/scripts/benchmarks.sh +++ b/scripts/benchmarks.sh @@ -18,6 +18,7 @@ PALLETS=( "pallet_asset_registry" "pallet_governance" "pallet_oracle" + "pallet_session_rotation" "pallet_staking" "pallet_xcm" ) @@ -40,7 +41,7 @@ Options: --repeat N Number of repetitions per benchmark (default: $REPEAT) --all Benchmark all custom pallets --list List available pallets - --check Only verify benchmarks compile (no execution) + --check Verify benchmark compilation and generated storage names (no execution) --extra Include Actors circular-chain diagnostic benchmarks excluded from production weights --extrinsic NAME Benchmark one extrinsic (requires one PALLET_NAME) @@ -192,7 +193,66 @@ check_only() { log_error "Benchmark compilation mutated the canonical production runtime Wasm" return 1 fi - log_success "All benchmarks compile successfully" + audit_generated_weight_storage_names + log_success "Benchmark compilation and generated storage-name audit passed" +} + +audit_generated_weight_storage_names() { + local actor_source="$TEMPLATE_DIR/pallets/actors/src/lib.rs" + require_commands node + node --input-type=module - "$actor_source" "$WEIGHTS_DIR" "$@" <<'NODE' +import fs from 'node:fs'; +import path from 'node:path'; + +const [, , actorSourcePath, weightsDirectory, ...requestedFiles] = process.argv; +const sourceLines = fs.readFileSync(actorSourcePath, 'utf8').split(/\r?\n/); +const storageNames = new Set(); +let pendingStorage = false; +let explicitPrefix; +for (const line of sourceLines) { + const trimmed = line.trim(); + if (trimmed === '#[pallet::storage]') { + pendingStorage = true; + explicitPrefix = undefined; + continue; + } + if (!pendingStorage) continue; + const prefix = trimmed.match(/^#\[pallet::storage_prefix\s*=\s*"([^"]+)"\]$/); + if (prefix) { + explicitPrefix = prefix[1]; + continue; + } + const type = trimmed.match(/^pub type ([A-Za-z0-9_]+)/); + if (type) { + storageNames.add(explicitPrefix ?? type[1]); + pendingStorage = false; + } +} +if (storageNames.size === 0) { + console.error(`Could not derive Actors storage names from ${actorSourcePath}`); + process.exit(1); +} + +const files = requestedFiles.length > 0 + ? requestedFiles + : fs.readdirSync(weightsDirectory) + .filter((name) => name.endsWith('.rs')) + .sort() + .map((name) => path.join(weightsDirectory, name)); +const stale = []; +for (const file of files) { + const text = fs.readFileSync(file, 'utf8'); + const references = new Set([...text.matchAll(/Storage: `Actors::([^`]+)`/g)].map((match) => match[1])); + for (const name of [...references].sort()) { + if (!storageNames.has(name)) stale.push(`${file}: Actors::${name}`); + } +} +if (stale.length > 0) { + console.error('Generated Weight files reference stale Actors storage names:'); + for (const entry of stale.sort()) console.error(` ${entry}`); + process.exit(1); +} +NODE } resolve_runtime_wasm_path() { @@ -241,6 +301,8 @@ verify_weight_file_contract() { local pallet_name="$1" local output_file="$2" + audit_generated_weight_storage_names "$output_file" + if [[ "$pallet_name" == "pallet_oracle" ]]; then local measured_proof measured_proof="$(sed -n '/fn register_feed_new_producer()/,/^[[:space:]]*}/p' "$output_file" \ @@ -305,12 +367,17 @@ verify_weight_file_contract() { "scheduler_paged_mixed_scan" "transaction_extension_ingress_base" "transaction_extension_ingress_notify" + "run_suspend" + "record_crossing_worker_fault" + "record_observation_fanout_worker_fault" + "record_wakeup_worker_fault" "crossing_worker_base" "crossing_transition_unit" "crossing_leaf_unit" "crossing_page_unit" "crossing_actor_unit" "predicate_set_evaluation" + "observation_fanout_blocked_page" ) for benchmark in "${required_runtime_benchmarks[@]}"; do if ! grep -q "fn ${benchmark}" "$output_file"; then @@ -357,7 +424,17 @@ run_pallet_benchmark() { "update_contract_observation_change" "precondition_all_max" "precondition_observation" - "observation_fanout_blocked_page" + "benchmark_monolithic_create" + "benchmark_chunked_create" + "benchmark_monolithic_close" + "benchmark_chunked_close" + "benchmark_monolithic_update" + "benchmark_monolithic_reconstruct" + "benchmark_chunked_reconstruct" + "benchmark_monolithic_load_first" + "benchmark_monolithic_load_tail" + "benchmark_chunked_load_first" + "benchmark_chunked_load_tail" ) local benchmark for benchmark in "${diagnostic_benchmarks[@]}"; do diff --git a/scripts/export-papi-metadata.sh b/scripts/export-papi-metadata.sh index 51fa6e42..d1615bcb 100755 --- a/scripts/export-papi-metadata.sh +++ b/scripts/export-papi-metadata.sh @@ -92,7 +92,7 @@ main() { run_shell_step \ "Export DEOS runtime metadata v$METADATA_VERSION" \ "" \ - "cd '$TEMPLATE_DIR' && cargo run --locked -p deos-runtime --example export_metadata -- '$OUTPUT_PATH' '$METADATA_VERSION'" + "cd '$TEMPLATE_DIR' && CARGO_TARGET_DIR='$TEMPLATE_DIR/target/metadata-export' cargo run --locked -p deos-runtime --example export_metadata -- '$OUTPUT_PATH' '$METADATA_VERSION'" if [[ "$GENERATE_DESCRIPTORS" == "1" ]]; then phase_banner "Generate PAPI descriptors" run_shell_step \ diff --git a/scripts/validate-local.sh b/scripts/validate-local.sh index 5a149b6e..a7731ff2 100755 --- a/scripts/validate-local.sh +++ b/scripts/validate-local.sh @@ -83,7 +83,11 @@ run_fast_checks() { run_heavy_checks() { phase_banner "Step 4: Heavy profile" run_shell_step "Clean web-client validation" "" "cd '$PROJECT_ROOT/web-client' && npm run validate" + if [[ "${SKIP_WASM_BUILD:-0}" == "1" ]]; then + REQUIRE_WASM_IDENTITY=0 run_script_step "Actors assurance without Wasm identity" "actors-assurance.sh" + else run_script_step "Actors assurance" "actors-assurance.sh" + fi run_script_step "Benchmark compilation" "benchmarks.sh" --check } @@ -105,7 +109,8 @@ regenerate_full_artifacts() { run_script_step "Deterministic production runtime" "03-build-runtime.sh" run_script_step "Runtime metadata and descriptors" "export-papi-metadata.sh" run_shell_step "Runtime-derived client evidence" "" "cd '$PROJECT_ROOT/web-client' && npm run generate:actors-abi && npm run generate:ingress-evidence && npm run generate:observation-evidence" - run_shell_step "Package-derived Actors evidence" "" "cd '$TEMPLATE_DIR' && cargo run -q --locked -p pallet-deos-actors --example semantic_manifest -- --check ../web-client/src/lib/automation/actors-semantic-manifest.json && cargo run -q --locked -p pallet-deos-actors --example fee_envelope_vectors -- --check ../web-client/src/lib/automation/actors-fee-envelope-vectors.json && cargo run -q --locked -p deos-runtime --example trigger_bond_vectors -- --check ../web-client/src/lib/automation/actors-trigger-bond-vectors.json" + run_shell_step "Package-derived Actors evidence" "" "cd '$TEMPLATE_DIR' && cargo run -q --locked -p pallet-deos-actors --example semantic_manifest -- --check ../web-client/src/lib/automation/actors-semantic-manifest.json && cargo run -q --locked -p pallet-deos-actors --example fee_envelope_vectors -- --check ../web-client/src/lib/automation/actors-fee-envelope-vectors.json" + run_shell_step "Runtime-derived Actors cost evidence" "" "cd '$TEMPLATE_DIR' && cargo run -q --locked -p deos-runtime --example actor_cost_vectors -- --check ../web-client/src/lib/automation/actors-cost-vectors.json" fingerprint_after="$(worktree_fingerprint)" if [[ "$fingerprint_after" != "$fingerprint_before" ]]; then log_error "Full artifact regeneration changed the candidate worktree" diff --git a/template/Cargo.lock b/template/Cargo.lock index b97c52de..66a18036 100644 --- a/template/Cargo.lock +++ b/template/Cargo.lock @@ -4097,7 +4097,7 @@ dependencies = [ [[package]] name = "deos-primitives" -version = "0.7.23" +version = "0.7.24" dependencies = [ "parity-scale-codec", "polkadot-sdk", @@ -4109,9 +4109,11 @@ dependencies = [ [[package]] name = "deos-runtime" -version = "0.7.23" +version = "0.7.24" dependencies = [ "cumulus-pallet-parachain-system", + "cumulus-primitives-parachain-inherent", + "cumulus-test-relay-sproof-builder", "deos-primitives", "docify", "hex-literal", @@ -4121,6 +4123,7 @@ dependencies = [ "pallet-deos-governance", "pallet-deos-oracle", "pallet-deos-router", + "pallet-deos-session-rotation", "pallet-deos-staking", "pallet-deos-tmc", "parity-scale-codec", @@ -8677,8 +8680,6 @@ dependencies = [ [[package]] name = "pallet-asset-conversion" version = "31.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9372e01886c4a9b625929c2549ac50d67cfe0f8e4daf232d884aecc843fe2557" dependencies = [ "frame-benchmarking", "frame-support", @@ -9403,7 +9404,7 @@ dependencies = [ [[package]] name = "pallet-deos-actors" -version = "0.7.23" +version = "0.7.24" dependencies = [ "parity-scale-codec", "polkadot-sdk", @@ -9417,7 +9418,7 @@ dependencies = [ [[package]] name = "pallet-deos-actors-embedding-fixture" -version = "0.7.23" +version = "0.7.24" dependencies = [ "pallet-deos-actors", "parity-scale-codec", @@ -9427,7 +9428,7 @@ dependencies = [ [[package]] name = "pallet-deos-asset-registry" -version = "0.7.23" +version = "0.7.24" dependencies = [ "deos-primitives", "parity-scale-codec", @@ -9439,7 +9440,7 @@ dependencies = [ [[package]] name = "pallet-deos-governance" -version = "0.7.23" +version = "0.7.24" dependencies = [ "parity-scale-codec", "polkadot-sdk", @@ -9449,7 +9450,7 @@ dependencies = [ [[package]] name = "pallet-deos-oracle" -version = "0.7.23" +version = "0.7.24" dependencies = [ "parity-scale-codec", "polkadot-sdk", @@ -9459,7 +9460,7 @@ dependencies = [ [[package]] name = "pallet-deos-router" -version = "0.7.23" +version = "0.7.24" dependencies = [ "deos-primitives", "pallet-deos-tmc", @@ -9474,7 +9475,7 @@ dependencies = [ [[package]] name = "pallet-deos-router-embedding-fixture" -version = "0.7.23" +version = "0.7.24" dependencies = [ "pallet-deos-router", "parity-scale-codec", @@ -9482,9 +9483,19 @@ dependencies = [ "scale-info", ] +[[package]] +name = "pallet-deos-session-rotation" +version = "0.7.24" +dependencies = [ + "parity-scale-codec", + "polkadot-sdk", + "polkadot-sdk-frame", + "scale-info", +] + [[package]] name = "pallet-deos-staking" -version = "0.7.23" +version = "0.7.24" dependencies = [ "parity-scale-codec", "polkadot-sdk", @@ -9494,7 +9505,7 @@ dependencies = [ [[package]] name = "pallet-deos-tmc" -version = "0.7.23" +version = "0.7.24" dependencies = [ "deos-primitives", "parity-scale-codec", @@ -10091,7 +10102,7 @@ dependencies = [ [[package]] name = "pallet-oracle-embedding-fixture" -version = "0.7.23" +version = "0.7.24" dependencies = [ "pallet-deos-oracle", "parity-scale-codec", diff --git a/template/Cargo.toml b/template/Cargo.toml index cb785651..1b8bf546 100644 --- a/template/Cargo.toml +++ b/template/Cargo.toml @@ -1,5 +1,5 @@ [workspace.package] -version = "0.7.23" +version = "0.7.24" license = "MIT-0" authors = ["DEOS Contributors"] homepage = "https://github.com/atmo-network/deos" @@ -8,7 +8,7 @@ edition = "2024" [workspace] default-members = ["pallets/router", "runtime"] -members = ["pallets/actors", "pallets/actors/embedding-runtime", "pallets/asset-registry", "pallets/router", "pallets/router/embedding-runtime", "pallets/governance", "pallets/oracle", "pallets/oracle/embedding-runtime", "pallets/staking", "pallets/tmc", "primitives", "runtime"] +members = ["pallets/actors", "pallets/actors/embedding-runtime", "pallets/asset-registry", "pallets/router", "pallets/router/embedding-runtime", "pallets/governance", "pallets/oracle", "pallets/oracle/embedding-runtime", "pallets/session-rotation", "pallets/staking", "pallets/tmc", "primitives", "runtime"] resolver = "2" [workspace.lints.rust] @@ -59,6 +59,7 @@ pallet-asset-registry = { package = "pallet-deos-asset-registry", path = "./pall pallet-deos-router = { package = "pallet-deos-router", path = "./pallets/router", default-features = false } pallet-governance = { package = "pallet-deos-governance", path = "./pallets/governance", default-features = false } pallet-oracle = { package = "pallet-deos-oracle", path = "./pallets/oracle", default-features = false } +pallet-session-rotation = { package = "pallet-deos-session-rotation", path = "./pallets/session-rotation", default-features = false } pallet-staking = { package = "pallet-deos-staking", path = "./pallets/staking", default-features = false } pallet-tmc = { package = "pallet-deos-tmc", path = "./pallets/tmc", default-features = false } @@ -83,6 +84,11 @@ smallvec = "1.11" sp-api = { version = "43.0.0", default-features = false } sp-arithmetic = { version = "28.0.1", default-features = false } +# Temporary minimal backport of Polkadot SDK PR #12408 / merge 408895c27a5aff4bac99e956df5426983566f8cb. +# Remove when the selected SDK line contains full-balance Asset Conversion reserve semantics. +[patch.crates-io] +pallet-asset-conversion = { path = "vendor/pallet-asset-conversion" } + [profile.release] opt-level = 3 panic = "unwind" diff --git a/template/pallets/actors/Cargo.toml b/template/pallets/actors/Cargo.toml index fa734552..02fdaf00 100644 --- a/template/pallets/actors/Cargo.toml +++ b/template/pallets/actors/Cargo.toml @@ -31,6 +31,7 @@ frame = { workspace = true } polkadot-sdk = { workspace = true, features = [ "frame-support", "frame-system", + "sp-inherents", "sp-runtime", "sp-weights", ] } diff --git a/template/pallets/actors/README.md b/template/pallets/actors/README.md index 08d1c478..9346d607 100644 --- a/template/pallets/actors/README.md +++ b/template/pallets/actors/README.md @@ -17,8 +17,8 @@ The current kernel/runtime slice provides: - Exactly one `Manual`, `AddressEvent`, `ObservationChange`, or timestamp-tick `Cadenced` trigger per Actor; one-feed subscriptions and latest revisions stay bounded in reusable paged state while independently metered deferred fanout coalesces into the existing readiness latch and scheduler - Bounded `on_idle` execution with sparse Healthy/Starving/Alerted state and one-time detection/recovery events - Fee admission, lifecycle controls, pause/resume, and pure prechecked terminal cleanup -- Sparse progress-preserving Continuation for Mutable actors, with scalar suffix cursor, Temporary-only retry, deterministic cancellation, and no prefix replay -- A bounded `simulate_current_contract` rollback core and versioned `ActorSimulationApi` declaration that require exact stored-contract identity, follow fresh/Continuation readiness, return ordered outcomes, and roll the entire attempt back +- Sparse progress-preserving `ActorRunState` for Mutable suspension, with an open nonce separate from finalized identity, one scalar cursor, exact eligibility, immutable Opening/funding snapshots, exact outcomes, Temporary-only retry, deterministic cancellation, and no prefix replay +- A bounded `simulate_current_contract` rollback core and versioned `ActorSimulationApi` declaration that require exact stored-contract identity, follow fresh/current-run readiness, return ordered outcomes, and roll the entire attempt back - A read-only `actor_eligibility` projection behind the versioned `ActorEligibilityApi` declaration that reports current readiness, the scheduler-owned phase, and the next eligible block by reusing the same cadence/cooldown/window/backoff/breaker/latch owners as admission - Runtime-configured adapters for assets, swaps, liquidity, staking, typed failure retryability, fee collection, direct ingress, and weights; swap adapters receive only the actor account and authoritative immutable `ActorType` through a minimal execution context - Exhaustive package-owned instruction contracts for every Task, Predicate, amount resolution, and error policy, with weight ownership delegated to the single `WeightInfo` interface @@ -48,7 +48,7 @@ Readiness and execution must stay deterministic and bounded: - Hot-path execution happens only under configured per-block limits - Timer readiness uses exact deterministic cadence with no actor-specific phase, probability, or entropy contract - `on_idle` does useful work only with remaining block budget -- Continuation retries reuse the same FIFO/wakeup substrate and admit only the unresolved suffix; they create no second scheduler, inbox, or off-chain correctness dependency +- Suspended-run retries reuse the same FIFO/wakeup substrate and admit only the unresolved suffix; they create no second scheduler, inbox, or off-chain correctness dependency ## Runtime-as-Config rule @@ -73,7 +73,7 @@ Minimal checklist: - Treat example Actor Contracts as reusable Task-language patterns; treat the DEOS/TMCTOL System Actor catalog as one runtime's topology, not as the pallet's required deployment shape. - Classify adapter mutation failures explicitly as Permanent or Temporary; unknown and unsupported failures stay Permanent. - Bind `MaxOpeningSnapshotEntries`, fixed `MaxRetryAttempts`, and generated suspension, retry, completion, cancellation, and suffix-admission weights when Mutable plans expose `RetryLater { max_attempts: 2..=MaxRetryAttempts }`. -- Validate adapter failure atomicity and Mutable User/System Continuation with runtime-local tests when adapters perform multi-step mutations. +- Validate adapter failure atomicity and Mutable User/System run suspension with runtime-local tests when adapters perform multi-step mutations. ## Non-goals of the current slice diff --git a/template/pallets/actors/docs/architecture.en.md b/template/pallets/actors/docs/architecture.en.md index 212ec568..c27b0f0f 100644 --- a/template/pallets/actors/docs/architecture.en.md +++ b/template/pallets/actors/docs/architecture.en.md @@ -17,12 +17,16 @@ The crate assigns no economic roles, assets, recipients, routes, actor IDs, or c ### Design Principles 1. `Deterministic scheduling`: one monotonic paged FIFO plus exact paged temporal wakeups, deterministic ordering, and explicit per-block caps -2. `Execution safety`: explicit weight and fee admission gates before cycle start -3. `Lifecycle correctness`: pause/close transitions are deterministic and reasoned (`FeeBudgetExhausted`, `BalanceExhausted`, `WindowExpired`, etc.) +2. `Execution safety`: exact current-Step control/effect Weight admission, complete Pipeline charging at Opening, and Action-only fee settlement around invocation +3. `Lifecycle correctness`: pause/close transitions are deterministic and reasoned (`CycleAdmissionInsufficient`, `WindowExpired`, etc.) 4. `Adapter isolation`: pallet never embeds DEX pricing logic or asset implementation specifics 5. `Hot-state decomposition`: `ActorHot` carries only measured scheduler/admission facts needed to avoid cold contract or detailed funding decode; the retired synchronized readiness mirror must not return -Canonical writes target `ActorIdentity`, `ActorHot`, `ActorContract`, and `ActorFunding` directly. `ActorFunding.trigger_state_bond` stores the exact runtime-policy amount for every Active User and zero for every System Actor. The host `TriggerStateBond` adapter adjusts refundable owner custody inside the same storage transaction before Trigger topology commits; Active creation and Dormant activation move zero to the family amount, Active replacement moves the stored amount to the prospective family amount, and deactivation or any terminal close moves it to zero before partition removal. TryRuntime recomputes the exact class/Trigger amount; the formula depends only on authored Trigger semantics, never current index occupancy. `TriggerTransitionPlan` is the shared read-only preflight owner for Crossing membership and broad observation subscriptions; it distinguishes genesis installation, Active creation, Dormant activation, Active replacement, deactivation, and close. `crossing.rs` and `subscriptions.rs` return bounded commit inputs, while lifecycle code commits both inside the enclosing control transaction before canonical Contract/Hot replacement. Schedule, placement, and bond planning remain separate implementation work. One crate-private loader reads those partitions plus `ContinuationState` and classifies the actor as `NotRegistered`, identity-only `Dormant`, exact `Active`, or `Corrupt`; exact Active requires all four canonical partitions and Continuation if and only if `cycle_state == Suspended`. Its loaded state has no write-back path, and the public `ActiveActorState` returns the canonical partitions without flattening. No derived context owns authored equality or mutation. +Canonical writes target `ActorIdentity`, `ActorHot`, `ActorContractHead`, `ActorAdmissionCertificate`, bounded `ActorContractTailChunk`, and `ActorFunding` directly. `ActorFunding` stores only bounded tracked-asset funding state; no Trigger-family amount or fee reserve is persisted there. `ActorStateHolds` records one named six-component refundable geometry quote per User Actor under `HoldReason::ActorState`; its run component reserves the type-derived maximum for the Active installed lifetime, so autonomous Opening never needs a new owner hold. Dormant Actors release that component, and System Actors remain host-capacity-backed and hold-exempt. + +`TriggerTransitionPlan` is the shared read-only preflight owner for Crossing membership and broad observation subscriptions; it distinguishes genesis installation, Active creation, Dormant activation, Active replacement, deactivation, and close. `crossing.rs` and `subscriptions.rs` return bounded commit inputs, while lifecycle code commits both inside the enclosing control transaction before canonical Contract/Hot replacement. Schedule and placement planning remain separate implementation work. + +One crate-private loader reconstructs the certified Contract from those partitions, reads optional `ActorRunState`, and classifies the actor as `NotRegistered`, identity-only `Dormant`, exact `Active`, or `Corrupt`; exact Active requires coherent Identity, Hot, C6 Contract geometry, Funding, and run state exactly while `cycle_state` is `Running` or `Suspended`. Its loaded state has no write-back path, and the public `ActiveActorState` returns the canonical partitions without flattening. No derived context owns authored equality or mutation. ### Host Composition Boundary @@ -50,7 +54,7 @@ Execution logic remains in `src/execution.rs`, `src/scheduler.rs`, `src/reaction | `User` | Signed owner + slot namespace | No | User automation | | `System` | Governance origin | Yes | Protocol automation | -User recovery now has an explicit slot-targeted surface: the default `create_user_actor` path allocates the lowest free slot, while `create_user_actor_at_slot` recreates control for a chosen slot/sovereign deterministically. +User recovery has an explicit slot-targeted surface: the default `create_user_actor` path allocates the lowest free slot, while `create_user_actor_at_slot` recreates fresh Mutable control for a released slot and therefore derives the same sovereign account. Close never moves custody, and the recovery Contract can use residual native or asset balances without a rescue subsystem. Current owner-slot representation is fixed-width and runtime-shaped: @@ -62,22 +66,27 @@ Current owner-slot representation is fixed-width and runtime-shaped: ### Current Actor-State Shape -The package stores each actor identity once and each active actor across three active-epoch values: +The package stores each actor identity once and decomposes each active epoch into bounded hot, certified Contract, funding, and optional run owners: - `ActorIdentities`: durable owner, `ActorClass`, mutability, sovereign account, logical-cycle nonce, and non-optional persistent control-mutation block shared by Active and Dormant lifecycle states -- `ActorHot`: typed Active lifecycle/run state, auto-close target, failure counter, `pending_signal`, queue/wakeup membership, terminal block, `schedule_anchor`, and optional `last_cycle_block` -- `ActorContract`: trigger/cooldown schedule, optional execution window, ordered `ContractSteps`, and `Persistent | CloseAfterProductiveCycle` completion policy +- `ActorHot`: typed Active lifecycle/run state, auto-close target, failure counter, `pending_signal`, one queue membership, independent Pipeline-service and Trigger-temporal wakeup pointers, terminal block, `schedule_anchor`, and optional `last_cycle_block` +- `ActorContractHeads`: schedule/completion header, semantic/body/admission commitments, Step count, optional inline Step 0, and its optional control/effect envelope +- `ActorAdmissionCertificates`: compact runtime semantics/layout/Weight and lifecycle identity; encoding is independent of the configured Step/resource ceiling +- `ActorContractTailChunks`: authority-bound gap-free Steps 1..N in chunks of at most four, with one aligned control/effect envelope per Step +- `ActorActivationAuthorities`: fixed ObservationChange activation projections bound to feed, semantic/body/admission identity, cooldown, window, and auto-close nonce - `ActorFunding`: canonical funding-source policy, bounded tracked assets, and bounded `funding_accumulated[asset]` checked deltas +- `ActorRunHeads`: mutable semantic/body/admission authority, current Step and Opening-predicate cursors, cumulative outcomes, causal commit block, eligibility, suspension facts, and immutable-payload commitment/count +- `ActorRunPayloads`: immutable Opening/funding snapshots retained only while a multi-block Cycle is open `ActorCreated` carries `actor_id`, owner, `actor_class`, mutability, sovereign account, and `initial_lifecycle`; User slot or System custody locator lives inside `ActorClass`. `actor_id` exists only as each storage-map key. Dormant identity carries no timestamp. Activation or schedule replacement derives block eligibility from `schedule_anchor` and window start, while `ActorHot.trigger_runtime_state::Cadenced` owns the optional timestamp-ceiled anchor tick. The typed lifecycle forbids contradictory pause state. -Package internals classify `ActorIdentities + ActorHot + ActorContract + ActorFunding + optional ContinuationState` through one crate-private loader before deriving an execution view. External consumers use `active_actor_state`, which returns those canonical partitions without flattening or synchronized mirrors. +Package internals reconstruct the generic execution view from `ActorIdentities + ActorHot + certified C6 Contract geometry + ActorFunding + optional ActorRunHeads/ActorRunPayloads`. Observation fanout instead loads the fixed activation authority and mutable run head needed for placement classification; it does not decode generic funding, immutable run payload, admission resource vectors, or unreached Steps. External consumers use `active_actor_state`, which returns canonical semantic partitions without synchronized mirrors. This is intentionally more concrete than the paired specification: the spec defines the required logical field groups, while this document records the current package storage realization. ### Contract Steps Structure -Each actor stores a bounded `ContractSteps` of ordered `Step`s. One configurable `MaxContractSteps` binding applies identically to User and System actors across creation, activation, replacement, simulation, genesis, and benchmark construction. It must remain in `1..=255`; the current DEOS baseline is `8`. Mutable `RetryLater` admits only `2..=MaxRetryAttempts`, with the protocol-fixed metadata constant set to 10; package integrity checks both bounds and their checked composition. +Each actor admits `0..=MaxContractSteps` ordered Steps and stores them as C6 geometry: optional Step 0 and its optional resource envelope live in the head, while Steps 1..N and aligned envelopes occupy gap-free authority-bound chunks of at most four. A zero-Step Contract has neither inline body nor tail fragments and reconstructs from its certified header alone. One configurable `MaxContractSteps` binding applies identically to User and System actors across creation, activation, replacement, simulation, genesis, and benchmark construction. The configured maximum must remain in `1..=255`; the DEOS production binding is `32`. For nonempty Contracts, the Step-0 control context charges exactly the authored Opening tail-chunk count, from zero for one Step through eight at the production ceiling; later cursors charge no Opening reconstruction. Mutable `RetryLater` admits only `2..=MaxRetryAttempts`, with the protocol-fixed metadata constant set to 10. - `precondition: Option>` - `task: Task` @@ -85,11 +94,11 @@ Each actor stores a bounded `ContractSteps` of ordered `Step`s. One configurable `None` is the sole unconditional Step form. `Some(Precondition { clauses })` stores one bounded DNF: outer clauses are OR and each inner clause is AND. Runtime admission rejects empty outer and inner vectors, caps each dimension at four and each step at four total predicates, evaluates every admitted predicate without short-circuit, and executes or skips the step exactly once. -Each `TimedPredicate` names `ObservationTiming::Opening` or `Current`. Fresh-cycle opening evaluates and stores one `Result` per Opening predicate before any task; Continuation retains the full bounded result vector and reuses it by canonical linear position. Current predicates evaluate immediately before their step and therefore observe successful earlier-step effects in the same attempt. +Each `TimedPredicate` names `ObservationTiming::Opening` or `Current`. Fresh-cycle opening evaluates and stores one `Result` per Opening predicate before any task; ActorRunState retains the full bounded result vector and reuses it by canonical linear position. Current predicates evaluate immediately before their Step and therefore observe committed earlier-Step effects from the same logical multi-block cycle. Admission sorts predicates and clauses by canonical typed SCALE, removes repeated predicates within a clause, rejects clauses that become semantically identical, absorbs exact predicate-superset clauses, and stores only the canonical form. `update_contract` canonicalizes before equality and returns an exact no-op before rate limiting, cancellation, writes, placement reconstruction, or events when the resulting contract is unchanged. -Evaluation fees and cycle/suffix Weight use `evaluation_units = total predicates + Opening predicates`, then chunk the units by `MaxPredicatesPerStep` before calling the benchmarked component. This accounts for opening capture plus full expression visitation without relying on the generated component clamp. A false DNF expression emits `StepSkipped(PreconditionFalse)` and advances one fixed cursor; evaluation errors remain task-independent failures routed through the authored step policy. +Current-Step control fees use `evaluation_units = total predicates + Opening predicates`, then chunk the units by `MaxPredicatesPerStep` before calling the benchmarked component. This accounts for opening capture plus full expression visitation without relying on the generated component clamp. A false DNF expression emits `StepSkipped(PreconditionFalse)` and advances one fixed cursor; evaluation errors remain task-independent failures routed through the authored step policy. `ObservationProvider` is the generic current-scalar boundary. The host receives `feed`, `now`, and `max_age_blocks`; `Fresh` returns both `value` and `observed_at`. Actors accepts Fresh only when `observed_at <= now` and checked age stays within the authored maximum. Future or over-age Fresh maps to `PredicateError::InvalidObservation`, while explicit Unavailable, Uninitialized, and Stale states produce ordinary false results. @@ -121,12 +130,13 @@ Task set in implementation: | Predicate | `BalanceAbove`, `BalanceBelow`, `BalanceEquals`, `BalanceNotEquals`, `BlockNumberAbove`, `BlockNumberBelow`, `ObservationAbove`, `ObservationBelow`, `ObservationEquals`, `ObservationNotEquals` | Active contract calls; `every_predicate_is_pure_and_bounded`, predicate evaluator and observation tests | | Task | `Transfer`, `SplitTransfer`, `SwapIn`, `SwapOut`, `AddLiquidity`, `RemoveLiquidity`, `Burn`, `Mint`, `Stake`, `DonateLiquidity`, `Unstake`, `StopCycle` | Active contract calls; `every_task_has_one_exhaustive_semantic_contract`, task tests, and independent runtime profiles | | Amount and exact-output bound | `Fixed`, `PercentageOfCurrent`, `PercentageAtOpening`, `PercentageOfLastFunding`, `AllAvailable`; `LiveQuote`, `Absolute` | Task constructors; amount classifier/resolution tests and independent exact-output evidence | -| Trigger | Exactly one of `Manual`, `AddressEvent`, `ObservationChange`, `ObservationCrossing`, or `Cadenced`; source `Any`, `OwnerOnly`, `Whitelist`; asset `Any`, `Whitelist` | Actor Contract constructors plus raw typed calls; exhaustive Active replacement and Dormant lifecycle matrices; manual, certified-ingress, observation, timestamp-cadence, and embedding tests | +| Trigger | Exactly one of `Manual`, `AddressEvent`, `ObservationChange`, `ObservationCrossing`, `AtTime`, or `Cadenced`; source `Any`, `OwnerOnly`, `Whitelist`; asset `Any`, `Whitelist` | Actor Contract constructors plus raw typed calls; exhaustive Active replacement and Dormant lifecycle matrices; manual, certified-ingress, observation, timestamp-cadence, and embedding tests | | Funding | `OwnerOnly`, `SignedAllowlist`, `RuntimePolicy`, `AnyVerifiedIngress`; provenance `Signed`, `InternalProtocol`, `Xcm` | Active contract and certified producer constructors; funding-policy package tests and DEOS producer inventory | | Completion and step policy | `Persistent`, `CloseAfterProductiveCycle`; `AbortCycle`, `ContinueNextStep`, `RetryLater` | Active contract constructors; productive-close and exhaustive transition-matrix tests | -| Attempt and step views | `AttemptDisposition::{Completed, Failed, Suspended, Closed}`; `StepOutcome::{Executed, Stopped, Skipped, FundingUnavailable, Failed}` | Shared production evaluator; production/simulation parity tests | +| Attempt and step views | `AttemptDisposition::{Completed, Continued, Failed, Suspended, Closed}`; `StepOutcome::{Executed, Stopped, Skipped, FundingUnavailable, Failed}` | Shared production evaluator; production/simulation parity tests | | Eligibility view | `NotRegistered`, `Dormant`; Active phases `Ready`, `Paused`, `GlobalCircuitBreaker`, `WaitingSignal`, `WaitingRetry`, `WaitingBlock`, `WaitingCadenceTick` plus terminal reason | `actor_eligibility`; `eligibility_projection_*` tests | -| Simulation | `FreshCurrentPlan`, `CurrentContinuation`; every `SimulationError` in specification Section 7.2 | `simulate_current_contract`; package simulation and independent runtime tests | +| Cost quote | Named Creation, family-specific Trigger, upfront Pipeline Machine/cleanup, current maximum Action, and refundable state-hold components with independent Weight/admission identities | `actor_cost_quote`; `actor_cost_quote_keeps_fee_boundaries_and_state_hold_provenance_separate` | +| Simulation | `FreshCurrentPlan`, `CurrentRun`; every `SimulationError` in specification Section 7.2 | `simulate_current_contract`; package simulation and independent runtime tests | | Adapter result | `RetryClass::{Permanent, Temporary}`; scalar observation `Unavailable`, `Uninitialized`, `Fresh`, `Stale` | Host adapters and fail-closed unit implementations; runtime Oracle mapping, retry matrix, and embedding tests | | Event | Every variant in specification Section 8 and runtime metadata | Production `deposit_event` sites; event-order, task, lifecycle, ingress, scheduler, and generated ABI tests | | Error | Every variant in specification Section 9.2 and runtime metadata | Production `ensure!`/error branches; rejection, rollback, exact metadata/spec, package, and embedding tests | @@ -141,7 +151,7 @@ Liquidity tasks also carry fixed non-zero outputs. `AddLiquidity.min_lp_out` rea `StopCycle` executes only after its precondition and ordinary User fee collection succeed. The canonical evaluator produces `StepOutcome::Stopped`, production emits `CycleStopped { actor_id, cycle_nonce, step_index }`, and the shared policy interpreter ends the logical cycle successfully at that cursor. -The shared completion path emits the cumulative summary, leaves later funding accumulation untouched, evaluates completion policy and auto-close, clears a resumed Continuation, and leaves the suffix unreachable. +The shared completion path emits the cumulative summary, leaves later funding accumulation untouched, evaluates completion policy and auto-close, clears the persisted run, and leaves the suffix unreachable. The instruction does not resolve an amount, invoke a runtime adapter, select a successor, or directly mutate actor lifecycle/scheduler state. It increments `executed_steps` but not `committed_effectful_tasks`, so an empty stop cannot close a one-shot productive actor. A false Precondition advances normally; Predicate-evaluation or fee-collection failure occurs before successful stop admission and follows its owning runtime boundary. @@ -171,9 +181,13 @@ Resolution outcomes are deterministic: `FundingUnavailable` is a deterministic resolution outcome for both actor classes. It advances as a non-terminal skip under `AbortCycle` and `ContinueNextStep`, while valid Mutable `RetryLater { max_attempts }` suspends at the current cursor. It covers missing or zero tracked snapshots, tracked-balance overspend, staking-share overspend, and preserve-spend resolution that would cross the minimum-balance ceiling. Untracked assets remain `SnapshotUnavailable`. -`attempt_fee_envelope` owns User per-step evaluation/execution/total fees and checked suffix totals. `settle_attempt_fee_step` releases each selected upper reservation before selecting evaluation-only or attempted-step charge; System settlement stays zero. +`PipelineMachineEnvelope` stores the complete bounded User control/cleanup quote in the certified Contract head; scheduler admission reads that O(1) authority only when Idle paid readiness is consumable. Zero-Step machine work uses generated `scheduler_inner_zero_step_complete`. Nonempty geometry checked-sums each Step's generated maximum control owner across its authored total attempt count; `StopCycle` folds its control-only effect into machine work and remains Action-fee-free. The cleanup component uses generated `close_actor`, not the broader certificate lifecycle maximum. The production Weight identity commits zero-Step, Opening, retry/error, continuation/RunFrame, completion, placement, and cleanup owners. + +Insufficient `MinUserBalance + Pipeline total` selects `CycleAdmissionInsufficient` before Opening, while Running/Suspended never reclassify economically. `StepFeeBreakdown` owns Action effect fees only: non-invoked effects, false Precondition, skipped resolution, and `FundingUnavailable` charge zero; every invoked success or typed failure settles valid actual effect Weight. System settlement stays zero. -Admission, execution, retry admission, simulation, and benchmarks derive the selected full-plan or Continuation-suffix fee envelope from the current bounded contract. Overflow fails before mutation. The package-owned `fee_envelope_vectors` example emits deterministic User/System suffix, release, rollback-pricing, and protected-floor vectors consumed by browser forecasting tests. +`attempt_fee_envelope` and `settle_attempt_fee_step` remain bounded forecast-vector utilities for comparative client evidence; they do not authorize runtime admission or settlement. The package-owned `fee_envelope_vectors` example emits deterministic User/System forecast, release, rollback-pricing, and protected-floor vectors consumed by browser tests. + +The User state hold is paid by the identity owner through the runtime's `StateHoldCurrency` under the aggregate `RuntimeHoldReason::Actors(ActorState)` reason. Each present component prices one configured base plus a configured per-byte rate. Identity uses its concrete locator authority; the Active header combines fixed `ActorHot` capacity with the actual C6 head and admission certificate; body uses only present tail chunks; detector uses actor-owned activation/subscription/Crossing/temporal records without shared-page slack; funding and run use their current encoded state. Create, update, ingress, Opening/progress/suspension, cancellation, deactivation, and close reconcile exact per-Actor deltas in the owning storage transaction. Positive-delta failure rolls back semantic state, while close releases the record without touching sovereign custody. TryRuntime rederives every record and compares each owner's aggregate dedicated currency hold. Resolution and charging follow these rules: @@ -191,9 +205,15 @@ Task execution is wrapped in a task-scoped storage transaction. If an adapter fa The control-flow firewall combines closed types with adversarial evidence. `Step` metadata exposes exactly `precondition`, `task`, and `on_error`; exhaustive contracts admit no successor, nested contract, callback, generic `RuntimeCall`, or opaque dispatch field. Optional `Precondition` classification fixes full bounded visitation, whole-expression error, one admitted task, and false advance. Predicates and amount classifiers expose read dependencies and never a control target. +The scheduler service loads canonical Identity, Hot, Funding, Run, admission certificate, hot Contract head, and exactly one certificate-gated current C6 Step; it does not reconstruct an unreached tail for classification or planning. Admission consumes the resulting `CurrentStepPlanOf` control/effect envelope, and FIFO head consumption atomically revalidates ticket/topology authority immediately before the evaluator executes the carried Step and fee authority. [actors/EXP-0013](../../../../.agents/skills/architecture-experiments/tracks/actors/EXP-0013.md) selected loaded-state reuse after reducing cheap-Step marginal RefTime by 17.87% without changing ProofSize or storage-key models. + +Fresh multi-Step Opening reconstructs the full body only after admission because immutable Opening snapshots require every authored dependency; one-Step Opening and Running/Suspended service remain head/current-fragment only. Post-placement, enqueue/invalidation, tombstone drain, and wakeup schedule/drain also use the current service state, so unreached-tail corruption cannot block an otherwise authoritative running prefix. General activation, eligibility, detector, funding, and liveness helpers remain separate hot-header conversion work. + +The execution kernel visits at most one current Step per actor per block after exact ticket, admission-certificate, resource, fee, and same-block revalidation. A non-terminal commit persists `CycleState::Running`, cumulative outcomes, `last_committed_step_block`, and `eligible_at >= now + 1`; scheduler placement emits one exact successor ticket, while final, abort, suspension, and close branches commit atomically without replaying the prefix. + The execution kernel produces one `StepOutcome` for each visited Step after canonical precondition, amount, fee, and task evaluation. `StepOutcome::Failed(TaskFailure)` retains the concrete `DispatchError` cause and orthogonal `RetryClass`; `resolve_step_control` interprets that outcome with the authored policy without a simulation-only failure vocabulary. -One `AttemptDisposition::{Completed, Failed, Suspended, Closed}` owns final production and simulation meaning. Production emits events and commits state from it; simulation returns the same disposition and final counters while its transaction rolls back. Bounded trace records wrap shared Step outcomes and do not reconstruct task, predicate, amount, fee, failure, or finalization semantics. +One `AttemptDisposition::{Completed, Continued, Failed, Suspended, Closed}` owns production and simulation meaning; `Continued` commits exactly one non-terminal Step and persists the causal successor cursor. Production emits events and commits state from it; simulation returns the same disposition and final counters while its transaction rolls back. Bounded trace records wrap shared Step outcomes and do not reconstruct task, predicate, amount, fee, failure, or finalization semantics. `resolve_step_control` remains the only runtime transition owner, with the exhaustive policy/mutability/failure matrix and same-cursor Continuation regressions pinning retry identity. Task-local transactions forbid callback-visible partial mutation. Bounded vectors, adapter capability contracts, generated worst-case weights, maximum-plan tests, and circular scheduler stress bound adapter and cross-actor work without interpreting local plans recursively. @@ -209,16 +229,28 @@ Adapters return typed task failures. Only explicitly classified Temporary failur Host-specific quotes, route selection, fees, oracle guards, slippage policy, and failure mapping belong in the integration architecture and embedding evidence. +Under `runtime-benchmarks`, the opaque `MaximumContextInherent` helper fixture is prepared outside measurement, dispatched inside `maximum_context_inherent`, and verified afterward. This gives the host runtime one complete maximum-context falsification owner without importing Cumulus payload or relay-proof types into the reusable package contract. + ## Scheduler Architecture ### Hook Separation -- `on_initialize`: - - no Actors work - - zero Actors Weight +- `on_initialize` performs no Actor work; the mandatory inherent is the sole pre-external phase owner. +- `actor_prepass` inherent: + - is payload-free, versioned at the provider-data boundary, Mandatory, fee-free, and required under the runtime inherent contract + - consults the host `PrepassContext` and rejects before mutation unless required Timestamp and parachain consensus context are present + - opens and settles the generated cutoff-capture owner against Actor Control + - freezes `PrepassExecutionCutoff`, performs one bounded saturated-FIFO stale-cleanup quantum, services the rotated wakeup/Crossing/fanout materialization families against reserved Actor Control, and keeps every newly materialized ticket behind that cutoff + - executes the mandatory strict-FIFO base pass against remaining Actor Control and `ActorBaseTurn`, then advances to `ExternalPhase` + - rejects duplicate or stale execution without moving the accepted cutoff - `on_idle`: - - bounded temporal and queue housekeeping - - bounded queue-driven cycle execution using remaining `on_idle` weight + - derives finalization and Drain capacity from current-block Actor Control remaining after Prepass + - pre-reserves the remaining Drain-control maximum and settles generated actual control before final reconciliation + - rejects stale, wrong-phase, or optional-halted resource state before housekeeping mutation + - advances `ExternalPhase -> FreshDrain`, meters queue service against remaining Actor Control and Shared Economic effect capacity, writes the latest non-authoritative finalized snapshot, and finishes successful reconciliation in `Finalizable` +- `on_finalize`: + - consumes and requires the current block's one-pass `Finalizable` marker, zero outstanding reservations, and matching telemetry block tag + - makes any incomplete resource protocol consensus-invalid rather than silently carrying state into the next block ### Admission Gates @@ -237,12 +269,15 @@ Deferral/terminal paths: - insufficient Weight or scan budget → silent state-preserving deferral; actor remains active - pure terminal cleanup prechecks every fallible identity, funding, count, reverse-index, and User-slot invariant before mutation; no close retry or requeue state exists -- Pre-cycle close precedence is deterministic: `WindowExpired` > `BalanceExhausted` > `FeeBudgetExhausted` -- User fee shortfall at admission → terminal `FeeBudgetExhausted` close +- Pre-cycle close precedence is deterministic: `WindowExpired` precedes `CycleAdmissionInsufficient`, then independently owned nonce/failure/lifecycle terminals +- Paid readiness that cannot preserve `MinUserBalance` while paying complete Pipeline Machine/cleanup → terminal `CycleAdmissionInsufficient` process cleanup with custody untouched - `CycleResult::Completed` means authored control reached terminal without an abort: skip-only and all-failed-`ContinueNextStep` runs remain Completed, reset `unsuccessful_attempt_streak`, and may satisfy nonce auto-close. Their counters remain factual; only at least one committed non-`StopCycle` task satisfies productive close. Abort emits `Failed`; explicit invalidation emits `Cancelled`. - Post-failure close is inclusive at `unsuccessful_attempt_streak >= MaxConsecutiveFailures`; an admitted cycle emits its authoritative `CycleSummary` before pure cleanup emits `ActorClosed`, matching post-success `AutoCloseNonceReached` ordering - Explicit, automatic, lifecycle-touch, dormant, and sweep paths share one pure cleanup routine: no Task, Precondition, fee, funding restoration, sovereign-balance movement, or shared queue/wakeup scan occurs +- Signed User owner close requires `Mutable`; User Immutable rejects pause, resume, semantic Contract replacement, auto-close replacement, deactivation, cancellation, and close while still admitting its authored Manual source. Scheduler, sweep, authored completion, auto-close, window, and Pipeline-admission terminals remain runtime-owned. - Active close prevalidates identity, counts, reverse ownership, slot ownership, funding presence, and subscriptions, then commits cancellation, actor-store deletion, counter/locator release, and the close event in one storage transaction; any residual late error rolls back the complete terminal mutation +- Package and runtime tests preserve native and non-native residual custody across Mutable owner close, productive close, User Immutable zero-Step AtTime auto-close, and Pipeline-admission apoptosis; exact-slot recreation receives a fresh actor id and nonce but executes against the same sovereign account +- Pipeline-admission insufficiency meters generated `pipeline_admission_apoptosis` cleanup at `161,616,000 / 5,736` with 15 reads and 15 writes for maximum Contract deletion without Crossing, ObservationChange, temporal, or Run topology. Other terminal paths and the economically collected cleanup upper remain bound to conservative `close_actor`; the narrow owner changes scheduler admission only. - Removing `ActorHot` lazily invalidates its ticket and wakeup pointer; bounded stale records converge through ordinary page draining - Every bounded window validates checked `end + 1` representability, rejects overflow, and schedules that exact terminal block through `ActorHot.terminal_at`; trigger and terminal readiness share one live wakeup pointer and retain the earlier target - Paused actors remain hot-only before terminal time and load `ActorContract` only when closure is due @@ -256,31 +291,37 @@ Lifecycle lease-by-cycles is authored by `ActorContract.auto_close_at_cycle_nonc ### Fee Collection Boundary -The generic pallet collects creation and per-step User fees through one runtime-supplied `FeeCollector`; terminal cleanup charges no Actors fee. Both User creation calls collect `ActorCreationFee` for Active and Dormant admission before identity, slot, counter, or next-id mutation; failed collection leaves every actor store and owner balance unchanged. System creation remains exempt. +The generic pallet collects creation, successfully processed Manual, AddressEvent, ObservationChange, and ObservationCrossing-fire Trigger occurrences, per-admitted-cycle Pipeline Machine/cleanup, and per-invoked-Action User fees through one runtime-supplied `FeeCollector`; Crossing rearm and terminal cleanup charge no Actors fee. Both User creation calls collect `ActorCreationFee` for Active and Dormant admission before identity, slot, counter, or next-id mutation; failed collection leaves every actor store and owner balance unchanged. System creation remains exempt. -Actors invokes the runtime-supplied collector exactly once per selected charge and assigns no trigger semantics to that movement. Collection is ledger-only: it performs no Actors ingress preflight or notification, funding accumulation, readiness latch, or scheduler placement. Zero collection is a no-op, and any collector failure rolls back movement and all Actors state. +Actors invokes the runtime-supplied collector exactly once only when one Trigger changes `pending_signal` from false to true. Manual, matching AddressEvent, ObservationChange, fired ObservationCrossing, AtTime, and Cadenced use their generated family owner and emit `TriggerOccurrenceProcessed` only for that useful transition. An already-latched occurrence performs no Actor-specific fee, event, activation, or causal-history update. Trigger underfunding rolls back queue/latch mutation without closing the process, except that a consumed one-shot User `AtTime` closes through prepaid fee-free minimal apoptosis with `TriggerAdmissionInsufficient`. Fee-collector infrastructure failure rolls back consumption and retains the wakeup. Collection is ledger-only: it performs no Actors ingress preflight, funding accumulation, readiness mutation, or scheduler placement. Zero collection is a no-op, and collector failure rolls back movement and all Actors state. - Predicate and Task preparation run read-only before collection determines the Step outcome. -- Every attempted User Step invokes `FeeCollector` at most once: false Precondition or resolution/funding non-execution charges evaluation-only, while an executable Step charges evaluation plus generated execution fee together. -- Collection failure rolls back the complete scheduler attempt before step policy, task dispatch, queue consumption, or any persistent fee, event, counter, nonce, cursor, or snapshot mutation. Simulation reports interface-local `FeeCollectionFailed`. -- After successful collection, adapter failure rolls back task-local effects but retains the one combined charge; `ContinueNextStep` and `AbortCycle` never alter that charge or trigger another collection. +- Idle readiness consumption first charges the fixed-size certified Pipeline Machine/cleanup total and emits `PipelineFeeCharged`; collection failure rolls back queue/latch mutation and preserves the live head. Current-Step admission still meters control/effect Weight component-wise but reserves only the Action-effect maximum economically. Before a Suspended User retry, the scheduler proves the current maximum Action liability above `MinUserBalance`; insufficiency closes through fee-free minimal apoptosis before invocation, while the enclosing Step transaction settles valid actual cost and releases the remainder. False Precondition, skipped resolution, `FundingUnavailable`, and `StopCycle` charge no Action fee; an invoked success or typed failure settles valid actual effect Weight and appends `ActionFeeCharged` after semantic boundary events as the attempt's final economic receipt. +- Collection failure rolls back the complete scheduler transaction, including provisional task dispatch, placement, close, queue consumption, and every fee, event, counter, nonce, cursor, or snapshot mutation. Missing or excessive actual evidence fails before collection and rolls back the same atom. Simulation reports interface-local `FeeCollectionFailed`. +- An invoked adapter failure reports `Invoked` effect evidence and retains its valid actual effect fee when the enclosing attempt and collection commit; `ContinueNextStep` and `AbortCycle` never alter that charge or trigger another collection. Zero actual total fee produces no collector call. +- `TaskEffectWeightProvider` owns maximum admission and one closed post-dispatch branch: `NotInvoked` returns zero, while `Invoked` returns the canonical generated Task-family Weight whether the operation commits or returns typed failure. Scheduler service rejects absent or component-wise greater-than-reserved evidence and rolls back the complete queue/Step/effect transaction. Successful non-invocation consumes control only and releases the effect reservation inside the pass budget. +- `StepControlWeightProvider` receives the starting phase, committed outcome, and canonical post-placement class (`None`, `Queue`, or `Wakeup`) after Step mutation and successor placement share one transaction. Missing or component-wise greater-than-reserved actual control evidence rolls back that transaction. The production maximum benchmark materializes eight current/Opening evaluation units, eight C6 tail chunks, 64 Opening snapshot entries, 128 Opening results, and 40 funding entries. That exact context conservatively selects the measured full-cycle owner so the maximum remains dispatchable, even though scheduler queue service is also metered outside it. Independently estimated ProofSize cannot be subtracted soundly; a direct inner owner must replace this temporary overlap. + +Running, Suspended, and fresh-Opening progress select direct inner owners. Fresh-Opening progress has separate minimal and maximum realizable geometry owners at each authored C6 tail count; minimal paths retain only a negligible in-memory slope instead of inheriting maximum Opening capture. Execution reuses the transaction-prevalidated full Contract, hot state, run authority, and admission identity for run persistence and FIFO successor placement without reloading the next tail fragment. Opening-heavy Suspended owners use the worst realizable one-Opening/three-Current composition rather than a non-monotonic mixed-unit axis. Fresh-Opening completion selects separate minimal and maximum realizable direct owners; ordinary completion no longer reconstructs cold tails merely to prove that no terminal close condition applies. + +The minimal completion owner keeps fixed `5,696` ProofSize and `3/3` reads/writes across `0..=8` tail chunks, while its `524,747` RefTime slope accounts only for bounded in-memory authored geometry. Fresh-Opening retry and non-closing failure likewise select minimal and maximum-realizable direct owners; wakeup placement reuses the loaded hot authority and adds no Contract fragment read to the minimal path. Actor close preserves the logical completed/failed control outcome, while `close_actor` remains the separately reserved, non-fee terminal-cleanup owner. Full minimal/maximum close benchmarks are composition assurance only and are never substituted for actual Step control or charged again through the Step fee. - `fee_native_protected_minimum` applies `max(MinUserBalance, asset minimum)` to User fee-native direct preserve-spend capacity and `SwapOut` input capacity after the selected reservation; other assets retain their adapter minimum. -Pallet regressions cover each outcome, collection failure, one-call cardinality, task rollback, release-to-zero, direct User-floor preservation, and exact-output input-cap failure. User fee admission derives each step and complete-plan reserve from host-bound generated weights plus configured step fees; package tests reject execution immediately below that exact reserve. +Pallet regressions cover one useful charge and no redundant latched charge for every Trigger family, detector disable/re-arm, automatic underfunding, independent temporal pointers, Crossing batch-to-scalar progress, Pipeline collection rollback, exact admission boundaries, zero-Step service, each Action outcome, task rollback, valid-actual release to zero, missing/excessive evidence, direct User-floor preservation, and exact-output input-cap failure. Package tests reject Pipeline service one unit below `MinUserBalance + Pipeline total`, preserve committed Trigger fees, and prove Running/Suspended service has no renewed machine-solvency owner. ### Progress-Preserving Continuation -`ActorHot.cycle_state` is the sole discovery marker. `Idle` reads no Continuation value; `Suspended` requires exactly one `ContinuationState[actor_id]`. The sparse value stores scalar `cursor`, logical-cycle-wide `attempt`, `unsuccessful_attempts_at_cursor`, `last_attempt_block`, frozen typed suffix snapshots, and cumulative outcomes. Try-state enforces marker/store equivalence, cursor bounds, Mutable-only bounded retry policy, a live cursor-local count below its nonzero limit, and snapshot-surface validity. +`ActorHot.cycle_state` is the sole discovery marker. `Idle` reads no Actor run value; `Suspended` requires exactly one `ActorRunState[actor_id]`. The sparse value stores scalar `cursor`, `unsuccessful_attempts_at_cursor`, `last_attempt_block`, frozen typed suffix snapshots, and cumulative outcomes. `Running` is present in metadata for the accepted block-paced contract but is not persisted by the current suffix executor; its implementation remains open in I1/I3. Try-state enforces marker/store equivalence, cursor bounds, Mutable-only bounded retry policy, a live cursor-local count below its nonzero limit, and snapshot-surface validity. Attempt `0` opens one logical cycle and increments `cycle_nonce` once. A Temporary `TaskFailure` or `FundingUnavailable` under `RetryLater { max_attempts }` increments both global failure state and the cursor-local count. The first suspension stores `1`; same-cursor suspension uses checked increment after admission proves both retry bounds, while a later cursor resets to `1`. `transition_failure_streak` is the sole mutation formula: an unsuccessful attempt checked-increments, while completed execution or semantic Step replacement resets to zero. Suspension and terminal failure call that owner once, classification only reads its result, and simulation inherits the same transition through canonical execution. -Post-attempt counters use checked addition. Inclusive local exhaustion closes with `RetryAttemptsExhausted`; when both cutoffs land together this reason wins, while an earlier global cutoff closes with `ConsecutiveFailures`. Exhaustion clears Continuation, emits `CycleSummary(Failed)` without `CycleCancelled`, then closes. An already-reached global cutoff closes before another `CycleStarted` or `CycleContinued`. Persisted retry reuses the nonce, omits external cadence, and executes only the suffix. +Post-attempt counters use checked addition. Inclusive local exhaustion closes with `RetryAttemptsExhausted`; when both cutoffs land together this reason wins, while an earlier global cutoff closes with `ConsecutiveFailures`. Exhaustion clears ActorRunState, emits `CycleSummary(Failed)` without `CycleCancelled`, then closes. An already-reached global cutoff closes before another `CycleStarted` or `CycleContinued`. Persisted retry reuses the nonce, omits external cadence, and executes only the suffix. `scheduler::retry_backoff_blocks` uses checked capped exponentiation to map persisted attempt `0, 1, 2, ...` to `1, 2, 4, 8, 8...` blocks. -`Attempt identity proof`: `cycle_nonce` identifies the logical cycle, and the scheduler's one-execution-per-actor-per-block invariant makes `(actor_id, cycle_nonce, block_number, event_index)` unique for every opening or Continuation attempt. Cursor and `unsuccessful_attempts_at_cursor` state the semantic retry position. No cycle-global attempt ordinal is stored, emitted, simulated, or projected; executable evidence covers opening, repeated suspension, and completion coordinates. +`Attempt identity proof`: `cycle_nonce` identifies the logical cycle, and the scheduler's one-execution-per-actor-per-block invariant makes `(actor_id, cycle_nonce, block_number, event_index)` unique for every opening or resumed-run attempt. Cursor and `unsuccessful_attempts_at_cursor` state the semantic retry position. No cycle-global attempt ordinal is stored, emitted, simulated, or projected; executable evidence covers opening, repeated suspension, and completion coordinates. Eligibility uses the larger of this delay and schedule cooldown, omits external cadence for the open run, then respects window start. A one-block delay uses the existing next-block FIFO ticket; longer delays use the existing paged wakeup pointer. @@ -290,7 +331,7 @@ Eligibility uses the larger of this delay and schedule cooldown, omits external `resolve_step_control` is the private exhaustive transition owner for executed, stopped, skipped, funding-unavailable, Permanent-failure, and Temporary-failure outcomes across all three error policies. Production execution and `simulate_current_contract` both invoke `execute_single_cycle_traced`. -`canonical_step_transition_matrix_has_production_simulation_parity` runs every Section 3.4 row, Actor type, mutability, error-policy variant, Step-outcome variant, local/global bound, and fresh/Continuation attempt. Variant counts fail closed when either canonical enum grows. +`canonical_step_transition_matrix_has_production_simulation_parity` runs every Section 3.4 row, Actor type, mutability, error-policy variant, Step-outcome variant, local/global bound, and fresh/resumed-run attempt. Variant counts fail closed when either canonical enum grows. The matrix compares rollback-only simulation with independently observed production events, counters, cursors, failure streaks, fees, balances, custody, suffix effects, and final disposition. The protocol-coherence audit rejects specification/matrix and enum-inventory drift, and Actors assurance executes the focused matrix. @@ -298,9 +339,9 @@ Task-scoped rollback leaves earlier successful steps committed. The named `SwapI `simulate_current_contract` is the package-owned rollback core behind versioned `ActorSimulationApi` runtime metadata. It requires exact stored Active Actor Contract, actor type, mutability, mode/run-state, readiness, liveness, and User fee budget. Simulation and production admission use the same package-owned suffix envelope and predicate: checked fee-native balance above `MinUserBalance` must cover `attempt_fee_upper`; raw balance alone never admits a User attempt. -The API executes the production path with an optional bounded trace and exposes fresh or Continuation outcomes including `unsuccessful_attempts_at_cursor`. Terminal closure returns `AttemptDisposition::Closed(CloseReason)`. The whole attempt runs inside `TransactionOutcome::Rollback`; package tests prove actor state, balances, events, fees, adapter effects, and stored Continuation remain unchanged. +The API repeatedly executes the one-Step production path under rollback, advances a virtual causal block argument for `Continued`, and exposes fresh or persisted-run outcomes including `unsuccessful_attempts_at_cursor`. Terminal closure returns `AttemptDisposition::Closed(CloseReason)`. The whole attempt runs inside `TransactionOutcome::Rollback`; package tests prove actor state, balances, events, fees, adapter effects, and stored ActorRunState remain unchanged. -Semantic Contract Steps, funding-policy, schedule, window, deactivation, terminal, and close transitions share `cancel_continuation_internal`. Cancellation validates and mutates `ActorHot` through one fallible storage owner; missing or contradictory state returns `ContinuationInvariant`. Continuation writes load all five canonical partitions, return `ActorNotFound` for absent or Dormant identity, and return `ActorInvariant` for malformed partition ownership, without panic-only mutation assumptions. Continuation attempt opening returns a state-preserving failed disposition when its canonical state disappears, and checked fee-envelope disagreement does the same before cycle mutation or events. Exact encoded plan/completion-policy, funding-policy, schedule/window, auto-close-target, and global active-limit updates return without storage or event mutation; semantic auto-close target changes preserve Continuation. +Semantic Contract Steps, funding-policy, schedule, window, deactivation, terminal, and close transitions share `cancel_run_internal`. Cancellation validates and mutates `ActorHot` through one fallible storage owner; missing or contradictory state returns `ActorRunInvariant`. Actor run writes load all five canonical partitions, return `ActorNotFound` for absent or Dormant identity, and return `ActorInvariant` for malformed partition ownership, without panic-only mutation assumptions. Continuation attempt opening returns a state-preserving failed disposition when its canonical state disappears, and checked fee-envelope disagreement does the same before cycle mutation or events. Exact encoded plan/completion-policy, funding-policy, schedule/window, auto-close-target, and global active-limit updates return without storage or event mutation; semantic auto-close target changes preserve Continuation. Every external control that invalidates or reconstructs ordinary membership shares one class-independent actor/block clock across signed-owner and governance origins; its identity write and Contract replacement hot-state/reset write are fallible transaction-owned mutations returning typed invariant errors rather than panic-only post-prevalidation assumptions. Exact no-ops, internal transitions, and terminal cleanup remain exempt. @@ -310,19 +351,27 @@ Cancellation emits `CycleCancelled` before one cumulative terminal `CycleSummary ### Read-Only Eligibility Projection -Version 4 `ActorEligibilityApi` owns `actor_eligibility` plus the bounded `materialization_faults` projection. `actor_eligibility` is the read-only semantic Actor projection. It mirrors `apply_admission` and reuses the exact cadence, retry, window, failure-limit, breaker, and latch owners, so clients do not reproduce scheduler arithmetic. +Version 6 `ActorEligibilityApi` owns `actor_eligibility` plus the bounded named `materialization_faults` and `crossing_capacity` projections. `actor_eligibility` is the read-only semantic Actor projection. It mirrors `apply_admission` and reuses the exact cadence, retry, window, failure-limit, breaker, and latch owners, so clients do not reproduce scheduler arithmetic. -The projection is one algebra: `NotRegistered`, `Dormant`, or `Active(ActorClassification)`. Active Crossing includes canonical phase and installation revision plus bounded pending/processing revision facts; the companion fault method returns only current Crossing, broad-fanout, and wakeup faults, `crossing_capacity` returns per-feed User/total policy and exact counts without raw physical topology, and `trigger_state_bond` quotes the prospective refundable User amount from the host policy used by lifecycle dispatch. Active eligibility preserves terminal reason and the exact `ActorExecutionPhase`, including `WaitingRetry(block)`, `WaitingBlock(block)`, and `WaitingCadenceTick(tick)` payloads; no parallel phase or next-block field exists. +The projection is one algebra: `NotRegistered`, `Dormant`, or `Active(ActorClassification)`. Active Crossing includes canonical phase and installation revision plus bounded pending/processing revision facts; the companion fault method returns only current Crossing, broad-fanout, and wakeup faults, while `crossing_capacity` returns per-feed User/total policy and exact counts without raw physical topology. Active eligibility preserves terminal reason and the exact `ActorExecutionPhase`, including `WaitingRetry(block)`, `WaitingBlock(block)`, and `WaitingCadenceTick(tick)` payloads; no parallel phase or next-block field exists. -The projection persists no state, emits no event, and promises no service. Queue position and available Weight still decide actual admission. Arithmetic overflow and malformed Continuation state return typed projection errors rather than an inferred phase. Nonce exhaustion is projected as the same terminal close as execution without attempting an overflowing successor nonce. +The projection persists no state, emits no event, and promises no service. Queue position and available Weight still decide actual admission. Arithmetic overflow and malformed Actor run state return typed projection errors rather than an inferred phase. Nonce exhaustion is projected as the same terminal close as execution without attempting an overflowing successor nonce. Authoritative amount arithmetic uses checked accumulation/subtraction: normalized split legs plus retained remainder exactly conserve the resolved total, including `u128::MAX`. Fee reservations, existential protection, and percentage amount resolution intentionally floor unavailable spendable balance at zero. Saturating `Weight` composition remains a conservative upper-bound cap, scheduler pass statistics and starvation counters remain bounded telemetry caps, and try-state topology/accounting counters fail on overflow rather than masking malformed state. Code anchor: `src/scheduler.rs::actor_eligibility`; package tests prefixed `eligibility_projection_` falsify absence, dormancy, every Active phase, terminal coexistence, and exact temporal payloads. +### Read-Only Cost Projection + +`ActorCostApi::actor_cost_quote` returns one bounded current quote without combining economic owners. Active Actors expose the exact family-specific Trigger occurrence Weight and fee, the stored upfront Pipeline Machine/cleanup amounts and admission/production Weight identities, and the current Step's maximum Action-effect Weight and fee. Dormant Actors have no prospective Trigger or Pipeline charge, while a zero-Step or `StopCycle` current branch reports zero Action maximum. + +The quote returns the configured Creation Fee separately from the current refundable state hold. User hold provenance preserves identity, Contract head, actual retained tail body, detector, funding, and run components plus base/per-byte pricing and checked total; System Actors report explicit exemption and zero held components. The method reads bounded canonical state only, mutates nothing, exposes no remaining machine budget, and returns typed errors for absence, partition corruption, arithmetic overflow, or missing Weight authority. + +Code anchor: `src/lib.rs::actor_cost_quote`; `actor_cost_quote_keeps_fee_boundaries_and_state_hold_provenance_separate` falsifies User/Dormant/System separation, generated identities, current Action ownership, and hold totals. + ### Queue Execution Model (Monotonic Paged FIFO) -`classify_actor` owns terminal precedence, breaker and pause phase, retry/temporal timing, signal readiness, and cursor-sensitive User viability. Scheduler admission, sweep, simulation, eligibility, and certified ingress project that read-only result; malformed Active partitions and Continuation state map through typed classification errors. +`classify_actor` owns non-economic terminal precedence, breaker and pause phase, retry/temporal timing, and signal readiness. Scheduler admission alone checks User Pipeline capacity when Idle paid readiness is consumable; sweep, Running/Suspended classification, eligibility, and certified ingress never predict future affordability. Simulation mirrors the separate Pipeline boundary; malformed Active partitions and Continuation state map through typed classification errors. Scheduler execution is queue-first and deterministic: @@ -330,10 +379,10 @@ It uses two scheduler layers: a monotonic paged FIFO for work that can execute n 1. **Wakeup drain**: the coordinator fairly alternates block and tick domains when both are due; each cursor exposes its earliest deadline without scanning sparse gaps, and one admitted unit consumes one slot, preserves a partial bucket at the same minimum, and either appends live readiness to the active FIFO or lazily discards a stale pointer 2. **Ingress admission**: each matched producer call applies funding independently and sets the unified boolean signal latch; actor-local queue/wakeup membership remains bounded and may join the active run queue in the same `on_idle` pass -3. **Block-start cutoff**: after already-due wakeups materialize, the scheduler snapshots global `NextQueueTicket`; every later append receives a ticket at or beyond that cutoff and cannot execute in the current block +3. **Block-start cutoff**: `on_initialize` stores `(block, NextQueueTicket)` in `PrepassExecutionCutoff` before ordinary external causes; `NextBlock` Actor Drain consumes that cutoff only when its block tag matches the current block, while direct physical service helpers without a current hook-owned cutoff use their explicit current frontier. Every later append receives a ticket at or beyond the cutoff and cannot execute in that block 4. **Canonical head**: tri-state discovery returns `Empty`, `Head`, or `Blocked` only for the one global FIFO. It stops on an incomplete probe rather than treating the head as absent. 5. **Strict ticket order**: the scheduler may lazily consume tombstones before the cutoff, but it cannot bypass a live or blocked head. One O(1) preflight owns every FIFO mutation: checked `tail - head == QueueOccupancy`, configured capacity, and canonical current head/tail page-slot layout. Enqueue, live-head consume, and tombstone drain run as closed storage transactions after that preflight. Corruption rolls back exactly and returns an invariant live-head stall rather than `Empty` or progress. The next live ticket receives the only execution offer. -6. **Execution ceiling**: `last_cycle_block`, the shared scan/execution ceilings, and the common cutoff prevent a second scheduler invocation or circular/self-enqueue graph from executing one actor twice in the same block. Untouched suffix entries remain physically in place without reconstruction. +6. **Execution ceiling**: `last_cycle_block` records both Opening and terminal disposition blocks, while Running owns `last_committed_step_block`; these markers, shared scan/execution ceilings, and the common cutoff prevent another Step or retained-signal Cycle from executing for that actor in the same block. Untouched suffix entries remain physically in place without reconstruction. `ActorHot.queue_ticket` is the sole live-membership marker for the canonical FIFO. Replacement, close, pause, and cancellation use actor-local queue invalidation; stale queue entries drain lazily. FIFO enqueue, invalidation, liveness, tombstone drain, and scheduler admission load the exact five-partition actor state before interpreting the ticket, same-block marker, pause state, or Contract; corrupt state stalls without queue mutation. Live-head service carries that validated hot state into transactional consumption, which rechecks the exact physical actor/ticket pair before clearing membership; post-execution placement reloads canonical state because execution may change lifecycle, Continuation, funding, or terminal ownership. @@ -341,26 +390,32 @@ Wakeup schedule, invalidation, drain, and materialization use the same canonical No actor type, actor ID, execution share, or priority policy changes FIFO service. System and User actors receive bounded service exclusively by their global ticket order. +The production Actor Drain `execute_cycle_to_cutoff_with_resources` path pre-reserves the complete pass Actor Control maximum before queue scan/probe, reserves each Action's phase-owned effect maximum before semantic mutation, settles committed actual effects transactionally, and settles the full reconciled pass control afterward. Coverage includes successful Action, zero-Step Opening, precondition skip, paused/tombstone consumption, and proof-budget probe stall as exact control-only or split-domain outcomes. Missing actual evidence retains both authorities and halts optional Actor work, preventing final resource reconciliation from publishing a false domain split. + +`BlockResourceState::reserve_actor_step` atomically reserves one Step's Actor Control and phase-specific Actor Base/Drain effect maxima; a failed second reservation restores the complete prior state. `settle_actor_step` similarly commits both valid actual values or restores state and both one-shot reservation authorities. + +`CyclePass` separately accumulates committed actual Task-effect Weight and derives Actor Control as checked `consumed - effect`. A rolled-back Action-bearing attempt whose exact effect execution cannot be recovered marks domain reconciliation uncertain rather than fabricating actual evidence; resource integration can therefore halt before publishing a false split. + Wakeup draining, paged queue operations, actor probes, normal cycles, and pure terminal cleanup use two-dimensional `WeightMeter` fit checks. Direct ingress is charged at its originating producer. Close admission uses the generated worst-case User cleanup bound and performs no shared-container scan. Scheduler accounting stays local unless it controls consensus progress. `scanned` and `executed` enforce independent per-pass ceilings; `QueueDrainStats` and `WakeupDrainStats` expose bounded operation results to callers, tests, and benchmarks without storage writes. Operators derive completed admission from `CycleStarted`/`CycleSummary` and persistent blockage from bounded queue/wakeup state plus starvation detection/recovery events. Loaded-actor and page-touch diagnostics remain stress instrumentation rather than permanent consensus counters. ### Temporal Wakeup Layer -The temporal wakeup layer owns future eligibility and admits it only through the active run queue. Every active actor may own at most one exact pointer; replacement and closure invalidate that slot without scanning actors or blocks. `MaxActiveActors` bounds global live wakeups, while `WakeupPageSize` controls I/O granularity rather than same-block capacity. This makes spillover buckets, placement-drop events, and actor-key retry scans unnecessary. +The temporal wakeup layer owns future eligibility without conflating detection and execution. Every Active Actor may own at most one exact block-keyed Pipeline-service pointer and one independent tick-keyed Trigger pointer; each clock-domain replacement invalidates only its own slot without scanning actors or deadlines. `MaxActiveActors` bounds live memberships per clock, while `WakeupPageSize` controls I/O granularity rather than same-block capacity. This makes spillover buckets, placement-drop events, and actor-key retry scans unnecessary. One placement owner (`schedule_next_work_local`) maps signalled readiness, timestamp cadence, block cooldown/window/retry targets, capacity recovery, and terminal expiry to either the FIFO or one typed wakeup. Placement owners normalize `AlreadyLive` to success; a missed normalization at a defensive `map_err` boundary returns `QueueCapacityUnavailable` rather than panicking. `defer_wakeup` and `defer_tick_wakeup` share the same transactional substrate. Queue saturation falls back to an exact next-block wakeup; fallback failure leaves no false success claim (spec 8.1.4). Queue consumption, attempt effects/events, and all reachable post-attempt placement share one transaction. Ticket/page/wakeup exhaustion or topology failure rolls back the attempt and preserves the prior queue head. Wakeup removal and queue materialization likewise commit atomically or retain the exact wakeup. -Package coverage proves the strict post-worker cutoff: two manual triggers in one block latch one pending signal and one FIFO ticket, and exactly one `CycleStarted` is emitted (`executions(A, B) <= 1`). +Package coverage proves the hook-ordered `NextBlock` cutoff: a block-N Manual occurrence allocates one ticket strictly after the block-N prepass frontier, remains pending without `CycleStarted` through block-N Drain, and executes in block N+1. Direct physical FIFO tests remain independent of runtime hook timing. The package uses the paged wakeup substrate and sparse cursor: - `WakeupPages<(WakeupKey, page_id)>` and per-key `WakeupBuckets` own the paged topology, where `WakeupKey` is `Block(number)` or `Tick(number)`. - `WakeupCursorPages<(clock, page_id)>` plus `WakeupCursorLen` provide separate production paged binary min-heaps for block and tick keys; each bucket owns its exact reverse `cursor_index`. - Heap insertion, pop-min, and exact removal use at most `ceil(log2(MaxActiveActors))` sift steps, preserve contiguous per-clock cursor pages, and avoid scanning empty intermediate keys; try-state reconciles each clock's page shape, uniqueness, ordering, and reverse indices. Maximum-depth generated evidence belongs to `scheduler_wakeup_cursor_insert`, `scheduler_wakeup_cursor_pop_min`, and `scheduler_wakeup_cursor_remove_exact`. -- `ActorHot` owns `WakeupPointer { block: WakeupKey, page_id, slot }` and closed `trigger_runtime_state`; its Cadenced variant owns the optional anchor tick. The retained pointer field name `block` is SCALE representation, while its typed value owns the clock domain. +- `ActorHot.wakeup_pointer` owns `WakeupPointer { block: WakeupKey::Block, page_id, slot }` for Pipeline service, while `trigger_wakeup_pointer` owns `TriggerWakeupPointer { tick, page_id, slot }` for AtTime/Cadenced detection; `trigger_runtime_state::Cadenced` owns the optional anchor tick. - Pages use optional slots, a live count, a scan cursor, and bidirectional links. - Transactional replacement invalidates the prior exact slot, removes an emptied key from its clock cursor, creates the replacement bucket and cursor entry atomically, and rolls back on reverse-index mismatch; bounded neighboring-page work unlinks empty pages. - The cursor-driven overdue worker runs after timestamp inherent application and before the queue cutoff. It floors consensus milliseconds into the current tick, reselects after every admitted unit, and round-robins simultaneously due block/tick clocks while preserving FIFO order inside each clock. It meters each cursor lookup, page scan, queue append, and possible full-depth cursor removal before mutation; partial progress keeps each clock's same minimum for later resumption. @@ -394,38 +449,75 @@ Package coverage retains actor-local readiness through ProofSize-only exhaustion Recovery is governance-operated (circuit breaker or parameter adjustment); no emergency cycle execution occurs in `on_initialize`. -### Cadence +### Temporal Triggers -- Cadence readiness is deterministic only; Actors exposes no probability field, entropy provider, secure/insecure branch, hash fallback, probability event, or probability error. -- Cadence uses `cadence_anchor_tick = ceil(timestamp_millis / CadenceTickMillis)` as its exact origin and derives no actor-specific phase. `MaxCadenceDelayTicks` bounds this timestamp domain independently from the block-domain `MaxExecutionDelayBlocks`; neither numeric horizon is converted or reused across clocks. Genesis records an uninitialized anchor and one tick-zero bootstrap wakeup because no consensus timestamp exists during construction. -- The first ordinary wakeup service after timestamp inherent application replaces that marker with the ceiled anchor and one full-period deadline without latching readiness or entering FIFO. Active installation and replacement anchor directly because consensus time is then available. -- Eligibility and simulation project the exact actor-owned tick pointer. A pointer at or before the current scheduler tick remains `WaitingCadenceTick(exact_tick)` until wakeup materialization latches readiness; projection never advances it to a later cadence period. -- Admission prevalidates nonzero bounded `every_ticks`, zero cooldown, no window, and checked anchor/deadline arithmetic. Runtime rearm selects the first aligned tick strictly after the observed tick, so missed periods coalesce without catch-up bursts. -- A due initialized tick clears the typed temporal pointer, sets the existing pending latch, and appends one FIFO opportunity. Only FIFO service executes the Actor; after an attempt terminates, cadence rearms from the immutable anchor. -- Any future probabilistic execution requires a separate append-only admission policy and a concrete financially secure runtime entropy contract rather than an optional field on deterministic cadence. +- Temporal readiness is deterministic only; Actors exposes no probability field, entropy provider, secure/insecure branch, hash fallback, probability event, or probability error. +- `AtTime` and Cadenced use `temporal_anchor_tick = ceil(timestamp_millis / CadenceTickMillis)` as their exact origin and derive no actor-specific phase. `MaxTemporalDelayTicks` bounds this timestamp domain independently from block-domain `MaxExecutionDelayBlocks`; neither horizon is converted or reused across clocks. +- Genesis records an uninitialized temporal anchor and one tick-zero bootstrap wakeup because no consensus timestamp exists during construction. The first ordinary wakeup service anchors from the applied timestamp and installs one full-delay deadline without latching readiness or entering FIFO. +- Active installation and replacement anchor directly because consensus time is available. Eligibility projects authored AtTime/Cadenced geometry and one-shot consumption, while canonical Trigger runtime state and `TriggerWakeupPointer` own the exact deadline. +- AtTime admission prevalidates nonzero bounded `after_ticks`, zero cooldown, no window, and checked `anchor_tick + after_ticks` arithmetic. A due occurrence marks `consumed = true` and never rearms or catches up. +- Cadenced admission applies the same policy to `every_ticks`. Runtime rearm selects the first aligned tick strictly after the observed tick, so missed periods coalesce without catch-up bursts. +- A due AtTime occurrence clears its Trigger pointer and permanently marks the one-shot opportunity consumed. A useful Cadenced occurrence clears its Trigger pointer and leaves temporal detection disabled while readiness remains latched. Already-latched due work performs no Actor-specific processing. +- Opening atomically rearms Cadenced to the first aligned tick strictly after current consensus time before Pipeline charging. Failure rolls back readiness consumption and fee movement. Only Pipeline-service FIFO membership executes the Actor; any future probabilistic execution requires separate admission policy and secure runtime entropy. ### Trigger Sources -`Manual`, `AddressEvent`, `ObservationChange`, `ObservationCrossing`, and due `Cadenced` detection all enter `request_activation`. That sink loads canonical Active state once and builds one private prospective-state `ActivationPlan`: it latches `ActorHot.pending_signal` in memory, classifies with the loaded Continuation, and records exact close, coalesced, cadenced-enqueue, or non-cadenced placement action before commit. Canonical FIFO append likewise separates read-only `QueueAppendPlan` authority from mutation. One production `preflight_paged_enqueue_cohort` admits a nonempty vector bounded by `MaxCrossingActorsPerBlock`, validates queue topology once, and accumulates consecutive Actor tickets, final tail/occupancy, and each touched physical page in memory; over-cap, duplicate/live, capacity, and index failure retain no writes. Successful cadenced enqueue carries single-Actor authority into activation commit, while queue saturation retains the exact next-block wakeup fallback. Non-cadenced planning records no placement, immediate enqueue, or one exact block wakeup before commit. Source-specific code owns only detection and source-state progression; no parallel signal key or source tag enters consensus state. +Every Trigger family reaches activation only for an unset readiness latch. Direct families reject redundancy before family-specific evaluation or charging. Indexed families retain stable subscription/range topology and use `IndexedTriggerDetectionDisabled` as detector-local authority, allowing fanout and Crossing traversal to skip a latched identity before loading Actor state. Opening clears ObservationChange authority directly; Crossing Opening first relocates membership and resets phase/revision from the current observation, discarding latched-period transitions. Canonical FIFO append separates read-only `QueueAppendPlan` authority from mutation, and bounded Crossing cohort preflight validates queue topology once before commit. + +Successful temporal enqueue carries single-Actor authority into activation commit, while queue saturation retains the exact next-block wakeup fallback. Ordinary planning records no placement, immediate enqueue, or one exact block wakeup before commit. Source-specific code owns only detection and source-state progression; no parallel signal key or source tag enters consensus state. The scheduler carries one loaded Active state through live-ticket verification, same-block and lifecycle gates, classification, attempt admission, fee admission, and loaded-head consumption. Control preflight, ingress preflight/consequence, eligibility, simulation, sweep, and activation likewise classify with the Continuation returned by their complete canonical probe instead of rereading it through a view-only helper. A transaction boundary or adapter/task effect that may mutate actor state terminates that borrow and requires an explicit canonical reload before later placement. -Crossing phase and installation revision live only in `ActorHot.trigger_runtime_state`; generation-checked exact-threshold memberships store only key/page/offset/generation physical back-pointers in bounded dense pages under a sparse radix path. `MaxCrossingMembersPerFeed` is the independent total host bound, while `MaxUserCrossingMembersPerFeed` and its exact reconciled count reject User admission before topology writes and leave the difference exclusively available to System Actors. The reference runtime binds these to 10,000 total and 9,000 User memberships per feed; global Active capacity remains a separate aggregate ceiling. Dense-page swap compaction rewinds an active range cursor when an unprocessed tail member moves behind it, preventing close or replacement of a skipped newly installed member from erasing older transition work. If activation terminally closes the final feed member, ordinary close cleanup removes the membership, queue, cursor, and pending-feed link atomically; the worker recognizes the resulting zero membership count as completed cleanup and does not recreate source state. A non-terminal placement failure rolls the complete actor unit back. +Crossing phase and installation revision live only in `ActorHot.trigger_runtime_state`; generation-checked exact-threshold memberships store only key/page/offset/generation physical back-pointers in bounded dense pages under a sparse radix path. `MaxCrossingMembersPerFeed` is the independent total host bound, while `MaxUserCrossingMembersPerFeed` and its exact reconciled count reject User admission before topology writes and leave the difference exclusively available to System Actors. The reference runtime binds these to 10,000 total and 9,000 User memberships per feed; global Active capacity remains a separate aggregate ceiling. Dense-page swap compaction rewinds an active range cursor when an unprocessed tail member moves behind it, preventing close or replacement of a skipped newly installed member from erasing older transition work. + +If activation terminally closes the final feed member, ordinary close cleanup removes the membership, queue, cursor, and pending-feed link atomically; the worker recognizes the resulting zero membership count as completed cleanup and does not recreate source state. A non-terminal placement failure rolls the complete actor unit back. + +`classify_crossing_work` is a bounded read-only projection over the current pending-feed head, transition, range cursor, sparse radix result, generation-checked member, canonical Actor state, and prospective activation shape. Its production bounded contiguous-prefix authority is obtained through the internal `CrossingCohortSnapshot` helper bounded by Crossing-owned `CrossingPageSize` and `MaxCrossingActorsPerBlock`. Each snapshot binds the source leaf key, page, half-open `[start_offset, end_offset)` range, and contiguous member/generation/locator authorities; it validates only selected entries, clamps to page remainder, grants no authority outside that selection, and performs no mutation. Its production-used count authority also accepts an optional tail-refill limit, allowing non-tail selection to preserve packed pages with one bounded suffix. + +Production Weight owns the required maximum encoded P128 tail-page probe at `20,464,000 / 11,729` plus one database read. Compatibility pair fallback retains separate cursor and tail snapshots because sequential single-member compaction defines that execution shape; production larger batches use one admitted contiguous prefix and, for non-tail sources, one exact physical-tail refill suffix. First- and tail-candidate preflight use one branch-directed read-only cohort classifier for post-installation skip, rearm, pending fire, placed fire, coalesced fire, and terminal fire. Placed fire retains scheduler-owned prospective Hot state after the phase changes to `WaitingForRearm`, including its pending latch and exact immediate-FIFO versus block-wakeup action; aggregate queue authority therefore cannot restore the pre-fire phase. + +The first authority establishes the expected branch; later skip mismatches stop after Hot state, rearm mismatches stop after Hot plus Contract, and fire activation reads the full canonical shape only when that expected branch requires it. The bounded preflight admits only the homogeneous leading branch before mutation, leaving the first differing authority unadmitted without exceeding the generated branch probe. Internal work classification is distinct from the candidate-preflight result and carries branch, exact current admitted candidate count, and optional `(tail page, available suffix)` refill authority through probe fallback, full classification, and pair-to-single downgrade. Tail-page classification leaves refill authority absent; non-tail admission populates it only after reserving and charging the generated tail-page probe. + +Service admission fails closed if that count disagrees with the selected branch components, so it does not reconstruct count from mutable state after preflight. Production Weight now owns placed-fire, coalesced-fire, terminal-fire, post-installation-skip, and rearm preflight as parameterized `c ∈ [1, 4]` functions with generated RefTime, ProofSize, and respective `2 + 7c`, `2 + 7c`, `2 + 7c`, `2c`, and `3c` database-read formulas; mutation branches use generated single, pair, maximum tail-page, and trimmed/emptied non-tail owners. + +Current pair admission is work-conserving at component and two-dimensional Weight boundaries: one remaining transition/leaf/page/Actor unit selects one candidate, an unaffordable pair probe falls back to the corresponding single preflight, and an affordable pair probe with an unaffordable pair branch executes its already classified first candidate without rereading state. The production cohort extends this authority boundary without a second snapshot path. Its `CrossingWorkPlan` distinguishes empty, completion, seek, leaf/page advancement, installation skip, rearm, coalesced/placed/closed fire, and structural-fault branches without mutating state. Service admits the generated common probe before preliminary classification, conditionally admits the fire-only classification extension, and then selects one generated execution owner. + +Completion/seek miss use transition Weight; open/advance use the ordinary leaf/page maximum; skip, rearm, coalesced fire, placed fire, and terminal close each use their dedicated owner; structural fault reserves the terminal envelope. It never adds independently benchmarked whole-branch results as though they were disjoint components. The committing transaction revalidates canonical and physical ownership. Each pass also retains exact bounded counters for transition units, leaves, pages, candidates, canonical five-partition activation probes, activation calls, terminal closes, and rolled-back faults; the transition/leaf/page/candidate dimensions enforce existing component ceilings, while probe/activation/close/fault dimensions support exact branch evidence without entering consensus storage. + +The release load profile installs 10,000 members on one feed and proves a zero-match transition finishes in one configured pass with zero candidate, canonical-probe, activation, close, fault, or FIFO-placement work. Its following eight-member crossed cohort records exactly eight candidates, canonical probes, activations, and FIFO placements, with zero closes or faults; the other 9,992 members remain unprobed. Latched members retain physical range authority but later transitions take the generated disabled-skip branch without Actor-state loading, fee, event, activation, placement, or phase accumulation. -`classify_crossing_work` is a bounded read-only projection over the current pending-feed head, transition, range cursor, sparse radix result, generation-checked member, canonical Actor state, and prospective activation shape. Its production bounded contiguous-prefix authority is obtained through the internal `CrossingCohortSnapshot` helper bounded by `ObservationPageSize` and `MaxCrossingActorsPerBlock`. Each snapshot binds the source leaf key, page, half-open `[start_offset, end_offset)` range, and contiguous member/generation/locator authorities; it validates only selected entries, clamps to page remainder, grants no authority outside that selection, and performs no mutation. Its production-used count authority also accepts an optional tail-refill limit, allowing non-tail selection to preserve packed pages with one bounded suffix. Production Weight owns the required maximum encoded tail-page probe at `17,530,000 / 4,561` plus one database read. Compatibility pair fallback retains separate cursor and tail snapshots because sequential single-member compaction defines that execution shape; production larger batches use one admitted contiguous prefix and, for non-tail sources, one exact physical-tail refill suffix. First- and tail-candidate preflight use one branch-directed read-only cohort classifier for post-installation skip, rearm, pending fire, placed fire, coalesced fire, and terminal fire. Placed fire retains scheduler-owned prospective Hot state after the phase changes to `WaitingForRearm`, including its pending latch and exact immediate-FIFO versus block-wakeup action; aggregate queue authority therefore cannot restore the pre-fire phase. The first authority establishes the expected branch; later skip mismatches stop after Hot state, rearm mismatches stop after Hot plus Contract, and fire activation reads the full canonical shape only when that expected branch requires it. The bounded preflight admits only the homogeneous leading branch before mutation, leaving the first differing authority unadmitted without exceeding the generated branch probe. Internal work classification is distinct from the candidate-preflight result and carries branch, exact current admitted candidate count, and optional `(tail page, available suffix)` refill authority through probe fallback, full classification, and pair-to-single downgrade. Tail-page classification leaves refill authority absent; non-tail admission populates it only after reserving and charging the generated tail-page probe. Service admission fails closed if that count disagrees with the selected branch components, so it does not reconstruct count from mutable state after preflight. Production Weight now owns placed-fire, coalesced-fire, terminal-fire, post-installation-skip, and rearm preflight as parameterized `c ∈ [1, 4]` functions with generated RefTime, ProofSize, and respective `2 + 7c`, `2 + 7c`, `2 + 7c`, `2c`, and `3c` database-read formulas; mutation branches use generated single, pair, maximum tail-page, and trimmed/emptied non-tail owners. Current pair admission is work-conserving at component and two-dimensional Weight boundaries: one remaining transition/leaf/page/Actor unit selects one candidate, an unaffordable pair probe falls back to the corresponding single preflight, and an affordable pair probe with an unaffordable pair branch executes its already classified first candidate without rereading state. The production cohort extends this authority boundary without a second snapshot path. Its `CrossingWorkPlan` distinguishes empty, completion, seek, leaf/page advancement, installation skip, rearm, coalesced/placed/closed fire, and structural-fault branches without mutating state. Service admits the generated common probe before preliminary classification, conditionally admits the fire-only classification extension, and then selects one generated execution owner. Completion/seek miss use transition Weight; open/advance use the ordinary leaf/page maximum; skip, rearm, coalesced fire, placed fire, and terminal close each use their dedicated owner; structural fault reserves the terminal envelope. It never adds independently benchmarked whole-branch results as though they were disjoint components. The committing transaction revalidates canonical and physical ownership. Each pass also retains exact bounded counters for transition units, leaves, pages, candidates, canonical five-partition activation probes, activation calls, terminal closes, and rolled-back faults; the transition/leaf/page/candidate dimensions enforce existing component ceilings, while probe/activation/close/fault dimensions support exact branch evidence without entering consensus storage. The release load profile installs 10,000 members on one feed and proves a zero-match transition finishes in one configured pass with zero candidate, canonical-probe, activation, close, fault, or FIFO-placement work. Its following eight-member crossed cohort records exactly eight candidates, canonical probes, activations, and FIFO placements, with zero closes or faults; the other 9,992 members remain unprobed. The same profile then converges a 10,000-member single-threshold fire/rearm/fire sequence in exactly 10,002 work units per transition, preserves one placement per Actor across oscillation, fills the 1,024-entry mock FIFO, defers the remaining 8,976 through canonical wakeups, loses no transition, and creates no duplicate placement. Pure rearm has a generated `crossing_rearm_unit` owner (`397,683,000 / 162,782`, 78 reads and 74 writes) and performs no activation probe, placement, or close during execution. Classification is staged: every nonempty unit first admits the eight-read `crossing_work_probe` (`46,305,000 / 12,200`), and only a fire-pending plan may admit the single 14-read `crossing_fire_probe` (`72,427,000 / 12,200`) or queue-page-boundary atomic-authority pair 25-read `crossing_fire_pair_probe` (`241,375,000 / 23,410` before execution. No-match, structural search, skip, and rearm never pay the five-partition fire extension. Already-latched and canonically placed fire uses generated `crossing_coalesced_unit` Weight (`419,403,000 / 162,782`, 82 reads and 74 writes), so it never pays queue placement or terminal cleanup. A newly latched nonterminal fire uses generated `crossing_placed_unit` Weight (`439,379,000 / 162,782`, 87 reads and 78 writes) for exactly one canonical FIFO placement without terminal cleanup. A post-installation transition skip uses generated `crossing_skip_unit` Weight (`165,038,000 / 81,886`, 40 reads and two writes), preserving phase and placement while advancing the stale candidate. The first cohort slice recognizes two homogeneous installation-skip, pure-rearm, coalesced-fire, or nonterminal placed-fire candidates on the same current tail page before mutation, admits a placed pair only when both scheduler plans require immediate FIFO and one read-only aggregate queue plan validates exact pages, consecutive tickets, final tail/occupancy, duplicates, capacity, and index arithmetic; otherwise it downgrades to one candidate. Dedicated pair probe and execution owners charge and commit both in one transaction. Placed-batch accounting and Weight admission carry exact bounded candidate counts: two uses the generated atomic pair owner, while three/four use the generated maximum-batch owner. Placed-batch production reconstructs one coherent bounded cohort authority; count two uses pair movement while a homogeneous contiguous tail-page prefix of three/four uses descending physical movement followed by stable retained-suffix rewrite and locator repair, and both commit one queue plan plus one cursor/feed advancement; a transactional full-tail-page prototype consumes four authorities in descending physical order with live-locator checks, rejects a corrupted third-candidate generation with exact-root rollback, and commits one aggregate queue plan on success; the fixed-depth radix seek has a separate generated owner charged only while `current_threshold` is absent, so candidate service reuses the retained threshold without sparse-work doubling; production non-tail batch classification charges the tail-refill probe, binds one generation-checked physical-tail suffix to the placed authority, preserves retained source order, groups equal destination obligations so each touched destination page is written once, repairs source/tail/destination locators, handles trimmed and emptied tail pages under separate generated mutation owners, meters their component-wise maximum, commits the aggregate queue and cursor once, and rejects stale authority transactionally, and a route-heterogeneous third candidate truncates a four-candidate prefix after its first two homogeneous immediate-FIFO authorities, consumes it through two movement-without-Hot operations with swapped-tail generation validation, commits one aggregate queue plan, installs final phase/tickets, and advances cursor/feed once; exact instrumentation and rollback evidence cover this path, while separate movement evidence covers split destinations. Compatibility coalesced/rearm/skip pair execution still restores its preflighted feed between scalar units so pending-list rotation cannot substitute another feed. Post-installation exclusion uses `crossing_skip_pair_probe` (`46,515,000 / 6,112`, nine reads) plus `crossing_skip_pair_unit` (`70,960,000 / 6,112`, 10 reads and two writes) and leaves both phases and placements unchanged. Pure rearm uses `crossing_rearm_pair_probe` (`58,249,000 / 23,410`, 11 reads) plus `crossing_rearm_pair_unit` (`430,439,000 / 162,782`, 82 reads and 76 writes) and performs zero canonical activation probes. Already-latched homogeneous fire uses `crossing_coalesced_pair_unit` (`478,630,000 / 162,782`, 89 reads and 76 writes) with no new FIFO placement. The generated atomic `crossing_placed_pair_unit` (`510,967,000 / 162,782`, 94 reads and 80 writes) and maximum four-candidate `crossing_placed_maximum_unit` (`712,602,000 / 162,782`, 108 reads and 84 writes) read/write bounded membership-page keys—the one source fire page and one shared destination rearm page—rather than reloading either key per Actor; generation, locator, canonical state, phase-before-activation, and rollback checks remain per candidate. A malformed second locator classifies fail-closed before execution and proves zero probes, activations, closes, or FIFO placements for its otherwise-valid first peer; a terminal peer is never absorbed into the nonterminal pair owner. A second candidate whose installation revision equals the pending revision truncates the pair before mutation: the valid first Actor fires singly, the second then advances through the dedicated installation-skip owner without latching, placement, retrofire, or phase change. Pair fallback snapshots the cursor member and physical tail member because its first single-member removal uses swap compaction; the tail then becomes the next cursor member for the second execution. Replacing the tail with the encoded adjacent member without replacing single-member compaction would classify one Actor and execute another. A larger cohort must therefore land bounded contiguous-prefix authority, complete preflight, and one-pass source compaction together rather than treating adjacency as a local classifier edit. A newly found threshold is retained before candidate service, homogeneous immediate-FIFO cohorts admit up to four candidates, and mixed routes truncate before mutation. +Pure unlatched rearm retains a generated owner and performs no occurrence collection, activation probe, placement, or close. Classification is staged: every nonempty unit first admits the generated common probe, and only a useful fire-pending plan may admit the single or pair fire extension. No-match, structural search, disabled skip, and unlatched rearm never pay that extension. A latched identity selects disabled skip before Actor-state classification; compatibility coalesced owners remain generated branch envelopes but no longer collect a redundant occurrence fee. + +A newly latched nonterminal fire uses generated `crossing_placed_unit` Weight (`558,181,000 / 162,782`, 89 reads and 80 writes) for phase movement, one User occurrence collection, and exactly one canonical FIFO placement without terminal cleanup. The separately generated economic owner `observation_crossing_trigger_occurrence` is `503,983,000 / 164,107` with 89 reads and 80 writes; it begins from an already published transition and retained threshold, so Oracle publication/signaller work is excluded. A post-installation transition skip uses generated `crossing_skip_unit` Weight (`171,882,000 / 81,886`, 40 reads and two writes), preserving phase and placement while advancing the stale candidate. The first cohort slice recognizes two homogeneous installation-skip, pure-rearm, coalesced-fire, or nonterminal placed-fire candidates on the same current tail page before mutation, admits a placed pair only when both scheduler plans require immediate FIFO and one read-only aggregate queue plan validates exact pages, consecutive tickets, final tail/occupancy, duplicates, capacity, and index arithmetic; otherwise it downgrades to one candidate. + +Dedicated pair probe and execution owners charge and commit both in one transaction. Placed-batch accounting and Weight admission carry exact bounded candidate counts: two uses the generated atomic pair owner, while homogeneous larger cohorts use the generated maximum-batch owner. + +Placed-batch production reconstructs one coherent bounded cohort authority. Count two uses pair movement; a homogeneous contiguous P128 tail prefix may admit up to 128 candidates and uses descending physical movement followed by stable retained-suffix rewrite and locator repair. Both paths commit one queue plan plus one cursor/feed advancement. Exact-root tests reject corrupted generation or compact authority before unsafe grouped mutation. The fixed-depth radix seek has a separate generated owner charged only while `current_threshold` is absent, so candidate service reuses the retained threshold without sparse-work doubling. + +Production non-tail batch classification charges the tail-refill probe, binds one generation-checked physical-tail suffix to the placed authority, preserves retained source order, groups equal destination obligations so each touched destination page is written once, repairs source/tail/destination locators, handles trimmed and emptied tail pages under separate generated mutation owners, meters their component-wise maximum, commits the aggregate queue and cursor once, and rejects stale authority transactionally, and a route-heterogeneous third candidate truncates a four-candidate prefix after its first two homogeneous immediate-FIFO authorities, consumes it through two movement-without-Hot operations with swapped-tail generation validation, commits one aggregate queue plan, installs final phase/tickets, and advances cursor/feed once. + +Exact instrumentation and rollback evidence cover this path, while separate movement evidence covers split destinations. Compatibility coalesced/rearm/skip pair execution still restores its preflighted feed between scalar units so pending-list rotation cannot substitute another feed. Post-installation exclusion uses `crossing_skip_pair_probe` (`46,515,000 / 6,112`, nine reads) plus `crossing_skip_pair_unit` (`70,960,000 / 6,112`, 10 reads and two writes) and leaves both phases and placements unchanged. Pure rearm uses `crossing_rearm_pair_probe` (`58,249,000 / 23,410`, 11 reads) plus `crossing_rearm_pair_unit` (`430,439,000 / 162,782`, 82 reads and 76 writes) and performs zero canonical activation probes. + +Latched homogeneous members use generated disabled-skip service and retain phase, membership, and placement unchanged. Generated atomic pair and maximum-cohort owners charge every useful User authority before committing one bounded membership/queue transaction; generation, locator, compact admission identity, canonical state, phase-before-activation, and rollback checks remain per candidate. A first compact-authority mismatch receives one exact generic scalar fallback; a later mismatch truncates before that candidate and permits only the preceding coherent prefix. A terminal peer is never absorbed into a nonterminal placed owner. + +A candidate whose installation revision equals the pending revision truncates a homogeneous prefix before mutation: earlier valid Actors may commit through their coherent prefix, while the excluded candidate advances through the dedicated installation-skip owner without latching, placement, retrofire, or phase change. A newly found threshold is retained before candidate service. Homogeneous immediate-FIFO preflight and tail cohorts admit up to 128 candidates; exact non-tail source/refill cohorts admit up to 64 because both source and physical-tail reverse locators must be proven. Mixed routes truncate before mutation. If any batch member cannot complete its User occurrence collection, the aggregate attempt rolls back and the admitted branch executes one scalar candidate: its Crossing phase/membership/cursor progression commits without fee/readiness/apoptosis when underfunded, then later members continue without a free retry loop. | Crossing capacity dimension | Reference value | Current evidence | | --- | --- | --- | -| Worker reserve | 10% maximum block Weight | Generated maximum placed-fire batch plus staged probes admits four candidates by both Weight dimensions after base | -| Candidate component cap | 4 per block | Exactly reachable by one admitted homogeneous maximum batch; prevents cheaper branches from escaping the production service contract | -| User memberships per feed | 9,000 | 2,250 uncontended blocks, or 3h45m at six-second blocks | -| Total memberships per feed | 10,000 | 2,500 uncontended blocks, or 4h10m; reserves 1,000 System positions | +| Actor Control envelope | `304,686,077,576 / 676,150` | Every generated complete branch fits component-wise after fixed-context composition | +| Physical page ownership | P128 Crossing / P64 broad fanout | Separate bounds prevent Crossing geometry from taxing broad-page proof; regenerated broad maximum is 304,734 | +| Tail/preflight candidate cap | 128 per admitted cohort | Generated P128 preflight and maximum tail owners fit at most 346,462 ProofSize | +| Non-tail candidate cap | 64 per admitted cohort | Exact source plus physical-tail reverse-locator validation fits at 327,262 ProofSize | +| User memberships per feed | 9,000 | At least 71 homogeneous tail cohorts or 141 exact non-tail cohorts; not a block-latency promise | +| Total memberships per feed | 10,000 | At least 79 homogeneous tail cohorts or 157 exact non-tail cohorts; reserves 1,000 System positions | | Canonical FIFO capacity | 10,000 | One maximum first-fire herd can retain one placement path per Actor | | Pending transitions per feed | 64 | Explicit producer backpressure; not a sustainable publication-rate promise | -The 10,000-member single-threshold release profile exhibits no failure mode distinct from the total per-feed horizon, so the runtime intentionally adds no separate leaf cap; exact page and worker bounds already constrain physical service. These are bounded capacity facts, not a latency SLA. The horizon assumes one maximum homogeneous placed-fire head transition, the configured Crossing reserve, solvent Actors, available placement, no competing materialization family pressure, and six-second blocks. Alternating revisions may occupy all 64 transition slots; admission then fails atomically until service creates capacity. Final release evidence must regenerate this ledger from the accepted production tree. +The 10,000-member single-threshold release profile exhibits no failure mode distinct from the total per-feed horizon, so the runtime intentionally adds no separate leaf cap; exact page and worker bounds already constrain physical service. These are bounded capacity facts, not a latency SLA. The cohort counts assume one homogeneous placed-fire head transition, sufficient Actor Control, solvent Actors, available placement, and no competing materialization pressure. Alternating revisions may occupy all 64 transition slots; admission then fails atomically until service creates capacity. + +`MaterializationFamilyCursor` rotates the block-start family across overdue wakeups, Crossing, and broad fanout as `0 → 1 → 2 → 0`. The coordinator services every family in rotated order, may service only the rotated first family once more through the bounded lending pass, preserves each worker's internal deadline/revision order and counters across grants, advances the cursor before the execution cutoff, and continues materialization while the global breaker suppresses Actor execution. The sum of the three configured ceilings is the single canonical materialization envelope. Each family also has one canonical maximum-unit minimum derived from generated branch Weight, and production integrity requires all three minima to fit together with a positive two-dimensional lendable remainder. -`MaterializationFamilyCursor` rotates the block-start family across overdue wakeups, Crossing, and broad fanout as `0 → 1 → 2 → 0`. The coordinator services every family in rotated order, may service only the rotated first family once more through the bounded lending pass, preserves each worker's internal deadline/revision order and counters across grants, advances the cursor before the execution cutoff, and continues materialization while the global breaker suppresses Actor execution. The sum of the three configured ceilings is the single canonical materialization envelope. Each family also has one canonical maximum-unit minimum derived from generated branch Weight, and production integrity requires all three minima to fit together with a positive two-dimensional lendable remainder. The shared envelope plus fixed base, cleanup, and coordinator Weight is subtracted once to derive the exact guaranteed Actor execution floor, so materialization cannot borrow from execution. A cursor outside `0..3` returns after fixed charged work without running or rewriting a family, and TryRuntime rejects it. Workers consume only the coordinator grant and retain per-block wakeup-scan, Crossing-component, and broad-page counters across grants. The rotated first family receives the production-proven lendable remainder after all three maximum-unit minima. After the first pass, generated coordinator Weight owns bounded due-wakeup, pending-Crossing, and dirty-fanout classification. When the rotated first family still has serviceable work, one bounded second pass offers it all trailing unused reservations without resetting counters, including when its first branch cost less than the maximum-unit minimum. +The shared envelope plus fixed base, cleanup, and coordinator Weight is subtracted once to derive the exact guaranteed Actor execution floor, so materialization cannot borrow from execution. A cursor outside `0..3` returns after fixed charged work without running or rewriting a family, and TryRuntime rejects it. Workers consume only the coordinator grant and retain per-block wakeup-scan, Crossing-component, and broad-page counters across grants. The rotated first family receives the production-proven lendable remainder after all three maximum-unit minima. After the first pass, generated coordinator Weight owns bounded due-wakeup, pending-Crossing, and dirty-fanout classification. When the rotated first family still has serviceable work, one bounded second pass offers it all trailing unused reservations without resetting counters, including when its first branch cost less than the maximum-unit minimum. A permanent Crossing materialization error rolls back the attempted unit, stores one bounded current fault with feed, revision, threshold, and typed class, and halts later Crossing service after base admission instead of retrying unchanged corrupt state every block. Observation fanout applies the same contract with feed, latest revision, available subscriber-page cursor, and class. Wakeup materialization records its temporal key, page cursor, and class after rolling back the unit, then stops at the next charged cursor probe. All three faults are exposed through bounded pallet projection. Their `GlobalBreakerOrigin` clear calls remove exactly the current record, emit its identity, and permit the preserved obligation to resume; callers repair the underlying invariant before clearing or the deterministic worker records the fault again. @@ -451,19 +543,19 @@ Opening-snapshot surface validation runs at genesis, creation, activation, and p `ObservationIngressRevisions` retains the highest accepted nonzero revision while a feed has at least one subscriber. Equal revision is an idempotent no-op, regression fails even after dirty cleanup, and final-subscriber removal deletes the baseline. Feeds without subscribers allocate neither baseline nor dirty state. -`DirtyObservationFeeds` coalesces each subscribed feed to one `latest_revision` equal to its retained baseline, records the first clean-to-dirty block as `dirty_since`, and stores reciprocal previous/next active-dirty links. New state receives zero `fanout_revision` and no subscriber-page cursor. Greater revisions preserve the uninterrupted interval timestamp; clean completion removes dirty state without removing the baseline, while later advancement starts a new interval. +`DirtyObservationFeeds` coalesces each subscribed feed to one `latest_revision` plus its exact `latest_cause_provenance` and `latest_cause_block`, records the first clean-to-dirty block as `dirty_since`, and stores reciprocal previous/next active-dirty links. Progress binds `fanout_revision` plus the independently frozen `fanout_cause_provenance` and `fanout_cause_block`, occupied page, exact subscriber position, `Ordinary | Terminal` branch, and optional `retry_after`. Starting or restarting fanout copies the matching latest revision and cause together; a newer revision may replace only the latest pair while the active pair finishes. The retained source block lets ordinary cohort and scalar/terminal fallback consume the same causal authority without inferring age from worker phase. The protocol-fixed prepass cutoff prevents every current-block Observation cause from executing before N+1; aged work receives no repeated floor and remains ordinary FIFO work. Greater revisions preserve the uninterrupted interval timestamp; clean completion removes dirty state without removing the baseline, while later advancement starts a new interval. -The first fanout unit snapshots the latest revision and exact occupied head; later units retain the next linked page id. +The first fanout turn snapshots the latest revision and exact occupied head. Later turns resume from the retained subscriber position without replaying a committed prefix. Temporary queue or wakeup capacity persists `retry_after = current_block + 1`, so neither direct service nor the outer worker retries the same cursor in the same block. `DirtyObservationListState` stores exact head, tail, fair cursor, and live count. First insertion initializes the list; later insertion appends through the current tail. Completion or last-subscriber removal unlinks at most two neighbors and repairs the cursor. Dirty ingress runs transactionally and reports one conservative Weight independent of subscriber count. Try-state walks the bounded list and reconciles reciprocal links, cursor membership, map ownership, and count. Ingress rejects capacity exhaustion as `DirtyObservationCapacityExceeded` and broken reciprocal ownership as `DirtyObservationInvariant`. Its transaction restores every touched dirty link and list field on failure. The producer decides whether and how to surface rejection; recovery is an explicit later retry after host/operator cleanup, with no package-owned replay queue. -The independently metered fanout worker runs before actor execution in `on_idle`. One admitted unit selects the exact active-dirty cursor and processes at most one occupied `QueuePageSize` subscriber page. It sets only `ActorHot.pending_signal`, preserves existing queue/wakeup membership, and requests canonical paged-queue admission only when neither exists. A capacity failure retains the exact page for retry after ordinary queue cleanup; the scheduler remains the sole execution owner. +The independently metered fanout worker runs before actor execution in `on_idle`. It checks detector-local disabled authority before loading compact Actor state. Useful ordinary service classifies exact non-economic terminal precedence and prepares canonical queue or wakeup placement without an affordability probe. Contiguous queue candidates commit through one bounded append plan; candidates sharing one wakeup key commit under one rollback boundary. Mixed branches split into dense cohorts without creating a second readiness topology. -The fair cursor advances to the selected feed's next active neighbor and wraps to the exact head without empty probes. Per-feed `fanout_revision` and `next_subscriber_page` preserve bounded progress. Completion deletes state only when `latest_revision == fanout_revision`; a newer revision resets the same state to the current occupied head, so an older pass cannot erase a newer change. +Terminal classification persists `TerminalDeferred`; a later scalar terminal turn owns cleanup separately from successful ordinary pages. Crossing transitions retain their source classification and block as causal evidence, while the fixed prepass execution cutoff uniformly prevents every current-block placement from executing in that block. Temporal occurrences remain `Deferred`; paid readiness and terminal substitution use the same FIFO/wakeup owners without a separate timing mode. The fair cursor advances only after its retained work commits. Completion deletes state only when `latest_revision == fanout_revision`; a newer revision restarts at the occupied head, so an older pass cannot erase a newer change. -Fanout admits one complete worst-case page Weight in RefTime and ProofSize before mutation. Host bounds set the independent per-block page ceiling and weight limit. One unit short leaves dirty state, actor latches, queue membership, and cursor unchanged. Package benchmarks cover empty, completing dense, and queue-blocked branches; production coefficients and delivery envelopes belong to the host integration. +Fanout first pays a branch probe, then admits the component-wise maximum of generated disabled-skip, queue, and wakeup page owners plus separately reserved fault-record Weight. Base, ordinary branches, scalar terminal cleanup, fault record, and fault clear retain disjoint owners. Exact fault identity is loaded only after transactional rollback and binds feed, revision, page, subscriber position, actor, semantic/body/admission authority, branch, and class. Host bounds set the independent page ceiling and Weight limit. The typed `AddressEventIngress::preflight`/`notify` boundary owns signal, filtering, and funding-accumulator effects. Producers perform literal read-only preflight and invoke exactly one consequence under their declared post-movement or transactional-precommit atomicity protocol. They propagate rejection and never mutate `ActorHot` or funding storage directly; the host integration owns protocol inventory and rollback evidence. @@ -486,11 +578,16 @@ Primary storage follows explicit owners. Section 13's stable behavioral stores c - `NextActorId`: monotonic actor ID allocator - `ActorIdentities`: one durable identity map for Active and Dormant actors, retaining owner, class/custody locator, mutability, sovereign account, `cycle_nonce`, and non-optional `last_control_mutation_block` - `ActorHot`: active/paused lifecycle, closed Trigger runtime state, counters, pending readiness, block anchor, live queue ticket, exact typed wakeup pointer, and direct `terminal_at` -- `ActorContract`: one scalar trigger, optional block window, and bounded ordered `ContractSteps`; the generated storage descriptor maximum is 8,735 bytes +- `ActorContractHeads`: scalar trigger, schedule/window/completion authority, commitments, Step count, optional inline Step 0, and its optional resource envelope; the P32 generated `MaxEncodedLen` ceiling is 2,237 bytes +- `ActorAdmissionCertificates`: compact runtime/layout/Weight and lifecycle identity; generated descriptor maximum is 208 bytes under both B8 and P32 +- `ActorContractTailChunks`: gap-free authority-bound Steps 1..N plus aligned envelopes in chunks of at most four; generated descriptor maximum is 4,070 bytes per chunk +- `ActorActivationAuthorities`: ObservationChange-only compact placement authority; generated descriptor maximum is 159 bytes - `ActorFunding`: active-only canonical funding-source policy, bounded tracked-asset set, and `funding_accumulated[asset] = amount`; authorized ingress adds checked deltas, fresh cycle opening atomically takes the map as its frozen snapshot, and later ingress remains accumulated for the next cycle +- `ActorRunHeads` / `ActorRunPayloads`: mutable cursor/outcome/authority state separated from immutable Opening and funding snapshots; payload commitment and predicate-result count bind the pair - `ActorIdentityCount`: transactionally maintained O(1) `ActorIdentities` cardinality bounded by `MaxActorIdentities` -- `ActiveActorCount`: transactionally maintained O(1) active/paused cardinality used by activation and operational-cap checks; try-runtime reconciles it against `ActorHot`, `ActorContract`, and `ActorFunding` +- `ActiveActorCount`: transactionally maintained O(1) active/paused cardinality used by activation and operational-cap checks; try-runtime reconciles it against hot state, certified C6 geometry, compact activation authority, funding, and optional split run state - `NextQueueTicket`: shared monotonic global age allocator and common block-start cutoff source +- `PrepassExecutionCutoff`: optional `(block, ticket frontier)` authority frozen by `on_initialize`; matching-block `NextBlock` Drain consumes it, TryRuntime rejects a frontier beyond `NextQueueTicket`, and absence/staleness is reserved for direct physical service fixtures rather than runtime timing conformance - `QueueHead` / `QueueTail` / `QueueOccupancy` / `QueuePages`: one global physical FIFO with a shared O(1) topology preflight, transactional append and exact-live-head consume, exact unconsumed physical-entry capacity, actor-local live-ticket dedup/invalidation, transactional tombstone drain, full-page reclamation, and checked empty partial-tail alignment. Entries store global `ticket` plus `actor_id`; ticket publication/clearing commits with its physical mutation. - `QueueOccupancy` includes tombstones, so physical tail gaps never weaken capacity; package coverage proves occupancy counts invalidated entries until the tombstone drain releases exactly their share, and the wakeup cursor's `MaxActiveActors` capacity overflow fails closed transactionally, preserving the actor's exact existing pointer and bucket. - `WakeupPages` / `WakeupBuckets` / `WakeupCursorPages` / `WakeupCursorLen`: typed paged temporal topology with separate sparse block/tick cursors; `NextWakeupClock` fairly selects the first domain when both are due. `ActorHot.wakeup_pointer` is the sole temporal-membership authority. Try-state rejects key/clock/pointer/slot disagreement and invalid terminal membership, while accepting bounded stale physical tombstones until normal drain converges. @@ -522,8 +619,8 @@ Created Dormant ⇄ Active → Ready → Admitted → Running ⇄ Suspended → Lifecycle calls preserve the split-store boundary: -- `activate_actor` accepts one typed `ActorContract` and validates trigger/cooldown/window, Contract Steps, funding policy, optional auto-close target, tracked assets, cached bounds, class restrictions, active capacity, and the host-configured idle envelope. It then creates matching `ActorHot`, `ActorContract`, and `ActorFunding` entries for a Mutable identity. -- `deactivate_actor` clears queues, wakeups, pending signal, funding, cycle, and fee state while preserving identity, owner slot, sovereign address, and balances. +- `activate_actor` accepts one typed `ActorContract` and validates trigger/cooldown/window, Contract Steps, funding policy, optional auto-close target, tracked assets, resource bounds, class restrictions, active capacity, and the host-configured idle envelope. It then writes matching hot state, certified C6 fragments, optional activation authority, and funding for a Mutable identity. +- `deactivate_actor` transactionally clears queues, wakeups, trigger topology, hot state, every C6 head/certificate/tail fragment, activation authority, funding, and both run partitions while preserving identity, owner slot, sovereign address, nonce, and balances. Reactivation derives a fresh complete active epoch, so Dormant identity cannot own or ambiguously reference orphan body fragments. Creation expresses dormancy by Contract absence: every creation path accepts `Option`, while activation and simulation accept a direct Contract because their Dormant branch is impossible. No lineage/reopen call or explicit actor-id creation path remains. @@ -547,8 +644,8 @@ Scheduler hygiene follows the specification's bounded liveness matrix: - `next_eligible_at` combines block cooldown and window start for signalled Actors; timestamp cadence has its separate aligned-tick owner. - Execution-created late enqueues join next-block queue state only when block eligibility reaches that point; later eligibility receives one typed wakeup. -- `Manual`, `AddressEvent`, and `ObservationChange` retain block cooldown/window gates. `Cadenced` admits neither and owns one timestamp deadline while Idle. -- A suspended Cadenced Actor owns only its ordinary block retry. After the cycle terminates, it rearms one aligned timestamp deadline strictly after the observed tick. +- `Manual`, `AddressEvent`, and `ObservationChange` retain block cooldown/window gates. `Cadenced` admits neither and owns one timestamp Trigger deadline throughout its Active epoch. +- A suspended Cadenced Actor may own its ordinary block retry while one useful due occurrence consumes the Trigger deadline into deferred readiness. No timestamp deadline exists while latched; the next Opening rearms it from current consensus time. - Closed or missing stale queue and wakeup entries are ignored deterministically. Pallet regressions cover paused-pop-resume, cooldown, and pre-window ordering. Runtime integration proves actor-to-actor ingress remains queued across the `on_idle` boundary. diff --git a/template/pallets/actors/docs/embedding.md b/template/pallets/actors/docs/embedding.md index b4bba420..8b397f1f 100644 --- a/template/pallets/actors/docs/embedding.md +++ b/template/pallets/actors/docs/embedding.md @@ -5,7 +5,7 @@ **Status** - **Component**: `pallet-deos-actors` (Rust crate `pallet_deos_actors`) -- **Release line**: `0.7.23` +- **Release line**: `0.7.24` - **Audience**: external runtime implementers embedding Actors without inheriting DEOS/TMCTOL topology - **Companions**: [`README.md`](../README.md), [DEOS Actors Specification](./specification.en.md), [DEOS Actors Architecture](./architecture.en.md) - **Source navigation**: `src/types.rs` is the stable public facade; the architecture map routes Contract, Lifecycle, Scheduler, and Observation type owners; `src/contract.rs` owns semantic classification rather than public type definitions @@ -13,7 +13,7 @@ `pallet-deos-actors` is a bounded deterministic actor kernel. A downstream runtime can embed it without inheriting DEOS governance policy, TMCTOL bucket topology, native staking design, or the current System Actor catalog. -The `0.7.22` package is fresh-genesis source only. The current baseline defines no migration from an earlier activation, detector, bond, or index layout and must not be used to upgrade a deployed chain; a downstream live lineage owns its own explicit bounded migration before adopting this storage baseline. +The `0.7.24` package is fresh-genesis source only. The current baseline defines no migration from an earlier activation, detector, hold, resource, or index layout and must not be used to upgrade a deployed chain; a downstream live lineage owns its own explicit bounded migration before adopting this storage baseline. Use this guide with the normative [DEOS Actors Specification](./specification.en.md) and shipped [DEOS Actors Architecture](./architecture.en.md). The specification defines portable semantics; this package document defines the host contract; the architecture document maps the reusable package implementation and source ownership. @@ -21,17 +21,17 @@ Use this guide with the normative [DEOS Actors Specification](./specification.en The repository's [`embedding-runtime`](https://github.com/atmo-network/deos/tree/main/template/pallets/actors/embedding-runtime) package is a minimal external-consumer runtime fixture owned by this pallet boundary. It uses local account and asset types, local weight bindings, zero genesis System Actors, smaller queue/wakeup pages, native balance ingress through its own transaction extension, and no DEOS/TMCTOL primitives or topology. -The default profile proves deterministic unsupported optional adapters and all three error policies, including that Permanent adapter failures never create Continuation. The opt-in `dex-fixture` profile adds User `SwapOut` with an absolute cap and an explicitly Temporary `SwapIn` fixture for Mutable User/System Continuation. This package is executable portability evidence, not a second product or a normative actor topology. +The default profile proves deterministic unsupported optional adapters and all three error policies, including that Permanent adapter failures never create suspended run state. The opt-in `dex-fixture` profile adds User `SwapOut` with an absolute cap and an explicitly Temporary `SwapIn` fixture for Mutable User/System suspended run state. This package is executable portability evidence, not a second product or a normative actor topology. | Portable capability | Independent evidence | | --- | --- | | Actor topology | Zero genesis System actors; local `u64` accounts and `u32` assets | | Error policy | Abort, continue, and Temporary-only retry paths | -| Continuation | Mutable User/System suspend, cooldown, suffix resume, cancel, and pure close | +| Actor run state | Mutable User/System suspension persists the open nonce separately from finalized identity, the sole cursor, exact retry eligibility, complete immutable Opening/funding snapshots, cumulative plus last-Step outcomes, typed suspension, cancel, and pure close | | Concurrent ingress | Executive transfer during suspension latches once without duplicate ticket or wakeup | | Permissions | Immutable rejects `RetryLater { max_attempts }`; User rejects `Mint`; User accepts `SwapOut` | | Optional capability | Default DEX/staking/donation fail Permanent; DEX fixture supplies only local pairs | -| State and weights | Canonical Trigger runtime state, refundable bond field, coordinator cursor, fresh storage version, try-state reconciliation, and nonzero production-derived classes | +| State and weights | Canonical Trigger runtime state, dedicated refundable Actor-state hold, coordinator cursor, fresh storage version, try-state reconciliation, and nonzero production-derived classes | Both profiles use the pallet's canonical FIFO and wakeup stores. The fixture defines no DEOS primitive, TMCTOL helper, System Actor catalog, custom scheduler, or product topology. @@ -40,26 +40,31 @@ Both profiles use the pallet's canonical FIFO and wakeup stores. The fixture def An embedding runtime must provide only the bounded host surface that Actors cannot own itself: - `AssetOps`: Transfer, burn, mint, balance, minimum-balance, and exact `preflight_transfer` consequences over local `AccountId`, `AssetId`, and `Balance` types. Preflight covers both source withdrawal and recipient depositability, preserves explicit retry classification, and must agree with the following transfer under unchanged state. Generic Actors does not promise that a provider-backed or reserved-only zero-free native recipient can accept sub-minimum ingress. +- `AdmissionCertificateAuthority`: Typed fail-closed owner for the current Actor semantics version, composed production Weight identity, body geometry version, configured-bounds commitment, and maximum lifecycle Weight. These runtime-owned facts remain separate from authored semantic Contract identity. +- `StepControlWeight`: Typed authority returning its production Weight identity, maximum Actor-control Weight for an exact Step/context, and valid actual Weight for the committed `StepControlExecution { outcome, placement }`. The context distinguishes the inline head from the populated C6 tail fragment, charges exact authored Opening tail-chunk reconstruction, and bounds predicate/Opening/funding geometry. Missing or component-wise greater-than-reserved actual evidence rolls back the complete attempt; a host may conservatively report the admitted maximum until atomic generated branch owners justify reclaim. +- `TaskEffectWeight`: Typed authority returning its production Weight identity, maximum admission Weight, and valid actual Weight for `TaskEffectExecution::{NotInvoked, Invoked}`. Non-invocation returns zero; invocation includes committed or typed-failure work from the canonical generated mechanism owner. Missing or component-wise greater-than-reserved actual evidence rolls back the complete attempt and halts the pass. - `ObservationProvider`: Current typed scalar state classified as Unavailable, Uninitialized, Fresh, or Stale over the embedding's `ObservationFeedId`; the same type identifies the scalar `ObservationChange { feed }` trigger. The one-feed subscription index, transactional latest-revision dirty ingress, and independently metered deferred fanout ship with the package. Fanout sets the existing pending latch and invokes the existing scheduler only; the boundary has no concrete Oracle, producer, history, or off-chain dependency. - `ObservationTransitionIngress`: Typed certified-publisher boundary accepting one externally owned monotonic feed revision with exact previous/current scalar values. The host publisher calls this trait rather than the pallet's broad-fanout helper directly; a rejection rolls back the publisher transaction. - `DexOps`: Caller-aware exact-in quotes and capacity-bounded exact-out swaps with deterministic fees, slippage, rounding, and failure behavior. Swap methods receive only `ExecutionContext { actor, actor_type }`; the package derives immutable `ActorType` from stored actor state so an embedding never reconstructs authority from account catalogs or sovereign-address heuristics. - `LiquidityOps`: Addition, removal, and pair-scoped donation; donation permits reserve strengthening without LP receipt minting when supported by runtime policy. - `StakingOps`: Generic staking operations plus adapter-visible share balance and optional transferable share-asset mapping for Unstake amount resolution. - `FundingAuthority`: Default-deny authorization for explicit actor/source pairs when an actor selects `RuntimePolicy`; pallet-owned policies do not delegate. -- `TriggerStateBond`: Exhaustive host policy mapping every authored Trigger family to the refundable amount reserved for an Active User. The adapter transactionally moves the old stored amount to the prospective amount before topology commit, releases it on deactivation and terminal close, reports one maximum for benchmark provisioning, and returns zero for System Actors. Bond policy must depend only on authored Trigger semantics, not pages, radix shape, queue occupancy, or detector history. - `SovereignAccountDeriver`: Deterministic infallible mapping from tagged User/System custody inputs into the host `AccountId`; it must preserve domain separation, reject no admitted identity through panic, and remain stable for custody reattachment. - `SystemActorContractValidator`: Read-only candidate validation called before an Active System Contract is installed or replaced. Bind `()` when the host defines no closed System activation topology; a host with an explicit catalog may reject undeclared or cyclic runtime-owned edges here. The validator must remain bounded and must not turn rank or edge metadata into Actor Contract identity or User policy. -- `Time`, `CadenceTickMillis`, and `MaxCadenceDelayTicks`: Deterministic consensus milliseconds, the host's nonzero tick quantum, and its ten-Julian-year cadence horizon. The pallet floors readiness, ceils activation anchors, and never derives cadence or its admission bound from local block count. `TargetBlockTime` and `MaxExecutionDelayBlocks` independently bind the ten-Julian-year consensus-block horizon. -- `WeightInfo`: The single runtime-derived numeric authority covers worst-case Task classes, calls, Predicates, fees, ingress, scheduler work, orchestration, Continuation, finalization, and cleanup in both Weight dimensions. Transfer, Mint, and split fanout include possible synchronous address-event ingress; Burn remains independently priced without transfer-ingress proof; adapter-free `StopCycle` prices its explicit stop event. -- `Weight generation`: The `SubstrateWeight` and `()` implementations shipped in `src/weights.rs` are hand-written estimates, not benchmark output, and both underprice execution. The DEOS reference runtime measures `create_user_actor` at roughly thirteen times the packaged RefTime with seven times its ProofSize. Run `frame-benchmarking` against your own runtime and bind the generated file. +- `Time`, `CadenceTickMillis`, and `MaxTemporalDelayTicks`: Deterministic consensus milliseconds, the host's nonzero tick quantum, and the shared ten-Julian-year AtTime/Cadenced horizon. The pallet floors readiness, ceils activation anchors, and never derives cadence or its admission bound from local block count. `TargetBlockTime` and `MaxExecutionDelayBlocks` independently bind the ten-Julian-year consensus-block horizon. +- `BlockResourceBudget`: Immutable checked two-dimensional budget derived from the host's conservative fixed block envelope. The host selects its own Actor Control and Shared Economic ratios, Actor/user base turns, and borrowing policy; generic Actors requires component-wise bounds, class-neutral FIFO service, maximum reservation, valid-actual settlement, and fail-closed invalid composition. The DEOS equal-thirds policy is reference-runtime composition, not package law. +- `Actor Prepass provider`: Every authoring node inserts `ActorPrepassInherentData` through `provide_actor_prepass_inherent_data`; the package accepts only `ACTOR_PREPASS_INHERENT_VERSION`, derives one payload-free Mandatory call, and rejects missing or unsupported provider data during inherent checking. `PrepassContext` is the host's bounded read-only authority proving required Timestamp and consensus-context inherents already executed; false rejects before resource or cutoff mutation. +- `WeightInfo`: The single runtime-derived numeric authority covers worst-case Task classes, calls, Predicates, fees, ingress, scheduler work, orchestration, Actor run state, finalization, and cleanup in both Weight dimensions. Transfer, Mint, and split fanout include possible synchronous address-event ingress; Burn remains independently priced without transfer-ingress proof; adapter-free `StopCycle` prices its explicit stop event. +- `Weight generation`: The `SubstrateWeight` and `()` implementations shipped in `src/weights.rs` are hand-written estimates, not benchmark output, and are not production authority. Run `frame-benchmarking` against the complete host runtime, bind the generated file, and regenerate after any adapter, bound, resource, storage, or hook change. - `contract`: Exhaustive read-only classification of each Task, Predicate, amount resolution, and error policy. Its task weight owner delegates to `WeightInfo`; it does not replace runtime measurement, capability configuration, or canonical task parameters. -- `WeightToFee`: Deterministic conversion from task weight upper bound to fee-native execution charge. +- `WeightToFee`: Deterministic conversion from generated family-specific Trigger-occurrence maxima into per-occurrence charges, complete Pipeline Machine/cleanup maxima into later Opening charges, and current Task-effect maximum/actual evidence into Action-fee settlement. - `FeeCollector` + `FeeSink`: One atomic ledger-only runtime boundary that transfers every User fee in full into the mandatory deposit-capable collection destination without invoking Actors ingress, funding, readiness, or placement. +- `StateHoldCurrency` + `RuntimeHoldReason`: One native fungible hold adapter with the pallet's dedicated `HoldReason::ActorState`, plus nonzero or explicitly policy-zero `ActorStateHoldBase` and `ActorStateHoldPerByte` prices. The host must preserve exact hold/release semantics and enough hold-reason capacity for this independently aggregated owner obligation. User owners back identity, fixed hot-header capacity, actual C6 head/body, detector, funding, and the type-derived maximum run footprint for the complete Active installed lifetime; deactivation releases Active components. This reservation prevents autonomous Opening from requiring new owner hold capacity. System Actor state remains host-capacity-owned. - `AddressEventIngress`: Typed certified-ingress boundary (`AddressEventIngress::preflight`/`notify`) over the package `AddressEvent` value. Preflight is read-only (lifecycle, funding, trigger, and required placement); notify executes exactly once at the host's declared post-movement or transactional-precommit consequence point and rejects through `IngressFailure { error, retry }` with the same closed Temporary/Permanent classification as `TaskFailure`. - Governance/system origins, a two-dimensional hook weight meter, runtime-reserved `ActorOnIdleReserve`, owner-slot/queue/wakeup/active/total-identity/sweep bounds, `MaxOpeningSnapshotEntries`, `MaxIdleStarvationBlocks`, and fee constants. - Reactive-capacity configuration: total and User Crossing memberships per feed, the protected System difference, transition/leaf/page/candidate caps, broad-fanout pages, wakeup scans, and three two-dimensional family ceilings. Their sum is one shared materialization envelope; generated maximum-unit minima, persisted rotated family order, resumable component counters, and one bounded lending pass preserve the exact Actor execution remainder. - Fault control: Host origins for the global breaker and bounded current Crossing, broad-fanout, and wakeup fault repair. A host read model may expose semantic current faults and per-feed capacity, but raw pages, radix geometry, generations, cursors, and unbounded history remain private topology or materialized data. -- Canonical FIFO configuration: non-zero page size, bounded `MaxQueueLength`, `MaxQueueEntriesScannedPerBlock`, and `MaxExecutionsPerBlock`. One `NextQueueTicket`, one cutoff, one actor-local ticket, one scheduler, one wakeup substrate, and one Continuation owner govern every actor. Actor class, actor id, execution share, and priority policy never change service order. +- Canonical FIFO configuration: non-zero page size, bounded `MaxQueueLength`, `MaxQueueEntriesScannedPerBlock`, and `MaxExecutionsPerBlock`. One `NextQueueTicket`, one cutoff, one actor-local ticket, one scheduler, one wakeup substrate, and one Actor run-state owner govern every actor. Actor class, actor id, execution share, and priority policy never change service order. - Under `runtime-benchmarks`, `setup_predicate_assets` must provide enough valid distinct assets to measure the maximum bounded-DNF predicate count honestly; repeated keys do not establish worst-case ProofSize. The host runtime owns those bindings. Actors core owns scheduling, admission, task orchestration, lifecycle, bounded state, fee reservation, amount resolution, task-scoped transactions, and observability events. Dormant identities retain address/ownership lineage under the total-identity bound but own no contract or scheduler state; runtime-specific custody-only addresses remain outside generic actor storage. @@ -71,13 +76,16 @@ Deterministic User and System custody derivation survives host account-provider A runtime may bind unit implementations for capabilities it does not expose: - `AssetOps = ()` rejects Transfer, Burn, and Mint mutation and reports no deposit viability; it never fabricates successful ledger effects. +- `AdmissionCertificateAuthority = ()` returns no current authority, so certificate construction fails closed. +- `StepControlWeight = ()` returns `None` for maximum and actual evidence, so no control envelope is admitted. +- `TaskEffectWeight = ()` returns `None` for maximum and actual evidence, so no Task effect envelope is admitted. - `ObservationProvider = ()` returns Unavailable for every feed and makes every observation Predicate false without creating a Task failure. - No DEX means swap tasks fail deterministically through `StepErrorPolicy`. - No liquidity support means `AddLiquidity`, `RemoveLiquidity`, and `DonateLiquidity` fail through `LiquidityOps`. - No staking means `Stake` and `Unstake` fail through the staking adapter. - No runtime-authorized funding pairs means `FundingAuthority = ()`, which safely denies every `RuntimePolicy` provenance. -Unsupported adapters are valid only when user-facing plan builders and runtime docs make the unsupported task surface clear. They must fail deterministically without panic, mutation, loops, or off-chain nondeterminism. Unsupported capability is permanent and must never become retryable Continuation state. +Unsupported adapters are valid only when user-facing plan builders and runtime docs make the unsupported task surface clear. They must fail deterministically without panic, mutation, loops, or off-chain nondeterminism. Unsupported capability is permanent and must never become retryable suspended run state. ## 3. Ownership Boundary @@ -110,15 +118,15 @@ The DEOS/TMCTOL catalog of burn actors, fee sink, liquidity actors, buckets, tre ## 6. Boundary Contracts -- `Fee admission`: Actors reserves fee-native spend capacity and invokes atomic `FeeCollector` at most once per attempted User step after read-only evaluation/preparation. Non-executable outcomes charge evaluation-only; executable outcomes charge evaluation plus upper-bound execution fee together. Collection transfers the full amount into `FeeSink`, while downstream allocation remains outside the generic pallet. -- `Fee conversion`: Actors asks `WeightToFee` for deterministic upper-bound pricing; runtime task bounds must include adapter and routing work in both Weight dimensions. -- `Task amount safety`: Transfer, SplitTransfer, Burn, exact-input swap, liquidity, staking, and donation spend through `PreserveSpend`, leaving the adapter-reported asset minimum intact. +- `Fee admission`: Creation charges process establishment once. Only a useful User Trigger transition from `pending_signal = false` to `true` performs Actor-specific Trigger work and charges its generated family owner; redundant latched activity does neither, including while busy. Later Idle readiness consumption charges complete Pipeline Machine/cleanup control before Opening and refunds no prior Trigger fee on insufficient admission. Each Action-bearing Task reserves only its current maximum effect and settles valid actual effect Weight on success or typed failure. Non-invoked `FundingUnavailable` charges zero Action fee. Ledger-only collection must not invoke Actors ingress; failure rolls back only its owning transition. +- `Fee conversion`: Actors asks `WeightToFee` for deterministic maximum and actual pricing. Every runtime control/effect provider must return trustworthy component-wise bounded actual evidence; task bounds include adapter and routing work in both Weight dimensions. +- `Task amount safety`: Transfer, SplitTransfer, Burn, exact-input swap, liquidity, staking, and donation spend through `PreserveSpend`, leaving the adapter-reported asset minimum and current-attempt Action-fee reservation intact; no future Action fee is held. - `DEX amount safety`: Exact-in quotes must match caller fees and executable route selection; exact-out receives a policy-derived maximum input that preserves minimum balance and future fee reserve. A host runtime that resolves `RemoveLiquidity` from an LP token must own an O(1) reverse pair index or an equivalently bounded generated lookup outside generic Actors and update it on every supported pool-creation path. - `Staking amount safety`: Unstake current/trigger/all modes resolve through adapter shares; last-funding mode requires an adapter-mapped transferable share asset. - `Ingress triggers`: Every supported producer must route through the typed `AddressEventIngress::preflight`/`notify` boundary and declare one atomic ordering protocol. Prefer read-only preflight, movement, then exactly one notification in one producer transaction. A host may precommit the Actors consequence only when a non-cloneable holding or equivalent ownership constraint prevents post-movement recovery, and then must prove deposit failure restores the exact holding, ledger, events, and Actors state. Recoverable queue/wakeup capacity is Temporary; monotonic exhaustion, corruption, and invariant failure are Permanent. Runtime event scanning, balance-diff discovery, silent failure, and protocol-order misdescription are unsupported. -- `Hook admission`: `on_idle` must reserve its generated fixed base before storage access. `ActorOnIdleReserve` must equal genuinely reserved gross headroom and Actors meters against the component-wise minimum of that reserve and actual remaining Weight. Opening plans and bounded `RetryLater` suffixes plus measured pure cleanup must fit after `scheduler_admission_overhead` in both Weight dimensions. Temporal and explicit repair units consume only available headroom and may defer actor execution, so exact actor-local markers and cross-block convergence form part of the embedding contract. +- `Mandatory phase and hook admission`: The host Executive must order required context inherents before exactly one Actor Prepass and every signed extrinsic after it. Prepass freezes one FIFO cutoff and enters `ExternalPhase`; `on_idle` drains only that cutoff through `FreshDrain`, and `on_finalize` requires `Finalizable` with zero outstanding reservations. `on_idle` must reserve its generated fixed base before storage access. `ActorOnIdleReserve` must equal genuinely reserved gross headroom and Actors meters against the component-wise minimum of that reserve and actual remaining Weight. Opening plans and bounded `RetryLater` suffixes plus measured pure cleanup must fit after `scheduler_admission_overhead` in both Weight dimensions. Temporal and explicit repair units consume only available headroom and may defer actor execution, so exact actor-local markers and cross-block convergence form part of the embedding contract. - `Starvation observability`: `IdleStarvationState` is `Healthy`, `Starving { consecutive_blocks }`, or `Alerted { consecutive_blocks }`. Each starved actor-service pass increments the stored count, threshold detection and alerted recovery emit once, and breaker periods freeze the state because no actor-service pass occurs. -- `Continuation`: Mutation adapters return explicit `TaskFailure { error, retry }`; unknown failures and unsupported unit adapters are Permanent. Only Mutable actors admit `RetryLater { max_attempts: 2..=MaxRetryAttempts }`; the protocol-fixed host constant is 10. Runtimes bind generated suspension, retry, completion, cancellation, snapshot-surface, and suffix-admission classes without adding another scheduler or off-chain keeper dependency. +- `Actor run state`: Mutation adapters return explicit `TaskFailure { error, retry }`; unknown failures and unsupported unit adapters are Permanent. Only Mutable actors admit `RetryLater { max_attempts: 2..=MaxRetryAttempts }`; the protocol-fixed host constant is 10. A suspended run stores its open `cycle_nonce` while identity retains the latest terminated nonce, the sole current cursor, exact `eligible_at`, complete immutable Opening/funding snapshots, cumulative outcomes, exact last-Step outcome, and typed suspension. Runtimes bind generated suspension, retry, completion, cancellation, snapshot-surface, and suffix-admission classes without adding another scheduler or off-chain keeper dependency. - `Task weight class`: The runtime must classify every task with a deterministic upper bound. Admission may be conservative; it must not underprice execution. - `Core task admission`: Extend the portable `Task` enum only for a reusable economic primitive that existing composition plus an adapter cannot express without violating atomicity or custody. The change must ship bounded typed parameters, amount/funding/donation semantics, adapter ownership, events/errors/rollback behavior, generated two-dimensional weights, production-budget evidence, semantic tests, and an explicit SCALE/schema-version decision together; runtime topology and product policy stay in adapters and actor graphs. - `Compatibility/versioning`: `0.7.x` is a fresh-genesis pre-launch line. The first launched downstream compatibility epoch, not an unrelated package-major declaration, begins append-only public enum discriminants and dispatch indices for that chain. Later encoded argument or storage-layout changes require the owning compatibility/migration contract, while additive host adapters and weight recalibration follow package/runtime version policy. A Cargo version never substitutes for pallet `StorageVersion`, runtime `spec_version`, or a bounded live-chain migration. @@ -162,10 +170,10 @@ A runtime embedding Actors should add local tests for any adapter that mutates m - Exact-out never debits above the Actors-provided capacity, and Unstake dynamic modes resolve against shares rather than the base asset. - Healthy empty `on_idle` leaves no starvation-state key or recovery event; first starvation, one-time alert, prolonged alert, breaker clearing, and one-time recovery match the transition contract. - Mutable User and System plans suspend only on explicitly Temporary failures or `FundingUnavailable` under `RetryLater { max_attempts }`; retry starts at the same cursor without prefix replay and uses one nonce across attempts. -- Permanent and unsupported-adapter failures create no Continuation; Immutable admission rejects bounded `RetryLater`; User `SwapOut` requires explicit `InputLimit::LiveQuote` or `InputLimit::Absolute(nonzero)` intent, always enforced with current preservable capacity; liquidity addition/removal require fixed non-zero output minima at every adapter boundary; System-only `Mint` remains unchanged. +- Permanent and unsupported-adapter failures create no run state; Immutable admission rejects bounded `RetryLater`; User `SwapOut` requires explicit `InputLimit::LiveQuote` or `InputLimit::Absolute(nonzero)` intent, always enforced with current preservable capacity; liquidity addition/removal require fixed non-zero output minima at every adapter boundary; System-only `Mint` remains unchanged. - Direct ingress during suspension preserves one queue ticket/wakeup and latches the next signal. The current retry retains its frozen funding snapshot; accepted later ingress checked-adds into `funding_accumulated` for the next fresh run regardless of current-run completion, failure, or cancellation. - Saturated mixed-class load preserves one strict global ticket order, drains only tombstones before the oldest live head, blocks followers behind an unadmitted head, preserves exact physical occupancy through lazy tombstones, and never executes one actor twice in a block. -- Explicit cancellation, plan/policy/schedule replacement, deactivation, terminal transition, and pure close delete Continuation without compensation, prefix rollback, funding restoration, or sovereign-balance movement. A retained Active actor's cancellation exactly invalidates its live wakeup before any latched next-run signal is re-primed. +- Explicit cancellation, plan/policy/schedule replacement, deactivation, terminal transition, and pure close delete Actor run state without compensation, prefix rollback, funding restoration, or sovereign-balance movement. A retained Active actor's cancellation exactly invalidates its live wakeup before any latched next-run signal is re-primed. ## 9. Non-Goals diff --git a/template/pallets/actors/docs/specification.en.md b/template/pallets/actors/docs/specification.en.md index 32640f5d..11fdb7a8 100644 --- a/template/pallets/actors/docs/specification.en.md +++ b/template/pallets/actors/docs/specification.en.md @@ -4,465 +4,770 @@ - **Target**: `pre-1.0.0` - **Status**: Normative -RFC 2119/RFC 8174 key words are normative when uppercase. This document defines runtime behavior and semantic type meaning. Runtime metadata defines exact SCALE encoding. Generated storage and Weight descriptors define physical layout and measured values. +RFC 2119/RFC 8174 key words are normative when uppercase. -Rust code blocks that do not declare public types, interfaces, events, errors, or calls are normative semantic pseudocode. They define branch order, state dependencies, and error propagation, but not source-level identifiers, ownership syntax, trait bounds, or metadata encoding. +This document owns runtime behavior and semantic type meaning. Runtime metadata owns exact SCALE encoding. Generated storage descriptors own exact physical layout. Generated Weight descriptors own measured `Weight(RefTime, ProofSize)`. + +Rust blocks that do not declare public types, calls, events, errors, or runtime APIs are normative semantic pseudocode. They define branch order, dependencies, and failure propagation, not source identifiers or Rust ownership syntax. + +--- + +## 1. Authority and sole ownership + +### 1.1 Sole-owner rule + +Every semantic function has exactly one normative owner in this specification. Other sections MAY use that function only by reference in parentheses, for example `Opening (§5.2)`. + +A repeated description MUST NOT redefine behavior. If two passages conflict, the passage in the sole-owner section wins. + +### 1.2 Owner registry + +| Function or semantic surface | Sole normative owner | +| --- | --- | +| Core determinism, boundedness, Q1, and atomicity | §2 | +| Actor Contract public shape | §3.1 | +| Semantic Contract identity and body commitment | §3.2 | +| C6 hot-head/lazy-tail geometry | §3.3 | +| Admission certificate and Pipeline Machine envelope | §3.4 | +| Canonical partitions and field ownership | §3.5 | +| Trigger families and runtime phase | §4.1 | +| Useful Trigger occurrence | §4.2 | +| Trigger latch and redundant-source behavior | §4.3 | +| Trigger underfunding and source advancement | §4.4 | +| Trigger re-arm | §4.5 | +| Causal cohort | §4.6 | +| Trigger-family timing and crossing semantics | §4.7 | +| Cycle and run state | §5.1 | +| Pipeline Opening | §5.2 | +| Opening and funding snapshots | §5.3 | +| Cycle nonce | §5.4 | +| Zero-Step Cycle | §5.5 | +| Cycle completion and cancellation | §5.6 | +| One-Step attempt transaction | §6.1 | +| Precondition | §6.2 | +| Amount resolution | §6.3 | +| Funding accumulation | §6.4 | +| Step outcome and error-policy interpretation | §6.5 | +| Task semantics | §6.6 | +| Creation fee and state hold | §7.2 | +| Trigger fee | §7.3 | +| Pipeline Machine fee | §7.4 | +| Action fee | §7.5 | +| Actor Control and Shared Economic meters | §7.6 | +| Active Actor classification | §8.1 | +| Placement and temporal readiness | §8.2 | +| Prepass, cutoff, Drain, and FIFO service | §8.3 | +| Detector workers, cohorts, and faults | §8.4 | +| Scheduler liveness | §8.5 | +| Class and mutability | §9.1 | +| Terminal precedence | §9.2 | +| Close cleanup | §9.3 | +| Minimal User apoptosis | §9.4 | +| Control transitions and breaker | §9.5 | +| Sovereign-account derivation | §10.1 | +| User slots and System locators | §10.2 | +| Custody reattachment and recovery | §10.3 | +| Protected minimum | §10.4 | +| Canonical Task-effect ownership | §11.1 | +| Adapter interfaces | §11.2 | +| Failure classification | §11.3 | +| Swap, liquidity, staking, and ingress special rules | §11.4 | +| Calls and authorization | §12.1 | +| Events and ordering | §12.2 | +| Runtime APIs | §12.3 | +| Errors and projections | §12.4 | +| Storage and integrity | §13.1 | +| Runtime upgrades | §13.2 | +| Runtime configuration | §13.3 | +| Conformance | §13.4 | --- -## 1. Core Contract - -- Equal state and block context MUST produce equal behavior. -- Work MUST be O(1) or O(K) under explicit finite bounds and admit complete `Weight(RefTime, ProofSize)` before mutation. -- An Actor Contract contains bounded linear `ContractSteps`: no loops, jumps, nested contracts, opaque dispatch, Task-authored memory, or authored whole-contract rollback. -- Every admitted path ends in a named completion, skip, failure, suspension, cancellation, close, or state-preserving deferral; unknown capability or failure fails closed. -- Actor Contract identity is the domain-separated digest of canonical typed SCALE bytes plus explicit runtime binding; JSON text, whitespace, object-key order, comments, labels, and client serialization choices MUST NOT affect it. -- `ActorIdentity`, `ActorHot`, `ActorContract`, `ActorFunding`, and optional `ContinuationState` are the only canonical actor partitions; composite views are read-only. -- Authored Trigger semantics live only in `ActorContract`; stateful Trigger phase lives only in `ActorHot.trigger_runtime_state`; detector pages, radix geometry, subscriptions, locators, cursors, generations, and worker limits are derived physical topology with no semantic authority. -- User and System share one strict FIFO, temporal-readiness layer, cutoff, and service order. -- One generated runtime `WeightInfo` owns every numeric Weight and every Weight-derived User fee. -- Attempt Weight and User fee bounds are derived from the current bounded contract at use time; no derived Weight or fee state is stored. -- Arithmetic is checked unless a formula explicitly names saturating arithmetic. A positive resolved exact amount MUST NOT be silently reduced to fit capacity; explicit cap formulas MAY use `min` or saturating subtraction, and insufficient exact capacity yields the specified non-executable outcome. -- Close deletes actor semantics, preserves sovereign balances at the deterministic account, releases the User slot or System locator, and performs no transfer. -- An authored terminal custody drain MAY transfer one explicitly named asset immediately before `ProductiveCycleCompleted` close. Custody left by any other close requires fresh reattachment to the same deterministic account. -- User creation pays only on committed creation; User executable steps are attempt-priced; System execution is Actors-fee-exempt. -- Actors has no recurring rent, task-fee refund, implicit balance discovery, or background solvency scan. - -### 1.1 Terms - -| Term | Contract | +## 2. Core contract + +### 2.1 Invariants + +1. Equal canonical state and block context MUST produce equal behavior. +2. Every path MUST be `O(1)` or `O(K)` under explicit finite bounds. +3. Complete `Weight(RefTime, ProofSize)` for the current transition MUST fit before semantic mutation. +4. An Actor Contract is a bounded linear sequence of `0..=MaxContractSteps` Steps (§3.1). +5. Contracts contain no loops, jumps, nested contracts, opaque dispatch, Task-authored memory, or authored whole-pipeline rollback. +6. The service quantum is Q1: one Actor MAY commit at most one Step in one block (§6.1). +7. A Pipeline MAY span multiple blocks and is non-atomic across committed Steps (§6.1). +8. An admitted Pipeline MUST NOT undergo economic apoptosis before its Cycle boundary (§9.4). +9. A cause observed in block `N` cannot authorize Actor execution before block `N + 1` (§8.3). +10. User and System Actors share one strict FIFO and class-neutral service order (§8.3). +11. Trigger occurrence, Pipeline Opening, and Action execution are independent transitions (§4.2, §5.2, §6.1). +12. Trigger, Pipeline Machine, and Action work have disjoint economic owners (§7.3-§7.5). +13. Close deletes process semantics and MUST preserve sovereign custody (§9.3, §10.3). +14. Unknown capability, stale authority, unbounded work, invalid actual Weight, or unknown downstream failure MUST fail closed. +15. Rejected control transitions restore Actors state, Actors events, scheduler state, topology, and Actors fee movement to pre-state. Host nonce and ordinary transaction-payment effects are outside this definition. + +### 2.2 Atomicity terms + +| Term | Meaning | | --- | --- | -| Rejected control transition | The Actors call returns `Err`; Actors state/events, subscriptions, scheduler state, and creation-fee movement equal pre-state. Host fee/nonce accounting is excluded. | -| Provisional task commit | A task layer succeeds inside an admitted scheduler attempt but remains reversible until the enclosing attempt commits. | -| Committed unsuccessful attempt | An admitted attempt durably commits failure or suspension; collected Section 4.3 fees remain charged. It is not a rejected transition. | -| Rolled-back scheduler attempt | The enclosing attempt fails before durability; queue consumption, nonce/attempt, snapshots, fees, tasks, events, and scheduler state equal pre-attempt state. | -| Opening | The atomic transition that starts one fresh logical cycle, captures its immutable bases, increments `cycle_nonce`, consumes the current latch and funding accumulator, and emits `CycleStarted`. | -| Attempt | One admitted execution of a fresh cycle or Continuation suffix. Retries are new attempts in the same cycle. | -| Committed effectful task | A successful `Transfer`, `SplitTransfer`, `SwapIn`, `SwapOut`, `AddLiquidity`, `RemoveLiquidity`, `Burn`, `Mint`, `Stake`, `DonateLiquidity`, or `Unstake` task layer that becomes durable with its enclosing attempt. `StopCycle`, skipped resolution, funding suspension, task failure, and rolled-back task success do not count. | -| Cycle | One logical execution from opening through completion, terminal failure, suspension chain, cancellation, or close. | -| Placement | One live FIFO ticket, one exact temporal target for the earliest pending ordinary or terminal requirement, or none. | -| Membership | A physical scheduler/subscription record authorized by actor-local or reverse-index ownership. Physical records without current ownership are stale and have no authority. | -| Semantic replacement | A submitted canonical value differs from the stored canonical value. Canonical equality is an exact no-op before rate limiting, clock mutation, cancellation, placement reconstruction, storage writes, or events. | -| Economic apoptosis | Mandatory User close with `BalanceExhausted` or `FeeBudgetExhausted`. | -| Custody reattachment | Fresh actor creation with the same User owner and exact slot, or the same vacant System locator, deriving the same sovereign account and inheriting only its current adapter-exposed custody. | -| Terminal custody drain | A sole-step `Transfer(AllAvailable)` plan under `CloseAfterProductiveCycle` that may consume the protected minimum because successful transfer and close commit atomically. | - -A step failure inside an admitted attempt is not a rejected transition. Authored whole-plan rollback is forbidden; rollback of the enclosing scheduler transaction remains required on its own failure. - -### 1.2 Declarative Trigger Topology - -The canonical Trigger machine is: +| Rejected control transition | Call returns `Err`; Actors-owned state and events equal pre-state. | +| Provisional Task commit | Task layer succeeded inside the current Step transaction but is not durable until that transaction commits. | +| Committed unsuccessful attempt | Current Step transaction durably commits suspension or failure; fees owned by that committed transition remain charged. | +| Rolled-back scheduler attempt | Current Step transaction fails; queue, run, Task effects, Actors fees, and Actors events equal pre-attempt state. | + +--- + +## 3. Actor Contract and canonical state + +### 3.1 Public Contract model + +```rust +struct ActorContract { + trigger: Trigger, + cooldown_blocks: u32, + window: Option>, + steps: Steps, + funding: FundingPolicy, + completion: CompletionPolicy, + auto_close_at_cycle_nonce: Option, +} + +struct Step { + precondition: Option, + task: Task, + on_error: StepErrorPolicy, +} + +struct ScheduleWindow { + start: BlockNumber, + end: BlockNumber, +} + +enum CompletionPolicy { + Persistent, + CloseAfterProductiveCycle, +} + +enum InitialLifecycle { Dormant, Active } +``` + +`steps.len()` MUST be in `0..=MaxContractSteps`. Zero Steps is first-class (§5.5). + +Every semantic Contract replacement replaces the complete authored value. Canonical equality is an exact no-op before rate limiting, cancellation, clocks, topology, writes, fees, or events. + +### 3.2 Semantic identity + +The semantic Contract ID is the protocol-fixed digest of the admitted canonical authored typed SCALE bytes only: ```text -Actor Contract semantics --> canonical Trigger runtime state --> one derived current detector obligation --> specialized physical detection topology --> semantically relevant transition --> one normalized activation operation --> at most one canonical placement --> one bounded FIFO execution attempt +SemanticContractId = Blake2_256( + SCALE(( + b"DEOS_ACTOR_CONTRACT", // fixed [u8; 19] + ( + trigger, + cooldown_blocks, + window, + funding, + completion, + auto_close_at_cycle_nonce, + ), + ordered_steps, + )) +) ``` -An Actor owns exactly one authored Trigger, one compatible canonical Trigger runtime state, at most one external-signal latch, and at most one live scheduler placement. Detection remains heterogeneous: Manual, AddressEvent, ObservationChange, ObservationCrossing, and Cadenced retain specialized bounded physical paths, but every readiness request converges through one activation and one FIFO execution plane. +The body commitment is independent: + +```text +BodyCommitment = Blake2_256( + SCALE(( + b"DEOS_ACTOR_BODY", // fixed [u8; 15] + [(0u32, Step[0]), (1u32, Step[1]), ...], + )) +) +``` -Physical topology MAY coalesce readiness but MUST NOT fabricate or lose causal Trigger-state transitions. A committed producer transition that can alter canonical Trigger state MUST be durably represented for bounded deferred service or the producer mutation MUST fail atomically. Irrelevant work MUST NOT probe unrelated Actors; relevant work MAY scale only with the exact affected cohort under explicit finite bounds and generated multidimensional Weight. `MaxCrossingMembersPerFeed` independently bounds one feed's total exact-threshold membership and MUST be nonzero and no greater than global Active capacity. `MaxUserCrossingMembersPerFeed` MUST be nonzero and strictly lower; its exact reconciled count rejects User installation with a distinct typed capacity error and reserves the difference for System Actors. Lifecycle installation or replacement above either applicable cap fails atomically before the prospective Active state commits. +Indexes MUST be contiguous and equal the exact Step count. Predicate canonicalization (§6.2) occurs before identity derivation; whitelists are accepted only when already canonical (§4.1). JSON formatting, labels, comments, storage wrappers, Trigger runtime state, run state, tickets, topology, runtime Weight, and physical fragments MUST NOT affect either identity. -Trigger lifecycle mutation MUST use one bounded preflight/commit owner across Active creation, Dormant activation, Active replacement, deactivation, close, and genesis installation. The owner derives the prospective canonical state and physical obligation, returns exact semantic no-op before control mutation, prevalidates every fallible dependency, and commits Contract, Trigger runtime state, detector topology, economic state, and placement reconstruction atomically. +### 3.3 C6 physical geometry -Work ownership is disjoint: the originating call or producer owns its complete synchronous consequence; readiness materialization owns deferred detector search and affected-cohort progression; activation owns latch and placement request; placement owns canonical FIFO/temporal mutation; execution owns one admitted FIFO attempt. The same storage or branch MUST NOT be charged through two owners, and no owner MAY defer an uncharged synchronous consequence into another budget. +One semantic Contract is stored as: -Economic ownership is likewise explicit. Call origin pays synchronous lifecycle mutation through transaction fees. Oracle publisher pays bounded cardinality-independent publication ingress, not deferred matching. Runtime reserve pays bounded detector search and readiness materialization under hard caps. User execution remains Actor-funded and System execution remains Actors-fee-exempt. Every persistent Active User detector footprint MUST have an exhaustive runtime-owned refundable Trigger-state bond or a measured zero-cost justification; physical occupancy MUST NOT make the bond unpredictable, and no implicit per-observation, per-rearm, or per-page User debit exists. +```text +Hot head: + authored header + step_count + first_step: Option + SemanticContractId + BodyCommitment + admission authority + fixed-size PipelineMachineEnvelope + bounded Opening-dependency chunk locator plan + fixed-size maximum run-state hold quote + +Lazy tail: + contiguous authority-bound chunks + each containing at most four Steps + beginning at Step 1 +``` -### 1.3 Non-goals +Rules: -Actors defines no ownership transfer, arbitrary dispatch, loops, same-block actor-graph continuity, priority lane, actual-Weight refund, readiness-bypassing simulation, signal-payload amount resolution, adaptive rent/deposit pricing, generic migration engine, close-time asset enumeration, implicit multi-asset refund, or Router route semantics. Uncertified balance movements are not retroactively interpreted as Actors ingress. +1. `first_step == None` iff `step_count == 0`. +2. `first_step == Some(Step 0)` iff `step_count > 0`. +3. Zero-Step and one-Step Contracts have no tail chunk. +4. Tail chunks are gap-free, non-overlapping, and keyed by their first covered Step index. +5. Ordinary non-Opening execution loads only the current Step's head or one tail chunk. Opening MAY additionally load only fragments named by the exact Opening dependency plan (§5.3). +6. Unreached Steps MUST NOT add ordinary execution ProofSize merely because they exist; only their explicitly authored Opening dependencies may affect Opening cost (§5.3). +7. `MaxContractSteps` MUST NOT enlarge the maximum current-Step storage read. +8. Full reconstruction is owned only by lifecycle mutation, runtime projection, integrity checks, and upgrade work (§12.3, §13.1, §13.2). -## 2. Actor Model +A fragment MUST bind Actor id, semantic Contract ID, body commitment, and exact index range. Missing, stale, overlapping, foreign, or orphan authority fails closed. -### 2.1 Canonical State +### 3.4 Admission certificate and Pipeline Machine envelope + +```rust +struct ActorAdmissionCertificate { + semantic_contract_id: Hash, + body_commitment: Hash, + runtime_actor_semantics_version: u32, + production_weight_identity: Hash, + body_geometry_version: u32, + configured_bounds_commitment: Hash, + admission_identity: Hash, +} + +struct ActorStepResourceEnvelope { + control_weight_upper: Weight, + effect_weight_upper: Weight, +} + +struct PipelineMachineEnvelope { + service_identity: Hash, + control_weight_upper: Weight, +} +``` + +Each authored Step is stored with one runtime-owned `ActorStepResourceEnvelope`. The envelope is bound to the Step index, populated C6 fragment, predicate/read geometry, runtime Weight identity, and configured bounds. It is not authored state. + +```text +AdmissionIdentity = Blake2_256(SCALE(( + b"DEOS_ACTOR_ADMISSION", // fixed [u8; 20] + semantic_contract_id, + body_commitment, + runtime_actor_semantics_version, + production_weight_identity, + body_geometry_version, + configured_bounds_commitment, +))) +``` + +The certificate is runtime-owned derived authority. It MUST bind the current semantic Contract, body, runtime semantics, production Weight identity, C6 geometry, and configured bounds. + +`PipelineMachineEnvelope.control_weight_upper` is the generated maximum **reachable** Actor Control work of one complete admitted Pipeline. Its derivation MUST include: + +- Opening exactly once (§5.2); +- Each reachable current-Step control path (§6.1); +- Repeatable retry control only up to authored bounds (§6.5); +- At most one reachable finalization or close-control branch (§5.6, §9.3); +- No Trigger work (§4.2); +- No Task-effect work (§11.1); +- No minimal pre-Opening apoptosis (§9.4). + +The envelope MUST NOT be the arithmetic sum of mutually exclusive Step maxima. Opening-only work MUST NOT be multiplied by Step 0 retries. Finalization-only work MUST NOT be multiplied across Steps. + +Create and semantic update are the sole generation owners. Opening reads the fixed-size envelope in `O(1)` and MUST NOT load tail chunks solely to quote Pipeline service (§5.2, §7.4). + +A stale certificate or envelope fails closed until bounded re-certification or authorized Contract replacement. Re-certification MUST preserve semantic identity when authored bytes are unchanged. + +### 3.5 Canonical partitions and semantic ownership + +The only canonical Actor partitions are: ```rust type ActorId = u64; type OwnerSlot = u8; -type OwnerSlotBitmap = [u8; 32]; type SystemSovereignId = u64; -type QueueTicket = u64; -type WakeupPageId = u64; -type WakeupSlot = u32; -type ObservationRevision = u64; enum ActorType { User, System } -enum InitialLifecycle { Dormant, Active } -enum ActorClass { User { owner_slot: OwnerSlot }, System { sovereign_id: SystemSovereignId } } -enum SystemSovereignState { Vacant, Occupied(ActorId) } + +enum ActorClass { + User { owner_slot: OwnerSlot }, + System { sovereign_id: SystemSovereignId }, +} + enum Mutability { Mutable, Immutable } enum ActiveLifecycle { Active, Paused } -enum CycleState { Idle, Suspended } -enum CrossingPhase { Armed, WaitingForRearm } -enum TriggerRuntimeState { - Stateless, - ObservationCrossing { - phase: CrossingPhase, - installed_at_revision: ObservationRevision, - }, - Cadenced { anchor_tick: Option }, -} +enum CycleState { Idle, Running, Suspended } struct ActorIdentity { - sovereign_account: AccountId, owner: AccountId, actor_class: ActorClass, - mutability: Mutability, cycle_nonce: u64, last_control_mutation_block: BlockNumber, -} -struct WakeupPointer { - block: BlockNumber, page_id: WakeupPageId, slot: WakeupSlot, + sovereign_account: AccountId, + owner: AccountId, + actor_class: ActorClass, + mutability: Mutability, + cycle_nonce: u64, + last_control_mutation_block: BlockNumber, } + struct ActorHot { - lifecycle: ActiveLifecycle, cycle_state: CycleState, + lifecycle: ActiveLifecycle, + cycle_state: CycleState, trigger_runtime_state: TriggerRuntimeState, unsuccessful_attempt_streak: u32, - pending_signal: bool, queue_ticket: Option, - wakeup_pointer: Option>, terminal_at: Option, - schedule_anchor: BlockNumber, last_cycle_block: Option, -} -struct ActorContract { - trigger: Trigger, cooldown_blocks: u32, - window: Option>, steps: Steps, - funding: FundingPolicy, completion: CompletionPolicy, - auto_close_at_cycle_nonce: Option, + pending_signal: bool, + queue_ticket: Option, + pipeline_wakeup: Option>, + trigger_wakeup: Option, + schedule_anchor: BlockNumber, + last_cycle_block: Option, + terminal_at: Option, } + struct ActorFunding { funding_accumulated: BoundedBTreeMap, funding_tracked_assets: BoundedBTreeSet, } -struct ContinuationState { - cursor: u32, unsuccessful_attempts_at_cursor: u32, - last_attempt_block: BlockNumber, - opening_snapshot: BoundedBTreeMap, Balance, MaxOpeningSnapshotEntries>, - opening_predicate_results: BoundedVec, MaxOpeningPredicateResults>, - funding_snapshot: BoundedBTreeMap, - cumulative_outcomes: OutcomeTotals, -} + struct OutcomeTotals { - executed_steps: u32, committed_effectful_tasks: u32, - precondition_skips: u32, skipped_resolution: u32, - skipped_funding_unavailable: u32, failed_steps: u32, + executed_steps: u32, + committed_effectful_tasks: u32, + precondition_skips: u32, + skipped_resolution: u32, + skipped_funding_unavailable: u32, + failed_steps: u32, } -enum CloseReason { - OwnerInitiated, BalanceExhausted, FeeBudgetExhausted, WindowExpired, - CycleNonceExhausted, RetryAttemptsExhausted, ConsecutiveFailures, - ProductiveCycleCompleted, AutoCloseNonceReached, SchedulerIndexExhausted, -} -enum CycleResult { Completed, Failed, Cancelled } enum SuspensionReason { FundingUnavailable, Temporary } -enum CancellationReason { - Explicit, ContractReplaced, Deactivated, Closing(CloseReason), -} -enum StepSkippedReason { PreconditionFalse, ResolutionSkipped, FundingUnavailable } +enum CycleResult { Completed, Failed, Cancelled } ``` -Relations: +`ActorRunState` is defined only in §5.1. -- Active iff identity, hot, contract, and funding partitions exist; Dormant iff only identity exists. -- `Manual`, `AddressEvent`, and `ObservationChange` require `Stateless`; `ObservationCrossing` requires `ObservationCrossing`; `Cadenced` requires `Cadenced`. Any mismatch is `ActorInvariant`. -- Trigger runtime phase, observation revision, physical membership generation, locator, cursor, and every index field are excluded from Actor Contract identity. -- `ContinuationState` exists iff `cycle_state == Suspended`. -- `ActorClass` solely determines class; `ActorType` is derived and never stored. -- A composite actor value is read-only and MUST NOT become another write model. -- One runtime `MaxContractSteps` applies to both classes. -- `funding_tracked_assets` is the exact bounded derivation of the current Contract Steps; no duplicate count is stored. -- `terminal_at` is `end + 1` exactly when a schedule window exists and otherwise `None`. -- Dormant creation performs no contract scan, subscription, funding tracking, readiness, or placement. -- Active admission derives the complete current cycle or suffix Weight and User fee envelope before collection or mutation. -- `WakeupPointer` authorizes the actor's single physical temporal membership for the earliest pending ordinary or terminal requirement. `terminal_at` independently authorizes terminal classification. Runtime metadata/storage descriptors define physical fields and topology. +Semantic owners: -### 2.2 Class and Mutability +| Fact | Sole canonical owner | +| --- | --- | +| Owner, class, mutability, sovereign account, latest terminated nonce | `ActorIdentity` | +| Authored Contract, identities, Step count, first Step, admission authority, Pipeline envelope | C6 hot head (§3.3) | +| Authored Steps 1..N | C6 tail chunks (§3.3) | +| Lifecycle, cycle phase, Trigger phase, latch, placement pointers, failure streak, clocks | `ActorHot` | +| Funding accumulation and tracked set | `ActorFunding` | +| Open-cycle cursor, snapshots, outcomes, retry state, paid Pipeline authority | `ActorRunState` (§5.1) | +| Physical queue, detector, wakeup, locator, and page authority | derived topology only (§8.2, §8.4, §10.2) | -User creation is signed, makes the caller the owner, consumes one owner slot, and pays User Actors fees. System creation requires `SystemOrigin`, names an explicit owner, consumes no User slot, and pays no User Actors fee. Both classes share FIFO order. +`ActorType` is derived from `ActorClass` and MUST NOT be stored. -Actor-scoped authorization is: +Composite Actor values and runtime API views are read-only and MUST NOT become write models. -| Actor | Authorized control origin | -| --- | --- | -| User | signed owner | -| System | signed owner or `SystemOrigin` | +Dormant means only `ActorIdentity` and class locator/slot authority exist. Dormant Actors own no hot Contract, hot state, funding, run state, detector membership, ticket, wakeup, or Active-state hold. -This rule includes `manual_trigger`. Unauthorized signed control returns `NotOwner`; an origin accepted by `SystemOrigin` that targets a User actor returns `NotGovernance`. +--- -Mutable control may replace schedule/window, Contract Steps/completion policy, funding policy, lifecycle, Continuation, close target, or Active/Dormant state. Dormant creation requires `Mutable`; Immutable creation MUST install an Active Actor Contract because no later activation authority exists. +## 4. Trigger machine -Immutability covers the Actor Contract and control authority only. It does not freeze runtime Weight, fee conversion, minimum-balance policy, adapter behavior, or host economic parameters. +### 4.1 Trigger families and runtime state -User Immutable: +```rust +enum Trigger { + Manual, + AddressEvent { + source_filter: SourceFilter, + asset_filter: AssetFilter, + }, + ObservationChange { feed: FeedId }, + ObservationCrossing { + feed: FeedId, + direction: CrossingDirection, + threshold: u128, + rearm_threshold: u128, + }, + AtTime { after_ticks: u64 }, + Cadenced { every_ticks: u64 }, +} -- Rejects every semantic replacement, pause/resume, activate/deactivate, cancellation, auto-close change, and explicit close; -- MAY invoke an authored Manual readiness signal; -- Cannot use `RetryLater`; -- Remains bound to its authored role while economically viable; -- MUST close through `BalanceExhausted` or `FeeBudgetExhausted` when scheduler classification or permissionless sweep observes that it cannot fund the current full-plan or Continuation-suffix attempt. +enum SourceFilter { + Any, + OwnerOnly, + Whitelist(BoundedVec), +} -Economic apoptosis applies to Mutable and Immutable User actors. Only scheduler classification and permissionless sweep evaluate its predicates; Actors performs no background solvency scan. +enum AssetFilter { + Any, + Whitelist(BoundedVec), +} -After any User close, including economic apoptosis of User Immutable, the former owner MAY create a fresh Mutable User actor at the released exact slot. The new actor inherits no mutability, contract, authority, lifecycle, or terminal state from the closed actor; Section 2.3 custody reattachment is the only continuity. +enum FundingProvenance { Signed, InternalProtocol, Xcm } -System Immutable: +struct AddressEvent { + destination: AccountId, + source: Option, + asset: AssetId, + amount: Balance, + provenance: Option, +} -- Rejects every actor-scoped control; -- Cannot contain Manual or `RetryLater`; -- Pays no Actors attempt fee; -- Is not required to contain a finite actor-lifetime terminal path; every admitted attempt remains bounded and reaches a named result; -- Remains Active until a mandatory terminal predicate or Section 9.4 upgrade removes it. +enum TriggerRuntimeState { + Stateless, + ObservationCrossing { + phase: CrossingPhase, + installed_at_revision: ObservationRevision, + }, + AtTime { + anchor_tick: Option, + consumed: bool, + }, + Cadenced { + anchor_tick: Option, + }, +} -`2 <= RetryLater.max_attempts <= MaxRetryAttempts`; `max_attempts` counts unsuccessful executions of the cursor including the opening attempt, so it permits at most `max_attempts - 1` retries. +enum CrossingPhase { Armed, WaitingForRearm } +enum CrossingDirection { Rising, Falling } +``` + +Compatibility is exact: + +- `Manual`, `AddressEvent`, `ObservationChange` require `Stateless`; +- `ObservationCrossing` requires crossing state; +- `AtTime` requires AtTime state; +- `Cadenced` requires cadence state. + +Mismatch is `ActorInvariant` (§12.4). Whitelists MUST be nonempty, duplicate-free, and strictly ordered by canonical typed SCALE bytes; runtime admission MUST reject rather than normalize noncanonical input. + +### 4.2 Useful Trigger occurrence + +A Trigger occurrence is exactly one family-specific cause that: + +1. Passes source/detector authority; +2. Is semantically relevant; +3. Observes `pending_signal == false`; +4. Successfully charges the User Trigger fee or is System fee-exempt (§7.3); +5. Commits `pending_signal: false -> true`; +6. Disables further Actor-specific occurrence work until re-arm (§4.3); +7. Emits `TriggerOccurrenceProcessed` (§12.2). + +A Trigger occurrence is not a Cycle and does not charge Pipeline service (§5.2, §7.4). -`SystemOrigin` has no call-level override for System Immutable existence or contract state. A runtime upgrade MAY override it only under Section 9.4 and MUST define actor disposition, custody consequences, and Continuation policy. +Source publication, movement, or explicit-call work has its own owner (§7.7, §11.4). Trigger occurrence owns only Actor-specific detection/matching/materialization attributable to `false -> true`. -### 2.3 Sovereign Accounts and Identifiers +### 4.3 Latch and redundant activity + +`pending_signal` is the sole readiness latch. ```text -User seed = Blake2_256(SCALE(ActorsPalletId, b"user", owner, owner_slot)) -System seed = Blake2_256(SCALE(ActorsPalletId, b"system", system_sovereign_id)) -account = HostSovereignAccountDeriver(seed) +false -- useful occurrence --> true -- Opening --> false ``` -The decoder MUST be total and deterministic for every 32-byte seed. - -- `OwnerSlotBitmap` is owner-local. It has 256 bits; valid slots satisfy `owner_slot < MaxOwnerSlots`, and every bit outside that range is zero. -- The reference profile exposes slots `0..=254`; bit `255` is a permanently invalid sentinel so `u8::MAX` never denotes an owned slot. -- Default User creation selects the lowest free valid slot; exact-slot creation requires a valid free slot; close clears it. -- `create_system_actor` allocates a fresh locator with `system_sovereign_id = actor_id`, records `Occupied(actor_id)`, and consumes lifetime locator capacity. -- `create_system_actor_at_sovereign_id` requires an existing `Vacant` locator, assigns a fresh `actor_id`, and records `Occupied(fresh_actor_id)`; unknown and occupied locator cases are distinct errors. -- Close marks a System locator `Vacant`; deactivation leaves it occupied; reuse never reuses an actor id. -- `SystemSovereignCount` includes vacant locators; reuse does not change it. -- The host deriver MUST map the tagged seed deterministically without a runtime panic, preserve owner/slot and System-locator separation, and keep every previously admitted custody identity stable. -- `SovereignIndex` covers current Active or Dormant ownership only. Nonzero balance or host provider state at an otherwise unindexed derived account is custody, not a live Actors collision, and MUST NOT block creation or reattachment. -- Fresh User-slot and fresh System-locator derivation reject reserved accounts. Exact User-slot reattachment remains a fresh derivation and applies the same reserved-account check. -- Reattachment to an already registered vacant System locator is permitted for that exact locator even if host policy later classifies its derived account as reserved. This exception applies only to that registered locator. -- A deployed runtime change MUST NOT make previously reattachable custody unreachable without the Section 9.4 custody disposition. Live collision, reserved collision, unknown/occupied locator, and capacity exhaustion remain distinct errors. -- `NextActorId` and queue tickets checked-increment and never repeat. -- A fresh User actor derives the closed actor's sovereign account iff both signed owner and exact `owner_slot` equal those used for the closed actor. The same numeric slot under another owner derives a different account. -- A fresh System actor derives the closed actor's sovereign account iff it reuses the same vacant `system_sovereign_id`. -- Such reattachment inherits adapter-exposed custody only. Owner/class binding, mutability, contract, nonce, funding, Continuation, clocks, readiness, and guarantees are newly created. -- Custody derivation survives host account-provider removal. -- Every Actors-declared ordinary debit other than a Section 3.4 terminal custody drain obeys the protected minimum in Section 3.3. -- A successful ordinary User fee-native debit preserves `MinUserBalance`; a terminal custody drain may consume it only after reserving and collecting the current attempt fee. Adapters MUST NOT add hidden debits. -- External host actions MAY reap or dust an account; deterministic reattachment does not recreate value removed by host policy. -- After close, no Actors authority exists over preserved balances until successful reattachment; knowledge of the deterministic account alone grants no control. - -### 2.4 Lifecycle, Economic Viability, and Terminal Order - -Terminal predicates are evaluated in this precedence when their owning transition applies. The first satisfied predicate is selected and lower-precedence predicates are not evaluated: - -| State | Close reason | -| --- | --- | -| `current_block > window.end` | `WindowExpired` | -| User fee-native balance `< MinUserBalance` | `BalanceExhausted` | -| User available fee budget `< attempt_fee_envelope(plan, cursor, User).total` | `FeeBudgetExhausted` | -| `cycle_nonce == u64::MAX` before activation or fresh opening | `CycleNonceExhausted` | -| cursor-local unsuccessful executions reach `max_attempts` | `RetryAttemptsExhausted` | -| `unsuccessful_attempt_streak` reaches `MaxConsecutiveFailures` | `ConsecutiveFailures` | -| completed productive cycle under `CloseAfterProductiveCycle` | `ProductiveCycleCompleted` | -| non-suspended cycle reaches auto-close target | `AutoCloseNonceReached` | -| due materialization or post-attempt placement exhausts a monotonic scheduler index | `SchedulerIndexExhausted` | - -For User economic viability: +While `pending_signal == true`: + +- Source activity MUST NOT create another Actor-specific occurrence; +- No additional Trigger fee is charged; +- No additional Pipeline is queued; +- No Trigger history is accumulated for the Actor; +- No Opening/funding snapshot is changed; +- Where practical, the Actor MUST be absent or disabled in the relevant detector topology. + +Source-owned canonical state MAY continue changing. Economic ingress MAY still update custody and funding (§6.4, §11.4). + +The latch bounds one Actor to: ```text -cursor = - ContinuationState.cursor when Suspended; - 0 otherwise +at most one open Pipeline ++ at most one deferred Pipeline request +``` + +There is no direct latch-clear call. Opening consumes it; deactivation or Close deletes it (§5.2, §9.3). Manual occurrence while Running or Suspended requests only the next Cycle and never alters the current cursor, retry, or eligibility (§4.7). + +### 4.4 Trigger underfunding and source advancement + +User Trigger underfunding means the sovereign account cannot pay the current Trigger fee (§7.3). It MUST NOT create free polling or repeated charging of the same cause. + +| Family | Underfunded result | +| --- | --- | +| `Manual` | Reject the call; no Actors state/event/Trigger fee changes. Ordinary signed transaction payment remains. | +| `AddressEvent` | The certified movement and independent funding semantics MAY commit; no latch or Trigger fee. The same movement is not retried as a Trigger. | +| `ObservationChange` | The source revision frontier advances; no latch or Trigger fee. A later revision may form a new cause. | +| `ObservationCrossing` | The transition frontier advances and crossing phase consumes the fire as if no readiness were purchased; no latch or Trigger fee. A later re-arm and later crossing may form a new cause. | +| `AtTime` | The one-shot source is consumed. A User Actor undergoes minimal apoptosis with `TriggerAdmissionInsufficient` (§9.4). System is fee-exempt. | +| `Cadenced` | The due point is skipped and the next future cadence point is installed; no latch or Trigger fee. | + +Fee-collector infrastructure failure is not underfunding. It preserves the exact source obligation, records/returns the owning failure, and performs no occurrence mutation (§7.3, §8.4). + +### 4.5 Trigger re-arm + +Opening consumes the current latch and re-arms the Trigger (§5.2): + +| Family | Re-arm rule | +| --- | --- | +| `Manual` | Stateless and enabled. | +| `AddressEvent` | Stateless and enabled. | +| `ObservationChange` | Re-enable from the current authoritative revision; no replay of latched revisions. | +| `ObservationCrossing` | Derive phase from the current authoritative value and revision; no replay of latched transitions. | +| `AtTime` | Never re-arm; `consumed == true`. | +| `Cadenced` | Install the first canonical cadence deadline strictly after the current authoritative tick; no catch-up. | + +A Running or Suspended Actor may therefore acquire one new latch for the next Cycle while its current Cycle continues (§5.1). + +### 4.6 Causal cohort + +A causal cohort is a bounded ordered set of Actors affected by one shared Trigger cause. + +Cohort eligibility depends only on shared causal authority, not Contract length, Actor class, Task kind, fee, or Weight. -fee_native_balance = - AssetOps::balance(sovereign_account, FeeNativeAssetId) +The cohort MAY amortize: -available_fee_budget = - fee_native_balance.saturating_sub(MinUserBalance) +- Source/detector traversal; +- Candidate classification; +- Useful occurrence materialization (§4.2); +- Grouped physical placement writes (§8.4); +- First Opening/Step-0 control when generated branches remain compatible (§5.2, §6.1). -required_attempt_fee = - attempt_fee_envelope(contract_steps, cursor, User).total +Every Actor retains its own Trigger fee, latch, ticket, events, Pipeline fee, and Step outcome. + +After Step 0 commits, each nonterminal Actor leaves the initial cohort and follows independent Q1 scheduling (§6.1, §8.3). A zero-Step member ends at Opening (§5.5). A one-Step member is the degenerate case whose Step 0 also completes its Cycle. + +Unreached pipeline length MUST NOT add first-reaction work merely because it exists. First reaction may load only Step 0 plus exact authored Opening dependencies; unrelated tail fragments remain cold (§3.3, §5.3). + +### 4.7 Trigger-specific semantics + +#### Manual + +Only the authorized owner may call `manual_trigger` (§12.1). A useful call follows Trigger occurrence (§4.2). A latched call is an exact Actor no-op and retains ordinary transaction payment (§7.3). + +#### AddressEvent + +Only certified positive non-self movement may form an AddressEvent cause (§11.4). Trigger matching is independent from funding acceptance (§6.4). `SourceFilter::Any` accepts any source, including absent source; `OwnerOnly` requires concrete source equal to owner; `Whitelist` requires a concrete listed source. `AssetFilter::Whitelist` requires the exact listed asset. + +#### ObservationChange + +Every accepted changed feed revision is a source cause. Equality is an exact source no-op. Revision regression fails. Subscriber topology is paged and bounded (§8.4). + +#### ObservationCrossing + +Rising requires `rearm_threshold < threshold`. Falling requires `rearm_threshold > threshold`. + +```text +Rising fire: previous < threshold && current >= threshold +Rising rearm: previous > rearm_threshold && current <= rearm_threshold + +Falling fire: previous > threshold && current <= threshold +Falling rearm: previous < rearm_threshold && current >= rearm_threshold ``` -System ignores User balance and fee predicates. User resource predicates apply regardless of mutability. Productive close precedes auto-close. Auto-close applies only when `cycle_state == Idle`. Windows are inclusive; terminal readiness occurs at representable `end + 1`. Attempt finalization evaluates `ProductiveCycleCompleted` only after `Completed`, then evaluates `AutoCloseNonceReached` after any non-suspended terminal cycle. +Repeated equal observations cause neither transition. Installation derives phase from current canonical observation and never retrofires history. Every accepted revision transition is processed in per-feed revision order (§8.4). -Terminal ownership: +#### AtTime -- `WindowExpired` is checked by actor-targeting control, address-event notification, scheduler classification, and sweep. -- `BalanceExhausted` and `FeeBudgetExhausted` are checked by scheduler classification and sweep. -- `CycleNonceExhausted` is checked before Dormant activation, fresh-cycle opening, scheduler classification, or sweep. Activation substitutes close of the Dormant identity. -- Attempt finalization applies retry-, failure-, productive-, post-cycle auto-close, and permanent scheduler-index reasons. Due materialization also closes on permanent queue-index exhaustion before any attempt. Scheduler classification and sweep close an actor when its stored retry count, failure count, Idle cycle nonce, or Idle auto-close target already satisfies a terminal predicate; neither infers `ProductiveCycleCompleted` or `SchedulerIndexExhausted` from stored state. `AutoCloseNonceReached` is not due while Suspended. -- Under the global breaker, due temporal membership MAY materialize, but scheduler-owned automatic close does not execute. Explicit Mutable close, actor-targeting expiry substitution, ingress-triggered expiry close, and permissionless sweep remain available. +`after_ticks > 0`. The source is one relative consensus-time deadline. It fires at most once, never catches up, and never re-arms (§4.5). -Address-event notification performs inline terminal close only for `WindowExpired`. User resource predicates are evaluated after any credited value only when scheduler classification or sweep later runs. +#### Cadenced -An admitted attempt is unsuccessful exactly when it commits `Suspended` or terminal `Failed`. +`every_ticks > 0`. Cadence is anchored at Active installation using ceiling quantization. A useful occurrence disables cadence detection until Opening re-arms it (§4.5). Missed latched-period points are forgotten. -- A `RetryLater` suspension candidate increments `unsuccessful_attempts_at_cursor` and `unsuccessful_attempt_streak` exactly once before terminal-bound evaluation; it commits `CycleSuspended` only when neither bound is reached. -- A non-retry terminal `Failed` attempt increments `unsuccessful_attempt_streak` exactly once. -- `Completed` resets `unsuccessful_attempt_streak` to zero before rearm and completion-driven close evaluation. -- Deferral, cancellation, pause/resume, exact no-op, an advancing `FundingUnavailable` skip, and an individual `ContinueNextStep` failure do not independently change either counter. -- Fresh Active installation and semantic Contract Steps or completion-policy replacement reset the global streak. -- Schedule and funding-policy replacement preserve the global streak. +#### Time bounds -For a suspension candidate at `cursor`, `prior_continuation` is the immutable `ContinuationState` loaded at attempt start, or `None` for a fresh opening. +The reference tick is 500 ms. `AtTime` and `Cadenced` disallow `cooldown_blocks` and `ScheduleWindow`. Their delay/period MUST fit `MaxTemporalDelayTicks` (§13.3). -```rust -let next_local = match prior_continuation { - Some(previous) if previous.cursor == cursor => - checked_add(previous.unsuccessful_attempts_at_cursor, 1)?, - _ => 1, -}; +For signal-driven Triggers, block eligibility is owned by: -let next_global = - checked_add(unsuccessful_attempt_streak, 1)?; +```text +cooldown_anchor = last_cycle_block.or(schedule_anchor) -fn backoff(index: u32) -> u32 { - 1u32.checked_shl(index).unwrap_or(8).min(8) -} +cooldown_eligible_at = + schedule_anchor for the first Cycle + checked_add(cooldown_anchor, cooldown_blocks) otherwise + +signal_eligible_at(cause_floor) = + max(cause_floor, cooldown_eligible_at, window.start when present) +``` -if next_local >= max_attempts { - finalize_failed_and_close(CloseReason::RetryAttemptsExhausted)?; -} else if next_global >= MaxConsecutiveFailures { - finalize_failed_and_close(CloseReason::ConsecutiveFailures)?; -} else { - persist_suspension(cursor, next_local, next_global)?; +A ScheduleWindow is inclusive. Validation requires `end > start`, representable `end + 1`, inclusive length `>= MinWindowLength`, current block `<= end`, bounded future start delay, and first possible eligibility `<= end`. `terminal_at = end + 1` (§9.2). Active installation sets `schedule_anchor = max(now, window.start)` when a future window exists, otherwise `now`. + +Temporal arithmetic is constant-time: + +```text +first_due = ceil(now_millis / tick_millis) + period +next_due = first canonical cadence point strictly greater than now_tick +``` + +Missed cadence points are never iterated or replayed. At genesis, AtTime/Cadenced anchors are uninitialized; the first ordinary temporal service after the timestamp inherent installs the full future deadline without creating readiness. + +--- + +## 5. Pipeline and Cycle + +### 5.1 Cycle and run state + +```rust +struct ActorRunState { + semantic_contract_id: Hash, + body_commitment: Hash, + admission_identity: Hash, + pipeline_service_identity: Hash, + cycle_nonce: u64, + cursor: u32, + opening_predicate_cursor: u32, + unsuccessful_attempts_at_cursor: u32, + last_attempt_block: BlockNumber, + last_committed_step_block: Option, + eligible_at: BlockNumber, + opening_snapshot: BoundedBTreeMap, Balance, MaxOpeningSnapshotEntries>, + opening_predicate_results: BoundedVec, MaxOpeningPredicateResults>, + funding_snapshot: BoundedBTreeMap, + cumulative_outcomes: OutcomeTotals, + last_step_outcome: Option, + suspension: Option, } ``` -`backoff(checked_sub(next_local, 1)?)` determines retry timing under Section 3.2 and is not stored separately. Local precedence wins when both bounds are reached by the same attempt. `ContinuationState.cursor` is the previous suspended cursor; no additional cursor field exists. +`ActorRunState` exists iff `CycleState` is `Running` or `Suspended`. -A fresh opening or first suspension at a cursor different from `prior_continuation.cursor` starts cursor-local counting at `1`. Repeated suspension at the same stored cursor checked-increments the stored count. `(actor_id, cycle_nonce, block_number, event_index)` identifies each attempt; no cycle-global attempt ordinal is stored or emitted. +- `Running` requires `suspension == None`. +- `Suspended` requires `suspension == Some(_)`. +- `Idle` requires no run. +- `cursor` is the sole current Step index. +- The run binds the exact semantic Contract, body, admission, paid Pipeline service, and open-cycle nonce. +- Completed-run history is not retained in consensus state. -A non-retry terminal `Failed` attempt applies only `next_global`; threshold close follows `CycleSummary(Failed)`. +### 5.2 Opening -Fresh Active creation and activation install one Active epoch: +Opening is the sole transition that consumes paid pending readiness into one Cycle. -| Surface | Installed value | -| --- | --- | -| identity nonce | `0` for fresh creation; preserved Dormant nonce for activation | -| lifecycle/cycle | `Active / Idle`; no Continuation | -| schedule clocks | `schedule_anchor = max(now, window.start)` when a future window exists, otherwise `now`; `last_cycle_block = None`; `terminal_at = end + 1` or `None` | -| readiness | `pending_signal = false`; one canonical FIFO/temporal path derived from the schedule, or none when no readiness is due | -| failure/economics | `unsuccessful_attempt_streak = 0`; empty accumulator; tracked assets rederived from the contract | -| subscriptions | exact feeds derived from trigger sources | -| persistent control clock | identity `last_control_mutation_block = now` | +Opening MUST execute in this order: + +1. Validate current Actor, latch, placement, Contract, admission, and Pipeline envelope (§3.4, §8.1, §8.2). +2. Apply Opening-specific terminal checks (§9.2). +3. Validate that the active-installed maximum run-state hold remains present (§7.2). +4. Check and charge the complete Pipeline Machine fee (§7.4). +5. Preserve the existing run-state hold without a Cycle-local hold mutation (§7.2). +6. Derive `run.cycle_nonce = identity.cycle_nonce + 1` without changing `identity.cycle_nonce` (§5.4). +7. Capture Opening and funding snapshots (§5.3). +8. Consume `pending_signal` and the current funding accumulator (§4.3, §6.4). +9. Re-arm the Trigger (§4.5). +10. Emit `CycleStarted` (§12.2). +11. For a nonempty Contract, execute Step 0 in the same current-Step transaction (§6.1). +12. For a zero-Step Contract, finalize atomically (§5.5). + +If the Actor cannot pay the Pipeline Machine fee, or its active-installed run-state hold authority is inconsistent, Opening MUST NOT partially occur. Insufficient Pipeline payment invokes minimal apoptosis with `CycleAdmissionInsufficient` (§9.4); inconsistent hold authority fails closed as an Actor invariant. The prior Trigger fee remains final (§7.3). -Activation does not increment `cycle_nonce`. Every later fresh cycle computes: +If Pipeline fee collection fails despite valid capacity, the entire Opening attempt rolls back, the latch remains consumable, and the live FIFO head is preserved (§7.4, §8.3). + +### 5.3 Opening and funding snapshots + +Create/update derive one body-commitment-bound `OpeningDependencyPlan` containing only exact fragment locators and counts for authored `Opening` predicates and `PercentageAtOpening` surfaces. It is derived authority, not a second semantic owner. Opening loads only those named fragments and rejects any locator/body mismatch. A Contract with no Opening dependency loads none. + +After the Pipeline fee debit and before any Step-0 Task effect, Opening captures once: ```text -next_cycle_nonce = checked_add(stored_cycle_nonce, 1) +opening_snapshot = every unique OpeningSurface referenced by any Step +funding_snapshot = funding_accumulated before its consumption +opening_predicate_results = every admitted Opening predicate result ``` -and `CycleStarted.cycle_nonce == next_cycle_nonce`. Retries reuse the opened nonce. A Dormant identity carrying nonce `n` remains at `n` on activation and opens its next cycle at `n + 1`. +Opening balances are read after the Pipeline fee debit and before any transient Action-fee debit. Each later exact debit independently preserves its own current Action-fee reservation (§7.5, §10.4). -Transition deltas: +Every admitted Opening surface exists in the snapshot even when zero. Missing admitted keys are invariant failures. -| Transition | Preserved / changed | -| --- | --- | -| pause on Paused / resume on Active | exact no-op before rate check; no event | -| pause | preserve contract, funding, Continuation, failure streak, latch, clocks, and terminal target; remove ordinary readiness and retain only terminal placement when present | -| resume | preserve all semantic state; reconstruct the earliest ordinary or terminal placement from current state | -| semantic Actor Contract replacement with schedule/window change | cancel Continuation, diff subscriptions, reset `schedule_anchor` and `terminal_at`, apply every authored field atomically, recompute tracked assets when steps change, preserve the funding accumulator except entries no longer tracked, reset the failure streak when steps change, preserve the latch, then reconstruct placement | -| semantic Actor Contract replacement without schedule/window change | cancel Continuation, apply every authored field atomically, recompute tracked assets and prune untracked accumulator entries when steps change, reset the failure streak when steps change, preserve schedule clocks and latch, then reconstruct placement only if cancellation removed a retry path | -| auto-close target change | change only the target; do not cancel Continuation or consume the control-mutation limit | -| explicit `cancel_continuation` | require Suspended; emit `CycleCancelled(Explicit)`, then `CycleSummary(Cancelled)` from stored cumulative outcomes; delete `ContinuationState`; set `cycle_state = Idle`; preserve identity, nonce, contract, funding accumulator/tracked set, `pending_signal`, failure streak, schedule clocks, auto-close target, and balances; reconstruct the earliest ordinary or terminal placement; update the persistent control clock | -| deactivation | cancel Continuation and delete the complete Active epoch while preserving identity, locator, nonce, persistent control clock, and balances | +Opening facts are immutable until Cycle termination. Later Steps MAY load only snapshot keys/results referenced by the current Step, but MUST NOT recapture, prune, or rewrite them. `opening_predicate_cursor` maps the current Step to its exact frozen predicate-result range; advance adds that Step's admitted Opening-predicate count, and retry preserves the cursor. -Deleting `ContinuationState` removes its cursor, attempt, unsuccessful counter, `last_attempt_block`, opening snapshot, funding snapshot, and stored cumulative outcomes after the cancellation events have projected those outcomes. +Funding accepted after Opening belongs to the next Cycle and cannot repair the current funding snapshot (§6.4). -Every creation initializes `last_control_mutation_block` to the current block; genesis uses block zero. The clock is non-optional and survives deactivation. +### 5.4 Cycle nonce -`activate_actor`, `deactivate_actor`, semantic `pause_actor`, semantic `resume_actor`, semantic `update_contract`, and `cancel_continuation` are limited to one committed call per actor per block. Exact no-op returns before checking or updating the clock. `manual_trigger`, auto-close target calls, explicit close, sweep, global controls, ingress, and internal scheduler/cleanup transitions are exempt. Expiry-substituted close does not update a clock it immediately deletes. Rejection uses `ControlMutationRateLimited`. +`ActorIdentity.cycle_nonce` is the latest terminated Cycle nonce. -Close performs bounded deletion/index repair, leaves every balance at the sovereign account, and releases the User slot or marks the System locator vacant. It never enumerates assets or selects a refund recipient. `Persistent` remains Active after completion; `CloseAfterProductiveCycle` requires `committed_effectful_tasks > 0`. +Opening derives: -### 2.5 Continuation, Funding Delta, and Cycle Accounting +```text +run.cycle_nonce = checked_add(identity.cycle_nonce, 1) +``` -Explicit Continuation cancellation requires Mutable control and `cycle_state == Suspended`. +Opening does not mutate the identity nonce. Final completion, failure, cancellation, or close assigns `identity.cycle_nonce = run.cycle_nonce` exactly once. Retry, suspension, Step progress, breaker deferral, and congestion do not change either nonce. -Semantic Actor Contract replacement; deactivation; close; expiry; or incompatible upgrade cancels before changed meaning applies. Exact no-op and auto-close-target changes do not cancel. Pause/resume and breaker preserve Continuation. +### 5.5 Zero-Step Cycle -Cancellation performs no compensation, funding restoration, prefix rollback, or balance movement and emits `CycleCancelled`, then `CycleSummary(Cancelled)`. Before a retained Active actor is re-primed, cancellation exactly invalidates its current wakeup slot and reverse pointer in the same transaction; a latched next-run signal cannot create a new pointer behind an old physical slot. +A zero-Step Contract has no Task, Step event, Step fragment, Opening surface, or `ActorRunState`. -If close encounters a Continuation, cancellation uses `Closing(reason)` and precedes `ActorClosed(reason)`. A pure close means no Continuation exists and emits only `ActorClosed`. +A ready zero-Step Cycle consumes one FIFO service opportunity and atomically: -`update_contract(actor_id, contract)` replaces the complete authored Actor Contract atomically. Exact equality returns before rate limiting, clock mutation, writes, fees, and events. When trigger/cooldown/window, Steps, or funding changes, replacement cancels Continuation with `ContractReplaced`. Completion and auto-close-only replacement preserve a live Continuation. One successful semantic replacement emits only `ContractUpdated { actor_id }`. +1. Performs Opening (§5.2); +2. Charges the zero-Step Pipeline Machine fee (§7.4); +3. Emits `CycleStarted`; +4. Commits the new Cycle nonce (§5.4); +5. Emits `CycleSummary(Completed)`; +6. Applies terminal policy (§9.2). -Funding authority is independent from trigger matching: +`auto_close_at_cycle_nonce = Some(1)` makes a fresh zero-Step Contract one-shot. -| Policy | Authoritative funding acceptance | -| --- | --- | -| `OwnerOnly` | `provenance == Signed` and concrete `source == owner` | -| `SignedAllowlist` | `provenance == Signed` and concrete `source` is allowlisted | -| `RuntimePolicy` | `FundingAuthority::permits` accepts the exact source/provenance pair; the all-`None` pair MUST be denied | -| `AnyVerifiedIngress` | `source.is_some()` or `provenance.is_some()`; certification alone does not make the all-`None` pair acceptable | +### 5.6 Completion and cancellation -Contract admission derives bounded `funding_tracked_assets`, including `StakingOps::share_asset(position)` for `PercentageOfLastFunding` used by Unstake. +Cycle completion occurs at plan end or successful `StopCycle` (§6.6). -`AnyVerifiedIngress` permits third parties to shape the next funding basis only by delivering real accepted value; it grants no withdrawal authority and every debit remains bounded by current capacity. +Finalization MUST: -A credit in an untracked asset, or a tracked credit rejected by funding policy, is balance-only: it does not mutate `funding_accumulated` and emits no `FundingAccumulated`; trigger matching may still latch readiness. A tracked accepted positive credit checked-adds to the accumulator. Bound or arithmetic overflow returns `FundingAccumulatorOverflow` and rolls back the producer transaction. +1. Record the final Step outcome when present (§6.5); +2. Emit the required boundary events (§12.2); +3. Commit the run nonce (§5.4); +4. Delete `ActorRunState` while preserving the active-installed run-state hold (§7.2); +5. Update `last_cycle_block`; +6. Apply terminal precedence (§9.2); +7. Otherwise return to `Idle`; +8. If `pending_signal == true`, create one next-cycle obligation eligible no earlier than the next block (§8.2). -When semantic plan/completion replacement removes an asset from the tracked set, deleting its accumulator entry changes only future `PercentageOfLastFunding` resolution. It does not move, burn, reserve, lock, or otherwise alter the corresponding balance. +Cancellation deletes the run without compensating or rolling back earlier committed Steps. It emits `CycleCancelled`, then `CycleSummary(Cancelled)`, commits the run nonce, and deletes the run. Contract replacement, deactivation, close, and incompatible upgrade own their cancellation reason (§9.3, §9.5, §13.2). -`PercentageOfLastFunding` means accepted tracked funding since the previous logical cycle opening in the current Active epoch, or since Active installation for the first cycle. +--- -On suspension, the persisted funding snapshot is the exact projection of the pre-clear funding snapshot onto assets referenced by `PercentageOfLastFunding` in `contract_steps[cursor..]`, including the unresolved cursor step and mapped Unstake share assets. Entries outside that suffix cannot be read because the cursor never decreases. Projection retains every present suffix-referenced entry and synthesizes no missing key. Absence continues to resolve as `FundingUnavailable`. +## 6. Step execution -Later funding belongs to the next cycle. Funding accepted after opening cannot make a missing or zero current funding-snapshot entry available to a suspended cursor; `RetryLater` does not wait for post-opening funding. Completion, failure, cancellation, pause, and breaker neither restore the consumed snapshot nor alter the next accumulator. Deactivation and close delete accumulation. +### 6.1 One-Step attempt transaction -A cycle completes at plan end or successful `StopCycle`; accepted `ContinueNextStep` failures may coexist with `Completed`. `AbortCycle`, Permanent `RetryLater`, exhausted bounds, and cancellation do not complete. `cycle_nonce` increments once per cycle; retries reuse it and increment `attempt`; `last_cycle_block` records opening and `last_attempt_block` the latest admitted attempt. Deferral changes no cycle state or event stream. Signals during an open cycle affect only possible future readiness. +An Attempt is one admitted execution of exactly one current Step. A retry is a later Attempt at the same cursor. -## 3. Actor Contract Model +Before reading any `Current` predicate or dynamic amount, the scheduler MUST reserve: -### 3.1 Public Types +- The exact current Step's maximum Actor Control Weight (§7.6); +- The exact current Step's maximum Task-effect Weight (§7.6); +- For User Actors, the maximum current Action fee (§7.5). + +If any reservation does not fit, the Attempt defers without semantic evaluation or mutation. + +One current-Step transaction atomically owns: + +```text +run/ticket/Contract validation ++ optional Opening for cursor 0 ++ current-Step Precondition ++ amount resolution ++ at most one Task effect ++ Action fee settlement when a Task is invoked ++ StepOutcome and counters ++ cursor, suspension, completion, or close transition ++ successor or retry placement ++ Actors events +``` + +A Task MUST NOT be split across blocks. Earlier committed Steps are never rolled back by later failure. + +After a committed Step, `last_committed_step_block` equals the current block. A second Step commit for the same Actor in that block MUST fail closed independently of ticket correctness. + +An advancing Step with a successor sets `eligible_at = current_block + 1` and creates one future obligation (§8.2). It does not load or execute the successor Step. + +### 6.2 Precondition ```rust -struct ActorContract { - trigger: Trigger, cooldown_blocks: u32, - window: Option>, steps: Steps, - funding: FundingPolicy, completion: CompletionPolicy, - auto_close_at_cycle_nonce: Option, -} -struct ScheduleWindow { start: BlockNumber, end: BlockNumber } -enum Trigger { - Manual, - AddressEvent { source_filter: SourceFilter, asset_filter: AssetFilter }, - ObservationChange { feed: FeedId }, - ObservationCrossing { feed: FeedId, direction: CrossingDirection, - threshold: u128, rearm_threshold: u128 }, - Cadenced { every_ticks: u64 }, -} -enum CrossingDirection { Rising, Falling } -enum SourceFilter { Any, OwnerOnly, Whitelist(BoundedVec) } -enum AssetFilter { Any, Whitelist(BoundedVec) } -enum FundingSourcePolicy { - OwnerOnly, SignedAllowlist(BoundedBTreeSet), - RuntimePolicy, AnyVerifiedIngress, -} -enum FundingProvenance { Signed, InternalProtocol, Xcm } -struct AddressEvent { - destination: AccountId, source: Option, asset: AssetId, - amount: Balance, provenance: Option, -} +enum ObservationTiming { Opening, Current } -enum AmountResolution { - Fixed(Balance), PercentageOfCurrent(Perbill), PercentageAtOpening(Perbill), - PercentageOfLastFunding(Perbill), AllAvailable, -} -enum OpeningSurface { - PreservableAsset(AssetId), TargetAsset(AssetId), StakingShares(AssetId), +struct TimedPredicate

{ + timing: ObservationTiming, + predicate: P, } -enum InputLimit { LiveQuote, Absolute(Balance) } -struct Step { - precondition: Option>, - task: Task, on_error: StepErrorPolicy, -} -enum ObservationTiming { Opening, Current } -struct TimedPredicate

{ timing: ObservationTiming, predicate: P } struct Precondition { clauses: BoundedVec, MaxPerClause>, MaxClauses>, } +``` + +```rust +enum PredicateError { InvalidObservation } + enum Predicate { BalanceAbove { asset: AssetId, threshold: Balance }, BalanceBelow { asset: AssetId, threshold: Balance }, @@ -475,381 +780,372 @@ enum Predicate { ObservationEquals { feed: FeedId, threshold: u128, max_age_blocks: u32 }, ObservationNotEquals { feed: FeedId, threshold: u128, max_age_blocks: u32 }, } - -struct SplitLeg { to: AccountId, share: Perbill } -enum Task { - Transfer { to: AccountId, asset: AssetId, amount: AmountResolution }, - SplitTransfer { asset: AssetId, amount: AmountResolution, - legs: BoundedVec, MaxSplitTransferLegs> }, - SwapIn { asset_in: AssetId, amount_in: AmountResolution, - asset_out: AssetId, slippage_tolerance: Perbill }, - SwapOut { asset_out: AssetId, amount_out: AmountResolution, - asset_in: AssetId, input_limit: InputLimit, slippage_tolerance: Perbill }, - AddLiquidity { asset_a: AssetId, asset_b: AssetId, - amount_a: AmountResolution, amount_b: AmountResolution, - min_lp_out: Balance }, - RemoveLiquidity { lp_asset: AssetId, asset_a: AssetId, asset_b: AssetId, - lp_amount: AmountResolution, min_amount_a: Balance, min_amount_b: Balance }, - Burn { asset: AssetId, amount: AmountResolution }, - Mint { asset: AssetId, amount: AmountResolution }, - Stake { asset: AssetId, amount: AmountResolution }, - DonateLiquidity { asset_a: AssetId, asset_b: AssetId, - max_amount_a: AmountResolution, max_ratio_error: Perbill }, - Unstake { asset: AssetId, shares: AmountResolution }, - StopCycle, -} -enum StepErrorPolicy { AbortCycle, ContinueNextStep, RetryLater { max_attempts: u32 } } -enum CompletionPolicy { Persistent, CloseAfterProductiveCycle } ``` -Every Active Actor Contract contains `1..=MaxContractSteps`; empty `ContractSteps` returns `EmptyContractSteps`, and oversized `ContractSteps` returns `TooManyContractSteps`. - -Every `Task` variant contains at most two `AmountResolution` fields. Each contributes at most one amount `OpeningSurface` exactly for `PercentageAtOpening`. Each distinct `Opening` Predicate contributes one frozen `Result`; `Current` Predicates contribute none. With `S = MaxContractSteps`, `A = 2`, and `P = MaxPredicatesPerStep`, configuration requires `MaxOpeningSnapshotEntries = S * A` and `MaxOpeningPredicateResults = S * P`; the reference aggregate opening bound is 48. +A present Precondition is nonempty DNF: outer clauses are OR; inner predicates are AND. -For each Step, `1 <= clauses <= MaxPreconditionClauses`, `1 <= predicates_per_clause <= MaxPredicatesPerClause`, and the sum of clause predicates is at most `P`. Opening capture plus full Step evaluation therefore costs at most `2 * P` atomic evaluation units per Step and `2 * S * P` per attempt. Weight and User evaluation fees split those units into chunks of at most `P` before calling the benchmarked `predicate_set_evaluation` component; no runtime call relies on its defensive clamp. +Admission canonicalizes predicates and clauses by canonical typed SCALE order, rejects duplicate clauses after predicate deduplication, and absorbs exact supersets under `A OR (A AND B) = A`. -Each Actor Contract contains exactly one Trigger. `Manual`, `AddressEvent`, `ObservationChange`, and `ObservationCrossing` are externally signalled; `Cadenced` is internally timed and has no signal source. No Trigger variant contains an OR-set, nested Trigger, priority, or secondary admission source. +Every admitted predicate is evaluated; there is no short-circuit because Weight MUST be data-independent. -Every `Whitelist` and `SignedAllowlist` is nonempty, duplicate-free, and strictly ordered by canonical SCALE bytes. Runtime admission MUST reject non-canonical order, duplicates, or non-canonical list/set encoding and MUST NOT sort, deduplicate, or otherwise normalize submitted values. +`Opening` predicates are evaluated once during Opening and frozen (§5.3). `Current` predicates are evaluated immediately before the owning Step and observe prior committed Steps plus intervening external state. -Actor composition is asynchronous and provides no same-block or one-event-one-cycle guarantee. `SourceFilter::Any` permits any certified positive movement that is not self/no-op and satisfies the authored `AssetFilter`, independently of funding acceptance, to set `pending_signal`. Actors charges no signaller, requires no accepted funding, and imposes no trigger-specific minimum amount; any resulting User attempt charges the actor's fee budget. +Predicate error is Permanent Step failure. False emits `StepSkipped(PreconditionFalse)` and advances (§6.5). -User Actors MAY form externally closed self-cycles and arbitrary multi-Actor cycles. Actors performs no User graph analysis, stores no topological rank, and imposes no authored cycle prohibition. The FIFO cutoff and one live latch/ticket prevent synchronous recursion and more than one execution per Actor per block; every admitted User attempt still pays its complete suffix fee envelope, and an unreplenished cycle closes through `BalanceExhausted` or `FeeBudgetExhausted`. +`Above` is strict `>` and `Below` is strict `<`; equality satisfies neither. Observation `Unavailable`, `Uninitialized`, or `Stale` evaluates false. Structurally invalid `Fresh` observation is `PredicateError::InvalidObservation` and Permanent. `max_age_blocks` MUST be positive. Balance predicates read only the Actor sovereign account, subtract the current transient Action-fee reservation for the fee-native asset, and do not grant spending authority (§7.5, §10.4). -System-to-System activation policy is host-owned. Before an Active System Contract is installed or replaced, the pallet invokes the configured `SystemActorContractValidator` with the assigned Actor id and candidate Contract. The default validator accepts all contracts; a host MAY reject candidates against a bounded runtime-owned manifest. Host graph rank, edge metadata, and external-effect inventory are neither Actor Contract identity nor User authoring surface. +### 6.3 Amount resolution -### 3.2 Triggers and Timing +```rust +enum AmountResolution { + Fixed(Balance), + PercentageOfCurrent(Perbill), + PercentageAtOpening(Perbill), + PercentageOfLastFunding(Perbill), + AllAvailable, +} -| Trigger | Source | Runtime state | Physical binding | Latch and coalescing | Temporal behavior | Cleanup | Economic footprint | -| --- | --- | --- | --- | --- | --- | --- | --- | -| `Manual` | Admitted owner call | `Stateless` | None | Sets the sole `pending_signal`; repeats coalesce | Block cooldown/window after signal | Deactivation deletes latch; close deletes Actor | Lifecycle caller pays mutation; User pays eventual attempt | -| `AddressEvent` | Matching certified positive movement | `Stateless` | Certified ingress only; no detector index | Sets the sole latch; matching movements coalesce while latched | Block cooldown/window after movement | Deactivation removes readiness; close deletes Actor | Producer pays synchronous consequence; User pays eventual attempt | -| `ObservationChange` | Every accepted changed feed revision | `Stateless` | Actor/feed reverse ownership plus broad subscriber pages | Deferred fanout sets the sole latch; newer revisions remain represented while a pass drains | Block cooldown/window after materialization | Deactivation removes subscription and final-subscriber dirty state; close does the same | Publisher pays O(1) ingress; runtime reserve pays fanout; User pays eventual attempt | -| `ObservationCrossing` | Ordered exact `previous -> current` feed revisions | `ObservationCrossing { phase, installed_at_revision }` | One generation-checked fire or rearm membership under the sparse threshold index | Fire sets the sole latch; rearm never latches; a full fire/rearm/fire while already latched may coalesce to one later cycle | Block cooldown/window after fire materialization | Deactivation removes membership and queued work loses authority; close removes all derived topology | Publisher pays O(1) ingress; runtime reserve pays Crossing service; User pays eventual attempt | -| `Cadenced` | Consensus timestamp deadline | `Cadenced { anchor_tick }` | One cadence wakeup, temporarily replaced by one retry wakeup while suspended | Uses no external latch; delayed periods coalesce to one current opportunity without catch-up | Timestamp cadence while Idle; block backoff while Suspended | Deactivation removes wakeup; close removes all temporal state | Runtime reserve pays wakeup materialization; User pays eventual attempt | +enum OpeningSurface { + PreservableAsset(AssetId), + TargetAsset(AssetId), + StakingShares(AssetId), +} +``` -Pause preserves accepted latch, Trigger runtime state, detector membership, pending observation work, and cadence meaning while blocking execution; resume reconstructs only the canonical placement needed by retained state. The global breaker likewise blocks Actor execution, not bounded detector evolution, wakeup materialization, fault recording, or mandatory cleanup. +Each Task field resolves to exactly one of: -One persisted bounded coordinator MUST give overdue temporal wakeups, Crossing transitions, and broad observation fanout one service opportunity per block in deterministic rotated family order before capturing the Actor-execution cutoff. It MAY give only the rotated first family one additional bounded lending opportunity when resumable per-block counters and charged remaining-work classification preserve every component cap. Each family MUST preserve its own canonical deadline or revision order, MUST retain one admissible configured service quantum when nonempty, and MUST yield without creating work when empty. Fixed coordination, mandatory cleanup, and all three service ceilings MUST be deducted before deriving the guaranteed Actor-execution floor. Corrupt family-order state MUST fail closed without granting one family service authority or mutating its work. Unused family capacity MAY be lent only within one explicit bounded materialization budget; fixed percentages that strand available materialization Weight are not the target contract. +```text +Resolved(positive value) +Skipped +FundingUnavailable +``` -Observation revision equality is an exact no-op and regression fails. Unavailable current observation rejects Active Crossing installation without inventing historical state. Once installed, accepted queued revisions remain ordered through temporary service unavailability; replacement or deactivation makes old generation/revision work physically removable but semantically unauthorized, so it cannot retrofire. Broad `ObservationChange` remains subscribed to accepted changed revisions independently of Crossing phase. +Rules: -A Trigger changes readiness only. Every detector converges through one activation operation that sets `pending_signal` `false -> true` and requests the same canonical placement contract. `pending_signal` is the sole external-signal latch. An AddressEvent without concrete source matches only `SourceFilter::Any`. One Actor never combines independent readiness sources. +- Percentages use widened floor division; +- Dynamic zero is `Skipped`; +- Absent/zero last-funding basis is `FundingUnavailable`; +- A positive exact debit above current capacity is `FundingUnavailable`; +- Missing admitted Opening state is an invariant failure; +- A positive exact amount MUST NOT be silently reduced; +- `AllAvailable` is an ordinary authored amount mode with no lifecycle privilege. -`pending_signal` has no direct clear call. Fresh opening consumes it; deactivation deletes it. Mutable deactivation followed by activation is the only actor-control reset. An Immutable actor retains an accepted latch until opening or close. +For multiple amount fields: -Fresh Active installation does not set `pending_signal`. `Manual`, `AddressEvent`, `ObservationChange`, and `ObservationCrossing` remain unready until their one source matches. `every_ticks` defines eligibility, not execution frequency. +```text +any FundingUnavailable -> FundingUnavailable +else any Skipped -> Skipped +else -> Executable(all values) +``` -`ObservationCrossing` authored identity contains exactly `feed`, `direction`, `threshold`, and `rearm_threshold`. Rising requires `rearm_threshold < threshold`; Falling requires `rearm_threshold > threshold`. Equality is invalid. One shared semantic validator owns this rule for creation, activation, replacement, genesis, simulation expected-value validation, and runtime integrity. +Source-capacity calculations preserve the current Action-fee reservation and protected minimum (§7.5, §10.4). -An armed Rising crossing fires exactly when `previous < threshold && current >= threshold`; its disarmed state rearms exactly when `previous > rearm_threshold && current <= rearm_threshold`. An armed Falling crossing fires exactly when `previous > threshold && current <= threshold`; its disarmed state rearms exactly when `previous < rearm_threshold && current >= rearm_threshold`. Repeated equal observations cause neither transition. +| Resolution surface | Tasks | Current/Opening basis | +| --- | --- | --- | +| Preserve-source | Transfer, SplitTransfer, SwapIn, AddLiquidity, RemoveLiquidity, Burn, Stake, DonateLiquidity | current preservable balance / `OpeningSurface::PreservableAsset` | +| Output-target | Mint, SwapOut | current spendable target / `OpeningSurface::TargetAsset`; `AllAvailable` forbidden | +| Share-spend | Unstake | current staking shares / `OpeningSurface::StakingShares` | -Active installation, reactivation, or semantic replacement initializes Crossing state from the current canonical observation without retroactive activation. A Rising value already `>= threshold`, or a Falling value already `<= threshold`, starts disarmed and waits for rearm; every other valid current value starts armed. Unavailable or uninitialized current state rejects Active admission through a typed error. Dormant authoring remains valid because it establishes no derived detection membership. +Fixed, Opening, and last-funding source/share values MUST fit current capacity. Output-target values are bounded by their own authored/adaptor rules, not current target balance. `PercentageAtOpening` never reads Trigger payload. -One fire commits canonical `WaitingForRearm` before requesting activation and atomically moves the derived membership to the rearm boundary. Queue pressure, delayed activation materialization, duplicate publication, and a set `pending_signal` cannot produce another fire before a qualifying rearm. Rearm commits canonical `Armed` without requesting activation and atomically restores fire membership. `ActorHot.trigger_runtime_state` owns phase and installation revision; physical membership contains only bounded removal/stale-rejection back-pointers and has no activation authority. The canonical FIFO owns eventual execution. +### 6.4 Funding accumulation -Every committed canonical observation update owns a monotonically increasing feed revision and one exact `previous -> current` transition. A transition that could fire or rearm is either durably admitted for bounded ordered processing or the observation publication fails atomically. Implementations MUST NOT coalesce revisions when an intermediate reversal, fire, or rearm could be lost, and later revisions for one feed cannot overtake its earlier incomplete transition. +```rust +enum FundingSourcePolicy { + OwnerOnly, + SignedAllowlist(BoundedBTreeSet), + RuntimePolicy, + AnyVerifiedIngress, +} +``` -`ObservationChange` remains a separate broad trigger over every committed change. It may share the same revision identity with Crossing processing, but it retains its subscriber pages and MUST NOT be reinterpreted as a threshold crossing or optimized by dropping subscribers. +Contract admission derives the exact bounded `funding_tracked_assets` set from all `PercentageOfLastFunding` references, including the canonical staking-share asset mapped for `Unstake`. -The reference cadence tick is 500 milliseconds of consensus timestamp. `now_tick = floor(timestamp_millis / 500)` decides readiness. Fresh activation uses `anchor_tick = ceil(timestamp_millis / 500)` and sets its first deadline to `anchor_tick + every_ticks`, so quantization never shortens the authored period. Genesis has no consensus timestamp: it stores an uninitialized anchor and one tick-zero bootstrap wakeup. The first ordinary wakeup service after timestamp inherent application sets the same ceiled anchor and full-period deadline without latching readiness or entering FIFO. +A positive certified credit is accumulated only when: -`Cadenced` requires `cooldown_blocks == 0` and no ScheduleWindow. This keeps each Actor in at most one temporal membership across the timestamp cadence index and the block-keyed retry/window substrate. A suspended Cadenced Actor temporarily uses only its ordinary block retry wakeup; after the cycle terminates, cadence rearms strictly after the observed tick. +1. Its asset is tracked; and +2. The funding policy accepts source and provenance. -Contract timing admission uses: +Policy acceptance is exact: -```rust -ensure!( - contract.cooldown_blocks <= MaxExecutionDelayBlocks, - Error::ExecutionDelayTooLong -); - -if let Trigger::Cadenced { every_ticks } = contract.trigger { - ensure!(every_ticks > 0, Error::InvalidTriggerConfiguration); - ensure!(every_ticks <= MaxCadenceDelayTicks, Error::ExecutionDelayTooLong); - ensure!(contract.cooldown_blocks == 0, Error::InvalidTriggerConfiguration); - ensure!(contract.window.is_none(), Error::InvalidScheduleWindow); -} -``` - -A zero cadence returns `InvalidTriggerConfiguration`. A delay above its typed protocol horizon returns `ExecutionDelayTooLong`. `MaxCadenceDelayTicks` is measured in cadence ticks and never reused as a consensus-block bound; `MaxExecutionDelayBlocks` is measured in consensus blocks and never reused as a cadence bound. The reference values are independently derived ten-Julian-year horizons: 631,152,000 ticks at 500 milliseconds and 52,596,000 blocks at 6 seconds. Overflow of otherwise valid cadence arithmetic returns `SchedulerIndexExhausted`. +| Policy | Accepted source | +| --- | --- | +| `OwnerOnly` | signed provenance and concrete source equal to owner | +| `SignedAllowlist` | signed provenance and concrete allowlisted source | +| `RuntimePolicy` | configured authority accepts the exact source/provenance pair; all-`None` is denied | +| `AnyVerifiedIngress` | concrete source or typed provenance exists; all-`None` is denied | -```text -cooldown_anchor = - last_cycle_block.or(schedule_anchor) +Rejected or untracked credit remains custody only. Trigger matching is independent (§4.7). -cooldown_eligible_at = - schedule_anchor - if cycle_nonce == 0 && last_cycle_block is None; - checked_add(cooldown_anchor, cooldown_blocks) - otherwise -``` +Opening snapshots and clears the accumulator (§5.2, §5.3). Later funding belongs to the next Cycle. Close or deactivation deletes the accumulator but does not move custody (§9.3). -Cadence arithmetic is constant-time: +### 6.5 Step outcome and error policy ```rust -fn first_cadence_due_tick(timestamp_millis, tick_millis, every_ticks) { - checked_add(ceil(timestamp_millis / tick_millis), every_ticks) +enum StepErrorPolicy { + AbortCycle, + ContinueNextStep, + RetryLater { max_attempts: u32 }, } -fn next_cadence_due_tick(anchor_tick, every_ticks, now_tick) { - first_due = checked_add(anchor_tick, every_ticks); - lower = checked_add(now_tick, 1); - if lower <= first_due { return first_due; } - periods = ceil(checked_sub(lower, anchor_tick) / every_ticks); - checked_add(anchor_tick, checked_mul(periods, every_ticks)) +enum StepSkippedReason { PreconditionFalse, ResolutionSkipped, FundingUnavailable } + +enum StepOutcome { + Executed, + Stopped, + Skipped(StepSkippedReason), + FundingUnavailable, + Failed(TaskFailure), } ``` -Admission guarantees nonzero `tick_millis` and `every_ticks`. Readiness floors the current timestamp; activation anchors ceil it. Rearming chooses the first cadence point strictly after the observed tick, so delayed service coalesces missed points into one opportunity and never creates catch-up cycles. +`RetryLater.max_attempts` counts the first unsuccessful execution and all retries. It MUST be in `2..=MaxRetryAttempts`. -```text -window_floor = - schedule_window.start when a window exists; - BlockNumber::zero() otherwise +Each step whose enclosing scheduler attempt commits selects exactly one row from the closed transition table: -signal_eligible_at(lower) = - max(lower, cooldown_eligible_at, window_floor) -``` - -Classification for `Manual`, `AddressEvent`, `ObservationChange`, and `ObservationCrossing` uses block-number eligibility plus `pending_signal`. `Cadenced` uses only its current timestamp tick while Idle, and block retry eligibility while Suspended. +| ID | Step result | Policy | Durable transition | +| --- | --- | --- | --- | +| `ST-01` | Precondition false | any | Record skip; advance or complete. | +| `ST-02` | Resolution `Skipped` | any | Record skip; advance or complete. | +| `ST-03` | `FundingUnavailable` | `AbortCycle` or `ContinueNextStep` | Record funding skip; advance or complete. | +| `ST-04` | `FundingUnavailable` | `RetryLater`, below bounds | Suspend same cursor. | +| `ST-05` | `FundingUnavailable` | `RetryLater`, local or global bound reached | Record terminal failure. | +| `ST-06` | Effect success | any | Record effectful success; advance or complete. | +| `ST-07` | `StopCycle` success | any | Record stopped; complete immediately. | +| `ST-08` | Temporary failure | `ContinueNextStep` | Record failure; advance or complete. | +| `ST-09` | Temporary failure | `AbortCycle` | Record failure; terminate Failed. | +| `ST-10` | Temporary failure | `RetryLater`, below bounds | Record failure; suspend same cursor. | +| `ST-11` | Temporary failure | `RetryLater`, local or global bound reached | Record terminal failure. | +| `ST-12` | Permanent failure or predicate error | `ContinueNextStep` | Record failure; advance or complete. | +| `ST-13` | Permanent failure or predicate error | `AbortCycle` or `RetryLater` | Record failure; terminate Failed. | -If `cycle_nonce == 0 && last_cycle_block == None`, cooldown begins at `schedule_anchor` without addition. Otherwise cooldown is added to `last_cycle_block.or(schedule_anchor)`. +A step increments at most one primary outcome counter. -Retry timing is: +A Retry suspension increments cursor-local and global unsuccessful counters exactly once. Local bound wins when local and global bounds are reached by the same Attempt. ```text -retry_eligible_at = - checked_add( - last_attempt_block, - max(cooldown_blocks, backoff(unsuccessful_attempts_at_cursor - 1)) - ) +next_local = previous suspension at same cursor ? previous + 1 : 1 +next_global = unsuccessful_attempt_streak + 1 +backoff(i) = min(2^i, 8 blocks) ``` -Every Cadenced Actor with the same `anchor_tick` and period has the same temporal gates. FIFO admission and bounded service, not an authored or derived phase, determine execution order. +`FundingUnavailable` is not a Task failure. `AbortCycle` aborts on Task failure, not unavailable funding. -For window installation: +Outcome deltas are exact: precondition skip increments `precondition_skips`; resolution skip increments `skipped_resolution`; advancing funding skip increments `skipped_funding_unavailable`; successful effect increments `executed_steps` and `committed_effectful_tasks`; successful `StopCycle` increments `executed_steps`; invoked Task failure increments `failed_steps`. No other counter changes. -```text -start_delay = - start.saturating_sub(now) +A terminal Failed Attempt increments the global unsuccessful streak once. A `ContinueNextStep` failure does not. A completed Cycle resets the global unsuccessful streak. Deferral, pause, cancellation, exact no-op, and advancing funding skip do not increment it. -first_temporal_eligible = - temporal_eligible_at(schedule_anchor) under the prospective Active state +Retry eligibility is: + +```text +retry_eligible_at = last_attempt_block + max(cooldown_blocks, backoff(next_local - 1)) ``` -Validation requires `end > start`, representable `end + 1`, inclusive length `>= MinWindowLength`, `now <= end`, `start_delay <= MaxExecutionDelayBlocks`, and `first_temporal_eligible <= end`. This ScheduleWindow contract applies only to `Manual`, `AddressEvent`, `ObservationChange`, and `ObservationCrossing`; no future signal is assumed. +### 6.6 Task semantics -### 3.3 Precondition and Amounts +```rust +struct SplitLeg { to: AccountId, share: Perbill } -An absent Step `precondition` is unconditional. A present `Precondition` is one non-empty disjunctive-normal-form value: outer clauses compose as OR and each inner clause composes as AND. JSON uses only singular `precondition`; `{"precondition":{"clauses":[[A, B], [C]]}}` means `(A AND B) OR C`. An empty outer array or any empty inner array is invalid, and no plural field alias is accepted. +enum InputLimit { LiveQuote, Absolute(Balance) } -Every predicate explicitly carries `ObservationTiming::{Opening, Current}`. No predicate has an implicit timing. `Opening` evaluates through the single predicate evaluator while opening the logical cycle and freezes the resulting truth value for every step, retry, and Continuation in that cycle. `Current` invokes the same evaluator immediately before its owning step and therefore sees successful effects committed by earlier steps in the same attempt. Any predicate error fails the precondition expression permanently; false yields `StepSkipped(PreconditionFalse)` and never creates failure, retry, or suspension. +enum Task { + Transfer { to: AccountId, asset: AssetId, amount: AmountResolution }, + SplitTransfer { asset: AssetId, amount: AmountResolution, legs: BoundedVec, MaxSplitTransferLegs> }, + SwapIn { asset_in: AssetId, amount_in: AmountResolution, asset_out: AssetId, slippage_tolerance: Perbill }, + SwapOut { asset_out: AssetId, amount_out: AmountResolution, asset_in: AssetId, input_limit: InputLimit, slippage_tolerance: Perbill }, + AddLiquidity { asset_a: AssetId, asset_b: AssetId, amount_a: AmountResolution, amount_b: AmountResolution, min_lp_out: Balance }, + RemoveLiquidity { lp_asset: AssetId, asset_a: AssetId, asset_b: AssetId, lp_amount: AmountResolution, min_amount_a: Balance, min_amount_b: Balance }, + Burn { asset: AssetId, amount: AmountResolution }, + Mint { asset: AssetId, amount: AmountResolution }, + Stake { asset: AssetId, amount: AmountResolution }, + DonateLiquidity { asset_a: AssetId, asset_b: AssetId, max_amount_a: AmountResolution, max_ratio_error: Perbill }, + Unstake { asset: AssetId, shares: AmountResolution }, + StopCycle, +} +``` -Admission canonicalizes each clause by canonical typed SCALE order, removes repeated predicates within that clause, then orders clauses by their canonical typed SCALE encoding. Two clauses that become identical after predicate deduplication are rejected. After that rejection, a clause that is an exact predicate superset of another clause is absorbed by the subset under `A OR (A AND B) = A`. Canonical equality and exact no-op comparison use this admitted form before rate limiting, cancellation, storage writes, placement reconstruction, or events. +General rules: -A present `Precondition` contains `1..=MaxPreconditionClauses` conjunctions, each conjunction contains `1..=MaxPredicatesPerClause` predicates, and the sum across all clauses is `1..=MaxPredicatesPerStep`. Production, continuation, simulation, fee estimation, Weight derivation, forecast, generated vectors, and client explanation consume one evaluator result type and visit every admitted predicate without short-circuit. The bounded full visit prevents data-dependent Weight while preserving ordinary AND/OR truth. +- At most one Task effect is invoked per Attempt (§6.1); +- `Mint` is System-only; +- Self-transfer and duplicate split recipients are invalid; +- Each Task contains at most two `AmountResolution` fields; +- Every debit preserves the protected minimum (§10.4); +- `Transfer(AllAvailable)` has no close-specific privilege; +- `CloseAfterProductiveCycle` observes only committed effectful Tasks (§9.2); +- Task effects use canonical host operations (§11.1). -Every `Above` predicate uses strict `>` comparison. Every `Below` predicate uses strict `<` comparison. Equality satisfies neither. +`SplitTransfer` requires 2..=`MaxSplitTransferLegs`, positive unique shares, and total share `<= 1`. Each leg is floored; rounding dust remains with the Actor. -Observation `Stale`, `Uninitialized`, or `Unavailable` is false. A structurally invalid `Fresh` yields `PredicateError::InvalidObservation` and is Permanent. `max_age_blocks > 0`. Predicates are pure. +`SwapIn` and `SwapOut` use current executable quotes inside the adapter boundary (§11.4). `InputLimit::Absolute` is a cap, not an admission gate. -The subject of every `Balance*` predicate is the actor's sovereign account. Predicates cannot read a third-party account. They use the ordinary `AssetOps::balance` surface. For `FeeNativeAssetId`, they subtract the current transient fee reservation but not the protected minimum. They never redirect to `StakingOps::share_balance`. +`AddLiquidity` amounts are debit caps; actual used amounts and LP output are returned. `RemoveLiquidity` debits the exact resolved LP amount. `DonateLiquidity` uses one authored cap plus one current derived preservable cap (§11.4). -Predicate truth does not authorize spending; resolution independently enforces capacity. +--- -Percentages use widened floor division. `PercentageOfCurrent` and `PercentageAtOpening` use the same policy-specific surface and differ only in read time. `PercentageAtOpening` never reads signal payload or AddressEvent amount. +## 7. Economics and block resources -| Policy | Tasks | Current/opening base and capacity | -| --- | --- | --- | -| preserve source | Transfer, SplitTransfer, SwapIn, AddLiquidity, RemoveLiquidity, Burn, Stake, DonateLiquidity | Outside a terminal custody drain, `AllAvailable` and `PercentageOfCurrent` use `preservable_balance`; `PercentageAtOpening` reads `OpeningSurface::PreservableAsset`; fixed/opening/funding values must fit current preservable capacity. An admitted terminal custody drain uses `terminal_drain_balance` for its sole Transfer. | -| output target | Mint, SwapOut | `PercentageOfCurrent` uses `spendable_balance`; `PercentageAtOpening` reads `OpeningSurface::TargetAsset`; fixed/opening/funding target values are not capped by current target balance; `AllAvailable` is forbidden | -| share spend | Unstake | current basis is `StakingOps::share_balance`; `PercentageAtOpening` reads `OpeningSurface::StakingShares`; `AllAvailable` spends all current shares; fixed/opening/funding values must fit current shares | +### 7.1 Economic surfaces + +Actors has four service-fee boundaries plus one refundable state resource: ```text -spendable_balance(actor, asset, reserved_fee_remaining) = - AssetOps::balance(actor, asset).saturating_sub(reserved_fee_remaining) - if asset == FeeNativeAssetId; - AssetOps::balance(actor, asset) - otherwise - -protected_minimum(actor, asset) = - MinUserBalance - if actor is User and asset == FeeNativeAssetId; - AssetOps::minimum_balance(asset) - otherwise - -preservable_balance(actor, asset, reserved_fee_remaining) = - spendable_balance(actor, asset, reserved_fee_remaining) - .saturating_sub(protected_minimum(actor, asset)) - -terminal_drain_balance(actor, asset, reserved_fee_remaining) = - spendable_balance(actor, asset, reserved_fee_remaining) +committed User creation -> ActorCreationFee +useful Trigger occurrence -> TriggerFee +Pipeline Opening -> PipelineMachineFee +invoked Action attempt -> ActionExecutionFee +retained User Actor state -> ActorStateHold ``` -`reserved_fee_remaining` is zero for System attempts. +Ordinary transaction payment remains owned by the host transaction-payment contract. Task-native protocol fees remain owned by their underlying mechanism (§11.1). -A field yields: +System Actors are exempt from Actors Creation, Trigger, Pipeline, and Action fees, but consume identical block resources. Task-native protocol fees MAY still apply. -- `Resolved(value)` for a positive valid value; -- `Skipped` for a valid dynamic zero, including zero current or opening basis; -- `FundingUnavailable` for absent/zero `PercentageOfLastFunding` basis, a positive debit above current source/share capacity, or a required auxiliary debit cap of zero. +### 7.2 Creation fee and state hold -Every admitted `PercentageAtOpening` surface exists in `opening_snapshot`, including zero-valued surfaces. A missing key is `SnapshotUnavailable` and indicates invariant failure. +`ActorCreationFee` is a fixed non-refundable process-admission and anti-spam charge. It is paid by the signed creator only when User creation commits. -Multi-surface aggregation: +It MUST NOT be described as a second payment for create-call Weight. The ordinary transaction fee owns synchronous call computation. -```text -any FundingUnavailable -> FundingUnavailable -else any Skipped -> Skipped -else -> Executable(all resolved fields) -``` +The Creation Fee also economically backs the protocol obligation to perform minimal pre-Opening apoptosis if later required (§9.4). It does not prepay Trigger, Pipeline, or Action service. -Admission rejects zero fixed/percentage/absolute bounds, `AllAvailable` on Mint/SwapOut, identical pairs, `Transfer.to == sovereign_account`, any split recipient equal to the sovereign account, duplicate split recipients, mismatched LP pair, unsupported/class-forbidden modes, and zero required liquidity minima. Self-recipient rejection uses `SelfTransferNotAllowed`. +`ActorStateHold` is a refundable geometry-backed hold on the User owner account, not a fee and not rent. -Outside an admitted terminal custody drain, `AllAvailable` preserves the current protected minimum and reserves no fee budget beyond the current attempt. Inside an admitted terminal custody drain, `AllAvailable` resolves to `terminal_drain_balance`; successful transfer cannot leave the actor open because transfer and close share one scheduler-attempt transaction. +Rules: -### 3.4 Tasks and Canonical Step Control +- Elapsed time does not change it; +- No recurring collection exists; +- Active identity/head/body/detector/funding state is held by actual retained geometry; +- Zero/one-Step Contracts MUST NOT reserve a 32-Step body footprint; +- Active installation reserves one type-derived maximum admitted run-state hold before autonomous Trigger service becomes possible; +- The maximum is derived from bounded runtime types and MUST NOT use a hand-maintained byte constant; +- Opening, Q1 progress, retry, suspension, Cycle boundary, and cancellation MUST NOT mutate or release the run-state hold; +- Deactivation or Close releases the active-installed run-state hold atomically with removal of Active authority; +- State transitions reserve/release all other exact hold deltas atomically; +- Inability to reserve the active-installed run hold rejects creation, activation, or replacement atomically before autonomous service is admitted; +- Close releases all Actor-state hold (§9.3); +- Sovereign custody is not Actor state and is not held by this mechanism (§10.3). -Task variants own their declared asset and recipient fields. Mint is System-only. +Deactivation releases Active Contract/topology/funding/run holds and retains only the Dormant identity hold (§3.5, §9.5). System state remains host-owned bounded capacity and MAY be hold-exempt, but exemption grants no resource or scheduler preference. -| Task | Debit / output contract | -| --- | --- | -| Transfer, Burn | debit exactly the resolved amount | -| Stake | stake exactly the resolved amount | -| Unstake | unstake exactly the resolved shares; `AllAvailable` first resolves to the full current share balance | -| SwapIn | debit exactly the resolved input; `DexOps` owns executable quote and output protection | -| SwapOut | credit exactly the resolved output; Actors supplies one finite input-cap ceiling and `DexOps` owns executable quote/tolerance cap | -| AddLiquidity | resolved `amount_a` and `amount_b` are debit caps; return exact positive used amounts and LP output meeting `min_lp_out` | -| RemoveLiquidity | debit exactly the resolved `lp_amount`; `min_amount_a` and `min_amount_b` are output floors | -| DonateLiquidity | `max_amount_a` and derived `max_amount_b` are debit caps; return exact positive used amounts within both caps | +Generated state descriptors own exact byte accounting. The fixed Creation Fee is not a cleanup escrow: minimal apoptosis remains a protocol obligation even if later cleanup cost exceeds the historical fee (§9.4). -Ordered LP identity is validated at admission and rechecked at execution. `RemoveLiquidity` MUST NOT perform a smaller partial LP debit than the resolved `lp_amount`. +### 7.3 Trigger fee -An Actor Contract is a terminal custody drain exactly when every listed property holds: +```text +TriggerFee = WeightToFee(maximum generated control Weight of one useful family-specific occurrence) +``` -- `completion_policy == CloseAfterProductiveCycle`; -- `contract_steps` contains exactly one step; -- That step has `precondition: None`; -- That step is `Transfer { to, asset, amount: AllAvailable }`; -- That step uses `AbortCycle`. +A User Trigger fee is charged exactly once from fee-native balance above `MinUserBalance` before `pending_signal: false -> true` (§4.2, §10.4). Redundant latched-period activity performs no Actor-specific Trigger work and charges no Trigger fee (§4.3). -For a terminal custody drain only: +A useful User `manual_trigger` call charges the complete generated Manual Trigger owner to the sovereign account and returns `Pays::No`. A redundant, rejected, underfunded, or System Manual call retains ordinary transaction payment and charges no User Trigger fee. This is the complete no-double-charge rule for Manual. -- The Transfer amount resolves to `terminal_drain_balance`, not `preservable_balance`; -- For `FeeNativeAssetId`, the complete current attempt reservation is excluded before resolution, and the attempted-step fee is collected before transfer; -- Successful Transfer completes the one-step plan with `committed_effectful_tasks == 1` and MUST close with `ProductiveCycleCompleted` in the same scheduler-attempt transaction; -- Failure after provisional Transfer success, including terminal-cleanup failure, rolls back the Transfer, fees, events, and close with the enclosing attempt; -- Transfer failure commits no Transfer or close and follows ordinary `AbortCycle` fee and failure semantics. -- If `terminal_drain_balance == 0`, the sole step follows ordinary zero-resolution semantics: it emits `StepSkipped(ResolutionSkipped)`, increments `skipped_resolution`, completes the cycle with no committed effectful task, does not close with `ProductiveCycleCompleted`, and retains the actor under ordinary post-cycle placement. Actors defines no empty-drain close reason. +Automatic source ingress/publishers pay their own source work; the Actor pays only its occurrence materialization (§7.7, §11.4). -A zero terminal drain under `Cadenced` may therefore open later empty cycles and charge their User evaluation fees. This is an explicit consequence of requiring productive close rather than an implicit empty-balance terminal path; authors that do not want recurring probes use signal-driven readiness or another lifecycle policy. +Insufficient Trigger capacity follows the family table (§4.4). Trigger-fee collection failure rolls back occurrence materialization and preserves the exact retry/fault authority (§4.4, §8.4). -No other plan may debit `protected_minimum`. One terminal custody drain names one asset. Recovering multiple known assets requires repeated successful reattachment and terminal-drain cycles; Actors performs no custody asset scan. +Committed Trigger fees are non-refundable and independent from later Pipeline admission. -For DonateLiquidity: +### 7.4 Pipeline Machine fee ```text -max_amount_b = - preservable_balance(actor, asset_b, reserved_fee_remaining) +PipelineMachineFee = WeightToFee(PipelineMachineEnvelope.control_weight_upper) ``` -A derived `max_amount_b == 0` contributes `FundingUnavailable`. The adapter receives both finite caps and `max_ratio_error`; it MUST NOT invent a larger cap. Dynamic pool ratio/liquidity/cap movement is Temporary; malformed pair or configuration is Permanent. `LiquidityDonated` records exact derived caps and actual used amounts. +The envelope is defined in §3.4. -`FundingUnavailable` is a pre-task amount-resolution outcome, not a task failure. For this outcome, `ContinueNextStep` and `AbortCycle` both emit `StepSkipped(FundingUnavailable)` and advance. Only `RetryLater` replaces that advancing result with suspension or unsuccessful-bound failure. An implementation MUST NOT apply the task-failure semantics of `AbortCycle` to `FundingUnavailable`. +The User fee is charged once from fee-native balance above `MinUserBalance` at Opening (§5.2, §10.4). It prepays all reachable Actor Control work of that Pipeline through its Cycle boundary, including reachable completion/close control. There is: -The authoring meaning is explicit: `AbortCycle` aborts on task failure, not unavailable funding. Likewise, a cycle whose attempted tasks all fail under `ContinueNextStep` reaches `Completed`, resets `unsuccessful_attempt_streak`, and retains factual `failed_steps`; the streak counts unsuccessful attempts, not failed steps. +- No separate `CleanupFee`; +- No per-Step machine charge; +- No remaining machine-budget ledger; +- No hold/refund settlement; +- No Trigger work in the quote; +- No Task-effect work in the quote. -Each step whose enclosing scheduler attempt commits selects exactly one row of the following closed transition table. A rolled-back attempt persists no row: +Running and Suspended Attempts consume paid machine authority without a balance predicate. -| Row | Step result | Policy / bound state | Step event | `OutcomeTotals` delta | Transition and boundary event | -| --- | --- | --- | --- | --- | --- | -| `ST-01` | precondition false | any | `StepSkipped(PreconditionFalse)` | `precondition_skips += 1` | advance | -| `ST-02` | resolution `Skipped` | any | `StepSkipped(ResolutionSkipped)` | `skipped_resolution += 1` | advance | -| `ST-03` | `FundingUnavailable` | `ContinueNextStep` or `AbortCycle` | `StepSkipped(FundingUnavailable)` | `skipped_funding_unavailable += 1` | advance | -| `ST-04` | `FundingUnavailable` | `RetryLater`, neither unsuccessful bound reached | none | none | remain at cursor; apply one local/global unsuccessful increment; emit `CycleSuspended(FundingUnavailable)` | -| `ST-05` | `FundingUnavailable` | `RetryLater`, a local/global unsuccessful bound reached | none | none | finalize `Failed`; emit `CycleSummary(Failed)`, then close with the Section 2.4 bound reason | -| `ST-06` | successful effectful task | any | the task success event | `executed_steps += 1`; `committed_effectful_tasks += 1` | advance; at plan end finalize `Completed` | -| `ST-07` | successful `StopCycle` | any | `CycleStopped` | `executed_steps += 1` | finalize `Completed` immediately | -| `ST-08` | Temporary task failure | `ContinueNextStep` | `StepFailed(Temporary)` | `failed_steps += 1` | advance | -| `ST-09` | Temporary task failure | `AbortCycle` | `StepFailed(Temporary)` | `failed_steps += 1` | apply one global unsuccessful increment; finalize `Failed`; emit `CycleSummary(Failed)` and close with `ConsecutiveFailures` when that bound is reached | -| `ST-10` | Temporary task failure | `RetryLater`, neither unsuccessful bound reached | `StepFailed(Temporary)` | `failed_steps += 1` | remain at cursor; apply one local/global unsuccessful increment; emit `CycleSuspended(Temporary)` | -| `ST-11` | Temporary task failure | `RetryLater`, a local/global unsuccessful bound reached | `StepFailed(Temporary)` | `failed_steps += 1` | finalize `Failed`; emit `CycleSummary(Failed)`, then close with the Section 2.4 bound reason | -| `ST-12` | Permanent task failure | `ContinueNextStep` | `StepFailed(Permanent)` | `failed_steps += 1` | advance | -| `ST-13` | Permanent task failure | `AbortCycle` or `RetryLater` | `StepFailed(Permanent)` | `failed_steps += 1` | apply one global unsuccessful increment; finalize `Failed`; emit `CycleSummary(Failed)` and close with `ConsecutiveFailures` when that bound is reached | +Insufficient current capacity causes minimal apoptosis before Opening (§9.4). Fee-collector infrastructure failure rolls back Opening and preserves the latch (§5.2). -A step increments at most one `skipped_*` counter. `CycleSuspended` and `CycleSummary` carry cumulative outcomes after the current row's delta. A step event precedes the boundary event caused by that step. Prior successful task layers and their outcome deltas remain provisional until the enclosing scheduler attempt commits. The cursor only remains, advances by one, or terminates. +Committed Pipeline fees are non-refundable. Every Actors fee transfer is ledger-only: it MUST NOT create AddressEvent ingress, funding accumulation, Trigger readiness, or placement (§11.4). -For SwapOut: +### 7.5 Action fee -```text -capacity_cap = - preservable_balance(actor, asset_in, reserved_fee_remaining) +Before an Action-bearing Task is invoked, the current Attempt reserves the maximum Task-effect fee from the User Actor's fee-native balance above `MinUserBalance` (§10.4). -authored_cap = - capacity_cap - for InputLimit::LiveQuote; - min(capacity_cap, absolute) - for InputLimit::Absolute(absolute) +```text +cannot reserve -> FundingUnavailable; no Action invocation; no Action fee +can reserve -> invoke canonical effect + -> settle valid actual effect Weight + -> charge actual Action fee on success or typed failure ``` -`authored_cap == 0` is `FundingUnavailable`. No Actors-side quote call exists. +A retry is a new independently charged Action attempt. Before a Suspended User Actor receives retry execution, the scheduler proves that its fee-native balance above `MinUserBalance` can cover the current Action maximum. The Step effect and actual-fee settlement remain one transaction, so unused maximum liability is released and no terminal consensus debt can survive rollback. Insufficient retry liability selects fee-free custody-neutral minimal apoptosis before invocation. -Inside one `DexOps` task layer: +False Precondition, skipped resolution, `FundingUnavailable`, and `StopCycle` invoke no Action and charge no Action fee. -```text -ceil_perbill(x,p) = - (wide(x) * p.deconstruct() + Perbill::ACCURACY - 1) - / Perbill::ACCURACY - -quoted_tolerance_cap = - checked_add( - quote_required_in, - checked_narrow(ceil_perbill(quote_required_in, tolerance)) - ) - -effective_max_in = - min(authored_cap, quoted_tolerance_cap) -``` +Invalid or greater-than-reserved actual Weight rolls back the current-Step transaction and fails closed. -The adapter returns Temporary before mutation when quote is unavailable/zero, `effective_max_in < quote_required_in`, or execution cannot satisfy exact output within `effective_max_in`. `InputLimit::Absolute` is a cap, not an admission gate. +### 7.6 Actor Control and Shared Economic meters -For SwapIn, the adapter obtains one current executable quote before mutation and requires: +The production block policy is component-wise: ```text -actual_out > 0 -actual_out >= floor((1 - tolerance) * quote_output) +Actor Control hard ceiling = floor(SchedulableBlockWeight / 3) +Shared Economic = SchedulableBlockWeight - Actor Control +Actor base turn = floor(Shared Economic / 2) +User base turn = Shared Economic - Actor base turn ``` -`slippage_tolerance == Perbill::one()` is allowed but does not waive `actual_out > 0` or any finite SwapOut cap. Exact-input quotes are never inverted across balance width. +One third is the maximum permissible Actor Control share. Further throughput work MUST optimize inside that envelope and MUST NOT take additional guaranteed capacity from Actor effects or user dispatch. Deterministic floor/remainder ownership prevents resource loss in either Weight component. -Quote, tolerance arithmetic, System guard, Router call, actual validation, paired ingress, rollback, and success event belong to one task Weight class and one task transaction. Actors exposes no competing quote surface. +Actor Control owns: -### 3.5 SplitTransfer +- Detector and Trigger occurrence work; +- Latch and placement; +- Pipeline Opening; +- Precondition and amount resolution; +- Run persistence, retry, completion, and close control; +- Scheduler and bounded cleanup. -Requires `2..=MaxSplitTransferLegs`, positive shares, unique non-self recipients, and sum `<= 1`. +Shared Economic owns: -```text -leg_i = floor(total * share_i) -distributed = checked_sum(leg_i) -retained = total - distributed -effective_legs = count(leg_i > 0) -``` +- Ordinary external economic calls; +- Actor Task effects. + +A Task effect and its external equivalent use the same canonical host mechanism and effect Weight (§11.1). -`distributed == 0` emits `StepSkipped(ResolutionSkipped)`. Otherwise every nonzero leg preflights and commits in one task layer; `effective_legs == 1` is valid. Undeclared share and rounding dust remain on the sovereign account. Recipient deposit failure is Temporary `RecipientDepositUnavailable`. +After mandatory Actor work is accounted, an explicit deterministic host policy MAY lend unused Actor Control capacity to Shared Economic work. Actor Control MUST NOT exceed its one-third ceiling or borrow Shared Economic capacity. Lending MUST NOT change FIFO, causal cutoff, or Q1. -## 4. Execution and Economics +### 7.7 No double charging -### 4.1 Actor Classification and Attempt Sequence +No two economic surfaces may charge the same Weight or retained byte. -One pure runtime function owns Active actor classification. Scheduler admission, permissionless sweep, actor-targeting expiry substitution, certified-ingress expiry handling, simulation, and eligibility MUST use its result. Absence and a valid Dormant identity are resolved by the caller before classification; a partially present Active partition set is an invariant error. +| Work | Sole economic owner | +| --- | --- | +| External call dispatch/origin work | ordinary transaction fee | +| User process admission | Actor Creation Fee (§7.2) | +| Retained Actor state | Actor State Hold (§7.2) | +| Useful readiness `false -> true` | Trigger Fee (§7.3) | +| Complete admitted Pipeline control | Pipeline Machine Fee (§7.4) | +| Invoked Task effect | Action Fee (§7.5) | +| Router trading fee, staking fee, etc. | underlying host mechanism (§11.1) | + +--- + +## 8. Scheduler and readiness + +### 8.1 Active Actor classification + +One pure classifier owns structural Active-state classification. ```rust enum ActorExecutionPhase { GlobalCircuitBreaker, Paused, + WaitingNextBlock(BlockNumber), WaitingRetry(BlockNumber), - WaitingTemporal(BlockNumber), + WaitingCadenceTick(u64), WaitingSignal, Ready, } @@ -858,388 +1154,366 @@ struct ActorClassification { terminal_reason: Option, execution_phase: ActorExecutionPhase, } - -enum ActorClassificationError { - ActorInvariant, - ContinuationInvariant, - ComputationOverflow, -} ``` -These are internal semantic types. Public runtime APIs expose only the projections in Section 7. +The classifier: -Classification returns `Result, ActorClassificationError>`. +1. validates canonical Active partitions (§3.5); +2. validates `CycleState` against `ActorRunState` (§5.1); +3. validates current Contract/run/admission bindings (§3.4, §5.1); +4. computes stored-state terminal predicates (§9.2); +5. computes phase in this order: breaker, pause, terminal-ready, next-block/retry/tick wait, signal wait, ready. -`terminal_reason` and `execution_phase` are separate outputs. `Ready` means only that no execution-phase gate applies; it does not override `terminal_reason` or authorize an attempt when `terminal_reason` exists. +Opening-specific fee and nonce checks are not classifier-owned; they belong to Opening (§5.2). -Classification rules: +Classification errors are typed and MUST NOT become absence, waiting, or Ready (§12.4). -1. Validate the Active identity, hot, contract, and funding partitions and their required cross-partition relations. Missing or inconsistent required Active state returns `ActorInvariant`. -2. Validate `cycle_state` against `ContinuationState`, including cursor range, retry-policy ownership, positive and representable counters, and suffix snapshot bounds. A counter that reaches a terminal threshold remains valid terminal state and is evaluated by rule 4. A Suspended actor without one valid Continuation, or an Idle actor with a Continuation, returns `ContinuationInvariant`. -3. Select `cursor = 0` for Idle or `cursor = ContinuationState.cursor` for Suspended. -4. Compute `terminal_reason` by Section 2.4 precedence from terminal predicates already true in current stored state. Select the first satisfied predicate and do not evaluate lower-precedence predicates. User fee-budget evaluation uses the full plan from cursor `0` for Idle or the current suffix from `cursor` for Suspended. +### 8.2 Placement and temporal readiness - `CycleNonceExhausted` and `AutoCloseNonceReached` are classification predicates only while Idle. A stored retry or failure counter at its terminal threshold is valid terminal state. `ProductiveCycleCompleted` and counter changes that would be caused only by the current attempt are not predicted. -5. Compute `execution_phase` in this order: - 1. `GlobalCircuitBreaker` when the global breaker is active; - 2. `Paused` when the actor is paused; - 3. `Ready` when `terminal_reason` exists and neither preceding gate applies; no retry, temporal, or signal arithmetic is then evaluated; - 4. For Suspended, `WaitingRetry(next_block)` when retry eligibility is later than the current block, otherwise `Ready`; - 5. For Idle Cadenced, `Ready` when `pending_signal == true`; otherwise `WaitingCadenceTick(due_tick)` carrying the exact actor-owned tick pointer, even when that tick is already due but the wakeup worker has not materialized it; - 6. For other Idle triggers, `WaitingBlock(next_block)` when block eligibility is later than the current block; - 7. For signal-driven Idle, `WaitingSignal` when block eligibility has opened and `pending_signal == false`; - 8. `Ready` otherwise. -6. Checked arithmetic required to classify the existing actor, including current fee-envelope rederivation and retry/block-temporal target computation, returns `ComputationOverflow` on failure. Cadence classification reads its exact live tick pointer and never computes a later period. A classification error MUST NOT be converted into a phase, `NotReady`, absence, or `Ready`. +An Actor owns at most: -`WaitingSignal` carries no block because the next matching signal may arrive in any future block or never arrive. +- One live FIFO ticket; and +- One pipeline temporal target; and +- One independent Trigger temporal target for `AtTime`/`Cadenced`. -Scheduler admission applies the classification in this order: +```rust +type QueueTicket = u64; +type WakeupPageId = u64; +type WakeupSlot = u32; +type ObservationRevision = u64; -1. When `execution_phase == GlobalCircuitBreaker`, defer without closing or attempting. -2. When `terminal_reason` exists, admit the complete terminal-cleanup Weight; defer if it does not fit, otherwise close transactionally. -3. When `execution_phase != Ready`, defer without mutation. -4. Continue to attempt admission only when `terminal_reason == None` and `execution_phase == Ready`. +enum WakeupKey { Block(BlockNumber), Tick(u64) } -Permissionless sweep closes when `terminal_reason` exists and otherwise performs no actor transition. It ignores `execution_phase`. +struct PipelineWakeupPointer { + key: WakeupKey, + page_id: WakeupPageId, + slot: WakeupSlot, +} -Actor-targeting control and certified ingress use the same classification for expiry substitution. They act only when `terminal_reason == Some(WindowExpired)`; every other terminal reason remains owned by scheduler admission or sweep and does not replace the requested control or ingress effect. They ignore `execution_phase` for this purpose. An authorized Mutable actor MAY therefore repair a non-window terminal condition before scheduler or sweep commits close; User Immutable has no such control path. +struct TriggerWakeupPointer { + tick: u64, + page_id: WakeupPageId, + slot: WakeupSlot, +} -Simulation and eligibility apply only their interface-specific presence, expected-value, and mode checks, then project the canonical classification. They MUST NOT independently recompute terminal or execution-phase predicates. +struct ActorStepTicket { + actor_id: ActorId, + cycle_nonce: u64, + cursor: u32, + ticket: QueueTicket, + eligible_at: BlockNumber, + semantic_contract_id: Hash, + body_commitment: Hash, +} +``` -A scheduler classification error preserves the live head, performs no actor mutation, and stops the pass as `InvariantStalledLiveHead`. Every dispatch and runtime-API caller preserves classification errors through the exact projection in Section 9.2. +Future work remains in bounded wakeup topology until eligible. The ready FIFO contains only eligible work. -The attempt sequence begins only after scheduler admission accepts `terminal_reason == None` and `execution_phase == Ready`: +A successor, retry, or retained next-cycle request becomes eligible no earlier than the following block (§6.1, §5.6). Queue pressure preserves the exact obligation; it does not lose readiness or report completion. -1. Admit the complete current attempt Weight and, for User, the exact current attempt fee envelope. -2. Load Continuation inputs or prepare fresh opening and funding snapshots from pre-opening state. -3. For a fresh cycle, atomically increment `cycle_nonce`, consume the latch and funding accumulator, update opening clocks, and emit `CycleStarted`. For a retry, atomically increment `attempt` and emit `CycleContinued`. -4. For each suffix step: - 1. Evaluate the Step Precondition through all admitted Predicates; - 2. Resolve all amounts against the current reservation; - 3. Remove that step's complete envelope from the reservation exactly once; - 4. Collect the selected User fee; failure follows Section 4.3, bypasses `StepErrorPolicy`, and rolls back the enclosing attempt before task dispatch; - 5. Execute one nested task layer when the task is attempted; - 6. Select the Section 3.4 transition row; an advancing row continues, and a suspension or terminal row exits the loop. -5. Finalize the selected row or plan end: apply the Section 2.4 failure-counter transition, emit the Section 3.4/Section 8 boundary event, and determine any failure- or completion-driven close reason. -6. Close when finalization requires it; otherwise install at most one future scheduler path, or install none when no readiness is due. -7. Commit the scheduler-attempt transaction. +An Idle Opening ticket binds the checked prospective nonce `identity.cycle_nonce + 1`; a Running/Suspended ticket binds `run.cycle_nonce`. Ticket and wakeup authority are exact. Mismatch creates a stale tombstone with no semantic authority. -New cycles start at cursor `0`. Retries start at the Continuation cursor. `executions(actor_id, block) <= 1`. +### 8.3 Prepass, cutoff, Drain, and FIFO service -### 4.2 Snapshots and Fresh Opening +Every block contains exactly one mandatory Actor Prepass after required context inherents and before ordinary external extrinsics. -Fresh-cycle read-only preparation occurs after fee reservation and before `CycleStarted`. +Prepass: -```text -opening_snapshot = - every unique policy-specific OpeningSurface referenced by - PercentageAtOpening in contract_steps[0..] +1. Services bounded stale cleanup; +2. Services only detector/temporal obligations caused before the current block boundary; +3. Materializes eligible readiness (§4.2, §8.4); +4. Freezes one immutable maximum ticket cutoff; +5. Executes the Actor base pass in strict FIFO order. -proposed_funding_snapshot = - funding_accumulated as observed before opening -``` +After ordinary external dispatch, Actors Drain MAY continue service from the same FIFO head using the same cutoff and actual remaining meters (§7.6). -Opening surfaces: +Tickets created after cutoff MUST NOT execute in the current block. Current-block source activity is next-block work. This prevents same-block Actor recursion. -```text -PreservableAsset(asset) -> preservable_balance(actor, asset, reservation) -TargetAsset(asset) -> spendable_balance(actor, asset, reservation) -StakingShares(asset) -> StakingOps::share_balance(actor, asset) -``` +After bounded stale-head cleanup, the valid live FIFO head is authoritative: + +- It cannot be bypassed; +- No cheap/heavy/System/User reordering exists; +- If its complete current transition does not fit, the Actor pass stops; +- Later tickets remain untouched. -The opening snapshot is independent of trigger kind, signal payload, and funding tracking. Every admitted surface is present even when zero. Contract admission computes the exact unique surface set, requires every share mapping to exist, and returns `AdmissionBoundOverflow` rather than truncating when the set exceeds `MaxOpeningSnapshotEntries`. The snapshot contains no sender, event amount, or event list. +A maximum valid Step may be the only Actor Step in a block. This is paid bounded service, not structural starvation. Pipeline- or Action-fee collector failure rolls back the current transition, preserves the live head, and stops the pass; it is not a Task failure (§7.4, §7.5). -After every fallible opening check succeeds, one scheduler-attempt transaction atomically performs: +### 8.4 Detector workers, cohorts, and faults + +Detector geometry is source-specific: ```text -next_cycle_nonce = checked_add(stored_cycle_nonce, 1) -stored_cycle_nonce = next_cycle_nonce -pending_signal = false -last_cycle_block = now -funding_accumulated = empty -emit CycleStarted { cycle_nonce: next_cycle_nonce } +Manual -> direct call +AddressEvent -> certified destination/filter path +ObservationChange -> paged broad subscribers +ObservationCrossing -> ordered sparse threshold index +AtTime/Cadenced -> temporal index ``` -The first step reads the exact opening snapshot and pre-clear funding snapshot. Failed pre-opening admission consumes neither latch nor accumulator and emits no `CycleStarted`. A signal accepted after opening may latch the next cycle but never changes open snapshots. +A universal Trigger index is forbidden. -Retry performs no new capture. Suspension persists: +Workers MUST: -```text -opening_snapshot restricted to OpeningSurface keys referenced by - PercentageAtOpening in contract_steps[cursor..] +- Preserve source revision/time order; +- Inspect only exact affected candidates; +- Admit complete multidimensional Weight before mutation; +- Use bounded candidate/page/chunk counts; +- Preserve one exact source frontier on fault; +- Never skip corruption or let later source work overtake it. -funding_snapshot restricted to entries present in proposed_funding_snapshot and - referenced by PercentageOfLastFunding in contract_steps[cursor..] -``` +A homogeneous causal cohort (§4.6) MAY aggregate physical writes only when every included candidate follows the same generated control branch and compatible destination shape. Cohorting MUST NOT reorder candidates or create Task-shape affinity. -The unresolved cursor step is part of the suffix. The cursor never decreases. An absent funding entry denotes no accepted funding at opening; a missing admitted opening key is `SnapshotUnavailable`. +A worker fault records one bounded current fault. Repeated observation of the same uncleared fault emits no duplicate first-recorded event. Repair is explicit and bounded (§12.1, §12.2). The reference profile MUST admit a homogeneous causal cohort of at least 128 candidates when its generated Actor Control Weight and destination capacity fit. -### 4.3 User Fees and Economic Apoptosis +### 8.5 Scheduler liveness -```text -eval_weight_upper_i = - step_evaluation_weight_upper(User, step_i) +Given: -eval_fee_i = - WeightToFee(eval_weight_upper_i) +- A finite pre-cutoff ticket set; +- Recurring conforming Actor Control and Shared Economic capacity; +- Finite stale churn; +- Eventual placement capacity; +- No structural invariant fault; -exec_fee_upper_i = - WeightToFee(task_weight_upper(User, task_i)) +all live tickets receive service in FIFO order. -step_envelope_i = - checked_add(eval_fee_i, exec_fee_upper_i) +The protocol promises no fixed block latency. Increasing runnable population MAY increase inter-Step service gaps while preserving order and eventual service. -attempt_fee_envelope(plan, cursor, User).total = - checked_sum(step_envelope_i for i in cursor..plan.len) +Starvation telemetry MUST NOT change priority, order, or execution authority. -attempt_fee_envelope(_, _, System).total = - 0 -``` +--- -`step_evaluation_weight_upper` includes complete Predicate evaluation, opening/current/funding amount preparation, fee collection, and the largest non-task step event reachable before task dispatch. It comes from the same generated `WeightInfo` as execution. No `StepBaseFee`, legacy condition-read fee, parallel Task-weight table, or client-owned numeric fee model may exist. +## 9. Lifecycle -One `attempt_fee_envelope(plan, cursor, class)` is used by economic viability, admission, reservation, execution, and simulation. +### 9.1 Class and mutability -For a User attempt beginning at cursor `c`: +User creation is signed; the caller becomes owner and consumes one owner slot (§10.2). System creation requires `SystemOrigin` and consumes a System locator (§10.2). -```text -R_c = attempt_fee_envelope(plan, c, User).total +Actor-scoped authorization: -for each visited step i: - conditions_i and amount_resolution_i read R_i +| Actor | Authorized control | +| --- | --- | +| User | signed owner | +| System | signed owner or `SystemOrigin` | - charged_i = - eval_fee_i - if the task is not attempted; - step_envelope_i - if the task is attempted +User Mutable MAY update Contract, pause/resume, cancel a run, deactivate, reactivate, and explicitly close. - R_(i+1) = - checked_sub(R_i, step_envelope_i) -``` +Dormant creation requires `Mutable`. Immutable creation MUST install an Active Contract because no later activation authority exists. -The full Step envelope leaves reservation exactly once regardless of Precondition truth, resolution, failure class, or charged amount. The decrement occurs after that Step's Precondition/resolution and before any later Step's Precondition/resolution. +User Immutable: -After a visited step, its full envelope is removed from reservation before any later step is resolved. +- Rejects Contract replacement, pause/resume, activation/deactivation, cancellation, and owner close; +- MAY use an authored Manual Trigger; +- Cannot author `RetryLater`; +- An authored `AtTime` Contract MUST set `auto_close_at_cycle_nonce = Some(1)`, so its consumed one-shot source cannot leave an owner-inaccessible inert process; +- May terminate only through authored/lifecycle terminal rules, Trigger-underfunded one-shot apoptosis (§4.4, §9.4), Pipeline-admission apoptosis (§9.4), or deployed-lineage upgrade (§13.2). -Skip and pre-task resolution failure charge evaluation only. A task attempt charges the combined upper bound, not measured actual Weight. Adapter failure retains that combined fee only if the enclosing scheduler attempt commits. Retry creates a new suffix envelope and charges again. +System Immutable: -Successful User attempt admission proves that the payer holds `MinUserBalance` plus the complete current suffix fee envelope before attempt mutation. The reservation model prevents every ordinary task debit from consuming any fee amount still required by the current attempt. A conforming `FeeCollector::collect_fee` therefore MUST NOT fail solely because the payer lacks the reserved fee amount. +- Rejects actor-scoped control; +- Cannot author Manual or `RetryLater`; +- Is Actors-fee-exempt; +- Remains Active until an independently owned terminal rule or upgrade removes it. -Failure of `FeeCollector::collect_fee` is not a step outcome or `TaskFailure`. It denotes failure of the fee-asset ledger movement, FeeSink deposit capability, fee-path configuration, or an invariant. Fee collection MUST remain ledger-only and MUST NOT invoke Actors ingress preflight or notification, accumulate funding, latch readiness, or create scheduler placement. +Immutability fixes policy and owner control. It does not freeze runtime Weight, fee conversion, adapter behavior, or host economics. `auto_close_at_cycle_nonce` is changed only by complete Mutable Contract replacement and MUST be strictly above the current terminated nonce within `MaxAutoCloseNonceHorizon`. -Failure MUST roll back the complete scheduler-attempt transaction and MUST NOT invoke `StepErrorPolicy`. It persists no head consumption, fee, event, counter, nonce, cursor, snapshot, or task effect. It MUST NOT map to `InsufficientFee`, `BalanceExhausted`, or `FeeBudgetExhausted`, be represented as `TaskFailure`, or be processed by `StepErrorPolicy`. +### 9.2 Terminal precedence -Actor service stops as `FeeCollectionStalledLiveHead`. Unused reservation is not charged; durable fees are never refunded. +Terminal predicates are evaluated only by their owning transition. -A User actor is economically viable only while its protected balance and current attempt envelope satisfy Section 2.4. When scheduler or sweep observes nonviability, it closes through `BalanceExhausted` or `FeeBudgetExhausted`; an open Continuation is cancelled first. This does not grant control authority to User Immutable. A terminal custody drain remains subject to the same opening viability check; after the attempted-step fee is collected, its sole Transfer may consume the remaining protected minimum because productive close is mandatory in the same scheduler-attempt transaction. +#### Stored-state classifier precedence (§8.1) -User creation charges `ActorCreationFee` to the signed caller/owner inside the control transaction. The sovereign account never pays the creation fee. Inability to collect returns `InsufficientFee`; rejected creation restores every fee effect. System creation, System attempts, and terminal cleanup charge no Actors execution fee. Task-defined asset effects and Router fees still apply. +1. `WindowExpired`; +2. `RetryAttemptsExhausted`; +3. `ConsecutiveFailures`; +4. `AutoCloseNonceReached` while Idle; +5. Already-materialized permanent `SchedulerIndexExhausted`. -### 4.4 Atomicity +#### Opening precedence (§5.2) -- **Task layer**: adapter movement, paired ingress, and success event provisionally commit or roll back together. A successful task layer becomes durable only when the enclosing scheduler attempt commits. -- **Control transaction**: identity/locator, lifecycle, contract/funding, Continuation, subscriptions, readiness, membership, counters, creation fee, events, and required placement commit together. `Err` is a rejected transition. Exact no-op mutates/emits nothing; expiry substitutes atomic close. -- **Scheduler-attempt transaction**: head consumption, opening/retry, nested task layers, finalization, and future placement/close commit together. Late capacity, namespace, or invariant failure rolls back the complete attempt, including tasks and fees. -- **Terminal cleanup**: preflight ownership/index consequences, then delete/repair without task, fee, balance movement, retry, or requeue. In a terminal custody drain, the Transfer occurs in the preceding task layer; cleanup itself remains balance-neutral. +1. Stored-state terminal reason from classification (§8.1); +2. `CycleNonceExhausted`; +3. `CycleAdmissionInsufficient` when the Pipeline Machine fee cannot be provided (§7.4); active-installed run-hold inconsistency fails as an Actor invariant (§7.2). -Actors uses explicit storage transactions; FRAME dispatch failure alone does not provide the required rollback semantics. +#### Step-finalization precedence (§6.5) -## 5. Scheduler and Reactive Ingress +1. Retry-local bound; +2. Global failure bound; +3. `ProductiveCycleCompleted` after complete productive Cycle; +4. `AutoCloseNonceReached` after non-suspended Cycle boundary; +5. Permanent successor-placement `SchedulerIndexExhausted`. -### 5.1 FIFO and Temporal Readiness +#### Trigger-worker terminal (§4.4) -One logical FIFO owns ordinary readiness. Each actor has at most one live ticket; stale physical entries have no authority. After bounded temporal and observation work, `on_idle` snapshots `cutoff = NextQueueTicket`; only older tickets execute. +A due User `AtTime` source that cannot pay its Trigger fee selects `TriggerAdmissionInsufficient` and minimal apoptosis (§9.4). -Readiness `<= now` uses FIFO; later readiness remains an exact temporal target. A live ticket materialized before the cutoff MAY execute in the same block subject to FIFO order, the one-execution guard, and available Weight. A ticket created at or after the cutoff is necessarily `>= cutoff` and MUST NOT execute in that actor-service pass. Post-cutoff readiness for `now + 1` MAY therefore use a ticket only for a later pass. Queue saturation preserves readiness through an exact next-block target. +`CloseAfterProductiveCycle` closes only after the complete Cycle reaches `Completed` and `committed_effectful_tasks > 0` (§5.6, §6.5). -At any actor state, at most one ordinary temporal target exists. It is either block-keyed or timestamp-tick-keyed, never both. +### 9.3 Close and cancellation cleanup -```rust -let ordinary_target = if actor.lifecycle == ActiveLifecycle::Paused { - None -} else { - match (actor.cycle_state, actor.trigger, actor.pending_signal) { - (CycleState::Suspended, _, _) => - Some(Block(retry_eligible_at(actor)?)), - (CycleState::Idle, Trigger::Cadenced { .. }, _) => - Some(Tick(next_cadence_due_tick(actor)?)), - (CycleState::Idle, Trigger::Manual, true) | - (CycleState::Idle, Trigger::AddressEvent { .. }, true) | - (CycleState::Idle, Trigger::ObservationChange { .. }, true) | - (CycleState::Idle, Trigger::ObservationCrossing { .. }, true) => - Some(Block(signal_eligible_at(actor, lower)?)), - (CycleState::Idle, _, _) => None, - } -}; -``` +Close is lifecycle cleanup only. -A retry target and an Idle cadence target never coexist. A Paused actor has no ordinary target. Cadenced disallows ScheduleWindow, so timestamp cadence and a block terminal target never coexist. Signal-driven actors may select the earlier of their ordinary block target and `terminal_at`, retaining terminal precedence on equality. +It MUST atomically: -A future target is represented by one typed `wakeup_pointer`; its domain, key, page, and slot identify the actor-owned physical membership exactly. Both bounded temporal indexes are owned by one coordinator and only materialize due readiness into the same FIFO. Neither index executes Actors directly. +1. Cancel any open run (§5.6); +2. Revoke execution authority; +3. Remove hot Contract head, tail chunks, payloads, admission certificate, hot state, funding state, run state, detector memberships, tickets, wakeups, reverse indexes, and holds; +4. Release the User slot or mark the System locator vacant (§10.2); +5. Emit `ActorClosed` (§12.2). -Due temporal work removes the physical wakeup and materializes one FIFO service obligation. Actor classification later evaluates terminal predicates before ordinary readiness. Under the global breaker, materialization MAY occur but scheduler-owned close waits until the breaker clears. +Close MUST NOT: -Temporal insert, replacement, removal, earliest-due discovery, and materialization are bounded and scan neither empty block ranges nor all actors. Materialization and ownership removal are atomic. +- Enumerate assets; +- Transfer balances; +- Unwind positions; +- Invoke a Task; +- Select a refund recipient; +- Alter sovereign custody. -After bounded stale cleanup, actor service admits the live head or stops; it never scans behind, retickets, or demotes it. +Explicit User close is available only to the signed owner of a Mutable User Actor (§12.1). Its ordinary transaction fee is paid by the caller and does not require Actor solvency. -| Stop class | Meaning | -| --- | --- | -| `Empty` | no live eligible pre-cutoff entry remains after bounded stale cleanup | -| `Head` | canonical live pre-cutoff head is known | -| `WeightBlockedLiveHead` | the complete next discovery/service unit does not fit the meter | -| `FeeCollectionStalledLiveHead` | the runtime fee-collection path failed and the rolled-back live head remains retryable | -| `PassExhausted` | the bounded scan/attempt ceiling or same-block pass boundary ended this pass without invariant failure | -| `InvariantStalledLiveHead` | topology, ownership, Continuation, or transactional invariant prevents safe progress at a live head | +### 9.4 Minimal User apoptosis -```rust -enum IdleStarvationPhase { - Healthy, - Starving { consecutive_blocks: u32 }, - Alerted { consecutive_blocks: u32 }, -} -``` +Minimal User apoptosis is automatic process-state garbage collection with no custody effect. -A semantically admitted actor cannot stall FIFO merely by using the maximum plan. Complete attempt Weight is known before mutation, one maximum attempt fits `ActorServiceReserve`, and an admitted attempt reaches a named bounded result. A maximum User attempt MAY consume the whole actor-service budget and be the only attempt in that block; it remains paid bounded service, not structural starvation. +It occurs only for: -`InvariantStalledLiveHead` therefore denotes malformed canonical state or transactional topology, not an expensive valid actor. It preserves the head and performs no actor mutation. Actors defines no call-level quarantine, demotion, reticketing, skip, or forced cleanup because those paths would create a second service order and repair authority. Persistent repair is available only through Section 9.4 for a deployed lineage, or through a fresh canonical genesis before one exists. +- `TriggerAdmissionInsufficient` from one-shot User `AtTime` (§4.4); +- `CycleAdmissionInsufficient` before Opening (§5.2); or +- `CycleAdmissionInsufficient` before a Suspended User retry whose current maximum Action liability is unavailable (§7.5). -Fee-collection failure yields `FeeCollectionStalledLiveHead`, preserves the head, and retries only through later ordinary actor service. A starved actor-service pass is `WeightBlockedLiveHead`, `FeeCollectionStalledLiveHead`, or `InvariantStalledLiveHead` with no committed attempt. Starved passes saturating-increment `consecutive_blocks`; `PassExhausted` does not. Weight/pass deferral changes no actor state or event. +It MUST NOT occur after an Action effect or leave fee debt. Trigger and Pipeline funding never close a Running or Suspended Pipeline; only the bounded pre-invocation retry-liability rule may close a Suspended User Actor. -For every finite set of pre-cutoff tickets, recurring conforming reserve, finite stale churn, eventual placement capacity, conforming host interfaces, and no structural invariant failure imply eventual service in FIFO order. This liveness statement does not promise a fixed block latency or success of the authored economic task. +Minimal apoptosis performs Close (§9.3) without a Task, custody scan, fee reserve, or economic policy. Its protocol cleanup obligation is economically backed by the committed Actor Creation Fee (§7.2). -### 5.2 Observation Delivery +There is no `ActorFundingWait` and no automatic `Transfer(AllAvailable)`. -Broad subscriber pages derive only from `ObservationChange`. Runtime maintains bounded exact actor/feed and reverse ownership; one fanout unit addresses at most `ObservationPageSize` positions. `ObservationCrossing` instead derives sparse ordered fire/rearm membership from the same feed identity. Physical topology is implementation-owned and neither representation changes authored Trigger identity. +### 9.5 Control transitions and breaker -Total subscriptions are bounded by `MaxActiveActors`; each Actor owns at most one observation feed. Distinct subscribed feeds cannot exceed total subscriptions, and dirty obligations cannot exceed distinct subscribed feeds. No separate unbounded feed registry exists. +Semantic Contract replacement, deactivation, explicit run cancellation, close, and incompatible upgrade MUST cancel the open run before changed meaning becomes executable (§5.6). -Creation/activation installs, schedule replacement diffs, and deactivation/close removes subscriptions inside the owning transaction. Installing the first subscriber infers no historical revision and creates no dirty obligation. Publication while a feed has no subscribers allocates no baseline or dirty state. The first later accepted changed publication with subscribers sets the baseline and creates one dirty obligation. Removing the final subscriber deletes both. +Pause preserves Contract, run, snapshots, latch, clocks, failure state, and Trigger detector evolution, but removes ordinary Pipeline execution placement. Resume reconstructs the exact required placement (§8.2). A Trigger may therefore latch one next-Cycle request while the Actor is Paused, but no Step executes. -The generated certified-publisher inventory is the sole observation-publication authority. Each certified publisher owns the monotonically increasing revision sequence and exact previous/current scalar values for its feeds and calls `ObservationTransitionIngress::note_observation_transition(feed, transition)`. Actors validates but does not synthesize revisions or transition values. +Active creation or activation installs a new Active epoch with `Active/Idle`, no run, `pending_signal = false`, zero failure streak, empty funding accumulator, Contract-derived tracked assets, current Trigger runtime state, schedule clocks, terminal marker, topology, and exact state hold (§3.5, §4.1, §7.2). Activation preserves the Dormant identity nonce. Deactivation cancels any run and removes the complete Active epoch while preserving identity, locator/slot, nonce, custody, and persistent control clock (§5.6, §10.3). -Changed publication atomically maintains one highest accepted revision and one pending obligation per subscribed feed. Revision `0` or regression fails; equality is exact no-op; greater revision updates. Publication is O(1) and does not inspect subscriber groups, mutate actors, enqueue, evaluate, or execute. A path outside the certified inventory has no Actors observation effect. +Authorized Mutable control MAY repair a non-window stored terminal condition before scheduler or sweep commits Close. `WindowExpired` substitutes Close before the requested mutation (§9.2). -Crossing service preserves revision order per feed and deterministic feed rotation globally. One admitted search unit traverses the fixed radix depth at most once and either records one exact occupied threshold for later candidate service, records exhaustion, or completes the head transition; search never evaluates or activates an Actor. A recorded threshold is not searched again until its current source leaf is exhausted or removed. +Mutable control transitions are limited to one committed semantic mutation per Actor per block. Exact no-op returns before rate limiting. -One candidate unit owns a bounded prefix of exactly one source membership page. Before mutation it derives the admitted candidate count from the encoded page remainder, `ObservationPageSize`, remaining multidimensional Weight, and explicit transition/leaf/page/Actor caps. Zero candidates is a state-preserving deferral. The source page is loaded once, candidate authority is restricted to the snapshotted `(actor_id, generation, locator)` prefix, and no candidate may authorize reads or writes for another Actor. +While the global breaker is active: -Candidate preflight validates every snapshotted locator, canonical Trigger phase and installation revision, authored Crossing semantics, destination obligation capacity, activation branch, and terminal branch before cohort commit. Any structural failure rolls back that candidate unit, preserves all prior committed units, and records the first deterministic bounded worker fault. The unit never skips corruption, expands its prefix after admission, or converts one Actor's failure into authority over another. +- FIFO Step effects and ordinary automatic terminal close do not run; +- Mandatory minimal apoptosis (§9.4), explicit close, and bounded sweep cleanup MAY run because they invoke no economic Task; +- Bounded detector, wakeup, stale-cleanup, and fault work MAY continue; +- New Active creation and activation fail; +- Authorized control over existing Mutable Actors remains available; +- Explicit close and permissionless sweep of independently owned terminal state remain available; +- FIFO order and existing placement remain unchanged. -Commit applies phase movement before fire activation, groups identical destination obligations only as a storage optimization, compacts the source page once in deterministic snapshot order, writes each touched destination page at most once, and updates every moved locator exactly. Post-installation skips retain phase and obligation. A canonical phase change moves membership exactly once. Fire coalescing, new FIFO placement, and terminal close remain semantically distinct generated branches; rearm never activates. The cursor resumes at the first unprocessed source entry or advances exactly once after source exhaustion, including partial cohorts and closure-driven membership removal. +Permissionless sweep is bounded and closes only stored-state terminal reasons (§9.2). It never predicts Trigger or Pipeline affordability. Actor-targeting control and certified ingress may substitute Close only for `WindowExpired`; all other terminal reasons remain scheduler/sweep owned. Dormant activation with exhausted Cycle nonce closes the identity with `CycleNonceExhausted` rather than installing an unusable Active epoch. -Crossing Weight has disjoint generated owners for base probes, radix search, source-page I/O parameterized by admitted candidates, destination groups, canonical membership movement, activation classification, coalesced fire, placed fire, terminal close, rearm, skip, and transition completion. A component is charged only when its physical operation occurs. The count and both Weight dimensions MUST admit the complete selected unit before mutation; `MaxCrossingActorsPerBlock` is an independently enforced defense-in-depth ceiling over actually inspected candidates. +--- -Fanout runs before cutoff. One admitted unit reserves complete Weight, visits one bounded group, latches live actors, and ensures future placement before advancing durable progress. A pass snapshots one revision. Newer revisions update only latest revision and do not reset the current pass. Under recurring budget, eventual placement capacity, and finite subscription churn, each snapshotted pass completes, although the feed may remain dirty indefinitely. Completion means subscriber groups were visited, not actors executed. +## 10. Sovereign custody and identifiers -### 5.3 Address-Event Ingress +### 10.1 Sovereign-account derivation -Only a certified producer creates Actors AddressEvent semantics. The generated certified-producer inventory is the sole producer authority: it names every runtime movement path that claims Actors ingress and records one typed protocol, provenance, source availability, read-only preflight owner, consequence owner, atomic rollback owner, retry mapping, and Weight bound. +```text +User seed = Blake2_256(SCALE(ActorsPalletId, b"user", owner, owner_slot)) +System seed = Blake2_256(SCALE(ActorsPalletId, b"system", system_sovereign_id)) +account = HostSovereignAccountDeriver(seed) +``` -Actors Transfer, every SplitTransfer leg, and Mint are certified producers whenever their destination resolves to an Actors sovereign account. +Derivation MUST be total, deterministic, class-separated, and stable for every previously admitted custody identity. -| Protocol | Ordering | Atomicity owner | -| --- | --- | --- | -| `PostMovementNotify` | Read-only preflight, value movement, exactly one notify | Producer storage transaction | -| `BlockAtomicPostDispatch` | Read-only preflight, successful dispatch, exactly one post-dispatch notify | Block author/import state transaction; a notify rejection invalidates the candidate block | -| `XcmTransactionalPrecommit` | Read-only preflight, exactly one Actors precommit, consume and deposit the non-cloneable holding | Asset-transactor storage transaction | +### 10.2 User slots and System locators -`XcmTransactionalPrecommit` is the sole allowed pre-movement Actors mutation. It exists because `AssetsInHolding` is non-cloneable: successful deposit commits the precommit, while precommit or deposit failure restores every Actors effect, event, ledger entry, and the exact original holding. It is observationally equivalent at the transaction boundary to one successful movement plus one Actors consequence and MUST NOT be described as post-movement notification. +- User slots are owner-local. +- `0 < MaxOwnerSlots <= 255`. +- The reference profile exposes `0..=254`; `u8::MAX` is invalid. +- Default User creation selects the lowest free slot. +- Exact-slot creation requires that exact free slot. +- User close releases the slot. +- System creation allocates a unique locator. +- System close marks its locator vacant. +- Locator reuse creates a new Actor id but the same sovereign account. +- Actor ids and queue tickets never repeat. -Every protocol begins with literal read-only preflight covering lifecycle, funding, trigger, and required placement. Any later failure restores movement and every Actors effect at the named atomicity boundary. Certification is atomic, not advisory: after preflight, a certified movement MUST NOT fall back to balance-only when its complete Actors consequence cannot commit. +Sovereign custody at an unindexed derived account does not block exact reattachment. Reserved-account and live-collision errors remain distinct (§12.4). A previously registered vacant System locator remains reattachable even if later host policy classifies its derived account as reserved; the exception applies only to that exact locator. Custody identity survives host account-provider removal, although host dust/reaping policy may remove value independently. -A certified third-party movement to an Active actor MAY fail when its complete Actors consequence cannot commit. Recoverable queue/wakeup capacity or placement unavailability is Temporary. Monotonic ticket/index exhaustion, topology corruption, invalid provenance, and invariant failure are Permanent. Actors tasks preserve the classification through `TaskFailure`; non-Actors producers map it to their outer dispatch error. +### 10.3 Custody reattachment and recovery -A movement not named in the certified inventory is **balance-only**: it cannot latch readiness, update `funding_accumulated`, or emit `FundingAccumulated`, even when destination is an Actors sovereign account. It is not rejected by Actors scheduler pressure. No event scan, balance-diff scan, inbox, or implicit producer discovery is permitted. +Process lifetime and custody lifetime are independent. -A new balance-movement surface remains balance-only until the certified-producer inventory defines its typed protocol, preflight, consequence, rollback owner, failure classification, and Weight. +After Close (§9.3): -Additional rules: +- The sovereign account remains; +- Its balances and adapter-exposed positions remain host-ledger state; +- No Actors authority exists until reattachment. -- Absent or Dormant destination is balance-only. -- Zero or self/no-op movement creates no Actors ingress. -- Terminal handling follows Section 2.4 before funding/readiness processing. If notification closes the actor, credited value remains on the sovereign account. -- Funding acceptance follows Section 2.5. Untracked or policy-rejected credit is balance-only for funding but MAY independently match an AddressEvent trigger. -- Concrete source and typed provenance remain independent. -- Equal certified movements count separately for funding while readiness coalesces. -- Fee movement is outside the certified AddressEvent producer inventory. It commits only the ledger debit and FeeSink credit, never invokes Actors ingress, and remains independent from destination actor state and scheduler capacity. Downstream Fee Sink allocation follows its independently configured cadence. +A fresh User Actor reattaches the same custody iff owner and exact slot are equal to the closed Actor. A fresh System Actor reattaches the same custody iff it reuses the same vacant System locator. -### 5.4 Hooks, Breaker, and Reserved Actor Service +Reattachment inherits custody only. It does not inherit Contract, mutability, nonce, Trigger state, lifecycle, funding accumulator, run state, or guarantees. -`on_initialize` performs no Actors work and returns zero Actors Weight. +Recovery uses ordinary authored Contracts and Tasks (§6.6). Clients MAY automate exact-slot recreation, funding, recovery Contract generation, withdrawal, and re-close. Actors provides no direct recovery-transfer call. -`ActorOnIdleReserve` is an embedding guarantee: immediately before Actors `on_idle` in every conforming block, both remaining Weight dimensions are at least this value. Runtime MUST enforce the guarantee through block dispatch limits, mandatory-hook maxima, and hook order. Actors meters itself against `min(actual_remaining, ActorOnIdleReserve)` and runs before lower-priority idle consumers. +### 10.4 Protected minimum -`on_idle` order: +For User Actors and the fee-native asset: ```text -base --> bounded saturated-FIFO stale cleanup --> due temporal work --> observation fanout --> cutoff snapshot --> FIFO attempts / scheduler-owned terminal cleanup when the breaker is clear --> starvation update +protected_minimum = MinUserBalance ``` -`OnIdleBaseWeightUpper` covers entry, breaker read, fixed orchestration, cutoff snapshot, and maximum starvation-state branch. Temporal and observation worker bases live inside their own limits. FIFO discovery, consumption, attempt, final placement, and terminal cleanup live inside the admitted actor-service bound. +For other assets/classes: + +```text +protected_minimum = host asset minimum balance +``` -Insufficient remaining Weight is silent deferral and MUST preserve worker state without creating a fault. A deferred structural error MUST roll back its complete unit, retain at most one bounded typed current fault, and halt unchanged-mechanism retries for that obligation. The projection identifies the worker family, source or temporal key, available revision/cursor identity, and failure class. Resumption requires an authorized explicit clear after repair of the underlying invariant; clearing the record MUST NOT discard or advance the preserved obligation, and unchanged corruption deterministically faults again. No unbounded failure history is canonical state. +Current Action-fee reservation is also unavailable to Task debit (§7.5). ```text -ActorServiceReserve = - ActorOnIdleReserve - - OnIdleBaseWeightUpper - - SaturatedQueueCleanupWeightUpper - - WakeupWeightLimit - - ObservationFanoutWeightLimit +spendable_balance = balance - current_action_fee_reservation +preservable_balance = spendable_balance - protected_minimum ``` -Subtraction is checked component-wise. One maximum actor attempt including any reachable terminal cleanup, or one standalone terminal cleanup, MUST fit. `MaxExecutionsPerBlock` is only a count ceiling. +All subtraction is saturating only where explicitly shown. Every authored debit, including `AllAvailable`, uses preservable capacity. No lifecycle branch grants source-exhaustion privilege. -While the global breaker is active: +--- + +## 11. Host effects and adapters + +### 11.1 Canonical Task-effect ownership -- FIFO attempts and scheduler-owned automatic terminal cleanup do not run; -- Bounded stale queue, temporal, and observation housekeeping continues; -- Creation, System locator reuse, and activation fail with `GlobalCircuitBreakerActive`; -- Otherwise authorized control over existing Mutable actors remains available subject to control-mutation limits; -- Manual latch, address/observation ingress, actor-targeting or ingress-triggered expiry close, explicit Mutable close, permissionless sweep, breaker control, and active-limit control remain available; -- No head is demoted or reticketed because FIFO discovery is not entered; -- Starvation accounting is frozen. +Each effectful Actor Task invokes the same canonical host economic mechanism as its external equivalent. -Clearing the breaker resumes ordinary FIFO service without changing preexisting order. +Equivalence means identical: -`permissionless_sweep` is O(1). `permissionless_sweep_many` is O(K), `K <= MaxSweepBatch`, and executes in one control transaction. It classifies Active actors only: terminal actors count as `closed`, nonterminal actors as `alive`, and absent or Dormant ids as `missing`. A classification or cleanup error rolls back the batch and its event; a malformed Active actor is never counted as `missing`. +- State-transition owner; +- Atomicity boundary; +- Synchronous consequences; +- Typed failure classification; +- Production effect Weight. -Reaching `MaxIdleStarvationBlocks` enters `Alerted` and emits `IdleStarvationDetected` once. Each later starved pass updates the stored count without another detection event. A nonstarved actor-service pass returns to `Healthy` and emits `IdleStarvationRecovered` once if the prior state was `Alerted`. While the breaker is active no actor-service pass occurs and the count is unchanged. Starvation changes no breaker, priority, order, or execution authority. +Actors MAY use narrow typed ports and need not construct a `RuntimeCall`. A cheaper shadow mechanism is forbidden. -## 6. Host Adapters and Weight +Actors owns orchestration control only. The host mechanism owns Task-effect Weight and task-native fees. -### 6.1 Failure Contract +### 11.2 Adapter interfaces ```rust -enum RetryClass { Permanent, Temporary } +enum RetryClass { Temporary, Permanent } struct TaskFailure { error: DispatchError, retry: RetryClass, } -``` - -Retryability never derives from strings, module indices, broad token errors, or raw coincidence. Unknown is Permanent. - -Temporary includes dynamic slippage, authored-cap insufficiency against a current quote, reference freshness/deviation, liquidity/output/ratio movement, recipient deposit unavailability, and recoverable queue/wakeup capacity or placement unavailability reached through paired ingress. -Permanent includes unsupported/malformed configuration, invalid provenance, missing static capability, monotonic ticket/index namespace exhaustion, topology corruption, and invariant failure. - -`FundingUnavailable` is a resolution outcome, not `TaskFailure`. `FeeCollector` failure is an enclosing-attempt infrastructure failure under Section 4.3, not a `TaskFailure` or `RetryClass`. Successful adapters remain within declared effects and debit surfaces. - -### 6.2 Interfaces - -```rust -struct ExecutionContext<'a, A> { actor: &'a A, actor_type: ActorType } -struct IngressFailure { error: DispatchError, retry: RetryClass } +struct ExecutionContext<'a, A> { + actor: &'a A, + actor_type: ActorType, +} trait AssetOps { fn transfer(from: &A, to: &A, asset: I, amount: B) -> Result<(), TaskFailure>; @@ -1249,580 +1523,480 @@ trait AssetOps { fn minimum_balance(asset: I) -> B; fn preflight_transfer(from: &A, to: &A, asset: I, amount: B) -> Result<(), TaskFailure>; } + trait DexOps { - fn swap_exact_in(context: ExecutionContext<'_, A>, asset_in: I, asset_out: I, - amount_in: B, tolerance: Perbill) -> Result; - fn swap_exact_out(context: ExecutionContext<'_, A>, asset_in: I, asset_out: I, - amount_out: B, authored_input_cap: B, tolerance: Perbill) -> Result; + fn swap_exact_in(context: ExecutionContext<'_, A>, asset_in: I, asset_out: I, amount_in: B, tolerance: Perbill) -> Result; + fn swap_exact_out(context: ExecutionContext<'_, A>, asset_in: I, asset_out: I, amount_out: B, authored_input_cap: B, tolerance: Perbill) -> Result; +} + +trait LiquidityOps { + fn lp_assets(lp: I) -> Option<(I, I)>; + fn add_liquidity(who: &A, a: I, b: I, max_a: B, max_b: B, min_lp: B) -> Result<(B, B, B), TaskFailure>; + fn remove_liquidity(who: &A, lp: I, a: I, b: I, amount: B, min_a: B, min_b: B) -> Result<(B, B), TaskFailure>; + fn donate_liquidity(who: &A, a: I, b: I, max_a: B, max_b: B, max_ratio_error: Perbill) -> Result<(B, B), TaskFailure>; } + trait StakingOps { fn stake(who: &A, asset: I, amount: B) -> Result<(), TaskFailure>; fn unstake(who: &A, asset: I, shares: B) -> Result<(), TaskFailure>; fn share_balance(who: &A, asset: I) -> B; fn share_asset(asset: I) -> Option; } -trait LiquidityOps { - fn lp_assets(lp: I) -> Option<(I, I)>; - fn add_liquidity(who: &A, a: I, b: I, max_a: B, max_b: B, min_lp: B) - -> Result<(B, B, B), TaskFailure>; - fn remove_liquidity(who: &A, lp: I, a: I, b: I, lp_amount: B, min_a: B, min_b: B) - -> Result<(B, B), TaskFailure>; - fn donate_liquidity(who: &A, a: I, b: I, max_a: B, max_b: B, - max_ratio_error: Perbill) -> Result<(B, B), TaskFailure>; -} + trait FeeCollector { fn collect_fee(payer: &A, sink: &A, fee_asset: I, amount: B) -> DispatchResult; } -enum ScalarObservationState { - Unavailable, Uninitialized, Fresh { value: u128, observed_at: N }, Stale, -} -trait ObservationProvider { - fn observe(feed: &F, now: N, max_age: u32) -> ScalarObservationState; -} -trait FundingAuthority { - fn permits(actor_id: ActorId, owner: &A, source: Option<&A>, - provenance: Option<&FundingProvenance>) -> bool; -} -trait SovereignAccountPolicy { fn is_reserved(account: &A) -> bool; } -trait AddressEventIngress { - fn preflight(event: &AddressEvent) -> Result<(), IngressFailure>; - fn notify(event: &AddressEvent) -> Result<(), IngressFailure>; -} -struct ObservationTransition { - revision: ObservationRevision, previous: Option, current: u128, -} -trait ObservationTransitionIngress { - fn note_observation_transition(feed: F, transition: ObservationTransition) -> DispatchResult; -} ``` -`AssetOps.balance` is the pre-reservation ordinary balance surface. Mint is System-only; transfer preflight models withdrawal, recipient deposit, and any certified destination ingress consequence. Actors owns source preservation. `AssetOps::preflight_transfer` and `AssetOps::transfer` MUST permit an admitted terminal custody drain to consume the source minimum and MUST NOT impose a hidden preservation floor. Staking-share and ordered LP bindings are admitted Actor Contract identity, MUST NOT be reinterpreted in place, and change only through Section 9.4. +Missing mutation capability fails closed. -`DexOps` is an encapsulated quote-and-execute boundary. It obtains the current executable quote internally before mutation, applies Section 3.4 tolerance/cap formulas and Section 6.3 System guard, executes through Router, validates actual returned amounts, and returns actual output/input. No caller-visible quote method or stale caller-supplied quote exists. +### 11.3 Failure classification -Liquidity success uses positive amounts within supplied caps/minima and no undeclared debit. For DonateLiquidity, Actors derives `max_b`; `LiquidityOps` may choose actual amounts within both caps but MUST NOT derive or exceed another cap. +Retryability MUST NOT derive from strings, module indexes, or broad token errors. -Valid `Fresh` requires `observed_at <= now` and age `<= max_age`; invalid Fresh is Permanent. Missing observation is `Unavailable`; missing funding authority denies. Every missing mutation capability fails closed. +Temporary includes dynamic slippage, current quote/cap insufficiency, stale reference, liquidity movement, recipient deposit unavailability, and recoverable placement capacity. -### 6.3 System Swap Guard and Router Boundary +Permanent includes malformed configuration, invalid provenance, missing static capability, monotonic namespace exhaustion, topology corruption, and invariant failure. -Actors supplies resolved intent, class, finite authored/custody caps, tolerance, and failure policy. `DexOps` owns current quote acquisition, quote protection, the Actors-specific System reference guard, Router invocation, and actual validation. Router owns route discovery, path validation, Router fees/protection, Oracle publication, execution, and route outcome. Actors neither defines nor alters route semantics. +Unknown downstream error is Permanent. -For User context, `DexOps` MUST NOT apply System reference parameters. For System context, before mutation and again against returned actual amounts when they may differ from executable quote, it requires fresh nonzero directed reference values and enforces: +`FundingUnavailable` is amount/action admission outcome, not `TaskFailure` (§6.3, §7.5). -```text -exec_out * ref_in * Perbill::ACCURACY - >= -(Perbill::ACCURACY - SystemSwapMaxReferenceDeviation.deconstruct()) - * ref_out - * exec_in -``` - -All products use widened checked arithmetic. Better-than-reference execution passes. Missing/stale/zero reference or worse execution beyond the bound is Temporary and rolls back the task layer. Router does not own or reinterpret the System guard. - -### 6.4 Weight and Fee Derivation - -One generated `WeightInfo` owns calls, Task classes, Predicate/amount evaluation, fee collection, ingress, scheduler/observation units, probes, orchestration, Continuation, finalization, events, and cleanup. +### 11.4 Special rules -Task Weight covers internal quote, tolerance arithmetic, System guard, Router/adapter work, paired ingress, rollback, actual validation, and success event. Transfer Weight upper-bounds ordinary preservation and the source-exhausting terminal custody-drain branch. A swap task bound covers every Router Weight class reachable through its adapter input. +#### Swap -Step evaluation Weight covers every Predicate, amount preparation, fee collection, and non-task outcome. Attempt Weight adds probes, opening snapshots, finalization, future placement or close, and cleanup. Composition overflow is `AdmissionBoundOverflow`. Simulation and fee conversion use the same authority. +`DexOps` obtains a current executable quote internally, applies authored cap/tolerance, invokes the canonical Router, validates actual amounts, and returns typed failure. Actors exposes no independent quote surface. -Current bounds are derived on demand: +For `SwapOut`: ```text -cycle_weight_upper = - derive_weight(actor_class, contract_steps, current Weight/adapter bindings) +capacity_cap = preservable input balance -cycle_fee_upper = - derive_fee(actor_class, contract_steps, current WeightToFee bindings) - -suffix_weight_upper(cursor) = - derive_weight(actor_class, contract_steps[cursor..], current bindings) +authored_cap = + capacity_cap for LiveQuote + min(capacity_cap, absolute_cap) for Absolute -suffix_fee_upper(cursor) = - derive_fee(actor_class, contract_steps[cursor..], current bindings) +effective_max_in = min(authored_cap, quote + ceil(quote * tolerance)) ``` -No attempt may admit, reserve, charge, simulate, or report from stored derived Weight or fee state. - -A generated Weight or adapter change that would make a previously admitted semantic Weight class exceed its owning execution envelope MUST preserve that class's admission or apply Section 9.4 before ordinary execution resumes. Compatibility is determined from the complete finite semantic Weight-class domain, not by unbounded actor iteration. Actors defines no permanent generic cache-revalidation state. +Zero or insufficient cap is `FundingUnavailable` or Temporary according to whether dispatch was invoked (§6.3, §11.3). `SwapIn` requires positive actual output and output no worse than the authored tolerance relative to its current executable quote; 100% tolerance never permits zero output. -`WeightToFee`, `MinUserBalance`, and fee changes induced by an admission-compatible `WeightInfo` update are economic-policy changes. They require no actor iteration and apply immediately to every User actor, including Immutable actors, unless a Section 9.4 migration defines a grace, subsidy, or alternate disposition. Any resulting close transitions with `BalanceExhausted` or `FeeBudgetExhausted`, including mass economic apoptosis, are canonical. System actors remain Actors-fee-exempt. +#### System swap guard -## 7. Calls and Runtime APIs - -### 7.1 Calls +User swaps have no Actors-specific reference guard. System swaps require fresh nonzero directed reference values and MUST satisfy, before mutation and against returned actual amounts when they differ from quote: ```text -create_user_actor create_user_actor_at_slot -create_system_actor create_system_actor_at_sovereign_id -activate_actor / deactivate_actor pause_actor / resume_actor -manual_trigger close_actor -update_contract cancel_continuation -set_global_circuit_breaker set_active_actor_limit -permissionless_sweep permissionless_sweep_many -clear_crossing_worker_fault clear_observation_fanout_worker_fault -clear_wakeup_worker_fault +exec_out * ref_in * Perbill::ACCURACY + >= +(Perbill::ACCURACY - max_deviation) * ref_out * exec_in ``` -Authorization: - -| Call group | Required origin | -| --- | --- | -| User creation | signed caller, who becomes owner | -| System creation, System locator reuse, active-limit control | `SystemOrigin` | -| breaker and materialization-fault repair control | `GlobalBreakerOrigin` | -| User actor control | signed owner | -| System actor control | signed owner or `SystemOrigin` | -| sweep | any signed origin | - -`manual_trigger` requires Active, unpaused, nonexpired state and an authored Manual source. User Immutable MAY use it; System Immutable cannot author Manual. It sets `pending_signal` only `false -> true`, atomically ensures one future path, and emits `ManualTriggerSet` only for that transition. A previously latched actor with valid placement changes nothing. - -When Suspended, Manual only latches readiness for the next logical cycle. It does not cancel, replace, accelerate, duplicate, or retarget the current retry path. - -Actor-targeting control obtains the Section 4.1 classification before the control-mutation rate check. Complete Contract equality returns before expiry substitution, rate limiting, cancellation, clock mutation, placement reconstruction, writes, fees, or events. A non-equal replacement then applies expiry substitution and typed classification errors before mutation. - -Canonical-equality pause/resume and unchanged breaker or active-limit values also return before mutation or event. +All products use widened checked arithmetic. Missing/stale/zero reference or excessive negative deviation is Temporary. The Router does not own or reinterpret this guard. -Calls that may close include cleanup Weight. +#### Liquidity -Active creation and activation respect `ActiveActorLimit`; all creation respects `MaxActorIdentities`; fresh System locator creation respects `MaxSystemSovereigns`. +Ordered LP identity is validated at admission and execution. Add/Donate actual debits MUST remain within supplied caps. RemoveLiquidity MUST debit the exact resolved LP amount. -User Active creation and User activation require the prospective/current sovereign fee-native balance to cover: +For `DonateLiquidity`, Actors derives `max_b = preservable_balance(asset_b)`. Zero `max_b` is `FundingUnavailable`; the adapter MUST NOT invent a larger cap. -```text -MinUserBalance + attempt_fee_envelope(plan, 0, User).total -``` +#### Staking -before creation fee or Active state commits. Failure returns `InsufficientBalance`. The unfunded lifecycle is `create Dormant -> fund deterministic sovereign account -> activate`. +Staking-share identity is admitted and stable. `Unstake(AllAvailable)` resolves to the full current share balance. Transferable staking receipts/NFTs, when provided by the host, remain ordinary custody (§10.3). -User Dormant and Active creation charge `ActorCreationFee` to the signed caller/owner. +#### Certified AddressEvent ingress -Auto-close `Some(t)` requires: +Only generated certified producers create AddressEvent semantics (§4.7). Permitted protocol shapes are: -```text -1 <= t - cycle_nonce <= MaxAutoCloseNonceHorizon -``` +| Protocol | Ordering and atomicity owner | +| --- | --- | +| `PostMovementNotify` | read-only preflight, movement, one notify; producer storage transaction | +| `BlockAtomicPostDispatch` | read-only preflight, successful dispatch, one notify; block/import state transaction | +| `XcmTransactionalPrecommit` | read-only preflight, Actors precommit, consume/deposit holding; asset-transactor storage transaction | -Auto-close is changed only through complete Mutable Contract replacement; there is no field-specific setter or increment call. +Every certified protocol defines: -Active-limit update requires nonzero: +- Source/provenance; +- Read-only preflight; +- Atomicity owner; +- Actors consequence owner; +- Failure mapping; +- Complete Weight. -```text -ActiveActorCount <= limit <= min(MaxActiveActors, MaxQueueLength) -``` +Uncertified movement is balance-only. Absent or Dormant destination is balance-only. Zero and self/no-op movement create no Actors ingress. Terminal substitution occurs before funding/readiness consequence (§9.2). Fee collection is never certified AddressEvent ingress. -System account derivation is a pure helper. No direct recovery-transfer call or close-time refund exists. Custody recovery uses fresh exact User-slot or vacant System-locator reattachment followed by a Section 3.4 terminal custody drain. +#### Observation publication -For User custody recovery after close: +Only generated certified publishers create observation revisions. Publication is `O(1)` and records the exact previous/current transition and monotonic revision. Deferred matching belongs to detector workers (§8.4). -1. The same signed owner calls `create_user_actor_at_slot` for the released exact slot, creating a fresh Mutable Dormant or Active actor; -2. The deterministic sovereign account is funded as required by ordinary Active admission; -3. A terminal custody drain transfers one named asset and closes the fresh actor; -4. The owner MAY repeat the sequence for other known assets. +--- -Recovery of a non-fee asset requires prior funding of the same deterministic sovereign account with sufficient `FeeNativeAssetId` for Active admission and the current attempt. Actors provides no sponsored recovery path. +## 12. Calls, events, APIs, and errors -For System custody recovery, `SystemOrigin` reuses the same vacant locator and installs a fresh Mutable terminal custody-drain actor. Reattachment never restores the closed actor's semantics. +### 12.1 Calls and authorization -### 7.2 Simulation +Canonical calls: -```rust -enum SimulationMode { FreshCurrentPlan, CurrentContinuation } -enum AttemptDisposition { Completed, Failed, Suspended, Closed(CloseReason) } -enum StepOutcome { - Executed, Stopped, Skipped(StepSkippedReason), FundingUnavailable, - Failed(TaskFailure), -} -struct SimulationStepRecord { step_index: u32, outcome: StepOutcome } -enum SimulationError { - TransactionDepthExceeded, Classification(ActorClassificationError), ActorNotFound, - TypeMismatch, MutabilityMismatch, ContractMismatch, ModeCycleStateMismatch, - GlobalCircuitBreaker, Paused, NotReady, FeeCollectionFailed, -} -struct SimulationResult { - status: AttemptDisposition, cycle_nonce: u64, start_cursor: u32, - continuation_cursor: Option, unsuccessful_attempts_at_cursor: Option, - cumulative_outcomes: OutcomeTotals, - steps: BoundedVec, -} -trait ActorSimulationApi { - fn simulate_current_contract( - actor_id: ActorId, expected_type: ActorType, expected_mutability: Mutability, - expected_contract: Contract, mode: SimulationMode, - ) -> Result; -} +```text +create_user_actor +create_user_actor_at_slot +create_system_actor +create_system_actor_at_sovereign_id +activate_actor +deactivate_actor +pause_actor +resume_actor +manual_trigger +update_contract +cancel_run +close_actor +set_global_circuit_breaker +set_active_actor_limit +permissionless_sweep +permissionless_sweep_many +clear_crossing_worker_fault +clear_observation_fanout_worker_fault +clear_wakeup_worker_fault +actor_prepass ``` -One call simulates exactly one fresh or Continuation attempt; `steps.len() <= contract_steps.len() - start_cursor <= MaxContractSteps`. - -For an actor that passes Section 4.1 classification, mode ownership is exact: - -| `SimulationMode` | Idle | Suspended | -| --- | --- | --- | -| `FreshCurrentPlan` | simulate one fresh attempt | `ModeCycleStateMismatch` | -| `CurrentContinuation` | `ModeCycleStateMismatch` | simulate one Continuation attempt | - -Simulation resolves presence, invokes Section 4.1 once, applies expected type/mutability/contract checks and the mode matrix, then projects the result in this order: - -1. A classification error maps exactly under Section 9.2. -2. Expected type, mutability, contract, or mode mismatch returns its interface error. -3. `GlobalCircuitBreaker` returns `SimulationError::GlobalCircuitBreaker`, including when `terminal_reason` exists. -4. When the breaker is clear and `terminal_reason` exists, return `Closed(reason)` with no step records. -5. `Paused` returns `SimulationError::Paused`. -6. `WaitingRetry`, `WaitingTemporal`, or `WaitingSignal` returns `SimulationError::NotReady`. -7. `Ready` with no terminal reason simulates one attempt. A fee-collection failure during that rollback-only attempt returns `SimulationError::FeeCollectionFailed` and no `SimulationResult`. - -The canonical Step evaluator produces one `StepOutcome` before the authored error policy is interpreted. Production events/counters and rollback-only simulation consume that same value; simulation adds only the bounded `SimulationStepRecord` wrapper. +Authorization: -| Step transition | `StepOutcome` | +| Call | Origin | | --- | --- | -| successful effectful task | `Executed` | -| successful `StopCycle` | `Stopped` | -| precondition or zero resolution that advances | `Skipped(reason)` | -| `FundingUnavailable`, whether it advances, suspends, or reaches a bound | `FundingUnavailable` | -| task failure, whether it advances, suspends, or terminates | `Failed(TaskFailure { error, retry })` | +| User creation | signed creator | +| System creation/locator reuse/active limit | `SystemOrigin` | +| breaker/fault repair | configured control origin | +| User control | signed owner, subject to mutability (§9.1) | +| System control | signed owner or `SystemOrigin`, subject to mutability (§9.1) | +| sweep | any signed origin | +| prepass | mandatory unsigned inherent origin | -A failed outcome preserves the concrete `DispatchError` cause independently from `RetryClass`; the policy interpreter may use retry disposition but MUST NOT erase the cause. Fee-collection failure remains an enclosing-attempt infrastructure error and produces no committed Step outcome. +Creation charges ordinary transaction fee, Actor Creation Fee, and state-hold delta (§7.2). -The shared `AttemptDisposition` distinguishes completion, suspension, terminal failure, and close for production finalization and simulation status. The simulated `cumulative_outcomes` is the exact production finalization value, including `failed_steps += 1` for a Temporary task failure whose disposition is `Suspended`; it is not reconstructed from trace records. +`manual_trigger` follows Manual semantics (§4.7, §7.3). -Attempt disposition mapping: +`close_actor` is signed Mutable User control and follows Close (§9.3). -- `Completed`: successful cycle terminal and actor remains open. -- `Failed`: terminal `CycleResult::Failed` without suspension or close. -- `Suspended`: attempt would persist Continuation. -- `Closed(reason)`: current classification or attempt finalization would close with `reason`. +### 12.2 Events and ordering -Result fields: +The ordered event ABI is normative: -| Case | `cycle_nonce` | `start_cursor` | -| --- | --- | --- | -| fresh attempt | `stored_cycle_nonce + 1` | `0` | -| Continuation attempt | stored cycle nonce | stored cursor | -| pre-attempt close | stored cycle nonce | stored cursor or `0` | +```rust +enum Event { + ActorCreated { actor_id: ActorId, owner: AccountId, actor_class: ActorClass, mutability: Mutability, sovereign_account: AccountId, initial_lifecycle: InitialLifecycle }, + ActorActivated { actor_id: ActorId }, + ActorDeactivated { actor_id: ActorId }, + ActorPaused { actor_id: ActorId }, + ActorResumed { actor_id: ActorId }, + ActorClosed { actor_id: ActorId, reason: CloseReason }, + CycleStarted { actor_id: ActorId, cycle_nonce: u64 }, + CycleSummary { actor_id: ActorId, cycle_nonce: u64, result: CycleResult, outcomes: OutcomeTotals }, + CycleSuspended { actor_id: ActorId, cycle_nonce: u64, cursor: u32, reason: SuspensionReason, cumulative_outcomes: OutcomeTotals }, + CycleContinued { actor_id: ActorId, cycle_nonce: u64, cursor: u32 }, + CycleCancelled { actor_id: ActorId, cycle_nonce: u64, reason: CancellationReason }, + CycleStopped { actor_id: ActorId, cycle_nonce: u64, step_index: u32 }, + StepSkipped { actor_id: ActorId, cycle_nonce: u64, step_index: u32, reason: StepSkippedReason }, + StepFailed { actor_id: ActorId, cycle_nonce: u64, step_index: u32, retry_class: RetryClass, error: DispatchError }, + TransferExecuted { actor_id: ActorId, cycle_nonce: u64, step_index: u32, asset: AssetId, amount: Balance, to: AccountId }, + SplitTransferExecuted { actor_id: ActorId, cycle_nonce: u64, step_index: u32, asset: AssetId, total: Balance, distributed: Balance, retained: Balance, legs: u32, effective_legs: u32 }, + SwapExecuted { actor_id: ActorId, cycle_nonce: u64, step_index: u32, asset_in: AssetId, asset_out: AssetId, amount_in: Balance, amount_out: Balance }, + BurnExecuted { actor_id: ActorId, cycle_nonce: u64, step_index: u32, asset: AssetId, amount: Balance }, + MintExecuted { actor_id: ActorId, cycle_nonce: u64, step_index: u32, asset: AssetId, amount: Balance }, + StakeExecuted { actor_id: ActorId, cycle_nonce: u64, step_index: u32, asset: AssetId, amount: Balance }, + UnstakeExecuted { actor_id: ActorId, cycle_nonce: u64, step_index: u32, asset: AssetId, shares: Balance }, + LiquidityDonated { actor_id: ActorId, cycle_nonce: u64, step_index: u32, asset_a: AssetId, asset_b: AssetId, max_amount_a: Balance, max_amount_b: Balance, amount_a: Balance, amount_b: Balance }, + LiquidityAdded { actor_id: ActorId, cycle_nonce: u64, step_index: u32, asset_a: AssetId, asset_b: AssetId, amount_a: Balance, amount_b: Balance, lp_minted: Balance }, + LiquidityRemoved { actor_id: ActorId, cycle_nonce: u64, step_index: u32, lp_asset: AssetId, lp_amount: Balance, asset_a: AssetId, asset_b: AssetId, amount_a: Balance, amount_b: Balance }, + ContractUpdated { actor_id: ActorId }, + ActiveActorLimitSet { old_limit: u32, new_limit: u32 }, + GlobalCircuitBreakerSet { paused: bool }, + ActorFaultRecorded { fault_id: FaultId, kind: ActorFaultKind, first_recorded_block: BlockNumber, context: FaultContext }, + CrossingWorkerFaultCleared { feed: ObservationFeedId, revision: Option, class: CrossingWorkerFaultClass }, + ObservationFanoutWorkerFaultCleared { feed: ObservationFeedId, revision: ObservationRevision, subscriber_page: Option, class: CrossingWorkerFaultClass }, + WakeupWorkerFaultCleared { key: WakeupKey, page: WakeupPageId, class: CrossingWorkerFaultClass }, + ManualTriggerSet { actor_id: ActorId }, + TriggerOccurrenceProcessed { actor_id: ActorId, trigger_family: TriggerFamily, fee: Balance }, + PipelineFeeCharged { actor_id: ActorId, fee: Balance }, + ActionFeeCharged { actor_id: ActorId, cycle_nonce: u64, step_index: u32, actual_effect_weight: Weight, fee: Balance }, + FundingAccumulated { actor_id: ActorId, asset: AssetId, added: Balance, accumulated: Balance }, + SweepBatchProcessed { requested: u32, closed: u32, alive: u32, missing: u32 }, + IdleStarvationDetected { consecutive_blocks: u32 }, + IdleStarvationRecovered { consecutive_blocks: u32 }, +} +``` -`continuation_cursor` and `unsuccessful_attempts_at_cursor` are present iff the simulated result is `Suspended`. `cumulative_outcomes` and `steps` describe the simulated transition. A pre-attempt close has no step records. +Ordering: -Error precedence: +1. Useful Trigger fee and latch commit before `TriggerOccurrenceProcessed` (§4.2, §7.3). +2. Opening emits `PipelineFeeCharged`, then `CycleStarted` (§5.2, §7.4). +3. A Step event precedes any Cycle boundary event caused by that Step (§6.5). +4. Cancellation emits `CycleCancelled`, then `CycleSummary(Cancelled)` (§5.6). +5. A closing Cycle boundary emits `CycleSummary`, then `ActorClosed` (§9.3); pure Idle close emits only `ActorClosed`. +6. `ActionFeeCharged` is the final receipt of a committed Action-bearing Attempt (§7.5). +7. Fee-collection or enclosing transaction failure emits no rolled-back event. +8. Zero-Step Opening emits `CycleStarted`, then `CycleSummary(Completed)`, then optional `ActorClosed` (§5.5). +9. Redundant latched activity emits no Trigger event (§4.3). +10. Active creation emits `ActorCreated` only; Dormant-to-Active emits `ActorActivated`. Contract replacement or deactivation emits cancellation events first when a run exists, then `ContractUpdated` or `ActorDeactivated` (§5.6, §9.5). -```text -TransactionDepthExceeded -ActorNotFound -ActorInvariant -ContinuationInvariant -ComputationOverflow -TypeMismatch -MutabilityMismatch -ContractMismatch -ModeCycleStateMismatch -GlobalCircuitBreaker -Paused -NotReady -FeeCollectionFailed -``` +Exact fields and discriminants come from metadata. -`ActorNotFound` includes a valid Dormant identity because no Active Actor Contract exists. Partial Active partitions return `ActorInvariant`; malformed Continuation returns `ContinuationInvariant`. Old `expected_contract` after semantic replacement returns `ContractMismatch`. Current contract with `CurrentContinuation` after cancellation returns `ModeCycleStateMismatch`. +### 12.3 Runtime APIs -Simulation executes the production attempt path in rollback-only storage, uses the shared predicate, amount, fee, task, `StepOutcome`, policy interpretation, counters, and `AttemptDisposition`, and persists no state/event. Simulation-specific state is limited to rollback/non-persistence, bounded trace records, explicit transaction-depth failure at the rollback boundary, and requested mode checks. `FeeCollectionFailed` is interface-local and is not an authored step failure. +```rust +enum TriggerFamily { Manual, AddressEvent, ObservationChange, ObservationCrossing, AtTime, Cadenced } -### 7.3 Eligibility +enum RunPhase { + WaitingNextBlock { not_before: BlockNumber }, + WaitingRetry { not_before: BlockNumber }, + WaitingCadenceTick { due_tick: u64 }, + WaitingSignal, + Paused, + Breaker, + Ready, +} -```rust enum ActorEligibility { NotRegistered, Dormant, - Active(ActorClassification), -} -#[api_version(4)] -trait ActorEligibilityApi { - fn actor_eligibility(actor_id: ActorId) - -> Result, ActorClassificationError>; - fn materialization_faults() -> ( - Option>, - Option>, - Option>, - ); - fn crossing_capacity(feed: FeedId) - -> (user_cap: u32, total_cap: u32, user_count: u32, total_count: u32); - fn trigger_state_bond(trigger: Trigger) -> Balance; + Active { terminal_reason: Option, phase: RunPhase }, } -``` - -The Active Trigger projection is semantic rather than physical. Crossing includes authored direction/thresholds, canonical `phase`, canonical `installed_at_revision`, pending revision count, current processing revision, latch, placement, and eligibility; it exposes no radix key, page, offset, generation, or cursor geometry. `materialization_faults` returns only the three bounded current typed faults and no history or raw topology. `crossing_capacity` returns runtime policy plus exact semantic User/total membership counts for one feed; it does not expose occupancy by leaf or page. `trigger_state_bond` returns the prospective refundable User bond from the same exhaustive runtime policy consumed by lifecycle dispatch, without consulting physical detector occupancy. -Eligibility resolves absent and valid Dormant ids before invoking Section 4.1. For an Active actor, a classification error maps exactly under Section 9.2 and success returns the canonical `ActorClassification` without stripping terminal reason or execution-phase payloads. `WaitingRetry(block)` and `WaitingTemporal(block)` retain their exact block; no parallel next-block field exists. +struct TriggerFeeBreakdown { + trigger_family: TriggerFamily, + trigger_fee: Balance, +} -Simulation evaluates one attempt at the current block. Eligibility exposes current classification. Neither predicts a future signal. +struct PipelineFeeBreakdown { + pipeline_machine_fee: Balance, +} -## 8. Events and Ordering +struct ActionFeeBreakdown { + maximum_effect_fee: Balance, + actual_effect_fee: Option, +} -```rust -ActorCreated { actor_id: ActorId, owner: AccountId, actor_class: ActorClass, mutability: Mutability, sovereign_account: AccountId, initial_lifecycle: InitialLifecycle } -ActorActivated { actor_id: ActorId } -ActorDeactivated { actor_id: ActorId } -ActorPaused { actor_id: ActorId } -ActorResumed { actor_id: ActorId } -ActorClosed { actor_id: ActorId, reason: CloseReason } - -CycleStarted { actor_id: ActorId, cycle_nonce: u64 } -CycleSummary { actor_id: ActorId, cycle_nonce: u64, result: CycleResult, outcomes: OutcomeTotals } -CycleSuspended { actor_id: ActorId, cycle_nonce: u64, cursor: u32, reason: SuspensionReason, cumulative_outcomes: OutcomeTotals } -CycleContinued { actor_id: ActorId, cycle_nonce: u64, cursor: u32 } -CycleCancelled { actor_id: ActorId, cycle_nonce: u64, reason: CancellationReason } -CycleStopped { actor_id: ActorId, cycle_nonce: u64, step_index: u32 } - -StepSkipped { actor_id: ActorId, cycle_nonce: u64, step_index: u32, reason: StepSkippedReason } -StepFailed { actor_id: ActorId, cycle_nonce: u64, step_index: u32, retry_class: RetryClass, error: DispatchError } - -TransferExecuted { actor_id: ActorId, cycle_nonce: u64, step_index: u32, asset: AssetId, amount: Balance, to: AccountId } -SplitTransferExecuted { actor_id: ActorId, cycle_nonce: u64, step_index: u32, asset: AssetId, total: Balance, distributed: Balance, retained: Balance, legs: u32, effective_legs: u32 } -SwapExecuted { actor_id: ActorId, cycle_nonce: u64, step_index: u32, asset_in: AssetId, asset_out: AssetId, amount_in: Balance, amount_out: Balance } -BurnExecuted { actor_id: ActorId, cycle_nonce: u64, step_index: u32, asset: AssetId, amount: Balance } -MintExecuted { actor_id: ActorId, cycle_nonce: u64, step_index: u32, asset: AssetId, amount: Balance } -StakeExecuted { actor_id: ActorId, cycle_nonce: u64, step_index: u32, asset: AssetId, amount: Balance } -UnstakeExecuted { actor_id: ActorId, cycle_nonce: u64, step_index: u32, asset: AssetId, shares: Balance } -LiquidityDonated { actor_id: ActorId, cycle_nonce: u64, step_index: u32, asset_a: AssetId, asset_b: AssetId, max_amount_a: Balance, max_amount_b: Balance, amount_a: Balance, amount_b: Balance } -LiquidityAdded { actor_id: ActorId, cycle_nonce: u64, step_index: u32, asset_a: AssetId, asset_b: AssetId, amount_a: Balance, amount_b: Balance, lp_minted: Balance } -LiquidityRemoved { actor_id: ActorId, cycle_nonce: u64, step_index: u32, lp_asset: AssetId, lp_amount: Balance, asset_a: AssetId, asset_b: AssetId, amount_a: Balance, amount_b: Balance } - -ContractUpdated { actor_id: ActorId } -ActiveActorLimitSet { old_limit: u32, new_limit: u32 } -GlobalCircuitBreakerSet { paused: bool } -CrossingWorkerFaultCleared { feed: FeedId, revision: Option, class: CrossingWorkerFaultClass } -ObservationFanoutWorkerFaultCleared { feed: FeedId, revision: u64, subscriber_page: Option, class: ObservationFanoutWorkerFaultClass } -WakeupWorkerFaultCleared { key: WakeupKey, page: u32, class: WakeupWorkerFaultClass } -ManualTriggerSet { actor_id: ActorId } -FundingAccumulated { actor_id: ActorId, asset: AssetId, added: Balance, accumulated: Balance } -SweepBatchProcessed { requested: u32, closed: u32, alive: u32, missing: u32 } -IdleStarvationDetected { consecutive_blocks: u32 } -IdleStarvationRecovered { consecutive_blocks: u32 } +struct StepProgress { + terminal_reason: Option, + phase: RunPhase, + cycle_nonce: u64, + cursor: Option, + total_steps: u32, + completed_steps: u32, + next_eligibility: Option, + suspension: Option, + last_step_outcome: Option, +} ``` -Exact field order and complete task-event fields come from metadata. - -Event durability follows Section 4.4. `ManualTriggerSet` emits only on latch `false -> true`. `FundingAccumulated` emits only for tracked policy-accepted credit. Cycle key is `(actor_id, cycle_nonce)`; attempt adds `attempt`. - -Active creation emits exactly one `ActorCreated { initial_lifecycle: Active }` and does not emit `ActorActivated`. `ActorActivated` belongs only to Dormant -> Active transition of an existing identity. - -Pause/resume exact no-op emits no event. +The runtime MUST expose named structures for: -Opening emits `CycleStarted` before every step event. Retry begins with `CycleContinued`, emits no second start, and consumes no second snapshot. Within an attempt, the step event in Section 3.4 precedes any boundary event caused by that step. +- Complete semantic Contract and identities; +- Actor progress and current phase; +- Current control/effect maxima; +- Prospective Trigger fee; +- Prospective Pipeline Machine fee; +- Maximum next Action fee and last actual Action fee; +- Actor state hold and body footprint; +- Block resource limits/counters; +- Current bounded faults; +- Crossing capacity. -Exact boundary ordering is: +Clients MUST NOT assemble semantic Contracts from raw heads, chunks, pages, or locators. -- An advancing skip or failure emits only its `StepSkipped` or `StepFailed` event before the next visited step; -- `FundingUnavailable` selected for `RetryLater` emits no `StepSkipped`; it emits `CycleSuspended(FundingUnavailable)`, or `CycleSummary(Failed)` then `ActorClosed(bound_reason)` when an unsuccessful bound is reached; -- Temporary task failure selected for `RetryLater` emits `StepFailed`, then `CycleSuspended(Temporary)`, or `CycleSummary(Failed)` then `ActorClosed(bound_reason)` when an unsuccessful bound is reached; -- Terminal task failure emits `StepFailed`, then `CycleSummary(Failed)`, then any failure-driven `ActorClosed`; -- Successful completion emits `CycleSummary(Completed)`, then any productive, auto-close, or post-attempt scheduler-index `ActorClosed`; -- A successful terminal custody drain emits `CycleStarted`, `TransferExecuted`, `CycleSummary(Completed)`, then `ActorClosed(ProductiveCycleCompleted)`; -- A zero terminal custody drain emits `CycleStarted`, `StepSkipped(ResolutionSkipped)`, then `CycleSummary(Completed)` and no productive close; -- Fee-collection failure rolls back the complete attempt and emits nothing. +No API exposes remaining Pipeline Machine budget because none exists (§7.4). -`CycleSuspended` and `CycleSummary` expose cumulative outcomes after the current step delta. Cancellation emits `CycleCancelled`, then `CycleSummary(Cancelled)`. If caused by close, `ActorClosed` follows. If caused by semantic update or deactivation, the corresponding event follows. Pure close emits only `ActorClosed`. Weight/pass deferral emits nothing. Unbounded history is off-chain. +Simulation MUST be rollback-only and simulate at most one current Step under an explicit synthetic Actor Control and Shared Economic budget (§6.1, §7.6). It MUST use the same classifier, precondition, amount, Task, fee, outcome, and policy owners (§8.1, §6.2-§6.6, §7.4-§7.5). Waiting state returns `NotReady`; insufficient synthetic resource returns a resource-deferral result; fee-collector failure returns its typed simulation error. Simulation persists no state or event. -## 9. ABI, Errors, Storage, and Upgrades +### 12.4 Errors and projections -### 9.1 ABI Authority +Core classification errors: -Sections 1-8 own semantic meaning. Exact indices, discriminants, field order, bounds, and runtime API encoding come only from complete generated metadata. - -Metadata MUST expose only canonical shapes; reserved compatibility indices, stale aliases, dual names, and compatibility storage are forbidden. Canonical terminology is `Cycle`, `CloseAfterProductiveCycle`, and `ProductiveCycleCompleted`. - -After a `1.0.0` stability declaration, existing public shapes are immutable; additions append or use a new typed wrapper. +```rust +enum ActorClassificationError { + ActorInvariant, + RunInvariant, + ComputationOverflow, +} +``` -### 9.2 Errors +They project exactly to dispatch/runtime-API/simulation errors and MUST NOT become absence, dormancy, waiting, or scheduler exhaustion. ```rust enum Error { ActorIdOverflow, ActorNotFound, ActiveActorCapacityExceeded, ActiveActorCountInvariant, - ActorIdentityCapacityExceeded, ActorIdentityCountInvariant, ActorInvariant, - ActorAlreadyActive, ActorDormant, - ActiveActorLimitExceedsQueueCapacity, ActiveActorLimitTooHigh, - ActiveActorLimitTooLow, ActiveActorLimitBelowCurrent, - ActorPaused, EmptyContractSteps, ContractStepsExceedOnIdleBudget, - ExecutionDelayTooLong, GlobalCircuitBreakerActive, ImmutableActor, - InsufficientBalance, InsufficientFee, - InvalidAmountResolution, InvalidPredicate, InvalidAutoCloseNonce, - InvalidScheduleWindow, InvalidSplitTransfer, InvalidTriggerConfiguration, - InvalidTradeBound, InvalidRetryAttemptLimit, InvalidObservationMaxAge, - SelfTransferNotAllowed, MintNotAllowedForUserActor, - NotGovernance, NotOwner, - OwnerSlotCapacityExceeded, OwnerSlotOccupied, InvalidOwnerSlot, ActorIdOccupied, - SystemSovereignCapacityExceeded, SystemSovereignUnknown, - SystemSovereignOccupied, SystemSovereignInvariant, - SovereignAccountCollision, ReservedSovereignAccount, - TooManyContractSteps, SnapshotUnavailable, FundingAccumulatorOverflow, - QueueTicketExhausted, SchedulerIndexExhausted, + ActorIdentityCapacityExceeded, ActorIdentityCountInvariant, ActorInvariant, ActorAlreadyActive, + ActorDormant, ActiveActorLimitExceedsQueueCapacity, ActiveActorLimitTooHigh, + ActiveActorLimitTooLow, ActiveActorLimitBelowCurrent, ActorPaused, + ContractStepsExceedOnIdleBudget, ExecutionDelayTooLong, GlobalCircuitBreakerActive, + ImmutableActor, InsufficientBalance, InsufficientFee, InvalidAmountResolution, InvalidPredicate, + InvalidAutoCloseNonce, InvalidScheduleWindow, InvalidSplitTransfer, InvalidTriggerConfiguration, + InvalidTradeBound, InvalidRetryAttemptLimit, InvalidObservationMaxAge, SelfTransferNotAllowed, + MintNotAllowedForUserActor, NotGovernance, NotOwner, OwnerSlotCapacityExceeded, + OwnerSlotOccupied, InvalidOwnerSlot, ActorIdOccupied, SystemSovereignCapacityExceeded, + SystemSovereignUnknown, SystemSovereignOccupied, SystemSovereignInvariant, + SovereignAccountCollision, ReservedSovereignAccount, TooManyContractSteps, SnapshotUnavailable, + FundingAccumulatorOverflow, QueueTicketExhausted, SchedulerIndexExhausted, AutoCloseNonceHorizonExceeded, ControlMutationRateLimited, QueueCapacityUnavailable, - RetryLaterNotAllowedForImmutableActor, ContinuationNotFound, ContinuationInvariant, - ComputationOverflow, EmptyPrecondition, ManualSourceDisabled, - RecipientDepositUnavailable, + RetryLaterNotAllowedForImmutableActor, ActorRunNotFound, ActorRunInvariant, ComputationOverflow, + EmptyPrecondition, ManualSourceDisabled, RecipientDepositUnavailable, ObservationSubscriptionCapacityExceeded, ObservationSubscriptionInvariant, - InvalidObservationRevision, DirtyObservationCapacityExceeded, - DirtyObservationInvariant, ObservationUnavailable, ObservationUninitialized, - CrossingIndexCapacityExceeded, CrossingUserCapacityExceeded, - CrossingIndexInvariant, CrossingGenerationExhausted, - CrossingTransitionCapacityExceeded, CrossingTransitionInvariant, - CrossingWorkerFaultNotFound, ObservationFanoutWorkerFaultNotFound, - WakeupWorkerFaultNotFound, SystemActorTopologyInvalid, AdmissionBoundOverflow, + InvalidObservationRevision, DirtyObservationCapacityExceeded, DirtyObservationInvariant, + ObservationUnavailable, ObservationUninitialized, CrossingIndexCapacityExceeded, + CrossingUserCapacityExceeded, CrossingIndexInvariant, CrossingGenerationExhausted, + CrossingTransitionCapacityExceeded, CrossingTransitionInvariant, CrossingWorkerFaultNotFound, + ObservationFanoutWorkerFaultNotFound, WakeupWorkerFaultNotFound, SystemActorTopologyInvalid, + AdmissionBoundOverflow, StateHoldUnavailable, StateHoldInvariant, StateHoldOverflow, + PrepassDuplicateOrStale, ResourceProtocolFailed, PrepassContextIncomplete, } ``` -Section 4.1 classification errors have one exact projection: +Required distinguishable failure domains, represented either by a dedicated variant or by the documented classification projection, include: -| `ActorClassificationError` | Dispatch `Error` | `SimulationError` | -| --- | --- | --- | -| `ActorInvariant` | `ActorInvariant` | `Classification(ActorInvariant)` | -| `ContinuationInvariant` | `ContinuationInvariant` | `Classification(ContinuationInvariant)` | -| `ComputationOverflow` | `ComputationOverflow` | `Classification(ComputationOverflow)` | +- Absence/dormancy/active-state mismatch; +- Authorization/mutability; +- Invalid Contract/Trigger/Predicate/amount/trade/retry/window; +- Capacity and bound overflow; +- Slot/locator/collision/reserved account; +- State hold and fee collection; +- Queue/wakeup/detector faults and monotonic exhaustion; +- Stale admission or body authority projects to `ActorInvariant`; stale run authority projects to `ActorRunInvariant`; +- Control rate limit and breaker; +- Prepass/cutoff/resource protocol failure. + +Required Close reasons include: + +```rust +enum CloseReason { + OwnerInitiated, + TriggerAdmissionInsufficient, + CycleAdmissionInsufficient, + WindowExpired, + CycleNonceExhausted, + RetryAttemptsExhausted, + ConsecutiveFailures, + ProductiveCycleCompleted, + AutoCloseNonceReached, + SchedulerIndexExhausted, +} +``` -Eligibility returns `ActorClassificationError` directly without an interface-local duplicate. Simulation wraps that exact core once and adds only simulation-specific failures. +Resolution outcomes are not pallet errors. `TaskFailure.error` MAY carry a stable dispatch diagnostic without converting the Attempt into a rejected extrinsic. -Every dispatch path that invokes classification MUST preserve this mapping. `permissionless_sweep_many` rolls back its complete transaction on a classification error. Certified ingress carries the corresponding pallet `Error` inside Permanent `IngressFailure`. A classification error MUST NOT map to `ActorNotFound`, `ActorDormant`, `NotReady`, `SchedulerIndexExhausted`, `AdmissionBoundOverflow`, or a waiting phase. +--- -`ActorInvariant` belongs only to a malformed existing Active actor whose required identity, hot state, contract, funding state, or cross-partition relation is missing or inconsistent. An absent id and a valid Dormant identity use their caller-specific ordinary result and are not actor invariants. +## 13. Storage, upgrades, configuration, and conformance -`ContinuationInvariant` belongs to disagreement between `cycle_state` and `ContinuationState`, or to malformed Continuation cursor, policy, counter, snapshot, or suffix state. +### 13.1 Storage and integrity -`ComputationOverflow` belongs only to checked arithmetic required to classify an existing Active actor, including current fee-envelope rederivation and retry/temporal target computation. Contract creation or semantic replacement uses `AdmissionBoundOverflow`; funding accumulation, scheduler placement, auto-close mutation, and other call-specific transitions retain their own errors. +Generated descriptors own exact keys, hashers, prefixes, values, and page geometry. -Resolution outcomes are not pallet errors. `TaskFailure.error` MAY carry a pallet error only as a stable execution diagnostic, without converting the step into an extrinsic-level rejection. +Normatively required: -`RecipientDepositUnavailable` is Temporary only in that execution-time role. `InsufficientFee` belongs only to User creation-fee collection. `FeeCollectionFailed` belongs only to rollback-only simulation of an otherwise Ready User attempt whose runtime fee-collection path fails; production preserves the head as `FeeCollectionStalledLiveHead`. `ActorIdOccupied` belongs only to creation against already-owned `NextActorId`. `NotGovernance` applies when an origin accepted by `SystemOrigin` targets a User actor. Unknown adapter failure remains Permanent. +- Only canonical partitions from §3.5 and `ActorRunState` from §5.1; +- Bounded collections and exact reverse ownership; +- One live ticket and bounded wakeup authority per §8.2; +- No unbounded execution history; +- No persistent execution cache duplicating authored fields, Steps, cursor, snapshots, outcomes, lifecycle, or latch; +- No remaining Pipeline budget, fee cache, or generic cache-revalidation workset; +- Exact hold/state reconciliation (§7.2); +- `try_state` verification of Contract/body/admission, run, latch, topology, counters, funding, slots, locators, and orphans. -The error ABI contains no compatibility-only variant. +Orphan physical records are never semantic authority and are integrity failures unless they are transaction-local writes that roll back. -### 9.3 Storage Contract +### 13.2 Runtime upgrades -Generated storage descriptors own exact prefixes, hashers, keys, values, and physical topology. +Before first production genesis, a fresh canonical baseline MAY replace storage or ABI without migration compatibility. -Normatively required: +After deployed production lineage, every storage or semantic rewrite MUST define: -- Section 2 canonical partitions; -- Persistent non-optional identity control clock; -- Exact slot/locator/reverse-index/subscription/readiness ownership; -- One live ticket and at most one ordinary temporal target per actor, subject to terminal coexistence; -- Bounded collections and exact counters/reverse ownership; -- Canonical ordered encoding; -- No unbounded execution history; -- No stored Weight/fee cache, cache epoch, cache-revalidation workset, parallel fee table, or writable composite actor; -- `try_state` reconciliation of ownership, cardinality, membership, revision, Continuation, terminal marker, tracked funding derivation, and processing bounds. - -Initial state contains no migration, dual write, legacy decoder, bridge, stale alias, or compatibility storage. It initializes nonzero `ActiveActorLimit`, validates `SystemSovereignCount <= MaxSystemSovereigns`, sets a representable `NextActorId` above every configured actor id and reserved System locator id, and reconciles every configured System actor/custody locator, identity counter, reverse index, subscription, and initial scheduler path. - -### 9.4 Deployed-Lineage Runtime Upgrade Migration - -This section applies only after a host runtime has established persisted production lineage. Before first production genesis, a fresh canonical baseline MAY replace storage or ABI without migration ceremony, historical decoders, or compatibility aliases. - -For a deployed lineage, each storage change, semantic state rewrite, incompatible Weight/admission change, or runtime-upgrade override of an Immutable actor first ships a migration-specific specification defining: - -- Source and target schemas/semantics; -- Bounded work unit and durable progress owner; -- Temporary execution gate; -- Weight per invocation; -- Failure, interruption, resume, and completion behavior; -- Semantic mapping and actor disposition; -- Custody consequences; -- Terminal invariant; -- Continuation `Cancel | PreserveWithProof` policy; -- Storage-version transition and idempotence. - -Idempotence means that after target completion, later invocations perform no semantic mutation. Partial migration cannot expose reinterpreted state to ordinary execution. Continuation preservation proves equivalent contract, frozen inputs, failures, fees, Weight, and eligibility; otherwise cancel. - -A deployed-lineage migration that repairs a persistent structurally caused `InvariantStalledLiveHead` is the sole authority to rewrite or remove the malformed state. It MUST define actor disposition, custody preservation, queue ownership correction, FIFO consequences, and the exact service-resumption point. - -A deployed-lineage change to sovereign-account derivation or reserved-account policy that could alter existing custody or reattachment is a semantic rewrite and MUST define the same custody disposition. - -Migration state is specific to the concrete upgrade and MUST be absent or inert after completion. Actors defines no permanent generic migration subsystem. - -## 10. Runtime Configuration - -### 10.1 Relations - -Required bindings include `ActorsPalletId`, `FeeNativeAssetId`, `SystemOrigin`, `GlobalBreakerOrigin`, adapters/services, `FeeSink`, `ActorCreationFee`, `WeightToFee`, one `WeightInfo`, `ActorOnIdleReserve`, generated base/cleanup bounds, `MaxSweepBatch`, independent queue/wakeup/observation processing bounds, System reference parameters, and `TargetBlockTime`. - -1. `0 < ActiveActorLimit <= min(MaxActiveActors, MaxQueueLength)`; after update, `ActiveActorLimit >= ActiveActorCount`. -2. `MaxActorIdentities >= MaxActiveActors`. -3. `MaxSystemSovereigns > 0`; every count/index type represents its configured bound. -4. `0 < MaxOwnerSlots <= 255`. -5. `0 < MaxContractSteps <= 255`. -6. `MaxRetryAttempts >= 2`. -7. `MaxContractSteps * MaxRetryAttempts <= u32::MAX`, checked; this bounds every `OutcomeTotals` counter within one cycle. -8. `MaxConsecutiveFailures > 0`. -9. `MaxOpeningSnapshotEntries == 2 * MaxContractSteps` and `MaxOpeningPredicateResults == MaxContractSteps * MaxPredicatesPerStep`, checked; persisted funding entries fit `MaxFundingTrackedAssets`. -10. Observation subscriptions and dirty obligations obey the `MaxActiveActors` bound in Section 5.2. -11. `QueuePageSize`, `WakeupPageSize`, and `ObservationPageSize` are independently named and nonzero. -12. `MaxSweepBatch > 0`. -13. Every collection, scan, attempt, worker, and processing-unit bound is nonzero and has one owner. -14. Each worker limit covers one complete worst-case unit. -15. Runtime guarantees `remaining_weight_at_ACTORS_on_idle >= ActorOnIdleReserve` in both dimensions. -16. Section 5.4 subtraction is representable and `ActorServiceReserve` covers one maximum actor attempt including reachable terminal cleanup, or one standalone terminal cleanup. -17. Every admitted plan/suffix, producer consequence, control transition, and cleanup fits its owning envelope. -18. `MinUserBalance >= AssetOps::minimum_balance(FeeNativeAssetId)`. -19. `ActorCreationFee > 0`. -20. `WeightToFee` maps every admitted User evaluation/task upper bound to a positive fee. -21. `SystemSwapEmaMaxAgeBlocks > 0`; `SystemSwapMaxReferenceDeviation < Perbill::one()`. -22. `TargetBlockTime > 0`. -23. `MaxExecutionDelayBlocks = ceil(10 Julian years / TargetBlockTime)` and `MaxCadenceDelayTicks = ceil(10 Julian years / CadenceTickMillis)`; each horizon is nonzero, typed in its own clock, and its arithmetic is representable. -24. `MaxAutoCloseNonceHorizon > 0`. -25. `MaxIdleStarvationBlocks > 0`. -26. Simulation records are bounded by the same `MaxContractSteps`. -27. Every loop and storage bound appears in metadata or generated descriptors. -28. `MaxSplitTransferLegs >= 2`. -29. No Config binding or storage item exists for `StepBaseFee`, a legacy condition-read fee, stored derived Weight/fee bounds, cache epochs, or generic cache revalidation. -30. `AssetOps` supports source-exhausting Transfer for an admitted terminal custody drain; ordinary source preservation remains owned by Section 3.3 amount resolution. - -### 10.2 Semantic Reference Profile +- Source and target schemas; +- Bounded migration unit and progress owner; +- Execution gate; +- Weight; +- Interruption/resume/idempotence; +- Actor and custody disposition; +- Open-run `Cancel | PreserveWithProof` policy; +- Admission re-certification; +- Storage-version transition. -```text -TargetBlockTime = 6 seconds -MaxActiveActors = 10_000 MaxOwnerSlots = 255 -MaxContractSteps = 8 MaxRetryAttempts = 10 -MaxFundingTrackedAssets = 10 MaxOpeningSnapshotEntries = 16 -MaxOpeningPredicateResults = 32 MaxPreconditionClauses = 4 -MaxPredicatesPerClause = 4 MaxPredicatesPerStep = 4 -MaxCadenceDelayTicks = 631_152_000 MaxWhitelistSize = 16 -MaxSplitTransferLegs = 8 MaxConsecutiveFailures = 10 -MaxQueueLength = 10_000 MaxExecutionDelayBlocks = 52_596_000 -MinWindowLength = 100 -MaxAutoCloseNonceHorizon = 10_000 MaxIdleStarvationBlocks = 25 -MaxSweepBatch = 5 -MinUserBalance = 5 * existential deposit -ActorCreationFee = existential deposit -``` +A Weight or fee-policy change may alter runtime admission identity but MUST NOT alter semantic Contract identity (§3.2, §3.4). Open paid Pipelines retain their paid service identity through the Cycle boundary unless the migration specification proves another disposition. -`52_596_000` is ten Julian years at six seconds. The reference profile exposes 255 User slots and reserves bitmap bit `255` as the permanently invalid `u8::MAX` sentinel. +A deployed change MUST NOT make previously reattachable custody unreachable without explicit custody disposition (§10.3). -### 10.3 Measured Runtime Bindings +### 13.3 Runtime configuration -`QueuePageSize`, `WakeupPageSize`, `ObservationPageSize`, `MaxQueueEntriesScannedPerBlock`, `MaxExecutionsPerBlock`, `MaxWakeupsPerBlock`, `MaxObservationFanoutPagesPerBlock`, `WakeupWeightLimit`, `ObservationFanoutWeightLimit`, `ActorOnIdleReserve`, base/cleanup bounds, and adapter worst cases are measured runtime outputs. +Required relations: -Generated runtime and Weight descriptors define the values. Count ceilings do not guarantee throughput. +1. `0 < MaxContractSteps <= 255`; production reference is 32. +2. `0 < MaxOwnerSlots <= 255`; reference is 255. +3. `MaxRetryAttempts >= 2`; reference is 10. +4. `MaxContractSteps * MaxRetryAttempts` fits outcome counters. +5. Opening snapshot/result bounds cover every admitted Contract. +6. `MaxCrossingMembersPerFeed` covers at least 10,000 User memberships plus every separately bounded host-owned membership reserved for the feed. +7. Every queue, wakeup, detector, cohort, sweep, and worker bound is nonzero and owns one complete worst-case unit. +8. One maximum current-Step control/effect transition fits the guaranteed base pass (§7.6, §8.3). +9. Maximum admitted create, activate, update, deactivate, cancel, and close paths remain dispatchable under their owning call limits. +10. `MinUserBalance >= host minimum balance`. +11. `ActorCreationFee > 0`. +12. `WeightToFee` maps every nonzero User Trigger, Pipeline, and Action Weight upper bound to positive fee. +13. Trigger and Pipeline fee owners are disjoint (§7.3, §7.4). +14. Actor Control is floor one third of schedulable Weight; Shared Economic owns the remainder and its two base turns split floor/remainder (§7.6). +15. `MaxTemporalDelayTicks` and `MaxExecutionDelayBlocks` are clock-specific and representable. +16. `MaxSplitTransferLegs >= 2`. -## 11. Conformance +Reference profile: -A runtime conforms to this specification iff every requirement holds: +```text +TargetBlockTime = 6 seconds +CadenceTick = 500 milliseconds +MaxActiveActors = 10_000 +MaxOwnerSlots = 255 +MaxContractSteps = 32 +MaxRetryAttempts = 10 +MaxConsecutiveFailures = 10 +MaxFundingTrackedAssets = 40 +MaxOpeningSnapshotEntries = 64 +MaxOpeningPredicateResults = 128 +MaxPreconditionClauses = 4 +MaxPredicatesPerClause = 4 +MaxPredicatesPerStep = 4 +MaxWhitelistSize = 16 +MaxSplitTransferLegs = 8 +MaxQueueLength = 10_000 +MaxSweepBatch = 5 +MinUserBalance = 5 * existential deposit +ActorCreationFee = 2 * existential deposit +MaxTemporalDelayTicks = 631_152_000 +MaxExecutionDelayBlocks = 52_596_000 +ActorControlLimit = floor(SchedulableBlockWeight / 3) +SharedEconomicLimit = SchedulableBlockWeight - ActorControlLimit +ActorBaseTurn = floor(SharedEconomicLimit / 2) +UserBaseTurn = SharedEconomicLimit - ActorBaseTurn +``` -1. Actors runtime metadata exposes only the calls, public types, events, errors, bounds, and runtime APIs defined here. -2. Every reachable control, scheduler, ingress, task, cancellation, and close transition follows the specified state delta, ordering, and rollback boundary. -3. Scheduler admission, sweep, actor-targeting expiry substitution, certified-ingress expiry handling, simulation, and eligibility use one actor-classification implementation and preserve the exact Section 9.2 error projections. -4. User and System fee, mutability, and terminal behavior match Sections 2.2, 2.4, and 4.3. -5. Storage satisfies Section 9.3 and `try_state` verifies its ownership, cardinality, membership, Continuation, terminal, funding, and scheduler invariants. -6. One generated `WeightInfo` supplies every numeric Weight; `WeightToFee` derives every User attempt fee from it. -7. Generated Weight descriptors upper-bound every call, task, worker, ingress path, cleanup path, and admitted maximum composition in RefTime and ProofSize. -8. Certified producer and publisher inventories contain every path that claims Actors ingress or observation authority; paths outside them have no Actors semantic effect. -9. Adapter retry classification is exhaustive and maps unknown failure to Permanent. -10. Simulation is rollback-only and observationally equivalent to one production attempt at the same state and block. -11. Simulation and eligibility project actor classification without independently recomputing terminal or execution-phase predicates, and every classification error follows the exact Section 9.2 mapping. -12. Initial state and every completed migration contain no compatibility alias, dual write, stale derived cache, or permanent generic migration state. -13. Close preserves custody without transfer; exact User-slot and System-locator reattachment derive the same sovereign account; an admitted terminal custody drain can exhaust one named asset, including the fee-native balance net of current attempt fees, and closes atomically on positive success while a zero drain follows the specified non-closing skip path. -14. Canonical input tests prove runtime rejection without sorting or deduplication; task-shape tests prove at most two `AmountResolution` fields and at most two opening surfaces per step; opening-surface admission never truncates; every Section 3.4 step row has identical production, simulation, counter, and event behavior. -15. Fee-collector failure rolls back the complete attempt, projects `FeeCollectionFailed` in simulation, cannot arise solely from missing reserved payer balance, and cannot be downgraded by authored step policy. When a host elects certified FeeSink ingress, that movement never silently degrades to balance-only and notifies exactly once; ledger-only hosts create no trigger consequence. -16. Cutoff tests prove the same-block `MAY`/`MUST NOT` boundary; maximum-plan tests prove bounded paid completion without structural starvation; Active Fee Sink tests prove first-signal placement at maximum canonical occupancy; fee-collection stalls may retry only through later ordinary actor service; no call-level path bypasses `InvariantStalledLiveHead`; deployed-lineage structural repair follows Section 9.4. -17. Cursor-local retry tests distinguish fresh suspension, repeated suspension at the same cursor, and first suspension at a later cursor. Timing tests prove floor-readiness, ceil-activation, a full first cadence period, constant-time missed-period coalescing, no cadence catch-up, Paused actors without ordinary targets, and one typed temporal membership per Actor. +Measured runtime descriptors own page sizes, per-block worker counts, exact Weight limits, adapter maxima, and generated fee-envelope identities. + +### 13.4 Conformance + +A runtime conforms iff: + +1. Metadata exposes only canonical public shapes; +2. Every semantic function follows its sole owner (§1.2); +3. Every cross-reference uses, rather than redefines, the owned function; +4. Every reachable transition is bounded, pre-admitted, and transactionally atomic at its defined boundary; +5. Trigger occurrence, latch, re-arm, and underfunding follow §4; +6. Opening, snapshots, nonce, zero-Step, and Cycle boundaries follow §5; +7. Q1 Step execution, error policies, and Tasks follow §6; +8. Creation, Trigger, Pipeline, Action, state-hold, and resource ownership follow §7 without overlap; +9. Classification, cutoff, FIFO, cohorts, faults, and liveness follow §8; +10. Mutability, terminal precedence, close, apoptosis, and breaker follow §9; +11. Sovereign custody survives process deletion and exact reattachment follows §10; +12. Each Task effect maps to one canonical host owner and typed failure surface (§11); +13. Calls, events, APIs, and errors follow §12; +14. Storage integrity and deployed upgrades follow §13.1-§13.2; +15. Generated Weight upper-bounds every admitted control/effect branch in both dimensions; +16. Simulation is rollback-only and observationally equivalent to zero or one production current-Step transition. --- diff --git a/template/pallets/actors/embedding-runtime/README.md b/template/pallets/actors/embedding-runtime/README.md index b93301bb..68eb739d 100644 --- a/template/pallets/actors/embedding-runtime/README.md +++ b/template/pallets/actors/embedding-runtime/README.md @@ -11,7 +11,7 @@ The fixture deliberately uses: - A runtime-local transaction extension proving successful and failed Executive transfer ingress without event scanning. - Default-deny funding authority and deterministic unsupported DEX, liquidity, and staking adapters in the default profile. - An opt-in `dex-fixture` profile with one fixed-rate exact-output pair and one explicitly Temporary exact-input fixture, with no imported pool topology. -- Mutable User and System Continuation coverage for cooldown, suffix resumption, concurrent Executive ingress, cancellation, pure close, and try-state integrity. +- Mutable User and System `ActorRunState` coverage for open/finalized nonce separation, cursor and eligibility ownership, immutable Opening/funding facts, exact outcomes, cooldown, suffix resumption, concurrent Executive ingress, cancellation, pure close, and try-state integrity. - No DEOS primitives, TMCTOL topology, governance catalog, DEOS Router, TMC, or staking pallet. Run its focused evidence from `template/`: @@ -25,4 +25,4 @@ cargo check -p pallet-deos-actors-embedding-fixture --no-default-features cargo clippy -p pallet-deos-actors-embedding-fixture --all-targets --all-features -- -D warnings ``` -Capability, ingress, Continuation, lifecycle, metadata, and optional-adapter evidence belongs in this crate so failures expose pressure on the public Actors embedding contract rather than borrowing DEOS runtime helpers. +Capability, ingress, run-state, lifecycle, metadata, and optional-adapter evidence belongs in this crate so failures expose pressure on the public Actors embedding contract rather than borrowing DEOS runtime helpers. diff --git a/template/pallets/actors/embedding-runtime/src/lib.rs b/template/pallets/actors/embedding-runtime/src/lib.rs index 857a4ca3..c349e080 100644 --- a/template/pallets/actors/embedding-runtime/src/lib.rs +++ b/template/pallets/actors/embedding-runtime/src/lib.rs @@ -582,6 +582,19 @@ impl pallet_deos_actors::BenchmarkHelper ) -> polkadot_sdk::frame_support::dispatch::DispatchResult { Err(DispatchError::Other("BenchmarkIngressUnsupported")) } + + type MaximumContextInherent = (); + fn prepare_maximum_context_inherent() {} + fn execute_maximum_context_inherent(_: ()) -> DispatchResult { + Err(DispatchError::Other("BenchmarkContextUnsupported")) + } + fn verify_maximum_context_inherent() {} + fn prepare_maximum_xcm_version_discovery() {} + fn execute_maximum_xcm_version_discovery() {} + fn verify_maximum_xcm_version_discovery() {} + fn prepare_block_resource_meter_extension() {} + fn execute_block_resource_meter_extension() {} + fn verify_block_resource_meter_extension() {} } pub struct RuntimeTime; @@ -594,11 +607,91 @@ impl Time for RuntimeTime { } } +pub struct FixtureStepControlWeight; +impl pallet_deos_actors::StepControlWeightProvider> + for FixtureStepControlWeight +{ + fn production_weight_identity() -> Option<[u8; 32]> { + Some([1; 32]) + } + + fn maximum_control_weight( + _: pallet_deos_actors::StepControlWeightContext, + _: &pallet_deos_actors::StepOf, + ) -> Option { + Some(Weight::from_parts(100_000_000, 100_000)) + } + + fn actual_control_weight( + _: pallet_deos_actors::StepControlWeightContext, + _: &pallet_deos_actors::StepOf, + maximum: Weight, + _: pallet_deos_actors::StepControlExecution, + ) -> Option { + Some(maximum) + } +} + +pub struct FixtureTaskEffectWeight; +impl pallet_deos_actors::TaskEffectWeightProvider> + for FixtureTaskEffectWeight +{ + fn production_weight_identity() -> Option<[u8; 32]> { + Some([2; 32]) + } + + fn maximum_effect_weight(_: &pallet_deos_actors::TaskOf) -> Option { + Some(Weight::from_parts(100_000_000, 100_000)) + } + + fn actual_effect_weight( + _: &pallet_deos_actors::TaskOf, + execution: pallet_deos_actors::TaskEffectExecution, + ) -> Option { + Some(match execution { + pallet_deos_actors::TaskEffectExecution::NotInvoked => Weight::zero(), + pallet_deos_actors::TaskEffectExecution::Invoked => Weight::from_parts(100_000_000, 100_000), + }) + } +} + +pub struct FixtureAdmissionCertificateAuthority; +impl pallet_deos_actors::AdmissionCertificateAuthorityProvider + for FixtureAdmissionCertificateAuthority +{ + fn current() -> Option { + Some(pallet_deos_actors::AdmissionCertificateAuthority { + runtime_actor_semantics_version: 1, + production_weight_identity: + pallet_deos_actors::AdmissionCertificateAuthority::compose_production_weight_identity( + [1; 32], [2; 32], + ), + body_geometry_version: 1, + configured_bounds_commitment: [3; 32], + maximum_lifecycle_weight: Weight::from_parts(500_000_000, 500_000), + }) + } +} + +pub struct EmbeddingBlockResourceBudget; +impl Get for EmbeddingBlockResourceBudget { + fn get() -> pallet_deos_actors::BlockResourceBudget { + pallet_deos_actors::BlockResourceBudget::new( + Weight::from_parts(1_000_000_000_000, 5_000_000), + Weight::zero(), + ) + .unwrap_or_else(|_| pallet_deos_actors::BlockResourceBudget::fail_closed(Weight::zero())) + } +} + impl pallet_deos_actors::Config for Runtime { type AssetId = AssetId; type Balance = Balance; type FeeNativeAssetId = ConstU32; type AssetOps = NativeAssetOps; + type AdmissionCertificateAuthority = FixtureAdmissionCertificateAuthority; + type StepControlWeight = FixtureStepControlWeight; + type TaskEffectWeight = FixtureTaskEffectWeight; type ObservationFeedId = u32; type ObservationProvider = (); type FundingAuthority = (); @@ -626,6 +719,7 @@ impl pallet_deos_actors::Config for Runtime { type QueuePageSize = ConstU32<8>; type WakeupPageSize = ConstU32<8>; type ObservationPageSize = ConstU32<8>; + type CrossingPageSize = ConstU32<8>; type MaxCrossingTransitionsPerFeed = ConstU32<8>; type MaxCrossingMembersPerFeed = ConstU32<16>; type MaxUserCrossingMembersPerFeed = ConstU32<12>; @@ -644,7 +738,7 @@ impl pallet_deos_actors::Config for Runtime { type MaxSplitTransferLegs = ConstU32<4>; type TargetBlockTime = ConstU64<315_576>; type MaxExecutionDelayBlocks = ConstU64<1_000>; - type MaxCadenceDelayTicks = ConstU64<631_152_000>; + type MaxTemporalDelayTicks = ConstU64<631_152_000>; type MaxIdleStarvationBlocks = ConstU32<3>; type ActorOnIdleReserve = ActorOnIdleReserve; type MaxAutoCloseNonceHorizon = ConstU64<1_000>; @@ -652,14 +746,19 @@ impl pallet_deos_actors::Config for Runtime { type MaxActorIdentities = ConstU32<96>; type MaxSystemSovereigns = ConstU32<96>; type ActorCreationFee = ConstU128<100>; + type RuntimeHoldReason = RuntimeHoldReason; + type StateHoldCurrency = Balances; + type ActorStateHoldBase = ConstU128<1>; + type ActorStateHoldPerByte = ConstU128<1>; type WeightToFee = LinearWeightToFee; type FeeSink = FeeSink; type FeeCollector = NativeFeeCollector; - type TriggerStateBond = (); type MaxConsecutiveFailures = ConstU32<2>; type MaxRetryAttempts = ConstU32<10>; type MinUserBalance = ConstU128<10>; type WeightInfo = pallet_deos_actors::weights::SubstrateWeight; + type BlockResourceBudget = EmbeddingBlockResourceBudget; + type PrepassContext = (); type GenesisSystemActors = (); type SystemActorContractValidator = (); #[cfg(feature = "runtime-benchmarks")] @@ -728,6 +827,31 @@ mod tests { UncheckedExtrinsic::new_signed(call, signer, signature, tx_ext) } + fn service_actor_idle(block: BlockNumber) { + if pallet_deos_actors::CurrentBlockResourceState::::get() + .is_none_or(|state| state.block_number() != block) + { + assert_ok!(Actors::actor_prepass(RuntimeOrigin::none())); + } + Actors::on_idle(block, Weight::MAX); + } + + fn run_until_not_running(actor_id: pallet_deos_actors::ActorId) { + let mut block = System::block_number(); + service_actor_idle(block); + for _ in 1..16 { + if Actors::actor_hot(actor_id) + .is_none_or(|hot| hot.cycle_state != pallet_deos_actors::CycleState::Running) + { + break; + } + block = block.checked_add(1).expect("fixture block advances"); + System::set_block_number(block); + Actors::on_initialize(block); + service_actor_idle(block); + } + } + fn contract_steps( task: pallet_deos_actors::TaskOf, ) -> pallet_deos_actors::ContractSteps { @@ -777,12 +901,8 @@ mod tests { /// Spec 7.1 prefunding requirement: `MinUserBalance + attempt fee envelope`. fn user_prefunding_requirement(plan: &pallet_deos_actors::ContractSteps) -> Balance { - let min_user_balance: Balance = ::MinUserBalance::get(); - min_user_balance.saturating_add( - Actors::attempt_fee_envelope(pallet_deos_actors::ActorType::User, plan, 0) - .expect("fixture plan has a checked fee envelope") - .total, - ) + Actors::user_pipeline_machine_capacity_requirement(plan) + .expect("fixture plan has a checked opening-Step fee") } fn lowest_free_owner_slot(owner: AccountId) -> u8 { @@ -914,7 +1034,8 @@ mod tests { b"ActorHot".as_slice(), b"ActorContract".as_slice(), b"ActorFunding".as_slice(), - b"ContinuationState".as_slice(), + b"ActorRunHead".as_slice(), + b"ActorRunPayload".as_slice(), b"QueuePages".as_slice(), b"WakeupPages".as_slice(), b"Precondition".as_slice(), @@ -978,7 +1099,7 @@ mod tests { #[test] fn independent_runtime_starts_from_the_fresh_schema_without_system_topology() { new_test_ext().execute_with(|| { - let baseline = StorageVersion::new(9); + let baseline = StorageVersion::new(15); assert_eq!(Actors::in_code_storage_version(), baseline); assert_eq!(Actors::on_chain_storage_version(), baseline); assert_eq!(Actors::actor_identity_count(), 0); @@ -1071,7 +1192,7 @@ mod tests { RuntimeOrigin::signed(ALICE), actor_id )); - let _ = Actors::on_idle(1, Weight::MAX); + service_actor_idle(1); assert_eq!(Balances::free_balance(BOB), bob_before.saturating_add(50)); assert_eq!( Actors::active_actor_state(actor_id) @@ -1150,7 +1271,7 @@ mod tests { Executive::apply_extrinsic(signed_extrinsic(ALICE, 1, trigger)), Ok(Ok(_)) )); - let _ = Actors::on_idle(1, Weight::MAX); + run_until_not_running(actor_id); assert_eq!(Balances::free_balance(BOB), bob_before.saturating_add(31)); }); } @@ -1183,7 +1304,7 @@ mod tests { assert!(matches!(result, Ok(Ok(_))), "{result:?}"); assert!(Actors::pending_signal(actor_id)); let bob_before = Balances::free_balance(BOB); - let _ = Actors::on_idle(1, Weight::MAX); + service_actor_idle(1); assert_eq!(Balances::free_balance(BOB), bob_before.saturating_add(50)); System::set_block_number(2); let _ = Actors::on_idle(2, Weight::MAX); @@ -1316,7 +1437,7 @@ mod tests { NATIVE_ASSET, 10_000_000_000, )); - let _ = Actors::on_idle(1, Weight::MAX); + service_actor_idle(1); assert_eq!(Balances::free_balance(BOB), bob_before.saturating_add(50)); System::set_block_number(2); let _ = Actors::on_idle(2, Weight::MAX); @@ -1359,9 +1480,9 @@ mod tests { pallet_deos_actors::WakeupPages::::iter().count(), 2 ); - for block in 21..=25 { + for block in 20..=30 { System::set_block_number(block); - let _ = Actors::on_idle(block, Weight::MAX); + service_actor_idle(block); } for actor_id in &actor_ids { assert_eq!( @@ -1377,7 +1498,7 @@ mod tests { assert!( pallet_deos_actors::ActorHot::::get(*actor_id) .expect("hot state remains") - .wakeup_pointer + .trigger_wakeup_pointer .is_some() ); } @@ -1460,8 +1581,7 @@ mod tests { )); let active_id = pallet_deos_actors::NextActorId::::get().saturating_sub(1); System::set_block_number(3); - let mut contract = pallet_deos_actors::ActorContracts::::get(active_id) - .expect("Actor Contract exists"); + let mut contract = Actors::actor_contract(active_id).expect("Actor Contract exists"); contract.steps = contract_steps(mint_task()); contract.completion = pallet_deos_actors::CompletionPolicy::Persistent; assert_noop!( @@ -1487,7 +1607,7 @@ mod tests { RuntimeOrigin::signed(ALICE), system_id )); - let _ = Actors::on_idle(1, Weight::MAX); + service_actor_idle(1); assert_eq!(Balances::free_balance(sovereign), 100); assert_eq!( Actors::active_actor_state(system_id) @@ -1501,7 +1621,7 @@ mod tests { #[cfg(feature = "dex-fixture")] #[test] - fn mutable_user_continuation_preserves_prefix_and_residual_admission() { + fn mutable_user_run_preserves_prefix_and_residual_admission() { new_test_ext().execute_with(|| { System::set_block_number(1); let plan = active_contract( @@ -1545,24 +1665,26 @@ mod tests { RuntimeOrigin::signed(ALICE), actor_id )); - let _ = Actors::on_idle(1, Weight::MAX); - - let suspended = Actors::continuation_state(actor_id).expect("temporary failure suspends"); - assert_eq!(suspended.cursor, 1); - assert_eq!(suspended.unsuccessful_attempts_at_cursor, 1); - assert_eq!(Balances::free_balance(BOB), bob_before.saturating_add(5)); - System::set_block_number(2); - let _ = Actors::on_idle(2, Weight::MAX); + service_actor_idle(1); assert_eq!( - Actors::continuation_state(actor_id) - .expect("cooldown defers retry") - .unsuccessful_attempts_at_cursor, + Actors::actor_run_state(actor_id) + .expect("first committed Step preserves the run") + .cursor, 1 ); + assert_eq!(Balances::free_balance(BOB), bob_before.saturating_add(5)); + + System::set_block_number(2); + service_actor_idle(2); + let resumed = Actors::actor_run_state(actor_id).expect("successful retry advances one Step"); + assert_eq!(resumed.cycle_nonce, 1); + assert_eq!(resumed.cursor, 2); + assert_eq!(resumed.unsuccessful_attempts_at_cursor, 0); + assert_eq!(resumed.suspension, None); System::set_block_number(3); - let _ = Actors::on_idle(3, Weight::MAX); - assert!(Actors::continuation_state(actor_id).is_none()); + service_actor_idle(3); + assert!(Actors::actor_run_state(actor_id).is_none()); assert_eq!( Actors::active_actor_state(actor_id) .expect("actor completes") @@ -1576,7 +1698,7 @@ mod tests { #[cfg(feature = "dex-fixture")] #[test] - fn mutable_system_continuation_retries_without_external_topology() { + fn mutable_system_run_retries_without_external_topology() { new_test_ext().execute_with(|| { System::set_block_number(1); assert_ok!(Actors::create_system_actor( @@ -1600,18 +1722,24 @@ mod tests { RuntimeOrigin::signed(ALICE), actor_id )); - let _ = Actors::on_idle(1, Weight::MAX); + service_actor_idle(1); + let suspended = Actors::active_actor_state(actor_id).expect("system actor suspends"); assert_eq!( - Actors::active_actor_state(actor_id) - .expect("system actor suspends") - .hot - .cycle_state, + suspended.hot.cycle_state, pallet_deos_actors::CycleState::Suspended ); + assert_eq!(suspended.identity.cycle_nonce, 0); + assert_eq!( + suspended + .run_state + .expect("system run persists") + .cycle_nonce, + 1 + ); System::set_block_number(2); - let _ = Actors::on_idle(2, Weight::MAX); - assert!(Actors::continuation_state(actor_id).is_none()); + service_actor_idle(2); + assert!(Actors::actor_run_state(actor_id).is_none()); assert_eq!( Actors::active_actor_state(actor_id) .expect("system actor completes") @@ -1645,7 +1773,7 @@ mod tests { NATIVE_ASSET, 100_000_000_000, )); - let _ = Actors::on_idle(1, Weight::MAX); + service_actor_idle(1); let before = Actors::active_actor_state(actor_id).expect("actor suspends"); let before_hot = pallet_deos_actors::ActorHot::::get(actor_id).expect("hot state exists"); @@ -1653,6 +1781,13 @@ mod tests { before.hot.cycle_state, pallet_deos_actors::CycleState::Suspended ); + assert_eq!(before.identity.cycle_nonce, 0); + let opening_funding = before + .run_state + .as_ref() + .expect("open run persists funding") + .funding_snapshot + .clone(); assert!(before_hot.wakeup_pointer.is_some()); assert!(before_hot.queue_ticket.is_none()); @@ -1670,6 +1805,14 @@ mod tests { let after_hot = pallet_deos_actors::ActorHot::::get(actor_id).expect("hot state remains"); assert!(after.hot.pending_signal); + assert_eq!( + after + .run_state + .as_ref() + .expect("ingress preserves open run") + .funding_snapshot, + opening_funding + ); assert_eq!(after_hot.wakeup_pointer, before_hot.wakeup_pointer); assert_eq!(after_hot.queue_ticket, before_hot.queue_ticket); let repeated_call = @@ -1686,32 +1829,29 @@ mod tests { assert_eq!(repeated_hot.queue_ticket, after_hot.queue_ticket); assert_eq!(repeated_hot.wakeup_pointer, after_hot.wakeup_pointer); System::set_block_number(2); - assert_ok!(Actors::cancel_continuation( - RuntimeOrigin::signed(ALICE), - actor_id, - )); - assert!(Actors::continuation_state(actor_id).is_none()); + assert_ok!(Actors::cancel_run(RuntimeOrigin::signed(ALICE), actor_id,)); + assert!(Actors::actor_run_state(actor_id).is_none()); assert!(Actors::pending_signal(actor_id)); let reprime_hot = pallet_deos_actors::ActorHot::::get(actor_id).expect("hot state remains"); assert_eq!(reprime_hot.wakeup_pointer, before_hot.wakeup_pointer); System::set_block_number(3); - let _ = Actors::on_idle(3, Weight::MAX); - assert!(Actors::continuation_state(actor_id).is_none()); + service_actor_idle(3); + assert!(Actors::actor_run_state(actor_id).is_none()); assert_eq!( Actors::active_actor_state(actor_id) - .expect("latched run completes") + .expect("latched run completes after cancelled cycle remains uncommitted") .identity .cycle_nonce, - 2 + 1 ); }); } #[cfg(feature = "dex-fixture")] #[test] - fn continuation_cancel_then_pure_close_preserves_sovereign_balance() { + fn run_cancel_then_pure_close_preserves_sovereign_balance() { new_test_ext().execute_with(|| { System::set_block_number(1); let contract = active_contract( @@ -1740,14 +1880,11 @@ mod tests { RuntimeOrigin::signed(ALICE), actor_id )); - let _ = Actors::on_idle(1, Weight::MAX); - assert!(Actors::continuation_state(actor_id).is_some()); + service_actor_idle(1); + assert!(Actors::actor_run_state(actor_id).is_some()); let balance_before = Balances::free_balance(sovereign); System::set_block_number(2); - assert_ok!(Actors::cancel_continuation( - RuntimeOrigin::signed(ALICE), - actor_id, - )); + assert_ok!(Actors::cancel_run(RuntimeOrigin::signed(ALICE), actor_id,)); assert_ok!(Actors::close_actor(RuntimeOrigin::signed(ALICE), actor_id)); assert!(Actors::active_actor_state(actor_id).is_none()); assert_eq!(Balances::free_balance(sovereign), balance_before); @@ -1785,16 +1922,16 @@ mod tests { RuntimeOrigin::signed(ALICE), actor_id )); - let _ = Actors::on_idle(1, Weight::MAX); + service_actor_idle(1); let actor = Actors::active_actor_state(actor_id).expect("actor remains after first failure"); assert_eq!(actor.hot.unsuccessful_attempt_streak, 1); assert_eq!(actor.hot.cycle_state, pallet_deos_actors::CycleState::Idle); - assert!(Actors::continuation_state(actor_id).is_none()); + assert!(Actors::actor_run_state(actor_id).is_none()); }); } #[test] - fn immutable_and_unsupported_paths_never_create_continuation() { + fn immutable_and_unsupported_paths_never_create_run_state() { new_test_ext().execute_with(|| { System::set_block_number(1); let unsupported_retry = active_contract( @@ -1835,7 +1972,7 @@ mod tests { actor_id )); let _ = Actors::on_idle(1, Weight::MAX); - assert!(Actors::continuation_state(actor_id).is_none()); + assert!(Actors::actor_run_state(actor_id).is_none()); assert_eq!( Actors::active_actor_state(actor_id) .expect("actor remains") @@ -1847,12 +1984,12 @@ mod tests { } #[test] - fn independent_runtime_binds_nonzero_continuation_weights() { - let suspend = as pallet_deos_actors::WeightInfo>::continuation_suspend(12); + fn independent_runtime_binds_nonzero_run_weights() { + let suspend = as pallet_deos_actors::WeightInfo>::run_suspend(); let retry = - as pallet_deos_actors::WeightInfo>::continuation_retry( + as pallet_deos_actors::WeightInfo>::run_retry( ); - let cancel = as pallet_deos_actors::WeightInfo>::continuation_cancel(); + let cancel = as pallet_deos_actors::WeightInfo>::run_cancel(); assert!(suspend.ref_time() > retry.ref_time()); assert!(cancel.proof_size() > retry.proof_size()); } @@ -1884,7 +2021,7 @@ mod tests { actor_id )); let _ = Actors::on_idle(1, Weight::MAX); - assert!(Actors::continuation_state(actor_id).is_some()); + assert!(Actors::actor_run_state(actor_id).is_some()); assert!(Actors::try_state(1).is_ok()); }); } @@ -1941,7 +2078,7 @@ mod tests { RuntimeOrigin::signed(ALICE), actor_id )); - let _ = Actors::on_idle(1, Weight::MAX); + run_until_not_running(actor_id); assert_eq!(Balances::free_balance(BOB), bob_before.saturating_add(5)); assert_eq!( Actors::active_actor_state(actor_id) @@ -1984,7 +2121,7 @@ mod tests { RuntimeOrigin::signed(ALICE), actor_id )); - let _ = Actors::on_idle(1, Weight::MAX); + service_actor_idle(1); assert_eq!( Balances::free_balance(DEX_SINK), sink_before.saturating_add(50) diff --git a/template/pallets/actors/src/adapters.rs b/template/pallets/actors/src/adapters.rs index cb7dc8e8..9807b89f 100644 --- a/template/pallets/actors/src/adapters.rs +++ b/template/pallets/actors/src/adapters.rs @@ -4,6 +4,7 @@ use frame::prelude::*; use polkadot_sdk::{ frame_support::PalletId, sp_runtime::{DispatchResult, Perbill}, + sp_weights::Weight, }; use crate::types::AddressEvent; @@ -41,8 +42,11 @@ pub struct ObservationTransition { /// Certified bounded ingress for one externally owned observation transition. pub trait ObservationTransitionIngress { - fn note_observation_transition(feed: FeedId, transition: ObservationTransition) - -> DispatchResult; + fn note_observation_transition( + feed: FeedId, + transition: ObservationTransition, + cause_provenance: crate::TriggerCauseProvenance, + ) -> DispatchResult; } /// Minimal authoritative actor context for adapter operations whose policy depends on Actors type. @@ -286,6 +290,156 @@ pub trait AssetOps { ) -> Result<(), TaskFailure>; } +/// Runtime-owned non-semantic authority committed by an Actor admission certificate. +#[derive( + Clone, Copy, Debug, Decode, DecodeWithMemTracking, Encode, Eq, MaxEncodedLen, PartialEq, TypeInfo, +)] +pub struct AdmissionCertificateAuthority { + pub runtime_actor_semantics_version: u32, + pub production_weight_identity: [u8; 32], + pub body_geometry_version: u32, + pub configured_bounds_commitment: [u8; 32], + pub maximum_lifecycle_weight: Weight, +} + +impl AdmissionCertificateAuthority { + pub fn compose_production_weight_identity( + control_identity: [u8; 32], + effect_identity: [u8; 32], + ) -> [u8; 32] { + ( + *b"DEOS_ACTOR_PRODUCTION_WEIGHT", + control_identity, + effect_identity, + ) + .using_encoded(frame::hashing::blake2_256) + } +} + +/// Fail-closed host boundary for the exact runtime authority used to admit Actor Contracts. +pub trait AdmissionCertificateAuthorityProvider { + fn current() -> Option; +} + +impl AdmissionCertificateAuthorityProvider for () { + fn current() -> Option { + None + } +} + +/// Exact physical location facts needed to price one lazy current-Step load. +#[derive( + Clone, Copy, Debug, Decode, DecodeWithMemTracking, Encode, Eq, MaxEncodedLen, PartialEq, TypeInfo, +)] +pub struct StepControlWeightContext { + pub cursor: u32, + pub steps_in_fragment: u32, + pub opening_tail_chunks: u32, + pub predicate_evaluation_units: u32, + pub opening_snapshot_entries: u32, + pub opening_predicate_results: u32, + pub funding_snapshot_entries: u32, +} + +/// Runtime-owned maximum Actor-control Weight used by admission before semantic evaluation. +#[derive( + Clone, Copy, Debug, Decode, DecodeWithMemTracking, Encode, Eq, MaxEncodedLen, PartialEq, TypeInfo, +)] +pub enum StepControlPhase { + Opening, + Running, + Suspended, +} + +#[derive( + Clone, Copy, Debug, Decode, DecodeWithMemTracking, Encode, Eq, MaxEncodedLen, PartialEq, TypeInfo, +)] +pub enum StepControlOutcome { + Continued, + Suspended, + Completed, + Failed, +} + +#[derive( + Clone, Copy, Debug, Decode, DecodeWithMemTracking, Encode, Eq, MaxEncodedLen, PartialEq, TypeInfo, +)] +pub enum StepControlPlacement { + None, + Queue, + Wakeup, +} + +#[derive( + Clone, Copy, Debug, Decode, DecodeWithMemTracking, Encode, Eq, MaxEncodedLen, PartialEq, TypeInfo, +)] +pub struct StepControlExecution { + pub phase: StepControlPhase, + pub outcome: StepControlOutcome, + pub placement: StepControlPlacement, +} + +pub trait StepControlWeightProvider { + fn production_weight_identity() -> Option<[u8; 32]>; + fn maximum_control_weight(context: StepControlWeightContext, step: &Step) -> Option; + fn actual_control_weight( + context: StepControlWeightContext, + step: &Step, + maximum: Weight, + execution: StepControlExecution, + ) -> Option; +} + +impl StepControlWeightProvider for () { + fn production_weight_identity() -> Option<[u8; 32]> { + None + } + + fn maximum_control_weight(_: StepControlWeightContext, _: &Step) -> Option { + None + } + + fn actual_control_weight( + _: StepControlWeightContext, + _: &Step, + _: Weight, + _: StepControlExecution, + ) -> Option { + None + } +} + +/// Exact post-dispatch branch owned by one current-Step Task effect. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum TaskEffectExecution { + /// Predicate evaluation, amount resolution, or funding classification ended before the Task + /// operation was invoked. The exact effect Weight is zero. + NotInvoked, + /// The canonical Task operation was invoked, whether it committed or returned a typed failure. + Invoked, +} + +/// Runtime-owned maximum and valid-actual Task-effect Weight authority. +pub trait TaskEffectWeightProvider { + fn production_weight_identity() -> Option<[u8; 32]>; + fn maximum_effect_weight(task: &Task) -> Option; + fn actual_effect_weight(task: &Task, execution: TaskEffectExecution) -> Option; +} + +impl TaskEffectWeightProvider for () { + fn production_weight_identity() -> Option<[u8; 32]> { + None + } + + fn maximum_effect_weight(_: &Task) -> Option { + None + } + + fn actual_effect_weight(_: &Task, _: TaskEffectExecution) -> Option { + None + } +} + /// Runtime staking operations. Required only when Stake or Unstake appears in a plan. pub trait StakingOps { fn stake(who: &AccountId, asset: AssetId, amount: Balance) -> Result<(), TaskFailure>; diff --git a/template/pallets/actors/src/benchmarking.rs b/template/pallets/actors/src/benchmarking.rs index 883282bc..c09abe28 100644 --- a/template/pallets/actors/src/benchmarking.rs +++ b/template/pallets/actors/src/benchmarking.rs @@ -2,9 +2,10 @@ extern crate alloc; +use crate::scheduler::AttemptTransactionError; use crate::types::Task as ActorTask; use crate::*; -use alloc::vec; +use alloc::{vec, vec::Vec}; use frame::prelude::*; use polkadot_sdk::frame_benchmarking::{account, v2::*}; use polkadot_sdk::frame_support::traits::Hooks; @@ -15,6 +16,10 @@ use polkadot_sdk::sp_runtime::Perbill; mod benches { use super::*; + const CROSSING_COHORT_BENCHMARK_MAX: u32 = 128; + const CROSSING_NON_TAIL_BENCHMARK_MAX: u32 = 64; + const CROSSING_TRIMMED_BENCHMARK_TAIL: u32 = CROSSING_NON_TAIL_BENCHMARK_MAX + 2; + #[derive(Clone)] struct Schedule { trigger: Trigger, @@ -29,11 +34,9 @@ mod benches { // account; benchmark setup must establish the same prerequisite explicitly. polkadot_sdk::frame_system::Pallet::::inc_providers(owner); let creation_fee = T::ActorCreationFee::get(); - if creation_fee.is_zero() { - return; - } + let hold_capacity: T::Balance = (u64::MAX / 4).saturated_into(); let amount = creation_fee - .saturating_add(T::TriggerStateBond::maximum()) + .saturating_add(hold_capacity) .saturating_add(One::one()); let _ = T::AssetOps::mint(owner, T::FeeNativeAssetId::get(), amount); } @@ -43,9 +46,8 @@ mod benches { slot: u8, contract_steps: &ContractSteps, ) { - let envelope = Pallet::::attempt_fee_envelope(ActorType::User, contract_steps, 0) - .expect("benchmark execution plan has a checked fee envelope"); - let required = T::MinUserBalance::get().saturating_add(envelope.total); + let required = Pallet::::user_pipeline_machine_capacity_requirement(contract_steps) + .expect("benchmark User Cycle has a checked prefunding requirement"); let sovereign = Pallet::::sovereign_account_id(owner, slot); let _ = T::AssetOps::mint(&sovereign, T::FeeNativeAssetId::get(), required); } @@ -59,20 +61,6 @@ mod benches { prefund_user_sovereign::(owner, slot, contract_steps); } - fn deplete_user_sovereign(actor_id: ActorId) { - let instance = - Pallet::::active_actor_view(actor_id).expect("benchmark actor must exist for depletion"); - let requirement = Pallet::::attempt_fee_envelope(ActorType::User, &instance.steps, 0) - .expect("benchmark execution plan has a checked fee envelope"); - let required = T::MinUserBalance::get().saturating_add(requirement.total); - T::AssetOps::burn( - &instance.sovereign_account, - T::FeeNativeAssetId::get(), - required, - ) - .expect("benchmark depletion must not overdraw the sovereign"); - } - fn user_contract( schedule: ScheduleOf, contract_steps: ContractSteps, @@ -127,6 +115,24 @@ mod benches { } } + fn opening_all( + predicates: alloc::vec::Vec>, + ) -> PreconditionOf { + let clause = BoundedVec::try_from( + predicates + .into_iter() + .map(|predicate| TimedPredicate { + timing: ObservationTiming::Opening, + predicate, + }) + .collect::>(), + ) + .expect("benchmark Opening predicates fit"); + Precondition { + clauses: BoundedVec::try_from(alloc::vec![clause]).expect("Opening clause fits"), + } + } + fn current_any( predicates: alloc::vec::Vec>, ) -> PreconditionOf { @@ -158,6 +164,30 @@ mod benches { BoundedVec::try_from(vec![step]).expect("single-step contract_steps must fit") } + fn make_max_contract_steps(recipient: T::AccountId) -> ContractSteps { + let step = Step { + precondition: None, + task: ActorTask::Transfer { + to: recipient, + asset: T::FeeNativeAssetId::get(), + amount: AmountResolution::AllAvailable, + }, + on_error: StepErrorPolicy::AbortCycle, + }; + BoundedVec::try_from(vec![step; T::MaxContractSteps::get() as usize]) + .expect("maximum Contract Steps must fit") + } + + fn assert_max_contract_geometry(actor_id: ActorId) { + let expected_tail_chunks = T::MaxContractSteps::get() + .saturating_sub(1) + .div_ceil(MAX_STEPS_PER_TAIL_CHUNK) as usize; + assert_eq!( + ActorContractTailChunks::::iter_prefix(actor_id).count(), + expected_tail_chunks + ); + } + fn make_tracked_funding_contract_steps(recipient: T::AccountId) -> ContractSteps { BoundedVec::try_from(vec![Step { precondition: None, @@ -286,11 +316,22 @@ mod benches { caller: T::AccountId, trigger: TriggerOf, ) -> ActorId { - ensure_creation_balance::(&caller); let recipient = T::AccountId::decode(&mut polkadot_sdk::sp_runtime::traits::TrailingZeroInput::zeroes()) .expect("decode zero account"); - let contract_steps = make_contract_steps::(recipient); + bench_create_user_with_trigger_and_steps::( + caller, + trigger, + make_contract_steps::(recipient), + ) + } + + fn bench_create_user_with_trigger_and_steps( + caller: T::AccountId, + trigger: TriggerOf, + contract_steps: ContractSteps, + ) -> ActorId { + ensure_creation_balance::(&caller); prefund_active_user_creation::(&caller, &contract_steps); let schedule = Schedule { trigger, @@ -315,7 +356,7 @@ mod benches { let recipient = T::AccountId::decode(&mut polkadot_sdk::sp_runtime::traits::TrailingZeroInput::zeroes()) .expect("decode zero account"); - let contract_steps = make_contract_steps::(recipient); + let contract_steps = make_max_contract_steps::(recipient); prefund_user_sovereign::(&caller, expected_slot, &contract_steps); let feed = observation_feed_pool::(1)[0]; let schedule = Schedule { @@ -332,6 +373,7 @@ mod benches { let inst = Pallet::::active_actor_view(actor_id).expect("Actors must exist after create_user_actor"); assert_eq!(inst.actor_class.owner_slot(), Some(expected_slot)); + assert_max_contract_geometry::(actor_id); assert!(CrossingMemberships::::contains_key(actor_id)); assert!(ActorObservationFeeds::::get(actor_id).is_none()); } @@ -344,7 +386,7 @@ mod benches { let recipient = T::AccountId::decode(&mut polkadot_sdk::sp_runtime::traits::TrailingZeroInput::zeroes()) .expect("decode zero account"); - let contract_steps = make_contract_steps::(recipient); + let contract_steps = make_max_contract_steps::(recipient); prefund_user_sovereign::(&caller, requested_slot, &contract_steps); let feed = observation_feed_pool::(1)[0]; let schedule = Schedule { @@ -362,6 +404,7 @@ mod benches { let inst = Pallet::::active_actor_view(actor_id) .expect("Actors must exist after create_user_actor_at_slot"); assert_eq!(inst.actor_class.owner_slot(), Some(requested_slot)); + assert_max_contract_geometry::(actor_id); assert!(CrossingMemberships::::contains_key(actor_id)); } @@ -371,7 +414,7 @@ mod benches { let recipient = T::AccountId::decode(&mut polkadot_sdk::sp_runtime::traits::TrailingZeroInput::zeroes()) .expect("decode zero account"); - let contract_steps = make_contract_steps::(recipient); + let contract_steps = make_max_contract_steps::(recipient); let feed = observation_feed_pool::(1)[0]; let schedule = Schedule { trigger: Trigger::observation_crossing(feed, CrossingDirection::Rising, u128::MAX, 0), @@ -393,6 +436,7 @@ mod benches { sovereign_id: actor_id, } ); + assert_max_contract_geometry::(actor_id); assert!(CrossingMemberships::::contains_key(actor_id)); } @@ -402,7 +446,7 @@ mod benches { let recipient = T::AccountId::decode(&mut polkadot_sdk::sp_runtime::traits::TrailingZeroInput::zeroes()) .expect("decode zero account"); - let contract_steps = make_contract_steps::(recipient.clone()); + let contract_steps = make_max_contract_steps::(recipient.clone()); let schedule = Schedule { trigger: Trigger::manual(), cooldown_blocks: 100, @@ -432,6 +476,7 @@ mod benches { system_contract::(crossing_schedule, contract_steps), ); assert!(Pallet::::active_actor_exists(fresh_id)); + assert_max_contract_geometry::(fresh_id); assert!(CrossingMemberships::::contains_key(fresh_id)); } @@ -476,13 +521,13 @@ mod benches { ensure_creation_balance::(&caller); let feed = observation_feed_pool::(1)[0]; let threshold = u128::MAX; - for index in 0..T::ObservationPageSize::get() { + for index in 0..T::CrossingPageSize::get() { let guard_owner: T::AccountId = account("crossing-page-guard", index, 0); let _ = bench_create_system_crossing::(guard_owner, feed, threshold); } let expected_slot = prefill_owner_slots_for_worst_case::(&caller); let recipient: T::AccountId = account("crossing-page-recipient", 0, 0); - let contract_steps = make_contract_steps::(recipient); + let contract_steps = make_max_contract_steps::(recipient); prefund_user_sovereign::(&caller, expected_slot, &contract_steps); let schedule = Schedule { trigger: Trigger::observation_crossing(feed, CrossingDirection::Rising, threshold, 0), @@ -500,6 +545,7 @@ mod benches { let actor_id = NextActorId::::get().saturating_sub(1); let locator = CrossingMemberships::::get(actor_id).expect("measured membership exists"); let leaf = CrossingLeafStates::::get(locator.key).expect("Crossing leaf exists"); + assert_max_contract_geometry::(actor_id); assert_eq!(locator.page, 1); assert_eq!(locator.offset, 0); assert_eq!(leaf.page_count, 2); @@ -536,7 +582,7 @@ mod benches { trigger: Trigger::observation_crossing(feed, CrossingDirection::Rising, u128::MAX, 0), cooldown_blocks: 100, }, - make_contract_steps::(recipient), + make_max_contract_steps::(recipient), ); #[extrinsic_call] activate_actor( @@ -546,6 +592,7 @@ mod benches { ); assert!(Pallet::::active_actor_exists(actor_id)); assert!(ActorIdentities::::contains_key(actor_id)); + assert_max_contract_geometry::(actor_id); assert!(CrossingMemberships::::contains_key(actor_id)); assert!(ActorObservationFeeds::::get(actor_id).is_none()); } @@ -554,7 +601,7 @@ mod benches { fn deactivate_actor() { let owner: T::AccountId = whitelisted_caller(); let recipient: T::AccountId = account("deactivate-recipient", 0, 0); - let contract_steps = make_contract_steps::(recipient); + let contract_steps = make_max_contract_steps::(recipient); let feed = observation_feed_pool::(1)[0]; Pallet::::create_system_actor( RawOrigin::Root.into(), @@ -570,11 +617,16 @@ mod benches { ) .expect("System Actors creation must succeed"); let actor_id = NextActorId::::get().saturating_sub(1); - install_continuation::(actor_id, T::MaxOpeningSnapshotEntries::get()); + install_run_state::(actor_id, T::MaxOpeningSnapshotEntries::get()); #[extrinsic_call] deactivate_actor(RawOrigin::Signed(owner), actor_id); assert!(!Pallet::::active_actor_exists(actor_id)); assert!(ActorIdentities::::contains_key(actor_id)); + assert!( + ActorContractTailChunks::::iter_prefix(actor_id) + .next() + .is_none() + ); assert!(!CrossingMemberships::::contains_key(actor_id)); } @@ -615,6 +667,78 @@ mod benches { assert!(inst.pending_signal); } + /// Measures one matched User AddressEvent occurrence without source-publication or funding-state + /// work: filter detection, exact Trigger capacity/collection, readiness materialization, and + /// canonical placement are the complete disjoint Actor-owned boundary. + #[benchmark(pov_mode = Measured)] + fn address_event_trigger_occurrence() { + let caller: T::AccountId = whitelisted_caller(); + let recipient: T::AccountId = account("address-event-recipient", 0, 0); + let contract_steps = make_contract_steps::(recipient); + ensure_creation_balance::(&caller); + prefund_active_user_creation::(&caller, &contract_steps); + Pallet::::create_user_actor( + RawOrigin::Signed(caller.clone()).into(), + Mutability::Mutable, + user_contract::( + Schedule { + trigger: Trigger::address_event(SourceFilter::Any, AssetFilter::Any), + cooldown_blocks: 0, + }, + contract_steps, + ), + ) + .expect("AddressEvent benchmark Actor exists"); + let actor_id = NextActorId::::get().saturating_sub(1); + frame_system::Pallet::::set_block_number(1u32.into()); + #[block] + { + Pallet::::notify_address_event(actor_id, T::FeeNativeAssetId::get(), One::one(), &caller) + .expect("matched AddressEvent occurrence commits"); + } + let hot = ActorHot::::get(actor_id).expect("AddressEvent Actor remains active"); + assert!(hot.pending_signal); + assert!(hot.queue_ticket.is_some() || hot.wakeup_pointer.is_some()); + } + + /// Measures the exact lifecycle-only cleanup selected when an Idle User cannot admit a paid + /// pending Pipeline. The maximum Contract is retained, while Crossing, ObservationChange, + /// temporal membership, and Run state are intentionally absent from this apoptosis branch. + #[benchmark] + fn pipeline_admission_apoptosis() { + let owner: T::AccountId = whitelisted_caller(); + let recipient: T::AccountId = account("pipeline-apoptosis-recipient", 0, 0); + let contract_steps = make_contract_steps::(recipient); + ensure_creation_balance::(&owner); + prefund_active_user_creation::(&owner, &contract_steps); + Pallet::::create_user_actor( + RawOrigin::Signed(owner).into(), + Mutability::Mutable, + user_contract::( + Schedule { + trigger: Trigger::Manual, + cooldown_blocks: 0u32.into(), + }, + contract_steps, + ), + ) + .expect("apoptosis benchmark Actor exists"); + let actor_id = NextActorId::::get().saturating_sub(1); + Pallet::::request_activation(actor_id).expect("apoptosis readiness must latch"); + let instance = Pallet::::active_actor_view(actor_id).expect("active Actor view exists"); + assert_eq!(instance.cycle_state, CycleState::Idle); + assert!(instance.pending_signal); + assert!(ActorRunStateStore::::get(actor_id).is_none()); + assert!(ActorObservationFeeds::::get(actor_id).is_none()); + assert!(CrossingMemberships::::get(actor_id).is_none()); + #[block] + { + Pallet::::finalize_actor(actor_id, &instance, CloseReason::CycleAdmissionInsufficient) + .expect("minimal Pipeline-admission apoptosis must succeed"); + } + assert!(!Pallet::::active_actor_exists(actor_id)); + } + #[benchmark] fn close_actor() { let owner: T::AccountId = whitelisted_caller(); @@ -626,7 +750,7 @@ mod benches { trigger: Trigger::observation_crossing(feed, CrossingDirection::Rising, u128::MAX, 0), cooldown_blocks: 1, }; - let contract_steps = make_contract_steps::(recipient); + let contract_steps = make_max_contract_steps::(recipient); prefund_user_sovereign::(&owner, owner_slot, &contract_steps); Pallet::::create_user_actor_at_slot( RawOrigin::Signed(owner.clone()).into(), @@ -658,10 +782,15 @@ mod benches { exhausted: false, }, ); - install_continuation::(actor_id, T::MaxOpeningSnapshotEntries::get()); + install_run_state::(actor_id, T::MaxOpeningSnapshotEntries::get()); #[extrinsic_call] close_actor(RawOrigin::Signed(owner), actor_id); assert!(!Pallet::::active_actor_exists(actor_id)); + assert!( + ActorContractTailChunks::::iter_prefix(actor_id) + .next() + .is_none() + ); assert!(!CrossingMemberships::::contains_key(actor_id)); assert!(ActorObservationFeeds::::get(actor_id).is_none()); assert!(!CrossingTransitionQueues::::contains_key(feed)); @@ -676,7 +805,7 @@ mod benches { ensure_creation_balance::(&owner); let feed = observation_feed_pool::(1)[0]; let threshold = u128::MAX; - for index in 0..T::ObservationPageSize::get() { + for index in 0..T::CrossingPageSize::get() { let guard_owner: T::AccountId = account("crossing-remove-page-guard", index, 0); let _ = bench_create_system_crossing::(guard_owner, feed, threshold); } @@ -698,7 +827,7 @@ mod benches { let actor_id = NextActorId::::get().saturating_sub(1); let removed = CrossingMemberships::::get(actor_id).expect("tail-page membership exists"); assert_eq!(removed.page, 1); - install_continuation::(actor_id, T::MaxOpeningSnapshotEntries::get()); + install_run_state::(actor_id, T::MaxOpeningSnapshotEntries::get()); #[block] { Pallet::::close_actor(RawOrigin::Signed(owner).into(), actor_id) @@ -709,7 +838,7 @@ mod benches { let leaf = CrossingLeafStates::::get(removed.key).expect("surviving leaf exists"); assert_eq!(leaf.tail_page, 0); assert_eq!(leaf.page_count, 1); - assert_eq!(leaf.member_count, T::ObservationPageSize::get()); + assert_eq!(leaf.member_count, T::CrossingPageSize::get()); } // Diagnostic removal branch: remove the tail member while its leaf page survives. @@ -738,7 +867,7 @@ mod benches { .expect("tail-removal User setup must succeed"); let actor_id = NextActorId::::get().saturating_sub(1); let removed = CrossingMemberships::::get(actor_id).expect("tail membership exists"); - install_continuation::(actor_id, T::MaxOpeningSnapshotEntries::get()); + install_run_state::(actor_id, T::MaxOpeningSnapshotEntries::get()); #[block] { Pallet::::close_actor(RawOrigin::Signed(owner).into(), actor_id) @@ -791,7 +920,7 @@ mod benches { exhausted: false, }, ); - install_continuation::(actor_id, T::MaxOpeningSnapshotEntries::get()); + install_run_state::(actor_id, T::MaxOpeningSnapshotEntries::get()); #[block] { Pallet::::close_actor(RawOrigin::Signed(owner).into(), actor_id) @@ -830,7 +959,7 @@ mod benches { let tail_owner: T::AccountId = account("crossing-middle-tail", 0, 0); let tail_id = bench_create_system_crossing::(tail_owner, feed, threshold); let removed = CrossingMemberships::::get(actor_id).expect("middle membership exists"); - install_continuation::(actor_id, T::MaxOpeningSnapshotEntries::get()); + install_run_state::(actor_id, T::MaxOpeningSnapshotEntries::get()); #[block] { Pallet::::close_actor(RawOrigin::Signed(owner).into(), actor_id) @@ -868,7 +997,7 @@ mod benches { ) .expect("ObservationChange close benchmark setup must succeed"); let actor_id = NextActorId::::get().saturating_sub(1); - install_continuation::(actor_id, T::MaxOpeningSnapshotEntries::get()); + install_run_state::(actor_id, T::MaxOpeningSnapshotEntries::get()); mark_observation_chain_dirty::(&feeds); #[block] { @@ -920,7 +1049,7 @@ mod benches { bench_create_user_with_trigger::(caller.clone(), observation_trigger::(replaced)); let dirty_chain = alloc::vec![feeds[1], replaced, guard_high]; mark_observation_chain_dirty::(&dirty_chain); - install_continuation::(actor_id, T::MaxOpeningSnapshotEntries::get()); + install_run_state::(actor_id, T::MaxOpeningSnapshotEntries::get()); ActorHot::::mutate(actor_id, |maybe_hot| { maybe_hot .as_mut() @@ -984,11 +1113,13 @@ mod benches { fn update_contract() { let caller: T::AccountId = whitelisted_caller(); let feed = observation_feed_pool::(1)[0]; - let actor_id = bench_create_user_with_trigger::( + let old_recipient: T::AccountId = account("crossing-update-old-recipient", 0, 0); + let actor_id = bench_create_user_with_trigger_and_steps::( caller.clone(), Trigger::observation_crossing(feed, CrossingDirection::Rising, u128::MAX - 1, 0), + make_max_contract_steps::(old_recipient), ); - install_continuation::(actor_id, T::MaxOpeningSnapshotEntries::get()); + install_run_state::(actor_id, T::MaxOpeningSnapshotEntries::get()); ActorHot::::mutate(actor_id, |maybe_hot| { maybe_hot .as_mut() @@ -1006,7 +1137,7 @@ mod benches { } }); let recipient = account("crossing-update-recipient", 0, 0); - let replacement = make_contract_steps::(recipient); + let replacement = make_max_contract_steps::(recipient); let mut allowed: BoundedBTreeSet = BoundedBTreeSet::default(); for index in 0..T::MaxWhitelistSize::get() { @@ -1034,6 +1165,7 @@ mod benches { } let locator = CrossingMemberships::::get(actor_id).expect("Crossing replacement membership must exist"); + assert_max_contract_geometry::(actor_id); assert_eq!(locator.key.threshold, u128::MAX); assert!(ActorObservationFeeds::::get(actor_id).is_none()); } @@ -1045,6 +1177,66 @@ mod benches { assert!(GlobalCircuitBreaker::::get()); } + #[benchmark] + fn record_crossing_worker_fault() { + let fault = CrossingWorkerFault { + feed: observation_feed_pool::(1)[0], + revision: Some(u64::MAX), + threshold: Some(u128::MAX), + class: CrossingWorkerFaultClass::Other, + }; + let mut meter = polkadot_sdk::sp_weights::WeightMeter::with_limit(Weight::MAX); + #[block] + { + assert!(Pallet::::record_crossing_worker_fault(&mut meter, fault)); + } + assert_eq!(CrossingWorkerFaultState::::get(), Some(fault)); + } + + #[benchmark] + fn record_observation_fanout_worker_fault() { + let feed = observation_feed_pool::(1)[0]; + let owner: T::AccountId = account("observation-fault", 0, 0); + let actor_id = bench_create_system_observation::(owner, feed); + let fault = ObservationFanoutWorkerFault { + feed, + revision: u64::MAX, + subscriber_page: Some(u32::MAX), + subscriber_position: u32::MAX, + actor_id: Some(actor_id), + semantic_contract_id: Some([u8::MAX; 32]), + body_commitment: Some([u8::MAX; 32]), + admission_identity: Some([u8::MAX; 32]), + branch: ObservationFanoutBranch::Terminal, + class: CrossingWorkerFaultClass::Other, + }; + let mut meter = polkadot_sdk::sp_weights::WeightMeter::with_limit(Weight::MAX); + #[block] + { + core::hint::black_box(ObservationSubscriberPages::::get(feed, 0)); + core::hint::black_box(ActorActivationAuthorities::::get(actor_id)); + assert!(Pallet::::record_observation_fanout_worker_fault( + &mut meter, fault + )); + } + assert_eq!(ObservationFanoutWorkerFaultState::::get(), Some(fault)); + } + + #[benchmark] + fn record_wakeup_worker_fault() { + let fault = WakeupWorkerFault { + key: WakeupKey::Tick(u64::MAX), + page: u64::MAX, + class: CrossingWorkerFaultClass::Other, + }; + let mut meter = polkadot_sdk::sp_weights::WeightMeter::with_limit(Weight::MAX); + #[block] + { + assert!(Pallet::::record_wakeup_worker_fault(&mut meter, fault)); + } + assert_eq!(WakeupWorkerFaultState::::get(), Some(fault)); + } + #[benchmark] fn clear_crossing_worker_fault() { CrossingWorkerFaultState::::put(CrossingWorkerFault { @@ -1064,6 +1256,12 @@ mod benches { feed: observation_feed_pool::(1)[0], revision: 1, subscriber_page: Some(0), + subscriber_position: 0, + actor_id: None, + semantic_contract_id: None, + body_commitment: None, + admission_identity: None, + branch: ObservationFanoutBranch::Ordinary, class: CrossingWorkerFaultClass::Invariant, }); #[extrinsic_call] @@ -1122,9 +1320,14 @@ mod benches { ) .expect("create_user_actor must succeed in permissionless_sweep_many setup"); let actor_id = NextActorId::::get().saturating_sub(1); - // Restore the zombie fixture state: swept actors must be balance-exhausted so the - // sweep closes them, keeping the prefunding admission honest but the postcondition real. - deplete_user_sovereign::(actor_id); + // Permissionless sweep no longer predicts economic affordability. Use the canonical + // nonce terminal so every requested live Actor still exercises bounded close cleanup. + ActorIdentities::::mutate(actor_id, |maybe| { + maybe + .as_mut() + .expect("benchmark actor identity exists") + .cycle_nonce = u64::MAX; + }); actor_ids .try_push(actor_id) .expect("benchmark n must fit MaxSweepBatch"); @@ -1544,13 +1747,108 @@ mod benches { NextActorId::::get().saturating_sub(1) } + fn admitted_contract_geometry( + actor_id: ActorId, + contract: &ActorContractOf, + ) -> ( + ActorAdmissionCertificateOf, + ActorContractHeadOf, + Vec<(u32, ActorStepChunkOf)>, + ) { + let certificate = ActorAdmissionCertificate::new( + contract.semantic_contract_id(), + contract + .body_commitment() + .expect("bounded benchmark body commitment"), + 1, + [2u8; 32], + 1, + [3u8; 32], + Weight::zero(), + ); + let (head, chunks) = Pallet::::decompose_admitted_contract_geometry( + actor_id, + ActorType::System, + contract, + &certificate, + ) + .expect("admitted benchmark Contract decomposes"); + (certificate, head, chunks) + } + + fn benchmark_queue_entry( + ticket: u64, + actor_id: ActorId, + ) -> QueueEntry> { + QueueEntry { + actor_id, + cycle_nonce: 0, + cursor: 0, + ticket, + eligible_at: Zero::zero(), + contract_commitment: ActorContractCommitment { + semantic_contract_id: [0; 32], + body_commitment: [0; 32], + }, + } + } + + fn install_chunked_contract(actor_id: ActorId, contract: &ActorContractOf) { + let (certificate, head, chunks) = admitted_contract_geometry::(actor_id, contract); + ActorRunStateStore::::mutate(actor_id, |maybe| { + if let Some(run) = maybe { + run.contract_authority = ActorRunAuthority { + semantic_contract_id: certificate.semantic_contract_id, + body_commitment: certificate.body_commitment, + admission_identity: certificate.admission_identity, + }; + } + }); + ActorAdmissionCertificates::::insert(actor_id, certificate); + ActorContractHeads::::insert(actor_id, head); + for (chunk_index, chunk) in chunks { + ActorContractTailChunks::::insert(actor_id, chunk_index, chunk); + } + } + + fn bench_create_user_observation_with_cooldown( + owner: T::AccountId, + feed: T::ObservationFeedId, + cooldown_blocks: u32, + ) -> ActorId { + let contract_steps = make_inert_contract_steps::(); + ensure_creation_balance::(&owner); + prefund_active_user_creation::(&owner, &contract_steps); + Pallet::::create_user_actor( + RawOrigin::Signed(owner).into(), + Mutability::Mutable, + user_contract::( + Schedule { + trigger: Trigger::observation_change(feed), + cooldown_blocks, + }, + contract_steps, + ), + ) + .expect("User observation benchmark actor creation must succeed"); + NextActorId::::get().saturating_sub(1) + } + fn bench_create_system_observation( owner: T::AccountId, feed: T::ObservationFeedId, + ) -> ActorId { + bench_create_system_observation_with_cooldown::(owner, feed, 0) + } + + fn bench_create_system_observation_with_cooldown( + owner: T::AccountId, + feed: T::ObservationFeedId, + cooldown_blocks: u32, ) -> ActorId { let schedule = Schedule { trigger: Trigger::observation_change(feed), - cooldown_blocks: 0, + cooldown_blocks, }; Pallet::::create_system_actor( RawOrigin::Root.into(), @@ -1562,6 +1860,29 @@ mod benches { NextActorId::::get().saturating_sub(1) } + fn bench_create_expiring_system_observation( + owner: T::AccountId, + feed: T::ObservationFeedId, + ) -> (ActorId, BlockNumberFor) { + let now = frame_system::Pallet::::block_number(); + let end = now.saturating_add(T::MinWindowLength::get()); + let schedule = Schedule { + trigger: Trigger::observation_change(feed), + cooldown_blocks: 0, + }; + let mut contract = system_contract::(schedule, make_inert_contract_steps::()) + .expect("system observation contract exists"); + contract.window = Some(ScheduleWindow { start: now, end }); + Pallet::::create_system_actor( + RawOrigin::Root.into(), + owner, + Mutability::Mutable, + Some(contract), + ) + .expect("expiring observation benchmark actor creation must succeed"); + (NextActorId::::get().saturating_sub(1), end) + } + fn bench_create_system_crossing( owner: T::AccountId, feed: T::ObservationFeedId, @@ -1581,6 +1902,13 @@ mod benches { NextActorId::::get().saturating_sub(1) } + fn clear_indexed_detection_disablement() { + let actor_ids = IndexedTriggerDetectionDisabled::::iter_keys().collect::>(); + for actor_id in actor_ids { + IndexedTriggerDetectionDisabled::::remove(actor_id); + } + } + fn prepare_crossing_work(threshold: u128) -> (T::ObservationFeedId, ActorId) { let feed = T::BenchmarkHelper::setup_observation_feeds(1) .expect("Crossing benchmark feed must be available") @@ -1606,7 +1934,7 @@ mod benches { fn prepare_non_tail_crossing_batch(tail_members: u32) -> alloc::vec::Vec { let (feed, first_actor) = prepare_crossing_work::(2); - let total = T::ObservationPageSize::get().saturating_add(tail_members); + let total = T::CrossingPageSize::get().saturating_add(tail_members); let mut actors = alloc::vec![first_actor]; for index in 0..total.saturating_sub(1) { let owner: T::AccountId = account("crossing-non-tail-unit", index, tail_members); @@ -1644,11 +1972,11 @@ mod benches { NextActorId::::get().saturating_sub(1) } - fn install_continuation(actor_id: ActorId, snapshot_entries: u32) { + fn install_run_state(actor_id: ActorId, snapshot_entries: u32) { let bounded = snapshot_entries.min(T::MaxOpeningSnapshotEntries::get()); let asset_count = bounded.saturating_add(1) / 2; let assets = T::BenchmarkHelper::funding_assets(asset_count); - let mut opening_snapshot = ContinuationSnapshotOf::::default(); + let mut opening_snapshot = RunOpeningSnapshotOf::::default(); for asset in assets { if opening_snapshot.len() as u32 >= bounded { break; @@ -1664,15 +1992,13 @@ mod benches { .expect("benchmark snapshot staking entry fits"); } assert_eq!(opening_snapshot.len() as u32, bounded); - ActorContracts::::mutate(actor_id, |maybe_contract| { - maybe_contract - .as_mut() - .expect("benchmark actor contract exists") - .steps[0] - .on_error = StepErrorPolicy::RetryLater { - max_attempts: T::MaxRetryAttempts::get(), - }; - }); + let mut admitted_contract = + Pallet::::load_actor_contract(actor_id).expect("benchmark actor contract exists"); + admitted_contract.steps[0].on_error = StepErrorPolicy::RetryLater { + max_attempts: T::MaxRetryAttempts::get(), + }; + Pallet::::store_actor_contract(actor_id, admitted_contract) + .expect("benchmark retry Contract remains admitted"); ActorHot::::mutate(actor_id, |maybe_hot| { let hot = maybe_hot .as_mut() @@ -1682,26 +2008,79 @@ mod benches { hot.queue_ticket = None; hot.wakeup_pointer = None; }); - ActorIdentities::::mutate(actor_id, |maybe_identity| { - maybe_identity - .as_mut() - .expect("benchmark actor identity exists") - .cycle_nonce = 1; - }); - ContinuationStateStore::::insert( + let contract = + Pallet::::load_actor_contract(actor_id).expect("benchmark Actor Contract exists"); + let last_attempt_block = 1u32.into(); + let eligible_at = Pallet::::suspension_eligible_at( + contract.cooldown_blocks, + contract.window, + last_attempt_block, + 1, + ) + .expect("benchmark retry target is representable"); + let admission = ActorAdmissionCertificates::::get(actor_id) + .expect("benchmark admission certificate exists"); + ActorRunStateStore::::insert( actor_id, - ContinuationState { + ActorRunState { + contract_authority: ActorRunAuthority { + semantic_contract_id: admission.semantic_contract_id, + body_commitment: admission.body_commitment, + admission_identity: admission.admission_identity, + }, + cycle_nonce: 1, cursor: 0, + opening_predicate_cursor: 0, unsuccessful_attempts_at_cursor: 1, - last_attempt_block: 1u32.into(), + last_attempt_block, + last_committed_step_block: None, + eligible_at, opening_snapshot, opening_predicate_results: Default::default(), funding_snapshot: Default::default(), cumulative_outcomes: OutcomeTotals::default(), + last_step_outcome: Some(StepOutcome::FundingUnavailable), + suspension: Some(SuspensionReason::FundingUnavailable), }, ); } + fn maximize_run_state_geometry(state: &mut ActorRunStateOf) { + let assets = T::BenchmarkHelper::funding_assets(T::MaxFundingTrackedAssets::get()); + for asset in assets { + state + .funding_snapshot + .try_insert(asset, One::one()) + .expect("benchmark funding snapshot entry fits"); + } + assert_eq!( + state.funding_snapshot.len() as u32, + T::MaxFundingTrackedAssets::get() + ); + for index in 0..T::MaxOpeningPredicateResults::get() { + state + .opening_predicate_results + .try_push(if index % 2 == 0 { + Ok(true) + } else { + Err(PredicateError::InvalidObservation) + }) + .expect("benchmark Opening predicate result fits"); + } + state.cumulative_outcomes = OutcomeTotals { + executed_steps: u32::MAX, + committed_effectful_tasks: u32::MAX, + precondition_skips: u32::MAX, + skipped_resolution: u32::MAX, + skipped_funding_unavailable: u32::MAX, + failed_steps: u32::MAX, + }; + state.last_step_outcome = Some(StepOutcome::Failed(TaskFailure::temporary( + Error::::ActorInvariant, + ))); + state.suspension = Some(SuspensionReason::Temporary); + } + fn install_wakeup_cursor_page(page_id: WakeupPageId, len: u32) { let page_size = T::WakeupPageSize::get(); let page_start = u32::try_from(page_id) @@ -1729,14 +2108,23 @@ mod benches { } fn clear_host_genesis_wakeup_placements() { - let actor_ids = ActorHot::::iter() + let block_actor_ids = ActorHot::::iter() .filter_map(|(actor_id, hot)| hot.wakeup_pointer.map(|_| actor_id)) .collect::>(); - for actor_id in actor_ids { + for actor_id in block_actor_ids { Pallet::::wakeup_substrate_invalidate(actor_id) - .expect("host genesis wakeup placement must be removable"); + .expect("host genesis Pipeline wakeup placement must be removable"); + } + let trigger_actor_ids = ActorHot::::iter() + .filter_map(|(actor_id, hot)| hot.trigger_wakeup_pointer.map(|_| actor_id)) + .collect::>(); + for actor_id in trigger_actor_ids { + Pallet::::trigger_wakeup_substrate_invalidate_inner(actor_id) + .expect("host genesis Trigger wakeup placement is coherent") + .expect("host genesis Trigger wakeup placement must be removable"); } assert_eq!(WakeupCursorLen::::get(WakeupClock::Block), 0); + assert_eq!(WakeupCursorLen::::get(WakeupClock::Tick), 0); } fn add_wakeup_cursor_page(page_ids: &mut alloc::vec::Vec, index: u32, size: u32) { @@ -1791,9 +2179,11 @@ mod benches { let first = page_id.saturating_mul(page_size); let len = page_size.min(capacity.saturating_sub(first)); let entries = (0..len) - .map(|offset| QueueEntry { - ticket: u64::from(first.saturating_add(offset)), - actor_id: u64::MAX.saturating_sub(u64::from(first.saturating_add(offset))), + .map(|offset| { + benchmark_queue_entry::( + u64::from(first.saturating_add(offset)), + u64::MAX.saturating_sub(u64::from(first.saturating_add(offset))), + ) }) .collect::>(); QueuePages::::insert( @@ -1845,12 +2235,11 @@ mod benches { let actor_id = NextActorId::::get().saturating_sub(1); let recipient = Pallet::::sovereign_account_id_system(actor_id); frame_system::Pallet::::set_block_number(1u32.into()); - ActorContracts::::mutate(actor_id, |maybe| { - maybe - .as_mut() - .expect("benchmark Actor Contract exists") - .funding = FundingSourcePolicy::AnyVerifiedIngress; - }); + let mut contract = + Pallet::::load_actor_contract(actor_id).expect("benchmark Actor Contract exists"); + contract.funding = FundingSourcePolicy::AnyVerifiedIngress; + Pallet::::store_actor_contract(actor_id, contract) + .expect("benchmark ingress Contract remains admitted"); ActorFunding::::mutate(actor_id, |maybe| { let funding = maybe.as_mut().expect("benchmark actor funding exists"); funding @@ -1862,6 +2251,43 @@ mod benches { (actor_id, recipient) } + #[benchmark] + fn scheduler_on_initialize_cutoff() { + NextQueueTicket::::put(7); + CurrentBlockResourceState::::kill(); + #[block] + { + let now = frame_system::Pallet::::block_number(); + assert!(CurrentBlockResourceState::::get().is_none()); + let budget = T::BlockResourceBudget::get(); + let mut state = BlockResourceState::new(now); + state.begin_prepass().expect("benchmark prepass opens"); + let mut reservation = state + .reserve( + budget.limits(), + BlockResourceDomain::ActorControl, + Weight::zero(), + ) + .expect("zero cutoff owner reserves"); + PrepassExecutionCutoff::::put((now, NextQueueTicket::::get())); + state + .settle(&mut reservation, Weight::zero()) + .expect("zero cutoff owner settles"); + state + .open_external_phase() + .expect("empty benchmark prepass closes"); + CurrentBlockResourceState::::put(state); + } + assert_eq!( + PrepassExecutionCutoff::::get(), + Some((frame_system::Pallet::::block_number(), 7)) + ); + assert_eq!( + CurrentBlockResourceState::::get().map(|state| state.phase()), + Some(BlockResourcePhase::ExternalPhase) + ); + } + #[benchmark] fn scheduler_on_idle_base() { let threshold = T::MaxIdleStarvationBlocks::get().max(1); @@ -1871,8 +2297,28 @@ mod benches { IdleStarvationState::::put(IdleStarvationPhase::Starving { consecutive_blocks: 1, }); + let mut state = BlockResourceState::new(now); + state.begin_prepass().expect("benchmark state opens"); // deos-bypass: panic-owner — fresh benchmark state has no reservations. + state + .open_external_phase() + .expect("benchmark prepass closes"); // deos-bypass: panic-owner — preceding transition establishes empty PrepassExecuting. + CurrentBlockResourceState::::put(state); #[block] { + let mut resource_state = + CurrentBlockResourceState::::get().expect("benchmark resource state exists"); // deos-bypass: panic-owner — setup writes this exact storage value. + let limits = T::BlockResourceBudget::get().limits(); + let mut reservation = resource_state + .reserve( + limits, + BlockResourceDomain::ActorControl, + limits.actor_control(), + ) + .expect("benchmark Actor Control reservation fits"); // deos-bypass: panic-owner — maximum equals the configured empty Actor Control limit. + resource_state + .settle(&mut reservation, Weight::zero()) + .expect("benchmark reservation settles"); // deos-bypass: panic-owner — zero actual is component-wise within the reserved maximum. + CurrentBlockResourceState::::put(resource_state); let _breaker_active = GlobalCircuitBreaker::::get(); core::hint::black_box(QueueHead::::get()); core::hint::black_box(QueueTail::::get()); @@ -1915,30 +2361,536 @@ mod benches { assert!(!IdleStarvationState::::exists()); } - #[benchmark] - fn scheduler_actor_state_probe() { - let actor_id = bench_create_system_manual::(3_000); - install_continuation::(actor_id, T::MaxOpeningSnapshotEntries::get()); - let assets = T::BenchmarkHelper::funding_assets(T::MaxFundingTrackedAssets::get()); - ActorFunding::::mutate(actor_id, |maybe_funding| { - let funding = maybe_funding - .as_mut() - .expect("benchmark actor funding state must exist"); - for asset in assets { - funding - .funding_tracked_assets - .try_insert(asset) - .expect("benchmark tracked asset fits"); - funding - .funding_accumulated - .try_insert(asset, One::one()) - .expect("benchmark funding entry fits"); - } - }); - #[block] - { - core::hint::black_box(Pallet::::load_actor_state(actor_id)); - } + #[benchmark(pov_mode = Measured)] + fn benchmark_monolithic_create(n: Linear<1, 8>) { + let actor_id = 2_940; + let contract = system_contract::( + Schedule { + trigger: Trigger::manual(), + cooldown_blocks: 0, + }, + inert_contract_steps_of_len::(n), + ) + .expect("active benchmark Contract"); + #[block] + { + Pallet::::store_actor_contract(actor_id, contract) + .expect("benchmark Contract remains admitted"); + } + assert!(ActorContractHeads::::contains_key(actor_id)); + } + + #[benchmark(pov_mode = Measured)] + fn benchmark_chunked_create(c: Linear<0, 2>) { + let actor_id = 2_951; + let n = 1u32.saturating_add(c.saturating_mul(4).min(7)); + let contract = system_contract::( + Schedule { + trigger: Trigger::manual(), + cooldown_blocks: 0, + }, + inert_contract_steps_of_len::(n), + ) + .expect("active benchmark Contract"); + let (certificate, head, chunks) = admitted_contract_geometry::(actor_id, &contract); + let chunk_count = u32::try_from(chunks.len()).expect("bounded chunk count fits u32"); + assert_eq!(chunk_count, c); + core::hint::black_box((head, chunks)); + #[block] + { + assert!(Pallet::::insert_admitted_contract_geometry( + actor_id, + &contract, + &certificate, + )); + } + assert!(ActorAdmissionCertificates::::contains_key(actor_id)); + assert!(ActorContractHeads::::contains_key(actor_id)); + for chunk_index in 0..chunk_count { + assert!(ActorContractTailChunks::::contains_key( + actor_id, + chunk_index + )); + } + } + + #[benchmark(pov_mode = Measured)] + fn benchmark_monolithic_close(n: Linear<1, 8>) { + let actor_id = bench_create_system_with_plan::(2_960, inert_contract_steps_of_len::(n)); + #[block] + { + core::hint::black_box( + Pallet::::remove_actor_contract(actor_id) + .expect("benchmark Contract removes coherently"), + ); + } + assert!(!ActorContractHeads::::contains_key(actor_id)); + } + + #[benchmark(pov_mode = Measured)] + fn benchmark_chunked_close(c: Linear<0, 2>) { + let n = 1u32.saturating_add(c.saturating_mul(4).min(7)); + let actor_id = bench_create_system_with_plan::(2_971, inert_contract_steps_of_len::(n)); + let contract = Pallet::::load_actor_contract(actor_id).expect("benchmark Contract exists"); + install_chunked_contract::(actor_id, &contract); + let chunk_count = c; + #[block] + { + core::hint::black_box( + Pallet::::remove_admitted_contract_geometry(actor_id) + .expect("benchmark geometry removes coherently"), + ); + } + assert!(!ActorAdmissionCertificates::::contains_key(actor_id)); + assert!(!ActorContractHeads::::contains_key(actor_id)); + for chunk_index in 0..chunk_count { + assert!(!ActorContractTailChunks::::contains_key( + actor_id, + chunk_index + )); + } + } + + #[benchmark(pov_mode = Measured)] + fn benchmark_monolithic_update(n: Linear<1, 8>) { + let actor_id = bench_create_system_with_plan::(2_975, inert_contract_steps_of_len::(n)); + let mut replacement = + Pallet::::load_actor_contract(actor_id).expect("benchmark Contract exists"); + replacement.cooldown_blocks = 1; + #[block] + { + Pallet::::store_actor_contract(actor_id, replacement) + .expect("benchmark replacement remains admitted"); + } + assert_eq!( + Pallet::::load_actor_contract(actor_id).map(|contract| contract.cooldown_blocks), + Some(1) + ); + } + + #[benchmark(pov_mode = Measured)] + fn benchmark_monolithic_reconstruct(n: Linear<1, 8>) { + let actor_id = bench_create_system_with_plan::(2_977, inert_contract_steps_of_len::(n)); + #[block] + { + let contract = Pallet::::load_actor_contract(actor_id).expect("benchmark Contract exists"); + let semantic_contract_id = contract.semantic_contract_id(); + let body_commitment = contract + .body_commitment() + .expect("bounded benchmark body commitment"); + core::hint::black_box((contract, semantic_contract_id, body_commitment)); + } + } + + #[benchmark(pov_mode = Measured)] + fn benchmark_chunked_reconstruct(c: Linear<0, 2>) { + let n = 1u32.saturating_add(c.saturating_mul(4).min(7)); + let actor_id = bench_create_system_with_plan::(2_979, inert_contract_steps_of_len::(n)); + let contract = Pallet::::load_actor_contract(actor_id).expect("benchmark Contract exists"); + install_chunked_contract::(actor_id, &contract); + assert_eq!( + u32::try_from(n.saturating_sub(1).div_ceil(4)).expect("chunk count fits"), + c + ); + #[block] + { + core::hint::black_box( + Pallet::::load_admitted_contract_geometry(actor_id) + .expect("benchmark geometry reconstructs coherently"), + ); + } + } + + #[benchmark(pov_mode = Measured)] + fn benchmark_monolithic_load_first() { + let actor_id = bench_create_system_with_plan::(2_980, inert_contract_steps_of_len::(1)); + #[block] + { + let contract = Pallet::::load_actor_contract(actor_id).expect("benchmark Contract exists"); + core::hint::black_box(contract.steps.first().expect("benchmark Step 0 exists")); + } + } + + #[benchmark(pov_mode = Measured)] + fn benchmark_monolithic_load_tail(n: Linear<2, 8>) { + let actor_id = bench_create_system_with_plan::(2_991, inert_contract_steps_of_len::(n)); + let current = n.saturating_sub(1); + #[block] + { + let contract = Pallet::::load_actor_contract(actor_id).expect("benchmark Contract exists"); + core::hint::black_box( + contract + .steps + .get(current as usize) + .expect("benchmark tail Step exists"), + ); + } + } + + #[benchmark(pov_mode = Measured)] + fn benchmark_chunked_load_first() { + let actor_id = bench_create_system_with_plan::(2_992, inert_contract_steps_of_len::(1)); + let contract = Pallet::::load_actor_contract(actor_id).expect("benchmark Contract exists"); + install_chunked_contract::(actor_id, &contract); + #[block] + { + core::hint::black_box( + Pallet::::load_current_step_from_storage(actor_id, 0) + .expect("benchmark head Step loads coherently"), + ); + } + } + + #[benchmark(pov_mode = Measured)] + fn benchmark_chunked_load_tail(s: Linear<1, 4>) { + let n = 1u32.saturating_add(s); + let actor_id = bench_create_system_with_plan::(2_993, inert_contract_steps_of_len::(n)); + let contract = Pallet::::load_actor_contract(actor_id).expect("benchmark Contract exists"); + install_chunked_contract::(actor_id, &contract); + let current = s; + #[block] + { + core::hint::black_box( + Pallet::::load_current_step_from_storage(actor_id, current) + .expect("benchmark tail Step loads coherently"), + ); + } + } + + #[benchmark(pov_mode = Measured)] + fn contract_geometry_create(c: Linear<0, 8>) { + let c = c.min( + T::MaxContractSteps::get() + .saturating_sub(1) + .div_ceil(MAX_STEPS_PER_TAIL_CHUNK), + ); + let actor_id = 2_994; + let n = 1u32.saturating_add(c.saturating_mul(4).min(31)); + let contract = system_contract::( + Schedule { + trigger: Trigger::manual(), + cooldown_blocks: 0, + }, + inert_contract_steps_of_len::(n), + ) + .expect("active benchmark Contract"); + let (certificate, _, chunks) = admitted_contract_geometry::(actor_id, &contract); + assert_eq!(u32::try_from(chunks.len()).expect("chunk count fits"), c); + #[block] + { + assert!(Pallet::::insert_admitted_contract_geometry( + actor_id, + &contract, + &certificate, + )); + } + assert!(ActorAdmissionCertificates::::contains_key(actor_id)); + assert!(ActorContractHeads::::contains_key(actor_id)); + } + + #[benchmark(pov_mode = Measured)] + fn contract_geometry_close(c: Linear<0, 8>) { + let c = c.min( + T::MaxContractSteps::get() + .saturating_sub(1) + .div_ceil(MAX_STEPS_PER_TAIL_CHUNK), + ); + let n = 1u32.saturating_add(c.saturating_mul(4).min(31)); + let actor_id = bench_create_system_with_plan::(2_995, inert_contract_steps_of_len::(n)); + let contract = Pallet::::load_actor_contract(actor_id).expect("benchmark Contract exists"); + install_chunked_contract::(actor_id, &contract); + #[block] + { + core::hint::black_box( + Pallet::::remove_admitted_contract_geometry(actor_id) + .expect("benchmark geometry removes coherently"), + ); + } + assert!(!ActorAdmissionCertificates::::contains_key(actor_id)); + assert!(!ActorContractHeads::::contains_key(actor_id)); + } + + #[benchmark(pov_mode = Measured)] + fn contract_geometry_reconstruct(c: Linear<0, 8>) { + let c = c.min( + T::MaxContractSteps::get() + .saturating_sub(1) + .div_ceil(MAX_STEPS_PER_TAIL_CHUNK), + ); + let n = 1u32.saturating_add(c.saturating_mul(4).min(31)); + let actor_id = bench_create_system_with_plan::(2_996, inert_contract_steps_of_len::(n)); + let contract = Pallet::::load_actor_contract(actor_id).expect("benchmark Contract exists"); + install_chunked_contract::(actor_id, &contract); + #[block] + { + core::hint::black_box( + Pallet::::load_admitted_contract_geometry(actor_id) + .expect("benchmark geometry reconstructs coherently"), + ); + } + } + + #[benchmark(pov_mode = Measured)] + fn current_step_load_head() { + let actor_id = bench_create_system_with_plan::(2_997, inert_contract_steps_of_len::(1)); + let contract = Pallet::::load_actor_contract(actor_id).expect("benchmark Contract exists"); + install_chunked_contract::(actor_id, &contract); + #[block] + { + core::hint::black_box( + Pallet::::load_current_step_from_storage(actor_id, 0) + .expect("benchmark head Step loads coherently"), + ); + } + } + + #[benchmark(pov_mode = Measured)] + fn current_step_load_tail(s: Linear<1, 4>) { + let n = 1u32.saturating_add(s); + let actor_id = bench_create_system_with_plan::(2_998, inert_contract_steps_of_len::(n)); + let contract = Pallet::::load_actor_contract(actor_id).expect("benchmark Contract exists"); + install_chunked_contract::(actor_id, &contract); + #[block] + { + core::hint::black_box( + Pallet::::load_current_step_from_storage(actor_id, s) + .expect("benchmark tail Step loads coherently"), + ); + } + } + + #[benchmark(pov_mode = Measured)] + fn current_step_plan_opening_head() { + let now: BlockNumberFor = 1u32.into(); + frame_system::Pallet::::set_block_number(now); + let actor_id = bench_create_system_with_plan::(2_999, inert_contract_steps_of_len::(1)); + let contract = Pallet::::load_actor_contract(actor_id).expect("benchmark Contract exists"); + install_chunked_contract::(actor_id, &contract); + ActorHot::::mutate(actor_id, |maybe| { + maybe + .as_mut() + .expect("benchmark hot state exists") + .queue_ticket = Some(9); + }); + let identity = ActorIdentities::::get(actor_id).expect("benchmark identity exists"); + let certificate = ActorAdmissionCertificates::::get(actor_id) + .expect("benchmark admission certificate exists"); + let ticket = ActorStepTicket { + actor_id, + cycle_nonce: identity.cycle_nonce.saturating_add(1), + cursor: 0, + ticket: 9, + eligible_at: now, + contract_commitment: ActorContractCommitment { + semantic_contract_id: certificate.semantic_contract_id, + body_commitment: certificate.body_commitment, + }, + }; + #[block] + { + core::hint::black_box( + Pallet::::load_current_step_plan_from_storage(ticket) + .expect("benchmark Opening plan loads coherently"), + ); + } + } + + #[benchmark(pov_mode = Measured)] + fn current_step_plan_suspended_head() { + let actor_id = bench_create_system_with_plan::(3_000, inert_contract_steps_of_len::(1)); + install_run_state::(actor_id, T::MaxOpeningSnapshotEntries::get()); + ActorRunStateStore::::mutate(actor_id, |maybe| { + maximize_run_state_geometry::(maybe.as_mut().expect("benchmark run state exists")); + }); + let contract = Pallet::::load_actor_contract(actor_id).expect("benchmark Contract exists"); + install_chunked_contract::(actor_id, &contract); + ActorHot::::mutate(actor_id, |maybe| { + maybe + .as_mut() + .expect("benchmark hot state exists") + .queue_ticket = Some(9); + }); + let run = ActorRunStateStore::::get(actor_id).expect("benchmark run state exists"); + frame_system::Pallet::::set_block_number(run.eligible_at); + let certificate = ActorAdmissionCertificates::::get(actor_id) + .expect("benchmark admission certificate exists"); + let ticket = ActorStepTicket { + actor_id, + cycle_nonce: run.cycle_nonce, + cursor: 0, + ticket: 9, + eligible_at: run.eligible_at, + contract_commitment: ActorContractCommitment { + semantic_contract_id: certificate.semantic_contract_id, + body_commitment: certificate.body_commitment, + }, + }; + #[block] + { + core::hint::black_box( + Pallet::::load_current_step_plan_from_storage(ticket) + .expect("benchmark Suspended head plan loads coherently"), + ); + } + } + + #[benchmark(pov_mode = Measured)] + fn current_step_plan_running_tail(s: Linear<1, 4>) { + let now: BlockNumberFor = 2u32.into(); + frame_system::Pallet::::set_block_number(now); + let actor_id = + bench_create_system_with_plan::(3_000, inert_contract_steps_of_len::(1 + s)); + install_run_state::(actor_id, T::MaxOpeningSnapshotEntries::get()); + ActorRunStateStore::::mutate(actor_id, |maybe| { + let run = maybe.as_mut().expect("benchmark run state exists"); + maximize_run_state_geometry::(run); + run.cursor = s; + run.last_committed_step_block = Some(1u32.into()); + run.eligible_at = now; + run.suspension = None; + }); + let contract = Pallet::::load_actor_contract(actor_id).expect("benchmark Contract exists"); + install_chunked_contract::(actor_id, &contract); + ActorHot::::mutate(actor_id, |maybe| { + let hot = maybe.as_mut().expect("benchmark hot state exists"); + hot.cycle_state = CycleState::Running; + hot.queue_ticket = Some(9); + }); + let run = ActorRunStateStore::::get(actor_id).expect("benchmark run state exists"); + let certificate = ActorAdmissionCertificates::::get(actor_id) + .expect("benchmark admission certificate exists"); + let ticket = ActorStepTicket { + actor_id, + cycle_nonce: run.cycle_nonce, + cursor: run.cursor, + ticket: 9, + eligible_at: run.eligible_at, + contract_commitment: ActorContractCommitment { + semantic_contract_id: certificate.semantic_contract_id, + body_commitment: certificate.body_commitment, + }, + }; + #[block] + { + core::hint::black_box( + Pallet::::load_current_step_plan_from_storage(ticket) + .expect("benchmark Running plan loads coherently"), + ); + } + } + + #[benchmark(pov_mode = Measured)] + fn opening_snapshot_capture(e: Linear<1, 16>) { + let actor: T::AccountId = account("opening_snapshot_actor", 0, 0); + let assets = T::BenchmarkHelper::setup_predicate_assets(&actor, e) + .expect("benchmark Opening assets exist"); + assert_eq!(u32::try_from(assets.len()).expect("asset count fits"), e); + for asset in &assets { + let _ = T::AssetOps::mint(&actor, *asset, One::one()); + } + let mut steps = ContractSteps::::default(); + for pair in assets.chunks(2) { + let asset_a = pair[0]; + let asset_b = pair.get(1).copied().unwrap_or(asset_a); + steps + .try_push(Step { + precondition: None, + task: ActorTask::AddLiquidity { + asset_a, + asset_b, + amount_a: AmountResolution::PercentageAtOpening(Perbill::one()), + amount_b: if pair.len() == 2 { + AmountResolution::PercentageAtOpening(Perbill::one()) + } else { + AmountResolution::Fixed(Zero::zero()) + }, + min_lp_out: Zero::zero(), + }, + on_error: StepErrorPolicy::AbortCycle, + }) + .expect("benchmark Opening Step fits"); + } + #[block] + { + let snapshot = + Pallet::::capture_opening_snapshot(ActorType::System, &actor, &steps, Zero::zero()); + assert_eq!( + u32::try_from(snapshot.len()).expect("snapshot count fits"), + e + ); + core::hint::black_box(snapshot); + } + } + + #[benchmark(pov_mode = Measured)] + fn opening_predicate_capture(p: Linear<1, 32>) { + let actor: T::AccountId = account("opening_predicate_actor", 0, 0); + let assets = T::BenchmarkHelper::setup_predicate_assets(&actor, p) + .expect("benchmark predicate assets exist"); + assert_eq!(u32::try_from(assets.len()).expect("asset count fits"), p); + for asset in &assets { + let _ = T::AssetOps::mint(&actor, *asset, One::one()); + } + let mut steps = ContractSteps::::default(); + for chunk in assets.chunks(T::MaxPredicatesPerClause::get() as usize) { + let predicates = BoundedVec::try_from( + chunk + .iter() + .map(|asset| TimedPredicate { + timing: ObservationTiming::Opening, + predicate: Predicate::BalanceAbove { + asset: *asset, + threshold: Zero::zero(), + }, + }) + .collect::>(), + ) + .expect("benchmark predicate clause fits"); + steps + .try_push(Step { + precondition: Some(Precondition { + clauses: BoundedVec::try_from(vec![predicates]).expect("benchmark predicate set fits"), + }), + task: ActorTask::StopCycle, + on_error: StepErrorPolicy::AbortCycle, + }) + .expect("benchmark predicate Step fits"); + } + #[block] + { + let results = Pallet::::capture_opening_predicate_results(&actor, &steps, Zero::zero()); + assert_eq!(u32::try_from(results.len()).expect("result count fits"), p); + core::hint::black_box(results); + } + } + + #[benchmark] + fn scheduler_actor_state_probe() { + let actor_id = bench_create_system_manual::(3_000); + install_run_state::(actor_id, T::MaxOpeningSnapshotEntries::get()); + let assets = T::BenchmarkHelper::funding_assets(T::MaxFundingTrackedAssets::get()); + ActorFunding::::mutate(actor_id, |maybe_funding| { + let funding = maybe_funding + .as_mut() + .expect("benchmark actor funding state must exist"); + for asset in assets { + funding + .funding_tracked_assets + .try_insert(asset) + .expect("benchmark tracked asset fits"); + funding + .funding_accumulated + .try_insert(asset, One::one()) + .expect("benchmark funding entry fits"); + } + }); + #[block] + { + core::hint::black_box( + Pallet::::load_current_step_service_state(actor_id) + .expect("benchmark current-Step service state loads coherently"), + ); + } assert!(matches!( Pallet::::load_actor_state(actor_id), LoadedActorStateOf::Active(_) @@ -2346,12 +3298,11 @@ mod benches { let first = bench_create_system_manual::(34_100_000); let second = bench_create_system_manual::(34_100_001); for actor_id in [first, second] { - ActorContracts::::mutate(actor_id, |maybe_contract| { - maybe_contract - .as_mut() - .expect("benchmark actor contract exists") - .trigger = Trigger::Cadenced { every_ticks: 1 }; - }); + let mut contract = + Pallet::::load_actor_contract(actor_id).expect("benchmark actor contract exists"); + contract.trigger = Trigger::Cadenced { every_ticks: 1 }; + Pallet::::store_actor_contract(actor_id, contract) + .expect("benchmark cadence Contract remains admitted"); ActorHot::::mutate(actor_id, |maybe_hot| { maybe_hot .as_mut() @@ -2376,7 +3327,7 @@ mod benches { Some(1) ); let rearmed_tick = ActorHot::::get(first) - .and_then(|hot| hot.trigger_runtime_state.cadence_anchor_tick()) + .and_then(|hot| hot.trigger_runtime_state.temporal_anchor_tick()) .and_then(|anchor| anchor.checked_add(1)) .expect("benchmark cadence re-anchors"); assert_eq!( @@ -2385,23 +3336,132 @@ mod benches { ); } + /// Measures one due User AtTime occurrence independently from timestamp inherent work: + /// one-shot consumption, exact Trigger collection, and canonical readiness placement. #[benchmark(pov_mode = Measured)] - fn scheduler_wakeup_cursor_worker_remove() { + fn at_time_trigger_occurrence() { clear_host_genesis_wakeup_placements::(); - let cursor_len = T::MaxActiveActors::get(); - let wakeup_block = prepare_wakeup_cursor_repair::(0); - let actor_id = bench_create_system_manual::(34_200_000); - let mut entries = WakeupPageEntriesOf::::default(); - entries - .try_push(Some(WakeupEntry { actor_id })) - .expect("one wakeup entry fits"); - WakeupPages::::insert( - (WakeupKey::Block(wakeup_block), 0), - WakeupPage { - entries, - live_entries: 1, - scan_slot: 0, - previous_page: None, + let owner: T::AccountId = whitelisted_caller(); + let recipient: T::AccountId = account("at-time-occurrence-recipient", 0, 0); + let contract_steps = make_contract_steps::(recipient); + ensure_creation_balance::(&owner); + prefund_active_user_creation::(&owner, &contract_steps); + Pallet::::create_user_actor( + RawOrigin::Signed(owner).into(), + Mutability::Mutable, + user_contract::( + Schedule { + trigger: Trigger::at_time(1), + cooldown_blocks: 0, + }, + contract_steps, + ), + ) + .expect("AtTime benchmark Actor exists"); + let actor_id = NextActorId::::get().saturating_sub(1); + seed_actor_for_cycle::(actor_id); + Pallet::::trigger_wakeup_substrate_invalidate_inner(actor_id) + .expect("initial AtTime pointer is coherent") + .expect("initial AtTime pointer exists"); + ActorHot::::mutate(actor_id, |maybe_hot| { + maybe_hot + .as_mut() + .expect("AtTime Actor hot state exists") + .trigger_runtime_state = TriggerRuntimeState::AtTime { + anchor_tick: Some(0), + consumed: false, + }; + }); + Pallet::::benchmark_defer_tick_wakeup(actor_id, 1).expect("due AtTime occurrence fits"); + Pallet::::trigger_wakeup_substrate_invalidate_inner(actor_id) + .expect("due AtTime pointer is coherent") + .expect("due AtTime pointer exists"); + #[block] + { + assert_eq!( + Pallet::::process_due_temporal_occurrence(actor_id, 1), + Ok(false) + ); + } + let hot = ActorHot::::get(actor_id).expect("AtTime Actor remains active"); + assert!(hot.pending_signal); + assert!(hot.queue_ticket.is_some()); + assert!(hot.trigger_wakeup_pointer.is_none()); + assert!(matches!( + hot.trigger_runtime_state, + TriggerRuntimeState::AtTime { consumed: true, .. } + )); + } + + /// Measures one due User Cadenced occurrence independently from timestamp inherent work: + /// trigger-deadline advancement, exact Trigger collection, and canonical readiness placement. + #[benchmark(pov_mode = Measured)] + fn cadenced_trigger_occurrence() { + clear_host_genesis_wakeup_placements::(); + let owner: T::AccountId = whitelisted_caller(); + let recipient: T::AccountId = account("cadenced-occurrence-recipient", 0, 0); + let contract_steps = make_contract_steps::(recipient); + ensure_creation_balance::(&owner); + prefund_active_user_creation::(&owner, &contract_steps); + Pallet::::create_user_actor( + RawOrigin::Signed(owner).into(), + Mutability::Mutable, + user_contract::( + Schedule { + trigger: Trigger::cadenced(1), + cooldown_blocks: 0, + }, + contract_steps, + ), + ) + .expect("Cadenced benchmark Actor exists"); + let actor_id = NextActorId::::get().saturating_sub(1); + seed_actor_for_cycle::(actor_id); + Pallet::::trigger_wakeup_substrate_invalidate_inner(actor_id) + .expect("initial Cadenced pointer is coherent") + .expect("initial Cadenced pointer exists"); + ActorHot::::mutate(actor_id, |maybe_hot| { + maybe_hot + .as_mut() + .expect("Cadenced Actor hot state exists") + .trigger_runtime_state = TriggerRuntimeState::Cadenced { + anchor_tick: Some(0), + }; + }); + Pallet::::benchmark_defer_tick_wakeup(actor_id, 0).expect("due Cadenced occurrence fits"); + Pallet::::trigger_wakeup_substrate_invalidate_inner(actor_id) + .expect("due Cadenced pointer is coherent") + .expect("due Cadenced pointer exists"); + #[block] + { + assert_eq!( + Pallet::::process_due_temporal_occurrence(actor_id, 0), + Ok(false) + ); + } + let hot = ActorHot::::get(actor_id).expect("Cadenced Actor remains active"); + assert!(hot.pending_signal); + assert!(hot.queue_ticket.is_some()); + assert!(hot.trigger_wakeup_pointer.is_none()); + } + + #[benchmark(pov_mode = Measured)] + fn scheduler_wakeup_cursor_worker_remove() { + clear_host_genesis_wakeup_placements::(); + let cursor_len = T::MaxActiveActors::get(); + let wakeup_block = prepare_wakeup_cursor_repair::(0); + let actor_id = bench_create_system_manual::(34_200_000); + let mut entries = WakeupPageEntriesOf::::default(); + entries + .try_push(Some(WakeupEntry { actor_id })) + .expect("one wakeup entry fits"); + WakeupPages::::insert( + (WakeupKey::Block(wakeup_block), 0), + WakeupPage { + entries, + live_entries: 1, + scan_slot: 0, + previous_page: None, next_page: None, }, ); @@ -2498,14 +3558,16 @@ mod benches { let remaining = u64::from(bounded).saturating_sub(ticket); let entries = remaining.min(u64::from(page_size)); let page = (0..entries) - .map(|offset| QueueEntry { - ticket: ticket.saturating_add(offset), - actor_id: 37_000_000u64.saturating_add(ticket).saturating_add(offset), + .map(|offset| { + benchmark_queue_entry::( + ticket.saturating_add(offset), + 37_000_000u64.saturating_add(ticket).saturating_add(offset), + ) }) .collect::>(); QueuePages::::insert( page_id, - BoundedVec::::try_from(page) + BoundedVec::>, T::QueuePageSize>::try_from(page) .expect("benchmark queue page must fit configured page size"), ); ticket = ticket.saturating_add(entries); @@ -2533,7 +3595,7 @@ mod benches { let template_id = bench_create_system_manual::(38_000_000); let hot_template = ActorHot::::get(template_id).expect("benchmark hot template"); let contract_template = - ActorContracts::::get(template_id).expect("benchmark contract template"); + Pallet::::load_actor_contract(template_id).expect("benchmark contract template"); let funding_template = ActorFunding::::get(template_id).expect("benchmark funding template"); let mut identity_template = ActorIdentities::::get(template_id).expect("benchmark identity template"); @@ -2552,46 +3614,3635 @@ mod benches { hot.queue_ticket = Some(logical_ticket); ActorIdentities::::insert(actor_id, identity_template.clone()); ActorHot::::insert(actor_id, hot); - ActorContracts::::insert(actor_id, contract_template.clone()); + Pallet::::store_actor_contract(actor_id, contract_template.clone()) + .expect("benchmark mixed Contract remains admitted"); ActorFunding::::insert(actor_id, funding_template.clone()); } - QueueEntry { - ticket: logical_ticket, - actor_id, + benchmark_queue_entry::(logical_ticket, actor_id) + }) + .collect::>(); + QueuePages::::insert( + page_id, + BoundedVec::>, T::QueuePageSize>::try_from(page) + .expect("benchmark queue page must fit configured page size"), + ); + ticket = ticket.saturating_add(entries); + } + ActorIdentities::::remove(template_id); + ActorHot::::remove(template_id); + Pallet::::remove_actor_contract(template_id).expect("benchmark template Contract removes"); + ActorFunding::::remove(template_id); + QueueHead::::put(0); + QueueTail::::put(u64::from(bounded)); + QueueOccupancy::::put(bounded); + NextQueueTicket::::put(u64::from(bounded)); + let cutoff = u64::from(bounded); + #[block] + { + while QueueHead::::get() < cutoff { + core::hint::black_box( + Pallet::::paged_drain_tombstones(cutoff, bounded) + .expect("benchmark mixed queue topology is valid"), + ); + if let Some((_, entry)) = Pallet::::paged_head_entry() { + assert!(Pallet::::paged_consume_head(entry.ticket)); + } + } + } + assert!(QueueHead::::get() >= cutoff); + assert_eq!(QueueHead::::get(), QueueTail::::get()); + assert_eq!(QueueOccupancy::::get(), 0); + } + + /// Measures one persistent zero-Step Opening after FIFO head consumption. The branch charges no + /// Action, creates no Run state, commits the Cycle nonce and summary, and leaves no successor + /// Pipeline placement for a Manual Contract. + #[benchmark(pov_mode = Measured)] + fn scheduler_inner_zero_step_complete() { + let owner: T::AccountId = account("zero_step_opening_owner", 0, 0); + let schedule = Schedule { + trigger: Trigger::manual(), + cooldown_blocks: 0, + }; + Pallet::::create_system_actor( + RawOrigin::Root.into(), + owner, + Mutability::Mutable, + system_contract::(schedule, ContractSteps::::default()), + ) + .expect("zero-Step System Contract exists"); + let actor_id = NextActorId::::get().saturating_sub(1); + ActorHot::::mutate(actor_id, |maybe| { + let hot = maybe.as_mut().expect("zero-Step actor is active"); + hot.pending_signal = true; + hot.queue_ticket = Some(9); + }); + GlobalCircuitBreaker::::put(false); + let now: BlockNumberFor = 1u32.into(); + frame_system::Pallet::::set_block_number(now); + let LoadedActorStateOf::Active(state) = Pallet::::load_actor_state(actor_id) else { + panic!("zero-Step actor state exists"); + }; + let instance = Pallet::::derive_active_actor_view(state.identity, state.hot, state.contract); + ActorHot::::mutate(actor_id, |maybe| { + maybe + .as_mut() + .expect("zero-Step actor remains active") + .queue_ticket = None; + }); + #[block] + { + polkadot_sdk::frame_support::storage::with_transaction(|| { + match Pallet::::execute_zero_step_opening_and_place(actor_id, instance, now) { + Ok(()) => { + polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok::< + (), + AttemptTransactionError, + >(())) + } + Err(error) => { + polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)) + } + } + }) + .expect("zero-Step inner atom commits"); + } + let identity = ActorIdentities::::get(actor_id).expect("zero-Step actor remains registered"); + assert_eq!(identity.cycle_nonce, 1); + assert!(ActorRunStateStore::::get(actor_id).is_none()); + assert!(ActorHot::::get(actor_id).is_some_and(|hot| { + !hot.pending_signal && hot.queue_ticket.is_none() && hot.wakeup_pointer.is_none() + })); + } + + /// Measures one complete fresh-Opening control path at maximum configured Contract geometry. + /// Every Step references one zero-balance Opening amount, so resolution skips and no Task effect + /// executes. The measured branch owns queue service, exact current-Step planning, maximum Contract + /// and funding geometry, immutable Opening capture, Running persistence, and causal successor + /// placement without effect-Weight contamination. Parameterized inner benchmarks separately own + /// maximum snapshot, predicate, and heavier Task envelopes. + #[benchmark(pov_mode = Measured)] + fn scheduler_paged_execute_opening_max() { + let _ = QueuePages::::clear(u32::MAX, None); + QueueHead::::put(0); + QueueTail::::put(0); + QueueOccupancy::::put(0); + NextQueueTicket::::put(0); + GlobalCircuitBreaker::::put(false); + let step_count = T::MaxContractSteps::get(); + let asset_count = step_count + .checked_mul(2) + .expect("maximum Opening asset count fits") + .min(T::MaxFundingTrackedAssets::get()); + let asset_owner: T::AccountId = account("opening_atomic_assets", 0, 0); + let assets = T::BenchmarkHelper::setup_predicate_assets(&asset_owner, asset_count) + .expect("maximum Opening assets exist"); + assert_eq!(assets.len(), asset_count as usize); + let asset = assets[0]; + let destination: T::AccountId = account("opening_atomic_destination", 0, 0); + let mut steps = ContractSteps::::default(); + for _ in 0..step_count { + steps + .try_push(Step { + precondition: None, + task: ActorTask::Transfer { + to: destination.clone(), + asset, + amount: AmountResolution::PercentageAtOpening(Perbill::one()), + }, + on_error: StepErrorPolicy::AbortCycle, + }) + .expect("maximum Opening Contract fits"); + } + assert_eq!(steps.len(), step_count as usize); + let admission = Pallet::::contract_steps_admission_weight_upper(ActorType::User, &steps); + let service = Pallet::::guaranteed_actor_service_weight() + .expect("Opening benchmark service floor exists"); + assert!( + admission.all_lte(service), + "maximum public Opening fixture must fit admission: admission={admission:?}, service={service:?}" + ); + let owner: T::AccountId = account("opening_atomic_owner", 0, 0); + ensure_creation_balance::(&owner); + prefund_active_user_creation::(&owner, &steps); + let schedule = Schedule { + trigger: Trigger::manual(), + cooldown_blocks: 0, + }; + Pallet::::create_user_actor( + RawOrigin::Signed(owner).into(), + Mutability::Mutable, + user_contract::(schedule, steps), + ) + .expect("maximum User Opening Contract exists"); + let actor_id = NextActorId::::get().saturating_sub(1); + ActorFunding::::mutate(actor_id, |maybe| { + let funding = maybe + .as_mut() + .expect("maximum Opening actor funding exists"); + for asset in assets + .iter(/* deos-bypass: bounded-iter */) + .take(T::MaxFundingTrackedAssets::get() as usize) + { + funding + .funding_tracked_assets + .try_insert(*asset) + .expect("maximum Opening tracked asset fits"); + funding + .funding_accumulated + .try_insert(*asset, One::one()) + .expect("maximum Opening funding snapshot fits"); + } + }); + assert_eq!( + ActorFunding::::get(actor_id) + .expect("maximum Opening funding exists") + .funding_accumulated + .len(), + T::MaxFundingTrackedAssets::get() as usize, + ); + ActorHot::::mutate(actor_id, |maybe| { + maybe + .as_mut() + .expect("maximum Opening actor is active") + .pending_signal = true; + }); + assert!(Pallet::::paged_enqueue(actor_id)); + let now: BlockNumberFor = 1u32.into(); + frame_system::Pallet::::set_block_number(now); + #[block] + { + core::hint::black_box(Pallet::::execute_cycle(Weight::MAX)); + } + let run = ActorRunStateStore::::get(actor_id).expect("maximum Opening run persists"); + assert_eq!(run.cursor, 1); + assert_eq!(run.opening_snapshot.len(), 1); + assert_eq!( + run.funding_snapshot.len(), + T::MaxFundingTrackedAssets::get() as usize, + ); + assert!(run.opening_predicate_results.is_empty()); + assert_eq!(run.last_committed_step_block, Some(now)); + assert_eq!(run.eligible_at, now.saturating_add(One::one())); + assert_eq!( + QueueHead::::get().saturating_add(1), + QueueTail::::get() + ); + assert_eq!(QueueOccupancy::::get(), 1); + } + + /// Measures direct minimal-geometry fresh-Opening Actor close at every C6 tail count. StopCycle + /// reaches auto-close nonce one and removes the complete authored geometry without an effect. + #[benchmark(pov_mode = Measured)] + fn scheduler_inner_opening_close_min(t: Linear<0, 8>) { + let tail_chunks = t.min( + T::MaxContractSteps::get() + .saturating_sub(1) + .div_ceil(MAX_STEPS_PER_TAIL_CHUNK), + ); + let step_count = 1u32 + .saturating_add(tail_chunks.saturating_mul(MAX_STEPS_PER_TAIL_CHUNK)) + .min(T::MaxContractSteps::get()); + let mut steps = inert_contract_steps_of_len::(step_count); + steps[0].task = ActorTask::StopCycle; + let actor_id = bench_create_system_manual::(41_607_812); + let mut contract = system_contract::( + Schedule { + trigger: Trigger::manual(), + cooldown_blocks: 0, + }, + steps, + ) + .expect("minimal Opening-close Contract exists"); + contract.auto_close_at_cycle_nonce = Some(1); + Pallet::::store_actor_contract(actor_id, contract) + .expect("minimal Opening-close geometry stores"); + let funding_assets = T::BenchmarkHelper::funding_assets(T::MaxFundingTrackedAssets::get()); + ActorFunding::::mutate(actor_id, |maybe| { + let funding = maybe + .as_mut() + .expect("minimal Opening-close funding exists"); + for asset in funding_assets { + funding + .funding_tracked_assets + .try_insert(asset) + .expect("minimal Opening-close tracked asset fits"); + funding + .funding_accumulated + .try_insert(asset, One::one()) + .expect("minimal Opening-close funding value fits"); + } + }); + ActorHot::::mutate(actor_id, |maybe| { + let hot = maybe + .as_mut() + .expect("minimal Opening-close actor is active"); + hot.pending_signal = true; + hot.queue_ticket = Some(9); + }); + GlobalCircuitBreaker::::put(false); + let now: BlockNumberFor = 1u32.into(); + frame_system::Pallet::::set_block_number(now); + let (state, admission, loaded_step) = Pallet::::load_current_step_service_state(actor_id) + .expect("minimal Opening-close service state is coherent"); + let LoadedActorStateOf::Active(full_state) = Pallet::::load_actor_state(actor_id) else { + panic!("minimal Opening-close full state exists"); + }; + let execution_instance = Pallet::::derive_active_actor_view( + full_state.identity, + full_state.hot, + full_state.contract, + ); + let control_context = Pallet::::execution_step_control_weight_context( + &execution_instance, + state.run_state.as_ref(), + &loaded_step, + ) + .expect("minimal Opening-close control context exists"); + assert_eq!(control_context.opening_tail_chunks, tail_chunks); + assert_eq!(control_context.opening_snapshot_entries, 0); + assert_eq!(control_context.opening_predicate_results, 0); + assert_eq!(control_context.predicate_evaluation_units, 0); + assert_eq!( + T::StepControlWeight::maximum_control_weight(control_context, &loaded_step.step), + Some(loaded_step.resources.control), + ); + ActorHot::::mutate(actor_id, |maybe| { + maybe + .as_mut() + .expect("minimal Opening-close hot state remains") + .queue_ticket = None; + }); + #[block] + { + let ticket = Pallet::::build_actor_step_ticket( + actor_id, + 9, + now, + &state.identity, + &state.hot, + state.run_state.as_ref(), + &admission, + ) + .expect("minimal Opening-close Step ticket builds"); + let maximum_fee = Pallet::::maximum_current_step_fee( + state.identity.actor_class.actor_type(), + loaded_step.resources, + ) + .expect("minimal Opening-close maximum fee exists"); + let plan = Pallet::::build_current_step_plan( + actor_id, + state.identity, + state.hot, + state.run_state, + state.funding, + admission, + ticket, + loaded_step, + maximum_fee, + ) + .expect("minimal Opening-close plan builds"); + polkadot_sdk::frame_support::storage::with_transaction(|| { + match Pallet::::execute_current_step_and_place(actor_id, execution_instance, plan, now) { + Ok(evidence) => { + assert!(!evidence.closed_for_exhaustion); + core::hint::black_box(evidence); + polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok::< + (), + AttemptTransactionError, + >(())) + } + Err(error) => { + polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)) + } + } + }) + .expect("minimal Opening-close inner atom commits"); + } + assert!(ActorHot::::get(actor_id).is_none()); + assert!(ActorIdentities::::get(actor_id).is_none()); + assert!(ActorContractHeads::::get(actor_id).is_none()); + assert_eq!( + ActorContractTailChunks::::iter_prefix(actor_id).count(), + 0, + ); + } + + /// Measures direct minimal-geometry fresh-Opening failure at every C6 tail count. An unfunded + /// fixed Transfer aborts the cycle without effect invocation, run persistence, or placement. + #[benchmark(pov_mode = Measured)] + fn scheduler_inner_opening_failed_min(t: Linear<0, 8>) { + let tail_chunks = t.min( + T::MaxContractSteps::get() + .saturating_sub(1) + .div_ceil(MAX_STEPS_PER_TAIL_CHUNK), + ); + let step_count = 1u32 + .saturating_add(tail_chunks.saturating_mul(MAX_STEPS_PER_TAIL_CHUNK)) + .min(T::MaxContractSteps::get()); + let mut steps = inert_contract_steps_of_len::(step_count); + let actor_id = bench_create_system_manual::(41_609_375); + let funding_assets = T::BenchmarkHelper::funding_assets(T::MaxFundingTrackedAssets::get()); + let invalid_legs = SplitTransferLegsOf::::try_from(alloc::vec![ + SplitLeg { + to: account("opening_failure_leg", 0, 0), + share: Perbill::from_percent(50), + }, + SplitLeg { + to: account("opening_failure_leg", 0, 0), + share: Perbill::from_percent(50), + }, + ]) + .expect("minimal Opening-failure split legs fit"); + steps[0].task = ActorTask::SplitTransfer { + asset: T::FeeNativeAssetId::get(), + amount: AmountResolution::Fixed(One::one()), + legs: invalid_legs, + }; + steps[0].on_error = StepErrorPolicy::AbortCycle; + let contract = system_contract::( + Schedule { + trigger: Trigger::manual(), + cooldown_blocks: 0, + }, + steps, + ) + .expect("minimal Opening-failure Contract exists"); + Pallet::::store_actor_contract(actor_id, contract) + .expect("minimal Opening-failure geometry stores"); + ActorFunding::::mutate(actor_id, |maybe| { + let funding = maybe + .as_mut() + .expect("minimal Opening-failure funding exists"); + for asset in funding_assets { + funding + .funding_tracked_assets + .try_insert(asset) + .expect("minimal Opening-failure tracked asset fits"); + funding + .funding_accumulated + .try_insert(asset, One::one()) + .expect("minimal Opening-failure funding value fits"); + } + }); + ActorHot::::mutate(actor_id, |maybe| { + let hot = maybe + .as_mut() + .expect("minimal Opening-failure actor is active"); + hot.pending_signal = true; + hot.queue_ticket = Some(9); + }); + GlobalCircuitBreaker::::put(false); + let now: BlockNumberFor = 1u32.into(); + frame_system::Pallet::::set_block_number(now); + let (state, admission, loaded_step) = Pallet::::load_current_step_service_state(actor_id) + .expect("minimal Opening-failure service state is coherent"); + let LoadedActorStateOf::Active(full_state) = Pallet::::load_actor_state(actor_id) else { + panic!("minimal Opening-failure full state exists"); + }; + let execution_instance = Pallet::::derive_active_actor_view( + full_state.identity, + full_state.hot, + full_state.contract, + ); + let control_context = Pallet::::execution_step_control_weight_context( + &execution_instance, + state.run_state.as_ref(), + &loaded_step, + ) + .expect("minimal Opening-failure control context exists"); + assert_eq!(control_context.opening_tail_chunks, tail_chunks); + assert_eq!(control_context.opening_snapshot_entries, 0); + assert_eq!(control_context.opening_predicate_results, 0); + assert_eq!(control_context.predicate_evaluation_units, 0); + assert_eq!( + T::StepControlWeight::maximum_control_weight(control_context, &loaded_step.step), + Some(loaded_step.resources.control), + ); + ActorHot::::mutate(actor_id, |maybe| { + maybe + .as_mut() + .expect("minimal Opening-failure hot state remains") + .queue_ticket = None; + }); + #[block] + { + let ticket = Pallet::::build_actor_step_ticket( + actor_id, + 9, + now, + &state.identity, + &state.hot, + state.run_state.as_ref(), + &admission, + ) + .expect("minimal Opening-failure Step ticket builds"); + let maximum_fee = Pallet::::maximum_current_step_fee( + state.identity.actor_class.actor_type(), + loaded_step.resources, + ) + .expect("minimal Opening-failure maximum fee exists"); + let plan = Pallet::::build_current_step_plan( + actor_id, + state.identity, + state.hot, + state.run_state, + state.funding, + admission, + ticket, + loaded_step, + maximum_fee, + ) + .expect("minimal Opening-failure plan builds"); + polkadot_sdk::frame_support::storage::with_transaction(|| { + match Pallet::::execute_current_step_and_place(actor_id, execution_instance, plan, now) { + Ok(evidence) => { + core::hint::black_box(evidence); + polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok::< + (), + AttemptTransactionError, + >(())) + } + Err(error) => { + polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)) + } + } + }) + .expect("minimal Opening-failure inner atom commits"); + } + assert!(ActorRunStateStore::::get(actor_id).is_none()); + let hot = ActorHot::::get(actor_id).expect("minimal Opening-failure hot state persists"); + assert_eq!(hot.cycle_state, CycleState::Idle); + assert!(hot.queue_ticket.is_none()); + assert!(hot.wakeup_pointer.is_none()); + assert_eq!(hot.unsuccessful_attempt_streak, 1); + } + + /// Measures direct minimal-geometry fresh-Opening retry at every C6 tail count. An unfunded + /// fixed Transfer suspends at cursor zero and installs one wakeup without Opening capture. + #[benchmark(pov_mode = Measured)] + fn scheduler_inner_opening_retry_min(t: Linear<0, 8>) { + let _ = WakeupBuckets::::clear(u32::MAX, None); + let _ = WakeupPages::::clear(u32::MAX, None); + let _ = WakeupCursorPages::::clear(u32::MAX, None); + let _ = WakeupCursorLen::::clear(u32::MAX, None); + let tail_chunks = t.min( + T::MaxContractSteps::get() + .saturating_sub(1) + .div_ceil(MAX_STEPS_PER_TAIL_CHUNK), + ); + let step_count = 1u32 + .saturating_add(tail_chunks.saturating_mul(MAX_STEPS_PER_TAIL_CHUNK)) + .min(T::MaxContractSteps::get()); + let mut steps = inert_contract_steps_of_len::(step_count); + let actor_id = bench_create_system_manual::(41_612_500); + let funding_assets = T::BenchmarkHelper::funding_assets(T::MaxFundingTrackedAssets::get()); + let retry_asset = *funding_assets + .first() + .expect("minimal Opening-retry funding asset exists"); + steps[0].task = ActorTask::Transfer { + to: account("opening_retry_to", 0, 0), + asset: retry_asset, + amount: AmountResolution::Fixed(One::one()), + }; + steps[0].on_error = StepErrorPolicy::RetryLater { + max_attempts: T::MaxRetryAttempts::get(), + }; + let contract = system_contract::( + Schedule { + trigger: Trigger::manual(), + cooldown_blocks: 2, + }, + steps, + ) + .expect("minimal Opening-retry Contract exists"); + Pallet::::store_actor_contract(actor_id, contract) + .expect("minimal Opening-retry geometry stores"); + ActorFunding::::mutate(actor_id, |maybe| { + let funding = maybe + .as_mut() + .expect("minimal Opening-retry funding exists"); + for asset in funding_assets { + funding + .funding_tracked_assets + .try_insert(asset) + .expect("minimal Opening-retry tracked asset fits"); + funding + .funding_accumulated + .try_insert(asset, One::one()) + .expect("minimal Opening-retry funding value fits"); + } + }); + ActorHot::::mutate(actor_id, |maybe| { + let hot = maybe + .as_mut() + .expect("minimal Opening-retry actor is active"); + hot.pending_signal = true; + hot.queue_ticket = Some(9); + }); + GlobalCircuitBreaker::::put(false); + let now: BlockNumberFor = 1u32.into(); + frame_system::Pallet::::set_block_number(now); + let (state, admission, loaded_step) = Pallet::::load_current_step_service_state(actor_id) + .expect("minimal Opening-retry service state is coherent"); + let LoadedActorStateOf::Active(full_state) = Pallet::::load_actor_state(actor_id) else { + panic!("minimal Opening-retry full state exists"); + }; + let execution_instance = Pallet::::derive_active_actor_view( + full_state.identity, + full_state.hot, + full_state.contract, + ); + let control_context = Pallet::::execution_step_control_weight_context( + &execution_instance, + state.run_state.as_ref(), + &loaded_step, + ) + .expect("minimal Opening-retry control context exists"); + assert_eq!(control_context.opening_tail_chunks, tail_chunks); + assert_eq!(control_context.opening_snapshot_entries, 0); + assert_eq!(control_context.opening_predicate_results, 0); + assert_eq!(control_context.predicate_evaluation_units, 0); + assert_eq!( + T::StepControlWeight::maximum_control_weight(control_context, &loaded_step.step), + Some(loaded_step.resources.control), + ); + ActorHot::::mutate(actor_id, |maybe| { + maybe + .as_mut() + .expect("minimal Opening-retry hot state remains") + .queue_ticket = None; + }); + #[block] + { + let ticket = Pallet::::build_actor_step_ticket( + actor_id, + 9, + now, + &state.identity, + &state.hot, + state.run_state.as_ref(), + &admission, + ) + .expect("minimal Opening-retry Step ticket builds"); + let maximum_fee = Pallet::::maximum_current_step_fee( + state.identity.actor_class.actor_type(), + loaded_step.resources, + ) + .expect("minimal Opening-retry maximum fee exists"); + let plan = Pallet::::build_current_step_plan( + actor_id, + state.identity, + state.hot, + state.run_state, + state.funding, + admission, + ticket, + loaded_step, + maximum_fee, + ) + .expect("minimal Opening-retry plan builds"); + polkadot_sdk::frame_support::storage::with_transaction(|| { + match Pallet::::execute_current_step_and_place(actor_id, execution_instance, plan, now) { + Ok(evidence) => { + core::hint::black_box(evidence); + polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok::< + (), + AttemptTransactionError, + >(())) + } + Err(error) => { + polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)) + } + } + }) + .expect("minimal Opening-retry inner atom commits"); + } + let run = ActorRunStateStore::::get(actor_id).expect("minimal Opening-retry run persists"); + assert_eq!(run.cursor, 0); + assert_eq!(run.unsuccessful_attempts_at_cursor, 1); + assert_eq!(run.suspension, Some(SuspensionReason::FundingUnavailable)); + assert!(ActorHot::::get(actor_id).is_some_and(|hot| { + hot.cycle_state == CycleState::Suspended && hot.wakeup_pointer.is_some() + })); + } + + /// Measures direct maximum-realizable fresh-Opening failure at every C6 tail count. Opening + /// dependencies are fully captured before an invalid split plan fails pre-effect and aborts. + #[benchmark(pov_mode = Measured)] + fn scheduler_inner_opening_failed_max(t: Linear<0, 8>) { + let tail_chunks = t.min( + T::MaxContractSteps::get() + .saturating_sub(1) + .div_ceil(MAX_STEPS_PER_TAIL_CHUNK), + ); + let step_count = 1u32 + .saturating_add(tail_chunks.saturating_mul(MAX_STEPS_PER_TAIL_CHUNK)) + .min(T::MaxContractSteps::get()); + let opening_asset_count = step_count + .checked_mul(2) + .expect("Opening-failure asset count fits"); + let setup_asset_count = opening_asset_count.max(T::MaxFundingTrackedAssets::get()); + let asset_owner: T::AccountId = account("opening_failure_assets", 0, 0); + let assets = T::BenchmarkHelper::setup_predicate_assets(&asset_owner, setup_asset_count) + .expect("Opening-failure assets exist"); + let invalid_legs = SplitTransferLegsOf::::try_from(alloc::vec![ + SplitLeg { + to: account("opening_failure_max_leg", 0, 0), + share: Perbill::from_percent(50), + }, + SplitLeg { + to: account("opening_failure_max_leg", 0, 0), + share: Perbill::from_percent(50), + }, + ]) + .expect("Opening-failure split legs fit"); + let mut steps = ContractSteps::::default(); + for (step_index, pair) in assets + .iter(/* deos-bypass: bounded-iter */) + .take(opening_asset_count as usize) + .copied() + .collect::>() + .chunks_exact(2) + .enumerate() + { + let asset_a = pair[0]; + let asset_b = pair[1]; + let predicates = alloc::vec![ + Predicate::BalanceEquals { + asset: asset_a, + threshold: Zero::zero(), + }, + Predicate::BalanceBelow { + asset: asset_a, + threshold: One::one(), + }, + Predicate::BalanceNotEquals { + asset: asset_b, + threshold: One::one(), + }, + Predicate::BalanceEquals { + asset: asset_b, + threshold: Zero::zero(), + }, + ]; + let task = if step_index == 0 { + ActorTask::SplitTransfer { + asset: T::FeeNativeAssetId::get(), + amount: AmountResolution::Fixed(One::one()), + legs: invalid_legs.clone(), + } + } else { + ActorTask::AddLiquidity { + asset_a, + asset_b, + amount_a: AmountResolution::PercentageAtOpening(Perbill::one()), + amount_b: AmountResolution::PercentageAtOpening(Perbill::one()), + min_lp_out: One::one(), + } + }; + steps + .try_push(Step { + precondition: Some(opening_all::(predicates)), + task, + on_error: StepErrorPolicy::AbortCycle, + }) + .expect("Opening-failure Contract fits"); + } + let actor_id = bench_create_system_manual::(41_615_625); + let contract = system_contract::( + Schedule { + trigger: Trigger::manual(), + cooldown_blocks: 0, + }, + steps, + ) + .expect("Opening-failure Contract exists"); + Pallet::::store_actor_contract(actor_id, contract).expect("Opening-failure geometry stores"); + ActorFunding::::mutate(actor_id, |maybe| { + let funding = maybe + .as_mut() + .expect("Opening-failure actor funding exists"); + for asset in assets + .iter(/* deos-bypass: bounded-iter */) + .take(T::MaxFundingTrackedAssets::get() as usize) + { + funding + .funding_tracked_assets + .try_insert(*asset) + .expect("Opening-failure tracked asset fits"); + funding + .funding_accumulated + .try_insert(*asset, One::one()) + .expect("Opening-failure funding snapshot fits"); + } + }); + ActorHot::::mutate(actor_id, |maybe| { + let hot = maybe.as_mut().expect("Opening-failure actor is active"); + hot.pending_signal = true; + hot.queue_ticket = Some(9); + }); + GlobalCircuitBreaker::::put(false); + let now: BlockNumberFor = 1u32.into(); + frame_system::Pallet::::set_block_number(now); + let (state, admission, loaded_step) = Pallet::::load_current_step_service_state(actor_id) + .expect("Opening-failure service state is coherent"); + let LoadedActorStateOf::Active(full_state) = Pallet::::load_actor_state(actor_id) else { + panic!("Opening-failure full state exists"); + }; + let execution_instance = Pallet::::derive_active_actor_view( + full_state.identity, + full_state.hot, + full_state.contract, + ); + let control_context = Pallet::::execution_step_control_weight_context( + &execution_instance, + state.run_state.as_ref(), + &loaded_step, + ) + .expect("Opening-failure control context exists"); + assert_eq!(control_context.opening_tail_chunks, tail_chunks); + assert_eq!( + control_context.opening_snapshot_entries, + step_count.saturating_sub(1).saturating_mul(2), + ); + assert_eq!( + control_context.opening_predicate_results, + step_count.saturating_mul(T::MaxPredicatesPerStep::get()), + ); + assert_eq!(control_context.predicate_evaluation_units, 8); + assert_eq!( + T::StepControlWeight::maximum_control_weight(control_context, &loaded_step.step), + Some(loaded_step.resources.control), + ); + ActorHot::::mutate(actor_id, |maybe| { + maybe + .as_mut() + .expect("Opening-failure hot state remains") + .queue_ticket = None; + }); + #[block] + { + let ticket = Pallet::::build_actor_step_ticket( + actor_id, + 9, + now, + &state.identity, + &state.hot, + state.run_state.as_ref(), + &admission, + ) + .expect("Opening-failure Step ticket builds"); + let maximum_fee = Pallet::::maximum_current_step_fee( + state.identity.actor_class.actor_type(), + loaded_step.resources, + ) + .expect("Opening-failure maximum fee exists"); + let plan = Pallet::::build_current_step_plan( + actor_id, + state.identity, + state.hot, + state.run_state, + state.funding, + admission, + ticket, + loaded_step, + maximum_fee, + ) + .expect("Opening-failure plan builds"); + polkadot_sdk::frame_support::storage::with_transaction(|| { + match Pallet::::execute_current_step_and_place(actor_id, execution_instance, plan, now) { + Ok(evidence) => { + core::hint::black_box(evidence); + polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok::< + (), + AttemptTransactionError, + >(())) + } + Err(error) => { + polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)) + } + } + }) + .expect("Opening-failure inner atom commits"); + } + assert!(ActorRunStateStore::::get(actor_id).is_none()); + let hot = ActorHot::::get(actor_id).expect("Opening-failure hot state persists"); + assert_eq!(hot.cycle_state, CycleState::Idle); + assert_eq!(hot.unsuccessful_attempt_streak, 1); + } + + /// Measures direct maximum-realizable fresh-Opening retry at every C6 tail count. Four true + /// Opening predicates per Step and two tail-Step Opening surfaces are captured before an + /// unfunded PercentageOfLastFunding Transfer suspends cursor zero into one wakeup. + #[benchmark(pov_mode = Measured)] + fn scheduler_inner_opening_retry_max(t: Linear<0, 8>) { + let _ = WakeupBuckets::::clear(u32::MAX, None); + let _ = WakeupPages::::clear(u32::MAX, None); + let _ = WakeupCursorPages::::clear(u32::MAX, None); + let _ = WakeupCursorLen::::clear(u32::MAX, None); + let tail_chunks = t.min( + T::MaxContractSteps::get() + .saturating_sub(1) + .div_ceil(MAX_STEPS_PER_TAIL_CHUNK), + ); + let step_count = 1u32 + .saturating_add(tail_chunks.saturating_mul(MAX_STEPS_PER_TAIL_CHUNK)) + .min(T::MaxContractSteps::get()); + let opening_asset_count = step_count + .checked_mul(2) + .expect("Opening-retry asset count fits"); + let setup_asset_count = opening_asset_count.max(T::MaxFundingTrackedAssets::get()); + let asset_owner: T::AccountId = account("opening_retry_assets", 0, 0); + let assets = T::BenchmarkHelper::setup_predicate_assets(&asset_owner, setup_asset_count) + .expect("Opening-retry assets exist"); + assert_eq!(assets.len(), setup_asset_count as usize); + let recipient: T::AccountId = account("opening_retry_recipient", 0, 0); + let mut steps = ContractSteps::::default(); + for (step_index, pair) in assets + .iter(/* deos-bypass: bounded-iter */) + .take(opening_asset_count as usize) + .copied() + .collect::>() + .chunks_exact(2) + .enumerate() + { + let asset_a = pair[0]; + let asset_b = pair[1]; + let predicates = alloc::vec![ + Predicate::BalanceEquals { + asset: asset_a, + threshold: Zero::zero(), + }, + Predicate::BalanceBelow { + asset: asset_a, + threshold: One::one(), + }, + Predicate::BalanceNotEquals { + asset: asset_b, + threshold: One::one(), + }, + Predicate::BalanceEquals { + asset: asset_b, + threshold: Zero::zero(), + }, + ]; + let task = if step_index == 0 { + ActorTask::Transfer { + to: recipient.clone(), + asset: asset_a, + amount: AmountResolution::PercentageOfLastFunding(Perbill::one()), + } + } else { + ActorTask::AddLiquidity { + asset_a, + asset_b, + amount_a: AmountResolution::PercentageAtOpening(Perbill::one()), + amount_b: AmountResolution::PercentageAtOpening(Perbill::one()), + min_lp_out: One::one(), + } + }; + steps + .try_push(Step { + precondition: Some(opening_all::(predicates)), + task, + on_error: if step_index == 0 { + StepErrorPolicy::RetryLater { + max_attempts: T::MaxRetryAttempts::get(), + } + } else { + StepErrorPolicy::AbortCycle + }, + }) + .expect("Opening-retry Contract fits"); + } + let actor_id = bench_create_system_manual::(41_618_750); + let contract = system_contract::( + Schedule { + trigger: Trigger::manual(), + cooldown_blocks: 2, + }, + steps, + ) + .expect("Opening-retry Contract exists"); + Pallet::::store_actor_contract(actor_id, contract).expect("Opening-retry geometry stores"); + ActorFunding::::mutate(actor_id, |maybe| { + let funding = maybe.as_mut().expect("Opening-retry actor funding exists"); + for asset in assets + .iter(/* deos-bypass: bounded-iter */) + .take(T::MaxFundingTrackedAssets::get() as usize) + { + funding + .funding_tracked_assets + .try_insert(*asset) + .expect("Opening-retry tracked asset fits"); + funding + .funding_accumulated + .try_insert(*asset, One::one()) + .expect("Opening-retry funding snapshot fits"); + } + }); + ActorHot::::mutate(actor_id, |maybe| { + let hot = maybe.as_mut().expect("Opening-retry actor is active"); + hot.pending_signal = true; + hot.queue_ticket = Some(9); + }); + GlobalCircuitBreaker::::put(false); + let now: BlockNumberFor = 1u32.into(); + frame_system::Pallet::::set_block_number(now); + let (state, admission, loaded_step) = Pallet::::load_current_step_service_state(actor_id) + .expect("Opening-retry service state is coherent"); + let LoadedActorStateOf::Active(full_state) = Pallet::::load_actor_state(actor_id) else { + panic!("Opening-retry full state exists"); + }; + let execution_instance = Pallet::::derive_active_actor_view( + full_state.identity, + full_state.hot, + full_state.contract, + ); + let control_context = Pallet::::execution_step_control_weight_context( + &execution_instance, + state.run_state.as_ref(), + &loaded_step, + ) + .expect("Opening-retry control context exists"); + assert_eq!(control_context.opening_tail_chunks, tail_chunks); + assert_eq!( + control_context.opening_snapshot_entries, + step_count.saturating_sub(1).saturating_mul(2), + ); + assert_eq!( + control_context.opening_predicate_results, + step_count + .checked_mul(T::MaxPredicatesPerStep::get()) + .expect("Opening-retry predicate count fits"), + ); + assert_eq!(control_context.predicate_evaluation_units, 8); + assert_eq!( + T::StepControlWeight::maximum_control_weight(control_context, &loaded_step.step), + Some(loaded_step.resources.control), + ); + ActorHot::::mutate(actor_id, |maybe| { + maybe + .as_mut() + .expect("Opening-retry hot state remains") + .queue_ticket = None; + }); + #[block] + { + let ticket = Pallet::::build_actor_step_ticket( + actor_id, + 9, + now, + &state.identity, + &state.hot, + state.run_state.as_ref(), + &admission, + ) + .expect("Opening-retry Step ticket builds"); + let maximum_fee = Pallet::::maximum_current_step_fee( + state.identity.actor_class.actor_type(), + loaded_step.resources, + ) + .expect("Opening-retry maximum fee exists"); + let plan = Pallet::::build_current_step_plan( + actor_id, + state.identity, + state.hot, + state.run_state, + state.funding, + admission, + ticket, + loaded_step, + maximum_fee, + ) + .expect("Opening-retry plan builds"); + polkadot_sdk::frame_support::storage::with_transaction(|| { + match Pallet::::execute_current_step_and_place(actor_id, execution_instance, plan, now) { + Ok(evidence) => { + core::hint::black_box(evidence); + polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok::< + (), + AttemptTransactionError, + >(())) + } + Err(error) => { + polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)) + } + } + }) + .expect("Opening-retry inner atom commits"); + } + let run = ActorRunStateStore::::get(actor_id).expect("Opening-retry run persists"); + assert_eq!(run.cursor, 0); + assert_eq!(run.unsuccessful_attempts_at_cursor, 1); + assert_eq!(run.suspension, Some(SuspensionReason::FundingUnavailable)); + assert_eq!( + run.opening_snapshot.len(), + step_count.saturating_sub(1).saturating_mul(2) as usize, + ); + assert_eq!( + run.opening_predicate_results.len(), + step_count.saturating_mul(T::MaxPredicatesPerStep::get()) as usize, + ); + assert!(ActorHot::::get(actor_id).is_some_and(|hot| { + hot.cycle_state == CycleState::Suspended && hot.wakeup_pointer.is_some() + })); + } + + /// Measures direct minimal-geometry fresh-Opening completion at every C6 tail count. StopCycle + /// terminates the cycle without an effect, Opening capture, run persistence, or placement. + #[benchmark(pov_mode = Measured)] + fn scheduler_inner_opening_complete_min(t: Linear<0, 8>) { + let tail_chunks = t.min( + T::MaxContractSteps::get() + .saturating_sub(1) + .div_ceil(MAX_STEPS_PER_TAIL_CHUNK), + ); + let step_count = 1u32 + .saturating_add(tail_chunks.saturating_mul(MAX_STEPS_PER_TAIL_CHUNK)) + .min(T::MaxContractSteps::get()); + let mut steps = inert_contract_steps_of_len::(step_count); + steps[0].task = ActorTask::StopCycle; + let actor_id = bench_create_system_manual::(41_625_000); + let contract = system_contract::( + Schedule { + trigger: Trigger::manual(), + cooldown_blocks: 0, + }, + steps, + ) + .expect("minimal Opening-completion Contract exists"); + Pallet::::store_actor_contract(actor_id, contract) + .expect("minimal Opening-completion geometry stores"); + let funding_assets = T::BenchmarkHelper::funding_assets(T::MaxFundingTrackedAssets::get()); + ActorFunding::::mutate(actor_id, |maybe| { + let funding = maybe + .as_mut() + .expect("minimal Opening-completion funding exists"); + for asset in funding_assets { + funding + .funding_tracked_assets + .try_insert(asset) + .expect("minimal Opening-completion tracked asset fits"); + funding + .funding_accumulated + .try_insert(asset, One::one()) + .expect("minimal Opening-completion funding value fits"); + } + }); + ActorHot::::mutate(actor_id, |maybe| { + let hot = maybe + .as_mut() + .expect("minimal Opening-completion actor is active"); + hot.pending_signal = true; + hot.queue_ticket = Some(9); + }); + GlobalCircuitBreaker::::put(false); + let now: BlockNumberFor = 1u32.into(); + frame_system::Pallet::::set_block_number(now); + let (state, admission, loaded_step) = Pallet::::load_current_step_service_state(actor_id) + .expect("minimal Opening-completion service state is coherent"); + let LoadedActorStateOf::Active(full_state) = Pallet::::load_actor_state(actor_id) else { + panic!("minimal Opening-completion full state exists"); + }; + let execution_instance = Pallet::::derive_active_actor_view( + full_state.identity, + full_state.hot, + full_state.contract, + ); + let control_context = Pallet::::execution_step_control_weight_context( + &execution_instance, + state.run_state.as_ref(), + &loaded_step, + ) + .expect("minimal Opening-completion control context exists"); + assert_eq!(control_context.opening_tail_chunks, tail_chunks); + assert_eq!(control_context.opening_snapshot_entries, 0); + assert_eq!(control_context.opening_predicate_results, 0); + assert_eq!(control_context.predicate_evaluation_units, 0); + assert_eq!( + T::StepControlWeight::maximum_control_weight(control_context, &loaded_step.step), + Some(loaded_step.resources.control), + ); + let _ = QueuePages::::clear(u32::MAX, None); + QueueHead::::put(0); + QueueTail::::put(0); + QueueOccupancy::::put(0); + NextQueueTicket::::put(0); + ActorHot::::mutate(actor_id, |maybe| { + maybe + .as_mut() + .expect("minimal Opening-completion hot state remains") + .queue_ticket = None; + }); + #[block] + { + let ticket = Pallet::::build_actor_step_ticket( + actor_id, + 9, + now, + &state.identity, + &state.hot, + state.run_state.as_ref(), + &admission, + ) + .expect("minimal Opening-completion Step ticket builds"); + let maximum_fee = Pallet::::maximum_current_step_fee( + state.identity.actor_class.actor_type(), + loaded_step.resources, + ) + .expect("minimal Opening-completion maximum fee exists"); + let plan = Pallet::::build_current_step_plan( + actor_id, + state.identity, + state.hot, + state.run_state, + state.funding, + admission, + ticket, + loaded_step, + maximum_fee, + ) + .expect("minimal Opening-completion plan builds"); + polkadot_sdk::frame_support::storage::with_transaction(|| { + match Pallet::::execute_current_step_and_place(actor_id, execution_instance, plan, now) { + Ok(evidence) => { + core::hint::black_box(evidence); + polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok::< + (), + AttemptTransactionError, + >(())) + } + Err(error) => { + polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)) + } + } + }) + .expect("minimal Opening-completion inner atom commits"); + } + assert!(ActorRunStateStore::::get(actor_id).is_none()); + assert_eq!(QueueOccupancy::::get(), 0); + assert!(ActorHot::::get(actor_id).is_some_and(|hot| { + hot.cycle_state == CycleState::Idle + && hot.queue_ticket.is_none() + && hot.wakeup_pointer.is_none() + })); + } + + /// Measures the direct minimal-geometry fresh-Opening progress path at every C6 tail count. + /// The current fixed-zero Transfer resolves to an effect-free skip; no Step contributes Opening + /// surfaces or predicates, while the conservative funding payload is fully materialized. + #[benchmark(pov_mode = Measured)] + fn scheduler_inner_opening_progress_min(t: Linear<1, 8>) { + let tail_chunks = t.min( + T::MaxContractSteps::get() + .saturating_sub(1) + .div_ceil(MAX_STEPS_PER_TAIL_CHUNK), + ); + let step_count = 1u32 + .saturating_add(tail_chunks.saturating_mul(MAX_STEPS_PER_TAIL_CHUNK)) + .min(T::MaxContractSteps::get()); + let mut steps = inert_contract_steps_of_len::(step_count); + let actor_id = bench_create_system_manual::(41_650_000); + let actor = Pallet::::sovereign_account_id_system(actor_id); + steps[0].task = ActorTask::Transfer { + to: actor, + asset: T::FeeNativeAssetId::get(), + amount: AmountResolution::Fixed(Zero::zero()), + }; + let contract = system_contract::( + Schedule { + trigger: Trigger::manual(), + cooldown_blocks: 0, + }, + steps, + ) + .expect("minimal Opening-progress Contract exists"); + Pallet::::store_actor_contract(actor_id, contract) + .expect("minimal Opening-progress geometry stores"); + let funding_assets = T::BenchmarkHelper::funding_assets(T::MaxFundingTrackedAssets::get()); + ActorFunding::::mutate(actor_id, |maybe| { + let funding = maybe + .as_mut() + .expect("minimal Opening-progress funding exists"); + for asset in funding_assets { + funding + .funding_tracked_assets + .try_insert(asset) + .expect("minimal Opening-progress tracked asset fits"); + funding + .funding_accumulated + .try_insert(asset, One::one()) + .expect("minimal Opening-progress funding value fits"); + } + }); + ActorHot::::mutate(actor_id, |maybe| { + let hot = maybe + .as_mut() + .expect("minimal Opening-progress actor is active"); + hot.pending_signal = true; + hot.queue_ticket = Some(9); + }); + GlobalCircuitBreaker::::put(false); + let now: BlockNumberFor = 1u32.into(); + frame_system::Pallet::::set_block_number(now); + let (state, admission, loaded_step) = Pallet::::load_current_step_service_state(actor_id) + .expect("minimal Opening-progress service state is coherent"); + let LoadedActorStateOf::Active(full_state) = Pallet::::load_actor_state(actor_id) else { + panic!("minimal Opening-progress full state exists"); + }; + let execution_instance = Pallet::::derive_active_actor_view( + full_state.identity, + full_state.hot, + full_state.contract, + ); + let control_context = Pallet::::execution_step_control_weight_context( + &execution_instance, + state.run_state.as_ref(), + &loaded_step, + ) + .expect("minimal Opening-progress control context exists"); + assert_eq!(control_context.opening_tail_chunks, tail_chunks); + assert_eq!(control_context.opening_snapshot_entries, 0); + assert_eq!(control_context.opening_predicate_results, 0); + assert_eq!(control_context.predicate_evaluation_units, 0); + assert_eq!( + T::StepControlWeight::maximum_control_weight(control_context, &loaded_step.step), + Some(loaded_step.resources.control), + ); + let _ = QueuePages::::clear(u32::MAX, None); + QueueHead::::put(0); + QueueTail::::put(0); + QueueOccupancy::::put(0); + NextQueueTicket::::put(0); + ActorHot::::mutate(actor_id, |maybe| { + maybe + .as_mut() + .expect("minimal Opening-progress hot state remains") + .queue_ticket = None; + }); + #[block] + { + let ticket = Pallet::::build_actor_step_ticket( + actor_id, + 9, + now, + &state.identity, + &state.hot, + state.run_state.as_ref(), + &admission, + ) + .expect("minimal Opening-progress Step ticket builds"); + let maximum_fee = Pallet::::maximum_current_step_fee( + state.identity.actor_class.actor_type(), + loaded_step.resources, + ) + .expect("minimal Opening-progress maximum fee exists"); + let plan = Pallet::::build_current_step_plan( + actor_id, + state.identity, + state.hot, + state.run_state, + state.funding, + admission, + ticket, + loaded_step, + maximum_fee, + ) + .expect("minimal Opening-progress plan builds"); + polkadot_sdk::frame_support::storage::with_transaction(|| { + match Pallet::::execute_current_step_and_place(actor_id, execution_instance, plan, now) { + Ok(evidence) => { + core::hint::black_box(evidence); + polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok::< + (), + AttemptTransactionError, + >(())) + } + Err(error) => { + polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)) + } + } + }) + .expect("minimal Opening-progress inner atom commits"); + } + let run = + ActorRunStateStore::::get(actor_id).expect("minimal Opening-progress run persists"); + assert_eq!(run.cursor, 1); + assert_eq!(run.opening_snapshot.len(), 0); + assert_eq!(run.opening_predicate_results.len(), 0); + assert_eq!( + run.funding_snapshot.len(), + T::MaxFundingTrackedAssets::get() as usize, + ); + assert_eq!(QueueOccupancy::::get(), 1); + } + + /// Measures direct maximum-realizable fresh-Opening Actor close at every C6 tail count. Opening + /// dependencies are captured before StopCycle reaches auto-close nonce one and removes the Actor. + #[benchmark(pov_mode = Measured)] + fn scheduler_inner_opening_close_max(t: Linear<0, 8>) { + let tail_chunks = t.min( + T::MaxContractSteps::get() + .saturating_sub(1) + .div_ceil(MAX_STEPS_PER_TAIL_CHUNK), + ); + let step_count = 1u32 + .saturating_add(tail_chunks.saturating_mul(MAX_STEPS_PER_TAIL_CHUNK)) + .min(T::MaxContractSteps::get()); + let opening_asset_count = step_count + .checked_mul(2) + .expect("Opening-close asset count fits"); + let setup_asset_count = opening_asset_count.max(T::MaxFundingTrackedAssets::get()); + let asset_owner: T::AccountId = account("opening_close_assets", 0, 0); + let assets = T::BenchmarkHelper::setup_predicate_assets(&asset_owner, setup_asset_count) + .expect("Opening-close assets exist"); + let mut steps = ContractSteps::::default(); + for (step_index, pair) in assets + .iter(/* deos-bypass: bounded-iter */) + .take(opening_asset_count as usize) + .copied() + .collect::>() + .chunks_exact(2) + .enumerate() + { + let asset_a = pair[0]; + let asset_b = pair[1]; + let predicates = alloc::vec![ + Predicate::BalanceEquals { + asset: asset_a, + threshold: Zero::zero(), + }, + Predicate::BalanceBelow { + asset: asset_a, + threshold: One::one(), + }, + Predicate::BalanceNotEquals { + asset: asset_b, + threshold: One::one(), + }, + Predicate::BalanceEquals { + asset: asset_b, + threshold: Zero::zero(), + }, + ]; + let task = if step_index == 0 { + ActorTask::StopCycle + } else { + ActorTask::AddLiquidity { + asset_a, + asset_b, + amount_a: AmountResolution::PercentageAtOpening(Perbill::one()), + amount_b: AmountResolution::PercentageAtOpening(Perbill::one()), + min_lp_out: One::one(), + } + }; + steps + .try_push(Step { + precondition: Some(opening_all::(predicates)), + task, + on_error: StepErrorPolicy::AbortCycle, + }) + .expect("Opening-close Contract fits"); + } + let actor_id = bench_create_system_manual::(41_671_875); + let mut contract = system_contract::( + Schedule { + trigger: Trigger::manual(), + cooldown_blocks: 0, + }, + steps, + ) + .expect("Opening-close Contract exists"); + contract.auto_close_at_cycle_nonce = Some(1); + Pallet::::store_actor_contract(actor_id, contract).expect("Opening-close geometry stores"); + ActorFunding::::mutate(actor_id, |maybe| { + let funding = maybe.as_mut().expect("Opening-close actor funding exists"); + for asset in assets + .iter(/* deos-bypass: bounded-iter */) + .take(T::MaxFundingTrackedAssets::get() as usize) + { + funding + .funding_tracked_assets + .try_insert(*asset) + .expect("Opening-close tracked asset fits"); + funding + .funding_accumulated + .try_insert(*asset, One::one()) + .expect("Opening-close funding snapshot fits"); + } + }); + ActorHot::::mutate(actor_id, |maybe| { + let hot = maybe.as_mut().expect("Opening-close actor is active"); + hot.pending_signal = true; + hot.queue_ticket = Some(9); + }); + GlobalCircuitBreaker::::put(false); + let now: BlockNumberFor = 1u32.into(); + frame_system::Pallet::::set_block_number(now); + let (state, admission, loaded_step) = Pallet::::load_current_step_service_state(actor_id) + .expect("Opening-close service state is coherent"); + let LoadedActorStateOf::Active(full_state) = Pallet::::load_actor_state(actor_id) else { + panic!("Opening-close full state exists"); + }; + let execution_instance = Pallet::::derive_active_actor_view( + full_state.identity, + full_state.hot, + full_state.contract, + ); + let control_context = Pallet::::execution_step_control_weight_context( + &execution_instance, + state.run_state.as_ref(), + &loaded_step, + ) + .expect("Opening-close control context exists"); + assert_eq!(control_context.opening_tail_chunks, tail_chunks); + assert_eq!( + control_context.opening_snapshot_entries, + step_count.saturating_sub(1).saturating_mul(2), + ); + assert_eq!( + control_context.opening_predicate_results, + step_count.saturating_mul(T::MaxPredicatesPerStep::get()), + ); + assert_eq!(control_context.predicate_evaluation_units, 8); + assert_eq!( + T::StepControlWeight::maximum_control_weight(control_context, &loaded_step.step), + Some(loaded_step.resources.control), + ); + ActorHot::::mutate(actor_id, |maybe| { + maybe + .as_mut() + .expect("Opening-close hot state remains") + .queue_ticket = None; + }); + #[block] + { + let ticket = Pallet::::build_actor_step_ticket( + actor_id, + 9, + now, + &state.identity, + &state.hot, + state.run_state.as_ref(), + &admission, + ) + .expect("Opening-close Step ticket builds"); + let maximum_fee = Pallet::::maximum_current_step_fee( + state.identity.actor_class.actor_type(), + loaded_step.resources, + ) + .expect("Opening-close maximum fee exists"); + let plan = Pallet::::build_current_step_plan( + actor_id, + state.identity, + state.hot, + state.run_state, + state.funding, + admission, + ticket, + loaded_step, + maximum_fee, + ) + .expect("Opening-close plan builds"); + polkadot_sdk::frame_support::storage::with_transaction(|| { + match Pallet::::execute_current_step_and_place(actor_id, execution_instance, plan, now) { + Ok(evidence) => { + core::hint::black_box(evidence); + polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok::< + (), + AttemptTransactionError, + >(())) + } + Err(error) => { + polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)) + } + } + }) + .expect("Opening-close inner atom commits"); + } + assert!(ActorHot::::get(actor_id).is_none()); + assert!(ActorIdentities::::get(actor_id).is_none()); + assert!(ActorContractHeads::::get(actor_id).is_none()); + assert_eq!( + ActorContractTailChunks::::iter_prefix(actor_id).count(), + 0, + ); + } + + /// Measures direct maximum-geometry fresh-Opening completion at every C6 tail count. Every + /// authored Step contributes two Opening surfaces and four true Opening predicates; Step 0 + /// executes effect-free StopCycle and leaves no run or placement. + #[benchmark(pov_mode = Measured)] + fn scheduler_inner_opening_complete_max(t: Linear<0, 8>) { + let tail_chunks = t.min( + T::MaxContractSteps::get() + .saturating_sub(1) + .div_ceil(MAX_STEPS_PER_TAIL_CHUNK), + ); + let step_count = 1u32 + .saturating_add(tail_chunks.saturating_mul(MAX_STEPS_PER_TAIL_CHUNK)) + .min(T::MaxContractSteps::get()); + let opening_asset_count = step_count + .checked_mul(2) + .expect("Opening-completion asset count fits"); + let setup_asset_count = opening_asset_count.max(T::MaxFundingTrackedAssets::get()); + let asset_owner: T::AccountId = account("opening_complete_assets", 0, 0); + let assets = T::BenchmarkHelper::setup_predicate_assets(&asset_owner, setup_asset_count) + .expect("Opening-completion assets exist"); + assert_eq!(assets.len(), setup_asset_count as usize); + let mut steps = ContractSteps::::default(); + for (step_index, pair) in assets + .iter(/* deos-bypass: bounded-iter */) + .take(opening_asset_count as usize) + .copied() + .collect::>() + .chunks_exact(2) + .enumerate() + { + let asset_a = pair[0]; + let asset_b = pair[1]; + let predicates = alloc::vec![ + Predicate::BalanceEquals { + asset: asset_a, + threshold: Zero::zero(), + }, + Predicate::BalanceBelow { + asset: asset_a, + threshold: One::one(), + }, + Predicate::BalanceNotEquals { + asset: asset_b, + threshold: One::one(), + }, + Predicate::BalanceEquals { + asset: asset_b, + threshold: Zero::zero(), + }, + ]; + let task = if step_index == 0 { + ActorTask::StopCycle + } else { + ActorTask::AddLiquidity { + asset_a, + asset_b, + amount_a: AmountResolution::PercentageAtOpening(Perbill::one()), + amount_b: AmountResolution::PercentageAtOpening(Perbill::one()), + min_lp_out: One::one(), + } + }; + steps + .try_push(Step { + precondition: Some(opening_all::(predicates)), + task, + on_error: StepErrorPolicy::AbortCycle, + }) + .expect("Opening-completion Contract fits"); + } + assert_eq!(steps.len(), step_count as usize); + let actor_id = bench_create_system_manual::(41_675_000); + let contract = system_contract::( + Schedule { + trigger: Trigger::manual(), + cooldown_blocks: 0, + }, + steps, + ) + .expect("Opening-completion Contract exists"); + Pallet::::store_actor_contract(actor_id, contract) + .expect("Opening-completion geometry stores"); + ActorFunding::::mutate(actor_id, |maybe| { + let funding = maybe + .as_mut() + .expect("Opening-completion actor funding exists"); + for asset in assets + .iter(/* deos-bypass: bounded-iter */) + .take(T::MaxFundingTrackedAssets::get() as usize) + { + funding + .funding_tracked_assets + .try_insert(*asset) + .expect("Opening-completion tracked asset fits"); + funding + .funding_accumulated + .try_insert(*asset, One::one()) + .expect("Opening-completion funding snapshot fits"); + } + }); + ActorHot::::mutate(actor_id, |maybe| { + let hot = maybe.as_mut().expect("Opening-completion actor is active"); + hot.pending_signal = true; + hot.queue_ticket = Some(9); + }); + GlobalCircuitBreaker::::put(false); + let now: BlockNumberFor = 1u32.into(); + frame_system::Pallet::::set_block_number(now); + let (state, admission, loaded_step) = Pallet::::load_current_step_service_state(actor_id) + .expect("Opening-completion service state is coherent"); + let LoadedActorStateOf::Active(full_state) = Pallet::::load_actor_state(actor_id) else { + panic!("Opening-completion full state exists"); + }; + let execution_instance = Pallet::::derive_active_actor_view( + full_state.identity, + full_state.hot, + full_state.contract, + ); + let control_context = Pallet::::execution_step_control_weight_context( + &execution_instance, + state.run_state.as_ref(), + &loaded_step, + ) + .expect("Opening-completion control context exists"); + assert_eq!(control_context.opening_tail_chunks, tail_chunks); + assert_eq!( + control_context.opening_snapshot_entries, + step_count.saturating_sub(1).saturating_mul(2), + ); + assert_eq!( + control_context.opening_predicate_results, + step_count + .checked_mul(T::MaxPredicatesPerStep::get()) + .expect("Opening-completion predicate count fits"), + ); + assert_eq!(control_context.predicate_evaluation_units, 8); + assert_eq!( + T::StepControlWeight::maximum_control_weight(control_context, &loaded_step.step), + Some(loaded_step.resources.control), + ); + let _ = QueuePages::::clear(u32::MAX, None); + QueueHead::::put(0); + QueueTail::::put(0); + QueueOccupancy::::put(0); + NextQueueTicket::::put(0); + ActorHot::::mutate(actor_id, |maybe| { + maybe + .as_mut() + .expect("Opening-completion hot state remains") + .queue_ticket = None; + }); + #[block] + { + let ticket = Pallet::::build_actor_step_ticket( + actor_id, + 9, + now, + &state.identity, + &state.hot, + state.run_state.as_ref(), + &admission, + ) + .expect("Opening-completion Step ticket builds"); + let maximum_fee = Pallet::::maximum_current_step_fee( + state.identity.actor_class.actor_type(), + loaded_step.resources, + ) + .expect("Opening-completion maximum fee exists"); + let plan = Pallet::::build_current_step_plan( + actor_id, + state.identity, + state.hot, + state.run_state, + state.funding, + admission, + ticket, + loaded_step, + maximum_fee, + ) + .expect("Opening-completion plan builds"); + polkadot_sdk::frame_support::storage::with_transaction(|| { + match Pallet::::execute_current_step_and_place(actor_id, execution_instance, plan, now) { + Ok(evidence) => { + core::hint::black_box(evidence); + polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok::< + (), + AttemptTransactionError, + >(())) + } + Err(error) => { + polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)) + } + } + }) + .expect("Opening-completion inner atom commits"); + } + assert!(ActorRunStateStore::::get(actor_id).is_none()); + assert_eq!(QueueOccupancy::::get(), 0); + assert!(ActorHot::::get(actor_id).is_some_and(|hot| { + hot.cycle_state == CycleState::Idle + && hot.queue_ticket.is_none() + && hot.wakeup_pointer.is_none() + })); + } + + /// Measures direct maximum-geometry fresh-Opening progress after queue discovery, complete state + /// loading, and physical head consumption. Each tail geometry materializes the maximum realizable Contract + /// for that chunk count, maximum funding payload, four Opening predicates and two Opening amount + /// surfaces per Step, and effect-free false evaluation before causal FIFO successor placement. + #[benchmark(pov_mode = Measured)] + fn scheduler_inner_opening_progress_max(t: Linear<1, 8>) { + let tail_chunks = t.min( + T::MaxContractSteps::get() + .saturating_sub(1) + .div_ceil(MAX_STEPS_PER_TAIL_CHUNK), + ); + let step_count = 1u32 + .saturating_add(tail_chunks.saturating_mul(MAX_STEPS_PER_TAIL_CHUNK)) + .min(T::MaxContractSteps::get()); + let opening_asset_count = step_count + .checked_mul(2) + .expect("Opening-progress asset count fits"); + let setup_asset_count = opening_asset_count.max(T::MaxFundingTrackedAssets::get()); + let asset_owner: T::AccountId = account("opening_inner_assets", 0, 0); + let assets = T::BenchmarkHelper::setup_predicate_assets(&asset_owner, setup_asset_count) + .expect("Opening-progress assets exist"); + assert_eq!(assets.len(), setup_asset_count as usize); + let mut steps = ContractSteps::::default(); + for pair in assets + .iter(/* deos-bypass: bounded-iter */) + .take(opening_asset_count as usize) + .copied() + .collect::>() + .chunks_exact(2) + { + let asset_a = pair[0]; + let asset_b = pair[1]; + let predicates = alloc::vec![ + Predicate::BalanceAbove { + asset: asset_a, + threshold: One::one(), + }, + Predicate::BalanceBelow { + asset: asset_a, + threshold: One::one(), + }, + Predicate::BalanceEquals { + asset: asset_b, + threshold: Zero::zero(), + }, + Predicate::BalanceNotEquals { + asset: asset_b, + threshold: One::one(), + }, + ]; + steps + .try_push(Step { + precondition: Some(opening_all::(predicates)), + task: ActorTask::AddLiquidity { + asset_a, + asset_b, + amount_a: AmountResolution::PercentageAtOpening(Perbill::one()), + amount_b: AmountResolution::PercentageAtOpening(Perbill::one()), + min_lp_out: One::one(), + }, + on_error: StepErrorPolicy::AbortCycle, + }) + .expect("Opening-progress Contract fits"); + } + assert_eq!(steps.len(), step_count as usize); + let actor_id = bench_create_system_manual::(41_700_000); + let schedule = Schedule { + trigger: Trigger::manual(), + cooldown_blocks: 0, + }; + let contract = system_contract::(schedule, steps).expect("Opening-progress Contract exists"); + Pallet::::store_actor_contract(actor_id, contract) + .expect("Opening-progress geometry stores"); + ActorFunding::::mutate(actor_id, |maybe| { + let funding = maybe + .as_mut() + .expect("Opening-progress actor funding exists"); + for asset in assets + .iter(/* deos-bypass: bounded-iter */) + .take(T::MaxFundingTrackedAssets::get() as usize) + { + funding + .funding_tracked_assets + .try_insert(*asset) + .expect("Opening-progress tracked asset fits"); + funding + .funding_accumulated + .try_insert(*asset, One::one()) + .expect("Opening-progress funding snapshot fits"); + } + }); + assert_eq!( + ActorFunding::::get(actor_id) + .expect("Opening-progress funding exists") + .funding_accumulated + .len(), + T::MaxFundingTrackedAssets::get() as usize, + ); + ActorHot::::mutate(actor_id, |maybe| { + let hot = maybe.as_mut().expect("Opening-progress actor is active"); + hot.pending_signal = true; + hot.queue_ticket = Some(9); + }); + GlobalCircuitBreaker::::put(false); + let now: BlockNumberFor = 1u32.into(); + frame_system::Pallet::::set_block_number(now); + let (state, admission, loaded_step) = Pallet::::load_current_step_service_state(actor_id) + .expect("Opening-progress service state is coherent"); + let LoadedActorStateOf::Active(full_state) = Pallet::::load_actor_state(actor_id) else { + panic!("Opening-progress full state exists"); + }; + let execution_instance = Pallet::::derive_active_actor_view( + full_state.identity, + full_state.hot, + full_state.contract, + ); + let control_context = Pallet::::execution_step_control_weight_context( + &execution_instance, + state.run_state.as_ref(), + &loaded_step, + ) + .expect("Opening-progress control context exists"); + assert_eq!( + control_context.opening_tail_chunks, tail_chunks, + "Opening-progress context carries authored tail geometry", + ); + assert_eq!( + T::StepControlWeight::maximum_control_weight(control_context, &loaded_step.step), + Some(loaded_step.resources.control), + "Opening-progress execution context matches admission", + ); + let _ = QueuePages::::clear(u32::MAX, None); + QueueHead::::put(0); + QueueTail::::put(0); + QueueOccupancy::::put(0); + NextQueueTicket::::put(0); + ActorHot::::mutate(actor_id, |maybe| { + maybe + .as_mut() + .expect("Opening-progress hot state remains") + .queue_ticket = None; + }); + #[block] + { + let ticket = Pallet::::build_actor_step_ticket( + actor_id, + 9, + now, + &state.identity, + &state.hot, + state.run_state.as_ref(), + &admission, + ) + .expect("Opening-progress Step ticket builds"); + let maximum_fee = Pallet::::maximum_current_step_fee( + state.identity.actor_class.actor_type(), + loaded_step.resources, + ) + .expect("Opening-progress maximum fee exists"); + let plan = Pallet::::build_current_step_plan( + actor_id, + state.identity, + state.hot, + state.run_state, + state.funding, + admission, + ticket, + loaded_step, + maximum_fee, + ) + .expect("Opening-progress plan builds"); + polkadot_sdk::frame_support::storage::with_transaction(|| { + match Pallet::::execute_current_step_and_place(actor_id, execution_instance, plan, now) { + Ok(evidence) => { + core::hint::black_box(evidence); + polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok::< + (), + AttemptTransactionError, + >(())) + } + Err(error) => { + polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)) + } + } + }) + .expect("Opening-progress inner atom commits"); + } + let run = ActorRunStateStore::::get(actor_id).expect("Opening-progress run persists"); + assert_eq!(run.cursor, 1); + assert_eq!(run.opening_snapshot.len(), opening_asset_count as usize); + assert_eq!( + run.funding_snapshot.len(), + T::MaxFundingTrackedAssets::get() as usize, + ); + assert_eq!( + run.opening_predicate_results.len(), + step_count + .checked_mul(T::MaxPredicatesPerStep::get()) + .expect("Opening-progress predicate count fits") as usize, + ); + assert_eq!(run.last_committed_step_block, Some(now)); + assert_eq!(QueueOccupancy::::get(), 1); + assert!(ActorHot::::get(actor_id).is_some_and(|hot| hot.queue_ticket.is_some())); + } + + /// Measures the direct inner Running-final control owner after queue discovery, current-state + /// loading, and physical head consumption. The measured atom builds the exact carried plan, + /// evaluates one current Step, commits completion, validates actual evidence, and performs + /// post-placement. Task effect Weight is zero for StopCycle and false-predicate branches. + #[benchmark(pov_mode = Measured)] + fn scheduler_inner_running_complete(s: Linear<1, 4>, p: Linear<0, 4>) { + GlobalCircuitBreaker::::put(false); + let now: BlockNumberFor = 2u32.into(); + frame_system::Pallet::::set_block_number(now); + let actor_id = + bench_create_system_with_plan::(40_600_000, inert_contract_steps_of_len::(1 + s)); + let actor = Pallet::::sovereign_account_id_system(actor_id); + let predicate_count = p.min(T::MaxPredicatesPerStep::get()); + let assets = T::BenchmarkHelper::setup_predicate_assets(&actor, predicate_count) + .expect("Running predicate assets exist"); + assert_eq!(assets.len(), predicate_count as usize); + let mut contract = + Pallet::::load_actor_contract(actor_id).expect("Running benchmark Contract exists"); + if predicate_count > 0 { + let predicates = assets + .into_iter() + .map(|asset| Predicate::BalanceAbove { + asset, + threshold: One::one(), + }) + .collect::>(); + contract.steps[s as usize].precondition = Some(current_any::(predicates)); + } + Pallet::::store_actor_contract(actor_id, contract) + .expect("Running benchmark Contract remains admitted"); + install_run_state::(actor_id, 0); + ActorRunStateStore::::mutate(actor_id, |maybe| { + let run = maybe.as_mut().expect("Running benchmark run exists"); + maximize_run_state_geometry::(run); + run.cursor = s; + run.opening_predicate_cursor = 0; + run.unsuccessful_attempts_at_cursor = 0; + run.cumulative_outcomes = OutcomeTotals::default(); + run.last_step_outcome = None; + run.last_committed_step_block = Some(1u32.into()); + run.eligible_at = now; + run.suspension = None; + }); + ActorHot::::mutate(actor_id, |maybe| { + let hot = maybe.as_mut().expect("Running benchmark hot state exists"); + hot.cycle_state = CycleState::Running; + hot.queue_ticket = Some(9); + hot.wakeup_pointer = None; + }); + let (state, admission, loaded_step) = Pallet::::load_current_step_service_state(actor_id) + .expect("Running benchmark service state is coherent"); + let execution_instance = Pallet::::derive_active_actor_view( + state.identity.clone(), + state.hot.clone(), + state.contract.clone(), + ); + ActorHot::::mutate(actor_id, |maybe| { + maybe + .as_mut() + .expect("Running benchmark hot state remains") + .queue_ticket = None; + }); + #[block] + { + let ticket = Pallet::::build_actor_step_ticket( + actor_id, + 9, + now, + &state.identity, + &state.hot, + state.run_state.as_ref(), + &admission, + ) + .expect("Running benchmark Step ticket builds"); + let maximum_fee = Pallet::::maximum_current_step_fee( + state.identity.actor_class.actor_type(), + loaded_step.resources, + ) + .expect("Running benchmark maximum fee exists"); + let plan = Pallet::::build_current_step_plan( + actor_id, + state.identity, + state.hot, + state.run_state, + state.funding, + admission, + ticket, + loaded_step, + maximum_fee, + ) + .expect("Running benchmark plan builds"); + polkadot_sdk::frame_support::storage::with_transaction(|| { + match Pallet::::execute_current_step_and_place(actor_id, execution_instance, plan, now) { + Ok(evidence) => { + core::hint::black_box(evidence); + polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok::< + (), + AttemptTransactionError, + >(())) + } + Err(error) => { + polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)) + } + } + }) + .expect("Running benchmark inner atom commits"); + } + assert!(ActorRunStateStore::::get(actor_id).is_none()); + assert_eq!( + ActorIdentities::::get(actor_id) + .expect("Running benchmark identity remains") + .cycle_nonce, + 1, + ); + } + + /// Measures the direct inner Running-progress owner through causal FIFO successor placement. + /// A fixed-zero Transfer or false current predicates skip without Task effect execution. + #[benchmark(pov_mode = Measured)] + fn scheduler_inner_running_progress(s: Linear<2, 4>, p: Linear<0, 4>) { + let _ = QueuePages::::clear(u32::MAX, None); + QueueHead::::put(0); + QueueTail::::put(0); + QueueOccupancy::::put(0); + NextQueueTicket::::put(0); + GlobalCircuitBreaker::::put(false); + let now: BlockNumberFor = 2u32.into(); + frame_system::Pallet::::set_block_number(now); + let actor_id = + bench_create_system_with_plan::(40_700_000, inert_contract_steps_of_len::(1 + s)); + let actor = Pallet::::sovereign_account_id_system(actor_id); + let predicate_count = p.min(T::MaxPredicatesPerStep::get()); + let assets = T::BenchmarkHelper::setup_predicate_assets(&actor, predicate_count) + .expect("Running-progress predicate assets exist"); + assert_eq!(assets.len(), predicate_count as usize); + let mut contract = + Pallet::::load_actor_contract(actor_id).expect("Running-progress Contract exists"); + contract.steps[1].task = ActorTask::Transfer { + to: actor.clone(), + asset: T::FeeNativeAssetId::get(), + amount: AmountResolution::Fixed(Zero::zero()), + }; + if predicate_count > 0 { + let predicates = assets + .into_iter() + .map(|asset| Predicate::BalanceAbove { + asset, + threshold: One::one(), + }) + .collect::>(); + contract.steps[1].precondition = Some(current_any::(predicates)); + } + Pallet::::store_actor_contract(actor_id, contract) + .expect("Running-progress Contract remains admitted"); + install_run_state::(actor_id, 0); + ActorRunStateStore::::mutate(actor_id, |maybe| { + let run = maybe.as_mut().expect("Running-progress run exists"); + maximize_run_state_geometry::(run); + run.cursor = 1; + run.opening_predicate_cursor = 0; + run.unsuccessful_attempts_at_cursor = 0; + run.cumulative_outcomes = OutcomeTotals::default(); + run.last_step_outcome = None; + run.last_committed_step_block = Some(1u32.into()); + run.eligible_at = now; + run.suspension = None; + }); + ActorHot::::mutate(actor_id, |maybe| { + let hot = maybe.as_mut().expect("Running-progress hot state exists"); + hot.cycle_state = CycleState::Running; + hot.queue_ticket = Some(9); + hot.wakeup_pointer = None; + }); + let (state, admission, loaded_step) = Pallet::::load_current_step_service_state(actor_id) + .expect("Running-progress service state is coherent"); + let execution_instance = Pallet::::derive_active_actor_view( + state.identity.clone(), + state.hot.clone(), + state.contract.clone(), + ); + ActorHot::::mutate(actor_id, |maybe| { + maybe + .as_mut() + .expect("Running-progress hot state remains") + .queue_ticket = None; + }); + #[block] + { + let ticket = Pallet::::build_actor_step_ticket( + actor_id, + 9, + now, + &state.identity, + &state.hot, + state.run_state.as_ref(), + &admission, + ) + .expect("Running-progress Step ticket builds"); + let maximum_fee = Pallet::::maximum_current_step_fee( + state.identity.actor_class.actor_type(), + loaded_step.resources, + ) + .expect("Running-progress maximum fee exists"); + let plan = Pallet::::build_current_step_plan( + actor_id, + state.identity, + state.hot, + state.run_state, + state.funding, + admission, + ticket, + loaded_step, + maximum_fee, + ) + .expect("Running-progress plan builds"); + polkadot_sdk::frame_support::storage::with_transaction(|| { + match Pallet::::execute_current_step_and_place(actor_id, execution_instance, plan, now) { + Ok(evidence) => { + core::hint::black_box(evidence); + polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok::< + (), + AttemptTransactionError, + >(())) + } + Err(error) => { + polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)) + } + } + }) + .expect("Running-progress inner atom commits"); + } + let run = ActorRunStateStore::::get(actor_id).expect("Running-progress run remains"); + assert_eq!(run.cursor, 2); + assert_eq!(run.last_committed_step_block, Some(now)); + assert_eq!(QueueOccupancy::::get(), 1); + assert!(ActorHot::::get(actor_id).is_some_and(|hot| hot.queue_ticket.is_some())); + } + + /// Measures the direct inner Suspended-tail retry owner through durable wakeup placement. + /// Current predicates are true and an unfunded fixed Transfer yields FundingUnavailable before + /// Task effect invocation, preserving retry classification without effect contamination. + #[benchmark(pov_mode = Measured)] + fn scheduler_inner_suspended_tail_retry(s: Linear<1, 4>, p: Linear<0, 4>) { + let _ = WakeupBuckets::::clear(u32::MAX, None); + let _ = WakeupPages::::clear(u32::MAX, None); + let _ = WakeupCursorPages::::clear(u32::MAX, None); + let _ = WakeupCursorLen::::clear(u32::MAX, None); + GlobalCircuitBreaker::::put(false); + let now: BlockNumberFor = 2u32.into(); + frame_system::Pallet::::set_block_number(now); + let actor_id = + bench_create_system_with_plan::(40_800_000, inert_contract_steps_of_len::(1 + s)); + let actor = Pallet::::sovereign_account_id_system(actor_id); + let predicate_count = p.min(T::MaxPredicatesPerStep::get()); + let assets = T::BenchmarkHelper::setup_predicate_assets(&actor, predicate_count) + .expect("Suspended-tail predicate assets exist"); + assert_eq!(assets.len(), predicate_count as usize); + let mut contract = + Pallet::::load_actor_contract(actor_id).expect("Suspended-tail Contract exists"); + contract.steps[s as usize].task = ActorTask::Transfer { + to: actor.clone(), + asset: T::FeeNativeAssetId::get(), + amount: AmountResolution::Fixed(One::one()), + }; + contract.steps[s as usize].on_error = StepErrorPolicy::RetryLater { + max_attempts: T::MaxRetryAttempts::get(), + }; + if predicate_count > 0 { + let predicates = assets + .into_iter() + .map(|asset| Predicate::BalanceBelow { + asset, + threshold: One::one(), + }) + .collect::>(); + contract.steps[s as usize].precondition = Some(current_any::(predicates)); + } + Pallet::::store_actor_contract(actor_id, contract) + .expect("Suspended-tail Contract remains admitted"); + install_run_state::(actor_id, 0); + ActorRunStateStore::::mutate(actor_id, |maybe| { + let run = maybe.as_mut().expect("Suspended-tail run exists"); + maximize_run_state_geometry::(run); + run.cursor = s; + run.opening_predicate_cursor = 0; + run.cumulative_outcomes = OutcomeTotals::default(); + run.last_step_outcome = Some(StepOutcome::FundingUnavailable); + run.last_attempt_block = 1u32.into(); + run.last_committed_step_block = Some(0u32.into()); + run.eligible_at = now; + run.suspension = Some(SuspensionReason::FundingUnavailable); + }); + ActorHot::::mutate(actor_id, |maybe| { + let hot = maybe.as_mut().expect("Suspended-tail hot state exists"); + hot.cycle_state = CycleState::Suspended; + hot.queue_ticket = Some(9); + hot.wakeup_pointer = None; + }); + let (state, admission, loaded_step) = Pallet::::load_current_step_service_state(actor_id) + .expect("Suspended-tail service state is coherent"); + let execution_instance = Pallet::::derive_active_actor_view( + state.identity.clone(), + state.hot.clone(), + state.contract.clone(), + ); + ActorHot::::mutate(actor_id, |maybe| { + maybe + .as_mut() + .expect("Suspended-tail hot state remains") + .queue_ticket = None; + }); + #[block] + { + let ticket = Pallet::::build_actor_step_ticket( + actor_id, + 9, + now, + &state.identity, + &state.hot, + state.run_state.as_ref(), + &admission, + ) + .expect("Suspended-tail Step ticket builds"); + let maximum_fee = Pallet::::maximum_current_step_fee( + state.identity.actor_class.actor_type(), + loaded_step.resources, + ) + .expect("Suspended-tail maximum fee exists"); + let plan = Pallet::::build_current_step_plan( + actor_id, + state.identity, + state.hot, + state.run_state, + state.funding, + admission, + ticket, + loaded_step, + maximum_fee, + ) + .expect("Suspended-tail plan builds"); + polkadot_sdk::frame_support::storage::with_transaction(|| { + match Pallet::::execute_current_step_and_place(actor_id, execution_instance, plan, now) { + Ok(evidence) => { + core::hint::black_box(evidence); + polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok::< + (), + AttemptTransactionError, + >(())) + } + Err(error) => { + polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)) + } + } + }) + .expect("Suspended-tail inner atom commits"); + } + let run = ActorRunStateStore::::get(actor_id).expect("Suspended-tail run remains"); + assert_eq!(run.cursor, s); + assert_eq!(run.unsuccessful_attempts_at_cursor, 2); + assert_eq!(run.last_attempt_block, now); + assert!(ActorHot::::get(actor_id).is_some_and(|hot| hot.wakeup_pointer.is_some())); + } + + /// Measures the direct inner Suspended-tail successful-completion owner. + /// A fixed-zero Transfer or false current predicates skips without Task effect execution. + #[benchmark(pov_mode = Measured)] + fn scheduler_inner_suspended_tail_complete(s: Linear<1, 4>, p: Linear<0, 4>) { + GlobalCircuitBreaker::::put(false); + let now: BlockNumberFor = 2u32.into(); + frame_system::Pallet::::set_block_number(now); + let actor_id = + bench_create_system_with_plan::(40_900_000, inert_contract_steps_of_len::(1 + s)); + let actor = Pallet::::sovereign_account_id_system(actor_id); + let predicate_count = p.min(T::MaxPredicatesPerStep::get()); + let assets = T::BenchmarkHelper::setup_predicate_assets(&actor, predicate_count) + .expect("Suspended-complete predicate assets exist"); + assert_eq!(assets.len(), predicate_count as usize); + let mut contract = + Pallet::::load_actor_contract(actor_id).expect("Suspended-complete Contract exists"); + contract.steps[s as usize].task = ActorTask::Transfer { + to: actor.clone(), + asset: T::FeeNativeAssetId::get(), + amount: AmountResolution::Fixed(Zero::zero()), + }; + contract.steps[s as usize].on_error = StepErrorPolicy::RetryLater { + max_attempts: T::MaxRetryAttempts::get(), + }; + if predicate_count > 0 { + let predicates = assets + .into_iter() + .map(|asset| Predicate::BalanceAbove { + asset, + threshold: One::one(), + }) + .collect::>(); + contract.steps[s as usize].precondition = Some(current_any::(predicates)); + } + Pallet::::store_actor_contract(actor_id, contract) + .expect("Suspended-complete Contract remains admitted"); + install_run_state::(actor_id, 0); + ActorRunStateStore::::mutate(actor_id, |maybe| { + let run = maybe.as_mut().expect("Suspended-complete run exists"); + maximize_run_state_geometry::(run); + run.cursor = s; + run.opening_predicate_cursor = 0; + run.cumulative_outcomes = OutcomeTotals::default(); + run.last_step_outcome = Some(StepOutcome::FundingUnavailable); + run.last_attempt_block = 1u32.into(); + run.last_committed_step_block = Some(0u32.into()); + run.eligible_at = now; + run.suspension = Some(SuspensionReason::FundingUnavailable); + }); + ActorHot::::mutate(actor_id, |maybe| { + let hot = maybe.as_mut().expect("Suspended-complete hot state exists"); + hot.cycle_state = CycleState::Suspended; + hot.queue_ticket = Some(9); + hot.wakeup_pointer = None; + }); + let (state, admission, loaded_step) = Pallet::::load_current_step_service_state(actor_id) + .expect("Suspended-complete service state is coherent"); + let execution_instance = Pallet::::derive_active_actor_view( + state.identity.clone(), + state.hot.clone(), + state.contract.clone(), + ); + ActorHot::::mutate(actor_id, |maybe| { + maybe + .as_mut() + .expect("Suspended-complete hot state remains") + .queue_ticket = None; + }); + #[block] + { + let ticket = Pallet::::build_actor_step_ticket( + actor_id, + 9, + now, + &state.identity, + &state.hot, + state.run_state.as_ref(), + &admission, + ) + .expect("Suspended-complete Step ticket builds"); + let maximum_fee = Pallet::::maximum_current_step_fee( + state.identity.actor_class.actor_type(), + loaded_step.resources, + ) + .expect("Suspended-complete maximum fee exists"); + let plan = Pallet::::build_current_step_plan( + actor_id, + state.identity, + state.hot, + state.run_state, + state.funding, + admission, + ticket, + loaded_step, + maximum_fee, + ) + .expect("Suspended-complete plan builds"); + polkadot_sdk::frame_support::storage::with_transaction(|| { + match Pallet::::execute_current_step_and_place(actor_id, execution_instance, plan, now) { + Ok(evidence) => { + core::hint::black_box(evidence); + polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok::< + (), + AttemptTransactionError, + >(())) + } + Err(error) => { + polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)) + } + } + }) + .expect("Suspended-complete inner atom commits"); + } + assert!(ActorRunStateStore::::get(actor_id).is_none()); + assert!(ActorHot::::get(actor_id).is_some_and(|hot| { + hot.cycle_state == CycleState::Idle + && hot.queue_ticket.is_none() + && hot.wakeup_pointer.is_none() + })); + } + + /// Measures the direct inner Suspended-tail successful-progress owner through causal FIFO + /// successor placement. A fixed-zero Transfer or false current predicates skips without Task + /// effect execution. + #[benchmark(pov_mode = Measured)] + fn scheduler_inner_suspended_tail_progress(s: Linear<2, 4>, p: Linear<0, 4>) { + let _ = QueuePages::::clear(u32::MAX, None); + QueueHead::::put(0); + QueueTail::::put(0); + QueueOccupancy::::put(0); + NextQueueTicket::::put(0); + GlobalCircuitBreaker::::put(false); + let now: BlockNumberFor = 2u32.into(); + frame_system::Pallet::::set_block_number(now); + let actor_id = + bench_create_system_with_plan::(41_000_000, inert_contract_steps_of_len::(1 + s)); + let actor = Pallet::::sovereign_account_id_system(actor_id); + let predicate_count = p.min(T::MaxPredicatesPerStep::get()); + let assets = T::BenchmarkHelper::setup_predicate_assets(&actor, predicate_count) + .expect("Suspended-progress predicate assets exist"); + assert_eq!(assets.len(), predicate_count as usize); + let mut contract = + Pallet::::load_actor_contract(actor_id).expect("Suspended-progress Contract exists"); + contract.steps[1].task = ActorTask::Transfer { + to: actor.clone(), + asset: T::FeeNativeAssetId::get(), + amount: AmountResolution::Fixed(Zero::zero()), + }; + contract.steps[1].on_error = StepErrorPolicy::RetryLater { + max_attempts: T::MaxRetryAttempts::get(), + }; + if predicate_count > 0 { + let predicates = assets + .into_iter() + .map(|asset| Predicate::BalanceAbove { + asset, + threshold: One::one(), + }) + .collect::>(); + contract.steps[1].precondition = Some(current_any::(predicates)); + } + Pallet::::store_actor_contract(actor_id, contract) + .expect("Suspended-progress Contract remains admitted"); + install_run_state::(actor_id, 0); + ActorRunStateStore::::mutate(actor_id, |maybe| { + let run = maybe.as_mut().expect("Suspended-progress run exists"); + maximize_run_state_geometry::(run); + run.cursor = 1; + run.opening_predicate_cursor = 0; + run.cumulative_outcomes = OutcomeTotals::default(); + run.last_step_outcome = Some(StepOutcome::FundingUnavailable); + run.last_attempt_block = 1u32.into(); + run.last_committed_step_block = Some(0u32.into()); + run.eligible_at = now; + run.suspension = Some(SuspensionReason::FundingUnavailable); + }); + ActorHot::::mutate(actor_id, |maybe| { + let hot = maybe.as_mut().expect("Suspended-progress hot state exists"); + hot.cycle_state = CycleState::Suspended; + hot.queue_ticket = Some(9); + hot.wakeup_pointer = None; + }); + let (state, admission, loaded_step) = Pallet::::load_current_step_service_state(actor_id) + .expect("Suspended-progress service state is coherent"); + let execution_instance = Pallet::::derive_active_actor_view( + state.identity.clone(), + state.hot.clone(), + state.contract.clone(), + ); + ActorHot::::mutate(actor_id, |maybe| { + maybe + .as_mut() + .expect("Suspended-progress hot state remains") + .queue_ticket = None; + }); + #[block] + { + let ticket = Pallet::::build_actor_step_ticket( + actor_id, + 9, + now, + &state.identity, + &state.hot, + state.run_state.as_ref(), + &admission, + ) + .expect("Suspended-progress Step ticket builds"); + let maximum_fee = Pallet::::maximum_current_step_fee( + state.identity.actor_class.actor_type(), + loaded_step.resources, + ) + .expect("Suspended-progress maximum fee exists"); + let plan = Pallet::::build_current_step_plan( + actor_id, + state.identity, + state.hot, + state.run_state, + state.funding, + admission, + ticket, + loaded_step, + maximum_fee, + ) + .expect("Suspended-progress plan builds"); + polkadot_sdk::frame_support::storage::with_transaction(|| { + match Pallet::::execute_current_step_and_place(actor_id, execution_instance, plan, now) { + Ok(evidence) => { + core::hint::black_box(evidence); + polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok::< + (), + AttemptTransactionError, + >(())) + } + Err(error) => { + polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)) + } + } + }) + .expect("Suspended-progress inner atom commits"); + } + let run = ActorRunStateStore::::get(actor_id).expect("Suspended-progress run remains"); + assert_eq!(run.cursor, 2); + assert_eq!(run.last_committed_step_block, Some(now)); + assert_eq!(QueueOccupancy::::get(), 1); + assert!( + ActorHot::::get(actor_id).is_some_and(|hot| { + hot.cycle_state == CycleState::Running && hot.queue_ticket.is_some() + }) + ); + } + + /// Measures the direct inner Suspended-head retry owner across retained immutable payload + /// geometry and current predicates. An unfunded fixed Transfer yields FundingUnavailable before + /// Task effect invocation. + #[benchmark(pov_mode = Measured)] + fn scheduler_inner_suspended_head_retry( + n: Linear<0, 64>, + r: Linear<0, 128>, + f: Linear<0, 40>, + p: Linear<0, 4>, + ) { + let _ = WakeupBuckets::::clear(u32::MAX, None); + let _ = WakeupPages::::clear(u32::MAX, None); + let _ = WakeupCursorPages::::clear(u32::MAX, None); + let _ = WakeupCursorLen::::clear(u32::MAX, None); + GlobalCircuitBreaker::::put(false); + let now: BlockNumberFor = 2u32.into(); + frame_system::Pallet::::set_block_number(now); + let actor_id = + bench_create_system_with_plan::(41_100_000, inert_contract_steps_of_len::(1)); + let actor = Pallet::::sovereign_account_id_system(actor_id); + let evaluation_units = p.min(T::MaxPredicatesPerStep::get().saturating_mul(2)); + let opening_count = evaluation_units.saturating_sub(T::MaxPredicatesPerStep::get()); + let current_count = evaluation_units.saturating_sub(opening_count.saturating_mul(2)); + let predicate_count = opening_count.saturating_add(current_count); + let assets = T::BenchmarkHelper::setup_predicate_assets(&actor, predicate_count) + .expect("Suspended-head predicate assets exist"); + assert_eq!(assets.len(), predicate_count as usize); + let mut contract = + Pallet::::load_actor_contract(actor_id).expect("Suspended-head Contract exists"); + contract.steps[0].task = ActorTask::Transfer { + to: actor.clone(), + asset: T::FeeNativeAssetId::get(), + amount: AmountResolution::Fixed(One::one()), + }; + contract.steps[0].on_error = StepErrorPolicy::RetryLater { + max_attempts: T::MaxRetryAttempts::get(), + }; + if predicate_count > 0 { + let clause = assets + .into_iter() + .enumerate() + .map(|(index, asset)| TimedPredicate { + timing: if index < opening_count as usize { + ObservationTiming::Opening + } else { + ObservationTiming::Current + }, + predicate: Predicate::BalanceBelow { + asset, + threshold: One::one(), + }, + }) + .collect::>(); + contract.steps[0].precondition = Some(Precondition { + clauses: BoundedVec::try_from(alloc::vec![ + BoundedVec::try_from(clause).expect("Suspended-head predicates fit"), + ]) + .expect("Suspended-head clause fits"), + }); + } + Pallet::::store_actor_contract(actor_id, contract) + .expect("Suspended-head Contract remains admitted"); + let snapshot_count = n.min(T::MaxOpeningSnapshotEntries::get()); + let result_count = r + .min(T::MaxOpeningPredicateResults::get()) + .max(opening_count); + let funding_count = f.min(T::MaxFundingTrackedAssets::get()); + install_run_state::(actor_id, snapshot_count); + ActorRunStateStore::::mutate(actor_id, |maybe| { + let run = maybe.as_mut().expect("Suspended-head run exists"); + for index in 0..result_count { + run + .opening_predicate_results + .try_push(if index < opening_count { + Ok(true) + } else if index % 2 == 0 { + Ok(true) + } else { + Err(PredicateError::InvalidObservation) + }) + .expect("Suspended-head Opening result fits"); + } + for asset in T::BenchmarkHelper::funding_assets(funding_count) { + run + .funding_snapshot + .try_insert(asset, One::one()) + .expect("Suspended-head funding entry fits"); + } + assert_eq!(run.opening_snapshot.len() as u32, snapshot_count); + assert_eq!(run.opening_predicate_results.len() as u32, result_count); + assert_eq!(run.funding_snapshot.len() as u32, funding_count); + run.cursor = 0; + run.opening_predicate_cursor = 0; + run.cumulative_outcomes = OutcomeTotals::default(); + run.last_step_outcome = Some(StepOutcome::FundingUnavailable); + run.last_attempt_block = 1u32.into(); + run.last_committed_step_block = None; + run.eligible_at = now; + run.suspension = Some(SuspensionReason::FundingUnavailable); + }); + ActorHot::::mutate(actor_id, |maybe| { + let hot = maybe.as_mut().expect("Suspended-head hot state exists"); + hot.cycle_state = CycleState::Suspended; + hot.queue_ticket = Some(9); + hot.wakeup_pointer = None; + }); + let (state, admission, loaded_step) = Pallet::::load_current_step_service_state(actor_id) + .expect("Suspended-head service state is coherent"); + let execution_instance = Pallet::::derive_active_actor_view( + state.identity.clone(), + state.hot.clone(), + state.contract.clone(), + ); + ActorHot::::mutate(actor_id, |maybe| { + maybe + .as_mut() + .expect("Suspended-head hot state remains") + .queue_ticket = None; + }); + #[block] + { + let ticket = Pallet::::build_actor_step_ticket( + actor_id, + 9, + now, + &state.identity, + &state.hot, + state.run_state.as_ref(), + &admission, + ) + .expect("Suspended-head Step ticket builds"); + let maximum_fee = Pallet::::maximum_current_step_fee( + state.identity.actor_class.actor_type(), + loaded_step.resources, + ) + .expect("Suspended-head maximum fee exists"); + let plan = Pallet::::build_current_step_plan( + actor_id, + state.identity, + state.hot, + state.run_state, + state.funding, + admission, + ticket, + loaded_step, + maximum_fee, + ) + .expect("Suspended-head plan builds"); + polkadot_sdk::frame_support::storage::with_transaction(|| { + match Pallet::::execute_current_step_and_place(actor_id, execution_instance, plan, now) { + Ok(evidence) => { + core::hint::black_box(evidence); + polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok::< + (), + AttemptTransactionError, + >(())) + } + Err(error) => { + polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)) + } + } + }) + .expect("Suspended-head inner atom commits"); + } + let run = ActorRunStateStore::::get(actor_id).expect("Suspended-head run remains"); + assert_eq!(run.cursor, 0); + assert_eq!(run.unsuccessful_attempts_at_cursor, 2); + assert_eq!(run.last_attempt_block, now); + assert!(ActorHot::::get(actor_id).is_some_and(|hot| hot.wakeup_pointer.is_some())); + } + + /// Measures the direct inner current-predicate Suspended-head completion owner across retained + /// immutable payload geometry. A fixed-zero Transfer or false current predicates skips without + /// Task effect execution. + #[benchmark(pov_mode = Measured)] + fn scheduler_inner_suspended_head_complete( + n: Linear<0, 64>, + r: Linear<0, 128>, + f: Linear<0, 40>, + p: Linear<0, 4>, + ) { + GlobalCircuitBreaker::::put(false); + let now: BlockNumberFor = 2u32.into(); + frame_system::Pallet::::set_block_number(now); + let actor_id = + bench_create_system_with_plan::(41_200_000, inert_contract_steps_of_len::(1)); + let actor = Pallet::::sovereign_account_id_system(actor_id); + let evaluation_units = p.min(T::MaxPredicatesPerStep::get().saturating_mul(2)); + let opening_count = evaluation_units.saturating_sub(T::MaxPredicatesPerStep::get()); + let current_count = evaluation_units.saturating_sub(opening_count.saturating_mul(2)); + let predicate_count = opening_count.saturating_add(current_count); + let assets = T::BenchmarkHelper::setup_predicate_assets(&actor, predicate_count) + .expect("Suspended-head completion predicate assets exist"); + assert_eq!(assets.len(), predicate_count as usize); + let mut contract = Pallet::::load_actor_contract(actor_id) + .expect("Suspended-head completion Contract exists"); + contract.steps[0].task = ActorTask::Transfer { + to: actor.clone(), + asset: T::FeeNativeAssetId::get(), + amount: AmountResolution::Fixed(Zero::zero()), + }; + contract.steps[0].on_error = StepErrorPolicy::RetryLater { + max_attempts: T::MaxRetryAttempts::get(), + }; + if predicate_count > 0 { + let clause = assets + .into_iter() + .enumerate() + .map(|(index, asset)| TimedPredicate { + timing: if index < opening_count as usize { + ObservationTiming::Opening + } else { + ObservationTiming::Current + }, + predicate: if index + 1 == predicate_count as usize && current_count > 0 { + Predicate::BalanceAbove { + asset, + threshold: One::one(), + } + } else { + Predicate::BalanceBelow { + asset, + threshold: One::one(), + } + }, + }) + .collect::>(); + contract.steps[0].precondition = Some(Precondition { + clauses: BoundedVec::try_from(alloc::vec![ + BoundedVec::try_from(clause).expect("Suspended-head completion predicates fit"), + ]) + .expect("Suspended-head completion clause fits"), + }); + } + Pallet::::store_actor_contract(actor_id, contract) + .expect("Suspended-head completion Contract remains admitted"); + let snapshot_count = n.min(T::MaxOpeningSnapshotEntries::get()); + let result_count = r + .min(T::MaxOpeningPredicateResults::get()) + .max(opening_count); + let funding_count = f.min(T::MaxFundingTrackedAssets::get()); + install_run_state::(actor_id, snapshot_count); + ActorRunStateStore::::mutate(actor_id, |maybe| { + let run = maybe + .as_mut() + .expect("Suspended-head completion run exists"); + for index in 0..result_count { + let frozen = if current_count == 0 && index + 1 == opening_count { + Ok(false) + } else if index < opening_count || index % 2 == 0 { + Ok(true) + } else { + Err(PredicateError::InvalidObservation) + }; + run + .opening_predicate_results + .try_push(frozen) + .expect("Suspended-head completion Opening result fits"); + } + for asset in T::BenchmarkHelper::funding_assets(funding_count) { + run + .funding_snapshot + .try_insert(asset, One::one()) + .expect("Suspended-head completion funding entry fits"); + } + assert_eq!(run.opening_snapshot.len() as u32, snapshot_count); + assert_eq!(run.opening_predicate_results.len() as u32, result_count); + assert_eq!(run.funding_snapshot.len() as u32, funding_count); + run.cursor = 0; + run.opening_predicate_cursor = 0; + run.cumulative_outcomes = OutcomeTotals::default(); + run.last_step_outcome = Some(StepOutcome::FundingUnavailable); + run.last_attempt_block = 1u32.into(); + run.last_committed_step_block = None; + run.eligible_at = now; + run.suspension = Some(SuspensionReason::FundingUnavailable); + }); + ActorHot::::mutate(actor_id, |maybe| { + let hot = maybe + .as_mut() + .expect("Suspended-head completion hot state exists"); + hot.cycle_state = CycleState::Suspended; + hot.queue_ticket = Some(9); + hot.wakeup_pointer = None; + }); + let (state, admission, loaded_step) = Pallet::::load_current_step_service_state(actor_id) + .expect("Suspended-head completion service state is coherent"); + let execution_instance = Pallet::::derive_active_actor_view( + state.identity.clone(), + state.hot.clone(), + state.contract.clone(), + ); + ActorHot::::mutate(actor_id, |maybe| { + maybe + .as_mut() + .expect("Suspended-head completion hot state remains") + .queue_ticket = None; + }); + #[block] + { + let ticket = Pallet::::build_actor_step_ticket( + actor_id, + 9, + now, + &state.identity, + &state.hot, + state.run_state.as_ref(), + &admission, + ) + .expect("Suspended-head completion Step ticket builds"); + let maximum_fee = Pallet::::maximum_current_step_fee( + state.identity.actor_class.actor_type(), + loaded_step.resources, + ) + .expect("Suspended-head completion maximum fee exists"); + let plan = Pallet::::build_current_step_plan( + actor_id, + state.identity, + state.hot, + state.run_state, + state.funding, + admission, + ticket, + loaded_step, + maximum_fee, + ) + .expect("Suspended-head completion plan builds"); + polkadot_sdk::frame_support::storage::with_transaction(|| { + match Pallet::::execute_current_step_and_place(actor_id, execution_instance, plan, now) { + Ok(evidence) => { + core::hint::black_box(evidence); + polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok::< + (), + AttemptTransactionError, + >(())) + } + Err(error) => { + polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)) + } + } + }) + .expect("Suspended-head completion inner atom commits"); + } + assert!(ActorRunStateStore::::get(actor_id).is_none()); + assert!(ActorHot::::get(actor_id).is_some_and(|hot| { + hot.cycle_state == CycleState::Idle + && hot.queue_ticket.is_none() + && hot.wakeup_pointer.is_none() + })); + } + + /// Measures the direct inner current-predicate Suspended-head progress owner across retained + /// immutable payload geometry and causal FIFO successor placement. A fixed-zero Transfer or + /// false current predicates skips without Task effect execution. + #[benchmark(pov_mode = Measured)] + fn scheduler_inner_suspended_head_progress( + n: Linear<0, 64>, + r: Linear<0, 128>, + f: Linear<0, 40>, + p: Linear<0, 4>, + ) { + let _ = QueuePages::::clear(u32::MAX, None); + QueueHead::::put(0); + QueueTail::::put(0); + QueueOccupancy::::put(0); + NextQueueTicket::::put(0); + GlobalCircuitBreaker::::put(false); + let now: BlockNumberFor = 2u32.into(); + frame_system::Pallet::::set_block_number(now); + let actor_id = + bench_create_system_with_plan::(41_300_000, inert_contract_steps_of_len::(2)); + let actor = Pallet::::sovereign_account_id_system(actor_id); + let evaluation_units = p.min(T::MaxPredicatesPerStep::get().saturating_mul(2)); + let opening_count = evaluation_units.saturating_sub(T::MaxPredicatesPerStep::get()); + let current_count = evaluation_units.saturating_sub(opening_count.saturating_mul(2)); + let predicate_count = opening_count.saturating_add(current_count); + let assets = T::BenchmarkHelper::setup_predicate_assets(&actor, predicate_count) + .expect("Suspended-head progress predicate assets exist"); + assert_eq!(assets.len(), predicate_count as usize); + let mut contract = + Pallet::::load_actor_contract(actor_id).expect("Suspended-head progress Contract exists"); + contract.steps[0].task = ActorTask::Transfer { + to: actor.clone(), + asset: T::FeeNativeAssetId::get(), + amount: AmountResolution::Fixed(Zero::zero()), + }; + contract.steps[0].on_error = StepErrorPolicy::RetryLater { + max_attempts: T::MaxRetryAttempts::get(), + }; + if predicate_count > 0 { + let clause = assets + .into_iter() + .enumerate() + .map(|(index, asset)| TimedPredicate { + timing: if index < opening_count as usize { + ObservationTiming::Opening + } else { + ObservationTiming::Current + }, + predicate: if index + 1 == predicate_count as usize && current_count > 0 { + Predicate::BalanceAbove { + asset, + threshold: One::one(), + } + } else { + Predicate::BalanceBelow { + asset, + threshold: One::one(), + } + }, + }) + .collect::>(); + contract.steps[0].precondition = Some(Precondition { + clauses: BoundedVec::try_from(alloc::vec![ + BoundedVec::try_from(clause).expect("Suspended-head progress predicates fit"), + ]) + .expect("Suspended-head progress clause fits"), + }); + } + Pallet::::store_actor_contract(actor_id, contract) + .expect("Suspended-head progress Contract remains admitted"); + let snapshot_count = n.min(T::MaxOpeningSnapshotEntries::get()); + let result_count = r + .min(T::MaxOpeningPredicateResults::get()) + .max(opening_count); + let funding_count = f.min(T::MaxFundingTrackedAssets::get()); + install_run_state::(actor_id, snapshot_count); + ActorRunStateStore::::mutate(actor_id, |maybe| { + let run = maybe.as_mut().expect("Suspended-head progress run exists"); + for index in 0..result_count { + let frozen = if current_count == 0 && index + 1 == opening_count { + Ok(false) + } else if index < opening_count || index % 2 == 0 { + Ok(true) + } else { + Err(PredicateError::InvalidObservation) + }; + run + .opening_predicate_results + .try_push(frozen) + .expect("Suspended-head progress Opening result fits"); + } + for asset in T::BenchmarkHelper::funding_assets(funding_count) { + run + .funding_snapshot + .try_insert(asset, One::one()) + .expect("Suspended-head progress funding entry fits"); + } + assert_eq!(run.opening_snapshot.len() as u32, snapshot_count); + assert_eq!(run.opening_predicate_results.len() as u32, result_count); + assert_eq!(run.funding_snapshot.len() as u32, funding_count); + run.cursor = 0; + run.opening_predicate_cursor = 0; + run.cumulative_outcomes = OutcomeTotals::default(); + run.last_step_outcome = Some(StepOutcome::FundingUnavailable); + run.last_attempt_block = 1u32.into(); + run.last_committed_step_block = None; + run.eligible_at = now; + run.suspension = Some(SuspensionReason::FundingUnavailable); + }); + ActorHot::::mutate(actor_id, |maybe| { + let hot = maybe + .as_mut() + .expect("Suspended-head progress hot state exists"); + hot.cycle_state = CycleState::Suspended; + hot.queue_ticket = Some(9); + hot.wakeup_pointer = None; + }); + let (state, admission, loaded_step) = Pallet::::load_current_step_service_state(actor_id) + .expect("Suspended-head progress service state is coherent"); + let execution_instance = Pallet::::derive_active_actor_view( + state.identity.clone(), + state.hot.clone(), + state.contract.clone(), + ); + ActorHot::::mutate(actor_id, |maybe| { + maybe + .as_mut() + .expect("Suspended-head progress hot state remains") + .queue_ticket = None; + }); + #[block] + { + let ticket = Pallet::::build_actor_step_ticket( + actor_id, + 9, + now, + &state.identity, + &state.hot, + state.run_state.as_ref(), + &admission, + ) + .expect("Suspended-head progress Step ticket builds"); + let maximum_fee = Pallet::::maximum_current_step_fee( + state.identity.actor_class.actor_type(), + loaded_step.resources, + ) + .expect("Suspended-head progress maximum fee exists"); + let plan = Pallet::::build_current_step_plan( + actor_id, + state.identity, + state.hot, + state.run_state, + state.funding, + admission, + ticket, + loaded_step, + maximum_fee, + ) + .expect("Suspended-head progress plan builds"); + polkadot_sdk::frame_support::storage::with_transaction(|| { + match Pallet::::execute_current_step_and_place(actor_id, execution_instance, plan, now) { + Ok(evidence) => { + core::hint::black_box(evidence); + polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok::< + (), + AttemptTransactionError, + >(())) + } + Err(error) => { + polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)) + } + } + }) + .expect("Suspended-head progress inner atom commits"); + } + let run = ActorRunStateStore::::get(actor_id).expect("Suspended-head progress run remains"); + assert_eq!(run.cursor, 1); + assert_eq!(run.last_committed_step_block, Some(now)); + assert_eq!(QueueOccupancy::::get(), 1); + assert!( + ActorHot::::get(actor_id).is_some_and(|hot| { + hot.cycle_state == CycleState::Running && hot.queue_ticket.is_some() + }) + ); + } + + /// Measures the direct inner Opening-heavy Suspended-head retry owner. The fixed worst + /// realizable mixed composition is one frozen Opening predicate plus three Current predicates; + /// all evaluate true before an unfunded Transfer yields FundingUnavailable without Task effect. + #[benchmark(pov_mode = Measured)] + fn scheduler_inner_suspended_head_opening_retry( + n: Linear<0, 64>, + r: Linear<0, 128>, + f: Linear<0, 40>, + ) { + let _ = WakeupBuckets::::clear(u32::MAX, None); + let _ = WakeupPages::::clear(u32::MAX, None); + let _ = WakeupCursorPages::::clear(u32::MAX, None); + let _ = WakeupCursorLen::::clear(u32::MAX, None); + GlobalCircuitBreaker::::put(false); + let now: BlockNumberFor = 2u32.into(); + frame_system::Pallet::::set_block_number(now); + let actor_id = + bench_create_system_with_plan::(41_400_000, inert_contract_steps_of_len::(1)); + let actor = Pallet::::sovereign_account_id_system(actor_id); + let assets = T::BenchmarkHelper::setup_predicate_assets(&actor, 4) + .expect("Opening-heavy retry predicate assets exist"); + assert_eq!(assets.len(), 4); + let clause = assets + .into_iter() + .enumerate() + .map(|(index, asset)| TimedPredicate { + timing: if index == 0 { + ObservationTiming::Opening + } else { + ObservationTiming::Current + }, + predicate: Predicate::BalanceBelow { + asset, + threshold: One::one(), + }, + }) + .collect::>(); + let precondition = Precondition { + clauses: BoundedVec::try_from(alloc::vec![ + BoundedVec::try_from(clause).expect("Opening-heavy retry predicates fit"), + ]) + .expect("Opening-heavy retry clause fits"), + }; + assert_eq!(precondition.evaluation_units(), 5); + let mut contract = + Pallet::::load_actor_contract(actor_id).expect("Opening-heavy retry Contract exists"); + contract.steps[0].precondition = Some(precondition); + contract.steps[0].task = ActorTask::Transfer { + to: actor.clone(), + asset: T::FeeNativeAssetId::get(), + amount: AmountResolution::Fixed(One::one()), + }; + contract.steps[0].on_error = StepErrorPolicy::RetryLater { + max_attempts: T::MaxRetryAttempts::get(), + }; + Pallet::::store_actor_contract(actor_id, contract) + .expect("Opening-heavy retry Contract remains admitted"); + let snapshot_count = n.min(T::MaxOpeningSnapshotEntries::get()); + let result_count = r.min(T::MaxOpeningPredicateResults::get()).max(1); + let funding_count = f.min(T::MaxFundingTrackedAssets::get()); + install_run_state::(actor_id, snapshot_count); + ActorRunStateStore::::mutate(actor_id, |maybe| { + let run = maybe.as_mut().expect("Opening-heavy retry run exists"); + for index in 0..result_count { + run + .opening_predicate_results + .try_push(if index == 0 { + Ok(true) + } else if index % 2 == 0 { + Ok(true) + } else { + Err(PredicateError::InvalidObservation) + }) + .expect("Opening-heavy retry result fits"); + } + for asset in T::BenchmarkHelper::funding_assets(funding_count) { + run + .funding_snapshot + .try_insert(asset, One::one()) + .expect("Opening-heavy retry funding entry fits"); + } + assert_eq!(run.opening_snapshot.len() as u32, snapshot_count); + assert_eq!(run.opening_predicate_results.len() as u32, result_count); + assert_eq!(run.funding_snapshot.len() as u32, funding_count); + run.cursor = 0; + run.opening_predicate_cursor = 0; + run.cumulative_outcomes = OutcomeTotals::default(); + run.last_step_outcome = Some(StepOutcome::FundingUnavailable); + run.last_attempt_block = 1u32.into(); + run.last_committed_step_block = None; + run.eligible_at = now; + run.suspension = Some(SuspensionReason::FundingUnavailable); + }); + ActorHot::::mutate(actor_id, |maybe| { + let hot = maybe + .as_mut() + .expect("Opening-heavy retry hot state exists"); + hot.cycle_state = CycleState::Suspended; + hot.queue_ticket = Some(9); + hot.wakeup_pointer = None; + }); + let (state, admission, loaded_step) = Pallet::::load_current_step_service_state(actor_id) + .expect("Opening-heavy retry service state is coherent"); + let execution_instance = Pallet::::derive_active_actor_view( + state.identity.clone(), + state.hot.clone(), + state.contract.clone(), + ); + ActorHot::::mutate(actor_id, |maybe| { + maybe + .as_mut() + .expect("Opening-heavy retry hot state remains") + .queue_ticket = None; + }); + #[block] + { + let ticket = Pallet::::build_actor_step_ticket( + actor_id, + 9, + now, + &state.identity, + &state.hot, + state.run_state.as_ref(), + &admission, + ) + .expect("Opening-heavy retry Step ticket builds"); + let maximum_fee = Pallet::::maximum_current_step_fee( + state.identity.actor_class.actor_type(), + loaded_step.resources, + ) + .expect("Opening-heavy retry maximum fee exists"); + let plan = Pallet::::build_current_step_plan( + actor_id, + state.identity, + state.hot, + state.run_state, + state.funding, + admission, + ticket, + loaded_step, + maximum_fee, + ) + .expect("Opening-heavy retry plan builds"); + polkadot_sdk::frame_support::storage::with_transaction(|| { + match Pallet::::execute_current_step_and_place(actor_id, execution_instance, plan, now) { + Ok(evidence) => { + core::hint::black_box(evidence); + polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok::< + (), + AttemptTransactionError, + >(())) + } + Err(error) => { + polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)) + } + } + }) + .expect("Opening-heavy retry inner atom commits"); + } + let run = ActorRunStateStore::::get(actor_id).expect("Opening-heavy retry run remains"); + assert_eq!(run.cursor, 0); + assert_eq!(run.unsuccessful_attempts_at_cursor, 2); + assert_eq!(run.last_attempt_block, now); + assert!(ActorHot::::get(actor_id).is_some_and(|hot| hot.wakeup_pointer.is_some())); + } + + /// Measures the direct inner Opening-heavy Suspended-head completion owner. The fixed worst + /// mixed composition is one frozen Opening predicate plus three Current predicates; the final + /// Current predicate is false so completion commits without Task effect execution. + #[benchmark(pov_mode = Measured)] + fn scheduler_inner_suspended_head_opening_complete( + n: Linear<0, 64>, + r: Linear<0, 128>, + f: Linear<0, 40>, + ) { + GlobalCircuitBreaker::::put(false); + let now: BlockNumberFor = 2u32.into(); + frame_system::Pallet::::set_block_number(now); + let actor_id = + bench_create_system_with_plan::(41_500_000, inert_contract_steps_of_len::(1)); + let actor = Pallet::::sovereign_account_id_system(actor_id); + let assets = T::BenchmarkHelper::setup_predicate_assets(&actor, 4) + .expect("Opening-heavy completion predicate assets exist"); + assert_eq!(assets.len(), 4); + let clause = assets + .into_iter() + .enumerate() + .map(|(index, asset)| TimedPredicate { + timing: if index == 0 { + ObservationTiming::Opening + } else { + ObservationTiming::Current + }, + predicate: if index == 3 { + Predicate::BalanceAbove { + asset, + threshold: One::one(), + } + } else { + Predicate::BalanceBelow { + asset, + threshold: One::one(), + } + }, + }) + .collect::>(); + let precondition = Precondition { + clauses: BoundedVec::try_from(alloc::vec![ + BoundedVec::try_from(clause).expect("Opening-heavy completion predicates fit"), + ]) + .expect("Opening-heavy completion clause fits"), + }; + assert_eq!(precondition.evaluation_units(), 5); + let mut contract = + Pallet::::load_actor_contract(actor_id).expect("Opening-heavy completion Contract exists"); + contract.steps[0].precondition = Some(precondition); + contract.steps[0].task = ActorTask::Transfer { + to: actor.clone(), + asset: T::FeeNativeAssetId::get(), + amount: AmountResolution::Fixed(Zero::zero()), + }; + contract.steps[0].on_error = StepErrorPolicy::RetryLater { + max_attempts: T::MaxRetryAttempts::get(), + }; + Pallet::::store_actor_contract(actor_id, contract) + .expect("Opening-heavy completion Contract remains admitted"); + let snapshot_count = n.min(T::MaxOpeningSnapshotEntries::get()); + let result_count = r.min(T::MaxOpeningPredicateResults::get()).max(1); + let funding_count = f.min(T::MaxFundingTrackedAssets::get()); + install_run_state::(actor_id, snapshot_count); + ActorRunStateStore::::mutate(actor_id, |maybe| { + let run = maybe.as_mut().expect("Opening-heavy completion run exists"); + for index in 0..result_count { + run + .opening_predicate_results + .try_push(if index == 0 { + Ok(true) + } else if index % 2 == 0 { + Ok(true) + } else { + Err(PredicateError::InvalidObservation) + }) + .expect("Opening-heavy completion result fits"); + } + for asset in T::BenchmarkHelper::funding_assets(funding_count) { + run + .funding_snapshot + .try_insert(asset, One::one()) + .expect("Opening-heavy completion funding entry fits"); + } + assert_eq!(run.opening_snapshot.len() as u32, snapshot_count); + assert_eq!(run.opening_predicate_results.len() as u32, result_count); + assert_eq!(run.funding_snapshot.len() as u32, funding_count); + run.cursor = 0; + run.opening_predicate_cursor = 0; + run.cumulative_outcomes = OutcomeTotals::default(); + run.last_step_outcome = Some(StepOutcome::FundingUnavailable); + run.last_attempt_block = 1u32.into(); + run.last_committed_step_block = None; + run.eligible_at = now; + run.suspension = Some(SuspensionReason::FundingUnavailable); + }); + ActorHot::::mutate(actor_id, |maybe| { + let hot = maybe + .as_mut() + .expect("Opening-heavy completion hot state exists"); + hot.cycle_state = CycleState::Suspended; + hot.queue_ticket = Some(9); + hot.wakeup_pointer = None; + }); + let (state, admission, loaded_step) = Pallet::::load_current_step_service_state(actor_id) + .expect("Opening-heavy completion service state is coherent"); + let execution_instance = Pallet::::derive_active_actor_view( + state.identity.clone(), + state.hot.clone(), + state.contract.clone(), + ); + ActorHot::::mutate(actor_id, |maybe| { + maybe + .as_mut() + .expect("Opening-heavy completion hot state remains") + .queue_ticket = None; + }); + #[block] + { + let ticket = Pallet::::build_actor_step_ticket( + actor_id, + 9, + now, + &state.identity, + &state.hot, + state.run_state.as_ref(), + &admission, + ) + .expect("Opening-heavy completion Step ticket builds"); + let maximum_fee = Pallet::::maximum_current_step_fee( + state.identity.actor_class.actor_type(), + loaded_step.resources, + ) + .expect("Opening-heavy completion maximum fee exists"); + let plan = Pallet::::build_current_step_plan( + actor_id, + state.identity, + state.hot, + state.run_state, + state.funding, + admission, + ticket, + loaded_step, + maximum_fee, + ) + .expect("Opening-heavy completion plan builds"); + polkadot_sdk::frame_support::storage::with_transaction(|| { + match Pallet::::execute_current_step_and_place(actor_id, execution_instance, plan, now) { + Ok(evidence) => { + core::hint::black_box(evidence); + polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok::< + (), + AttemptTransactionError, + >(())) + } + Err(error) => { + polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)) } - }) - .collect::>(); - QueuePages::::insert( - page_id, - BoundedVec::::try_from(page) - .expect("benchmark queue page must fit configured page size"), - ); - ticket = ticket.saturating_add(entries); + } + }) + .expect("Opening-heavy completion inner atom commits"); } - ActorIdentities::::remove(template_id); - ActorHot::::remove(template_id); - ActorContracts::::remove(template_id); - ActorFunding::::remove(template_id); + assert!(ActorRunStateStore::::get(actor_id).is_none()); + assert!(ActorHot::::get(actor_id).is_some_and(|hot| { + hot.cycle_state == CycleState::Idle + && hot.queue_ticket.is_none() + && hot.wakeup_pointer.is_none() + })); + } + + /// Measures the direct inner Opening-heavy Suspended-head progress owner. The fixed worst mixed + /// composition is one frozen Opening predicate plus three Current predicates; the final Current + /// predicate is false before causal FIFO successor placement, without Task effect execution. + #[benchmark(pov_mode = Measured)] + fn scheduler_inner_suspended_head_opening_progress( + n: Linear<0, 64>, + r: Linear<0, 128>, + f: Linear<0, 40>, + ) { + let _ = QueuePages::::clear(u32::MAX, None); QueueHead::::put(0); - QueueTail::::put(u64::from(bounded)); - QueueOccupancy::::put(bounded); - NextQueueTicket::::put(u64::from(bounded)); - let cutoff = u64::from(bounded); + QueueTail::::put(0); + QueueOccupancy::::put(0); + NextQueueTicket::::put(0); + GlobalCircuitBreaker::::put(false); + let now: BlockNumberFor = 2u32.into(); + frame_system::Pallet::::set_block_number(now); + let actor_id = + bench_create_system_with_plan::(41_600_000, inert_contract_steps_of_len::(2)); + let actor = Pallet::::sovereign_account_id_system(actor_id); + let assets = T::BenchmarkHelper::setup_predicate_assets(&actor, 4) + .expect("Opening-heavy progress predicate assets exist"); + assert_eq!(assets.len(), 4); + let clause = assets + .into_iter() + .enumerate() + .map(|(index, asset)| TimedPredicate { + timing: if index == 0 { + ObservationTiming::Opening + } else { + ObservationTiming::Current + }, + predicate: if index == 3 { + Predicate::BalanceAbove { + asset, + threshold: One::one(), + } + } else { + Predicate::BalanceBelow { + asset, + threshold: One::one(), + } + }, + }) + .collect::>(); + let precondition = Precondition { + clauses: BoundedVec::try_from(alloc::vec![ + BoundedVec::try_from(clause).expect("Opening-heavy progress predicates fit"), + ]) + .expect("Opening-heavy progress clause fits"), + }; + assert_eq!(precondition.evaluation_units(), 5); + let mut contract = + Pallet::::load_actor_contract(actor_id).expect("Opening-heavy progress Contract exists"); + contract.steps[0].precondition = Some(precondition); + contract.steps[0].task = ActorTask::Transfer { + to: actor.clone(), + asset: T::FeeNativeAssetId::get(), + amount: AmountResolution::Fixed(Zero::zero()), + }; + contract.steps[0].on_error = StepErrorPolicy::RetryLater { + max_attempts: T::MaxRetryAttempts::get(), + }; + Pallet::::store_actor_contract(actor_id, contract) + .expect("Opening-heavy progress Contract remains admitted"); + let snapshot_count = n.min(T::MaxOpeningSnapshotEntries::get()); + let result_count = r.min(T::MaxOpeningPredicateResults::get()).max(1); + let funding_count = f.min(T::MaxFundingTrackedAssets::get()); + install_run_state::(actor_id, snapshot_count); + ActorRunStateStore::::mutate(actor_id, |maybe| { + let run = maybe.as_mut().expect("Opening-heavy progress run exists"); + for index in 0..result_count { + run + .opening_predicate_results + .try_push(if index == 0 { + Ok(true) + } else if index % 2 == 0 { + Ok(true) + } else { + Err(PredicateError::InvalidObservation) + }) + .expect("Opening-heavy progress result fits"); + } + for asset in T::BenchmarkHelper::funding_assets(funding_count) { + run + .funding_snapshot + .try_insert(asset, One::one()) + .expect("Opening-heavy progress funding entry fits"); + } + assert_eq!(run.opening_snapshot.len() as u32, snapshot_count); + assert_eq!(run.opening_predicate_results.len() as u32, result_count); + assert_eq!(run.funding_snapshot.len() as u32, funding_count); + run.cursor = 0; + run.opening_predicate_cursor = 0; + run.cumulative_outcomes = OutcomeTotals::default(); + run.last_step_outcome = Some(StepOutcome::FundingUnavailable); + run.last_attempt_block = 1u32.into(); + run.last_committed_step_block = None; + run.eligible_at = now; + run.suspension = Some(SuspensionReason::FundingUnavailable); + }); + ActorHot::::mutate(actor_id, |maybe| { + let hot = maybe + .as_mut() + .expect("Opening-heavy progress hot state exists"); + hot.cycle_state = CycleState::Suspended; + hot.queue_ticket = Some(9); + hot.wakeup_pointer = None; + }); + let (state, admission, loaded_step) = Pallet::::load_current_step_service_state(actor_id) + .expect("Opening-heavy progress service state is coherent"); + let execution_instance = Pallet::::derive_active_actor_view( + state.identity.clone(), + state.hot.clone(), + state.contract.clone(), + ); + ActorHot::::mutate(actor_id, |maybe| { + maybe + .as_mut() + .expect("Opening-heavy progress hot state remains") + .queue_ticket = None; + }); #[block] { - while QueueHead::::get() < cutoff { - core::hint::black_box( - Pallet::::paged_drain_tombstones(cutoff, bounded) - .expect("benchmark mixed queue topology is valid"), - ); - if let Some((_, entry)) = Pallet::::paged_head_entry() { - assert!(Pallet::::paged_consume_head(entry.ticket)); + let ticket = Pallet::::build_actor_step_ticket( + actor_id, + 9, + now, + &state.identity, + &state.hot, + state.run_state.as_ref(), + &admission, + ) + .expect("Opening-heavy progress Step ticket builds"); + let maximum_fee = Pallet::::maximum_current_step_fee( + state.identity.actor_class.actor_type(), + loaded_step.resources, + ) + .expect("Opening-heavy progress maximum fee exists"); + let plan = Pallet::::build_current_step_plan( + actor_id, + state.identity, + state.hot, + state.run_state, + state.funding, + admission, + ticket, + loaded_step, + maximum_fee, + ) + .expect("Opening-heavy progress plan builds"); + polkadot_sdk::frame_support::storage::with_transaction(|| { + match Pallet::::execute_current_step_and_place(actor_id, execution_instance, plan, now) { + Ok(evidence) => { + core::hint::black_box(evidence); + polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok::< + (), + AttemptTransactionError, + >(())) + } + Err(error) => { + polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)) + } } - } + }) + .expect("Opening-heavy progress inner atom commits"); } - assert!(QueueHead::::get() >= cutoff); - assert_eq!(QueueHead::::get(), QueueTail::::get()); - assert_eq!(QueueOccupancy::::get(), 0); + let run = ActorRunStateStore::::get(actor_id).expect("Opening-heavy progress run remains"); + assert_eq!(run.cursor, 1); + assert_eq!(run.last_committed_step_block, Some(now)); + assert_eq!(QueueOccupancy::::get(), 1); + assert!( + ActorHot::::get(actor_id).is_some_and(|hot| { + hot.cycle_state == CycleState::Running && hot.queue_ticket.is_some() + }) + ); } /// Measures actual scheduler admission and complete execution for up to 1,000 @@ -2618,11 +7269,11 @@ mod benches { let identity_template = ActorIdentities::::get(template_id).expect("benchmark identity template"); let contract_template = - ActorContracts::::get(template_id).expect("benchmark contract template"); + Pallet::::load_actor_contract(template_id).expect("benchmark contract template"); let funding_template = ActorFunding::::get(template_id).expect("benchmark funding template"); ActorIdentities::::remove(template_id); ActorHot::::remove(template_id); - ActorContracts::::remove(template_id); + Pallet::::remove_actor_contract(template_id).expect("benchmark template Contract removes"); ActorFunding::::remove(template_id); let first_id = 41_000_000u64; @@ -2636,7 +7287,8 @@ mod benches { hot.queue_ticket = None; ActorIdentities::::insert(actor_id, identity); ActorHot::::insert(actor_id, hot); - ActorContracts::::insert(actor_id, contract_template.clone()); + Pallet::::store_actor_contract(actor_id, contract_template.clone()) + .expect("benchmark queued Contract remains admitted"); ActorFunding::::insert(actor_id, funding_template.clone()); assert!(Pallet::::paged_enqueue(actor_id)); } @@ -2683,7 +7335,9 @@ mod benches { ActorIdentities::::take(system_template_id).expect("System identity template"); let system_hot = ActorHot::::take(system_template_id).expect("System hot template"); let contract_template = - ActorContracts::::take(system_template_id).expect("System contract template"); + Pallet::::load_actor_contract(system_template_id).expect("System contract template"); + Pallet::::remove_actor_contract(system_template_id) + .expect("System template Contract removes"); let funding_template = ActorFunding::::take(system_template_id).expect("System funding template"); let first_id = 43_000_000u64; @@ -2695,7 +7349,7 @@ mod benches { let template_id = bench_create_user::(owner); let identity = ActorIdentities::::take(template_id).expect("User identity template"); ActorHot::::remove(template_id); - ActorContracts::::remove(template_id); + Pallet::::remove_actor_contract(template_id).expect("User template Contract removes"); ActorFunding::::remove(template_id); identity } else { @@ -2708,7 +7362,8 @@ mod benches { hot.queue_ticket = None; ActorIdentities::::insert(actor_id, identity); ActorHot::::insert(actor_id, hot); - ActorContracts::::insert(actor_id, contract_template.clone()); + Pallet::::store_actor_contract(actor_id, contract_template.clone()) + .expect("benchmark mixed-class Contract remains admitted"); ActorFunding::::insert(actor_id, funding_template.clone()); assert!(Pallet::::paged_enqueue(actor_id)); } @@ -2746,59 +7401,76 @@ mod benches { } #[benchmark(pov_mode = Measured)] - fn continuation_suspend(s: Linear<0, 20>) { + fn run_progress() { + let actor_id = + bench_create_system_with_plan::(49_999_999, inert_contract_steps_of_len::(2)); + install_run_state::(actor_id, T::MaxOpeningSnapshotEntries::get()); + let mut maximum = ActorRunStateStore::::take(actor_id).expect("benchmark Actor run exists"); + maximize_run_state_geometry::(&mut maximum); + ActorRunStateStore::::insert(actor_id, maximum); + let mut state = ActorRunStateStore::::get(actor_id).expect("maximum Actor run exists"); + state.cursor = 1; + state.unsuccessful_attempts_at_cursor = 0; + state.last_attempt_block = 1u32.into(); + state.last_committed_step_block = Some(1u32.into()); + state.eligible_at = 2u32.into(); + state.suspension = None; + #[block] + { + Pallet::::persist_run_progress(actor_id, state) + .expect("benchmark Running progress must persist"); + } + assert!(ActorRunStateStore::::contains_key(actor_id)); + assert!(ActorHot::::get(actor_id).is_some_and(|hot| hot.cycle_state == CycleState::Running)); + } + + #[benchmark(pov_mode = Measured)] + fn run_suspend() { let actor_id = bench_create_system_manual::(50_000_000); - install_continuation::(actor_id, s); - let state = ContinuationStateStore::::take(actor_id).expect("benchmark continuation exists"); - ActorHot::::mutate(actor_id, |maybe_hot| { - maybe_hot - .as_mut() - .expect("benchmark actor hot state exists") - .cycle_state = CycleState::Idle; - }); + install_run_state::(actor_id, T::MaxOpeningSnapshotEntries::get()); + let mut maximum = ActorRunStateStore::::take(actor_id).expect("benchmark Actor run exists"); + maximize_run_state_geometry::(&mut maximum); + ActorRunStateStore::::insert(actor_id, maximum); + let state = ActorRunStateStore::::get(actor_id).expect("maximum Actor run exists"); #[block] { - Pallet::::persist_continuation_suspension(actor_id, 1, state, SuspensionReason::Temporary) + Pallet::::persist_run_suspension(actor_id, state) .expect("benchmark suspension must persist"); } - assert!(ContinuationStateStore::::contains_key(actor_id)); + assert!(ActorRunStateStore::::contains_key(actor_id)); } #[benchmark(pov_mode = Measured)] - fn continuation_retry() { + fn run_retry() { let actor_id = bench_create_system_manual::(50_000_001); - install_continuation::(actor_id, T::MaxOpeningSnapshotEntries::get()); + install_run_state::(actor_id, T::MaxOpeningSnapshotEntries::get()); #[block] { - core::hint::black_box(Pallet::::begin_continuation_attempt( - actor_id, - 1, - 2u32.into(), - )); + core::hint::black_box(Pallet::::begin_run_attempt(actor_id, 2u32.into())); } assert_eq!( - ContinuationStateStore::::get(actor_id) - .expect("benchmark continuation remains") + ActorRunStateStore::::get(actor_id) + .expect("benchmark Actor run remains") .last_attempt_block, 2u32.into() ); } #[benchmark(pov_mode = Measured)] - fn continuation_complete() { + fn run_complete() { let actor_id = bench_create_system_manual::(50_000_002); - install_continuation::(actor_id, T::MaxOpeningSnapshotEntries::get()); + install_run_state::(actor_id, T::MaxOpeningSnapshotEntries::get()); #[block] { - Pallet::::write_continuation_state(actor_id, None) - .expect("benchmark completion must clear continuation"); + Pallet::::write_run_state(actor_id, None) + .expect("benchmark completion must clear Actor run"); } - assert!(ContinuationStateStore::::get(actor_id).is_none()); + assert!(ActorRunStateStore::::get(actor_id).is_none()); assert!(ActorHot::::get(actor_id).is_some_and(|hot| hot.cycle_state == CycleState::Idle)); } #[benchmark] - fn continuation_cancel() { + fn run_cancel() { let page_size = T::WakeupPageSize::get(); let wakeup_block = 100u32.into(); for i in 0..page_size { @@ -2812,7 +7484,7 @@ mod benches { middle_fillers.push(filler); } let actor_id = bench_create_system_manual::(52_000_003); - install_continuation::(actor_id, T::MaxOpeningSnapshotEntries::get()); + install_run_state::(actor_id, T::MaxOpeningSnapshotEntries::get()); assert!(Pallet::::wakeup_substrate_schedule( actor_id, wakeup_block @@ -2832,8 +7504,8 @@ mod benches { .pending_signal = true; }); #[extrinsic_call] - cancel_continuation(RawOrigin::Root, actor_id); - assert!(ContinuationStateStore::::get(actor_id).is_none()); + cancel_run(RawOrigin::Root, actor_id); + assert!(ActorRunStateStore::::get(actor_id).is_none()); assert!(ActorHot::::get(actor_id).is_some_and(|hot| { hot.cycle_state == CycleState::Idle && hot.pending_signal && hot.queue_ticket.is_some() })); @@ -2844,9 +7516,9 @@ mod benches { } #[benchmark] - fn continuation_suffix_admission(n: Linear<1, 10>) { + fn run_suffix_admission(n: Linear<1, 10>) { let bounded = n.min(T::MaxContractSteps::get()); - let recipient: T::AccountId = account("continuation_suffix_recipient", 0, 0); + let recipient: T::AccountId = account("run_suffix_recipient", 0, 0); let mut plan = ContractSteps::::default(); for _ in 0..bounded { plan @@ -2880,6 +7552,48 @@ mod benches { } } + /// Measures one affected User ObservationChange occurrence independently from publisher ingress + /// and fanout traversal: exact Trigger collection, readiness materialization, and canonical + /// placement are the complete Actor-owned boundary. + #[benchmark(pov_mode = Measured)] + fn observation_change_trigger_occurrence() { + let owner: T::AccountId = whitelisted_caller(); + let recipient: T::AccountId = account("observation-occurrence-recipient", 0, 0); + let feed = observation_feed_pool::(1)[0]; + let contract_steps = make_contract_steps::(recipient); + ensure_creation_balance::(&owner); + prefund_active_user_creation::(&owner, &contract_steps); + Pallet::::create_user_actor( + RawOrigin::Signed(owner).into(), + Mutability::Mutable, + user_contract::( + Schedule { + trigger: observation_trigger::(feed), + cooldown_blocks: 0, + }, + contract_steps, + ), + ) + .expect("ObservationChange benchmark Actor exists"); + let actor_id = NextActorId::::get().saturating_sub(1); + frame_system::Pallet::::set_block_number(1u32.into()); + #[block] + { + assert!( + Pallet::::signal_observation_subscriber( + actor_id, + feed, + TriggerCauseProvenance::Deferred, + 0, + ) + .expect("ObservationChange occurrence commits") + ); + } + let hot = ActorHot::::get(actor_id).expect("ObservationChange Actor remains active"); + assert!(hot.pending_signal); + assert!(hot.queue_ticket.is_some() || hot.wakeup_pointer.is_some()); + } + #[benchmark] fn observation_change_ingress() { let owner: T::AccountId = account("observation-ingress", 0, 0); @@ -2912,6 +7626,23 @@ mod benches { } } + #[benchmark] + fn observation_fanout_branch_probe() { + let feed = T::BenchmarkHelper::setup_observation_feeds(1) + .expect("observation benchmark feed must be available") + .into_iter() + .next() + .expect("one observation benchmark feed is required"); + let owner: T::AccountId = account("observation-branch-probe", 0, 0); + let _ = bench_create_system_observation::(owner, feed); + Pallet::::note_observation_changed(feed, 1) + .expect("observation change ingress must succeed"); + #[block] + { + core::hint::black_box(Pallet::::observation_fanout_branch_probe()); + } + } + #[benchmark] fn observation_fanout_page() { let mut feeds = T::BenchmarkHelper::setup_observation_feeds(3) @@ -2923,7 +7654,9 @@ mod benches { let mut actors = alloc::vec::Vec::new(); for index in 0..T::ObservationPageSize::get() { let owner: T::AccountId = account("observation-fanout", index, 0); - actors.push(bench_create_system_observation::(owner, feed)); + actors.push(bench_create_user_observation_with_cooldown::( + owner, feed, 0, + )); } let previous_owner: T::AccountId = account("observation-fanout-previous", 0, 0); let next_owner: T::AccountId = account("observation-fanout-next", 0, 0); @@ -2948,6 +7681,98 @@ mod benches { } } + #[benchmark] + fn observation_fanout_wakeup_page() { + let feed = T::BenchmarkHelper::setup_observation_feeds(1) + .expect("observation benchmark feed must be available") + .into_iter() + .next() + .expect("one observation benchmark feed is required"); + let mut actors = alloc::vec::Vec::new(); + for index in 0..T::ObservationPageSize::get() { + let owner: T::AccountId = account("observation-wakeup", index, 0); + let actor_id = bench_create_user_observation_with_cooldown::(owner, feed, 100); + ActorHot::::mutate(actor_id, |maybe| { + maybe + .as_mut() + .expect("observation wakeup actor") + .last_cycle_block = Some(One::one()); + }); + actors.push(actor_id); + } + Pallet::::note_observation_changed(feed, 1) + .expect("observation change ingress must succeed"); + #[block] + { + Pallet::::do_fanout_dirty_observation_page() + .expect("observation wakeup page must succeed"); + } + assert!(DirtyObservationFeeds::::get(feed).is_none()); + for actor_id in actors { + assert!(ActorHot::::get(actor_id).is_some_and(|hot| { + hot.pending_signal && hot.queue_ticket.is_none() && hot.wakeup_pointer.is_some() + })); + } + } + + #[benchmark] + fn observation_fanout_coalesced_page() { + let feed = T::BenchmarkHelper::setup_observation_feeds(1) + .expect("observation benchmark feed must be available") + .into_iter() + .next() + .expect("one observation benchmark feed is required"); + let mut actors = alloc::vec::Vec::new(); + for index in 0..T::ObservationPageSize::get() { + let owner: T::AccountId = account("observation-coalesced", index, 0); + let actor_id = bench_create_user_observation_with_cooldown::(owner, feed, 0); + Pallet::::request_observation_activation_compact(actor_id, feed) + .expect("initial observation activation must succeed"); + actors.push(actor_id); + } + Pallet::::note_observation_changed(feed, 1) + .expect("observation change ingress must succeed"); + #[block] + { + Pallet::::do_fanout_dirty_observation_page() + .expect("coalesced observation page must succeed"); + } + assert!(DirtyObservationFeeds::::get(feed).is_none()); + for actor_id in actors { + assert!( + ActorHot::::get(actor_id) + .is_some_and(|hot| { hot.pending_signal && hot.queue_ticket.is_some() }) + ); + } + } + + #[benchmark] + fn observation_fanout_terminal() { + let feed = T::BenchmarkHelper::setup_observation_feeds(1) + .expect("observation benchmark feed must be available") + .into_iter() + .next() + .expect("one observation benchmark feed is required"); + let owner: T::AccountId = account("observation-terminal", 0, 0); + let (actor_id, end) = bench_create_expiring_system_observation::(owner, feed); + frame_system::Pallet::::set_block_number(end.saturating_add(One::one())); + Pallet::::note_observation_changed(feed, 1) + .expect("observation change ingress must succeed"); + Pallet::::do_fanout_dirty_observation_page() + .expect("ordinary turn must persist terminal branch"); + assert!( + DirtyObservationFeeds::::get(feed) + .is_some_and(|state| { state.next_subscriber_branch == ObservationFanoutBranch::Terminal }) + ); + #[block] + { + Pallet::::do_fanout_dirty_observation_page() + .expect("terminal observation fanout must succeed"); + } + assert!(ActorHot::::get(actor_id).is_none()); + assert!(DirtyObservationFeeds::::get(feed).is_none()); + } + #[benchmark] fn observation_fanout_blocked_page() { let feed = T::BenchmarkHelper::setup_observation_feeds(1) @@ -2958,7 +7783,9 @@ mod benches { let mut actors = alloc::vec::Vec::new(); for index in 0..T::ObservationPageSize::get() { let owner: T::AccountId = account("observation-fanout-blocked", index, 0); - actors.push(bench_create_system_observation::(owner, feed)); + actors.push(bench_create_user_observation_with_cooldown::( + owner, feed, 0, + )); } install_saturated_tombstone_queue::(); Pallet::::note_observation_changed(feed, 1) @@ -2991,6 +7818,7 @@ mod benches { while CrossingPendingFeedListState::::get().count > 0 { Pallet::::crossing_work_unit().expect("initial Crossing fire must drain"); } + clear_indexed_detection_disablement::(); Pallet::::note_observation_transition( feed, ObservationTransition { @@ -3021,6 +7849,33 @@ mod benches { } } + /// Measures one affected User ObservationCrossing fire independently from observation + /// publication: threshold traversal, phase movement, exact Trigger collection, and canonical + /// readiness placement are the complete Actor-owned occurrence boundary. + #[benchmark(pov_mode = Measured)] + fn observation_crossing_trigger_occurrence() { + let (feed, actor_id) = prepare_crossing_work::(2); + CrossingRangeCursors::::insert( + feed, + CrossingRangeCursor { + revision: 2, + traversal: CrossingTraversal::Upward, + search_bound: 2, + current_threshold: Some(2), + page: 0, + offset: 0, + exhausted: false, + }, + ); + #[block] + { + Pallet::::crossing_work_unit().expect("Crossing occurrence commits"); + } + let hot = ActorHot::::get(actor_id).expect("Crossing Actor remains active"); + assert!(hot.pending_signal); + assert!(hot.queue_ticket.is_some() || hot.wakeup_pointer.is_some()); + } + #[benchmark] fn crossing_search_probe() { let (feed, _) = prepare_crossing_work::(2); @@ -3104,7 +7959,7 @@ mod benches { Trigger::observation_crossing(feed, CrossingDirection::Rising, 2, 0), ); } - let system_count = T::ObservationPageSize::get() + let system_count = T::CrossingPageSize::get() .saturating_add(4) .saturating_sub(8); for index in 0..system_count { @@ -3133,7 +7988,7 @@ mod benches { } #[benchmark] - fn crossing_fire_cohort_preflight(c: Linear<1, 4>) { + fn crossing_fire_cohort_preflight(c: Linear<1, { CROSSING_COHORT_BENCHMARK_MAX }>) { let (feed, first) = prepare_crossing_work::(2); for index in 1..c { let owner: T::AccountId = account("crossing-cohort-preflight", index, 0); @@ -3149,6 +8004,8 @@ mod benches { revision: 2, previous: 0, current: 2, + cause_provenance: TriggerCauseProvenance::Deferred, + cause_block: 0, }; #[block] { @@ -3168,7 +8025,7 @@ mod benches { } #[benchmark] - fn crossing_coalesced_cohort_preflight(c: Linear<1, 4>) { + fn crossing_coalesced_cohort_preflight(c: Linear<1, { CROSSING_COHORT_BENCHMARK_MAX }>) { let (feed, first) = prepare_crossing_work::(2); let mut actors = alloc::vec![first]; for index in 1..c { @@ -3188,6 +8045,8 @@ mod benches { revision: 2, previous: 0, current: 2, + cause_provenance: TriggerCauseProvenance::Deferred, + cause_block: 0, }; #[block] { @@ -3212,7 +8071,7 @@ mod benches { } #[benchmark] - fn crossing_terminal_cohort_preflight(c: Linear<1, 4>) { + fn crossing_terminal_cohort_preflight(c: Linear<1, { CROSSING_COHORT_BENCHMARK_MAX }>) { let (feed, first) = prepare_crossing_work::(2); for index in 1..c { let owner: T::AccountId = account("crossing-terminal-cohort", index, 0); @@ -3229,6 +8088,8 @@ mod benches { revision: 2, previous: 0, current: 2, + cause_provenance: TriggerCauseProvenance::Deferred, + cause_block: 0, }; #[block] { @@ -3253,7 +8114,7 @@ mod benches { } #[benchmark] - fn crossing_skip_cohort_preflight(c: Linear<1, 4>) { + fn crossing_skip_cohort_preflight(c: Linear<1, { CROSSING_COHORT_BENCHMARK_MAX }>) { let (feed, first) = prepare_crossing_work::(2); let mut actors = alloc::vec![first]; for index in 1..c { @@ -3283,6 +8144,8 @@ mod benches { revision: 2, previous: 0, current: 2, + cause_provenance: TriggerCauseProvenance::Deferred, + cause_block: 0, }; #[block] { @@ -3310,7 +8173,7 @@ mod benches { } #[benchmark] - fn crossing_rearm_cohort_preflight(c: Linear<1, 4>) { + fn crossing_rearm_cohort_preflight(c: Linear<1, { CROSSING_COHORT_BENCHMARK_MAX }>) { let (feed, first) = prepare_crossing_work::(2); for index in 1..c { let owner: T::AccountId = account("crossing-rearm-cohort", index, 0); @@ -3322,6 +8185,7 @@ mod benches { while CrossingPendingFeedListState::::get().count > 0 { Pallet::::crossing_work_unit().expect("initial Crossing fire must drain"); } + clear_indexed_detection_disablement::(); Pallet::::note_observation_transition( feed, ObservationTransition { @@ -3338,6 +8202,8 @@ mod benches { revision: 3, previous: 2, current: 0, + cause_provenance: TriggerCauseProvenance::Deferred, + cause_block: 0, }; #[block] { @@ -3372,6 +8238,7 @@ mod benches { while CrossingPendingFeedListState::::get().count > 0 { Pallet::::crossing_work_unit().expect("initial pair fire must drain"); } + clear_indexed_detection_disablement::(); Pallet::::note_observation_transition( feed, ObservationTransition { @@ -3486,6 +8353,7 @@ mod benches { while CrossingPendingFeedListState::::get().count > 0 { Pallet::::crossing_work_unit().expect("initial Crossing fire must drain"); } + clear_indexed_detection_disablement::(); Pallet::::note_observation_transition( feed, ObservationTransition { @@ -3519,6 +8387,7 @@ mod benches { while CrossingPendingFeedListState::::get().count > 0 { Pallet::::crossing_work_unit().expect("initial pair fire must drain"); } + clear_indexed_detection_disablement::(); Pallet::::note_observation_transition( feed, ObservationTransition { @@ -3684,7 +8553,7 @@ mod benches { fn crossing_placed_maximum_unit() { let (feed, first_actor) = prepare_crossing_work::(2); let mut actors = alloc::vec![first_actor]; - for index in 0..T::ObservationPageSize::get().saturating_sub(1) { + for index in 0..T::CrossingPageSize::get().saturating_sub(1) { let owner: T::AccountId = account("crossing-maximum-unit", index, 0); actors.push(if index < 3 { bench_create_user_with_trigger::( @@ -3709,10 +8578,13 @@ mod benches { ); #[block] { - Pallet::::crossing_placed_batch_work_unit(4) + Pallet::::crossing_placed_batch_work_unit(CROSSING_COHORT_BENCHMARK_MAX) .expect("placed Crossing maximum batch must succeed"); } - for actor_id in actors.into_iter().take(4) { + for actor_id in actors + .into_iter() + .take(CROSSING_COHORT_BENCHMARK_MAX as usize) + { assert!(ActorHot::::get(actor_id).is_some_and(|hot| { hot.pending_signal && hot.queue_ticket.is_some() @@ -3729,13 +8601,16 @@ mod benches { #[benchmark] fn crossing_placed_non_tail_emptied_unit() { - let actors = prepare_non_tail_crossing_batch::(4); + let actors = prepare_non_tail_crossing_batch::(CROSSING_NON_TAIL_BENCHMARK_MAX); #[block] { - Pallet::::crossing_placed_batch_work_unit(4) + Pallet::::crossing_placed_batch_work_unit(CROSSING_COHORT_BENCHMARK_MAX) .expect("non-tail Crossing batch with emptied tail must succeed"); } - for actor_id in actors.into_iter().take(4) { + for actor_id in actors + .into_iter() + .take(CROSSING_NON_TAIL_BENCHMARK_MAX as usize) + { assert!( ActorHot::::get(actor_id) .is_some_and(|hot| { hot.pending_signal && hot.queue_ticket.is_some() }) @@ -3745,13 +8620,16 @@ mod benches { #[benchmark] fn crossing_placed_non_tail_trimmed_unit() { - let actors = prepare_non_tail_crossing_batch::(6); + let actors = prepare_non_tail_crossing_batch::(CROSSING_TRIMMED_BENCHMARK_TAIL); #[block] { - Pallet::::crossing_placed_batch_work_unit(4) + Pallet::::crossing_placed_batch_work_unit(CROSSING_COHORT_BENCHMARK_MAX) .expect("non-tail Crossing batch with trimmed tail must succeed"); } - for actor_id in actors.into_iter().take(4) { + for actor_id in actors + .into_iter() + .take(CROSSING_NON_TAIL_BENCHMARK_MAX as usize) + { assert!( ActorHot::::get(actor_id) .is_some_and(|hot| { hot.pending_signal && hot.queue_ticket.is_some() }) @@ -3886,7 +8764,7 @@ mod benches { fn transaction_extension_ingress_notify() { let source: T::AccountId = account("ingress_source", 0, 0); let (actor_id, recipient) = prepare_saturated_address_actor::(0, Some(source.clone())); - install_continuation::(actor_id, T::MaxOpeningSnapshotEntries::get()); + install_run_state::(actor_id, T::MaxOpeningSnapshotEntries::get()); T::BenchmarkHelper::setup_address_event_ingress(&recipient, &source, One::one()) .expect("benchmark helper must prepare a matched producer event"); #[block] @@ -3989,7 +8867,7 @@ mod benches { on_error: StepErrorPolicy::AbortCycle, }]) .expect("transfer contract_steps fits"); - let contract = ActorContracts::::get(*actor_id).expect("Actor Contract exists"); + let contract = Pallet::::load_actor_contract(*actor_id).expect("Actor Contract exists"); Pallet::::update_contract( RawOrigin::Root.into(), *actor_id, @@ -4074,6 +8952,69 @@ mod benches { } } + #[benchmark] + fn maximum_context_inherent() { + let inherent = T::BenchmarkHelper::prepare_maximum_context_inherent(); + #[block] + { + T::BenchmarkHelper::execute_maximum_context_inherent(inherent) + .expect("maximum context inherent execution must succeed"); + } + T::BenchmarkHelper::verify_maximum_context_inherent(); + } + + #[benchmark] + fn block_resource_finalize() { + let now = frame_system::Pallet::::block_number(); + let budget = T::BlockResourceBudget::get(); + let mut state = BlockResourceState::new(now); + state.begin_prepass().expect("benchmark state opens"); // deos-bypass: panic-owner — fresh benchmark state has no reservations. + state + .open_external_phase() + .expect("benchmark prepass closes"); // deos-bypass: panic-owner — preceding transition establishes empty PrepassExecuting. + state.begin_drain().expect("benchmark drain opens"); // deos-bypass: panic-owner — preceding transition establishes ExternalPhase. + state + .finish_drain(budget, budget.fixed_envelope()) + .expect("benchmark state reconciles"); // deos-bypass: panic-owner — empty usage plus the configured fixed envelope exactly satisfies the budget. + CurrentBlockResourceState::::put(state); + #[block] + { + let current = + CurrentBlockResourceState::::take().expect("finalized benchmark state exists"); // deos-bypass: panic-owner — setup writes this exact storage value. + FinalizedBlockResourceTelemetry::::put( + current + .finalized_snapshot() + .expect("finalized snapshot exists"), // deos-bypass: panic-owner — successful finish_drain establishes Finalizable and fixed actual. + ); + let valid = current.ensure_block(now).is_ok() + && current.phase() == BlockResourcePhase::Finalizable + && current.outstanding_reservations() == 0 + && FinalizedBlockResourceTelemetry::::get() + .is_some_and(|snapshot| snapshot.block_number() == now); + assert!(valid); + } + } + + #[benchmark] + fn block_resource_meter_extension() { + T::BenchmarkHelper::prepare_block_resource_meter_extension(); + #[block] + { + T::BenchmarkHelper::execute_block_resource_meter_extension(); + } + T::BenchmarkHelper::verify_block_resource_meter_extension(); + } + + #[benchmark] + fn maximum_xcm_version_discovery() { + T::BenchmarkHelper::prepare_maximum_xcm_version_discovery(); + #[block] + { + T::BenchmarkHelper::execute_maximum_xcm_version_discovery(); + } + T::BenchmarkHelper::verify_maximum_xcm_version_discovery(); + } + #[cfg(test)] use crate::mock::{Test, new_test_ext}; #[cfg(test)] diff --git a/template/pallets/actors/src/crossing.rs b/template/pallets/actors/src/crossing.rs index df6342bd..1a9d6dd7 100644 --- a/template/pallets/actors/src/crossing.rs +++ b/template/pallets/actors/src/crossing.rs @@ -4,7 +4,7 @@ use crate::scheduler::ActivationOutcome; use crate::types::{ CrossingDirection, CrossingLeafKey, CrossingLeafState, CrossingMember, CrossingMembershipLocator, CrossingMembershipRole, CrossingPhase, CrossingRadixNodeKey, CrossingTransition, - CrossingTraversal, ObservationCrossing, TriggerRuntimeState, + CrossingTraversal, ObservationCrossing, TriggerFamily, TriggerRuntimeState, }; use alloc::vec::Vec; use polkadot_sdk::frame_support::{ @@ -59,7 +59,7 @@ struct CrossingPlacedCohortAuthority { candidates: BoundedVec, T::MaxCrossingActorsPerBlock>, crossings: BoundedVec, T::MaxCrossingActorsPerBlock>, - tail_refill: Option>, + tail_refill: Option>, queue_plan: crate::scheduler::QueueAppendPlan, } @@ -123,6 +123,11 @@ impl CrossingWorkOutcome { self } + const fn with_canonical_probe(mut self) -> Self { + self.canonical_probes = 1; + self + } + fn combine(self, next: Self) -> Self { Self { has_more: next.has_more, @@ -160,7 +165,7 @@ impl Pallet { let mut expected_user_feed_counts = BTreeMap::new(); let mut expected_radix = BTreeMap::, u16>::new(); let mut indexed_actors = BTreeSet::new(); - let page_size = T::ObservationPageSize::get(); + let page_size = T::CrossingPageSize::get(); for (key, state) in CrossingLeafStates::::iter(/* deos-bypass: bounded-iter */) { if page_size == 0 @@ -200,7 +205,7 @@ impl Pallet { .checked_add(1) .ok_or_else(&invalid)?; } - let contract = ActorContracts::::get(member.actor_id).ok_or_else(&invalid)?; + let contract = Self::load_actor_contract(member.actor_id).ok_or_else(&invalid)?; let crossing = Self::crossing_from_trigger(&contract.trigger).ok_or_else(&invalid)?; let hot = ActorHot::::get(member.actor_id).ok_or_else(&invalid)?; let TriggerRuntimeState::ObservationCrossing { phase, .. } = hot.trigger_runtime_state @@ -253,11 +258,13 @@ impl Pallet { return Err(invalid()); } for (actor_id, hot) in ActorHot::::iter(/* deos-bypass: bounded-iter */) { - let contract = ActorContracts::::get(actor_id).ok_or_else(&invalid)?; + let contract = Self::load_actor_contract(actor_id).ok_or_else(&invalid)?; let contract_crossing = Self::crossing_from_trigger(&contract.trigger); let runtime_crossing = match hot.trigger_runtime_state { TriggerRuntimeState::ObservationCrossing { phase, .. } => Some(phase), - TriggerRuntimeState::Stateless | TriggerRuntimeState::Cadenced { .. } => None, + TriggerRuntimeState::Stateless + | TriggerRuntimeState::AtTime { .. } + | TriggerRuntimeState::Cadenced { .. } => None, }; if contract_crossing.is_some() != runtime_crossing.is_some() || runtime_crossing.is_some() != CrossingMemberships::::contains_key(actor_id) @@ -431,9 +438,16 @@ impl Pallet { !CrossingMemberships::::contains_key(actor_id), Error::::CrossingIndexInvariant ); - let page_size = T::ObservationPageSize::get(); + let page_size = T::CrossingPageSize::get(); ensure!(page_size > 0, Error::::CrossingIndexInvariant); let (key, _) = Self::crossing_obligation(&crossing, phase); + let counterpart_threshold = match phase { + CrossingPhase::Armed => crossing.rearm_threshold, + CrossingPhase::WaitingForRearm => crossing.threshold, + }; + let admission_identity = ActorAdmissionCertificates::::get(actor_id) + .map(|certificate| certificate.admission_identity) + .unwrap_or([0; 32]); ensure!( CrossingFeedMembershipCount::::get(key.feed) < T::MaxCrossingMembersPerFeed::get(), Error::::CrossingIndexCapacityExceeded @@ -473,6 +487,8 @@ impl Pallet { .try_push(CrossingMember { actor_id, generation, + counterpart_threshold, + admission_identity, }) .map_err(|_| Error::::CrossingIndexCapacityExceeded)?; if state.member_count == 0 { @@ -510,6 +526,37 @@ impl Pallet { Ok(()) } + pub(crate) fn sync_crossing_compiled_authority( + actor_id: ActorId, + crossing: ObservationCrossing, + phase: CrossingPhase, + admission_identity: [u8; 32], + ) -> DispatchResult { + let locator = + CrossingMemberships::::get(actor_id).ok_or(Error::::CrossingIndexInvariant)?; + let counterpart_threshold = match phase { + CrossingPhase::Armed => crossing.rearm_threshold, + CrossingPhase::WaitingForRearm => crossing.threshold, + }; + CrossingMemberPages::::try_mutate( + locator.key, + locator.page, + |maybe_page| -> DispatchResult { + let member = maybe_page + .as_mut() + .and_then(|page| page.entries.get_mut(locator.offset as usize)) + .ok_or(Error::::CrossingIndexInvariant)?; + ensure!( + member.actor_id == actor_id && member.generation == locator.generation, + Error::::CrossingIndexInvariant + ); + member.counterpart_threshold = counterpart_threshold; + member.admission_identity = admission_identity; + Ok(()) + }, + ) + } + fn remove_crossing_member(actor_id: ActorId, clear_feed_queue: bool) -> DispatchResult { let Some(locator) = CrossingMemberships::::get(actor_id) else { return Ok(()); @@ -520,10 +567,11 @@ impl Pallet { let mut page = CrossingMemberPages::::get(locator.key, locator.page) .ok_or(Error::::CrossingIndexInvariant)?; ensure!( - page.entries.get(locator.offset as usize).copied() - == Some(CrossingMember { - actor_id, - generation: locator.generation, + page + .entries + .get(locator.offset as usize) + .is_some_and(|member| { + member.actor_id == actor_id && member.generation == locator.generation }), Error::::CrossingIndexInvariant ); @@ -853,6 +901,8 @@ impl Pallet { revision: 1, previous: first_locator.key.threshold, current: first_locator.key.threshold, + cause_provenance: crate::TriggerCauseProvenance::Deferred, + cause_block: 0, }; let cursor = CrossingRangeCursor { revision: transition.revision, @@ -1101,6 +1151,7 @@ impl Pallet { actor_id: ActorId, hot: ActorHotStateOf, contract: ActorContractOf, + _transition: CrossingTransitionObligation, ) -> Result<(CrossingWorkPlan, bool, Option>), DispatchError> { let mut loaded = match Self::load_actor_state(actor_id) { LoadedActorStateOf::Active(state) => state, @@ -1125,7 +1176,7 @@ impl Pallet { .map_err(|_| Error::::ActorInvariant)?; if activation.terminal_reason.is_some() || NextQueueTicket::::get() == u64::MAX { Ok((CrossingWorkPlan::FireCohortClosed, false, None)) - } else if activation.already_pending || activation.instance.queue_ticket.is_some() { + } else if activation.already_pending { Ok((CrossingWorkPlan::FireCohortCoalesced, false, None)) } else { let immediate_fifo = matches!( @@ -1148,6 +1199,13 @@ impl Pallet { transition: CrossingTransitionObligation, classify_fire: bool, ) -> Result<(CrossingWorkPlan, bool, Option>), DispatchError> { + if IndexedTriggerDetectionDisabled::::contains_key(member.actor_id) { + return Ok(( + CrossingWorkPlan::SkipPostInstallationTransition, + false, + None, + )); + } let hot = ActorHot::::get(member.actor_id).ok_or(Error::::ActorInvariant)?; let TriggerRuntimeState::ObservationCrossing { phase, @@ -1164,7 +1222,7 @@ impl Pallet { )); } let contract = - ActorContracts::::get(member.actor_id).ok_or(Error::::CrossingIndexInvariant)?; + Self::load_actor_contract(member.actor_id).ok_or(Error::::CrossingIndexInvariant)?; let crossing = Self::crossing_from_trigger(&contract.trigger).ok_or(Error::::CrossingIndexInvariant)?; let transition_kind = crossing.transition(phase, transition.previous, transition.current); @@ -1183,63 +1241,86 @@ impl Pallet { if !classify_fire { return Ok((CrossingWorkPlan::FireCohortPending, false, None)); } - Self::classify_fire_activation(member.actor_id, hot, contract) + Self::classify_fire_activation(member.actor_id, hot, contract, transition) } - fn crossing_candidate_matches_plan( - member: CrossingMember, + fn classify_crossing_idle_candidate( + authority: &CrossingCandidateAuthority, transition: CrossingTransitionObligation, - expected: CrossingWorkPlan, classify_fire: bool, - ) -> Result<(bool, bool, Option>), DispatchError> { - let hot = ActorHot::::get(member.actor_id).ok_or(Error::::ActorInvariant)?; + ) -> Result>)>, DispatchError> { + let member = authority.member; + if IndexedTriggerDetectionDisabled::::contains_key(member.actor_id) { + return Ok(Some(( + CrossingWorkPlan::SkipPostInstallationTransition, + false, + None, + ))); + } + if authority.locator.generation != member.generation + || authority.locator.key.traversal + != if transition.current > transition.previous { + CrossingTraversal::Upward + } else { + CrossingTraversal::Downward + } + { + return Err(Error::::CrossingIndexInvariant.into()); + } + let Some(state) = + Self::load_crossing_idle_activation_state(member.actor_id, authority.locator.key.feed) + else { + return Ok(None); + }; + if member.admission_identity != state.authority.admission_identity { + return Ok(None); + } let TriggerRuntimeState::ObservationCrossing { phase, installed_at_revision, - } = hot.trigger_runtime_state + } = state.hot.trigger_runtime_state else { - return Err(Error::::ActorInvariant.into()); + return Err(Error::::CrossingIndexInvariant.into()); }; - if expected == CrossingWorkPlan::SkipPostInstallationTransition { - return Ok((installed_at_revision >= transition.revision, false, None)); - } if installed_at_revision >= transition.revision { - return Ok((false, false, None)); - } - let contract = - ActorContracts::::get(member.actor_id).ok_or(Error::::CrossingIndexInvariant)?; - let crossing = - Self::crossing_from_trigger(&contract.trigger).ok_or(Error::::CrossingIndexInvariant)?; - let transition_kind = crossing.transition(phase, transition.previous, transition.current); - let (_, role) = Self::crossing_obligation(&crossing, phase); - ensure!( - matches!( - (role, transition_kind), - (CrossingMembershipRole::Fire, CrossingTransition::Fire) - | (CrossingMembershipRole::Rearm, CrossingTransition::Rearm) - ), - Error::::CrossingIndexInvariant - ); - if expected == CrossingWorkPlan::RearmCohort { - return Ok((transition_kind == CrossingTransition::Rearm, false, None)); - } - if expected == CrossingWorkPlan::FireCohortPending { - return Ok(( - transition_kind == CrossingTransition::Fire && !classify_fire, + return Ok(Some(( + CrossingWorkPlan::SkipPostInstallationTransition, false, None, - )); + ))); + } + if phase == CrossingPhase::WaitingForRearm { + return Ok(Some((CrossingWorkPlan::RearmCohort, false, None))); + } + if !classify_fire { + return Ok(Some((CrossingWorkPlan::FireCohortPending, false, None))); + } + let classification = Self::classify_observation_activation_compact(&state) + .map_err(|_| Error::::ActorInvariant)?; + if classification.terminal_reason.is_some() || NextQueueTicket::::get() == u64::MAX { + return Ok(Some((CrossingWorkPlan::FireCohortClosed, false, None))); } - if transition_kind != CrossingTransition::Fire || !classify_fire { - return Ok((false, false, None)); + if state.hot.pending_signal { + return Ok(Some((CrossingWorkPlan::FireCohortCoalesced, false, None))); } - let (plan, immediate_fifo, queue_hot) = - Self::classify_fire_activation(member.actor_id, hot, contract)?; - Ok((plan == expected, immediate_fifo, queue_hot)) + if classification.execution_phase != crate::ActorExecutionPhase::Ready { + return Ok(None); + } + let mut queue_hot = state.hot; + queue_hot.pending_signal = true; + queue_hot.trigger_runtime_state = TriggerRuntimeState::ObservationCrossing { + phase: CrossingPhase::WaitingForRearm, + installed_at_revision, + }; + Ok(Some(( + CrossingWorkPlan::FireCohortPlaced, + true, + Some(queue_hot), + ))) } pub(crate) fn preflight_crossing_cohort( - snapshot: &CrossingCohortSnapshot, + snapshot: &CrossingCohortSnapshot, transition: CrossingTransitionObligation, classify_fire: bool, expected_plan: Option, @@ -1249,22 +1330,25 @@ impl Pallet { let mut queue_candidates = Vec::new(); let mut admitted_candidates = 0u32; for authority in snapshot.candidates.iter(/* deos-bypass: bounded-iter */) { - let (candidate_immediate_fifo, queue_hot) = if let Some(expected) = plan { - let (matches, immediate_fifo, queue_hot) = Self::crossing_candidate_matches_plan( - authority.member, - transition, - expected, - classify_fire, - )?; - if !matches { + let (candidate_immediate_fifo, queue_hot, truncate_after_candidate) = { + let compact = Self::classify_crossing_idle_candidate(authority, transition, classify_fire)?; + if compact.is_none() && admitted_candidates > 0 { break; } - (immediate_fifo, queue_hot) - } else { - let (candidate_plan, immediate_fifo, queue_hot) = - Self::classify_crossing_candidate(authority.member, transition, classify_fire)?; - plan = Some(candidate_plan); - (immediate_fifo, queue_hot) + let truncate_after_candidate = compact.is_none(); + let (candidate_plan, immediate_fifo, queue_hot) = if let Some(compact) = compact { + compact + } else { + Self::classify_crossing_candidate(authority.member, transition, classify_fire)? + }; + if let Some(expected) = plan { + if candidate_plan != expected { + break; + } + } else { + plan = Some(candidate_plan); + } + (immediate_fifo, queue_hot, truncate_after_candidate) }; if plan == Some(CrossingWorkPlan::FireCohortPlaced) { if placed_immediate_fifo.is_some_and(|expected| expected != candidate_immediate_fifo) { @@ -1278,6 +1362,9 @@ impl Pallet { admitted_candidates = admitted_candidates .checked_add(1) .ok_or(Error::::CrossingIndexInvariant)?; + if truncate_after_candidate { + break; + } } Ok(CrossingCohortPreflight { plan: plan.ok_or(Error::::CrossingIndexInvariant)?, @@ -1303,13 +1390,17 @@ impl Pallet { .map_err(|_| Error::::CrossingIndexInvariant)?; let mut crossings = BoundedVec::default(); for candidate in &candidates { - let contract = ActorContracts::::get(candidate.member.actor_id) - .ok_or(Error::::CrossingIndexInvariant)?; + let crossing = ObservationCrossing { + feed: candidate.locator.key.feed, + direction: match candidate.locator.key.traversal { + CrossingTraversal::Upward => CrossingDirection::Rising, + CrossingTraversal::Downward => CrossingDirection::Falling, + }, + threshold: candidate.locator.key.threshold, + rearm_threshold: candidate.member.counterpart_threshold, + }; crossings - .try_push( - Self::crossing_from_trigger(&contract.trigger) - .ok_or(Error::::CrossingIndexInvariant)?, - ) + .try_push(crossing) .map_err(|_| Error::::CrossingIndexInvariant)?; } let queue_plan = Self::preflight_paged_enqueue_cohort(queue_candidates) @@ -1334,7 +1425,7 @@ impl Pallet { >, crossings: &BoundedVec, T::MaxCrossingActorsPerBlock>, ) -> DispatchResult { - let page_size = T::ObservationPageSize::get(); + let page_size = T::CrossingPageSize::get(); ensure!(page_size > 0, Error::::CrossingIndexInvariant); let mut processed = alloc::vec![false; candidates.len()]; let mut user_count = 0u32; @@ -1371,9 +1462,14 @@ impl Pallet { } let candidate = candidates[index]; let offset = page.entries.len() as u32; + let destination_member = { + let mut member = candidate.member; + member.counterpart_threshold = crossings[index].threshold; + member + }; page .entries - .try_push(candidate.member) + .try_push(destination_member) .map_err(|_| Error::::CrossingIndexCapacityExceeded)?; CrossingMemberships::::insert( candidate.member.actor_id, @@ -1426,7 +1522,7 @@ impl Pallet { .tail_refill .take() .ok_or(Error::::CrossingIndexInvariant)?; - let source: CrossingCohortSnapshot = + let source: CrossingCohortSnapshot = CrossingCohortSnapshot { key: authority.candidates[0].locator.key, page: authority.cursor.page, @@ -1556,7 +1652,7 @@ impl Pallet { #[cfg(test)] pub(crate) fn test_placed_cohort_authority_count( - snapshot: &CrossingCohortSnapshot, + snapshot: &CrossingCohortSnapshot, transition: CrossingTransitionObligation, malform_later_locator: bool, ) -> Result { @@ -1615,8 +1711,8 @@ impl Pallet { } pub(crate) fn rewrite_non_tail_source( - source: &CrossingCohortSnapshot, - tail_refill: &CrossingCohortSnapshot, + source: &CrossingCohortSnapshot, + tail_refill: &CrossingCohortSnapshot, commit: bool, ) -> Result { ensure!( @@ -1718,8 +1814,8 @@ impl Pallet { #[cfg(test)] pub(crate) fn test_non_tail_placed_authority_count( - source: &CrossingCohortSnapshot, - tail_refill: &CrossingCohortSnapshot, + source: &CrossingCohortSnapshot, + tail_refill: &CrossingCohortSnapshot, transition: CrossingTransitionObligation, ) -> Result { ensure!( @@ -2014,7 +2110,7 @@ impl Pallet { let refill_limit = tail_refill_available.unwrap_or(u32::MAX); core::cmp::min( core::cmp::min(core::cmp::min(requested, remaining), refill_limit), - T::ObservationPageSize::get(), + T::CrossingPageSize::get(), ) } @@ -2030,8 +2126,7 @@ impl Pallet { key: CrossingLeafKeyOf, source_page: u32, count: u32, - ) -> Result, DispatchError> - { + ) -> Result, DispatchError> { let state = CrossingLeafStates::::get(key).ok_or(Error::::CrossingIndexInvariant)?; ensure!( count > 0 && source_page < state.tail_page, @@ -2078,8 +2173,7 @@ impl Pallet { page: &CrossingMemberPageOf, offset: u32, limit: u32, - ) -> Result, DispatchError> - { + ) -> Result, DispatchError> { let start = offset as usize; ensure!( start <= page.entries.len(), @@ -2222,6 +2316,53 @@ impl Pallet { Self::advance_crossing_pending_feed(feed) } + fn charge_crossing_fire_occurrence( + actor_id: ActorId, + ) -> Result>, DispatchError> { + use crate::weights::WeightInfo as _; + + let identity = ActorIdentities::::get(actor_id).ok_or(Error::::ActorInvariant)?; + let actor_type = identity.actor_class.actor_type(); + let breakdown = Self::trigger_fee_for_weight( + actor_type, + TriggerFamily::ObservationCrossing, + T::WeightInfo::observation_crossing_trigger_occurrence(), + ); + Self::try_charge_automatic_trigger_occurrence( + actor_type, + &identity.sovereign_account, + breakdown, + ) + .map(|charged| charged.then_some(breakdown)) + } + + fn deposit_crossing_fire_occurrence( + actor_id: ActorId, + breakdown: crate::TriggerFeeBreakdown, + ) { + Self::deposit_event(Event::TriggerOccurrenceProcessed { + actor_id, + trigger_family: breakdown.trigger_family, + fee: breakdown.trigger_fee, + }); + } + + fn charge_crossing_placed_cohort(authority: &CrossingPlacedCohortAuthority) -> DispatchResult { + let mut charged = Vec::with_capacity(authority.candidates.len()); + for candidate in &authority.candidates { + let Some(breakdown) = Self::charge_crossing_fire_occurrence(candidate.member.actor_id)? + else { + return Err(Error::::InsufficientFee.into()); + }; + charged.push((candidate.member.actor_id, breakdown)); + } + for (actor_id, breakdown) in charged { + IndexedTriggerDetectionDisabled::::insert(actor_id, ()); + Self::deposit_crossing_fire_occurrence(actor_id, breakdown); + } + Ok(()) + } + fn do_crossing_work_unit() -> Result { let list = CrossingPendingFeedListState::::get(); if list.count == 0 { @@ -2322,6 +2463,15 @@ impl Pallet { && locator.generation == member.generation, Error::::CrossingIndexInvariant ); + if IndexedTriggerDetectionDisabled::::contains_key(member.actor_id) { + cursor.offset = cursor + .offset + .checked_add(1) + .ok_or(Error::::CrossingIndexInvariant)?; + CrossingRangeCursors::::insert(feed, cursor); + Self::advance_crossing_pending_feed(feed)?; + return Ok(CrossingWorkOutcome::new(true, 1, 1, 1, 1)); + } let hot = ActorHot::::get(member.actor_id).ok_or(Error::::ActorInvariant)?; let TriggerRuntimeState::ObservationCrossing { phase, @@ -2340,7 +2490,7 @@ impl Pallet { return Ok(CrossingWorkOutcome::new(true, 1, 1, 1, 1)); } let contract = - ActorContracts::::get(member.actor_id).ok_or(Error::::CrossingIndexInvariant)?; + Self::load_actor_contract(member.actor_id).ok_or(Error::::CrossingIndexInvariant)?; let crossing = Self::crossing_from_trigger(&contract.trigger).ok_or(Error::::CrossingIndexInvariant)?; let transition_kind = crossing.transition(phase, transition.previous, transition.current); @@ -2358,17 +2508,36 @@ impl Pallet { CrossingTransition::Rearm => CrossingPhase::Armed, CrossingTransition::None => return Err(Error::::CrossingIndexInvariant.into()), }; + let fire_plan = if transition_kind == CrossingTransition::Fire { + Some(Self::classify_fire_activation(member.actor_id, hot, contract, transition)?.0) + } else { + None + }; Self::move_crossing_membership(member.actor_id, crossing, next_phase, locator)?; - let mut activation_closed = false; - if transition_kind == CrossingTransition::Fire { - let activated = - Self::request_activation(member.actor_id).map_err(Self::activation_failure_error)?; - ensure!( - activated != ActivationOutcome::IgnoredStale, - Error::::CrossingIndexInvariant - ); - activation_closed = activated == ActivationOutcome::Closed; - if activation_closed && CrossingFeedMembershipCount::::get(feed) == 0 { + let mut activation = None; + if let Some(fire_plan) = fire_plan { + if fire_plan == CrossingWorkPlan::FireCohortClosed { + activation = + Some(Self::request_activation(member.actor_id).map_err(Self::activation_failure_error)?); + } else if fire_plan != CrossingWorkPlan::FireCohortCoalesced + && let Some(breakdown) = Self::charge_crossing_fire_occurrence(member.actor_id)? + { + let activated = + Self::request_activation(member.actor_id).map_err(Self::activation_failure_error)?; + ensure!( + matches!( + activated, + ActivationOutcome::Latched | ActivationOutcome::Coalesced + ), + Error::::CrossingIndexInvariant + ); + IndexedTriggerDetectionDisabled::::insert(member.actor_id, ()); + Self::deposit_crossing_fire_occurrence(member.actor_id, breakdown); + activation = Some(activated); + } + if activation == Some(ActivationOutcome::Closed) + && CrossingFeedMembershipCount::::get(feed) == 0 + { // Closing the final member clears this feed's queue, cursor, and pending // link. Report possible outer work conservatively; the next bounded // unit observes the canonical pending-list state without recreating it. @@ -2377,10 +2546,10 @@ impl Pallet { } Self::persist_crossing_cursor_after_movement(feed, &transition, cursor, key, threshold)?; let outcome = CrossingWorkOutcome::new(true, 1, 1, 1, 1); - Ok(if transition_kind == CrossingTransition::Fire { - outcome.with_activation(activation_closed) - } else { - outcome + Ok(match activation { + Some(activated) => outcome.with_activation(activated == ActivationOutcome::Closed), + None if transition_kind == CrossingTransition::Fire => outcome.with_canonical_probe(), + None => outcome, }) } @@ -2558,7 +2727,12 @@ impl Pallet { CrossingLeafStates::::get(key), CrossingMemberPages::::get(key, cursor.page), ) { - let count = Self::crossing_source_prefix_count(&page, cursor.offset, max_candidates); + let branch_candidates = if cursor.page == state.tail_page { + max_candidates + } else { + max_candidates.min(64) + }; + let count = Self::crossing_source_prefix_count(&page, cursor.offset, branch_candidates); if count >= 3 { let snapshot = Self::snapshot_crossing_source_prefix(key, cursor.page, &page, cursor.offset, count)?; @@ -2608,6 +2782,7 @@ impl Pallet { .current_threshold .ok_or(Error::::CrossingIndexInvariant)?; let key = authority.candidates[0].locator.key; + Self::charge_crossing_placed_cohort(&authority)?; if authority.tail_refill.is_some() { Self::commit_non_tail_placed_cohort_authority(authority)?; } else if count == 2 { @@ -2628,6 +2803,21 @@ impl Pallet { }) } + fn do_crossing_placed_batch_or_single_unit( + admitted_candidates: u32, + ) -> Result { + let batch = polkadot_sdk::frame_support::storage::with_transaction(|| { + match Self::do_crossing_atomic_placed_batch_unit(admitted_candidates) { + Ok(outcome) => TransactionOutcome::Commit(Ok(outcome)), + Err(error) => TransactionOutcome::Rollback(Err(error)), + } + }); + match batch { + Err(error) if error == Error::::InsufficientFee.into() => Self::do_crossing_work_unit(), + result => result, + } + } + pub(crate) fn crossing_plan_weight_for_admission( plan: CrossingWorkPlan, admitted_candidates: u32, @@ -2840,7 +3030,9 @@ impl Pallet { }; weight }; - let branch_weight = if meter.can_consume(admitted_branch_weight) { + let fault_weight = T::WeightInfo::record_crossing_worker_fault(); + let branch_weight = if meter.can_consume(admitted_branch_weight.saturating_add(fault_weight)) + { admitted_branch_weight } else if let Some(single_plan) = Self::crossing_single_candidate_plan(plan) { plan = single_plan; @@ -2850,6 +3042,9 @@ impl Pallet { else { break; }; + if !meter.can_consume(single_weight.saturating_add(fault_weight)) { + break; + } single_weight } else { break; @@ -2866,7 +3061,9 @@ impl Pallet { { break; } - if plan == CrossingWorkPlan::Empty || !meter.can_consume(branch_weight) { + if plan == CrossingWorkPlan::Empty + || !meter.can_consume(branch_weight.saturating_add(fault_weight)) + { break; } let list = CrossingPendingFeedListState::::get(); @@ -2881,7 +3078,7 @@ impl Pallet { Self::test_record_first_crossing_branch_weight(branch_weight); let result = polkadot_sdk::frame_support::storage::with_transaction(|| { if plan == CrossingWorkPlan::FireCohortPlacedBatch { - match Self::do_crossing_atomic_placed_batch_unit(admitted_candidates) { + match Self::do_crossing_placed_batch_or_single_unit(admitted_candidates) { Ok(outcome) => TransactionOutcome::Commit(Ok(outcome)), Err(error) => TransactionOutcome::Rollback(Err(error)), } @@ -2918,12 +3115,19 @@ impl Pallet { } else { CrossingWorkerFaultClass::Other }; - CrossingWorkerFaultState::::put(CrossingWorkerFault { - feed, - revision: fault_revision, - threshold: fault_cursor.and_then(|cursor| cursor.current_threshold), - class, - }); + let recorded = Self::record_crossing_worker_fault( + &mut meter, + CrossingWorkerFault { + feed, + revision: fault_revision, + threshold: fault_cursor.and_then(|cursor| cursor.current_threshold), + class, + }, + ); + debug_assert!( + recorded, + "fault Weight was reserved before Crossing mutation" + ); } break; }; @@ -2943,6 +3147,39 @@ impl Pallet { (meter.consumed(), counters) } + pub(crate) fn rearm_disabled_crossing_after_opening( + actor_id: ActorId, + trigger: &TriggerOf, + ) -> DispatchResult { + if !IndexedTriggerDetectionDisabled::::contains_key(actor_id) { + return Ok(()); + } + let crossing = Self::crossing_from_trigger(trigger).ok_or(Error::::ActorInvariant)?; + let locator = + CrossingMemberships::::get(actor_id).ok_or(Error::::CrossingIndexInvariant)?; + let (current, installed_at_revision) = match T::ObservationProvider::current(&crossing.feed) { + crate::CanonicalObservationState::Available { value, revision } => (value, revision), + crate::CanonicalObservationState::Unavailable => { + return Err(Error::::ObservationUnavailable.into()); + } + crate::CanonicalObservationState::Uninitialized => { + return Err(Error::::ObservationUninitialized.into()); + } + }; + let phase = crossing.initial_phase(current); + Self::move_crossing_membership(actor_id, crossing, phase, locator)?; + ActorHot::::try_mutate(actor_id, |maybe_hot| -> DispatchResult { + let hot = maybe_hot.as_mut().ok_or(Error::::ActorInvariant)?; + hot.trigger_runtime_state = TriggerRuntimeState::ObservationCrossing { + phase, + installed_at_revision, + }; + Ok(()) + })?; + IndexedTriggerDetectionDisabled::::remove(actor_id); + Ok(()) + } + pub(crate) fn preflight_crossing_membership( actor_id: ActorId, trigger: &TriggerOf, @@ -2962,7 +3199,7 @@ impl Pallet { crossing.has_valid_hysteresis(), Error::::InvalidTriggerConfiguration ); - if ActorContracts::::get(actor_id).is_some_and(|contract| contract.trigger == *trigger) { + if Self::load_actor_contract(actor_id).is_some_and(|contract| contract.trigger == *trigger) { let hot = ActorHot::::get(actor_id).ok_or(Error::::ActorInvariant)?; let TriggerRuntimeState::ObservationCrossing { phase, diff --git a/template/pallets/actors/src/execution.rs b/template/pallets/actors/src/execution.rs index 9af47c23..f1efacae 100644 --- a/template/pallets/actors/src/execution.rs +++ b/template/pallets/actors/src/execution.rs @@ -2,16 +2,13 @@ use super::pallet::*; use super::types::{InputLimit, Task as ActorTask}; use super::{ AssetOps, DexOps, FeeChargeKind, FeeCollector, LiquidityOps, ObservationProvider as _, - RetryClass, ScalarObservationState, StakingOps, TaskFailure, WeightInfo as _, - fee_native_protected_minimum, settle_attempt_fee_step, + RetryClass, ScalarObservationState, StakingOps, TaskEffectExecution, TaskFailure, + WeightInfo as _, fee_native_protected_minimum, settle_attempt_fee_step, }; use frame::prelude::*; -use polkadot_sdk::{ - sp_runtime::{ - Perbill, - traits::{SaturatedConversion, Zero}, - }, - sp_weights::WeightToFee as _, +use polkadot_sdk::sp_runtime::{ + Perbill, + traits::{SaturatedConversion, Zero}, }; /// Checked increment for protocol-semantic counters. The admitted bound @@ -201,108 +198,192 @@ pub(crate) struct AttemptExecution { weight: Weight, disposition: AttemptDisposition, fee_collection_failed: bool, + effect_execution: TaskEffectExecution, outcomes: OutcomeTotals, } impl Pallet { - pub(crate) fn cancel_continuation_internal( + fn commit_cycle_nonce(actor_id: ActorId, cycle_nonce: u64) -> DispatchResult { + ActorIdentities::::try_mutate(actor_id, |maybe| -> DispatchResult { + let identity = maybe.as_mut().ok_or(Error::::ActorRunInvariant)?; + let expected = identity + .cycle_nonce + .checked_add(1) + .ok_or(Error::::ActorRunInvariant)?; + ensure!(cycle_nonce == expected, Error::::ActorRunInvariant); + identity.cycle_nonce = cycle_nonce; + Ok(()) + }) + } + + pub(crate) fn cancel_run_internal( actor_id: ActorId, reason: CancellationReason, outcomes: Option, ) -> Result { - let Some(continuation) = ContinuationStateStore::::get(actor_id) else { + let Some(run_state) = ActorRunStateStore::::get(actor_id) else { return Ok(false); }; - let identity = ActorIdentities::::get(actor_id).ok_or(Error::::ContinuationInvariant)?; - ensure!(identity.cycle_nonce > 0, Error::::ContinuationInvariant); - Self::wakeup_substrate_invalidate_inner(actor_id) - .map_err(|_| Error::::ContinuationInvariant)?; + let identity = ActorIdentities::::get(actor_id).ok_or(Error::::ActorRunInvariant)?; + ensure!( + identity.cycle_nonce.checked_add(1) == Some(run_state.cycle_nonce), + Error::::ActorRunInvariant + ); + Self::wakeup_substrate_invalidate_inner(actor_id).map_err(|_| Error::::ActorRunInvariant)?; ActorHot::::try_mutate(actor_id, |maybe| -> DispatchResult { - let hot = maybe.as_mut().ok_or(Error::::ContinuationInvariant)?; + let hot = maybe.as_mut().ok_or(Error::::ActorRunInvariant)?; ensure!( - hot.cycle_state == CycleState::Suspended, - Error::::ContinuationInvariant + matches!(hot.cycle_state, CycleState::Running | CycleState::Suspended), + Error::::ActorRunInvariant ); hot.cycle_state = CycleState::Idle; hot.queue_ticket = None; hot.wakeup_pointer = None; Ok(()) })?; - ContinuationStateStore::::remove(actor_id); - let totals = outcomes.unwrap_or(continuation.cumulative_outcomes); + Self::commit_cycle_nonce(actor_id, run_state.cycle_nonce)?; + ActorRunStateStore::::remove(actor_id); + Self::reconcile_actor_state_hold(actor_id)?; + let totals = outcomes.unwrap_or(run_state.cumulative_outcomes); Self::deposit_event(Event::CycleCancelled { actor_id, - cycle_nonce: identity.cycle_nonce, + cycle_nonce: run_state.cycle_nonce, reason, }); Self::deposit_event(Event::CycleSummary { actor_id, - cycle_nonce: identity.cycle_nonce, + cycle_nonce: run_state.cycle_nonce, result: CycleResult::Cancelled, outcomes: totals, }); Ok(true) } - pub(crate) fn write_continuation_state( + pub(crate) fn write_run_state( actor_id: ActorId, - state: Option>, + state: Option>, ) -> DispatchResult { - let loaded = match Self::load_actor_state(actor_id) { - LoadedActorStateOf::Active(state) => state, - LoadedActorStateOf::NotRegistered | LoadedActorStateOf::Dormant(_) => { - return Err(Error::::ActorNotFound.into()); - } - LoadedActorStateOf::Corrupt => return Err(Error::::ActorInvariant.into()), + let Some((loaded, admission, _)) = Self::load_current_step_service_state(actor_id) else { + return Err(Error::::ActorInvariant.into()); }; let identity = loaded.identity; - if let Some(continuation) = state.as_ref() { - let contract = loaded.contract; + let contract = loaded.contract; + let existing_run = loaded.run_state; + let target_step = state + .as_ref() + .and_then(|run| Self::load_current_step_from_storage(actor_id, run.cursor)); + let expected_cycle_nonce = identity + .cycle_nonce + .checked_add(1) + .ok_or(Error::::ActorRunInvariant)?; + if let Some(run_state) = state.as_ref() { ensure!( - identity.mutability == Mutability::Mutable - && continuation.cursor < contract.steps.len() as u32, - Error::::ContinuationInvariant + run_state.cycle_nonce == expected_cycle_nonce + && existing_run.as_ref().is_none_or(|existing| { + existing.cycle_nonce == run_state.cycle_nonce + && existing.opening_snapshot == run_state.opening_snapshot + && existing.opening_predicate_results == run_state.opening_predicate_results + && existing.funding_snapshot == run_state.funding_snapshot + }) + && run_state.cursor < contract.steps.len() as u32 + && run_state.has_contract_authority( + admission.semantic_contract_id, + admission.body_commitment, + admission.admission_identity, + ) + && target_step.is_some(), + Error::::ActorRunInvariant ); - let max_attempts = contract.steps[continuation.cursor as usize] - .on_error - .retry_max_attempts() - .ok_or(Error::::ContinuationInvariant)?; + if run_state.suspension.is_some() { + ensure!( + identity.mutability == Mutability::Mutable && run_state.suspension_is_coherent(), + Error::::ActorRunInvariant + ); + let max_attempts = target_step + .as_ref() + .ok_or(Error::::ActorRunInvariant)? + .step + .on_error + .retry_max_attempts() + .ok_or(Error::::ActorRunInvariant)?; + let expected_eligible_at = Self::suspension_eligible_at( + contract.cooldown_blocks, + contract.window, + run_state.last_attempt_block, + run_state.unsuccessful_attempts_at_cursor, + ) + .map_err(|_| Error::::ActorRunInvariant)?; + ensure!( + run_state.unsuccessful_attempts_at_cursor > 0 + && run_state.unsuccessful_attempts_at_cursor < max_attempts + && run_state.eligible_at == expected_eligible_at, + Error::::ActorRunInvariant + ); + } else { + ensure!( + run_state.unsuccessful_attempts_at_cursor == 0 && run_state.running_is_coherent(), + Error::::ActorRunInvariant + ); + } + } else { + let run_state = existing_run.as_ref().ok_or(Error::::ActorRunInvariant)?; ensure!( - continuation.unsuccessful_attempts_at_cursor > 0 - && continuation.unsuccessful_attempts_at_cursor < max_attempts, - Error::::ContinuationInvariant + run_state.cycle_nonce == expected_cycle_nonce, + Error::::ActorRunInvariant ); } + let next_cycle_state = state.as_ref().map(|run| { + if run.suspension.is_some() { + CycleState::Suspended + } else { + CycleState::Running + } + }); polkadot_sdk::frame_support::storage::with_transaction(|| { + if state.is_none() + && let Err(error) = Self::commit_cycle_nonce(actor_id, expected_cycle_nonce) + { + return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)); + } let hot_update = ActorHot::::try_mutate(actor_id, |maybe| -> DispatchResult { - maybe.as_mut().ok_or(Error::::ActorNotFound)?.cycle_state = if state.is_some() { - CycleState::Suspended - } else { - CycleState::Idle - }; + maybe.as_mut().ok_or(Error::::ActorNotFound)?.cycle_state = + next_cycle_state.unwrap_or(CycleState::Idle); Ok(()) }); if let Err(error) = hot_update { return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)); } - if let Some(continuation) = state { - ContinuationStateStore::::insert(actor_id, continuation); + if let Some(run_state) = state { + ActorRunStateStore::::insert(actor_id, run_state); } else { - ContinuationStateStore::::remove(actor_id); + ActorRunStateStore::::remove(actor_id); + } + if let Err(error) = Self::reconcile_actor_state_hold(actor_id) { + return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)); } polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(())) }) } - pub(crate) fn persist_continuation_suspension( + #[cfg(any(test, feature = "runtime-benchmarks"))] + pub(crate) fn persist_run_progress( actor_id: ActorId, - cycle_nonce: u64, - state: ContinuationStateOf, - reason: SuspensionReason, + state: ActorRunStateOf, + ) -> DispatchResult { + ensure!(state.running_is_coherent(), Error::::ActorRunInvariant); + Self::write_run_state(actor_id, Some(state)) + } + + #[cfg(feature = "runtime-benchmarks")] + pub(crate) fn persist_run_suspension( + actor_id: ActorId, + state: ActorRunStateOf, ) -> DispatchResult { + let cycle_nonce = state.cycle_nonce; let cursor = state.cursor; let cumulative_outcomes = state.cumulative_outcomes; - Self::write_continuation_state(actor_id, Some(state))?; + let reason = state.suspension.ok_or(Error::::ActorRunInvariant)?; + Self::write_run_state(actor_id, Some(state))?; Self::deposit_event(Event::CycleSuspended { actor_id, cycle_nonce, @@ -313,28 +394,62 @@ impl Pallet { Ok(()) } - pub(crate) fn begin_continuation_attempt( + fn persist_loaded_run_state(actor_id: ActorId, state: ActorRunStateOf) -> DispatchResult { + ensure!( + state.running_is_coherent() || state.suspension_is_coherent(), + Error::::ActorRunInvariant + ); + ActorHot::::try_mutate(actor_id, |maybe| -> DispatchResult { + let hot = maybe.as_mut().ok_or(Error::::ActorRunInvariant)?; + hot.cycle_state = if state.suspension.is_some() { + CycleState::Suspended + } else { + CycleState::Running + }; + Ok(()) + })?; + ActorRunStateStore::::insert(actor_id, state); + Self::reconcile_actor_state_hold(actor_id)?; + Ok(()) + } + + fn persist_loaded_run_suspension(actor_id: ActorId, state: ActorRunStateOf) -> DispatchResult { + let cycle_nonce = state.cycle_nonce; + let cursor = state.cursor; + let cumulative_outcomes = state.cumulative_outcomes; + let reason = state.suspension.ok_or(Error::::ActorRunInvariant)?; + Self::persist_loaded_run_state(actor_id, state)?; + Self::deposit_event(Event::CycleSuspended { + actor_id, + cycle_nonce, + cursor, + reason, + cumulative_outcomes, + }); + Ok(()) + } + + pub(crate) fn begin_run_attempt( actor_id: ActorId, - cycle_nonce: u64, now: BlockNumberFor, - ) -> Option> { - let updated = ContinuationStateStore::::mutate(actor_id, |maybe| { - let Some(continuation) = maybe.as_mut() else { + ) -> Option> { + let updated = ActorRunStateStore::::mutate(actor_id, |maybe| { + let Some(run_state) = maybe.as_mut() else { return false; }; - continuation.last_attempt_block = now; + run_state.last_attempt_block = now; true }); if !updated { return None; } - let continuation = ContinuationStateStore::::get(actor_id)?; + let run_state = ActorRunStateStore::::get(actor_id)?; Self::deposit_event(Event::CycleContinued { actor_id, - cycle_nonce, - cursor: continuation.cursor, + cycle_nonce: run_state.cycle_nonce, + cursor: run_state.cursor, }); - Some(continuation) + Some(run_state) } pub(crate) fn record_stop_cycle_event(actor_id: ActorId, cycle_nonce: u64, step_index: u32) { @@ -347,9 +462,11 @@ impl Pallet { fn record_step_outcome( trace: &mut Option<&mut alloc::vec::Vec>, + last_step_outcome: &mut Option, step_index: u32, outcome: StepOutcome, ) { + *last_step_outcome = Some(outcome.clone()); if let Some(records) = trace.as_deref_mut() { records.push(SimulationStepRecord { step_index, @@ -358,41 +475,254 @@ impl Pallet { } } + #[cfg(any(test, feature = "runtime-benchmarks"))] pub(crate) fn execute_single_cycle( actor_id: ActorId, instance: ActiveActorViewOf, now: BlockNumberFor, ) -> (Weight, bool) { - let result = Self::execute_single_cycle_traced(actor_id, instance, now, None); + let result = Self::execute_single_cycle_traced(actor_id, instance, now, None, None); (result.weight, result.fee_collection_failed) } + pub(crate) fn execute_zero_step_cycle( + actor_id: ActorId, + instance: ActiveActorViewOf, + now: BlockNumberFor, + ) -> (bool, AttemptDisposition) { + let result = Self::execute_single_cycle_traced(actor_id, instance, now, None, None); + (result.fee_collection_failed, result.disposition) + } + + pub(crate) fn execute_current_step_plan( + actor_id: ActorId, + instance: ActiveActorViewOf, + plan: CurrentStepPlanOf, + now: BlockNumberFor, + ) -> (Weight, bool, TaskEffectExecution, AttemptDisposition) { + let result = Self::execute_single_cycle_traced(actor_id, instance, now, Some(plan), None); + ( + result.weight, + result.fee_collection_failed, + result.effect_execution, + result.disposition, + ) + } + pub(crate) fn execute_single_cycle_traced( actor_id: ActorId, instance: ActiveActorViewOf, now: BlockNumberFor, + step_plan: Option>, mut trace: Option<&mut alloc::vec::Vec>, ) -> AttemptExecution { let base_weight = T::WeightInfo::cycle_orchestration(); - let is_continuation = instance.cycle_state == CycleState::Suspended; + let collect_fee_in_execution = step_plan.is_none(); + let has_persisted_run = matches!( + instance.cycle_state, + CycleState::Running | CycleState::Suspended + ); + let is_suspended_run = instance.cycle_state == CycleState::Suspended; let actor = instance.sovereign_account.clone(); let contract_steps = &instance.steps; - let Ok(fee_envelope) = Self::attempt_fee_envelope( - instance.actor_class.actor_type(), - contract_steps, - if is_continuation { - Self::continuation_state(actor_id).map_or(0, |state| state.cursor as usize) + if contract_steps.is_empty() { + if has_persisted_run || ActorRunStateStore::::contains_key(actor_id) { + return AttemptExecution { + weight: base_weight, + disposition: AttemptDisposition::Failed, + fee_collection_failed: false, + effect_execution: TaskEffectExecution::NotInvoked, + outcomes: OutcomeTotals::default(), + }; + } + let Some(cycle_nonce) = instance.cycle_nonce.checked_add(1) else { + return AttemptExecution { + weight: base_weight, + disposition: AttemptDisposition::Closed(CloseReason::CycleNonceExhausted), + fee_collection_failed: false, + effect_execution: TaskEffectExecution::NotInvoked, + outcomes: OutcomeTotals::default(), + }; + }; + if Self::commit_cycle_nonce(actor_id, cycle_nonce).is_err() { + return AttemptExecution { + weight: base_weight, + disposition: AttemptDisposition::Failed, + fee_collection_failed: false, + effect_execution: TaskEffectExecution::NotInvoked, + outcomes: OutcomeTotals::default(), + }; + } + ActorHot::::mutate(actor_id, |maybe| { + if let Some(hot) = maybe.as_mut() { + hot.pending_signal = false; + hot.last_cycle_block = Some(now); + hot.unsuccessful_attempt_streak = 0; + } + }); + ActorFunding::::mutate(actor_id, |maybe| { + if let Some(funding) = maybe.as_mut() { + funding.funding_accumulated.clear(); + } + }); + Self::deposit_event(Event::CycleStarted { + actor_id, + cycle_nonce, + }); + let outcomes = OutcomeTotals::default(); + Self::deposit_event(Event::CycleSummary { + actor_id, + cycle_nonce, + result: CycleResult::Completed, + outcomes, + }); + let close_reason = instance + .auto_close_at_cycle_nonce + .filter(|target| cycle_nonce >= *target) + .map(|_| CloseReason::AutoCloseNonceReached); + let disposition = if let Some(reason) = close_reason { + let Some(current) = Self::active_actor_view(actor_id) else { + return AttemptExecution { + weight: base_weight, + disposition: AttemptDisposition::Failed, + fee_collection_failed: false, + effect_execution: TaskEffectExecution::NotInvoked, + outcomes, + }; + }; + if Self::finalize_actor_loaded(actor_id, ¤t, reason).is_err() { + return AttemptExecution { + weight: base_weight, + disposition: AttemptDisposition::Failed, + fee_collection_failed: false, + effect_execution: TaskEffectExecution::NotInvoked, + outcomes, + }; + } + AttemptDisposition::Closed(reason) } else { - 0 - }, + AttemptDisposition::Completed + }; + return AttemptExecution { + weight: base_weight, + disposition, + fee_collection_failed: false, + effect_execution: TaskEffectExecution::NotInvoked, + outcomes, + }; + } + let fee_cursor = if has_persisted_run { + step_plan + .as_ref() + .and_then(|plan| plan.run.as_ref()) + .map(|state| state.cursor as usize) + .or_else(|| Self::actor_run_state(actor_id).map(|state| state.cursor as usize)) + .unwrap_or(0) + } else { + 0 + }; + if step_plan.as_ref().is_some_and(|plan| { + plan.ticket.actor_id != actor_id + || plan.ticket.cursor as usize != fee_cursor + || plan.loaded_step.cursor as usize != fee_cursor + || instance.steps.get(fee_cursor) != Some(&plan.loaded_step.step) + }) { + return AttemptExecution { + weight: base_weight, + disposition: AttemptDisposition::Failed, + fee_collection_failed: false, + effect_execution: TaskEffectExecution::NotInvoked, + outcomes: OutcomeTotals::default(), + }; + } + let admission = step_plan + .as_ref() + .map(|plan| plan.admission.clone()) + .or_else(|| ActorAdmissionCertificates::::get(actor_id)); + let Some(admission) = admission else { + return AttemptExecution { + weight: base_weight, + disposition: AttemptDisposition::Failed, + fee_collection_failed: false, + effect_execution: TaskEffectExecution::NotInvoked, + outcomes: OutcomeTotals::default(), + }; + }; + let step_resources = step_plan + .as_ref() + .map(|plan| plan.loaded_step.resources) + .or_else(|| { + u32::try_from(fee_cursor) + .ok() + .and_then(|cursor| Self::load_current_step_from_storage(actor_id, cursor)) + .map(|loaded| loaded.resources) + }); + let Some(step_resources) = step_resources else { + return AttemptExecution { + weight: base_weight, + disposition: AttemptDisposition::Failed, + fee_collection_failed: false, + effect_execution: TaskEffectExecution::NotInvoked, + outcomes: OutcomeTotals::default(), + }; + }; + let contract_authority = ActorRunAuthority { + semantic_contract_id: admission.semantic_contract_id, + body_commitment: admission.body_commitment, + admission_identity: admission.admission_identity, + }; + let Some(current_step) = instance.steps.get(fee_cursor) else { + return AttemptExecution { + weight: base_weight, + disposition: AttemptDisposition::Failed, + fee_collection_failed: false, + effect_execution: TaskEffectExecution::NotInvoked, + outcomes: OutcomeTotals::default(), + }; + }; + let Ok(expected_step_fee) = Self::maximum_current_action_fee( + instance.actor_class.actor_type(), + current_step, + step_resources, ) else { return AttemptExecution { weight: base_weight, disposition: AttemptDisposition::Failed, fee_collection_failed: false, + effect_execution: TaskEffectExecution::NotInvoked, outcomes: OutcomeTotals::default(), }; }; + let step_fee = step_plan + .as_ref() + .map(|plan| plan.maximum_fee.clone()) + .unwrap_or_else(|| expected_step_fee.clone()); + if step_fee != expected_step_fee { + return AttemptExecution { + weight: base_weight, + disposition: AttemptDisposition::Failed, + fee_collection_failed: false, + effect_execution: TaskEffectExecution::NotInvoked, + outcomes: OutcomeTotals::default(), + }; + } + let Ok(fee_steps) = BoundedVec::try_from(alloc::vec![crate::StepFeeEnvelope { + evaluation: step_fee.control_fee, + execution: step_fee.effect_fee, + total: step_fee.total_fee, + }]) else { + return AttemptExecution { + weight: base_weight, + disposition: AttemptDisposition::Failed, + fee_collection_failed: false, + effect_execution: TaskEffectExecution::NotInvoked, + outcomes: OutcomeTotals::default(), + }; + }; + let fee_envelope = AttemptFeeEnvelopeOf:: { + steps: fee_steps, + total: step_fee.total_fee, + }; let mut reserved_fee_remaining = fee_envelope.total; let mut fee_collection_failed = false; macro_rules! collect_step_fee { @@ -413,25 +743,36 @@ impl Pallet { funding_snapshot, opening_snapshot, opening_predicate_results, - ) = if is_continuation { - let Some(continuation) = - Self::begin_continuation_attempt(actor_id, instance.cycle_nonce, now) - else { + opening_predicate_cursor, + mut last_step_outcome, + ) = if has_persisted_run { + let run_state = if is_suspended_run { + Self::begin_run_attempt(actor_id, now) + } else { + step_plan + .as_ref() + .and_then(|plan| plan.run.clone()) + .or_else(|| Self::actor_run_state(actor_id)) + }; + let Some(run_state) = run_state else { return AttemptExecution { weight: base_weight, disposition: AttemptDisposition::Failed, fee_collection_failed: false, + effect_execution: TaskEffectExecution::NotInvoked, outcomes: OutcomeTotals::default(), }; }; ( - instance.cycle_nonce, - continuation.cursor, - continuation.unsuccessful_attempts_at_cursor, - continuation.cumulative_outcomes, - continuation.funding_snapshot, - continuation.opening_snapshot, - continuation.opening_predicate_results, + run_state.cycle_nonce, + run_state.cursor, + run_state.unsuccessful_attempts_at_cursor, + run_state.cumulative_outcomes, + run_state.funding_snapshot, + run_state.opening_snapshot, + run_state.opening_predicate_results, + run_state.opening_predicate_cursor, + run_state.last_step_outcome, ) } else { if instance.cycle_nonce == u64::MAX { @@ -441,6 +782,7 @@ impl Pallet { weight: base_weight, disposition: AttemptDisposition::Closed(CloseReason::CycleNonceExhausted), fee_collection_failed: false, + effect_execution: TaskEffectExecution::NotInvoked, outcomes: OutcomeTotals::default(), }; } @@ -455,15 +797,12 @@ impl Pallet { ); let opening_predicate_results = Self::capture_opening_predicate_results(&actor, contract_steps, reserved_fee_remaining); - let Some(cycle_nonce) = ActorIdentities::::mutate(actor_id, |maybe| { - let identity = maybe.as_mut()?; - identity.cycle_nonce = identity.cycle_nonce.checked_add(1)?; - Some(identity.cycle_nonce) - }) else { + let Some(cycle_nonce) = instance.cycle_nonce.checked_add(1) else { return AttemptExecution { weight: base_weight, disposition: AttemptDisposition::Failed, fee_collection_failed: false, + effect_execution: TaskEffectExecution::NotInvoked, outcomes: OutcomeTotals::default(), }; }; @@ -486,6 +825,8 @@ impl Pallet { funding_snapshot, opening_snapshot, opening_predicate_results, + 0, + None, ) }; let is_user = instance.actor_class.actor_type() == ActorType::User; @@ -498,18 +839,24 @@ impl Pallet { let mut failed_steps = cumulative_outcomes.failed_steps; let mut attempt_executed_steps: u32 = 0; let mut contract_steps_failed = false; + let mut step_completed_cycle = false; let mut failure_close_reason = None; let mut suspended_at: Option<(u32, SuspensionReason)> = None; - if !is_continuation { + let mut effect_execution = TaskEffectExecution::NotInvoked; + if !has_persisted_run { Self::deposit_event(Event::CycleStarted { actor_id, cycle_nonce, }); } - let mut opening_predicate_index = - Self::opening_predicate_count_before(contract_steps, start_cursor as usize); - for step_idx in start_cursor as usize..contract_steps.len() { - let step = &contract_steps[step_idx]; + let opening_predicate_start = opening_predicate_cursor as usize; + let mut opening_predicate_index = opening_predicate_start; + for step_idx in start_cursor as usize..contract_steps.len().min(start_cursor as usize + 1) { + let step = if let Some(plan) = step_plan.as_ref() { + &plan.loaded_step.step + } else { + &contract_steps[step_idx] + }; let step_num = step_idx as u32; let step_fee = &fee_envelope.steps[step_idx - start_cursor as usize]; // Fee settlement can fail at six points in this step walk, and every one resolves the step @@ -524,7 +871,12 @@ impl Pallet { .expect("semantic counter bound is precluded by admission"); let failure = TaskFailure::permanent(step_error); let outcome = StepOutcome::Failed(failure.clone()); - Self::record_step_outcome(&mut trace, step_num, outcome.clone()); + Self::record_step_outcome( + &mut trace, + &mut last_step_outcome, + step_num, + outcome.clone(), + ); Self::deposit_event(Event::StepFailed { actor_id, cycle_nonce, @@ -549,7 +901,7 @@ impl Pallet { ) { Ok(true) => {} Ok(false) => { - if is_user { + if is_user && collect_fee_in_execution { let charged_fee = Self::settle_user_step_fee( &mut reserved_fee_remaining, step_fee, @@ -563,6 +915,7 @@ impl Pallet { .expect("semantic counter bound is precluded by admission"); Self::record_step_outcome( &mut trace, + &mut last_step_outcome, step_num, StepOutcome::Skipped(StepSkippedReason::PreconditionFalse), ); @@ -575,7 +928,7 @@ impl Pallet { continue; } Err(error) => { - let charged_error = if is_user { + let charged_error = if is_user && collect_fee_in_execution { let charged_fee = Self::settle_user_step_fee( &mut reserved_fee_remaining, step_fee, @@ -598,7 +951,7 @@ impl Pallet { ) { Ok(PreparedTaskOutcome::Executable(task)) => task, Ok(PreparedTaskOutcome::Skipped) => { - if is_user { + if is_user && collect_fee_in_execution { let charged_fee = Self::settle_user_step_fee( &mut reserved_fee_remaining, step_fee, @@ -612,6 +965,7 @@ impl Pallet { .expect("semantic counter bound is precluded by admission"); Self::record_step_outcome( &mut trace, + &mut last_step_outcome, step_num, StepOutcome::Skipped(StepSkippedReason::ResolutionSkipped), ); @@ -624,7 +978,7 @@ impl Pallet { continue; } Ok(PreparedTaskOutcome::FundingUnavailable) => { - if is_user { + if is_user && collect_fee_in_execution { let charged_fee = Self::settle_user_step_fee( &mut reserved_fee_remaining, step_fee, @@ -635,7 +989,12 @@ impl Pallet { } } let outcome = StepOutcome::FundingUnavailable; - Self::record_step_outcome(&mut trace, step_num, outcome.clone()); + Self::record_step_outcome( + &mut trace, + &mut last_step_outcome, + step_num, + outcome.clone(), + ); match resolve_step_control(&outcome, step.on_error) { StepControl::Advance => {} StepControl::CompleteCycle => { @@ -661,7 +1020,7 @@ impl Pallet { continue; } Err(error) => { - let charged_error = if is_user { + let charged_error = if is_user && collect_fee_in_execution { let charged_fee = Self::settle_user_step_fee( &mut reserved_fee_remaining, step_fee, @@ -674,7 +1033,7 @@ impl Pallet { fail_step_with_fee_error!(charged_error); } }; - if is_user { + if is_user && collect_fee_in_execution { let charged_fee = Self::settle_user_step_fee( &mut reserved_fee_remaining, step_fee, @@ -684,6 +1043,7 @@ impl Pallet { fail_step_with_fee_error!(error); } } + effect_execution = TaskEffectExecution::Invoked; if let Err(failure) = Self::execute_prepared_task( prepared_task, actor_id, @@ -697,7 +1057,7 @@ impl Pallet { let retry = failure.retry; let outcome = StepOutcome::Failed(failure.clone()); let control = resolve_step_control(&outcome, step.on_error); - Self::record_step_outcome(&mut trace, step_num, outcome); + Self::record_step_outcome(&mut trace, &mut last_step_outcome, step_num, outcome); Self::deposit_event(Event::StepFailed { actor_id, cycle_nonce, @@ -731,7 +1091,7 @@ impl Pallet { } else { debug_assert_eq!(successful_control, StepControl::Advance); } - Self::record_step_outcome(&mut trace, step_num, outcome); + Self::record_step_outcome(&mut trace, &mut last_step_outcome, step_num, outcome); executed_steps = checked_semantic_increment(executed_steps) .expect("semantic counter bound is precluded by admission"); attempt_executed_steps = checked_semantic_increment(attempt_executed_steps) @@ -741,6 +1101,7 @@ impl Pallet { .expect("semantic counter bound is precluded by admission"); } if successful_control == StepControl::CompleteCycle { + step_completed_cycle = true; break; } } @@ -749,6 +1110,93 @@ impl Pallet { } else { T::WeightInfo::step_orchestration(attempt_executed_steps) }; + if suspended_at.is_none() + && !contract_steps_failed + && !step_completed_cycle + && (start_cursor as usize).saturating_add(1) < contract_steps.len() + { + let Some(next_cursor) = start_cursor.checked_add(1) else { + return AttemptExecution { + weight: attempt_weight, + disposition: AttemptDisposition::Failed, + fee_collection_failed, + effect_execution, + outcomes: OutcomeTotals { + executed_steps, + committed_effectful_tasks, + precondition_skips, + skipped_resolution, + skipped_funding_unavailable, + failed_steps, + }, + }; + }; + let Some(eligible_at) = now.checked_add(&One::one()) else { + return AttemptExecution { + weight: attempt_weight, + disposition: AttemptDisposition::Failed, + fee_collection_failed, + effect_execution, + outcomes: OutcomeTotals { + executed_steps, + committed_effectful_tasks, + precondition_skips, + skipped_resolution, + skipped_funding_unavailable, + failed_steps, + }, + }; + }; + let cumulative_outcomes = OutcomeTotals { + executed_steps, + committed_effectful_tasks, + precondition_skips, + skipped_resolution, + skipped_funding_unavailable, + failed_steps, + }; + ActorHot::::mutate(actor_id, |maybe| { + if let Some(hot) = maybe { + hot.unsuccessful_attempt_streak = 0; + } + }); + if Self::persist_loaded_run_state( + actor_id, + ActorRunState { + contract_authority, + cycle_nonce, + cursor: next_cursor, + opening_predicate_cursor: opening_predicate_index as u32, + unsuccessful_attempts_at_cursor: 0, + last_attempt_block: now, + last_committed_step_block: Some(now), + eligible_at, + opening_snapshot: opening_snapshot.clone(), + opening_predicate_results: opening_predicate_results.clone(), + funding_snapshot: funding_snapshot.clone(), + cumulative_outcomes, + last_step_outcome: last_step_outcome.clone(), + suspension: None, + }, + ) + .is_err() + { + return AttemptExecution { + weight: attempt_weight, + disposition: AttemptDisposition::Failed, + fee_collection_failed, + effect_execution, + outcomes: cumulative_outcomes, + }; + } + return AttemptExecution { + weight: attempt_weight, + disposition: AttemptDisposition::Continued, + fee_collection_failed, + effect_execution, + outcomes: cumulative_outcomes, + }; + } let mut failure_already_recorded = false; if let Some((cursor, suspension_reason)) = suspended_at { let unsuccessful_attempt_streak = ActorHot::::mutate(actor_id, |maybe| { @@ -763,7 +1211,7 @@ impl Pallet { hot.unsuccessful_attempt_streak }); failure_already_recorded = true; - let unsuccessful_attempts_at_cursor = if is_continuation && cursor == start_cursor { + let unsuccessful_attempts_at_cursor = if has_persisted_run && cursor == start_cursor { prior_unsuccessful_attempts_at_cursor .checked_add(1) .expect("admitted cursor-local retry counter remains below its u32 bound") @@ -777,66 +1225,80 @@ impl Pallet { let local_limit_reached = unsuccessful_attempts_at_cursor >= max_attempts; let global_limit_reached = Self::failure_limit_reached(unsuccessful_attempt_streak); if !local_limit_reached && !global_limit_reached { - let cumulative_outcomes = OutcomeTotals { - executed_steps, - committed_effectful_tasks, - precondition_skips, - skipped_resolution, - skipped_funding_unavailable, - failed_steps, - }; - Self::persist_continuation_suspension( - actor_id, - cycle_nonce, - ContinuationState { - cursor, - unsuccessful_attempts_at_cursor, - last_attempt_block: now, - opening_snapshot: Self::trim_opening_snapshot( - contract_steps, - cursor as usize, - &opening_snapshot, - ), - opening_predicate_results: opening_predicate_results.clone(), - funding_snapshot: funding_snapshot.clone(), - cumulative_outcomes, - }, - suspension_reason, - ) - .expect("admitted mutable RetryLater plan has a valid unresolved cursor"); - return AttemptExecution { - weight: attempt_weight, - disposition: AttemptDisposition::Suspended, - fee_collection_failed, - outcomes: cumulative_outcomes, - }; - } - failure_close_reason = Some(if local_limit_reached { - CloseReason::RetryAttemptsExhausted + if let Ok(eligible_at) = Self::suspension_eligible_at( + instance.cooldown_blocks, + instance.window, + now, + unsuccessful_attempts_at_cursor, + ) { + let cumulative_outcomes = OutcomeTotals { + executed_steps, + committed_effectful_tasks, + precondition_skips, + skipped_resolution, + skipped_funding_unavailable, + failed_steps, + }; + Self::persist_loaded_run_suspension( + actor_id, + ActorRunState { + contract_authority, + cycle_nonce, + cursor, + opening_predicate_cursor: opening_predicate_start as u32, + unsuccessful_attempts_at_cursor, + last_attempt_block: now, + last_committed_step_block: None, + eligible_at, + opening_snapshot: opening_snapshot.clone(), + opening_predicate_results: opening_predicate_results.clone(), + funding_snapshot: funding_snapshot.clone(), + cumulative_outcomes, + last_step_outcome: last_step_outcome.clone(), + suspension: Some(suspension_reason), + }, + ) + .expect("admitted mutable RetryLater plan has a valid unresolved cursor"); // deos-bypass: panic-owner — run persistence is prevalidated by admission and canonical loaded state. + return AttemptExecution { + weight: attempt_weight, + disposition: AttemptDisposition::Suspended, + fee_collection_failed, + effect_execution, + outcomes: cumulative_outcomes, + }; + } + failure_close_reason = Some(CloseReason::SchedulerIndexExhausted); } else { - CloseReason::ConsecutiveFailures - }); + failure_close_reason = Some(if local_limit_reached { + CloseReason::RetryAttemptsExhausted + } else { + CloseReason::ConsecutiveFailures + }); + } contract_steps_failed = true; } - if is_continuation { - Self::write_continuation_state(actor_id, None) - .expect("terminal Continuation can be cleared atomically"); + if has_persisted_run { + Self::write_run_state(actor_id, None).expect("terminal Actor run can be cleared atomically"); // deos-bypass: panic-owner — admitted terminal run clear is mechanically prevalidated. + } else { + Self::commit_cycle_nonce(actor_id, cycle_nonce) + .expect("fresh terminal cycle nonce was checked before Opening"); // deos-bypass: panic-owner — fresh Opening derives exactly identity nonce plus one. } - ActorHot::::mutate(actor_id, |maybe| { + let post_failure_streak = ActorHot::::mutate(actor_id, |maybe| { let Some(hot) = maybe.as_mut() else { - return; + return 0; }; - if contract_steps_failed && failure_already_recorded { - return; + hot.last_cycle_block = Some(now); + if !(contract_steps_failed && failure_already_recorded) { + let transition = if contract_steps_failed { + FailureStreakTransition::UnsuccessfulAttempt + } else { + FailureStreakTransition::Reset + }; + hot.unsuccessful_attempt_streak = + transition_failure_streak(hot.unsuccessful_attempt_streak, transition) + .expect("failure bound (MaxConsecutiveFailures) is precluded by admission"); // deos-bypass: panic-owner — attempt classification excludes a terminal failure streak. } - let transition = if contract_steps_failed { - FailureStreakTransition::UnsuccessfulAttempt - } else { - FailureStreakTransition::Reset - }; - hot.unsuccessful_attempt_streak = - transition_failure_streak(hot.unsuccessful_attempt_streak, transition) - .expect("failure bound (MaxConsecutiveFailures) is precluded by admission"); + hot.unsuccessful_attempt_streak }); let outcomes = OutcomeTotals { executed_steps, @@ -856,35 +1318,28 @@ impl Pallet { }, outcomes, }); + let close_reason = if contract_steps_failed { + failure_close_reason.or_else(|| { + Self::failure_limit_reached(post_failure_streak).then_some(CloseReason::ConsecutiveFailures) + }) + } else if instance.completion == CompletionPolicy::CloseAfterProductiveCycle + && committed_effectful_tasks > 0 + { + Some(CloseReason::ProductiveCycleCompleted) + } else { + instance + .auto_close_at_cycle_nonce + .filter(|target_nonce| cycle_nonce >= *target_nonce) + .map(|_| CloseReason::AutoCloseNonceReached) + }; let mut terminal_close_reason = None; - if contract_steps_failed { - if let Some(inst) = Self::active_actor_view(actor_id) { - let close_reason = failure_close_reason.or_else(|| { - Self::failure_limit_reached(inst.unsuccessful_attempt_streak) - .then_some(CloseReason::ConsecutiveFailures) - }); - if !inst.lifecycle.is_paused() { - if let Some(reason) = close_reason { - Self::finalize_actor(actor_id, &inst, reason) - .expect("fresh execution snapshot satisfies terminal preconditions"); - terminal_close_reason = Some(reason); - } - } - } - } else if let Some(inst) = Self::active_actor_view(actor_id) { - if inst.completion == CompletionPolicy::CloseAfterProductiveCycle - && committed_effectful_tasks > 0 - { - Self::finalize_actor(actor_id, &inst, CloseReason::ProductiveCycleCompleted) - .expect("fresh productive execution snapshot satisfies terminal preconditions"); - terminal_close_reason = Some(CloseReason::ProductiveCycleCompleted); - } else if let Some(target_nonce) = inst.auto_close_at_cycle_nonce { - if cycle_nonce >= target_nonce { - Self::finalize_actor(actor_id, &inst, CloseReason::AutoCloseNonceReached) - .expect("fresh execution snapshot satisfies terminal preconditions"); - terminal_close_reason = Some(CloseReason::AutoCloseNonceReached); - } - } + if let Some(reason) = close_reason + && (!contract_steps_failed || !instance.lifecycle.is_paused()) + && let Some(inst) = Self::active_actor_view(actor_id) + { + Self::finalize_actor_loaded(actor_id, &inst, reason) + .expect("fresh execution snapshot satisfies terminal preconditions"); // deos-bypass: panic-owner — active_actor_view supplies exact current close authority. + terminal_close_reason = Some(reason); } let disposition = if let Some(reason) = terminal_close_reason { AttemptDisposition::Closed(reason) @@ -897,6 +1352,7 @@ impl Pallet { weight: attempt_weight, disposition, fee_collection_failed, + effect_execution, outcomes, } } @@ -924,7 +1380,7 @@ impl Pallet { )); } }; - let continuation = state.continuation; + let run_state = state.run_state; let instance = Self::derive_active_actor_view(state.identity, state.hot, state.contract); if instance.actor_class.actor_type() != expected_type { return Err(SimulationError::TypeMismatch); @@ -932,45 +1388,64 @@ impl Pallet { if instance.mutability != expected_mutability { return Err(SimulationError::MutabilityMismatch); } - if ActorContracts::::get(actor_id) != Some(expected_contract) { + if Self::load_actor_contract(actor_id) != Some(expected_contract) { return Err(SimulationError::ContractMismatch); } match mode { SimulationMode::FreshCurrentPlan if instance.cycle_state != CycleState::Idle => { return Err(SimulationError::ModeCycleStateMismatch); } - SimulationMode::CurrentContinuation if instance.cycle_state != CycleState::Suspended => { + SimulationMode::CurrentRun if instance.cycle_state != CycleState::Suspended => { return Err(SimulationError::ModeCycleStateMismatch); } _ => {} } - let classification = Self::classify_actor_loaded(&instance, continuation.as_ref()) + let classification = Self::classify_actor_loaded(&instance, run_state.as_ref()) .map_err(SimulationError::Classification)?; if classification.execution_phase == ActorExecutionPhase::GlobalCircuitBreaker { return Err(SimulationError::GlobalCircuitBreaker); } if let Some(reason) = classification.terminal_reason { - let (start_cursor, cumulative_outcomes) = match mode { - SimulationMode::FreshCurrentPlan => (0, OutcomeTotals::default()), - SimulationMode::CurrentContinuation => { - let continuation = continuation - .as_ref() - .ok_or(SimulationError::Classification( - ActorClassificationError::ContinuationInvariant, - ))?; - (continuation.cursor, continuation.cumulative_outcomes) + let (cycle_nonce, start_cursor, cumulative_outcomes) = match mode { + SimulationMode::FreshCurrentPlan => (instance.cycle_nonce, 0, OutcomeTotals::default()), + SimulationMode::CurrentRun => { + let run_state = run_state.as_ref().ok_or(SimulationError::Classification( + ActorClassificationError::RunInvariant, + ))?; + ( + run_state.cycle_nonce, + run_state.cursor, + run_state.cumulative_outcomes, + ) } }; return Ok(SimulationResult { status: AttemptDisposition::Closed(reason), - cycle_nonce: instance.cycle_nonce, + cycle_nonce, start_cursor, - continuation_cursor: None, + run_cursor: None, unsuccessful_attempts_at_cursor: None, cumulative_outcomes, steps: BoundedVec::default(), }); } + if mode == SimulationMode::FreshCurrentPlan + && instance.actor_class.actor_type() == ActorType::User + && !Self::pipeline_capacity_sufficient(actor_id, ActorType::User, &instance.sovereign_account) + .map_err(|_| { + SimulationError::Classification(ActorClassificationError::ComputationOverflow) + })? + { + return Ok(SimulationResult { + status: AttemptDisposition::Closed(CloseReason::CycleAdmissionInsufficient), + cycle_nonce: instance.cycle_nonce, + start_cursor: 0, + run_cursor: None, + unsuccessful_attempts_at_cursor: None, + cumulative_outcomes: OutcomeTotals::default(), + steps: BoundedVec::default(), + }); + } let (cycle_nonce, start_cursor) = match mode { SimulationMode::FreshCurrentPlan => ( instance @@ -981,13 +1456,11 @@ impl Pallet { ))?, 0, ), - SimulationMode::CurrentContinuation => { - let continuation = continuation - .as_ref() - .ok_or(SimulationError::Classification( - ActorClassificationError::ContinuationInvariant, - ))?; - (instance.cycle_nonce, continuation.cursor) + SimulationMode::CurrentRun => { + let run_state = run_state.as_ref().ok_or(SimulationError::Classification( + ActorClassificationError::RunInvariant, + ))?; + (run_state.cycle_nonce, run_state.cursor) } }; match classification.execution_phase { @@ -1002,31 +1475,79 @@ impl Pallet { let now = frame_system::Pallet::::block_number(); polkadot_sdk::frame_support::storage::transactional::with_transaction_opaque_err(|| { let mut trace = alloc::vec::Vec::new(); - let attempt = Self::execute_single_cycle_traced(actor_id, instance, now, Some(&mut trace)); + let mut simulation_now = now; + let mut simulation_instance = instance; + if mode == SimulationMode::FreshCurrentPlan + && simulation_instance.actor_class.actor_type() == ActorType::User + { + if Self::collect_pipeline_fee( + actor_id, + ActorType::User, + &simulation_instance.sovereign_account, + ) + .is_err() + { + return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err( + SimulationError::FeeCollectionFailed, + )); + } + } + let mut attempt = Self::execute_single_cycle_traced( + actor_id, + simulation_instance, + simulation_now, + None, + Some(&mut trace), + ); + for _ in 1..T::MaxContractSteps::get() { + if attempt.disposition != AttemptDisposition::Continued { + break; + } + let Some(next_block) = simulation_now.checked_add(&One::one()) else { + return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err( + SimulationError::Classification(ActorClassificationError::ComputationOverflow), + )); + }; + simulation_now = next_block; + let LoadedActorStateOf::Active(state) = Self::load_actor_state(actor_id) else { + return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err( + SimulationError::Classification(ActorClassificationError::RunInvariant), + )); + }; + simulation_instance = + Self::derive_active_actor_view(state.identity, state.hot, state.contract); + attempt = Self::execute_single_cycle_traced( + actor_id, + simulation_instance, + simulation_now, + None, + Some(&mut trace), + ); + } if attempt.fee_collection_failed { return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err( SimulationError::FeeCollectionFailed, )); } - let continuation = ContinuationStateStore::::get(actor_id); - let continuation_cursor = continuation.as_ref().map(|state| state.cursor); - let unsuccessful_attempts_at_cursor = continuation + let run_state = ActorRunStateStore::::get(actor_id); + let run_cursor = run_state.as_ref().map(|state| state.cursor); + let unsuccessful_attempts_at_cursor = run_state .as_ref() .map(|state| state.unsuccessful_attempts_at_cursor); - if let Some(state) = continuation.as_ref() { + if let Some(state) = run_state.as_ref() { debug_assert_eq!(state.cumulative_outcomes, attempt.outcomes); } debug_assert!(trace.len() <= T::MaxContractSteps::get() as usize); let Ok(steps) = BoundedVec::try_from(trace) else { return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err( - SimulationError::Classification(ActorClassificationError::ContinuationInvariant), + SimulationError::Classification(ActorClassificationError::RunInvariant), )); }; polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Ok(SimulationResult { status: attempt.disposition, cycle_nonce, start_cursor, - continuation_cursor, + run_cursor, unsuccessful_attempts_at_cursor, cumulative_outcomes: attempt.outcomes, steps, @@ -1055,18 +1576,6 @@ impl Pallet { weight } - pub(crate) fn compute_eval_fee_checked(evaluation_units: u32) -> Result, Error> { - let evaluation_weight = Self::predicate_evaluation_weight(evaluation_units) - .saturating_add(T::WeightInfo::fee_collection()); - Ok(T::WeightToFee::weight_to_fee(&evaluation_weight)) - } - - #[cfg(test)] - pub(crate) fn compute_eval_fee(num_conditions: u32) -> BalanceOf { - Self::compute_eval_fee_checked(num_conditions) - .expect("admitted execution plans have checked evaluation fees") - } - fn settle_user_step_fee( reservation: &mut T::Balance, step: &super::StepFeeEnvelope, @@ -1078,7 +1587,7 @@ impl Pallet { settlement.charged } - fn collect_user_step_fee(actor: &T::AccountId, fee: T::Balance) -> DispatchResult { + pub(crate) fn collect_user_step_fee(actor: &T::AccountId, fee: T::Balance) -> DispatchResult { if fee.is_zero() { return Ok(()); } @@ -1190,19 +1699,7 @@ impl Pallet { surfaces } - fn opening_predicate_count_before(contract_steps: &ContractSteps, end_step: usize) -> usize { - contract_steps - .iter() - .take(end_step) - .map(|step| { - step.precondition.as_ref().map_or(0, |precondition| { - precondition.opening_predicate_count() as usize - }) - }) - .sum() - } - - fn capture_opening_predicate_results( + pub(crate) fn capture_opening_predicate_results( actor: &T::AccountId, contract_steps: &ContractSteps, reserved: T::Balance, @@ -1226,13 +1723,13 @@ impl Pallet { results } - fn capture_opening_snapshot( + pub(crate) fn capture_opening_snapshot( actor_type: ActorType, actor: &T::AccountId, contract_steps: &ContractSteps, reserved: T::Balance, - ) -> ContinuationSnapshotOf { - let mut snapshot = ContinuationSnapshotOf::::default(); + ) -> RunOpeningSnapshotOf { + let mut snapshot = RunOpeningSnapshotOf::::default(); for surface in Self::opening_surfaces(contract_steps, 0) { let balance = match surface { OpeningSurface::PreservableAsset(asset) => { @@ -1248,24 +1745,8 @@ impl Pallet { snapshot } - fn trim_opening_snapshot( - contract_steps: &ContractSteps, - start_cursor: usize, - source: &ContinuationSnapshotOf, - ) -> ContinuationSnapshotOf { - let mut snapshot = ContinuationSnapshotOf::::default(); - for surface in Self::opening_surfaces(contract_steps, start_cursor) { - if let Some(balance) = source.get(&surface) { - snapshot - .try_insert(surface, *balance) - .unwrap_or_else(|_| panic!("suffix surfaces fit MaxOpeningSnapshotEntries")); - } - } - snapshot - } - fn opening_balance( - opening_snapshot: &ContinuationSnapshotOf, + opening_snapshot: &RunOpeningSnapshotOf, surface: OpeningSurface, ) -> Result { opening_snapshot @@ -1279,7 +1760,7 @@ impl Pallet { actor: &T::AccountId, actor_type: ActorType, reserved: T::Balance, - trigger_balances: &ContinuationSnapshotOf, + trigger_balances: &RunOpeningSnapshotOf, funding_snapshots: &FundingSnapshotOf, ) -> Result, DispatchError> { match task { @@ -1887,7 +2368,7 @@ impl Pallet { actor: &T::AccountId, actor_type: ActorType, reserved: T::Balance, - trigger_balances: &ContinuationSnapshotOf, + trigger_balances: &RunOpeningSnapshotOf, funding_snapshots: &FundingSnapshotOf, policy: AmountResolutionPolicy, ) -> Result, DispatchError> { @@ -2059,7 +2540,7 @@ impl Pallet { spec: &AmountResolution, position_asset: T::AssetId, who: &T::AccountId, - trigger_share_balances: &ContinuationSnapshotOf, + trigger_share_balances: &RunOpeningSnapshotOf, funding_snapshots: &FundingSnapshotOf, ) -> Result, DispatchError> { let current_shares = T::StakingOps::share_balance(who, position_asset); @@ -2098,7 +2579,7 @@ impl Pallet { who: &T::AccountId, actor_type: ActorType, reserved: T::Balance, - trigger_balances: &ContinuationSnapshotOf, + trigger_balances: &RunOpeningSnapshotOf, funding_snapshots: &FundingSnapshotOf, policy: AmountResolutionPolicy, ) -> Result, DispatchError> { @@ -2294,7 +2775,7 @@ mod step_control_tests { before_conditions, ); - let opening_snapshot = ContinuationSnapshotOf::::default(); + let opening_snapshot = RunOpeningSnapshotOf::::default(); let funding_snapshots = FundingSnapshotOf::::default(); let before_resolution = polkadot_sdk::sp_io::storage::root(StateVersion::V1); assert_eq!( diff --git a/template/pallets/actors/src/lib.rs b/template/pallets/actors/src/lib.rs index 67b51e5d..d27dda67 100644 --- a/template/pallets/actors/src/lib.rs +++ b/template/pallets/actors/src/lib.rs @@ -10,6 +10,34 @@ use polkadot_sdk::{ pub use pallet::*; +pub trait ActorPrepassContext { + fn context_ready() -> bool; +} + +impl ActorPrepassContext for () { + fn context_ready() -> bool { + true + } +} + +pub const ACTOR_PREPASS_INHERENT_VERSION: u8 = 1; + +#[derive(codec::Decode, codec::Encode)] +pub struct ActorPrepassInherentData { + version: u8, +} + +pub fn provide_actor_prepass_inherent_data( + data: &mut polkadot_sdk::sp_inherents::InherentData, +) -> Result<(), polkadot_sdk::sp_inherents::Error> { + data.put_data( + ACTOR_PREPASS_INHERENT_IDENTIFIER, + &ActorPrepassInherentData { + version: ACTOR_PREPASS_INHERENT_VERSION, + }, + ) +} + pub mod contract; pub mod types; @@ -24,12 +52,20 @@ pub use scheduler::EnqueueOutcome; pub mod adapters; pub use adapters::{ - AddressEventIngress, AssetOps, CanonicalObservationState, DexOps, DexSwapOutcome, - ExecutionContext, FundingAuthority, IngressFailure, LiquidityOps, ObservationProvider, - ObservationTransition, ObservationTransitionIngress, RetryClass, ScalarObservationState, - SovereignAccountDeriver, StakingOps, SystemActorContractValidator, TaskFailure, + AddressEventIngress, AdmissionCertificateAuthority, AdmissionCertificateAuthorityProvider, + AssetOps, CanonicalObservationState, DexOps, DexSwapOutcome, ExecutionContext, FundingAuthority, + IngressFailure, LiquidityOps, ObservationProvider, ObservationTransition, + ObservationTransitionIngress, RetryClass, ScalarObservationState, SovereignAccountDeriver, + StakingOps, StepControlExecution, StepControlOutcome, StepControlPhase, StepControlPlacement, + StepControlWeightContext, StepControlWeightProvider, SystemActorContractValidator, + TaskEffectExecution, TaskEffectWeightProvider, TaskFailure, +}; +pub use types::{ + ActorStepResourceReservation, AddressEvent, BlockResourceBudget, BlockResourceDomain, + BlockResourceLimits, BlockResourcePhase, BlockResourceReservation, BlockResourceState, + CrossingCapacity, FinalizedBlockResourceSnapshot, FixedBlockWeightComponents, InputLimit, + MAX_STEPS_PER_TAIL_CHUNK, MaterializationFaults, Task, WakeupBucketState, WakeupCursorIndex, }; -pub use types::{AddressEvent, InputLimit, Task, WakeupBucketState, WakeupCursorIndex}; pub mod weights; pub use weights::WeightInfo; @@ -86,36 +122,18 @@ pub trait BenchmarkHelper { source: &AccountId, amount: Balance, ) -> polkadot_sdk::sp_runtime::DispatchResult; -} - -pub trait TriggerStateBond { - fn amount(trigger: &Trigger) -> Balance; - fn maximum() -> Balance; - fn set_bond( - owner: &AccountId, - current: Balance, - target: Balance, - ) -> polkadot_sdk::frame_support::dispatch::DispatchResult; -} - -impl TriggerStateBond - for () -{ - fn amount(_trigger: &Trigger) -> Balance { - Zero::zero() - } - - fn maximum() -> Balance { - Zero::zero() - } - - fn set_bond( - _owner: &AccountId, - _current: Balance, - _target: Balance, - ) -> polkadot_sdk::frame_support::dispatch::DispatchResult { - Ok(()) - } + type MaximumContextInherent; + fn prepare_maximum_context_inherent() -> Self::MaximumContextInherent; + fn execute_maximum_context_inherent( + inherent: Self::MaximumContextInherent, + ) -> polkadot_sdk::sp_runtime::DispatchResult; + fn verify_maximum_context_inherent(); + fn prepare_maximum_xcm_version_discovery(); + fn execute_maximum_xcm_version_discovery(); + fn verify_maximum_xcm_version_discovery(); + fn prepare_block_resource_meter_extension(); + fn execute_block_resource_meter_extension(); + fn verify_block_resource_meter_extension(); } pub trait FeeCollector { @@ -140,6 +158,26 @@ pub struct StepFeeEnvelope { pub total: Balance, } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct TriggerFeeBreakdown { + pub trigger_family: types::TriggerFamily, + pub trigger_fee: Balance, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct PipelineFeeBreakdown { + pub pipeline_machine_fee: Balance, + pub cleanup_fee: Balance, + pub total_fee: Balance, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct StepFeeBreakdown { + pub control_fee: Balance, + pub effect_fee: Balance, + pub total_fee: Balance, +} + #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum FeeChargeKind { EvaluationOnly, @@ -275,60 +313,109 @@ sp_api::decl_runtime_apis! { ) -> Result; } + /// Read-only named Actors cost projection with independent fee and hold provenance. + pub trait ActorCostApi + where + Balance: codec::Codec, + { + fn actor_cost_quote( + actor_id: types::ActorId, + ) -> Result, types::ActorCostQuoteError>; + } + + /// Bounded current and finalized block-resource projection. + pub trait ActorResourceApi + where + BlockNumber: codec::Codec, + { + fn block_resource_budget() -> types::BlockResourceBudget; + + fn current_block_resource_state() -> Option>; + + fn finalized_block_resource_snapshot( + ) -> Option>; + } + /// Read-only eligibility projection for one actor (spec 7.3). /// /// Returns absence/dormancy or the canonical Active classification, reusing /// the same pure owners as /// admission so clients do not reimplement cadence phase, cooldown, window /// floor, retry backoff, breaker, or latch arithmetic. - #[api_version(4)] - pub trait ActorEligibilityApi + #[api_version(6)] + pub trait ActorEligibilityApi where FeedId: codec::Codec, BlockNumber: codec::Codec, - Trigger: codec::Codec, - Balance: codec::Codec, { fn actor_eligibility( actor_id: types::ActorId, ) -> Result, types::ActorClassificationError>; - fn materialization_faults() -> ( - Option>, - Option>, - Option>, - ); + fn materialization_faults() -> types::MaterializationFaults; - fn crossing_capacity(feed: FeedId) -> (u32, u32, u32, u32); + fn crossing_capacity(feed: FeedId) -> types::CrossingCapacity; - fn trigger_state_bond(trigger: Trigger) -> Balance; } } #[frame::pallet] pub mod pallet { use super::{ - AssetOps, AttemptFeeEnvelope, DexOps, FeeCollector, FeeEnvelopeError, FeeEnvelopeInput, - FundingAuthority, LiquidityOps, ObservationProvider, TriggerStateBond, WeightInfo, - compose_attempt_fee_envelope, contract_steps_bound_is_valid, + ACTOR_PREPASS_INHERENT_VERSION, ActorPrepassContext, ActorPrepassInherentData, + AdmissionCertificateAuthorityProvider, AssetOps, AttemptFeeEnvelope, DexOps, FeeCollector, + FeeEnvelopeError, FeeEnvelopeInput, FundingAuthority, LiquidityOps, ObservationProvider, + PipelineFeeBreakdown, StepControlWeightContext, StepControlWeightProvider, StepFeeBreakdown, + TaskEffectWeightProvider, TriggerFeeBreakdown, WeightInfo, compose_attempt_fee_envelope, + contract_steps_bound_is_valid, }; use crate::adapters::{ RetryClass, SovereignAccountDeriver as _, SovereignAccountPolicy, StakingOps as _, SystemActorContractValidator as _, }; + use alloc::{collections::BTreeSet, vec::Vec}; use frame::prelude::*; use polkadot_sdk::{ frame_support::{ PalletId, - traits::{EnsureOrigin, Time}, + traits::{ + EnsureOrigin, Time, + fungible::{InspectHold, MutateHold}, + tokens::Precision, + }, + }, + sp_inherents::{InherentData, InherentIdentifier, IsFatalError}, + sp_runtime::traits::{ + CheckedAdd, CheckedMul, CheckedSub, One, SaturatedConversion, Saturating, Zero, }, - sp_runtime::traits::{CheckedAdd, One, SaturatedConversion, Saturating, Zero}, sp_weights::{WeightMeter, WeightToFee as _}, }; + pub const ACTOR_PREPASS_INHERENT_IDENTIFIER: InherentIdentifier = *b"deosact0"; + + #[derive(codec::Encode, Debug)] + pub enum ActorPrepassInherentError { + MissingData, + MissingCall, + UnsupportedVersion, + } + + impl IsFatalError for ActorPrepassInherentError { + fn is_fatal_error(&self) -> bool { + true + } + } + use super::types::Task as ActorTask; pub use super::types::*; + #[pallet::composite_enum] + pub enum HoldReason { + /// Refundable backing for retained User Actor process state. + #[codec(index = 0)] + ActorState, + } + #[pallet::config] pub trait Config: frame_system::Config { type AssetId: Parameter + Member + Copy + MaybeSerializeDeserialize + MaxEncodedLen + Ord; @@ -345,6 +432,9 @@ pub mod pallet { type FeeNativeAssetId: Get; type AssetOps: AssetOps; + type AdmissionCertificateAuthority: AdmissionCertificateAuthorityProvider; + type StepControlWeight: StepControlWeightProvider>; + type TaskEffectWeight: TaskEffectWeightProvider>; type ObservationFeedId: Parameter + Member + Copy + MaxEncodedLen + Ord; type ObservationProvider: ObservationProvider>; type FundingAuthority: FundingAuthority; @@ -394,6 +484,9 @@ pub mod pallet { /// Physical I/O granularity for observation subscriber pages. #[pallet::constant] type ObservationPageSize: Get; + /// Physical I/O granularity for ObservationCrossing membership pages. + #[pallet::constant] + type CrossingPageSize: Get; #[pallet::constant] type MaxCrossingMembersPerFeed: Get; #[pallet::constant] @@ -436,7 +529,7 @@ pub mod pallet { #[pallet::constant] type MaxExecutionDelayBlocks: Get>; #[pallet::constant] - type MaxCadenceDelayTicks: Get; + type MaxTemporalDelayTicks: Get; #[pallet::constant] type MaxIdleStarvationBlocks: Get; /// Gross two-dimensional `on_idle` weight guaranteed by the embedding runtime. @@ -457,13 +550,20 @@ pub mod pallet { #[pallet::constant] type ActorCreationFee: Get; + type RuntimeHoldReason: Parameter + Member + MaxEncodedLen + Copy + From; + type StateHoldCurrency: InspectHold + + MutateHold; + /// Fixed accounting price for each present retained-state component. + #[pallet::constant] + type ActorStateHoldBase: Get; + /// Linear accounting price for each SCALE-encoded retained byte. + #[pallet::constant] + type ActorStateHoldPerByte: Get; /// Converts weight to fee for execution cost calculation type WeightToFee: polkadot_sdk::sp_weights::WeightToFee; /// Runtime-bound upper weights for every Actors task variant type FeeSink: Get; type FeeCollector: FeeCollector; - type TriggerStateBond: TriggerStateBond, Self::Balance>; - #[pallet::constant] type MaxConsecutiveFailures: Get; #[pallet::constant] @@ -473,6 +573,11 @@ pub mod pallet { type WeightInfo: WeightInfo; + /// Runtime-owned immutable block-resource budget derived from the fixed envelope. + type BlockResourceBudget: Get; + + type PrepassContext: ActorPrepassContext; + /// Provides System Actors specs to initialize at genesis. /// Use `()` for no genesis System Actors (default). type GenesisSystemActors: GenesisSystemActors>; @@ -497,7 +602,7 @@ pub mod pallet { pub type ObservationSubscriberPageOf = ObservationSubscriberPage<::ObservationPageSize>; pub type ObservationFreeSlotPageOf = BoundedVec::ObservationPageSize>; - pub type CrossingMemberPageOf = CrossingMemberPage<::ObservationPageSize>; + pub type CrossingMemberPageOf = CrossingMemberPage<::CrossingPageSize>; pub type CrossingLeafKeyOf = CrossingLeafKey<::ObservationFeedId>; pub type CrossingRadixNodeKeyOf = CrossingRadixNodeKey<::ObservationFeedId>; pub type CrossingMembershipLocatorOf = @@ -576,6 +681,51 @@ pub mod pallet { FundingSourcePolicyOf, >; + pub type ActorContractHeaderOf = super::types::ActorContractHeader< + TriggerOf, + BlockNumberFor, + FundingSourcePolicyOf, + ::Balance, + [u8; 32], + >; + + pub type ActorContractHeadOf = + super::types::ActorContractHead, StepOf>; + + pub type ActorActivationAuthorityOf = super::types::ActorActivationAuthority< + ::ObservationFeedId, + BlockNumberFor, + [u8; 32], + >; + + pub type ActorStepChunkOf = super::types::ActorStepChunk< + ActorId, + [u8; 32], + BoundedVec, ConstU32<{ super::types::MAX_STEPS_PER_TAIL_CHUNK }>>, + BoundedVec>, + >; + + pub type ActorAdmissionResourcesOf = + BoundedVec::MaxContractSteps>; + + pub type ActorAdmissionCertificateOf = ActorAdmissionCertificate>; + + pub type ActorStepTicketOf = + ActorStepTicket, ActorContractCommitment<[u8; 32]>>; + + pub type LoadedActorStepOf = LoadedActorStep>; + + pub type CurrentStepPlanOf = StepExecutionPlan< + ActorIdentityOf, + ActorHotStateOf, + ActorRunStateOf, + ActorFundingStateOf, + ActorAdmissionCertificateOf, + ActorStepTicketOf, + LoadedActorStepOf, + StepFeeBreakdown<::Balance>, + >; + pub type FundingAccumulatedOf = BoundedBTreeMap< ::AssetId, ::Balance, @@ -587,7 +737,7 @@ pub mod pallet { pub type FundingSnapshotOf = FundingAccumulatedOf; - pub type ContinuationSnapshotOf = BoundedBTreeMap< + pub type RunOpeningSnapshotOf = BoundedBTreeMap< OpeningSurface<::AssetId>, ::Balance, ::MaxOpeningSnapshotEntries, @@ -596,7 +746,17 @@ pub mod pallet { pub type OpeningPredicateResultsOf = BoundedVec, ::MaxOpeningPredicateResults>; - pub type ContinuationStateOf = ContinuationState< + pub type ActorRunHeadOf = ActorRunHead>; + + pub type ActorRunPayloadOf = ActorRunPayload< + ::AssetId, + ::Balance, + ::MaxOpeningSnapshotEntries, + ::MaxFundingTrackedAssets, + ::MaxOpeningPredicateResults, + >; + + pub type ActorRunStateOf = ActorRunState< ::AssetId, ::Balance, BlockNumberFor, @@ -605,7 +765,7 @@ pub mod pallet { ::MaxOpeningPredicateResults, >; - pub type QueuePageOf = BoundedVec::QueuePageSize>; + pub type QueuePageOf = BoundedVec>, ::QueuePageSize>; pub type WakeupPageEntriesOf = BoundedVec, ::WakeupPageSize>; pub type WakeupPageOf = WakeupPage>; pub type WakeupCursorPageOf = @@ -621,17 +781,21 @@ pub mod pallet { pub type ActorHotStateOf = ActorHotState>; pub type ActorFundingStateOf = - ActorFundingState, FundingTrackedAssetsOf, ::Balance>; + ActorFundingState, FundingTrackedAssetsOf>; pub type ActorIdentityOf = ActorIdentity<::AccountId, BlockNumberFor>; + pub type ActorStateHoldBreakdownOf = ActorStateHoldBreakdown<::Balance>; + pub type ActorStateHoldRecordOf = + ActorStateHoldRecord<::AccountId, ::Balance>; + pub type ActiveActorStateOf = ActiveActorState< ActorIdentityOf, ActorHotStateOf, ActorContractOf, ActorFundingStateOf, - ContinuationStateOf, + ActorRunStateOf, >; pub(crate) enum LoadedActorStateOf { @@ -641,11 +805,20 @@ pub mod pallet { Corrupt, } + pub struct ObservationActivationState { + pub actor_id: ActorId, + pub identity: ActorIdentityOf, + pub hot: ActorHotStateOf, + pub authority: ActorActivationAuthorityOf, + pub run_head: Option>, + pub loaded_step: Option>, + } + #[pallet::pallet] #[pallet::storage_version(STORAGE_VERSION)] pub struct Pallet(_); - const STORAGE_VERSION: StorageVersion = StorageVersion::new(9); + const STORAGE_VERSION: StorageVersion = StorageVersion::new(15); #[pallet::storage] #[pallet::getter(fn next_actor_id)] @@ -656,24 +829,1215 @@ pub mod pallet { pub type ActorHot = StorageMap<_, Blake2_128Concat, ActorId, ActorHotStateOf, OptionQuery>; - #[pallet::storage] - #[pallet::storage_prefix = "ActorContract"] - #[pallet::getter(fn actor_contract)] - pub type ActorContracts = - StorageMap<_, Blake2_128Concat, ActorId, ActorContractOf, OptionQuery>; + #[pallet::storage] + #[pallet::storage_prefix = "ActorContractHead"] + pub type ActorContractHeads = + StorageMap<_, Blake2_128Concat, ActorId, ActorContractHeadOf, OptionQuery>; + + #[pallet::storage] + #[pallet::storage_prefix = "ActorActivationAuthority"] + pub type ActorActivationAuthorities = + StorageMap<_, Blake2_128Concat, ActorId, ActorActivationAuthorityOf, OptionQuery>; + + #[pallet::storage] + #[pallet::storage_prefix = "ActorAdmissionCertificate"] + pub type ActorAdmissionCertificates = + StorageMap<_, Blake2_128Concat, ActorId, ActorAdmissionCertificateOf, OptionQuery>; + + #[pallet::storage] + #[pallet::storage_prefix = "ActorContractTailChunk"] + pub type ActorContractTailChunks = StorageDoubleMap< + _, + Blake2_128Concat, + ActorId, + Blake2_128Concat, + u32, + ActorStepChunkOf, + OptionQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn actor_funding)] + pub type ActorFunding = + StorageMap<_, Blake2_128Concat, ActorId, ActorFundingStateOf, OptionQuery>; + + #[pallet::storage] + #[pallet::storage_prefix = "ActorRunHead"] + pub type ActorRunHeads = + StorageMap<_, Blake2_128Concat, ActorId, ActorRunHeadOf, OptionQuery>; + + #[pallet::storage] + #[pallet::storage_prefix = "ActorRunPayload"] + pub type ActorRunPayloads = + StorageMap<_, Blake2_128Concat, ActorId, ActorRunPayloadOf, OptionQuery>; + + pub struct ActorRunStateStore(core::marker::PhantomData); + + impl ActorRunStateStore { + pub fn get(actor_id: ActorId) -> Option> { + ActorRunState::from_tiers( + ActorRunHeads::::get(actor_id)?, + ActorRunPayloads::::get(actor_id)?, + ) + } + + pub fn insert(actor_id: ActorId, state: ActorRunStateOf) { + let (head, payload) = state.into_tiers(); + let payload_changed = ActorRunHeads::::get(actor_id) + .is_none_or(|current| current.payload_commitment != head.payload_commitment); + ActorRunHeads::::insert(actor_id, head); + if payload_changed { + ActorRunPayloads::::insert(actor_id, payload); + } + } + + pub fn remove(actor_id: ActorId) { + ActorRunHeads::::remove(actor_id); + ActorRunPayloads::::remove(actor_id); + } + + pub fn take(actor_id: ActorId) -> Option> { + let state = Self::get(actor_id)?; + Self::remove(actor_id); + Some(state) + } + + pub fn contains_key(actor_id: ActorId) -> bool { + ActorRunHeads::::contains_key(actor_id) && ActorRunPayloads::::contains_key(actor_id) + } + + pub fn iter_keys() -> impl Iterator { + ActorRunHeads::::iter_keys() + } + + pub fn mutate( + actor_id: ActorId, + mutate: impl FnOnce(&mut Option>) -> R, + ) -> R { + let mut state = Self::get(actor_id); + let result = mutate(&mut state); + if let Some(state) = state { + Self::insert(actor_id, state); + } else { + Self::remove(actor_id); + } + result + } + } + + impl Pallet { + pub fn actor_run_state(actor_id: ActorId) -> Option> { + ActorRunStateStore::::get(actor_id) + } + pub fn actor_contract(actor_id: ActorId) -> Option> { + Self::load_actor_contract(actor_id) + } + + pub fn actor_cost_quote( + actor_id: ActorId, + ) -> Result, ActorCostQuoteError> { + let (state, admission) = Self::load_actor_state_with_admission(actor_id); + let (identity, active) = match state { + LoadedActorStateOf::NotRegistered => return Err(ActorCostQuoteError::ActorNotFound), + LoadedActorStateOf::Dormant(identity) => (identity, None), + LoadedActorStateOf::Active(state) => (state.identity.clone(), Some(state)), + LoadedActorStateOf::Corrupt => return Err(ActorCostQuoteError::ActorInvariant), + }; + let actor_type = identity.actor_class.actor_type(); + let creation_fee = if actor_type == ActorType::System { + T::Balance::zero() + } else { + T::ActorCreationFee::get() + }; + let effect_identity = T::TaskEffectWeight::production_weight_identity() + .ok_or(ActorCostQuoteError::WeightAuthorityUnavailable)?; + let zero_action = ActorActionFeeQuote { + maximum_effect_weight: Weight::zero(), + maximum_effect_fee: T::Balance::zero(), + production_weight_identity: effect_identity, + }; + let (prospective_trigger_fee, prospective_pipeline_fee, maximum_next_action_fee) = + match active { + None => (None, None, zero_action), + Some(state) => { + let admission = admission.ok_or(ActorCostQuoteError::ActorInvariant)?; + let trigger_family = state.contract.trigger.family(); + let maximum_weight = Self::trigger_occurrence_weight(trigger_family); + let trigger_fee = + Self::trigger_fee_for_weight(actor_type, trigger_family, maximum_weight); + let machine_envelope = ActorContractHeads::::get(actor_id) + .ok_or(ActorCostQuoteError::ActorInvariant)? + .header + .pipeline_machine_envelope; + let pipeline_fee = Self::pipeline_fee_breakdown(actor_type, machine_envelope) + .map_err(|_| ActorCostQuoteError::ComputationOverflow)?; + let cursor = state.run_state.as_ref().map_or(0, |run| run.cursor); + let action = if state.contract.steps.is_empty() { + zero_action + } else { + let loaded = Self::load_current_step_from_storage(actor_id, cursor) + .ok_or(ActorCostQuoteError::ActorInvariant)?; + let maximum = + Self::maximum_current_action_fee(actor_type, &loaded.step, loaded.resources) + .map_err(|_| ActorCostQuoteError::ComputationOverflow)?; + ActorActionFeeQuote { + maximum_effect_weight: if matches!(loaded.step.task, ActorTask::StopCycle) { + Weight::zero() + } else { + loaded.resources.effect + }, + maximum_effect_fee: maximum.effect_fee, + production_weight_identity: effect_identity, + } + }; + ( + Some(ActorTriggerFeeQuote { + trigger_family, + maximum_weight, + fee: trigger_fee.trigger_fee, + production_weight_identity: Self::trigger_weight_identity(), + }), + Some(ActorPipelineFeeQuote { + pipeline_machine_fee: pipeline_fee.pipeline_machine_fee, + cleanup_fee: pipeline_fee.cleanup_fee, + total_fee: pipeline_fee.total_fee, + strategy: PipelineMachineFeeStrategy::UpfrontBounded, + admission_identity: admission.admission_identity, + production_weight_identity: admission.production_weight_identity, + }), + action, + ) + } + }; + let actor_state_hold = Self::actor_state_hold_quote(actor_id, actor_type)?; + Ok(ActorCostQuote { + actor_type, + creation_fee, + prospective_trigger_fee, + prospective_pipeline_fee, + maximum_next_action_fee, + actor_state_hold, + }) + } + + pub(crate) fn load_actor_contract(actor_id: ActorId) -> Option> { + Self::load_admitted_contract_geometry(actor_id).map(|(contract, _)| contract) + } + + pub(crate) fn store_actor_contract( + actor_id: ActorId, + contract: ActorContractOf, + ) -> DispatchResult { + let certificate = + Self::build_admission_certificate(&contract).ok_or(Error::::AdmissionBoundOverflow)?; + let stored = if ActorContractHeads::::contains_key(actor_id) { + Self::replace_admitted_contract_geometry(actor_id, &contract, &certificate) + } else { + Self::insert_admitted_contract_geometry(actor_id, &contract, &certificate) + }; + ensure!(stored, Error::::ActorInvariant); + if CrossingMemberships::::contains_key(actor_id) + && let Some(crossing) = Self::crossing_from_trigger(&contract.trigger) + { + let phase = match ActorHot::::get(actor_id).map(|hot| hot.trigger_runtime_state) { + Some(TriggerRuntimeState::ObservationCrossing { phase, .. }) => phase, + _ => return Err(Error::::ActorInvariant.into()), + }; + Self::sync_crossing_compiled_authority( + actor_id, + crossing, + phase, + certificate.admission_identity, + )?; + } + Self::sync_activation_authority(actor_id, &contract, &certificate); + Ok(()) + } + + fn sync_activation_authority( + actor_id: ActorId, + contract: &ActorContractOf, + certificate: &ActorAdmissionCertificateOf, + ) { + let feed = match &contract.trigger { + Trigger::ObservationChange { feed } => Some(*feed), + Trigger::ObservationCrossing { feed, .. } => Some(*feed), + _ => None, + }; + if let Some(feed) = feed { + ActorActivationAuthorities::::insert( + actor_id, + ActorActivationAuthority { + feed, + cooldown_blocks: contract.cooldown_blocks, + window: contract.window, + auto_close_at_cycle_nonce: contract.auto_close_at_cycle_nonce, + semantic_contract_id: certificate.semantic_contract_id, + body_commitment: certificate.body_commitment, + admission_identity: certificate.admission_identity, + }, + ); + } else { + ActorActivationAuthorities::::remove(actor_id); + } + } + + pub(crate) fn remove_actor_contract(actor_id: ActorId) -> DispatchResult { + ensure!( + Self::remove_admitted_contract_geometry(actor_id).is_some(), + Error::::ActorInvariant + ); + Ok(()) + } + + pub(crate) fn insert_admitted_contract_geometry( + actor_id: ActorId, + contract: &ActorContractOf, + certificate: &ActorAdmissionCertificateOf, + ) -> bool { + if ActorContractHeads::::contains_key(actor_id) + || ActorAdmissionCertificates::::contains_key(actor_id) + { + return false; + } + // An orphan Contract partition has no execution authority. System's zero-fee projection + // keeps corruption-mask tests and diagnostic geometry independent of identity state. + let actor_type = ActorIdentities::::get(actor_id) + .map(|identity| identity.actor_class.actor_type()) + .unwrap_or(ActorType::System); + let Some((head, chunks)) = + Self::decompose_admitted_contract_geometry(actor_id, actor_type, contract, certificate) + else { + return false; + }; + if chunks + .iter(/* deos-bypass: bounded-iter */) + .any(|(chunk_index, _)| { + ActorContractTailChunks::::contains_key(actor_id, chunk_index) + }) + { + return false; + } + ActorAdmissionCertificates::::insert(actor_id, certificate); + ActorContractHeads::::insert(actor_id, head); + for (chunk_index, chunk) in chunks { + ActorContractTailChunks::::insert(actor_id, chunk_index, chunk); + } + true + } + + #[allow( + dead_code, + reason = "I2 bounded reconstruction is staged behind the centralized Contract owner" + )] + pub(crate) fn load_admitted_contract_geometry( + actor_id: ActorId, + ) -> Option<(ActorContractOf, ActorAdmissionCertificateOf)> { + let head = ActorContractHeads::::get(actor_id)?; + let certificate = ActorAdmissionCertificates::::get(actor_id)?; + if !certificate.has_valid_identity() + || certificate.semantic_contract_id != head.header.semantic_contract_id + || certificate.body_commitment != head.header.body_commitment + || certificate.admission_identity != head.header.admission_identity + { + return None; + } + let chunk_count = head + .header + .step_count + .saturating_sub(1) + .div_ceil(MAX_STEPS_PER_TAIL_CHUNK); + let chunks = (0..chunk_count) + .map(|chunk_index| { + Some(( + chunk_index, + ActorContractTailChunks::::get(actor_id, chunk_index)?, + )) + }) + .collect::>>()?; + let mut resource_count = usize::from(head.header.step_count > 0); + for (_, chunk) in &chunks { + if chunk.steps.len() != chunk.step_resources.len() { + return None; + } + resource_count = resource_count.checked_add(chunk.step_resources.len())?; + } + if resource_count != head.header.step_count as usize { + return None; + } + let contract = Self::reconstruct_contract_geometry(actor_id, head, &chunks)?; + Some((contract, certificate)) + } + + #[allow( + dead_code, + reason = "I2 lazy production loading is staged before current-Step scheduler wiring" + )] + pub(crate) fn load_current_step_from_storage( + actor_id: ActorId, + cursor: u32, + ) -> Option> { + let head = ActorContractHeads::::get(actor_id)?; + let certificate = ActorAdmissionCertificates::::get(actor_id)?; + let tail_chunk = if cursor == 0 { + None + } else { + let chunk_index = cursor.checked_sub(1)? / MAX_STEPS_PER_TAIL_CHUNK; + Some(( + chunk_index, + ActorContractTailChunks::::get(actor_id, chunk_index)?, + )) + }; + Self::load_current_step_from_geometry( + actor_id, + &head, + &certificate, + cursor, + tail_chunk.as_ref().map(|(index, chunk)| (*index, chunk)), + ) + } + + pub(crate) fn replace_admitted_contract_geometry( + actor_id: ActorId, + contract: &ActorContractOf, + certificate: &ActorAdmissionCertificateOf, + ) -> bool { + let Some((current_contract, _)) = Self::load_admitted_contract_geometry(actor_id) else { + return false; + }; + let actor_type = ActorIdentities::::get(actor_id) + .map(|identity| identity.actor_class.actor_type()) + .unwrap_or(ActorType::System); + let Some((head, chunks)) = + Self::decompose_admitted_contract_geometry(actor_id, actor_type, contract, certificate) + else { + return false; + }; + let Ok(old_step_count) = u32::try_from(current_contract.steps.len()) else { + return false; + }; + let old_chunk_count = old_step_count + .saturating_sub(1) + .div_ceil(MAX_STEPS_PER_TAIL_CHUNK); + let Ok(new_chunk_count) = u32::try_from(chunks.len()) else { + return false; + }; + ActorAdmissionCertificates::::insert(actor_id, certificate); + ActorContractHeads::::insert(actor_id, head); + for (chunk_index, chunk) in chunks { + ActorContractTailChunks::::insert(actor_id, chunk_index, chunk); + } + for chunk_index in new_chunk_count..old_chunk_count { + ActorContractTailChunks::::remove(actor_id, chunk_index); + } + true + } + + pub(crate) fn remove_admitted_contract_geometry( + actor_id: ActorId, + ) -> Option> { + let (contract, _) = Self::load_admitted_contract_geometry(actor_id)?; + let chunk_count = u32::try_from(contract.steps.len()) + .ok()? + .saturating_sub(1) + .div_ceil(MAX_STEPS_PER_TAIL_CHUNK); + ActorActivationAuthorities::::remove(actor_id); + ActorAdmissionCertificates::::remove(actor_id); + ActorContractHeads::::remove(actor_id); + for chunk_index in 0..chunk_count { + ActorContractTailChunks::::remove(actor_id, chunk_index); + } + Some(contract) + } + + #[allow( + dead_code, + reason = "I4 dual-meter admission is staged before scheduler plan wiring" + )] + pub(crate) fn current_step_resources_fit( + control_meter: &WeightMeter, + effect_meter: &WeightMeter, + resources: ActorStepResourceEnvelope, + ) -> bool { + control_meter.can_consume(resources.control) && effect_meter.can_consume(resources.effect) + } + + fn trigger_occurrence_weight(trigger_family: TriggerFamily) -> Weight { + match trigger_family { + TriggerFamily::Manual => T::WeightInfo::manual_trigger(), + TriggerFamily::AddressEvent => T::WeightInfo::address_event_trigger_occurrence(), + TriggerFamily::ObservationChange => T::WeightInfo::observation_change_trigger_occurrence(), + TriggerFamily::ObservationCrossing => { + T::WeightInfo::observation_crossing_trigger_occurrence() + } + TriggerFamily::AtTime => T::WeightInfo::at_time_trigger_occurrence(), + TriggerFamily::Cadenced => T::WeightInfo::cadenced_trigger_occurrence(), + } + } + + fn trigger_weight_identity() -> [u8; 32] { + ( + b"DEOS_ACTOR_TRIGGER_WEIGHT_V1", + Self::trigger_occurrence_weight(TriggerFamily::Manual), + Self::trigger_occurrence_weight(TriggerFamily::AddressEvent), + Self::trigger_occurrence_weight(TriggerFamily::ObservationChange), + Self::trigger_occurrence_weight(TriggerFamily::ObservationCrossing), + Self::trigger_occurrence_weight(TriggerFamily::AtTime), + Self::trigger_occurrence_weight(TriggerFamily::Cadenced), + ) + .using_encoded(frame::hashing::blake2_256) + } + + pub(crate) fn trigger_fee_for_weight( + actor_type: ActorType, + trigger_family: TriggerFamily, + weight: Weight, + ) -> TriggerFeeBreakdown { + let trigger_fee = if actor_type == ActorType::System || weight == Weight::zero() { + Zero::zero() + } else { + T::WeightToFee::weight_to_fee(&weight) + }; + TriggerFeeBreakdown { + trigger_family, + trigger_fee, + } + } + + pub(crate) fn pipeline_fee_breakdown( + actor_type: ActorType, + envelope: PipelineMachineEnvelope, + ) -> Result, Error> { + if actor_type == ActorType::System { + return Ok(PipelineFeeBreakdown { + pipeline_machine_fee: Zero::zero(), + cleanup_fee: Zero::zero(), + total_fee: Zero::zero(), + }); + } + let total_fee = envelope + .pipeline_machine_fee_upper + .checked_add(&envelope.cleanup_fee_upper) + .ok_or(Error::::AdmissionBoundOverflow)?; + Ok(PipelineFeeBreakdown { + pipeline_machine_fee: envelope.pipeline_machine_fee_upper, + cleanup_fee: envelope.cleanup_fee_upper, + total_fee, + }) + } + + pub(crate) fn step_fee_for_resources( + actor_type: ActorType, + resources: ActorStepResourceEnvelope, + ) -> Result, Error> { + if actor_type == ActorType::System { + return Ok(StepFeeBreakdown { + control_fee: Zero::zero(), + effect_fee: Zero::zero(), + total_fee: Zero::zero(), + }); + } + let control_fee: T::Balance = Zero::zero(); + let effect_fee = if resources.effect == Weight::zero() { + Zero::zero() + } else { + T::WeightToFee::weight_to_fee(&resources.effect) + }; + let total_fee = control_fee + .checked_add(&effect_fee) + .ok_or(Error::::AdmissionBoundOverflow)?; + Ok(StepFeeBreakdown { + control_fee, + effect_fee, + total_fee, + }) + } + + pub(crate) fn maximum_current_action_fee( + actor_type: ActorType, + step: &StepOf, + resources: ActorStepResourceEnvelope, + ) -> Result, Error> { + if matches!(step.task, super::types::Task::StopCycle) { + return Ok(StepFeeBreakdown { + control_fee: Zero::zero(), + effect_fee: Zero::zero(), + total_fee: Zero::zero(), + }); + } + Self::step_fee_for_resources(actor_type, resources) + } + + pub(crate) fn maximum_current_step_fee( + actor_type: ActorType, + resources: ActorStepResourceEnvelope, + ) -> Result, Error> { + Self::step_fee_for_resources(actor_type, resources) + } + + pub(crate) fn derive_pipeline_machine_envelope( + actor_type: ActorType, + contract_steps: &ContractSteps, + resources: &ActorAdmissionResourcesOf, + ) -> Result, Error> { + ensure!( + contract_steps.len() == resources.len(), + Error::::ActorRunInvariant + ); + if actor_type == ActorType::System { + return Ok(PipelineMachineEnvelope { + pipeline_machine_fee_upper: Zero::zero(), + cleanup_fee_upper: Zero::zero(), + }); + } + let mut pipeline_machine_fee_upper: T::Balance = if contract_steps.is_empty() { + T::WeightToFee::weight_to_fee(&T::WeightInfo::scheduler_inner_zero_step_complete()) + } else { + Zero::zero() + }; + for (step, resource) in contract_steps + .iter(/* deos-bypass: bounded-iter — admitted Contract Steps bound the complete visit. */) + .zip( + resources + .iter(/* deos-bypass: bounded-iter — admitted resource count equals Step count. */), + ) + { + let machine_weight = if matches!(step.task, super::types::Task::StopCycle) { + resource.control.saturating_add(resource.effect) + } else { + resource.control + }; + let machine_attempt_fee = if machine_weight == Weight::zero() { + Zero::zero() + } else { + T::WeightToFee::weight_to_fee(&machine_weight) + }; + let attempts = step.on_error.retry_max_attempts().unwrap_or(1); + for _ in 0..attempts { + pipeline_machine_fee_upper = pipeline_machine_fee_upper + .checked_add(&machine_attempt_fee) + .ok_or(Error::::AdmissionBoundOverflow)?; + } + } + let cleanup_fee_upper = T::WeightToFee::weight_to_fee(&T::WeightInfo::close_actor()); + pipeline_machine_fee_upper + .checked_add(&cleanup_fee_upper) + .ok_or(Error::::AdmissionBoundOverflow)?; + Ok(PipelineMachineEnvelope { + pipeline_machine_fee_upper, + cleanup_fee_upper, + }) + } + + pub(crate) fn step_control_weight_context( + step_count: u32, + cursor: u32, + predicate_evaluation_units: u32, + opening_snapshot_entries: u32, + opening_predicate_results: u32, + funding_snapshot_entries: u32, + ) -> Option { + if step_count == 0 || step_count > T::MaxContractSteps::get() || cursor >= step_count { + return None; + } + if cursor == 0 { + return Some(StepControlWeightContext { + cursor, + steps_in_fragment: 1, + opening_tail_chunks: step_count + .saturating_sub(1) + .div_ceil(MAX_STEPS_PER_TAIL_CHUNK), + predicate_evaluation_units, + opening_snapshot_entries, + opening_predicate_results, + funding_snapshot_entries, + }); + } + let chunk_index = cursor.checked_sub(1)? / MAX_STEPS_PER_TAIL_CHUNK; + let first_step_index = + 1u32.checked_add(chunk_index.checked_mul(MAX_STEPS_PER_TAIL_CHUNK)?)?; + Some(StepControlWeightContext { + cursor, + steps_in_fragment: step_count + .checked_sub(first_step_index)? + .min(MAX_STEPS_PER_TAIL_CHUNK), + opening_tail_chunks: 0, + predicate_evaluation_units, + opening_snapshot_entries: 0, + opening_predicate_results: 0, + funding_snapshot_entries: 0, + }) + } + + fn opening_control_geometry(steps: &ContractSteps) -> Option<(u32, u32)> { + let snapshot_entries = u32::try_from( + Self::opening_surfaces(steps, 0) + .into_iter() + .collect::>() + .len(), + ) + .ok()?; + let predicate_results = steps + .iter(/* deos-bypass: bounded-iter */) + .try_fold(0u32, |total, step| { + total.checked_add( + step + .precondition + .as_ref() + .map_or(0, Precondition::opening_predicate_count), + ) + })?; + Some((snapshot_entries, predicate_results)) + } + + pub(crate) fn execution_step_control_weight_context( + instance: &ActiveActorViewOf, + run: Option<&ActorRunStateOf>, + loaded_step: &LoadedActorStepOf, + ) -> Option { + let step_count = u32::try_from(instance.steps.len()).ok()?; + let cursor = loaded_step.cursor; + if instance.cycle_state == CycleState::Idle && cursor != 0 { + return None; + } + let predicate_evaluation_units = loaded_step + .step + .precondition + .as_ref() + .map_or(0, Precondition::evaluation_units); + let (opening_snapshot_entries, opening_predicate_results) = if cursor == 0 { + if instance.cycle_state == CycleState::Idle { + Self::opening_control_geometry(&instance.steps)? + } else { + let run = run?; + ( + u32::try_from(run.opening_snapshot.len()).ok()?, + u32::try_from(run.opening_predicate_results.len()).ok()?, + ) + } + } else { + (0, 0) + }; + Self::step_control_weight_context( + step_count, + cursor, + predicate_evaluation_units, + opening_snapshot_entries, + opening_predicate_results, + T::MaxFundingTrackedAssets::get(), + ) + } + + #[allow( + dead_code, + reason = "I4 resource derivation is staged before the production control-Weight owner" + )] + pub(crate) fn derive_step_resource_envelopes( + contract: &ActorContractOf, + ) -> Option> { + let step_count = u32::try_from(contract.steps.len()).ok()?; + let (opening_snapshot_entries, opening_predicate_results) = + Self::opening_control_geometry(&contract.steps)?; + contract + .steps + .iter(/* deos-bypass: bounded-iter */) + .enumerate() + .map(|(cursor, step)| { + let cursor = u32::try_from(cursor).ok()?; + let predicate_evaluation_units = step + .precondition + .as_ref() + .map_or(0, Precondition::evaluation_units); + let context = Self::step_control_weight_context( + step_count, + cursor, + predicate_evaluation_units, + opening_snapshot_entries, + opening_predicate_results, + T::MaxFundingTrackedAssets::get(), + )?; + Some(ActorStepResourceEnvelope { + control: T::StepControlWeight::maximum_control_weight(context, step)?, + effect: T::TaskEffectWeight::maximum_effect_weight(&step.task)?, + }) + }) + .collect::>>()? + .try_into() + .ok() + } + + #[allow( + dead_code, + reason = "I2 certificate construction is staged before centralized Contract persistence" + )] + pub(crate) fn build_admission_certificate( + contract: &ActorContractOf, + ) -> Option> { + let authority = T::AdmissionCertificateAuthority::current()?; + Some(ActorAdmissionCertificate::new( + contract.semantic_contract_id(), + contract.body_commitment()?, + authority.runtime_actor_semantics_version, + authority.production_weight_identity, + authority.body_geometry_version, + authority.configured_bounds_commitment, + authority.maximum_lifecycle_weight, + )) + } + + #[allow( + dead_code, + reason = "I2 admitted geometry is staged before production storage wiring" + )] + pub(crate) fn decompose_admitted_contract_geometry( + actor_id: ActorId, + actor_type: ActorType, + contract: &ActorContractOf, + certificate: &ActorAdmissionCertificateOf, + ) -> Option<(ActorContractHeadOf, Vec<(u32, ActorStepChunkOf)>)> { + let semantic_contract_id = contract.semantic_contract_id(); + let body_commitment = contract.body_commitment()?; + let step_resources = Self::derive_step_resource_envelopes(contract)?; + if !certificate.has_valid_identity() + || certificate.semantic_contract_id != semantic_contract_id + || certificate.body_commitment != body_commitment + { + return None; + } + let pipeline_machine_envelope = + Self::derive_pipeline_machine_envelope(actor_type, &contract.steps, &step_resources) + .ok()?; + Self::decompose_contract_geometry( + actor_id, + contract, + certificate.admission_identity, + pipeline_machine_envelope, + &step_resources, + ) + } + + #[allow(clippy::too_many_arguments)] + pub(crate) fn build_actor_step_ticket( + actor_id: ActorId, + queue_ticket: QueueTicket, + eligible_at: BlockNumberFor, + identity: &ActorIdentityOf, + hot: &ActorHotStateOf, + run: Option<&ActorRunStateOf>, + admission: &ActorAdmissionCertificateOf, + ) -> Option> { + if hot.queue_ticket != Some(queue_ticket) || !admission.has_valid_identity() { + return None; + } + let (cycle_nonce, cursor, eligible_at) = match (hot.cycle_state, run) { + (CycleState::Idle, None) => (identity.cycle_nonce.checked_add(1)?, 0, eligible_at), + (CycleState::Running, Some(run)) + if run.running_is_coherent() + && run.has_contract_authority( + admission.semantic_contract_id, + admission.body_commitment, + admission.admission_identity, + ) => + { + if run.eligible_at != eligible_at { + return None; + } + (run.cycle_nonce, run.cursor, run.eligible_at) + } + (CycleState::Suspended, Some(run)) + if run.suspension_is_coherent() + && run.has_contract_authority( + admission.semantic_contract_id, + admission.body_commitment, + admission.admission_identity, + ) => + { + if run.eligible_at != eligible_at { + return None; + } + (run.cycle_nonce, run.cursor, run.eligible_at) + } + _ => return None, + }; + Some(ActorStepTicket { + actor_id, + cycle_nonce, + cursor, + ticket: queue_ticket, + eligible_at, + contract_commitment: ActorContractCommitment { + semantic_contract_id: admission.semantic_contract_id, + body_commitment: admission.body_commitment, + }, + }) + } + + #[allow(clippy::too_many_arguments)] + pub(crate) fn build_current_step_plan( + actor_id: ActorId, + identity: ActorIdentityOf, + hot: ActorHotStateOf, + run: Option>, + funding: ActorFundingStateOf, + admission: ActorAdmissionCertificateOf, + ticket: ActorStepTicketOf, + loaded_step: LoadedActorStepOf, + maximum_fee: StepFeeBreakdown, + ) -> Option> { + let queue_ticket = hot.queue_ticket?; + if !Self::validate_loaded_step_authority( + actor_id, + queue_ticket, + &admission, + &ticket, + &loaded_step, + ) { + return None; + } + match (hot.cycle_state, run.as_ref()) { + (CycleState::Idle, None) => { + if ticket.cursor != 0 || ticket.cycle_nonce != identity.cycle_nonce.checked_add(1)? { + return None; + } + } + (CycleState::Running, Some(run)) => { + if !run.running_is_coherent() + || !run.has_contract_authority( + admission.semantic_contract_id, + admission.body_commitment, + admission.admission_identity, + ) + || ticket.cycle_nonce != run.cycle_nonce + || ticket.cursor != run.cursor + || ticket.eligible_at != run.eligible_at + { + return None; + } + } + (CycleState::Suspended, Some(run)) => { + if !run.suspension_is_coherent() + || !run.has_contract_authority( + admission.semantic_contract_id, + admission.body_commitment, + admission.admission_identity, + ) + || ticket.cycle_nonce != run.cycle_nonce + || ticket.cursor != run.cursor + || ticket.eligible_at != run.eligible_at + { + return None; + } + } + _ => return None, + } + Some(StepExecutionPlan { + identity, + hot, + run, + funding, + admission, + ticket, + loaded_step, + maximum_fee, + }) + } + + #[cfg(any(test, feature = "runtime-benchmarks"))] + pub(crate) fn load_current_step_plan_from_storage( + ticket: ActorStepTicketOf, + ) -> Option> { + if ticket.eligible_at > frame_system::Pallet::::block_number() { + return None; + } + let actor_id = ticket.actor_id; + let identity = ActorIdentities::::get(actor_id)?; + let hot = ActorHot::::get(actor_id)?; + let run = ActorRunStateStore::::get(actor_id); + let funding = ActorFunding::::get(actor_id)?; + let admission = ActorAdmissionCertificates::::get(actor_id)?; + let loaded_step = Self::load_current_step_from_storage(actor_id, ticket.cursor)?; + let maximum_fee = + Self::maximum_current_step_fee(identity.actor_class.actor_type(), loaded_step.resources) + .ok()?; + Self::build_current_step_plan( + actor_id, + identity, + hot, + run, + funding, + admission, + ticket, + loaded_step, + maximum_fee, + ) + } + + pub(crate) fn validate_loaded_step_authority( + actor_id: ActorId, + queue_ticket: QueueTicket, + certificate: &ActorAdmissionCertificateOf, + ticket: &ActorStepTicketOf, + loaded_step: &LoadedActorStepOf, + ) -> bool { + certificate.has_valid_identity() + && ticket.actor_id == actor_id + && ticket.ticket == queue_ticket + && ticket.cursor == loaded_step.cursor + && ticket.contract_commitment.semantic_contract_id == certificate.semantic_contract_id + && ticket.contract_commitment.body_commitment == certificate.body_commitment + } + + pub(crate) fn load_current_step_from_geometry( + actor_id: ActorId, + head: &ActorContractHeadOf, + certificate: &ActorAdmissionCertificateOf, + cursor: u32, + tail_chunk: Option<(u32, &ActorStepChunkOf)>, + ) -> Option>> { + if !certificate.has_valid_identity() + || certificate.semantic_contract_id != head.header.semantic_contract_id + || certificate.body_commitment != head.header.body_commitment + || certificate.admission_identity != head.header.admission_identity + { + return None; + } + if cursor == 0 { + if tail_chunk.is_some() || head.header.step_count == 0 { + return None; + } + return Some(LoadedActorStep { + cursor, + step: head.first_step.clone()?, + resources: head.first_step_resources?, + }); + } + let expected_chunk_index = cursor.checked_sub(1)? / MAX_STEPS_PER_TAIL_CHUNK; + let expected_first_step_index = + 1u32.checked_add(expected_chunk_index.checked_mul(MAX_STEPS_PER_TAIL_CHUNK)?)?; + let (chunk_index, chunk) = tail_chunk?; + if chunk_index != expected_chunk_index + || !chunk.matches( + &actor_id, + &head.header.semantic_contract_id, + &head.header.body_commitment, + &head.header.admission_identity, + expected_first_step_index, + ) + { + return None; + } + let local_index = cursor.checked_sub(expected_first_step_index)? as usize; + let resources = *chunk.step_resources.get(local_index)?; + if chunk.steps.len() != chunk.step_resources.len() { + return None; + } + Some(LoadedActorStep { + cursor, + step: chunk.steps.get(local_index)?.clone(), + resources, + }) + } + + #[allow( + dead_code, + reason = "I2 geometry decomposition is staged before production storage wiring" + )] + pub(crate) fn decompose_contract_geometry( + actor_id: ActorId, + contract: &ActorContractOf, + admission_identity: [u8; 32], + pipeline_machine_envelope: PipelineMachineEnvelope, + resources: &ActorAdmissionResourcesOf, + ) -> Option<(ActorContractHeadOf, Vec<(u32, ActorStepChunkOf)>)> { + let semantic_contract_id = contract.semantic_contract_id(); + let body_commitment = contract.body_commitment()?; + let header = contract.try_header( + semantic_contract_id, + body_commitment, + admission_identity, + pipeline_machine_envelope, + )?; + if resources.len() != contract.steps.len() { + return None; + } + let first_step = contract.steps.first().cloned(); + let first_step_resources = resources.first().copied(); + let authority = ActorBodyAuthority { + actor_id, + semantic_contract_id, + body_commitment, + admission_identity, + }; + let chunks = contract + .steps + .as_slice() + .get(1..) + .unwrap_or_default() + .chunks(MAX_STEPS_PER_TAIL_CHUNK as usize) + .enumerate() + .map(|(chunk_index, steps)| { + let chunk_index = u32::try_from(chunk_index).ok()?; + let first_step_index = + 1u32.checked_add(chunk_index.checked_mul(MAX_STEPS_PER_TAIL_CHUNK)?)?; + let first_resource = usize::try_from(first_step_index).ok()?; + let last_resource = first_resource.checked_add(steps.len())?; + Some(( + chunk_index, + ActorStepChunk { + authority: authority.clone(), + first_step_index, + steps: BoundedVec::try_from(steps.to_vec()).ok()?, + step_resources: BoundedVec::try_from( + resources + .as_slice() + .get(first_resource..last_resource)? + .to_vec(), + ) + .ok()?, + }, + )) + }) + .collect::>>()?; + Some(( + ActorContractHead { + header, + first_step, + first_step_resources, + }, + chunks, + )) + } + + #[allow( + dead_code, + reason = "I2 bounded reconstruction is staged before production storage wiring" + )] + pub(crate) fn reconstruct_contract_geometry( + actor_id: ActorId, + head: ActorContractHeadOf, + chunks: &[(u32, ActorStepChunkOf)], + ) -> Option> { + let mut steps = Vec::with_capacity(head.header.step_count as usize); + match (head.first_step, head.first_step_resources) { + (Some(first_step), Some(_)) if head.header.step_count > 0 => steps.push(first_step), + (None, None) if head.header.step_count == 0 => {} + _ => return None, + } + for (expected_chunk_index, (chunk_index, chunk)) in chunks + .iter(/* deos-bypass: bounded-iter */) + .enumerate() + { + let expected_chunk_index = u32::try_from(expected_chunk_index).ok()?; + if *chunk_index != expected_chunk_index + || chunk.steps.is_empty() + || chunk.steps.len() != chunk.step_resources.len() + || (expected_chunk_index + 1 < chunks.len() as u32 + && chunk.steps.len() != MAX_STEPS_PER_TAIL_CHUNK as usize) + { + return None; + } + let first_step_index = + 1u32.checked_add(expected_chunk_index.checked_mul(MAX_STEPS_PER_TAIL_CHUNK)?)?; + if !chunk.matches( + &actor_id, + &head.header.semantic_contract_id, + &head.header.body_commitment, + &head.header.admission_identity, + first_step_index, + ) { + return None; + } + steps.extend(chunk + .steps + .iter(/* deos-bypass: bounded-iter */) + .cloned()); + } + if steps.len() != head.header.step_count as usize { + return None; + } + let contract = ActorContract { + trigger: head.header.trigger, + cooldown_blocks: head.header.cooldown_blocks, + window: head.header.window, + steps: ContractSteps::::try_from(steps).ok()?, + funding: head.header.funding, + completion: head.header.completion, + auto_close_at_cycle_nonce: head.header.auto_close_at_cycle_nonce, + }; + if contract.semantic_contract_id() != head.header.semantic_contract_id + || contract.body_commitment()? != head.header.body_commitment + { + return None; + } + Some(contract) + } + + pub(crate) fn record_crossing_worker_fault( + meter: &mut WeightMeter, + fault: CrossingWorkerFault, + ) -> bool { + if CrossingWorkerFaultState::::exists() { + return false; + } + let weight = T::WeightInfo::record_crossing_worker_fault(); + if !meter.can_consume(weight) { + return false; + } + meter.consume(weight); + CrossingWorkerFaultState::::put(fault); + Self::deposit_event(Event::ActorFaultRecorded { + fault_id: FaultId::CrossingWorker, + kind: ActorFaultKind::Detector, + first_recorded_block: frame_system::Pallet::::block_number(), + context: FaultContext::Crossing(fault), + }); + true + } - #[pallet::storage] - #[pallet::getter(fn actor_funding)] - pub type ActorFunding = - StorageMap<_, Blake2_128Concat, ActorId, ActorFundingStateOf, OptionQuery>; + pub(crate) fn record_observation_fanout_worker_fault( + meter: &mut WeightMeter, + fault: ObservationFanoutWorkerFault, + ) -> bool { + if ObservationFanoutWorkerFaultState::::exists() { + return false; + } + let weight = T::WeightInfo::record_observation_fanout_worker_fault(); + if !meter.can_consume(weight) { + return false; + } + meter.consume(weight); + ObservationFanoutWorkerFaultState::::put(fault); + Self::deposit_event(Event::ActorFaultRecorded { + fault_id: FaultId::ObservationFanoutWorker, + kind: ActorFaultKind::Detector, + first_recorded_block: frame_system::Pallet::::block_number(), + context: FaultContext::ObservationFanout(fault), + }); + true + } - #[pallet::storage] - #[pallet::storage_prefix = "ContinuationState"] - #[pallet::getter(fn continuation_state)] - pub type ContinuationStateStore = - StorageMap<_, Blake2_128Concat, ActorId, ContinuationStateOf, OptionQuery>; + pub(crate) fn record_wakeup_worker_fault( + meter: &mut WeightMeter, + fault: WakeupWorkerFault>, + ) -> bool { + if WakeupWorkerFaultState::::exists() { + return false; + } + let weight = T::WeightInfo::record_wakeup_worker_fault(); + if !meter.can_consume(weight) { + return false; + } + meter.consume(weight); + WakeupWorkerFaultState::::put(fault); + Self::deposit_event(Event::ActorFaultRecorded { + fault_id: FaultId::WakeupWorker, + kind: ActorFaultKind::Wakeup, + first_recorded_block: frame_system::Pallet::::block_number(), + context: FaultContext::Wakeup(fault), + }); + true + } - impl Pallet { pub(crate) fn derive_active_actor_view( identity: ActorIdentityOf, hot: ActorHotStateOf, @@ -697,38 +2061,314 @@ pub mod pallet { pending_signal: hot.pending_signal, queue_ticket: hot.queue_ticket, wakeup_pointer: hot.wakeup_pointer, + trigger_wakeup_pointer: hot.trigger_wakeup_pointer, last_control_mutation_block: identity.last_control_mutation_block, schedule_anchor: hot.schedule_anchor, - cadence_anchor_tick: hot.trigger_runtime_state.cadence_anchor_tick(), + temporal_anchor_tick: hot.trigger_runtime_state.temporal_anchor_tick(), + temporal_occurrence_consumed: hot.trigger_runtime_state.temporal_occurrence_consumed(), last_cycle_block: hot.last_cycle_block, } } - pub(crate) fn load_actor_state(actor_id: ActorId) -> LoadedActorStateOf { + pub(crate) fn load_actor_state_with_admission( + actor_id: ActorId, + ) -> ( + LoadedActorStateOf, + Option>, + ) { let identity = ActorIdentities::::get(actor_id); let hot = ActorHot::::get(actor_id); - let contract = ActorContracts::::get(actor_id); + let admitted_contract = Self::load_admitted_contract_geometry(actor_id); let funding = ActorFunding::::get(actor_id); - let continuation = ContinuationStateStore::::get(actor_id); - match (identity, hot, contract, funding, continuation) { - (None, None, None, None, None) => LoadedActorStateOf::NotRegistered, - (Some(identity), None, None, None, None) => LoadedActorStateOf::Dormant(identity), - (Some(identity), Some(hot), Some(contract), Some(funding), continuation) - if (hot.cycle_state == CycleState::Suspended) == continuation.is_some() - && hot - .trigger_runtime_state - .is_compatible_with(&contract.trigger) => + let run_state = ActorRunStateStore::::get(actor_id); + match (identity, hot, admitted_contract, funding, run_state) { + (None, None, None, None, None) => (LoadedActorStateOf::NotRegistered, None), + (Some(identity), None, None, None, None) => (LoadedActorStateOf::Dormant(identity), None), + (Some(identity), Some(hot), Some((contract, admission)), Some(funding), run_state) + if match (hot.cycle_state, run_state.as_ref()) { + (CycleState::Idle, None) => true, + (CycleState::Running, Some(run)) => { + run.running_is_coherent() + && run.has_contract_authority( + admission.semantic_contract_id, + admission.body_commitment, + admission.admission_identity, + ) + } + (CycleState::Suspended, Some(run)) => { + run.suspension_is_coherent() + && run.has_contract_authority( + admission.semantic_contract_id, + admission.body_commitment, + admission.admission_identity, + ) + } + _ => false, + } && hot + .trigger_runtime_state + .is_compatible_with(&contract.trigger) => { - LoadedActorStateOf::Active(ActiveActorState { - identity, - hot, - contract, - funding, - continuation, - }) + ( + LoadedActorStateOf::Active(ActiveActorState { + identity, + hot, + contract, + funding, + run_state, + }), + Some(admission), + ) + } + _ => (LoadedActorStateOf::Corrupt, None), + } + } + + pub(crate) fn load_actor_state(actor_id: ActorId) -> LoadedActorStateOf { + Self::load_actor_state_with_admission(actor_id).0 + } + + pub fn load_crossing_idle_activation_state( + actor_id: ActorId, + feed: T::ObservationFeedId, + ) -> Option> { + let identity = ActorIdentities::::get(actor_id)?; + let hot = ActorHot::::get(actor_id)?; + if hot.cycle_state != CycleState::Idle + || !matches!( + hot.trigger_runtime_state, + TriggerRuntimeState::ObservationCrossing { .. } + ) + || ActorRunHeads::::contains_key(actor_id) + { + return None; + } + let authority = ActorActivationAuthorities::::get(actor_id)?; + let certificate = ActorAdmissionCertificates::::get(actor_id)?; + if authority.feed != feed + || authority.semantic_contract_id != certificate.semantic_contract_id + || authority.body_commitment != certificate.body_commitment + || authority.admission_identity != certificate.admission_identity + { + return None; + } + Some(ObservationActivationState { + actor_id, + identity, + hot, + authority, + run_head: None, + loaded_step: None, + }) + } + + pub(crate) fn load_observation_activation_state( + actor_id: ActorId, + feed: T::ObservationFeedId, + ) -> Option> { + let identity = ActorIdentities::::get(actor_id)?; + let hot = ActorHot::::get(actor_id)?; + let authority = ActorActivationAuthorities::::get(actor_id)?; + if authority.feed != feed { + return None; + } + let head = ActorContractHeads::::get(actor_id)?; + if !matches!( + &head.header.trigger, + Trigger::ObservationChange { feed: contract_feed } if *contract_feed == feed + ) || authority.cooldown_blocks != head.header.cooldown_blocks + || authority.window != head.header.window + || authority.auto_close_at_cycle_nonce != head.header.auto_close_at_cycle_nonce + || authority.semantic_contract_id != head.header.semantic_contract_id + || authority.body_commitment != head.header.body_commitment + || authority.admission_identity != head.header.admission_identity + || !hot + .trigger_runtime_state + .is_compatible_with(&head.header.trigger) + { + return None; + } + let run_head = ActorRunHeads::::get(actor_id); + let cursor = match (hot.cycle_state, run_head.as_ref()) { + (CycleState::Idle, None) => 0, + (CycleState::Running, Some(run)) + if run.running_is_coherent() + && run.has_contract_authority( + authority.semantic_contract_id, + authority.body_commitment, + authority.admission_identity, + ) => + { + run.cursor + } + (CycleState::Suspended, Some(run)) + if run.suspension_is_coherent() + && run.has_contract_authority( + authority.semantic_contract_id, + authority.body_commitment, + authority.admission_identity, + ) => + { + run.cursor + } + _ => return None, + }; + if cursor > head.header.step_count + || (cursor == head.header.step_count && head.header.step_count > 0) + { + return None; + } + let loaded_step = if head.header.step_count == 0 { + if cursor != 0 || head.first_step.is_some() || head.first_step_resources.is_some() { + return None; + } + None + } else if cursor == 0 { + Some(LoadedActorStep { + cursor, + step: head.first_step.clone()?, + resources: head.first_step_resources?, + }) + } else { + let chunk_index = cursor.checked_sub(1)? / MAX_STEPS_PER_TAIL_CHUNK; + let chunk = ActorContractTailChunks::::get(actor_id, chunk_index)?; + let expected_first_step_index = + 1u32.checked_add(chunk_index.checked_mul(MAX_STEPS_PER_TAIL_CHUNK)?)?; + if !chunk.matches( + &actor_id, + &authority.semantic_contract_id, + &authority.body_commitment, + &authority.admission_identity, + expected_first_step_index, + ) || chunk.steps.len() != chunk.step_resources.len() + { + return None; + } + let local_index = cursor.checked_sub(expected_first_step_index)? as usize; + Some(LoadedActorStep { + cursor, + step: chunk.steps.get(local_index)?.clone(), + resources: *chunk.step_resources.get(local_index)?, + }) + }; + Some(ObservationActivationState { + actor_id, + identity, + hot, + authority, + run_head, + loaded_step, + }) + } + + pub(crate) fn load_actor_service_state( + actor_id: ActorId, + ) -> Option<( + ActiveActorStateOf, + ActorAdmissionCertificateOf, + Option>, + )> { + let identity = ActorIdentities::::get(actor_id)?; + let hot = ActorHot::::get(actor_id)?; + let head = ActorContractHeads::::get(actor_id)?; + let funding = ActorFunding::::get(actor_id)?; + let run_state = ActorRunStateStore::::get(actor_id); + let admission = ActorAdmissionCertificates::::get(actor_id)?; + let cursor = match (hot.cycle_state, run_state.as_ref()) { + (CycleState::Idle, None) => 0, + (CycleState::Running, Some(run)) + if run.running_is_coherent() + && run.has_contract_authority( + admission.semantic_contract_id, + admission.body_commitment, + admission.admission_identity, + ) => + { + run.cursor + } + (CycleState::Suspended, Some(run)) + if run.suspension_is_coherent() + && run.has_contract_authority( + admission.semantic_contract_id, + admission.body_commitment, + admission.admission_identity, + ) => + { + run.cursor + } + _ => return None, + }; + let tail_chunk = if cursor == 0 { + None + } else { + let chunk_index = cursor.checked_sub(1)? / MAX_STEPS_PER_TAIL_CHUNK; + Some(( + chunk_index, + ActorContractTailChunks::::get(actor_id, chunk_index)?, + )) + }; + let loaded_step = if head.header.step_count == 0 { + if cursor != 0 + || tail_chunk.is_some() + || head.first_step.is_some() + || head.first_step_resources.is_some() + { + return None; + } + None + } else { + Some(Self::load_current_step_from_geometry( + actor_id, + &head, + &admission, + cursor, + tail_chunk + .as_ref() + .map(|(chunk_index, chunk)| (*chunk_index, chunk)), + )?) + }; + if !hot + .trigger_runtime_state + .is_compatible_with(&head.header.trigger) + { + return None; + } + let mut steps = ContractSteps::::default(); + if let Some(loaded_step) = loaded_step.as_ref() { + for _ in 0..head.header.step_count { + steps.try_push(loaded_step.step.clone()).ok()?; } - _ => LoadedActorStateOf::Corrupt, } + let contract = ActorContract { + trigger: head.header.trigger, + cooldown_blocks: head.header.cooldown_blocks, + window: head.header.window, + funding: head.header.funding, + steps, + completion: head.header.completion, + auto_close_at_cycle_nonce: head.header.auto_close_at_cycle_nonce, + }; + Some(( + ActiveActorState { + identity, + hot, + contract, + funding, + run_state, + }, + admission, + loaded_step, + )) + } + + pub(crate) fn load_current_step_service_state( + actor_id: ActorId, + ) -> Option<( + ActiveActorStateOf, + ActorAdmissionCertificateOf, + LoadedActorStepOf, + )> { + let (state, admission, loaded_step) = Self::load_actor_service_state(actor_id)?; + Some((state, admission, loaded_step?)) } pub(crate) fn active_actor_view(actor_id: ActorId) -> Option> { @@ -824,6 +2464,7 @@ pub mod pallet { prospective_is_user: Option, ) -> Result, DispatchError> { let _intent = plan.intent; + IndexedTriggerDetectionDisabled::::remove(actor_id); let is_user = prospective_is_user.unwrap_or_else(|| { ActorIdentities::::get(actor_id) .is_some_and(|identity| matches!(identity.actor_class, ActorClass::User { .. })) @@ -848,31 +2489,36 @@ pub mod pallet { )?; hot.trigger_runtime_state = Self::installed_trigger_runtime_state( &contract.trigger, - hot.trigger_runtime_state.cadence_anchor_tick(), + hot.trigger_runtime_state.temporal_anchor_tick(), crossing_state, )?; ActorIdentities::::insert(actor_id, identity); ActorHot::::insert(actor_id, hot); - ActorContracts::::insert(actor_id, contract); - Ok(()) + Self::store_actor_contract(actor_id, contract) } fn provisional_trigger_runtime_state( trigger: &TriggerOf, - cadence_anchor_tick: Option, + temporal_anchor_tick: Option, ) -> TriggerRuntimeState { - if matches!(trigger, Trigger::Cadenced { .. }) { - TriggerRuntimeState::Cadenced { - anchor_tick: cadence_anchor_tick, - } - } else { - TriggerRuntimeState::Stateless + match trigger { + Trigger::AtTime { .. } => TriggerRuntimeState::AtTime { + anchor_tick: temporal_anchor_tick, + consumed: false, + }, + Trigger::Cadenced { .. } => TriggerRuntimeState::Cadenced { + anchor_tick: temporal_anchor_tick, + }, + Trigger::Manual + | Trigger::AddressEvent { .. } + | Trigger::ObservationChange { .. } + | Trigger::ObservationCrossing { .. } => TriggerRuntimeState::Stateless, } } fn installed_trigger_runtime_state( trigger: &TriggerOf, - cadence_anchor_tick: Option, + temporal_anchor_tick: Option, crossing_state: Option<(CrossingPhase, ObservationRevision)>, ) -> Result { match trigger { @@ -884,8 +2530,12 @@ pub mod pallet { installed_at_revision, }) } + Trigger::AtTime { .. } => Ok(TriggerRuntimeState::AtTime { + anchor_tick: temporal_anchor_tick, + consumed: false, + }), Trigger::Cadenced { .. } => Ok(TriggerRuntimeState::Cadenced { - anchor_tick: cadence_anchor_tick, + anchor_tick: temporal_anchor_tick, }), Trigger::Manual | Trigger::AddressEvent { .. } | Trigger::ObservationChange { .. } => { Ok(TriggerRuntimeState::Stateless) @@ -899,8 +2549,8 @@ pub mod pallet { ) -> DispatchResult { Self::commit_trigger_transition(actor_id, trigger_transition, None)?; ActorHot::::remove(actor_id); - ActorContracts::::remove(actor_id); - ContinuationStateStore::::remove(actor_id); + Self::remove_actor_contract(actor_id)?; + ActorRunStateStore::::remove(actor_id); Ok(()) } } @@ -914,6 +2564,12 @@ pub mod pallet { #[pallet::getter(fn actor_identity_count)] pub type ActorIdentityCount = StorageValue<_, u32, ValueQuery>; + /// Per-Actor authority for the owner's aggregate dedicated Actor-state hold. + #[pallet::storage] + #[pallet::getter(fn actor_state_hold)] + pub type ActorStateHolds = + StorageMap<_, Blake2_128Concat, ActorId, ActorStateHoldRecordOf, OptionQuery>; + #[pallet::storage] #[pallet::getter(fn active_actor_count)] pub type ActiveActorCount = StorageValue<_, u32, ValueQuery>; @@ -927,11 +2583,29 @@ pub mod pallet { #[pallet::getter(fn system_sovereign_count)] pub type SystemSovereignCount = StorageValue<_, u32, ValueQuery>; - /// Next never-used ticket and current-block cutoff source for the canonical FIFO. + /// Next never-used ticket for the canonical FIFO. #[pallet::storage] #[pallet::getter(fn next_queue_ticket)] pub type NextQueueTicket = StorageValue<_, QueueTicket, ValueQuery>; + /// Ticket frontier frozen at block initialization before ordinary external causes. + #[pallet::storage] + #[pallet::getter(fn prepass_execution_cutoff)] + pub type PrepassExecutionCutoff = + StorageValue<_, (BlockNumberFor, QueueTicket), OptionQuery>; + + /// Authoritative transient resource protocol state for the current block. + #[pallet::storage] + #[pallet::getter(fn block_resource_state)] + pub type CurrentBlockResourceState = + StorageValue<_, BlockResourceState>, OptionQuery>; + + /// Latest successfully reconciled block resource counters; read-only and non-authoritative. + #[pallet::storage] + #[pallet::getter(fn finalized_block_resource_telemetry)] + pub type FinalizedBlockResourceTelemetry = + StorageValue<_, FinalizedBlockResourceSnapshot>, OptionQuery>; + /// Physical head position of the canonical paged FIFO. #[pallet::storage] #[pallet::getter(fn queue_head)] @@ -1009,6 +2683,12 @@ pub mod pallet { #[pallet::getter(fn configured_active_actor_limit)] pub type ActiveActorLimit = StorageValue<_, u32, ValueQuery>; + /// Detector-local latch authority for indexed Trigger memberships retained during active traversal. + #[pallet::storage] + #[pallet::getter(fn indexed_trigger_detection_disabled)] + pub type IndexedTriggerDetectionDisabled = + StorageMap<_, Blake2_128Concat, ActorId, (), OptionQuery>; + /// Canonical observation feed ownership derived from each active actor's trigger policy. #[pallet::storage] #[pallet::getter(fn actor_observation_feeds)] @@ -1267,10 +2947,6 @@ pub mod pallet { mutability == Mutability::Mutable || !contract.trigger.manual_source_enabled(), "genesis System Immutable Actors cannot admit Manual readiness" ); - assert!( - !contract.steps.is_empty(), - "genesis System Actors Contract Steps cannot be empty" - ); Pallet::::validate_trigger(&contract.trigger, contract.cooldown_blocks) .expect("genesis trigger and cooldown must be valid"); if let Some(ref window) = contract.window { @@ -1302,9 +2978,9 @@ pub mod pallet { let funding_tracked_assets = Pallet::::derive_funding_tracked_assets(&contract.steps) .expect("genesis contract steps must have valid funding-tracked assets"); let schedule_anchor = Pallet::::schedule_anchor_at(contract.window, Zero::zero()); - // Genesis has no consensus timestamp. Cadenced actors use `None` as a bounded bootstrap - // marker and re-anchor from the first timestamp observed by ordinary wakeup service. - let cadence_anchor_tick = None; + // Genesis has no consensus timestamp. Temporal actors use `None` as a bounded bootstrap + // marker and anchor from the first timestamp observed by ordinary wakeup service. + let temporal_anchor_tick = None; let identity = ActorIdentity { sovereign_account: sovereign_account.clone(), owner: owner.clone(), @@ -1316,7 +2992,7 @@ pub mod pallet { last_control_mutation_block: Zero::zero(), }; let trigger_runtime_state = - Pallet::::provisional_trigger_runtime_state(&contract.trigger, cadence_anchor_tick); + Pallet::::provisional_trigger_runtime_state(&contract.trigger, temporal_anchor_tick); let hot = ActorHotState { lifecycle: ActiveLifecycle::Active, cycle_state: CycleState::Idle, @@ -1325,6 +3001,7 @@ pub mod pallet { pending_signal: false, queue_ticket: None, wakeup_pointer: None, + trigger_wakeup_pointer: None, terminal_at: contract .window .map(|window| Pallet::::window_terminal_at(&window)), @@ -1361,7 +3038,6 @@ pub mod pallet { ActorFundingState { funding_accumulated: Default::default(), funding_tracked_assets, - trigger_state_bond: Zero::zero(), }, ); ActiveActorCount::::put( @@ -1514,6 +3190,184 @@ pub mod pallet { _ => Weight::zero(), } } + + fn service_materialization_families( + now: BlockNumberFor, + available: Weight, + ) -> Option { + let family_cursor = MaterializationFamilyCursor::::get(); + if family_cursor >= 3 { + return None; + } + let shared_limit = Self::materialization_weight_limit(); + let mut remaining = Weight::from_parts( + shared_limit.ref_time().min(available.ref_time()), + shared_limit.proof_size().min(available.proof_size()), + ); + let mut consumed_total = Weight::zero(); + let mut wakeups = WakeupDrainStats::default(); + let mut crossing = crate::crossing::CrossingWorkCounters::default(); + let mut fanout_pages = 0u32; + let all_minimum_quanta = Self::materialization_family_minimum(0) + .saturating_add(Self::materialization_family_minimum(1)) + .saturating_add(Self::materialization_family_minimum(2)); + let can_reserve_all_minima = all_minimum_quanta.all_lte(remaining); + for offset in 0u8..3 { + let family = family_cursor.saturating_add(offset) % 3; + let family_budget = Self::materialization_family_budget( + family_cursor, + offset, + remaining, + can_reserve_all_minima, + ); + let consumed = Self::service_materialization_family( + family, + now, + family_budget, + &mut wakeups, + &mut crossing, + &mut fanout_pages, + ); + consumed_total = consumed_total.saturating_add(consumed); + remaining = remaining.saturating_sub(consumed); + } + if !remaining.is_zero() && Self::materialization_family_has_work(family_cursor, now) { + consumed_total = consumed_total.saturating_add(Self::service_materialization_family( + family_cursor, + now, + remaining, + &mut wakeups, + &mut crossing, + &mut fanout_pages, + )); + } + MaterializationFamilyCursor::::put(family_cursor.saturating_add(1) % 3); + Some(consumed_total) + } + } + + impl Pallet { + fn execute_mandatory_prepass(now: BlockNumberFor) -> Result> { + ensure!( + T::PrepassContext::context_ready(), + Error::::PrepassContextIncomplete + ); + let control_weight = T::WeightInfo::scheduler_on_initialize_cutoff(); + if let Some(mut existing) = CurrentBlockResourceState::::get() + && existing.ensure_block(now).is_ok() + { + existing.halt_optional_actor_work(); + CurrentBlockResourceState::::put(existing); + return Err(Error::::PrepassDuplicateOrStale); + } + + let budget = T::BlockResourceBudget::get(); + let mut state = BlockResourceState::new(now); + if state.begin_prepass().is_err() { + state.halt_optional_actor_work(); + CurrentBlockResourceState::::put(state); + return Err(Error::::ResourceProtocolFailed); + } + let cutoff = NextQueueTicket::::get(); + let mut cutoff_reservation = state + .reserve( + budget.limits(), + BlockResourceDomain::ActorControl, + control_weight, + ) + .map_err(|_| Error::::ResourceProtocolFailed)?; + PrepassExecutionCutoff::::put((now, cutoff)); + state + .settle(&mut cutoff_reservation, control_weight) + .map_err(|_| Error::::ResourceProtocolFailed)?; + + let cleanup_units = u32::from( + QueueHead::::get() < QueueTail::::get() + && Self::combined_queue_occupancy() >= u64::from(T::MaxQueueLength::get()), + ); + let cleanup_weight = if cleanup_units > 0 { + T::WeightInfo::scheduler_paged_tombstone_drain(cleanup_units) + } else { + Weight::zero() + }; + if cleanup_units > 0 { + let mut cleanup_reservation = state + .reserve( + budget.limits(), + BlockResourceDomain::ActorControl, + cleanup_weight, + ) + .map_err(|_| Error::::ResourceProtocolFailed)?; + let _ = Self::paged_drain_tombstones(cutoff, 1); + state + .settle(&mut cleanup_reservation, cleanup_weight) + .map_err(|_| Error::::ResourceProtocolFailed)?; + } + + let materialization_configured = T::WeightInfo::materialization_coordinator_base() + .saturating_add(Self::materialization_weight_limit()); + let materialization_remaining = budget + .limits() + .actor_control() + .checked_sub(&state.usage().actor_control_used()) + .ok_or(Error::::ResourceProtocolFailed)?; + let materialization_maximum = Weight::from_parts( + materialization_configured + .ref_time() + .min(materialization_remaining.ref_time()), + materialization_configured + .proof_size() + .min(materialization_remaining.proof_size()), + ); + ensure!( + T::WeightInfo::materialization_coordinator_base().all_lte(materialization_maximum), + Error::::ResourceProtocolFailed + ); + let materialization_family_budget = + materialization_maximum.saturating_sub(T::WeightInfo::materialization_coordinator_base()); + let mut materialization_reservation = state + .reserve( + budget.limits(), + BlockResourceDomain::ActorControl, + materialization_maximum, + ) + .map_err(|_| Error::::ResourceProtocolFailed)?; + let materialization_weight = + Self::service_materialization_families(now, materialization_family_budget) + .ok_or(Error::::ResourceProtocolFailed)?; + let materialization_actual = + T::WeightInfo::materialization_coordinator_base().saturating_add(materialization_weight); + state + .settle(&mut materialization_reservation, materialization_actual) + .map_err(|_| Error::::ResourceProtocolFailed)?; + + let control_remaining = budget + .limits() + .actor_control() + .checked_sub(&state.usage().actor_control_used()) + .unwrap_or_else(Weight::zero); + let prepass_limit = control_remaining + .checked_add(&budget.limits().actor_base_turn()) + .unwrap_or_else(Weight::zero); + let pass = Self::execute_cycle_to_cutoff_with_resources( + prepass_limit, + cutoff, + &mut state, + budget.limits(), + BlockResourceDomain::ActorBaseEffect, + control_remaining, + ); + if state.open_external_phase().is_err() { + state.halt_optional_actor_work(); + } + CurrentBlockResourceState::::put(state); + Ok( + control_weight + .saturating_add(cleanup_weight) + .saturating_add(materialization_actual) + .saturating_add(pass.consumed), + ) + } } #[pallet::hooks] @@ -1521,7 +3375,7 @@ pub mod pallet { fn integrity_test() { assert!( T::MaxConsecutiveFailures::get() > 0, - "MaxConsecutiveFailures must be non-zero for bounded Continuation lifetime" + "MaxConsecutiveFailures must be non-zero for bounded Actor run lifetime" ); assert!( contract_steps_bound_is_valid(T::MaxContractSteps::get()), @@ -1545,11 +3399,11 @@ pub mod pallet { "CadenceTickMillis must be non-zero" ); let cadence_tick_millis = T::CadenceTickMillis::get(); - let expected_cadence_horizon = 315_576_000_000u64.div_ceil(cadence_tick_millis); + let expected_temporal_horizon = 315_576_000_000u64.div_ceil(cadence_tick_millis); assert_eq!( - T::MaxCadenceDelayTicks::get(), - expected_cadence_horizon, - "MaxCadenceDelayTicks must cover exactly ten Julian years" + T::MaxTemporalDelayTicks::get(), + expected_temporal_horizon, + "MaxTemporalDelayTicks must cover exactly ten Julian years" ); let expected_horizon = 315_576_000u64.div_ceil(target_block_time); let configured_horizon: u64 = T::MaxExecutionDelayBlocks::get().saturated_into(); @@ -1589,6 +3443,10 @@ pub mod pallet { T::ObservationPageSize::get() > 0, "ObservationPageSize must be non-zero" ); + assert!( + T::CrossingPageSize::get() > 0, + "CrossingPageSize must be non-zero" + ); assert!( T::MaxCrossingMembersPerFeed::get() > 0 && T::MaxCrossingMembersPerFeed::get() <= T::MaxActiveActors::get(), @@ -1629,8 +3487,10 @@ pub mod pallet { fanout_limit.ref_time() > 0 && fanout_limit.proof_size() > 0, "observation fanout Weight limit must be non-zero in both dimensions" ); - let fanout_unit = T::WeightInfo::observation_fanout_base() - .saturating_add(T::WeightInfo::observation_fanout_page()); + let fanout_unit = T::WeightInfo::observation_fanout_base().saturating_add( + Self::observation_fanout_ordinary_weight_upper() + .max(T::WeightInfo::observation_fanout_terminal()), + ); assert!( fanout_unit.all_lte(fanout_limit), "positive observation fanout cap must admit one complete page unit" @@ -1691,23 +3551,75 @@ pub mod pallet { Self::do_try_state() } + fn on_initialize(_now: BlockNumberFor) -> Weight { + Weight::zero() + } + fn on_idle(now: BlockNumberFor, remaining_weight: Weight) -> Weight { let reserved = T::ActorOnIdleReserve::get(); let available = Weight::from_parts( remaining_weight.ref_time().min(reserved.ref_time()), remaining_weight.proof_size().min(reserved.proof_size()), ); + let (control_available, resource_state) = match CurrentBlockResourceState::::get() { + Some(state) + if state.ensure_block(now).is_ok() + && state.phase() == BlockResourcePhase::ExternalPhase + && !state.optional_actor_work_halted() => + { + ( + T::BlockResourceBudget::get() + .limits() + .actor_control() + .checked_sub(&state.usage().actor_control_used()) + .map(|remaining| { + Weight::from_parts( + available.ref_time().min(remaining.ref_time()), + available.proof_size().min(remaining.proof_size()), + ) + }) + .unwrap_or_else(Weight::zero), + Some(state), + ) + } + Some(_) => (Weight::zero(), None), + None => (available, None), + }; + let legacy_unmetered_materialization = resource_state.is_none(); let base_weight = T::WeightInfo::scheduler_on_idle_base(); - let coordinator_weight = T::WeightInfo::materialization_coordinator_base(); - let fixed_weight = base_weight.saturating_add(coordinator_weight); - if !fixed_weight.all_lte(available) { + let coordinator_weight = if legacy_unmetered_materialization { + T::WeightInfo::materialization_coordinator_base() + } else { + Weight::zero() + }; + let finalize_weight = T::WeightInfo::block_resource_finalize(); + let fixed_weight = base_weight + .saturating_add(coordinator_weight) + .saturating_add(finalize_weight); + if !fixed_weight.all_lte(control_available) { return Weight::zero(); } + let mut control_authority = match resource_state { + Some(mut state) => match state.reserve( + T::BlockResourceBudget::get().limits(), + BlockResourceDomain::ActorControl, + control_available, + ) { + Ok(reservation) => Some((state, reservation)), + Err(_) => { + state.halt_optional_actor_work(); + CurrentBlockResourceState::::put(state); + return Weight::zero(); + } + }, + None => None, + }; let breaker_active = GlobalCircuitBreaker::::get(); - let after_base = available.saturating_sub(fixed_weight); + let after_base = control_available.saturating_sub(fixed_weight); let cleanup_units = u32::from(QueueHead::::get() < QueueTail::::get()); let queue_cleanup_weight = T::WeightInfo::scheduler_paged_tombstone_drain(cleanup_units); - let saturated_cleanup_weight = if cleanup_units > 0 + let saturated_cleanup_weight = if legacy_unmetered_materialization + && cleanup_units > 0 && Self::combined_queue_occupancy() >= u64::from(T::MaxQueueLength::get()) && queue_cleanup_weight.all_lte(after_base) { @@ -1721,73 +3633,81 @@ pub mod pallet { Weight::zero() }; let remaining_after_cleanup = after_base.saturating_sub(saturated_cleanup_weight); - let family_cursor = MaterializationFamilyCursor::::get(); - if family_cursor >= 3 { - return fixed_weight.saturating_add(saturated_cleanup_weight); - } - let mut materialization_weight = Weight::zero(); - let shared_limit = Self::materialization_weight_limit(); - let mut remaining_materialization_budget = Weight::from_parts( - shared_limit - .ref_time() - .min(remaining_after_cleanup.ref_time()), - shared_limit - .proof_size() - .min(remaining_after_cleanup.proof_size()), - ); - let mut wakeup_counters = WakeupDrainStats::default(); - let mut crossing_counters = crate::crossing::CrossingWorkCounters::default(); - let mut fanout_pages = 0u32; - let all_minimum_quanta = Self::materialization_family_minimum(0) - .saturating_add(Self::materialization_family_minimum(1)) - .saturating_add(Self::materialization_family_minimum(2)); - let can_reserve_all_minima = all_minimum_quanta.all_lte(remaining_materialization_budget); - for offset in 0u8..3 { - let family = family_cursor.saturating_add(offset) % 3; - let family_budget = Self::materialization_family_budget( - family_cursor, - offset, - remaining_materialization_budget, - can_reserve_all_minima, - ); - let consumed = Self::service_materialization_family( - family, - now, - family_budget, - &mut wakeup_counters, - &mut crossing_counters, - &mut fanout_pages, - ); - materialization_weight = materialization_weight.saturating_add(consumed); - remaining_materialization_budget = - remaining_materialization_budget.saturating_sub(consumed); - } - if !remaining_materialization_budget.is_zero() - && Self::materialization_family_has_work(family_cursor, now) - { - let consumed = Self::service_materialization_family( - family_cursor, - now, - remaining_materialization_budget, - &mut wakeup_counters, - &mut crossing_counters, - &mut fanout_pages, - ); - materialization_weight = materialization_weight.saturating_add(consumed); - } - MaterializationFamilyCursor::::put(family_cursor.saturating_add(1) % 3); - let remaining_after_housekeeping = - remaining_after_cleanup.saturating_sub(materialization_weight); + let materialization_weight = if legacy_unmetered_materialization { + let Some(consumed) = Self::service_materialization_families(now, remaining_after_cleanup) + else { + return fixed_weight.saturating_add(saturated_cleanup_weight); + }; + consumed + } else { + Weight::zero() + }; let housekeeping_weight = fixed_weight .saturating_add(saturated_cleanup_weight) .saturating_add(materialization_weight); + let remaining_after_housekeeping = available.saturating_sub(housekeeping_weight); + Self::settle_on_idle_control(&mut control_authority, housekeeping_weight); if breaker_active { + Self::finalize_empty_actor_drain(now); return housekeeping_weight; } - let pass = Self::execute_cycle(remaining_after_housekeeping); + let execution_cutoff = PrepassExecutionCutoff::::get() + .filter(|(cutoff_block, _)| *cutoff_block == now) + .map(|(_, cutoff)| cutoff) + .unwrap_or_else(NextQueueTicket::::get); + let pass = match CurrentBlockResourceState::::get() { + Some(mut state) + if state.ensure_block(now).is_ok() + && state.phase() == BlockResourcePhase::ExternalPhase + && !state.optional_actor_work_halted() => + { + let budget = T::BlockResourceBudget::get(); + if state.begin_drain().is_err() { + state.halt_optional_actor_work(); + CurrentBlockResourceState::::put(state); + return housekeeping_weight; + } + let control_maximum = budget + .limits() + .actor_control() + .checked_sub(&state.usage().actor_control_used()) + .unwrap_or_else(Weight::zero); + let pass = Self::execute_cycle_to_cutoff_with_resources( + remaining_after_housekeeping, + execution_cutoff, + &mut state, + budget.limits(), + BlockResourceDomain::ActorDrainEffect, + control_maximum, + ); + if state.finish_drain(budget, budget.fixed_envelope()).is_err() { + state.halt_optional_actor_work(); + } else if let Ok(snapshot) = state.finalized_snapshot() { + FinalizedBlockResourceTelemetry::::put(snapshot); + } + CurrentBlockResourceState::::put(state); + pass + } + Some(mut state) => { + state.halt_optional_actor_work(); + CurrentBlockResourceState::::put(state); + return housekeeping_weight; + } + None => Self::execute_cycle_to_cutoff(remaining_after_housekeeping, execution_cutoff), + }; Self::update_idle_starvation_state(now, pass.starved); housekeeping_weight.saturating_add(pass.consumed) } + + fn on_finalize(now: BlockNumberFor) { + let valid = CurrentBlockResourceState::::take().is_some_and(|state| { + state.ensure_block(now).is_ok() + && state.phase() == BlockResourcePhase::Finalizable + && state.outstanding_reservations() == 0 + }) && FinalizedBlockResourceTelemetry::::get() + .is_some_and(|snapshot| snapshot.block_number() == now); + assert!(valid, "Actors block resource protocol did not finalize"); // deos-bypass: panic-owner — invalid phase/reservation/telemetry makes the authored block consensus-invalid; finalization tests cover every accepted marker. + } } #[pallet::event] @@ -1960,6 +3880,12 @@ pub mod pallet { GlobalCircuitBreakerSet { paused: bool, }, + ActorFaultRecorded { + fault_id: FaultId, + kind: ActorFaultKind, + first_recorded_block: BlockNumberFor, + context: FaultContext>, + }, CrossingWorkerFaultCleared { feed: T::ObservationFeedId, revision: Option, @@ -1976,8 +3902,24 @@ pub mod pallet { page: WakeupPageId, class: CrossingWorkerFaultClass, }, - ManualTriggerSet { + ManualTriggerSet { + actor_id: ActorId, + }, + TriggerOccurrenceProcessed { + actor_id: ActorId, + trigger_family: TriggerFamily, + fee: BalanceOf, + }, + PipelineFeeCharged { + actor_id: ActorId, + fee: BalanceOf, + }, + ActionFeeCharged { actor_id: ActorId, + cycle_nonce: u64, + step_index: u32, + actual_effect_weight: Weight, + fee: BalanceOf, }, FundingAccumulated { actor_id: ActorId, @@ -2015,7 +3957,6 @@ pub mod pallet { ActiveActorLimitTooLow, ActiveActorLimitBelowCurrent, ActorPaused, - EmptyContractSteps, ContractStepsExceedOnIdleBudget, ExecutionDelayTooLong, GlobalCircuitBreakerActive, @@ -2054,8 +3995,8 @@ pub mod pallet { ControlMutationRateLimited, QueueCapacityUnavailable, RetryLaterNotAllowedForImmutableActor, - ContinuationNotFound, - ContinuationInvariant, + ActorRunNotFound, + ActorRunInvariant, ComputationOverflow, EmptyPrecondition, ManualSourceDisabled, @@ -2078,6 +4019,12 @@ pub mod pallet { WakeupWorkerFaultNotFound, SystemActorTopologyInvalid, AdmissionBoundOverflow, + StateHoldUnavailable, + StateHoldInvariant, + StateHoldOverflow, + PrepassDuplicateOrStale, + ResourceProtocolFailed, + PrepassContextIncomplete, } #[pallet::call] @@ -2148,13 +4095,17 @@ pub mod pallet { #[pallet::weight(T::WeightInfo::pause_actor().saturating_add(Pallet::::close_dispatch_weight_upper()))] pub fn pause_actor(origin: OriginFor, actor_id: ActorId) -> DispatchResult { let state = Self::active_actor_state_for_control(actor_id)?; - let continuation = state.continuation; + let continuation = state.run_state; let snapshot = Self::derive_active_actor_view(state.identity, state.hot, state.contract); Self::ensure_control_origin(origin.clone(), &snapshot)?; Self::ensure_not_system_immutable(&snapshot)?; if Self::expiry_substitution_due_loaded(&snapshot, continuation.as_ref())? { return Self::finalize_actor(actor_id, &snapshot, CloseReason::WindowExpired); } + ensure!( + snapshot.mutability == Mutability::Mutable, + Error::::ImmutableActor + ); if snapshot.lifecycle.is_paused() { return Ok(()); } @@ -2181,13 +4132,17 @@ pub mod pallet { #[pallet::weight(T::WeightInfo::resume_actor().saturating_add(Pallet::::close_dispatch_weight_upper()))] pub fn resume_actor(origin: OriginFor, actor_id: ActorId) -> DispatchResult { let state = Self::active_actor_state_for_control(actor_id)?; - let continuation = state.continuation; + let continuation = state.run_state; let snapshot = Self::derive_active_actor_view(state.identity, state.hot, state.contract); Self::ensure_control_origin(origin.clone(), &snapshot)?; Self::ensure_not_system_immutable(&snapshot)?; if Self::expiry_substitution_due_loaded(&snapshot, continuation.as_ref())? { return Self::finalize_actor(actor_id, &snapshot, CloseReason::WindowExpired); } + ensure!( + snapshot.mutability == Mutability::Mutable, + Error::::ImmutableActor + ); if !snapshot.lifecycle.is_paused() { return Ok(()); } @@ -2211,26 +4166,52 @@ pub mod pallet { #[pallet::call_index(6)] #[pallet::weight(T::WeightInfo::manual_trigger().saturating_add(Pallet::::close_dispatch_weight_upper()))] - pub fn manual_trigger(origin: OriginFor, actor_id: ActorId) -> DispatchResult { + pub fn manual_trigger(origin: OriginFor, actor_id: ActorId) -> DispatchResultWithPostInfo { let state = Self::active_actor_state_for_control(actor_id)?; - let continuation = state.continuation; + let continuation = state.run_state; let snapshot = Self::derive_active_actor_view(state.identity, state.hot, state.contract); Self::ensure_control_origin(origin.clone(), &snapshot)?; Self::ensure_not_system_immutable(&snapshot)?; if Self::expiry_substitution_due_loaded(&snapshot, continuation.as_ref())? { - return Self::finalize_actor(actor_id, &snapshot, CloseReason::WindowExpired); + Self::finalize_actor(actor_id, &snapshot, CloseReason::WindowExpired)?; + return Ok(Pays::Yes.into()); } ensure!(!snapshot.lifecycle.is_paused(), Error::::ActorPaused); ensure!( snapshot.trigger.manual_source_enabled(), Error::::ManualSourceDisabled ); + let actor_type = snapshot.actor_class.actor_type(); + let breakdown = Self::trigger_fee_for_weight( + actor_type, + TriggerFamily::Manual, + T::WeightInfo::manual_trigger(), + ); + let mut trigger_processed = false; Self::with_control_transaction(|| { - let outcome = Self::request_activation(actor_id).map_err(Self::activation_failure_error)?; + let Some(outcome) = Self::commit_trigger_occurrence( + actor_id, + actor_type, + &snapshot.sovereign_account, + breakdown, + TriggerCauseProvenance::ExternalPhase, + )? + else { + return Ok(()); + }; if matches!(outcome, crate::scheduler::ActivationOutcome::Latched) { Self::deposit_event(Event::ManualTriggerSet { actor_id }); } + trigger_processed = true; Ok(()) + })?; + Ok(PostDispatchInfo { + actual_weight: Some(T::WeightInfo::manual_trigger()), + pays_fee: if actor_type == ActorType::User && trigger_processed { + Pays::No + } else { + Pays::Yes + }, }) } @@ -2241,7 +4222,10 @@ pub mod pallet { LoadedActorStateOf::Active(state) => { let instance = Self::derive_active_actor_view(state.identity, state.hot, state.contract); Self::ensure_control_origin(origin, &instance)?; - Self::ensure_not_system_immutable(&instance)?; + ensure!( + instance.mutability == Mutability::Mutable, + Error::::ImmutableActor + ); Self::finalize_actor(actor_id, &instance, CloseReason::OwnerInitiated) } LoadedActorStateOf::Dormant(identity) => { @@ -2260,7 +4244,6 @@ pub mod pallet { actor_id: ActorId, mut contract: ActorContractOf, ) -> DispatchResult { - ensure!(!contract.steps.is_empty(), Error::::EmptyContractSteps); Self::canonicalize_preconditions(&mut contract.steps)?; Self::validate_trigger(&contract.trigger, contract.cooldown_blocks)?; if let Some(ref window) = contract.window { @@ -2268,11 +4251,11 @@ pub mod pallet { } Self::validate_future_schedule_targets(&contract)?; let state = Self::active_actor_state_for_control(actor_id)?; - let continuation = state.continuation; + let continuation = state.run_state; let snapshot = Self::derive_active_actor_view(state.identity, state.hot, state.contract); Self::ensure_control_origin(origin.clone(), &snapshot)?; let current_contract = - ActorContracts::::get(actor_id).ok_or(Error::::ActorInvariant)?; + Self::load_actor_contract(actor_id).ok_or(Error::::ActorInvariant)?; if current_contract == contract { return Ok(()); } @@ -2289,7 +4272,6 @@ pub mod pallet { || current_contract.cooldown_blocks != contract.cooldown_blocks || current_contract.window != contract.window; let steps_changed = current_contract.steps != contract.steps; - let funding_changed = current_contract.funding != contract.funding; let now = frame_system::Pallet::::block_number(); Self::ensure_control_mutation_allowed(&snapshot, now)?; Self::validate_contract_steps_shape(snapshot.actor_class.actor_type(), &contract.steps)?; @@ -2318,22 +4300,16 @@ pub mod pallet { } let new_tracked = Self::derive_funding_tracked_assets(&contract.steps)?; let mut funding_state = ActorFunding::::get(actor_id).ok_or(Error::::ActorNotFound)?; - let target_trigger_state_bond = if snapshot.actor_class.actor_type() == ActorType::User { - T::TriggerStateBond::amount(&contract.trigger) - } else { - Zero::zero() - }; - let current_trigger_state_bond = funding_state.trigger_state_bond; - funding_state.trigger_state_bond = target_trigger_state_bond; funding_state.funding_tracked_assets = new_tracked.clone(); funding_state .funding_accumulated .retain(|asset, _| new_tracked.contains(asset)); - let cancellation_reason = (schedule_changed || steps_changed || funding_changed) - .then_some(CancellationReason::ContractReplaced); + // Every non-no-op Contract update rotates semantic and admission authority, so an open run + // cannot remain bound to the replaced Contract even when only completion policy changes. + let cancellation_reason = Some(CancellationReason::ContractReplaced); let schedule_anchor = Self::schedule_anchor_at(contract.window, now); - let cadence_anchor_tick = - Self::cadence_anchor_tick(&contract.trigger).map_err(Self::placement_error)?; + let temporal_anchor_tick = + Self::temporal_anchor_tick(&contract.trigger).map_err(Self::placement_error)?; let trigger_transition = schedule_changed .then(|| { Self::preflight_trigger_transition( @@ -2344,13 +4320,8 @@ pub mod pallet { }) .transpose()?; Self::with_control_transaction(|| { - T::TriggerStateBond::set_bond( - &snapshot.owner, - current_trigger_state_bond, - target_trigger_state_bond, - )?; - let continuation_cancelled = if let Some(reason) = cancellation_reason { - Self::cancel_continuation_internal(actor_id, reason, None)? + let run_cancelled = if let Some(reason) = cancellation_reason { + Self::cancel_run_internal(actor_id, reason, None)? } else { false }; @@ -2359,14 +4330,19 @@ pub mod pallet { } else { None }; - ActorContracts::::insert(actor_id, contract.clone()); + if schedule_changed + && ActorHot::::get(actor_id).is_some_and(|hot| hot.trigger_wakeup_pointer.is_some()) + { + Self::trigger_wakeup_substrate_invalidate_inner(actor_id) + .map_err(Self::placement_error)?; + } ActorHot::::try_mutate(actor_id, |maybe| -> DispatchResult { let hot = maybe.as_mut().ok_or(Error::::ActorNotFound)?; if schedule_changed { hot.schedule_anchor = schedule_anchor; hot.trigger_runtime_state = Self::installed_trigger_runtime_state( &contract.trigger, - cadence_anchor_tick, + temporal_anchor_tick, crossing_state, )?; hot.terminal_at = contract @@ -2383,13 +4359,17 @@ pub mod pallet { Self::record_control_mutation(actor_id, now)?; Ok(()) })?; + // Crossing compilation binds the newly installed runtime phase to the replacement + // admission identity, so publish hot schedule authority before storing its Contract. + Self::store_actor_contract(actor_id, contract.clone())?; ActorFunding::::insert(actor_id, funding_state); Self::deposit_event(Event::ContractUpdated { actor_id }); #[cfg(test)] crate::mock::control_atomicity_checkpoint(actor_id)?; - if schedule_changed || continuation_cancelled { + if schedule_changed || run_cancelled { Self::prime_actor_schedule(actor_id).map_err(Self::placement_error)?; } + Self::reconcile_actor_state_hold(actor_id)?; Ok(()) }) } @@ -2465,7 +4445,7 @@ pub mod pallet { } LoadedActorStateOf::Corrupt => return Err(Error::::ActorInvariant.into()), }; - let continuation = state.continuation; + let continuation = state.run_state; let instance = Self::derive_active_actor_view(state.identity, state.hot, state.contract); if let Some(reason) = Self::classify_actor_loaded(&instance, continuation.as_ref()) .map_err(Self::classification_dispatch_error)? @@ -2568,34 +4548,91 @@ pub mod pallet { } #[pallet::call_index(19)] - #[pallet::weight(T::WeightInfo::continuation_cancel())] - pub fn cancel_continuation(origin: OriginFor, actor_id: ActorId) -> DispatchResult { + #[pallet::weight(T::WeightInfo::run_cancel())] + pub fn cancel_run(origin: OriginFor, actor_id: ActorId) -> DispatchResult { let state = Self::active_actor_state_for_control(actor_id)?; - let continuation = state.continuation; + let run_state = state.run_state; let instance = Self::derive_active_actor_view(state.identity, state.hot, state.contract); Self::ensure_control_origin(origin, &instance)?; ensure!( instance.mutability == Mutability::Mutable, Error::::ImmutableActor ); - if Self::expiry_substitution_due_loaded(&instance, continuation.as_ref())? { + if Self::expiry_substitution_due_loaded(&instance, run_state.as_ref())? { return Self::finalize_actor(actor_id, &instance, CloseReason::WindowExpired); } ensure!( - instance.cycle_state == CycleState::Suspended, - Error::::ContinuationNotFound + matches!( + instance.cycle_state, + CycleState::Running | CycleState::Suspended + ), + Error::::ActorRunNotFound ); let now = frame_system::Pallet::::block_number(); Self::ensure_control_mutation_allowed(&instance, now)?; Self::with_control_transaction(|| { ensure!( - Self::cancel_continuation_internal(actor_id, CancellationReason::Explicit, None)?, - Error::::ContinuationNotFound + Self::cancel_run_internal(actor_id, CancellationReason::Explicit, None)?, + Error::::ActorRunNotFound ); Self::record_control_mutation(actor_id, now)?; Self::prime_actor_schedule(actor_id).map_err(Self::placement_error) }) } + + #[pallet::call_index(23)] + #[pallet::weight(( + T::BlockResourceBudget::get() + .limits() + .actor_control() + .saturating_add(T::BlockResourceBudget::get().limits().actor_base_turn()), + DispatchClass::Mandatory, + Pays::No, + ))] + pub fn actor_prepass(origin: OriginFor) -> DispatchResultWithPostInfo { + ensure_none(origin)?; + let now = frame_system::Pallet::::block_number(); + let actual_weight = Self::execute_mandatory_prepass(now)?; + Ok(PostDispatchInfo { + actual_weight: Some(actual_weight), + pays_fee: Pays::No, + }) + } + } + + #[pallet::inherent] + impl ProvideInherent for Pallet { + type Call = Call; + type Error = ActorPrepassInherentError; + const INHERENT_IDENTIFIER: InherentIdentifier = ACTOR_PREPASS_INHERENT_IDENTIFIER; + + fn create_inherent(data: &InherentData) -> Option { + data + .get_data::(&Self::INHERENT_IDENTIFIER) + .ok() + .flatten() + .filter(|provided| provided.version == ACTOR_PREPASS_INHERENT_VERSION) + .map(|_| Call::actor_prepass {}) + } + + fn is_inherent_required(_data: &InherentData) -> Result, Self::Error> { + Ok(Some(ActorPrepassInherentError::MissingCall)) + } + + fn check_inherent(call: &Self::Call, data: &InherentData) -> Result<(), Self::Error> { + if !Self::is_inherent(call) { + return Ok(()); + } + match data.get_data::(&Self::INHERENT_IDENTIFIER) { + Ok(Some(provided)) if provided.version == ACTOR_PREPASS_INHERENT_VERSION => Ok(()), + Ok(Some(_)) => Err(ActorPrepassInherentError::UnsupportedVersion), + _ => Err(ActorPrepassInherentError::MissingData), + } + } + + fn is_inherent(call: &Self::Call) -> bool { + matches!(call, Call::actor_prepass {}) + } } impl Pallet { @@ -2651,11 +4688,10 @@ pub mod pallet { .retry_max_attempts() .is_some() }) { - let snapshot_entries = Self::opening_surfaces(contract_steps, start_cursor).len() as u32; upper = upper.saturating_add( - T::WeightInfo::continuation_suspend(snapshot_entries) - .max(T::WeightInfo::continuation_complete()) - .max(T::WeightInfo::continuation_cancel()), + T::WeightInfo::run_suspend() + .max(T::WeightInfo::run_complete()) + .max(T::WeightInfo::run_cancel()), ); } upper @@ -2675,21 +4711,13 @@ pub mod pallet { ) -> Result, Error> { let mut inputs = BoundedVec::default(); for step in contract_steps { - let evaluation = if actor_type == ActorType::User { - Self::compute_eval_fee_checked( - step - .precondition - .as_ref() - .map_or(0, Precondition::evaluation_units), - )? - } else { - Zero::zero() - }; - let execution = if actor_type == ActorType::User { - T::WeightToFee::weight_to_fee(&Self::weight_upper_bound(&step.task)) - } else { - Zero::zero() - }; + let evaluation = Zero::zero(); + let execution = + if actor_type == ActorType::User && !matches!(step.task, super::types::Task::StopCycle) { + T::WeightToFee::weight_to_fee(&Self::weight_upper_bound(&step.task)) + } else { + Zero::zero() + }; inputs .try_push(FeeEnvelopeInput { evaluation, @@ -2699,12 +4727,13 @@ pub mod pallet { } compose_attempt_fee_envelope(actor_type, &inputs, start_cursor).map_err(|error| match error { FeeEnvelopeError::CursorOutOfBounds | FeeEnvelopeError::ReservationUnderflow => { - Error::::ContinuationInvariant + Error::::ActorRunInvariant } FeeEnvelopeError::Overflow => Error::::AdmissionBoundOverflow, }) } + #[cfg(test)] pub(crate) fn attempt_weight_upper_bound( instance: &ActiveActorViewOf, start_cursor: usize, @@ -2716,10 +4745,10 @@ pub mod pallet { ); if instance.cycle_state == CycleState::Suspended { let suffix_steps = instance.steps.len().saturating_sub(start_cursor) as u32; - // Retry and terminal transition touch the same bounded Continuation value. The transition + // Retry and terminal transition touch the same bounded Actor run value. The transition // envelope already carries its maximum proof, so only incremental RefTime composes here. - let retry = T::WeightInfo::continuation_retry(); - let suffix_admission = T::WeightInfo::continuation_suffix_admission(suffix_steps); + let retry = T::WeightInfo::run_retry(); + let suffix_admission = T::WeightInfo::run_suffix_admission(suffix_steps); upper = upper .saturating_add(Weight::from_parts(retry.ref_time(), 0)) .saturating_add(Weight::from_parts(suffix_admission.ref_time(), 0)); @@ -2727,19 +4756,6 @@ pub mod pallet { upper } - pub(crate) fn attempt_fee_upper_bound( - instance: &ActiveActorViewOf, - start_cursor: usize, - ) -> BalanceOf { - Self::attempt_fee_envelope( - instance.actor_class.actor_type(), - &instance.steps, - start_cursor, - ) - .expect("admitted execution plans have a checked fee envelope") - .total - } - pub(crate) fn close_cycle_weight_upper_bound(_instance: &ActiveActorViewOf) -> Weight { Self::close_cleanup_weight_upper() } @@ -2747,36 +4763,71 @@ pub mod pallet { /// Upper-bounds one prospective run plus pure terminal cleanup after the baseline scheduler /// envelope. Independently metered durable housekeeping may defer this work across blocks. pub fn contract_steps_admission_weight_upper( - actor_type: ActorType, + _actor_type: ActorType, contract_steps: &ContractSteps, ) -> Weight { - let funding_count = Self::derive_funding_tracked_assets(contract_steps) - .map(|assets| assets.len() as u32) - .unwrap_or_else(|_| T::MaxFundingTrackedAssets::get()); - let snapshot_open = if funding_count == 0 { - Weight::zero() - } else { - T::WeightInfo::funding_snapshot_open(funding_count) - }; - let continuation_retry = if (0..contract_steps.len()).any(|step_index| { - contract_steps[step_index] - .on_error - .retry_max_attempts() - .is_some() - }) { - let retry = T::WeightInfo::continuation_retry(); - let suffix = T::WeightInfo::continuation_suffix_admission(contract_steps.len() as u32); - Weight::from_parts(retry.ref_time().saturating_add(suffix.ref_time()), 0) + let maximum_step = if contract_steps.is_empty() { + T::WeightInfo::scheduler_inner_zero_step_complete() } else { - Weight::zero() + Self::derive_step_resource_envelopes(&ActorContract { + trigger: Trigger::manual(), + cooldown_blocks: Zero::zero(), + window: None, + steps: contract_steps.clone(), + funding: FundingSourcePolicy::OwnerOnly, + completion: CompletionPolicy::Persistent, + auto_close_at_cycle_nonce: None, + }) + .map(|resources| { + resources + .into_iter() + .fold(Weight::zero(), |maximum, resource| { + let current = resource.control.saturating_add(resource.effect); + Weight::from_parts( + maximum.ref_time().max(current.ref_time()), + maximum.proof_size().max(current.proof_size()), + ) + }) + }) + .unwrap_or(Weight::MAX) }; Self::scheduler_admission_overhead() - .saturating_add(Self::compute_cycle_weight_upper(actor_type, contract_steps)) - .saturating_add(continuation_retry) - .saturating_add(snapshot_open) + .saturating_add(maximum_step) .saturating_add(Self::close_cleanup_weight_upper()) } + fn finalize_empty_actor_drain(now: BlockNumberFor) { + let Some(mut state) = CurrentBlockResourceState::::get() else { + return; + }; + let budget = T::BlockResourceBudget::get(); + if state.ensure_block(now).is_err() + || state.begin_drain().is_err() + || state.finish_drain(budget, budget.fixed_envelope()).is_err() + { + state.halt_optional_actor_work(); + } else if let Ok(snapshot) = state.finalized_snapshot() { + FinalizedBlockResourceTelemetry::::put(snapshot); + } + CurrentBlockResourceState::::put(state); + } + + fn settle_on_idle_control( + authority: &mut Option<( + BlockResourceState>, + BlockResourceReservation, + )>, + actual: Weight, + ) { + let Some((mut state, mut reservation)) = authority.take() else { + return; + }; + if state.settle(&mut reservation, actual).is_err() { + state.halt_optional_actor_work(); + } + CurrentBlockResourceState::::put(state); + } + pub fn materialization_weight_limit() -> Weight { T::WakeupWeightLimit::get() .saturating_add(T::CrossingWorkerWeightLimit::get()) @@ -2802,6 +4853,22 @@ pub mod pallet { .unwrap_or_else(Weight::zero) } + pub fn observation_fanout_ordinary_weight_upper() -> Weight { + [ + T::WeightInfo::observation_fanout_page(), + T::WeightInfo::observation_fanout_wakeup_page(), + T::WeightInfo::observation_fanout_coalesced_page(), + T::WeightInfo::observation_fanout_blocked_page(), + ] + .into_iter() + .fold(Weight::zero(), |maximum, weight| { + Weight::from_parts( + maximum.ref_time().max(weight.ref_time()), + maximum.proof_size().max(weight.proof_size()), + ) + }) + } + pub fn materialization_family_minimum(family: u8) -> Weight { match family { 0 => T::WeightInfo::scheduler_wakeup_cursor_worker_future() @@ -2828,9 +4895,11 @@ pub mod pallet { .max(T::WeightInfo::crossing_skip_pair_probe()), ) .saturating_add(branch) + .saturating_add(T::WeightInfo::record_crossing_worker_fault()) } 2 => T::WeightInfo::observation_fanout_base() - .saturating_add(T::WeightInfo::observation_fanout_page()), + .saturating_add(Self::observation_fanout_ordinary_weight_upper()) + .saturating_add(T::WeightInfo::record_observation_fanout_worker_fault()), _ => Weight::zero(), } } @@ -2858,6 +4927,21 @@ pub mod pallet { .all_lte(actor_service), Error::::ContractStepsExceedOnIdleBudget ); + if actor_type == ActorType::User { + let resources = Self::derive_step_resource_envelopes(&ActorContract { + trigger: Trigger::manual(), + cooldown_blocks: Zero::zero(), + window: None, + steps: contract_steps.clone(), + funding: FundingSourcePolicy::OwnerOnly, + completion: CompletionPolicy::Persistent, + auto_close_at_cycle_nonce: None, + }) + .ok_or(Error::::ActorRunInvariant)?; + for resource in resources.iter(/* deos-bypass: bounded-iter */) { + Self::maximum_current_step_fee(actor_type, *resource)?; + } + } Ok(()) } @@ -2884,66 +4968,542 @@ pub mod pallet { true } - fn owner_slot_is_set(bitmap: &OwnerSlotBitmap, owner_slot: u8) -> bool { - let byte = (owner_slot / 8) as usize; - let bit = owner_slot % 8; - bitmap[byte] & (1u8 << bit) != 0 + fn owner_slot_is_set(bitmap: &OwnerSlotBitmap, owner_slot: u8) -> bool { + let byte = (owner_slot / 8) as usize; + let bit = owner_slot % 8; + bitmap[byte] & (1u8 << bit) != 0 + } + + fn set_owner_slot(bitmap: &mut OwnerSlotBitmap, owner_slot: u8) { + let byte = (owner_slot / 8) as usize; + let bit = owner_slot % 8; + bitmap[byte] |= 1u8 << bit; + } + + fn clear_owner_slot(bitmap: &mut OwnerSlotBitmap, owner_slot: u8) { + let byte = (owner_slot / 8) as usize; + let bit = owner_slot % 8; + bitmap[byte] &= !(1u8 << bit); + } + + fn owner_slot_bitmap_is_empty(bitmap: &OwnerSlotBitmap) -> bool { + for byte in bitmap.as_slice() { + if *byte != 0 { + return false; + } + } + true + } + + fn state_hold_component(encoded_bytes: usize) -> Result> { + if encoded_bytes == 0 { + return Ok(T::Balance::zero()); + } + let encoded_bytes = + u32::try_from(encoded_bytes).map_err(|_| Error::::StateHoldOverflow)?; + let bytes: T::Balance = encoded_bytes.into(); + T::ActorStateHoldPerByte::get() + .checked_mul(&bytes) + .and_then(|priced_bytes| priced_bytes.checked_add(&T::ActorStateHoldBase::get())) + .ok_or(Error::::StateHoldOverflow) + } + + fn state_hold_total(breakdown: &ActorStateHoldBreakdownOf) -> Result> { + [ + breakdown.identity, + breakdown.contract_head, + breakdown.contract_body, + breakdown.detector, + breakdown.funding, + breakdown.run, + ] + .into_iter() + .try_fold(T::Balance::zero(), |total, component| { + total + .checked_add(&component) + .ok_or(Error::::StateHoldOverflow) + }) + } + + fn actor_state_hold_quote( + actor_id: ActorId, + actor_type: ActorType, + ) -> Result, ActorCostQuoteError> { + let breakdown = if actor_type == ActorType::System { + if ActorStateHolds::::contains_key(actor_id) { + return Err(ActorCostQuoteError::ActorInvariant); + } + ActorStateHoldBreakdown { + identity: T::Balance::zero(), + contract_head: T::Balance::zero(), + contract_body: T::Balance::zero(), + detector: T::Balance::zero(), + funding: T::Balance::zero(), + run: T::Balance::zero(), + } + } else { + ActorStateHolds::::get(actor_id) + .ok_or(ActorCostQuoteError::ActorInvariant)? + .breakdown + }; + let total = + Self::state_hold_total(&breakdown).map_err(|_| ActorCostQuoteError::ComputationOverflow)?; + Ok(ActorStateHoldQuote { + exempt: actor_type == ActorType::System, + base_per_component: T::ActorStateHoldBase::get(), + per_encoded_byte: T::ActorStateHoldPerByte::get(), + breakdown, + total, + }) + } + + fn add_state_hold_encoded_size( + total: &mut usize, + value: &Value, + ) -> Result<(), Error> { + *total = total + .checked_add(codec::Encode::encoded_size(value)) + .ok_or(Error::::StateHoldOverflow)?; + Ok(()) + } + + fn derive_actor_state_hold( + actor_id: ActorId, + identity: &ActorIdentityOf, + ) -> Result, Error> { + let mut identity_bytes = 0usize; + Self::add_state_hold_encoded_size(&mut identity_bytes, &actor_id)?; + Self::add_state_hold_encoded_size(&mut identity_bytes, identity)?; + Self::add_state_hold_encoded_size(&mut identity_bytes, &identity.sovereign_account)?; + + let mut breakdown = ActorStateHoldBreakdown { + identity: Self::state_hold_component(identity_bytes)?, + contract_head: T::Balance::zero(), + contract_body: T::Balance::zero(), + detector: T::Balance::zero(), + funding: T::Balance::zero(), + run: T::Balance::zero(), + }; + if identity.actor_class.actor_type() == ActorType::System { + return Ok(ActorStateHoldBreakdown { + identity: T::Balance::zero(), + ..breakdown + }); + } + + let Some(hot) = ActorHot::::get(actor_id) else { + ensure!( + !ActorContractHeads::::contains_key(actor_id) + && !ActorAdmissionCertificates::::contains_key(actor_id) + && !ActorFunding::::contains_key(actor_id) + && !ActorRunStateStore::::contains_key(actor_id), + Error::::StateHoldInvariant + ); + return Ok(breakdown); + }; + let head = ActorContractHeads::::get(actor_id).ok_or(Error::::StateHoldInvariant)?; + let admission = + ActorAdmissionCertificates::::get(actor_id).ok_or(Error::::StateHoldInvariant)?; + let funding = ActorFunding::::get(actor_id).ok_or(Error::::StateHoldInvariant)?; + + let mut head_bytes = as codec::MaxEncodedLen>::max_encoded_len(); + Self::add_state_hold_encoded_size(&mut head_bytes, &head)?; + Self::add_state_hold_encoded_size(&mut head_bytes, &admission)?; + breakdown.contract_head = Self::state_hold_component(head_bytes)?; + + let chunk_count = head + .header + .step_count + .saturating_sub(1) + .div_ceil(MAX_STEPS_PER_TAIL_CHUNK); + let mut body_bytes = 0usize; + for chunk_index in 0..chunk_count { + let chunk = ActorContractTailChunks::::get(actor_id, chunk_index) + .ok_or(Error::::StateHoldInvariant)?; + Self::add_state_hold_encoded_size(&mut body_bytes, &chunk)?; + } + breakdown.contract_body = Self::state_hold_component(body_bytes)?; + + let mut detector_bytes = 0usize; + let activation = ActorActivationAuthorities::::get(actor_id); + if let Some(activation) = &activation { + Self::add_state_hold_encoded_size(&mut detector_bytes, activation)?; + } + if let Some(feeds) = ActorObservationFeeds::::get(actor_id) { + Self::add_state_hold_encoded_size(&mut detector_bytes, &feeds)?; + } + if let Some(slot) = ObservationSubscriptionSlot::::get(actor_id) { + Self::add_state_hold_encoded_size(&mut detector_bytes, &slot)?; + } + let crossing_locator = CrossingMemberships::::get(actor_id); + if let Some(locator) = &crossing_locator { + Self::add_state_hold_encoded_size(&mut detector_bytes, locator)?; + } + if activation.is_some() || crossing_locator.is_some() { + Self::add_state_hold_encoded_size(&mut detector_bytes, &())?; + } + if let Some(pointer) = hot.trigger_wakeup_pointer { + Self::add_state_hold_encoded_size(&mut detector_bytes, &pointer)?; + } + breakdown.detector = Self::state_hold_component(detector_bytes)?; + breakdown.funding = Self::state_hold_component(codec::Encode::encoded_size(&funding))?; + breakdown.run = Self::state_hold_component( + as codec::MaxEncodedLen>::max_encoded_len(), + )?; + Ok(breakdown) + } + + pub(crate) fn ensure_funding_state_hold_capacity( + actor_id: ActorId, + identity: &ActorIdentityOf, + prospective_funding: &ActorFundingStateOf, + ) -> DispatchResult { + if identity.actor_class.actor_type() == ActorType::System { + return Ok(()); + } + let existing = ActorStateHolds::::get(actor_id).ok_or(Error::::StateHoldInvariant)?; + ensure!( + existing.owner == identity.owner, + Error::::StateHoldInvariant + ); + let prospective = + Self::state_hold_component(codec::Encode::encoded_size(prospective_funding))?; + if prospective > existing.breakdown.funding { + let increase = prospective + .checked_sub(&existing.breakdown.funding) + .ok_or(Error::::StateHoldOverflow)?; + let reason: T::RuntimeHoldReason = HoldReason::ActorState.into(); + T::StateHoldCurrency::ensure_can_hold(&reason, &identity.owner, increase) + .map_err(|_| Error::::StateHoldUnavailable)?; + } + Ok(()) + } + + pub(crate) fn reconcile_actor_state_hold(actor_id: ActorId) -> DispatchResult { + let existing = ActorStateHolds::::get(actor_id); + let target = match ActorIdentities::::get(actor_id) { + Some(identity) if identity.actor_class.actor_type() == ActorType::User => Some(( + identity.owner.clone(), + Self::derive_actor_state_hold(actor_id, &identity)?, + )), + Some(_) | None => None, + }; + let (owner, target_breakdown) = match (existing.as_ref(), target) { + (Some(existing), Some((owner, breakdown))) => { + ensure!(existing.owner == owner, Error::::StateHoldInvariant); + (owner, breakdown) + } + (None, Some(target)) => target, + (Some(existing), None) => ( + existing.owner.clone(), + ActorStateHoldBreakdown { + identity: T::Balance::zero(), + contract_head: T::Balance::zero(), + contract_body: T::Balance::zero(), + detector: T::Balance::zero(), + funding: T::Balance::zero(), + run: T::Balance::zero(), + }, + ), + (None, None) => return Ok(()), + }; + if existing + .as_ref() + .is_some_and(|record| record.owner == owner && record.breakdown == target_breakdown) + { + return Ok(()); + } + let old_total = existing + .as_ref() + .map(|record| Self::state_hold_total(&record.breakdown)) + .transpose()? + .unwrap_or_else(T::Balance::zero); + let target_total = Self::state_hold_total(&target_breakdown)?; + let reason: T::RuntimeHoldReason = HoldReason::ActorState.into(); + if target_total > old_total { + let increase = target_total + .checked_sub(&old_total) + .ok_or(Error::::StateHoldOverflow)?; + T::StateHoldCurrency::hold(&reason, &owner, increase) + .map_err(|_| Error::::StateHoldUnavailable)?; + } else if old_total > target_total { + let decrease = old_total + .checked_sub(&target_total) + .ok_or(Error::::StateHoldOverflow)?; + let released = T::StateHoldCurrency::release(&reason, &owner, decrease, Precision::Exact) + .map_err(|_| Error::::StateHoldInvariant)?; + ensure!(released == decrease, Error::::StateHoldInvariant); + } + if target_total.is_zero() { + ActorStateHolds::::remove(actor_id); + } else { + ActorStateHolds::::insert( + actor_id, + ActorStateHoldRecord { + owner, + breakdown: target_breakdown, + }, + ); + } + Ok(()) + } + + fn charge_creation_fee(owner: &T::AccountId) -> DispatchResult { + let creation_fee = T::ActorCreationFee::get(); + if creation_fee.is_zero() { + return Ok(()); + } + let native = T::FeeNativeAssetId::get(); + let fee_sink = T::FeeSink::get(); + T::FeeCollector::collect_fee(owner, &fee_sink, native, creation_fee) + .map_err(|_| Error::::InsufficientFee.into()) + } + + fn ensure_trigger_occurrence_capacity( + actor_type: ActorType, + sovereign_account: &T::AccountId, + breakdown: TriggerFeeBreakdown, + ) -> DispatchResult { + ensure!( + Self::trigger_occurrence_capacity_sufficient(actor_type, sovereign_account, breakdown,)?, + Error::::InsufficientFee + ); + Ok(()) + } + + pub(crate) fn trigger_occurrence_capacity_sufficient( + actor_type: ActorType, + sovereign_account: &T::AccountId, + breakdown: TriggerFeeBreakdown, + ) -> Result> { + if actor_type == ActorType::System || breakdown.trigger_fee.is_zero() { + return Ok(true); + } + let required = T::MinUserBalance::get() + .checked_add(&breakdown.trigger_fee) + .ok_or(Error::::AdmissionBoundOverflow)?; + Ok(T::AssetOps::balance(sovereign_account, T::FeeNativeAssetId::get()) >= required) + } + + fn charge_trigger_occurrence( + actor_type: ActorType, + sovereign_account: &T::AccountId, + breakdown: TriggerFeeBreakdown, + ) -> DispatchResult { + Self::ensure_trigger_occurrence_capacity(actor_type, sovereign_account, breakdown)?; + if actor_type == ActorType::System || breakdown.trigger_fee.is_zero() { + return Ok(()); + } + let native = T::FeeNativeAssetId::get(); + let fee_sink = T::FeeSink::get(); + T::FeeCollector::collect_fee(sovereign_account, &fee_sink, native, breakdown.trigger_fee) + .map_err(|_| Error::::InsufficientFee.into()) + } + + pub(crate) fn commit_trigger_occurrence( + actor_id: ActorId, + actor_type: ActorType, + sovereign_account: &T::AccountId, + breakdown: TriggerFeeBreakdown, + _cause_provenance: TriggerCauseProvenance, + ) -> Result, DispatchError> { + if ActorHot::::get(actor_id).is_some_and(|hot| hot.pending_signal) { + return Ok(None); + } + Self::ensure_trigger_occurrence_capacity(actor_type, sovereign_account, breakdown)?; + let outcome = Self::request_activation(actor_id).map_err(Self::activation_failure_error)?; + if matches!(outcome, crate::scheduler::ActivationOutcome::Closed) { + return Ok(None); + } + Self::charge_trigger_occurrence(actor_type, sovereign_account, breakdown)?; + Self::deposit_event(Event::TriggerOccurrenceProcessed { + actor_id, + trigger_family: breakdown.trigger_family, + fee: breakdown.trigger_fee, + }); + Ok(Some(outcome)) + } + + pub(crate) fn try_commit_automatic_trigger_occurrence( + actor_id: ActorId, + actor_type: ActorType, + sovereign_account: &T::AccountId, + breakdown: TriggerFeeBreakdown, + cause_provenance: TriggerCauseProvenance, + ) -> Result, DispatchError> { + polkadot_sdk::frame_support::storage::with_transaction(|| { + match Self::commit_trigger_occurrence( + actor_id, + actor_type, + sovereign_account, + breakdown, + cause_provenance, + ) { + Ok(outcome) => { + polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(outcome)) + } + Err(error) if error == Error::::InsufficientFee.into() => { + polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Ok(None)) + } + Err(error) => { + polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)) + } + } + }) + } + + pub(crate) fn try_charge_prechecked_automatic_trigger_occurrence( + actor_type: ActorType, + sovereign_account: &T::AccountId, + breakdown: TriggerFeeBreakdown, + ) -> Result { + polkadot_sdk::frame_support::storage::with_transaction(|| { + if actor_type == ActorType::System || breakdown.trigger_fee.is_zero() { + return polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(true)); + } + let result = T::FeeCollector::collect_fee( + sovereign_account, + &T::FeeSink::get(), + T::FeeNativeAssetId::get(), + breakdown.trigger_fee, + ); + match result { + Ok(()) => polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(true)), + Err(_) => polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Ok(false)), + } + }) + } + + pub(crate) fn try_charge_automatic_trigger_occurrence( + actor_type: ActorType, + sovereign_account: &T::AccountId, + breakdown: TriggerFeeBreakdown, + ) -> Result { + polkadot_sdk::frame_support::storage::with_transaction(|| { + match Self::charge_trigger_occurrence(actor_type, sovereign_account, breakdown) { + Ok(()) => polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(true)), + Err(error) if error == Error::::InsufficientFee.into() => { + polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Ok(false)) + } + Err(error) => { + polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)) + } + } + }) } - fn set_owner_slot(bitmap: &mut OwnerSlotBitmap, owner_slot: u8) { - let byte = (owner_slot / 8) as usize; - let bit = owner_slot % 8; - bitmap[byte] |= 1u8 << bit; + pub(crate) fn pipeline_fee_for_actor( + actor_id: ActorId, + actor_type: ActorType, + ) -> Result, Error> { + let head = ActorContractHeads::::get(actor_id).ok_or(Error::::ActorInvariant)?; + Self::pipeline_fee_breakdown(actor_type, head.header.pipeline_machine_envelope) } - fn clear_owner_slot(bitmap: &mut OwnerSlotBitmap, owner_slot: u8) { - let byte = (owner_slot / 8) as usize; - let bit = owner_slot % 8; - bitmap[byte] &= !(1u8 << bit); + pub(crate) fn pipeline_capacity_sufficient( + actor_id: ActorId, + actor_type: ActorType, + sovereign_account: &T::AccountId, + ) -> Result> { + if actor_type == ActorType::System { + return Ok(true); + } + let breakdown = Self::pipeline_fee_for_actor(actor_id, actor_type)?; + let required = T::MinUserBalance::get() + .checked_add(&breakdown.total_fee) + .ok_or(Error::::AdmissionBoundOverflow)?; + Ok(T::AssetOps::balance(sovereign_account, T::FeeNativeAssetId::get()) >= required) } - fn owner_slot_bitmap_is_empty(bitmap: &OwnerSlotBitmap) -> bool { - for byte in bitmap.as_slice() { - if *byte != 0 { - return false; - } + pub(crate) fn action_capacity_sufficient( + actor_type: ActorType, + sovereign_account: &T::AccountId, + step: &StepOf, + resources: ActorStepResourceEnvelope, + ) -> Result> { + if actor_type == ActorType::System { + return Ok(true); } - true + let fee = Self::maximum_current_action_fee(actor_type, step, resources)?; + if fee.total_fee.is_zero() { + return Ok(true); + } + let required = T::MinUserBalance::get() + .checked_add(&fee.total_fee) + .ok_or(Error::::AdmissionBoundOverflow)?; + Ok(T::AssetOps::balance(sovereign_account, T::FeeNativeAssetId::get()) >= required) } - fn charge_creation_fee(owner: &T::AccountId) -> DispatchResult { - let creation_fee = T::ActorCreationFee::get(); - if creation_fee.is_zero() { - return Ok(()); + pub(crate) fn collect_pipeline_fee( + actor_id: ActorId, + actor_type: ActorType, + sovereign_account: &T::AccountId, + ) -> Result, DispatchError> { + let breakdown = Self::pipeline_fee_for_actor(actor_id, actor_type)?; + if actor_type == ActorType::System || breakdown.total_fee.is_zero() { + return Ok(breakdown); } + ensure!( + Self::pipeline_capacity_sufficient(actor_id, actor_type, sovereign_account)?, + Error::::InsufficientFee + ); let native = T::FeeNativeAssetId::get(); - let fee_sink = T::FeeSink::get(); - T::FeeCollector::collect_fee(owner, &fee_sink, native, creation_fee) - .map_err(|_| Error::::InsufficientFee.into()) + T::FeeCollector::collect_fee( + sovereign_account, + &T::FeeSink::get(), + native, + breakdown.total_fee, + ) + .map_err(|_| Error::::InsufficientFee)?; + Ok(breakdown) } - /// User Active creation/activation prefunding requirement (spec 7.1): the prospective/current - /// sovereign fee-native balance must cover `MinUserBalance + attempt_fee_envelope(plan, 0, User).total` - /// so the first opening can charge the whole cycle fee while preserving `MinUserBalance`. - fn user_active_prefunding_requirement( + #[cfg(test)] + pub(crate) fn maximum_contract_step_fee( + actor_type: ActorType, contract_steps: &ContractSteps, - ) -> Result, Error> { - let envelope_total = Self::attempt_fee_envelope(ActorType::User, contract_steps, 0)?.total; - T::MinUserBalance::get() - .checked_add(&envelope_total) - .ok_or(Error::::AdmissionBoundOverflow) + cursor: usize, + ) -> Result, Error> { + let resources = Self::derive_step_resource_envelopes(&ActorContract { + trigger: Trigger::manual(), + cooldown_blocks: Zero::zero(), + window: None, + steps: contract_steps.clone(), + funding: FundingSourcePolicy::OwnerOnly, + completion: CompletionPolicy::Persistent, + auto_close_at_cycle_nonce: None, + }) + .and_then(|resources| resources.get(cursor).copied()) + .ok_or(Error::::ActorRunInvariant)?; + let step = contract_steps + .get(cursor) + .ok_or(Error::::ActorRunInvariant)?; + Self::maximum_current_action_fee(actor_type, step, resources) } - fn ensure_user_active_prefunding( - sovereign_account: &T::AccountId, + /// Returns ledger minimum plus the generated Pipeline Machine and cleanup charge. + /// Trigger-family pricing is composed separately at ready Opening. + pub fn user_pipeline_machine_capacity_requirement( contract_steps: &ContractSteps, - ) -> DispatchResult { - let required = Self::user_active_prefunding_requirement(contract_steps)?; - ensure!( - T::AssetOps::balance(sovereign_account, T::FeeNativeAssetId::get()) >= required, - Error::::InsufficientBalance - ); - Ok(()) + ) -> Result, Error> { + let contract = ActorContract { + trigger: Trigger::manual(), + cooldown_blocks: Zero::zero(), + window: None, + steps: contract_steps.clone(), + funding: FundingSourcePolicy::OwnerOnly, + completion: CompletionPolicy::Persistent, + auto_close_at_cycle_nonce: None, + }; + let resources = + Self::derive_step_resource_envelopes(&contract).ok_or(Error::::ActorRunInvariant)?; + let envelope = + Self::derive_pipeline_machine_envelope(ActorType::User, contract_steps, &resources)?; + envelope + .pipeline_machine_fee_upper + .checked_add(&envelope.cleanup_fee_upper) + .and_then(|cycle_requirement| T::MinUserBalance::get().checked_add(&cycle_requirement)) + .ok_or(Error::::AdmissionBoundOverflow) } pub fn sovereign_account_id(owner: &T::AccountId, owner_slot: u8) -> T::AccountId { @@ -3146,6 +5706,9 @@ pub mod pallet { if actor_type == ActorType::User || requested_system_sovereign_id.is_none() { frame_system::Pallet::::inc_providers(&sovereign_account); } + if let Err(error) = Self::reconcile_actor_state_hold(actor_id) { + return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)); + } Self::deposit_event(Event::ActorCreated { actor_id, owner, @@ -3225,7 +5788,6 @@ pub mod pallet { !GlobalCircuitBreaker::::get(), Error::::GlobalCircuitBreakerActive ); - ensure!(!contract.steps.is_empty(), Error::::EmptyContractSteps); Self::canonicalize_preconditions(&mut contract.steps)?; ensure!( (contract.steps.len() as u32) <= T::MaxContractSteps::get(), @@ -3333,29 +5895,14 @@ pub mod pallet { }, }; if actor_type == ActorType::User { - // Spec 7.1: the allocated sovereign fee-native balance must cover - // `MinUserBalance + attempt_fee_envelope(plan, 0, User).total` before the opening - // fee or Active state commits; Dormant creation remains unfunded. - if let Err(error) = - Self::ensure_user_active_prefunding(&sovereign_account, &contract.steps) - { - return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)); - } + // Creation establishes process state only. Sovereign activation capacity is checked + // when a Trigger-owned Opening becomes ready. if let Err(error) = Self::charge_creation_fee(&owner) { return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)); } } - let trigger_state_bond = if actor_type == ActorType::User { - T::TriggerStateBond::amount(&contract.trigger) - } else { - Zero::zero() - }; - if let Err(error) = T::TriggerStateBond::set_bond(&owner, Zero::zero(), trigger_state_bond) - { - return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)); - } let schedule_anchor = Self::schedule_anchor_at(contract.window, now); - let cadence_anchor_tick = match Self::cadence_anchor_tick(&contract.trigger) { + let temporal_anchor_tick = match Self::temporal_anchor_tick(&contract.trigger) { Ok(anchor) => anchor, Err(error) => { return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err( @@ -3372,7 +5919,7 @@ pub mod pallet { last_control_mutation_block: now, }; let trigger_runtime_state = - Self::provisional_trigger_runtime_state(&contract.trigger, cadence_anchor_tick); + Self::provisional_trigger_runtime_state(&contract.trigger, temporal_anchor_tick); let hot = ActorHotState { lifecycle: ActiveLifecycle::Active, cycle_state: CycleState::Idle, @@ -3381,6 +5928,7 @@ pub mod pallet { pending_signal: false, queue_ticket: None, wakeup_pointer: None, + trigger_wakeup_pointer: None, terminal_at: contract .window .map(|window| Self::window_terminal_at(&window)), @@ -3402,7 +5950,6 @@ pub mod pallet { ActorFundingState { funding_accumulated: Default::default(), funding_tracked_assets, - trigger_state_bond, }, ); if let Err(error) = ActiveActorCount::::try_mutate(|count| -> DispatchResult { @@ -3458,6 +6005,9 @@ pub mod pallet { if let Err(error) = Self::prime_actor_schedule(actor_id).map_err(Self::placement_error) { return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)); } + if let Err(error) = Self::reconcile_actor_state_hold(actor_id) { + return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)); + } polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(())) }) } @@ -3476,7 +6026,6 @@ pub mod pallet { Error::::ImmutableActor ); let actor_type = identity.actor_class.actor_type(); - ensure!(!contract.steps.is_empty(), Error::::EmptyContractSteps); Self::canonicalize_preconditions(&mut contract.steps)?; ensure!( (contract.steps.len() as u32) <= T::MaxContractSteps::get(), @@ -3510,9 +6059,6 @@ pub mod pallet { Self::active_instance_count() < Self::effective_active_actor_limit(), Error::::ActiveActorCapacityExceeded ); - if actor_type == ActorType::User { - Self::ensure_user_active_prefunding(&identity.sovereign_account, &contract.steps)?; - } let now = frame_system::Pallet::::block_number(); ensure!( identity.last_control_mutation_block != now, @@ -3523,15 +6069,10 @@ pub mod pallet { // state has no last_cycle_block, so cooldown/cadence use this conservative anchor // rather than block zero (spec 4.3.3). let schedule_anchor = Self::schedule_anchor_at(contract.window, now); - let cadence_anchor_tick = - Self::cadence_anchor_tick(&contract.trigger).map_err(Self::placement_error)?; + let temporal_anchor_tick = + Self::temporal_anchor_tick(&contract.trigger).map_err(Self::placement_error)?; let trigger_runtime_state = - Self::provisional_trigger_runtime_state(&contract.trigger, cadence_anchor_tick); - let trigger_state_bond = if actor_type == ActorType::User { - T::TriggerStateBond::amount(&contract.trigger) - } else { - Zero::zero() - }; + Self::provisional_trigger_runtime_state(&contract.trigger, temporal_anchor_tick); let hot = ActorHotState { lifecycle: ActiveLifecycle::Active, cycle_state: CycleState::Idle, @@ -3540,6 +6081,7 @@ pub mod pallet { pending_signal: false, queue_ticket: None, wakeup_pointer: None, + trigger_wakeup_pointer: None, terminal_at: contract .window .map(|window| Self::window_terminal_at(&window)), @@ -3552,11 +6094,6 @@ pub mod pallet { Error::::ActorAlreadyActive.into(), )); } - if let Err(error) = - T::TriggerStateBond::set_bond(&identity.owner, Zero::zero(), trigger_state_bond) - { - return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)); - } if let Err(error) = Self::insert_active_actor( actor_id, identity, @@ -3571,7 +6108,6 @@ pub mod pallet { ActorFundingState { funding_accumulated: Default::default(), funding_tracked_assets, - trigger_state_bond, }, ); if let Err(error) = ActiveActorCount::::try_mutate(|count| -> DispatchResult { @@ -3590,6 +6126,9 @@ pub mod pallet { if let Err(error) = Self::prime_actor_schedule(actor_id).map_err(Self::placement_error) { return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)); } + if let Err(error) = Self::reconcile_actor_state_hold(actor_id) { + return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)); + } polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(())) }) } @@ -3603,7 +6142,7 @@ pub mod pallet { return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)); } if let Err(error) = - Self::cancel_continuation_internal(actor_id, CancellationReason::Deactivated, None) + Self::cancel_run_internal(actor_id, CancellationReason::Deactivated, None) { return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)); } @@ -3622,12 +6161,12 @@ pub mod pallet { Error::::ActorNotFound.into(), )); } - if let Err(error) = T::TriggerStateBond::set_bond( - &state.identity.owner, - state.funding.trigger_state_bond, - Zero::zero(), - ) { - return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)); + if state.hot.trigger_wakeup_pointer.is_some() + && Self::trigger_wakeup_substrate_invalidate_inner(actor_id).is_err() + { + return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err( + Error::::ActorNotFound.into(), + )); } if let Err(error) = Self::remove_active_actor(actor_id, trigger_transition) { return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)); @@ -3641,6 +6180,9 @@ pub mod pallet { }) { return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)); } + if let Err(error) = Self::reconcile_actor_state_hold(actor_id) { + return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)); + } Self::deposit_event(Event::ActorDeactivated { actor_id }); #[cfg(test)] if let Err(error) = crate::mock::control_atomicity_checkpoint(actor_id) { @@ -3671,16 +6213,33 @@ pub mod pallet { ); } let max_block_delay: u32 = T::MaxExecutionDelayBlocks::get().saturated_into(); - if let Trigger::Cadenced { every_ticks } = trigger { - ensure!(*every_ticks > 0, Error::::InvalidTriggerConfiguration); - ensure!( - *every_ticks <= T::MaxCadenceDelayTicks::get(), - Error::::ExecutionDelayTooLong - ); - ensure!( - cooldown_blocks == 0, - Error::::InvalidTriggerConfiguration - ); + match trigger { + Trigger::AtTime { after_ticks } => { + ensure!(*after_ticks > 0, Error::::InvalidTriggerConfiguration); + ensure!( + *after_ticks <= T::MaxTemporalDelayTicks::get(), + Error::::ExecutionDelayTooLong + ); + ensure!( + cooldown_blocks == 0, + Error::::InvalidTriggerConfiguration + ); + } + Trigger::Cadenced { every_ticks } => { + ensure!(*every_ticks > 0, Error::::InvalidTriggerConfiguration); + ensure!( + *every_ticks <= T::MaxTemporalDelayTicks::get(), + Error::::ExecutionDelayTooLong + ); + ensure!( + cooldown_blocks == 0, + Error::::InvalidTriggerConfiguration + ); + } + Trigger::Manual + | Trigger::AddressEvent { .. } + | Trigger::ObservationChange { .. } + | Trigger::ObservationCrossing { .. } => {} } ensure!( cooldown_blocks <= max_block_delay, @@ -3719,7 +6278,10 @@ pub mod pallet { schedule_anchor.checked_add(&cooldown).is_some(), Error::::SchedulerIndexExhausted ); - if matches!(contract.trigger, Trigger::Cadenced { .. }) { + if matches!( + contract.trigger, + Trigger::AtTime { .. } | Trigger::Cadenced { .. } + ) { ensure!(contract.window.is_none(), Error::::InvalidScheduleWindow); return Ok(()); } @@ -3856,14 +6418,13 @@ pub mod pallet { } fn validate_contract_steps_shape( - actor_type: ActorType, + _actor_type: ActorType, contract_steps: &ContractSteps, ) -> DispatchResult { ensure!( contract_steps_bound_is_valid(T::MaxContractSteps::get()), Error::::TooManyContractSteps ); - Self::attempt_fee_envelope(actor_type, contract_steps, 0)?; for step in contract_steps.as_slice() { if let Some(max_attempts) = step.on_error.retry_max_attempts() { ensure!( @@ -4217,6 +6778,14 @@ pub mod pallet { Self::active_actor_view(actor_id).as_ref() == Some(instance), Error::::ActorNotFound ); + Self::finalize_actor_loaded(actor_id, instance, reason) + } + + pub(crate) fn finalize_actor_loaded( + actor_id: ActorId, + instance: &ActiveActorViewOf, + reason: CloseReason, + ) -> DispatchResult { ensure!( ActorFunding::::contains_key(actor_id), Error::::ActorNotFound @@ -4249,19 +6818,15 @@ pub mod pallet { Error::::SystemSovereignInvariant ); } - let trigger_state_bond = ActorFunding::::get(actor_id) - .ok_or(Error::::ActorNotFound)? - .trigger_state_bond; let trigger_transition = Self::preflight_trigger_cleanup(actor_id, TriggerTransitionIntent::Close)?; polkadot_sdk::frame_support::storage::with_transaction(|| { let result = (|| -> DispatchResult { - Self::cancel_continuation_internal(actor_id, CancellationReason::Closing(reason), None)?; + Self::cancel_run_internal(actor_id, CancellationReason::Closing(reason), None)?; // Actor-local ticket/pointer ownership makes shared queue and wakeup entries stale as // soon as hot state disappears. Terminal cleanup performs no shared-container scan. - T::TriggerStateBond::set_bond(&instance.owner, trigger_state_bond, Zero::zero())?; Self::remove_active_actor(actor_id, trigger_transition)?; ActorIdentities::::remove(actor_id); ActorFunding::::remove(actor_id); @@ -4288,6 +6853,7 @@ pub mod pallet { SystemSovereigns::::insert(sovereign_id, SystemSovereignState::Vacant); } } + Self::reconcile_actor_state_hold(actor_id)?; Self::deposit_event(Event::ActorClosed { actor_id, reason }); Ok(()) })(); @@ -4348,6 +6914,7 @@ pub mod pallet { SystemSovereigns::::insert(sovereign_id, SystemSovereignState::Vacant); } } + Self::reconcile_actor_state_hold(actor_id)?; Self::deposit_event(Event::ActorClosed { actor_id, reason }); Ok(()) }) @@ -4405,6 +6972,13 @@ pub mod pallet { #[cfg(feature = "try-runtime")] pub(crate) fn do_try_state() -> Result<(), polkadot_sdk::sp_runtime::TryRuntimeError> { use polkadot_sdk::sp_runtime::TryRuntimeError; + if PrepassExecutionCutoff::::get() + .is_some_and(|(_, cutoff)| cutoff > NextQueueTicket::::get()) + { + return Err(TryRuntimeError::Other( + "prepass execution cutoff exceeds the allocated ticket frontier", + )); + } if MaterializationFamilyCursor::::get() >= 3 { return Err(TryRuntimeError::Other( "materialization family cursor is outside the canonical three-family domain", @@ -4448,7 +7022,7 @@ pub mod pallet { "ActorIdentityCount exceeds MaxActorIdentities", )); } - for actor_id in ActorContracts::::iter_keys() { + for actor_id in ActorContractHeads::::iter_keys() { if !matches!( Self::load_actor_state(actor_id), LoadedActorStateOf::Active(_) @@ -4469,6 +7043,66 @@ pub mod pallet { let hot = state.hot; let contract = state.contract; let funding = state.funding; + if !ActorAdmissionCertificates::::contains_key(actor_id) { + return Err(TryRuntimeError::Other( + "Active actor has no admission certificate", + )); + } + let head = ActorContractHeads::::get(actor_id).ok_or(TryRuntimeError::Other( + "Active actor has no C6 Contract head", + ))?; + let resources = Self::derive_step_resource_envelopes(&contract).ok_or( + TryRuntimeError::Other("Active actor Step resources cannot be rederived"), + )?; + let expected_pipeline_machine_envelope = Self::derive_pipeline_machine_envelope( + identity.actor_class.actor_type(), + &contract.steps, + &resources, + ) + .map_err(|_| { + TryRuntimeError::Other("Active actor Pipeline Machine envelope cannot be rederived") + })?; + if head.header.pipeline_machine_envelope != expected_pipeline_machine_envelope { + return Err(TryRuntimeError::Other( + "Active actor Pipeline Machine envelope disagrees with canonical Step control resources", + )); + } + let activation_authority = ActorActivationAuthorities::::get(actor_id); + let expected_activation_feed = match &contract.trigger { + Trigger::ObservationChange { feed } => Some(*feed), + Trigger::ObservationCrossing { feed, .. } => Some(*feed), + _ => None, + }; + match (expected_activation_feed, activation_authority) { + (Some(feed), Some(authority)) => { + let certificate = ActorAdmissionCertificates::::get(actor_id).ok_or( + TryRuntimeError::Other("indexed Observation actor has no admission certificate"), + )?; + if authority.feed != feed + || authority.cooldown_blocks != contract.cooldown_blocks + || authority.window != contract.window + || authority.auto_close_at_cycle_nonce != contract.auto_close_at_cycle_nonce + || authority.semantic_contract_id != certificate.semantic_contract_id + || authority.body_commitment != certificate.body_commitment + || authority.admission_identity != certificate.admission_identity + { + return Err(TryRuntimeError::Other( + "indexed Observation activation authority disagrees with C6 Contract authority", + )); + } + } + (Some(_), None) => { + return Err(TryRuntimeError::Other( + "indexed Observation actor has no activation authority", + )); + } + (None, Some(_)) => { + return Err(TryRuntimeError::Other( + "non-indexed-Observation actor retains activation authority", + )); + } + (None, None) => {} + } // Terminal membership is derived from the schedule window: `terminal_at` is the sole // terminal-membership authority and must equal the window's exact terminal block, or be // absent without a window (spec 5.1). @@ -4479,17 +7113,34 @@ pub mod pallet { "ActorHot terminal_at disagrees with schedule window terminal membership", )); } - let instance = Self::derive_active_actor_view(identity, hot, contract); - let expected_trigger_state_bond = if instance.actor_class.actor_type() == ActorType::User { - T::TriggerStateBond::amount(&instance.trigger) - } else { - Zero::zero() - }; - if funding.trigger_state_bond != expected_trigger_state_bond { - return Err(TryRuntimeError::Other( - "ActorFunding Trigger-state bond disagrees with Actor class and Trigger policy", - )); + match hot.trigger_runtime_state { + TriggerRuntimeState::AtTime { consumed: true, .. } + if hot.trigger_wakeup_pointer.is_some() => + { + return Err(TryRuntimeError::Other( + "consumed AtTime actor retains Trigger temporal membership", + )); + } + TriggerRuntimeState::AtTime { + consumed: false, .. + } + | TriggerRuntimeState::Cadenced { .. } + if hot.trigger_wakeup_pointer.is_none() => + { + return Err(TryRuntimeError::Other( + "pending temporal actor has no Trigger temporal membership", + )); + } + TriggerRuntimeState::Stateless | TriggerRuntimeState::ObservationCrossing { .. } + if hot.trigger_wakeup_pointer.is_some() => + { + return Err(TryRuntimeError::Other( + "non-temporal actor retains Trigger temporal membership", + )); + } + _ => {} } + let instance = Self::derive_active_actor_view(identity, hot, contract); if !Self::contract_steps_admission_weight_upper( instance.actor_class.actor_type(), &instance.steps, @@ -4556,52 +7207,90 @@ pub mod pallet { )); } } - for actor_id in ContinuationStateStore::::iter_keys() { + for actor_id in ActorRunHeads::::iter_keys() { + if !ActorRunPayloads::::contains_key(actor_id) { + return Err(TryRuntimeError::Other( + "ActorRunHead has no immutable ActorRunPayload", + )); + } let LoadedActorStateOf::Active(state) = Self::load_actor_state(actor_id) else { return Err(TryRuntimeError::Other( - "ContinuationState entry belongs to a corrupt actor partition set", + "ActorRunState entry belongs to a corrupt actor partition set", )); }; - let continuation = state.continuation.ok_or(TryRuntimeError::Other( - "ContinuationState key is absent from loaded Active state", + let run_state = state.run_state.ok_or(TryRuntimeError::Other( + "ActorRunState key is absent from loaded Active state", ))?; let hot = state.hot; let identity = state.identity; let contract = state.contract; - if hot.cycle_state != CycleState::Suspended - || identity.mutability != Mutability::Mutable - || identity.cycle_nonce == 0 - || continuation.cursor >= contract.steps.len() as u32 + if !matches!(hot.cycle_state, CycleState::Running | CycleState::Suspended) + || identity.cycle_nonce.checked_add(1) != Some(run_state.cycle_nonce) + || run_state.cursor >= contract.steps.len() as u32 { return Err(TryRuntimeError::Other( - "ContinuationState violates run marker, mutability, or cursor bounds", + "ActorRunState violates run marker, nonce, mutability, or cursor bounds", )); } - let max_attempts = contract.steps[continuation.cursor as usize] - .on_error - .retry_max_attempts() - .ok_or(TryRuntimeError::Other( - "ContinuationState cursor does not own RetryLater", - ))?; - if continuation.unsuccessful_attempts_at_cursor == 0 - || continuation.unsuccessful_attempts_at_cursor >= max_attempts - { - return Err(TryRuntimeError::Other( - "ContinuationState cursor-local attempt count is outside its live range", - )); + match hot.cycle_state { + CycleState::Suspended => { + if identity.mutability != Mutability::Mutable || !run_state.suspension_is_coherent() { + return Err(TryRuntimeError::Other( + "Suspended ActorRunState has incoherent outcome or suspension authority", + )); + } + let max_attempts = contract.steps[run_state.cursor as usize] + .on_error + .retry_max_attempts() + .ok_or(TryRuntimeError::Other( + "ActorRunState cursor does not own RetryLater", + ))?; + if run_state.unsuccessful_attempts_at_cursor == 0 + || run_state.unsuccessful_attempts_at_cursor >= max_attempts + { + return Err(TryRuntimeError::Other( + "ActorRunState cursor-local attempt count is outside its live range", + )); + } + let expected_eligible_at = Self::suspension_eligible_at( + contract.cooldown_blocks, + contract.window, + run_state.last_attempt_block, + run_state.unsuccessful_attempts_at_cursor, + ) + .map_err(|_| { + TryRuntimeError::Other("ActorRunState retry eligibility is unrepresentable") + })?; + if run_state.eligible_at != expected_eligible_at { + return Err(TryRuntimeError::Other( + "ActorRunState retry eligibility disagrees with its suspension facts", + )); + } + } + CycleState::Running => { + if run_state.unsuccessful_attempts_at_cursor != 0 || !run_state.running_is_coherent() { + return Err(TryRuntimeError::Other( + "Running ActorRunState lacks a causal committed-Step boundary", + )); + } + } + CycleState::Idle => { + return Err(TryRuntimeError::Other( + "Idle Actor cannot retain ActorRunState", + )); + } } - let expected_surfaces = - Self::opening_surfaces(&contract.steps, continuation.cursor as usize); - let mut surfaces_match = expected_surfaces.len() == continuation.opening_snapshot.len(); + let expected_surfaces = Self::opening_surfaces(&contract.steps, 0); + let mut surfaces_match = expected_surfaces.len() == run_state.opening_snapshot.len(); for surface in &expected_surfaces { - if !continuation.opening_snapshot.contains_key(surface) { + if !run_state.opening_snapshot.contains_key(surface) { surfaces_match = false; break; } } if !surfaces_match { return Err(TryRuntimeError::Other( - "ContinuationState opening snapshot disagrees with unresolved suffix", + "ActorRunState opening snapshot disagrees with the complete Contract", )); } let expected_opening_predicates = contract @@ -4613,9 +7302,25 @@ pub mod pallet { }) }) .sum::(); - if continuation.opening_predicate_results.len() != expected_opening_predicates { + if run_state.opening_predicate_results.len() != expected_opening_predicates { + return Err(TryRuntimeError::Other( + "ActorRunState opening predicate results disagree with the Actor Contract", + )); + } + if run_state + .funding_snapshot + .keys() + .any(|asset| !state.funding.funding_tracked_assets.contains(asset)) + { return Err(TryRuntimeError::Other( - "ContinuationState opening predicate results disagree with the Actor Contract", + "ActorRunState funding snapshot contains an untracked asset", + )); + } + } + for actor_id in ActorRunPayloads::::iter_keys() { + if !ActorRunHeads::::contains_key(actor_id) { + return Err(TryRuntimeError::Other( + "ActorRunPayload has no mutable ActorRunHead", )); } } @@ -4632,7 +7337,8 @@ pub mod pallet { continue; } if ActorFunding::::contains_key(actor_id) - || ContinuationStateStore::::contains_key(actor_id) + || ActorRunHeads::::contains_key(actor_id) + || ActorRunPayloads::::contains_key(actor_id) { return Err(TryRuntimeError::Other( "Dormant identity owns active scheduler or readiness state", @@ -4972,11 +7678,9 @@ pub mod pallet { *key_live = key_live .checked_add(live_entries) .ok_or(TryRuntimeError::Other("wakeup bucket live count overflows"))?; - // `wakeup_pointer` is the sole ordinary temporal-membership authority (spec 5.1): a - // physical page slot is a live member only when the actor's pointer addresses exactly - // this slot. A slot whose actor owns a different pointer is corruption; a slot whose - // actor owns no pointer (or no hot state after a lazy terminal cleanup) is a stale - // physical entry with no authority until the bounded drain converges. + // Block-keyed Pipeline service and tick-keyed Trigger detection own independent exact + // pointers into one physical substrate. A slot whose clock-domain pointer differs is + // corruption; an absent pointer is a lazy stale entry until bounded drain converges. for slot in 0..page.entries.len() { let Some(entry) = &page.entries[slot] else { continue; @@ -4986,7 +7690,15 @@ pub mod pallet { page_id, slot: slot as WakeupSlot, }; - match ActorHot::::get(entry.actor_id).and_then(|hot| hot.wakeup_pointer) { + let authoritative = ActorHot::::get(entry.actor_id).and_then(|hot| match block { + WakeupKey::Block(_) => hot.wakeup_pointer, + WakeupKey::Tick(_) => hot.trigger_wakeup_pointer.map(|pointer| WakeupPointer { + block: WakeupKey::Tick(pointer.tick), + page_id: pointer.page_id, + slot: pointer.slot, + }), + }); + match authoritative { Some(pointer) if pointer == expected => { live_wakeup_memberships = live_wakeup_memberships @@ -4997,18 +7709,18 @@ pub mod pallet { } Some(_) => { return Err(TryRuntimeError::Other( - "WakeupPage slot addresses an actor with a different wakeup pointer", + "WakeupPage slot addresses an actor with a different clock-domain pointer", )); } None => {} } } } - // Live wakeup memberships are one-per-active-actor and never exceed the active set; - // stale physical pages and cursor blocks may legitimately outlive their actors. - if live_wakeup_memberships > active_count { + // One Active Actor may own one Pipeline-service pointer and one Trigger-temporal pointer. + // Stale physical pages and cursor blocks may legitimately outlive their actors. + if live_wakeup_memberships > active_count.saturating_mul(2) { return Err(TryRuntimeError::Other( - "live wakeup memberships exceed active actor count", + "live wakeup memberships exceed dual active-actor capacity", )); } for block in WakeupBuckets::::iter_keys() { @@ -5113,28 +7825,52 @@ pub mod pallet { let hot = ActorHot::::get(actor_id).ok_or(TryRuntimeError::Other( "wakeup-pointer hot key has no value", ))?; - let Some(pointer) = hot.wakeup_pointer else { - continue; - }; - if !live_wakeup_pointers.insert((pointer.block, pointer.page_id, pointer.slot)) { - return Err(TryRuntimeError::Other( - "multiple actors own the same wakeup pointer", - )); - } - if !Self::wakeup_page_entry_matches(pointer, actor_id) { - return Err(TryRuntimeError::Other( - "ActorHot wakeup pointer does not resolve to its actor", - )); + if let Some(pointer) = hot.wakeup_pointer { + if !live_wakeup_pointers.insert((pointer.block, pointer.page_id, pointer.slot)) { + return Err(TryRuntimeError::Other( + "multiple actors own the same wakeup pointer", + )); + } + if !Self::wakeup_page_entry_matches(pointer, actor_id) { + return Err(TryRuntimeError::Other( + "ActorHot Pipeline wakeup pointer does not resolve to its actor", + )); + } + if let Some(terminal_at) = hot.terminal_at + && !matches!(pointer.block, WakeupKey::Block(block) if block <= terminal_at) + { + return Err(TryRuntimeError::Other( + "ActorHot Pipeline wakeup pointer exceeds its terminal membership", + )); + } } - // The earlier due requirement determines the next temporal service point (spec 5.1): - // every placement path clamps the wakeup target to the terminal block, so an ordinary - // or terminal wakeup must never be scheduled beyond `terminal_at`. - if let Some(terminal_at) = hot.terminal_at - && !matches!(pointer.block, WakeupKey::Block(block) if block <= terminal_at) - { - return Err(TryRuntimeError::Other( - "ActorHot wakeup pointer exceeds its terminal membership", - )); + if let Some(trigger_pointer) = hot.trigger_wakeup_pointer { + let pointer = WakeupPointer { + block: WakeupKey::Tick(trigger_pointer.tick), + page_id: trigger_pointer.page_id, + slot: trigger_pointer.slot, + }; + if !matches!( + hot.trigger_runtime_state, + TriggerRuntimeState::AtTime { + consumed: false, + .. + } | TriggerRuntimeState::Cadenced { .. } + ) { + return Err(TryRuntimeError::Other( + "non-pending temporal Actor owns a Trigger wakeup pointer", + )); + } + if !live_wakeup_pointers.insert((pointer.block, pointer.page_id, pointer.slot)) { + return Err(TryRuntimeError::Other( + "multiple actors own the same wakeup pointer", + )); + } + if !Self::wakeup_page_entry_matches(pointer, actor_id) { + return Err(TryRuntimeError::Other( + "ActorHot Trigger wakeup pointer does not resolve to its actor", + )); + } } } let next_id = NextActorId::::get(); @@ -5170,6 +7906,58 @@ pub mod pallet { "SystemSovereignCount disagrees with bounded locator registry", )); } + let mut owner_hold_totals = alloc::collections::BTreeMap::new(); + for (actor_id, identity) in ActorIdentities::::iter() { + match identity.actor_class.actor_type() { + ActorType::System => { + if ActorStateHolds::::contains_key(actor_id) { + return Err(TryRuntimeError::Other( + "System Actor retains a User Actor-state hold", + )); + } + } + ActorType::User => { + let expected = Self::derive_actor_state_hold(actor_id, &identity).map_err(|_| { + TryRuntimeError::Other("User Actor-state hold geometry cannot be rederived") + })?; + let record = ActorStateHolds::::get(actor_id).ok_or(TryRuntimeError::Other( + "User Actor has no Actor-state hold record", + ))?; + if record.owner != identity.owner || record.breakdown != expected { + return Err(TryRuntimeError::Other( + "User Actor-state hold record disagrees with retained geometry", + )); + } + let total = Self::state_hold_total(&expected).map_err(|_| { + TryRuntimeError::Other("User Actor-state hold total cannot be rederived") + })?; + let owner_total = owner_hold_totals + .entry(record.owner) + .or_insert(T::Balance::zero()); + *owner_total = owner_total + .checked_add(&total) + .ok_or(TryRuntimeError::Other( + "aggregate User Actor-state hold overflows", + ))?; + } + } + } + for (actor_id, _) in ActorStateHolds::::iter() { + // deos-bypass: bounded-iter -- MaxActorIdentities bounds per-Actor hold records. + if !ActorIdentities::::contains_key(actor_id) { + return Err(TryRuntimeError::Other( + "Actor-state hold record has no Actor identity", + )); + } + } + let hold_reason: T::RuntimeHoldReason = HoldReason::ActorState.into(); + for (owner, expected) in owner_hold_totals { + if T::StateHoldCurrency::balance_on_hold(&hold_reason, &owner) != expected { + return Err(TryRuntimeError::Other( + "owner Actor-state hold balance disagrees with per-Actor records", + )); + } + } Self::do_try_state_observation_subscriptions()?; Self::do_try_state_dirty_observations()?; Self::do_try_state_crossing()?; diff --git a/template/pallets/actors/src/mock.rs b/template/pallets/actors/src/mock.rs index cfa1aeea..300384bf 100644 --- a/template/pallets/actors/src/mock.rs +++ b/template/pallets/actors/src/mock.rs @@ -10,6 +10,7 @@ use polkadot_sdk::{ BuildStorage, Perbill, traits::{BlakeTwo256, IdentityLookup}, }, + sp_weights::Weight, }; use alloc::vec; @@ -149,6 +150,14 @@ thread_local! { static GUARANTEED_ON_IDLE_WEIGHT: RefCell = RefCell::new(polkadot_sdk::sp_weights::Weight::MAX); + static STEP_CONTROL_ACTUAL_WEIGHT_OVERRIDE: RefCell> = + RefCell::new(None); + static LAST_STEP_CONTROL_EXECUTION: RefCell> = + RefCell::new(None); + static MISSING_STEP_CONTROL_ACTUAL_WEIGHT: RefCell = RefCell::new(false); + static TASK_EFFECT_ACTUAL_WEIGHT_OVERRIDE: RefCell> = + RefCell::new(None); + static MISSING_TASK_EFFECT_ACTUAL_WEIGHT: RefCell = RefCell::new(false); static FEE_COLLECTIONS: RefCell> = RefCell::new(alloc::vec::Vec::new()); static FAIL_CREATE_CHECKPOINT: RefCell = RefCell::new(false); static FAIL_FEE_SINK_TRANSFER: RefCell = RefCell::new(false); @@ -227,6 +236,11 @@ pub fn reset_mock_adapters() { UNSTAKED.with(|b| b.borrow_mut().clear()); DONATED_LIQUIDITY.with(|b| b.borrow_mut().clear()); GUARANTEED_ON_IDLE_WEIGHT.with(|v| *v.borrow_mut() = polkadot_sdk::sp_weights::Weight::MAX); + STEP_CONTROL_ACTUAL_WEIGHT_OVERRIDE.with(|v| *v.borrow_mut() = None); + LAST_STEP_CONTROL_EXECUTION.with(|v| *v.borrow_mut() = None); + MISSING_STEP_CONTROL_ACTUAL_WEIGHT.with(|v| *v.borrow_mut() = false); + TASK_EFFECT_ACTUAL_WEIGHT_OVERRIDE.with(|v| *v.borrow_mut() = None); + MISSING_TASK_EFFECT_ACTUAL_WEIGHT.with(|v| *v.borrow_mut() = false); FEE_COLLECTIONS.with(|v| v.borrow_mut().clear()); FAIL_CREATE_CHECKPOINT.with(|v| *v.borrow_mut() = false); FAIL_FEE_SINK_TRANSFER.with(|v| *v.borrow_mut() = false); @@ -932,6 +946,19 @@ impl crate::BenchmarkHelper for MockBenchmar Ok(()) } + type MaximumContextInherent = (); + fn prepare_maximum_context_inherent() {} + fn execute_maximum_context_inherent(_: ()) -> DispatchResult { + Ok(()) + } + fn verify_maximum_context_inherent() {} + fn prepare_maximum_xcm_version_discovery() {} + fn execute_maximum_xcm_version_discovery() {} + fn verify_maximum_xcm_version_discovery() {} + fn prepare_block_resource_meter_extension() {} + fn execute_block_resource_meter_extension() {} + fn verify_block_resource_meter_extension() {} + fn setup_remove_liquidity( owner: &AccountId, ) -> Result<(TestAsset, TestAsset, TestAsset, Balance), DispatchError> { @@ -987,8 +1014,8 @@ impl Get for TestMaxExecutionDelayBlocks { } } -pub struct TestMaxCadenceDelayTicks; -impl Get for TestMaxCadenceDelayTicks { +pub struct TestMaxTemporalDelayTicks; +impl Get for TestMaxTemporalDelayTicks { fn get() -> u64 { 631_152_000 } @@ -1054,6 +1081,17 @@ impl Get for TestMinUserBalance { } } +pub struct TestBlockResourceBudget; +impl Get for TestBlockResourceBudget { + fn get() -> crate::BlockResourceBudget { + crate::BlockResourceBudget::new( + Weight::from_parts(1_000_000_000_000, 5_000_000), + Weight::zero(), + ) + .unwrap_or_else(|_| crate::BlockResourceBudget::fail_closed(Weight::zero())) + } +} + pub struct TestMaxSweepBatch; impl Get for TestMaxSweepBatch { fn get() -> u32 { @@ -1100,6 +1138,108 @@ impl crate::adapters::SovereignAccountPolicy for MockSovereignAccount } } +pub struct MockAdmissionCertificateAuthority; + +impl crate::AdmissionCertificateAuthorityProvider for MockAdmissionCertificateAuthority { + fn current() -> Option { + Some(crate::AdmissionCertificateAuthority { + runtime_actor_semantics_version: 1, + production_weight_identity: + crate::AdmissionCertificateAuthority::compose_production_weight_identity([41; 32], [42; 32]), + body_geometry_version: 1, + configured_bounds_commitment: [6; 32], + maximum_lifecycle_weight: Weight::from_parts(77, 88), + }) + } +} + +pub fn set_step_control_actual_weight_override(weight: Option) { + STEP_CONTROL_ACTUAL_WEIGHT_OVERRIDE.with(|value| *value.borrow_mut() = weight); +} + +pub fn last_step_control_execution() -> Option { + LAST_STEP_CONTROL_EXECUTION.with(|value| *value.borrow()) +} + +pub fn set_missing_step_control_actual_weight(missing: bool) { + MISSING_STEP_CONTROL_ACTUAL_WEIGHT.with(|value| *value.borrow_mut() = missing); +} + +pub struct MockStepControlWeight; + +impl crate::StepControlWeightProvider> for MockStepControlWeight { + fn production_weight_identity() -> Option<[u8; 32]> { + Some([41; 32]) + } + + fn maximum_control_weight( + context: crate::StepControlWeightContext, + _: &crate::StepOf, + ) -> Option { + Some(Weight::from_parts( + 100_000_011u64 + .saturating_add(u64::from(context.cursor)) + .saturating_add(u64::from(context.opening_tail_chunks)) + .saturating_add(u64::from(context.predicate_evaluation_units)) + .saturating_add(u64::from(context.opening_snapshot_entries)) + .saturating_add(u64::from(context.opening_predicate_results)) + .saturating_add(u64::from(context.funding_snapshot_entries)), + 100_022u64.saturating_add(u64::from(context.steps_in_fragment)), + )) + } + + fn actual_control_weight( + _: crate::StepControlWeightContext, + _: &crate::StepOf, + maximum: Weight, + execution: crate::StepControlExecution, + ) -> Option { + LAST_STEP_CONTROL_EXECUTION.with(|value| *value.borrow_mut() = Some(execution)); + if MISSING_STEP_CONTROL_ACTUAL_WEIGHT.with(|missing| *missing.borrow()) { + return None; + } + STEP_CONTROL_ACTUAL_WEIGHT_OVERRIDE + .with(|override_weight| (*override_weight.borrow()).or(Some(maximum))) + } +} + +pub fn set_task_effect_actual_weight_override(weight: Option) { + TASK_EFFECT_ACTUAL_WEIGHT_OVERRIDE.with(|value| *value.borrow_mut() = weight); +} + +pub fn set_missing_task_effect_actual_weight(missing: bool) { + MISSING_TASK_EFFECT_ACTUAL_WEIGHT.with(|value| *value.borrow_mut() = missing); +} + +pub struct MockTaskEffectWeight; + +impl crate::TaskEffectWeightProvider> for MockTaskEffectWeight { + fn production_weight_identity() -> Option<[u8; 32]> { + Some([42; 32]) + } + + fn maximum_effect_weight(_: &crate::TaskOf) -> Option { + Some(Weight::from_parts(33, 44)) + } + + fn actual_effect_weight( + _: &crate::TaskOf, + execution: crate::TaskEffectExecution, + ) -> Option { + if MISSING_TASK_EFFECT_ACTUAL_WEIGHT.with(|missing| *missing.borrow()) { + return None; + } + TASK_EFFECT_ACTUAL_WEIGHT_OVERRIDE.with(|override_weight| { + override_weight.borrow().or_else(|| { + Some(match execution { + crate::TaskEffectExecution::NotInvoked => Weight::zero(), + crate::TaskEffectExecution::Invoked => Weight::from_parts(33, 44), + }) + }) + }) + } +} + pub struct MockTime; impl Time for MockTime { @@ -1115,6 +1255,9 @@ impl pallet_deos_actors::Config for Test { type Balance = Balance; type FeeNativeAssetId = NativeAsset; type AssetOps = MockAssetOps; + type AdmissionCertificateAuthority = MockAdmissionCertificateAuthority; + type StepControlWeight = MockStepControlWeight; + type TaskEffectWeight = MockTaskEffectWeight; type ObservationFeedId = u32; type ObservationProvider = MockObservationProvider; type FundingAuthority = MockFundingAuthority; @@ -1142,6 +1285,7 @@ impl pallet_deos_actors::Config for Test { type QueuePageSize = ConstU32<32>; type WakeupPageSize = ConstU32<32>; type ObservationPageSize = ConstU32<16>; + type CrossingPageSize = ConstU32<16>; type MaxCrossingTransitionsPerFeed = ConstU32<4>; type MaxCrossingMembersPerFeed = ConstU32<10_000>; type MaxUserCrossingMembersPerFeed = ConstU32<8>; @@ -1160,7 +1304,7 @@ impl pallet_deos_actors::Config for Test { type MaxSplitTransferLegs = ConstU32<8>; type TargetBlockTime = ConstU64<63_116>; type MaxExecutionDelayBlocks = TestMaxExecutionDelayBlocks; - type MaxCadenceDelayTicks = TestMaxCadenceDelayTicks; + type MaxTemporalDelayTicks = TestMaxTemporalDelayTicks; type MaxIdleStarvationBlocks = TestMaxIdleStarvationBlocks; type ActorOnIdleReserve = TestActorOnIdleReserve; type MaxAutoCloseNonceHorizon = TestMaxAutoCloseNonceHorizon; @@ -1168,14 +1312,19 @@ impl pallet_deos_actors::Config for Test { type MaxActorIdentities = ConstU32<10_000>; type MaxSystemSovereigns = ConstU32<10_000>; type ActorCreationFee = TestActorCreationFee; + type RuntimeHoldReason = RuntimeHoldReason; + type StateHoldCurrency = Balances; + type ActorStateHoldBase = ConstU128<1>; + type ActorStateHoldPerByte = ConstU128<1>; type WeightToFee = TestWeightToFee; type FeeSink = TestFeeSink; type FeeCollector = MockFeeCollector; - type TriggerStateBond = (); type MaxConsecutiveFailures = TestMaxConsecutiveFailures; type MaxRetryAttempts = ConstU32<10>; type MinUserBalance = TestMinUserBalance; type WeightInfo = crate::weights::TestWeightInfo; + type BlockResourceBudget = TestBlockResourceBudget; + type PrepassContext = (); type GenesisSystemActors = (); type SystemActorContractValidator = (); #[cfg(feature = "runtime-benchmarks")] diff --git a/template/pallets/actors/src/reactions.rs b/template/pallets/actors/src/reactions.rs index 6da1f86c..1b2104ce 100644 --- a/template/pallets/actors/src/reactions.rs +++ b/template/pallets/actors/src/reactions.rs @@ -1,10 +1,18 @@ use crate::pallet::*; -use crate::scheduler::{ActivationFailure, ActivationOutcome}; -use crate::types::{DirtyObservationList, DirtyObservationState, ObservationRevision}; +use crate::scheduler::{ + ActivationFailure, ActivationOutcome, ObservationActivationOutcome, ObservationPlacementCandidate, +}; +use crate::types::{ + DirtyObservationList, DirtyObservationState, ObservationFanoutBranch, ObservationRevision, +}; use crate::weights::WeightInfo as _; +use alloc::vec; use polkadot_sdk::frame_support::{ensure, storage::TransactionOutcome, traits::Get}; use polkadot_sdk::frame_system::{Pallet as System, pallet_prelude::BlockNumberFor}; -use polkadot_sdk::sp_runtime::{DispatchError, DispatchResult}; +use polkadot_sdk::sp_runtime::{ + DispatchError, DispatchResult, + traits::{One, SaturatedConversion, Saturating}, +}; impl Pallet { fn append_crossing_pending_feed(feed: T::ObservationFeedId) -> DispatchResult { @@ -190,6 +198,8 @@ impl Pallet { fn append_dirty_observation_feed( feed: T::ObservationFeedId, revision: ObservationRevision, + cause_provenance: crate::TriggerCauseProvenance, + cause_block: u64, ) -> DispatchResult { ensure!( !DirtyObservationFeeds::::contains_key(feed), @@ -224,9 +234,16 @@ impl Pallet { feed, DirtyObservationState { latest_revision: revision, + latest_cause_provenance: cause_provenance, + latest_cause_block: cause_block, fanout_revision: 0, + fanout_cause_provenance: crate::TriggerCauseProvenance::Deferred, + fanout_cause_block: 0, dirty_since: System::::block_number(), next_subscriber_page: None, + next_subscriber_position: 0, + next_subscriber_branch: ObservationFanoutBranch::Ordinary, + retry_after: None, previous_dirty_feed: list.tail, next_dirty_feed: None, }, @@ -243,8 +260,10 @@ impl Pallet { fn do_note_observation_changed( feed: T::ObservationFeedId, revision: ObservationRevision, + cause_provenance: crate::TriggerCauseProvenance, ) -> DispatchResult { ensure!(revision > 0, Error::::InvalidObservationRevision); + let cause_block = System::::block_number().saturated_into::(); if ObservationSubscriberCount::::get(feed) == 0 { ensure!( !ObservationIngressRevisions::::contains_key(feed) @@ -272,10 +291,12 @@ impl Pallet { ObservationIngressRevisions::::insert(feed, revision); if let Some(mut state) = dirty { state.latest_revision = revision; + state.latest_cause_provenance = cause_provenance; + state.latest_cause_block = cause_block; DirtyObservationFeeds::::insert(feed, state); return Ok(()); } - Self::append_dirty_observation_feed(feed, revision) + Self::append_dirty_observation_feed(feed, revision, cause_provenance, cause_block) } pub fn observation_change_ingress_weight() -> polkadot_sdk::frame_support::weights::Weight { @@ -287,12 +308,15 @@ impl Pallet { feed: T::ObservationFeedId, revision: ObservationRevision, ) -> DispatchResult { - Self::with_reused_transaction(|| Self::do_note_observation_changed(feed, revision)) + Self::with_reused_transaction(|| { + Self::do_note_observation_changed(feed, revision, crate::TriggerCauseProvenance::Deferred) + }) } fn do_note_observation_transition( feed: T::ObservationFeedId, transition: crate::ObservationTransition, + cause_provenance: crate::TriggerCauseProvenance, ) -> DispatchResult { ensure!( transition.revision > 0, @@ -309,7 +333,8 @@ impl Pallet { Error::::CrossingTransitionInvariant ); } - Self::do_note_observation_changed(feed, transition.revision)?; + let cause_block = System::::block_number().saturated_into::(); + Self::do_note_observation_changed(feed, transition.revision, cause_provenance)?; if CrossingFeedMembershipCount::::get(feed) == 0 || transition.previous.is_none() { return Ok(()); } @@ -334,6 +359,8 @@ impl Pallet { .previous .ok_or(Error::::CrossingTransitionInvariant)?, current: transition.current, + cause_provenance, + cause_block, }) .map_err(|_| Error::::CrossingTransitionCapacityExceeded)?; Ok(()) @@ -348,7 +375,21 @@ impl Pallet { feed: T::ObservationFeedId, transition: crate::ObservationTransition, ) -> DispatchResult { - Self::with_reused_transaction(|| Self::do_note_observation_transition(feed, transition)) + Self::note_observation_transition_with_provenance( + feed, + transition, + crate::TriggerCauseProvenance::Deferred, + ) + } + + pub fn note_observation_transition_with_provenance( + feed: T::ObservationFeedId, + transition: crate::ObservationTransition, + cause_provenance: crate::TriggerCauseProvenance, + ) -> DispatchResult { + Self::with_reused_transaction(|| { + Self::do_note_observation_transition(feed, transition, cause_provenance) + }) } pub(crate) fn preflight_clear_dirty_observation_feeds( @@ -425,19 +466,175 @@ impl Pallet { Ok(()) } - pub(crate) fn signal_observation_subscriber(actor_id: ActorId) -> Result { - match Self::request_activation(actor_id) { - Ok( + fn process_observation_change_occurrence( + actor_id: ActorId, + feed: T::ObservationFeedId, + execute_terminal: bool, + cause_provenance: crate::TriggerCauseProvenance, + cause_block: u64, + ) -> Result { + if IndexedTriggerDetectionDisabled::::contains_key(actor_id) { + return Ok(ObservationActivationOutcome::Ordinary( + ActivationOutcome::IgnoredStale, + )); + } + let Some(state) = Self::load_observation_activation_state(actor_id, feed) else { + return if execute_terminal { + Self::request_observation_activation_compact_with_cause( + actor_id, + feed, + cause_provenance, + cause_block, + ) + .map(ObservationActivationOutcome::Ordinary) + } else { + Self::request_observation_activation_ordinary_with_cause( + actor_id, + feed, + cause_provenance, + cause_block, + ) + }; + }; + if state.hot.pending_signal { + return Ok(ObservationActivationOutcome::Ordinary( + ActivationOutcome::IgnoredStale, + )); + } + let classification = + Self::classify_observation_activation_compact(&state).map_err(|error| { + ActivationFailure::Permanent(Self::classification_dispatch_error(error).into()) + })?; + if classification.terminal_reason.is_some() { + return if execute_terminal { + Self::request_observation_activation_compact_with_cause( + actor_id, + feed, + cause_provenance, + cause_block, + ) + .map(ObservationActivationOutcome::Ordinary) + } else { + Self::request_observation_activation_ordinary_with_cause( + actor_id, + feed, + cause_provenance, + cause_block, + ) + }; + } + polkadot_sdk::frame_support::storage::with_transaction(|| { + let actor_type = state.identity.actor_class.actor_type(); + let breakdown = Self::trigger_fee_for_weight( + actor_type, + TriggerFamily::ObservationChange, + T::WeightInfo::observation_change_trigger_occurrence(), + ); + let charged = match Self::try_charge_automatic_trigger_occurrence( + actor_type, + &state.identity.sovereign_account, + breakdown, + ) { + Ok(charged) => charged, + Err(error) => { + return TransactionOutcome::Rollback(Err(ActivationFailure::Permanent(error))); + } + }; + if !charged { + return TransactionOutcome::Commit(Ok(ObservationActivationOutcome::Ordinary( + ActivationOutcome::IgnoredStale, + ))); + } + let outcome = if execute_terminal { + Self::request_observation_activation_compact_with_cause( + actor_id, + feed, + cause_provenance, + cause_block, + ) + .map(ObservationActivationOutcome::Ordinary) + } else { + Self::request_observation_activation_ordinary_with_cause( + actor_id, + feed, + cause_provenance, + cause_block, + ) + }; + let outcome = match outcome { + Ok(outcome) => outcome, + Err(error) => return TransactionOutcome::Rollback(Err(error)), + }; + let ObservationActivationOutcome::Ordinary(activation) = outcome else { + return TransactionOutcome::Rollback(Ok(outcome)); + }; + if matches!( + activation, + ActivationOutcome::Latched | ActivationOutcome::Coalesced + ) { + IndexedTriggerDetectionDisabled::::insert(actor_id, ()); + Self::deposit_event(Event::TriggerOccurrenceProcessed { + actor_id, + trigger_family: breakdown.trigger_family, + fee: breakdown.trigger_fee, + }); + TransactionOutcome::Commit(Ok(outcome)) + } else { + TransactionOutcome::Rollback(Ok(outcome)) + } + }) + } + + pub(crate) fn signal_observation_subscriber( + actor_id: ActorId, + feed: T::ObservationFeedId, + cause_provenance: crate::TriggerCauseProvenance, + cause_block: u64, + ) -> Result { + match Self::process_observation_change_occurrence( + actor_id, + feed, + true, + cause_provenance, + cause_block, + ) { + Ok(ObservationActivationOutcome::Ordinary( ActivationOutcome::IgnoredStale | ActivationOutcome::Coalesced | ActivationOutcome::Latched | ActivationOutcome::Closed, - ) => Ok(true), + )) => Ok(true), + Ok(ObservationActivationOutcome::TerminalDeferred) => Err(Error::::ActorInvariant.into()), Err(ActivationFailure::Temporary(_)) => Ok(false), Err(error @ ActivationFailure::Permanent(_)) => Err(Self::activation_failure_error(error)), } } + fn signal_observation_subscriber_ordinary( + actor_id: ActorId, + feed: T::ObservationFeedId, + cause_provenance: crate::TriggerCauseProvenance, + cause_block: u64, + ) -> Result, DispatchError> { + match Self::process_observation_change_occurrence( + actor_id, + feed, + false, + cause_provenance, + cause_block, + ) { + Ok(ObservationActivationOutcome::Ordinary( + ActivationOutcome::IgnoredStale + | ActivationOutcome::Coalesced + | ActivationOutcome::Latched + | ActivationOutcome::Closed, + )) => Ok(Some(true)), + Ok(ObservationActivationOutcome::TerminalDeferred) => Ok(None), + Err(ActivationFailure::Temporary(_)) => Ok(Some(false)), + Err(error @ ActivationFailure::Permanent(_)) => Err(Self::activation_failure_error(error)), + } + } + pub fn dirty_observation_feed_count() -> u32 { DirtyObservationListState::::get().count } @@ -446,6 +643,22 @@ impl Pallet { Self::dirty_observation_feed_count() } + pub(crate) fn observation_fanout_branch_probe() -> Option { + let list = DirtyObservationListState::::get(); + if list.count == 0 { + return None; + } + let feed = list.cursor.or(list.head)?; + let state = DirtyObservationFeeds::::get(feed)?; + if state + .retry_after + .is_some_and(|retry_after| retry_after > System::::block_number()) + { + return None; + } + Some(state.next_subscriber_branch) + } + fn advance_dirty_observation_cursor( list: &mut DirtyObservationList, state: &DirtyObservationState>, @@ -465,6 +678,14 @@ impl Pallet { let feed = list.cursor.ok_or(Error::::DirtyObservationInvariant)?; let mut state = DirtyObservationFeeds::::get(feed).ok_or(Error::::DirtyObservationInvariant)?; + if let Some(retry_after) = state.retry_after { + if retry_after > System::::block_number() { + Self::advance_dirty_observation_cursor(&mut list, &state); + DirtyObservationListState::::put(list); + return Ok(true); + } + state.retry_after = None; + } ensure!( Self::dirty_observation_links_are_valid(feed, &state, &list), Error::::DirtyObservationInvariant @@ -474,13 +695,25 @@ impl Pallet { ensure!(page_list.count > 0, Error::::DirtyObservationInvariant); if state.fanout_revision == 0 { state.fanout_revision = state.latest_revision; + state.fanout_cause_provenance = state.latest_cause_provenance; + state.fanout_cause_block = state.latest_cause_block; state.next_subscriber_page = Some(page_list.head); + state.next_subscriber_position = 0; + state.next_subscriber_branch = ObservationFanoutBranch::Ordinary; } let Some(page_id) = state.next_subscriber_page else { + ensure!( + state.next_subscriber_position == 0 + && state.next_subscriber_branch == ObservationFanoutBranch::Ordinary + && state.retry_after.is_none(), + Error::::DirtyObservationInvariant + ); if state.latest_revision == state.fanout_revision { Self::clear_dirty_observation_feed(feed)?; } else { state.fanout_revision = state.latest_revision; + state.fanout_cause_provenance = state.latest_cause_provenance; + state.fanout_cause_block = state.latest_cause_block; state.next_subscriber_page = Some(page_list.head); Self::advance_dirty_observation_cursor(&mut list, &state); DirtyObservationFeeds::::insert(feed, state); @@ -491,9 +724,149 @@ impl Pallet { let page = ObservationSubscriberPages::::get(feed, page_id) .ok_or(Error::::DirtyObservationInvariant)?; let next_page = page.next; + let page_len = + u32::try_from(page.entries.len()).map_err(|_| Error::::DirtyObservationInvariant)?; + ensure!( + state.next_subscriber_position < page_len, + Error::::DirtyObservationInvariant + ); let mut page_complete = true; - for actor_id in page.entries.into_iter().flatten() { - page_complete &= Self::signal_observation_subscriber(actor_id)?; + 'page: while state.next_subscriber_position < page_len { + if state.next_subscriber_branch == ObservationFanoutBranch::Ordinary { + let cohort_start = state.next_subscriber_position as usize; + if let Some(actor_id) = page.entries[cohort_start] + && let Some(first) = Self::prepare_observation_placement_candidate( + actor_id, + feed, + state.fanout_cause_provenance, + state.fanout_cause_block, + ) + .map_err(Self::activation_failure_error)? + { + let wakeup_key = first.wakeup_key(); + let mut cohort_end = cohort_start.saturating_add(1); + let committed = match first { + ObservationPlacementCandidate::Queue(first) => { + let mut candidates = vec![first]; + while cohort_end < page.entries.len() { + let Some(actor_id) = page.entries[cohort_end] else { + break; + }; + let Some(ObservationPlacementCandidate::Queue(candidate)) = + Self::prepare_observation_placement_candidate( + actor_id, + feed, + state.fanout_cause_provenance, + state.fanout_cause_block, + ) + .map_err(Self::activation_failure_error)? + else { + break; + }; + candidates.push(candidate); + cohort_end = cohort_end.saturating_add(1); + } + Self::commit_observation_queue_cohort(candidates).is_ok() + } + ObservationPlacementCandidate::Wakeup(first) => { + let mut candidates = vec![first]; + while cohort_end < page.entries.len() { + let Some(actor_id) = page.entries[cohort_end] else { + break; + }; + let Some(candidate) = Self::prepare_observation_placement_candidate( + actor_id, + feed, + state.fanout_cause_provenance, + state.fanout_cause_block, + ) + .map_err(Self::activation_failure_error)? + else { + break; + }; + if candidate.wakeup_key() != wakeup_key { + break; + } + let ObservationPlacementCandidate::Wakeup(candidate) = candidate else { + break; + }; + candidates.push(candidate); + cohort_end = cohort_end.saturating_add(1); + } + match Self::commit_observation_wakeup_cohort(candidates) { + Ok(()) => true, + Err( + crate::scheduler::EnqueueOutcome::CapacityUnavailable + | crate::scheduler::EnqueueOutcome::WakeupCapacityExhausted, + ) => { + state.retry_after = Some(System::::block_number().saturating_add(One::one())); + page_complete = false; + break 'page; + } + Err(_) => return Err(Error::::SchedulerIndexExhausted.into()), + } + } + }; + if committed { + state.next_subscriber_position = + u32::try_from(cohort_end).map_err(|_| Error::::DirtyObservationInvariant)?; + continue; + } + } + } + + let position = state.next_subscriber_position as usize; + let maybe_actor_id = page.entries[position]; + let next_position = state + .next_subscriber_position + .checked_add(1) + .ok_or(Error::::DirtyObservationInvariant)?; + match state.next_subscriber_branch { + ObservationFanoutBranch::Ordinary => { + let Some(actor_id) = maybe_actor_id else { + state.next_subscriber_position = next_position; + continue; + }; + match Self::signal_observation_subscriber_ordinary( + actor_id, + feed, + state.fanout_cause_provenance, + state.fanout_cause_block, + )? { + Some(true) => state.next_subscriber_position = next_position, + Some(false) => { + state.retry_after = Some(System::::block_number().saturating_add(One::one())); + page_complete = false; + break; + } + None => { + state.next_subscriber_branch = ObservationFanoutBranch::Terminal; + page_complete = false; + break; + } + } + } + ObservationFanoutBranch::Terminal => { + let actor_id = maybe_actor_id.ok_or(Error::::DirtyObservationInvariant)?; + if !Self::signal_observation_subscriber( + actor_id, + feed, + state.fanout_cause_provenance, + state.fanout_cause_block, + )? { + state.retry_after = Some(System::::block_number().saturating_add(One::one())); + page_complete = false; + break; + } + if !DirtyObservationFeeds::::contains_key(feed) { + return Ok(Self::dirty_observation_feed_count() > 0); + } + state.next_subscriber_position = next_position; + state.next_subscriber_branch = ObservationFanoutBranch::Ordinary; + page_complete = next_position == page_len; + break; + } + } } if !page_complete { Self::advance_dirty_observation_cursor(&mut list, &state); @@ -501,12 +874,21 @@ impl Pallet { DirtyObservationListState::::put(list); return Ok(true); } + ensure!( + state.next_subscriber_position == page_len, + Error::::DirtyObservationInvariant + ); state.next_subscriber_page = next_page; + state.next_subscriber_position = 0; + state.next_subscriber_branch = ObservationFanoutBranch::Ordinary; + state.retry_after = None; if next_page.is_none() { if state.latest_revision == state.fanout_revision { Self::clear_dirty_observation_feed(feed)?; } else { state.fanout_revision = state.latest_revision; + state.fanout_cause_provenance = state.latest_cause_provenance; + state.fanout_cause_block = state.latest_cause_block; state.next_subscriber_page = Some(page_list.head); Self::advance_dirty_observation_cursor(&mut list, &state); DirtyObservationFeeds::::insert(feed, state); @@ -549,9 +931,21 @@ impl Pallet { if Self::dirty_observation_fanout_base_probe() == 0 { return (meter.consumed(), pages_serviced); } - let unit_weight = T::WeightInfo::observation_fanout_page(); + let branch_probe_weight = T::WeightInfo::observation_fanout_branch_probe(); + let fault_weight = T::WeightInfo::record_observation_fanout_worker_fault(); for _ in pages_serviced..T::MaxObservationFanoutPagesPerBlock::get() { - if !meter.can_consume(unit_weight) { + if !meter.can_consume(branch_probe_weight) { + break; + } + meter.consume(branch_probe_weight); + let Some(branch) = Self::observation_fanout_branch_probe() else { + break; + }; + let unit_weight = match branch { + ObservationFanoutBranch::Ordinary => Self::observation_fanout_ordinary_weight_upper(), + ObservationFanoutBranch::Terminal => T::WeightInfo::observation_fanout_terminal(), + }; + if !meter.can_consume(unit_weight.saturating_add(fault_weight)) { break; } let list = DirtyObservationListState::::get(); @@ -580,12 +974,36 @@ impl Pallet { } else { CrossingWorkerFaultClass::Other }; - ObservationFanoutWorkerFaultState::::put(ObservationFanoutWorkerFault { - feed, - revision: state.latest_revision, - subscriber_page: state.next_subscriber_page, - class, + let actor_id = state.next_subscriber_page.and_then(|page_id| { + ObservationSubscriberPages::::get(feed, page_id).and_then(|page| { + page + .entries + .get(state.next_subscriber_position as usize) + .copied() + .flatten() + }) }); + let authority = actor_id.and_then(ActorActivationAuthorities::::get); + let recorded = Self::record_observation_fanout_worker_fault( + &mut meter, + ObservationFanoutWorkerFault { + feed, + revision: state.latest_revision, + subscriber_page: state.next_subscriber_page, + subscriber_position: state.next_subscriber_position, + actor_id, + semantic_contract_id: authority + .as_ref() + .map(|authority| authority.semantic_contract_id), + body_commitment: authority + .as_ref() + .map(|authority| authority.body_commitment), + admission_identity: authority.map(|authority| authority.admission_identity), + branch: state.next_subscriber_branch, + class, + }, + ); + debug_assert!(recorded, "fault Weight was reserved before fanout mutation"); } break; } @@ -642,10 +1060,25 @@ impl Pallet { .ok_or(TryRuntimeError::Other("dirty observation key has no state"))?; if state.latest_revision == 0 || state.fanout_revision > state.latest_revision - || (state.fanout_revision == 0 && state.next_subscriber_page.is_some()) - || state - .next_subscriber_page - .is_some_and(|page_id| ObservationSubscriberPages::::get(feed, page_id).is_none()) + || (state.fanout_revision == 0 + && (state.fanout_cause_provenance != crate::TriggerCauseProvenance::Deferred + || state.fanout_cause_block != 0 + || state.next_subscriber_page.is_some() + || state.next_subscriber_position != 0 + || state.next_subscriber_branch != ObservationFanoutBranch::Ordinary + || state.retry_after.is_some())) + || state.next_subscriber_page.map_or( + state.next_subscriber_position != 0 + || state.next_subscriber_branch != ObservationFanoutBranch::Ordinary + || state.retry_after.is_some(), + |page_id| { + ObservationSubscriberPages::::get(feed, page_id).is_none_or(|page| { + state.next_subscriber_position >= page.entries.len() as u32 + || (state.next_subscriber_branch == ObservationFanoutBranch::Terminal + && page.entries[state.next_subscriber_position as usize].is_none()) + }) + }, + ) || ObservationSubscriberCount::::get(feed) == 0 || ObservationSubscriberPageLists::::get(feed).is_none() || ObservationIngressRevisions::::get(feed) != Some(state.latest_revision) @@ -681,7 +1114,8 @@ impl crate::ObservationTransitionIngress for Pa fn note_observation_transition( feed: T::ObservationFeedId, transition: crate::ObservationTransition, + cause_provenance: crate::TriggerCauseProvenance, ) -> DispatchResult { - Pallet::::note_observation_transition(feed, transition) + Pallet::::note_observation_transition_with_provenance(feed, transition, cause_provenance) } } diff --git a/template/pallets/actors/src/scheduler.rs b/template/pallets/actors/src/scheduler.rs index 25a51d6c..a67e49ac 100644 --- a/template/pallets/actors/src/scheduler.rs +++ b/template/pallets/actors/src/scheduler.rs @@ -1,5 +1,10 @@ use super::pallet::*; -use super::{AddressEvent, AssetOps, FundingAuthority, IngressFailure, weights::WeightInfo}; +use super::{ + AddressEvent, AssetOps, BlockResourceDomain, BlockResourceLimits, BlockResourceState, + FundingAuthority, IngressFailure, StepControlExecution, StepControlOutcome, StepControlPhase, + StepControlPlacement, StepControlWeightProvider as _, TaskEffectWeightProvider as _, + weights::WeightInfo, +}; use alloc::{vec, vec::Vec}; use frame::prelude::*; use polkadot_sdk::frame_support::storage::transactional::with_transaction_opaque_err; @@ -65,6 +70,39 @@ pub(crate) enum ActivationOutcome { Closed, } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum ObservationActivationOutcome { + Ordinary(ActivationOutcome), + TerminalDeferred, +} + +pub(crate) struct ObservationQueueCandidate { + state: ObservationActivationState, + hot: ActorHotStateOf, +} + +pub(crate) struct ObservationWakeupCandidate { + actor_id: ActorId, + actor_type: ActorType, + sovereign_account: T::AccountId, + hot: ActorHotStateOf, + wakeup_key: WakeupKey>, +} + +pub(crate) enum ObservationPlacementCandidate { + Queue(ObservationQueueCandidate), + Wakeup(ObservationWakeupCandidate), +} + +impl ObservationPlacementCandidate { + pub(crate) fn wakeup_key(&self) -> Option>> { + match self { + Self::Queue(_) => None, + Self::Wakeup(candidate) => Some(candidate.wakeup_key), + } + } +} + /// Typed activation failure. Temporary pressure preserves the producer's /// retryable work; permanent corruption fails the enclosing transition closed. #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -86,9 +124,9 @@ pub(crate) enum PrimeSchedulePlan { } pub(crate) enum ActivationAction { - CloseWindowExpired, + Close(CloseReason), CoalesceLive, - EnqueueCadenced(Result, EnqueueOutcome>), + EnqueueTemporal(Result, EnqueueOutcome>), PrimeSchedule(Result>, EnqueueOutcome>), } @@ -108,6 +146,7 @@ std::thread_local! { static CORRUPT_QUEUE_BEFORE_CLOSE_CONSUME: core::cell::Cell = const { core::cell::Cell::new(false) }; static FAIL_WAKEUP_PLACEMENT_WITH_CAPACITY: core::cell::Cell = const { core::cell::Cell::new(false) }; static QUEUE_APPEND_COMMITS: core::cell::Cell = const { core::cell::Cell::new(0) }; + static OBSERVATION_WAKEUP_COHORT_COMMITS: core::cell::Cell = const { core::cell::Cell::new(0) }; static CROSSING_CURSOR_COMMITS: core::cell::Cell = const { core::cell::Cell::new(0) }; static FIRST_CROSSING_BRANCH_WEIGHT: core::cell::Cell> = const { core::cell::Cell::new(None) }; } @@ -121,12 +160,19 @@ enum BlockKind { NonWeight, } -#[derive(Clone, Copy)] -enum AttemptTransactionError { +#[derive(Clone, Copy, Debug)] +pub(crate) enum AttemptTransactionError { FeeCollection, + StateHold, Invariant, } +pub(crate) struct StepCommitEvidence { + pub(crate) closed_for_exhaustion: bool, + pub(crate) actual_control_weight: Weight, + pub(crate) actual_effect_weight: Weight, +} + impl From for AttemptTransactionError { fn from(_: polkadot_sdk::sp_runtime::DispatchError) -> Self { Self::Invariant @@ -137,43 +183,121 @@ impl From for AttemptTransactionError { /// derived from the terminal block reason (spec 8.6.3). pub(crate) struct CyclePass { pub(crate) consumed: Weight, + pub(crate) effect_consumed: Weight, + pub(crate) effect_reconciliation_uncertain: bool, pub(crate) starved: bool, } +impl CyclePass { + pub(crate) fn reconciled_domains(&self) -> Option<(Weight, Weight)> { + if self.effect_reconciliation_uncertain { + return None; + } + self + .consumed + .checked_sub(&self.effect_consumed) + .map(|control| (control, self.effect_consumed)) + } +} + enum FifoStepResult { NoWork, Progress { executed: bool }, Blocked(BlockKind), } -enum HeadDiscovery { +enum HeadDiscovery { Empty, - Head(QueueTicket, QueueEntry), + Head(QueueTicket, QueueEntry), WeightStall, PassExhausted, InvariantStall, } impl Pallet { + #[cfg(any(test, feature = "runtime-benchmarks"))] pub(crate) fn execute_cycle(remaining_weight: Weight) -> CyclePass { + Self::execute_cycle_to_cutoff(remaining_weight, NextQueueTicket::::get()) + } + + pub(crate) fn execute_cycle_to_cutoff( + remaining_weight: Weight, + cutoff: QueueTicket, + ) -> CyclePass { + Self::execute_cycle_to_cutoff_inner(remaining_weight, cutoff, None) + } + + pub(crate) fn execute_cycle_to_cutoff_with_resources( + remaining_weight: Weight, + cutoff: QueueTicket, + state: &mut BlockResourceState>, + limits: BlockResourceLimits, + effect_domain: BlockResourceDomain, + control_maximum: Weight, + ) -> CyclePass { + Self::execute_cycle_to_cutoff_inner( + remaining_weight, + cutoff, + Some((state, limits, effect_domain, control_maximum)), + ) + } + + fn execute_cycle_to_cutoff_inner( + remaining_weight: Weight, + cutoff: QueueTicket, + mut resources: Option<( + &mut BlockResourceState>, + BlockResourceLimits, + BlockResourceDomain, + Weight, + )>, + ) -> CyclePass { if remaining_weight.is_zero() { return CyclePass { consumed: Weight::zero(), + effect_consumed: Weight::zero(), + effect_reconciliation_uncertain: false, starved: false, }; } + let mut pass_control_reservation = match resources.as_mut() { + Some((state, limits, _, control_maximum)) => { + match state.reserve(*limits, BlockResourceDomain::ActorControl, *control_maximum) { + Ok(reservation) => Some(reservation), + Err(_) => { + state.halt_optional_actor_work(); + return CyclePass { + consumed: Weight::zero(), + effect_consumed: Weight::zero(), + effect_reconciliation_uncertain: false, + starved: true, + }; + } + } + } + None => None, + }; let mut cycle_meter = WeightMeter::with_limit(remaining_weight); + let mut scan_control_meter = resources + .as_ref() + .map(|(_, _, _, control_maximum)| WeightMeter::with_limit(*control_maximum)); let now = frame_system::Pallet::::block_number(); - // The cutoff is captured after the on_idle wakeup and fanout phases (spec 8.2.1); - // only tickets below the cutoff may execute in this actor-service pass. - let cutoff = NextQueueTicket::::get(); + // Only tickets below the caller-owned stage cutoff may execute in this actor-service pass. let max_executions = T::MaxExecutionsPerBlock::get(); let max_scanned = T::MaxQueueEntriesScannedPerBlock::get(); let mut executed = 0u32; let mut scanned = 0u32; + let mut effect_consumed = Weight::zero(); + let mut effect_reconciliation_uncertain = false; let mut starved = false; while executed < max_executions && scanned < max_scanned { - let head = Self::live_queue_head(cutoff, &mut cycle_meter, &mut scanned, max_scanned); + let head = Self::live_queue_head( + cutoff, + &mut cycle_meter, + scan_control_meter.as_mut(), + &mut scanned, + max_scanned, + ); match head { HeadDiscovery::Empty => break, HeadDiscovery::WeightStall | HeadDiscovery::InvariantStall => { @@ -182,7 +306,16 @@ impl Pallet { } HeadDiscovery::PassExhausted => break, HeadDiscovery::Head(position, entry) => { - match Self::service_live_queue_entry((position, entry), now, &mut cycle_meter) { + match Self::service_live_queue_entry( + (position, entry), + now, + &mut cycle_meter, + &mut effect_consumed, + &mut effect_reconciliation_uncertain, + resources + .as_mut() + .map(|(state, limits, domain, _)| (&mut **state, *limits, *domain)), + ) { FifoStepResult::Progress { executed: did_execute, } => executed = executed.saturating_add(u32::from(did_execute)), @@ -195,13 +328,32 @@ impl Pallet { } } } - CyclePass { + let pass = CyclePass { consumed: cycle_meter.consumed(), + effect_consumed, + effect_reconciliation_uncertain, starved, + }; + if let (Some((state, _, _, control_maximum)), Some(reservation)) = + (resources.as_mut(), pass_control_reservation.as_mut()) + { + let actual_control = pass + .reconciled_domains() + .map(|(actual, _)| actual) + .unwrap_or(*control_maximum); + if state.settle(reservation, actual_control).is_err() { + // Preserve the admitted maximum while releasing transition authority. This second + // settlement is deterministic because the reservation still owns that exact maximum. + let _ = state.settle(reservation, *control_maximum); + state.halt_optional_actor_work(); + } else if pass.reconciled_domains().is_none() { + state.halt_optional_actor_work(); + } } + pass } - fn classify_current_queue(cutoff: QueueTicket) -> HeadDiscovery { + fn classify_current_queue(cutoff: QueueTicket) -> HeadDiscovery> { if Self::queue_topology_preflight(QueueMutation::Head).is_err() { return HeadDiscovery::InvariantStall; } @@ -225,15 +377,20 @@ impl Pallet { fn live_queue_head( cutoff: QueueTicket, cycle_meter: &mut WeightMeter, + mut scan_control_meter: Option<&mut WeightMeter>, scanned: &mut u32, max_scanned: u32, - ) -> HeadDiscovery { + ) -> HeadDiscovery> { let scan_weight = T::WeightInfo::scheduler_paged_tombstone_drain(1); while *scanned < max_scanned { if Self::queue_topology_preflight(QueueMutation::Head).is_err() { return HeadDiscovery::InvariantStall; } - if !cycle_meter.can_consume(scan_weight) { + if !cycle_meter.can_consume(scan_weight) + || scan_control_meter + .as_ref() + .is_some_and(|meter| !meter.can_consume(scan_weight)) + { return if Self::head_blocked_by_weight(cutoff) { HeadDiscovery::WeightStall } else { @@ -241,6 +398,9 @@ impl Pallet { }; } cycle_meter.consume(scan_weight); + if let Some(meter) = scan_control_meter.as_deref_mut() { + meter.consume(scan_weight); + } let before = QueueHead::::get(); let stats = match Self::paged_drain_tombstones(cutoff, 1) { Ok(stats) => stats, @@ -268,10 +428,10 @@ impl Pallet { scan_limit: u32, scanned_start: u32, weight: Weight, - ) -> (u8, Option, u32) { + ) -> (u8, Option>>, u32) { let mut meter = WeightMeter::with_limit(weight); let mut scanned = scanned_start; - let discovery = Self::live_queue_head(cutoff, &mut meter, &mut scanned, scan_limit); + let discovery = Self::live_queue_head(cutoff, &mut meter, None, &mut scanned, scan_limit); match discovery { HeadDiscovery::Empty => (0, None, scanned), HeadDiscovery::Head(_, entry) => (1, Some(entry), scanned), @@ -281,10 +441,212 @@ impl Pallet { } } + fn charge_pipeline_opening( + actor_id: ActorId, + instance: &ActiveActorViewOf, + ) -> Result<(), AttemptTransactionError> { + if instance.cycle_state != CycleState::Idle + || instance.actor_class.actor_type() != ActorType::User + { + return Ok(()); + } + let breakdown = + Self::collect_pipeline_fee(actor_id, ActorType::User, &instance.sovereign_account) + .map_err(|_| AttemptTransactionError::FeeCollection)?; + Self::deposit_event(Event::PipelineFeeCharged { + actor_id, + fee: breakdown.total_fee, + }); + Ok(()) + } + + pub(crate) fn execute_zero_step_opening_and_place( + actor_id: ActorId, + mut instance: ActiveActorViewOf, + now: BlockNumberFor, + ) -> Result<(), AttemptTransactionError> { + if !instance.steps.is_empty() + || instance.cycle_state != CycleState::Idle + || ActorRunStateStore::::contains_key(actor_id) + { + return Err(AttemptTransactionError::Invariant); + } + Self::rearm_trigger_after_opening(actor_id).map_err(|_| AttemptTransactionError::Invariant)?; + Self::charge_pipeline_opening(actor_id, &instance)?; + let (fee_collection_failed, disposition) = + Self::execute_zero_step_cycle(actor_id, instance.clone(), now); + if fee_collection_failed + || !matches!( + disposition, + AttemptDisposition::Completed | AttemptDisposition::Closed(_) + ) + { + return Err(AttemptTransactionError::Invariant); + } + if matches!(disposition, AttemptDisposition::Closed(_)) { + return Ok(()); + } + let hot = ActorHot::::get(actor_id).ok_or(AttemptTransactionError::Invariant)?; + instance.cycle_nonce = instance + .cycle_nonce + .checked_add(1) + .ok_or(AttemptTransactionError::Invariant)?; + instance.lifecycle = hot.lifecycle; + instance.cycle_state = hot.cycle_state; + instance.pending_signal = hot.pending_signal; + instance.queue_ticket = hot.queue_ticket; + instance.wakeup_pointer = hot.wakeup_pointer; + instance.last_cycle_block = hot.last_cycle_block; + if Self::schedule_next_work_loaded(actor_id, &instance, hot, now, true).is_err() { + if Self::close_for_scheduler_index_exhaustion(actor_id).is_err() { + return Err(AttemptTransactionError::Invariant); + } + } + Self::reconcile_actor_state_hold(actor_id).map_err(|_| AttemptTransactionError::StateHold)?; + Ok(()) + } + + pub(crate) fn execute_current_step_and_place( + actor_id: ActorId, + execution_instance: ActiveActorViewOf, + commit_plan: CurrentStepPlanOf, + now: BlockNumberFor, + ) -> Result { + let control_step = commit_plan.loaded_step.step.clone(); + let effect_task = control_step.task.clone(); + let action_cycle_nonce = commit_plan.ticket.cycle_nonce; + let action_step_index = commit_plan.ticket.cursor; + let fee_actor_type = execution_instance.actor_class.actor_type(); + let fee_actor = execution_instance.sovereign_account.clone(); + let reserved_control_weight = commit_plan.loaded_step.resources.control; + let reserved_effect_weight = commit_plan.loaded_step.resources.effect; + let control_phase = match execution_instance.cycle_state { + CycleState::Idle => StepControlPhase::Opening, + CycleState::Running => StepControlPhase::Running, + CycleState::Suspended => StepControlPhase::Suspended, + }; + let control_context = Self::execution_step_control_weight_context( + &execution_instance, + commit_plan.run.as_ref(), + &commit_plan.loaded_step, + ) + .ok_or(AttemptTransactionError::Invariant)?; + if control_phase == StepControlPhase::Opening + && T::StepControlWeight::maximum_control_weight(control_context, &control_step) + != Some(reserved_control_weight) + { + return Err(AttemptTransactionError::Invariant); + } + if control_phase == StepControlPhase::Opening { + Self::rearm_trigger_after_opening(actor_id) + .map_err(|_| AttemptTransactionError::Invariant)?; + } + Self::charge_pipeline_opening(actor_id, &execution_instance)?; + let mut placement_instance = execution_instance.clone(); + let (_control_observation, fee_collection_failed, effect_execution, disposition) = + Self::execute_current_step_plan(actor_id, execution_instance, commit_plan, now); + if fee_collection_failed { + return Err(AttemptTransactionError::FeeCollection); + } + let actual_effect_weight = + T::TaskEffectWeight::actual_effect_weight(&effect_task, effect_execution) + .ok_or(AttemptTransactionError::Invariant)?; + if !actual_effect_weight.all_lte(reserved_effect_weight) { + return Err(AttemptTransactionError::Invariant); + } + let mut closed_for_exhaustion = false; + let placement = if let Some(hot) = ActorHot::::get(actor_id) { + placement_instance.lifecycle = hot.lifecycle; + placement_instance.cycle_state = hot.cycle_state; + placement_instance.unsuccessful_attempt_streak = hot.unsuccessful_attempt_streak; + placement_instance.pending_signal = hot.pending_signal; + placement_instance.queue_ticket = hot.queue_ticket; + placement_instance.wakeup_pointer = hot.wakeup_pointer; + placement_instance.schedule_anchor = hot.schedule_anchor; + placement_instance.temporal_anchor_tick = hot.trigger_runtime_state.temporal_anchor_tick(); + placement_instance.last_cycle_block = hot.last_cycle_block; + match Self::schedule_next_work_loaded(actor_id, &placement_instance, hot, now, true) { + Ok(placement) => placement, + Err(error) => { + if !Self::scheduler_index_is_exhausted(error) + || Self::close_for_scheduler_index_exhaustion(actor_id).is_err() + { + return Err(AttemptTransactionError::Invariant); + } + closed_for_exhaustion = true; + StepControlPlacement::None + } + } + } else { + StepControlPlacement::None + }; + let control_execution = StepControlExecution { + phase: control_phase, + outcome: match disposition { + AttemptDisposition::Completed => StepControlOutcome::Completed, + AttemptDisposition::Continued => StepControlOutcome::Continued, + AttemptDisposition::Failed => StepControlOutcome::Failed, + AttemptDisposition::Suspended => StepControlOutcome::Suspended, + AttemptDisposition::Closed( + CloseReason::ProductiveCycleCompleted | CloseReason::AutoCloseNonceReached, + ) => StepControlOutcome::Completed, + AttemptDisposition::Closed(_) => StepControlOutcome::Failed, + }, + placement, + }; + let actual_control_weight = T::StepControlWeight::actual_control_weight( + control_context, + &control_step, + reserved_control_weight, + control_execution, + ) + .ok_or(AttemptTransactionError::Invariant)?; + if !actual_control_weight.all_lte(reserved_control_weight) { + return Err(AttemptTransactionError::Invariant); + } + let actual_fee = Self::maximum_current_action_fee( + fee_actor_type, + &control_step, + ActorStepResourceEnvelope { + control: Weight::zero(), + effect: actual_effect_weight, + }, + ) + .map_err(|_| AttemptTransactionError::Invariant)?; + if fee_actor_type == ActorType::User && !actual_fee.total_fee.is_zero() { + Self::collect_user_step_fee(&fee_actor, actual_fee.total_fee) + .map_err(|_| AttemptTransactionError::FeeCollection)?; + } + if matches!(effect_execution, super::TaskEffectExecution::Invoked) + && !matches!(control_step.task, super::types::Task::StopCycle) + { + Self::deposit_event(Event::ActionFeeCharged { + actor_id, + cycle_nonce: action_cycle_nonce, + step_index: action_step_index, + actual_effect_weight, + fee: actual_fee.effect_fee, + }); + } + Self::reconcile_actor_state_hold(actor_id).map_err(|_| AttemptTransactionError::StateHold)?; + Ok(StepCommitEvidence { + closed_for_exhaustion, + actual_control_weight, + actual_effect_weight, + }) + } + fn service_live_queue_entry( - (position, entry): (QueueTicket, QueueEntry), + (position, entry): (QueueTicket, QueueEntry>), now: BlockNumberFor, cycle_meter: &mut WeightMeter, + effect_consumed: &mut Weight, + effect_reconciliation_uncertain: &mut bool, + mut resources: Option<( + &mut BlockResourceState>, + BlockResourceLimits, + BlockResourceDomain, + )>, ) -> FifoStepResult { let consume_weight = T::WeightInfo::scheduler_paged_consume_preserve_page() .max(T::WeightInfo::scheduler_paged_consume_delete_page()); @@ -292,7 +654,8 @@ impl Pallet { if !cycle_meter.can_consume(state_probe_weight.saturating_add(consume_weight)) { return FifoStepResult::Blocked(BlockKind::Weight); } - let LoadedActorStateOf::Active(state) = Self::load_actor_state(entry.actor_id) else { + let Some((state, admission, loaded_step)) = Self::load_actor_service_state(entry.actor_id) + else { cycle_meter.consume(state_probe_weight); return FifoStepResult::Blocked(BlockKind::NonWeight); }; @@ -300,11 +663,42 @@ impl Pallet { if state.hot.queue_ticket != Some(entry.ticket) { return FifoStepResult::NoWork; } + let current_ticket = loaded_step.as_ref().and_then(|_| { + Self::build_actor_step_ticket( + entry.actor_id, + entry.ticket, + entry.eligible_at, + &state.identity, + &state.hot, + state.run_state.as_ref(), + &admission, + ) + }); + let current_plan = loaded_step.as_ref().and_then(|loaded_step| { + let ticket = current_ticket?; + let maximum_fee = Self::maximum_current_action_fee( + state.identity.actor_class.actor_type(), + &loaded_step.step, + loaded_step.resources, + ) + .ok()?; + Self::build_current_step_plan( + entry.actor_id, + state.identity.clone(), + state.hot.clone(), + state.run_state.clone(), + state.funding.clone(), + admission.clone(), + ticket, + loaded_step.clone(), + maximum_fee, + ) + }); if state.hot.cycle_state == CycleState::Suspended { if state - .continuation + .run_state .as_ref() - .is_some_and(|continuation| continuation.last_attempt_block == now) + .is_some_and(|run_state| run_state.last_attempt_block == now) { return FifoStepResult::Blocked(BlockKind::NonWeight); } @@ -345,13 +739,124 @@ impl Pallet { return FifoStepResult::Progress { executed: false }; } let actor_id = entry.actor_id; - let loaded_continuation = state.continuation; + let loaded_run_state = state.run_state.clone(); let instance = Self::derive_active_actor_view(state.identity, state.hot, state.contract); - match Self::apply_admission_loaded(&instance, loaded_continuation.as_ref(), cycle_meter) { + match Self::apply_admission_loaded( + actor_id, + &instance, + loaded_run_state.as_ref(), + current_plan.as_ref(), + cycle_meter, + ) { AdmissionDecision::Admit { weight, terminal_cleanup_reserved, } => { + if loaded_step.is_none() { + if !instance.steps.is_empty() + || instance.cycle_state != CycleState::Idle + || state.run_state.is_some() + { + return FifoStepResult::Blocked(BlockKind::NonWeight); + } + let attempt_weight = consume_weight.saturating_add(weight); + if !cycle_meter.can_consume(attempt_weight) { + return FifoStepResult::Blocked(BlockKind::Weight); + } + let outcome = polkadot_sdk::frame_support::storage::with_transaction(|| { + if Self::paged_consume_loaded_head_at( + position, + entry.actor_id, + entry.ticket, + queue_owner_hot, + ) + .is_err() + { + return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err( + AttemptTransactionError::Invariant, + )); + } + match Self::execute_zero_step_opening_and_place(actor_id, instance, now) { + Ok(()) => polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(())), + Err(error) => { + polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)) + } + } + }); + cycle_meter.consume(attempt_weight); + return match outcome { + Ok(()) => FifoStepResult::Progress { executed: true }, + Err(AttemptTransactionError::FeeCollection) => { + FifoStepResult::Blocked(BlockKind::FeeCollection) + } + Err(AttemptTransactionError::StateHold | AttemptTransactionError::Invariant) => { + FifoStepResult::Blocked(BlockKind::NonWeight) + } + }; + } + if current_ticket != Some(entry) || current_plan.is_none() { + let outcome: DispatchResult = + polkadot_sdk::frame_support::storage::with_transaction(|| { + if Self::paged_consume_loaded_head_at( + position, + entry.actor_id, + entry.ticket, + queue_owner_hot, + ) + .is_err() + { + return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err( + polkadot_sdk::sp_runtime::DispatchError::Other( + "stale Step ticket consume failed", + ), + )); + } + ActorHot::::mutate(entry.actor_id, |maybe| { + if let Some(current) = maybe + && current.queue_ticket == Some(entry.ticket) + { + current.queue_ticket = None; + } + }); + match Self::loaded_active_view_for_placement(entry.actor_id) { + Ok(Some(updated)) => { + if Self::schedule_next_work(entry.actor_id, &updated, now, true).is_err() { + return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback( + Err(polkadot_sdk::sp_runtime::DispatchError::Other( + "stale Step ticket successor placement failed", + )), + ); + } + } + Ok(None) => {} + Err(_) => { + return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err( + polkadot_sdk::sp_runtime::DispatchError::Other( + "stale Step ticket placement state is corrupt", + ), + )); + } + } + polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(())) + }); + if outcome.is_err() { + return FifoStepResult::Blocked(BlockKind::NonWeight); + } + cycle_meter.consume(consume_weight); + return FifoStepResult::Progress { executed: false }; + } + let execution_instance = + if instance.cycle_state == CycleState::Idle && instance.steps.len() > 1 { + let LoadedActorStateOf::Active(full_state) = Self::load_actor_state(actor_id) else { + return FifoStepResult::Blocked(BlockKind::NonWeight); + }; + Self::derive_active_actor_view(full_state.identity, full_state.hot, full_state.contract) + } else { + instance + }; + let Some(commit_plan) = current_plan else { + return FifoStepResult::Blocked(BlockKind::NonWeight); + }; let attempt_weight = consume_weight.saturating_add(weight); let exhaustion_close_weight = if terminal_cleanup_reserved { Weight::zero() @@ -361,6 +866,17 @@ impl Pallet { if !cycle_meter.can_consume(attempt_weight.saturating_add(exhaustion_close_weight)) { return FifoStepResult::Blocked(BlockKind::Weight); } + let reserved_control_weight = commit_plan.loaded_step.resources.control; + let reserved_effect_weight = commit_plan.loaded_step.resources.effect; + let mut resource_reservation = match resources.as_mut() { + Some((state, limits, effect_domain)) => { + match state.reserve(*limits, *effect_domain, reserved_effect_weight) { + Ok(reservation) => Some(reservation), + Err(_) => return FifoStepResult::Blocked(BlockKind::Weight), + } + } + None => None, + }; let outcome = polkadot_sdk::frame_support::storage::with_transaction(|| { if Self::paged_consume_loaded_head_at( position, @@ -374,47 +890,70 @@ impl Pallet { AttemptTransactionError::Invariant, )); } - let (_actual, fee_collection_failed) = - Self::execute_single_cycle(actor_id, instance, now); - if fee_collection_failed { - return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err( - AttemptTransactionError::FeeCollection, - )); - } - match Self::loaded_active_view_for_placement(actor_id) { - Ok(Some(updated)) => { - if let Err(error) = Self::schedule_next_work(actor_id, &updated, now, true) { - if !Self::scheduler_index_is_exhausted(error) - || Self::close_for_scheduler_index_exhaustion(actor_id).is_err() - { - return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err( - AttemptTransactionError::Invariant, - )); - } - return polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(true)); + match Self::execute_current_step_and_place(actor_id, execution_instance, commit_plan, now) + { + Ok(evidence) => { + if let (Some((state, _, _)), Some(reservation)) = + (resources.as_mut(), resource_reservation.as_mut()) + && state + .settle(reservation, evidence.actual_effect_weight) + .is_err() + { + state.halt_optional_actor_work(); + return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err( + AttemptTransactionError::Invariant, + )); } + polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(evidence)) } - Ok(None) => {} - Err(_) => { - return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err( - AttemptTransactionError::Invariant, - )); + Err(error) => { + polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)) } } - polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(false)) }); - cycle_meter.consume(attempt_weight); + if outcome.is_err() + && let (Some(reservation), Some((state, _, _))) = + (resource_reservation.as_mut(), resources.as_mut()) + { + // The storage attempt rolled back its settlement together with the effect. Retain the + // admitted maximum, but consume this reservation's transition authority outside that + // transaction so external user work and final reconciliation remain reachable. + let _ = state.settle(reservation, reserved_effect_weight); + state.halt_optional_actor_work(); + } match outcome { - Ok(closed_for_exhaustion) => { + Ok(StepCommitEvidence { + closed_for_exhaustion, + actual_control_weight, + actual_effect_weight, + }) => { + let Some(non_step_reservation) = attempt_weight + .checked_sub(&reserved_control_weight) + .and_then(|weight| weight.checked_sub(&reserved_effect_weight)) + else { + cycle_meter.consume(attempt_weight); + return FifoStepResult::Blocked(BlockKind::NonWeight); + }; + let actual_attempt_weight = non_step_reservation + .saturating_add(actual_control_weight) + .saturating_add(actual_effect_weight); + cycle_meter.consume(actual_attempt_weight); + effect_consumed.saturating_accrue(actual_effect_weight); if closed_for_exhaustion { cycle_meter.consume(exhaustion_close_weight); } FifoStepResult::Progress { executed: true } } Err(AttemptTransactionError::FeeCollection) => { + *effect_reconciliation_uncertain |= !reserved_effect_weight.is_zero(); + cycle_meter.consume(attempt_weight); FifoStepResult::Blocked(BlockKind::FeeCollection) } - Err(AttemptTransactionError::Invariant) => FifoStepResult::Blocked(BlockKind::NonWeight), + Err(AttemptTransactionError::StateHold | AttemptTransactionError::Invariant) => { + *effect_reconciliation_uncertain |= !reserved_effect_weight.is_zero(); + cycle_meter.consume(attempt_weight); + FifoStepResult::Blocked(BlockKind::NonWeight) + } } } AdmissionDecision::Close { reason, weight } => { @@ -508,6 +1047,32 @@ impl Pallet { } } + fn enqueue_loaded(actor_id: ActorId, hot: ActorHotStateOf) -> Result<(), EnqueueOutcome> { + let result = + with_transaction_opaque_err( + || match Self::preflight_paged_enqueue_loaded(actor_id, hot) { + Ok(plan) => { + Self::commit_paged_enqueue(plan); + polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(())) + } + Err(error) => { + polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)) + } + }, + ) + .map_err(|_| EnqueueOutcome::CorruptedTopology)?; + match result { + Ok(()) | Err(EnqueueOutcome::AlreadyLive) => Ok(()), + Err(EnqueueOutcome::CapacityUnavailable) => { + let next_block = frame_system::Pallet::::block_number() + .checked_add(&One::one()) + .ok_or(EnqueueOutcome::SchedulerIndexExhausted)?; + Self::defer_wakeup(actor_id, next_block) + } + Err(other) => Err(other), + } + } + #[cfg(test)] pub(crate) fn test_corrupt_queue_before_close_consume() { CORRUPT_QUEUE_BEFORE_CLOSE_CONSUME.with(|flag| flag.set(true)); @@ -635,6 +1200,96 @@ impl Pallet { Self::preflight_paged_enqueue_cohort(vec![(actor_id, hot)]) } + fn preflight_paged_enqueue_observation_loaded( + state: &ObservationActivationState, + hot: ActorHotStateOf, + ) -> Result, EnqueueOutcome> { + let topology = Self::queue_topology_preflight(QueueMutation::Enqueue)?; + let mut plan = QueueAppendPlan { + actors: Vec::new(), + pages: Vec::new(), + next_ticket: NextQueueTicket::::get(), + next_tail: topology.tail, + next_occupancy: topology.occupancy, + }; + Self::reserve_following_paged_enqueue_observation(&mut plan, state, hot)?; + Ok(plan) + } + + fn reserve_following_paged_enqueue_observation( + plan: &mut QueueAppendPlan, + state: &ObservationActivationState, + mut hot: ActorHotStateOf, + ) -> Result<(), EnqueueOutcome> { + let actor_id = state.actor_id; + if hot.queue_ticket.is_some() || plan.next_occupancy >= T::MaxQueueLength::get() { + return Err(EnqueueOutcome::CapacityUnavailable); + } + let ticket = plan.next_ticket; + let next_ticket = ticket + .checked_add(1) + .ok_or(EnqueueOutcome::TicketExhausted)?; + let position = plan.next_tail; + let next_tail = position + .checked_add(1) + .ok_or(EnqueueOutcome::SchedulerIndexExhausted)?; + let next_occupancy = plan + .next_occupancy + .checked_add(1) + .ok_or(EnqueueOutcome::SchedulerIndexExhausted)?; + hot.queue_ticket = Some(ticket); + let (cycle_nonce, cursor, eligible_at) = match (hot.cycle_state, state.run_head.as_ref()) { + (CycleState::Idle, None) => ( + state + .identity + .cycle_nonce + .checked_add(1) + .ok_or(EnqueueOutcome::SchedulerIndexExhausted)?, + 0, + frame_system::Pallet::::block_number(), + ), + (CycleState::Running | CycleState::Suspended, Some(run)) => { + (run.cycle_nonce, run.cursor, run.eligible_at) + } + _ => return Err(EnqueueOutcome::CorruptedTopology), + }; + let step_ticket = ActorStepTicket { + actor_id, + cycle_nonce, + cursor, + ticket, + eligible_at, + contract_commitment: ActorContractCommitment { + semantic_contract_id: state.authority.semantic_contract_id, + body_commitment: state.authority.body_commitment, + }, + }; + let (page_id, slot) = Self::queue_page_and_slot(position); + let page = if let Some((_, page)) = plan + .pages + .iter_mut(/* deos-bypass: bounded-iter */) + .find(|(planned_page_id, _)| *planned_page_id == page_id) + { + page + } else { + plan + .pages + .push((page_id, QueuePages::::get(page_id).unwrap_or_default())); + let Some((_, page)) = plan.pages.last_mut() else { + return Err(EnqueueOutcome::CorruptedTopology); + }; + page + }; + if page.len() != slot || page.try_push(step_ticket).is_err() { + return Err(EnqueueOutcome::CorruptedTopology); + } + plan.actors.push((actor_id, hot)); + plan.next_ticket = next_ticket; + plan.next_tail = next_tail; + plan.next_occupancy = next_occupancy; + Ok(()) + } + pub(crate) fn reserve_following_paged_enqueue( plan: &mut QueueAppendPlan, actor_id: ActorId, @@ -666,6 +1321,26 @@ impl Pallet { .next_occupancy .checked_add(1) .ok_or(EnqueueOutcome::SchedulerIndexExhausted)?; + hot.queue_ticket = Some(ticket); + let identity = ActorIdentities::::get(actor_id).ok_or(EnqueueOutcome::CorruptedTopology)?; + let run_state = ActorRunStateStore::::get(actor_id); + let admission = + ActorAdmissionCertificates::::get(actor_id).ok_or(EnqueueOutcome::CorruptedTopology)?; + let eligible_at = run_state + .as_ref() + .map_or_else(frame_system::Pallet::::block_number, |run| { + run.eligible_at + }); + let step_ticket = Self::build_actor_step_ticket( + actor_id, + ticket, + eligible_at, + &identity, + &hot, + run_state.as_ref(), + &admission, + ) + .ok_or(EnqueueOutcome::CorruptedTopology)?; let (page_id, slot) = Self::queue_page_and_slot(position); let page = if let Some((_, page)) = plan .pages @@ -682,10 +1357,9 @@ impl Pallet { }; page }; - if page.len() != slot || page.try_push(QueueEntry { ticket, actor_id }).is_err() { + if page.len() != slot || page.try_push(step_ticket).is_err() { return Err(EnqueueOutcome::CorruptedTopology); } - hot.queue_ticket = Some(ticket); plan.actors.push((actor_id, hot)); plan.next_ticket = next_ticket; plan.next_tail = next_tail; @@ -694,14 +1368,14 @@ impl Pallet { } fn preflight_paged_enqueue(actor_id: ActorId) -> Result, EnqueueOutcome> { - let hot = match Self::load_actor_state(actor_id) { - LoadedActorStateOf::Active(state) => state.hot, - LoadedActorStateOf::NotRegistered | LoadedActorStateOf::Dormant(_) => { - return Err(EnqueueOutcome::CapacityUnavailable); - } - LoadedActorStateOf::Corrupt => return Err(EnqueueOutcome::CorruptedTopology), + let Some((state, _, _)) = Self::load_actor_service_state(actor_id) else { + return if ActorHot::::contains_key(actor_id) { + Err(EnqueueOutcome::CorruptedTopology) + } else { + Err(EnqueueOutcome::CapacityUnavailable) + }; }; - Self::preflight_paged_enqueue_loaded(actor_id, hot) + Self::preflight_paged_enqueue_loaded(actor_id, state.hot) } #[cfg(test)] @@ -818,70 +1492,504 @@ impl Pallet { if recorded.get().is_none() { recorded.set(Some(weight)); } - }); - } - - #[cfg(test)] - pub(crate) fn test_reset_queue_append_commits() { - QUEUE_APPEND_COMMITS.with(|count| count.set(0)); - } - - #[cfg(test)] - pub(crate) fn test_queue_append_commits() -> u32 { - QUEUE_APPEND_COMMITS.with(core::cell::Cell::get) + }); + } + + #[cfg(test)] + pub(crate) fn test_reset_queue_append_commits() { + QUEUE_APPEND_COMMITS.with(|count| count.set(0)); + } + + #[cfg(test)] + pub(crate) fn test_queue_append_commits() -> u32 { + QUEUE_APPEND_COMMITS.with(core::cell::Cell::get) + } + + pub(crate) fn commit_paged_enqueue(plan: QueueAppendPlan) { + #[cfg(test)] + QUEUE_APPEND_COMMITS.with(|count| count.set(count.get().saturating_add(1))); + for (page_id, page) in plan.pages { + QueuePages::::insert(page_id, page); + } + QueueTail::::put(plan.next_tail); + QueueOccupancy::::put(plan.next_occupancy); + NextQueueTicket::::put(plan.next_ticket); + for (actor_id, hot) in plan.actors { + ActorHot::::insert(actor_id, hot); + } + } + + pub fn try_paged_enqueue(actor_id: ActorId) -> Result<(), EnqueueOutcome> { + with_transaction_opaque_err(|| match Self::preflight_paged_enqueue(actor_id) { + Ok(plan) => { + Self::commit_paged_enqueue(plan); + polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(())) + } + Err(error) => polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)), + }) + .map_err(|_| EnqueueOutcome::CorruptedTopology)? + } + + fn scheduler_index_is_exhausted(outcome: EnqueueOutcome) -> bool { + matches!( + outcome, + EnqueueOutcome::TicketExhausted + | EnqueueOutcome::SchedulerIndexExhausted + | EnqueueOutcome::WakeupIndexExhausted + ) + } + + fn close_for_scheduler_index_exhaustion(actor_id: ActorId) -> DispatchResult { + let instance = Self::active_actor_view(actor_id).ok_or(Error::::ActorInvariant)?; + Self::finalize_actor(actor_id, &instance, CloseReason::SchedulerIndexExhausted) + } + + pub(crate) fn request_activation( + actor_id: ActorId, + ) -> Result { + if polkadot_sdk::frame_support::storage::transactional::is_transactional() { + return Self::request_activation_inner(actor_id); + } + polkadot_sdk::frame_support::storage::with_transaction(|| match Self::request_activation_inner( + actor_id, + ) { + Ok(outcome) => polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(outcome)), + Err(error) => polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)), + }) + } + + #[cfg(any(test, feature = "runtime-benchmarks"))] + pub(crate) fn request_observation_activation_compact( + actor_id: ActorId, + feed: T::ObservationFeedId, + ) -> Result { + Self::request_observation_activation_compact_with_cause( + actor_id, + feed, + TriggerCauseProvenance::Deferred, + frame_system::Pallet::::block_number().saturated_into::(), + ) + } + + pub(crate) fn request_observation_activation_compact_with_cause( + actor_id: ActorId, + feed: T::ObservationFeedId, + cause_provenance: TriggerCauseProvenance, + cause_block: u64, + ) -> Result { + let activate = || match Self::request_observation_activation_compact_inner( + actor_id, + feed, + true, + cause_provenance, + cause_block, + )? { + ObservationActivationOutcome::Ordinary(outcome) => Ok(outcome), + ObservationActivationOutcome::TerminalDeferred => Err(ActivationFailure::Permanent( + Error::::ActorInvariant.into(), + )), + }; + if polkadot_sdk::frame_support::storage::transactional::is_transactional() { + return activate(); + } + polkadot_sdk::frame_support::storage::with_transaction(|| match activate() { + Ok(outcome) => polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(outcome)), + Err(error) => polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)), + }) + } + + pub(crate) fn request_observation_activation_ordinary_with_cause( + actor_id: ActorId, + feed: T::ObservationFeedId, + cause_provenance: TriggerCauseProvenance, + cause_block: u64, + ) -> Result { + Self::request_observation_activation_compact_inner( + actor_id, + feed, + false, + cause_provenance, + cause_block, + ) + } + + pub(crate) fn prepare_observation_placement_candidate( + actor_id: ActorId, + feed: T::ObservationFeedId, + _cause_provenance: TriggerCauseProvenance, + _cause_block: u64, + ) -> Result>, ActivationFailure> { + if IndexedTriggerDetectionDisabled::::contains_key(actor_id) { + return Ok(None); + } + let Some(state) = Self::load_observation_activation_state(actor_id, feed) else { + return Ok(None); + }; + let classification = + Self::classify_observation_activation_compact(&state).map_err(|error| { + ActivationFailure::Permanent(Self::classification_dispatch_error(error).into()) + })?; + if classification.terminal_reason.is_some() + || state.hot.queue_ticket.is_some() + || state.hot.wakeup_pointer.is_some() + { + return Ok(None); + } + let now = frame_system::Pallet::::block_number(); + let wakeup_at = if state.hot.lifecycle.is_paused() { + let Some(window) = state.authority.window else { + return Ok(None); + }; + Self::window_terminal_at(&window) + } else { + let eligible_at = if state.hot.cycle_state == CycleState::Suspended { + state + .run_head + .as_ref() + .ok_or(ActivationFailure::Permanent( + Error::::ActorRunInvariant.into(), + ))? + .eligible_at + } else { + let cooldown_anchor = state + .hot + .last_cycle_block + .unwrap_or(state.hot.schedule_anchor); + let cooldown_eligible_at = + if state.identity.cycle_nonce == 0 && state.hot.last_cycle_block.is_none() { + state.hot.schedule_anchor + } else { + cooldown_anchor + .checked_add(&state.authority.cooldown_blocks.into()) + .ok_or(ActivationFailure::Permanent( + Error::::SchedulerIndexExhausted.into(), + ))? + }; + let window_floor = state + .authority + .window + .map(|window| window.start) + .unwrap_or_else(Zero::zero); + now.max(cooldown_eligible_at).max(window_floor) + }; + state.authority.window.map_or(eligible_at, |window| { + eligible_at.min(Self::window_terminal_at(&window)) + }) + }; + let exact_next_block = now + .checked_add(&One::one()) + .ok_or(ActivationFailure::Permanent( + Error::::SchedulerIndexExhausted.into(), + ))?; + let mut hot = state.hot.clone(); + hot.pending_signal = true; + if wakeup_at >= exact_next_block { + return Ok(Some(ObservationPlacementCandidate::Wakeup( + ObservationWakeupCandidate { + actor_id, + actor_type: state.identity.actor_class.actor_type(), + sovereign_account: state.identity.sovereign_account.clone(), + hot, + wakeup_key: WakeupKey::Block(wakeup_at), + }, + ))); + } + Ok(Some(ObservationPlacementCandidate::Queue( + ObservationQueueCandidate { state, hot }, + ))) + } + + #[cfg(test)] + pub(crate) fn test_reset_observation_wakeup_cohort_commits() { + OBSERVATION_WAKEUP_COHORT_COMMITS.with(|count| count.set(0)); + } + + #[cfg(test)] + pub(crate) fn test_observation_wakeup_cohort_commits() -> u32 { + OBSERVATION_WAKEUP_COHORT_COMMITS.with(core::cell::Cell::get) + } + + pub(crate) fn commit_observation_queue_cohort( + candidates: Vec>, + ) -> Result<(), EnqueueOutcome> { + if candidates.is_empty() || candidates.len() > T::ObservationPageSize::get() as usize { + return Err(EnqueueOutcome::CapacityUnavailable); + } + with_transaction_opaque_err(|| { + let mut admitted = Vec::new(); + let mut occurrences = Vec::new(); + for candidate in candidates.into_iter(/* deos-bypass: bounded-iter */) { + let actor_type = candidate.state.identity.actor_class.actor_type(); + let breakdown = Self::trigger_fee_for_weight( + actor_type, + TriggerFamily::ObservationChange, + T::WeightInfo::observation_change_trigger_occurrence(), + ); + let charged = match Self::try_charge_automatic_trigger_occurrence( + actor_type, + &candidate.state.identity.sovereign_account, + breakdown, + ) { + Ok(charged) => charged, + Err(_) => { + return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err( + EnqueueOutcome::CorruptedTopology, + )); + } + }; + if charged { + occurrences.push(( + candidate.state.actor_id, + breakdown, + candidate.state.hot.pending_signal, + )); + admitted.push(candidate); + } + } + if admitted.is_empty() { + return polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(())); + } + let topology = match Self::queue_topology_preflight(QueueMutation::Enqueue) { + Ok(topology) => topology, + Err(error) => { + return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)); + } + }; + let mut plan = QueueAppendPlan { + actors: Vec::new(), + pages: Vec::new(), + next_ticket: NextQueueTicket::::get(), + next_tail: topology.tail, + next_occupancy: topology.occupancy, + }; + for candidate in admitted.into_iter(/* deos-bypass: bounded-iter */) { + if let Err(error) = Self::reserve_following_paged_enqueue_observation( + &mut plan, + &candidate.state, + candidate.hot, + ) { + return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)); + } + } + Self::commit_paged_enqueue(plan); + for (actor_id, breakdown, _) in occurrences { + IndexedTriggerDetectionDisabled::::insert(actor_id, ()); + Self::deposit_event(Event::TriggerOccurrenceProcessed { + actor_id, + trigger_family: breakdown.trigger_family, + fee: breakdown.trigger_fee, + }); + } + polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(())) + }) + .map_err(|_| EnqueueOutcome::CorruptedTopology)? } - pub(crate) fn commit_paged_enqueue(plan: QueueAppendPlan) { - #[cfg(test)] - QUEUE_APPEND_COMMITS.with(|count| count.set(count.get().saturating_add(1))); - for (page_id, page) in plan.pages { - QueuePages::::insert(page_id, page); + pub(crate) fn commit_observation_wakeup_cohort( + candidates: Vec>, + ) -> Result<(), EnqueueOutcome> { + if candidates.is_empty() || candidates.len() > T::ObservationPageSize::get() as usize { + return Err(EnqueueOutcome::CapacityUnavailable); } - QueueTail::::put(plan.next_tail); - QueueOccupancy::::put(plan.next_occupancy); - NextQueueTicket::::put(plan.next_ticket); - for (actor_id, hot) in plan.actors { - ActorHot::::insert(actor_id, hot); + let wakeup_key = candidates[0].wakeup_key; + if candidates + .iter(/* deos-bypass: bounded-iter */) + .any(|candidate| candidate.wakeup_key != wakeup_key) + { + return Err(EnqueueOutcome::CorruptedTopology); } - } - - pub fn try_paged_enqueue(actor_id: ActorId) -> Result<(), EnqueueOutcome> { - with_transaction_opaque_err(|| match Self::preflight_paged_enqueue(actor_id) { - Ok(plan) => { - Self::commit_paged_enqueue(plan); - polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(())) + let result = with_transaction_opaque_err(|| { + for candidate in candidates { + let breakdown = Self::trigger_fee_for_weight( + candidate.actor_type, + TriggerFamily::ObservationChange, + T::WeightInfo::observation_change_trigger_occurrence(), + ); + let charged = match Self::try_charge_automatic_trigger_occurrence( + candidate.actor_type, + &candidate.sovereign_account, + breakdown, + ) { + Ok(charged) => charged, + Err(_) => { + return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err( + EnqueueOutcome::CorruptedTopology, + )); + } + }; + if !charged { + continue; + } + match Self::try_wakeup_substrate_schedule_transition_loaded( + candidate.actor_id, + candidate.wakeup_key, + candidate.hot.clone(), + ) { + Ok(()) => {} + Err(EnqueueOutcome::AlreadyLive) => { + ActorHot::::insert(candidate.actor_id, candidate.hot); + } + Err(error) => { + return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)); + } + } + IndexedTriggerDetectionDisabled::::insert(candidate.actor_id, ()); + Self::deposit_event(Event::TriggerOccurrenceProcessed { + actor_id: candidate.actor_id, + trigger_family: breakdown.trigger_family, + fee: breakdown.trigger_fee, + }); } - Err(error) => polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)), + polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(())) }) - .map_err(|_| EnqueueOutcome::CorruptedTopology)? + .map_err(|_| EnqueueOutcome::CorruptedTopology)?; + #[cfg(test)] + if result.is_ok() { + OBSERVATION_WAKEUP_COHORT_COMMITS.with(|count| count.set(count.get().saturating_add(1))); + } + result } - fn scheduler_index_is_exhausted(outcome: EnqueueOutcome) -> bool { - matches!( - outcome, - EnqueueOutcome::TicketExhausted - | EnqueueOutcome::SchedulerIndexExhausted - | EnqueueOutcome::WakeupIndexExhausted - ) - } + fn request_observation_activation_compact_inner( + actor_id: ActorId, + feed: T::ObservationFeedId, + execute_terminal: bool, + _cause_provenance: TriggerCauseProvenance, + _cause_block: u64, + ) -> Result { + let Some(state) = Self::load_observation_activation_state(actor_id, feed) else { + return if ActorHot::::contains_key(actor_id) { + Err(ActivationFailure::Permanent( + Error::::ActorInvariant.into(), + )) + } else { + Ok(ObservationActivationOutcome::Ordinary( + ActivationOutcome::IgnoredStale, + )) + }; + }; + let classification = + Self::classify_observation_activation_compact(&state).map_err(|error| { + ActivationFailure::Permanent(Self::classification_dispatch_error(error).into()) + })?; + if classification.terminal_reason.is_some() { + return if execute_terminal { + Self::request_activation_inner(actor_id).map(ObservationActivationOutcome::Ordinary) + } else { + Ok(ObservationActivationOutcome::TerminalDeferred) + }; + } - fn close_for_scheduler_index_exhaustion(actor_id: ActorId) -> DispatchResult { - let instance = Self::active_actor_view(actor_id).ok_or(Error::::ActorInvariant)?; - Self::finalize_actor(actor_id, &instance, CloseReason::SchedulerIndexExhausted) - } + let already_pending = state.hot.pending_signal; + let mut hot = state.hot.clone(); + hot.pending_signal = true; + if hot.queue_ticket.is_some() { + if !already_pending { + ActorHot::::insert(actor_id, hot); + } + return Ok(ObservationActivationOutcome::Ordinary(if already_pending { + ActivationOutcome::Coalesced + } else { + ActivationOutcome::Latched + })); + } - pub(crate) fn request_activation( - actor_id: ActorId, - ) -> Result { - if polkadot_sdk::frame_support::storage::transactional::is_transactional() { - return Self::request_activation_inner(actor_id); + enum CompactPlacement { + None, + Queue, + Wakeup(BlockNumber), + } + let placement = if hot.lifecycle.is_paused() { + state + .authority + .window + .map(|window| CompactPlacement::Wakeup(Self::window_terminal_at(&window))) + .unwrap_or(CompactPlacement::None) + } else { + let now = frame_system::Pallet::::block_number(); + let eligible_at = if hot.cycle_state == CycleState::Suspended { + state + .run_head + .as_ref() + .ok_or(ActivationFailure::Permanent( + Error::::ActorRunInvariant.into(), + ))? + .eligible_at + } else { + let cooldown_anchor = hot.last_cycle_block.unwrap_or(hot.schedule_anchor); + let cooldown_eligible_at = + if state.identity.cycle_nonce == 0 && hot.last_cycle_block.is_none() { + hot.schedule_anchor + } else { + cooldown_anchor + .checked_add(&state.authority.cooldown_blocks.into()) + .ok_or(ActivationFailure::Permanent( + Error::::SchedulerIndexExhausted.into(), + ))? + }; + let window_floor = state + .authority + .window + .map(|window| window.start) + .unwrap_or_else(Zero::zero); + now.max(cooldown_eligible_at).max(window_floor) + }; + let wakeup_at = state.authority.window.map_or(eligible_at, |window| { + eligible_at.min(Self::window_terminal_at(&window)) + }); + let exact_next_block = now + .checked_add(&One::one()) + .ok_or(ActivationFailure::Permanent( + Error::::SchedulerIndexExhausted.into(), + ))?; + if wakeup_at < exact_next_block { + CompactPlacement::Queue + } else { + CompactPlacement::Wakeup(wakeup_at) + } + }; + + let placement_result = match placement { + CompactPlacement::None => { + ActorHot::::insert(actor_id, hot); + Ok(()) + } + CompactPlacement::Queue => { + match Self::preflight_paged_enqueue_observation_loaded(&state, hot) { + Ok(plan) => { + Self::commit_paged_enqueue(plan); + Ok(()) + } + Err(_) => { + return Self::request_activation_inner(actor_id) + .map(ObservationActivationOutcome::Ordinary); + } + } + } + CompactPlacement::Wakeup(block) => { + ActorHot::::insert(actor_id, hot.clone()); + Self::try_wakeup_substrate_schedule_transition_loaded( + actor_id, + WakeupKey::Block(block), + hot, + ) + } + }; + match placement_result { + Ok(()) | Err(EnqueueOutcome::AlreadyLive) => { + Ok(ObservationActivationOutcome::Ordinary(if already_pending { + ActivationOutcome::Coalesced + } else { + ActivationOutcome::Latched + })) + } + Err(EnqueueOutcome::CapacityUnavailable | EnqueueOutcome::WakeupCapacityExhausted) => Err( + ActivationFailure::Temporary(Error::::QueueCapacityUnavailable.into()), + ), + Err(_) => Err(ActivationFailure::Permanent( + Error::::SchedulerIndexExhausted.into(), + )), } - polkadot_sdk::frame_support::storage::with_transaction(|| match Self::request_activation_inner( - actor_id, - ) { - Ok(outcome) => polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(outcome)), - Err(error) => polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)), - }) } pub(crate) fn preflight_activation_loaded( @@ -889,24 +1997,32 @@ impl Pallet { state: ActiveActorStateOf, ) -> Result, ActivationFailure> { let already_pending = state.hot.pending_signal; - let continuation = state.continuation; + let run_state = state.run_state; let mut hot = state.hot; hot.pending_signal = true; let instance = Self::derive_active_actor_view(state.identity, hot.clone(), state.contract); let classification = - Self::classify_actor_loaded(&instance, continuation.as_ref()).map_err(|error| { + Self::classify_actor_loaded(&instance, run_state.as_ref()).map_err(|error| { ActivationFailure::Permanent(Self::classification_dispatch_error(error).into()) })?; - let action = if classification.terminal_reason == Some(CloseReason::WindowExpired) { - ActivationAction::CloseWindowExpired + let action = if matches!( + classification.terminal_reason, + Some(CloseReason::WindowExpired | CloseReason::CycleNonceExhausted) + ) { + ActivationAction::Close(classification.terminal_reason.ok_or( + ActivationFailure::Permanent(Error::::ActorInvariant.into()), + )?) } else if instance.queue_ticket.is_some() { ActivationAction::CoalesceLive - } else if matches!(instance.trigger, Trigger::Cadenced { .. }) { - ActivationAction::EnqueueCadenced(Self::preflight_paged_enqueue_loaded(actor_id, hot.clone())) + } else if matches!( + instance.trigger, + Trigger::AtTime { .. } | Trigger::Cadenced { .. } + ) { + ActivationAction::EnqueueTemporal(Self::preflight_paged_enqueue_loaded(actor_id, hot.clone())) } else { ActivationAction::PrimeSchedule(Self::preflight_prime_schedule_loaded( &instance, - continuation.as_ref(), + run_state.as_ref(), )) }; Ok(ActivationPlan { @@ -928,9 +2044,9 @@ impl Pallet { }; let plan = Self::preflight_activation_loaded(actor_id, state)?; Ok(match plan.action { - ActivationAction::CloseWindowExpired => 0, + ActivationAction::Close(_) => 0, ActivationAction::CoalesceLive => 1, - ActivationAction::EnqueueCadenced(_) => 2, + ActivationAction::EnqueueTemporal(_) => 2, ActivationAction::PrimeSchedule(Ok(PrimeSchedulePlan::None)) => 3, ActivationAction::PrimeSchedule(Ok(PrimeSchedulePlan::Enqueue)) => 4, ActivationAction::PrimeSchedule(Ok(PrimeSchedulePlan::BlockWakeup(_))) => 5, @@ -953,9 +2069,8 @@ impl Pallet { ActorHot::::insert(actor_id, prospective_hot); } match action { - ActivationAction::CloseWindowExpired => { - Self::finalize_actor(actor_id, &instance, CloseReason::WindowExpired) - .map_err(ActivationFailure::Permanent)?; + ActivationAction::Close(reason) => { + Self::finalize_actor(actor_id, &instance, reason).map_err(ActivationFailure::Permanent)?; return Ok(ActivationOutcome::Closed); } ActivationAction::CoalesceLive => { @@ -965,25 +2080,25 @@ impl Pallet { ActivationOutcome::Latched }); } - ActivationAction::EnqueueCadenced(_) | ActivationAction::PrimeSchedule(_) => {} + ActivationAction::EnqueueTemporal(_) | ActivationAction::PrimeSchedule(_) => {} } let placement = match action { - ActivationAction::EnqueueCadenced(Ok(queue_plan)) => { + ActivationAction::EnqueueTemporal(Ok(queue_plan)) => { Self::commit_paged_enqueue(queue_plan); Ok(()) } - ActivationAction::EnqueueCadenced(Err(EnqueueOutcome::CapacityUnavailable)) => { + ActivationAction::EnqueueTemporal(Err(EnqueueOutcome::CapacityUnavailable)) => { Self::enqueue(actor_id) } - ActivationAction::EnqueueCadenced(Err(error)) => Err(error), + ActivationAction::EnqueueTemporal(Err(error)) => Err(error), ActivationAction::PrimeSchedule(Ok(PrimeSchedulePlan::None)) => Ok(()), ActivationAction::PrimeSchedule(Ok(PrimeSchedulePlan::Enqueue)) => Self::enqueue(actor_id), ActivationAction::PrimeSchedule(Ok(PrimeSchedulePlan::BlockWakeup(block))) => { Self::defer_wakeup(actor_id, block) } ActivationAction::PrimeSchedule(Err(error)) => Err(error), - ActivationAction::CloseWindowExpired | ActivationAction::CoalesceLive => { + ActivationAction::Close(_) | ActivationAction::CoalesceLive => { return Err(ActivationFailure::Permanent( Error::::ActorInvariant.into(), )); @@ -1081,18 +2196,19 @@ impl Pallet { pub(crate) fn try_paged_invalidate( actor_id: ActorId, ) -> Result, EnqueueOutcome> { - match Self::load_actor_state(actor_id) { - LoadedActorStateOf::Active(mut state) => { - let ticket = state.hot.queue_ticket.take(); - ActorHot::::insert(actor_id, state.hot); - Ok(ticket) - } - LoadedActorStateOf::NotRegistered | LoadedActorStateOf::Dormant(_) => Ok(None), - LoadedActorStateOf::Corrupt => Err(EnqueueOutcome::CorruptedTopology), + if let Some((mut state, _, _)) = Self::load_actor_service_state(actor_id) { + let ticket = state.hot.queue_ticket.take(); + ActorHot::::insert(actor_id, state.hot); + return Ok(ticket); + } + if ActorHot::::contains_key(actor_id) { + Err(EnqueueOutcome::CorruptedTopology) + } else { + Ok(None) } } - pub fn paged_head_entry() -> Option<(QueueTicket, QueueEntry)> { + pub fn paged_head_entry() -> Option<(QueueTicket, QueueEntry>)> { let head = QueueHead::::get(); if head >= QueueTail::::get() { return None; @@ -1147,15 +2263,21 @@ impl Pallet { } Some(hot) } else { - match Self::load_actor_state(entry.actor_id) { - LoadedActorStateOf::Active(state) if !owner_was_closed => { - if state.hot.queue_ticket != Some(entry.ticket) { - return Err(EnqueueOutcome::CorruptedTopology); - } - Some(state.hot) + if owner_was_closed { + if ActorIdentities::::contains_key(entry.actor_id) + || ActorHot::::contains_key(entry.actor_id) + { + return Err(EnqueueOutcome::CorruptedTopology); + } + None + } else { + let Some((state, _, _)) = Self::load_actor_service_state(entry.actor_id) else { + return Err(EnqueueOutcome::CorruptedTopology); + }; + if state.hot.queue_ticket != Some(entry.ticket) { + return Err(EnqueueOutcome::CorruptedTopology); } - LoadedActorStateOf::NotRegistered if owner_was_closed => None, - _ => return Err(EnqueueOutcome::CorruptedTopology), + Some(state.hot) } }; let next_head = topology @@ -1266,14 +2388,11 @@ impl Pallet { return corrupt(); }; stats.entries_scanned = entries_scanned; - match Self::load_actor_state(entry.actor_id) { - LoadedActorStateOf::Active(state) if state.hot.queue_ticket == Some(entry.ticket) => { - break 'pages; - } - LoadedActorStateOf::Active(_) - | LoadedActorStateOf::NotRegistered - | LoadedActorStateOf::Dormant(_) => {} - LoadedActorStateOf::Corrupt => return corrupt(), + match Self::load_actor_service_state(entry.actor_id) { + Some((state, _, _)) if state.hot.queue_ticket == Some(entry.ticket) => break 'pages, + Some(_) => {} + None if !ActorHot::::contains_key(entry.actor_id) => {} + None => return corrupt(), } let Some(tombstones_skipped) = stats.tombstones_skipped.checked_add(1) else { return corrupt(); @@ -1343,13 +2462,35 @@ impl Pallet { .is_some_and(|entry| entry.actor_id == actor_id) } - pub(crate) fn wakeup_substrate_invalidate_inner( + fn wakeup_pointer_for_clock( + hot: &ActorHotStateOf, + clock: WakeupClock, + ) -> Option>> { + match clock { + WakeupClock::Block => hot.wakeup_pointer, + WakeupClock::Tick => hot.trigger_wakeup_pointer.map(|pointer| WakeupPointer { + block: WakeupKey::Tick(pointer.tick), + page_id: pointer.page_id, + slot: pointer.slot, + }), + } + } + + fn clear_wakeup_pointer_for_clock(hot: &mut ActorHotStateOf, clock: WakeupClock) { + match clock { + WakeupClock::Block => hot.wakeup_pointer = None, + WakeupClock::Tick => hot.trigger_wakeup_pointer = None, + } + } + + fn wakeup_substrate_invalidate_clock_inner( actor_id: ActorId, + clock: WakeupClock, ) -> Result>>, EnqueueOutcome> { - let LoadedActorStateOf::Active(mut state) = Self::load_actor_state(actor_id) else { + let Some((mut state, _, _)) = Self::load_actor_service_state(actor_id) else { return Err(EnqueueOutcome::CorruptedTopology); }; - let Some(pointer) = state.hot.wakeup_pointer else { + let Some(pointer) = Self::wakeup_pointer_for_clock(&state.hot, clock) else { return Ok(None); }; let key = (pointer.block, pointer.page_id); @@ -1415,7 +2556,7 @@ impl Pallet { page.entries[pointer.slot as usize] = None; page.live_entries = next_page_live; bucket.live_entries = next_bucket_live; - state.hot.wakeup_pointer = None; + Self::clear_wakeup_pointer_for_clock(&mut state.hot, clock); ActorHot::::insert(actor_id, state.hot); if page.live_entries > 0 { WakeupPages::::insert(key, page); @@ -1450,6 +2591,18 @@ impl Pallet { Ok(Some(pointer)) } + pub(crate) fn wakeup_substrate_invalidate_inner( + actor_id: ActorId, + ) -> Result>>, EnqueueOutcome> { + Self::wakeup_substrate_invalidate_clock_inner(actor_id, WakeupClock::Block) + } + + pub(crate) fn trigger_wakeup_substrate_invalidate_inner( + actor_id: ActorId, + ) -> Result>>, EnqueueOutcome> { + Self::wakeup_substrate_invalidate_clock_inner(actor_id, WakeupClock::Tick) + } + pub fn wakeup_substrate_invalidate( actor_id: ActorId, ) -> Option>> { @@ -1504,14 +2657,28 @@ impl Pallet { actor_id: ActorId, wakeup_block: WakeupKey>, ) -> Result<(), EnqueueOutcome> { - let LoadedActorStateOf::Active(state) = Self::load_actor_state(actor_id) else { + let Some((state, _, _)) = Self::load_actor_service_state(actor_id) else { return Err(EnqueueOutcome::CorruptedTopology); }; - if let Some(pointer) = state.hot.wakeup_pointer { + Self::try_wakeup_substrate_schedule_transition_loaded(actor_id, wakeup_block, state.hot) + } + + fn try_wakeup_substrate_schedule_transition_loaded( + actor_id: ActorId, + wakeup_block: WakeupKey>, + mut hot: ActorHotStateOf, + ) -> Result<(), EnqueueOutcome> { + #[cfg(test)] + if FAIL_WAKEUP_PLACEMENT_WITH_CAPACITY.with(|flag| flag.replace(false)) { + return Err(EnqueueOutcome::WakeupCapacityExhausted); + } + let clock = wakeup_block.clock(); + if let Some(pointer) = Self::wakeup_pointer_for_clock(&hot, clock) { if pointer.block == wakeup_block && Self::wakeup_page_entry_matches(pointer, actor_id) { return Err(EnqueueOutcome::AlreadyLive); } - Self::wakeup_substrate_invalidate_inner(actor_id)?; + Self::wakeup_substrate_invalidate_clock_inner(actor_id, clock)?; + Self::clear_wakeup_pointer_for_clock(&mut hot, clock); } let (page_id, slot) = if let Some(mut bucket) = WakeupBuckets::::get(wakeup_block) { @@ -1578,7 +2745,7 @@ impl Pallet { bucket.next_page_id = next_page_id; bucket.live_entries = next_bucket_live; WakeupBuckets::::insert(wakeup_block, bucket); - Self::set_wakeup_pointer(actor_id, wakeup_block, page_id, 0)?; + Self::set_wakeup_pointer_loaded(actor_id, hot, wakeup_block, page_id, 0); return Ok(()); }; tail_page.live_entries = tail_page @@ -1620,30 +2787,34 @@ impl Pallet { } (0, 0) }; - Self::set_wakeup_pointer(actor_id, wakeup_block, page_id, slot)?; + Self::set_wakeup_pointer_loaded(actor_id, hot, wakeup_block, page_id, slot); Ok(()) } - fn set_wakeup_pointer( + fn set_wakeup_pointer_loaded( actor_id: ActorId, + mut hot: ActorHotStateOf, block: WakeupKey>, page_id: WakeupPageId, slot: WakeupSlot, - ) -> Result<(), EnqueueOutcome> { - let pointer = WakeupPointer { - block, - page_id, - slot, - }; - let LoadedActorStateOf::Active(mut state) = Self::load_actor_state(actor_id) else { - return Err(EnqueueOutcome::CorruptedTopology); - }; - if state.hot.wakeup_pointer.is_some() { - return Err(EnqueueOutcome::CorruptedTopology); + ) { + match block { + WakeupKey::Block(_) => { + hot.wakeup_pointer = Some(WakeupPointer { + block, + page_id, + slot, + }); + } + WakeupKey::Tick(tick) => { + hot.trigger_wakeup_pointer = Some(TriggerWakeupPointer { + tick, + page_id, + slot, + }); + } } - state.hot.wakeup_pointer = Some(pointer); - ActorHot::::insert(actor_id, state.hot); - Ok(()) + ActorHot::::insert(actor_id, hot); } pub fn wakeup_substrate_schedule(actor_id: ActorId, wakeup_block: BlockNumberFor) -> bool { @@ -1712,19 +2883,22 @@ impl Pallet { page_id, slot: pointer_slot as WakeupSlot, }; - let mut actor_hot = match Self::load_actor_state(entry.actor_id) { - LoadedActorStateOf::Active(state) if state.hot.wakeup_pointer == Some(pointer) => { + let clock = wakeup_block.clock(); + let mut actor_hot = match Self::load_actor_service_state(entry.actor_id) { + Some((state, _, _)) + if Self::wakeup_pointer_for_clock(&state.hot, clock) == Some(pointer) => + { state.hot } - LoadedActorStateOf::Active(state) if state.hot.wakeup_pointer.is_none() => { + Some((state, _, _)) if Self::wakeup_pointer_for_clock(&state.hot, clock).is_none() => { stats.stale_entries = stats.stale_entries.saturating_add(1); continue; } - LoadedActorStateOf::NotRegistered | LoadedActorStateOf::Dormant(_) => { + None if !ActorHot::::contains_key(entry.actor_id) => { stats.stale_entries = stats.stale_entries.saturating_add(1); continue; } - LoadedActorStateOf::Active(_) | LoadedActorStateOf::Corrupt => return None, + Some(_) | None => return None, }; if ready.try_push(entry.actor_id).is_err() { page.entries[pointer_slot] = Some(entry); @@ -1736,7 +2910,7 @@ impl Pallet { WakeupBuckets::::insert(wakeup_block, bucket); return Some((ready, stats)); } - actor_hot.wakeup_pointer = None; + Self::clear_wakeup_pointer_for_clock(&mut actor_hot, clock); ActorHot::::insert(entry.actor_id, actor_hot); stats.ready_entries = stats.ready_entries.saturating_add(1); } @@ -2090,14 +3264,25 @@ impl Pallet { fn loaded_active_view_for_placement( actor_id: ActorId, ) -> Result>, EnqueueOutcome> { - match Self::load_actor_state(actor_id) { - LoadedActorStateOf::NotRegistered | LoadedActorStateOf::Dormant(_) => Ok(None), - LoadedActorStateOf::Active(state) => Ok(Some(Self::derive_active_actor_view( + if let Some((state, _, _)) = Self::load_actor_service_state(actor_id) { + return Ok(Some(Self::derive_active_actor_view( state.identity, state.hot, state.contract, - ))), - LoadedActorStateOf::Corrupt => Err(EnqueueOutcome::CorruptedTopology), + ))); + } + match ( + ActorIdentities::::contains_key(actor_id), + ActorHot::::contains_key(actor_id), + ActorContractHeads::::contains_key(actor_id), + ActorAdmissionCertificates::::contains_key(actor_id), + ActorFunding::::contains_key(actor_id), + ActorRunStateStore::::contains_key(actor_id), + ) { + (false, false, false, false, false, false) | (true, false, false, false, false, false) => { + Ok(None) + } + _ => Err(EnqueueOutcome::CorruptedTopology), } } @@ -2110,7 +3295,7 @@ impl Pallet { fn preflight_prime_schedule_loaded( instance: &ActiveActorViewOf, - continuation: Option<&ContinuationStateOf>, + run_state: Option<&ActorRunStateOf>, ) -> Result>, EnqueueOutcome> { if instance.lifecycle.is_paused() { return Ok( @@ -2122,7 +3307,7 @@ impl Pallet { let eligible_at = if instance.cycle_state == CycleState::Suspended { Self::retry_eligible_at_loaded( instance, - continuation.ok_or(EnqueueOutcome::CorruptedTopology)?, + run_state.ok_or(EnqueueOutcome::CorruptedTopology)?, )? } else if instance.pending_signal { Self::next_eligible_at(instance, now)? @@ -2153,12 +3338,37 @@ impl Pallet { if instance.lifecycle.is_paused() { return Self::schedule_window_expiry(actor_id, &instance); } - if matches!(instance.trigger, Trigger::Cadenced { .. }) - && instance.cadence_anchor_tick.is_none() - { - return Self::defer_tick_wakeup(actor_id, 0); + if instance.trigger_wakeup_pointer.is_none() { + let due_tick = match instance.trigger { + Trigger::AtTime { after_ticks } if !instance.temporal_occurrence_consumed => { + let due_tick = instance.temporal_anchor_tick.map_or(Ok(0), |anchor_tick| { + anchor_tick + .checked_add(after_ticks) + .ok_or(EnqueueOutcome::SchedulerIndexExhausted) + })?; + Some(due_tick) + } + Trigger::Cadenced { every_ticks } => Some( + instance + .temporal_anchor_tick + .map(|anchor_tick| { + next_cadence_due_tick(anchor_tick, every_ticks, Self::current_scheduler_tick()?) + .ok_or(EnqueueOutcome::SchedulerIndexExhausted) + }) + .transpose()? + .unwrap_or(0), + ), + Trigger::Manual + | Trigger::AddressEvent { .. } + | Trigger::ObservationChange { .. } + | Trigger::ObservationCrossing { .. } + | Trigger::AtTime { .. } => None, + }; + if let Some(due_tick) = due_tick { + Self::defer_tick_wakeup(actor_id, due_tick)?; + } } - Self::schedule_next_work(actor_id, &instance, now, false) + Self::schedule_next_work(actor_id, &instance, now, false).map(|_| ()) } fn window_expiry_wakeup(instance: &ActiveActorViewOf) -> Option> { @@ -2172,7 +3382,7 @@ impl Pallet { instance: &ActiveActorViewOf, ) -> Result<(), EnqueueOutcome> { if let Some(expiry) = Self::window_expiry_wakeup(instance) { - Self::defer_wakeup(actor_id, expiry) + Self::defer_wakeup_loaded(actor_id, expiry, instance) } else { Ok(()) } @@ -2200,18 +3410,49 @@ impl Pallet { fn defer_wakeup( actor_id: ActorId, wakeup_block: BlockNumberFor, + ) -> Result<(), EnqueueOutcome> { + let instance = + Self::loaded_active_view_for_placement(actor_id)?.ok_or(EnqueueOutcome::CorruptedTopology)?; + Self::defer_wakeup_loaded(actor_id, wakeup_block, &instance) + } + + fn defer_wakeup_loaded( + actor_id: ActorId, + wakeup_block: BlockNumberFor, + instance: &ActiveActorViewOf, + ) -> Result<(), EnqueueOutcome> { + let hot = ActorHot::::get(actor_id).ok_or(EnqueueOutcome::CorruptedTopology)?; + Self::defer_wakeup_with_hot(actor_id, wakeup_block, instance, hot) + } + + fn defer_wakeup_with_hot( + actor_id: ActorId, + wakeup_block: BlockNumberFor, + instance: &ActiveActorViewOf, + hot: ActorHotStateOf, ) -> Result<(), EnqueueOutcome> { #[cfg(test)] if FAIL_WAKEUP_PLACEMENT_WITH_CAPACITY.with(|flag| flag.replace(false)) { return Err(EnqueueOutcome::WakeupCapacityExhausted); } - let target = Self::loaded_active_view_for_placement(actor_id)? - .and_then(|instance| Self::window_expiry_wakeup(&instance)) + let target = Self::window_expiry_wakeup(instance) .map(|expiry| wakeup_block.min(expiry)) .unwrap_or(wakeup_block); - match Self::try_wakeup_substrate_schedule_key_inner(actor_id, WakeupKey::Block(target)) { - Ok(()) => Ok(()), - Err(EnqueueOutcome::AlreadyLive) => Ok(()), + match with_transaction_opaque_err( + || match Self::try_wakeup_substrate_schedule_transition_loaded( + actor_id, + WakeupKey::Block(target), + hot, + ) { + Ok(()) => polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(())), + Err(error) => { + polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)) + } + }, + ) + .map_err(|_| EnqueueOutcome::CorruptedTopology)? + { + Ok(()) | Err(EnqueueOutcome::AlreadyLive) => Ok(()), Err(other) => Err(other), } } @@ -2262,17 +3503,249 @@ impl Pallet { Self::defer_tick_wakeup(actor_id, wakeup_tick) } - fn wakeup_cursor_drain_branch_weight(removes_bucket: bool) -> Weight { - if removes_bucket { + fn wakeup_cursor_drain_branch_weight(removes_bucket: bool, clock: WakeupClock) -> Weight { + let physical = if removes_bucket { T::WeightInfo::scheduler_wakeup_cursor_worker_remove() } else { T::WeightInfo::scheduler_wakeup_cursor_worker_partial() + }; + match clock { + WakeupClock::Block => physical, + WakeupClock::Tick => { + let at_time = T::WeightInfo::at_time_trigger_occurrence(); + let cadenced = T::WeightInfo::cadenced_trigger_occurrence(); + physical.saturating_add(Weight::from_parts( + at_time.ref_time().max(cadenced.ref_time()), + at_time.proof_size().max(cadenced.proof_size()), + )) + } + } + } + + fn wakeup_cursor_drain_unit_weight_for(removes_bucket: bool, clock: WakeupClock) -> Weight { + Self::wakeup_cursor_drain_branch_weight(removes_bucket, clock) + .saturating_add(Self::close_cleanup_weight_upper()) + .saturating_add(T::WeightInfo::record_wakeup_worker_fault()) + } + + pub fn wakeup_cursor_drain_unit_weight_upper(removes_bucket: bool) -> Weight { + Self::wakeup_cursor_drain_unit_weight_for(removes_bucket, WakeupClock::Tick) + } + + #[cfg(test)] + pub(crate) fn block_wakeup_cursor_drain_unit_weight_upper(removes_bucket: bool) -> Weight { + Self::wakeup_cursor_drain_unit_weight_for(removes_bucket, WakeupClock::Block) + } + + pub(crate) fn rearm_trigger_after_opening(actor_id: ActorId) -> DispatchResult { + let hot = ActorHot::::get(actor_id).ok_or(Error::::ActorInvariant)?; + let contract = Self::load_actor_contract(actor_id).ok_or(Error::::ActorInvariant)?; + match &contract.trigger { + Trigger::ObservationChange { .. } => { + IndexedTriggerDetectionDisabled::::remove(actor_id); + Ok(()) + } + Trigger::ObservationCrossing { .. } => { + Self::rearm_disabled_crossing_after_opening(actor_id, &contract.trigger) + } + Trigger::Cadenced { every_ticks } => { + if hot.trigger_wakeup_pointer.is_some() { + Self::trigger_wakeup_substrate_invalidate_inner(actor_id) + .map_err(|_| Error::::ActorInvariant)?; + } + let anchor_tick = hot + .trigger_runtime_state + .temporal_anchor_tick() + .ok_or(Error::::ActorInvariant)?; + let now_tick = Self::current_scheduler_tick().map_err(Self::placement_error)?; + let due_tick = next_cadence_due_tick(anchor_tick, *every_ticks, now_tick) + .ok_or(Error::::AdmissionBoundOverflow)?; + Self::defer_tick_wakeup(actor_id, due_tick).map_err(Self::placement_error) + } + _ => Ok(()), + } + } + + pub(crate) fn process_due_temporal_occurrence( + actor_id: ActorId, + now_tick: SchedulerTick, + ) -> Result { + let Some((mut state, _, _)) = Self::load_actor_service_state(actor_id) else { + return Err(DispatchError::Other("tick wakeup owner is corrupt")); + }; + if state.hot.pending_signal { + return Ok(false); + } + let (delay_ticks, trigger_family, occurrence_weight) = match state.contract.trigger { + Trigger::AtTime { after_ticks } => ( + after_ticks, + TriggerFamily::AtTime, + T::WeightInfo::at_time_trigger_occurrence(), + ), + Trigger::Cadenced { every_ticks } => ( + every_ticks, + TriggerFamily::Cadenced, + T::WeightInfo::cadenced_trigger_occurrence(), + ), + Trigger::Manual + | Trigger::AddressEvent { .. } + | Trigger::ObservationChange { .. } + | Trigger::ObservationCrossing { .. } => { + return Err(DispatchError::Other("tick wakeup owner is not temporal")); + } + }; + if state + .hot + .trigger_runtime_state + .temporal_anchor_tick() + .is_none() + { + let Some(anchor_tick) = Self::temporal_anchor_tick(&state.contract.trigger) + .map_err(|_| DispatchError::Other("genesis temporal anchor failed"))? + else { + return Err(DispatchError::Other("genesis temporal anchor failed")); + }; + let due_tick = anchor_tick + .checked_add(delay_ticks) + .ok_or(DispatchError::Other("genesis temporal deadline failed"))?; + state.hot.trigger_runtime_state = match state.contract.trigger { + Trigger::AtTime { .. } => TriggerRuntimeState::AtTime { + anchor_tick: Some(anchor_tick), + consumed: false, + }, + Trigger::Cadenced { .. } => TriggerRuntimeState::Cadenced { + anchor_tick: Some(anchor_tick), + }, + _ => return Err(DispatchError::Other("tick wakeup owner changed trigger")), + }; + ActorHot::::insert(actor_id, state.hot); + if let Err(error) = Self::defer_tick_wakeup(actor_id, due_tick) { + if !Self::scheduler_index_is_exhausted(error) + || Self::close_for_scheduler_index_exhaustion(actor_id).is_err() + { + return Err(DispatchError::Other("genesis temporal placement failed")); + } + return Ok(true); + } + return Ok(false); + } + let anchor_tick = state + .hot + .trigger_runtime_state + .temporal_anchor_tick() + .ok_or(DispatchError::Other( + "initialized temporal anchor is missing", + ))?; + match state.hot.trigger_runtime_state { + TriggerRuntimeState::AtTime { + anchor_tick, + consumed: false, + } => { + state.hot.trigger_runtime_state = TriggerRuntimeState::AtTime { + anchor_tick, + consumed: true, + }; + ActorHot::::insert(actor_id, state.hot); + } + TriggerRuntimeState::Cadenced { .. } => { + let next_due_tick = next_cadence_due_tick(anchor_tick, delay_ticks, now_tick) + .ok_or(DispatchError::Other("cadence deadline failed"))?; + if let Err(error) = Self::defer_tick_wakeup(actor_id, next_due_tick) { + if !Self::scheduler_index_is_exhausted(error) + || Self::close_for_scheduler_index_exhaustion(actor_id).is_err() + { + return Err(DispatchError::Other("cadence rearm failed")); + } + return Ok(true); + } + } + TriggerRuntimeState::AtTime { consumed: true, .. } + | TriggerRuntimeState::Stateless + | TriggerRuntimeState::ObservationCrossing { .. } => { + return Err(DispatchError::Other( + "temporal runtime state is incompatible", + )); + } + } + Self::reconcile_actor_state_hold(actor_id) + .map_err(|_| DispatchError::Other("temporal state hold reconciliation failed"))?; + let Some((state, _, _)) = Self::load_actor_service_state(actor_id) else { + return Err(DispatchError::Other( + "temporal progression state is corrupt", + )); + }; + let activation = Self::preflight_activation_loaded(actor_id, state.clone()) + .map_err(|_| DispatchError::Other("temporal activation preflight failed"))?; + let closes_without_occurrence = activation.terminal_reason.is_some() + || matches!( + activation.action, + ActivationAction::Close(_) + | ActivationAction::EnqueueTemporal(Err( + EnqueueOutcome::TicketExhausted + | EnqueueOutcome::SchedulerIndexExhausted + | EnqueueOutcome::WakeupIndexExhausted + )) + ); + if closes_without_occurrence { + return match Self::request_activation(actor_id) { + Ok(ActivationOutcome::Closed) => Ok(true), + Ok(ActivationOutcome::Latched | ActivationOutcome::Coalesced) => Ok(false), + _ => Err(DispatchError::Other( + "temporal terminal substitution failed", + )), + }; + } + let actor_type = state.identity.actor_class.actor_type(); + let breakdown = Self::trigger_fee_for_weight(actor_type, trigger_family, occurrence_weight); + if trigger_family == TriggerFamily::AtTime { + let temporal_capacity = Self::trigger_occurrence_capacity_sufficient( + actor_type, + &state.identity.sovereign_account, + breakdown, + ) + .map_err(|_| DispatchError::Other("temporal capacity calculation failed"))?; + if !temporal_capacity { + Self::finalize_actor_loaded( + actor_id, + &Self::derive_active_actor_view(state.identity, state.hot, state.contract), + CloseReason::TriggerAdmissionInsufficient, + ) + .map_err(|_| DispatchError::Other("underfunded temporal apoptosis failed"))?; + return Ok(true); + } + if !Self::try_charge_prechecked_automatic_trigger_occurrence( + actor_type, + &state.identity.sovereign_account, + breakdown, + ) + .map_err(|_| DispatchError::Other("temporal collection failed"))? + { + return Err(DispatchError::Other("temporal fee collection failed")); + } + } else if !Self::try_charge_automatic_trigger_occurrence( + actor_type, + &state.identity.sovereign_account, + breakdown, + ) + .map_err(|_| DispatchError::Other("temporal collection failed"))? + { + return Ok(false); + } + match Self::request_activation(actor_id) { + Ok(ActivationOutcome::Coalesced | ActivationOutcome::Latched) => { + if trigger_family == TriggerFamily::Cadenced { + Self::trigger_wakeup_substrate_invalidate_inner(actor_id) + .map_err(|_| DispatchError::Other("cadence latch disable failed"))?; + } + Self::deposit_event(Event::TriggerOccurrenceProcessed { + actor_id, + trigger_family: breakdown.trigger_family, + fee: breakdown.trigger_fee, + }); + Ok(false) + } + _ => Err(DispatchError::Other("temporal activation failed")), } - } - - pub fn wakeup_cursor_drain_unit_weight_upper(removes_bucket: bool) -> Weight { - Self::wakeup_cursor_drain_branch_weight(removes_bucket) - .saturating_add(Self::close_cleanup_weight_upper()) } pub fn drain_overdue_wakeups_cursor( @@ -2332,7 +3805,7 @@ impl Pallet { let Some((wakeup_key, next_clock_after_success)) = selected else { break; }; - let base_weight = Self::wakeup_cursor_drain_branch_weight(false); + let base_weight = Self::wakeup_cursor_drain_branch_weight(false, wakeup_key.clock()); if Self::combined_queue_occupancy() >= u64::from(T::MaxActiveActors::get()) || !meter.can_consume(base_weight) { @@ -2343,8 +3816,9 @@ impl Pallet { break; }; let removes_bucket = bucket.live_entries <= 1; - let unit_weight = Self::wakeup_cursor_drain_branch_weight(removes_bucket); - let admission_weight = Self::wakeup_cursor_drain_unit_weight_upper(removes_bucket); + let unit_weight = Self::wakeup_cursor_drain_branch_weight(removes_bucket, wakeup_key.clock()); + let admission_weight = + Self::wakeup_cursor_drain_unit_weight_for(removes_bucket, wakeup_key.clock()); if !meter.can_consume(admission_weight) { meter.consume(base_weight); break; @@ -2360,60 +3834,11 @@ impl Pallet { let mut closed_for_exhaustion = false; for actor_id in ready { if matches!(wakeup_key, WakeupKey::Tick(_)) { - let LoadedActorStateOf::Active(mut state) = Self::load_actor_state(actor_id) else { - return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err( - polkadot_sdk::sp_runtime::DispatchError::Other("tick wakeup owner is corrupt"), - )); - }; - let Trigger::Cadenced { every_ticks } = &state.contract.trigger else { - return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err( - polkadot_sdk::sp_runtime::DispatchError::Other("tick wakeup owner is not cadenced"), - )); - }; - if state - .hot - .trigger_runtime_state - .cadence_anchor_tick() - .is_none() - { - let Ok(Some(anchor_tick)) = Self::cadence_anchor_tick(&state.contract.trigger) else { - return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err( - polkadot_sdk::sp_runtime::DispatchError::Other("genesis cadence anchor failed"), - )); - }; - let Some(due_tick) = next_cadence_due_tick(anchor_tick, *every_ticks, now_tick) - else { - return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err( - polkadot_sdk::sp_runtime::DispatchError::Other("genesis cadence deadline failed"), - )); - }; - state.hot.trigger_runtime_state = TriggerRuntimeState::Cadenced { - anchor_tick: Some(anchor_tick), - }; - ActorHot::::insert(actor_id, state.hot); - if let Err(error) = Self::defer_tick_wakeup(actor_id, due_tick) { - if !Self::scheduler_index_is_exhausted(error) - || Self::close_for_scheduler_index_exhaustion(actor_id).is_err() - { - return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err( - polkadot_sdk::sp_runtime::DispatchError::Other("genesis cadence rearm failed"), - )); - } - closed_for_exhaustion = true; - } - continue; - } - match Self::request_activation(actor_id) { - Ok(ActivationOutcome::IgnoredStale) => { - return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err( - polkadot_sdk::sp_runtime::DispatchError::Other("cadence owner is stale"), - )); - } - Ok(ActivationOutcome::Closed) => closed_for_exhaustion = true, - Ok(ActivationOutcome::Coalesced | ActivationOutcome::Latched) => {} - Err(_) => { + match Self::process_due_temporal_occurrence(actor_id, now_tick) { + Ok(closed) => closed_for_exhaustion |= closed, + Err(error) => { return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err( - polkadot_sdk::sp_runtime::DispatchError::Other("cadence activation failed"), + error, )); } } @@ -2438,11 +3863,15 @@ impl Pallet { let (stats, closed_for_exhaustion) = match outcome { Ok(outcome) => outcome, Err(_) => { - WakeupWorkerFaultState::::put(WakeupWorkerFault { - key: wakeup_key, - page: bucket.head_page, - class: CrossingWorkerFaultClass::Invariant, - }); + let recorded = Self::record_wakeup_worker_fault( + meter, + WakeupWorkerFault { + key: wakeup_key, + page: bucket.head_page, + class: CrossingWorkerFaultClass::Invariant, + }, + ); + debug_assert!(recorded, "fault Weight was reserved before wakeup mutation"); break; } }; @@ -2470,10 +3899,10 @@ impl Pallet { .ok_or(EnqueueOutcome::SchedulerIndexExhausted) } - pub(crate) fn cadence_anchor_tick( + pub(crate) fn temporal_anchor_tick( trigger: &TriggerOf, ) -> Result, EnqueueOutcome> { - if !matches!(trigger, Trigger::Cadenced { .. }) { + if !matches!(trigger, Trigger::AtTime { .. } | Trigger::Cadenced { .. }) { return Ok(None); } scheduler_tick_ceil( @@ -2529,58 +3958,81 @@ impl Pallet { actor_id: ActorId, instance: &ActiveActorViewOf, ) -> Result, EnqueueOutcome> { - let continuation = - ContinuationStateStore::::get(actor_id).ok_or(EnqueueOutcome::CorruptedTopology)?; - Self::retry_eligible_at_loaded(instance, &continuation) + let run_state = + ActorRunStateStore::::get(actor_id).ok_or(EnqueueOutcome::CorruptedTopology)?; + Self::retry_eligible_at_loaded(instance, &run_state) } - fn retry_eligible_at_loaded( - instance: &ActiveActorViewOf, - continuation: &ContinuationStateOf, + pub(crate) fn suspension_eligible_at( + cooldown_blocks: u32, + window: Option>>, + last_attempt_block: BlockNumberFor, + unsuccessful_attempts_at_cursor: u32, ) -> Result, EnqueueOutcome> { - let cooldown: BlockNumberFor = instance.cooldown_blocks.into(); - let cursor_local_attempt = continuation - .unsuccessful_attempts_at_cursor - .saturating_sub(1); + let cooldown: BlockNumberFor = cooldown_blocks.into(); + let cursor_local_attempt = unsuccessful_attempts_at_cursor.saturating_sub(1); let backoff: BlockNumberFor = Self::retry_backoff_blocks(cursor_local_attempt).into(); let retry_delay = cooldown.max(backoff); - let mut eligible_at = continuation - .last_attempt_block + let mut eligible_at = last_attempt_block .checked_add(&retry_delay) .ok_or(EnqueueOutcome::SchedulerIndexExhausted)?; - if let Some(window) = instance.window { + if let Some(window) = window { eligible_at = eligible_at.max(window.start); } Ok(eligible_at) } + fn retry_eligible_at_loaded( + instance: &ActiveActorViewOf, + run_state: &ActorRunStateOf, + ) -> Result, EnqueueOutcome> { + let expected = Self::suspension_eligible_at( + instance.cooldown_blocks, + instance.window, + run_state.last_attempt_block, + run_state.unsuccessful_attempts_at_cursor, + )?; + if run_state.eligible_at != expected { + return Err(EnqueueOutcome::CorruptedTopology); + } + Ok(run_state.eligible_at) + } + fn schedule_next_work_local( actor_id: ActorId, instance: &ActiveActorViewOf, + hot: Option>, now: BlockNumberFor, cutoff_snapshotted: bool, requeues: &mut Vec, - ) -> Result<(), EnqueueOutcome> { + ) -> Result { if instance.lifecycle.is_paused() { - return Self::schedule_window_expiry(actor_id, instance); - } - if instance.cycle_state != CycleState::Suspended - && let Trigger::Cadenced { every_ticks } = instance.trigger - { - let anchor_tick = instance - .cadence_anchor_tick - .ok_or(EnqueueOutcome::CorruptedTopology)?; - let due_tick = - next_cadence_due_tick(anchor_tick, every_ticks, Self::current_scheduler_tick()?) - .ok_or(EnqueueOutcome::SchedulerIndexExhausted)?; - return Self::defer_tick_wakeup(actor_id, due_tick); + return Self::schedule_window_expiry(actor_id, instance).map(|()| { + if instance.window.is_some() { + StepControlPlacement::Wakeup + } else { + StepControlPlacement::None + } + }); } - let eligible_at = if instance.cycle_state == CycleState::Suspended { + let eligible_at = if instance.cycle_state == CycleState::Running { + let run = ActorRunStateStore::::get(actor_id).ok_or(EnqueueOutcome::CorruptedTopology)?; + if !run.running_is_coherent() { + return Err(EnqueueOutcome::CorruptedTopology); + } + run.eligible_at + } else if instance.cycle_state == CycleState::Suspended { Self::retry_eligible_at(actor_id, instance)? } else if instance.pending_signal { Self::next_eligible_at(instance, now)? } else { - return Self::schedule_window_expiry(actor_id, instance); + return Self::schedule_window_expiry(actor_id, instance).map(|()| { + if instance.window.is_some() { + StepControlPlacement::Wakeup + } else { + StepControlPlacement::None + } + }); }; let wakeup_at = instance.window.map_or(eligible_at, |window| { eligible_at.min(Self::window_terminal_at(&window)) @@ -2590,9 +4042,13 @@ impl Pallet { .ok_or(EnqueueOutcome::SchedulerIndexExhausted)?; if wakeup_at < exact_next_block || wakeup_at == exact_next_block && cutoff_snapshotted { requeues.push(actor_id); - Ok(()) + Ok(StepControlPlacement::Queue) } else { - Self::defer_wakeup(actor_id, wakeup_at) + match hot { + Some(hot) => Self::defer_wakeup_with_hot(actor_id, wakeup_at, instance, hot), + None => Self::defer_wakeup_loaded(actor_id, wakeup_at, instance), + } + .map(|()| StepControlPlacement::Wakeup) } } @@ -2601,13 +4057,45 @@ impl Pallet { instance: &ActiveActorViewOf, now: BlockNumberFor, cutoff_snapshotted: bool, - ) -> Result<(), EnqueueOutcome> { + ) -> Result { let mut requeues = Vec::new(); - Self::schedule_next_work_local(actor_id, instance, now, cutoff_snapshotted, &mut requeues)?; + let placement = Self::schedule_next_work_local( + actor_id, + instance, + None, + now, + cutoff_snapshotted, + &mut requeues, + )?; for actor_id in requeues { Self::enqueue(actor_id)?; } - Ok(()) + Ok(placement) + } + + fn schedule_next_work_loaded( + actor_id: ActorId, + instance: &ActiveActorViewOf, + hot: ActorHotStateOf, + now: BlockNumberFor, + cutoff_snapshotted: bool, + ) -> Result { + let mut requeues = Vec::new(); + let placement = Self::schedule_next_work_local( + actor_id, + instance, + Some(hot.clone()), + now, + cutoff_snapshotted, + &mut requeues, + )?; + for queued_actor in requeues { + if queued_actor != actor_id { + return Err(EnqueueOutcome::CorruptedTopology); + } + Self::enqueue_loaded(queued_actor, hot.clone())?; + } + Ok(placement) } pub(crate) fn is_window_expired(instance: &ActiveActorViewOf) -> bool { @@ -2618,90 +4106,169 @@ impl Pallet { .unwrap_or(false) } - pub(crate) fn user_native_balance(instance: &ActiveActorViewOf) -> T::Balance { - let native = T::FeeNativeAssetId::get(); - T::AssetOps::balance(&instance.sovereign_account, native) - } - pub(crate) fn classification_dispatch_error(error: ActorClassificationError) -> Error { match error { ActorClassificationError::ActorInvariant => Error::::ActorInvariant, - ActorClassificationError::ContinuationInvariant => Error::::ContinuationInvariant, + ActorClassificationError::RunInvariant => Error::::ActorRunInvariant, ActorClassificationError::ComputationOverflow => Error::::ComputationOverflow, } } pub(crate) fn expiry_substitution_due_loaded( instance: &ActiveActorViewOf, - continuation: Option<&ContinuationStateOf>, + run_state: Option<&ActorRunStateOf>, ) -> Result> { - Self::classify_actor_loaded(instance, continuation) + Self::classify_actor_loaded(instance, run_state) .map(|classification| classification.terminal_reason == Some(CloseReason::WindowExpired)) .map_err(Self::classification_dispatch_error) } - // Deterministic User viability precedence is BalanceExhausted, then - // FeeBudgetExhausted. The caller supplies the current contract cursor. - pub(crate) fn user_viability_close_reason( - instance: &ActiveActorViewOf, - start_cursor: usize, - ) -> Option { - if instance.actor_class.actor_type() != ActorType::User { - return None; - } - let native_balance = Self::user_native_balance(instance); - if native_balance < T::MinUserBalance::get() { - return Some(CloseReason::BalanceExhausted); - } - // The complete suffix envelope must fit above MinUserBalance: charging it - // must not cross the protected floor (spec 5.2.1). - let available_fee_budget = native_balance - .checked_sub(&T::MinUserBalance::get()) - .unwrap_or_default(); - if available_fee_budget < Self::attempt_fee_upper_bound(instance, start_cursor) { - return Some(CloseReason::FeeBudgetExhausted); - } - None - } - #[cfg(test)] pub(crate) fn classify_actor( actor_id: ActorId, instance: &ActiveActorViewOf, ) -> Result>, ActorClassificationError> { - let continuation = ContinuationStateStore::::get(actor_id); - Self::classify_actor_loaded(instance, continuation.as_ref()) + let run_state = ActorRunStateStore::::get(actor_id); + Self::classify_actor_loaded(instance, run_state.as_ref()) } - pub(crate) fn classify_actor_loaded( - instance: &ActiveActorViewOf, - continuation: Option<&ContinuationStateOf>, + pub(crate) fn classify_observation_activation_compact( + state: &ObservationActivationState, ) -> Result>, ActorClassificationError> { - if (instance.cycle_state == CycleState::Suspended) != continuation.is_some() { - return Err(ActorClassificationError::ContinuationInvariant); - } - let cursor = continuation - .as_ref() - .map_or(0, |state| state.cursor as usize); - if let Some(state) = continuation.as_ref() { - if cursor >= instance.steps.len() - || state.unsuccessful_attempts_at_cursor == 0 - || instance.steps[cursor] + let now = frame_system::Pallet::::block_number(); + let run_head = state.run_head.as_ref(); + let terminal_reason = if state + .authority + .window + .is_some_and(|window| now > window.end) + { + Some(CloseReason::WindowExpired) + } else if state.hot.cycle_state == CycleState::Idle && state.identity.cycle_nonce == u64::MAX { + Some(CloseReason::CycleNonceExhausted) + } else if run_head.is_some_and(|run| { + state.loaded_step.as_ref().is_some_and(|loaded_step| { + loaded_step + .step .on_error .retry_max_attempts() - .is_none() - { - return Err(ActorClassificationError::ContinuationInvariant); + .is_some_and(|max_attempts| run.unsuccessful_attempts_at_cursor >= max_attempts) + }) + }) { + Some(CloseReason::RetryAttemptsExhausted) + } else if Self::failure_limit_reached(state.hot.unsuccessful_attempt_streak) { + Some(CloseReason::ConsecutiveFailures) + } else if state.hot.cycle_state == CycleState::Idle + && state + .authority + .auto_close_at_cycle_nonce + .is_some_and(|target| state.identity.cycle_nonce >= target) + { + Some(CloseReason::AutoCloseNonceReached) + } else { + None + }; + + let execution_phase = if GlobalCircuitBreaker::::get() { + ActorExecutionPhase::GlobalCircuitBreaker + } else if state.hot.lifecycle.is_paused() { + ActorExecutionPhase::Paused + } else if terminal_reason.is_some() { + ActorExecutionPhase::Ready + } else if state.hot.cycle_state == CycleState::Running { + let run = run_head.ok_or(ActorClassificationError::RunInvariant)?; + if run.eligible_at > now { + ActorExecutionPhase::WaitingBlock(run.eligible_at) + } else { + ActorExecutionPhase::Ready + } + } else if state.hot.cycle_state == CycleState::Suspended { + let run = run_head.ok_or(ActorClassificationError::RunInvariant)?; + let expected = Self::suspension_eligible_at( + state.authority.cooldown_blocks, + state.authority.window, + run.last_attempt_block, + run.unsuccessful_attempts_at_cursor, + ) + .map_err(|outcome| match outcome { + EnqueueOutcome::SchedulerIndexExhausted => ActorClassificationError::ComputationOverflow, + _ => ActorClassificationError::RunInvariant, + })?; + if expected != run.eligible_at { + return Err(ActorClassificationError::RunInvariant); + } + if run.eligible_at > now { + ActorExecutionPhase::WaitingRetry(run.eligible_at) + } else { + ActorExecutionPhase::Ready + } + } else { + let cooldown_anchor = state + .hot + .last_cycle_block + .unwrap_or(state.hot.schedule_anchor); + let cooldown_eligible_at = + if state.identity.cycle_nonce == 0 && state.hot.last_cycle_block.is_none() { + state.hot.schedule_anchor + } else { + cooldown_anchor + .checked_add(&state.authority.cooldown_blocks.into()) + .ok_or(ActorClassificationError::ComputationOverflow)? + }; + let window_floor = state + .authority + .window + .map(|window| window.start) + .unwrap_or_else(Zero::zero); + let eligible_at = now.max(cooldown_eligible_at).max(window_floor); + if eligible_at > now { + ActorExecutionPhase::WaitingBlock(eligible_at) + } else { + ActorExecutionPhase::Ready + } + }; + Ok(ActorClassification { + terminal_reason, + execution_phase, + }) + } + + pub(crate) fn classify_actor_loaded( + instance: &ActiveActorViewOf, + run_state: Option<&ActorRunStateOf>, + ) -> Result>, ActorClassificationError> { + let cursor = run_state.as_ref().map_or(0, |state| state.cursor as usize); + match (instance.cycle_state, run_state) { + (CycleState::Idle, None) => {} + (CycleState::Running | CycleState::Suspended, Some(state)) => { + let expected_cycle_nonce = instance + .cycle_nonce + .checked_add(1) + .ok_or(ActorClassificationError::RunInvariant)?; + if cursor >= instance.steps.len() || state.cycle_nonce != expected_cycle_nonce { + return Err(ActorClassificationError::RunInvariant); + } + if instance.cycle_state == CycleState::Running { + if state.unsuccessful_attempts_at_cursor != 0 || !state.running_is_coherent() { + return Err(ActorClassificationError::RunInvariant); + } + } else if state.unsuccessful_attempts_at_cursor == 0 + || !state.suspension_is_coherent() + || instance.steps[cursor] + .on_error + .retry_max_attempts() + .is_none() + { + return Err(ActorClassificationError::RunInvariant); + } } + _ => return Err(ActorClassificationError::RunInvariant), } let terminal_reason = if Self::is_window_expired(instance) { Some(CloseReason::WindowExpired) - } else if let Some(reason) = Self::user_viability_close_reason(instance, cursor) { - Some(reason) } else if instance.cycle_state == CycleState::Idle && instance.cycle_nonce == u64::MAX { Some(CloseReason::CycleNonceExhausted) - } else if continuation.as_ref().is_some_and(|state| { + } else if run_state.as_ref().is_some_and(|state| { instance.steps[state.cursor as usize] .on_error .retry_max_attempts() @@ -2726,14 +4293,22 @@ impl Pallet { ActorExecutionPhase::Paused } else if terminal_reason.is_some() { ActorExecutionPhase::Ready + } else if instance.cycle_state == CycleState::Running { + let state = run_state.ok_or(ActorClassificationError::RunInvariant)?; + let now = frame_system::Pallet::::block_number(); + if state.eligible_at > now { + ActorExecutionPhase::WaitingBlock(state.eligible_at) + } else { + ActorExecutionPhase::Ready + } } else if instance.cycle_state == CycleState::Suspended { let eligible_at = Self::retry_eligible_at_loaded( instance, - continuation.ok_or(ActorClassificationError::ContinuationInvariant)?, + run_state.ok_or(ActorClassificationError::RunInvariant)?, ) .map_err(|outcome| match outcome { EnqueueOutcome::SchedulerIndexExhausted => ActorClassificationError::ComputationOverflow, - _ => ActorClassificationError::ContinuationInvariant, + _ => ActorClassificationError::RunInvariant, })?; let now = frame_system::Pallet::::block_number(); if eligible_at > now { @@ -2741,14 +4316,16 @@ impl Pallet { } else { ActorExecutionPhase::Ready } - } else if matches!(instance.trigger, Trigger::Cadenced { .. }) { + } else if matches!( + instance.trigger, + Trigger::AtTime { .. } | Trigger::Cadenced { .. } + ) { if instance.pending_signal { ActorExecutionPhase::Ready + } else if instance.temporal_occurrence_consumed { + ActorExecutionPhase::WaitingSignal } else { - let Some(WakeupPointer { - block: WakeupKey::Tick(due_tick), - .. - }) = instance.wakeup_pointer + let Some(TriggerWakeupPointer { tick: due_tick, .. }) = instance.trigger_wakeup_pointer else { return Err(ActorClassificationError::ActorInvariant); }; @@ -2777,7 +4354,14 @@ impl Pallet { reason: CloseReason, meter: &WeightMeter, ) -> AdmissionDecision { - let weight = Self::close_cycle_weight_upper_bound(instance); + let weight = if matches!( + reason, + CloseReason::CycleAdmissionInsufficient | CloseReason::TriggerAdmissionInsufficient + ) { + T::WeightInfo::pipeline_admission_apoptosis() + } else { + Self::close_cycle_weight_upper_bound(instance) + }; if !meter.can_consume(weight) { return AdmissionDecision::Defer; } @@ -2792,22 +4376,16 @@ impl Pallet { if Self::failure_limit_reached(instance.unsuccessful_attempt_streak.saturating_add(1)) { return true; } - for index in start_cursor..instance.steps.len() { - let step = &instance.steps[index]; - if step.on_error.retry_max_attempts().is_some_and(|limit| { - let next_attempt = if index == start_cursor { - prior_unsuccessful_attempts_at_cursor - .unwrap_or_default() - .saturating_add(1) - } else { - 1 - }; - next_attempt >= limit - }) { - return true; - } - } - false + instance + .steps + .get(start_cursor) + .and_then(|step| step.on_error.retry_max_attempts()) + .is_some_and(|limit| { + prior_unsuccessful_attempts_at_cursor + .unwrap_or_default() + .saturating_add(1) + >= limit + }) } fn cycle_may_auto_close_on_success(instance: &ActiveActorViewOf) -> bool { @@ -2829,51 +4407,77 @@ impl Pallet { ) || Self::cycle_may_auto_close_on_success(instance) } - fn cycle_admission_weight_upper( - instance: &ActiveActorViewOf, - start_cursor: usize, - prior_unsuccessful_attempts_at_cursor: Option, - ) -> Weight { - let mut weight = Self::attempt_weight_upper_bound(instance, start_cursor); - if Self::cycle_requires_terminal_cleanup_budget( - instance, - start_cursor, - prior_unsuccessful_attempts_at_cursor, - ) { - weight = weight.saturating_add(Self::close_cycle_weight_upper_bound(instance)); - } - weight - } - fn apply_admission_loaded( + actor_id: ActorId, instance: &ActiveActorViewOf, - continuation: Option<&ContinuationStateOf>, + run_state: Option<&ActorRunStateOf>, + step_plan: Option<&CurrentStepPlanOf>, meter: &WeightMeter, ) -> AdmissionDecision { - let Ok(classification) = Self::classify_actor_loaded(instance, continuation) else { + let Ok(classification) = Self::classify_actor_loaded(instance, run_state) else { return AdmissionDecision::Invariant; }; if classification.execution_phase == ActorExecutionPhase::GlobalCircuitBreaker { return AdmissionDecision::Skip; } + if classification.terminal_reason == Some(CloseReason::WindowExpired) { + return Self::close_admission_decision(instance, CloseReason::WindowExpired, meter); + } + if instance.actor_class.actor_type() == ActorType::User + && instance.cycle_state == CycleState::Idle + && instance.pending_signal + { + match Self::pipeline_capacity_sufficient( + actor_id, + ActorType::User, + &instance.sovereign_account, + ) { + Ok(true) => {} + Ok(false) => { + return Self::close_admission_decision( + instance, + CloseReason::CycleAdmissionInsufficient, + meter, + ); + } + Err(_) => return AdmissionDecision::Invariant, + } + } if let Some(reason) = classification.terminal_reason { return Self::close_admission_decision(instance, reason, meter); } if classification.execution_phase != ActorExecutionPhase::Ready { return AdmissionDecision::Skip; } - let continuation = if instance.cycle_state == CycleState::Suspended { - let Some(continuation) = continuation else { + let run_state = if matches!( + instance.cycle_state, + CycleState::Running | CycleState::Suspended + ) { + let Some(run_state) = run_state else { return AdmissionDecision::Skip; }; - Some(continuation) + Some(run_state) } else { None }; - let start_cursor = continuation - .as_ref() - .map_or(0, |state| state.cursor as usize); - let prior_unsuccessful_attempts_at_cursor = continuation + if instance.steps.is_empty() { + if instance.cycle_state != CycleState::Idle || run_state.is_some() || step_plan.is_some() { + return AdmissionDecision::Invariant; + } + let zero_step_weight = Self::contract_steps_admission_weight_upper( + instance.actor_class.actor_type(), + &instance.steps, + ); + if !meter.can_consume(zero_step_weight) { + return AdmissionDecision::Defer; + } + return AdmissionDecision::Admit { + weight: zero_step_weight, + terminal_cleanup_reserved: true, + }; + } + let start_cursor = run_state.as_ref().map_or(0, |state| state.cursor as usize); + let prior_unsuccessful_attempts_at_cursor = run_state .as_ref() .map(|state| state.unsuccessful_attempts_at_cursor); let terminal_cleanup_reserved = Self::cycle_requires_terminal_cleanup_budget( @@ -2881,16 +4485,53 @@ impl Pallet { start_cursor, prior_unsuccessful_attempts_at_cursor, ); - let cycle_weight_upper = Self::cycle_admission_weight_upper( - instance, - start_cursor, - prior_unsuccessful_attempts_at_cursor, - ); - if !meter.can_consume(cycle_weight_upper) { + let resources = step_plan + .filter(|plan| { + plan.ticket.actor_id == actor_id && plan.ticket.cursor as usize == start_cursor + }) + .map(|plan| plan.loaded_step.resources) + .or_else(|| { + u32::try_from(start_cursor) + .ok() + .and_then(|cursor| Self::load_current_step_from_storage(actor_id, cursor)) + .map(|loaded| loaded.resources) + }); + let Some(resources) = resources else { + return AdmissionDecision::Invariant; + }; + if instance.actor_class.actor_type() == ActorType::User + && instance.cycle_state == CycleState::Suspended + { + let Some(step) = instance.steps.get(start_cursor) else { + return AdmissionDecision::Invariant; + }; + match Self::action_capacity_sufficient( + ActorType::User, + &instance.sovereign_account, + step, + resources, + ) { + Ok(true) => {} + Ok(false) => { + return Self::close_admission_decision( + instance, + CloseReason::CycleAdmissionInsufficient, + meter, + ); + } + Err(_) => return AdmissionDecision::Invariant, + } + } + let mut current_step_weight = resources.control.saturating_add(resources.effect); + if terminal_cleanup_reserved { + current_step_weight = + current_step_weight.saturating_add(Self::close_cycle_weight_upper_bound(instance)); + } + if !meter.can_consume(current_step_weight) { return AdmissionDecision::Defer; } AdmissionDecision::Admit { - weight: cycle_weight_upper, + weight: current_step_weight, terminal_cleanup_reserved, } } @@ -2972,6 +4613,15 @@ impl Pallet { .map(|cursor| cursor.revision), } } + Trigger::AtTime { after_ticks } => { + let TriggerRuntimeState::AtTime { consumed, .. } = state.hot.trigger_runtime_state else { + return Err(ActorClassificationError::ActorInvariant); + }; + ActorTriggerActivation::AtTime { + after_ticks: *after_ticks, + consumed, + } + } Trigger::Cadenced { every_ticks } => ActorTriggerActivation::Cadenced { every_ticks: *every_ticks, }, @@ -2980,7 +4630,7 @@ impl Pallet { trigger, pending_signal: state.hot.pending_signal, placement, - eligibility: Self::classify_actor_loaded(&instance, state.continuation.as_ref())?, + eligibility: Self::classify_actor_loaded(&instance, state.run_state.as_ref())?, })) } @@ -3018,6 +4668,7 @@ impl Pallet { amount, Some(source), Some(&provenance), + TriggerCauseProvenance::ExternalPhase, ) } @@ -3034,6 +4685,7 @@ impl Pallet { amount, Some(source), Some(&provenance), + TriggerCauseProvenance::Deferred, ) } @@ -3050,6 +4702,7 @@ impl Pallet { amount, Some(source), Some(&provenance), + TriggerCauseProvenance::Deferred, ) } @@ -3058,7 +4711,14 @@ impl Pallet { asset: T::AssetId, amount: T::Balance, ) -> DispatchResult { - Self::notify_address_event_with_context(actor_id, asset, amount, None, None) + Self::notify_address_event_with_context( + actor_id, + asset, + amount, + None, + None, + TriggerCauseProvenance::Deferred, + ) } fn funding_event_authorized( @@ -3102,9 +4762,11 @@ impl Pallet { source, provenance, ); - let funding = state.funding; + let mut funding = state.funding; + let run_state = state.run_state; + let identity = state.identity.clone(); let instance = Self::derive_active_actor_view(state.identity, state.hot, state.contract); - let classification = Self::classify_actor_loaded(&instance, state.continuation.as_ref()) + let classification = Self::classify_actor_loaded(&instance, run_state.as_ref()) .map_err(Self::classification_dispatch_error)?; if classification.terminal_reason == Some(CloseReason::WindowExpired) || amount.is_zero() { return Ok(()); @@ -3112,13 +4774,17 @@ impl Pallet { if !authorized || !funding.funding_tracked_assets.contains(&asset) { return Ok(()); } - if let Some(accumulated) = funding.funding_accumulated.get(&asset) { - ensure!( - accumulated.checked_add(&amount).is_some(), - Error::::FundingAccumulatorOverflow - ); + if let Some(accumulated) = funding.funding_accumulated.get_mut(&asset) { + *accumulated = accumulated + .checked_add(&amount) + .ok_or(Error::::FundingAccumulatorOverflow)?; + } else { + funding + .funding_accumulated + .try_insert(asset, amount) + .map_err(|_| Error::::FundingAccumulatorOverflow)?; } - Ok(()) + Self::ensure_funding_state_hold_capacity(actor_id, &identity, &funding) } /// Typed certified-ingress preflight (spec 5.3, 6.2). Read-only and covers @@ -3130,6 +4796,14 @@ impl Pallet { let Some(actor_id) = Self::sovereign_index(&event.destination) else { return Ok(()); }; + if T::AssetOps::balance(&event.destination, event.asset) + .checked_add(&event.amount) + .is_none() + { + return Err(IngressFailure::permanent( + Error::::FundingAccumulatorOverflow, + )); + } Self::preflight_funding_event( actor_id, event.asset, @@ -3137,7 +4811,23 @@ impl Pallet { event.source.as_ref(), event.provenance.as_ref(), ) - .map_err(IngressFailure::permanent) + .map_err(Self::classify_ingress_error) + } + + fn trigger_cause_provenance(provenance: Option<&FundingProvenance>) -> TriggerCauseProvenance { + match provenance { + Some(FundingProvenance::Signed) => TriggerCauseProvenance::ExternalPhase, + Some(FundingProvenance::InternalProtocol | FundingProvenance::Xcm) | None => { + TriggerCauseProvenance::Deferred + } + } + } + + #[cfg(test)] + pub(crate) fn test_trigger_cause_provenance( + provenance: Option<&FundingProvenance>, + ) -> TriggerCauseProvenance { + Self::trigger_cause_provenance(provenance) } /// Typed certified-ingress consequence (spec 5.3, 6.2). Executes exactly once at @@ -3158,6 +4848,7 @@ impl Pallet { event.amount, event.source.as_ref(), event.provenance.as_ref(), + Self::trigger_cause_provenance(event.provenance.as_ref()), ) .map_err(Self::classify_ingress_error) } @@ -3165,11 +4856,13 @@ impl Pallet { /// Maps one certified-ingress error to its closed retry class. /// /// Recoverable queue/wakeup capacity or placement unavailability surfaces as - /// `QueueCapacityUnavailable` (queue saturation and failed wakeup placement) - /// and is Temporary. Monotonic ticket/index exhaustion, topology corruption, - /// and invariant failure are Permanent. + /// `QueueCapacityUnavailable` (queue saturation and failed wakeup placement) and + /// `StateHoldUnavailable` (owner may fund the positive geometry delta) are Temporary. + /// Monotonic ticket/index exhaustion, topology corruption, and invariant failure are Permanent. fn classify_ingress_error(error: DispatchError) -> IngressFailure { - if error == Error::::QueueCapacityUnavailable.into() { + if error == Error::::QueueCapacityUnavailable.into() + || error == Error::::StateHoldUnavailable.into() + { IngressFailure::temporary(error) } else { IngressFailure::permanent(error) @@ -3182,6 +4875,7 @@ impl Pallet { amount: T::Balance, source: Option<&T::AccountId>, provenance: Option<&FundingProvenance>, + cause_provenance: TriggerCauseProvenance, ) -> DispatchResult { // Zero or self/no-op movement creates no Actors ingress (spec 5.3). if amount.is_zero() { @@ -3189,7 +4883,16 @@ impl Pallet { } Self::preflight_funding_event(actor_id, asset, amount, source, provenance)?; Self::with_reused_transaction(|| { - Self::apply_address_event_parts(actor_id, asset, amount, source, provenance, true, true) + Self::apply_address_event_parts( + actor_id, + asset, + amount, + source, + provenance, + cause_provenance, + true, + true, + ) }) } @@ -3199,6 +4902,7 @@ impl Pallet { amount: T::Balance, source: Option<&T::AccountId>, provenance: Option<&FundingProvenance>, + cause_provenance: TriggerCauseProvenance, apply_trigger: bool, apply_funding: bool, ) -> DispatchResult { @@ -3215,13 +4919,15 @@ impl Pallet { provenance, ); let mut funding = state.funding; + let run_state = state.run_state; let instance = Self::derive_active_actor_view(state.identity, state.hot, state.contract); - let classification = Self::classify_actor_loaded(&instance, state.continuation.as_ref()) + let classification = Self::classify_actor_loaded(&instance, run_state.as_ref()) .map_err(Self::classification_dispatch_error)?; if classification.terminal_reason == Some(CloseReason::WindowExpired) { return Self::finalize_actor(actor_id, &instance, CloseReason::WindowExpired); } let signal_matched = if apply_trigger + && !instance.pending_signal && let Trigger::AddressEvent { source_filter, asset_filter, @@ -3247,6 +4953,7 @@ impl Pallet { amount }; ActorFunding::::insert(actor_id, funding); + Self::reconcile_actor_state_hold(actor_id)?; Self::deposit_event(Event::FundingAccumulated { actor_id, asset, @@ -3256,7 +4963,19 @@ impl Pallet { } } if signal_matched { - Self::request_activation(actor_id).map_err(Self::activation_failure_error)?; + let actor_type = instance.actor_class.actor_type(); + let breakdown = Self::trigger_fee_for_weight( + actor_type, + TriggerFamily::AddressEvent, + T::WeightInfo::address_event_trigger_occurrence(), + ); + let _ = Self::try_commit_automatic_trigger_occurrence( + actor_id, + actor_type, + &instance.sovereign_account, + breakdown, + cause_provenance, + )?; } Ok(()) } @@ -3269,9 +4988,9 @@ impl Pallet { } LoadedActorStateOf::Corrupt => return Err(Error::::ActorInvariant.into()), }; - let continuation = state.continuation; + let run_state = state.run_state; let instance = Self::derive_active_actor_view(state.identity, state.hot, state.contract); - if let Some(reason) = Self::classify_actor_loaded(&instance, continuation.as_ref()) + if let Some(reason) = Self::classify_actor_loaded(&instance, run_state.as_ref()) .map_err(Self::classification_dispatch_error)? .terminal_reason { diff --git a/template/pallets/actors/src/subscriptions.rs b/template/pallets/actors/src/subscriptions.rs index 8b08d5bf..ad782cca 100644 --- a/template/pallets/actors/src/subscriptions.rs +++ b/template/pallets/actors/src/subscriptions.rs @@ -689,6 +689,24 @@ impl Pallet { "observation subscription aggregate count disagrees", )); } + for actor_id in IndexedTriggerDetectionDisabled::::iter_keys() { + let hot = ActorHot::::get(actor_id).ok_or(TryRuntimeError::Other( + "disabled indexed detector has no active Actor", + ))?; + let contract = Self::load_actor_contract(actor_id).ok_or(TryRuntimeError::Other( + "disabled indexed detector has no Actor Contract", + ))?; + if !hot.pending_signal + || !matches!( + contract.trigger, + Trigger::ObservationChange { .. } | Trigger::ObservationCrossing { .. } + ) + { + return Err(TryRuntimeError::Other( + "disabled indexed detector disagrees with useful readiness", + )); + } + } Ok(()) } } diff --git a/template/pallets/actors/src/tests.rs b/template/pallets/actors/src/tests.rs index 7d2fa093..1e4090e6 100644 --- a/template/pallets/actors/src/tests.rs +++ b/template/pallets/actors/src/tests.rs @@ -1,28 +1,89 @@ use crate::{ - ActiveLifecycle, ActorActivationPlacement, ActorClass, ActorClassification, - ActorClassificationError, ActorContract, ActorContracts, ActorEligibility, ActorExecutionPhase, - ActorFunding, ActorHot, ActorId, ActorIdentities, ActorTriggerActivation, ActorType, - AmountResolution, AssetFilter, AssetFilterOf, AttemptDisposition, CancellationReason, - CloseReason, ContinuationStateStore, CrossingDirection, CrossingMemberPages, CrossingMemberships, - CrossingPhase, CrossingTransition, CycleResult, CycleState, Error, Event, FeeChargeKind, - FeeEnvelopeError, FeeEnvelopeInput, FundingSourcePolicy, GlobalCircuitBreaker, - IdleStarvationPhase, IdleStarvationState, InitialLifecycle, InputLimit, LoadedActorStateOf, - Mutability, NextActorId, ObservationCrossing, ObservationSubscriberPageList, ObservationTiming, - OpeningSurface, OutcomeTotals, OwnerSlotBitmaps, Precondition, Predicate, QueueEntry, QueueHead, - QueueOccupancy, QueuePages, QueueTail, RetryClass, ScheduleWindow, SimulationError, - SimulationMode, SimulationStepRecord, SourceFilter, SourceFilterOf, SovereignIndex, SplitLeg, - SplitTransferLegsOf, StepErrorPolicy, StepOf, StepOutcome, StepSkippedReason, SuspensionReason, - SystemSovereignState, Task, TaskFailure, TaskOf, TimedPredicate, Trigger, TriggerRuntimeState, - WakeupBucketState, WakeupBuckets, WakeupClock, WakeupEntry, WakeupKey, WakeupPage, WakeupPages, - WakeupPointer, adapters::AssetOps, compose_attempt_fee_envelope, fee_native_protected_minimum, - mock::*, settle_attempt_fee_step, + ActiveLifecycle, ActorActivationPlacement, ActorAdmissionCertificates, ActorClass, + ActorClassification, ActorClassificationError, ActorContract, ActorEligibility, + ActorExecutionPhase, ActorFunding, ActorHot, ActorId, ActorIdentities, ActorRunAuthority, + ActorRunStateStore, ActorTriggerActivation, ActorType, AmountResolution, AssetFilter, + AssetFilterOf, AttemptDisposition, CancellationReason, CloseReason, CrossingDirection, + CrossingMemberPages, CrossingMemberships, CrossingPhase, CrossingTransition, CycleResult, + CycleState, Error, Event, FeeChargeKind, FeeEnvelopeError, FeeEnvelopeInput, FundingSourcePolicy, + GlobalCircuitBreaker, IdleStarvationPhase, IdleStarvationState, InitialLifecycle, InputLimit, + LoadedActorStateOf, Mutability, NextActorId, ObservationCrossing, ObservationSubscriberPageList, + ObservationTiming, OpeningSurface, OutcomeTotals, OwnerSlotBitmaps, Precondition, Predicate, + QueueEntry, QueueHead, QueueOccupancy, QueuePages, QueueTail, RetryClass, ScheduleWindow, + SimulationError, SimulationMode, SimulationStepRecord, SourceFilter, SourceFilterOf, + SovereignIndex, SplitLeg, SplitTransferLegsOf, StepErrorPolicy, StepOf, StepOutcome, + StepSkippedReason, SuspensionReason, SystemSovereignState, Task, TaskFailure, TaskOf, + TimedPredicate, Trigger, TriggerFamily, TriggerRuntimeState, WakeupBucketState, WakeupBuckets, + WakeupClock, WakeupEntry, WakeupKey, WakeupPage, WakeupPages, WakeupPointer, adapters::AssetOps, + compose_attempt_fee_envelope, fee_native_protected_minimum, mock::*, settle_attempt_fee_step, }; use alloc::collections::BTreeSet; const RETRY_LATER: StepErrorPolicy = StepErrorPolicy::RetryLater { max_attempts: 10 }; +fn manual_trigger_fee() -> Balance { + Actors::trigger_fee_for_weight( + ActorType::User, + TriggerFamily::Manual, + ::manual_trigger(), + ) + .trigger_fee +} + +fn address_event_trigger_fee() -> Balance { + Actors::trigger_fee_for_weight( + ActorType::User, + TriggerFamily::AddressEvent, + ::address_event_trigger_occurrence(), + ) + .trigger_fee +} + +fn observation_change_trigger_fee() -> Balance { + Actors::trigger_fee_for_weight( + ActorType::User, + TriggerFamily::ObservationChange, + ::observation_change_trigger_occurrence(), + ) + .trigger_fee +} + +fn observation_crossing_trigger_fee() -> Balance { + Actors::trigger_fee_for_weight( + ActorType::User, + TriggerFamily::ObservationCrossing, + ::observation_crossing_trigger_occurrence(), + ) + .trigger_fee +} + +fn at_time_trigger_fee() -> Balance { + Actors::trigger_fee_for_weight( + ActorType::User, + TriggerFamily::AtTime, + ::at_time_trigger_occurrence(), + ) + .trigger_fee +} + +fn cadenced_trigger_fee() -> Balance { + Actors::trigger_fee_for_weight( + ActorType::User, + TriggerFamily::Cadenced, + ::cadenced_trigger_occurrence(), + ) + .trigger_fee +} + +fn pipeline_opening_fee(plan: &crate::ContractSteps) -> Balance { + Actors::user_pipeline_machine_capacity_requirement(plan) + .expect("fixture Pipeline Machine requirement fits") + .checked_sub(TestMinUserBalance::get()) + .expect("Pipeline requirement contains the ledger minimum") +} + fn update_contract_parts(actor_id: ActorId) -> crate::ActorContractOf { - ActorContracts::::get(actor_id).expect("Actor Contract exists") + Actors::load_actor_contract(actor_id).expect("Actor Contract exists") } fn replace_auto_close( @@ -101,17 +162,46 @@ type RuntimeTrigger = crate::TriggerOf; type RuntimeTask = TaskOf; type RuntimeStep = StepOf; type RuntimeActorContract = crate::ActorContractOf; -type RuntimeContinuationState = crate::ContinuationStateOf; +type RuntimeActorRunState = crate::ActorRunStateOf; type MockBlockNumber = polkadot_sdk::frame_system::pallet_prelude::BlockNumberFor; type TestWeightInfo = crate::weights::TestWeightInfo; +fn run_contract_authority(actor_id: ActorId) -> ActorRunAuthority<[u8; 32]> { + let admission = + ActorAdmissionCertificates::::get(actor_id).expect("Actor admission certificate exists"); + ActorRunAuthority { + semantic_contract_id: admission.semantic_contract_id, + body_commitment: admission.body_commitment, + admission_identity: admission.admission_identity, + } +} + fn scheduled_wakeup_block(actor_id: crate::ActorId) -> Option { - Actors::actor_hot(actor_id).and_then(|hot| match hot.wakeup_pointer?.block { - WakeupKey::Block(block) => Some(block), - WakeupKey::Tick(tick) => Some(tick), + Actors::actor_hot(actor_id).and_then(|hot| { + hot + .wakeup_pointer + .map(|pointer| match pointer.block { + WakeupKey::Block(block) => block, + WakeupKey::Tick(tick) => tick, + }) + .or_else(|| hot.trigger_wakeup_pointer.map(|pointer| pointer.tick)) }) } +fn queue_entry(ticket: u64, actor_id: ActorId) -> QueueEntry { + QueueEntry { + actor_id, + cycle_nonce: 0, + cursor: 0, + ticket, + eligible_at: 0, + contract_commitment: crate::ActorContractCommitment { + semantic_contract_id: [0; 32], + body_commitment: [0; 32], + }, + } +} + fn seed_saturated_tombstone_queue() { let page_size: u32 = ::QueuePageSize::get(); let capacity: u32 = ::MaxQueueLength::get(); @@ -119,9 +209,11 @@ fn seed_saturated_tombstone_queue() { let first = page_id * page_size; let len = page_size.min(capacity - first); let entries = (0..len) - .map(|offset| QueueEntry { - ticket: u64::from(first + offset), - actor_id: 10_000_000 + u64::from(first + offset), + .map(|offset| { + queue_entry( + u64::from(first + offset), + 10_000_000 + u64::from(first + offset), + ) }) .collect::>(); QueuePages::::insert( @@ -227,7 +319,9 @@ fn crossing_phase(actor_id: ActorId) -> CrossingPhase { .trigger_runtime_state { TriggerRuntimeState::ObservationCrossing { phase, .. } => phase, - TriggerRuntimeState::Stateless | TriggerRuntimeState::Cadenced { .. } => { + TriggerRuntimeState::Stateless + | TriggerRuntimeState::AtTime { .. } + | TriggerRuntimeState::Cadenced { .. } => { panic!("actor does not own Crossing runtime state") } } @@ -337,6 +431,13 @@ fn observation_schedule(feeds: Vec) -> RuntimeSchedule { } } +fn at_time_schedule(after_ticks: u32) -> RuntimeSchedule { + Schedule { + trigger: Trigger::at_time(u64::from(after_ticks)), + cooldown_blocks: 0, + } +} + fn timer_schedule(every_ticks: u32) -> RuntimeSchedule { Schedule { trigger: Trigger::cadenced(u64::from(every_ticks)), @@ -567,6 +668,18 @@ fn native_balance(who: &AccountId) -> Balance { >::free_balance(who) } +fn actor_state_hold_total(actor_id: ActorId) -> Balance { + let hold = Actors::actor_state_hold(actor_id).expect("User Actor state hold exists"); + hold + .breakdown + .identity + .saturating_add(hold.breakdown.contract_head) + .saturating_add(hold.breakdown.contract_body) + .saturating_add(hold.breakdown.detector) + .saturating_add(hold.breakdown.funding) + .saturating_add(hold.breakdown.run) +} + fn asset_balance(who: &AccountId, asset: TestAsset) -> Balance { MockAssetOps::balance(who, asset) } @@ -618,32 +731,34 @@ fn create_suspended_system_retry(block: u64) -> u64 { actor_id )); run_idle(Weight::MAX); - assert!(Actors::continuation_state(actor_id).is_some()); + assert!(Actors::actor_run_state(actor_id).is_some()); actor_id } fn user_step_fee(step: &StepOf) -> Balance { let plan = BoundedVec::try_from(vec![step.clone()]).expect("one step fits the shared bound"); - Actors::attempt_fee_envelope(ActorType::User, &plan, 0) - .expect("one step has a checked fee envelope") - .total + Actors::maximum_contract_step_fee(ActorType::User, &plan, 0) + .expect("one Step has a checked fee") + .total_fee } fn fund_native_raw(who: &AccountId, amount: Balance) { let _ = >::deposit_creating(who, amount); } -/// User Active prefunding requirement: `MinUserBalance + attempt_fee_envelope(plan, 0, User).total`. +/// Fixture funding: ledger minimum plus one Manual Trigger occurrence and the complete +/// Pipeline Machine/cleanup maximum. Creation itself still requires no service prefunding. fn user_prefunding_requirement(plan: &crate::ContractSteps) -> Balance { - ::MinUserBalance::get().saturating_add( - Actors::attempt_fee_envelope(ActorType::User, plan, 0) - .expect("fixture plan has a checked fee envelope") - .total, - ) + let pipeline = Actors::user_pipeline_machine_capacity_requirement(plan) + .expect("fixture plan has a checked Pipeline Machine requirement"); + let trigger = manual_trigger_fee(); + pipeline + .checked_add(trigger) + .expect("fixture Trigger and Pipeline funding fits") } -/// Pre-funds the deterministic User sovereign account so Active creation/activation admits -/// (spec 7.1), without mutating any pallet state. +/// Pre-funds the deterministic User sovereign account for later activation/execution fixtures +/// without mutating Actors state. fn prefund_user_sovereign( owner: AccountId, slot: u8, @@ -668,9 +783,59 @@ fn deplete_user_sovereign(actor_id: u64, amount: Balance) { MockAssetOps::burn(&acc, TestAsset::Native, amount).expect("fixture depletion burn succeeds"); } -fn run_idle(weight: Weight) { +fn run_actor_hook_order_with_external( + now: u64, + external: impl FnOnce(), + idle_weight: Weight, +) -> Weight { + frame_system::Pallet::::set_block_number(now); + let initialize_weight = Actors::on_initialize(now); + let prepass_weight = Actors::actor_prepass(RuntimeOrigin::none()) + .expect("fixture prepass succeeds") + .actual_weight + .unwrap_or_else(Weight::zero); + external(); + initialize_weight + .saturating_add(prepass_weight) + .saturating_add(Actors::on_idle(now, idle_weight)) +} + +fn run_next_idle(weight: Weight) { + let now = frame_system::Pallet::::block_number() + .checked_add(1) + .expect("test block number advances"); + frame_system::Pallet::::set_block_number(now); + Actors::on_initialize(now); + run_prepass(); + run_idle(weight); +} + +fn run_prepass() { let now = frame_system::Pallet::::block_number(); + if Actors::block_resource_state().is_some_and(|state| state.ensure_block(now).is_err()) { + crate::CurrentBlockResourceState::::kill(); + } + assert_ok!(Actors::actor_prepass(RuntimeOrigin::none())); +} + +fn run_idle(weight: Weight) { + let mut now = frame_system::Pallet::::block_number(); Actors::on_idle(now, weight); + let max_blocks = + <::MaxContractSteps as Get>::get().saturating_mul(2u32); + for _ in 1..max_blocks { + if !ActorHot::::iter_values().any(|hot| hot.cycle_state == CycleState::Running) { + break; + } + let Some(next) = now.checked_add(1) else { + break; + }; + now = next; + frame_system::Pallet::::set_block_number(now); + Actors::on_initialize(now); + run_prepass(); + Actors::on_idle(now, weight); + } } fn starvation_observation_weight() -> Weight { @@ -712,14 +877,19 @@ fn run_idle_until_cycle_nonce(actor_id: u64, target_cycle_nonce: u64) { panic!("cycle nonce did not reach target"); } -fn has_actor_event(predicate: impl Fn(&Event) -> bool) -> bool { +fn actor_event_count(predicate: impl Fn(&Event) -> bool) -> usize { frame_system::Pallet::::events() .into_iter() .filter_map(|record| match record.event { RuntimeEvent::Actors(event) => Some(event), _ => None, }) - .any(|event| predicate(&event)) + .filter(predicate) + .count() +} + +fn has_actor_event(predicate: impl Fn(&Event) -> bool) -> bool { + actor_event_count(predicate) > 0 } fn mixed_materialization_ticket_trace() -> Vec { @@ -824,12 +994,18 @@ fn assert_scheduler_close_requires_atomic_budget(reason: CloseReason, shortfall: }; match reason { CloseReason::WindowExpired => fund_native(actor_id, 1_000), - CloseReason::BalanceExhausted => {} - CloseReason::FeeBudgetExhausted => fund_native(actor_id, 60), + CloseReason::CycleAdmissionInsufficient => { + let balance = native_balance(&sovereign_account(actor_id)); + deplete_user_sovereign(actor_id, balance); + fund_native( + actor_id, + TestMinUserBalance::get().saturating_add(manual_trigger_fee()), + ); + } CloseReason::CycleNonceExhausted => { fund_native(actor_id, 1_000); ActorIdentities::::mutate(actor_id, |maybe| { - maybe.as_mut().expect("actor identity exists").cycle_nonce = u64::MAX; + maybe.as_mut().expect("actor identity exists").cycle_nonce = u64::MAX - 1; }); } CloseReason::ConsecutiveFailures => { @@ -844,12 +1020,9 @@ fn assert_scheduler_close_requires_atomic_budget(reason: CloseReason, shortfall: ActorIdentities::::mutate(actor_id, |maybe| { maybe.as_mut().expect("actor identity exists").cycle_nonce = 1; }); - ActorContracts::::mutate(actor_id, |maybe| { - maybe - .as_mut() - .expect("actor contract exists") - .auto_close_at_cycle_nonce = Some(1); - }); + let mut contract = Actors::load_actor_contract(actor_id).expect("actor contract exists"); + contract.auto_close_at_cycle_nonce = Some(1); + assert_ok!(Actors::store_actor_contract(actor_id, contract)); } unsupported => panic!("unsupported admission-time close reason: {unsupported:?}"), } @@ -857,12 +1030,16 @@ fn assert_scheduler_close_requires_atomic_budget(reason: CloseReason, shortfall: RuntimeOrigin::signed(ALICE), actor_id )); - if reason == CloseReason::WindowExpired { - ActorContracts::::mutate(actor_id, |maybe| { - maybe.as_mut().expect("actor contract exists").window = - Some(ScheduleWindow { start: 0, end: 0 }); + if reason == CloseReason::CycleNonceExhausted { + ActorIdentities::::mutate(actor_id, |maybe| { + maybe.as_mut().expect("actor identity exists").cycle_nonce = u64::MAX; }); } + if reason == CloseReason::WindowExpired { + let mut contract = Actors::load_actor_contract(actor_id).expect("actor contract exists"); + contract.window = Some(ScheduleWindow { start: 0, end: 0 }); + assert_ok!(Actors::store_actor_contract(actor_id, contract)); + } let before = Actors::actor_hot(actor_id) .unwrap_or_else(|| panic!("{reason:?} actor remains active before scheduler admission")); let queue_head = QueueHead::::get(); @@ -872,7 +1049,11 @@ fn assert_scheduler_close_requires_atomic_budget(reason: CloseReason, shortfall: // admission budget covers only the queue discovery and actor probes plus the close. let pre_admission = discovery.saturating_add(Actors::scheduler_actor_state_probe_weight_upper()); - let close = Actors::close_cleanup_weight_upper(); + let close = if reason == CloseReason::CycleAdmissionInsufficient { + ::pipeline_admission_apoptosis() + } else { + Actors::close_cleanup_weight_upper() + }; let consume = ::scheduler_paged_consume_preserve_page() .max(::scheduler_paged_consume_delete_page()); let budget = pre_admission @@ -898,15 +1079,15 @@ mod proptest_actor { use super::Schedule; use super::{ RETRY_LATER, all_conditions, asset_balance, create_system_with, fund_native, make_step, - manual_schedule, native_balance, prefund_active_user_creation, run_idle, set_asset_balance, - setup_pool, setup_temporary_retry_pool, sovereign_account, + manual_schedule, native_balance, prefund_active_user_creation, run_idle, run_next_idle, + run_prepass, set_asset_balance, setup_pool, setup_temporary_retry_pool, sovereign_account, }; use crate::{ - ActorContracts, ActorFunding, ActorHot, ActorIdentities, AmountResolution, AssetFilter, - ContinuationStateStore, CrossingDirection, CrossingPhase, CrossingTransition, CycleState, - Event, FundingSourcePolicy, Mutability, ObservationCrossing, QueueOccupancy, QueuePages, - SourceFilter, StepErrorPolicy, StepOf, SystemSovereignState, SystemSovereigns, Task, Trigger, - WakeupBuckets, WakeupPages, mock::*, + ActorFunding, ActorHot, ActorIdentities, ActorRunStateStore, AmountResolution, AssetFilter, + CrossingDirection, CrossingPhase, CrossingTransition, CycleState, Event, FundingSourcePolicy, + Mutability, ObservationCrossing, QueueOccupancy, QueuePages, SourceFilter, StepErrorPolicy, + StepOf, SystemSovereignState, SystemSovereigns, Task, Trigger, WakeupBuckets, WakeupPages, + mock::*, }; use codec::Encode; use polkadot_sdk::frame_support::{ @@ -1064,9 +1245,7 @@ mod proptest_actor { )); run_idle(Weight::MAX); assert_eq!( - Actors::continuation_state(actor_id) - .expect("suspended") - .cursor, + Actors::actor_run_state(actor_id).expect("suspended").cursor, 1 ); assert_eq!(native_balance(&actor), 80); @@ -1074,10 +1253,14 @@ mod proptest_actor { let output_after_prefix = asset_balance(&actor, TestAsset::Local(77)); assert!(output_after_prefix > 0); - frame_system::Pallet::::set_block_number(2); + let retry_block = frame_system::Pallet::::block_number().saturating_add(1); + frame_system::Pallet::::set_block_number(retry_block); + Actors::on_initialize(retry_block); + run_prepass(); run_idle(Weight::MAX); + run_next_idle(Weight::MAX); assert_eq!( - Actors::continuation_state(actor_id) + Actors::actor_run_state(actor_id) .expect("same cursor") .cursor, 1 @@ -1089,9 +1272,17 @@ mod proptest_actor { ); set_temporary_add_liquidity_failure(false); - frame_system::Pallet::::set_block_number(4); + let eligible_at = Actors::actor_run_state(actor_id) + .expect("retry remains") + .eligible_at; + let prepass_block = + eligible_at.max(frame_system::Pallet::::block_number().saturating_add(1)); + frame_system::Pallet::::set_block_number(prepass_block); + Actors::on_initialize(prepass_block); + run_prepass(); run_idle(Weight::MAX); - assert!(Actors::continuation_state(actor_id).is_none()); + run_next_idle(Weight::MAX); + assert!(Actors::actor_run_state(actor_id).is_none()); assert_eq!(native_balance(&actor), 70); assert_eq!(native_balance(&BOB), bob_before + 10); assert_eq!( @@ -1147,7 +1338,7 @@ mod proptest_actor { run_idle(Weight::MAX); assert_eq!(native_balance(&actor), 90); assert_eq!( - Actors::continuation_state(actor_id).expect("suspended").cursor, + Actors::actor_run_state(actor_id).expect("suspended").cursor, 1 ); @@ -1155,13 +1346,13 @@ mod proptest_actor { run_idle(Weight::MAX); assert_eq!(native_balance(&actor), 90); assert_eq!( - Actors::continuation_state(actor_id).expect("same cursor").cursor, + Actors::actor_run_state(actor_id).expect("same cursor").cursor, 1 ); - assert_ok!(Actors::cancel_continuation(RuntimeOrigin::root(), actor_id)); + assert_ok!(Actors::cancel_run(RuntimeOrigin::root(), actor_id)); assert_eq!(native_balance(&actor), 90); - assert!(Actors::continuation_state(actor_id).is_none()); + assert!(Actors::actor_run_state(actor_id).is_none()); assert_eq!( System::events() .iter() @@ -1277,17 +1468,17 @@ mod proptest_actor { conserved_total: Balance, ) { let hot_ids: std::collections::BTreeSet<_> = ActorHot::::iter_keys().collect(); - let contract_ids: std::collections::BTreeSet<_> = ActorContracts::::iter_keys().collect(); + let contract_ids: std::collections::BTreeSet<_> = + crate::ActorContractHeads::::iter_keys().collect(); let funding_ids: std::collections::BTreeSet<_> = ActorFunding::::iter_keys().collect(); let identity_ids: std::collections::BTreeSet<_> = ActorIdentities::::iter_keys().collect(); let dormant_ids: std::collections::BTreeSet<_> = identity_ids.difference(&hot_ids).copied().collect(); - let continuation_ids: std::collections::BTreeSet<_> = - ContinuationStateStore::::iter_keys().collect(); + let run_ids: std::collections::BTreeSet<_> = ActorRunStateStore::::iter_keys().collect(); assert_eq!(hot_ids, contract_ids); assert_eq!(hot_ids, funding_ids); - assert!(continuation_ids.is_subset(&hot_ids)); + assert!(run_ids.is_subset(&hot_ids)); assert!(hot_ids.is_subset(&identity_ids)); assert_eq!(Actors::active_actor_count() as usize, hot_ids.len()); assert_eq!(Actors::actor_identity_count() as usize, identity_ids.len()); @@ -1302,8 +1493,8 @@ mod proptest_actor { Some(*actor_id) ); assert_eq!( - hot.cycle_state == CycleState::Suspended, - continuation_ids.contains(actor_id) + matches!(hot.cycle_state, CycleState::Running | CycleState::Suspended), + run_ids.contains(actor_id) ); let funding = ActorFunding::::get(actor_id).expect("funding key resolves"); assert!( @@ -1312,10 +1503,23 @@ mod proptest_actor { .keys() .all(|asset| funding.funding_tracked_assets.contains(asset)) ); - if let Some(continuation) = ContinuationStateStore::::get(actor_id) { - let contract = ActorContracts::::get(actor_id).expect("contract key resolves"); - assert!((continuation.cursor as usize) < contract.steps.len()); - assert!(continuation.cumulative_outcomes.executed_steps <= continuation.cursor); + if let Some(run_state) = ActorRunStateStore::::get(*actor_id) { + let contract = Actors::load_actor_contract(*actor_id).expect("contract key resolves"); + assert_eq!( + identity.cycle_nonce.checked_add(1), + Some(run_state.cycle_nonce) + ); + assert!((run_state.cursor as usize) < contract.steps.len()); + assert!(run_state.cumulative_outcomes.executed_steps <= run_state.cursor); + assert!(run_state.last_step_outcome.is_some()); + match hot.cycle_state { + CycleState::Running => { + assert!(run_state.suspension.is_none()); + assert!(run_state.last_committed_step_block.is_some()); + } + CycleState::Suspended => assert!(run_state.suspension.is_some()), + CycleState::Idle => panic!("Idle Actor cannot retain run state"), + } } if let Some(ticket) = hot.queue_ticket { assert!( @@ -1343,6 +1547,21 @@ mod proptest_actor { })) ); } + if let Some(pointer) = hot.trigger_wakeup_pointer { + let key = crate::WakeupKey::Tick(pointer.tick); + assert!( + live_wakeups.insert((key, pointer.page_id, pointer.slot)), + "duplicate live Trigger wakeup pointer" + ); + let page = WakeupPages::::get((key, pointer.page_id)) + .expect("live Trigger wakeup page exists"); + assert_eq!( + page.entries.get(pointer.slot as usize), + Some(&Some(crate::WakeupEntry { + actor_id: *actor_id + })) + ); + } } for actor_id in &dormant_ids { let identity = ActorIdentities::::get(actor_id).expect("dormant key resolves"); @@ -1621,7 +1840,7 @@ mod proptest_actor { })] #[test] - fn seeded_continuation_state_machine_preserves_cross_store_and_scheduler_invariants( + fn seeded_actor_run_state_machine_preserves_cross_store_and_scheduler_invariants( operations in prop::collection::vec(model_op(), 1..80), ) { new_test_ext().execute_with(|| { @@ -1675,7 +1894,7 @@ mod proptest_actor { let before_hot: std::collections::BTreeMap<_, _> = ActorHot::::iter().collect(); let before_identities: std::collections::BTreeMap<_, _> = ActorIdentities::::iter().collect(); - let before_continuation = ContinuationStateStore::::get(system_id); + let before_continuation = ActorRunStateStore::::get(system_id); let before_funding = ActorFunding::::get(system_id); let before_system_balance = Balances::free_balance(system_sovereign); let before_bob_balance = Balances::free_balance(BOB); @@ -1856,12 +2075,12 @@ mod proptest_actor { let _ = Actors::on_idle(block, Weight::MAX); } } - ModelOp::Continue if ContinuationStateStore::::contains_key(system_id) => { + ModelOp::Continue if ActorRunStateStore::::contains_key(system_id) => { set_temporary_dex_failure(false); let _ = Actors::on_idle(block, Weight::MAX); } - ModelOp::Cancel if ContinuationStateStore::::contains_key(system_id) => { - let _ = Actors::cancel_continuation(RuntimeOrigin::root(), system_id); + ModelOp::Cancel if ActorRunStateStore::::contains_key(system_id) => { + let _ = Actors::cancel_run(RuntimeOrigin::root(), system_id); } ModelOp::Close if !closed => { if Actors::close_actor(RuntimeOrigin::root(), system_id).is_ok() { @@ -1950,15 +2169,22 @@ mod proptest_actor { closed = true; } - let after_continuation = ContinuationStateStore::::get(system_id); + let after_continuation = ActorRunStateStore::::get(system_id); if matches!(operation, ModelOp::Cancel) && before_continuation.is_some() { - assert!(after_continuation.is_none()); - assert_eq!(Balances::free_balance(system_sovereign), before_system_balance); - assert_eq!(Balances::free_balance(BOB), before_bob_balance); - assert_eq!( - ActorFunding::::get(system_id).as_ref().map(Encode::encode), - before_funding.as_ref().map(Encode::encode) - ); + if after_continuation.is_some() { + assert_eq!( + after_continuation.as_ref().map(Encode::encode), + before_continuation.as_ref().map(Encode::encode), + "rejected cancellation preserves the exact run" + ); + } else { + assert_eq!(Balances::free_balance(system_sovereign), before_system_balance); + assert_eq!(Balances::free_balance(BOB), before_bob_balance); + assert_eq!( + ActorFunding::::get(system_id).as_ref().map(Encode::encode), + before_funding.as_ref().map(Encode::encode) + ); + } } if matches!(operation, ModelOp::Pause | ModelOp::Resume) && before_continuation.is_some() @@ -2322,7 +2548,7 @@ fn parity_target_step(case: StepParityCase) -> StepOf { all_conditions(vec![Predicate::ObservationAbove { feed: 1, threshold: 1, - max_age_blocks: 5, + max_age_blocks: 1, }]), transfer(AmountResolution::Fixed(5)), ), @@ -2456,7 +2682,7 @@ fn observed_attempt_projection( let (_, outcomes) = summary.expect("attempt close follows its cycle summary"); return (AttemptDisposition::Closed(reason), outcomes, None, None); } - if let Some(continuation) = Actors::continuation_state(actor_id) { + if let Some(continuation) = Actors::actor_run_state(actor_id) { return ( AttemptDisposition::Suspended, continuation.cumulative_outcomes, diff --git a/template/pallets/actors/src/tests/core.rs b/template/pallets/actors/src/tests/core.rs index 4946c24a..b47f9468 100644 --- a/template/pallets/actors/src/tests/core.rs +++ b/template/pallets/actors/src/tests/core.rs @@ -172,6 +172,38 @@ fn create_admission_enforces_both_idle_weight_dimensions_before_charging() { }); } +#[test] +fn maximum_contract_admission_uses_one_step_envelope_instead_of_suffix_sum() { + new_test_ext().execute_with(|| { + let step = transfer_contract_steps(BOB, 10)[0].clone(); + let contract_steps = crate::ContractSteps::::try_from(vec![ + step; + <::MaxContractSteps as Get>::get() + as usize + ]) + .expect("maximum Contract Steps fit"); + let required = Actors::contract_steps_admission_weight_upper(ActorType::User, &contract_steps); + let fixed = ::scheduler_on_idle_base() + .saturating_add(::materialization_coordinator_base()) + .saturating_add(::scheduler_paged_tombstone_drain(1)) + .saturating_add(TestWakeupWeightLimit::get()) + .saturating_add(TestCrossingWorkerWeightLimit::get()) + .saturating_add(TestObservationFanoutWeightLimit::get()); + set_guaranteed_on_idle_weight(required.saturating_add(fixed)); + prefund_active_user_creation(ALICE, &contract_steps); + assert_ok!(Actors::create_user_actor( + RuntimeOrigin::signed(ALICE), + Mutability::Mutable, + user_active_contract(manual_schedule(), None, contract_steps), + )); + let actor_id = crate::NextActorId::::get().saturating_sub(1); + assert_eq!( + crate::ActorContractTailChunks::::iter_prefix(actor_id).count(), + 2 + ); + }); +} + #[test] fn plan_updates_reject_a_prospective_run_above_the_idle_budget() { new_test_ext().execute_with(|| { @@ -225,9 +257,17 @@ fn test_weight_fallback_equals_reference_interface_for_all_classes() { pause_actor, resume_actor, manual_trigger, + address_event_trigger_occurrence, + observation_change_trigger_occurrence, + observation_crossing_trigger_occurrence, + at_time_trigger_occurrence, + cadenced_trigger_occurrence, observation_change_ingress, observation_fanout_base, observation_fanout_page, + record_crossing_worker_fault, + record_observation_fanout_worker_fault, + record_wakeup_worker_fault, close_actor, fee_collection, cycle_orchestration, @@ -243,6 +283,9 @@ fn test_weight_fallback_equals_reference_interface_for_all_classes() { task_unstake, task_dex_exact_in, task_dex_exact_out, + current_step_load_head, + current_step_plan_opening_head, + current_step_plan_suspended_head, scheduler_on_idle_base, scheduler_paged_append_existing_page, scheduler_paged_append_new_page, @@ -262,12 +305,15 @@ fn test_weight_fallback_equals_reference_interface_for_all_classes() { scheduler_wakeup_cursor_worker_future, scheduler_paged_consume_preserve_page, scheduler_paged_consume_delete_page, + scheduler_inner_zero_step_complete, scheduler_actor_state_probe, transaction_extension_ingress_base, transaction_extension_ingress_notify, - continuation_retry, - continuation_complete, - continuation_cancel, + run_progress, + run_suspend, + run_retry, + run_complete, + run_cancel, update_contract, set_global_circuit_breaker, set_active_actor_limit, @@ -282,6 +328,13 @@ fn test_weight_fallback_equals_reference_interface_for_all_classes() { ); })+}; } + same_at!(contract_geometry_create, 0, 1, 2); + same_at!(contract_geometry_close, 0, 1, 2); + same_at!(contract_geometry_reconstruct, 0, 1, 2); + same_at!(current_step_load_tail, 1, 2, 4); + same_at!(current_step_plan_running_tail, 1, 2, 4); + same_at!(opening_snapshot_capture, 1, 8, 16); + same_at!(opening_predicate_capture, 1, 16, 32); same_at!(predicate_set_evaluation, 0, 1, 8); same_at!(task_split_transfer, 0, 1, 8); same_at!(step_orchestration, 0, 1, 8); @@ -290,8 +343,7 @@ fn test_weight_fallback_equals_reference_interface_for_all_classes() { same_at!(scheduler_paged_execute_cheap, 0, 1, 1_000); same_at!(scheduler_paged_execute_cheap_mixed, 0, 1, 1_000); same_at!(funding_snapshot_open, 0, 1, 10); - same_at!(continuation_suspend, 0, 1, 16); - same_at!(continuation_suffix_admission, 0, 1, 8); + same_at!(run_suffix_admission, 0, 1, 8); same_at!(permissionless_sweep_many, 0, 1, 5); } @@ -375,7 +427,9 @@ fn create_atomicity_checkpoint_failure_rolls_back_all_state() { fn control_transition_reuses_existing_transaction_at_depth_limit() { fn run_nested(depth: u32, actor_id: ActorId) -> polkadot_sdk::sp_runtime::DispatchResult { if depth == 0 { - return Actors::manual_trigger(RuntimeOrigin::signed(ALICE), actor_id); + return Actors::manual_trigger(RuntimeOrigin::signed(ALICE), actor_id) + .map(|_| ()) + .map_err(|error| error.error); } polkadot_sdk::frame_support::storage::with_transaction(|| { match run_nested(depth - 1, actor_id) { @@ -547,11 +601,11 @@ fn whitelist_size_is_bounded_by_runtime_type_limit() { } #[test] -fn create_rejects_timer_delay_above_max() { +fn create_rejects_temporal_delay_above_max() { new_test_ext().execute_with(|| { frame_system::Pallet::::set_block_number(1); - let max_delay = u32::try_from(::MaxCadenceDelayTicks::get()) - .expect("test cadence horizon fits u32"); + let max_delay = u32::try_from(::MaxTemporalDelayTicks::get()) + .expect("test temporal horizon fits u32"); let schedule = timer_schedule(max_delay.saturating_add(1)); assert_noop!( Actors::create_user_actor( @@ -564,6 +618,65 @@ fn create_rejects_timer_delay_above_max() { }); } +#[test] +fn create_rejects_invalid_at_time_delay_and_block_schedule_policy() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + assert_noop!( + Actors::create_user_actor( + RuntimeOrigin::signed(ALICE), + Mutability::Mutable, + user_active_contract(at_time_schedule(0), None, transfer_contract_steps(BOB, 1)), + ), + Error::::InvalidTriggerConfiguration + ); + assert_noop!( + Actors::create_user_actor( + RuntimeOrigin::signed(ALICE), + Mutability::Mutable, + user_active_contract( + Schedule { + trigger: Trigger::at_time( + ::MaxTemporalDelayTicks::get().saturating_add(1), + ), + cooldown_blocks: 0, + }, + None, + transfer_contract_steps(BOB, 1), + ), + ), + Error::::ExecutionDelayTooLong + ); + assert_noop!( + Actors::create_user_actor( + RuntimeOrigin::signed(ALICE), + Mutability::Mutable, + user_active_contract( + Schedule { + trigger: Trigger::at_time(1), + cooldown_blocks: 1, + }, + None, + transfer_contract_steps(BOB, 1), + ), + ), + Error::::InvalidTriggerConfiguration + ); + assert_noop!( + Actors::create_user_actor( + RuntimeOrigin::signed(ALICE), + Mutability::Mutable, + user_active_contract( + at_time_schedule(1), + Some(ScheduleWindow { start: 1, end: 101 }), + transfer_contract_steps(BOB, 1), + ), + ), + Error::::InvalidScheduleWindow + ); + }); +} + #[test] fn address_event_waits_through_cooldown_without_second_signal() { new_test_ext().execute_with(|| { @@ -923,7 +1036,39 @@ fn matching_runtime_simulation_reports_stop_and_rolls_everything_back() { ); assert_eq!(Actors::active_actor_view(actor_id), Some(actor_before)); assert_eq!(System::events(), events_before); - assert!(Actors::continuation_state(actor_id).is_none()); + assert!(Actors::actor_run_state(actor_id).is_none()); + }); +} + +#[test] +fn zero_step_simulation_completes_without_step_records_or_state_mutation() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let contract = system_active_contract(manual_schedule(), None, BoundedVec::default()) + .expect("zero-Step Contract exists"); + let actor_id = create_system_with(ALICE, manual_schedule(), None, BoundedVec::default()); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + let events_before = System::events(); + let actor_before = Actors::active_actor_view(actor_id).expect("Actor exists"); + + let result = Actors::simulate_current_contract( + actor_id, + ActorType::System, + Mutability::Mutable, + contract, + SimulationMode::FreshCurrentPlan, + ) + .expect("ready zero-Step plan simulates"); + + assert_eq!(result.status, AttemptDisposition::Completed); + assert_eq!(result.cumulative_outcomes, OutcomeTotals::default()); + assert!(result.steps.is_empty()); + assert_eq!(Actors::active_actor_view(actor_id), Some(actor_before)); + assert_eq!(System::events(), events_before); + assert!(Actors::actor_run_state(actor_id).is_none()); }); } @@ -981,6 +1126,12 @@ fn signal_during_suspension_latches_a_later_logical_run() { Actors::active_actor_view(actor_id) .expect("suspended") .cycle_nonce, + 0 + ); + assert_eq!( + Actors::actor_run_state(actor_id) + .expect("open run") + .cycle_nonce, 1 ); assert!(!Actors::pending_signal(actor_id)); @@ -1065,12 +1216,28 @@ fn explicit_cancellation_preserves_committed_effects_and_emits_terminal_summary( )); run_idle(Weight::MAX); assert_eq!(native_balance(&BOB), bob_before + 10); + let open_run = Actors::actor_run_state(actor_id).expect("cycle remains suspended"); + assert_eq!(open_run.cycle_nonce, 1); + assert_eq!(open_run.funding_snapshot.get(&TestAsset::Native), Some(&20)); + assert_eq!( + Actors::actor_identities(actor_id) + .expect("identity remains live") + .cycle_nonce, + 0 + ); assert_ok!(Actors::notify_address_event( actor_id, TestAsset::Native, 7, &ALICE )); + assert_eq!( + Actors::actor_run_state(actor_id) + .expect("later funding does not replace the open snapshot") + .funding_snapshot + .get(&TestAsset::Native), + Some(&20) + ); let actor_before_cancel = native_balance(&actor); let failures_before_cancel = Actors::active_actor_view(actor_id) .expect("suspended actor") @@ -1078,11 +1245,8 @@ fn explicit_cancellation_preserves_committed_effects_and_emits_terminal_summary( frame_system::Pallet::::set_block_number(2); frame_system::Pallet::::reset_events(); - assert_ok!(Actors::cancel_continuation( - RuntimeOrigin::signed(ALICE), - actor_id - )); - assert!(Actors::continuation_state(actor_id).is_none()); + assert_ok!(Actors::cancel_run(RuntimeOrigin::signed(ALICE), actor_id)); + assert!(Actors::actor_run_state(actor_id).is_none()); let actor_state = Actors::active_actor_view(actor_id).expect("cancelled actor remains"); assert_eq!(actor_state.cycle_state, CycleState::Idle); assert_eq!(actor_state.cycle_nonce, 1); @@ -1132,8 +1296,8 @@ fn explicit_cancellation_preserves_committed_effects_and_emits_terminal_summary( } if id == actor_id )); assert_noop!( - Actors::cancel_continuation(RuntimeOrigin::signed(ALICE), actor_id), - Error::::ContinuationNotFound + Actors::cancel_run(RuntimeOrigin::signed(ALICE), actor_id), + Error::::ActorRunNotFound ); }); } @@ -1149,14 +1313,14 @@ fn cancellation_is_mutable_only() { system_active_contract(timer_schedule(1), None, inert_contract_steps()), )); assert_noop!( - Actors::cancel_continuation(RuntimeOrigin::root(), actor_id), + Actors::cancel_run(RuntimeOrigin::root(), actor_id), Error::::ImmutableActor ); }); } #[test] -fn permissionless_sweep_many_ignores_missing_ids() { +fn permissionless_sweep_many_ignores_missing_and_preserves_unfunded_idle_users() { new_test_ext().execute_with(|| { frame_system::Pallet::::set_block_number(1); let prefunded = user_prefunding_requirement(&transfer_contract_steps(BOB, 1)); @@ -1175,14 +1339,14 @@ fn permissionless_sweep_many_ignores_missing_ids() { RuntimeOrigin::signed(CHARLIE), sweep_ids, )); - assert!(Actors::active_actor_view(user_a).is_none()); + assert!(Actors::active_actor_view(user_a).is_some()); assert!(has_actor_event(|event| { matches!( event, Event::SweepBatchProcessed { requested: 2, - closed: 1, - alive: 0, + closed: 0, + alive: 1, missing: 1, } ) @@ -1191,7 +1355,7 @@ fn permissionless_sweep_many_ignores_missing_ids() { } #[test] -fn on_initialize_is_a_zero_weight_noop() { +fn on_initialize_freezes_cutoff_and_executes_the_mandatory_base_pass() { new_test_ext().execute_with(|| { frame_system::Pallet::::set_block_number(1); let actor_id = create_system_with( @@ -1207,9 +1371,20 @@ fn on_initialize_is_a_zero_weight_noop() { )); frame_system::Pallet::::set_block_number(2); assert_eq!(Actors::on_initialize(2), Weight::zero()); + let consumed = Actors::actor_prepass(RuntimeOrigin::none()) + .expect("mandatory prepass succeeds") + .actual_weight + .expect("mandatory prepass reports actual Weight"); + assert!( + ::scheduler_on_initialize_cutoff().all_lte(consumed) + ); + assert_eq!( + Actors::prepass_execution_cutoff(), + Some((2, Actors::next_queue_ticket())) + ); let inst = Actors::active_actor_view(actor_id).expect("Actors exists"); - assert_eq!(inst.cycle_nonce, 0); - assert!(!has_actor_event(|event| { + assert_eq!(inst.cycle_nonce, 1); + assert!(has_actor_event(|event| { matches!( event, Event::CycleStarted { @@ -1218,11 +1393,15 @@ fn on_initialize_is_a_zero_weight_noop() { } if *id == actor_id ) })); + let state = Actors::block_resource_state().expect("prepass opens resource state"); + assert_eq!(state.phase(), crate::BlockResourcePhase::ExternalPhase); + assert!(state.usage().actor_effect_used() != Weight::zero()); + assert_eq!(state.outstanding_reservations(), 0); }); } #[test] -fn breaker_keeps_explicit_repair_sweep_operational() { +fn breaker_sweep_does_not_predict_future_pipeline_affordability() { new_test_ext().execute_with(|| { frame_system::Pallet::::set_block_number(1); let prefunded = user_prefunding_requirement(&transfer_contract_steps(BOB, 1)); @@ -1242,16 +1421,11 @@ fn breaker_keeps_explicit_repair_sweep_operational() { RuntimeOrigin::signed(BOB), actor_id, )); - assert!(Actors::active_actor_view(actor_id).is_none()); - assert!(has_actor_event(|event| { - matches!( - event, - Event::ActorClosed { - actor_id: id, - reason: CloseReason::BalanceExhausted, - } if *id == actor_id - ) - })); + assert!(Actors::active_actor_view(actor_id).is_some()); + assert!(!has_actor_event(|event| matches!( + event, + Event::ActorClosed { actor_id: id, .. } if *id == actor_id + ))); }); } @@ -1432,7 +1606,7 @@ fn canonical_control_replacements_are_exact_noops_before_rate_limiting() { ); let before = Actors::active_actor_view(actor_id).expect("actor exists"); let funding_before = actor_funding(actor_id); - let funding_policy_before = ActorContracts::::get(actor_id) + let funding_policy_before = Actors::load_actor_contract(actor_id) .expect("active Actor Contract") .funding; System::reset_events(); @@ -2133,7 +2307,7 @@ fn fresh_current_plan_simulation_returns_runtime_trace_and_rolls_back_every_writ assert_eq!(result.status, AttemptDisposition::Completed); assert_eq!(result.cycle_nonce, 1); assert_eq!(result.start_cursor, 0); - assert_eq!(result.continuation_cursor, None); + assert_eq!(result.run_cursor, None); assert_eq!(result.unsuccessful_attempts_at_cursor, None); assert_eq!(result.cumulative_outcomes.executed_steps, 1); assert_eq!( @@ -2157,6 +2331,6 @@ fn fresh_current_plan_simulation_returns_runtime_trace_and_rolls_back_every_writ frame_system::Pallet::::event_count(), event_count_before ); - assert!(Actors::continuation_state(actor_id).is_none()); + assert!(Actors::actor_run_state(actor_id).is_none()); }); } diff --git a/template/pallets/actors/src/tests/crossing.rs b/template/pallets/actors/src/tests/crossing.rs index 2410b228..4c37063a 100644 --- a/template/pallets/actors/src/tests/crossing.rs +++ b/template/pallets/actors/src/tests/crossing.rs @@ -1,4 +1,5 @@ use super::*; +use crate::scheduler::ActivationOutcome; use crate::weights::WeightInfo as _; #[test] @@ -108,6 +109,374 @@ fn observation_crossing_semantics_are_exact_and_hysteretic() { } } +#[test] +fn observation_crossing_fire_charges_before_readiness() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + set_observation( + 7, + crate::ScalarObservationState::Fresh { + value: 50, + observed_at: 1, + }, + ); + let actor_id = create_user_with( + ALICE, + Mutability::Mutable, + Schedule { + trigger: RuntimeTrigger::observation_crossing(7, CrossingDirection::Rising, 100, 80), + cooldown_blocks: 0, + }, + None, + transfer_contract_steps(BOB, 1), + ); + { + let installed = CrossingMemberships::::get(actor_id).expect("Crossing membership"); + let installed_member = CrossingMemberPages::::get(installed.key, installed.page) + .and_then(|page| page.entries.get(installed.offset as usize).copied()) + .expect("installed Crossing member"); + assert_eq!(installed_member.counterpart_threshold, 80); + let compact = Actors::load_crossing_idle_activation_state(actor_id, 7) + .expect("Idle Crossing activation authority"); + assert!(compact.run_head.is_none()); + assert!(compact.loaded_step.is_none()); + } + + let sovereign = sovereign_account(actor_id); + let sovereign_before = native_balance(&sovereign); + let sink_before = native_balance(&TestFeeSink::get()); + clear_fee_collections(); + + assert_ok!(Actors::note_observation_transition_with_provenance( + 7, + crate::ObservationTransition { + revision: 2, + previous: Some(50), + current: 150, + }, + crate::TriggerCauseProvenance::ExternalPhase, + )); + assert_eq!( + Actors::crossing_transition_queue(7) + .and_then(|queue| queue.first().copied()) + .map(|transition| (transition.cause_provenance, transition.cause_block)), + Some((crate::TriggerCauseProvenance::ExternalPhase, 1)) + ); + drain_crossing_work(); + + let fee = observation_crossing_trigger_fee(); + assert_eq!(fee_collections(), vec![fee]); + assert_eq!(native_balance(&sovereign), sovereign_before - fee); + assert_eq!(native_balance(&TestFeeSink::get()), sink_before + fee); + let hot = ActorHot::::get(actor_id).expect("Crossing Actor remains active"); + assert!(hot.pending_signal); + assert!(hot.queue_ticket.is_some() || hot.wakeup_pointer.is_some()); + assert_eq!(crossing_phase(actor_id), CrossingPhase::WaitingForRearm); + { + let rearmed = CrossingMemberships::::get(actor_id).expect("rearm membership"); + let rearmed_member = CrossingMemberPages::::get(rearmed.key, rearmed.page) + .and_then(|page| page.entries.get(rearmed.offset as usize).copied()) + .expect("rearm Crossing member"); + assert_eq!(rearmed.key.threshold, 80); + assert_eq!(rearmed_member.counterpart_threshold, 100); + } + assert!(has_actor_event(|event| matches!( + event, + Event::TriggerOccurrenceProcessed { + actor_id: id, + trigger_family: TriggerFamily::ObservationCrossing, + fee: charged, + } if *id == actor_id && *charged == fee + ))); + }); +} + +#[test] +fn repeated_latched_crossing_fires_charge_only_the_useful_transition() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + set_observation( + 7, + crate::ScalarObservationState::Fresh { + value: 50, + observed_at: 1, + }, + ); + let actor_id = create_user_with( + ALICE, + Mutability::Mutable, + Schedule { + trigger: RuntimeTrigger::observation_crossing(7, CrossingDirection::Rising, 100, 80), + cooldown_blocks: 0, + }, + None, + transfer_contract_steps(BOB, 1), + ); + clear_fee_collections(); + + for (revision, previous, current) in [(2, 50, 150), (3, 150, 70), (4, 70, 150)] { + assert_ok!(Actors::note_observation_transition( + 7, + crate::ObservationTransition { + revision, + previous: Some(previous), + current, + }, + )); + drain_crossing_work(); + } + + let fee = observation_crossing_trigger_fee(); + assert_eq!(fee_collections(), vec![fee]); + let hot = ActorHot::::get(actor_id).expect("Crossing Actor remains active"); + assert!(hot.pending_signal); + assert!(hot.queue_ticket.is_some() || hot.wakeup_pointer.is_some()); + assert_eq!( + frame_system::Pallet::::events() + .iter() + .filter(|record| matches!( + &record.event, + RuntimeEvent::Actors(Event::TriggerOccurrenceProcessed { + actor_id: id, + trigger_family: TriggerFamily::ObservationCrossing, + .. + }) if *id == actor_id + )) + .count(), + 1 + ); + assert!(has_actor_event(|event| matches!( + event, + Event::TriggerOccurrenceProcessed { + actor_id: id, + trigger_family: TriggerFamily::ObservationCrossing, + .. + } if *id == actor_id + ))); + }); +} + +#[test] +fn busy_crossing_fire_charges_and_latches_only_the_future_pipeline() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + set_observation( + 7, + crate::ScalarObservationState::Fresh { + value: 50, + observed_at: 1, + }, + ); + let steps = BoundedVec::try_from(vec![ + make_step(Task::Transfer { + to: BOB, + asset: TestAsset::Native, + amount: AmountResolution::Fixed(1), + }), + make_step(Task::Transfer { + to: CHARLIE, + asset: TestAsset::Native, + amount: AmountResolution::Fixed(1), + }), + ]) + .expect("two-Step Contract fits"); + let actor_id = create_user_with( + ALICE, + Mutability::Mutable, + Schedule { + trigger: RuntimeTrigger::observation_crossing(7, CrossingDirection::Rising, 100, 80), + cooldown_blocks: 0, + }, + None, + steps, + ); + fund_native(actor_id, 1_000_000); + assert_eq!( + Actors::request_activation(actor_id), + Ok(ActivationOutcome::Latched) + ); + Actors::on_idle(1, Weight::MAX); + let run_before = ActorRunStateStore::::get(actor_id).expect("Pipeline is Running"); + assert_eq!( + ActorHot::::get(actor_id).map(|hot| hot.cycle_state), + Some(CycleState::Running) + ); + clear_fee_collections(); + + assert_ok!(Actors::note_observation_transition( + 7, + crate::ObservationTransition { + revision: 2, + previous: Some(50), + current: 150, + }, + )); + drain_crossing_work(); + + assert_eq!(fee_collections(), vec![observation_crossing_trigger_fee()]); + let hot = ActorHot::::get(actor_id).expect("busy Actor remains active"); + assert_eq!(hot.cycle_state, CycleState::Running); + assert!(hot.pending_signal); + let run_after = ActorRunStateStore::::get(actor_id).expect("Pipeline remains Running"); + assert_eq!(run_after.cursor, run_before.cursor); + assert_eq!(run_after.cycle_nonce, run_before.cycle_nonce); + }); +} + +#[test] +fn underfunded_crossing_fire_advances_without_fee_readiness_or_apoptosis() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + set_observation( + 7, + crate::ScalarObservationState::Fresh { + value: 50, + observed_at: 1, + }, + ); + let actor_id = create_user_with( + ALICE, + Mutability::Mutable, + Schedule { + trigger: RuntimeTrigger::observation_crossing(7, CrossingDirection::Rising, 100, 80), + cooldown_blocks: 0, + }, + None, + inert_contract_steps(), + ); + let sovereign = sovereign_account(actor_id); + let balance = native_balance(&sovereign); + deplete_user_sovereign(actor_id, balance - TestMinUserBalance::get()); + clear_fee_collections(); + + assert_ok!(Actors::note_observation_transition( + 7, + crate::ObservationTransition { + revision: 2, + previous: Some(50), + current: 150, + }, + )); + drain_crossing_work(); + + assert!(fee_collections().is_empty()); + assert_eq!(native_balance(&sovereign), TestMinUserBalance::get()); + let hot = ActorHot::::get(actor_id).expect("process remains live"); + assert!(!hot.pending_signal); + assert!(hot.queue_ticket.is_none()); + assert!(hot.wakeup_pointer.is_none()); + assert_eq!(crossing_phase(actor_id), CrossingPhase::WaitingForRearm); + assert!(Actors::crossing_transition_queue(7).is_none()); + assert!(Actors::active_actor_view(actor_id).is_some()); + }); +} + +#[test] +fn crossing_batch_falls_back_to_scalar_progress_for_an_underfunded_member() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + set_observation( + 7, + crate::ScalarObservationState::Fresh { + value: 50, + observed_at: 1, + }, + ); + let schedule = Schedule { + trigger: RuntimeTrigger::observation_crossing(7, CrossingDirection::Rising, 100, 80), + cooldown_blocks: 0, + }; + let underfunded = create_user_with( + ALICE, + Mutability::Mutable, + schedule.clone(), + None, + inert_contract_steps(), + ); + let funded = create_user_with( + BOB, + Mutability::Mutable, + schedule, + None, + inert_contract_steps(), + ); + let balance = native_balance(&sovereign_account(underfunded)); + deplete_user_sovereign(underfunded, balance - TestMinUserBalance::get()); + clear_fee_collections(); + + assert_ok!(Actors::note_observation_transition( + 7, + crate::ObservationTransition { + revision: 2, + previous: Some(50), + current: 150, + }, + )); + Actors::service_crossing_transitions(Weight::MAX); + + assert!(Actors::crossing_transition_queue(7).is_none()); + assert!(Actors::crossing_worker_fault().is_none()); + let underfunded_hot = ActorHot::::get(underfunded).expect("underfunded process remains"); + assert!(!underfunded_hot.pending_signal); + assert!(underfunded_hot.queue_ticket.is_none()); + assert_eq!(crossing_phase(underfunded), CrossingPhase::WaitingForRearm); + let funded_hot = ActorHot::::get(funded).expect("funded process remains"); + assert!(funded_hot.pending_signal); + assert!(funded_hot.queue_ticket.is_some() || funded_hot.wakeup_pointer.is_some()); + assert_eq!(fee_collections(), vec![observation_crossing_trigger_fee()]); + }); +} + +#[test] +fn crossing_fire_collection_failure_advances_without_readiness() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + set_observation( + 7, + crate::ScalarObservationState::Fresh { + value: 50, + observed_at: 1, + }, + ); + let actor_id = create_user_with( + ALICE, + Mutability::Mutable, + Schedule { + trigger: RuntimeTrigger::observation_crossing(7, CrossingDirection::Rising, 100, 80), + cooldown_blocks: 0, + }, + None, + inert_contract_steps(), + ); + let sovereign = sovereign_account(actor_id); + let before = native_balance(&sovereign); + set_fail_fee_sink_transfer(true); + + assert_ok!(Actors::note_observation_transition( + 7, + crate::ObservationTransition { + revision: 2, + previous: Some(50), + current: 150, + }, + )); + drain_crossing_work(); + set_fail_fee_sink_transfer(false); + + assert_eq!(native_balance(&sovereign), before); + let hot = ActorHot::::get(actor_id).expect("process remains live"); + assert!(!hot.pending_signal); + assert!(hot.queue_ticket.is_none()); + assert!(hot.wakeup_pointer.is_none()); + assert_eq!(crossing_phase(actor_id), CrossingPhase::WaitingForRearm); + assert!(Actors::crossing_transition_queue(7).is_none()); + assert!(!has_actor_event(|event| matches!( + event, + Event::TriggerOccurrenceProcessed { actor_id: id, .. } if *id == actor_id + ))); + }); +} + #[test] fn observation_crossing_u128_boundaries_and_adjacent_hysteresis_are_exact() { let rising = ObservationCrossing { @@ -394,7 +763,7 @@ fn exact_crossing_trigger_equality_preserves_canonical_and_physical_state() { contract_steps_with_step(make_step(Task::StopCycle)), ); frame_system::Pallet::::set_block_number(2); - let contract = ActorContracts::::get(actor_id).expect("active Actor Contract"); + let contract = Actors::load_actor_contract(actor_id).expect("active Actor Contract"); let hot = ActorHot::::get(actor_id).expect("active Actor hot state"); let locator = Actors::crossing_membership(actor_id).expect("Crossing membership"); System::reset_events(); @@ -403,7 +772,7 @@ fn exact_crossing_trigger_equality_preserves_canonical_and_physical_state() { actor_id, contract.clone(), )); - assert_eq!(ActorContracts::::get(actor_id), Some(contract)); + assert_eq!(Actors::load_actor_contract(actor_id), Some(contract)); assert_eq!(ActorHot::::get(actor_id), Some(hot)); assert_eq!(Actors::crossing_membership(actor_id), Some(locator)); assert!(System::events().is_empty()); @@ -597,7 +966,7 @@ fn crossing_source_prefix_snapshot_grants_only_contiguous_validated_authority() }) .collect::>(); let locator = Actors::crossing_membership(actors[0]).expect("first locator"); - let first_contract = ActorContracts::::get(actors[0]).expect("first contract"); + let first_contract = Actors::load_actor_contract(actors[0]).expect("first contract"); let first_crossing = Actors::crossing_from_trigger(&first_contract.trigger).expect("first Crossing contract"); let movement_root = polkadot_sdk::sp_io::storage::root(StateVersion::V1); @@ -707,6 +1076,8 @@ fn crossing_source_prefix_snapshot_grants_only_contiguous_validated_authority() revision: 2, previous: 50, current: 150, + cause_provenance: crate::TriggerCauseProvenance::Deferred, + cause_block: 0, }; let homogeneous = Actors::preflight_crossing_cohort(&prefix, transition, false, None) .expect("homogeneous preflight"); @@ -770,10 +1141,10 @@ fn crossing_source_prefix_snapshot_grants_only_contiguous_validated_authority() "maximum placed prefix and aggregate FIFO authority must remain read-only" ); - let original_contract = ActorContracts::::get(actors[2]).expect("second prefix contract"); + let original_contract = Actors::load_actor_contract(actors[2]).expect("second prefix contract"); let mut wakeup_contract = original_contract.clone(); wakeup_contract.cooldown_blocks = 10; - ActorContracts::::insert(actors[2], wakeup_contract); + assert_ok!(Actors::store_actor_contract(actors[2], wakeup_contract)); let mut wakeup_hot = ActorHot::::get(actors[2]).expect("second prefix actor"); let original_wakeup_hot = wakeup_hot.clone(); wakeup_hot.last_cycle_block = Some(1); @@ -802,7 +1173,7 @@ fn crossing_source_prefix_snapshot_grants_only_contiguous_validated_authority() .expect("wakeup-only preflight"); assert_eq!(wakeup_preflight.admitted_candidates, 1); assert_eq!(wakeup_preflight.placed_immediate_fifo, Some(false)); - ActorContracts::::insert(actors[2], original_contract); + assert_ok!(Actors::store_actor_contract(actors[2], original_contract)); ActorHot::::insert(actors[2], original_wakeup_hot); let mut heterogeneous_hot = ActorHot::::get(actors[2]).expect("second prefix actor"); @@ -906,10 +1277,11 @@ fn crossing_tail_suffix_snapshot_grants_exact_generation_checked_refill_authorit ) }) .collect::>(); - ActorContracts::::mutate(actors[2], |maybe| { - maybe.as_mut().expect("split-destination contract").trigger = - RuntimeTrigger::observation_crossing(7, CrossingDirection::Rising, 100, 70); - }); + let mut split_contract = + Actors::load_actor_contract(actors[2]).expect("split-destination contract"); + split_contract.trigger = + RuntimeTrigger::observation_crossing(7, CrossingDirection::Rising, 100, 70); + assert_ok!(Actors::store_actor_contract(actors[2], split_contract)); assert_ok!(Actors::note_observation_transition( 7, crate::ObservationTransition { @@ -949,6 +1321,8 @@ fn crossing_tail_suffix_snapshot_grants_exact_generation_checked_refill_authorit revision: 2, previous: 50, current: 150, + cause_provenance: crate::TriggerCauseProvenance::Deferred, + cause_block: 0, }, ), Ok(4) @@ -1028,7 +1402,8 @@ fn crossing_tail_suffix_snapshot_grants_exact_generation_checked_refill_authorit .saturating_add(::crossing_work_probe()) .saturating_add(::crossing_fire_pair_probe()) .saturating_add(::crossing_tail_refill_probe()) - .saturating_add(non_tail_weight); + .saturating_add(non_tail_weight) + .saturating_add(::record_crossing_worker_fault()); Actors::service_crossing_transitions(production_budget); assert_eq!( Actors::test_first_crossing_branch_weight(), @@ -1197,7 +1572,8 @@ fn crossing_pair_downgrades_to_one_at_probe_weight_boundary() { let budget = ::WeightInfo::crossing_worker_base() .saturating_add(::WeightInfo::crossing_work_probe()) .saturating_add(::WeightInfo::crossing_fire_probe()) - .saturating_add(::WeightInfo::crossing_placed_unit()); + .saturating_add(::WeightInfo::crossing_placed_unit()) + .saturating_add(::WeightInfo::record_crossing_worker_fault()); let (_, counters) = Actors::service_crossing_transitions_resuming( budget, crate::crossing::CrossingWorkCounters::default(), @@ -1222,7 +1598,8 @@ fn crossing_pair_downgrades_to_one_at_branch_weight_boundary() { let budget = ::WeightInfo::crossing_worker_base() .saturating_add(::WeightInfo::crossing_work_probe()) .saturating_add(::WeightInfo::crossing_fire_pair_probe()) - .saturating_add(::WeightInfo::crossing_placed_unit()); + .saturating_add(::WeightInfo::crossing_placed_unit()) + .saturating_add(::WeightInfo::record_crossing_worker_fault()); let (_, counters) = Actors::service_crossing_transitions_resuming( budget, crate::crossing::CrossingWorkCounters::default(), @@ -1313,12 +1690,12 @@ fn crossing_worker_fires_then_rearms_in_revision_order_without_retrofire() { assert!(matches!( ActorHot::::get(actor_id).map(|hot| hot.trigger_runtime_state), Some(TriggerRuntimeState::ObservationCrossing { - phase: CrossingPhase::Armed, + phase: CrossingPhase::WaitingForRearm, .. }) )); - assert_eq!(locator.key.traversal, crate::CrossingTraversal::Upward); - assert_eq!(locator.key.threshold, 100); + assert_eq!(locator.key.traversal, crate::CrossingTraversal::Downward); + assert_eq!(locator.key.threshold, 80); assert!( ActorHot::::get(actor_id) .expect("hot state exists") @@ -1374,8 +1751,8 @@ fn falling_crossing_fires_and_rearms_without_duplicate_activation() { )); drain_crossing_work(); let rearmed = Actors::crossing_membership(actor_id).expect("membership remains"); - assert_eq!(crossing_phase(actor_id), CrossingPhase::Armed); - assert_eq!(rearmed.key.traversal, crate::CrossingTraversal::Downward); + assert_eq!(crossing_phase(actor_id), CrossingPhase::WaitingForRearm); + assert_eq!(rearmed.key.traversal, crate::CrossingTraversal::Upward); assert_eq!( ActorHot::::get(actor_id) .expect("hot state") @@ -1429,6 +1806,10 @@ fn paused_and_breaker_crossings_preserve_state_evolution_and_coalescing() { assert_eq!(crossing_phase(actor_id), CrossingPhase::WaitingForRearm); let first = ActorHot::::get(actor_id).expect("active Crossing actor"); assert!(first.pending_signal); + assert_eq!( + Actors::indexed_trigger_detection_disabled(actor_id), + Some(()) + ); let placement = (first.queue_ticket, first.wakeup_pointer); assert_ok!(Actors::note_observation_transition( @@ -1440,7 +1821,7 @@ fn paused_and_breaker_crossings_preserve_state_evolution_and_coalescing() { }, )); drain_crossing_work(); - assert_eq!(crossing_phase(actor_id), CrossingPhase::Armed); + assert_eq!(crossing_phase(actor_id), CrossingPhase::WaitingForRearm); let rearmed = ActorHot::::get(actor_id).expect("active Crossing actor"); assert!(rearmed.pending_signal); assert_eq!((rearmed.queue_ticket, rearmed.wakeup_pointer), placement); @@ -1476,11 +1857,12 @@ fn paused_and_breaker_crossings_preserve_state_evolution_and_coalescing() { assert_eq!( resumed.trigger_runtime_state, TriggerRuntimeState::ObservationCrossing { - phase: CrossingPhase::WaitingForRearm, + phase: CrossingPhase::Armed, installed_at_revision: 1, } ); assert!(!resumed.pending_signal); + assert_eq!(Actors::indexed_trigger_detection_disabled(actor_id), None); assert!(resumed.queue_ticket.is_none()); assert!(resumed.wakeup_pointer.is_none()); }); @@ -1498,7 +1880,7 @@ fn same_threshold_crossing_herd_spans_pages_without_loss_or_duplicate_ticket() { observed_at: 1, }, ); - let count = <::ObservationPageSize as Get>::get() + 1; + let count = <::CrossingPageSize as Get>::get() + 1; let mut actors = Vec::new(); for owner in 0..count { actors.push(create_system_with( @@ -1542,7 +1924,7 @@ fn crossing_compaction_rewinds_cursor_for_an_unprocessed_tail_member() { observed_at: 1, }, ); - let count = <::ObservationPageSize as Get>::get() + 1; + let count = <::CrossingPageSize as Get>::get() + 1; let actors = (0..count) .map(|owner| { create_system_with( @@ -1906,11 +2288,26 @@ fn crossing_scale_10k_zero_match_small_cohort_and_maximum_herd() { }, )); let rearm_herd_units = drain_crossing_work_with_limit(actor_count.saturating_mul(2)); - assert!(actors.iter().enumerate().all(|(index, actor_id)| { + for (index, actor_id) in actors.iter().enumerate() { let hot = ActorHot::::get(*actor_id).expect("herd actor remains active"); - crossing_phase(*actor_id) == CrossingPhase::Armed - && (hot.queue_ticket, hot.wakeup_pointer) == placements[index] - })); + assert_eq!( + ( + crossing_phase(*actor_id), + hot.queue_ticket, + hot.wakeup_pointer + ), + ( + if index < matched_count as usize { + CrossingPhase::Armed + } else { + CrossingPhase::WaitingForRearm + }, + placements[index].0, + placements[index].1 + ), + "rearm mismatch at {index}", + ); + } assert_ok!(Actors::note_observation_transition( 7, @@ -1928,7 +2325,8 @@ fn crossing_scale_10k_zero_match_small_cohort_and_maximum_herd() { })); assert_eq!( (initial_herd_units, rearm_herd_units, repeated_herd_units), - (10_002, 10_002, 10_002) + (10_002, 10_627, 10), + "latched members consume the rearm observation without re-enabling detection; only the eight explicitly replaced members remain armed for the repeated fire" ); }); } @@ -2152,11 +2550,16 @@ fn crossing_mixed_dense_sparse_directional_lifecycle_profile() { passes > 1, "mixed branch topology must span more than one component-limited pass" ); - assert_eq!( - frame_system::Pallet::::events().len(), - event_count_before_materialization, - "observation ingress and deferred Crossing materialization emit no lifecycle events" - ); + let events = frame_system::Pallet::::events(); + let materialization_events = &events[event_count_before_materialization..]; + assert_eq!(materialization_events.len(), 8); + assert!(materialization_events.iter().all(|record| matches!( + record.event, + RuntimeEvent::Actors(Event::TriggerOccurrenceProcessed { + trigger_family: TriggerFamily::ObservationCrossing, + .. + }) + ))); assert!( rising .into_iter() @@ -2164,15 +2567,11 @@ fn crossing_mixed_dense_sparse_directional_lifecycle_profile() { .chain(falling) .all(|actor_id| { ActorHot::::get(actor_id).is_some_and(|hot| hot.pending_signal) }) ); - assert!( - ActorHot::::get(insolvent) - .is_some_and(|hot| { hot.pending_signal && hot.queue_ticket.is_some() }) - ); - assert_ok!(Actors::permissionless_sweep( - RuntimeOrigin::signed(BOB), - insolvent - )); - assert!(ActorHot::::get(insolvent).is_none()); + let insolvent_hot = ActorHot::::get(insolvent).expect("insolvent Actor remains active"); + assert!(!insolvent_hot.pending_signal); + assert!(insolvent_hot.queue_ticket.is_none()); + assert_eq!(crossing_phase(insolvent), CrossingPhase::WaitingForRearm); + assert!(Actors::active_actor_view(insolvent).is_some()); }); } @@ -2319,7 +2718,7 @@ fn crossing_deactivation_and_close_remove_pending_members_without_stale_activati } #[test] -fn crossing_rearm_uses_its_generated_branch_without_activation_probe() { +fn latched_crossing_rearm_transition_uses_disabled_skip_without_activation_probe() { new_test_ext().execute_with(|| { frame_system::Pallet::::set_block_number(1); set_observation( @@ -2362,10 +2761,87 @@ fn crossing_rearm_uses_its_generated_branch_without_activation_probe() { assert_eq!(counters.activations, 0); assert_eq!(counters.closes, 0); assert_eq!(counters.faults, 0); - assert_eq!(crossing_phase(actor_id), CrossingPhase::Armed); + assert_eq!(crossing_phase(actor_id), CrossingPhase::WaitingForRearm); }); } +#[test] +fn crossing_compact_authority_mismatch_truncates_at_first_middle_and_final_boundaries() { + for mismatch_index in [0usize, 1, 3] { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + set_observation( + 7, + crate::ScalarObservationState::Fresh { + value: 50, + observed_at: 1, + }, + ); + let actors = [ALICE, BOB, CHARLIE, 44] + .into_iter() + .map(|owner| { + create_system_with( + owner, + Schedule { + trigger: RuntimeTrigger::observation_crossing(7, CrossingDirection::Rising, 100, 80), + cooldown_blocks: 0, + }, + None, + contract_steps_with_step(make_step(Task::StopCycle)), + ) + }) + .collect::>(); + let locator = Actors::crossing_membership(actors[0]).expect("first locator"); + let page = CrossingMemberPages::::get(locator.key, locator.page) + .expect("compact-authority source page"); + let snapshot = Actors::snapshot_crossing_source_prefix( + locator.key, + locator.page, + &page, + locator.offset, + 4, + ) + .expect("four-candidate compact snapshot"); + crate::ActorActivationAuthorities::::mutate( + actors[mismatch_index], + |maybe_authority| { + maybe_authority + .as_mut() + .expect("activation authority") + .admission_identity = [0xA5; 32]; + }, + ); + let root_before = polkadot_sdk::sp_io::storage::root(StateVersion::V1); + let preflight = Actors::preflight_crossing_cohort( + &snapshot, + crate::CrossingTransitionObligation { + revision: 2, + previous: 50, + current: 150, + cause_provenance: crate::TriggerCauseProvenance::Deferred, + cause_block: 1, + }, + true, + Some(crate::CrossingWorkPlan::FireCohortPlaced), + ) + .expect("mismatch must select an exact scalar fallback or compact prefix"); + assert_eq!( + preflight.admitted_candidates, + if mismatch_index == 0 { + 1 + } else { + mismatch_index as u32 + }, + ); + assert_eq!( + polkadot_sdk::sp_io::storage::root(StateVersion::V1), + root_before, + "preflight mismatch handling must remain read-only" + ); + }); + } +} + #[test] fn crossing_same_tail_page_places_two_candidates_in_one_admitted_cohort() { new_test_ext().execute_with(|| { @@ -2475,7 +2951,7 @@ fn crossing_same_tail_page_places_two_candidates_in_one_admitted_cohort() { ); assert_eq!( Actors::classify_crossing_work(), - crate::CrossingWorkPlan::RearmCohortPair + crate::CrossingWorkPlan::SkipPostInstallationPair ); let (_, rearm_counters) = Actors::service_crossing_transitions_with_counters(Weight::MAX); assert_eq!(rearm_counters.candidates, 2); @@ -2484,7 +2960,7 @@ fn crossing_same_tail_page_places_two_candidates_in_one_admitted_cohort() { assert_eq!(rearm_counters.closes, 0); assert_eq!(rearm_counters.faults, 0); for actor_id in [first, second] { - assert_eq!(crossing_phase(actor_id), CrossingPhase::Armed); + assert_eq!(crossing_phase(actor_id), CrossingPhase::WaitingForRearm); } }); } @@ -2699,7 +3175,84 @@ fn crossing_pair_preflight_fault_cannot_activate_its_valid_first_candidate() { assert_eq!(ActorHot::::get(first), Some(first_before)); assert_eq!(ActorHot::::get(second), Some(second_before)); assert_eq!(crate::QueueOccupancy::::get(), 0); - assert!(crate::CrossingWorkerFaultState::::exists()); + let fault = crate::CrossingWorkerFaultState::::get().expect("Crossing fault recorded"); + assert_eq!( + actor_event_count(|event| matches!( + event, + Event::ActorFaultRecorded { + fault_id: crate::FaultId::CrossingWorker, + kind: crate::ActorFaultKind::Detector, + first_recorded_block: 1, + context: crate::FaultContext::Crossing(recorded), + } if recorded == &fault + )), + 1 + ); + let _ = Actors::service_crossing_transitions_with_counters(Weight::MAX); + assert_eq!( + actor_event_count(|event| matches!( + event, + Event::ActorFaultRecorded { + fault_id: crate::FaultId::CrossingWorker, + .. + } + )), + 1, + "an uncleared Crossing fault emits only its first-recorded event" + ); + }); +} + +#[test] +fn crossing_fault_recording_admits_both_weight_dimensions_and_is_idempotent() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let fault = crate::CrossingWorkerFault { + feed: 7, + revision: Some(2), + threshold: Some(100), + class: crate::CrossingWorkerFaultClass::Invariant, + }; + let required = ::record_crossing_worker_fault(); + + let mut ref_time_short = polkadot_sdk::sp_weights::WeightMeter::with_limit(Weight::from_parts( + required.ref_time().saturating_sub(1), + u64::MAX, + )); + assert!(!Actors::record_crossing_worker_fault( + &mut ref_time_short, + fault + )); + assert!(crate::CrossingWorkerFaultState::::get().is_none()); + assert_eq!(System::events().len(), 0); + + let mut proof_short = polkadot_sdk::sp_weights::WeightMeter::with_limit(Weight::from_parts( + u64::MAX, + required.proof_size().saturating_sub(1), + )); + assert!(!Actors::record_crossing_worker_fault( + &mut proof_short, + fault + )); + assert!(crate::CrossingWorkerFaultState::::get().is_none()); + assert_eq!(System::events().len(), 0); + + let mut admitted = polkadot_sdk::sp_weights::WeightMeter::with_limit(required); + assert!(Actors::record_crossing_worker_fault(&mut admitted, fault)); + assert_eq!(admitted.consumed(), required); + let events_after_first = System::events(); + + let mut duplicate = polkadot_sdk::sp_weights::WeightMeter::with_limit(Weight::MAX); + assert!(!Actors::record_crossing_worker_fault( + &mut duplicate, + crate::CrossingWorkerFault { + class: crate::CrossingWorkerFaultClass::Other, + ..fault + }, + )); + assert_eq!(duplicate.consumed(), Weight::zero()); + assert_eq!(crate::CrossingWorkerFaultState::::get(), Some(fault)); + assert_eq!(System::events(), events_after_first); }); } @@ -2825,11 +3378,25 @@ fn crossing_seek_miss_uses_only_probe_and_transition_branch_weight() { Actors::classify_crossing_work(), crate::CrossingWorkPlan::SeekMiss ); - let budget = ::crossing_worker_base() - .saturating_add(::crossing_work_probe()) + let probe_expected = ::crossing_worker_base() + .saturating_add(::crossing_work_probe()); + let consumed_expected = probe_expected .saturating_add(::crossing_transition_unit()); + let root_before = polkadot_sdk::sp_io::storage::root(StateVersion::V1); + let (deferred, deferred_counters) = + Actors::service_crossing_transitions_with_counters(consumed_expected); + assert_eq!(deferred, probe_expected); + assert_eq!(deferred_counters, Default::default()); + assert_eq!( + polkadot_sdk::sp_io::storage::root(StateVersion::V1), + root_before, + "the branch must not mutate without its first-fault reserve" + ); + + let budget = consumed_expected + .saturating_add(::record_crossing_worker_fault()); let (consumed, counters) = Actors::service_crossing_transitions_with_counters(budget); - assert_eq!(consumed, budget); + assert_eq!(consumed, consumed_expected); assert_eq!( counters, crate::crossing::CrossingWorkCounters { @@ -3269,7 +3836,7 @@ fn crossing_try_state_rejects_canonical_locator_radix_and_pending_list_corruptio let identity = ActorIdentities::::get(actor_id).expect("actor identity exists"); let hot = ActorHot::::get(actor_id).expect("actor hot state exists"); - let contract = ActorContracts::::get(actor_id).expect("actor contract exists"); + let contract = Actors::load_actor_contract(actor_id).expect("actor contract exists"); let funding = ActorFunding::::get(actor_id).expect("actor funding exists"); ActorIdentities::::remove(actor_id); assert!(crate::Pallet::::do_try_state().is_err()); @@ -3277,9 +3844,9 @@ fn crossing_try_state_rejects_canonical_locator_radix_and_pending_list_corruptio ActorHot::::remove(actor_id); assert!(crate::Pallet::::do_try_state().is_err()); ActorHot::::insert(actor_id, hot.clone()); - ActorContracts::::remove(actor_id); + assert!(Actors::remove_admitted_contract_geometry(actor_id).is_some()); assert!(crate::Pallet::::do_try_state().is_err()); - ActorContracts::::insert(actor_id, contract); + assert_ok!(Actors::store_actor_contract(actor_id, contract)); ActorFunding::::remove(actor_id); assert!(crate::Pallet::::do_try_state().is_err()); ActorFunding::::insert(actor_id, funding); diff --git a/template/pallets/actors/src/tests/execution.rs b/template/pallets/actors/src/tests/execution.rs index 4e0fce07..877e5fca 100644 --- a/template/pallets/actors/src/tests/execution.rs +++ b/template/pallets/actors/src/tests/execution.rs @@ -1,5 +1,50 @@ use super::*; +#[test] +fn frozen_cycle_snapshot_dependency_is_closed_over_amounts_tasks_and_preconditions() { + assert!(!AmountResolution::Fixed(1u64).requires_frozen_cycle_snapshot()); + assert!( + !AmountResolution::::PercentageOfCurrent(Perbill::one()).requires_frozen_cycle_snapshot() + ); + assert!(!AmountResolution::::AllAvailable.requires_frozen_cycle_snapshot()); + assert!( + AmountResolution::::PercentageAtOpening(Perbill::one()).requires_frozen_cycle_snapshot() + ); + assert!( + AmountResolution::::PercentageOfLastFunding(Perbill::one()) + .requires_frozen_cycle_snapshot() + ); + + let mut step = make_step(Task::StopCycle); + assert!(!step.requires_frozen_cycle_snapshot()); + step.precondition = timed_all_conditions( + ObservationTiming::Opening, + vec![Predicate::BlockNumberAbove { threshold: 0 }], + ); + assert!(step.requires_frozen_cycle_snapshot()); + + let current = StepOf:: { + precondition: timed_all_conditions( + ObservationTiming::Current, + vec![Predicate::BlockNumberAbove { threshold: 0 }], + ), + task: Task::Transfer { + to: BOB, + asset: TestAsset::Native, + amount: AmountResolution::PercentageOfCurrent(Perbill::one()), + }, + on_error: StepErrorPolicy::AbortCycle, + }; + assert!(!current.requires_frozen_cycle_snapshot()); + + let opening_amount = make_step(Task::Transfer { + to: BOB, + asset: TestAsset::Native, + amount: AmountResolution::PercentageAtOpening(Perbill::one()), + }); + assert!(opening_amount.requires_frozen_cycle_snapshot()); +} + #[test] fn every_trigger_family_round_trips_dormant_and_active_lifecycle() { new_test_ext().execute_with(|| { @@ -61,7 +106,7 @@ fn every_trigger_family_round_trips_dormant_and_active_lifecycle() { LoadedActorStateOf::Dormant(_) )); assert!(Actors::actor_hot(actor_id).is_none()); - assert!(ActorContracts::::get(actor_id).is_none()); + assert!(Actors::load_actor_contract(actor_id).is_none()); assert!(ActorFunding::::get(actor_id).is_none()); assert!(Actors::crossing_membership(actor_id).is_none()); assert!(Actors::actor_observation_feeds(actor_id).is_none()); @@ -107,7 +152,7 @@ fn task_failure_defaults_unknown_errors_to_permanent() { } #[test] -fn continuation_schema_round_trips_retry_position_and_typed_snapshot_surfaces() { +fn actor_run_schema_round_trips_retry_position_and_typed_snapshot_surfaces() { new_test_ext().execute_with(|| { let mut contract_steps = BoundedVec::try_from(vec![ make_step(Task::Transfer { @@ -134,37 +179,52 @@ fn continuation_schema_round_trips_retry_position_and_typed_snapshot_surfaces() opening_snapshot .try_insert(OpeningSurface::StakingShares(TestAsset::Local(1)), 40) .expect("staking snapshot fits"); - let continuation = RuntimeContinuationState { + let run_state = RuntimeActorRunState { + contract_authority: run_contract_authority(actor_id), + cycle_nonce: 1, cursor: 0, + opening_predicate_cursor: 0, unsuccessful_attempts_at_cursor: 1, last_attempt_block: 1, + last_committed_step_block: None, + eligible_at: 2, opening_snapshot, opening_predicate_results: Default::default(), funding_snapshot: Default::default(), - cumulative_outcomes: OutcomeTotals { - executed_steps: 1, - ..Default::default() - }, + cumulative_outcomes: OutcomeTotals::default(), + last_step_outcome: Some(StepOutcome::FundingUnavailable), + suspension: Some(SuspensionReason::FundingUnavailable), }; - let encoded = continuation.encode(); - let decoded = - RuntimeContinuationState::decode(&mut &encoded[..]).expect("continuation decodes"); + let encoded = run_state.encode(); + let decoded = RuntimeActorRunState::decode(&mut &encoded[..]).expect("Actor run decodes"); + assert_eq!(decoded.cycle_nonce, 1); assert_eq!(decoded.cursor, 0); assert_eq!(decoded.unsuccessful_attempts_at_cursor, 1); assert_eq!(decoded.last_attempt_block, 1); + assert_eq!(decoded.eligible_at, 2); assert_eq!(decoded.opening_snapshot.len(), 2); - assert_eq!(decoded.cumulative_outcomes.executed_steps, 1); + assert_eq!( + decoded.last_step_outcome, + Some(StepOutcome::FundingUnavailable) + ); + assert_eq!( + decoded.suspension, + Some(SuspensionReason::FundingUnavailable) + ); - ContinuationStateStore::::insert(actor_id, continuation); + ActorRunStateStore::::insert(actor_id, run_state); ActorHot::::mutate(actor_id, |maybe| { maybe.as_mut().expect("active actor").cycle_state = CycleState::Suspended; }); - ActorIdentities::::mutate(actor_id, |maybe| { - maybe.as_mut().expect("actor identity").cycle_nonce = 1; - }); assert_eq!( - Actors::continuation_state(actor_id) - .expect("continuation exists") + ActorIdentities::::get(actor_id) + .expect("actor identity") + .cycle_nonce, + 0 + ); + assert_eq!( + Actors::actor_run_state(actor_id) + .expect("Actor run exists") .unsuccessful_attempts_at_cursor, 1 ); @@ -174,7 +234,7 @@ fn continuation_schema_round_trips_retry_position_and_typed_snapshot_surfaces() } #[test] -fn ordinary_one_attempt_run_keeps_continuation_sparse() { +fn ordinary_one_attempt_run_keeps_run_state_sparse() { new_test_ext().execute_with(|| { frame_system::Pallet::::set_block_number(1); let actor_id = create_system_with( @@ -189,7 +249,7 @@ fn ordinary_one_attempt_run_keeps_continuation_sparse() { .cycle_state, CycleState::Idle ); - assert!(Actors::continuation_state(actor_id).is_none()); + assert!(Actors::actor_run_state(actor_id).is_none()); fund_native(actor_id, 10); assert_ok!(Actors::manual_trigger( RuntimeOrigin::signed(ALICE), @@ -202,36 +262,222 @@ fn ordinary_one_attempt_run_keeps_continuation_sparse() { .cycle_state, CycleState::Idle ); - assert!(Actors::continuation_state(actor_id).is_none()); + assert!(Actors::actor_run_state(actor_id).is_none()); + }); +} + +#[test] +fn normal_running_progress_persists_one_causal_successor_and_rejects_stale_authority() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = create_system_with( + ALICE, + manual_schedule(), + None, + BoundedVec::try_from(vec![make_step(Task::StopCycle), make_step(Task::StopCycle)]) + .expect("two Steps fit"), + ); + let running = RuntimeActorRunState { + contract_authority: run_contract_authority(actor_id), + cycle_nonce: 1, + cursor: 1, + opening_predicate_cursor: 0, + unsuccessful_attempts_at_cursor: 0, + last_attempt_block: 1, + last_committed_step_block: Some(1), + eligible_at: 2, + opening_snapshot: Default::default(), + opening_predicate_results: Default::default(), + funding_snapshot: Default::default(), + cumulative_outcomes: OutcomeTotals { + executed_steps: 1, + committed_effectful_tasks: 0, + ..Default::default() + }, + last_step_outcome: Some(StepOutcome::Stopped), + suspension: None, + }; + assert_ok!(Actors::persist_run_progress(actor_id, running.clone())); + let stored = ActorRunStateStore::::get(actor_id).expect("Running state persists"); + assert_eq!(stored.cycle_nonce, running.cycle_nonce); + assert_eq!(stored.cursor, running.cursor); + assert_eq!( + stored.last_committed_step_block, + running.last_committed_step_block + ); + assert_eq!(stored.eligible_at, running.eligible_at); + assert_eq!(stored.cumulative_outcomes, running.cumulative_outcomes); + assert_eq!( + ActorHot::::get(actor_id).map(|hot| hot.cycle_state), + Some(CycleState::Running) + ); + assert!(matches!( + Actors::load_actor_state(actor_id), + crate::LoadedActorStateOf::Active(_) + )); + ActorRunStateStore::::mutate(actor_id, |maybe| { + maybe + .as_mut() + .expect("Running state exists") + .contract_authority + .admission_identity[0] ^= 1; + }); + assert!(matches!( + Actors::load_actor_state(actor_id), + crate::LoadedActorStateOf::Corrupt + )); }); } #[test] -fn continuation_attempt_missing_state_fails_without_panicking_or_mutating() { +fn running_state_carries_the_exact_opening_predicate_cursor() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = create_system_with( + ALICE, + manual_schedule(), + None, + BoundedVec::try_from(vec![ + StepOf:: { + precondition: timed_all_conditions( + ObservationTiming::Opening, + vec![Predicate::BlockNumberAbove { threshold: 10 }], + ), + task: Task::StopCycle, + on_error: StepErrorPolicy::AbortCycle, + }, + StepOf:: { + precondition: timed_all_conditions( + ObservationTiming::Opening, + vec![Predicate::BlockNumberAbove { threshold: 0 }], + ), + task: Task::Transfer { + to: BOB, + asset: TestAsset::Native, + amount: AmountResolution::Fixed(5), + }, + on_error: StepErrorPolicy::AbortCycle, + }, + ]) + .expect("two Steps fit"), + ); + fund_native(actor_id, 10); + let before = asset_balance(&BOB, TestAsset::Native); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + Actors::on_idle(1, Weight::MAX); + let run = Actors::actor_run_state(actor_id).expect("Running state persists"); + assert_eq!(run.cursor, 1); + assert_eq!(run.opening_predicate_cursor, 1); + assert_eq!( + run.opening_predicate_results.as_slice(), + [Ok(false), Ok(true)] + ); + + frame_system::Pallet::::set_block_number(2); + Actors::on_initialize(2); + Actors::on_idle(2, Weight::MAX); + assert_eq!(asset_balance(&BOB, TestAsset::Native), before + 5); + assert!(Actors::actor_run_state(actor_id).is_none()); + assert_eq!( + Actors::active_actor_view(actor_id).map(|actor| actor.cycle_nonce), + Some(1) + ); + }); +} + +#[test] +fn signal_during_running_is_retained_for_a_later_cycle() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let mut steps = inert_contract_steps(); + steps + .try_push(make_step(Task::StopCycle)) + .expect("second Step fits"); + let actor_id = create_system_with(ALICE, manual_schedule(), None, steps); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + Actors::on_idle(1, Weight::MAX); + assert_eq!( + ActorHot::::get(actor_id).map(|hot| hot.cycle_state), + Some(CycleState::Running) + ); + assert!(!Actors::pending_signal(actor_id)); + + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + assert!(Actors::pending_signal(actor_id)); + Actors::on_idle(1, Weight::MAX); + assert_eq!( + ActorHot::::get(actor_id).map(|hot| hot.cycle_state), + Some(CycleState::Running), + "the successor cannot execute in the commit block" + ); + + frame_system::Pallet::::set_block_number(2); + Actors::on_initialize(2); + Actors::on_idle(2, Weight::MAX); + let after_first_cycle = Actors::active_actor_view(actor_id).expect("Actor remains active"); + assert_eq!(after_first_cycle.cycle_nonce, 1); + assert_eq!(after_first_cycle.cycle_state, CycleState::Idle); + assert!(after_first_cycle.pending_signal); + + Actors::on_idle(2, Weight::MAX); + assert_eq!( + Actors::active_actor_view(actor_id).map(|actor| actor.cycle_nonce), + Some(1), + "the retained signal cannot open another cycle in the completion block" + ); + frame_system::Pallet::::set_block_number(3); + Actors::on_initialize(3); + Actors::on_idle(3, Weight::MAX); + let next_cycle = Actors::active_actor_view(actor_id).expect("next Cycle remains active"); + assert_eq!(next_cycle.cycle_nonce, 1); + assert_eq!(next_cycle.cycle_state, CycleState::Running); + assert!(!next_cycle.pending_signal); + assert_eq!( + Actors::actor_run_state(actor_id).map(|run| run.cycle_nonce), + Some(2) + ); + }); +} + +#[test] +fn run_attempt_missing_state_fails_without_panicking_or_mutating() { new_test_ext().execute_with(|| { let mut plan = transfer_contract_steps(BOB, 1); plan[0].on_error = RETRY_LATER; let actor_id = create_system_with(ALICE, manual_schedule(), None, plan); - ContinuationStateStore::::insert( + ActorRunStateStore::::insert( actor_id, - RuntimeContinuationState { + RuntimeActorRunState { + contract_authority: run_contract_authority(actor_id), + cycle_nonce: 1, cursor: 0, + opening_predicate_cursor: 0, unsuccessful_attempts_at_cursor: 1, last_attempt_block: 1, + last_committed_step_block: None, + eligible_at: 2, opening_snapshot: Default::default(), opening_predicate_results: Default::default(), funding_snapshot: Default::default(), cumulative_outcomes: Default::default(), + last_step_outcome: Some(StepOutcome::FundingUnavailable), + suspension: Some(SuspensionReason::FundingUnavailable), }, ); ActorHot::::mutate(actor_id, |maybe| { maybe.as_mut().expect("actor exists").cycle_state = CycleState::Suspended; }); - ActorIdentities::::mutate(actor_id, |maybe| { - maybe.as_mut().expect("identity exists").cycle_nonce = 1; - }); - let instance = Actors::active_actor_view(actor_id).expect("coherent continuation exists"); - ContinuationStateStore::::remove(actor_id); + let instance = Actors::active_actor_view(actor_id).expect("coherent Actor run exists"); + ActorRunStateStore::::remove(actor_id); let hot_before = Actors::actor_hot(actor_id).expect("hot state remains"); System::reset_events(); @@ -244,118 +490,142 @@ fn continuation_attempt_missing_state_fails_without_panicking_or_mutating() { #[cfg(feature = "try-runtime")] #[test] -fn continuation_try_state_rejects_marker_and_cursor_drift() { +fn actor_run_try_state_rejects_marker_and_cursor_drift() { new_test_ext().execute_with(|| { let mut plan = transfer_contract_steps(BOB, 1); plan[0].on_error = RETRY_LATER; let actor_id = create_system_with(ALICE, manual_schedule(), None, plan); - ContinuationStateStore::::insert( + ActorRunStateStore::::insert( actor_id, - RuntimeContinuationState { + RuntimeActorRunState { + contract_authority: run_contract_authority(actor_id), + cycle_nonce: 1, cursor: 0, + opening_predicate_cursor: 0, unsuccessful_attempts_at_cursor: 1, last_attempt_block: 1, + last_committed_step_block: None, + eligible_at: 2, opening_snapshot: Default::default(), opening_predicate_results: Default::default(), funding_snapshot: Default::default(), cumulative_outcomes: Default::default(), + last_step_outcome: Some(StepOutcome::FundingUnavailable), + suspension: Some(SuspensionReason::FundingUnavailable), }, ); assert!(crate::Pallet::::do_try_state().is_err()); ActorHot::::mutate(actor_id, |maybe| { maybe.as_mut().expect("actor exists").cycle_state = CycleState::Suspended; }); - ActorIdentities::::mutate(actor_id, |maybe| { - maybe.as_mut().expect("identity exists").cycle_nonce = 1; + assert_ok!(crate::Pallet::::do_try_state()); + ActorRunStateStore::::mutate(actor_id, |maybe| { + maybe.as_mut().expect("Actor run exists").suspension = Some(SuspensionReason::Temporary); + }); + assert!(crate::Pallet::::do_try_state().is_err()); + ActorRunStateStore::::mutate(actor_id, |maybe| { + let run_state = maybe.as_mut().expect("Actor run exists"); + run_state.suspension = Some(SuspensionReason::FundingUnavailable); + run_state.eligible_at = 3; + }); + assert!(crate::Pallet::::do_try_state().is_err()); + ActorRunStateStore::::mutate(actor_id, |maybe| { + maybe.as_mut().expect("Actor run exists").eligible_at = 2; }); assert_ok!(crate::Pallet::::do_try_state()); - ContinuationStateStore::::mutate(actor_id, |maybe| { + ActorRunStateStore::::mutate(actor_id, |maybe| { maybe .as_mut() - .expect("continuation exists") + .expect("Actor run exists") .opening_snapshot .try_insert(OpeningSurface::PreservableAsset(TestAsset::Native), 10) .expect("snapshot entry fits"); }); assert!(crate::Pallet::::do_try_state().is_err()); - ContinuationStateStore::::mutate(actor_id, |maybe| { + ActorRunStateStore::::mutate(actor_id, |maybe| { maybe .as_mut() - .expect("continuation exists") + .expect("Actor run exists") .opening_snapshot .clear(); }); - ContinuationStateStore::::mutate(actor_id, |maybe| { - maybe.as_mut().expect("continuation exists").cursor = 1; + ActorRunStateStore::::mutate(actor_id, |maybe| { + maybe.as_mut().expect("Actor run exists").cursor = 1; }); assert!(crate::Pallet::::do_try_state().is_err()); - ContinuationStateStore::::remove(actor_id); + ActorRunStateStore::::remove(actor_id); assert!(crate::Pallet::::do_try_state().is_err()); }); } #[test] -fn user_dormant_fund_then_activate_is_the_unfunded_lifecycle() { +fn user_dormant_activation_installs_process_without_service_prefunding() { new_test_ext().execute_with(|| { frame_system::Pallet::::set_block_number(1); - // The unfunded lifecycle is create Dormant -> fund the deterministic sovereign -> - // activate; activation of an unfunded sovereign fails InsufficientBalance. assert_ok!(Actors::create_user_actor( RuntimeOrigin::signed(ALICE), Mutability::Mutable, None, )); let actor_id = Actors::next_actor_id() - 1; - let plan = transfer_contract_steps(BOB, 1); + let sovereign = Actors::sovereign_account_id(&ALICE, 0); + assert_eq!(native_balance(&sovereign), 0); frame_system::Pallet::::set_block_number(2); - assert_noop!( - Actors::activate_actor( - RuntimeOrigin::signed(ALICE), - actor_id, - user_active_contract(manual_schedule(), None, plan.clone()).expect("direct Actor Contract"), - ), - Error::::InsufficientBalance - ); - assert!(Actors::actor_identities(actor_id).is_some()); - assert!(Actors::active_actor_view(actor_id).is_none()); - assert_eq!(Actors::active_actor_count(), 0); - // Funding the deterministic sovereign account admits activation. - fund_native_raw( - &Actors::sovereign_account_id(&ALICE, 0), - user_prefunding_requirement(&plan), - ); assert_ok!(Actors::activate_actor( RuntimeOrigin::signed(ALICE), actor_id, - user_active_contract(manual_schedule(), None, plan).expect("direct Actor Contract"), + user_active_contract(manual_schedule(), None, transfer_contract_steps(BOB, 1)) + .expect("direct Actor Contract"), )); + + assert_eq!(native_balance(&sovereign), 0); assert!(Actors::active_actor_view(actor_id).is_some()); assert_eq!(Actors::active_actor_count(), 1); }); } #[test] -fn generated_continuation_weights_cover_distinct_storage_paths() { +fn suspension_admission_uses_the_complete_persisted_run_weight_at_a_later_cursor() { + new_test_ext().execute_with(|| { + let mut retry_step = make_step(Task::StopCycle); + retry_step.on_error = StepErrorPolicy::RetryLater { max_attempts: 2 }; + let steps = BoundedVec::try_from(vec![make_step(Task::StopCycle), retry_step]) + .expect("two steps fit the test Contract"); + let transition = ::run_suspend() + .max(::run_complete()) + .max(::run_cancel()); + let expected = ::step_orchestration(2) + .saturating_add(::task_stop_cycle()) + .saturating_add(transition); + + assert_eq!( + Actors::compute_cycle_weight_upper_from(ActorType::System, &steps, 1), + expected, + "later-cursor admission must retain the full persisted run-state envelope" + ); + }); +} + +#[test] +fn generated_run_weights_cover_distinct_storage_paths() { new_test_ext().execute_with(|| { - let suspend_min = ::continuation_suspend(0); - let suspend_max = ::continuation_suspend(20); - assert_eq!(suspend_min, Weight::from_parts(27_920_348, 4_178)); - assert_eq!(suspend_max, Weight::from_parts(28_668_868, 4_178)); + let suspend = ::run_suspend(); + assert_eq!(suspend, Weight::from_parts(28_668_868, 4_178)); assert_eq!( - ::continuation_retry(), + ::run_retry(), Weight::from_parts(22_070_000, 4_266) ); assert_eq!( - ::continuation_complete(), + ::run_complete(), Weight::from_parts(18_019_000, 4_030) ); assert_eq!( - ::continuation_cancel(), + ::run_cancel(), Weight::from_parts(56_782_000, 8_120) ); - let suffix_min = ::continuation_suffix_admission(1); - let suffix_max = ::continuation_suffix_admission(10); + let suffix_min = ::run_suffix_admission(1); + let suffix_max = ::run_suffix_admission(10); assert_eq!(suffix_min, Weight::from_parts(1_439_006, 0)); assert_eq!(suffix_max, Weight::from_parts(1_442_894, 0)); assert!(suffix_min.ref_time() < suffix_max.ref_time()); @@ -495,7 +765,7 @@ fn authored_abort_preserves_earlier_provisional_task_commit() { } #[test] -fn continuation_control_placement_failures_roll_back_exactly() { +fn run_control_placement_failures_roll_back_exactly() { new_test_ext().execute_with(|| { let actor_id = create_suspended_system_retry(1); assert_ok!(Actors::manual_trigger( @@ -514,7 +784,7 @@ fn continuation_control_placement_failures_roll_back_exactly() { ), Error::::QueueTicketExhausted ); - assert!(Actors::continuation_state(actor_id).is_some()); + assert!(Actors::actor_run_state(actor_id).is_some()); assert_eq!(System::events(), events_before); assert_eq!( polkadot_sdk::sp_io::storage::root(StateVersion::V1), @@ -541,7 +811,7 @@ fn continuation_control_placement_failures_roll_back_exactly() { ), Error::::QueueTicketExhausted ); - assert!(Actors::continuation_state(actor_id).is_some()); + assert!(Actors::actor_run_state(actor_id).is_some()); assert_eq!(System::events(), events_before); assert_eq!( polkadot_sdk::sp_io::storage::root(StateVersion::V1), @@ -560,10 +830,10 @@ fn continuation_control_placement_failures_roll_back_exactly() { let events_before = System::events(); assert_noop!( - Actors::cancel_continuation(RuntimeOrigin::root(), actor_id), + Actors::cancel_run(RuntimeOrigin::root(), actor_id), Error::::QueueTicketExhausted ); - assert!(Actors::continuation_state(actor_id).is_some()); + assert!(Actors::actor_run_state(actor_id).is_some()); assert_eq!(System::events(), events_before); assert_eq!( polkadot_sdk::sp_io::storage::root(StateVersion::V1), @@ -989,7 +1259,7 @@ fn retry_later_aborts_permanent_failure_without_executing_suffix() { assert_eq!(instance.cycle_nonce, 1); assert_eq!(instance.unsuccessful_attempt_streak, 1); assert_eq!(instance.cycle_state, CycleState::Idle); - assert!(Actors::continuation_state(actor_id).is_none()); + assert!(Actors::actor_run_state(actor_id).is_none()); assert_eq!(native_balance(&CHARLIE), charlie_before); assert!(has_actor_event(|event| { matches!( @@ -1047,12 +1317,26 @@ fn retry_later_resumes_same_cursor_without_replaying_committed_prefix() { run_idle(Weight::MAX); let first = Actors::active_actor_view(actor_id).expect("suspended actor remains"); - let first_continuation = Actors::continuation_state(actor_id).expect("continuation exists"); + let first_continuation = Actors::actor_run_state(actor_id).expect("Actor run exists"); assert_eq!(first.cycle_state, CycleState::Suspended); - assert_eq!(first.cycle_nonce, 1); + assert_eq!(first.cycle_nonce, 0); assert_eq!(first.unsuccessful_attempt_streak, 1); + assert_eq!(first_continuation.cycle_nonce, 1); assert_eq!(first_continuation.cursor, 1); assert_eq!(first_continuation.unsuccessful_attempts_at_cursor, 1); + assert_eq!(first_continuation.last_attempt_block, 2); + assert_eq!(first_continuation.eligible_at, 3); + assert!(matches!( + first_continuation.last_step_outcome, + Some(StepOutcome::Failed(TaskFailure { + retry: RetryClass::Temporary, + .. + })) + )); + assert_eq!( + first_continuation.suspension, + Some(SuspensionReason::Temporary) + ); assert_eq!(first_continuation.cumulative_outcomes.executed_steps, 1); assert_eq!(first_continuation.cumulative_outcomes.failed_steps, 1); assert_eq!(native_balance(&BOB), bob_before + 10); @@ -1064,25 +1348,32 @@ fn retry_later_resumes_same_cursor_without_replaying_committed_prefix() { matches!(event, Event::CycleSummary { actor_id: id, .. } if *id == actor_id) })); - frame_system::Pallet::::set_block_number(2); + frame_system::Pallet::::set_block_number(first_continuation.eligible_at); run_idle(Weight::MAX); - let second_continuation = Actors::continuation_state(actor_id).expect("continuation remains"); + run_next_idle(Weight::MAX); + let second_continuation = Actors::actor_run_state(actor_id).expect("Actor run remains"); + assert_eq!(second_continuation.cycle_nonce, 1); assert_eq!(second_continuation.cursor, 1); assert_eq!(second_continuation.unsuccessful_attempts_at_cursor, 2); + assert_eq!(second_continuation.last_attempt_block, 4); + assert_eq!(second_continuation.eligible_at, 6); assert_eq!(second_continuation.cumulative_outcomes.executed_steps, 1); assert_eq!(second_continuation.cumulative_outcomes.failed_steps, 2); assert_eq!(native_balance(&BOB), bob_before + 10); assert_eq!(native_balance(&CHARLIE), charlie_before); set_temporary_dex_failure(false); - frame_system::Pallet::::set_block_number(4); + frame_system::Pallet::::set_block_number(second_continuation.eligible_at); + Actors::on_initialize(second_continuation.eligible_at); + run_prepass(); run_idle(Weight::MAX); + run_next_idle(Weight::MAX); let completed = Actors::active_actor_view(actor_id).expect("completed actor remains"); assert_eq!(completed.cycle_state, CycleState::Idle); assert_eq!(completed.cycle_nonce, 1); assert_eq!(completed.unsuccessful_attempt_streak, 0); - assert!(Actors::continuation_state(actor_id).is_none()); + assert!(Actors::actor_run_state(actor_id).is_none()); assert_eq!(native_balance(&BOB), bob_before + 10); assert_eq!(native_balance(&CHARLIE), charlie_before + 5); let starts = frame_system::Pallet::::events() @@ -1146,7 +1437,7 @@ fn stop_cycle_commits_prefix_and_completes_before_unreachable_suffix() { assert_eq!(actor.cycle_state, CycleState::Idle); assert_eq!(actor.cycle_nonce, 1); assert_eq!(actor.unsuccessful_attempt_streak, 0); - assert!(Actors::continuation_state(actor_id).is_none()); + assert!(Actors::actor_run_state(actor_id).is_none()); assert_eq!(native_balance(&BOB), bob_before + 10); assert_eq!(native_balance(&CHARLIE), charlie_before); assert!(has_actor_event(|event| matches!( @@ -1352,7 +1643,7 @@ fn close_after_productive_cycle_waits_for_retry_completion() { assert_ok!(Actors::manual_trigger(RuntimeOrigin::root(), actor_id)); run_idle(Weight::MAX); - assert!(Actors::continuation_state(actor_id).is_some()); + assert!(Actors::actor_run_state(actor_id).is_some()); assert!(Actors::active_actor_view(actor_id).is_some()); assert!(!has_actor_event(|event| matches!( event, @@ -1366,7 +1657,7 @@ fn close_after_productive_cycle_waits_for_retry_completion() { frame_system::Pallet::::set_block_number(2); run_idle(Weight::MAX); - assert!(Actors::continuation_state(actor_id).is_none()); + assert!(Actors::actor_run_state(actor_id).is_none()); assert!(Actors::active_actor_view(actor_id).is_none()); assert!(has_actor_event(|event| matches!( event, @@ -1412,7 +1703,7 @@ fn close_after_productive_cycle_keeps_retry_exhaustion_as_failure_terminal() { assert_ok!(Actors::manual_trigger(RuntimeOrigin::root(), actor_id)); run_idle(Weight::MAX); - assert!(Actors::continuation_state(actor_id).is_some()); + assert!(Actors::actor_run_state(actor_id).is_some()); frame_system::Pallet::::set_block_number(2); run_idle(Weight::MAX); @@ -1476,6 +1767,11 @@ fn stop_cycle_runs_normal_auto_close_after_the_summary() { .position(|event| matches!(event, Event::ActorClosed { actor_id: id, reason: CloseReason::AutoCloseNonceReached } if *id == actor_id)) .expect("auto-close event"); assert!(stopped < summary && summary < closed); + assert_eq!( + last_step_control_execution().map(|execution| execution.outcome), + Some(crate::StepControlOutcome::Completed), + "auto-close retains completed control ownership while cleanup is metered separately", + ); }); } @@ -1521,21 +1817,25 @@ fn continuation_can_complete_at_stop_cycle_without_replaying_prefix() { )); run_idle(Weight::MAX); assert_eq!( - Actors::continuation_state(actor_id) - .expect("suspended") - .cursor, + Actors::actor_run_state(actor_id).expect("suspended").cursor, 1 ); assert_eq!(native_balance(&BOB), bob_before + 10); set_temporary_dex_failure(false); - frame_system::Pallet::::set_block_number(2); + let eligible_at = Actors::actor_run_state(actor_id) + .expect("suspension remains") + .eligible_at; + frame_system::Pallet::::set_block_number(eligible_at); + Actors::on_initialize(eligible_at); + run_prepass(); run_idle(Weight::MAX); + run_next_idle(Weight::MAX); let actor = Actors::active_actor_view(actor_id).expect("actor remains active"); assert_eq!(actor.cycle_state, CycleState::Idle); assert_eq!(actor.unsuccessful_attempt_streak, 0); - assert!(Actors::continuation_state(actor_id).is_none()); + assert!(Actors::actor_run_state(actor_id).is_none()); assert_eq!(native_balance(&BOB), bob_before + 10); assert_eq!(native_balance(&CHARLIE), charlie_before); assert!(has_actor_event(|event| matches!( @@ -1600,7 +1900,7 @@ fn temporary_failure_keeps_continue_next_step_semantics() { let completed = Actors::active_actor_view(actor_id).expect("actor remains"); assert_eq!(completed.cycle_state, CycleState::Idle); assert_eq!(completed.unsuccessful_attempt_streak, 0); - assert!(Actors::continuation_state(actor_id).is_none()); + assert!(Actors::actor_run_state(actor_id).is_none()); assert_eq!(native_balance(&CHARLIE), charlie_before + 1); assert!(has_actor_event(|event| { matches!( @@ -1655,16 +1955,20 @@ fn retry_later_local_attempt_cutoff_closes_without_prefix_replay() { )); run_idle(Weight::MAX); assert_eq!( - Actors::continuation_state(actor_id) + Actors::actor_run_state(actor_id) .expect("first unsuccessful attempt persists") .unsuccessful_attempts_at_cursor, 1 ); - frame_system::Pallet::::set_block_number(2); + let first_eligible_at = Actors::actor_run_state(actor_id) + .expect("first unsuccessful attempt persists") + .eligible_at; + frame_system::Pallet::::set_block_number(first_eligible_at); run_idle(Weight::MAX); + run_next_idle(Weight::MAX); assert_eq!( - Actors::continuation_state(actor_id) + Actors::actor_run_state(actor_id) .expect("second unsuccessful attempt persists") .unsuccessful_attempts_at_cursor, 2 @@ -1679,11 +1983,17 @@ fn retry_later_local_attempt_cutoff_closes_without_prefix_replay() { .pending_signal ); - frame_system::Pallet::::set_block_number(4); + let second_eligible_at = Actors::actor_run_state(actor_id) + .expect("second unsuccessful attempt persists") + .eligible_at; + frame_system::Pallet::::set_block_number(second_eligible_at); + Actors::on_initialize(second_eligible_at); + run_prepass(); run_idle(Weight::MAX); + run_next_idle(Weight::MAX); assert!(Actors::active_actor_view(actor_id).is_none()); - assert!(Actors::continuation_state(actor_id).is_none()); + assert!(Actors::actor_run_state(actor_id).is_none()); assert_eq!(native_balance(&BOB), bob_before + 1); assert!(has_actor_event(|event| { matches!( @@ -1738,7 +2048,7 @@ fn retry_later_resets_local_attempt_count_after_cursor_advancement() { actor_id )); run_idle(Weight::MAX); - let first = Actors::continuation_state(actor_id).expect("first cursor suspends"); + let first = Actors::actor_run_state(actor_id).expect("first cursor suspends"); assert_eq!( (first.cursor, first.unsuccessful_attempts_at_cursor), (0, 1) @@ -1746,9 +2056,9 @@ fn retry_later_resets_local_attempt_count_after_cursor_advancement() { set_temporary_dex_failure(false); set_temporary_add_liquidity_failure(true); - frame_system::Pallet::::set_block_number(2); + frame_system::Pallet::::set_block_number(first.eligible_at); run_idle(Weight::MAX); - let advanced = Actors::continuation_state(actor_id).expect("later cursor suspends"); + let advanced = Actors::actor_run_state(actor_id).expect("later cursor suspends"); assert_eq!(advanced.cursor, 1); assert_eq!(advanced.unsuccessful_attempts_at_cursor, 1); }); @@ -1781,7 +2091,7 @@ fn global_failure_limit_can_close_before_retry_later_local_limit() { run_idle(Weight::MAX); assert!(Actors::active_actor_view(actor_id).is_none()); - assert!(Actors::continuation_state(actor_id).is_none()); + assert!(Actors::actor_run_state(actor_id).is_none()); assert!(!has_actor_event(|event| matches!( event, Event::CycleCancelled { actor_id: id, .. } if *id == actor_id @@ -1797,6 +2107,11 @@ fn global_failure_limit_can_close_before_retry_later_local_limit() { reason: CloseReason::ConsecutiveFailures, } if *id == actor_id ))); + assert_eq!( + last_step_control_execution().map(|execution| execution.outcome), + Some(crate::StepControlOutcome::Failed), + "failure close retains failed control ownership while cleanup is metered separately", + ); }); } @@ -1897,7 +2212,7 @@ fn temporary_retry_backoff_is_one_two_four_eight_then_capped() { assert!(initial_hot.queue_ticket.is_some()); assert!(initial_hot.wakeup_pointer.is_none()); assert_eq!( - Actors::continuation_state(actor_id) + Actors::actor_run_state(actor_id) .expect("attempt zero") .cursor, 0 @@ -1908,15 +2223,14 @@ fn temporary_retry_backoff_is_one_two_four_eight_then_capped() { frame_system::Pallet::::set_block_number(due - 1); run_idle(Weight::MAX); assert_eq!( - Actors::continuation_state(actor_id) + Actors::actor_run_state(actor_id) .expect("not eligible before due block") .unsuccessful_attempts_at_cursor, expected_attempt ); frame_system::Pallet::::set_block_number(due); run_idle(Weight::MAX); - let continuation = - Actors::continuation_state(actor_id).expect("temporary failure resuspends"); + let continuation = Actors::actor_run_state(actor_id).expect("temporary failure resuspends"); assert_eq!( continuation.unsuccessful_attempts_at_cursor, expected_attempt + 1 @@ -1929,7 +2243,7 @@ fn temporary_retry_backoff_is_one_two_four_eight_then_capped() { frame_system::Pallet::::set_block_number(23); run_idle(Weight::MAX); assert_eq!( - Actors::continuation_state(actor_id) + Actors::actor_run_state(actor_id) .expect("capped delay holds") .unsuccessful_attempts_at_cursor, 5 @@ -1937,7 +2251,7 @@ fn temporary_retry_backoff_is_one_two_four_eight_then_capped() { set_temporary_dex_failure(false); frame_system::Pallet::::set_block_number(24); run_idle(Weight::MAX); - assert!(Actors::continuation_state(actor_id).is_none()); + assert!(Actors::actor_run_state(actor_id).is_none()); assert_eq!(native_balance(&actor), balance_before - 10); assert_eq!( Actors::active_actor_view(actor_id) @@ -1949,7 +2263,7 @@ fn temporary_retry_backoff_is_one_two_four_eight_then_capped() { } #[test] -fn continuation_weight_deferral_does_not_admit_attempt() { +fn run_weight_deferral_does_not_admit_attempt() { new_test_ext().execute_with(|| { frame_system::Pallet::::set_block_number(1); setup_temporary_retry_pool(); @@ -1963,7 +2277,7 @@ fn continuation_weight_deferral_does_not_admit_attempt() { set_temporary_dex_failure(true); assert_ok!(Actors::manual_trigger(RuntimeOrigin::signed(ALICE), actor_id)); run_idle(Weight::MAX); - let before = Actors::continuation_state(actor_id).expect("suspended"); + let before = Actors::actor_run_state(actor_id).expect("suspended"); let ticket_before = Actors::actor_hot(actor_id) .expect("suspended actor") .queue_ticket; @@ -1982,7 +2296,7 @@ fn continuation_weight_deferral_does_not_admit_attempt() { .saturating_sub(1); Actors::execute_cycle(Weight::from_parts(u64::MAX, proof_limit)); - let after = Actors::continuation_state(actor_id).expect("deferral preserves continuation"); + let after = Actors::actor_run_state(actor_id).expect("deferral preserves Actor run"); assert_eq!( after.unsuccessful_attempts_at_cursor, before.unsuccessful_attempts_at_cursor @@ -2035,7 +2349,7 @@ fn suspended_retry_wakes_for_window_expiry_before_cooldown() { frame_system::Pallet::::set_block_number(102); run_idle(Weight::MAX); assert!(Actors::active_actor_view(actor_id).is_none()); - assert!(Actors::continuation_state(actor_id).is_none()); + assert!(Actors::actor_run_state(actor_id).is_none()); assert!(has_actor_event(|event| { matches!( event, @@ -2049,7 +2363,7 @@ fn suspended_retry_wakes_for_window_expiry_before_cooldown() { } #[test] -fn user_retry_admits_and_charges_only_the_unresolved_suffix() { +fn user_retry_admits_and_charges_only_each_current_step() { new_test_ext().execute_with(|| { frame_system::Pallet::::set_block_number(1); setup_temporary_retry_pool(); @@ -2079,10 +2393,10 @@ fn user_retry_admits_and_charges_only_the_unresolved_suffix() { let prefix_fee = user_step_fee(&plan[0]); let retry_fee = user_step_fee(&plan[1]); let tail_fee = user_step_fee(&plan[2]); + let pipeline_fee = pipeline_opening_fee(&plan); let actor_id = create_user_with(ALICE, Mutability::Mutable, manual_schedule(), None, plan); fund_native(actor_id, 1_000_000_000_000_000_000); let sink = TestFeeSink::get(); - let sink_before = native_balance(&sink); let bob_before = native_balance(&BOB); let charlie_before = native_balance(&CHARLIE); set_temporary_dex_failure(true); @@ -2091,13 +2405,15 @@ fn user_retry_admits_and_charges_only_the_unresolved_suffix() { RuntimeOrigin::signed(ALICE), actor_id )); + let sink_before = native_balance(&sink); run_idle(Weight::MAX); - assert_eq!(native_balance(&sink) - sink_before, prefix_fee + retry_fee); + assert_eq!( + native_balance(&sink) - sink_before, + pipeline_fee + prefix_fee + retry_fee + ); let suspended = Actors::active_actor_view(actor_id).expect("suspended User actor"); assert_eq!( - Actors::continuation_state(actor_id) - .expect("continuation") - .cursor, + Actors::actor_run_state(actor_id).expect("Actor run").cursor, 1 ); let retry_weight = Actors::attempt_weight_upper_bound(&suspended, 1); @@ -2105,7 +2421,12 @@ fn user_retry_admits_and_charges_only_the_unresolved_suffix() { assert!(retry_weight.ref_time() < full_weight.ref_time()); assert!(retry_weight.proof_size() < full_weight.proof_size()); - frame_system::Pallet::::set_block_number(2); + let first_eligible_at = Actors::actor_run_state(actor_id) + .expect("first retry remains") + .eligible_at; + frame_system::Pallet::::set_block_number(first_eligible_at); + Actors::on_initialize(first_eligible_at); + run_prepass(); let retry_budget = Actors::scheduler_admission_overhead() .saturating_add(retry_weight) .saturating_add(::scheduler_on_idle_base()) @@ -2115,34 +2436,92 @@ fn user_retry_admits_and_charges_only_the_unresolved_suffix() { .saturating_mul(2), ) .saturating_add(::crossing_worker_base()) - .saturating_add(::observation_fanout_base()); + .saturating_add(::observation_fanout_base()) + .saturating_add(::block_resource_finalize()); run_idle(retry_budget); + run_next_idle(retry_budget); assert_eq!( native_balance(&sink) - sink_before, - prefix_fee + retry_fee.saturating_mul(2) + pipeline_fee + prefix_fee + retry_fee.saturating_mul(2) ); assert_eq!( - Actors::continuation_state(actor_id) + Actors::actor_run_state(actor_id) .expect("retry remains") .unsuccessful_attempts_at_cursor, 2 ); - frame_system::Pallet::::set_block_number(4); + let second_eligible_at = Actors::actor_run_state(actor_id) + .expect("second retry remains") + .eligible_at; + frame_system::Pallet::::set_block_number(second_eligible_at); set_temporary_dex_failure(false); + Actors::on_initialize(second_eligible_at); + run_prepass(); run_idle(Weight::MAX); + run_next_idle(Weight::MAX); assert_eq!( native_balance(&sink) - sink_before, - prefix_fee + retry_fee.saturating_mul(3) + tail_fee + pipeline_fee + prefix_fee + retry_fee.saturating_mul(3) + tail_fee ); - assert!(Actors::continuation_state(actor_id).is_none()); + assert!(Actors::actor_run_state(actor_id).is_none()); assert_eq!(native_balance(&BOB), bob_before + 10); assert_eq!(native_balance(&CHARLIE), charlie_before + 10); }); } #[test] -fn continuation_snapshot_is_trimmed_frozen_and_capacity_checked_live() { +fn underfunded_retry_liability_selects_fee_free_minimal_apoptosis() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + setup_temporary_retry_pool(); + let plan = BoundedVec::try_from(vec![StepOf:: { + precondition: None, + task: Task::SwapIn { + asset_in: TestAsset::Native, + asset_out: TestAsset::Local(77), + amount_in: AmountResolution::Fixed(10), + slippage_tolerance: Perbill::one(), + }, + on_error: RETRY_LATER, + }]) + .expect("one retry Step fits"); + let actor_id = create_user_with(ALICE, Mutability::Mutable, manual_schedule(), None, plan); + fund_native(actor_id, 1_000_000_000_000_000_000); + set_temporary_dex_failure(true); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + run_idle(Weight::MAX); + let run = Actors::actor_run_state(actor_id).expect("temporary failure suspends"); + let sovereign = sovereign_account(actor_id); + let balance = native_balance(&sovereign); + deplete_user_sovereign(actor_id, balance - TestMinUserBalance::get()); + let custody_before = native_balance(&sovereign); + clear_fee_collections(); + + frame_system::Pallet::::set_block_number(run.eligible_at); + Actors::on_initialize(run.eligible_at); + run_prepass(); + run_idle(Weight::MAX); + run_next_idle(Weight::MAX); + + assert!(!Actors::active_actor_exists(actor_id)); + assert_eq!(native_balance(&sovereign), custody_before); + assert!(fee_collections().is_empty()); + assert!(has_actor_event(|event| matches!( + event, + Event::ActorClosed { + actor_id: id, + reason: CloseReason::CycleAdmissionInsufficient, + } if *id == actor_id + ))); + }); +} + +#[test] +fn run_opening_snapshot_is_complete_frozen_and_capacity_checked_live() { new_test_ext().execute_with(|| { frame_system::Pallet::::set_block_number(1); let asset_a = TestAsset::Local(1); @@ -2183,18 +2562,19 @@ fn continuation_snapshot_is_trimmed_frozen_and_capacity_checked_live() { signal_percentage_trigger(actor_id, asset_b); run_idle(Weight::MAX); - let continuation = Actors::continuation_state(actor_id).expect("suspended"); + let continuation = Actors::actor_run_state(actor_id).expect("suspended"); assert_eq!(continuation.cursor, 1); - assert_eq!(continuation.opening_snapshot.len(), 2); + assert_eq!(continuation.opening_snapshot.len(), 3); let suspended = Actors::active_actor_view(actor_id).expect("suspended actor"); let retry_weight = Actors::attempt_weight_upper_bound(&suspended, continuation.cursor as usize); let full_weight = Actors::attempt_weight_upper_bound(&suspended, 0); assert!(retry_weight.ref_time() < full_weight.ref_time()); assert!(retry_weight.proof_size() < full_weight.proof_size()); - assert!( - !continuation + assert_eq!( + continuation .opening_snapshot - .contains_key(&OpeningSurface::PreservableAsset(asset_a)) + .get(&OpeningSurface::PreservableAsset(asset_a)), + Some(&99) ); assert_eq!( continuation @@ -2202,13 +2582,27 @@ fn continuation_snapshot_is_trimmed_frozen_and_capacity_checked_live() { .get(&OpeningSurface::PreservableAsset(asset_b)), Some(&99) ); + let mut mutated_snapshot = continuation.clone(); + *mutated_snapshot + .opening_snapshot + .get_mut(&OpeningSurface::PreservableAsset(asset_a)) + .expect("Opening asset remains present") = 98; + assert_noop!( + Actors::write_run_state(actor_id, Some(mutated_snapshot)), + Error::::ActorRunInvariant + ); + assert_eq!( + Actors::actor_run_state(actor_id).map(|state| state.encode()), + Some(continuation.encode()) + ); assert_eq!(asset_balance(&BOB, asset_a), 9); assert_ok!(MockAssetOps::transfer(&actor, &BOB, asset_b, 20)); set_temporary_dex_failure(false); - frame_system::Pallet::::set_block_number(2); + frame_system::Pallet::::set_block_number(continuation.eligible_at); run_idle(Weight::MAX); - let after_capacity_failure = Actors::continuation_state(actor_id).expect("still suspended"); + run_next_idle(Weight::MAX); + let after_capacity_failure = Actors::actor_run_state(actor_id).expect("still suspended"); assert_eq!(after_capacity_failure.cursor, 1); assert_eq!(after_capacity_failure.unsuccessful_attempts_at_cursor, 2); assert_eq!( @@ -2219,9 +2613,12 @@ fn continuation_snapshot_is_trimmed_frozen_and_capacity_checked_live() { ); set_asset_balance(&actor, asset_b, 100); - frame_system::Pallet::::set_block_number(4); + frame_system::Pallet::::set_block_number(after_capacity_failure.eligible_at); + Actors::on_initialize(after_capacity_failure.eligible_at); + run_prepass(); run_idle(Weight::MAX); - assert!(Actors::continuation_state(actor_id).is_none()); + run_next_idle(Weight::MAX); + assert!(Actors::actor_run_state(actor_id).is_none()); assert_eq!(asset_balance(&actor, asset_b), 82); assert_eq!(asset_balance(&CHARLIE, asset_c), 9); assert_eq!(asset_balance(&BOB, asset_a), 9); @@ -2229,7 +2626,7 @@ fn continuation_snapshot_is_trimmed_frozen_and_capacity_checked_live() { } #[test] -fn maximal_continuation_snapshot_stays_bounded_to_unresolved_surfaces() { +fn maximal_run_opening_snapshot_stays_bounded_to_full_contract_surfaces() { new_test_ext().execute_with(|| { frame_system::Pallet::::set_block_number(1); let mut steps = Vec::new(); @@ -2260,7 +2657,7 @@ fn maximal_continuation_snapshot_stays_bounded_to_unresolved_surfaces() { signal_percentage_trigger(actor_id, TestAsset::Local(100)); run_idle(Weight::MAX); - let continuation = Actors::continuation_state(actor_id).expect("maximal continuation"); + let continuation = Actors::actor_run_state(actor_id).expect("maximal Actor run"); assert_eq!(continuation.cursor, 0); assert_eq!( continuation.opening_snapshot.len() as u32, @@ -2270,12 +2667,12 @@ fn maximal_continuation_snapshot_stays_bounded_to_unresolved_surfaces() { set_temporary_add_liquidity_failure(false); frame_system::Pallet::::set_block_number(2); run_idle(Weight::MAX); - assert!(Actors::continuation_state(actor_id).is_none()); + assert!(Actors::actor_run_state(actor_id).is_none()); }); } #[test] -fn continuation_attempts_have_unique_chain_coordinates_without_the_stored_ordinal() { +fn run_attempts_have_unique_chain_coordinates_without_the_stored_ordinal() { new_test_ext().execute_with(|| { frame_system::Pallet::::set_block_number(1); setup_temporary_retry_pool(); @@ -2301,9 +2698,12 @@ fn continuation_attempts_have_unique_chain_coordinates_without_the_stored_ordina _ => None, }) .collect(); - assert!(matches!(opening[1], Event::CycleStarted { actor_id: id, cycle_nonce: 1 } if id == actor_id)); - assert!(matches!(opening[2], Event::StepFailed { actor_id: id, cycle_nonce: 1, step_index: 0, .. } if id == actor_id)); - assert!(matches!(opening[3], Event::CycleSuspended { actor_id: id, cycle_nonce: 1, cursor: 0, reason: SuspensionReason::Temporary, .. } if id == actor_id)); + let cycle_started = opening + .iter() + .position(|event| matches!(event, Event::CycleStarted { actor_id: id, cycle_nonce: 1 } if *id == actor_id)) + .expect("Opening emits CycleStarted"); + assert!(matches!(opening[cycle_started + 1], Event::StepFailed { actor_id: id, cycle_nonce: 1, step_index: 0, .. } if id == actor_id)); + assert!(matches!(opening[cycle_started + 2], Event::CycleSuspended { actor_id: id, cycle_nonce: 1, cursor: 0, reason: SuspensionReason::Temporary, .. } if id == actor_id)); frame_system::Pallet::::set_block_number(2); frame_system::Pallet::::reset_events(); @@ -2356,11 +2756,11 @@ fn continuation_attempts_have_unique_chain_coordinates_without_the_stored_ordina } #[test] -fn completion_policy_only_replacement_preserves_continuation() { +fn completion_policy_only_replacement_cancels_contract_bound_run() { new_test_ext().execute_with(|| { let actor_id = create_suspended_system_retry(1); let before = Actors::active_actor_view(actor_id).expect("suspended actor"); - let continuation_before = Actors::continuation_state(actor_id).expect("suspended continuation"); + let continuation_before = Actors::actor_run_state(actor_id).expect("suspended Actor run"); frame_system::Pallet::::reset_events(); assert_ok!(update_contract_partial!( RuntimeOrigin::root(), @@ -2374,11 +2774,9 @@ fn completion_policy_only_replacement_preserves_continuation() { after.completion, crate::CompletionPolicy::CloseAfterProductiveCycle ); - assert_eq!(after.cycle_state, CycleState::Suspended); - assert_eq!( - Actors::continuation_state(actor_id).map(|state| state.encode()), - Some(continuation_before.encode()) - ); + assert_eq!(after.cycle_state, CycleState::Idle); + assert!(Actors::actor_run_state(actor_id).is_none()); + assert_eq!(after.cycle_nonce, continuation_before.cycle_nonce); let events: Vec<_> = frame_system::Pallet::::events() .into_iter() .filter_map(|record| match record.event { @@ -2386,9 +2784,26 @@ fn completion_policy_only_replacement_preserves_continuation() { _ => None, }) .collect(); - assert_eq!(events.len(), 1); + assert_eq!(events.len(), 3); assert!(matches!( events.first(), + Some(Event::CycleCancelled { + actor_id: id, + cycle_nonce, + reason: CancellationReason::ContractReplaced, + }) if *id == actor_id && *cycle_nonce == continuation_before.cycle_nonce + )); + assert!(matches!( + events.get(1), + Some(Event::CycleSummary { + actor_id: id, + cycle_nonce, + result: CycleResult::Cancelled, + .. + }) if *id == actor_id && *cycle_nonce == continuation_before.cycle_nonce + )); + assert!(matches!( + events.get(2), Some(Event::ContractUpdated { actor_id: id, .. }) if *id == actor_id )); }); @@ -2533,17 +2948,32 @@ fn cycle_summary_tracks_step_outcomes() { } if *id == actor_id ) })); - let last_actor_event = frame_system::Pallet::::events() + let actor_events = frame_system::Pallet::::events() .into_iter() - .rev() - .find_map(|record| match record.event { + .filter_map(|record| match record.event { RuntimeEvent::Actors(event) => Some(event), _ => None, }) - .expect("Actors event stream must not be empty"); + .collect::>(); + let summary_index = actor_events + .iter() + .position(|event| { + matches!( + event, + Event::CycleSummary { actor_id: id, cycle_nonce: 1, .. } if *id == actor_id + ) + }) + .expect("Cycle summary exists"); + assert!(summary_index < actor_events.len() - 1); assert!(matches!( - last_actor_event, - Event::CycleSummary { actor_id: id, cycle_nonce: 1, .. } if id == actor_id + actor_events.last(), + Some(Event::ActionFeeCharged { + actor_id: id, + cycle_nonce: 1, + step_index: 3, + fee: 0, + .. + }) if *id == actor_id )); }); } @@ -2590,13 +3020,19 @@ fn cycle_success_predicate_drives_failure_reset_auto_close_and_event_order() { let after_first = Actors::active_actor_view(continue_id).expect("successful actor remains active"); assert_eq!(after_first.cycle_nonce, 1); assert_eq!(after_first.unsuccessful_attempt_streak, 0); - frame_system::Pallet::::set_block_number(2); + frame_system::Pallet::::set_block_number(3); + Actors::on_initialize(3); + run_prepass(); assert_ok!(Actors::manual_trigger( RuntimeOrigin::signed(ALICE), continue_id )); frame_system::Pallet::::reset_events(); run_idle(Weight::MAX); + frame_system::Pallet::::set_block_number(4); + Actors::on_initialize(4); + run_prepass(); + run_idle(Weight::MAX); assert!(Actors::active_actor_view(continue_id).is_none()); let continue_events: Vec<_> = frame_system::Pallet::::events() .into_iter() @@ -2605,12 +3041,14 @@ fn cycle_success_predicate_drives_failure_reset_auto_close_and_event_order() { _ => None, }) .collect(); - assert_eq!(continue_events.len(), 5); + assert_eq!(continue_events.len(), 7); assert!(matches!(continue_events[0], Event::CycleStarted { actor_id, .. } if actor_id == continue_id)); assert!(matches!(continue_events[1], Event::StepFailed { actor_id, step_index: 0, .. } if actor_id == continue_id)); - assert!(matches!(continue_events[2], Event::StepFailed { actor_id, step_index: 1, .. } if actor_id == continue_id)); - assert!(matches!(continue_events[3], Event::CycleSummary { actor_id, result: CycleResult::Completed, outcomes: OutcomeTotals { failed_steps: 2, .. }, .. } if actor_id == continue_id)); - assert!(matches!(continue_events[4], Event::ActorClosed { actor_id, reason: CloseReason::AutoCloseNonceReached } if actor_id == continue_id)); + assert!(matches!(continue_events[2], Event::ActionFeeCharged { actor_id, step_index: 0, fee: 0, .. } if actor_id == continue_id)); + assert!(matches!(continue_events[3], Event::StepFailed { actor_id, step_index: 1, .. } if actor_id == continue_id)); + assert!(matches!(continue_events[4], Event::CycleSummary { actor_id, result: CycleResult::Completed, outcomes: OutcomeTotals { failed_steps: 2, .. }, .. } if actor_id == continue_id)); + assert!(matches!(continue_events[5], Event::ActorClosed { actor_id, reason: CloseReason::AutoCloseNonceReached } if actor_id == continue_id)); + assert!(matches!(continue_events[6], Event::ActionFeeCharged { actor_id, step_index: 1, fee: 0, .. } if actor_id == continue_id)); let skip_step = StepOf:: { precondition: all_conditions(vec![Predicate::BalanceAbove { asset: TestAsset::Native, @@ -2635,7 +3073,7 @@ fn cycle_success_predicate_drives_failure_reset_auto_close_and_event_order() { )); assert_ok!(Actors::manual_trigger(RuntimeOrigin::signed(ALICE), skip_id)); frame_system::Pallet::::reset_events(); - run_idle(Weight::MAX); + run_next_idle(Weight::MAX); assert!(Actors::active_actor_view(skip_id).is_none()); let skip_events: Vec<_> = frame_system::Pallet::::events() .into_iter() @@ -2670,7 +3108,7 @@ fn cycle_success_predicate_drives_failure_reset_auto_close_and_event_order() { )); assert_ok!(Actors::manual_trigger(RuntimeOrigin::signed(ALICE), abort_id)); frame_system::Pallet::::reset_events(); - run_idle(Weight::MAX); + run_next_idle(Weight::MAX); let abort_instance = Actors::active_actor_view(abort_id).expect("aborted actor remains active"); assert_eq!(abort_instance.unsuccessful_attempt_streak, 1); let abort_events: Vec<_> = frame_system::Pallet::::events() @@ -2680,10 +3118,11 @@ fn cycle_success_predicate_drives_failure_reset_auto_close_and_event_order() { _ => None, }) .collect(); - assert_eq!(abort_events.len(), 3); + assert_eq!(abort_events.len(), 4); assert!(matches!(abort_events[0], Event::CycleStarted { actor_id, .. } if actor_id == abort_id)); assert!(matches!(abort_events[1], Event::StepFailed { actor_id, step_index: 0, .. } if actor_id == abort_id)); assert!(matches!(abort_events[2], Event::CycleSummary { actor_id, result: CycleResult::Failed, outcomes: OutcomeTotals { executed_steps: 0, failed_steps: 1, .. }, .. } if actor_id == abort_id)); + assert!(matches!(abort_events[3], Event::ActionFeeCharged { actor_id, step_index: 0, fee: 0, .. } if actor_id == abort_id)); let close_only_id = create_system_with(ALICE, manual_schedule(), None, inert_contract_steps()); frame_system::Pallet::::reset_events(); assert_ok!(Actors::close_actor(RuntimeOrigin::root(), close_only_id)); @@ -2782,15 +3221,23 @@ fn notify_address_event_accumulates_without_pause_resume_cycle() { } #[test] -fn empty_contract_steps_rejected() { +fn zero_step_user_contract_is_first_class_geometry() { new_test_ext().execute_with(|| { - assert_noop!( - Actors::create_user_actor( - RuntimeOrigin::signed(ALICE), - Mutability::Mutable, - user_active_contract(manual_schedule(), None, BoundedVec::default()), - ), - Error::::EmptyContractSteps + assert_ok!(Actors::create_user_actor( + RuntimeOrigin::signed(ALICE), + Mutability::Mutable, + user_active_contract(manual_schedule(), None, BoundedVec::default()), + )); + let head = crate::ActorContractHeads::::get(0).expect("zero-Step head exists"); + assert_eq!(head.header.step_count, 0); + assert!(head.first_step.is_none()); + assert!(head.first_step_resources.is_none()); + assert_eq!( + Actors::actor_contract(0) + .expect("zero-Step Contract reconstructs") + .steps + .len(), + 0 ); }); } @@ -2896,7 +3343,7 @@ fn cycle_nonce_exhaustion_closes_user_actor() { .expect("user Actors identity exists") .cycle_nonce = u64::MAX; }); - let expected_contract = ActorContracts::::get(actor_id).expect("contract exists"); + let expected_contract = Actors::load_actor_contract(actor_id).expect("contract exists"); let simulation = Actors::simulate_current_contract( actor_id, ActorType::User, @@ -3003,19 +3450,26 @@ fn retry_target_uses_only_cursor_local_count_and_last_attempt_block() { new_test_ext().execute_with(|| { let actor_id = create_system_with(ALICE, manual_schedule(), None, inert_contract_steps()); let instance = Actors::active_actor_view(actor_id).expect("actor"); - let continuation = |last_attempt_block| RuntimeContinuationState { + let run_state = |last_attempt_block, eligible_at| RuntimeActorRunState { + contract_authority: run_contract_authority(actor_id), + cycle_nonce: 1, cursor: 0, + opening_predicate_cursor: 0, unsuccessful_attempts_at_cursor: 1, last_attempt_block, + last_committed_step_block: None, + eligible_at, opening_snapshot: Default::default(), opening_predicate_results: Default::default(), funding_snapshot: Default::default(), cumulative_outcomes: Default::default(), + last_step_outcome: Some(StepOutcome::FundingUnavailable), + suspension: Some(SuspensionReason::FundingUnavailable), }; - ContinuationStateStore::::insert(actor_id, continuation(u64::MAX - 1)); + ActorRunStateStore::::insert(actor_id, run_state(u64::MAX - 1, u64::MAX)); assert_eq!(Actors::retry_eligible_at(actor_id, &instance), Ok(u64::MAX)); - ContinuationStateStore::::insert(actor_id, continuation(u64::MAX)); + ActorRunStateStore::::insert(actor_id, run_state(u64::MAX, u64::MAX)); assert_eq!( Actors::retry_eligible_at(actor_id, &instance), Err(crate::EnqueueOutcome::SchedulerIndexExhausted) @@ -3103,7 +3557,7 @@ fn opening_and_current_predicates_observe_distinct_step_state() { } #[test] -fn opening_predicate_result_is_reused_by_continuation() { +fn opening_predicate_result_is_reused_by_run_state() { new_test_ext().execute_with(|| { frame_system::Pallet::::set_block_number(1); setup_temporary_retry_pool(); @@ -3132,7 +3586,7 @@ fn opening_predicate_result_is_reused_by_continuation() { actor_id )); run_idle(Weight::MAX); - let continuation = Actors::continuation_state(actor_id).expect("continuation exists"); + let continuation = Actors::actor_run_state(actor_id).expect("Actor run exists"); assert_eq!( continuation.opening_predicate_results.as_slice(), &[Ok(true)] @@ -3141,7 +3595,7 @@ fn opening_predicate_result_is_reused_by_continuation() { set_temporary_dex_failure(false); frame_system::Pallet::::set_block_number(2); run_idle(Weight::MAX); - assert!(Actors::continuation_state(actor_id).is_none()); + assert!(Actors::actor_run_state(actor_id).is_none()); assert!(has_actor_event(|event| matches!( event, Event::SwapExecuted { actor_id: id, .. } if *id == actor_id @@ -3249,7 +3703,7 @@ fn any_skip_cannot_create_continuation() { actor_id )); run_idle(Weight::MAX); - assert!(Actors::continuation_state(actor_id).is_none()); + assert!(Actors::actor_run_state(actor_id).is_none()); assert_eq!( Actors::active_actor_view(actor_id) .expect("actor remains") @@ -3291,16 +3745,14 @@ fn retry_re_evaluates_live_any_conditions_at_the_same_cursor() { )); run_idle(Weight::MAX); assert_eq!( - Actors::continuation_state(actor_id) - .expect("suspended") - .cursor, + Actors::actor_run_state(actor_id).expect("suspended").cursor, 0 ); set_temporary_dex_failure(false); frame_system::Pallet::::set_block_number(2); run_idle(Weight::MAX); - assert!(Actors::continuation_state(actor_id).is_none()); + assert!(Actors::actor_run_state(actor_id).is_none()); assert!(has_actor_event(|event| matches!( event, Event::CycleSummary { @@ -3482,7 +3934,7 @@ fn user_dca_complete_lifecycle() { set_asset_balance(&actor, foreign, 500); fund_native(actor_id, 500); // For fees // Step 3-4: Advance blocks and verify execution - for block in 2..7 { + for block in 2..8 { frame_system::Pallet::::set_block_number(block); Actors::on_initialize(block); Actors::on_idle(block, Weight::MAX); @@ -3503,7 +3955,7 @@ fn user_dca_complete_lifecycle() { } let bob_after = asset_balance(&BOB, foreign); assert!(bob_after > bob_before, "Bob should receive transfers"); - // Step 6-7: Drain native below MinUserBalance to trigger sweep close + // Step 6-7: Drain native below MinUserBalance; automatic cadence underfunding preserves process authority let actor_native = native_balance(&actor); let min_user = ::MinUserBalance::get(); let slash_amount = actor_native.saturating_sub(min_user / 2); @@ -3512,7 +3964,7 @@ fn user_dca_complete_lifecycle() { native_balance(&actor) < min_user, "Actor balance must be below MinUserBalance after slash" ); - // Sweep cursor iterates MaxSweepBatch=3 IDs per block; run enough blocks + // Later due occurrences advance without fee/readiness/apoptosis; lifecycle touchpoints do not predict solvency for block in 30..50 { frame_system::Pallet::::set_block_number(block); Actors::on_initialize(block); @@ -3521,10 +3973,10 @@ fn user_dca_complete_lifecycle() { break; } } - assert!( - Actors::active_actor_view(actor_id).is_none(), - "User Actors must be destroyed by sweep when native < MinUserBalance" - ); + let retained = Actors::active_actor_view(actor_id) + .expect("underfunded Cadenced User process remains active"); + assert!(!retained.pending_signal); + assert!(retained.trigger_wakeup_pointer.is_some()); }); } @@ -3661,7 +4113,7 @@ fn canonical_step_transition_matrix_has_production_simulation_parity() { 1, crate::ScalarObservationState::Fresh { value: 50, - observed_at: 11, + observed_at: u64::MAX, }, ); setup_temporary_retry_pool(); @@ -3688,7 +4140,9 @@ fn canonical_step_transition_matrix_has_production_simulation_parity() { }; fund_native( actor_id, - if case.stimulus == StepParityStimulus::FundingUnavailable { + if case.actor_type == ActorType::User { + 1_000 + } else if case.stimulus == StepParityStimulus::FundingUnavailable { 8 } else { 100 @@ -3718,15 +4172,29 @@ fn canonical_step_transition_matrix_has_production_simulation_parity() { )); } if case.bound == StepParityBound::LocalReached { - run_idle(Weight::MAX); + let mut now = frame_system::Pallet::::block_number(); + Actors::on_idle(now, Weight::MAX); + for _ in 0..2 { + if Actors::actor_hot(actor_id).is_some_and(|hot| hot.cycle_state == CycleState::Suspended) + { + break; + } + now = now.checked_add(1).expect("matrix block advances"); + frame_system::Pallet::::set_block_number(now); + Actors::on_initialize(now); + Actors::on_idle(now, Weight::MAX); + } assert_eq!( - Actors::continuation_state(actor_id).map(|state| state.cursor), + Actors::actor_run_state(actor_id).map(|state| state.cursor), Some(1) ); - frame_system::Pallet::::set_block_number(11); + let eligible_at = Actors::actor_run_state(actor_id) + .expect("matrix suspension persists") + .eligible_at; + frame_system::Pallet::::set_block_number(eligible_at); } let mode = if case.bound == StepParityBound::LocalReached { - SimulationMode::CurrentContinuation + SimulationMode::CurrentRun } else { SimulationMode::FreshCurrentPlan }; @@ -3737,7 +4205,7 @@ fn canonical_step_transition_matrix_has_production_simulation_parity() { .unsuccessful_attempt_streak; let before = ( Actors::active_actor_view(actor_id).map(|view| view.encode()), - Actors::continuation_state(actor_id).map(|state| state.encode()), + Actors::actor_run_state(actor_id).map(|state| state.encode()), native_balance(&actor), native_balance(&BOB), native_balance(&CHARLIE), @@ -3757,7 +4225,7 @@ fn canonical_step_transition_matrix_has_production_simulation_parity() { .unwrap_or_else(|error| panic!("{} simulation failed: {error:?}", case.name)); let after_simulation = ( Actors::active_actor_view(actor_id).map(|view| view.encode()), - Actors::continuation_state(actor_id).map(|state| state.encode()), + Actors::actor_run_state(actor_id).map(|state| state.encode()), native_balance(&actor), native_balance(&BOB), native_balance(&CHARLIE), @@ -3794,18 +4262,14 @@ fn canonical_step_transition_matrix_has_production_simulation_parity() { "{} outcomes", case.name ); - assert_eq!( - cursor, simulation.continuation_cursor, - "{} cursor", - case.name - ); + assert_eq!(cursor, simulation.run_cursor, "{} cursor", case.name); assert_eq!( attempts, simulation.unsuccessful_attempts_at_cursor, "{} local attempts", case.name ); match status { - AttemptDisposition::Completed => assert_eq!( + AttemptDisposition::Completed | AttemptDisposition::Continued => assert_eq!( Actors::active_actor_view(actor_id) .expect("completed persistent matrix actor remains active") .unsuccessful_attempt_streak, @@ -3917,13 +4381,13 @@ fn canonical_step_transition_matrix_has_production_simulation_parity() { } #[test] -fn continuation_simulation_preserves_retry_position_and_committed_state() { +fn run_simulation_preserves_retry_position_and_committed_state() { new_test_ext().execute_with(|| { let actor_id = create_suspended_system_retry(1); let expected_contract = system_active_contract(manual_schedule(), None, temporary_retry_swap_plan()) .expect("direct Actor Contract"); - let continuation_before = Actors::continuation_state(actor_id).expect("continuation exists"); + let continuation_before = Actors::actor_run_state(actor_id).expect("Actor run exists"); let actor_before = Actors::active_actor_view(actor_id).expect("actor exists"); let events_before = frame_system::Pallet::::event_count(); frame_system::Pallet::::set_block_number(2); @@ -3933,14 +4397,14 @@ fn continuation_simulation_preserves_retry_position_and_committed_state() { ActorType::System, Mutability::Mutable, expected_contract, - SimulationMode::CurrentContinuation, + SimulationMode::CurrentRun, ) - .expect("eligible continuation simulates"); + .expect("eligible Actor run simulates"); assert_eq!(result.status, AttemptDisposition::Suspended); - assert_eq!(result.cycle_nonce, actor_before.cycle_nonce); + assert_eq!(result.cycle_nonce, continuation_before.cycle_nonce); assert_eq!(result.start_cursor, continuation_before.cursor); - assert_eq!(result.continuation_cursor, Some(continuation_before.cursor)); + assert_eq!(result.run_cursor, Some(continuation_before.cursor)); assert_eq!( result.unsuccessful_attempts_at_cursor, Some( @@ -3960,7 +4424,7 @@ fn continuation_simulation_preserves_retry_position_and_committed_state() { }] ); assert_eq!( - Actors::continuation_state(actor_id).map(|state| state.encode()), + Actors::actor_run_state(actor_id).map(|state| state.encode()), Some(continuation_before.encode()) ); assert_eq!(Actors::active_actor_view(actor_id), Some(actor_before)); @@ -4005,7 +4469,7 @@ fn simulation_projects_first_retry_suspension_and_rolls_back_actor_mutation() { .expect("retry exhaustion simulates"); assert_eq!(result.status, AttemptDisposition::Suspended); - assert_eq!(result.continuation_cursor, Some(0)); + assert_eq!(result.run_cursor, Some(0)); assert_eq!(result.unsuccessful_attempts_at_cursor, Some(1)); assert_eq!(Actors::active_actor_view(actor_id), Some(actor_before)); assert_eq!(frame_system::Pallet::::event_count(), events_before); @@ -4075,7 +4539,7 @@ fn simulation_rejects_contract_and_mode_mismatch_without_execution() { Mutability::Mutable, system_active_contract(manual_schedule(), None, contract_steps) .expect("direct Actor Contract"), - SimulationMode::CurrentContinuation, + SimulationMode::CurrentRun, ) .err(), Some(SimulationError::ModeCycleStateMismatch) @@ -4091,23 +4555,30 @@ fn simulation_rejects_contract_and_mode_mismatch_without_execution() { } #[test] -fn canonical_loader_requires_continuation_exactly_for_suspended_state() { +fn canonical_loader_requires_run_state_exactly_for_suspended_state() { new_test_ext().execute_with(|| { frame_system::Pallet::::set_block_number(1); let mut plan = inert_contract_steps(); plan[0].on_error = RETRY_LATER; let actor_id = create_system_with(ALICE, manual_schedule(), None, plan); - let continuation = RuntimeContinuationState { + let run_state = RuntimeActorRunState { + contract_authority: run_contract_authority(actor_id), + cycle_nonce: 1, cursor: 0, + opening_predicate_cursor: 0, unsuccessful_attempts_at_cursor: 1, last_attempt_block: 1, + last_committed_step_block: None, + eligible_at: 2, opening_snapshot: Default::default(), opening_predicate_results: Default::default(), funding_snapshot: Default::default(), cumulative_outcomes: Default::default(), + last_step_outcome: Some(StepOutcome::FundingUnavailable), + suspension: Some(SuspensionReason::FundingUnavailable), }; - ContinuationStateStore::::insert(actor_id, continuation.clone()); + ActorRunStateStore::::insert(actor_id, run_state.clone()); assert!(matches!( Actors::load_actor_state(actor_id), LoadedActorStateOf::Corrupt @@ -4119,29 +4590,30 @@ fn canonical_loader_requires_continuation_exactly_for_suspended_state() { Actors::load_actor_state(actor_id), LoadedActorStateOf::Active(_) )); - ContinuationStateStore::::remove(actor_id); + ActorRunStateStore::::remove(actor_id); assert!(matches!( Actors::load_actor_state(actor_id), LoadedActorStateOf::Corrupt )); - ContinuationStateStore::::insert(actor_id, continuation); + ActorRunStateStore::::insert(actor_id, run_state); for remove_partition in 0u8..4 { - let identity = ActorIdentities::::take(actor_id); - let hot = ActorHot::::take(actor_id); - let contract = ActorContracts::::take(actor_id); - let funding = ActorFunding::::take(actor_id); + let identity = ActorIdentities::::take(actor_id).expect("identity exists"); + let hot = ActorHot::::take(actor_id).expect("hot state exists"); + let contract = Actors::load_actor_contract(actor_id).expect("Contract exists"); + assert!(Actors::remove_admitted_contract_geometry(actor_id).is_some()); + let funding = ActorFunding::::take(actor_id).expect("funding exists"); if remove_partition != 0 { - ActorIdentities::::insert(actor_id, identity.clone().unwrap()); + ActorIdentities::::insert(actor_id, identity.clone()); } if remove_partition != 1 { - ActorHot::::insert(actor_id, hot.clone().unwrap()); + ActorHot::::insert(actor_id, hot.clone()); } if remove_partition != 2 { - ActorContracts::::insert(actor_id, contract.clone().unwrap()); + assert_ok!(Actors::store_actor_contract(actor_id, contract.clone())); } if remove_partition != 3 { - ActorFunding::::insert(actor_id, funding.clone().unwrap()); + ActorFunding::::insert(actor_id, funding.clone()); } assert!(matches!( Actors::load_actor_state(actor_id), @@ -4149,12 +4621,12 @@ fn canonical_loader_requires_continuation_exactly_for_suspended_state() { )); ActorIdentities::::remove(actor_id); ActorHot::::remove(actor_id); - ActorContracts::::remove(actor_id); + let _ = Actors::remove_admitted_contract_geometry(actor_id); ActorFunding::::remove(actor_id); - ActorIdentities::::insert(actor_id, identity.unwrap()); - ActorHot::::insert(actor_id, hot.unwrap()); - ActorContracts::::insert(actor_id, contract.unwrap()); - ActorFunding::::insert(actor_id, funding.unwrap()); + ActorIdentities::::insert(actor_id, identity); + ActorHot::::insert(actor_id, hot); + assert_ok!(Actors::store_actor_contract(actor_id, contract)); + ActorFunding::::insert(actor_id, funding); } }); } diff --git a/template/pallets/actors/src/tests/fees_and_funding.rs b/template/pallets/actors/src/tests/fees_and_funding.rs index 79a78b22..24e25b4d 100644 --- a/template/pallets/actors/src/tests/fees_and_funding.rs +++ b/template/pallets/actors/src/tests/fees_and_funding.rs @@ -1,5 +1,318 @@ use super::*; +#[test] +fn actor_state_hold_prices_exact_contract_geometry_and_releases_on_close() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let dormant_owner_before = native_balance(&ALICE); + assert_ok!(Actors::create_user_actor( + RuntimeOrigin::signed(ALICE), + Mutability::Mutable, + None, + )); + let dormant = Actors::actor_state_hold(0).expect("Dormant User hold exists"); + assert!(dormant.breakdown.identity > 0); + assert_eq!(dormant.breakdown.contract_head, 0); + assert_eq!(dormant.breakdown.contract_body, 0); + assert_eq!(dormant.breakdown.detector, 0); + assert_eq!(dormant.breakdown.funding, 0); + assert_eq!(dormant.breakdown.run, 0); + assert_ok!(Actors::close_actor(RuntimeOrigin::signed(ALICE), 0)); + assert!(Actors::actor_state_hold(0).is_none()); + assert_eq!( + native_balance(&ALICE), + dormant_owner_before.saturating_sub(TestActorCreationFee::get()) + ); + + frame_system::Pallet::::set_block_number(2); + let one_step = create_user_with( + ALICE, + Mutability::Mutable, + manual_schedule(), + None, + transfer_contract_steps(BOB, 1), + ); + let five_steps = BoundedVec::try_from( + (0..5) + .map(|_| { + make_step(Task::Transfer { + to: BOB, + asset: TestAsset::Native, + amount: AmountResolution::Fixed(1), + }) + }) + .collect::>(), + ) + .expect("five Steps fit"); + let five_step = create_user_with( + CHARLIE, + Mutability::Mutable, + manual_schedule(), + None, + five_steps, + ); + let compact = Actors::actor_state_hold(one_step).expect("one-Step hold exists"); + let chunked = Actors::actor_state_hold(five_step).expect("five-Step hold exists"); + assert_eq!(compact.breakdown.contract_body, 0); + assert!(compact.breakdown.run > 0); + assert_eq!(chunked.breakdown.run, compact.breakdown.run); + assert!(chunked.breakdown.contract_body > 0); + assert!(chunked.breakdown.contract_head > 0); + assert!(chunked.breakdown.funding > 0); + let temporal = create_user_with( + ALICE, + Mutability::Mutable, + at_time_schedule(10), + None, + crate::ContractSteps::::default(), + ); + assert!( + Actors::actor_state_hold(temporal) + .expect("temporal hold exists") + .breakdown + .detector + > 0 + ); + + let system = create_system_with( + ALICE, + manual_schedule(), + None, + transfer_contract_steps(BOB, 1), + ); + assert!(Actors::actor_state_hold(system).is_none()); + }); +} + +#[test] +fn actor_state_hold_failure_and_lifecycle_deltas_are_atomic() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let underfunded_owner = 77u64; + let initial = TestActorCreationFee::get().saturating_add(1); + let _ = >::deposit_creating(&underfunded_owner, initial); + let sink_before = native_balance(&TestFeeSink::get()); + assert_noop!( + Actors::create_user_actor( + RuntimeOrigin::signed(underfunded_owner), + Mutability::Mutable, + None, + ), + Error::::StateHoldUnavailable + ); + assert_eq!(native_balance(&underfunded_owner), initial); + assert_eq!(native_balance(&TestFeeSink::get()), sink_before); + assert_eq!(Actors::next_actor_id(), 0); + assert_eq!(Actors::actor_identity_count(), 0); + + let owner = 78u64; + let _ = >::deposit_creating(&owner, 1_000_000); + let actor_id = create_user_with( + owner, + Mutability::Mutable, + manual_schedule(), + None, + transfer_contract_steps(BOB, 1), + ); + let old_contract = Actors::actor_contract(actor_id).expect("one-Step Contract exists"); + let old_hold = Actors::actor_state_hold(actor_id).expect("one-Step hold exists"); + let free = native_balance(&owner); + assert_ok!(>::transfer( + &owner, + &BOB, + free.saturating_sub(1), + polkadot_sdk::frame_support::traits::ExistenceRequirement::AllowDeath, + )); + let five_steps = BoundedVec::try_from( + (0..5) + .map(|_| { + make_step(Task::Transfer { + to: BOB, + asset: TestAsset::Native, + amount: AmountResolution::Fixed(1), + }) + }) + .collect::>(), + ) + .expect("five Steps fit"); + let replacement = user_active_contract(manual_schedule(), None, five_steps) + .expect("replacement Contract exists"); + frame_system::Pallet::::set_block_number(2); + assert_noop!( + Actors::update_contract(RuntimeOrigin::signed(owner), actor_id, replacement.clone()), + Error::::StateHoldUnavailable + ); + assert_eq!(Actors::actor_contract(actor_id), Some(old_contract)); + assert_eq!(Actors::actor_state_hold(actor_id), Some(old_hold)); + + let _ = >::deposit_creating(&owner, 1_000_000); + assert_ok!(Actors::update_contract( + RuntimeOrigin::signed(owner), + actor_id, + replacement, + )); + let active_hold = Actors::actor_state_hold(actor_id).expect("expanded hold exists"); + assert!(active_hold.breakdown.contract_body > 0); + + frame_system::Pallet::::set_block_number(3); + assert_ok!(Actors::deactivate_actor( + RuntimeOrigin::signed(owner), + actor_id, + )); + let dormant_hold = Actors::actor_state_hold(actor_id).expect("Dormant hold exists"); + assert!(dormant_hold.breakdown.identity > 0); + assert_eq!(dormant_hold.breakdown.contract_head, 0); + assert_eq!(dormant_hold.breakdown.contract_body, 0); + assert_eq!(dormant_hold.breakdown.detector, 0); + assert_eq!(dormant_hold.breakdown.funding, 0); + assert_eq!(dormant_hold.breakdown.run, 0); + assert_ok!(Actors::close_actor(RuntimeOrigin::signed(owner), actor_id)); + assert!(Actors::actor_state_hold(actor_id).is_none()); + }); +} + +#[test] +fn funding_ingress_preflights_positive_state_hold_delta_as_temporary() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let owner = 79u64; + let _ = >::deposit_creating(&owner, 1_000_000); + let tracked_steps = BoundedVec::try_from(vec![make_step(Task::Transfer { + to: BOB, + asset: TestAsset::Native, + amount: AmountResolution::PercentageOfLastFunding(Perbill::one()), + })]) + .expect("tracked Step fits"); + let actor_id = create_user_with( + owner, + Mutability::Mutable, + manual_schedule(), + None, + tracked_steps, + ); + let sovereign = sovereign_account(actor_id); + let free = native_balance(&owner); + assert_ok!(>::transfer( + &owner, + &BOB, + free.saturating_sub(1), + polkadot_sdk::frame_support::traits::ExistenceRequirement::AllowDeath, + )); + let failure = Actors::preflight_ingress(&crate::AddressEvent { + destination: sovereign, + source: Some(owner), + asset: TestAsset::Native, + amount: 1, + provenance: Some(crate::FundingProvenance::Signed), + }) + .expect_err("new funding entry needs additional owner hold capacity"); + assert_eq!(failure.retry, RetryClass::Temporary); + assert_eq!(failure.error, Error::::StateHoldUnavailable.into()); + assert!( + Actors::actor_funding(actor_id) + .expect("funding state exists") + .funding_accumulated + .is_empty() + ); + }); +} + +#[test] +fn actor_run_state_hold_is_reserved_for_the_active_installed_lifetime() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let mut retry_step = make_step(Task::Transfer { + to: BOB, + asset: TestAsset::Native, + amount: AmountResolution::Fixed(Balance::MAX), + }); + retry_step.on_error = RETRY_LATER; + let steps = BoundedVec::try_from(vec![retry_step]).expect("one retry Step fits"); + let actor_id = create_user_with(ALICE, Mutability::Mutable, manual_schedule(), None, steps); + let installed_run_hold = Actors::actor_state_hold(actor_id) + .expect("idle hold exists") + .breakdown + .run; + assert!(installed_run_hold > 0); + fund_native(actor_id, 1_000_000_000_000_000_000); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + frame_system::Pallet::::set_block_number(2); + run_idle(Weight::MAX); + frame_system::Pallet::::set_block_number(3); + run_idle(Weight::MAX); + assert!(Actors::actor_run_state(actor_id).is_some()); + assert_eq!( + Actors::actor_state_hold(actor_id) + .expect("Running hold exists") + .breakdown + .run, + installed_run_hold + ); + frame_system::Pallet::::set_block_number(4); + assert_ok!(Actors::cancel_run(RuntimeOrigin::signed(ALICE), actor_id)); + assert!(Actors::actor_run_state(actor_id).is_none()); + assert_eq!( + Actors::actor_state_hold(actor_id) + .expect("active installed hold remains") + .breakdown + .run, + installed_run_hold + ); + }); +} + +#[test] +fn active_lifetime_run_hold_preserves_autonomous_opening_after_owner_spends_free_balance() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let mut retry_step = make_step(Task::Transfer { + to: BOB, + asset: TestAsset::Native, + amount: AmountResolution::Fixed(Balance::MAX), + }); + retry_step.on_error = RETRY_LATER; + let steps = BoundedVec::try_from(vec![retry_step]).expect("one retry Step fits"); + let actor_id = create_user_with(ALICE, Mutability::Mutable, manual_schedule(), None, steps); + let reserved = Actors::actor_state_hold(actor_id) + .expect("active installed hold exists") + .breakdown + .run; + assert!(reserved > 0); + fund_native( + actor_id, + user_prefunding_requirement(&inert_contract_steps()), + ); + let owner_free = native_balance(&ALICE); + assert_ok!(>::transfer( + &ALICE, + &BOB, + owner_free.saturating_sub(1), + polkadot_sdk::frame_support::traits::ExistenceRequirement::AllowDeath, + )); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + frame_system::Pallet::::set_block_number(2); + run_idle(Weight::MAX); + frame_system::Pallet::::set_block_number(3); + run_idle(Weight::MAX); + + assert!(Actors::active_actor_view(actor_id).is_some()); + assert!(Actors::actor_run_state(actor_id).is_some()); + assert_eq!( + Actors::actor_state_hold(actor_id) + .expect("active installed hold remains") + .breakdown + .run, + reserved + ); + }); +} + #[test] fn create_user_charges_creation_fee_and_emits_event() { new_test_ext().execute_with(|| { @@ -17,7 +330,11 @@ fn create_user_charges_creation_fee_and_emits_event() { ); let inst = Actors::active_actor_view(actor_id).expect("Actors must exist"); assert_eq!(inst.actor_class, ActorClass::User { owner_slot: 0 }); - assert_eq!(native_balance(&ALICE), owner_before.saturating_sub(fee)); + let state_hold = actor_state_hold_total(actor_id); + assert_eq!( + native_balance(&ALICE), + owner_before.saturating_sub(fee).saturating_sub(state_hold) + ); assert_eq!(native_balance(&fee_sink), sink_before.saturating_add(fee)); assert_eq!(OwnerSlotBitmaps::::get(ALICE)[0], 0b0000_0001); assert!(has_actor_event(|event| { @@ -40,103 +357,640 @@ fn create_user_charges_creation_fee_and_emits_event() { } #[test] -fn both_user_creation_calls_charge_dormant_admission_fee_before_identity_mutation() { +fn both_user_creation_calls_charge_dormant_admission_fee_before_identity_mutation() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let fee = TestActorCreationFee::get(); + let fee_sink = TestFeeSink::get(); + let sink_before = native_balance(&fee_sink); + let alice_before = native_balance(&ALICE); + let charlie_before = native_balance(&CHARLIE); + + assert_ok!(Actors::create_user_actor( + RuntimeOrigin::signed(ALICE), + Mutability::Mutable, + None, + )); + assert_ok!(Actors::create_user_actor_at_slot( + RuntimeOrigin::signed(CHARLIE), + 2, + Mutability::Mutable, + None, + )); + + assert_eq!( + native_balance(&ALICE), + alice_before + .saturating_sub(fee) + .saturating_sub(actor_state_hold_total(0)) + ); + assert_eq!( + native_balance(&CHARLIE), + charlie_before + .saturating_sub(fee) + .saturating_sub(actor_state_hold_total(1)) + ); + assert_eq!( + native_balance(&fee_sink), + sink_before.saturating_add(fee.saturating_mul(2)) + ); + assert_eq!(Actors::owner_slot_bitmap(ALICE)[0], 0b0000_0001); + assert_eq!(Actors::owner_slot_bitmap(CHARLIE)[0], 0b0000_0100); + assert_eq!(Actors::actor_identity_count(), 2); + assert_eq!(Actors::active_actor_count(), 0); + }); +} + +#[test] +fn user_active_creation_charges_creation_fee_without_sovereign_service_prefunding() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let owner_before = native_balance(&ALICE); + let sink_before = native_balance(&TestFeeSink::get()); + let creation_fee = TestActorCreationFee::get(); + let sovereign = Actors::sovereign_account_id(&ALICE, 0); + assert_eq!(native_balance(&sovereign), 0); + + assert_ok!(Actors::create_user_actor( + RuntimeOrigin::signed(ALICE), + Mutability::Mutable, + user_active_contract(manual_schedule(), None, transfer_contract_steps(BOB, 1)), + )); + + let state_hold = actor_state_hold_total(0); + assert_eq!( + native_balance(&ALICE), + owner_before - creation_fee - state_hold + ); + assert_eq!( + native_balance(&TestFeeSink::get()), + sink_before + creation_fee + ); + assert_eq!(native_balance(&sovereign), 0); + assert_eq!(Actors::actor_identity_count(), 1); + assert_eq!(Actors::active_actor_count(), 1); + }); +} + +#[test] +fn user_active_creation_requires_no_sovereign_activation_prefunding() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let plan = BoundedVec::try_from(vec![ + make_step(Task::Transfer { + to: BOB, + asset: TestAsset::Native, + amount: AmountResolution::Fixed(1), + }), + make_step(Task::Transfer { + to: CHARLIE, + asset: TestAsset::Native, + amount: AmountResolution::Fixed(1), + }), + ]) + .expect("two Steps fit"); + let sovereign = Actors::sovereign_account_id(&ALICE, 0); + assert_eq!(native_balance(&sovereign), 0); + + assert_ok!(Actors::create_user_actor( + RuntimeOrigin::signed(ALICE), + Mutability::Mutable, + user_active_contract(manual_schedule(), None, plan), + )); + + assert_eq!(native_balance(&sovereign), 0); + let head = crate::ActorContractHeads::::get(0).expect("C6 head exists"); + assert!( + head + .header + .pipeline_machine_envelope + .pipeline_machine_fee_upper + > 0 + ); + assert_eq!(Actors::active_actor_count(), 1); + }); +} + +#[test] +fn zero_step_user_opening_charges_pipeline_but_no_action_fee() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = create_user_with( + ALICE, + Mutability::Mutable, + manual_schedule(), + None, + BoundedVec::default(), + ); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + clear_fee_collections(); + let fee_sink_before = native_balance(&TestFeeSink::get()); + + run_idle(Weight::MAX); + + let pipeline_fee = pipeline_opening_fee(&BoundedVec::default()); + assert_eq!(fee_collections(), vec![pipeline_fee]); + assert_eq!( + native_balance(&TestFeeSink::get()), + fee_sink_before.saturating_add(pipeline_fee) + ); + assert!(has_actor_event(|event| matches!( + event, + Event::PipelineFeeCharged { + actor_id: id, + fee, + } if *id == actor_id && *fee == pipeline_fee + ))); + assert_eq!( + Actors::actor_identities(actor_id) + .expect("persistent zero-Step User remains") + .cycle_nonce, + 1 + ); + assert!(ActorRunStateStore::::get(actor_id).is_none()); + }); +} + +#[test] +fn manual_trigger_charges_occurrence_before_pipeline_opening() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = create_user_with( + ALICE, + Mutability::Mutable, + manual_schedule(), + None, + transfer_contract_steps(BOB, 1), + ); + let sovereign = sovereign_account(actor_id); + let sovereign_before = native_balance(&sovereign); + let sink_before = native_balance(&TestFeeSink::get()); + clear_fee_collections(); + + let post = Actors::manual_trigger(RuntimeOrigin::signed(ALICE), actor_id) + .expect("funded Manual occurrence commits"); + + let fee = manual_trigger_fee(); + assert_eq!( + post.pays_fee, + polkadot_sdk::frame_support::dispatch::Pays::No + ); + assert_eq!(fee_collections(), vec![fee]); + assert_eq!(native_balance(&sovereign), sovereign_before - fee); + assert_eq!(native_balance(&TestFeeSink::get()), sink_before + fee); + let instance = Actors::active_actor_view(actor_id).expect("Actor remains"); + assert!(instance.pending_signal); + assert_eq!(instance.cycle_nonce, 0, "Trigger does not open a Pipeline"); + assert!(ActorRunStateStore::::get(actor_id).is_none()); + assert!(has_actor_event(|event| matches!( + event, + Event::TriggerOccurrenceProcessed { + actor_id: id, + trigger_family: TriggerFamily::Manual, + fee: charged, + } if *id == actor_id && *charged == fee + ))); + + let pipeline_fee = pipeline_opening_fee(&instance.steps); + frame_system::Pallet::::set_block_number(3); + run_idle(Weight::MAX); + assert_eq!(fee_collections().get(1), Some(&pipeline_fee)); + assert!(has_actor_event(|event| matches!( + event, + Event::PipelineFeeCharged { + actor_id: id, + fee, + } if *id == actor_id && *fee == pipeline_fee + ))); + assert_eq!( + Actors::active_actor_view(actor_id) + .expect("persistent Actor remains") + .cycle_nonce, + 1 + ); + }); +} + +#[test] +fn repeated_pending_manual_occurrence_is_latched_without_trigger_fee() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = create_user_with( + ALICE, + Mutability::Mutable, + manual_schedule(), + None, + transfer_contract_steps(BOB, 1), + ); + fund_native(actor_id, manual_trigger_fee()); + clear_fee_collections(); + + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + let ticket = Actors::actor_hot(actor_id) + .expect("Actor hot state") + .queue_ticket; + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + + assert_eq!(fee_collections(), vec![manual_trigger_fee()]); + let hot = Actors::actor_hot(actor_id).expect("Actor hot state"); + assert!(hot.pending_signal); + assert_eq!( + hot.queue_ticket, ticket, + "coalescing creates no second ticket" + ); + assert_eq!( + System::events() + .iter() + .filter(|record| matches!( + &record.event, + RuntimeEvent::Actors(Event::TriggerOccurrenceProcessed { + actor_id: id, + trigger_family: TriggerFamily::Manual, + .. + }) if *id == actor_id + )) + .count(), + 1 + ); + + run_idle(Weight::MAX); + assert_eq!( + System::events() + .iter() + .filter(|record| matches!( + &record.event, + RuntimeEvent::Actors(Event::PipelineFeeCharged { actor_id: id, .. }) + if *id == actor_id + )) + .count(), + 1 + ); + assert_eq!( + Actors::active_actor_view(actor_id) + .expect("persistent Actor remains") + .cycle_nonce, + 1 + ); + }); +} + +#[test] +fn busy_manual_occurrence_charges_and_latches_only_the_future_pipeline() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let plan = BoundedVec::try_from(vec![ + make_step(Task::Transfer { + to: BOB, + asset: TestAsset::Native, + amount: AmountResolution::Fixed(1), + }), + make_step(Task::Transfer { + to: CHARLIE, + asset: TestAsset::Native, + amount: AmountResolution::Fixed(1), + }), + ]) + .expect("two Steps fit"); + let actor_id = create_user_with(ALICE, Mutability::Mutable, manual_schedule(), None, plan); + fund_native(actor_id, 1_000_000); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + Actors::on_idle(1, Weight::MAX); + let run_before = ActorRunStateStore::::get(actor_id).expect("Pipeline is Running"); + assert_eq!(run_before.cursor, 1); + clear_fee_collections(); + frame_system::Pallet::::reset_events(); + + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + + assert_eq!(fee_collections(), vec![manual_trigger_fee()]); + assert!(!has_actor_event(|event| matches!( + event, + Event::PipelineFeeCharged { actor_id: id, .. } if *id == actor_id + ))); + let hot = Actors::actor_hot(actor_id).expect("Actor hot state"); + assert_eq!(hot.cycle_state, CycleState::Running); + assert!(hot.pending_signal); + let run_after = ActorRunStateStore::::get(actor_id).expect("Pipeline remains Running"); + assert_eq!(run_after.cursor, run_before.cursor); + assert_eq!(run_after.cycle_nonce, run_before.cycle_nonce); + assert_eq!( + run_after.contract_authority.semantic_contract_id, + run_before.contract_authority.semantic_contract_id + ); + }); +} + +#[test] +fn underfunded_manual_occurrence_creates_no_fee_readiness_or_apoptosis() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = create_user_with( + ALICE, + Mutability::Mutable, + manual_schedule(), + None, + transfer_contract_steps(BOB, 1), + ); + let sovereign = sovereign_account(actor_id); + let balance = native_balance(&sovereign); + deplete_user_sovereign(actor_id, balance - TestMinUserBalance::get()); + clear_fee_collections(); + + assert_noop!( + Actors::manual_trigger(RuntimeOrigin::signed(ALICE), actor_id), + Error::::InsufficientFee + ); + + assert!(fee_collections().is_empty()); + let hot = Actors::actor_hot(actor_id).expect("process remains live"); + assert!(!hot.pending_signal); + assert!(hot.queue_ticket.is_none()); + assert!(Actors::active_actor_view(actor_id).is_some()); + }); +} + +#[test] +fn manual_trigger_collection_failure_rolls_back_readiness_and_fee_movement() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = create_user_with( + ALICE, + Mutability::Mutable, + manual_schedule(), + None, + transfer_contract_steps(BOB, 1), + ); + let sovereign = sovereign_account(actor_id); + let sovereign_before = native_balance(&sovereign); + let sink_before = native_balance(&TestFeeSink::get()); + set_fail_fee_sink_transfer(true); + + assert_noop!( + Actors::manual_trigger(RuntimeOrigin::signed(ALICE), actor_id), + Error::::InsufficientFee + ); + set_fail_fee_sink_transfer(false); + + assert_eq!(native_balance(&sovereign), sovereign_before); + assert_eq!(native_balance(&TestFeeSink::get()), sink_before); + let hot = Actors::actor_hot(actor_id).expect("process remains live"); + assert!(!hot.pending_signal); + assert!(hot.queue_ticket.is_none()); + assert!(!has_actor_event(|event| matches!( + event, + Event::TriggerOccurrenceProcessed { actor_id: id, .. } if *id == actor_id + ))); + }); +} + +#[test] +fn address_event_charges_occurrence_before_pipeline_opening() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = create_user_with( + ALICE, + Mutability::Mutable, + percentage_trigger_schedule(), + None, + transfer_contract_steps(BOB, 1), + ); + let sovereign = sovereign_account(actor_id); + let sovereign_before = native_balance(&sovereign); + clear_fee_collections(); + + assert_ok!(Actors::notify_address_event( + actor_id, + TestAsset::Native, + 1, + &ALICE, + )); + + let fee = address_event_trigger_fee(); + assert_eq!(fee_collections(), vec![fee]); + assert_eq!(native_balance(&sovereign), sovereign_before - fee); + let instance = Actors::active_actor_view(actor_id).expect("Actor remains"); + assert!(instance.pending_signal); + assert_eq!(instance.cycle_nonce, 0); + assert!(ActorRunStateStore::::get(actor_id).is_none()); + assert!(has_actor_event(|event| matches!( + event, + Event::TriggerOccurrenceProcessed { + actor_id: id, + trigger_family: TriggerFamily::AddressEvent, + fee: charged, + } if *id == actor_id && *charged == fee + ))); + }); +} + +#[test] +fn repeated_pending_address_event_is_latched_without_trigger_fee() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = create_user_with( + ALICE, + Mutability::Mutable, + percentage_trigger_schedule(), + None, + transfer_contract_steps(BOB, 1), + ); + fund_native(actor_id, address_event_trigger_fee()); + clear_fee_collections(); + + assert_ok!(Actors::notify_address_event( + actor_id, + TestAsset::Native, + 1, + &ALICE, + )); + let ticket = Actors::actor_hot(actor_id) + .expect("Actor hot state") + .queue_ticket; + assert_ok!(Actors::notify_address_event( + actor_id, + TestAsset::Native, + 1, + &ALICE, + )); + + assert_eq!(fee_collections(), vec![address_event_trigger_fee()]); + let hot = Actors::actor_hot(actor_id).expect("Actor hot state"); + assert!(hot.pending_signal); + assert_eq!(hot.queue_ticket, ticket); + assert_eq!( + System::events() + .iter() + .filter(|record| matches!( + &record.event, + RuntimeEvent::Actors(Event::TriggerOccurrenceProcessed { + actor_id: id, + trigger_family: TriggerFamily::AddressEvent, + .. + }) if *id == actor_id + )) + .count(), + 1 + ); + }); +} + +#[test] +fn busy_address_event_charges_and_latches_only_the_future_pipeline() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let plan = BoundedVec::try_from(vec![ + make_step(Task::Transfer { + to: BOB, + asset: TestAsset::Native, + amount: AmountResolution::Fixed(1), + }), + make_step(Task::Transfer { + to: CHARLIE, + asset: TestAsset::Native, + amount: AmountResolution::Fixed(1), + }), + ]) + .expect("two Steps fit"); + let actor_id = create_user_with( + ALICE, + Mutability::Mutable, + percentage_trigger_schedule(), + None, + plan, + ); + fund_native(actor_id, 1_000_000); + assert_ok!(Actors::notify_address_event( + actor_id, + TestAsset::Native, + 1, + &ALICE, + )); + Actors::on_idle(1, Weight::MAX); + let run_before = ActorRunStateStore::::get(actor_id).expect("Pipeline is Running"); + clear_fee_collections(); + frame_system::Pallet::::reset_events(); + + assert_ok!(Actors::notify_address_event( + actor_id, + TestAsset::Native, + 1, + &ALICE, + )); + + assert_eq!(fee_collections(), vec![address_event_trigger_fee()]); + assert!(!has_actor_event(|event| matches!( + event, + Event::PipelineFeeCharged { actor_id: id, .. } if *id == actor_id + ))); + let hot = Actors::actor_hot(actor_id).expect("Actor hot state"); + assert_eq!(hot.cycle_state, CycleState::Running); + assert!(hot.pending_signal); + let run_after = ActorRunStateStore::::get(actor_id).expect("Pipeline remains Running"); + assert_eq!(run_after.cursor, run_before.cursor); + assert_eq!(run_after.cycle_nonce, run_before.cycle_nonce); + }); +} + +#[test] +fn underfunded_address_event_advances_without_fee_readiness_or_apoptosis() { new_test_ext().execute_with(|| { frame_system::Pallet::::set_block_number(1); - let fee = TestActorCreationFee::get(); - let fee_sink = TestFeeSink::get(); - let sink_before = native_balance(&fee_sink); - let alice_before = native_balance(&ALICE); - let charlie_before = native_balance(&CHARLIE); - - assert_ok!(Actors::create_user_actor( - RuntimeOrigin::signed(ALICE), - Mutability::Mutable, - None, - )); - assert_ok!(Actors::create_user_actor_at_slot( - RuntimeOrigin::signed(CHARLIE), - 2, + let actor_id = create_user_with( + ALICE, Mutability::Mutable, + percentage_trigger_schedule(), None, + contract_steps_with_step(make_step(Task::Transfer { + to: BOB, + asset: TestAsset::Native, + amount: AmountResolution::PercentageOfLastFunding(Perbill::one()), + })), + ); + let sovereign = sovereign_account(actor_id); + let balance = native_balance(&sovereign); + deplete_user_sovereign(actor_id, balance - TestMinUserBalance::get()); + clear_fee_collections(); + + assert_ok!(Actors::notify_address_event( + actor_id, + TestAsset::Native, + 1, + &ALICE, )); - assert_eq!(native_balance(&ALICE), alice_before.saturating_sub(fee)); - assert_eq!(native_balance(&CHARLIE), charlie_before.saturating_sub(fee)); + assert!(fee_collections().is_empty()); + assert_eq!(native_balance(&sovereign), TestMinUserBalance::get()); + let hot = Actors::actor_hot(actor_id).expect("process remains live"); + assert!(!hot.pending_signal); + assert!(hot.queue_ticket.is_none()); + assert!(Actors::active_actor_view(actor_id).is_some()); assert_eq!( - native_balance(&fee_sink), - sink_before.saturating_add(fee.saturating_mul(2)) + actor_funding(actor_id) + .funding_accumulated + .get(&TestAsset::Native), + Some(&1) ); - assert_eq!(Actors::owner_slot_bitmap(ALICE)[0], 0b0000_0001); - assert_eq!(Actors::owner_slot_bitmap(CHARLIE)[0], 0b0000_0100); - assert_eq!(Actors::actor_identity_count(), 2); - assert_eq!(Actors::active_actor_count(), 0); }); } #[test] -fn user_active_creation_requires_prefunded_sovereign_before_opening_fee() { +fn address_event_collection_failure_preserves_source_progress_without_readiness() { new_test_ext().execute_with(|| { frame_system::Pallet::::set_block_number(1); - // An unfunded User Active creation fails InsufficientBalance before the opening fee - // or any identity/locator mutation (spec 7.1). - let owner_before = native_balance(&ALICE); - let sink_before = native_balance(&TestFeeSink::get()); - assert_noop!( - Actors::create_user_actor( - RuntimeOrigin::signed(ALICE), - Mutability::Mutable, - user_active_contract(manual_schedule(), None, transfer_contract_steps(BOB, 1)), - ), - Error::::InsufficientBalance - ); - assert_eq!(Actors::next_actor_id(), 0); - assert_eq!(Actors::actor_identity_count(), 0); - assert_eq!(Actors::active_actor_count(), 0); - assert_eq!(Actors::owner_slot_bitmap(ALICE), [0; 32]); - assert_eq!(native_balance(&ALICE), owner_before); - assert_eq!(native_balance(&TestFeeSink::get()), sink_before); - assert!(fee_collections().is_empty()); - - // Dormant creation remains unfunded and admits with no sovereign balance. - assert_ok!(Actors::create_user_actor( - RuntimeOrigin::signed(ALICE), + let actor_id = create_user_with( + ALICE, Mutability::Mutable, + percentage_trigger_schedule(), None, + contract_steps_with_step(make_step(Task::Transfer { + to: BOB, + asset: TestAsset::Native, + amount: AmountResolution::PercentageOfLastFunding(Perbill::one()), + })), + ); + let sovereign = sovereign_account(actor_id); + let sovereign_before = native_balance(&sovereign); + let sink_before = native_balance(&TestFeeSink::get()); + set_fail_fee_sink_transfer(true); + + assert_ok!(Actors::notify_address_event( + actor_id, + TestAsset::Native, + 1, + &ALICE, )); - assert_eq!(native_balance(&Actors::sovereign_account_id(&ALICE, 0)), 0); - }); -} + set_fail_fee_sink_transfer(false); -#[test] -fn user_active_creation_prefunding_boundary_is_exact_floor_plus_envelope() { - new_test_ext().execute_with(|| { - frame_system::Pallet::::set_block_number(1); - let plan = transfer_contract_steps(BOB, 1); - let requirement = user_prefunding_requirement(&plan); - // requirement - 1 still fails closed before any opening-fee mutation. - fund_native_raw( - &Actors::sovereign_account_id(&ALICE, 0), - requirement.saturating_sub(1), - ); - assert_noop!( - Actors::create_user_actor( - RuntimeOrigin::signed(ALICE), - Mutability::Mutable, - user_active_contract(manual_schedule(), None, plan.clone()), - ), - Error::::InsufficientBalance + assert_eq!(native_balance(&sovereign), sovereign_before); + assert_eq!(native_balance(&TestFeeSink::get()), sink_before); + let hot = Actors::actor_hot(actor_id).expect("process remains live"); + assert!(!hot.pending_signal); + assert!(hot.queue_ticket.is_none()); + assert_eq!( + actor_funding(actor_id) + .funding_accumulated + .get(&TestAsset::Native), + Some(&1) ); - assert_eq!(Actors::actor_identity_count(), 0); - assert_eq!(Actors::owner_slot_bitmap(ALICE), [0; 32]); - // Exactly floor + envelope admits. - fund_native_raw(&Actors::sovereign_account_id(&ALICE, 0), 1); - assert_ok!(Actors::create_user_actor( - RuntimeOrigin::signed(ALICE), - Mutability::Mutable, - user_active_contract(manual_schedule(), None, plan), - )); - assert_eq!(Actors::active_actor_count(), 1); + assert!(!has_actor_event(|event| matches!( + event, + Event::TriggerOccurrenceProcessed { actor_id: id, .. } if *id == actor_id + ))); }); } @@ -455,36 +1309,61 @@ fn reserved_sovereign_account_rejects_creation_at_that_slot() { } #[test] -fn create_user_at_slot_reuses_same_sovereign_after_close() { +fn exact_slot_recovery_contract_accesses_residual_user_custody() { new_test_ext().execute_with(|| { frame_system::Pallet::::set_block_number(1); let target_slot = 3; + let asset = TestAsset::Local(7); + let recovery_steps = contract_steps_with_step(make_step(Task::Transfer { + to: BOB, + asset, + amount: AmountResolution::Fixed(333), + })); let first_id = create_user_with_slot( ALICE, target_slot, Mutability::Mutable, manual_schedule(), None, - transfer_contract_steps(BOB, 1), + recovery_steps.clone(), ); - let first = Actors::active_actor_view(first_id).expect("first Actors exists"); + let first = Actors::active_actor_view(first_id).expect("first actor exists"); assert_eq!(first.actor_class.owner_slot(), Some(target_slot)); assert_eq!( first.sovereign_account, Actors::sovereign_account_id(&ALICE, target_slot) ); + set_asset_balance(&first.sovereign_account, asset, 777); + let native_before_close = native_balance(&first.sovereign_account); assert_ok!(Actors::close_actor(RuntimeOrigin::signed(ALICE), first_id)); + assert_eq!( + native_balance(&first.sovereign_account), + native_before_close + ); + assert_eq!(asset_balance(&first.sovereign_account, asset), 777); + let second_id = create_user_with_slot( ALICE, target_slot, Mutability::Mutable, manual_schedule(), None, - transfer_contract_steps(BOB, 1), + recovery_steps, ); - let second = Actors::active_actor_view(second_id).expect("second Actors exists"); + let second = Actors::active_actor_view(second_id).expect("recovery actor exists"); + assert_ne!(second_id, first_id); assert_eq!(second.actor_class.owner_slot(), Some(target_slot)); assert_eq!(second.sovereign_account, first.sovereign_account); + assert_eq!(asset_balance(&second.sovereign_account, asset), 777); + + let bob_before = asset_balance(&BOB, asset); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + second_id + )); + run_idle(Weight::MAX); + assert_eq!(asset_balance(&BOB, asset), bob_before + 333); + assert_eq!(asset_balance(&second.sovereign_account, asset), 444); }); } @@ -544,7 +1423,6 @@ fn failed_pre_opening_weight_admission_preserves_latch_and_funding() { &ALICE, )); assert_ok!(Actors::manual_trigger(RuntimeOrigin::signed(ALICE), actor_id)); - let instance = Actors::active_actor_view(actor_id).expect("Actors exists"); let queue_weight = <::WeightInfo as crate::WeightInfo>::scheduler_paged_tombstone_drain(1) .saturating_add(Actors::scheduler_actor_probe_weight_upper()) .saturating_add( @@ -552,9 +1430,12 @@ fn failed_pre_opening_weight_admission_preserves_latch_and_funding() { .max(<::WeightInfo as crate::WeightInfo>::scheduler_paged_consume_delete_page()), ); // Only the RefTime dimension is exhausted; ProofSize remains unlimited. + let resources = Actors::load_current_step_from_storage(actor_id, 0) + .expect("current Step resources exist") + .resources; let ref_time_limit = queue_weight .ref_time() - .saturating_add(Actors::attempt_weight_upper_bound(&instance, 0).ref_time()) + .saturating_add(resources.control.saturating_add(resources.effect).ref_time()) .saturating_sub(1); Actors::execute_cycle(Weight::from_parts(ref_time_limit, u64::MAX)); let instance = Actors::active_actor_view(actor_id).expect("Actors exists"); @@ -569,11 +1450,293 @@ fn failed_pre_opening_weight_admission_preserves_latch_and_funding() { }); } -#[test] -fn cycle_closes_with_fee_budget_exhausted_when_unfunded() { +#[test] +fn non_invoked_task_releases_effect_weight_after_maximum_admission() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = create_system_with( + ALICE, + manual_schedule(), + None, + contract_steps_with_step(StepOf:: { + precondition: all_conditions(vec![Predicate::BlockNumberBelow { threshold: 0 }]), + task: Task::Transfer { + to: BOB, + asset: TestAsset::Native, + amount: AmountResolution::Fixed(10), + }, + on_error: StepErrorPolicy::AbortCycle, + }), + ); + fund_native(actor_id, 100); + assert_ok!(Actors::manual_trigger(RuntimeOrigin::signed(ALICE), actor_id)); + let resources = Actors::load_current_step_from_storage(actor_id, 0) + .expect("current Step resources exist") + .resources; + assert_ne!(resources.effect, Weight::zero()); + let scan = <::WeightInfo as crate::WeightInfo>::scheduler_paged_tombstone_drain(1); + let probe = Actors::scheduler_actor_probe_weight_upper(); + let consume = <::WeightInfo as crate::WeightInfo>::scheduler_paged_consume_preserve_page() + .max(<::WeightInfo as crate::WeightInfo>::scheduler_paged_consume_delete_page()); + + let bob_before = native_balance(&BOB); + let budget = TestBlockResourceBudget::get(); + let mut resource_state = crate::BlockResourceState::new(1); + assert_eq!(resource_state.begin_prepass(), Ok(())); + assert_eq!(resource_state.open_external_phase(), Ok(())); + assert_eq!(resource_state.begin_drain(), Ok(())); + let pass = Actors::execute_cycle_to_cutoff_with_resources( + Weight::MAX, + Actors::next_queue_ticket(), + &mut resource_state, + budget.limits(), + crate::BlockResourceDomain::ActorDrainEffect, + budget.limits().actor_control(), + ); + + assert_eq!( + pass.consumed, + scan + .saturating_mul(2) + .saturating_add(probe) + .saturating_add(consume) + .saturating_add(resources.control), + ); + assert_eq!( + pass.reconciled_domains(), + Some((pass.consumed, Weight::zero())) + ); + assert_eq!(resource_state.outstanding_reservations(), 0); + assert_eq!(resource_state.usage().actor_control_used(), pass.consumed); + assert_eq!(resource_state.usage().actor_effect_used(), Weight::zero()); + assert_eq!(native_balance(&BOB), bob_before); + assert!(has_actor_event(|event| matches!( + event, + Event::StepSkipped { + actor_id: id, + reason: StepSkippedReason::PreconditionFalse, + .. + } if *id == actor_id + ))); + }); +} + +#[test] +fn successful_actor_pass_separates_actual_effect_from_control() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let task = Task::Transfer { + to: BOB, + asset: TestAsset::Native, + amount: AmountResolution::Fixed(10), + }; + let actor_id = create_system_with( + ALICE, + manual_schedule(), + None, + contract_steps_with_step(StepOf:: { + precondition: all_conditions(vec![Predicate::BlockNumberAbove { threshold: 0 }]), + task: task.clone(), + on_error: StepErrorPolicy::AbortCycle, + }), + ); + fund_native(actor_id, 100); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + + let budget = TestBlockResourceBudget::get(); + let mut resource_state = crate::BlockResourceState::new(1); + assert_eq!(resource_state.begin_prepass(), Ok(())); + assert_eq!(resource_state.open_external_phase(), Ok(())); + assert_eq!(resource_state.begin_drain(), Ok(())); + let pass = Actors::execute_cycle_to_cutoff_with_resources( + Weight::MAX, + Actors::next_queue_ticket(), + &mut resource_state, + budget.limits(), + crate::BlockResourceDomain::ActorDrainEffect, + budget.limits().actor_control(), + ); + let expected_effect = + >::actual_effect_weight( + &task, + crate::TaskEffectExecution::Invoked, + ) + .expect("mock invoked effect has actual evidence"); // deos-bypass: panic-owner — mock provider defines total actual evidence for every invoked task. + assert_eq!(pass.effect_consumed, expected_effect); + let (control, effect) = pass + .reconciled_domains() + .expect("successful pass has complete effect evidence"); // deos-bypass: panic-owner — successful commit returns typed actual control/effect evidence. + assert_eq!(effect, expected_effect); + assert_eq!(control.saturating_add(effect), pass.consumed); + assert_eq!(resource_state.outstanding_reservations(), 0); + assert_eq!(resource_state.usage().actor_control_used(), control); + assert_eq!(resource_state.usage().actor_effect_used(), effect); + }); +} + +#[test] +fn valid_actual_control_replaces_the_maximum_in_pass_consumption() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = create_system_with( + ALICE, + manual_schedule(), + None, + contract_steps_with_step(StepOf:: { + precondition: all_conditions(vec![Predicate::BlockNumberBelow { threshold: 0 }]), + task: Task::Transfer { + to: BOB, + asset: TestAsset::Native, + amount: AmountResolution::Fixed(10), + }, + on_error: StepErrorPolicy::AbortCycle, + }), + ); + assert_ok!(Actors::manual_trigger(RuntimeOrigin::signed(ALICE), actor_id)); + let resources = Actors::load_current_step_from_storage(actor_id, 0) + .expect("current Step resources exist") + .resources; + let actual_control = resources + .control + .checked_sub(&Weight::from_parts(1, 1)) + .expect("mock control maximum is nonzero"); + set_step_control_actual_weight_override(Some(actual_control)); + let scan = <::WeightInfo as crate::WeightInfo>::scheduler_paged_tombstone_drain(1); + let probe = Actors::scheduler_actor_probe_weight_upper(); + let consume = <::WeightInfo as crate::WeightInfo>::scheduler_paged_consume_preserve_page() + .max(<::WeightInfo as crate::WeightInfo>::scheduler_paged_consume_delete_page()); + + let pass = Actors::execute_cycle(Weight::MAX); + + assert_eq!( + pass.consumed, + scan + .saturating_mul(2) + .saturating_add(probe) + .saturating_add(consume) + .saturating_add(actual_control), + ); + }); +} + +#[test] +fn valid_zero_actual_control_charges_pipeline_but_no_action_fee() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = create_user_with( + ALICE, + Mutability::Mutable, + manual_schedule(), + None, + contract_steps_with_step(StepOf:: { + precondition: all_conditions(vec![Predicate::BlockNumberBelow { threshold: 0 }]), + task: Task::Transfer { + to: BOB, + asset: TestAsset::Native, + amount: AmountResolution::Fixed(10), + }, + on_error: StepErrorPolicy::AbortCycle, + }), + ); + fund_native(actor_id, 1_000); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + set_step_control_actual_weight_override(Some(Weight::zero())); + clear_fee_collections(); + let actor_before = native_balance(&sovereign_account(actor_id)); + + let pass = Actors::execute_cycle(Weight::MAX); + + assert!(!pass.starved); + let pipeline_fee = pipeline_opening_fee( + &Actors::active_actor_view(actor_id) + .expect("Actor remains") + .steps, + ); + assert_eq!(fee_collections(), vec![pipeline_fee]); + assert_eq!( + native_balance(&sovereign_account(actor_id)), + actor_before.saturating_sub(pipeline_fee) + ); + assert!(has_actor_event(|event| matches!( + event, + Event::StepSkipped { + actor_id: id, + reason: StepSkippedReason::PreconditionFalse, + .. + } if *id == actor_id + ))); + assert!(!has_actor_event(|event| matches!( + event, + Event::ActionFeeCharged { actor_id: id, .. } if *id == actor_id + ))); + }); +} + +#[test] +fn valid_zero_actual_effect_releases_effect_fee_after_invocation() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = create_user_with( + ALICE, + Mutability::Mutable, + manual_schedule(), + None, + transfer_contract_steps(BOB, 10), + ); + fund_native(actor_id, 1_000); + let resources = Actors::load_current_step_from_storage(actor_id, 0) + .expect("current Step resources exist") + .resources; + let expected_action_fee = Actors::step_fee_for_resources( + ActorType::User, + crate::ActorStepResourceEnvelope { + control: resources.control, + effect: Weight::zero(), + }, + ) + .expect("actual Action fee is bounded") + .total_fee; + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + set_task_effect_actual_weight_override(Some(Weight::zero())); + clear_fee_collections(); + let bob_before = native_balance(&BOB); + + let pass = Actors::execute_cycle(Weight::MAX); + + assert!(!pass.starved); + assert_eq!(expected_action_fee, 0); + let pipeline_fee = pipeline_opening_fee( + &Actors::active_actor_view(actor_id) + .expect("Actor remains") + .steps, + ); + assert_eq!(fee_collections(), vec![pipeline_fee]); + assert_eq!(native_balance(&BOB), bob_before.saturating_add(10)); + assert!(has_actor_event(|event| matches!( + event, + Event::ActionFeeCharged { + actor_id: id, + cycle_nonce: 1, + step_index: 0, + actual_effect_weight, + fee: 0, + } if *id == actor_id && *actual_effect_weight == Weight::zero() + ))); + }); +} + +fn assert_missing_actual_weight_rolls_back_without_fee_collection(missing_control: bool) { new_test_ext().execute_with(|| { frame_system::Pallet::::set_block_number(1); - let prefunded = user_prefunding_requirement(&transfer_contract_steps(BOB, 10)); let actor_id = create_user_with( ALICE, Mutability::Mutable, @@ -581,83 +1744,142 @@ fn cycle_closes_with_fee_budget_exhausted_when_unfunded() { None, transfer_contract_steps(BOB, 10), ); - deplete_user_sovereign(actor_id, prefunded); - fund_native(actor_id, TestMinUserBalance::get()); + fund_native(actor_id, 1_000); assert_ok!(Actors::manual_trigger( RuntimeOrigin::signed(ALICE), actor_id )); - run_idle(Weight::MAX); - assert!(Actors::active_actor_view(actor_id).is_none()); - assert!(has_actor_event(|event| { - matches!( - event, - Event::ActorClosed { - actor_id: id, - reason: CloseReason::FeeBudgetExhausted, - } if *id == actor_id - ) - })); + if missing_control { + set_missing_step_control_actual_weight(true); + } else { + set_missing_task_effect_actual_weight(true); + } + frame_system::Pallet::::reset_events(); + clear_fee_collections(); + let bob_before = native_balance(&BOB); + let root_before = + polkadot_sdk::sp_io::storage::root(polkadot_sdk::sp_runtime::StateVersion::V1); + + let budget = TestBlockResourceBudget::get(); + let mut resource_state = crate::BlockResourceState::new(1); + assert_eq!(resource_state.begin_prepass(), Ok(())); + assert_eq!(resource_state.open_external_phase(), Ok(())); + assert_eq!(resource_state.begin_drain(), Ok(())); + let pass = Actors::execute_cycle_to_cutoff_with_resources( + Weight::MAX, + Actors::next_queue_ticket(), + &mut resource_state, + budget.limits(), + crate::BlockResourceDomain::ActorDrainEffect, + budget.limits().actor_control(), + ); + + assert!(pass.starved); + assert_eq!(pass.reconciled_domains(), None); + assert!(resource_state.optional_actor_work_halted()); + assert_eq!(resource_state.outstanding_reservations(), 0); + assert_eq!( + polkadot_sdk::sp_io::storage::root(polkadot_sdk::sp_runtime::StateVersion::V1), + root_before, + ); + assert_eq!(native_balance(&BOB), bob_before); + let pipeline_fee = pipeline_opening_fee( + &Actors::active_actor_view(actor_id) + .expect("Actor remains") + .steps, + ); + assert_eq!(fee_collections(), vec![pipeline_fee]); + assert!(frame_system::Pallet::::events().is_empty()); }); } #[test] -fn balance_exhausted_takes_precedence_over_fee_budget_exhausted() { +fn missing_actual_effect_weight_rolls_back_without_fee_collection() { + assert_missing_actual_weight_rolls_back_without_fee_collection(false); +} + +#[test] +fn missing_actual_control_weight_rolls_back_without_fee_collection() { + assert_missing_actual_weight_rolls_back_without_fee_collection(true); +} + +#[test] +fn greater_than_reserved_actual_effect_weight_rolls_back_the_complete_attempt() { new_test_ext().execute_with(|| { frame_system::Pallet::::set_block_number(1); - let prefunded = user_prefunding_requirement(&transfer_contract_steps(BOB, 10)); - let actor_id = create_user_with( + let actor_id = create_system_with( ALICE, - Mutability::Mutable, manual_schedule(), None, transfer_contract_steps(BOB, 10), ); - deplete_user_sovereign(actor_id, prefunded); - fund_native(actor_id, TestMinUserBalance::get() - 1); + fund_native(actor_id, 100); assert_ok!(Actors::manual_trigger( RuntimeOrigin::signed(ALICE), actor_id )); - run_idle(Weight::MAX); - assert!(Actors::active_actor_view(actor_id).is_none()); - assert!(has_actor_event(|event| { - matches!( - event, - Event::ActorClosed { - actor_id: id, - reason: CloseReason::BalanceExhausted, - } if *id == actor_id - ) - })); + set_task_effect_actual_weight_override(Some(Weight::from_parts(34, 44))); + frame_system::Pallet::::reset_events(); + let bob_before = native_balance(&BOB); + let root_before = + polkadot_sdk::sp_io::storage::root(polkadot_sdk::sp_runtime::StateVersion::V1); + + let pass = Actors::execute_cycle(Weight::MAX); + + assert!(pass.starved); + assert_eq!( + polkadot_sdk::sp_io::storage::root(polkadot_sdk::sp_runtime::StateVersion::V1), + root_before, + ); + assert_eq!(native_balance(&BOB), bob_before); + assert!(frame_system::Pallet::::events().is_empty()); + assert!(Actors::actor_hot(actor_id).is_some_and(|hot| hot.queue_ticket.is_some())); }); } #[test] -fn fee_insufficiency_is_terminal_without_deferral_guard() { +fn greater_than_reserved_actual_control_weight_rolls_back_the_complete_attempt() { new_test_ext().execute_with(|| { frame_system::Pallet::::set_block_number(1); - let prefunded = user_prefunding_requirement(&transfer_contract_steps(BOB, 10)); - let actor_id = create_user_with( + let actor_id = create_system_with( ALICE, - Mutability::Mutable, manual_schedule(), None, transfer_contract_steps(BOB, 10), ); - deplete_user_sovereign(actor_id, prefunded); - fund_native(actor_id, TestMinUserBalance::get()); + fund_native(actor_id, 100); assert_ok!(Actors::manual_trigger( RuntimeOrigin::signed(ALICE), actor_id )); - run_idle(Weight::MAX); - assert!(Actors::active_actor_view(actor_id).is_none()); + let reserved = Actors::load_current_step_from_storage(actor_id, 0) + .expect("current Step resources exist") + .resources + .control; + set_step_control_actual_weight_override(Some(Weight::from_parts( + reserved.ref_time().saturating_add(1), + reserved.proof_size(), + ))); + frame_system::Pallet::::reset_events(); + let bob_before = native_balance(&BOB); + let root_before = + polkadot_sdk::sp_io::storage::root(polkadot_sdk::sp_runtime::StateVersion::V1); + + let pass = Actors::execute_cycle(Weight::MAX); + + assert!(pass.starved); + assert_eq!( + polkadot_sdk::sp_io::storage::root(polkadot_sdk::sp_runtime::StateVersion::V1), + root_before, + ); + assert_eq!(native_balance(&BOB), bob_before); + assert!(frame_system::Pallet::::events().is_empty()); + assert!(Actors::actor_hot(actor_id).is_some_and(|hot| hot.queue_ticket.is_some())); }); } #[test] -fn condition_skip_fee_route_failure_aborts_before_skip_event() { +fn condition_skip_pipeline_fee_failure_aborts_before_skip_event() { new_test_ext().execute_with(|| { frame_system::Pallet::::set_block_number(1); let step = StepOf:: { @@ -681,18 +1903,23 @@ fn condition_skip_fee_route_failure_aborts_before_skip_event() { ); fund_native(actor_id, 1_000_000_000); let bob_before = native_balance(&BOB); - let fee_sink_before = native_balance(&TestFeeSink::get()); - clear_fee_collections(); - set_fail_fee_sink_transfer(true); assert_ok!(Actors::manual_trigger( RuntimeOrigin::signed(ALICE), actor_id )); + let fee_sink_before = native_balance(&TestFeeSink::get()); + let pipeline_fee = pipeline_opening_fee( + &Actors::active_actor_view(actor_id) + .expect("Actor remains") + .steps, + ); + clear_fee_collections(); + set_fail_fee_sink_transfer(true); run_idle(Weight::MAX); set_fail_fee_sink_transfer(false); assert_eq!(native_balance(&BOB), bob_before); assert_eq!(native_balance(&TestFeeSink::get()), fee_sink_before); - assert_eq!(fee_collections(), vec![Actors::compute_eval_fee(1)]); + assert_eq!(fee_collections(), vec![pipeline_fee]); assert!(!has_actor_event(|event| { matches!(event, Event::StepFailed { actor_id: id, step_index: 0, .. } if *id == actor_id) })); @@ -709,7 +1936,7 @@ fn condition_skip_fee_route_failure_aborts_before_skip_event() { } #[test] -fn combined_step_fee_route_failure_aborts_before_task_execution() { +fn pipeline_fee_route_failure_aborts_before_task_execution() { new_test_ext().execute_with(|| { frame_system::Pallet::::set_block_number(1); let task = Task::Transfer { @@ -726,21 +1953,23 @@ fn combined_step_fee_route_failure_aborts_before_task_execution() { ); fund_native(actor_id, 1_000_000_000); let bob_before = native_balance(&BOB); - let fee_sink_before = native_balance(&TestFeeSink::get()); - clear_fee_collections(); - set_fail_fee_sink_transfer(true); assert_ok!(Actors::manual_trigger( RuntimeOrigin::signed(ALICE), actor_id )); + let fee_sink_before = native_balance(&TestFeeSink::get()); + let pipeline_fee = pipeline_opening_fee( + &Actors::active_actor_view(actor_id) + .expect("Actor remains") + .steps, + ); + clear_fee_collections(); + set_fail_fee_sink_transfer(true); run_idle(Weight::MAX); set_fail_fee_sink_transfer(false); assert_eq!(native_balance(&BOB), bob_before); assert_eq!(native_balance(&TestFeeSink::get()), fee_sink_before); - let expected = Actors::compute_eval_fee(0).saturating_add(TestWeightToFee::weight_to_fee( - &Actors::weight_upper_bound(&task), - )); - assert_eq!(fee_collections(), vec![expected]); + assert_eq!(fee_collections(), vec![pipeline_fee]); assert!(!has_actor_event(|event| { matches!(event, Event::StepFailed { actor_id: id, step_index: 0, .. } if *id == actor_id) })); @@ -852,13 +2081,12 @@ fn stop_cycle_fee_failure_rolls_back_before_step_policy_or_stop() { contract_steps_with_step(make_step(Task::StopCycle)), ); fund_native(actor_id, 100_000); - let sink_before = native_balance(&TestFeeSink::get()); - set_fail_fee_sink_transfer(true); - assert_ok!(Actors::manual_trigger( RuntimeOrigin::signed(ALICE), actor_id )); + let sink_before = native_balance(&TestFeeSink::get()); + set_fail_fee_sink_transfer(true); run_idle(Weight::MAX); assert!(!has_actor_event(|event| matches!( @@ -885,10 +2113,10 @@ fn stop_cycle_fee_failure_rolls_back_before_step_policy_or_stop() { )); run_idle(Weight::MAX); - let task: TaskOf = Task::StopCycle; - let task_weight = Actors::weight_upper_bound(&task); - let expected_fee = Actors::compute_eval_fee(0).saturating_add( - ::weight_to_fee(&task_weight), + let expected_fee = pipeline_opening_fee( + &Actors::active_actor_view(actor_id) + .expect("Actor remains") + .steps, ); assert_eq!( native_balance(&TestFeeSink::get()), @@ -969,7 +2197,7 @@ fn retry_later_funding_unavailable_resumes_without_new_logical_run() { actor_id )); run_idle(Weight::MAX); - let suspended = Actors::continuation_state(actor_id).expect("funding retry persists"); + let suspended = Actors::actor_run_state(actor_id).expect("funding retry persists"); assert_eq!(suspended.cursor, 0); assert_eq!(suspended.cumulative_outcomes.failed_steps, 0); assert_eq!( @@ -985,7 +2213,7 @@ fn retry_later_funding_unavailable_resumes_without_new_logical_run() { let completed = Actors::active_actor_view(actor_id).expect("actor completes"); assert_eq!(completed.cycle_nonce, 1); assert_eq!(completed.cycle_state, CycleState::Idle); - assert!(Actors::continuation_state(actor_id).is_none()); + assert!(Actors::actor_run_state(actor_id).is_none()); assert_eq!(native_balance(&BOB), bob_before + 50); }); } @@ -1105,11 +2333,19 @@ fn funding_arrival_during_suspension_accumulates_for_the_next_cycle() { actor_id )); run_idle(Weight::MAX); + let suspended = Actors::active_actor_view(actor_id).expect("suspended"); + assert_eq!(suspended.cycle_state, CycleState::Suspended); + assert_eq!(suspended.cycle_nonce, 0); + let run_state = Actors::actor_run_state(actor_id).expect("funding suspension persists"); + assert_eq!(run_state.cycle_nonce, 1); + assert!(run_state.funding_snapshot.is_empty()); assert_eq!( - Actors::active_actor_view(actor_id) - .expect("suspended") - .cycle_state, - CycleState::Suspended + run_state.last_step_outcome, + Some(StepOutcome::FundingUnavailable) + ); + assert_eq!( + run_state.suspension, + Some(SuspensionReason::FundingUnavailable) ); assert!(has_actor_event(|event| matches!( event, @@ -1136,6 +2372,12 @@ fn funding_arrival_during_suspension_accumulates_for_the_next_cycle() { funding.funding_accumulated.get(&TestAsset::Native), Some(&7) ); + assert!( + Actors::actor_run_state(actor_id) + .expect("later funding does not mutate the open snapshot") + .funding_snapshot + .is_empty() + ); assert_eq!( Actors::actor_hot(actor_id) .expect("suspended actor") @@ -1181,7 +2423,7 @@ fn suspended_cycle_freezes_its_funding_snapshot_and_preserves_new_accumulation() )); set_temporary_dex_failure(true); run_idle(Weight::MAX); - let continuation = Actors::continuation_state(actor_id).expect("suspended"); + let continuation = Actors::actor_run_state(actor_id).expect("suspended"); assert!(continuation.opening_snapshot.is_empty()); assert_eq!(continuation.funding_snapshot.get(&asset_in), Some(&100)); @@ -1195,7 +2437,7 @@ fn suspended_cycle_freezes_its_funding_snapshot_and_preserves_new_accumulation() frame_system::Pallet::::set_block_number(2); run_idle(Weight::MAX); assert_eq!( - Actors::continuation_state(actor_id) + Actors::actor_run_state(actor_id) .expect("still suspended") .funding_snapshot .get(&asset_in), @@ -1210,7 +2452,7 @@ fn suspended_cycle_freezes_its_funding_snapshot_and_preserves_new_accumulation() set_temporary_dex_failure(false); frame_system::Pallet::::set_block_number(4); run_idle(Weight::MAX); - assert!(Actors::continuation_state(actor_id).is_none()); + assert!(Actors::actor_run_state(actor_id).is_none()); assert_eq!( actor_funding(actor_id).funding_accumulated.get(&asset_in), Some(&30) @@ -1271,7 +2513,7 @@ fn update_contract_prunes_stale_funding_accumulators() { } #[test] -fn permissionless_sweep_closes_user_below_min_balance() { +fn permissionless_sweep_preserves_user_below_future_pipeline_minimum() { new_test_ext().execute_with(|| { frame_system::Pallet::::set_block_number(1); let prefunded = user_prefunding_requirement(&transfer_contract_steps(BOB, 1)); @@ -1287,21 +2529,16 @@ fn permissionless_sweep_closes_user_below_min_balance() { RuntimeOrigin::signed(BOB), actor_id )); - assert!(Actors::active_actor_view(actor_id).is_none()); - assert!(has_actor_event(|event| { - matches!( - event, - Event::ActorClosed { - actor_id: id, - reason: CloseReason::BalanceExhausted, - } if *id == actor_id - ) - })); + assert!(Actors::active_actor_view(actor_id).is_some()); + assert!(!has_actor_event(|event| matches!( + event, + Event::ActorClosed { actor_id: id, .. } if *id == actor_id + ))); }); } #[test] -fn user_resolution_skip_charges_only_eval_fee() { +fn user_resolution_skip_charges_pipeline_but_no_action_fee() { new_test_ext().execute_with(|| { frame_system::Pallet::::set_block_number(1); let contract_steps = contract_steps_with_step(make_step(Task::Transfer { @@ -1318,16 +2555,21 @@ fn user_resolution_skip_charges_only_eval_fee() { ); let actor = sovereign_account(actor_id); fund_native(actor_id, 1_000); - let before = native_balance(&actor); - clear_fee_collections(); assert_ok!(Actors::manual_trigger( RuntimeOrigin::signed(ALICE), actor_id )); + let before = native_balance(&actor); + clear_fee_collections(); run_idle(Weight::MAX); let after = native_balance(&actor); - assert_eq!(after, before.saturating_sub(Actors::compute_eval_fee(0))); - assert_eq!(fee_collections(), vec![Actors::compute_eval_fee(0)]); + let pipeline_fee = pipeline_opening_fee( + &Actors::active_actor_view(actor_id) + .expect("Actor remains") + .steps, + ); + assert_eq!(after, before.saturating_sub(pipeline_fee)); + assert_eq!(fee_collections(), vec![pipeline_fee]); assert!(has_actor_event(|event| { matches!( event, @@ -1343,7 +2585,7 @@ fn user_resolution_skip_charges_only_eval_fee() { } #[test] -fn condition_skip_charges_one_evaluation_fee() { +fn condition_skip_charges_pipeline_but_no_action_fee() { new_test_ext().execute_with(|| { frame_system::Pallet::::set_block_number(1); let step = StepOf:: { @@ -1366,13 +2608,18 @@ fn condition_skip_charges_one_evaluation_fee() { contract_steps_with_step(step), ); fund_native(actor_id, 1_000); - clear_fee_collections(); assert_ok!(Actors::manual_trigger( RuntimeOrigin::signed(ALICE), actor_id )); + clear_fee_collections(); run_idle(Weight::MAX); - assert_eq!(fee_collections(), vec![Actors::compute_eval_fee(1)]); + let pipeline_fee = pipeline_opening_fee( + &Actors::active_actor_view(actor_id) + .expect("Actor remains") + .steps, + ); + assert_eq!(fee_collections(), vec![pipeline_fee]); assert!(has_actor_event(|event| matches!( event, Event::StepSkipped { @@ -1442,13 +2689,18 @@ fn true_balance_condition_can_precede_funding_unavailable_resolution() { }), ); fund_native(actor_id, 1_000); - clear_fee_collections(); assert_ok!(Actors::manual_trigger( RuntimeOrigin::signed(ALICE), actor_id )); + clear_fee_collections(); run_idle(Weight::MAX); - assert_eq!(fee_collections(), vec![Actors::compute_eval_fee(1)]); + let pipeline_fee = pipeline_opening_fee( + &Actors::active_actor_view(actor_id) + .expect("Actor remains") + .steps, + ); + assert_eq!(fee_collections(), vec![pipeline_fee]); assert!(has_actor_event(|event| matches!( event, Event::StepSkipped { @@ -1461,7 +2713,7 @@ fn true_balance_condition_can_precede_funding_unavailable_resolution() { } #[test] -fn executable_task_charges_eval_and_execution_fees() { +fn executable_task_charges_pipeline_and_independent_action_fee() { new_test_ext().execute_with(|| { frame_system::Pallet::::set_block_number(1); let task = Task::Transfer { @@ -1479,29 +2731,42 @@ fn executable_task_charges_eval_and_execution_fees() { ); let actor = sovereign_account(actor_id); fund_native(actor_id, 1_000); - let actor_before = native_balance(&actor); - let fee_sink_before = native_balance(&TestFeeSink::get()); let task_weight = Actors::weight_upper_bound(&task); assert!(task_weight.ref_time() > 0); - let expected_fee = - Actors::compute_eval_fee(0).saturating_add(TestWeightToFee::weight_to_fee(&task_weight)); + let expected_action_fee = TestWeightToFee::weight_to_fee(&task_weight); let instance = Actors::active_actor_view(actor_id).expect("user actor"); - assert_eq!(Actors::attempt_fee_upper_bound(&instance, 0), expected_fee); - clear_fee_collections(); + let current_fee = + Actors::maximum_contract_step_fee(instance.actor_class.actor_type(), &instance.steps, 0) + .expect("current-Step fee is bounded") + .total_fee; + assert_eq!(current_fee, expected_action_fee); assert_ok!(Actors::manual_trigger( RuntimeOrigin::signed(ALICE), actor_id )); + let actor_before = native_balance(&actor); + let fee_sink_before = native_balance(&TestFeeSink::get()); + clear_fee_collections(); run_idle(Weight::MAX); + let pipeline_fee = pipeline_opening_fee( + &Actors::active_actor_view(actor_id) + .expect("Actor remains") + .steps, + ); assert_eq!( native_balance(&actor), - actor_before.saturating_sub(expected_fee).saturating_sub(1) + actor_before + .saturating_sub(pipeline_fee) + .saturating_sub(expected_action_fee) + .saturating_sub(1) ); assert_eq!( native_balance(&TestFeeSink::get()), - fee_sink_before.saturating_add(expected_fee) + fee_sink_before + .saturating_add(pipeline_fee) + .saturating_add(expected_action_fee) ); - assert_eq!(fee_collections(), vec![expected_fee]); + assert_eq!(fee_collections(), vec![pipeline_fee, expected_action_fee]); assert!(has_actor_event(|event| { matches!( event, @@ -1523,7 +2788,7 @@ fn executable_task_charges_eval_and_execution_fees() { } #[test] -fn adapter_failure_retains_one_combined_step_fee() { +fn adapter_failure_retains_pipeline_and_one_action_fee() { new_test_ext().execute_with(|| { frame_system::Pallet::::set_block_number(1); let foreign = TestAsset::Local(77); @@ -1546,23 +2811,28 @@ fn adapter_failure_retains_one_combined_step_fee() { ); let actor = sovereign_account(actor_id); fund_native_raw(&actor, 1_000); - let actor_before = native_balance(&actor); let pool_before = native_balance(&pool_account); - let expected = Actors::compute_eval_fee(0).saturating_add(TestWeightToFee::weight_to_fee( - &Actors::weight_upper_bound(&task), - )); - clear_fee_collections(); + let expected_action_fee = TestWeightToFee::weight_to_fee(&Actors::weight_upper_bound(&task)); set_fail_dex_after_input_transfer(true); assert_ok!(Actors::manual_trigger( RuntimeOrigin::signed(ALICE), actor_id )); + let actor_before = native_balance(&actor); + clear_fee_collections(); run_idle(Weight::MAX); set_fail_dex_after_input_transfer(false); - assert_eq!(fee_collections(), vec![expected]); + let pipeline_fee = pipeline_opening_fee( + &Actors::active_actor_view(actor_id) + .expect("Actor remains") + .steps, + ); + assert_eq!(fee_collections(), vec![pipeline_fee, expected_action_fee]); assert_eq!( native_balance(&actor), - actor_before.saturating_sub(expected) + actor_before + .saturating_sub(pipeline_fee) + .saturating_sub(expected_action_fee) ); assert_eq!(native_balance(&pool_account), pool_before); assert!(has_actor_event(|event| { @@ -1580,7 +2850,6 @@ fn cycle_summary_fee_fairness_property_matrix() { (1_000u128, Perbill::from_percent(10), false), (10_000u128, Perbill::from_percent(50), false), ]; - let eval_fee = Actors::compute_eval_fee(0); for (idx, (funding, pct, expect_skip)) in cases.into_iter().enumerate() { let task = Task::Transfer { to: BOB, @@ -1588,6 +2857,7 @@ fn cycle_summary_fee_fairness_property_matrix() { amount: AmountResolution::PercentageOfCurrent(pct), }; let contract_steps = contract_steps_with_step(make_step(task.clone())); + let pipeline_fee = pipeline_opening_fee(&contract_steps); let actor_id = create_user_with( ALICE, Mutability::Mutable, @@ -1596,11 +2866,11 @@ fn cycle_summary_fee_fairness_property_matrix() { contract_steps, ); fund_native(actor_id, funding); - let fee_sink_before = native_balance(&TestFeeSink::get()); assert_ok!(Actors::manual_trigger( RuntimeOrigin::signed(ALICE), actor_id )); + let fee_sink_before = native_balance(&TestFeeSink::get()); run_idle(Weight::MAX); let fee_sink_after = native_balance(&TestFeeSink::get()); let fee_delta = fee_sink_after.saturating_sub(fee_sink_before); @@ -1628,7 +2898,7 @@ fn cycle_summary_fee_fairness_property_matrix() { assert_eq!(summary.2, 1); assert_eq!(summary.3, 0); assert_eq!(summary.4, 0); - assert_eq!(fee_delta, eval_fee); + assert_eq!(fee_delta, pipeline_fee); } else { let exec_fee = ::weight_to_fee( &Actors::weight_upper_bound(&task), @@ -1638,7 +2908,7 @@ fn cycle_summary_fee_fairness_property_matrix() { assert_eq!(summary.2, 0); assert_eq!(summary.3, 0); assert_eq!(summary.4, 0); - assert_eq!(fee_delta, eval_fee.saturating_add(exec_fee)); + assert_eq!(fee_delta, pipeline_fee.saturating_add(exec_fee)); } frame_system::Pallet::::set_block_number((idx as u64).saturating_add(2)); } @@ -1767,7 +3037,7 @@ fn system_keeps_running_on_last_funding_exhaustion_and_accepts_refill() { } #[test] -fn user_closes_when_the_full_fee_envelope_cannot_fit_above_the_floor() { +fn paid_readiness_closes_only_when_pipeline_charge_cannot_fit_above_floor() { new_test_ext().execute_with(|| { frame_system::Pallet::::set_block_number(1); let contract_steps = contract_steps_with_step(make_step(Task::Transfer { @@ -1776,6 +3046,7 @@ fn user_closes_when_the_full_fee_envelope_cannot_fit_above_the_floor() { amount: AmountResolution::PercentageOfLastFunding(Perbill::from_percent(50)), })); let prefunded = user_prefunding_requirement(&contract_steps); + let pipeline_fee = pipeline_opening_fee(&contract_steps); let actor_id = create_user_with( ALICE, Mutability::Mutable, @@ -1790,16 +3061,15 @@ fn user_closes_when_the_full_fee_envelope_cannot_fit_above_the_floor() { TestAsset::Native, 500 )); + fund_native(actor_id, manual_trigger_fee().saturating_add(pipeline_fee)); let bob_before = native_balance(&BOB); assert_ok!(Actors::manual_trigger( RuntimeOrigin::signed(ALICE), actor_id )); run_idle(Weight::MAX); - // After the first run transfers 250 and charges the fee envelope (~101), the - // remaining balance cannot fit the full envelope above MinUserBalance, so the - // second run is NOT admitted and the actor closes with FeeBudgetExhausted - // instead of running with a step skip (spec 5.2.1). + // The first Pipeline and Action leave exactly enough for another Trigger occurrence, + // but not enough to admit another complete Pipeline above MinUserBalance. frame_system::Pallet::::set_block_number(2); assert_ok!(Actors::manual_trigger( RuntimeOrigin::signed(ALICE), @@ -1811,7 +3081,7 @@ fn user_closes_when_the_full_fee_envelope_cannot_fit_above_the_floor() { event, Event::ActorClosed { actor_id: id, - reason: CloseReason::FeeBudgetExhausted, + reason: CloseReason::CycleAdmissionInsufficient, .. } if *id == actor_id ) @@ -1926,7 +3196,7 @@ fn default_funding_policies_authorize_system_runtime_sources_but_only_user_owner )); let sys_inst = actor_funding(system_actor); assert!(matches!( - ActorContracts::::get(system_actor) + Actors::load_actor_contract(system_actor) .expect("system Actor Contract") .funding, FundingSourcePolicy::RuntimePolicy @@ -1937,7 +3207,7 @@ fn default_funding_policies_authorize_system_runtime_sources_but_only_user_owner ); let user_inst = actor_funding(user_actor); assert!(matches!( - ActorContracts::::get(user_actor) + Actors::load_actor_contract(user_actor) .expect("user Actor Contract") .funding, FundingSourcePolicy::OwnerOnly @@ -2331,7 +3601,7 @@ fn funding_policy_replacement_preserves_placement_without_continuation() { frame_system::Pallet::::set_block_number(1); let actor_id = create_system_with(ALICE, timer_schedule(10), None, inert_contract_steps()); let before = Actors::actor_hot(actor_id).expect("actor hot state exists"); - assert!(before.wakeup_pointer.is_some()); + assert!(before.trigger_wakeup_pointer.is_some()); assert_ok!(update_contract_partial!( RuntimeOrigin::root(), actor_id, @@ -2340,6 +3610,7 @@ fn funding_policy_replacement_preserves_placement_without_continuation() { let after = Actors::actor_hot(actor_id).expect("actor hot state exists"); assert_eq!(after.queue_ticket, before.queue_ticket); assert_eq!(after.wakeup_pointer, before.wakeup_pointer); + assert_eq!(after.trigger_wakeup_pointer, before.trigger_wakeup_pointer); }); } @@ -2499,6 +3770,7 @@ fn overspend_resolves_to_funding_unavailable_for_user_without_closing() { amount: AmountResolution::Fixed(1_000_000), })); let prefunded = user_prefunding_requirement(&contract_steps); + let pipeline_fee = pipeline_opening_fee(&contract_steps); let actor_id = create_user_with( ALICE, Mutability::Mutable, @@ -2508,7 +3780,7 @@ fn overspend_resolves_to_funding_unavailable_for_user_without_closing() { ); deplete_user_sovereign(actor_id, prefunded); let actor = sovereign_account(actor_id); - fund_native(actor_id, 1_000); + fund_native(actor_id, 1_000 + manual_trigger_fee() + pipeline_fee); assert_ok!(Actors::manual_trigger( RuntimeOrigin::signed(ALICE), actor_id @@ -2526,15 +3798,12 @@ fn overspend_resolves_to_funding_unavailable_for_user_without_closing() { ) })); assert!(Actors::active_actor_view(actor_id).is_some()); - assert_eq!( - native_balance(&actor), - 1_000u128.saturating_sub(Actors::compute_eval_fee(0)) - ); + assert_eq!(native_balance(&actor), 1_000); }); } #[test] -fn funding_unavailable_releases_exec_fee_reservation_for_later_step_spend() { +fn funding_unavailable_releases_action_fee_reservation_for_later_step_spend() { new_test_ext().execute_with(|| { frame_system::Pallet::::set_block_number(1); let contract_steps = BoundedVec::try_from(vec![ @@ -2551,6 +3820,7 @@ fn funding_unavailable_releases_exec_fee_reservation_for_later_step_spend() { ]) .expect("execution plan must fit"); let prefunded = user_prefunding_requirement(&contract_steps); + let pipeline_fee = pipeline_opening_fee(&contract_steps); let actor_id = create_user_with( ALICE, Mutability::Mutable, @@ -2561,12 +3831,12 @@ fn funding_unavailable_releases_exec_fee_reservation_for_later_step_spend() { deplete_user_sovereign(actor_id, prefunded); let actor = sovereign_account(actor_id); let bob_before = native_balance(&BOB); - fund_native(actor_id, 1_000); - clear_fee_collections(); + fund_native(actor_id, 1_000 + manual_trigger_fee() + pipeline_fee); assert_ok!(Actors::manual_trigger( RuntimeOrigin::signed(ALICE), actor_id )); + clear_fee_collections(); run_idle(Weight::MAX); assert!(has_actor_event(|event| { matches!( @@ -2585,32 +3855,30 @@ fn funding_unavailable_releases_exec_fee_reservation_for_later_step_spend() { asset: TestAsset::Native, amount: AmountResolution::Fixed(650), }; - let expected_attempt_fee = Actors::compute_eval_fee(0).saturating_add( - TestWeightToFee::weight_to_fee(&Actors::weight_upper_bound(&transfer)), - ); - assert_eq!( - fee_collections(), - vec![Actors::compute_eval_fee(0), expected_attempt_fee] - ); + let expected_attempt_fee = + TestWeightToFee::weight_to_fee(&Actors::weight_upper_bound(&transfer)); + assert_eq!(fee_collections(), vec![pipeline_fee, expected_attempt_fee]); assert_eq!( native_balance(&actor), - 50, - "later step can spend the execution-fee reservation released by the funding skip" + 250, + "later Step spends custody released by the non-invoked first Action reservation" ); }); } #[test] -fn failed_executable_step_charges_eval_and_exec_fee_without_refund() { +fn failed_invoked_action_charges_effect_fee_without_refund() { new_test_ext().execute_with(|| { frame_system::Pallet::::set_block_number(1); - let contract_steps = contract_steps_with_step(make_step(Task::SwapIn { + let task = Task::SwapIn { asset_in: TestAsset::Native, asset_out: TestAsset::Local(99), amount_in: AmountResolution::Fixed(10), slippage_tolerance: Perbill::zero(), - })); + }; + let contract_steps = contract_steps_with_step(make_step(task.clone())); let prefunded = user_prefunding_requirement(&contract_steps); + let pipeline_fee = pipeline_opening_fee(&contract_steps); let actor_id = create_user_with( ALICE, Mutability::Mutable, @@ -2620,24 +3888,23 @@ fn failed_executable_step_charges_eval_and_exec_fee_without_refund() { ); deplete_user_sovereign(actor_id, prefunded); let actor = sovereign_account(actor_id); - fund_native(actor_id, 1_000); + fund_native(actor_id, 1_000 + manual_trigger_fee() + pipeline_fee); assert_ok!(Actors::manual_trigger( RuntimeOrigin::signed(ALICE), actor_id )); run_idle(Weight::MAX); - let expected_fee = Actors::compute_eval_fee(0).saturating_add(TestWeightToFee::weight_to_fee( - &Actors::weight_upper_bound(&Task::SwapIn { - asset_in: TestAsset::Native, - asset_out: TestAsset::Local(99), - amount_in: AmountResolution::Fixed(10), - slippage_tolerance: Perbill::zero(), - }), - )); + let expected_weight = + >::actual_effect_weight( + &task, + crate::TaskEffectExecution::Invoked, + ) + .expect("invoked Action actual Weight exists"); + let expected_fee = TestWeightToFee::weight_to_fee(&expected_weight); assert_eq!( native_balance(&actor), 1_000u128.saturating_sub(expected_fee), - "failed executable path should charge exactly eval+exec fee with no refund" + "failed invoked Action charges exactly its effect fee with no refund" ); assert!(has_actor_event(|event| { matches!( @@ -2649,28 +3916,31 @@ fn failed_executable_step_charges_eval_and_exec_fee_without_refund() { } if *id == actor_id ) })); + let receipt = System::events() + .iter() + .find_map(|record| match &record.event { + RuntimeEvent::Actors(Event::ActionFeeCharged { + actor_id, + cycle_nonce, + step_index, + actual_effect_weight, + fee, + }) => Some(( + *actor_id, + *cycle_nonce, + *step_index, + *actual_effect_weight, + *fee, + )), + _ => None, + }); + assert_eq!( + receipt, + Some((actor_id, 1, 0, expected_weight, expected_fee)) + ); }); } -#[test] -fn condition_fee_depends_only_on_total_atomic_count() { - let forward = all_conditions(vec![ - Predicate::BlockNumberAbove { threshold: 1 }, - Predicate::BlockNumberBelow { threshold: 10 }, - ]); - let reverse = any_conditions(vec![ - Predicate::BlockNumberBelow { threshold: 10 }, - Predicate::BlockNumberAbove { threshold: 1 }, - ]); - let forward = forward.expect("bounded precondition"); - let reverse = reverse.expect("bounded precondition"); - assert_eq!(forward.predicate_count(), reverse.predicate_count()); - assert_eq!( - Actors::compute_eval_fee(forward.predicate_count()), - Actors::compute_eval_fee(reverse.predicate_count()) - ); -} - #[test] fn condition_balance_above_skips_when_below() { new_test_ext().execute_with(|| { @@ -2875,7 +4145,7 @@ fn percentage_of_current_uses_native_preservable_balance_as_its_base() { } #[test] -fn user_all_available_preserves_current_floor_but_no_future_cycle_fee() { +fn user_all_available_preserves_floor_and_underfunded_future_trigger_keeps_process() { new_test_ext().execute_with(|| { frame_system::Pallet::::set_block_number(1); let task = Task::Transfer { @@ -2885,8 +4155,9 @@ fn user_all_available_preserves_current_floor_but_no_future_cycle_fee() { }; let contract_steps = contract_steps_with_step(make_step(task)); let fee = Actors::attempt_fee_envelope(ActorType::User, &contract_steps, 0) - .expect("User fee envelope") + .expect("User Action fee envelope") .total; + let pipeline_fee = pipeline_opening_fee(&contract_steps); let prefunded = user_prefunding_requirement(&contract_steps); let actor_id = create_user_with( ALICE, @@ -2898,36 +4169,32 @@ fn user_all_available_preserves_current_floor_but_no_future_cycle_fee() { deplete_user_sovereign(actor_id, prefunded); let actor = sovereign_account(actor_id); let floor = TestMinUserBalance::get(); - let initial = floor.saturating_add(fee).saturating_add(50); - fund_native(actor_id, initial); + let pipeline_balance = floor.saturating_add(fee).saturating_add(50); + fund_native( + actor_id, + pipeline_balance + .saturating_add(manual_trigger_fee()) + .saturating_add(pipeline_fee), + ); let bob_before = native_balance(&BOB); - clear_fee_collections(); - assert_ok!(Actors::manual_trigger( RuntimeOrigin::signed(ALICE), actor_id )); + clear_fee_collections(); run_idle(Weight::MAX); assert_eq!(native_balance(&actor), floor); assert_eq!(native_balance(&BOB), bob_before.saturating_add(50)); - assert_eq!(fee_collections(), vec![fee]); + assert_eq!(fee_collections(), vec![pipeline_fee, fee]); frame_system::Pallet::::set_block_number(2); - assert_ok!(Actors::manual_trigger( - RuntimeOrigin::signed(ALICE), - actor_id - )); - run_idle(Weight::MAX); - assert!(Actors::active_actor_view(actor_id).is_none()); + assert_noop!( + Actors::manual_trigger(RuntimeOrigin::signed(ALICE), actor_id), + Error::::InsufficientFee + ); + assert!(Actors::active_actor_view(actor_id).is_some()); assert_eq!(native_balance(&BOB), bob_before.saturating_add(50)); - assert!(has_actor_event(|event| matches!( - event, - Event::ActorClosed { - actor_id: id, - reason: CloseReason::FeeBudgetExhausted, - } if *id == actor_id - ))); }); } @@ -2940,14 +4207,11 @@ fn user_fee_admission_boundary_is_exact_floor_plus_envelope() { asset: TestAsset::Native, amount: AmountResolution::Fixed(10), })); - let fee = Actors::attempt_fee_envelope(ActorType::User, &contract_steps, 0) - .expect("User fee envelope") - .total; + let pipeline_fee = pipeline_opening_fee(&contract_steps); let floor = TestMinUserBalance::get(); - // floor + envelope - 1: the complete envelope cannot fit above the floor, so - // the User attempt is NOT admitted and the actor closes with FeeBudgetExhausted - // even though the raw balance covers the envelope (spec 5.2.1). + // Floor + Pipeline charge - 1 pays the Trigger occurrence but cannot consume the + // resulting readiness into a Cycle. let prefunded = user_prefunding_requirement(&contract_steps); let short = create_user_with( ALICE, @@ -2957,20 +4221,36 @@ fn user_fee_admission_boundary_is_exact_floor_plus_envelope() { contract_steps.clone(), ); deplete_user_sovereign(short, prefunded); - fund_native(short, floor.saturating_add(fee).saturating_sub(1)); + fund_native( + short, + floor + .saturating_add(pipeline_fee) + .saturating_sub(1) + .saturating_add(manual_trigger_fee()), + ); + let short_sovereign = sovereign_account(short); + clear_fee_collections(); assert_ok!(Actors::manual_trigger(RuntimeOrigin::signed(ALICE), short)); + assert_eq!(fee_collections(), vec![manual_trigger_fee()]); + let short_after_trigger = native_balance(&short_sovereign); run_idle(Weight::MAX); + assert_eq!(fee_collections(), vec![manual_trigger_fee()]); + assert_eq!(native_balance(&short_sovereign), short_after_trigger); + assert!(!has_actor_event(|event| matches!( + event, + Event::PipelineFeeCharged { actor_id, .. } if *actor_id == short + ))); assert!(Actors::active_actor_view(short).is_none()); assert!(has_actor_event(|e| matches!( e, Event::ActorClosed { actor_id: id, - reason: CloseReason::FeeBudgetExhausted, + reason: CloseReason::CycleAdmissionInsufficient, .. } if *id == short ))); - // floor + envelope: the envelope fits exactly above the floor and the run admits. + // floor + Pipeline charge admits exactly and leaves the protected floor for Actions. let prefunded = user_prefunding_requirement(&contract_steps); let exact = create_user_with( BOB, @@ -2980,9 +4260,16 @@ fn user_fee_admission_boundary_is_exact_floor_plus_envelope() { contract_steps, ); deplete_user_sovereign(exact, prefunded); - fund_native(exact, floor.saturating_add(fee)); + fund_native( + exact, + floor + .saturating_add(pipeline_fee) + .saturating_add(manual_trigger_fee()), + ); + clear_fee_collections(); assert_ok!(Actors::manual_trigger(RuntimeOrigin::signed(BOB), exact)); run_idle(Weight::MAX); + assert_eq!(fee_collections(), vec![manual_trigger_fee(), pipeline_fee]); // The attempt is admitted (the envelope fits exactly above the floor); the // transfer itself is floor-limited, so assert admission via the nonce advance. assert_eq!( @@ -3009,8 +4296,9 @@ fn user_swap_out_native_input_cap_preserves_fee_native_floor_after_failed_attemp set_pool_reserves(TestAsset::Native, asset_out, 10_000, 10_000); set_asset_balance(&u64::MAX, asset_out, 10_000); let contract_steps = contract_steps_with_step(make_step(task)); - let fee_envelope = - Actors::attempt_fee_envelope(ActorType::User, &contract_steps, 0).expect("User fee envelope"); + let fee_envelope = Actors::attempt_fee_envelope(ActorType::User, &contract_steps, 0) + .expect("User Action fee envelope"); + let pipeline_fee = pipeline_opening_fee(&contract_steps); let prefunded = user_prefunding_requirement(&contract_steps); let actor_id = create_user_with( ALICE, @@ -3022,22 +4310,26 @@ fn user_swap_out_native_input_cap_preserves_fee_native_floor_after_failed_attemp deplete_user_sovereign(actor_id, prefunded); let actor = sovereign_account(actor_id); let floor = TestMinUserBalance::get(); - let initial = floor.saturating_add(fee_envelope.total).saturating_add(50); - fund_native(actor_id, initial); - clear_fee_collections(); - + let pipeline_balance = floor.saturating_add(fee_envelope.total).saturating_add(50); + fund_native( + actor_id, + pipeline_balance + .saturating_add(manual_trigger_fee()) + .saturating_add(pipeline_fee), + ); assert_ok!(Actors::manual_trigger( RuntimeOrigin::signed(ALICE), actor_id )); + clear_fee_collections(); run_idle(Weight::MAX); assert_eq!( native_balance(&actor), - initial.saturating_sub(fee_envelope.total) + pipeline_balance.saturating_sub(fee_envelope.total) ); assert!(native_balance(&actor) >= floor); - assert_eq!(fee_collections(), vec![fee_envelope.total]); + assert_eq!(fee_collections(), vec![pipeline_fee, fee_envelope.total]); assert!(has_actor_event(|event| { matches!(event, Event::StepFailed { actor_id: id, step_index: 0, .. } if *id == actor_id) })); @@ -3253,7 +4545,7 @@ fn unstake_last_funding_fails_closed_if_share_mapping_disappears_mid_lifetime() assert_eq!(asset_balance(&actor, asset), 100); assert_eq!(unstaked_shares(actor, asset), 0); - assert!(Actors::continuation_state(actor_id).is_none()); + assert!(Actors::actor_run_state(actor_id).is_none()); assert!(has_actor_event(|event| matches!( event, Event::StepFailed { actor_id: id, step_index: 0, .. } if *id == actor_id @@ -3277,8 +4569,9 @@ fn donate_liquidity_user_fee_native_b_side_preserves_protected_floor() { max_ratio_error: Perbill::from_percent(1), })); let fee = Actors::attempt_fee_envelope(ActorType::User, &contract_steps, 0) - .expect("User fee envelope") + .expect("User Action fee envelope") .total; + let pipeline_fee = pipeline_opening_fee(&contract_steps); let prefunded = user_prefunding_requirement(&contract_steps); let actor_id = create_user_with( ALICE, @@ -3291,13 +4584,19 @@ fn donate_liquidity_user_fee_native_b_side_preserves_protected_floor() { let actor = sovereign_account(actor_id); let floor = TestMinUserBalance::get(); set_asset_balance(&actor, asset_a, 1_000); - // Fund the floor plus the reserved User fee plus a spendable donation budget. - fund_native(actor_id, floor.saturating_add(fee).saturating_add(60)); - clear_fee_collections(); + // Fund one Trigger occurrence, then the protected floor, current Action fee, and donation. + let pipeline_balance = floor.saturating_add(fee).saturating_add(60); + fund_native( + actor_id, + pipeline_balance + .saturating_add(manual_trigger_fee()) + .saturating_add(pipeline_fee), + ); assert_ok!(Actors::manual_trigger( RuntimeOrigin::signed(ALICE), actor_id )); + clear_fee_collections(); run_idle(Weight::MAX); let (used_a, used_b) = donated_liquidity(actor, asset_a, asset_b); assert!( @@ -3306,11 +4605,7 @@ fn donate_liquidity_user_fee_native_b_side_preserves_protected_floor() { ); assert_eq!( native_balance(&actor), - floor - .saturating_add(fee) - .saturating_add(60) - .saturating_sub(used_b) - .saturating_sub(fee), + pipeline_balance.saturating_sub(used_b).saturating_sub(fee), "the fee-native protected floor is preserved by the asset-b cap" ); assert!(native_balance(&actor) >= floor); @@ -3505,42 +4800,6 @@ fn condition_balance_not_equals_skips_when_equal() { }); } -#[test] -fn insufficient_fee_closes_cycle_immediately() { - new_test_ext().execute_with(|| { - frame_system::Pallet::::set_block_number(1); - let contract_steps = contract_steps_with_step(make_step(Task::Transfer { - to: BOB, - asset: TestAsset::Native, - amount: AmountResolution::Fixed(10), - })); - let prefunded = user_prefunding_requirement(&contract_steps); - let actor_id = create_user_with( - ALICE, - Mutability::Mutable, - manual_schedule(), - None, - contract_steps, - ); - deplete_user_sovereign(actor_id, prefunded); - // Fund above MinUserBalance(50) but below fee threshold (eval=1 + exec=100 = 101) - fund_native(actor_id, 60); - assert_ok!(Actors::manual_trigger( - RuntimeOrigin::signed(ALICE), - actor_id - )); - run_idle(Weight::MAX); - assert!(Actors::active_actor_view(actor_id).is_none()); - assert!(has_actor_event(|e| matches!( - e, - Event::ActorClosed { - actor_id: id, - reason: CloseReason::FeeBudgetExhausted, - } if *id == actor_id - ))); - }); -} - #[test] fn condition_sees_spendable_not_raw_balance_for_user_actor() { new_test_ext().execute_with(|| { @@ -3614,7 +4873,7 @@ fn condition_sees_full_balance_for_system_actor() { } #[test] -fn user_condition_combines_adapter_lock_with_reserved_fee_budget() { +fn user_pipeline_admission_combines_adapter_lock_with_machine_charge() { new_test_ext().execute_with(|| { frame_system::Pallet::::set_block_number(1); let step = StepOf:: { @@ -3643,22 +4902,26 @@ fn user_condition_combines_adapter_lock_with_reserved_fee_budget() { let sovereign = Actors::active_actor_view(actor_id) .expect("user actor") .sovereign_account; - // A transfer lock reduces the native reducible balance below what the full fee - // envelope needs above MinUserBalance, so the User attempt is not admitted and - // the actor closes with FeeBudgetExhausted (spec 5.2.1) rather than running. + let residual_asset = TestAsset::Local(8); + set_asset_balance(&sovereign, residual_asset, 808); + // A transfer lock leaves enough reducible balance for the Trigger occurrence but not + // the complete Pipeline charge above MinUserBalance, so no Step is attempted. set_native_transfer_lock(&sovereign, 150); let bob_before = native_balance(&BOB); assert_ok!(Actors::manual_trigger( RuntimeOrigin::signed(ALICE), actor_id )); + let native_after_trigger = native_balance(&sovereign); run_idle(Weight::MAX); assert_eq!(native_balance(&BOB), bob_before); + assert_eq!(native_balance(&sovereign), native_after_trigger); + assert_eq!(asset_balance(&sovereign, residual_asset), 808); assert!(has_actor_event(|e| matches!( e, Event::ActorClosed { actor_id: id, - reason: CloseReason::FeeBudgetExhausted, + reason: CloseReason::CycleAdmissionInsufficient, .. } if *id == actor_id ))); @@ -3716,6 +4979,9 @@ fn user_portfolio_rebalancer_both_directions() { frame_system::Pallet::::set_block_number(6); Actors::on_initialize(6); Actors::on_idle(6, Weight::MAX); + frame_system::Pallet::::set_block_number(7); + Actors::on_initialize(7); + Actors::on_idle(7, Weight::MAX); assert!( has_actor_event(|e| matches!( e, @@ -3724,14 +4990,18 @@ fn user_portfolio_rebalancer_both_directions() { )), "Step 0 should execute when spendable native > 5000" ); - // Second evaluation: slash native so spendable < 500, keep raw > fee_reserve (202) - // Set raw native to 600: spendable = 600 - 202 = 398 < 500 ✓ + frame_system::Pallet::::set_block_number(7); + Actors::on_idle(7, Weight::MAX); + // Second evaluation: slash native so spendable < 500 while preserving Pipeline admission. + // Raw 600 pays the complete Pipeline at Opening; the current Action reserve leaves < 500. let actor_native = native_balance(&actor); let _ = >::slash(&actor, actor_native.saturating_sub(600)); let charlie_before = asset_balance(&CHARLIE, foreign); frame_system::Pallet::::set_block_number(11); Actors::on_initialize(11); Actors::on_idle(11, Weight::MAX); + frame_system::Pallet::::set_block_number(12); + Actors::on_idle(12, Weight::MAX); assert!( asset_balance(&CHARLIE, foreign) > charlie_before, "Step 1 should execute when spendable native < 500 AND foreign > 500" diff --git a/template/pallets/actors/src/tests/lifecycle.rs b/template/pallets/actors/src/tests/lifecycle.rs index 6d06ad68..e9b13695 100644 --- a/template/pallets/actors/src/tests/lifecycle.rs +++ b/template/pallets/actors/src/tests/lifecycle.rs @@ -1,4 +1,5 @@ use super::*; +use crate::{ActorContractHeads, ActorContractTailChunks, ActorRunHeads, ActorRunPayloads}; #[test] fn trigger_grammar_is_single_source_and_non_nested() { @@ -6,6 +7,7 @@ fn trigger_grammar_is_single_source_and_non_nested() { let address = RuntimeTrigger::address_event(SourceFilter::OwnerOnly, AssetFilter::Any); let observation = RuntimeTrigger::observation_change(7); let crossing = RuntimeTrigger::observation_crossing(7, CrossingDirection::Rising, 100, 80); + let at_time = RuntimeTrigger::at_time(10); let cadence = RuntimeTrigger::cadenced(10); assert_eq!(observation.encode(), vec![2, 7, 0, 0, 0]); @@ -13,7 +15,8 @@ fn trigger_grammar_is_single_source_and_non_nested() { assert!(!cadence.manual_source_enabled()); assert_eq!(crossing.encode()[0], 3); - assert_eq!(cadence.encode()[0], 4); + assert_eq!(at_time.encode()[0], 4); + assert_eq!(cadence.encode()[0], 5); assert!(TriggerRuntimeState::Stateless.is_compatible_with(&manual)); assert!(TriggerRuntimeState::Stateless.is_compatible_with(&address)); @@ -25,11 +28,18 @@ fn trigger_grammar_is_single_source_and_non_nested() { } .is_compatible_with(&crossing) ); + assert!( + TriggerRuntimeState::AtTime { + anchor_tick: None, + consumed: false, + } + .is_compatible_with(&at_time) + ); assert!(TriggerRuntimeState::Cadenced { anchor_tick: None }.is_compatible_with(&cadence)); assert!(!TriggerRuntimeState::Stateless.is_compatible_with(&crossing)); assert!(!TriggerRuntimeState::Cadenced { anchor_tick: None }.is_compatible_with(&manual)); - for trigger in [manual, address, observation, crossing, cadence] { + for trigger in [manual, address, observation, crossing, at_time, cadence] { assert!(trigger.has_canonical_filters()); let encoded = trigger.encode(); assert!(encoded.len() <= RuntimeTrigger::max_encoded_len()); @@ -59,6 +69,7 @@ fn active_trigger_replacement_matrix_preserves_one_canonical_family() { RuntimeTrigger::address_event(SourceFilter::OwnerOnly, AssetFilter::Any), RuntimeTrigger::observation_change(7), RuntimeTrigger::observation_crossing(7, CrossingDirection::Rising, 100, 80), + RuntimeTrigger::at_time(10), RuntimeTrigger::cadenced(10), ]; let mut actor_id = 0; @@ -76,7 +87,7 @@ fn active_trigger_replacement_matrix_preserves_one_canonical_family() { contract_steps_with_step(make_step(Task::StopCycle)), ); frame_system::Pallet::::set_block_number(create_block + 1); - assert_ok!(update_contract_partial!( + let replacement = update_contract_partial!( RuntimeOrigin::root(), current_id, Schedule { @@ -84,8 +95,12 @@ fn active_trigger_replacement_matrix_preserves_one_canonical_family() { cooldown_blocks: 0, }, None, - )); - let contract = ActorContracts::::get(current_id).expect("active Actor Contract"); + ); + assert!( + replacement.is_ok(), + "trigger replacement failed: old={old_trigger:?}, new={new_trigger:?}, result={replacement:?}" + ); + let contract = Actors::load_actor_contract(current_id).expect("active Actor Contract"); let hot = ActorHot::::get(current_id).expect("active Actor hot state"); assert_eq!(&contract.trigger, new_trigger); assert!( @@ -146,7 +161,7 @@ fn late_trigger_transition_failure_rolls_back_canonical_and_derived_state() { polkadot_sdk::sp_io::storage::root(StateVersion::V1), root_before ); - let contract = ActorContracts::::get(actor_id).expect("active Actor Contract"); + let contract = Actors::load_actor_contract(actor_id).expect("active Actor Contract"); let hot = ActorHot::::get(actor_id).expect("active Actor hot state"); assert!(matches!( contract.trigger, @@ -272,6 +287,53 @@ fn exact_slot_user_creation_accepts_absent_contract() { }); } +#[test] +fn deactivation_removes_active_epoch_and_all_contract_fragments_without_orphans() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let inert_step = inert_contract_steps()[0].clone(); + let contract_steps = BoundedVec::try_from(vec![inert_step; 8]).expect("eight Steps fit"); + let user_id = create_user_with( + ALICE, + Mutability::Mutable, + manual_schedule(), + None, + contract_steps.clone(), + ); + let system_id = create_system_with(ALICE, manual_schedule(), None, contract_steps); + + for actor_id in [user_id, system_id] { + assert!(ActorHot::::contains_key(actor_id)); + assert!(ActorContractHeads::::contains_key(actor_id)); + assert!(ActorAdmissionCertificates::::contains_key(actor_id)); + assert!(ActorContractTailChunks::::contains_key(actor_id, 0)); + assert!(ActorContractTailChunks::::contains_key(actor_id, 1)); + } + + frame_system::Pallet::::set_block_number(2); + assert_ok!(Actors::deactivate_actor( + RuntimeOrigin::signed(ALICE), + user_id, + )); + assert_ok!(Actors::deactivate_actor(RuntimeOrigin::root(), system_id)); + + for actor_id in [user_id, system_id] { + assert!(Actors::actor_identities(actor_id).is_some()); + assert!(Actors::active_actor_view(actor_id).is_none()); + assert!(!ActorHot::::contains_key(actor_id)); + assert!(!ActorContractHeads::::contains_key(actor_id)); + assert!(!ActorAdmissionCertificates::::contains_key(actor_id)); + assert!(!ActorContractTailChunks::::contains_key(actor_id, 0)); + assert!(!ActorContractTailChunks::::contains_key(actor_id, 1)); + assert!(!ActorRunHeads::::contains_key(actor_id)); + assert!(!ActorRunPayloads::::contains_key(actor_id)); + assert!(!ActorFunding::::contains_key(actor_id)); + } + #[cfg(feature = "try-runtime")] + assert_ok!(Actors::do_try_state()); + }); +} + #[test] fn deactivate_activate_preserves_nonce_but_resets_active_epoch_state_for_both_classes() { new_test_ext().execute_with(|| { @@ -316,7 +378,7 @@ fn deactivate_activate_preserves_nonce_but_resets_active_epoch_state_for_both_cl let dormant = Actors::actor_identities(actor_id).expect("durable identity"); assert_eq!(dormant.cycle_nonce, 1); assert!(Actors::actor_funding(actor_id).is_none()); - assert!(Actors::continuation_state(actor_id).is_none()); + assert!(Actors::actor_run_state(actor_id).is_none()); } frame_system::Pallet::::set_block_number(3); @@ -833,12 +895,21 @@ fn control_permanent_placement_exhaustion_closes_through_the_unified_sink() { None, transfer_contract_steps(BOB, 1), ); + let trigger_deadline = Actors::actor_hot(actor_id) + .and_then(|hot| hot.trigger_wakeup_pointer) + .expect("Cadenced trigger deadline exists"); assert_ok!(Actors::pause_actor(RuntimeOrigin::signed(ALICE), actor_id)); - assert!(Actors::wakeup_substrate_invalidate(actor_id).is_some()); + assert_eq!( + Actors::actor_hot(actor_id).and_then(|hot| hot.trigger_wakeup_pointer), + Some(trigger_deadline) + ); frame_system::Pallet::::set_block_number(2); crate::NextQueueTicket::::put(u64::MAX); assert_ok!(Actors::resume_actor(RuntimeOrigin::signed(ALICE), actor_id)); - assert_eq!(scheduled_wakeup_block(actor_id), Some(3)); + assert_eq!( + Actors::actor_hot(actor_id).and_then(|hot| hot.trigger_wakeup_pointer), + Some(trigger_deadline) + ); }); new_test_ext().execute_with(|| { @@ -1286,14 +1357,14 @@ fn exact_update_noops_preserve_all_actor_state_and_emit_nothing() { let encoded_actor_state = |actor_id| { ( ActorHot::::get(actor_id).encode(), - ActorContracts::::get(actor_id).encode(), + Actors::load_actor_contract(actor_id).encode(), crate::ActorFunding::::get(actor_id).encode(), - ContinuationStateStore::::get(actor_id).encode(), + ActorRunStateStore::::get(actor_id).encode(), ) }; let plan_id = create_suspended_system_retry(1); let plan_before = encoded_actor_state(plan_id); - let stored_contract = ActorContracts::::get(plan_id).expect("active Actor Contract"); + let stored_contract = Actors::load_actor_contract(plan_id).expect("active Actor Contract"); frame_system::Pallet::::reset_events(); assert_ok!(update_contract_partial!( RuntimeOrigin::root(), @@ -1306,7 +1377,7 @@ fn exact_update_noops_preserve_all_actor_state_and_emit_nothing() { let policy_id = create_suspended_system_retry(2); let policy_before = encoded_actor_state(policy_id); - let policy = ActorContracts::::get(policy_id) + let policy = Actors::load_actor_contract(policy_id) .expect("active Actor Contract") .funding; frame_system::Pallet::::reset_events(); @@ -1320,7 +1391,7 @@ fn exact_update_noops_preserve_all_actor_state_and_emit_nothing() { let schedule_id = create_suspended_system_retry(3); let schedule_before = encoded_actor_state(schedule_id); - let stored_schedule = ActorContracts::::get(schedule_id).expect("active Actor Contract"); + let stored_schedule = Actors::load_actor_contract(schedule_id).expect("active Actor Contract"); let schedule = RuntimeSchedule { trigger: stored_schedule.trigger, cooldown_blocks: stored_schedule.cooldown_blocks, @@ -1337,20 +1408,22 @@ fn exact_update_noops_preserve_all_actor_state_and_emit_nothing() { assert!(frame_system::Pallet::::events().is_empty()); let auto_close_id = create_suspended_system_retry(4); - let continuation_before = ContinuationStateStore::::get(auto_close_id).encode(); + let continuation_before = + ActorRunStateStore::::get(auto_close_id).expect("suspended run exists"); frame_system::Pallet::::reset_events(); assert_ok!(replace_auto_close( RuntimeOrigin::root(), auto_close_id, Some(2), )); - assert_eq!( - ContinuationStateStore::::get(auto_close_id).encode(), - continuation_before - ); - assert!(!has_actor_event(|event| matches!( + assert!(ActorRunStateStore::::get(auto_close_id).is_none()); + assert!(has_actor_event(|event| matches!( event, - Event::CycleCancelled { actor_id, .. } if *actor_id == auto_close_id + Event::CycleCancelled { + actor_id, + cycle_nonce, + reason: CancellationReason::ContractReplaced, + } if *actor_id == auto_close_id && *cycle_nonce == continuation_before.cycle_nonce ))); assert!(has_actor_event(|event| matches!( event, @@ -1522,13 +1595,13 @@ fn user_immutable_manual_source_changes_readiness_without_mutating_contract() { transfer_contract_steps(BOB, 1), ); fund_native(actor_id, 1_000); - let contract_before = ActorContracts::::get(actor_id).expect("immutable contract"); + let contract_before = Actors::load_actor_contract(actor_id).expect("immutable contract"); assert_ok!(Actors::manual_trigger( RuntimeOrigin::signed(ALICE), actor_id )); assert!(Actors::pending_signal(actor_id)); - assert_eq!(ActorContracts::::get(actor_id), Some(contract_before)); + assert_eq!(Actors::load_actor_contract(actor_id), Some(contract_before)); run_idle(Weight::MAX); assert_eq!( Actors::active_actor_view(actor_id) @@ -1540,7 +1613,7 @@ fn user_immutable_manual_source_changes_readiness_without_mutating_contract() { } #[test] -fn immutable_actor_rejects_pause_and_update_contract() { +fn user_immutable_rejects_every_owner_mutation_and_close_path() { new_test_ext().execute_with(|| { frame_system::Pallet::::set_block_number(1); let actor_id = create_user_with( @@ -1554,6 +1627,10 @@ fn immutable_actor_rejects_pause_and_update_contract() { Actors::pause_actor(RuntimeOrigin::signed(ALICE), actor_id), Error::::ImmutableActor ); + assert_noop!( + Actors::resume_actor(RuntimeOrigin::signed(ALICE), actor_id), + Error::::ImmutableActor + ); let replacement = transfer_contract_steps(CHARLIE, 1); assert_noop!( update_contract_partial!( @@ -1564,6 +1641,25 @@ fn immutable_actor_rejects_pause_and_update_contract() { ), Error::::ImmutableActor ); + assert_noop!( + replace_auto_close(RuntimeOrigin::signed(ALICE), actor_id, Some(2)), + Error::::ImmutableActor + ); + assert_noop!( + Actors::deactivate_actor(RuntimeOrigin::signed(ALICE), actor_id), + Error::::ImmutableActor + ); + assert_noop!( + Actors::cancel_run(RuntimeOrigin::signed(ALICE), actor_id), + Error::::ImmutableActor + ); + assert_noop!( + Actors::close_actor(RuntimeOrigin::signed(ALICE), actor_id), + Error::::ImmutableActor + ); + let retained = Actors::active_actor_view(actor_id).expect("immutable actor remains active"); + assert_eq!(retained.mutability, Mutability::Immutable); + assert_eq!(retained.cycle_nonce, 0); }); } @@ -1614,8 +1710,13 @@ fn permissionless_sweep_many_closes_multiple_and_reports_counts() { None, transfer_contract_steps(ALICE, 1), ); - deplete_user_sovereign(user_a, user_a_prefunded); - deplete_user_sovereign(user_b, user_b_prefunded); + let _ = (user_a_prefunded, user_b_prefunded); + ActorIdentities::::mutate(user_a, |maybe| { + maybe.as_mut().expect("User A identity").cycle_nonce = u64::MAX; + }); + ActorIdentities::::mutate(user_b, |maybe| { + maybe.as_mut().expect("User B identity").cycle_nonce = u64::MAX; + }); let system_alive = create_system_with( ALICE, manual_schedule(), @@ -1665,8 +1766,13 @@ fn permissionless_sweep_many_rolls_back_prior_closes_on_late_failure() { None, transfer_contract_steps(ALICE, 1), ); - deplete_user_sovereign(user_a, user_a_prefunded); - deplete_user_sovereign(user_b, user_b_prefunded); + let _ = (user_a_prefunded, user_b_prefunded); + ActorIdentities::::mutate(user_a, |maybe| { + maybe.as_mut().expect("User A identity").cycle_nonce = u64::MAX; + }); + ActorIdentities::::mutate(user_b, |maybe| { + maybe.as_mut().expect("User B identity").cycle_nonce = u64::MAX; + }); OwnerSlotBitmaps::::remove(BOB); let sweep_ids: BoundedVec::MaxSweepBatch> = BoundedVec::try_from(vec![user_a, user_b]).expect("batch fits"); @@ -1697,6 +1803,7 @@ fn percentage_at_opening_uses_preservable_native_snapshot_for_user() { amount: AmountResolution::PercentageAtOpening(Perbill::one()), }; let contract_steps = contract_steps_with_step(make_step(task.clone())); + let pipeline_fee = pipeline_opening_fee(&contract_steps); let actor_id = create_user_with( ALICE, Mutability::Mutable, @@ -1707,14 +1814,15 @@ fn percentage_at_opening_uses_preservable_native_snapshot_for_user() { let actor = sovereign_account(actor_id); let funding = 500; fund_native(actor_id, funding); - let expected_fees = Actors::compute_eval_fee(0).saturating_add( + let expected_action_fee = ::weight_to_fee( &Actors::weight_upper_bound(&task), - ), - ); + ); let actor_before = native_balance(&actor); let expected_transfer = actor_before - .saturating_sub(expected_fees) + .saturating_sub(address_event_trigger_fee()) + .saturating_sub(pipeline_fee) + .saturating_sub(expected_action_fee) .saturating_sub(TestMinUserBalance::get()); let bob_before = native_balance(&BOB); signal_percentage_trigger(actor_id, TestAsset::Native); @@ -2815,12 +2923,9 @@ fn eligibility_projection_reports_failure_limit_auto_close_and_nonce_exhaustion( ); // A failed terminal cycle leaves the incremented nonce at the target without // closing; the next admission closes before any further cycle (spec 2.4). - ActorContracts::::mutate(actor_id, |maybe| { - maybe - .as_mut() - .expect("active Actor Contract") - .auto_close_at_cycle_nonce = Some(1); - }); + let mut contract = Actors::load_actor_contract(actor_id).expect("active Actor Contract"); + contract.auto_close_at_cycle_nonce = Some(1); + assert_ok!(Actors::store_actor_contract(actor_id, contract)); assert_eq!( active_eligibility(actor_id).terminal_reason, Some(CloseReason::AutoCloseNonceReached) diff --git a/template/pallets/actors/src/tests/market_tasks.rs b/template/pallets/actors/src/tests/market_tasks.rs index 406e4729..65b2cbe2 100644 --- a/template/pallets/actors/src/tests/market_tasks.rs +++ b/template/pallets/actors/src/tests/market_tasks.rs @@ -316,7 +316,7 @@ fn split_transfer_rejects_five_ineligible_legs_atomically_then_retries() { event, Event::SplitTransferExecuted { actor_id: id, .. } if *id == actor_id ))); - let continuation = Actors::continuation_state(actor_id).expect("temporary rejection suspends"); + let continuation = Actors::actor_run_state(actor_id).expect("temporary rejection suspends"); assert_eq!(continuation.cursor, 0); assert_eq!(continuation.unsuccessful_attempts_at_cursor, 1); @@ -331,7 +331,7 @@ fn split_transfer_rejects_five_ineligible_legs_atomically_then_retries() { for (recipient, before) in recipients.iter().zip(retry_balances) { assert_eq!(asset_balance(recipient, asset), before + 10); } - assert!(Actors::continuation_state(actor_id).is_none()); + assert!(Actors::actor_run_state(actor_id).is_none()); assert!(has_actor_event(|event| matches!( event, Event::SplitTransferExecuted { @@ -546,7 +546,7 @@ fn on_address_event_asset_filter_is_enforced() { } #[test] -fn fresh_attempt_result_commits_then_closes_when_tick_index_is_exhausted() { +fn cadence_rearm_capacity_failure_rolls_back_pipeline_opening() { new_test_ext().execute_with(|| { frame_system::Pallet::::set_block_number(1); let actor_id = create_system_with( @@ -558,6 +558,10 @@ fn fresh_attempt_result_commits_then_closes_when_tick_index_is_exhausted() { frame_system::Pallet::::set_block_number(2); let mut wakeup_meter = WeightMeter::with_limit(Weight::MAX); Actors::drain_overdue_wakeups_cursor(2, &mut wakeup_meter); + let ready = Actors::actor_hot(actor_id).expect("Cadenced Actor remains active after detection"); + assert!(ready.pending_signal); + assert!(ready.queue_ticket.is_some()); + assert!(ready.trigger_wakeup_pointer.is_none()); let bob_before = native_balance(&BOB); System::reset_events(); crate::WakeupCursorLen::::insert( @@ -568,13 +572,13 @@ fn fresh_attempt_result_commits_then_closes_when_tick_index_is_exhausted() { let _ = Actors::execute_cycle(Weight::MAX); assert_eq!(native_balance(&BOB), bob_before); - assert!(Actors::active_actor_view(actor_id).is_none()); - assert!(has_actor_event(|event| matches!( + let state = Actors::active_actor_state(actor_id).expect("Cadenced Actor remains active"); + assert_eq!(state.identity.cycle_nonce, 0); + assert!(state.hot.pending_signal); + assert!(state.hot.trigger_wakeup_pointer.is_none()); + assert!(!has_actor_event(|event| matches!( event, - Event::ActorClosed { - actor_id: id, - reason: CloseReason::SchedulerIndexExhausted, - } if *id == actor_id + Event::ActorClosed { actor_id: id, .. } if *id == actor_id ))); }); } @@ -1973,7 +1977,10 @@ fn user_dca_swap_then_cold_storage_transfer() { let cold_before = native_balance(&cold_wallet); frame_system::Pallet::::set_block_number(6); Actors::on_initialize(6); - Actors::on_idle(6, Weight::MAX); + run_idle(Weight::MAX); + frame_system::Pallet::::set_block_number(7); + Actors::on_initialize(7); + run_idle(Weight::MAX); assert!( native_balance(&cold_wallet) > cold_before, "Cold storage should receive native after swap + transfer" diff --git a/template/pallets/actors/src/tests/observations.rs b/template/pallets/actors/src/tests/observations.rs index fec90dfe..0c1d1017 100644 --- a/template/pallets/actors/src/tests/observations.rs +++ b/template/pallets/actors/src/tests/observations.rs @@ -1,4 +1,58 @@ use super::*; +use crate::scheduler::ActivationOutcome; + +fn observation_activation_placement_snapshot( + compact: bool, + window: Option>, + activation_block: u64, + repeat: bool, +) -> (ActivationOutcome, Vec) { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = create_system_with( + ALICE, + Schedule { + trigger: RuntimeTrigger::observation_change(55), + cooldown_blocks: 0, + }, + window, + inert_contract_steps(), + ); + frame_system::Pallet::::set_block_number(activation_block); + let activate = || { + if compact { + Actors::request_observation_activation_compact(actor_id, 55) + } else { + Actors::request_activation(actor_id) + } + }; + let mut outcome = activate().expect("activation placement succeeds"); + if repeat { + outcome = activate().expect("repeated activation placement succeeds"); + } + ( + outcome, + polkadot_sdk::sp_io::storage::root(StateVersion::V1), + ) + }) +} + +fn assert_compact_observation_classification_parity( + actor_id: u64, + feed: u32, +) -> crate::ActorClassification { + let compact = Actors::load_observation_activation_state(actor_id, feed) + .expect("compact activation state loads"); + let compact_classification = Actors::classify_observation_activation_compact(&compact) + .expect("compact classification succeeds"); + let mut instance = Actors::active_actor_view(actor_id).expect("full active view loads"); + instance.pending_signal = true; + let run = Actors::actor_run_state(actor_id); + let generic_classification = Actors::classify_actor_loaded(&instance, run.as_ref()) + .expect("generic classification succeeds"); + assert_eq!(compact_classification, generic_classification); + compact_classification +} #[test] fn observation_only_sources_admit_non_trigger_amount_resolutions() { @@ -15,6 +69,303 @@ fn observation_only_sources_admit_non_trigger_amount_resolutions() { }); } +#[test] +fn compact_observation_placement_matches_generic_ready_future_coalesced_and_terminal_state() { + for (window, activation_block, repeat) in [ + (None, 1, false), + ( + Some(crate::ScheduleWindow { + start: 10, + end: 200, + }), + 1, + false, + ), + (None, 1, true), + ( + Some(crate::ScheduleWindow { start: 1, end: 101 }), + 102, + false, + ), + ] { + assert_eq!( + observation_activation_placement_snapshot(true, window, activation_block, repeat), + observation_activation_placement_snapshot(false, window, activation_block, repeat), + ); + } +} + +#[test] +fn compact_observation_classification_matches_idle_and_running_authority() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let step = inert_contract_steps()[0].clone(); + let steps = + BoundedVec::try_from(vec![step.clone(), step.clone(), step]).expect("three Steps fit"); + let actor_id = create_system_with( + ALICE, + Schedule { + trigger: RuntimeTrigger::observation_change(6), + cooldown_blocks: 0, + }, + None, + steps, + ); + assert_compact_observation_classification_parity(actor_id, 6); + Actors::request_activation(actor_id).expect("ObservationChange activation places"); + Actors::execute_cycle(Weight::MAX); + frame_system::Pallet::::set_block_number(2); + assert_compact_observation_classification_parity(actor_id, 6); + }); +} + +#[test] +fn compact_observation_classification_matches_terminal_precedence() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let window_actor = create_system_with( + ALICE, + Schedule { + trigger: RuntimeTrigger::observation_change(8), + cooldown_blocks: 0, + }, + Some(crate::ScheduleWindow { start: 1, end: 101 }), + inert_contract_steps(), + ); + frame_system::Pallet::::set_block_number(102); + assert_eq!( + assert_compact_observation_classification_parity(window_actor, 8).terminal_reason, + Some(CloseReason::WindowExpired) + ); + + frame_system::Pallet::::set_block_number(1); + let nonce_actor = create_system_with( + BOB, + Schedule { + trigger: RuntimeTrigger::observation_change(9), + cooldown_blocks: 0, + }, + None, + inert_contract_steps(), + ); + ActorIdentities::::mutate(nonce_actor, |maybe| { + maybe.as_mut().expect("identity exists").cycle_nonce = u64::MAX; + }); + assert_eq!( + assert_compact_observation_classification_parity(nonce_actor, 9).terminal_reason, + Some(CloseReason::CycleNonceExhausted) + ); + + let plan = transfer_contract_steps(BOB, 1); + let prefunded = user_prefunding_requirement(&plan); + let user_actor = create_user_with( + CHARLIE, + Mutability::Mutable, + Schedule { + trigger: RuntimeTrigger::observation_change(10), + cooldown_blocks: 0, + }, + None, + plan, + ); + deplete_user_sovereign(user_actor, prefunded); + assert_eq!( + assert_compact_observation_classification_parity(user_actor, 10).terminal_reason, + None + ); + }); +} + +#[test] +fn compact_observation_classification_matches_suspension_and_retry_exhaustion() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + setup_pool(TestAsset::Native, TestAsset::Local(77), 10_000, 10_000); + set_asset_balance(&u64::MAX, TestAsset::Local(77), 10_000); + let actor_id = create_system_with( + ALICE, + Schedule { + trigger: RuntimeTrigger::observation_change(11), + cooldown_blocks: 0, + }, + None, + BoundedVec::try_from(vec![StepOf:: { + precondition: None, + task: Task::SwapIn { + asset_in: TestAsset::Native, + asset_out: TestAsset::Local(77), + amount_in: AmountResolution::Fixed(20), + slippage_tolerance: Perbill::one(), + }, + on_error: RETRY_LATER, + }]) + .expect("one retry Step fits"), + ); + fund_native(actor_id, 100); + set_temporary_dex_failure(true); + Actors::request_activation(actor_id).expect("ObservationChange activation places"); + run_idle(Weight::MAX); + + let suspended = Actors::actor_run_state(actor_id).expect("temporary failure suspends"); + assert_eq!( + assert_compact_observation_classification_parity(actor_id, 11).execution_phase, + ActorExecutionPhase::WaitingRetry(suspended.eligible_at) + ); + frame_system::Pallet::::set_block_number(suspended.eligible_at); + assert_eq!( + assert_compact_observation_classification_parity(actor_id, 11).execution_phase, + ActorExecutionPhase::Ready + ); + + crate::ActorRunHeads::::mutate(actor_id, |maybe| { + maybe + .as_mut() + .expect("run head exists") + .unsuccessful_attempts_at_cursor = 10; + }); + assert_eq!( + assert_compact_observation_classification_parity(actor_id, 11).terminal_reason, + Some(CloseReason::RetryAttemptsExhausted) + ); + }); +} + +#[test] +fn compact_observation_classification_matches_failure_auto_close_pause_and_breaker() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let failure_actor = create_system_with( + ALICE, + Schedule { + trigger: RuntimeTrigger::observation_change(12), + cooldown_blocks: 0, + }, + None, + inert_contract_steps(), + ); + ActorHot::::mutate(failure_actor, |maybe| { + maybe + .as_mut() + .expect("active Actor exists") + .unsuccessful_attempt_streak = ::MaxConsecutiveFailures::get(); + }); + assert_eq!( + assert_compact_observation_classification_parity(failure_actor, 12).terminal_reason, + Some(CloseReason::ConsecutiveFailures) + ); + + let auto_close_actor = create_system_with( + ALICE, + Schedule { + trigger: RuntimeTrigger::observation_change(13), + cooldown_blocks: 0, + }, + None, + inert_contract_steps(), + ); + Actors::request_activation(auto_close_actor).expect("ObservationChange activation places"); + run_idle(Weight::MAX); + let mut contract = Actors::load_actor_contract(auto_close_actor).expect("Contract loads"); + contract.auto_close_at_cycle_nonce = Some(1); + assert_ok!(Actors::store_actor_contract(auto_close_actor, contract)); + assert_eq!( + assert_compact_observation_classification_parity(auto_close_actor, 13).terminal_reason, + Some(CloseReason::AutoCloseNonceReached) + ); + + let paused_actor = create_system_with( + ALICE, + Schedule { + trigger: RuntimeTrigger::observation_change(14), + cooldown_blocks: 0, + }, + None, + inert_contract_steps(), + ); + assert_ok!(Actors::pause_actor( + RuntimeOrigin::signed(ALICE), + paused_actor + )); + assert_eq!( + assert_compact_observation_classification_parity(paused_actor, 14).execution_phase, + ActorExecutionPhase::Paused + ); + + let breaker_actor = create_system_with( + ALICE, + Schedule { + trigger: RuntimeTrigger::observation_change(15), + cooldown_blocks: 0, + }, + None, + inert_contract_steps(), + ); + assert_ok!(Actors::set_global_circuit_breaker( + RuntimeOrigin::root(), + true + )); + assert_eq!( + assert_compact_observation_classification_parity(breaker_actor, 15).execution_phase, + ActorExecutionPhase::GlobalCircuitBreaker + ); + }); +} + +#[test] +fn compact_observation_activation_loads_only_current_authority_tiers() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let step = inert_contract_steps()[0].clone(); + let steps = + BoundedVec::try_from(vec![step.clone(), step.clone(), step]).expect("three Steps fit"); + let actor_id = create_system_with( + ALICE, + Schedule { + trigger: RuntimeTrigger::observation_change(7), + cooldown_blocks: 0, + }, + None, + steps, + ); + Actors::request_activation(actor_id).expect("ObservationChange activation places"); + Actors::execute_cycle(Weight::MAX); + assert_eq!( + crate::ActorRunHeads::::get(actor_id).map(|head| head.cursor), + Some(1) + ); + + crate::ActorAdmissionCertificates::::remove(actor_id); + crate::ActorFunding::::remove(actor_id); + crate::ActorRunPayloads::::remove(actor_id); + let compact = Actors::load_observation_activation_state(actor_id, 7) + .expect("compact activation ignores extended execution payload"); + assert_eq!(compact.identity.cycle_nonce, 0); + assert_eq!(compact.hot.cycle_state, CycleState::Running); + assert_eq!(compact.authority.feed, 7); + assert_eq!(compact.run_head.as_ref().map(|head| head.cursor), Some(1)); + assert_eq!( + compact + .loaded_step + .expect("nonempty Contract has a current Step") + .cursor, + 1 + ); + assert!(matches!( + Actors::load_actor_state(actor_id), + crate::LoadedActorStateOf::Corrupt + )); + + crate::ActorRunHeads::::mutate(actor_id, |maybe| { + maybe + .as_mut() + .expect("run head exists") + .contract_authority + .body_commitment[0] ^= 1; + }); + assert!(Actors::load_observation_activation_state(actor_id, 7).is_none()); + }); +} + #[test] fn observation_subscriptions_follow_schedule_lifecycle_exactly() { new_test_ext().execute_with(|| { @@ -27,6 +378,17 @@ fn observation_subscriptions_follow_schedule_lifecycle_exactly() { inert_contract_steps(), ); let slot = Actors::observation_subscription_slot(actor_id).expect("subscription slot"); + let authority = crate::ActorActivationAuthorities::::get(actor_id) + .expect("ObservationChange activation authority"); + let certificate = crate::ActorAdmissionCertificates::::get(actor_id) + .expect("ObservationChange admission certificate"); + assert_eq!(authority.feed, 1); + assert_eq!( + authority.semantic_contract_id, + certificate.semantic_contract_id + ); + assert_eq!(authority.body_commitment, certificate.body_commitment); + assert_eq!(authority.admission_identity, certificate.admission_identity); assert_eq!( Actors::actor_observation_feeds(actor_id), Some(BoundedVec::truncate_from(vec![1])) @@ -56,6 +418,12 @@ fn observation_subscriptions_follow_schedule_lifecycle_exactly() { ); assert_eq!(Actors::observation_subscriber_count(1), 0); assert_eq!(Actors::observation_subscriber_count(3), 1); + assert_eq!( + crate::ActorActivationAuthorities::::get(actor_id) + .expect("updated activation authority") + .feed, + 3 + ); frame_system::Pallet::::set_block_number(2); assert_ok!(update_contract_partial!( RuntimeOrigin::signed(ALICE), @@ -74,6 +442,7 @@ fn observation_subscriptions_follow_schedule_lifecycle_exactly() { )); assert!(Actors::actor_observation_feeds(actor_id).is_none()); assert!(Actors::observation_subscription_slot(actor_id).is_none()); + assert!(crate::ActorActivationAuthorities::::get(actor_id).is_none()); assert_eq!(Actors::observation_subscription_count(), 0); assert_eq!(crate::ObservationFreeSlotLen::::get(), 1); frame_system::Pallet::::set_block_number(4); @@ -95,7 +464,14 @@ fn observation_subscriptions_follow_schedule_lifecycle_exactly() { )); assert_eq!(Actors::observation_subscription_slot(actor_id), Some(slot)); assert_eq!(crate::ObservationFreeSlotLen::::get(), 0); + assert_eq!( + crate::ActorActivationAuthorities::::get(actor_id) + .expect("reactivated activation authority") + .feed, + 4 + ); assert_ok!(Actors::close_actor(RuntimeOrigin::signed(ALICE), actor_id)); + assert!(crate::ActorActivationAuthorities::::get(actor_id).is_none()); assert_eq!(Actors::observation_subscription_count(), 0); assert_eq!(Actors::observation_subscriber_count(4), 0); assert!(Actors::observation_subscriber_pages(4, page_id).is_none()); @@ -174,8 +550,9 @@ fn observation_occupied_page_list_follows_live_pages_after_fragmentation() { <::WeightInfo as crate::WeightInfo>::observation_fanout_base(); let unit = <::WeightInfo as crate::WeightInfo>::observation_fanout_page(); + let fault = ::record_observation_fanout_worker_fault(); assert_eq!( - Actors::fanout_dirty_observations(base.saturating_add(unit)), + Actors::fanout_dirty_observations(base.saturating_add(unit).saturating_add(fault)), base.saturating_add(unit) ); assert!(Actors::dirty_observation_feeds(17).is_none()); @@ -262,12 +639,30 @@ fn observation_change_ingress_coalesces_latest_revision_without_subscriber_work( None, inert_contract_steps(), ); - assert_ok!(Actors::note_observation_changed(1, 1)); + assert_ok!(Actors::note_observation_transition_with_provenance( + 1, + crate::ObservationTransition { + revision: 1, + previous: None, + current: 10, + }, + crate::TriggerCauseProvenance::ExternalPhase, + )); assert_eq!(Actors::observation_ingress_revision(1), Some(1)); assert!(!Actors::pending_signal(actor_id)); let initial = Actors::dirty_observation_feeds(1).expect("dirty feed"); assert_eq!(initial.latest_revision, 1); + assert_eq!( + initial.latest_cause_provenance, + crate::TriggerCauseProvenance::ExternalPhase + ); + assert_eq!(initial.latest_cause_block, 1); assert_eq!(initial.fanout_revision, 0); + assert_eq!( + initial.fanout_cause_provenance, + crate::TriggerCauseProvenance::Deferred + ); + assert_eq!(initial.fanout_cause_block, 0); assert_eq!(initial.dirty_since, 1); assert_eq!(initial.next_subscriber_page, None); frame_system::Pallet::::set_block_number(5); @@ -278,6 +673,11 @@ fn observation_change_ingress_coalesces_latest_revision_without_subscriber_work( assert_eq!(Actors::observation_ingress_revision(1), Some(3)); let coalesced = Actors::dirty_observation_feeds(1).expect("coalesced dirty feed"); assert_eq!(coalesced.latest_revision, 3); + assert_eq!( + coalesced.latest_cause_provenance, + crate::TriggerCauseProvenance::Deferred + ); + assert_eq!(coalesced.latest_cause_block, 8); assert_eq!(coalesced.dirty_since, 1); assert_noop!( Actors::note_observation_changed(1, 2), @@ -481,11 +881,13 @@ fn multiple_dense_dirty_feeds_receive_round_robin_service() { <::WeightInfo as crate::WeightInfo>::observation_fanout_base(); let unit = <::WeightInfo as crate::WeightInfo>::observation_fanout_page(); - let budget = base.saturating_add(unit); + let fault = ::record_observation_fanout_worker_fault(); + let consumed = base.saturating_add(unit); + let budget = consumed.saturating_add(fault); for (index, feed) in feeds.into_iter().enumerate() { assert_eq!(Actors::dirty_observation_list().cursor, Some(feed)); - assert_eq!(Actors::fanout_dirty_observations(budget), budget); + assert_eq!(Actors::fanout_dirty_observations(budget), consumed); let delivered = actors[index] .iter() .filter(|actor_id| Actors::pending_signal(**actor_id)) @@ -558,7 +960,8 @@ fn fanout_requires_complete_ref_time_and_proof_size_before_mutation() { <::WeightInfo as crate::WeightInfo>::observation_fanout_base(); let unit = <::WeightInfo as crate::WeightInfo>::observation_fanout_page(); - let required = base.saturating_add(unit); + let fault = ::record_observation_fanout_worker_fault(); + let required = base.saturating_add(unit).saturating_add(fault); let ref_time_short = Weight::from_parts(required.ref_time().saturating_sub(1), u64::MAX); assert_eq!(Actors::fanout_dirty_observations(ref_time_short), base); @@ -590,19 +993,46 @@ fn fanout_structural_fault_is_bounded_and_requires_repair_before_resume() { let dirty_before = Actors::dirty_observation_feeds(10).expect("dirty feed"); Actors::fanout_dirty_observations(Weight::MAX); assert_eq!(Actors::dirty_observation_feeds(10), Some(dirty_before)); + let fault = crate::ObservationFanoutWorkerFault { + feed: 10, + revision: 1, + subscriber_page: None, + subscriber_position: 0, + actor_id: None, + semantic_contract_id: None, + body_commitment: None, + admission_identity: None, + branch: crate::ObservationFanoutBranch::Ordinary, + class: crate::CrossingWorkerFaultClass::Invariant, + }; + assert_eq!(Actors::observation_fanout_worker_fault(), Some(fault)); assert_eq!( - Actors::observation_fanout_worker_fault(), - Some(crate::ObservationFanoutWorkerFault { - feed: 10, - revision: 1, - subscriber_page: None, - class: crate::CrossingWorkerFaultClass::Invariant, - }) + actor_event_count(|event| matches!( + event, + Event::ActorFaultRecorded { + fault_id: crate::FaultId::ObservationFanoutWorker, + kind: crate::ActorFaultKind::Detector, + first_recorded_block: 1, + context: crate::FaultContext::ObservationFanout(recorded), + } if recorded == &fault + )), + 1 ); assert_eq!( Actors::fanout_dirty_observations(Weight::MAX), <::WeightInfo as crate::WeightInfo>::observation_fanout_base() ); + assert_eq!( + actor_event_count(|event| matches!( + event, + Event::ActorFaultRecorded { + fault_id: crate::FaultId::ObservationFanoutWorker, + .. + } + )), + 1, + "an uncleared fanout fault emits only its first-recorded event" + ); assert_noop!( Actors::clear_observation_fanout_worker_fault(RuntimeOrigin::signed(ALICE)), DispatchError::BadOrigin @@ -618,6 +1048,328 @@ fn fanout_structural_fault_is_bounded_and_requires_repair_before_resume() { }); } +#[test] +fn fanout_fault_captures_exact_page_position_actor_c6_authority_and_branch() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let schedule = Schedule { + trigger: RuntimeTrigger::observation_change(25), + cooldown_blocks: 100, + }; + let first = create_system_with(ALICE, schedule.clone(), None, inert_contract_steps()); + let second = create_system_with(ALICE, schedule, None, inert_contract_steps()); + assert_eq!( + Actors::request_observation_activation_compact(first, 25), + Ok(ActivationOutcome::Latched) + ); + ActorHot::::mutate(second, |hot| { + hot.as_mut().expect("second actor").last_cycle_block = Some(1); + }); + Actors::test_fail_wakeup_placement_with_capacity(); + assert_ok!(Actors::note_observation_changed(25, 1)); + assert_eq!(Actors::do_fanout_dirty_observation_page(), Ok(true)); + let retained = Actors::dirty_observation_feeds(25).expect("retry cursor"); + assert_eq!(retained.next_subscriber_page, Some(0)); + assert_eq!(retained.next_subscriber_position, 1); + + frame_system::Pallet::::set_block_number(2); + let authority = + crate::ActorActivationAuthorities::::get(second).expect("second activation authority"); + crate::WakeupBuckets::::insert( + WakeupKey::Block(101), + crate::WakeupBucketState { + head_page: 0, + tail_page: 0, + next_page_id: 1, + live_entries: 1, + cursor_index: None, + }, + ); + Actors::fanout_dirty_observations(Weight::MAX); + + assert_eq!( + Actors::observation_fanout_worker_fault(), + Some(crate::ObservationFanoutWorkerFault { + feed: 25, + revision: 1, + subscriber_page: Some(0), + subscriber_position: 1, + actor_id: Some(second), + semantic_contract_id: Some(authority.semantic_contract_id), + body_commitment: Some(authority.body_commitment), + admission_identity: Some(authority.admission_identity), + branch: crate::ObservationFanoutBranch::Ordinary, + class: crate::CrossingWorkerFaultClass::SchedulerExhausted, + }) + ); + assert_eq!(Actors::dirty_observation_feeds(25), Some(retained)); + }); +} + +#[test] +fn fanout_fault_recording_admits_both_weight_dimensions_and_is_idempotent() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let fault = crate::ObservationFanoutWorkerFault { + feed: 10, + revision: 1, + subscriber_page: Some(0), + subscriber_position: 0, + actor_id: None, + semantic_contract_id: None, + body_commitment: None, + admission_identity: None, + branch: crate::ObservationFanoutBranch::Ordinary, + class: crate::CrossingWorkerFaultClass::Invariant, + }; + let required = ::record_observation_fanout_worker_fault(); + + let mut ref_time_short = polkadot_sdk::sp_weights::WeightMeter::with_limit(Weight::from_parts( + required.ref_time().saturating_sub(1), + u64::MAX, + )); + assert!(!Actors::record_observation_fanout_worker_fault( + &mut ref_time_short, + fault + )); + assert!(crate::ObservationFanoutWorkerFaultState::::get().is_none()); + assert_eq!(System::events().len(), 0); + + let mut proof_short = polkadot_sdk::sp_weights::WeightMeter::with_limit(Weight::from_parts( + u64::MAX, + required.proof_size().saturating_sub(1), + )); + assert!(!Actors::record_observation_fanout_worker_fault( + &mut proof_short, + fault + )); + assert!(crate::ObservationFanoutWorkerFaultState::::get().is_none()); + assert_eq!(System::events().len(), 0); + + let mut admitted = polkadot_sdk::sp_weights::WeightMeter::with_limit(required); + assert!(Actors::record_observation_fanout_worker_fault( + &mut admitted, + fault + )); + assert_eq!(admitted.consumed(), required); + let events_after_first = System::events(); + + let mut duplicate = polkadot_sdk::sp_weights::WeightMeter::with_limit(Weight::MAX); + assert!(!Actors::record_observation_fanout_worker_fault( + &mut duplicate, + crate::ObservationFanoutWorkerFault { + class: crate::CrossingWorkerFaultClass::Other, + ..fault + }, + )); + assert_eq!(duplicate.consumed(), Weight::zero()); + assert_eq!( + crate::ObservationFanoutWorkerFaultState::::get(), + Some(fault) + ); + assert_eq!(System::events(), events_after_first); + }); +} + +#[test] +fn observation_change_charges_occurrence_before_pipeline_opening() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = create_user_with( + ALICE, + Mutability::Mutable, + observation_schedule(vec![31]), + None, + inert_contract_steps(), + ); + let sovereign = sovereign_account(actor_id); + let before = native_balance(&sovereign); + clear_fee_collections(); + + assert_ok!(Actors::note_observation_changed(31, 1)); + assert_eq!(Actors::do_fanout_dirty_observation_page(), Ok(false)); + + let fee = observation_change_trigger_fee(); + assert_eq!(fee_collections(), vec![fee]); + assert_eq!(native_balance(&sovereign), before - fee); + let hot = Actors::actor_hot(actor_id).expect("active Actor"); + assert!(hot.pending_signal); + assert!(hot.queue_ticket.is_some()); + assert_eq!( + Actors::actor_identities(actor_id) + .expect("identity remains") + .cycle_nonce, + 0 + ); + assert!(has_actor_event(|event| matches!( + event, + Event::TriggerOccurrenceProcessed { + actor_id: id, + trigger_family: TriggerFamily::ObservationChange, + fee: charged, + } if *id == actor_id && *charged == fee + ))); + }); +} + +#[test] +fn repeated_pending_observation_change_is_latched_without_trigger_fee() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = create_user_with( + ALICE, + Mutability::Mutable, + observation_schedule(vec![32]), + None, + inert_contract_steps(), + ); + fund_native(actor_id, observation_change_trigger_fee()); + clear_fee_collections(); + + assert_ok!(Actors::note_observation_changed(32, 1)); + assert_eq!(Actors::do_fanout_dirty_observation_page(), Ok(false)); + let ticket = Actors::actor_hot(actor_id) + .expect("active Actor") + .queue_ticket; + assert_eq!( + Actors::indexed_trigger_detection_disabled(actor_id), + Some(()) + ); + assert_ok!(Actors::note_observation_changed(32, 2)); + assert_eq!(Actors::do_fanout_dirty_observation_page(), Ok(false)); + + assert_eq!(fee_collections(), vec![observation_change_trigger_fee()]); + let hot = Actors::actor_hot(actor_id).expect("active Actor"); + assert!(hot.pending_signal); + assert_eq!(hot.queue_ticket, ticket); + assert_eq!( + System::events() + .iter() + .filter(|record| matches!( + &record.event, + RuntimeEvent::Actors(Event::TriggerOccurrenceProcessed { + actor_id: id, + trigger_family: TriggerFamily::ObservationChange, + .. + }) if *id == actor_id + )) + .count(), + 1 + ); + run_idle(Weight::MAX); + assert_eq!(Actors::indexed_trigger_detection_disabled(actor_id), None); + }); +} + +#[test] +fn busy_observation_change_charges_and_latches_only_the_future_pipeline() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let plan = BoundedVec::try_from(vec![ + make_step(Task::Transfer { + to: BOB, + asset: TestAsset::Native, + amount: AmountResolution::Fixed(1), + }), + make_step(Task::Transfer { + to: CHARLIE, + asset: TestAsset::Native, + amount: AmountResolution::Fixed(1), + }), + ]) + .expect("two Steps fit"); + let actor_id = create_user_with( + ALICE, + Mutability::Mutable, + observation_schedule(vec![33]), + None, + plan, + ); + fund_native(actor_id, 1_000_000); + assert_ok!(Actors::note_observation_changed(33, 1)); + assert_eq!(Actors::do_fanout_dirty_observation_page(), Ok(false)); + Actors::on_idle(1, Weight::MAX); + let run_before = ActorRunStateStore::::get(actor_id).expect("Pipeline is Running"); + clear_fee_collections(); + frame_system::Pallet::::reset_events(); + + assert_ok!(Actors::note_observation_changed(33, 2)); + assert_eq!(Actors::do_fanout_dirty_observation_page(), Ok(false)); + + assert_eq!(fee_collections(), vec![observation_change_trigger_fee()]); + assert!(!has_actor_event(|event| matches!( + event, + Event::PipelineFeeCharged { actor_id: id, .. } if *id == actor_id + ))); + let hot = Actors::actor_hot(actor_id).expect("active Actor"); + assert_eq!(hot.cycle_state, CycleState::Running); + assert!(hot.pending_signal); + let run_after = ActorRunStateStore::::get(actor_id).expect("Pipeline remains Running"); + assert_eq!(run_after.cursor, run_before.cursor); + assert_eq!(run_after.cycle_nonce, run_before.cycle_nonce); + }); +} + +#[test] +fn underfunded_observation_change_advances_without_fee_readiness_or_apoptosis() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = create_user_with( + ALICE, + Mutability::Mutable, + observation_schedule(vec![34]), + None, + inert_contract_steps(), + ); + let sovereign = sovereign_account(actor_id); + let balance = native_balance(&sovereign); + deplete_user_sovereign(actor_id, balance - TestMinUserBalance::get()); + clear_fee_collections(); + + assert_ok!(Actors::note_observation_changed(34, 1)); + assert_eq!(Actors::do_fanout_dirty_observation_page(), Ok(false)); + + assert!(fee_collections().is_empty()); + assert_eq!(native_balance(&sovereign), TestMinUserBalance::get()); + let hot = Actors::actor_hot(actor_id).expect("process remains live"); + assert!(!hot.pending_signal); + assert!(hot.queue_ticket.is_none()); + assert!(Actors::dirty_observation_feeds(34).is_none()); + assert!(Actors::active_actor_view(actor_id).is_some()); + }); +} + +#[test] +fn observation_change_collection_failure_advances_without_readiness() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = create_user_with( + ALICE, + Mutability::Mutable, + observation_schedule(vec![35]), + None, + inert_contract_steps(), + ); + let sovereign = sovereign_account(actor_id); + let before = native_balance(&sovereign); + set_fail_fee_sink_transfer(true); + + assert_ok!(Actors::note_observation_changed(35, 1)); + assert_eq!(Actors::do_fanout_dirty_observation_page(), Ok(false)); + set_fail_fee_sink_transfer(false); + + assert_eq!(native_balance(&sovereign), before); + let hot = Actors::actor_hot(actor_id).expect("process remains live"); + assert!(!hot.pending_signal); + assert!(hot.queue_ticket.is_none()); + assert!(Actors::dirty_observation_feeds(35).is_none()); + assert!(!has_actor_event(|event| matches!( + event, + Event::TriggerOccurrenceProcessed { actor_id: id, .. } if *id == actor_id + ))); + }); +} + #[test] fn one_fanout_page_sets_existing_latches_and_scheduler_membership() { new_test_ext().execute_with(|| { @@ -637,10 +1389,13 @@ fn one_fanout_page_sets_existing_latches_and_scheduler_membership() { <::WeightInfo as crate::WeightInfo>::observation_fanout_base(); let unit = <::WeightInfo as crate::WeightInfo>::observation_fanout_page(); + let fault = ::record_observation_fanout_worker_fault(); + Actors::test_reset_queue_append_commits(); assert_eq!( - Actors::fanout_dirty_observations(base.saturating_add(unit)), + Actors::fanout_dirty_observations(base.saturating_add(unit).saturating_add(fault)), base.saturating_add(unit) ); + assert_eq!(Actors::test_queue_append_commits(), 1); for actor_id in actors { let hot = Actors::actor_hot(actor_id).expect("active actor"); assert!(hot.pending_signal); @@ -654,6 +1409,72 @@ fn one_fanout_page_sets_existing_latches_and_scheduler_membership() { }); } +#[test] +fn mixed_observation_page_commits_each_contiguous_queue_cohort_once() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actors = (0..3) + .map(|_| { + create_system_with( + ALICE, + observation_schedule(vec![23]), + None, + inert_contract_steps(), + ) + }) + .collect::>(); + assert_eq!( + Actors::request_observation_activation_compact(actors[1], 23), + Ok(ActivationOutcome::Latched) + ); + assert_ok!(Actors::note_observation_changed(23, 1)); + Actors::test_reset_queue_append_commits(); + + assert_eq!(Actors::do_fanout_dirty_observation_page(), Ok(false)); + assert_eq!(Actors::test_queue_append_commits(), 2); + assert!(Actors::dirty_observation_feeds(23).is_none()); + for actor_id in actors { + let hot = Actors::actor_hot(actor_id).expect("active actor"); + assert!(hot.pending_signal); + assert!(hot.queue_ticket.is_some()); + } + }); +} + +#[test] +fn contiguous_observation_wakeup_run_commits_once() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let schedule = Schedule { + trigger: RuntimeTrigger::observation_change(24), + cooldown_blocks: 100, + }; + let actors = (0..3) + .map(|_| create_system_with(ALICE, schedule.clone(), None, inert_contract_steps())) + .collect::>(); + for actor_id in &actors { + ActorHot::::mutate(actor_id, |hot| { + hot.as_mut().expect("active actor").last_cycle_block = Some(1); + }); + } + assert_ok!(Actors::note_observation_changed(24, 1)); + Actors::test_reset_observation_wakeup_cohort_commits(); + + assert_eq!(Actors::do_fanout_dirty_observation_page(), Ok(false)); + assert_eq!(Actors::test_observation_wakeup_cohort_commits(), 1); + assert!(Actors::dirty_observation_feeds(24).is_none()); + for actor_id in actors { + let hot = Actors::actor_hot(actor_id).expect("active actor"); + assert!(hot.pending_signal); + assert!(hot.queue_ticket.is_none()); + assert_eq!( + hot.wakeup_pointer.map(|pointer| pointer.block), + Some(WakeupKey::Block(101)) + ); + } + }); +} + #[test] fn saturated_queue_materializes_fanout_through_the_canonical_deferred_wakeup() { new_test_ext().execute_with(|| { @@ -670,9 +1491,11 @@ fn saturated_queue_materializes_fanout_through_the_canonical_deferred_wakeup() { let first = page_id.saturating_mul(page_size); let len = page_size.min(capacity.saturating_sub(first)); let entries = (0..len) - .map(|offset| QueueEntry { - ticket: u64::from(first.saturating_add(offset)), - actor_id: 20_000_000u64.saturating_add(u64::from(first.saturating_add(offset))), + .map(|offset| { + queue_entry( + u64::from(first.saturating_add(offset)), + 20_000_000u64.saturating_add(u64::from(first.saturating_add(offset))), + ) }) .collect::>(); QueuePages::::insert( @@ -689,7 +1512,8 @@ fn saturated_queue_materializes_fanout_through_the_canonical_deferred_wakeup() { <::WeightInfo as crate::WeightInfo>::observation_fanout_base(); let unit = <::WeightInfo as crate::WeightInfo>::observation_fanout_page(); - let budget = base.saturating_add(unit); + let fault = ::record_observation_fanout_worker_fault(); + let budget = base.saturating_add(unit).saturating_add(fault); Actors::fanout_dirty_observations(budget); assert!(Actors::dirty_observation_feeds(16).is_none()); @@ -705,6 +1529,85 @@ fn saturated_queue_materializes_fanout_through_the_canonical_deferred_wakeup() { }); } +#[test] +fn fanout_position_cursor_does_not_replay_a_committed_page_prefix() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let schedule = Schedule { + trigger: RuntimeTrigger::observation_change(18), + cooldown_blocks: 100, + }; + let first = create_system_with(ALICE, schedule.clone(), None, inert_contract_steps()); + let second = create_system_with(ALICE, schedule, None, inert_contract_steps()); + assert_eq!( + Actors::request_observation_activation_compact(first, 18), + Ok(ActivationOutcome::Latched) + ); + ActorHot::::mutate(second, |hot| { + hot.as_mut().expect("second actor").last_cycle_block = Some(1); + }); + Actors::test_fail_wakeup_placement_with_capacity(); + assert_ok!(Actors::note_observation_changed(18, 1)); + + assert_eq!(Actors::do_fanout_dirty_observation_page(), Ok(true)); + let blocked = Actors::dirty_observation_feeds(18).expect("fanout remains retryable"); + assert_eq!(blocked.next_subscriber_page, Some(0)); + assert_eq!(blocked.next_subscriber_position, 1); + assert_eq!(blocked.retry_after, Some(2)); + assert_eq!(Actors::do_fanout_dirty_observation_page(), Ok(true)); + Actors::fanout_dirty_observations(Weight::MAX); + assert_eq!(Actors::dirty_observation_feeds(18), Some(blocked)); + assert!(Actors::observation_fanout_worker_fault().is_none()); + assert!( + Actors::actor_hot(second) + .expect("second actor") + .wakeup_pointer + .is_none() + ); + + crate::ActorActivationAuthorities::::remove(first); + frame_system::Pallet::::set_block_number(2); + assert_eq!(Actors::do_fanout_dirty_observation_page(), Ok(false)); + assert!(Actors::dirty_observation_feeds(18).is_none()); + assert!( + Actors::actor_hot(second) + .expect("second actor") + .wakeup_pointer + .is_some() + ); + }); +} + +#[test] +fn fanout_terminal_branch_is_durable_and_executes_under_a_later_scalar_turn() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = create_system_with( + ALICE, + observation_schedule(vec![21]), + Some(crate::ScheduleWindow { start: 1, end: 101 }), + inert_contract_steps(), + ); + frame_system::Pallet::::set_block_number(102); + assert_ok!(Actors::note_observation_changed(21, 1)); + + assert_eq!(Actors::do_fanout_dirty_observation_page(), Ok(true)); + let deferred = Actors::dirty_observation_feeds(21).expect("terminal branch is durable"); + assert_eq!(deferred.next_subscriber_page, Some(0)); + assert_eq!(deferred.next_subscriber_position, 0); + assert_eq!( + deferred.next_subscriber_branch, + crate::ObservationFanoutBranch::Terminal + ); + assert!(Actors::actor_hot(actor_id).is_some()); + + assert_eq!(Actors::do_fanout_dirty_observation_page(), Ok(false)); + assert!(Actors::actor_hot(actor_id).is_none()); + assert!(Actors::dirty_observation_feeds(21).is_none()); + assert_eq!(Actors::dirty_observation_feed_count(), 0); + }); +} + #[test] fn on_idle_fanout_feeds_the_existing_scheduler_without_direct_execution() { new_test_ext().execute_with(|| { @@ -758,7 +1661,8 @@ fn newer_revision_during_fanout_restarts_from_the_first_subscriber_page() { <::WeightInfo as crate::WeightInfo>::observation_fanout_base(); let unit = <::WeightInfo as crate::WeightInfo>::observation_fanout_page(); - Actors::fanout_dirty_observations(base.saturating_add(unit)); + let fault = ::record_observation_fanout_worker_fault(); + Actors::fanout_dirty_observations(base.saturating_add(unit).saturating_add(fault)); let in_progress = Actors::dirty_observation_feeds(12).expect("fanout remains in progress"); assert_eq!(in_progress.fanout_revision, 1); assert_eq!(in_progress.next_subscriber_page, Some(1)); @@ -769,20 +1673,20 @@ fn newer_revision_during_fanout_restarts_from_the_first_subscriber_page() { }); assert_ok!(Actors::note_observation_changed(12, 2)); - Actors::fanout_dirty_observations(base.saturating_add(unit)); + Actors::fanout_dirty_observations(base.saturating_add(unit).saturating_add(fault)); let restarted = Actors::dirty_observation_feeds(12).expect("new revision restarts fanout"); assert_eq!(restarted.latest_revision, 2); assert_eq!(restarted.fanout_revision, 2); assert_eq!(restarted.next_subscriber_page, Some(0)); assert!(!Actors::pending_signal(first)); - Actors::fanout_dirty_observations(base.saturating_add(unit)); - assert!(Actors::pending_signal(first)); + Actors::fanout_dirty_observations(base.saturating_add(unit).saturating_add(fault)); + assert!(!Actors::pending_signal(first)); assert_eq!( Actors::actor_hot(first).expect("first actor").queue_ticket, first_ticket ); - Actors::fanout_dirty_observations(base.saturating_add(unit)); + Actors::fanout_dirty_observations(base.saturating_add(unit).saturating_add(fault)); assert!(Actors::dirty_observation_feeds(12).is_none()); #[cfg(feature = "try-runtime")] assert_ok!(crate::Pallet::::do_try_state()); @@ -808,7 +1712,8 @@ fn latest_revision_fanout_model_converges_across_seeded_races() { <::WeightInfo as crate::WeightInfo>::observation_fanout_base(); let unit = <::WeightInfo as crate::WeightInfo>::observation_fanout_page(); - let budget = base.saturating_add(unit); + let fault = ::record_observation_fanout_worker_fault(); + let budget = base.saturating_add(unit).saturating_add(fault); let mut latest_revision = 1u64; let mut delivered = vec![0u64; actors.len()]; let mut seed = 0xDE05_0777u64; @@ -894,7 +1799,8 @@ fn partial_fanout_page_then_deactivation_reconciles_dirty_feed() { <::WeightInfo as crate::WeightInfo>::observation_fanout_base(); let unit = <::WeightInfo as crate::WeightInfo>::observation_fanout_page(); - Actors::fanout_dirty_observations(base.saturating_add(unit)); + let fault = ::record_observation_fanout_worker_fault(); + Actors::fanout_dirty_observations(base.saturating_add(unit).saturating_add(fault)); let in_progress = Actors::dirty_observation_feeds(30).expect("first page consumed"); assert_eq!(in_progress.next_subscriber_page, Some(1)); @@ -910,7 +1816,7 @@ fn partial_fanout_page_then_deactivation_reconciles_dirty_feed() { "unlinked page must not remain the fanout cursor" ); - Actors::fanout_dirty_observations(base.saturating_add(unit)); + Actors::fanout_dirty_observations(base.saturating_add(unit).saturating_add(fault)); assert!(Actors::dirty_observation_feeds(30).is_none()); for actor_id in &actors[..page_size as usize] { assert!( @@ -943,7 +1849,8 @@ fn subscriber_mutation_during_fanout_reconciles_without_invariant_errors() { <::WeightInfo as crate::WeightInfo>::observation_fanout_base(); let unit = <::WeightInfo as crate::WeightInfo>::observation_fanout_page(); - Actors::fanout_dirty_observations(base.saturating_add(unit)); + let fault = ::record_observation_fanout_worker_fault(); + Actors::fanout_dirty_observations(base.saturating_add(unit).saturating_add(fault)); assert_eq!( Actors::dirty_observation_feeds(31) .expect("in progress") @@ -975,7 +1882,7 @@ fn subscriber_mutation_during_fanout_reconciles_without_invariant_errors() { assert!(!Actors::pending_signal(late_subscriber)); for _ in 0..4 { - Actors::fanout_dirty_observations(base.saturating_add(unit)); + Actors::fanout_dirty_observations(base.saturating_add(unit).saturating_add(fault)); if Actors::dirty_observation_feeds(31).is_none() { break; } @@ -990,7 +1897,7 @@ fn subscriber_mutation_during_fanout_reconciles_without_invariant_errors() { assert_ok!(Actors::note_observation_changed(31, 2)); for _ in 0..4 { - Actors::fanout_dirty_observations(base.saturating_add(unit)); + Actors::fanout_dirty_observations(base.saturating_add(unit).saturating_add(fault)); if Actors::dirty_observation_feeds(31).is_none() { break; } @@ -1454,6 +2361,6 @@ fn unavailable_observation_skips_without_incrementing_failures() { .unsuccessful_attempt_streak, 0 ); - assert!(Actors::continuation_state(actor_id).is_none()); + assert!(Actors::actor_run_state(actor_id).is_none()); }); } diff --git a/template/pallets/actors/src/tests/scheduling.rs b/template/pallets/actors/src/tests/scheduling.rs index 1a8048cc..bc30a22d 100644 --- a/template/pallets/actors/src/tests/scheduling.rs +++ b/template/pallets/actors/src/tests/scheduling.rs @@ -1,4 +1,293 @@ use super::*; +use crate::{FundingProvenance, TriggerCauseProvenance}; + +#[test] +fn empty_on_idle_settles_housekeeping_into_actor_control() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let _ = Actors::on_initialize(1); + run_prepass(); + let before = Actors::block_resource_state().expect("prepass opens resource state"); + let consumed = Actors::on_idle(1, Weight::MAX); + let after = Actors::block_resource_state().expect("idle settlement retains resource state"); + assert_ne!(consumed, Weight::zero()); + assert_eq!(after.outstanding_reservations(), 0); + assert_eq!(after.phase(), crate::BlockResourcePhase::Finalizable); + let settled_housekeeping = after + .usage() + .actor_control_used() + .saturating_sub(before.usage().actor_control_used()); + assert_ne!(settled_housekeeping, Weight::zero()); + assert!(settled_housekeeping.all_lte(consumed)); + assert_eq!(after.usage().actor_effect_used(), Weight::zero()); + assert_eq!( + Actors::finalized_block_resource_telemetry().map(|snapshot| snapshot.block_number()), + Some(1) + ); + Actors::on_finalize(1); + }); +} + +#[test] +fn payload_free_actor_prepass_inherent_is_required_and_canonical() { + use polkadot_sdk::{frame_support::inherent::ProvideInherent, sp_inherents::InherentData}; + + let missing = InherentData::new(); + assert!(::create_inherent(&missing).is_none()); + assert!( + ::is_inherent_required(&missing) + .expect("required check is deterministic") + .is_some() + ); + + let mut present = InherentData::new(); + crate::provide_actor_prepass_inherent_data(&mut present).expect("empty prepass data encodes"); + let call = ::create_inherent(&present) + .expect("canonical empty data creates prepass"); + assert!(::is_inherent(&call)); + assert!(::check_inherent(&call, &present).is_ok()); + assert!(::check_inherent(&call, &missing).is_err()); + + let mut unsupported = InherentData::new(); + unsupported + .put_data( + ::INHERENT_IDENTIFIER, + &crate::ActorPrepassInherentData { + version: crate::ACTOR_PREPASS_INHERENT_VERSION.saturating_add(1), + }, + ) + .expect("unsupported fixture encodes"); + assert!(::create_inherent(&unsupported).is_none()); + assert!(::check_inherent(&call, &unsupported).is_err()); +} + +#[test] +fn actor_prepass_rejects_signed_origin_before_resource_mutation() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + assert_noop!( + Actors::actor_prepass(RuntimeOrigin::signed(ALICE)), + DispatchError::BadOrigin + ); + assert!(Actors::block_resource_state().is_none()); + assert!(Actors::prepass_execution_cutoff().is_none()); + }); +} + +#[test] +fn block_finalize_rejects_missing_drain_and_stale_state() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + Actors::on_initialize(1); + run_prepass(); + assert!(std::panic::catch_unwind(|| Actors::on_finalize(1)).is_err()); + }); + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + Actors::on_initialize(1); + run_prepass(); + assert!(std::panic::catch_unwind(|| Actors::on_finalize(2)).is_err()); + }); +} + +#[test] +fn block_finalize_rejects_unsettled_reservation_and_missing_telemetry() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + Actors::on_initialize(1); + run_prepass(); + let budget = TestBlockResourceBudget::get(); + let mut state = Actors::block_resource_state().expect("initialize opens resource state"); + state + .reserve( + budget.limits(), + crate::BlockResourceDomain::UserDispatch, + Weight::from_parts(1, 1), + ) + .expect("external phase admits user reservation"); + crate::CurrentBlockResourceState::::put(state); + assert!(std::panic::catch_unwind(|| Actors::on_finalize(1)).is_err()); + }); + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + Actors::on_initialize(1); + run_prepass(); + Actors::on_idle(1, Weight::MAX); + crate::FinalizedBlockResourceTelemetry::::kill(); + assert!(std::panic::catch_unwind(|| Actors::on_finalize(1)).is_err()); + }); +} + +#[test] +fn block_finalize_consumes_the_one_pass_marker() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + Actors::on_initialize(1); + run_prepass(); + Actors::on_idle(1, Weight::MAX); + Actors::on_finalize(1); + assert!(Actors::block_resource_state().is_none()); + assert!(std::panic::catch_unwind(|| Actors::on_finalize(1)).is_err()); + }); +} + +#[test] +fn exhausted_actor_control_prevents_on_idle_housekeeping_mutation() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let _ = Actors::on_initialize(1); + run_prepass(); + let budget = TestBlockResourceBudget::get(); + let mut state = Actors::block_resource_state().expect("prepass opens resource state"); + let remaining = budget + .limits() + .actor_control() + .checked_sub(&state.usage().actor_control_used()) + .expect("cutoff owner fits Actor Control"); // deos-bypass: panic-owner — runtime configuration test proves cutoff fit before this package fixture. + let mut reservation = state + .reserve( + budget.limits(), + crate::BlockResourceDomain::ActorControl, + remaining, + ) + .expect("remaining Actor Control is exactly reservable"); // deos-bypass: panic-owner — maximum equals the checked residual of the same limit and usage. + assert_eq!(state.settle(&mut reservation, remaining), Ok(())); + crate::CurrentBlockResourceState::::put(state); + let cursor_before = Actors::materialization_family_cursor(); + + assert_eq!(Actors::on_idle(1, Weight::MAX), Weight::zero()); + assert_eq!(Actors::materialization_family_cursor(), cursor_before); + }); +} + +#[test] +fn block_initialize_freezes_the_pre_external_ticket_frontier() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + crate::NextQueueTicket::::put(7); + crate::PrepassExecutionCutoff::::put((0, 3)); + assert_eq!(Actors::on_initialize(1), Weight::zero()); + let prepass = Actors::actor_prepass(RuntimeOrigin::none()).expect("prepass succeeds"); + let empty_prepass = prepass + .actual_weight + .expect("prepass reports actual Weight"); + assert!( + ::scheduler_on_initialize_cutoff() + .all_lte(empty_prepass) + ); + assert_eq!(Actors::prepass_execution_cutoff(), Some((1, 7))); + let state = Actors::block_resource_state().expect("resource state is opened once"); + assert_eq!(state.phase(), crate::BlockResourcePhase::ExternalPhase); + assert_eq!(state.usage().actor_control_used(), empty_prepass); + assert!(!state.optional_actor_work_halted()); + + crate::NextQueueTicket::::put(9); + assert_noop!( + Actors::actor_prepass(RuntimeOrigin::none()), + crate::Error::::PrepassDuplicateOrStale + ); + assert_eq!(Actors::prepass_execution_cutoff(), Some((1, 7))); + let duplicate = Actors::block_resource_state().expect("duplicate preserves state"); + assert_eq!(duplicate.usage(), state.usage()); + assert!(!duplicate.optional_actor_work_halted()); + }); +} + +#[test] +fn prepass_materialization_stays_behind_the_frozen_cutoff_until_next_block() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = create_system_with(ALICE, timer_schedule(1), None, inert_contract_steps()); + run_prepass(); + Actors::on_idle(1, Weight::MAX); + Actors::on_finalize(1); + + frame_system::Pallet::::set_block_number(2); + let cutoff = Actors::next_queue_ticket(); + run_prepass(); + assert_eq!(Actors::prepass_execution_cutoff(), Some((2, cutoff))); + let deferred = Actors::active_actor_view(actor_id).expect("timer Actor remains active"); + assert_eq!(deferred.cycle_nonce, 0); + assert!(deferred.pending_signal); + assert!(deferred.queue_ticket.is_some_and(|ticket| ticket >= cutoff)); + Actors::on_idle(2, Weight::MAX); + Actors::on_finalize(2); + + frame_system::Pallet::::set_block_number(3); + run_prepass(); + assert_eq!( + Actors::active_actor_view(actor_id).map(|actor| actor.cycle_nonce), + Some(1) + ); + }); +} + +#[test] +fn hook_order_fixture_exposes_external_ticket_after_prepass_cutoff() { + new_test_ext().execute_with(|| { + let actor_id = create_system_with(ALICE, manual_schedule(), None, inert_contract_steps()); + let before_external = Actors::next_queue_ticket(); + + let consumed = run_actor_hook_order_with_external( + 1, + || { + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + }, + Weight::MAX, + ); + + assert_eq!( + Actors::prepass_execution_cutoff(), + Some((1, before_external)) + ); + assert!( + Actors::prepass_execution_cutoff() + .is_some_and(|(_, cutoff)| Actors::next_queue_ticket() > cutoff) + ); + let hot = Actors::actor_hot(actor_id).expect("Actor remains pending for the next block"); + assert!(hot.pending_signal); + assert!(hot.queue_ticket.is_some()); + assert_eq!( + Actors::active_actor_view(actor_id).map(|actor| actor.cycle_nonce), + Some(0) + ); + assert!( + ::scheduler_on_initialize_cutoff().all_lte(consumed) + ); + + frame_system::Pallet::::set_block_number(2); + Actors::on_initialize(2); + run_prepass(); + Actors::on_idle(2, Weight::MAX); + assert_eq!( + Actors::active_actor_view(actor_id).map(|actor| actor.cycle_nonce), + Some(1) + ); + }); +} + +#[test] +fn certified_ingress_maps_funding_provenance_to_cause_phase() { + assert_eq!( + Actors::test_trigger_cause_provenance(Some(&FundingProvenance::Signed)), + TriggerCauseProvenance::ExternalPhase + ); + assert_eq!( + Actors::test_trigger_cause_provenance(Some(&FundingProvenance::Xcm)), + TriggerCauseProvenance::Deferred + ); + assert_eq!( + Actors::test_trigger_cause_provenance(Some(&FundingProvenance::InternalProtocol)), + TriggerCauseProvenance::Deferred + ); + assert_eq!( + Actors::test_trigger_cause_provenance(None), + TriggerCauseProvenance::Deferred + ); +} #[test] fn activation_preflight_selects_prime_schedule_without_mutating_state() { @@ -251,6 +540,117 @@ fn queue_pair_preflight_crosses_page_boundary_without_mutating_state() { }); } +#[test] +fn zero_step_opening_completes_without_step_or_run_state() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = create_system_with(ALICE, manual_schedule(), None, BoundedVec::default()); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + assert_eq!(Actors::queue_occupancy(), 1); + let (state, _, loaded_step) = + Actors::load_actor_service_state(actor_id).expect("zero-Step service state loads"); + assert!(loaded_step.is_none()); + let instance = Actors::derive_active_actor_view(state.identity, state.hot, state.contract); + assert_eq!( + Actors::classify_actor(actor_id, &instance) + .expect("zero-Step Actor classifies") + .execution_phase, + ActorExecutionPhase::Ready + ); + System::reset_events(); + + let budget = TestBlockResourceBudget::get(); + let mut resource_state = crate::BlockResourceState::new(1); + assert_eq!(resource_state.begin_prepass(), Ok(())); + assert_eq!(resource_state.open_external_phase(), Ok(())); + assert_eq!(resource_state.begin_drain(), Ok(())); + let pass = Actors::execute_cycle_to_cutoff_with_resources( + Weight::MAX, + Actors::next_queue_ticket(), + &mut resource_state, + budget.limits(), + crate::BlockResourceDomain::ActorDrainEffect, + budget.limits().actor_control(), + ); + let (control, effect) = pass + .reconciled_domains() + .expect("zero-Step pass has complete control evidence"); // deos-bypass: panic-owner — zero-Step executes no Task effect and returns bounded control evidence. + assert_eq!(effect, Weight::zero()); + assert_eq!(control, pass.consumed); + assert_eq!(resource_state.outstanding_reservations(), 0); + assert_eq!(resource_state.usage().actor_control_used(), control); + assert_eq!(resource_state.usage().actor_effect_used(), Weight::zero()); + + let identity = Actors::actor_identities(actor_id).expect("persistent zero-Step Actor remains"); + assert_eq!(identity.cycle_nonce, 1); + assert!(ActorRunStateStore::::get(actor_id).is_none()); + assert!( + Actors::actor_hot(actor_id) + .is_some_and(|hot| { hot.cycle_state == CycleState::Idle && hot.queue_ticket.is_none() }) + ); + let actor_events = System::events() + .into_iter() + .filter_map(|record| match record.event { + RuntimeEvent::Actors(event) => Some(event), + _ => None, + }) + .collect::>(); + assert_eq!( + actor_events, + vec![ + Event::CycleStarted { + actor_id, + cycle_nonce: 1, + }, + Event::CycleSummary { + actor_id, + cycle_nonce: 1, + result: CycleResult::Completed, + outcomes: OutcomeTotals::default(), + }, + ] + ); + }); +} + +#[test] +fn zero_step_auto_close_one_is_atomic_and_has_no_run_state() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = Actors::next_actor_id(); + let mut contract = system_active_contract(manual_schedule(), None, BoundedVec::default()) + .expect("zero-Step Contract exists"); + contract.auto_close_at_cycle_nonce = Some(1); + assert_ok!(Actors::create_system_actor( + RuntimeOrigin::root(), + ALICE, + Mutability::Mutable, + Some(contract), + )); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + System::reset_events(); + + run_idle(Weight::MAX); + + assert!(Actors::actor_identities(actor_id).is_none()); + assert!(ActorRunStateStore::::get(actor_id).is_none()); + assert!(System::events().iter().any(|record| matches!( + record.event, + RuntimeEvent::Actors(Event::ActorClosed { + actor_id: closed, + reason: CloseReason::AutoCloseNonceReached, + .. + }) if closed == actor_id + ))); + }); +} + #[test] fn dormant_identity_owns_no_scheduler_state_and_round_trips_activation() { new_test_ext().execute_with(|| { @@ -279,17 +679,6 @@ fn dormant_identity_owns_no_scheduler_state_and_round_trips_activation() { let preserved = 777; let _ = >::deposit_creating(&identity.sovereign_account, preserved); - assert_noop!( - Actors::activate_actor( - RuntimeOrigin::signed(ALICE), - actor_id, - user_active_contract(manual_schedule(), None, BoundedVec::default()) - .expect("direct empty contract"), - ), - Error::::EmptyContractSteps - ); - assert!(Actors::actor_identities(actor_id).is_some()); - assert_eq!(Actors::active_actor_count(), 0); assert_noop!( Actors::activate_actor( RuntimeOrigin::signed(ALICE), @@ -324,7 +713,7 @@ fn dormant_identity_owns_no_scheduler_state_and_round_trips_activation() { assert!(Actors::actor_identities(actor_id).is_some()); let _activated = Actors::active_actor_view(actor_id).expect("active Actor Contract exists"); assert_eq!( - ActorContracts::::get(actor_id) + Actors::load_actor_contract(actor_id) .expect("active Actor Contract") .funding, FundingSourcePolicy::AnyVerifiedIngress @@ -458,7 +847,26 @@ fn manual_trigger_survives_paused_queue_pop_and_resume() { actor_id )); assert_ok!(Actors::pause_actor(RuntimeOrigin::signed(ALICE), actor_id)); - run_idle(Weight::MAX); + let budget = TestBlockResourceBudget::get(); + let mut resource_state = crate::BlockResourceState::new(1); + assert_eq!(resource_state.begin_prepass(), Ok(())); + assert_eq!(resource_state.open_external_phase(), Ok(())); + assert_eq!(resource_state.begin_drain(), Ok(())); + let pass = Actors::execute_cycle_to_cutoff_with_resources( + Weight::MAX, + Actors::next_queue_ticket(), + &mut resource_state, + budget.limits(), + crate::BlockResourceDomain::ActorDrainEffect, + budget.limits().actor_control(), + ); + assert_eq!( + pass.reconciled_domains(), + Some((pass.consumed, Weight::zero())) + ); + assert_eq!(resource_state.outstanding_reservations(), 0); + assert_eq!(resource_state.usage().actor_control_used(), pass.consumed); + assert_eq!(resource_state.usage().actor_effect_used(), Weight::zero()); let paused = Actors::active_actor_view(actor_id).expect("Actors exists"); assert!(paused.pending_signal); assert_eq!(paused.cycle_nonce, 0); @@ -484,13 +892,28 @@ fn queued_actor_is_preserved_when_proof_budget_cannot_admit_probe() { <::WeightInfo as crate::WeightInfo>::scheduler_paged_tombstone_drain( 1, ); - Actors::execute_cycle(Weight::from_parts( - u64::MAX, - scan_weight - .proof_size() - .saturating_add(Actors::scheduler_actor_probe_weight_upper().proof_size()) - .saturating_sub(1), - )); + let budget = TestBlockResourceBudget::get(); + let mut resource_state = crate::BlockResourceState::new(1); + assert_eq!(resource_state.begin_prepass(), Ok(())); + assert_eq!(resource_state.open_external_phase(), Ok(())); + assert_eq!(resource_state.begin_drain(), Ok(())); + let pass = Actors::execute_cycle_to_cutoff_with_resources( + Weight::from_parts( + u64::MAX, + scan_weight + .proof_size() + .saturating_add(Actors::scheduler_actor_probe_weight_upper().proof_size()) + .saturating_sub(1), + ), + Actors::next_queue_ticket(), + &mut resource_state, + budget.limits(), + crate::BlockResourceDomain::ActorDrainEffect, + budget.limits().actor_control(), + ); + assert_eq!(resource_state.outstanding_reservations(), 0); + assert_eq!(resource_state.usage().actor_control_used(), pass.consumed); + assert_eq!(resource_state.usage().actor_effect_used(), Weight::zero()); let instance = Actors::active_actor_view(actor_id).expect("Actors exists"); assert!(instance.pending_signal); assert_eq!(instance.cycle_nonce, 0); @@ -673,10 +1096,7 @@ fn enqueue_rolls_back_on_missing_or_malformed_tail_page() { maybe_page .as_mut() .expect("tail page") - .try_push(QueueEntry { - ticket: 99_999, - actor_id: candidate, - }) + .try_push(queue_entry(99_999, candidate)) .expect("malformed extra slot fits"); }); } else { @@ -787,16 +1207,8 @@ fn paged_queue_uses_one_live_actor_ticket_and_lazy_invalidation() { Actors::actor_hot(actor_id).expect("hot state").queue_ticket, None ); - assert_eq!( - Actors::paged_head_entry(), - Some(( - 0, - QueueEntry { - ticket: 0, - actor_id - } - )) - ); + let (position, entry) = Actors::paged_head_entry().expect("queue head exists"); + assert_eq!((position, entry.ticket, entry.actor_id), (0, 0, actor_id)); let drained = Actors::paged_drain_tombstones(Actors::next_queue_ticket(), 1) .expect("invalidated head drains as a tombstone"); assert_eq!(drained.tombstones_skipped, 1); @@ -812,11 +1224,7 @@ fn canonical_queue_try_state_rejects_an_internal_physical_gap() { new_test_ext().execute_with(|| { QueuePages::::insert( 0, - BoundedVec::try_from(vec![QueueEntry { - ticket: 0, - actor_id: 99_999, - }]) - .expect("one queue entry fits"), + BoundedVec::try_from(vec![queue_entry(0, 99_999)]).expect("one queue entry fits"), ); QueueHead::::put(0); QueueTail::::put(2); @@ -844,15 +1252,10 @@ fn paged_queue_crosses_and_reclaims_page_boundaries_without_prefix_rewrites() { ); for (ticket, actor_id) in actors.iter().take(32).copied().enumerate() { + let (position, entry) = Actors::paged_head_entry().expect("queue head exists"); assert_eq!( - Actors::paged_head_entry(), - Some(( - ticket as u64, - QueueEntry { - ticket: ticket as u64, - actor_id, - }, - )) + (position, entry.ticket, entry.actor_id), + (ticket as u64, ticket as u64, actor_id) ); assert!(Actors::paged_consume_head(ticket as u64)); } @@ -872,6 +1275,43 @@ fn paged_queue_crosses_and_reclaims_page_boundaries_without_prefix_rewrites() { }); } +#[test] +fn stale_exact_step_ticket_is_consumed_without_execution() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = create_system_with(ALICE, manual_schedule(), None, inert_contract_steps()); + assert!(Actors::paged_enqueue(actor_id)); + let canonical = Actors::paged_head_entry() + .expect("canonical Step ticket exists") + .1; + assert_eq!(canonical.actor_id, actor_id); + assert_eq!(canonical.cycle_nonce, 1); + assert_eq!(canonical.cursor, 0); + QueuePages::::mutate(0, |maybe| { + maybe.as_mut().expect("queue page exists")[0] + .contract_commitment + .body_commitment = [99; 32]; + }); + + let pass = Actors::execute_cycle(Weight::MAX); + assert!(!pass.starved); + assert!(Actors::paged_head_entry().is_none()); + assert_eq!(Actors::queue_occupancy(), 0); + assert_eq!( + Actors::actor_hot(actor_id) + .expect("Actor remains") + .queue_ticket, + None + ); + assert_eq!( + Actors::actor_identities(actor_id) + .expect("identity remains") + .cycle_nonce, + 0 + ); + }); +} + #[test] fn paged_queue_replacement_ticket_leaves_old_entry_as_tombstone() { new_test_ext().execute_with(|| { @@ -895,16 +1335,8 @@ fn paged_queue_replacement_ticket_leaves_old_entry_as_tombstone() { .queue_ticket, Some(1) ); - assert_eq!( - Actors::paged_head_entry(), - Some(( - 0, - QueueEntry { - ticket: 0, - actor_id: actor_a, - }, - )) - ); + let (position, entry) = Actors::paged_head_entry().expect("queue head exists"); + assert_eq!((position, entry.ticket, entry.actor_id), (0, 0, actor_a)); let drained = Actors::paged_drain_tombstones(Actors::next_queue_ticket(), 1) .expect("replacement head drains as a tombstone"); assert_eq!(drained.tombstones_skipped, 1); @@ -914,16 +1346,8 @@ fn paged_queue_replacement_ticket_leaves_old_entry_as_tombstone() { .queue_ticket, Some(2) ); - assert_eq!( - Actors::paged_head_entry(), - Some(( - 1, - QueueEntry { - ticket: 1, - actor_id: actor_b, - }, - )) - ); + let (position, entry) = Actors::paged_head_entry().expect("queue head exists"); + assert_eq!((position, entry.ticket, entry.actor_id), (1, 1, actor_b)); #[cfg(feature = "try-runtime")] assert_ok!(crate::Pallet::::do_try_state()); }); @@ -966,6 +1390,19 @@ fn paged_tombstone_drain_is_scan_bounded_and_reclaims_multiple_pages() { }); } +#[test] +fn mandatory_prepass_reclaims_saturated_fifo_without_domain_spill() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + seed_saturated_tombstone_queue(); + run_prepass(); + assert!(Actors::queue_head() > 0); + let state = Actors::block_resource_state().expect("prepass resource state exists"); + assert_eq!(state.outstanding_reservations(), 0); + assert_eq!(state.phase(), crate::BlockResourcePhase::ExternalPhase); + }); +} + #[test] fn saturated_tombstone_queue_reclaims_head_before_ingress_and_recovers_deferred_work() { new_test_ext().execute_with(|| { @@ -977,11 +1414,13 @@ fn saturated_tombstone_queue_reclaims_head_before_ingress_and_recovers_deferred_ let first_ticket = page_id.saturating_mul(page_size); let len = page_size.min(capacity.saturating_sub(first_ticket)); let entries = (0..len) - .map(|offset| QueueEntry { - ticket: u64::from(first_ticket).saturating_add(u64::from(offset)), - actor_id: 10_000_000u64 - .saturating_add(u64::from(first_ticket)) - .saturating_add(u64::from(offset)), + .map(|offset| { + queue_entry( + u64::from(first_ticket).saturating_add(u64::from(offset)), + 10_000_000u64 + .saturating_add(u64::from(first_ticket)) + .saturating_add(u64::from(offset)), + ) }) .collect::>(); QueuePages::::insert( @@ -1008,6 +1447,9 @@ fn saturated_tombstone_queue_reclaims_head_before_ingress_and_recovers_deferred_ .saturating_add( <::WeightInfo as crate::WeightInfo>::materialization_coordinator_base(), ) + .saturating_add( + <::WeightInfo as crate::WeightInfo>::block_resource_finalize(), + ) .saturating_add( <::WeightInfo as crate::WeightInfo>::scheduler_paged_tombstone_drain( 1, @@ -1298,14 +1740,15 @@ fn simulation_and_scheduler_reject_the_same_protected_fee_floor_boundary() { contract_steps, ); deplete_user_sovereign(actor_id, prefunded); + let attempt_fee = Actors::maximum_contract_step_fee(ActorType::User, &contract.steps, 0) + .expect("current-Step fee is bounded") + .total_fee; + let raw_balance = attempt_fee.max(TestMinUserBalance::get()); + fund_native(actor_id, raw_balance.saturating_add(manual_trigger_fee())); assert_ok!(Actors::manual_trigger( RuntimeOrigin::signed(ALICE), actor_id )); - let instance = Actors::active_actor_view(actor_id).expect("User actor exists"); - let attempt_fee = Actors::attempt_fee_upper_bound(&instance, 0); - let raw_balance = attempt_fee.max(TestMinUserBalance::get()); - fund_native(actor_id, raw_balance); assert!( raw_balance >= attempt_fee, "raw balance covers the attempt envelope" @@ -1327,7 +1770,7 @@ fn simulation_and_scheduler_reject_the_same_protected_fee_floor_boundary() { .expect("terminal viability projects as a closed simulation"); assert_eq!( result.status, - AttemptDisposition::Closed(CloseReason::FeeBudgetExhausted) + AttemptDisposition::Closed(CloseReason::CycleAdmissionInsufficient) ); assert_eq!(Actors::active_actor_view(actor_id), Some(actor_before)); assert_eq!(System::events(), events_before); @@ -1338,7 +1781,7 @@ fn simulation_and_scheduler_reject_the_same_protected_fee_floor_boundary() { event, Event::ActorClosed { actor_id: id, - reason: CloseReason::FeeBudgetExhausted, + reason: CloseReason::CycleAdmissionInsufficient, } if *id == actor_id ))); }); @@ -1364,7 +1807,7 @@ fn scheduler_retries_manual_continuation_after_cooldown_without_new_signal() { run_idle(Weight::MAX); assert_eq!(scheduled_wakeup_block(actor_id), Some(3)); assert_eq!( - Actors::continuation_state(actor_id) + Actors::actor_run_state(actor_id) .expect("suspended") .unsuccessful_attempts_at_cursor, 1 @@ -1373,7 +1816,7 @@ fn scheduler_retries_manual_continuation_after_cooldown_without_new_signal() { frame_system::Pallet::::set_block_number(2); run_idle(Weight::MAX); assert_eq!( - Actors::continuation_state(actor_id) + Actors::actor_run_state(actor_id) .expect("still suspended") .unsuccessful_attempts_at_cursor, 1 @@ -1385,7 +1828,7 @@ fn scheduler_retries_manual_continuation_after_cooldown_without_new_signal() { let completed = Actors::active_actor_view(actor_id).expect("actor completes"); assert_eq!(completed.cycle_nonce, 1); assert_eq!(completed.cycle_state, CycleState::Idle); - assert!(Actors::continuation_state(actor_id).is_none()); + assert!(Actors::actor_run_state(actor_id).is_none()); }); } @@ -1528,7 +1971,7 @@ fn canonical_tombstones_cannot_bypass_the_oldest_live_head() { } #[test] -fn post_attempt_rearm_rolls_back_on_fifo_topology_corruption() { +fn pipeline_opening_rearms_cadence_from_current_tick() { new_test_ext().execute_with(|| { frame_system::Pallet::::set_block_number(1); let actor_id = create_system_with( @@ -1541,30 +1984,15 @@ fn post_attempt_rearm_rolls_back_on_fifo_topology_corruption() { frame_system::Pallet::::set_block_number(2); let mut wakeup_meter = WeightMeter::with_limit(Weight::MAX); Actors::drain_overdue_wakeups_cursor(2, &mut wakeup_meter); - crate::WakeupBuckets::::insert( - WakeupKey::Tick(3), - WakeupBucketState { - head_page: 0, - tail_page: 0, - next_page_id: 1, - live_entries: 0, - cursor_index: None, - }, - ); - let actor_before = Actors::active_actor_view(actor_id).expect("queued actor"); + assert!(Actors::actor_hot(actor_id).is_some_and(|hot| hot.trigger_wakeup_pointer.is_none())); let bob_before = native_balance(&BOB); - let events_before = System::events(); - let root_before = polkadot_sdk::sp_io::storage::root(StateVersion::V1); let _ = Actors::execute_cycle(Weight::MAX); - assert_eq!(native_balance(&BOB), bob_before); - assert_eq!(Actors::active_actor_view(actor_id), Some(actor_before)); - assert_eq!(System::events(), events_before); - assert_eq!( - polkadot_sdk::sp_io::storage::root(StateVersion::V1), - root_before - ); + assert_eq!(native_balance(&BOB), bob_before + 10); + let state = Actors::active_actor_state(actor_id).expect("Actor remains active"); + assert_eq!(state.identity.cycle_nonce, 1); + assert!(state.hot.trigger_wakeup_pointer.is_some()); }); } @@ -1601,7 +2029,7 @@ fn scheduler_close_rolls_back_on_fifo_topology_corruption() { } #[test] -fn continuation_retry_omits_external_timer_cadence() { +fn run_retry_preserves_independent_external_timer_cadence() { new_test_ext().execute_with(|| { frame_system::Pallet::::set_block_number(1); setup_temporary_retry_pool(); @@ -1617,15 +2045,14 @@ fn continuation_retry_omits_external_timer_cadence() { frame_system::Pallet::::set_block_number(cadence_due); run_idle(Weight::MAX); - assert_eq!(scheduled_wakeup_block(actor_id), None); - assert!( - Actors::actor_hot(actor_id) - .expect("suspended cadence actor") - .queue_ticket - .is_some() + let hot = Actors::actor_hot(actor_id).expect("suspended cadence actor"); + assert_eq!( + hot.trigger_wakeup_pointer.map(|pointer| pointer.tick), + Some(cadence_due + 100) ); + assert!(hot.queue_ticket.is_some()); assert_eq!( - Actors::continuation_state(actor_id) + Actors::actor_run_state(actor_id) .expect("suspended") .unsuccessful_attempts_at_cursor, 1 @@ -1647,7 +2074,7 @@ fn pause_and_breaker_gate_scheduler_owned_retry() { )); run_idle(Weight::MAX); assert_eq!( - Actors::continuation_state(actor_id) + Actors::actor_run_state(actor_id) .expect("suspended") .unsuccessful_attempts_at_cursor, 1 @@ -1657,7 +2084,7 @@ fn pause_and_breaker_gate_scheduler_owned_retry() { assert_ok!(Actors::pause_actor(RuntimeOrigin::signed(ALICE), actor_id)); run_idle(Weight::MAX); assert_eq!( - Actors::continuation_state(actor_id) + Actors::actor_run_state(actor_id) .expect("paused") .unsuccessful_attempts_at_cursor, 1 @@ -1671,7 +2098,7 @@ fn pause_and_breaker_gate_scheduler_owned_retry() { )); run_idle(Weight::MAX); assert_eq!( - Actors::continuation_state(actor_id) + Actors::actor_run_state(actor_id) .expect("breaker gated") .unsuccessful_attempts_at_cursor, 1 @@ -1684,7 +2111,7 @@ fn pause_and_breaker_gate_scheduler_owned_retry() { set_temporary_dex_failure(false); frame_system::Pallet::::set_block_number(4); run_idle(Weight::MAX); - assert!(Actors::continuation_state(actor_id).is_none()); + assert!(Actors::actor_run_state(actor_id).is_none()); assert_eq!( Actors::active_actor_view(actor_id) .expect("completed") @@ -1768,7 +2195,7 @@ fn semantic_control_origins_share_one_queue_churn_clock() { let cancel_id = create_suspended_system_retry(4); assert_ok!(Actors::pause_actor(RuntimeOrigin::root(), cancel_id)); assert_noop!( - Actors::cancel_continuation(RuntimeOrigin::signed(ALICE), cancel_id), + Actors::cancel_run(RuntimeOrigin::signed(ALICE), cancel_id), Error::::ControlMutationRateLimited ); @@ -1794,6 +2221,46 @@ fn semantic_control_origins_share_one_queue_churn_clock() { }); } +#[test] +#[ignore = "10,000-identity production profile; run explicitly in release mode"] +fn maximum_dormant_identity_population_adds_no_idle_scan() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + run_prepass(); + let empty_idle = Actors::on_idle(1, Weight::MAX); + Actors::on_finalize(1); + + let maximum = ::MaxActorIdentities::get(); + let first_dormant = Actors::next_actor_id(); + for _ in 0..maximum { + assert_ok!(Actors::create_system_actor( + RuntimeOrigin::root(), + ALICE, + Mutability::Mutable, + None, + )); + } + assert_eq!(crate::ActorIdentityCount::::get(), maximum); + assert_eq!(crate::ActiveActorCount::::get(), 0); + + System::set_block_number(2); + run_prepass(); + let saturated_dormant_idle = Actors::on_idle(2, Weight::MAX); + Actors::on_finalize(2); + assert_eq!(saturated_dormant_idle, empty_idle); + + System::set_block_number(3); + assert_ok!(Actors::activate_actor( + RuntimeOrigin::signed(ALICE), + first_dormant, + system_active_contract(manual_schedule(), None, inert_contract_steps()) + .expect("bounded activation Contract"), + )); + assert_eq!(crate::ActiveActorCount::::get(), 1); + assert_eq!(crate::ActorIdentityCount::::get(), maximum); + }); +} + #[test] fn zero_on_idle_budget_performs_no_storage_or_telemetry_work() { new_test_ext().execute_with(|| { @@ -1854,6 +2321,7 @@ fn empty_materialization_families_charge_only_their_measured_probes_and_yield() ) .saturating_add(::crossing_worker_base()) .saturating_add(::observation_fanout_base()) + .saturating_add(::block_resource_finalize()) .saturating_add(::scheduler_paged_tombstone_drain(1)); assert_eq!(Actors::on_idle(1, Weight::MAX), expected); @@ -1881,6 +2349,9 @@ fn materialization_family_cursor_rotates_deterministically_and_corruption_fails_ .saturating_add( <::WeightInfo as crate::WeightInfo>::materialization_coordinator_base( ), + ) + .saturating_add( + <::WeightInfo as crate::WeightInfo>::block_resource_finalize(), ); for expected in [1u8, 2, 0] { let used = Actors::on_idle(1, Weight::MAX); @@ -2130,7 +2601,7 @@ fn breaker_freezes_starvation_count_without_recovery_event() { } #[test] -fn breaker_defers_scheduler_owned_fee_budget_close_without_partial_terminal_events() { +fn breaker_defers_pipeline_admission_apoptosis_without_partial_events() { new_test_ext().execute_with(|| { frame_system::Pallet::::set_block_number(1); let prefunded = user_prefunding_requirement(&transfer_contract_steps(BOB, 1)); @@ -2142,7 +2613,7 @@ fn breaker_defers_scheduler_owned_fee_budget_close_without_partial_terminal_even transfer_contract_steps(BOB, 1), ); deplete_user_sovereign(actor_id, prefunded); - fund_native(actor_id, 60); + fund_native(actor_id, 60 + manual_trigger_fee()); assert_ok!(Actors::manual_trigger( RuntimeOrigin::signed(ALICE), actor_id @@ -2174,7 +2645,7 @@ fn breaker_defers_scheduler_owned_fee_budget_close_without_partial_terminal_even event, Event::ActorClosed { actor_id: id, - reason: CloseReason::FeeBudgetExhausted, + reason: CloseReason::CycleAdmissionInsufficient, } if *id == actor_id ))); }); @@ -2226,6 +2697,236 @@ fn breaker_defers_scheduler_owned_window_expiry_close() { }); } +#[test] +fn at_time_occurrence_charges_once_consumes_deadline_and_latches_readiness() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = create_user_with( + ALICE, + Mutability::Mutable, + at_time_schedule(1), + None, + inert_contract_steps(), + ); + clear_fee_collections(); + + frame_system::Pallet::::set_block_number(2); + let mut meter = WeightMeter::with_limit(Weight::MAX); + Actors::drain_overdue_wakeups_cursor(2, &mut meter); + + assert_eq!(fee_collections(), vec![at_time_trigger_fee()]); + let hot = Actors::actor_hot(actor_id).expect("AtTime Actor remains active"); + assert!(hot.pending_signal); + assert!(hot.queue_ticket.is_some()); + assert!(hot.trigger_wakeup_pointer.is_none()); + assert!(matches!( + hot.trigger_runtime_state, + TriggerRuntimeState::AtTime { consumed: true, .. } + )); + assert!(has_actor_event(|event| matches!( + event, + Event::TriggerOccurrenceProcessed { + actor_id: id, + trigger_family: TriggerFamily::AtTime, + .. + } if *id == actor_id + ))); + + frame_system::Pallet::::set_block_number(20); + let mut meter = WeightMeter::with_limit(Weight::MAX); + Actors::drain_overdue_wakeups_cursor(20, &mut meter); + assert_eq!(fee_collections(), vec![at_time_trigger_fee()]); + }); +} + +#[test] +fn busy_at_time_occurrence_charges_and_preserves_independent_run_service() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let steps = BoundedVec::try_from(vec![ + make_step(Task::Transfer { + to: BOB, + asset: TestAsset::Native, + amount: AmountResolution::Fixed(1), + }), + make_step(Task::Transfer { + to: CHARLIE, + asset: TestAsset::Native, + amount: AmountResolution::Fixed(1), + }), + ]) + .expect("two-Step Contract fits"); + let actor_id = create_user_with(ALICE, Mutability::Mutable, at_time_schedule(2), None, steps); + fund_native(actor_id, 1_000_000); + assert_eq!( + Actors::request_activation(actor_id), + Ok(crate::scheduler::ActivationOutcome::Latched) + ); + Actors::execute_cycle(Weight::MAX); + let run_before = ActorRunStateStore::::get(actor_id).expect("Pipeline is Running"); + clear_fee_collections(); + + frame_system::Pallet::::set_block_number(3); + let mut meter = WeightMeter::with_limit(Weight::MAX); + Actors::drain_overdue_wakeups_cursor(3, &mut meter); + + assert_eq!(fee_collections(), vec![at_time_trigger_fee()]); + let hot = Actors::actor_hot(actor_id).expect("busy AtTime Actor remains active"); + assert_eq!(hot.cycle_state, CycleState::Running); + assert!(hot.pending_signal); + assert!(hot.trigger_wakeup_pointer.is_none()); + assert!(matches!( + hot.trigger_runtime_state, + TriggerRuntimeState::AtTime { consumed: true, .. } + )); + let run_after = ActorRunStateStore::::get(actor_id).expect("Pipeline remains Running"); + assert_eq!(run_after.cursor, run_before.cursor); + assert_eq!(run_after.cycle_nonce, run_before.cycle_nonce); + }); +} + +#[test] +fn underfunded_at_time_occurrence_selects_prepaid_custody_neutral_apoptosis() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = create_user_with( + ALICE, + Mutability::Immutable, + at_time_schedule(1), + None, + inert_contract_steps(), + ); + let sovereign = sovereign_account(actor_id); + let balance = native_balance(&sovereign); + deplete_user_sovereign(actor_id, balance - TestMinUserBalance::get()); + let custody_before = native_balance(&sovereign); + clear_fee_collections(); + + frame_system::Pallet::::set_block_number(2); + let mut meter = WeightMeter::with_limit(Weight::MAX); + Actors::drain_overdue_wakeups_cursor(2, &mut meter); + + assert!(fee_collections().is_empty()); + assert!(!Actors::active_actor_exists(actor_id)); + assert_eq!(native_balance(&sovereign), custody_before); + assert!(has_actor_event(|event| matches!( + event, + Event::ActorClosed { + actor_id: id, + reason: CloseReason::TriggerAdmissionInsufficient, + } if *id == actor_id + ))); + }); +} + +#[test] +fn at_time_collection_failure_rolls_back_consumption_and_retains_wakeup() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = create_user_with( + ALICE, + Mutability::Mutable, + at_time_schedule(1), + None, + inert_contract_steps(), + ); + let sovereign = sovereign_account(actor_id); + let before = native_balance(&sovereign); + set_fail_fee_sink_transfer(true); + + frame_system::Pallet::::set_block_number(2); + let mut meter = WeightMeter::with_limit(Weight::MAX); + Actors::drain_overdue_wakeups_cursor(2, &mut meter); + set_fail_fee_sink_transfer(false); + + assert_eq!(native_balance(&sovereign), before); + let hot = Actors::actor_hot(actor_id).expect("process remains active"); + assert!(!hot.pending_signal); + assert!(hot.queue_ticket.is_none()); + assert!(hot.trigger_wakeup_pointer.is_some()); + assert!(matches!( + hot.trigger_runtime_state, + TriggerRuntimeState::AtTime { + consumed: false, + .. + } + )); + }); +} + +#[test] +fn immutable_zero_step_at_time_closes_at_authored_cycle_nonce() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let steps = crate::ContractSteps::::default(); + prefund_active_user_creation(ALICE, &steps); + let mut contract = user_active_contract(at_time_schedule(1), None, steps) + .expect("direct zero-Step Actor Contract"); + contract.auto_close_at_cycle_nonce = Some(1); + let actor_id = Actors::next_actor_id(); + let owner_before = native_balance(&ALICE); + let sink_before = native_balance(&TestFeeSink::get()); + assert_ok!(Actors::create_user_actor( + RuntimeOrigin::signed(ALICE), + Mutability::Immutable, + Some(contract), + )); + assert_eq!( + native_balance(&ALICE), + owner_before + .saturating_sub(TestActorCreationFee::get()) + .saturating_sub(actor_state_hold_total(actor_id)) + ); + assert_eq!( + native_balance(&TestFeeSink::get()), + sink_before.saturating_add(TestActorCreationFee::get()) + ); + let slot = Actors::active_actor_view(actor_id) + .and_then(|actor| actor.actor_class.owner_slot()) + .expect("immutable User slot exists"); + let sovereign = Actors::sovereign_account_id(&ALICE, slot); + let residual_asset = TestAsset::Local(9); + set_asset_balance(&sovereign, residual_asset, 919); + assert_noop!( + Actors::close_actor(RuntimeOrigin::signed(ALICE), actor_id), + Error::::ImmutableActor + ); + clear_fee_collections(); + + frame_system::Pallet::::set_block_number(2); + let mut meter = WeightMeter::with_limit(Weight::MAX); + Actors::drain_overdue_wakeups_cursor(2, &mut meter); + Actors::execute_cycle(Weight::MAX); + + assert!(Actors::active_actor_view(actor_id).is_none()); + assert_eq!(asset_balance(&sovereign, residual_asset), 919); + assert_eq!( + fee_collections(), + vec![ + at_time_trigger_fee(), + pipeline_opening_fee(&crate::ContractSteps::::default()), + ] + ); + assert!(has_actor_event(|event| matches!( + event, + Event::ActorClosed { + actor_id: id, + reason: CloseReason::AutoCloseNonceReached, + } if *id == actor_id + ))); + let fresh_id = create_user_with_slot( + ALICE, + slot, + Mutability::Mutable, + manual_schedule(), + None, + inert_contract_steps(), + ); + assert_eq!(sovereign_account(fresh_id), sovereign); + assert_eq!(asset_balance(&sovereign, residual_asset), 919); + }); +} + #[test] fn uninitialized_genesis_cadence_reanchors_without_first_service_execution() { new_test_ext().execute_with(|| { @@ -2245,13 +2946,165 @@ fn uninitialized_genesis_cadence_reanchors_without_first_service_execution() { let instance = Actors::active_actor_view(actor_id).expect("Actors exists"); assert_eq!(instance.cycle_nonce, 0); assert!(!instance.pending_signal); - assert_eq!(instance.cadence_anchor_tick, Some(100)); + assert_eq!(instance.temporal_anchor_tick, Some(100)); assert_eq!(scheduled_wakeup_block(actor_id), Some(101)); }); } #[test] -fn cadence_every_tick_rearms_one_future_tick_without_late_fifo_tickets() { +fn cadenced_latch_disables_detection_until_pipeline_opening() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = create_user_with( + ALICE, + Mutability::Mutable, + timer_schedule(1), + None, + inert_contract_steps(), + ); + clear_fee_collections(); + + frame_system::Pallet::::set_block_number(2); + let mut meter = WeightMeter::with_limit(Weight::MAX); + Actors::drain_overdue_wakeups_cursor(2, &mut meter); + let first = Actors::actor_hot(actor_id).expect("Cadenced Actor remains active"); + let placement = first.queue_ticket; + assert!(first.pending_signal); + assert!(placement.is_some()); + assert!(first.trigger_wakeup_pointer.is_none()); + + frame_system::Pallet::::set_block_number(3); + let mut meter = WeightMeter::with_limit(Weight::MAX); + Actors::drain_overdue_wakeups_cursor(3, &mut meter); + + let fee = cadenced_trigger_fee(); + assert_eq!(fee_collections(), vec![fee]); + let second = Actors::actor_hot(actor_id).expect("Cadenced Actor remains active"); + assert!(second.pending_signal); + assert_eq!(second.queue_ticket, placement); + assert!(second.trigger_wakeup_pointer.is_none()); + assert!(has_actor_event(|event| matches!( + event, + Event::TriggerOccurrenceProcessed { + actor_id: id, + trigger_family: TriggerFamily::Cadenced, + .. + } if *id == actor_id + ))); + }); +} + +#[test] +fn busy_cadenced_occurrence_charges_and_preserves_independent_run_service() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let steps = BoundedVec::try_from(vec![ + make_step(Task::Transfer { + to: BOB, + asset: TestAsset::Native, + amount: AmountResolution::Fixed(1), + }), + make_step(Task::Transfer { + to: CHARLIE, + asset: TestAsset::Native, + amount: AmountResolution::Fixed(1), + }), + ]) + .expect("two-Step Contract fits"); + let actor_id = create_user_with(ALICE, Mutability::Mutable, timer_schedule(1), None, steps); + fund_native(actor_id, 1_000_000); + frame_system::Pallet::::set_block_number(2); + let mut meter = WeightMeter::with_limit(Weight::MAX); + Actors::drain_overdue_wakeups_cursor(2, &mut meter); + Actors::execute_cycle(Weight::MAX); + let run_before = ActorRunStateStore::::get(actor_id).expect("Pipeline is Running"); + clear_fee_collections(); + + frame_system::Pallet::::set_block_number(3); + let mut meter = WeightMeter::with_limit(Weight::MAX); + Actors::drain_overdue_wakeups_cursor(3, &mut meter); + + assert_eq!(fee_collections(), vec![cadenced_trigger_fee()]); + let hot = Actors::actor_hot(actor_id).expect("busy Cadenced Actor remains active"); + assert_eq!(hot.cycle_state, CycleState::Running); + assert!(hot.pending_signal); + assert!(hot.trigger_wakeup_pointer.is_none()); + let run_after = ActorRunStateStore::::get(actor_id).expect("Pipeline remains Running"); + assert_eq!(run_after.cursor, run_before.cursor); + assert_eq!(run_after.cycle_nonce, run_before.cycle_nonce); + }); +} + +#[test] +fn underfunded_cadenced_occurrence_advances_without_fee_readiness_or_apoptosis() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = create_user_with( + ALICE, + Mutability::Mutable, + timer_schedule(1), + None, + inert_contract_steps(), + ); + let sovereign = sovereign_account(actor_id); + let balance = native_balance(&sovereign); + deplete_user_sovereign(actor_id, balance - TestMinUserBalance::get()); + clear_fee_collections(); + + frame_system::Pallet::::set_block_number(2); + let mut meter = WeightMeter::with_limit(Weight::MAX); + Actors::drain_overdue_wakeups_cursor(2, &mut meter); + + assert!(fee_collections().is_empty()); + let hot = Actors::actor_hot(actor_id).expect("underfunded process remains active"); + assert!(!hot.pending_signal); + assert!(hot.queue_ticket.is_none()); + assert!(hot.wakeup_pointer.is_none()); + assert_eq!( + hot.trigger_wakeup_pointer.map(|pointer| pointer.tick), + Some(3) + ); + assert_eq!(native_balance(&sovereign), TestMinUserBalance::get()); + }); +} + +#[test] +fn cadenced_collection_failure_advances_deadline_without_readiness() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = create_user_with( + ALICE, + Mutability::Mutable, + timer_schedule(1), + None, + inert_contract_steps(), + ); + let sovereign = sovereign_account(actor_id); + let before = native_balance(&sovereign); + set_fail_fee_sink_transfer(true); + + frame_system::Pallet::::set_block_number(2); + let mut meter = WeightMeter::with_limit(Weight::MAX); + Actors::drain_overdue_wakeups_cursor(2, &mut meter); + set_fail_fee_sink_transfer(false); + + assert_eq!(native_balance(&sovereign), before); + let hot = Actors::actor_hot(actor_id).expect("process remains active"); + assert!(!hot.pending_signal); + assert!(hot.queue_ticket.is_none()); + assert_eq!( + hot.trigger_wakeup_pointer.map(|pointer| pointer.tick), + Some(3) + ); + assert!(!has_actor_event(|event| matches!( + event, + Event::TriggerOccurrenceProcessed { actor_id: id, .. } if *id == actor_id + ))); + }); +} + +#[test] +fn next_block_cadence_rearms_after_each_deferred_opening_without_late_fifo_tickets() { new_test_ext().execute_with(|| { frame_system::Pallet::::set_block_number(1); let actor_id = create_system_with(ALICE, timer_schedule(1), None, inert_contract_steps()); @@ -2272,15 +3125,22 @@ fn cadence_every_tick_rearms_one_future_tick_without_late_fifo_tickets() { for block in 3..=6 { frame_system::Pallet::::set_block_number(block); Actors::on_initialize(block); + run_prepass(); Actors::on_idle(block, Weight::MAX); - assert_eq!(crate::WakeupCursorLen::::get(WakeupClock::Tick), 1); + let actor = Actors::active_actor_view(actor_id).expect("Actors exists"); + if block % 2 == 1 { + assert!(actor.pending_signal); + assert_eq!(crate::WakeupCursorLen::::get(WakeupClock::Tick), 0); + } else { + assert!(!actor.pending_signal); + assert_eq!(crate::WakeupCursorLen::::get(WakeupClock::Tick), 1); + } } - let cycle_nonce = Actors::active_actor_view(actor_id) - .expect("Actors exists") - .cycle_nonce; - assert!( - cycle_nonce >= 5, - "every-block timer should keep progressing" + assert_eq!( + Actors::active_actor_view(actor_id) + .expect("Actors exists") + .cycle_nonce, + 3 ); }); } @@ -2306,12 +3166,9 @@ fn paused_timer_waits_for_resume_without_queue_churn_or_signal_loss() { 0 ); assert_eq!(scheduled_wakeup_block(actor_id), None); - assert!( - Actors::actor_hot(actor_id) - .expect("paused actor") - .queue_ticket - .is_none() - ); + let paused = Actors::actor_hot(actor_id).expect("paused actor"); + assert!(paused.pending_signal); + assert!(paused.queue_ticket.is_none()); frame_system::Pallet::::set_block_number(7); assert_ok!(Actors::resume_actor(RuntimeOrigin::root(), actor_id)); run_idle(Weight::MAX); @@ -2319,7 +3176,7 @@ fn paused_timer_waits_for_resume_without_queue_churn_or_signal_loss() { Actors::active_actor_view(actor_id) .expect("Actors exists") .cycle_nonce, - 0 + 1 ); assert_eq!(scheduled_wakeup_block(actor_id), Some(8)); frame_system::Pallet::::set_block_number(8); @@ -2328,17 +3185,17 @@ fn paused_timer_waits_for_resume_without_queue_churn_or_signal_loss() { Actors::active_actor_view(actor_id) .expect("Actors exists") .cycle_nonce, - 1 + 2 ); }); } #[test] -fn timer_validation_uses_the_independent_exact_cadence_horizon() { +fn timer_validation_uses_the_independent_exact_temporal_horizon() { new_test_ext().execute_with(|| { frame_system::Pallet::::set_block_number(1); - let max_delay = u32::try_from(::MaxCadenceDelayTicks::get()) - .expect("test cadence horizon fits u32"); + let max_delay = u32::try_from(::MaxTemporalDelayTicks::get()) + .expect("test temporal horizon fits u32"); assert!( u64::from(max_delay) > ::MaxExecutionDelayBlocks::get(), "cadence ticks and consensus blocks have independent horizons" @@ -2435,6 +3292,10 @@ fn scheduler_continues_after_in_loop_close_and_executes_following_ready_actors() close_id, user_prefunding_requirement(&inert_contract_steps()), ); + fund_native( + close_id, + TestMinUserBalance::get().saturating_add(manual_trigger_fee()), + ); let live_id_1 = create_user_with( ALICE, Mutability::Mutable, @@ -2514,6 +3375,10 @@ fn queue_progress_handles_adjacent_removal() { inert_contract_steps(), ); deplete_user_sovereign(id3, user_prefunding_requirement(&inert_contract_steps())); + fund_native( + id3, + TestMinUserBalance::get().saturating_add(manual_trigger_fee()), + ); fund_native(id0, 1_000); fund_native(id1, 1_000); fund_native(id2, 1_000); @@ -2583,6 +3448,11 @@ fn queue_progress_matrix_keeps_progress_and_coverage() { ); if (funded_mask & (1 << idx)) != 0 { fund_native(*actor_id, 1_000); + } else { + fund_native( + *actor_id, + TestMinUserBalance::get().saturating_add(manual_trigger_fee()), + ); } assert_ok!(Actors::manual_trigger( RuntimeOrigin::signed(ALICE), @@ -2626,8 +3496,7 @@ fn queue_progress_matrix_keeps_progress_and_coverage() { fn admission_time_close_reasons_require_complete_queue_and_cleanup_budget() { let reasons = [ CloseReason::WindowExpired, - CloseReason::BalanceExhausted, - CloseReason::FeeBudgetExhausted, + CloseReason::CycleAdmissionInsufficient, CloseReason::CycleNonceExhausted, CloseReason::ConsecutiveFailures, CloseReason::AutoCloseNonceReached, @@ -2730,6 +3599,35 @@ fn mixed_fifo_stops_at_corrupt_actor_without_touching_valid_suffix() { } } +#[test] +fn eligibility_projection_reports_exact_at_time_gate_and_consumption() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = create_system_with(ALICE, at_time_schedule(20), None, inert_contract_steps()); + + assert_eq!( + active_eligibility(actor_id).execution_phase, + ActorExecutionPhase::WaitingCadenceTick(21) + ); + frame_system::Pallet::::set_block_number(21); + assert_eq!( + active_eligibility(actor_id).execution_phase, + ActorExecutionPhase::WaitingCadenceTick(21) + ); + let mut meter = WeightMeter::with_limit(Weight::MAX); + Actors::drain_overdue_wakeups_cursor(21, &mut meter); + assert_eq!( + active_eligibility(actor_id).execution_phase, + ActorExecutionPhase::Ready + ); + Actors::execute_cycle(Weight::MAX); + assert_eq!( + active_eligibility(actor_id).execution_phase, + ActorExecutionPhase::WaitingSignal + ); + }); +} + #[test] fn eligibility_projection_reports_exact_cadence_gate_without_actor_phase() { new_test_ext().execute_with(|| { diff --git a/template/pallets/actors/src/tests/storage_and_api.rs b/template/pallets/actors/src/tests/storage_and_api.rs index 387157e0..f96ed947 100644 --- a/template/pallets/actors/src/tests/storage_and_api.rs +++ b/template/pallets/actors/src/tests/storage_and_api.rs @@ -1,9 +1,37 @@ use super::*; +use crate::{ + ActorAdmissionCertificates, ActorContractHeads, ActorContractTailChunks, ActorCostQuoteError, + PipelineMachineFeeStrategy, +}; +use frame::traits::ConstU32; + +fn test_pipeline_machine_envelope() -> crate::PipelineMachineEnvelope { + crate::PipelineMachineEnvelope { + pipeline_machine_fee_upper: 11, + cleanup_fee_upper: 22, + } +} + +fn geometry_certificate( + contract: &RuntimeActorContract, +) -> crate::ActorAdmissionCertificateOf { + crate::ActorAdmissionCertificate::new( + contract.semantic_contract_id(), + contract.body_commitment().expect("body commitment"), + 1, + [4u8; 32], + 1, + [6u8; 32], + Weight::from_parts(77, 88), + ) +} #[test] fn public_api_error_signatures_use_shared_typed_cores() { let _: fn(ActorId) -> Result, ActorClassificationError> = Actors::actor_eligibility; + let _: fn(ActorId) -> Result, ActorCostQuoteError> = + Actors::actor_cost_quote; let _: fn( ActorId, ActorType, @@ -18,8 +46,8 @@ fn public_api_error_signatures_use_shared_typed_cores() { Error::::ActorInvariant, ), ( - ActorClassificationError::ContinuationInvariant, - Error::::ContinuationInvariant, + ActorClassificationError::RunInvariant, + Error::::ActorRunInvariant, ), ( ActorClassificationError::ComputationOverflow, @@ -34,6 +62,1453 @@ fn public_api_error_signatures_use_shared_typed_cores() { } } +#[test] +fn actor_cost_quote_keeps_fee_boundaries_and_state_hold_provenance_separate() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = create_user_with( + ALICE, + Mutability::Mutable, + manual_schedule(), + None, + transfer_contract_steps(BOB, 2), + ); + let quote = Actors::actor_cost_quote(actor_id).expect("active User cost quote exists"); + assert_eq!(quote.actor_type, ActorType::User); + assert_eq!(quote.creation_fee, 10); + let trigger = quote + .prospective_trigger_fee + .expect("active Trigger quote exists"); + assert_eq!(trigger.trigger_family, TriggerFamily::Manual); + assert_eq!( + trigger.maximum_weight, + ::manual_trigger() + ); + assert_eq!(trigger.fee, manual_trigger_fee()); + assert_ne!(trigger.production_weight_identity, [0; 32]); + let pipeline = quote + .prospective_pipeline_fee + .expect("active Pipeline quote exists"); + assert_eq!( + pipeline.strategy, + PipelineMachineFeeStrategy::UpfrontBounded + ); + assert_eq!( + pipeline.total_fee, + pipeline.pipeline_machine_fee + pipeline.cleanup_fee + ); + assert_eq!( + pipeline.production_weight_identity, + crate::AdmissionCertificateAuthority::compose_production_weight_identity([41; 32], [42; 32]) + ); + let loaded = + Actors::load_current_step_from_storage(actor_id, 0).expect("current Step resources exist"); + assert_eq!( + quote.maximum_next_action_fee.maximum_effect_weight, + loaded.resources.effect + ); + assert_eq!( + quote.maximum_next_action_fee.maximum_effect_fee, + TestWeightToFee::weight_to_fee(&loaded.resources.effect) + ); + let hold = crate::ActorStateHolds::::get(actor_id) + .expect("User hold record exists") + .breakdown; + let expected_hold_total = [ + hold.identity, + hold.contract_head, + hold.contract_body, + hold.detector, + hold.funding, + hold.run, + ] + .into_iter() + .sum::(); + assert_eq!(quote.actor_state_hold.total, expected_hold_total); + assert!(!quote.actor_state_hold.exempt); + assert_eq!(quote.actor_state_hold.base_per_component, 1); + assert_eq!(quote.actor_state_hold.per_encoded_byte, 1); + + assert_ok!(Actors::create_user_actor( + RuntimeOrigin::signed(BOB), + Mutability::Mutable, + None, + )); + let dormant_id = Actors::next_actor_id() - 1; + let dormant = Actors::actor_cost_quote(dormant_id).expect("Dormant quote exists"); + assert!(dormant.prospective_trigger_fee.is_none()); + assert!(dormant.prospective_pipeline_fee.is_none()); + assert_eq!(dormant.maximum_next_action_fee.maximum_effect_fee, 0); + assert!(dormant.actor_state_hold.total > 0); + + let system_id = create_system_with(ALICE, manual_schedule(), None, inert_contract_steps()); + let system = Actors::actor_cost_quote(system_id).expect("System quote exists"); + assert_eq!(system.actor_type, ActorType::System); + assert_eq!(system.creation_fee, 0); + assert_eq!(system.prospective_trigger_fee.expect("Trigger").fee, 0); + assert_eq!( + system.prospective_pipeline_fee.expect("Pipeline").total_fee, + 0 + ); + assert_eq!(system.maximum_next_action_fee.maximum_effect_fee, 0); + assert!(system.actor_state_hold.exempt); + assert_eq!(system.actor_state_hold.total, 0); + + assert_eq!( + Actors::actor_cost_quote(u64::MAX), + Err(ActorCostQuoteError::ActorNotFound) + ); + }); +} + +#[test] +fn contract_header_extracts_only_hot_metadata_and_runtime_bindings() { + let contract = system_active_contract(manual_schedule(), None, transfer_contract_steps(BOB, 2)) + .expect("active Contract"); + let semantic_contract_id = [1u8; 32]; + let body_commitment = [2u8; 32]; + let admission_identity = [3u8; 32]; + + let header = contract + .try_header( + semantic_contract_id, + body_commitment, + admission_identity, + test_pipeline_machine_envelope(), + ) + .expect("bounded Contract produces a header"); + + assert_eq!(header.trigger, contract.trigger); + assert_eq!(header.cooldown_blocks, contract.cooldown_blocks); + assert_eq!(header.window, contract.window); + assert_eq!(header.funding, contract.funding); + assert_eq!(header.completion, contract.completion); + assert_eq!( + header.auto_close_at_cycle_nonce, + contract.auto_close_at_cycle_nonce + ); + assert_eq!( + header.step_count, + u32::try_from(contract.steps.len()).expect("bounded Step count fits u32") + ); + assert_eq!(header.semantic_contract_id, semantic_contract_id); + assert_eq!(header.body_commitment, body_commitment); + assert_eq!(header.admission_identity, admission_identity); + assert_eq!( + header.pipeline_machine_envelope, + test_pipeline_machine_envelope() + ); +} + +#[test] +fn semantic_contract_id_uses_fixed_domain_and_authored_field_order() { + let contract = system_active_contract(manual_schedule(), None, transfer_contract_steps(BOB, 2)) + .expect("active Contract"); + assert_eq!(crate::ACTOR_CONTRACT_HASH_DOMAIN, *b"DEOS_ACTOR_CONTRACT"); + let expected = ( + crate::ACTOR_CONTRACT_HASH_DOMAIN, + ( + &contract.trigger, + contract.cooldown_blocks, + &contract.window, + &contract.funding, + contract.completion, + contract.auto_close_at_cycle_nonce, + ), + &contract.steps, + ) + .using_encoded(frame::hashing::blake2_256); + assert_eq!(contract.semantic_contract_id(), expected); + + let mut changed = contract.clone(); + changed.cooldown_blocks = changed.cooldown_blocks.saturating_add(1); + assert_ne!( + contract.semantic_contract_id(), + changed.semantic_contract_id() + ); +} + +#[test] +fn body_commitment_uses_fixed_domain_and_exact_ordered_indexes() { + let steps = BoundedVec::try_from(vec![ + make_step(Task::Transfer { + to: BOB, + asset: TestAsset::Native, + amount: AmountResolution::Fixed(1), + }), + make_step(Task::Transfer { + to: CHARLIE, + asset: TestAsset::Native, + amount: AmountResolution::Fixed(2), + }), + ]) + .expect("two Steps fit"); + let contract = system_active_contract(manual_schedule(), None, steps).expect("active Contract"); + assert_eq!(crate::ACTOR_BODY_HASH_DOMAIN, *b"DEOS_ACTOR_BODY"); + let indexed_steps = vec![(0u32, &contract.steps[0]), (1u32, &contract.steps[1])]; + let expected = (crate::ACTOR_BODY_HASH_DOMAIN, indexed_steps.as_slice()) + .using_encoded(frame::hashing::blake2_256); + assert_eq!(contract.body_commitment(), Some(expected)); + + let mut reordered = contract.clone(); + reordered.steps = + BoundedVec::try_from(vec![contract.steps[1].clone(), contract.steps[0].clone()]) + .expect("reordered Steps fit"); + assert_ne!(contract.body_commitment(), reordered.body_commitment()); +} + +#[test] +fn current_step_service_state_does_not_load_unreached_tail_chunks() { + new_test_ext().execute_with(|| { + let steps = BoundedVec::try_from( + (0..8) + .map(|_| make_step(Task::StopCycle)) + .collect::>(), + ) + .expect("eight Steps fit"); + let actor_id = create_system_with(ALICE, manual_schedule(), None, steps); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + ActorContractTailChunks::::remove(actor_id, 0); + + let (state, admission, loaded_step) = + Actors::load_current_step_service_state(actor_id).expect("Step 0 needs no tail chunk"); + assert_eq!(state.hot.cycle_state, CycleState::Idle); + assert_eq!(state.contract.steps.len(), 8); + assert_eq!(loaded_step.cursor, 0); + assert_eq!( + loaded_step.resources, + ActorContractHeads::::get(actor_id) + .expect("canonical head exists") + .first_step_resources + .expect("nonempty Contract has inline Step resources"), + "the loaded Step keeps exact fragment authority" + ); + assert!(admission.has_valid_identity()); + assert!(matches!( + Actors::load_actor_state(actor_id), + crate::LoadedActorStateOf::Corrupt + )); + System::reset_events(); + Actors::on_idle(1, Weight::MAX); + assert!(ActorHot::::get(actor_id).is_some_and(|hot| hot.pending_signal)); + assert!(!has_actor_event(|event| matches!( + event, + Event::CycleStarted { actor_id: id, .. } if *id == actor_id + ))); + }); +} + +#[test] +fn run_head_and_immutable_payload_remain_coherent_across_progress() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let step = inert_contract_steps()[0].clone(); + let steps = + BoundedVec::try_from(vec![step.clone(), step.clone(), step]).expect("three Steps fit"); + let actor_id = create_system_with(ALICE, manual_schedule(), None, steps); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + Actors::on_idle(1, Weight::MAX); + let first_head = crate::ActorRunHeads::::get(actor_id).expect("run head exists"); + let first_payload = crate::ActorRunPayloads::::get(actor_id).expect("run payload exists"); + + frame_system::Pallet::::set_block_number(2); + Actors::on_initialize(2); + Actors::execute_cycle(Weight::MAX); + let second_head = crate::ActorRunHeads::::get(actor_id).expect("run head persists"); + let second_payload = + crate::ActorRunPayloads::::get(actor_id).expect("run payload persists"); + assert_eq!(second_head.cursor, 2); + assert_eq!( + second_head.payload_commitment, + first_head.payload_commitment + ); + assert_eq!(second_payload.encode(), first_payload.encode()); + + crate::ActorRunHeads::::mutate(actor_id, |maybe| { + maybe.as_mut().expect("run head exists").payload_commitment[0] ^= 1; + }); + assert!(ActorRunStateStore::::get(actor_id).is_none()); + assert!(matches!( + Actors::load_actor_state(actor_id), + crate::LoadedActorStateOf::Corrupt + )); + }); +} + +#[test] +fn current_fragment_resources_are_authoritative_without_certificate_duplication() { + new_test_ext().execute_with(|| { + let actor_id = create_system_with(ALICE, manual_schedule(), None, inert_contract_steps()); + ActorContractHeads::::mutate(actor_id, |maybe| { + let head = maybe.as_mut().expect("canonical head exists"); + let resources = head + .first_step_resources + .as_mut() + .expect("nonempty Contract has inline Step resources"); + resources.control = resources.control.saturating_add(Weight::from_parts(1, 0)); + }); + let (_, _, loaded) = Actors::load_current_step_service_state(actor_id) + .expect("current fragment remains authoritative"); + assert_eq!( + loaded.resources.control, + ActorContractHeads::::get(actor_id) + .expect("canonical head exists") + .first_step_resources + .expect("nonempty Contract has inline Step resources") + .control + ); + assert!(matches!( + Actors::load_actor_state(actor_id), + crate::LoadedActorStateOf::Active(_) + )); + }); +} + +#[test] +fn running_execution_and_post_placement_ignore_unreached_tail_chunks() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let inert = inert_contract_steps()[0].clone(); + let steps = BoundedVec::try_from((0..8).map(|_| inert.clone()).collect::>()) + .expect("eight Steps fit"); + let actor_id = create_system_with(ALICE, manual_schedule(), None, steps); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + Actors::on_idle(1, Weight::MAX); + assert_eq!( + Actors::actor_run_state(actor_id).map(|run| run.cursor), + Some(1) + ); + + ActorContractTailChunks::::remove(actor_id, 1); + assert!(matches!( + Actors::load_actor_state(actor_id), + crate::LoadedActorStateOf::Corrupt + )); + frame_system::Pallet::::set_block_number(2); + Actors::on_initialize(2); + assert!(ActorHot::::get(actor_id).is_some_and(|hot| hot.queue_ticket.is_some())); + let (_, queued) = Actors::paged_head_entry().expect("successor is queued"); + assert_eq!(queued.actor_id, actor_id); + assert_eq!(queued.eligible_at, 2); + assert!(Actors::load_current_step_service_state(actor_id).is_some()); + Actors::execute_cycle(Weight::MAX); + assert!(has_actor_event(|event| matches!( + event, + Event::StepSkipped { actor_id: id, step_index: 1, .. } if *id == actor_id + ))); + let run = Actors::actor_run_state(actor_id).expect("Running suffix remains live"); + assert_eq!(run.cursor, 2); + assert_eq!(run.last_committed_step_block, Some(2)); + assert!(ActorHot::::get(actor_id).is_some_and(|hot| { + hot.cycle_state == CycleState::Running + && (hot.queue_ticket.is_some() || hot.wakeup_pointer.is_some()) + })); + }); +} + +#[test] +fn current_step_plan_builds_only_from_coherent_opening_authority() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = create_system_with(ALICE, manual_schedule(), None, inert_contract_steps()); + let identity = ActorIdentities::::get(actor_id).expect("identity exists"); + let mut hot = ActorHot::::get(actor_id).expect("hot state exists"); + hot.queue_ticket = Some(9); + ActorHot::::insert(actor_id, hot.clone()); + let funding = ActorFunding::::get(actor_id).expect("funding exists"); + let admission = ActorAdmissionCertificates::::get(actor_id) + .expect("canonical admission certificate exists"); + let head = ActorContractHeads::::get(actor_id).expect("canonical head exists"); + let loaded_step = Actors::load_current_step_from_geometry(actor_id, &head, &admission, 0, None) + .expect("Step 0 loads"); + let ticket = crate::ActorStepTicket { + actor_id, + cycle_nonce: identity.cycle_nonce + 1, + cursor: 0, + ticket: 9, + eligible_at: 1, + contract_commitment: crate::ActorContractCommitment { + semantic_contract_id: admission.semantic_contract_id, + body_commitment: admission.body_commitment, + }, + }; + assert_eq!( + Actors::build_actor_step_ticket(actor_id, 9, 1, &identity, &hot, None, &admission,), + Some(ticket) + ); + let mut stale_hot = hot.clone(); + stale_hot.queue_ticket = Some(10); + assert!( + Actors::build_actor_step_ticket(actor_id, 9, 1, &identity, &stale_hot, None, &admission,) + .is_none() + ); + let user_fee = Actors::maximum_current_step_fee(ActorType::User, loaded_step.resources) + .expect("User current-Step fee is representable"); + assert_eq!( + user_fee.total_fee, + user_fee.control_fee + user_fee.effect_fee + ); + let maximum_fee = Actors::maximum_current_step_fee(ActorType::System, loaded_step.resources) + .expect("System current-Step fee is representable"); + assert_eq!(maximum_fee.total_fee, 0); + let storage_plan = Actors::load_current_step_plan_from_storage(ticket) + .expect("storage-backed Opening plan builds"); + assert_eq!(storage_plan.ticket, ticket); + assert_eq!(storage_plan.loaded_step, loaded_step); + assert_eq!(storage_plan.maximum_fee, maximum_fee); + let mut future_ticket = ticket; + future_ticket.eligible_at = 2; + assert!(Actors::load_current_step_plan_from_storage(future_ticket).is_none()); + let plan = Actors::build_current_step_plan( + actor_id, + identity.clone(), + hot.clone(), + None, + funding.clone(), + admission.clone(), + ticket, + loaded_step.clone(), + maximum_fee.clone(), + ) + .expect("coherent Opening plan builds"); + assert_eq!(plan.loaded_step, loaded_step); + assert_eq!(plan.maximum_fee, maximum_fee); + + let mut running_hot = plan.hot.clone(); + running_hot.cycle_state = CycleState::Running; + let running = RuntimeActorRunState { + contract_authority: run_contract_authority(actor_id), + cycle_nonce: plan.ticket.cycle_nonce, + cursor: plan.ticket.cursor, + opening_predicate_cursor: 0, + unsuccessful_attempts_at_cursor: 0, + last_attempt_block: 0, + last_committed_step_block: Some(0), + eligible_at: plan.ticket.eligible_at, + opening_snapshot: Default::default(), + opening_predicate_results: Default::default(), + funding_snapshot: Default::default(), + cumulative_outcomes: Default::default(), + last_step_outcome: None, + suspension: None, + }; + assert_eq!( + Actors::build_actor_step_ticket( + actor_id, + 9, + running.eligible_at, + &plan.identity, + &running_hot, + Some(&running), + &plan.admission, + ), + Some(plan.ticket) + ); + assert!( + Actors::build_actor_step_ticket( + actor_id, + 9, + running.eligible_at + 1, + &plan.identity, + &running_hot, + Some(&running), + &plan.admission, + ) + .is_none() + ); + assert!( + Actors::build_current_step_plan( + actor_id, + plan.identity.clone(), + running_hot.clone(), + Some(running.clone()), + plan.funding.clone(), + plan.admission.clone(), + plan.ticket, + plan.loaded_step.clone(), + plan.maximum_fee.clone(), + ) + .is_some() + ); + let mut stale_run = running.clone(); + stale_run.contract_authority.body_commitment[0] ^= 1; + assert!( + Actors::build_actor_step_ticket( + actor_id, + 9, + stale_run.eligible_at, + &plan.identity, + &running_hot, + Some(&stale_run), + &plan.admission, + ) + .is_none() + ); + assert!( + Actors::build_current_step_plan( + actor_id, + plan.identity.clone(), + running_hot.clone(), + Some(stale_run), + plan.funding.clone(), + plan.admission.clone(), + plan.ticket, + plan.loaded_step.clone(), + plan.maximum_fee.clone(), + ) + .is_none() + ); + let mut suspended_hot = running_hot.clone(); + suspended_hot.cycle_state = CycleState::Suspended; + let mut suspended = running.clone(); + suspended.last_step_outcome = Some(StepOutcome::FundingUnavailable); + suspended.suspension = Some(SuspensionReason::FundingUnavailable); + assert!( + Actors::build_current_step_plan( + actor_id, + plan.identity.clone(), + suspended_hot, + Some(suspended), + plan.funding.clone(), + plan.admission.clone(), + plan.ticket, + plan.loaded_step.clone(), + plan.maximum_fee.clone(), + ) + .is_some() + ); + let mut incoherent_suspension = running; + incoherent_suspension.suspension = Some(SuspensionReason::Temporary); + assert!( + Actors::build_current_step_plan( + actor_id, + plan.identity.clone(), + running_hot, + Some(incoherent_suspension), + plan.funding.clone(), + plan.admission.clone(), + plan.ticket, + plan.loaded_step.clone(), + plan.maximum_fee.clone(), + ) + .is_none() + ); + + let mut stale_ticket = ticket; + stale_ticket.cycle_nonce += 1; + assert!( + Actors::build_current_step_plan( + actor_id, + identity, + hot, + None, + funding, + admission, + stale_ticket, + plan.loaded_step, + plan.maximum_fee, + ) + .is_none() + ); + }); +} + +#[test] +fn step_ticket_binds_run_cursor_fifo_eligibility_and_contract_commitment() { + let ticket = crate::ActorStepTicket { + actor_id: 7, + cycle_nonce: 8, + cursor: 2, + ticket: 9, + eligible_at: 10u64, + contract_commitment: crate::ActorContractCommitment { + semantic_contract_id: [1u8; 32], + body_commitment: [2u8; 32], + }, + }; + let decoded = + crate::ActorStepTicket::decode(&mut ticket.encode().as_slice()).expect("Step ticket decodes"); + assert_eq!(decoded, ticket); + assert_eq!(decoded.ticket, 9); + assert_eq!(decoded.cursor, 2); + assert_eq!(decoded.eligible_at, 10); + assert!(decoded.matches( + 7, + 8, + 2, + 9, + &10, + &crate::ActorContractCommitment { + semantic_contract_id: [1u8; 32], + body_commitment: [2u8; 32], + }, + )); + assert!(!decoded.matches(7, 8, 3, 9, &10, &decoded.contract_commitment,)); +} + +#[test] +fn admission_identity_binds_every_runtime_owned_domain_field() { + let certificate: crate::ActorAdmissionCertificateOf = crate::ActorAdmissionCertificate::new( + [1u8; 32], + [2u8; 32], + 3, + [4u8; 32], + 5, + [6u8; 32], + Weight::from_parts(77, 88), + ); + assert!(certificate.has_valid_identity()); + let mut stale = certificate.clone(); + stale.body_geometry_version = 6; + assert!(!stale.has_valid_identity()); + let mut stale = certificate.clone(); + stale.production_weight_identity[0] ^= 1; + assert!(!stale.has_valid_identity()); + let mut stale = certificate; + stale.maximum_lifecycle_weight = Weight::from_parts(78, 88); + assert!(!stale.has_valid_identity()); +} + +#[test] +fn admission_certificate_encoding_is_independent_of_resource_ceiling() { + type SmallResources = BoundedVec>; + type LargeResources = BoundedVec>; + let small: crate::ActorAdmissionCertificate = + crate::ActorAdmissionCertificate::new( + [1u8; 32], + [2u8; 32], + 3, + [4u8; 32], + 5, + [6u8; 32], + Weight::from_parts(77, 88), + ); + let large: crate::ActorAdmissionCertificate = + crate::ActorAdmissionCertificate::new( + [1u8; 32], + [2u8; 32], + 3, + [4u8; 32], + 5, + [6u8; 32], + Weight::from_parts(77, 88), + ); + assert_eq!(small.encode(), large.encode()); + assert_eq!(small.admission_identity, large.admission_identity); +} + +#[test] +fn admission_certificate_builder_composes_compact_host_authority() { + let contract = system_active_contract( + manual_schedule(), + None, + BoundedVec::try_from(vec![make_step(Task::StopCycle), make_step(Task::StopCycle)]) + .expect("two Steps fit"), + ) + .expect("active Contract"); + let certificate = Actors::build_admission_certificate(&contract).expect("host authority exists"); + assert!(certificate.has_valid_identity()); + assert_eq!( + certificate.semantic_contract_id, + contract.semantic_contract_id() + ); + assert_eq!( + certificate.body_commitment, + contract.body_commitment().expect("body commitment") + ); + assert_eq!(certificate.runtime_actor_semantics_version, 1); + assert_eq!( + certificate.production_weight_identity, + crate::AdmissionCertificateAuthority::compose_production_weight_identity([41; 32], [42; 32]) + ); + assert_eq!(certificate.body_geometry_version, 1); + assert_eq!(certificate.configured_bounds_commitment, [6; 32]); + assert_eq!( + certificate.maximum_lifecycle_weight, + Weight::from_parts(77, 88) + ); + assert_eq!( + certificate.marker, + std::marker::PhantomData::> + ); +} + +#[test] +fn step_resource_derivation_binds_authored_opening_and_predicate_geometry() { + let steps = BoundedVec::try_from(vec![StepOf:: { + precondition: timed_all_conditions( + ObservationTiming::Opening, + vec![Predicate::BalanceAbove { + asset: TestAsset::Native, + threshold: 1, + }], + ), + task: Task::Transfer { + to: BOB, + asset: TestAsset::Native, + amount: AmountResolution::PercentageAtOpening(Perbill::from_percent(50)), + }, + on_error: StepErrorPolicy::AbortCycle, + }]) + .expect("one Step fits"); + let contract = system_active_contract(manual_schedule(), None, steps).expect("active Contract"); + let resources = Actors::derive_step_resource_envelopes(&contract) + .expect("configured resource providers admit Contract"); + assert_eq!(resources.len(), 1); + assert_eq!( + resources[0].control, + Weight::from_parts(100_000_025, 100_023), + "control base + 2 evaluation units + 1 Opening surface + 1 Opening result + 10 funding entries", + ); +} + +#[test] +fn opening_control_envelope_charges_only_authored_tail_chunks() { + let one_step = system_active_contract( + manual_schedule(), + None, + BoundedVec::try_from(vec![make_step(Task::StopCycle)]).expect("one Step fits"), + ) + .expect("one-Step Contract exists"); + let eight_steps = system_active_contract( + manual_schedule(), + None, + BoundedVec::try_from( + (0..8) + .map(|_| make_step(Task::StopCycle)) + .collect::>(), + ) + .expect("eight Steps fit"), + ) + .expect("eight-Step Contract exists"); + + let one = + Actors::derive_step_resource_envelopes(&one_step).expect("one-Step resources exist")[0].control; + let eight = Actors::derive_step_resource_envelopes(&eight_steps) + .expect("eight-Step resources exist")[0] + .control; + + assert_eq!(eight.ref_time().saturating_sub(one.ref_time()), 2); + assert_eq!(eight.proof_size(), one.proof_size()); +} + +#[test] +fn unconfigured_resource_weight_ports_fail_closed() { + assert_eq!( + <() as crate::AdmissionCertificateAuthorityProvider>::current(), + None + ); + let step = make_step(Task::StopCycle); + assert_eq!( + <() as crate::StepControlWeightProvider>::production_weight_identity(), + None + ); + assert_eq!( + <() as crate::StepControlWeightProvider<_>>::maximum_control_weight( + crate::StepControlWeightContext { + cursor: 0, + steps_in_fragment: 1, + opening_tail_chunks: 0, + predicate_evaluation_units: 0, + opening_snapshot_entries: 0, + opening_predicate_results: 0, + funding_snapshot_entries: 0, + }, + &step, + ), + None + ); + assert_eq!( + <() as crate::StepControlWeightProvider>::actual_control_weight( + crate::StepControlWeightContext { + cursor: 0, + steps_in_fragment: 1, + opening_tail_chunks: 0, + predicate_evaluation_units: 0, + opening_snapshot_entries: 0, + opening_predicate_results: 0, + funding_snapshot_entries: 0, + }, + &step, + Weight::from_parts(1, 1), + crate::StepControlExecution { + phase: crate::StepControlPhase::Opening, + outcome: crate::StepControlOutcome::Completed, + placement: crate::StepControlPlacement::None, + }, + ), + None + ); + let task = + Task::::MaxSplitTransferLegs>::StopCycle; + assert_eq!( + <() as crate::TaskEffectWeightProvider>::production_weight_identity(), + None + ); + assert_eq!( + <() as crate::TaskEffectWeightProvider<_>>::maximum_effect_weight(&task), + None + ); + assert_eq!( + <() as crate::TaskEffectWeightProvider<_>>::actual_effect_weight( + &task, + crate::TaskEffectExecution::NotInvoked, + ), + None + ); +} + +#[test] +fn step_control_weight_context_matches_c6_head_and_tail_geometry() { + assert_eq!( + Actors::step_control_weight_context(1, 0, 7, 8, 9, 10), + Some(crate::StepControlWeightContext { + cursor: 0, + steps_in_fragment: 1, + opening_tail_chunks: 0, + predicate_evaluation_units: 7, + opening_snapshot_entries: 8, + opening_predicate_results: 9, + funding_snapshot_entries: 10, + }) + ); + assert_eq!( + Actors::step_control_weight_context(8, 0, 7, 8, 9, 10) + .expect("eight-Step head context exists") + .opening_tail_chunks, + 2, + ); + for (cursor, steps_in_fragment) in [(1, 4), (4, 4), (5, 3), (7, 3)] { + assert_eq!( + Actors::step_control_weight_context(8, cursor, 7, 8, 9, 10), + Some(crate::StepControlWeightContext { + cursor, + steps_in_fragment, + opening_tail_chunks: 0, + predicate_evaluation_units: 7, + opening_snapshot_entries: 0, + opening_predicate_results: 0, + funding_snapshot_entries: 0, + }) + ); + } + assert_eq!(Actors::step_control_weight_context(0, 0, 0, 0, 0, 0), None); + assert_eq!(Actors::step_control_weight_context(8, 8, 0, 0, 0, 0), None); + assert_eq!(Actors::step_control_weight_context(9, 0, 0, 0, 0, 0), None); +} + +#[test] +fn current_step_resource_admission_requires_both_weight_components() { + let resources = crate::ActorStepResourceEnvelope { + control: Weight::from_parts(11, 22), + effect: Weight::from_parts(33, 44), + }; + let control = WeightMeter::with_limit(resources.control); + let effect = WeightMeter::with_limit(resources.effect); + assert!(Actors::current_step_resources_fit( + &control, &effect, resources + )); + let short_control = WeightMeter::with_limit(Weight::from_parts(10, 22)); + assert!(!Actors::current_step_resources_fit( + &short_control, + &effect, + resources, + )); + let short_effect_proof = WeightMeter::with_limit(Weight::from_parts(33, 43)); + assert!(!Actors::current_step_resources_fit( + &control, + &short_effect_proof, + resources, + )); +} + +#[test] +fn step_resource_envelope_keeps_control_and_effect_weight_separate() { + let envelope = crate::ActorStepResourceEnvelope { + control: Weight::from_parts(11, 22), + effect: Weight::from_parts(33, 44), + }; + assert_eq!(envelope.control, Weight::from_parts(11, 22)); + assert_eq!(envelope.effect, Weight::from_parts(33, 44)); + assert_eq!( + crate::ActorStepResourceEnvelope::decode(&mut envelope.encode().as_slice()), + Ok(envelope) + ); +} + +#[test] +fn user_pipeline_machine_envelope_prices_control_retries_and_cleanup_only() { + new_test_ext().execute_with(|| { + let mut steps = BoundedVec::try_from(vec![ + make_step(Task::Transfer { + to: BOB, + asset: TestAsset::Native, + amount: AmountResolution::Fixed(1), + }), + make_step(Task::Transfer { + to: CHARLIE, + asset: TestAsset::Native, + amount: AmountResolution::Fixed(1), + }), + ]) + .expect("two Steps fit"); + steps[1].on_error = StepErrorPolicy::RetryLater { max_attempts: 3 }; + let contract = + system_active_contract(manual_schedule(), None, steps.clone()).expect("bounded Contract"); + let resources = + Actors::derive_step_resource_envelopes(&contract).expect("Step resources derive"); + let first_fee = Actors::maximum_current_step_fee(ActorType::User, resources[0]) + .expect("first Action fee fits"); + let second_fee = Actors::maximum_current_step_fee(ActorType::User, resources[1]) + .expect("second Action fee fits"); + let envelope = Actors::derive_pipeline_machine_envelope(ActorType::User, &steps, &resources) + .expect("Pipeline Machine envelope fits"); + + assert_eq!( + envelope.pipeline_machine_fee_upper, + TestWeightToFee::weight_to_fee(&resources[0].control) + + TestWeightToFee::weight_to_fee(&resources[1].control) * 3 + ); + assert_eq!(first_fee.control_fee, 0); + assert_eq!(second_fee.control_fee, 0); + assert!(first_fee.effect_fee > 0); + assert!(second_fee.effect_fee > 0); + assert_eq!( + envelope.cleanup_fee_upper, + TestWeightToFee::weight_to_fee(&::close_actor()) + ); + assert!( + envelope + .pipeline_machine_fee_upper + .checked_add(envelope.cleanup_fee_upper) + .is_some() + ); + }); +} + +#[test] +fn zero_step_pipeline_machine_envelope_prices_generated_control_and_cleanup() { + new_test_ext().execute_with(|| { + let steps = crate::ContractSteps::::default(); + let contract = + system_active_contract(manual_schedule(), None, steps.clone()).expect("zero-Step Contract"); + let resources = + Actors::derive_step_resource_envelopes(&contract).expect("empty resources derive"); + let user = Actors::derive_pipeline_machine_envelope(ActorType::User, &steps, &resources) + .expect("zero-Step User Pipeline Machine envelope fits"); + assert_eq!( + user.pipeline_machine_fee_upper, + TestWeightToFee::weight_to_fee( + &::scheduler_inner_zero_step_complete(), + ) + ); + assert_eq!( + user.cleanup_fee_upper, + TestWeightToFee::weight_to_fee(&::close_actor()) + ); + assert!(user.pipeline_machine_fee_upper > 0); + let system = Actors::derive_pipeline_machine_envelope(ActorType::System, &steps, &resources) + .expect("zero-Step System envelope fits"); + assert_eq!(system.pipeline_machine_fee_upper, 0); + assert_eq!(system.cleanup_fee_upper, 0); + }); +} + +#[test] +fn pipeline_machine_envelope_absorbs_stop_cycle_control_effect() { + new_test_ext().execute_with(|| { + let steps = BoundedVec::try_from(vec![make_step(Task::StopCycle)]).expect("one Step fits"); + let contract = + system_active_contract(manual_schedule(), None, steps.clone()).expect("bounded Contract"); + let resources = + Actors::derive_step_resource_envelopes(&contract).expect("Step resources derive"); + let action_fee = Actors::maximum_current_action_fee(ActorType::User, &steps[0], resources[0]) + .expect("StopCycle Action fee fits"); + let envelope = Actors::derive_pipeline_machine_envelope(ActorType::User, &steps, &resources) + .expect("Pipeline Machine envelope fits"); + + assert_eq!( + envelope.pipeline_machine_fee_upper, + TestWeightToFee::weight_to_fee(&resources[0].control.saturating_add(resources[0].effect)) + ); + assert_eq!(action_fee.total_fee, 0); + }); +} + +#[cfg(feature = "try-runtime")] +#[test] +fn try_state_rejects_pipeline_machine_envelope_drift() { + new_test_ext().execute_with(|| { + let actor_id = create_system_with( + ALICE, + manual_schedule(), + None, + transfer_contract_steps(BOB, 1), + ); + assert_ok!(crate::Pallet::::do_try_state()); + ActorContractHeads::::mutate(actor_id, |maybe| { + maybe + .as_mut() + .expect("C6 head exists") + .header + .pipeline_machine_envelope + .pipeline_machine_fee_upper = 1; + }); + + assert!(crate::Pallet::::do_try_state().is_err()); + }); +} + +#[cfg(feature = "try-runtime")] +#[test] +fn try_state_rejects_actor_state_hold_geometry_drift() { + new_test_ext().execute_with(|| { + let actor_id = create_user_with( + ALICE, + Mutability::Mutable, + manual_schedule(), + None, + transfer_contract_steps(BOB, 1), + ); + assert_ok!(crate::Pallet::::do_try_state()); + crate::ActorStateHolds::::mutate(actor_id, |maybe| { + maybe + .as_mut() + .expect("User Actor hold exists") + .breakdown + .contract_head = 1; + }); + assert!(crate::Pallet::::do_try_state().is_err()); + }); +} + +#[test] +fn step_chunk_binds_authority_and_contiguous_first_index() { + let chunk = crate::ActorStepChunk { + authority: crate::ActorBodyAuthority { + actor_id: 7u64, + semantic_contract_id: [1u8; 32], + body_commitment: [2u8; 32], + admission_identity: [3u8; 32], + }, + first_step_index: 1, + steps: vec![11u8, 12u8, 13u8, 14u8], + step_resources: vec![ + crate::ActorStepResourceEnvelope { + control: Weight::zero(), + effect: Weight::zero(), + }; + 4 + ], + }; + assert!(chunk.matches(&7, &[1u8; 32], &[2u8; 32], &[3u8; 32], 1)); + assert!(!chunk.matches(&8, &[1u8; 32], &[2u8; 32], &[3u8; 32], 1)); + assert!(!chunk.matches(&7, &[1u8; 32], &[2u8; 32], &[3u8; 32], 2)); + assert_eq!(chunk.steps.len(), 4); +} + +#[test] +fn contract_geometry_decomposition_is_gap_free_and_head_only_for_one_step() { + new_test_ext().execute_with(|| { + for step_count in [1u32, 8] { + let steps = BoundedVec::try_from( + (0..step_count) + .map(|_| make_step(Task::StopCycle)) + .collect::>(), + ) + .expect("bounded Steps fit"); + let contract = + system_active_contract(manual_schedule(), None, steps).expect("active Contract"); + let certificate = crate::ActorAdmissionCertificate::new( + contract.semantic_contract_id(), + contract.body_commitment().expect("body commitment"), + 1, + [4u8; 32], + 1, + [6u8; 32], + Weight::from_parts(77, 88), + ); + let (head, chunks) = + Actors::decompose_admitted_contract_geometry(7, ActorType::System, &contract, &certificate) + .expect("admitted Contract geometry decomposes"); + assert_eq!( + head.header.admission_identity, + certificate.admission_identity + ); + assert_eq!( + head.header.pipeline_machine_envelope, + crate::PipelineMachineEnvelope { + pipeline_machine_fee_upper: 0, + cleanup_fee_upper: 0, + } + ); + let loaded_first = Actors::load_current_step_from_geometry(7, &head, &certificate, 0, None) + .expect("Step 0 loads from the head only"); + assert_eq!(loaded_first.step, contract.steps[0]); + let ticket = crate::ActorStepTicket { + actor_id: 7, + cycle_nonce: 1, + cursor: 0, + ticket: 9, + eligible_at: 1u64, + contract_commitment: crate::ActorContractCommitment { + semantic_contract_id: certificate.semantic_contract_id, + body_commitment: certificate.body_commitment, + }, + }; + assert!(Actors::validate_loaded_step_authority( + 7, + 9, + &certificate, + &ticket, + &loaded_first, + )); + let mut stale_ticket = ticket; + stale_ticket.cursor = 1; + assert!(!Actors::validate_loaded_step_authority( + 7, + 9, + &certificate, + &stale_ticket, + &loaded_first, + )); + if step_count > 1 { + let cursor = step_count - 1; + let chunk_index = cursor.saturating_sub(1) / 4; + let chunk = chunks + .iter(/* deos-bypass: bounded-iter */) + .find(|(index, _)| *index == chunk_index) + .expect("current tail chunk exists"); + let loaded = Actors::load_current_step_from_geometry( + 7, + &head, + &certificate, + cursor, + Some((chunk.0, &chunk.1)), + ) + .expect("tail Step loads from exactly one chunk"); + assert_eq!(loaded.step, contract.steps[cursor as usize]); + assert!( + Actors::load_current_step_from_geometry( + 8, + &head, + &certificate, + cursor, + Some((chunk.0, &chunk.1)), + ) + .is_none() + ); + } + let mut stale_certificate = certificate.clone(); + stale_certificate.body_geometry_version += 1; + assert!( + Actors::decompose_admitted_contract_geometry( + 7, + ActorType::User, + &contract, + &stale_certificate, + ) + .is_none() + ); + assert_eq!(head.first_step, Some(contract.steps[0].clone())); + assert_eq!( + Actors::reconstruct_contract_geometry(7, head.clone(), &chunks), + Some(contract.clone()) + ); + if !chunks.is_empty() { + assert_eq!( + Actors::reconstruct_contract_geometry(8, head.clone(), &chunks), + None + ); + let mut stale_range = chunks.clone(); + stale_range[0].1.first_step_index = 2; + assert_eq!( + Actors::reconstruct_contract_geometry(7, head.clone(), &stale_range), + None + ); + } + assert_eq!( + chunks.len(), + usize::try_from(step_count.saturating_sub(1).div_ceil(4)).unwrap() + ); + let reconstructed = head + .first_step + .into_iter() + .chain( + chunks + .iter() + .flat_map(|(_, chunk)| chunk.steps.iter().cloned()), + ) + .collect::>(); + assert_eq!(reconstructed.as_slice(), contract.steps.as_slice()); + for (chunk_index, chunk) in chunks { + assert_eq!(chunk.first_step_index, 1 + chunk_index * 4); + assert!(!chunk.steps.is_empty()); + assert!(chunk.steps.len() <= 4); + assert!(chunk.matches( + &7, + &head.header.semantic_contract_id, + &head.header.body_commitment, + &head.header.admission_identity, + chunk.first_step_index, + )); + } + } + }); +} + +#[test] +fn admitted_contract_storage_loads_one_current_fragment_and_replaces_exact_tail() { + new_test_ext().execute_with(|| { + let actor_id = 77; + let contract = system_active_contract( + manual_schedule(), + None, + BoundedVec::try_from( + (0..8) + .map(|_| make_step(Task::StopCycle)) + .collect::>(), + ) + .expect("eight Steps fit"), + ) + .expect("active Contract"); + let certificate = geometry_certificate(&contract); + assert!(Actors::insert_admitted_contract_geometry( + actor_id, + &contract, + &certificate, + )); + assert!(!Actors::insert_admitted_contract_geometry( + actor_id, + &contract, + &certificate, + )); + assert_eq!( + Actors::load_admitted_contract_geometry(actor_id), + Some((contract.clone(), certificate.clone())) + ); + assert_eq!( + Actors::load_current_step_from_storage(actor_id, 0) + .expect("head Step loads") + .step, + contract.steps[0] + ); + assert_eq!( + Actors::load_current_step_from_storage(actor_id, 7) + .expect("one tail chunk loads current Step") + .step, + contract.steps[7] + ); + assert!(ActorContractTailChunks::::contains_key(actor_id, 0)); + assert!(ActorContractTailChunks::::contains_key(actor_id, 1)); + + let replacement = system_active_contract( + manual_schedule(), + None, + BoundedVec::try_from(vec![make_step(Task::StopCycle)]).expect("one Step fits"), + ) + .expect("replacement Contract"); + let replacement_certificate = geometry_certificate(&replacement); + assert!(Actors::replace_admitted_contract_geometry( + actor_id, + &replacement, + &replacement_certificate, + )); + assert!(!ActorContractTailChunks::::contains_key(actor_id, 0)); + assert!(!ActorContractTailChunks::::contains_key(actor_id, 1)); + assert!(Actors::load_current_step_from_storage(actor_id, 1).is_none()); + assert_eq!( + Actors::remove_admitted_contract_geometry(actor_id), + Some(replacement) + ); + assert!(!ActorContractHeads::::contains_key(actor_id)); + assert!(!ActorAdmissionCertificates::::contains_key(actor_id)); + assert!(Actors::remove_admitted_contract_geometry(actor_id).is_none()); + }); +} + +#[test] +fn admitted_contract_storage_fails_closed_before_mutation_on_stale_authority() { + new_test_ext().execute_with(|| { + let actor_id = 78; + let contract = system_active_contract( + manual_schedule(), + None, + BoundedVec::try_from(vec![make_step(Task::StopCycle)]).expect("one Step fits"), + ) + .expect("active Contract"); + let mut stale_certificate = geometry_certificate(&contract); + stale_certificate.body_geometry_version += 1; + assert!(!Actors::insert_admitted_contract_geometry( + actor_id, + &contract, + &stale_certificate, + )); + assert!(!ActorContractHeads::::contains_key(actor_id)); + assert!(!ActorAdmissionCertificates::::contains_key(actor_id)); + }); +} + +#[test] +fn inline_first_head_and_lazy_tail_have_unique_ordered_ownership() { + let steps = BoundedVec::try_from(vec![ + make_step(Task::Transfer { + to: BOB, + asset: TestAsset::Native, + amount: AmountResolution::Fixed(1), + }), + make_step(Task::Transfer { + to: CHARLIE, + asset: TestAsset::Native, + amount: AmountResolution::Fixed(2), + }), + ]) + .expect("two Steps fit"); + let contract = system_active_contract(manual_schedule(), None, steps).expect("active Contract"); + let semantic_contract_id = contract.semantic_contract_id(); + let body_commitment = contract.body_commitment().expect("bounded body commitment"); + let admission_identity = [3u8; 32]; + let header = contract + .try_header( + semantic_contract_id, + body_commitment, + admission_identity, + test_pipeline_machine_envelope(), + ) + .expect("bounded header"); + let resource = crate::ActorStepResourceEnvelope { + control: Weight::from_parts(11, 22), + effect: Weight::from_parts(33, 44), + }; + let head = crate::ActorContractHead { + header, + first_step: Some(contract.steps[0].clone()), + first_step_resources: Some(resource), + }; + let tail = crate::ActorStepChunk { + authority: crate::ActorBodyAuthority { + actor_id: 7u64, + semantic_contract_id, + body_commitment, + admission_identity, + }, + first_step_index: 1, + steps: BoundedVec::<_, ConstU32<4>>::try_from(vec![contract.steps[1].clone()]) + .expect("one tail Step fits"), + step_resources: BoundedVec::<_, ConstU32<4>>::try_from(vec![resource]) + .expect("one tail resource fits"), + }; + + assert_eq!(head.header.step_count, 2); + assert_eq!(head.first_step, Some(contract.steps[0].clone())); + assert_eq!(tail.steps[0], contract.steps[1]); + assert!(tail.matches( + &7, + &semantic_contract_id, + &body_commitment, + &admission_identity, + 1, + )); + assert!(!tail.matches( + &8, + &semantic_contract_id, + &body_commitment, + &admission_identity, + 1, + )); + assert!(!tail.matches( + &7, + &semantic_contract_id, + &body_commitment, + &admission_identity, + 0, + )); + let reconstructed = BoundedVec::<_, ::MaxContractSteps>::try_from(vec![ + head.first_step.expect("nonempty Contract has inline Step"), + tail.steps[0].clone(), + ]) + .expect("reconstructed Steps fit"); + assert_eq!(reconstructed, contract.steps); +} + +#[test] +#[ignore = "retained-state Contract geometry profile"] +fn profile_contract_geometry_state_footprint() { + for step_count in [1u32, 8] { + let steps = BoundedVec::try_from( + (0..step_count) + .map(|_| make_step(Task::StopCycle)) + .collect::>(), + ) + .expect("profile Steps fit"); + let contract = system_active_contract(manual_schedule(), None, steps).expect("active Contract"); + let semantic_contract_id = contract.semantic_contract_id(); + let body_commitment = contract.body_commitment().expect("bounded body commitment"); + let admission_identity = [3u8; 32]; + let header = contract + .try_header( + semantic_contract_id, + body_commitment, + admission_identity, + test_pipeline_machine_envelope(), + ) + .expect("bounded header"); + let authority = crate::ActorBodyAuthority { + actor_id: 7u64, + semantic_contract_id, + body_commitment, + admission_identity, + }; + let resource = crate::ActorStepResourceEnvelope { + control: Weight::zero(), + effect: Weight::zero(), + }; + let head = crate::ActorContractHead { + header, + first_step: Some(contract.steps[0].clone()), + first_step_resources: Some(resource), + }; + let chunks = contract.steps.as_slice()[1..] + .chunks(4) + .enumerate() + .map(|(chunk_index, steps)| crate::ActorStepChunk { + authority: authority.clone(), + first_step_index: 1u32.saturating_add( + u32::try_from(chunk_index) + .expect("bounded chunk index fits u32") + .saturating_mul(4), + ), + steps: steps.to_vec(), + step_resources: vec![resource; steps.len()], + }) + .collect::>(); + let b0_bytes = contract.encode().len(); + let head_bytes = head.encode().len(); + let chunk_bytes = chunks + .iter() + .map(|chunk| chunk.encode().len()) + .sum::(); + let max_chunk_bytes = chunks + .iter() + .map(|chunk| chunk.encode().len()) + .max() + .unwrap_or_default(); + println!( + "CONTRACT_GEOMETRY_FOOTPRINT step_count={step_count} monolithic_bytes={b0_bytes} chunked_total_bytes={} head_bytes={head_bytes} max_chunk_bytes={max_chunk_bytes}", + head_bytes.saturating_add(chunk_bytes), + ); + } +} + #[test] fn public_reachability_inventory_is_closed_and_canonical() { assert_variant_names::(&[ @@ -79,6 +1554,7 @@ fn public_reachability_inventory_is_closed_and_canonical() { "AddressEvent", "ObservationChange", "ObservationCrossing", + "AtTime", "Cadenced", ]); assert_variant_names::::MaxWhitelistSize>>( @@ -87,6 +1563,7 @@ fn public_reachability_inventory_is_closed_and_canonical() { "AddressEvent", "ObservationChange", "ObservationCrossing", + "AtTime", "Cadenced", ], ); @@ -95,8 +1572,14 @@ fn public_reachability_inventory_is_closed_and_canonical() { assert_variant_names::(&["Mutable", "Immutable"]); assert_variant_names::(&["Persistent", "CloseAfterProductiveCycle"]); assert_variant_names::(&["Active", "Paused"]); - assert_variant_names::(&["Idle", "Suspended"]); - assert_variant_names::(&["Completed", "Failed", "Suspended", "Closed"]); + assert_variant_names::(&["Idle", "Running", "Suspended"]); + assert_variant_names::(&[ + "Completed", + "Continued", + "Failed", + "Suspended", + "Closed", + ]); assert_variant_names::(&[ "Executed", "Stopped", @@ -111,11 +1594,11 @@ fn public_reachability_inventory_is_closed_and_canonical() { ]); assert_variant_names::(&[ "OwnerInitiated", - "BalanceExhausted", + "CycleAdmissionInsufficient", + "TriggerAdmissionInsufficient", "ConsecutiveFailures", "WindowExpired", "CycleNonceExhausted", - "FeeBudgetExhausted", "AutoCloseNonceReached", "RetryAttemptsExhausted", "ProductiveCycleCompleted", @@ -151,7 +1634,7 @@ fn public_reachability_inventory_is_closed_and_canonical() { "Stale", ]); assert_variant_names::>(&["NotRegistered", "Dormant", "Active"]); - assert_variant_names::(&["FreshCurrentPlan", "CurrentContinuation"]); + assert_variant_names::(&["FreshCurrentPlan", "CurrentRun"]); assert_variant_names::(&[ "TransactionDepthExceeded", "Classification", @@ -185,15 +1668,23 @@ fn actor_storage_schema_is_explicit() { [ "NextActorId", "ActorHot", - "ActorContract", + "ActorContractHead", + "ActorActivationAuthority", + "ActorAdmissionCertificate", + "ActorContractTailChunk", "ActorFunding", - "ContinuationState", + "ActorRunHead", + "ActorRunPayload", "ActorIdentities", "ActorIdentityCount", + "ActorStateHolds", "ActiveActorCount", "SystemSovereigns", "SystemSovereignCount", "NextQueueTicket", + "PrepassExecutionCutoff", + "CurrentBlockResourceState", + "FinalizedBlockResourceTelemetry", "QueueHead", "QueueTail", "QueueOccupancy", @@ -207,6 +1698,7 @@ fn actor_storage_schema_is_explicit() { "OwnerSlotBitmaps", "SovereignIndex", "ActiveActorLimit", + "IndexedTriggerDetectionDisabled", "ActorObservationFeeds", "ObservationSubscriptionSlot", "ObservationSubscriptionSlotOwner", @@ -264,15 +1756,23 @@ fn actor_storage_schema_is_explicit() { [ ("NextActorId", false, false), ("ActorHot", true, true), - ("ActorContract", true, true), + ("ActorContractHead", true, true), + ("ActorActivationAuthority", true, true), + ("ActorAdmissionCertificate", true, true), + ("ActorContractTailChunk", true, true), ("ActorFunding", true, true), - ("ContinuationState", true, true), + ("ActorRunHead", true, true), + ("ActorRunPayload", true, true), ("ActorIdentities", true, true), ("ActorIdentityCount", false, false), + ("ActorStateHolds", true, true), ("ActiveActorCount", false, false), ("SystemSovereigns", true, true), ("SystemSovereignCount", false, false), ("NextQueueTicket", false, false), + ("PrepassExecutionCutoff", true, false), + ("CurrentBlockResourceState", true, false), + ("FinalizedBlockResourceTelemetry", true, false), ("QueueHead", false, false), ("QueueTail", false, false), ("QueueOccupancy", false, false), @@ -286,6 +1786,7 @@ fn actor_storage_schema_is_explicit() { ("OwnerSlotBitmaps", false, true), ("SovereignIndex", true, true), ("ActiveActorLimit", false, false), + ("IndexedTriggerDetectionDisabled", true, true), ("ActorObservationFeeds", true, true), ("ObservationSubscriptionSlot", true, true), ("ObservationSubscriptionSlotOwner", true, true), @@ -317,11 +1818,46 @@ fn actor_storage_schema_is_explicit() { ] ); - let entries = &metadata.entries; - assert_plain_storage_type::(&entries[0]); - assert_map_storage_types::>(&entries[1]); - assert_map_storage_types::>(&entries[2]); - assert_map_storage_types::>(&entries[3]); + let entry = |name: &str| { + metadata + .entries + .iter() + .find(|entry| entry.name == name) + .expect("declared storage entry exists") + }; + assert_map_storage_types::>(entry("ActorContractHead")); + // The candidate projection's concrete generic identity is covered by the storage declaration, + // lifecycle round-trip tests, and generated metadata; its name/shape remain guarded above. + assert_map_storage_types::>(entry( + "ActorAdmissionCertificate", + )); + assert_map_storage_types::<(u64, u32), crate::ActorStepChunkOf>(entry( + "ActorContractTailChunk", + )); + assert_map_storage_types::>(entry("ActorStateHolds")); + assert_map_storage_types::(entry("IndexedTriggerDetectionDisabled")); + let entries = metadata + .entries + .iter() + .filter(|entry| { + !matches!( + entry.name, + "ActorActivationAuthority" + | "ActorAdmissionCertificate" + | "ActorContractTailChunk" + | "ActorRunPayload" + | "ActorStateHolds" + | "IndexedTriggerDetectionDisabled" + | "PrepassExecutionCutoff" + | "CurrentBlockResourceState" + | "FinalizedBlockResourceTelemetry" + ) + }) + .collect::>(); + assert_plain_storage_type::(entries[0]); + assert_map_storage_types::>(entries[1]); + assert_map_storage_types::>(entries[2]); + assert_map_storage_types::>(entries[3]); let mut registry = scale_info::Registry::new(); let contract_type = @@ -349,7 +1885,39 @@ fn actor_storage_schema_is_explicit() { "auto_close_at_cycle_nonce" ] ); - assert_map_storage_types::(&entries[4]); + assert_map_storage_types::>(&entries[4]); + assert_map_storage_types::>(entry("ActorRunPayload")); + let run_type = registry.register_type(&scale_info::meta_type::()); + let (_, run_state) = registry + .types() + .find(|(symbol, _)| symbol.id == run_type.id) + .expect("Actor run type is registered"); + let scale_info::TypeDef::Composite(run_fields) = &run_state.type_def else { + panic!("Actor run metadata must be composite"); + }; + assert_eq!( + run_fields + .fields + .iter() + .map(|field| field.name.as_deref().expect("named Actor run field")) + .collect::>(), + [ + "contract_authority", + "cycle_nonce", + "cursor", + "opening_predicate_cursor", + "unsuccessful_attempts_at_cursor", + "last_attempt_block", + "last_committed_step_block", + "eligible_at", + "opening_snapshot", + "opening_predicate_results", + "funding_snapshot", + "cumulative_outcomes", + "last_step_outcome", + "suspension" + ] + ); assert_map_storage_types::>(&entries[5]); assert_plain_storage_type::(&entries[6]); assert_plain_storage_type::(&entries[7]); @@ -648,6 +2216,21 @@ fn temporal_membership_try_state_rejects_terminal_at_drift() { }); } +#[test] +fn temporal_membership_try_state_rejects_unconsumed_at_time_without_pointer() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = create_system_with(ALICE, at_time_schedule(10), None, inert_contract_steps()); + #[cfg(feature = "try-runtime")] + assert_ok!(crate::Pallet::::do_try_state()); + Actors::trigger_wakeup_substrate_invalidate_inner(actor_id) + .expect("AtTime pointer is coherent") + .expect("AtTime pointer exists"); + #[cfg(feature = "try-runtime")] + assert!(crate::Pallet::::do_try_state().is_err()); + }); +} + #[test] fn temporal_membership_try_state_rejects_page_slot_pointing_at_different_actor() { new_test_ext().execute_with(|| { @@ -656,8 +2239,8 @@ fn temporal_membership_try_state_rejects_page_slot_pointing_at_different_actor() let other = create_system_with(BOB, manual_schedule(), None, inert_contract_steps()); assert!(Actors::wakeup_substrate_schedule(actor_id, 10)); assert!(Actors::wakeup_substrate_schedule(other, 10)); - // A physical slot whose entry addresses an actor that owns a different pointer is - // corruption: `wakeup_pointer` is the sole ordinary temporal-membership authority. + // A physical slot whose entry addresses an actor that owns a different pointer in the + // same clock domain is corruption. WakeupPages::::mutate((WakeupKey::Block(10), 0), |maybe| { let page = maybe.as_mut().expect("wakeup page"); page.entries[0] = Some(crate::WakeupEntry { actor_id: other }); @@ -665,7 +2248,10 @@ fn temporal_membership_try_state_rejects_page_slot_pointing_at_different_actor() #[cfg(feature = "try-runtime")] assert_eq!( crate::Pallet::::do_try_state().map_err(|error| format!("{error:?}")), - Err("Other(\"WakeupPage slot addresses an actor with a different wakeup pointer\")".into()) + Err( + "Other(\"WakeupPage slot addresses an actor with a different clock-domain pointer\")" + .into() + ) ); }); } @@ -699,7 +2285,7 @@ fn missing_frozen_snapshot_is_a_permanent_invariant_failure() { set_temporary_dex_failure(true); signal_percentage_trigger(actor_id, asset_in); run_idle(Weight::MAX); - ContinuationStateStore::::mutate(actor_id, |maybe| { + ActorRunStateStore::::mutate(actor_id, |maybe| { maybe .as_mut() .expect("suspended continuation") @@ -710,7 +2296,7 @@ fn missing_frozen_snapshot_is_a_permanent_invariant_failure() { set_temporary_dex_failure(false); frame_system::Pallet::::set_block_number(2); run_idle(Weight::MAX); - assert!(Actors::continuation_state(actor_id).is_none()); + assert!(Actors::actor_run_state(actor_id).is_none()); let actor_state = Actors::active_actor_view(actor_id).expect("actor remains after permanent failure"); assert_eq!(actor_state.cycle_state, CycleState::Idle); @@ -764,7 +2350,7 @@ fn canonical_loader_distinguishes_absence_dormancy_active_and_corruption() { )); assert!(!Actors::pending_signal(actor_id)); assert_noop!( - Actors::write_continuation_state(actor_id, None), + Actors::write_run_state(actor_id, None), Error::::ActorInvariant ); assert_noop!( @@ -788,9 +2374,10 @@ fn canonical_loader_classifies_every_four_partition_presence_mask() { let actor_id = create_system_with(ALICE, manual_schedule(), None, inert_contract_steps()); let identity = ActorIdentities::::take(actor_id).expect("identity fixture"); let hot = ActorHot::::take(actor_id).expect("hot fixture"); - let contract = ActorContracts::::take(actor_id).expect("contract fixture"); + let contract = Actors::load_actor_contract(actor_id).expect("contract fixture"); + assert!(Actors::remove_admitted_contract_geometry(actor_id).is_some()); let funding = ActorFunding::::take(actor_id).expect("funding fixture"); - ContinuationStateStore::::remove(actor_id); + ActorRunStateStore::::remove(actor_id); if mask & 0b0001 != 0 { ActorIdentities::::insert(actor_id, identity); } @@ -798,7 +2385,7 @@ fn canonical_loader_classifies_every_four_partition_presence_mask() { ActorHot::::insert(actor_id, hot); } if mask & 0b0100 != 0 { - ActorContracts::::insert(actor_id, contract); + assert_ok!(Actors::store_actor_contract(actor_id, contract)); } if mask & 0b1000 != 0 { ActorFunding::::insert(actor_id, funding); @@ -851,7 +2438,8 @@ fn try_state_rejects_every_corrupt_four_partition_presence_mask() { let actor_id = create_system_with(ALICE, manual_schedule(), None, inert_contract_steps()); let identity = ActorIdentities::::take(actor_id).expect("identity fixture"); let hot = ActorHot::::take(actor_id).expect("hot fixture"); - let contract = ActorContracts::::take(actor_id).expect("contract fixture"); + let contract = Actors::load_actor_contract(actor_id).expect("contract fixture"); + assert!(Actors::remove_admitted_contract_geometry(actor_id).is_some()); let funding = ActorFunding::::take(actor_id).expect("funding fixture"); if mask & 0b0001 != 0 { ActorIdentities::::insert(actor_id, identity); @@ -860,7 +2448,7 @@ fn try_state_rejects_every_corrupt_four_partition_presence_mask() { ActorHot::::insert(actor_id, hot); } if mask & 0b0100 != 0 { - ActorContracts::::insert(actor_id, contract); + assert_ok!(Actors::store_actor_contract(actor_id, contract)); } if mask & 0b1000 != 0 { ActorFunding::::insert(actor_id, funding); diff --git a/template/pallets/actors/src/tests/wakeups.rs b/template/pallets/actors/src/tests/wakeups.rs index c97636a7..03fedb01 100644 --- a/template/pallets/actors/src/tests/wakeups.rs +++ b/template/pallets/actors/src/tests/wakeups.rs @@ -816,7 +816,7 @@ fn cursor_wakeup_drain_halts_and_resumes_between_slot_units() { } let limit = <::WeightInfo as crate::WeightInfo>::scheduler_wakeup_cursor_worker_future() - .saturating_add(Actors::wakeup_cursor_drain_unit_weight_upper(false)); + .saturating_add(Actors::block_wakeup_cursor_drain_unit_weight_upper(false)); let mut one_slot = WeightMeter::with_limit(limit); let first = Actors::drain_overdue_wakeups_cursor(10, &mut one_slot); assert_eq!(first.entries_scanned, 1); @@ -857,7 +857,7 @@ fn cursor_wakeup_drain_stops_independently_on_reftime_and_proof_size() { assert!(Actors::wakeup_substrate_schedule(actor_id, 10)); let required = <::WeightInfo as crate::WeightInfo>::scheduler_wakeup_cursor_worker_future() - .saturating_add(Actors::wakeup_cursor_drain_unit_weight_upper(true)); + .saturating_add(Actors::block_wakeup_cursor_drain_unit_weight_upper(true)); let mut reftime_short = WeightMeter::with_limit(Weight::from_parts( required.ref_time().saturating_sub(1), @@ -891,7 +891,7 @@ fn cursor_wakeup_drain_stops_independently_on_reftime_and_proof_size() { fn on_idle_wakeup_worker_respects_remaining_weight_in_each_dimension() { let required = <::WeightInfo as crate::WeightInfo>::scheduler_wakeup_cursor_worker_future() - .saturating_add(Actors::wakeup_cursor_drain_unit_weight_upper(true)); + .saturating_add(Actors::block_wakeup_cursor_drain_unit_weight_upper(true)); assert_on_idle_wakeup_insufficiency_preserves_state(Weight::from_parts( required.ref_time().saturating_sub(1), u64::MAX, @@ -968,38 +968,75 @@ fn wakeup_materialization_faults_and_requires_repair_on_fifo_topology_corruption assert!(Actors::wakeup_substrate_schedule(actor_id, 10)); QueueTail::::put(1); QueueOccupancy::::put(0); - let events_before = System::events(); + let events_before = System::events().len(); let root_before = polkadot_sdk::sp_io::storage::root(StateVersion::V1); let mut meter = WeightMeter::with_limit(Weight::MAX); let stats = Actors::drain_overdue_wakeups_cursor(10, &mut meter); assert_eq!(stats.entries_scanned, 0); - assert_eq!(System::events(), events_before); + assert_eq!(System::events().len(), events_before + 1); assert_ne!( polkadot_sdk::sp_io::storage::root(StateVersion::V1), root_before ); assert_eq!(scheduled_wakeup_block(actor_id), Some(10)); + let fault = crate::WakeupWorkerFault { + key: WakeupKey::Block(10), + page: 0, + class: crate::CrossingWorkerFaultClass::Invariant, + }; + assert_eq!(Actors::wakeup_worker_fault(), Some(fault)); assert_eq!( - Actors::wakeup_worker_fault(), - Some(crate::WakeupWorkerFault { - key: WakeupKey::Block(10), - page: 0, - class: crate::CrossingWorkerFaultClass::Invariant, - }) + actor_event_count(|event| matches!( + event, + Event::ActorFaultRecorded { + fault_id: crate::FaultId::WakeupWorker, + kind: crate::ActorFaultKind::Wakeup, + first_recorded_block: 1, + context: crate::FaultContext::Wakeup(recorded), + } if recorded == &fault + )), + 1 ); let mut halted = WeightMeter::with_limit(Weight::MAX); assert_eq!( Actors::drain_overdue_wakeups_cursor(10, &mut halted).entries_scanned, 0 ); + assert_eq!( + actor_event_count(|event| matches!( + event, + Event::ActorFaultRecorded { + fault_id: crate::FaultId::WakeupWorker, + .. + } + )), + 1, + "an uncleared wakeup fault emits only its first-recorded event" + ); assert_noop!( Actors::clear_wakeup_worker_fault(RuntimeOrigin::signed(ALICE)), DispatchError::BadOrigin ); QueueTail::::put(0); assert_ok!(Actors::clear_wakeup_worker_fault(RuntimeOrigin::root())); + let actor_events: Vec<_> = System::events() + .into_iter() + .filter_map(|record| match record.event { + RuntimeEvent::Actors(event) => Some(event), + _ => None, + }) + .collect(); + let recorded_index = actor_events + .iter() + .position(|event| matches!(event, Event::ActorFaultRecorded { .. })) + .expect("first-recorded event exists"); + let cleared_index = actor_events + .iter() + .position(|event| matches!(event, Event::WakeupWorkerFaultCleared { .. })) + .expect("clear event exists"); + assert!(recorded_index < cleared_index); let mut repaired = WeightMeter::with_limit(Weight::MAX); assert_eq!( Actors::drain_overdue_wakeups_cursor(10, &mut repaired).ready_entries, @@ -1010,6 +1047,55 @@ fn wakeup_materialization_faults_and_requires_repair_on_fifo_topology_corruption }); } +#[test] +fn wakeup_fault_recording_admits_both_weight_dimensions_and_is_idempotent() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let fault = crate::WakeupWorkerFault { + key: WakeupKey::Block(10), + page: 0, + class: crate::CrossingWorkerFaultClass::Invariant, + }; + let required = ::record_wakeup_worker_fault(); + + let mut ref_time_short = WeightMeter::with_limit(Weight::from_parts( + required.ref_time().saturating_sub(1), + u64::MAX, + )); + assert!(!Actors::record_wakeup_worker_fault( + &mut ref_time_short, + fault + )); + assert!(crate::WakeupWorkerFaultState::::get().is_none()); + assert_eq!(System::events().len(), 0); + + let mut proof_short = WeightMeter::with_limit(Weight::from_parts( + u64::MAX, + required.proof_size().saturating_sub(1), + )); + assert!(!Actors::record_wakeup_worker_fault(&mut proof_short, fault)); + assert!(crate::WakeupWorkerFaultState::::get().is_none()); + assert_eq!(System::events().len(), 0); + + let mut admitted = WeightMeter::with_limit(required); + assert!(Actors::record_wakeup_worker_fault(&mut admitted, fault)); + assert_eq!(admitted.consumed(), required); + let events_after_first = System::events(); + + let mut duplicate = WeightMeter::with_limit(Weight::MAX); + assert!(!Actors::record_wakeup_worker_fault( + &mut duplicate, + crate::WakeupWorkerFault { + class: crate::CrossingWorkerFaultClass::Other, + ..fault + }, + )); + assert_eq!(duplicate.consumed(), Weight::zero()); + assert_eq!(crate::WakeupWorkerFaultState::::get(), Some(fault)); + assert_eq!(System::events(), events_after_first); + }); +} + #[test] fn paged_wakeup_cursor_orders_sparse_blocks_across_page_boundaries() { new_test_ext().execute_with(|| { @@ -1161,7 +1247,7 @@ fn wakeup_materialization_index_exhaustion_closes_without_an_attempt() { } #[test] -fn post_attempt_wakeup_index_exhaustion_commits_then_closes() { +fn trigger_rearm_index_exhaustion_closes_before_the_due_attempt() { new_test_ext().execute_with(|| { frame_system::Pallet::::set_block_number(1); let actor_id = create_system_with( @@ -1187,7 +1273,7 @@ fn post_attempt_wakeup_index_exhaustion_commits_then_closes() { run_idle(Weight::MAX); - assert_eq!(native_balance(&BOB), bob_before + 10); + assert_eq!(native_balance(&BOB), bob_before); assert!(Actors::actor_identities(actor_id).is_none()); assert!(Actors::actor_hot(actor_id).is_none()); assert_eq!(Actors::combined_queue_occupancy(), 0); @@ -1267,7 +1353,7 @@ fn wakeup_worker_stops_at_its_own_weight_envelope_without_lending() { // The shared on_idle meter has far more than the wakeup worker's dedicated envelope; the // worker must stop at its own ceiling and leave the surplus for actor service (spec 8.4.5). let cursor_probe = <::WeightInfo as crate::WeightInfo>::scheduler_wakeup_cursor_worker_future(); - let worker_unit = Actors::wakeup_cursor_drain_unit_weight_upper(true); + let worker_unit = Actors::block_wakeup_cursor_drain_unit_weight_upper(true); let worker_envelope = cursor_probe.saturating_add(worker_unit); let mut meter = polkadot_sdk::sp_weights::WeightMeter::with_limit( worker_envelope.saturating_sub(Weight::from_parts(1, 0)), @@ -1524,19 +1610,19 @@ fn temporal_membership_try_state_rejects_wakeup_pointer_beyond_terminal() { ActorHot::::mutate(actor_id, |maybe| { maybe.as_mut().expect("hot").terminal_at = Some(50); }); - ActorContracts::::mutate(actor_id, |maybe| { - maybe.as_mut().expect("contract").window = Some(ScheduleWindow { start: 1, end: 49 }); - }); + let mut contract = Actors::load_actor_contract(actor_id).expect("contract"); + contract.window = Some(ScheduleWindow { start: 1, end: 49 }); + assert_ok!(Actors::store_actor_contract(actor_id, contract)); assert_eq!( crate::Pallet::::do_try_state().map_err(|error| format!("{error:?}")), - Err("Other(\"ActorHot wakeup pointer exceeds its terminal membership\")".into()) + Err("Other(\"ActorHot Pipeline wakeup pointer exceeds its terminal membership\")".into()) ); ActorHot::::mutate(actor_id, |maybe| { maybe.as_mut().expect("hot").terminal_at = Some(102); }); - ActorContracts::::mutate(actor_id, |maybe| { - maybe.as_mut().expect("contract").window = Some(ScheduleWindow { start: 1, end: 101 }); - }); + let mut contract = Actors::load_actor_contract(actor_id).expect("contract"); + contract.window = Some(ScheduleWindow { start: 1, end: 101 }); + assert_ok!(Actors::store_actor_contract(actor_id, contract)); assert_ok!(crate::Pallet::::do_try_state()); } }); @@ -1707,7 +1793,7 @@ fn terminal_window_wakeup_survives_queue_saturation_and_continuation() { assert_ok!(Actors::manual_trigger(RuntimeOrigin::signed(ALICE), actor_id)); run_idle(Weight::MAX); assert!( - Actors::continuation_state(actor_id).is_some(), + Actors::actor_run_state(actor_id).is_some(), "retryable step leaves a Continuation" ); assert_eq!( @@ -1727,14 +1813,14 @@ fn terminal_window_wakeup_survives_queue_saturation_and_continuation() { let entries = (0..len) .map(|offset| { let ticket = u64::from(first.saturating_add(offset)); - QueueEntry { + queue_entry( ticket, - actor_id: if ticket == existing_ticket { + if ticket == existing_ticket { actor_id } else { 30_000_000u64.saturating_add(ticket) }, - } + ) }) .collect::>(); QueuePages::::insert( @@ -1758,7 +1844,7 @@ fn terminal_window_wakeup_survives_queue_saturation_and_continuation() { scheduled_wakeup_block(actor_id), Actors::actor_hot(actor_id).and_then(|hot| hot.queue_ticket), ); - assert!(Actors::continuation_state(actor_id).is_none()); + assert!(Actors::actor_run_state(actor_id).is_none()); assert!(has_actor_event(|event| matches!( event, Event::ActorClosed { @@ -1817,7 +1903,7 @@ fn continuation_attempt_rolls_back_when_retry_wakeup_topology_is_corrupt() { }, ); let actor_before = Actors::active_actor_view(actor_id).expect("queued continuation"); - let continuation_before = Actors::continuation_state(actor_id) + let continuation_before = Actors::actor_run_state(actor_id) .expect("continuation before corrupt retry placement") .encode(); let events_before = System::events(); @@ -1827,8 +1913,8 @@ fn continuation_attempt_rolls_back_when_retry_wakeup_topology_is_corrupt() { assert_eq!(Actors::active_actor_view(actor_id), Some(actor_before)); assert_eq!( - Actors::continuation_state(actor_id) - .expect("continuation survives failed placement") + Actors::actor_run_state(actor_id) + .expect("Actor run survives failed placement") .encode(), continuation_before, ); @@ -1870,7 +1956,7 @@ fn cancelled_continuation_exactly_invalidates_its_wakeup_before_reprime() { assert_eq!(scheduled_wakeup_block(actor_id), Some(11)); assert!(Actors::wakeup_buckets(11).is_some()); - assert_ok!(Actors::cancel_continuation(RuntimeOrigin::root(), actor_id)); + assert_ok!(Actors::cancel_run(RuntimeOrigin::root(), actor_id)); assert!(scheduled_wakeup_block(actor_id).is_none()); assert!(Actors::wakeup_buckets(11).is_none()); frame_system::Pallet::::set_block_number(11); @@ -1912,6 +1998,32 @@ fn queue_saturation_at_block_max_cannot_create_same_block_wakeup() { }); } +#[test] +fn pipeline_and_trigger_temporal_memberships_coexist_and_drain_independently() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = create_system_with(ALICE, timer_schedule(100), None, inert_contract_steps()); + let trigger_pointer = Actors::actor_hot(actor_id) + .and_then(|hot| hot.trigger_wakeup_pointer) + .expect("Cadenced trigger pointer exists"); + assert!(Actors::wakeup_substrate_schedule(actor_id, 10)); + let hot = Actors::actor_hot(actor_id).expect("Actor owns both temporal memberships"); + assert!(hot.wakeup_pointer.is_some()); + assert_eq!(hot.trigger_wakeup_pointer, Some(trigger_pointer)); + #[cfg(feature = "try-runtime")] + assert_ok!(crate::Pallet::::do_try_state()); + + let (ready, stats) = Actors::wakeup_substrate_drain_block(10, 1); + assert_eq!(ready.as_slice(), &[actor_id]); + assert_eq!(stats.ready_entries, 1); + let hot = Actors::actor_hot(actor_id).expect("Actor remains active"); + assert!(hot.wakeup_pointer.is_none()); + assert_eq!(hot.trigger_wakeup_pointer, Some(trigger_pointer)); + #[cfg(feature = "try-runtime")] + assert_ok!(crate::Pallet::::do_try_state()); + }); +} + #[test] fn timer_wakeup_uses_exact_cadence_without_actor_phase() { new_test_ext().execute_with(|| { diff --git a/template/pallets/actors/src/types.rs b/template/pallets/actors/src/types.rs index 8c861e67..92eccf9e 100644 --- a/template/pallets/actors/src/types.rs +++ b/template/pallets/actors/src/types.rs @@ -1,9 +1,15 @@ mod contract; +mod fault; mod lifecycle; mod observation; +mod plan; +mod resource; mod scheduler; pub use contract::*; +pub use fault::*; pub use lifecycle::*; pub use observation::*; +pub use plan::*; +pub use resource::*; pub use scheduler::*; diff --git a/template/pallets/actors/src/types/contract.rs b/template/pallets/actors/src/types/contract.rs index 77d56844..2c92d4d0 100644 --- a/template/pallets/actors/src/types/contract.rs +++ b/template/pallets/actors/src/types/contract.rs @@ -1,6 +1,7 @@ use super::lifecycle::{CompletionPolicy, StepErrorPolicy}; +use alloc::vec::Vec; use frame::prelude::*; -use polkadot_sdk::sp_runtime::Perbill; +use polkadot_sdk::{sp_runtime::Perbill, sp_weights::Weight}; #[derive( Clone, Copy, Debug, Decode, DecodeWithMemTracking, Encode, Eq, PartialEq, TypeInfo, MaxEncodedLen, @@ -13,6 +14,15 @@ pub enum AmountResolution { AllAvailable, } +impl AmountResolution { + pub fn requires_frozen_cycle_snapshot(&self) -> bool { + matches!( + self, + Self::PercentageAtOpening(_) | Self::PercentageOfLastFunding(_) + ) + } +} + #[derive( Clone, Copy, Debug, Decode, DecodeWithMemTracking, Encode, Eq, PartialEq, TypeInfo, MaxEncodedLen, )] @@ -95,6 +105,29 @@ pub enum Task> { StopCycle, } +impl> + Task +{ + pub fn requires_frozen_cycle_snapshot(&self) -> bool { + match self { + Self::Transfer { amount, .. } + | Self::SplitTransfer { amount, .. } + | Self::Burn { amount, .. } + | Self::Mint { amount, .. } + | Self::Stake { amount, .. } => amount.requires_frozen_cycle_snapshot(), + Self::SwapIn { amount_in, .. } => amount_in.requires_frozen_cycle_snapshot(), + Self::SwapOut { amount_out, .. } => amount_out.requires_frozen_cycle_snapshot(), + Self::AddLiquidity { + amount_a, amount_b, .. + } => amount_a.requires_frozen_cycle_snapshot() || amount_b.requires_frozen_cycle_snapshot(), + Self::RemoveLiquidity { lp_amount, .. } => lp_amount.requires_frozen_cycle_snapshot(), + Self::DonateLiquidity { max_amount_a, .. } => max_amount_a.requires_frozen_cycle_snapshot(), + Self::Unstake { shares, .. } => shares.requires_frozen_cycle_snapshot(), + Self::StopCycle => false, + } + } +} + impl> Clone for Task { @@ -701,6 +734,18 @@ impl ObservationCrossing { } } +#[derive( + Clone, Copy, Debug, Decode, DecodeWithMemTracking, Encode, Eq, MaxEncodedLen, PartialEq, TypeInfo, +)] +pub enum TriggerFamily { + Manual, + AddressEvent, + ObservationChange, + ObservationCrossing, + AtTime, + Cadenced, +} + #[derive(Decode, DecodeWithMemTracking, Encode, TypeInfo, MaxEncodedLen)] #[scale_info(skip_type_params(MaxWhitelistSize))] pub enum Trigger, ObservationFeedId = AssetId> { @@ -718,6 +763,9 @@ pub enum Trigger, ObservationFeed threshold: ObservationValue, rearm_threshold: ObservationValue, }, + AtTime { + after_ticks: u64, + }, Cadenced { every_ticks: u64, }, @@ -748,6 +796,9 @@ impl, ObservationFe threshold: *threshold, rearm_threshold: *rearm_threshold, }, + Self::AtTime { after_ticks } => Self::AtTime { + after_ticks: *after_ticks, + }, Self::Cadenced { every_ticks } => Self::Cadenced { every_ticks: *every_ticks, }, @@ -789,6 +840,10 @@ impl< .field("threshold", threshold) .field("rearm_threshold", rearm_threshold) .finish(), + Self::AtTime { after_ticks } => f + .debug_struct("AtTime") + .field("after_ticks", after_ticks) + .finish(), Self::Cadenced { every_ticks } => f .debug_struct("Cadenced") .field("every_ticks", every_ticks) @@ -839,6 +894,7 @@ impl< && left_threshold == right_threshold && left_rearm == right_rearm } + (Self::AtTime { after_ticks: left }, Self::AtTime { after_ticks: right }) => left == right, (Self::Cadenced { every_ticks: left }, Self::Cadenced { every_ticks: right }) => { left == right } @@ -904,6 +960,10 @@ impl, ObservationFeedId> } } + pub fn at_time(after_ticks: u64) -> Self { + Self::AtTime { after_ticks } + } + pub fn cadenced(every_ticks: u64) -> Self { Self::Cadenced { every_ticks } } @@ -922,6 +982,17 @@ impl, ObservationFeedId> } } + pub const fn family(&self) -> TriggerFamily { + match self { + Self::Manual => TriggerFamily::Manual, + Self::AddressEvent { .. } => TriggerFamily::AddressEvent, + Self::ObservationChange { .. } => TriggerFamily::ObservationChange, + Self::ObservationCrossing { .. } => TriggerFamily::ObservationCrossing, + Self::AtTime { .. } => TriggerFamily::AtTime, + Self::Cadenced { .. } => TriggerFamily::Cadenced, + } + } + pub fn manual_source_enabled(&self) -> bool { matches!(self, Self::Manual) } @@ -1127,6 +1198,34 @@ pub struct Step< pub on_error: StepErrorPolicy, } +impl< + AssetId, + Balance, + AccountId, + MaxPreconditionClauses: Get, + MaxPredicatesPerClause: Get, + MaxSplitTransferLegs: Get, + ObservationFeedId, +> + Step< + AssetId, + Balance, + AccountId, + MaxPreconditionClauses, + MaxPredicatesPerClause, + MaxSplitTransferLegs, + ObservationFeedId, + > +{ + pub fn requires_frozen_cycle_snapshot(&self) -> bool { + self + .precondition + .as_ref() + .is_some_and(|precondition| precondition.opening_predicate_count() > 0) + || self.task.requires_frozen_cycle_snapshot() + } +} + impl< AssetId: Clone, Balance: Clone, @@ -1312,6 +1411,11 @@ pub enum OpeningSurface { StakingShares(AssetId), } +pub const ACTOR_CONTRACT_HASH_DOMAIN: [u8; 19] = *b"DEOS_ACTOR_CONTRACT"; +pub const ACTOR_BODY_HASH_DOMAIN: [u8; 15] = *b"DEOS_ACTOR_BODY"; +pub const ACTOR_ADMISSION_HASH_DOMAIN: [u8; 20] = *b"DEOS_ACTOR_ADMISSION"; +pub const MAX_STEPS_PER_TAIL_CHUNK: u32 = 4; + #[derive( Clone, Debug, Decode, DecodeWithMemTracking, Encode, Eq, PartialEq, TypeInfo, MaxEncodedLen, )] @@ -1325,13 +1429,299 @@ pub struct ActorContract { pub auto_close_at_cycle_nonce: Option, } +#[derive( + Clone, Copy, Debug, Decode, DecodeWithMemTracking, Encode, Eq, PartialEq, TypeInfo, MaxEncodedLen, +)] +pub struct PipelineMachineEnvelope { + pub pipeline_machine_fee_upper: Balance, + pub cleanup_fee_upper: Balance, +} + +#[derive( + Clone, Debug, Decode, DecodeWithMemTracking, Encode, Eq, PartialEq, TypeInfo, MaxEncodedLen, +)] +pub struct ActorContractHeader { + pub trigger: Trigger, + pub cooldown_blocks: u32, + pub window: Option>, + pub funding: FundingPolicy, + pub completion: CompletionPolicy, + pub auto_close_at_cycle_nonce: Option, + pub step_count: u32, + pub semantic_contract_id: Hash, + pub body_commitment: Hash, + pub admission_identity: Hash, + pub pipeline_machine_envelope: PipelineMachineEnvelope, +} + +#[derive( + Clone, Debug, Decode, DecodeWithMemTracking, Encode, Eq, PartialEq, TypeInfo, MaxEncodedLen, +)] +pub struct ActorContractHead { + pub header: Header, + pub first_step: Option, + pub first_step_resources: Option, +} + +#[derive( + Clone, Debug, Decode, DecodeWithMemTracking, Encode, Eq, PartialEq, TypeInfo, MaxEncodedLen, +)] +pub struct ActorActivationAuthority { + pub feed: ObservationFeedId, + pub cooldown_blocks: u32, + pub window: Option>, + pub auto_close_at_cycle_nonce: Option, + pub semantic_contract_id: Hash, + pub body_commitment: Hash, + pub admission_identity: Hash, +} + +#[derive( + Clone, Debug, Decode, DecodeWithMemTracking, Encode, Eq, PartialEq, TypeInfo, MaxEncodedLen, +)] +pub struct ActorBodyAuthority { + pub actor_id: ActorId, + pub semantic_contract_id: Hash, + pub body_commitment: Hash, + pub admission_identity: Hash, +} + +#[derive( + Clone, Copy, Debug, Decode, DecodeWithMemTracking, Encode, Eq, PartialEq, TypeInfo, MaxEncodedLen, +)] +pub struct ActorContractCommitment { + pub semantic_contract_id: Hash, + pub body_commitment: Hash, +} + +#[derive( + Clone, Debug, Decode, DecodeWithMemTracking, Encode, Eq, PartialEq, TypeInfo, MaxEncodedLen, +)] +pub struct ActorStepChunk { + pub authority: ActorBodyAuthority, + pub first_step_index: u32, + pub steps: Steps, + pub step_resources: Resources, +} + +pub type ActorStepControlWeight = Weight; +pub type ActorTaskEffectWeight = Weight; + +#[derive( + Clone, Copy, Debug, Decode, DecodeWithMemTracking, Encode, Eq, PartialEq, TypeInfo, MaxEncodedLen, +)] +pub struct ActorStepResourceEnvelope { + pub control: ActorStepControlWeight, + pub effect: ActorTaskEffectWeight, +} + +#[derive( + Clone, Debug, Decode, DecodeWithMemTracking, Encode, Eq, PartialEq, TypeInfo, MaxEncodedLen, +)] +pub struct LoadedActorStep { + pub cursor: u32, + pub step: Step, + pub resources: ActorStepResourceEnvelope, +} + +#[derive( + Clone, Debug, Decode, DecodeWithMemTracking, Encode, Eq, PartialEq, TypeInfo, MaxEncodedLen, +)] +#[scale_info(skip_type_params(Resources))] +pub struct ActorAdmissionCertificate { + pub semantic_contract_id: [u8; 32], + pub body_commitment: [u8; 32], + pub runtime_actor_semantics_version: u32, + pub production_weight_identity: [u8; 32], + pub body_geometry_version: u32, + pub configured_bounds_commitment: [u8; 32], + pub maximum_lifecycle_weight: Weight, + pub admission_identity: [u8; 32], + pub marker: core::marker::PhantomData, +} + +impl ActorAdmissionCertificate { + pub fn new( + semantic_contract_id: [u8; 32], + body_commitment: [u8; 32], + runtime_actor_semantics_version: u32, + production_weight_identity: [u8; 32], + body_geometry_version: u32, + configured_bounds_commitment: [u8; 32], + maximum_lifecycle_weight: Weight, + ) -> Self { + let admission_identity = Self::derive_admission_identity( + &semantic_contract_id, + &body_commitment, + runtime_actor_semantics_version, + &production_weight_identity, + body_geometry_version, + &configured_bounds_commitment, + maximum_lifecycle_weight, + ); + Self { + semantic_contract_id, + body_commitment, + runtime_actor_semantics_version, + production_weight_identity, + body_geometry_version, + configured_bounds_commitment, + maximum_lifecycle_weight, + admission_identity, + marker: core::marker::PhantomData, + } + } + + pub fn derive_admission_identity( + semantic_contract_id: &[u8; 32], + body_commitment: &[u8; 32], + runtime_actor_semantics_version: u32, + production_weight_identity: &[u8; 32], + body_geometry_version: u32, + configured_bounds_commitment: &[u8; 32], + maximum_lifecycle_weight: Weight, + ) -> [u8; 32] { + ( + ACTOR_ADMISSION_HASH_DOMAIN, + semantic_contract_id, + body_commitment, + runtime_actor_semantics_version, + production_weight_identity, + body_geometry_version, + configured_bounds_commitment, + maximum_lifecycle_weight, + ) + .using_encoded(frame::hashing::blake2_256) + } + + pub fn has_valid_identity(&self) -> bool { + self.admission_identity + == Self::derive_admission_identity( + &self.semantic_contract_id, + &self.body_commitment, + self.runtime_actor_semantics_version, + &self.production_weight_identity, + self.body_geometry_version, + &self.configured_bounds_commitment, + self.maximum_lifecycle_weight, + ) + } +} + +impl ActorBodyAuthority { + pub fn matches( + &self, + actor_id: &ActorId, + semantic_contract_id: &Hash, + body_commitment: &Hash, + admission_identity: &Hash, + ) -> bool { + &self.actor_id == actor_id + && &self.semantic_contract_id == semantic_contract_id + && &self.body_commitment == body_commitment + && &self.admission_identity == admission_identity + } +} + +impl + ActorStepChunk +{ + pub fn matches( + &self, + actor_id: &ActorId, + semantic_contract_id: &Hash, + body_commitment: &Hash, + admission_identity: &Hash, + first_step_index: u32, + ) -> bool { + self.first_step_index == first_step_index + && self.authority.matches( + actor_id, + semantic_contract_id, + body_commitment, + admission_identity, + ) + } +} + +impl + ActorContract +{ + pub fn semantic_contract_id(&self) -> [u8; 32] + where + Trigger: Encode, + BlockNumber: Encode, + Steps: Encode, + FundingPolicy: Encode, + { + ( + ACTOR_CONTRACT_HASH_DOMAIN, + ( + &self.trigger, + self.cooldown_blocks, + &self.window, + &self.funding, + self.completion, + self.auto_close_at_cycle_nonce, + ), + &self.steps, + ) + .using_encoded(frame::hashing::blake2_256) + } + + pub fn body_commitment(&self) -> Option<[u8; 32]> + where + Steps: AsRef<[Step]>, + Step: Encode, + { + let indexed_steps: Vec<(u32, &Step)> = self + .steps + .as_ref() + .iter() // deos-bypass: bounded-iter — admitted Contract Steps bound the complete visit. + .enumerate() + .map(|(index, step)| u32::try_from(index).ok().map(|index| (index, step))) + .collect::>()?; + Some( + (ACTOR_BODY_HASH_DOMAIN, indexed_steps.as_slice()).using_encoded(frame::hashing::blake2_256), + ) + } + + pub fn try_header( + &self, + semantic_contract_id: Hash, + body_commitment: Hash, + admission_identity: Hash, + pipeline_machine_envelope: PipelineMachineEnvelope, + ) -> Option> + where + Trigger: Clone, + BlockNumber: Clone, + Steps: AsRef<[Step]>, + FundingPolicy: Clone, + { + let step_count = u32::try_from(self.steps.as_ref().len()).ok()?; + Some(ActorContractHeader { + trigger: self.trigger.clone(), + cooldown_blocks: self.cooldown_blocks, + window: self.window.clone(), + funding: self.funding.clone(), + completion: self.completion, + auto_close_at_cycle_nonce: self.auto_close_at_cycle_nonce, + step_count, + semantic_contract_id, + body_commitment, + admission_identity, + pipeline_machine_envelope, + }) + } +} + #[derive( Clone, Debug, Decode, DecodeWithMemTracking, Encode, Eq, PartialEq, TypeInfo, MaxEncodedLen, )] -pub struct ActorFundingState { +pub struct ActorFundingState { pub funding_accumulated: FundingAccumulated, pub funding_tracked_assets: FundingTrackedAssets, - pub trigger_state_bond: Balance, } #[derive( diff --git a/template/pallets/actors/src/types/fault.rs b/template/pallets/actors/src/types/fault.rs new file mode 100644 index 00000000..3dfa2fe6 --- /dev/null +++ b/template/pallets/actors/src/types/fault.rs @@ -0,0 +1,31 @@ +use super::{CrossingWorkerFault, ObservationFanoutWorkerFault, WakeupWorkerFault}; +use frame::prelude::*; + +#[derive( + Clone, Copy, Debug, Decode, DecodeWithMemTracking, Encode, Eq, MaxEncodedLen, PartialEq, TypeInfo, +)] +pub enum ActorFaultKind { + Control, + Body, + Detector, + Wakeup, + Queue, +} + +#[derive( + Clone, Copy, Debug, Decode, DecodeWithMemTracking, Encode, Eq, MaxEncodedLen, PartialEq, TypeInfo, +)] +pub enum FaultId { + CrossingWorker, + ObservationFanoutWorker, + WakeupWorker, +} + +#[derive( + Clone, Copy, Debug, Decode, DecodeWithMemTracking, Encode, Eq, MaxEncodedLen, PartialEq, TypeInfo, +)] +pub enum FaultContext { + Crossing(CrossingWorkerFault), + ObservationFanout(ObservationFanoutWorkerFault), + Wakeup(WakeupWorkerFault), +} diff --git a/template/pallets/actors/src/types/lifecycle.rs b/template/pallets/actors/src/types/lifecycle.rs index 9bca264c..5bc9baa4 100644 --- a/template/pallets/actors/src/types/lifecycle.rs +++ b/template/pallets/actors/src/types/lifecycle.rs @@ -1,12 +1,14 @@ use super::{ contract::{ CrossingDirection, CrossingPhase, OpeningSurface, PredicateError, ScheduleWindow, Trigger, + TriggerFamily, }, - scheduler::{WakeupKey, WakeupPointer}, + scheduler::{TriggerWakeupPointer, WakeupKey, WakeupPointer}, }; use frame::prelude::*; pub type ActorId = u64; +pub const ACTOR_RUN_PAYLOAD_HASH_DOMAIN: &[u8] = b"DEOS_ACTOR_RUN_PAYLOAD"; #[derive( Clone, Copy, Debug, Decode, DecodeWithMemTracking, Encode, Eq, PartialEq, TypeInfo, MaxEncodedLen, @@ -105,11 +107,11 @@ impl ActiveLifecycle { )] pub enum CloseReason { OwnerInitiated, - BalanceExhausted, + CycleAdmissionInsufficient, + TriggerAdmissionInsufficient, ConsecutiveFailures, WindowExpired, CycleNonceExhausted, - FeeBudgetExhausted, AutoCloseNonceReached, RetryAttemptsExhausted, ProductiveCycleCompleted, @@ -194,7 +196,7 @@ pub enum StepSkippedReason { )] pub enum SimulationMode { FreshCurrentPlan, - CurrentContinuation, + CurrentRun, } /// Final disposition of one canonical production or simulated attempt. @@ -203,6 +205,7 @@ pub enum SimulationMode { )] pub enum AttemptDisposition { Completed, + Continued, Failed, Suspended, Closed(CloseReason), @@ -261,7 +264,7 @@ pub struct SimulationResult> { pub status: AttemptDisposition, pub cycle_nonce: u64, pub start_cursor: u32, - pub continuation_cursor: Option, + pub run_cursor: Option, pub unsuccessful_attempts_at_cursor: Option, pub cumulative_outcomes: OutcomeTotals, pub steps: BoundedVec, @@ -295,7 +298,7 @@ pub struct ActorClassification { )] pub enum ActorClassificationError { ActorInvariant, - ContinuationInvariant, + RunInvariant, ComputationOverflow, } @@ -331,6 +334,10 @@ pub enum ActorTriggerActivation { pending_revisions: u32, processing_revision: Option, }, + AtTime { + after_ticks: u64, + consumed: bool, + }, Cadenced { every_ticks: u64, }, @@ -355,6 +362,14 @@ pub enum ActorEligibility { Active(ActiveActorActivation), } +#[derive( + Clone, Copy, Debug, Decode, DecodeWithMemTracking, Encode, Eq, PartialEq, TypeInfo, MaxEncodedLen, +)] +pub enum TriggerCauseProvenance { + ExternalPhase, + Deferred, +} + #[derive( Clone, Copy, @@ -371,6 +386,7 @@ pub enum ActorEligibility { pub enum CycleState { #[default] Idle, + Running, Suspended, } @@ -396,13 +412,109 @@ pub struct OutcomeTotals { pub failed_steps: u32, } -#[derive(Clone, Debug, Decode, DecodeWithMemTracking, Encode, TypeInfo, MaxEncodedLen)] +#[derive( + Clone, Copy, Debug, Decode, DecodeWithMemTracking, Encode, Eq, PartialEq, TypeInfo, MaxEncodedLen, +)] +pub struct ActorRunAuthority { + pub semantic_contract_id: Hash, + pub body_commitment: Hash, + pub admission_identity: Hash, +} + +#[derive( + Clone, Debug, Decode, DecodeWithMemTracking, Encode, Eq, PartialEq, TypeInfo, MaxEncodedLen, +)] +pub struct ActorRunHead { + pub contract_authority: ActorRunAuthority<[u8; 32]>, + pub payload_commitment: [u8; 32], + pub cycle_nonce: u64, + pub cursor: u32, + pub opening_predicate_cursor: u32, + pub opening_predicate_result_count: u32, + pub unsuccessful_attempts_at_cursor: u32, + pub last_attempt_block: BlockNumber, + pub last_committed_step_block: Option, + pub eligible_at: BlockNumber, + pub cumulative_outcomes: OutcomeTotals, + pub last_step_outcome: Option, + pub suspension: Option, +} + +impl ActorRunHead { + pub fn has_contract_authority( + &self, + semantic_contract_id: [u8; 32], + body_commitment: [u8; 32], + admission_identity: [u8; 32], + ) -> bool { + self.contract_authority + == ActorRunAuthority { + semantic_contract_id, + body_commitment, + admission_identity, + } + } + + fn opening_predicate_cursor_is_coherent(&self) -> bool { + self.opening_predicate_cursor <= self.opening_predicate_result_count + } + + pub fn running_is_coherent(&self) -> bool + where + BlockNumber: PartialOrd, + { + self.opening_predicate_cursor_is_coherent() + && self.suspension.is_none() + && self + .last_committed_step_block + .as_ref() + .is_some_and(|last_committed| last_committed < &self.eligible_at) + } + + pub fn suspension_is_coherent(&self) -> bool { + self.opening_predicate_cursor_is_coherent() + && matches!( + (&self.last_step_outcome, self.suspension), + ( + Some(StepOutcome::FundingUnavailable), + Some(SuspensionReason::FundingUnavailable) + ) | ( + Some(StepOutcome::Failed(crate::TaskFailure { + retry: crate::RetryClass::Temporary, + .. + })), + Some(SuspensionReason::Temporary) + ) + ) + } +} + +#[derive(Debug, Decode, DecodeWithMemTracking, Encode, TypeInfo, MaxEncodedLen)] #[scale_info(skip_type_params( MaxSnapshotEntries, MaxFundingTrackedAssets, MaxOpeningPredicateResults ))] -pub struct ContinuationState< +pub struct ActorRunPayload< + AssetId, + Balance, + MaxSnapshotEntries: Get, + MaxFundingTrackedAssets: Get, + MaxOpeningPredicateResults: Get, +> { + pub opening_snapshot: BoundedBTreeMap, Balance, MaxSnapshotEntries>, + pub opening_predicate_results: + BoundedVec, MaxOpeningPredicateResults>, + pub funding_snapshot: BoundedBTreeMap, +} + +#[derive(Debug, Decode, DecodeWithMemTracking, Encode, TypeInfo, MaxEncodedLen)] +#[scale_info(skip_type_params( + MaxSnapshotEntries, + MaxFundingTrackedAssets, + MaxOpeningPredicateResults +))] +pub struct ActorRunState< AssetId, Balance, BlockNumber, @@ -410,14 +522,199 @@ pub struct ContinuationState< MaxFundingTrackedAssets: Get, MaxOpeningPredicateResults: Get, > { + pub contract_authority: ActorRunAuthority<[u8; 32]>, + pub cycle_nonce: u64, pub cursor: u32, + pub opening_predicate_cursor: u32, pub unsuccessful_attempts_at_cursor: u32, pub last_attempt_block: BlockNumber, + pub last_committed_step_block: Option, + pub eligible_at: BlockNumber, pub opening_snapshot: BoundedBTreeMap, Balance, MaxSnapshotEntries>, pub opening_predicate_results: BoundedVec, MaxOpeningPredicateResults>, pub funding_snapshot: BoundedBTreeMap, pub cumulative_outcomes: OutcomeTotals, + pub last_step_outcome: Option, + pub suspension: Option, +} + +impl< + AssetId: Clone + Ord, + Balance: Clone, + BlockNumber: Clone, + MaxSnapshotEntries: Get, + MaxFundingTrackedAssets: Get, + MaxOpeningPredicateResults: Get, +> Clone + for ActorRunState< + AssetId, + Balance, + BlockNumber, + MaxSnapshotEntries, + MaxFundingTrackedAssets, + MaxOpeningPredicateResults, + > +{ + fn clone(&self) -> Self { + Self { + contract_authority: self.contract_authority, + cycle_nonce: self.cycle_nonce, + cursor: self.cursor, + opening_predicate_cursor: self.opening_predicate_cursor, + unsuccessful_attempts_at_cursor: self.unsuccessful_attempts_at_cursor, + last_attempt_block: self.last_attempt_block.clone(), + last_committed_step_block: self.last_committed_step_block.clone(), + eligible_at: self.eligible_at.clone(), + opening_snapshot: self.opening_snapshot.clone(), + opening_predicate_results: self.opening_predicate_results.clone(), + funding_snapshot: self.funding_snapshot.clone(), + cumulative_outcomes: self.cumulative_outcomes, + last_step_outcome: self.last_step_outcome.clone(), + suspension: self.suspension, + } + } +} + +impl< + AssetId: Encode, + Balance: Encode, + BlockNumber, + MaxSnapshotEntries: Get, + MaxFundingTrackedAssets: Get, + MaxOpeningPredicateResults: Get, +> + ActorRunState< + AssetId, + Balance, + BlockNumber, + MaxSnapshotEntries, + MaxFundingTrackedAssets, + MaxOpeningPredicateResults, + > +{ + pub fn into_tiers( + self, + ) -> ( + ActorRunHead, + ActorRunPayload< + AssetId, + Balance, + MaxSnapshotEntries, + MaxFundingTrackedAssets, + MaxOpeningPredicateResults, + >, + ) { + let opening_predicate_result_count = + u32::try_from(self.opening_predicate_results.len()).unwrap_or(u32::MAX); + let payload = ActorRunPayload { + opening_snapshot: self.opening_snapshot, + opening_predicate_results: self.opening_predicate_results, + funding_snapshot: self.funding_snapshot, + }; + let payload_commitment = + (ACTOR_RUN_PAYLOAD_HASH_DOMAIN, &payload).using_encoded(frame::hashing::blake2_256); + ( + ActorRunHead { + contract_authority: self.contract_authority, + payload_commitment, + cycle_nonce: self.cycle_nonce, + cursor: self.cursor, + opening_predicate_cursor: self.opening_predicate_cursor, + opening_predicate_result_count, + unsuccessful_attempts_at_cursor: self.unsuccessful_attempts_at_cursor, + last_attempt_block: self.last_attempt_block, + last_committed_step_block: self.last_committed_step_block, + eligible_at: self.eligible_at, + cumulative_outcomes: self.cumulative_outcomes, + last_step_outcome: self.last_step_outcome, + suspension: self.suspension, + }, + payload, + ) + } + + pub fn from_tiers( + head: ActorRunHead, + payload: ActorRunPayload< + AssetId, + Balance, + MaxSnapshotEntries, + MaxFundingTrackedAssets, + MaxOpeningPredicateResults, + >, + ) -> Option { + if (ACTOR_RUN_PAYLOAD_HASH_DOMAIN, &payload).using_encoded(frame::hashing::blake2_256) + != head.payload_commitment + || usize::try_from(head.opening_predicate_result_count).ok() + != Some(payload.opening_predicate_results.len()) + { + return None; + } + Some(Self { + contract_authority: head.contract_authority, + cycle_nonce: head.cycle_nonce, + cursor: head.cursor, + opening_predicate_cursor: head.opening_predicate_cursor, + unsuccessful_attempts_at_cursor: head.unsuccessful_attempts_at_cursor, + last_attempt_block: head.last_attempt_block, + last_committed_step_block: head.last_committed_step_block, + eligible_at: head.eligible_at, + opening_snapshot: payload.opening_snapshot, + opening_predicate_results: payload.opening_predicate_results, + funding_snapshot: payload.funding_snapshot, + cumulative_outcomes: head.cumulative_outcomes, + last_step_outcome: head.last_step_outcome, + suspension: head.suspension, + }) + } + + pub(crate) fn has_contract_authority( + &self, + semantic_contract_id: [u8; 32], + body_commitment: [u8; 32], + admission_identity: [u8; 32], + ) -> bool { + self.contract_authority + == ActorRunAuthority { + semantic_contract_id, + body_commitment, + admission_identity, + } + } + + fn opening_predicate_cursor_is_coherent(&self) -> bool { + (self.opening_predicate_cursor as usize) <= self.opening_predicate_results.len() + } + + pub(crate) fn running_is_coherent(&self) -> bool + where + BlockNumber: PartialOrd, + { + self.opening_predicate_cursor_is_coherent() + && self.suspension.is_none() + && self + .last_committed_step_block + .as_ref() + .is_some_and(|last_committed| last_committed < &self.eligible_at) + } + + pub(crate) fn suspension_is_coherent(&self) -> bool { + self.opening_predicate_cursor_is_coherent() + && matches!( + (&self.last_step_outcome, self.suspension), + ( + Some(StepOutcome::FundingUnavailable), + Some(SuspensionReason::FundingUnavailable) + ) | ( + Some(StepOutcome::Failed(crate::TaskFailure { + retry: crate::RetryClass::Temporary, + .. + })), + Some(SuspensionReason::Temporary) + ) + ) + } } #[derive( @@ -432,6 +729,99 @@ pub struct ActorIdentity { pub last_control_mutation_block: BlockNumber, } +/// Named refundable resource backing for one User Actor's exact retained geometry. +#[derive( + Clone, Copy, Debug, Decode, DecodeWithMemTracking, Encode, Eq, PartialEq, TypeInfo, MaxEncodedLen, +)] +pub struct ActorStateHoldBreakdown { + pub identity: Balance, + pub contract_head: Balance, + pub contract_body: Balance, + pub detector: Balance, + pub funding: Balance, + pub run: Balance, +} + +/// Per-Actor accounting authority for the owner's aggregate dedicated hold reason. +#[derive( + Clone, Debug, Decode, DecodeWithMemTracking, Encode, Eq, PartialEq, TypeInfo, MaxEncodedLen, +)] +pub struct ActorStateHoldRecord { + pub owner: AccountId, + pub breakdown: ActorStateHoldBreakdown, +} + +#[derive( + Clone, Copy, Debug, Decode, DecodeWithMemTracking, Encode, Eq, PartialEq, TypeInfo, MaxEncodedLen, +)] +pub enum PipelineMachineFeeStrategy { + UpfrontBounded, +} + +#[derive( + Clone, Copy, Debug, Decode, DecodeWithMemTracking, Encode, Eq, PartialEq, TypeInfo, MaxEncodedLen, +)] +pub struct ActorTriggerFeeQuote { + pub trigger_family: TriggerFamily, + pub maximum_weight: Weight, + pub fee: Balance, + pub production_weight_identity: [u8; 32], +} + +#[derive( + Clone, Copy, Debug, Decode, DecodeWithMemTracking, Encode, Eq, PartialEq, TypeInfo, MaxEncodedLen, +)] +pub struct ActorPipelineFeeQuote { + pub pipeline_machine_fee: Balance, + pub cleanup_fee: Balance, + pub total_fee: Balance, + pub strategy: PipelineMachineFeeStrategy, + pub admission_identity: [u8; 32], + pub production_weight_identity: [u8; 32], +} + +#[derive( + Clone, Copy, Debug, Decode, DecodeWithMemTracking, Encode, Eq, PartialEq, TypeInfo, MaxEncodedLen, +)] +pub struct ActorActionFeeQuote { + pub maximum_effect_weight: Weight, + pub maximum_effect_fee: Balance, + pub production_weight_identity: [u8; 32], +} + +#[derive( + Clone, Copy, Debug, Decode, DecodeWithMemTracking, Encode, Eq, PartialEq, TypeInfo, MaxEncodedLen, +)] +pub struct ActorStateHoldQuote { + pub exempt: bool, + pub base_per_component: Balance, + pub per_encoded_byte: Balance, + pub breakdown: ActorStateHoldBreakdown, + pub total: Balance, +} + +#[derive( + Clone, Copy, Debug, Decode, DecodeWithMemTracking, Encode, Eq, PartialEq, TypeInfo, MaxEncodedLen, +)] +pub struct ActorCostQuote { + pub actor_type: ActorType, + pub creation_fee: Balance, + pub prospective_trigger_fee: Option>, + pub prospective_pipeline_fee: Option>, + pub maximum_next_action_fee: ActorActionFeeQuote, + pub actor_state_hold: ActorStateHoldQuote, +} + +#[derive( + Clone, Copy, Debug, Decode, DecodeWithMemTracking, Encode, Eq, PartialEq, TypeInfo, MaxEncodedLen, +)] +pub enum ActorCostQuoteError { + ActorNotFound, + ActorInvariant, + ComputationOverflow, + WeightAuthorityUnavailable, +} + #[derive( Clone, Debug, Decode, DecodeWithMemTracking, Encode, Eq, PartialEq, TypeInfo, MaxEncodedLen, )] @@ -441,19 +831,27 @@ pub enum TriggerRuntimeState { phase: CrossingPhase, installed_at_revision: u64, }, + AtTime { + anchor_tick: Option, + consumed: bool, + }, Cadenced { anchor_tick: Option, }, } impl TriggerRuntimeState { - pub fn cadence_anchor_tick(&self) -> Option { + pub fn temporal_anchor_tick(&self) -> Option { match self { - Self::Cadenced { anchor_tick } => *anchor_tick, + Self::AtTime { anchor_tick, .. } | Self::Cadenced { anchor_tick } => *anchor_tick, Self::Stateless | Self::ObservationCrossing { .. } => None, } } + pub fn temporal_occurrence_consumed(&self) -> bool { + matches!(self, Self::AtTime { consumed: true, .. }) + } + pub fn is_compatible_with( &self, trigger: &Trigger, @@ -469,7 +867,8 @@ impl TriggerRuntimeState { ) | ( Self::ObservationCrossing { .. }, Trigger::ObservationCrossing { .. } - ) | (Self::Cadenced { .. }, Trigger::Cadenced { .. }) + ) | (Self::AtTime { .. }, Trigger::AtTime { .. }) + | (Self::Cadenced { .. }, Trigger::Cadenced { .. }) ) } } @@ -485,6 +884,7 @@ pub struct ActorHotState { pub pending_signal: bool, pub queue_ticket: Option, pub wakeup_pointer: Option>, + pub trigger_wakeup_pointer: Option, pub terminal_at: Option, pub schedule_anchor: BlockNumber, pub last_cycle_block: Option, @@ -493,12 +893,12 @@ pub struct ActorHotState { #[derive( Clone, Debug, Decode, DecodeWithMemTracking, Encode, Eq, PartialEq, TypeInfo, MaxEncodedLen, )] -pub struct ActiveActorState { +pub struct ActiveActorState { pub identity: Identity, pub hot: Hot, pub contract: Contract, pub funding: Funding, - pub continuation: Option, + pub run_state: Option, } #[derive( @@ -522,8 +922,10 @@ pub(crate) struct ActiveActorView { pub pending_signal: bool, pub queue_ticket: Option, pub wakeup_pointer: Option>, + pub trigger_wakeup_pointer: Option, pub last_control_mutation_block: BlockNumber, pub schedule_anchor: BlockNumber, - pub cadence_anchor_tick: Option, + pub temporal_anchor_tick: Option, + pub temporal_occurrence_consumed: bool, pub last_cycle_block: Option, } diff --git a/template/pallets/actors/src/types/observation.rs b/template/pallets/actors/src/types/observation.rs index 00fcd398..40ae7137 100644 --- a/template/pallets/actors/src/types/observation.rs +++ b/template/pallets/actors/src/types/observation.rs @@ -1,5 +1,5 @@ -use super::ObservationValue; use super::lifecycle::ActorId; +use super::{ObservationValue, WakeupWorkerFault}; use frame::prelude::*; pub type ObservationRevision = u64; @@ -98,6 +98,10 @@ pub struct CrossingLeafState { pub struct CrossingMember { pub actor_id: ActorId, pub generation: u64, + /// Detector-local destination for the paired fire/re-arm leaf. + pub counterpart_threshold: ObservationValue, + /// Binds page-local routing to the admitted Contract authority without loading its cold head. + pub admission_identity: [u8; 32], } #[derive( @@ -141,6 +145,8 @@ pub struct CrossingTransitionObligation { pub revision: ObservationRevision, pub previous: ObservationValue, pub current: ObservationValue, + pub cause_provenance: crate::TriggerCauseProvenance, + pub cause_block: u64, } #[derive( @@ -185,6 +191,25 @@ pub struct CrossingRangeCursor { pub exhausted: bool, } +#[derive( + Clone, Copy, Debug, Decode, DecodeWithMemTracking, Encode, Eq, MaxEncodedLen, PartialEq, TypeInfo, +)] +pub struct CrossingCapacity { + pub user_limit: u32, + pub total_limit: u32, + pub user_memberships: u32, + pub total_memberships: u32, +} + +#[derive( + Clone, Copy, Debug, Decode, DecodeWithMemTracking, Encode, Eq, MaxEncodedLen, PartialEq, TypeInfo, +)] +pub struct MaterializationFaults { + pub crossing: Option>, + pub fanout: Option>, + pub wakeup: Option>, +} + #[derive( Clone, Copy, Debug, Decode, DecodeWithMemTracking, Encode, Eq, MaxEncodedLen, PartialEq, TypeInfo, )] @@ -212,6 +237,12 @@ pub struct ObservationFanoutWorkerFault { pub feed: FeedId, pub revision: ObservationRevision, pub subscriber_page: Option, + pub subscriber_position: u32, + pub actor_id: Option, + pub semantic_contract_id: Option<[u8; 32]>, + pub body_commitment: Option<[u8; 32]>, + pub admission_identity: Option<[u8; 32]>, + pub branch: ObservationFanoutBranch, pub class: CrossingWorkerFaultClass, } @@ -292,14 +323,29 @@ impl> core::ops::DerefMut for ObservationSubscriberPage { pub latest_revision: ObservationRevision, + pub latest_cause_provenance: crate::TriggerCauseProvenance, + pub latest_cause_block: u64, pub fanout_revision: ObservationRevision, + pub fanout_cause_provenance: crate::TriggerCauseProvenance, + pub fanout_cause_block: u64, pub dirty_since: BlockNumber, pub next_subscriber_page: Option, + pub next_subscriber_position: u32, + pub next_subscriber_branch: ObservationFanoutBranch, + pub retry_after: Option, pub previous_dirty_feed: Option, pub next_dirty_feed: Option, } diff --git a/template/pallets/actors/src/types/plan.rs b/template/pallets/actors/src/types/plan.rs new file mode 100644 index 00000000..2b834d11 --- /dev/null +++ b/template/pallets/actors/src/types/plan.rs @@ -0,0 +1,11 @@ +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct StepExecutionPlan { + pub identity: Identity, + pub hot: Hot, + pub run: Option, + pub funding: Funding, + pub admission: Admission, + pub ticket: Ticket, + pub loaded_step: LoadedStep, + pub maximum_fee: Fee, +} diff --git a/template/pallets/actors/src/types/resource.rs b/template/pallets/actors/src/types/resource.rs new file mode 100644 index 00000000..cbe5c86a --- /dev/null +++ b/template/pallets/actors/src/types/resource.rs @@ -0,0 +1,1523 @@ +use frame::prelude::*; +use polkadot_sdk::frame_support::weights::Weight; + +/// One authoritative block-resource domain. +#[derive( + Clone, Copy, Debug, Decode, DecodeWithMemTracking, Encode, Eq, MaxEncodedLen, PartialEq, TypeInfo, +)] +pub enum BlockResourceDomain { + ActorControl, + ActorBaseEffect, + UserDispatch, + ActorDrainEffect, +} + +/// One-way consensus phase for the current block's Economic Zipper. +#[derive( + Clone, Copy, Debug, Decode, DecodeWithMemTracking, Encode, Eq, MaxEncodedLen, PartialEq, TypeInfo, +)] +pub enum BlockResourcePhase { + ContextIncomplete, + PrepassExecuting, + ExternalPhase, + FreshDrain, + Finalizable, +} + +/// Transient authoritative state for exactly one block. +#[derive( + Clone, Copy, Debug, Decode, DecodeWithMemTracking, Encode, Eq, MaxEncodedLen, PartialEq, TypeInfo, +)] +pub struct BlockResourceState { + block_number: BlockNumber, + phase: BlockResourcePhase, + usage: BlockResourceUsage, + outstanding_reservations: u32, + finalized_fixed_reserved: Option, + optional_actor_work_halted: bool, +} + +/// Checked reference-runtime limits for Actor control and shared economic work. +#[derive( + Clone, Copy, Debug, Decode, DecodeWithMemTracking, Encode, Eq, MaxEncodedLen, PartialEq, TypeInfo, +)] +pub struct BlockResourceLimits { + actor_control: Weight, + shared_economic: Weight, + actor_base_turn: Weight, + user_base_turn: Weight, +} + +/// Immutable relationship between FRAME maximum, fixed/context reserve, and schedulable limits. +#[derive( + Clone, Copy, Debug, Decode, DecodeWithMemTracking, Encode, Eq, MaxEncodedLen, PartialEq, TypeInfo, +)] +pub struct BlockResourceBudget { + maximum_block: Weight, + fixed_envelope: Weight, + limits: BlockResourceLimits, +} + +/// Independently evidenced owners of the fixed/context envelope. The first component owns FRAME +/// overhead, every non-Actor initialization/finalization hook not already owned by the four +/// explicit components, and bounded non-economic maintenance such as XCMP lazy-migration `on_idle`. +#[derive( + Clone, Copy, Debug, Decode, DecodeWithMemTracking, Encode, Eq, MaxEncodedLen, PartialEq, TypeInfo, +)] +pub struct FixedBlockWeightComponents { + other_runtime_hooks_and_frame_base: Weight, + timestamp: Weight, + parachain_validation: Weight, + downward_messages: Weight, + horizontal_messages: Weight, +} + +/// Finite pre-execution bounds for variable-size parachain context messages. +#[derive( + Clone, Copy, Debug, Decode, DecodeWithMemTracking, Encode, Eq, MaxEncodedLen, PartialEq, TypeInfo, +)] +pub struct ContextMessageLimits { + downward_messages: u32, + horizontal_messages: u32, + horizontal_channels: u32, +} + +/// Count-only witness extracted from shared parachain inherent data. +#[derive( + Clone, Copy, Debug, Decode, DecodeWithMemTracking, Encode, Eq, MaxEncodedLen, PartialEq, TypeInfo, +)] +pub struct ContextMessageGeometry { + downward_full: u32, + downward_hashed: u32, + horizontal_full: u32, + horizontal_hashed: u32, + horizontal_channels: u32, +} + +/// Finalized read-only observation; it is never resource authority. +#[derive( + Clone, Copy, Debug, Decode, DecodeWithMemTracking, Encode, Eq, MaxEncodedLen, PartialEq, TypeInfo, +)] +pub struct FinalizedBlockResourceSnapshot { + block_number: BlockNumber, + fixed_reserved: Weight, + usage: BlockResourceUsage, + optional_actor_work_halted: bool, +} + +/// Current authoritative usage. Actor base and Drain effects share one counter. +#[derive( + Clone, + Copy, + Debug, + Decode, + DecodeWithMemTracking, + Default, + Encode, + Eq, + MaxEncodedLen, + PartialEq, + TypeInfo, +)] +pub struct BlockResourceUsage { + actor_control: Weight, + actor_effect: Weight, + user_dispatch: Weight, +} + +/// A pre-mutation maximum reservation that can be settled only in its owning domain. +#[derive(Debug, Decode, DecodeWithMemTracking, Encode, Eq, MaxEncodedLen, PartialEq, TypeInfo)] +pub struct BlockResourceReservation { + domain: BlockResourceDomain, + maximum: Weight, + phase: BlockResourcePhase, + settled: bool, +} + +/// Atomic paired reservation for one Actor Step's control and Task-effect maxima. +#[derive(Debug, Decode, DecodeWithMemTracking, Encode, Eq, MaxEncodedLen, PartialEq, TypeInfo)] +pub struct ActorStepResourceReservation { + control: BlockResourceReservation, + effect: BlockResourceReservation, +} + +#[derive( + Clone, Copy, Debug, Decode, DecodeWithMemTracking, Encode, Eq, MaxEncodedLen, PartialEq, TypeInfo, +)] +pub enum BlockResourceError { + InvalidLimits, + ArithmeticOverflow, + LimitExceeded, + ActualExceedsReserved, + ReservationAlreadySettled, + ReservationOutstanding, + InconsistentReservation, + FixedEnvelopeExceeded, + ReconciliationMismatch, + WrongBlock, + InvalidPhase, + ContextGeometryExceeded, + OptionalActorWorkHalted, +} + +impl ContextMessageLimits { + pub fn new(downward_messages: u32, horizontal_messages: u32, horizontal_channels: u32) -> Self { + Self { + downward_messages, + horizontal_messages, + horizontal_channels, + } + } + + pub fn validate(&self, geometry: ContextMessageGeometry) -> Result<(), BlockResourceError> { + let downward = geometry + .downward_full + .checked_add(geometry.downward_hashed) + .ok_or(BlockResourceError::ArithmeticOverflow)?; + let horizontal = geometry + .horizontal_full + .checked_add(geometry.horizontal_hashed) + .ok_or(BlockResourceError::ArithmeticOverflow)?; + if downward > self.downward_messages + || horizontal > self.horizontal_messages + || geometry.horizontal_channels > self.horizontal_channels + { + return Err(BlockResourceError::ContextGeometryExceeded); + } + Ok(()) + } +} + +impl ContextMessageGeometry { + pub fn new( + downward_full: u32, + downward_hashed: u32, + horizontal_full: u32, + horizontal_hashed: u32, + horizontal_channels: u32, + ) -> Self { + Self { + downward_full, + downward_hashed, + horizontal_full, + horizontal_hashed, + horizontal_channels, + } + } +} + +impl FinalizedBlockResourceSnapshot { + pub fn block_number(&self) -> BlockNumber { + self.block_number + } + + pub fn fixed_reserved(&self) -> Weight { + self.fixed_reserved + } + + pub fn usage(&self) -> BlockResourceUsage { + self.usage + } + + pub fn optional_actor_work_halted(&self) -> bool { + self.optional_actor_work_halted + } +} + +impl BlockResourceState { + pub fn new(block_number: BlockNumber) -> Self { + Self { + block_number, + phase: BlockResourcePhase::ContextIncomplete, + usage: BlockResourceUsage::default(), + outstanding_reservations: 0, + finalized_fixed_reserved: None, + optional_actor_work_halted: false, + } + } + + pub fn block_number(&self) -> BlockNumber { + self.block_number + } + + pub fn phase(&self) -> BlockResourcePhase { + self.phase + } + + pub fn usage(&self) -> BlockResourceUsage { + self.usage + } + + pub fn outstanding_reservations(&self) -> u32 { + self.outstanding_reservations + } + + pub fn optional_actor_work_halted(&self) -> bool { + self.optional_actor_work_halted + } + + pub fn ensure_block(&self, block_number: BlockNumber) -> Result<(), BlockResourceError> { + if self.block_number != block_number { + return Err(BlockResourceError::WrongBlock); + } + Ok(()) + } + + pub fn begin_prepass(&mut self) -> Result<(), BlockResourceError> { + self.advance( + BlockResourcePhase::ContextIncomplete, + BlockResourcePhase::PrepassExecuting, + ) + } + + pub fn open_external_phase(&mut self) -> Result<(), BlockResourceError> { + self.advance( + BlockResourcePhase::PrepassExecuting, + BlockResourcePhase::ExternalPhase, + ) + } + + pub fn begin_drain(&mut self) -> Result<(), BlockResourceError> { + self.advance( + BlockResourcePhase::ExternalPhase, + BlockResourcePhase::FreshDrain, + ) + } + + pub fn finish_drain( + &mut self, + budget: BlockResourceBudget, + fixed_reserved: Weight, + ) -> Result<(), BlockResourceError> { + if self.phase != BlockResourcePhase::FreshDrain { + return Err(BlockResourceError::InvalidPhase); + } + if self.outstanding_reservations != 0 { + return Err(BlockResourceError::ReservationOutstanding); + } + self.usage.reconcile_with_block(budget, fixed_reserved)?; + self.finalized_fixed_reserved = Some(fixed_reserved); + self.phase = BlockResourcePhase::Finalizable; + Ok(()) + } + + pub fn finalized_snapshot( + &self, + ) -> Result, BlockResourceError> { + if self.phase != BlockResourcePhase::Finalizable { + return Err(BlockResourceError::InvalidPhase); + } + let fixed_reserved = self + .finalized_fixed_reserved + .ok_or(BlockResourceError::ReconciliationMismatch)?; + Ok(FinalizedBlockResourceSnapshot { + block_number: self.block_number, + fixed_reserved, + usage: self.usage, + optional_actor_work_halted: self.optional_actor_work_halted, + }) + } + + pub fn halt_optional_actor_work(&mut self) { + self.optional_actor_work_halted = true; + } + + pub fn reserve( + &mut self, + limits: BlockResourceLimits, + domain: BlockResourceDomain, + maximum: Weight, + ) -> Result { + let actor_domain = domain != BlockResourceDomain::UserDispatch; + if actor_domain && self.optional_actor_work_halted { + return Err(BlockResourceError::OptionalActorWorkHalted); + } + let phase_allows = match domain { + BlockResourceDomain::ActorControl => matches!( + self.phase, + BlockResourcePhase::PrepassExecuting + | BlockResourcePhase::ExternalPhase + | BlockResourcePhase::FreshDrain + ), + BlockResourceDomain::ActorBaseEffect => self.phase == BlockResourcePhase::PrepassExecuting, + BlockResourceDomain::UserDispatch => self.phase == BlockResourcePhase::ExternalPhase, + BlockResourceDomain::ActorDrainEffect => self.phase == BlockResourcePhase::FreshDrain, + }; + if !phase_allows { + return Err(BlockResourceError::InvalidPhase); + } + let outstanding = match self.outstanding_reservations.checked_add(1) { + Some(outstanding) => outstanding, + None => { + if actor_domain { + self.optional_actor_work_halted = true; + } + return Err(BlockResourceError::ArithmeticOverflow); + } + }; + let mut reservation = match self.usage.reserve(limits, domain, maximum) { + Ok(reservation) => reservation, + Err(error) => { + if actor_domain && error == BlockResourceError::ArithmeticOverflow { + self.optional_actor_work_halted = true; + } + return Err(error); + } + }; + reservation.phase = self.phase; + self.outstanding_reservations = outstanding; + Ok(reservation) + } + + pub fn reserve_actor_step( + &mut self, + limits: BlockResourceLimits, + effect_domain: BlockResourceDomain, + control_maximum: Weight, + effect_maximum: Weight, + ) -> Result { + if !matches!( + effect_domain, + BlockResourceDomain::ActorBaseEffect | BlockResourceDomain::ActorDrainEffect + ) { + return Err(BlockResourceError::InvalidPhase); + } + let before = *self; + let control = match self.reserve(limits, BlockResourceDomain::ActorControl, control_maximum) { + Ok(reservation) => reservation, + Err(error) => return Err(error), + }; + let effect = match self.reserve(limits, effect_domain, effect_maximum) { + Ok(reservation) => reservation, + Err(error) => { + *self = before; + return Err(error); + } + }; + Ok(ActorStepResourceReservation { control, effect }) + } + + pub fn settle_actor_step( + &mut self, + reservation: &mut ActorStepResourceReservation, + actual_control: Weight, + actual_effect: Weight, + ) -> Result<(), BlockResourceError> { + let before = *self; + let control_settled = reservation.control.settled; + let effect_settled = reservation.effect.settled; + if let Err(error) = self + .settle(&mut reservation.control, actual_control) + .and_then(|()| self.settle(&mut reservation.effect, actual_effect)) + { + *self = before; + reservation.control.settled = control_settled; + reservation.effect.settled = effect_settled; + return Err(error); + } + Ok(()) + } + + pub fn settle( + &mut self, + reservation: &mut BlockResourceReservation, + actual: Weight, + ) -> Result<(), BlockResourceError> { + let actor_domain = reservation.domain != BlockResourceDomain::UserDispatch; + if reservation.phase != self.phase { + if actor_domain { + self.optional_actor_work_halted = true; + } + return Err(BlockResourceError::InvalidPhase); + } + let outstanding = match self.outstanding_reservations.checked_sub(1) { + Some(outstanding) => outstanding, + None => { + if actor_domain { + self.optional_actor_work_halted = true; + } + return Err(BlockResourceError::InconsistentReservation); + } + }; + if let Err(error) = self.usage.settle(reservation, actual) { + if actor_domain { + self.optional_actor_work_halted = true; + } + return Err(error); + } + self.outstanding_reservations = outstanding; + Ok(()) + } + + fn advance( + &mut self, + expected: BlockResourcePhase, + next: BlockResourcePhase, + ) -> Result<(), BlockResourceError> { + if self.phase != expected { + return Err(BlockResourceError::InvalidPhase); + } + if self.outstanding_reservations != 0 { + return Err(BlockResourceError::ReservationOutstanding); + } + self.phase = next; + Ok(()) + } +} + +impl FixedBlockWeightComponents { + pub fn new( + other_runtime_hooks_and_frame_base: Weight, + timestamp: Weight, + parachain_validation: Weight, + downward_messages: Weight, + horizontal_messages: Weight, + ) -> Self { + Self { + other_runtime_hooks_and_frame_base, + timestamp, + parachain_validation, + downward_messages, + horizontal_messages, + } + } + + pub fn total(&self) -> Result { + [ + self.other_runtime_hooks_and_frame_base, + self.timestamp, + self.parachain_validation, + self.downward_messages, + self.horizontal_messages, + ] + .into_iter() + .try_fold(Weight::zero(), |total, component| { + total + .checked_add(&component) + .ok_or(BlockResourceError::ArithmeticOverflow) + }) + } +} + +impl BlockResourceBudget { + /// Produces a zero-schedulable budget when runtime composition cannot be trusted. + pub fn fail_closed(maximum_block: Weight) -> Self { + Self { + maximum_block, + fixed_envelope: maximum_block, + limits: BlockResourceLimits { + actor_control: Weight::zero(), + shared_economic: Weight::zero(), + actor_base_turn: Weight::zero(), + user_base_turn: Weight::zero(), + }, + } + } + + pub fn from_fixed_components( + maximum_block: Weight, + components: FixedBlockWeightComponents, + ) -> Result { + Self::new(maximum_block, components.total()?) + } + + pub fn new(maximum_block: Weight, fixed_envelope: Weight) -> Result { + Self::new_with_control_ratio(maximum_block, fixed_envelope, 1, 5) + } + + /// Constructs one fixed candidate allocation without changing resource-domain semantics. + pub fn new_with_control_ratio( + maximum_block: Weight, + fixed_envelope: Weight, + control_numerator: u64, + control_denominator: u64, + ) -> Result { + let schedulable = maximum_block + .checked_sub(&fixed_envelope) + .ok_or(BlockResourceError::InvalidLimits)?; + Ok(Self { + maximum_block, + fixed_envelope, + limits: BlockResourceLimits::from_schedulable_ratio( + schedulable, + control_numerator, + control_denominator, + )?, + }) + } + + pub fn maximum_block(&self) -> Weight { + self.maximum_block + } + + pub fn fixed_envelope(&self) -> Weight { + self.fixed_envelope + } + + pub fn limits(&self) -> BlockResourceLimits { + self.limits + } +} + +impl BlockResourceLimits { + fn from_schedulable_ratio( + schedulable: Weight, + control_numerator: u64, + control_denominator: u64, + ) -> Result { + if control_denominator == 0 || control_numerator > control_denominator { + return Err(BlockResourceError::InvalidLimits); + } + let actor_control = weight_floor_mul_div(schedulable, control_numerator, control_denominator)?; + let shared_economic = schedulable + .checked_sub(&actor_control) + .ok_or(BlockResourceError::ArithmeticOverflow)?; + let actor_base_turn = weight_floor_div(shared_economic, 2); + let user_base_turn = shared_economic + .checked_sub(&actor_base_turn) + .ok_or(BlockResourceError::ArithmeticOverflow)?; + Ok(Self { + actor_control, + shared_economic, + actor_base_turn, + user_base_turn, + }) + } + + pub fn new( + actor_control: Weight, + shared_economic: Weight, + actor_base_turn: Weight, + user_base_turn: Weight, + ) -> Result { + actor_control + .checked_add(&shared_economic) + .ok_or(BlockResourceError::ArithmeticOverflow)?; + let expected_actor_base = weight_floor_div(shared_economic, 2); + let expected_user_base = shared_economic + .checked_sub(&expected_actor_base) + .ok_or(BlockResourceError::ArithmeticOverflow)?; + if actor_base_turn != expected_actor_base || user_base_turn != expected_user_base { + return Err(BlockResourceError::InvalidLimits); + } + Ok(Self { + actor_control, + shared_economic, + actor_base_turn, + user_base_turn, + }) + } + + pub fn actor_control(&self) -> Weight { + self.actor_control + } + + pub fn shared_economic(&self) -> Weight { + self.shared_economic + } + + pub fn actor_base_turn(&self) -> Weight { + self.actor_base_turn + } + + pub fn user_base_turn(&self) -> Weight { + self.user_base_turn + } +} + +fn weight_floor_div(value: Weight, denominator: u64) -> Weight { + Weight::from_parts( + value.ref_time() / denominator, + value.proof_size() / denominator, + ) +} + +fn weight_floor_mul_div( + value: Weight, + numerator: u64, + denominator: u64, +) -> Result { + let component = |value: u64| { + let quotient = value / denominator; + let remainder = value % denominator; + quotient + .checked_mul(numerator) + .and_then(|scaled| { + remainder + .checked_mul(numerator) + .map(|tail| (scaled, tail / denominator)) + }) + .and_then(|(scaled, tail)| scaled.checked_add(tail)) + .ok_or(BlockResourceError::ArithmeticOverflow) + }; + Ok(Weight::from_parts( + component(value.ref_time())?, + component(value.proof_size())?, + )) +} + +fn checked_add_with_limit( + current: Weight, + added: Weight, + limit: Weight, +) -> Result { + let next = current + .checked_add(&added) + .ok_or(BlockResourceError::ArithmeticOverflow)?; + if !next.all_lte(limit) { + return Err(BlockResourceError::LimitExceeded); + } + Ok(next) +} + +impl BlockResourceUsage { + fn reserve( + &mut self, + limits: BlockResourceLimits, + domain: BlockResourceDomain, + maximum: Weight, + ) -> Result { + match domain { + BlockResourceDomain::ActorControl => { + self.actor_control = + checked_add_with_limit(self.actor_control, maximum, limits.actor_control)?; + } + BlockResourceDomain::ActorBaseEffect => { + let next = checked_add_with_limit(self.actor_effect, maximum, limits.actor_base_turn)?; + checked_add_with_limit(next, self.user_dispatch, limits.shared_economic)?; + self.actor_effect = next; + } + BlockResourceDomain::UserDispatch => { + let next = self + .user_dispatch + .checked_add(&maximum) + .ok_or(BlockResourceError::ArithmeticOverflow)?; + checked_add_with_limit(next, self.actor_effect, limits.shared_economic)?; + self.user_dispatch = next; + } + BlockResourceDomain::ActorDrainEffect => { + let next = self + .actor_effect + .checked_add(&maximum) + .ok_or(BlockResourceError::ArithmeticOverflow)?; + checked_add_with_limit(next, self.user_dispatch, limits.shared_economic)?; + self.actor_effect = next; + } + } + Ok(BlockResourceReservation { + domain, + maximum, + phase: BlockResourcePhase::ContextIncomplete, + settled: false, + }) + } + + fn settle( + &mut self, + reservation: &mut BlockResourceReservation, + actual: Weight, + ) -> Result<(), BlockResourceError> { + if reservation.settled { + return Err(BlockResourceError::ReservationAlreadySettled); + } + if !actual.all_lte(reservation.maximum) { + return Err(BlockResourceError::ActualExceedsReserved); + } + let target = match reservation.domain { + BlockResourceDomain::ActorControl => &mut self.actor_control, + BlockResourceDomain::ActorBaseEffect | BlockResourceDomain::ActorDrainEffect => { + &mut self.actor_effect + } + BlockResourceDomain::UserDispatch => &mut self.user_dispatch, + }; + *target = target + .checked_sub(&reservation.maximum) + .and_then(|released| released.checked_add(&actual)) + .ok_or(BlockResourceError::ArithmeticOverflow)?; + reservation.settled = true; + Ok(()) + } + + pub fn actor_control_used(&self) -> Weight { + self.actor_control + } + + pub fn actor_effect_used(&self) -> Weight { + self.actor_effect + } + + pub fn user_dispatch_used(&self) -> Weight { + self.user_dispatch + } + + pub fn shared_used(&self) -> Result { + self + .actor_effect + .checked_add(&self.user_dispatch) + .ok_or(BlockResourceError::ArithmeticOverflow) + } + + pub fn reconcile_with_block( + &self, + budget: BlockResourceBudget, + fixed_reserved: Weight, + ) -> Result<(), BlockResourceError> { + if !fixed_reserved.all_lte(budget.fixed_envelope) { + return Err(BlockResourceError::FixedEnvelopeExceeded); + } + let shared = self.shared_used()?; + if !self.actor_control.all_lte(budget.limits.actor_control) + || !shared.all_lte(budget.limits.shared_economic) + { + return Err(BlockResourceError::ReconciliationMismatch); + } + let total = fixed_reserved + .checked_add(&self.actor_control) + .and_then(|used| used.checked_add(&shared)) + .ok_or(BlockResourceError::ArithmeticOverflow)?; + if !total.all_lte(budget.maximum_block) { + return Err(BlockResourceError::ReconciliationMismatch); + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn weight(ref_time: u64, proof_size: u64) -> Weight { + Weight::from_parts(ref_time, proof_size) + } + + fn limits() -> BlockResourceLimits { + BlockResourceLimits { + actor_control: weight(20, 20), + shared_economic: weight(80, 80), + actor_base_turn: weight(40, 40), + user_base_turn: weight(40, 40), + } + } + + fn retained_reservation( + result: Result, + domain: BlockResourceDomain, + maximum: Weight, + ) -> BlockResourceReservation { + assert!(result.is_ok()); + result.unwrap_or(BlockResourceReservation { + domain, + maximum, + phase: BlockResourcePhase::ContextIncomplete, + settled: false, + }) + } + + #[test] + fn actor_step_pair_reserves_and_settles_atomically_in_base_phase() { + let mut state = BlockResourceState::new(1u32); + assert_eq!(state.begin_prepass(), Ok(())); + let mut reservation = state + .reserve_actor_step( + limits(), + BlockResourceDomain::ActorBaseEffect, + weight(10, 10), + weight(30, 30), + ) + .expect("paired maxima fit their base domains"); // deos-bypass: panic-owner — explicit test limits dominate both maxima component-wise. + assert_eq!(state.outstanding_reservations(), 2); + assert_eq!(state.usage().actor_control_used(), weight(10, 10)); + assert_eq!(state.usage().actor_effect_used(), weight(30, 30)); + assert_eq!( + state.settle_actor_step(&mut reservation, weight(5, 5), weight(20, 20)), + Ok(()) + ); + assert_eq!(state.outstanding_reservations(), 0); + assert_eq!(state.usage().actor_control_used(), weight(5, 5)); + assert_eq!(state.usage().actor_effect_used(), weight(20, 20)); + } + + #[test] + fn actor_step_pair_rolls_back_partial_reserve_and_failed_settlement() { + let mut state = BlockResourceState::new(1u32); + assert_eq!(state.begin_prepass(), Ok(())); + let before = state; + assert_eq!( + state.reserve_actor_step( + limits(), + BlockResourceDomain::ActorBaseEffect, + weight(10, 10), + weight(41, 41), + ), + Err(BlockResourceError::LimitExceeded) + ); + assert_eq!(state, before); + + let mut reservation = state + .reserve_actor_step( + limits(), + BlockResourceDomain::ActorBaseEffect, + weight(10, 10), + weight(30, 30), + ) + .expect("paired maxima fit"); // deos-bypass: panic-owner — explicit test limits dominate both maxima component-wise. + let reserved = state; + assert_eq!( + state.settle_actor_step(&mut reservation, weight(5, 5), weight(31, 31)), + Err(BlockResourceError::ActualExceedsReserved) + ); + assert_eq!(state, reserved); + assert_eq!(state.outstanding_reservations(), 2); + assert_eq!( + state.settle_actor_step(&mut reservation, weight(5, 5), weight(20, 20)), + Ok(()) + ); + } + + #[test] + fn actor_step_pair_uses_drain_effect_only_in_fresh_drain() { + let mut state = BlockResourceState::new(1u32); + assert_eq!(state.begin_prepass(), Ok(())); + assert_eq!(state.open_external_phase(), Ok(())); + assert_eq!(state.begin_drain(), Ok(())); + assert!( + state + .reserve_actor_step( + limits(), + BlockResourceDomain::ActorDrainEffect, + weight(1, 1), + weight(1, 1), + ) + .is_ok() + ); + } + + #[test] + fn context_geometry_checks_full_hashed_and_channel_dimensions() { + let limits = ContextMessageLimits::new(512, 1_000, 128); + assert_eq!( + limits.validate(ContextMessageGeometry::new(256, 256, 500, 500, 128)), + Ok(()) + ); + for geometry in [ + ContextMessageGeometry::new(256, 257, 0, 0, 0), + ContextMessageGeometry::new(0, 0, 500, 501, 128), + ContextMessageGeometry::new(0, 0, 0, 0, 129), + ] { + assert_eq!( + limits.validate(geometry), + Err(BlockResourceError::ContextGeometryExceeded) + ); + } + assert_eq!( + limits.validate(ContextMessageGeometry::new(u32::MAX, 1, 0, 0, 0)), + Err(BlockResourceError::ArithmeticOverflow) + ); + } + + #[test] + fn block_phase_is_block_tagged_one_way_and_duplicate_safe() { + let mut state = BlockResourceState::new(7u64); + assert_eq!(state.block_number(), 7); + assert_eq!(state.phase(), BlockResourcePhase::ContextIncomplete); + assert_eq!(state.ensure_block(8), Err(BlockResourceError::WrongBlock)); + assert_eq!(state.begin_prepass(), Ok(())); + assert_eq!(state.begin_prepass(), Err(BlockResourceError::InvalidPhase)); + assert_eq!(state.begin_drain(), Err(BlockResourceError::InvalidPhase)); + assert_eq!(state.open_external_phase(), Ok(())); + assert_eq!(state.begin_drain(), Ok(())); + state.halt_optional_actor_work(); + assert!(state.optional_actor_work_halted()); + let budget = BlockResourceBudget::new(weight(110, 110), weight(10, 10)); + assert!(budget.is_ok()); + let budget = budget.unwrap_or(BlockResourceBudget { + maximum_block: Weight::zero(), + fixed_envelope: Weight::zero(), + limits: limits(), + }); + assert_eq!( + state.finalized_snapshot(), + Err(BlockResourceError::InvalidPhase) + ); + assert_eq!(state.finish_drain(budget, weight(10, 10)), Ok(())); + assert_eq!(state.phase(), BlockResourcePhase::Finalizable); + let snapshot = state.finalized_snapshot(); + assert!(snapshot.is_ok()); + let snapshot = snapshot.unwrap_or(FinalizedBlockResourceSnapshot { + block_number: 0, + fixed_reserved: Weight::zero(), + usage: BlockResourceUsage::default(), + optional_actor_work_halted: false, + }); + assert_eq!(snapshot.block_number(), 7); + assert_eq!(snapshot.fixed_reserved(), weight(10, 10)); + assert!(snapshot.optional_actor_work_halted()); + assert_eq!( + state.finish_drain(budget, weight(10, 10)), + Err(BlockResourceError::InvalidPhase) + ); + } + + #[test] + fn finalization_reconciliation_fails_closed_without_advancing_phase() { + let budget = BlockResourceBudget::new(weight(110, 110), weight(10, 10)); + assert!(budget.is_ok()); + let budget = budget.unwrap_or(BlockResourceBudget { + maximum_block: Weight::zero(), + fixed_envelope: Weight::zero(), + limits: limits(), + }); + let mut state = BlockResourceState::new(1u64); + state.phase = BlockResourcePhase::FreshDrain; + state.usage.actor_control = weight(21, 1); + assert_eq!( + state.finish_drain(budget, weight(1, 1)), + Err(BlockResourceError::ReconciliationMismatch) + ); + assert_eq!(state.phase(), BlockResourcePhase::FreshDrain); + } + + #[test] + fn phase_owner_admits_only_its_domains_and_halt_preserves_user_dispatch() { + let mut state = BlockResourceState::new(1u64); + assert_eq!( + state.reserve(limits(), BlockResourceDomain::UserDispatch, weight(1, 1)), + Err(BlockResourceError::InvalidPhase) + ); + assert_eq!(state.begin_prepass(), Ok(())); + let base = state.reserve(limits(), BlockResourceDomain::ActorBaseEffect, weight(1, 1)); + assert!(base.is_ok()); + let mut base = base.unwrap_or(BlockResourceReservation { + domain: BlockResourceDomain::ActorBaseEffect, + maximum: Weight::zero(), + phase: BlockResourcePhase::PrepassExecuting, + settled: false, + }); + assert_eq!(state.outstanding_reservations(), 1); + assert_eq!( + state.open_external_phase(), + Err(BlockResourceError::ReservationOutstanding) + ); + assert_eq!(state.settle(&mut base, weight(1, 1)), Ok(())); + assert_eq!(state.outstanding_reservations(), 0); + assert_eq!(state.open_external_phase(), Ok(())); + assert_eq!( + state.reserve(limits(), BlockResourceDomain::ActorBaseEffect, weight(1, 1),), + Err(BlockResourceError::InvalidPhase) + ); + state.halt_optional_actor_work(); + assert_eq!( + state.reserve(limits(), BlockResourceDomain::ActorControl, weight(1, 1)), + Err(BlockResourceError::OptionalActorWorkHalted) + ); + assert!( + state + .reserve(limits(), BlockResourceDomain::UserDispatch, weight(1, 1)) + .is_ok() + ); + } + + #[test] + fn candidate_control_ratios_preserve_exact_component_partitions() { + for (numerator, denominator, expected_control, expected_shared) in [ + (1, 5, 20, 80), + (1, 4, 25, 75), + (3, 10, 30, 70), + (1, 3, 33, 67), + ] { + let candidate = BlockResourceBudget::new_with_control_ratio( + weight(100, 100), + Weight::zero(), + numerator, + denominator, + ); + assert!(candidate.is_ok()); + let budget = candidate.unwrap_or_else(|_| BlockResourceBudget::fail_closed(weight(100, 100))); + assert_eq!( + budget.limits().actor_control(), + weight(expected_control, expected_control) + ); + assert_eq!( + budget.limits().shared_economic(), + weight(expected_shared, expected_shared) + ); + assert_eq!( + budget + .limits() + .actor_base_turn() + .checked_add(&budget.limits().user_base_turn()), + Some(budget.limits().shared_economic()) + ); + } + for (numerator, denominator) in [(1, 0), (2, 1)] { + assert_eq!( + BlockResourceBudget::new_with_control_ratio( + weight(100, 100), + Weight::zero(), + numerator, + denominator, + ), + Err(BlockResourceError::InvalidLimits) + ); + } + } + + #[test] + fn candidate_ratios_preserve_saturated_base_turns_and_forward_borrowing() { + for (numerator, denominator) in [(1, 5), (1, 4), (3, 10), (1, 3)] { + let budget = BlockResourceBudget::new_with_control_ratio( + weight(100, 100), + Weight::zero(), + numerator, + denominator, + ) + .unwrap_or_else(|_| BlockResourceBudget::fail_closed(weight(100, 100))); + let limits = budget.limits(); + let mut saturated = BlockResourceUsage::default(); + assert!( + saturated + .reserve( + limits, + BlockResourceDomain::ActorBaseEffect, + limits.actor_base_turn(), + ) + .is_ok() + ); + assert!( + saturated + .reserve( + limits, + BlockResourceDomain::UserDispatch, + limits.user_base_turn(), + ) + .is_ok() + ); + assert_eq!(saturated.shared_used(), Ok(limits.shared_economic())); + + let mut borrowed = BlockResourceUsage::default(); + let actor_actual = weight_floor_div(limits.actor_base_turn(), 2); + let mut actor = borrowed + .reserve( + limits, + BlockResourceDomain::ActorBaseEffect, + limits.actor_base_turn(), + ) + .unwrap_or(BlockResourceReservation { + domain: BlockResourceDomain::ActorBaseEffect, + maximum: Weight::zero(), + phase: BlockResourcePhase::ContextIncomplete, + settled: true, + }); + assert_eq!(borrowed.settle(&mut actor, actor_actual), Ok(())); + let user_remainder = limits.shared_economic().saturating_sub(actor_actual); + assert!( + borrowed + .reserve(limits, BlockResourceDomain::UserDispatch, user_remainder,) + .is_ok() + ); + assert_eq!(borrowed.shared_used(), Ok(limits.shared_economic())); + } + } + + #[test] + fn limits_follow_floor_and_remainder_ownership_exactly() { + assert_eq!( + BlockResourceLimits::new( + weight(20, 20), + weight(80, 80), + weight(40, 40), + weight(40, 40), + ), + Ok(limits()) + ); + assert_eq!( + BlockResourceLimits::new( + weight(20, 21), + weight(83, 86), + weight(41, 43), + weight(42, 43), + ), + Ok(BlockResourceLimits { + actor_control: weight(20, 21), + shared_economic: weight(83, 86), + actor_base_turn: weight(41, 43), + user_base_turn: weight(42, 43), + }) + ); + assert_eq!( + BlockResourceLimits::new( + weight(30, 30), + weight(70, 70), + weight(35, 35), + weight(35, 35), + ), + Ok(BlockResourceLimits { + actor_control: weight(30, 30), + shared_economic: weight(70, 70), + actor_base_turn: weight(35, 35), + user_base_turn: weight(35, 35), + }) + ); + assert_eq!( + BlockResourceLimits::new( + weight(20, 20), + weight(80, 80), + weight(39, 40), + weight(41, 40), + ), + Err(BlockResourceError::InvalidLimits) + ); + } + + #[test] + fn actor_base_cannot_borrow_user_turn_in_either_dimension() { + for maximum in [weight(41, 1), weight(1, 41)] { + let mut usage = BlockResourceUsage::default(); + assert_eq!( + usage.reserve(limits(), BlockResourceDomain::ActorBaseEffect, maximum), + Err(BlockResourceError::LimitExceeded) + ); + assert_eq!(usage, BlockResourceUsage::default()); + } + } + + #[test] + fn user_and_drain_borrow_only_actual_shared_remainder() { + let mut usage = BlockResourceUsage::default(); + let mut actor = retained_reservation( + usage.reserve( + limits(), + BlockResourceDomain::ActorBaseEffect, + weight(10, 10), + ), + BlockResourceDomain::ActorBaseEffect, + weight(10, 10), + ); + assert_eq!(usage.settle(&mut actor, weight(8, 8)), Ok(())); + let _user = retained_reservation( + usage.reserve(limits(), BlockResourceDomain::UserDispatch, weight(60, 60)), + BlockResourceDomain::UserDispatch, + weight(60, 60), + ); + assert_eq!( + usage.reserve( + limits(), + BlockResourceDomain::ActorDrainEffect, + weight(13, 1) + ), + Err(BlockResourceError::LimitExceeded) + ); + let _drain = retained_reservation( + usage.reserve( + limits(), + BlockResourceDomain::ActorDrainEffect, + weight(12, 12), + ), + BlockResourceDomain::ActorDrainEffect, + weight(12, 12), + ); + assert_eq!(usage.shared_used(), Ok(weight(80, 80))); + } + + #[test] + fn reservation_settles_only_valid_component_wise_actual() { + let mut usage = BlockResourceUsage::default(); + let mut reservation = retained_reservation( + usage.reserve(limits(), BlockResourceDomain::ActorControl, weight(10, 10)), + BlockResourceDomain::ActorControl, + weight(10, 10), + ); + assert_eq!( + usage.settle(&mut reservation, weight(11, 9)), + Err(BlockResourceError::ActualExceedsReserved) + ); + assert_eq!(usage.actor_control, weight(10, 10)); + assert_eq!(usage.settle(&mut reservation, weight(7, 8)), Ok(())); + assert_eq!(usage.actor_control, weight(7, 8)); + assert_eq!( + usage.settle(&mut reservation, weight(7, 8)), + Err(BlockResourceError::ReservationAlreadySettled) + ); + } + + #[test] + fn small_weight_grid_preserves_partition_and_component_admission() { + for ref_time in 0..=12 { + for proof_size in 0..=12 { + let schedulable = weight(ref_time, proof_size); + let budget = BlockResourceBudget::new(schedulable, Weight::zero()); + assert!(budget.is_ok()); + let budget = budget.unwrap_or(BlockResourceBudget { + maximum_block: Weight::zero(), + fixed_envelope: Weight::zero(), + limits: BlockResourceLimits::from_schedulable_ratio(Weight::zero(), 1, 5) + .unwrap_or(limits()), + }); + let limits = budget.limits(); + assert_eq!( + limits + .actor_control() + .checked_add(&limits.shared_economic()), + Some(schedulable) + ); + assert_eq!( + limits + .actor_base_turn() + .checked_add(&limits.user_base_turn()), + Some(limits.shared_economic()) + ); + + for candidate_ref in 0..=ref_time.saturating_add(1) { + for candidate_proof in 0..=proof_size.saturating_add(1) { + let candidate = weight(candidate_ref, candidate_proof); + let mut usage = BlockResourceUsage::default(); + assert_eq!( + usage + .reserve(limits, BlockResourceDomain::ActorBaseEffect, candidate) + .is_ok(), + candidate.all_lte(limits.actor_base_turn()) + ); + } + } + } + } + } + + #[test] + fn shared_envelope_is_work_conserving_after_actor_and_user_turns() { + let limits = limits(); + for actor_ref in (0..=40).step_by(10) { + for actor_proof in (0..=40).step_by(10) { + let actor = weight(actor_ref, actor_proof); + for user_ref in (0..=80).step_by(10) { + for user_proof in (0..=80).step_by(10) { + let user = weight(user_ref, user_proof); + let mut usage = BlockResourceUsage::default(); + assert!( + usage + .reserve(limits, BlockResourceDomain::ActorBaseEffect, actor) + .is_ok() + ); + let expected_user = actor + .checked_add(&user) + .map(|total| total.all_lte(limits.shared_economic())) + .unwrap_or(false); + let user_result = usage.reserve(limits, BlockResourceDomain::UserDispatch, user); + assert_eq!(user_result.is_ok(), expected_user); + if !expected_user { + continue; + } + let remainder = limits + .shared_economic() + .checked_sub(&actor) + .and_then(|available| available.checked_sub(&user)); + assert!(remainder.is_some()); + let remainder = remainder.unwrap_or(Weight::zero()); + let before_drain = usage; + assert!( + usage + .reserve(limits, BlockResourceDomain::ActorDrainEffect, remainder,) + .is_ok() + ); + assert_eq!(usage.shared_used(), Ok(limits.shared_economic())); + + let mut ref_overrun = before_drain; + assert_eq!( + ref_overrun.reserve( + limits, + BlockResourceDomain::ActorDrainEffect, + remainder.saturating_add(weight(1, 0)), + ), + Err(BlockResourceError::LimitExceeded) + ); + let mut proof_overrun = before_drain; + assert_eq!( + proof_overrun.reserve( + limits, + BlockResourceDomain::ActorDrainEffect, + remainder.saturating_add(weight(0, 1)), + ), + Err(BlockResourceError::LimitExceeded) + ); + } + } + } + } + } + + #[test] + fn untrustworthy_actor_actual_halts_actor_work_but_user_failure_does_not() { + let mut actor_state = BlockResourceState::new(1u64); + assert_eq!(actor_state.begin_prepass(), Ok(())); + let actor = actor_state.reserve(limits(), BlockResourceDomain::ActorControl, weight(1, 1)); + assert!(actor.is_ok()); + let mut actor = actor.unwrap_or(BlockResourceReservation { + domain: BlockResourceDomain::ActorControl, + maximum: Weight::zero(), + phase: BlockResourcePhase::PrepassExecuting, + settled: false, + }); + assert_eq!( + actor_state.settle(&mut actor, weight(2, 1)), + Err(BlockResourceError::ActualExceedsReserved) + ); + assert!(actor_state.optional_actor_work_halted()); + + let mut user_state = BlockResourceState::new(1u64); + assert_eq!(user_state.begin_prepass(), Ok(())); + assert_eq!(user_state.open_external_phase(), Ok(())); + let user = user_state.reserve(limits(), BlockResourceDomain::UserDispatch, weight(1, 1)); + assert!(user.is_ok()); + let mut user = user.unwrap_or(BlockResourceReservation { + domain: BlockResourceDomain::UserDispatch, + maximum: Weight::zero(), + phase: BlockResourcePhase::ExternalPhase, + settled: false, + }); + assert_eq!( + user_state.settle(&mut user, weight(1, 2)), + Err(BlockResourceError::ActualExceedsReserved) + ); + assert!(!user_state.optional_actor_work_halted()); + } + + #[test] + fn settlement_grid_accepts_exactly_component_wise_actuals() { + for maximum_ref in 0..=4 { + for maximum_proof in 0..=4 { + for actual_ref in 0..=5 { + for actual_proof in 0..=5 { + let maximum = weight(maximum_ref, maximum_proof); + let actual = weight(actual_ref, actual_proof); + let mut state = BlockResourceState::new(1u64); + assert_eq!(state.begin_prepass(), Ok(())); + let reservation = state.reserve(limits(), BlockResourceDomain::ActorControl, maximum); + assert!(reservation.is_ok()); + let mut reservation = reservation.unwrap_or(BlockResourceReservation { + domain: BlockResourceDomain::ActorControl, + maximum, + phase: BlockResourcePhase::PrepassExecuting, + settled: false, + }); + assert_eq!( + state.settle(&mut reservation, actual).is_ok(), + actual.all_lte(maximum) + ); + } + } + } + } + } + + #[test] + fn arithmetic_overflow_is_not_reported_as_capacity_pressure() { + let mut usage = BlockResourceUsage { + actor_control: Weight::MAX, + ..Default::default() + }; + assert_eq!( + usage.reserve(limits(), BlockResourceDomain::ActorControl, weight(1, 0)), + Err(BlockResourceError::ArithmeticOverflow) + ); + } + + #[test] + fn fixed_components_are_complete_checked_and_feed_the_budget() { + let components = FixedBlockWeightComponents::new( + weight(1, 2), + weight(3, 4), + weight(5, 6), + weight(7, 8), + weight(9, 10), + ); + assert_eq!(components.total(), Ok(weight(25, 30))); + assert_eq!( + BlockResourceBudget::from_fixed_components(weight(125, 130), components), + BlockResourceBudget::new(weight(125, 130), weight(25, 30)) + ); + let overflowing = FixedBlockWeightComponents::new( + Weight::MAX, + weight(1, 0), + Weight::zero(), + Weight::zero(), + Weight::zero(), + ); + assert_eq!( + overflowing.total(), + Err(BlockResourceError::ArithmeticOverflow) + ); + } + + #[test] + fn whole_block_fixed_envelope_cannot_admit_a_mandatory_prepass() { + let budget = BlockResourceBudget::new(weight(100, 100), weight(100, 100)); + assert!(budget.is_ok()); + let budget = budget.unwrap_or(BlockResourceBudget { + maximum_block: Weight::zero(), + fixed_envelope: Weight::zero(), + limits: limits(), + }); + assert_eq!(budget.limits().actor_control(), Weight::zero()); + let mut state = BlockResourceState::new(1u64); + assert_eq!(state.begin_prepass(), Ok(())); + assert_eq!( + state.reserve( + budget.limits(), + BlockResourceDomain::ActorControl, + weight(1, 1), + ), + Err(BlockResourceError::LimitExceeded) + ); + } + + #[test] + fn budget_derives_schedulable_limits_and_rejects_component_underflow() { + let budget = BlockResourceBudget::new(weight(110, 210), weight(10, 10)); + assert_eq!( + budget, + Ok(BlockResourceBudget { + maximum_block: weight(110, 210), + fixed_envelope: weight(10, 10), + limits: BlockResourceLimits { + actor_control: weight(20, 40), + shared_economic: weight(80, 160), + actor_base_turn: weight(40, 80), + user_base_turn: weight(40, 80), + }, + }) + ); + assert_eq!( + BlockResourceBudget::new(weight(110, 9), weight(10, 10)), + Err(BlockResourceError::InvalidLimits) + ); + } + + #[test] + fn block_reconciliation_rejects_fixed_overrun_and_corrupt_usage() { + let budget = BlockResourceBudget::new(weight(110, 110), weight(10, 10)); + assert!(budget.is_ok()); + let budget = budget.unwrap_or(BlockResourceBudget { + maximum_block: Weight::zero(), + fixed_envelope: Weight::zero(), + limits: limits(), + }); + let usage = BlockResourceUsage { + actor_control: weight(20, 10), + actor_effect: weight(30, 40), + user_dispatch: weight(40, 30), + }; + assert_eq!(usage.reconcile_with_block(budget, weight(10, 10)), Ok(())); + assert_eq!( + usage.reconcile_with_block(budget, weight(11, 10)), + Err(BlockResourceError::FixedEnvelopeExceeded) + ); + let corrupt = BlockResourceUsage { + actor_control: weight(21, 10), + ..usage + }; + assert_eq!( + corrupt.reconcile_with_block(budget, weight(1, 1)), + Err(BlockResourceError::ReconciliationMismatch) + ); + } +} diff --git a/template/pallets/actors/src/types/scheduler.rs b/template/pallets/actors/src/types/scheduler.rs index 87fc8bbb..1c4e8b03 100644 --- a/template/pallets/actors/src/types/scheduler.rs +++ b/template/pallets/actors/src/types/scheduler.rs @@ -1,4 +1,4 @@ -use super::lifecycle::ActorId; +use super::{contract::ActorContractCommitment, lifecycle::ActorId}; use frame::prelude::*; pub type QueueTicket = u64; @@ -196,6 +196,15 @@ pub struct WakeupPointer { pub slot: WakeupSlot, } +#[derive( + Clone, Copy, Debug, Decode, DecodeWithMemTracking, Encode, Eq, PartialEq, TypeInfo, MaxEncodedLen, +)] +pub struct TriggerWakeupPointer { + pub tick: SchedulerTick, + pub page_id: WakeupPageId, + pub slot: WakeupSlot, +} + #[derive( Clone, Copy, Debug, Decode, DecodeWithMemTracking, Encode, Eq, PartialEq, TypeInfo, MaxEncodedLen, )] @@ -225,12 +234,39 @@ pub struct WakeupBucketState { pub cursor_index: Option, } +pub type QueueEntry = ActorStepTicket>; + #[derive( Clone, Copy, Debug, Decode, DecodeWithMemTracking, Encode, Eq, PartialEq, TypeInfo, MaxEncodedLen, )] -pub struct QueueEntry { - pub ticket: QueueTicket, +pub struct ActorStepTicket { pub actor_id: ActorId, + pub cycle_nonce: u64, + pub cursor: u32, + pub ticket: QueueTicket, + pub eligible_at: BlockNumber, + pub contract_commitment: ContractCommitment, +} + +impl + ActorStepTicket +{ + pub fn matches( + &self, + actor_id: ActorId, + cycle_nonce: u64, + cursor: u32, + ticket: QueueTicket, + eligible_at: &BlockNumber, + contract_commitment: &ContractCommitment, + ) -> bool { + self.actor_id == actor_id + && self.cycle_nonce == cycle_nonce + && self.cursor == cursor + && self.ticket == ticket + && &self.eligible_at == eligible_at + && &self.contract_commitment == contract_commitment + } } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] diff --git a/template/pallets/actors/src/weights.rs b/template/pallets/actors/src/weights.rs index e355701e..70082061 100644 --- a/template/pallets/actors/src/weights.rs +++ b/template/pallets/actors/src/weights.rs @@ -31,9 +31,22 @@ pub trait WeightInfo { fn pause_actor() -> Weight; fn resume_actor() -> Weight; fn manual_trigger() -> Weight; + fn address_event_trigger_occurrence() -> Weight; + fn observation_change_trigger_occurrence() -> Weight; + fn observation_crossing_trigger_occurrence() -> Weight; + fn at_time_trigger_occurrence() -> Weight; + fn cadenced_trigger_occurrence() -> Weight; fn observation_change_ingress() -> Weight; fn observation_fanout_base() -> Weight; + fn observation_fanout_branch_probe() -> Weight; fn observation_fanout_page() -> Weight; + fn observation_fanout_wakeup_page() -> Weight; + fn observation_fanout_coalesced_page() -> Weight; + fn observation_fanout_blocked_page() -> Weight; + fn observation_fanout_terminal() -> Weight; + fn record_crossing_worker_fault() -> Weight; + fn record_observation_fanout_worker_fault() -> Weight; + fn record_wakeup_worker_fault() -> Weight; fn crossing_worker_base() -> Weight { Weight::from_parts(25_000_000, 8_000) } fn crossing_work_probe() -> Weight { Weight::from_parts(400_000_000, 20_000) } fn crossing_search_probe() -> Weight { Weight::from_parts(400_000_000, 100_000) } @@ -77,6 +90,7 @@ pub trait WeightInfo { fn crossing_skip_unit() -> Weight { Weight::from_parts(500_000_000, 180_000) } fn crossing_skip_pair_unit() -> Weight { Weight::from_parts(750_000_000, 250_000) } fn crossing_actor_unit() -> Weight { Weight::from_parts(750_000_000, 250_000) } + fn pipeline_admission_apoptosis() -> Weight; fn close_actor() -> Weight; fn fee_collection() -> Weight; fn predicate_set_evaluation(predicates: u32) -> Weight; @@ -95,6 +109,17 @@ pub trait WeightInfo { fn task_unstake() -> Weight; fn task_dex_exact_in() -> Weight; fn task_dex_exact_out() -> Weight; + fn contract_geometry_create(chunks: u32) -> Weight; + fn contract_geometry_close(chunks: u32) -> Weight; + fn contract_geometry_reconstruct(chunks: u32) -> Weight; + fn current_step_load_head() -> Weight; + fn current_step_load_tail(steps_in_chunk: u32) -> Weight; + fn current_step_plan_opening_head() -> Weight; + fn current_step_plan_suspended_head() -> Weight; + fn current_step_plan_running_tail(steps_in_chunk: u32) -> Weight; + fn opening_snapshot_capture(entries: u32) -> Weight; + fn opening_predicate_capture(predicates: u32) -> Weight; + fn scheduler_on_initialize_cutoff() -> Weight; fn scheduler_on_idle_base() -> Weight; fn materialization_coordinator_base() -> Weight; fn scheduler_paged_append_existing_page() -> Weight; @@ -117,17 +142,68 @@ pub trait WeightInfo { fn scheduler_paged_consume_delete_page() -> Weight; fn scheduler_paged_tombstone_drain(entries: u32) -> Weight; fn scheduler_paged_mixed_scan(entries: u32) -> Weight; + fn scheduler_inner_zero_step_complete() -> Weight; + fn scheduler_paged_execute_opening_max() -> Weight; + fn scheduler_inner_opening_close_min(tail_chunks: u32) -> Weight; + fn scheduler_inner_opening_close_max(tail_chunks: u32) -> Weight; + fn scheduler_inner_opening_failed_min(tail_chunks: u32) -> Weight; + fn scheduler_inner_opening_failed_max(tail_chunks: u32) -> Weight; + fn scheduler_inner_opening_retry_min(tail_chunks: u32) -> Weight; + fn scheduler_inner_opening_retry_max(tail_chunks: u32) -> Weight; + fn scheduler_inner_opening_complete_min(tail_chunks: u32) -> Weight; + fn scheduler_inner_opening_complete_max(tail_chunks: u32) -> Weight; + fn scheduler_inner_opening_progress_min(tail_chunks: u32) -> Weight; + fn scheduler_inner_opening_progress_max(tail_chunks: u32) -> Weight; + fn scheduler_inner_running_complete(steps_in_fragment: u32, predicates: u32) -> Weight; + fn scheduler_inner_running_progress(steps_in_fragment: u32, predicates: u32) -> Weight; + fn scheduler_inner_suspended_tail_retry(steps_in_fragment: u32, predicates: u32) -> Weight; + fn scheduler_inner_suspended_tail_complete(steps_in_fragment: u32, predicates: u32) -> Weight; + fn scheduler_inner_suspended_tail_progress(steps_in_fragment: u32, predicates: u32) -> Weight; + fn scheduler_inner_suspended_head_retry( + opening_snapshot_entries: u32, + opening_predicate_results: u32, + funding_snapshot_entries: u32, + predicates: u32, + ) -> Weight; + fn scheduler_inner_suspended_head_complete( + opening_snapshot_entries: u32, + opening_predicate_results: u32, + funding_snapshot_entries: u32, + predicates: u32, + ) -> Weight; + fn scheduler_inner_suspended_head_progress( + opening_snapshot_entries: u32, + opening_predicate_results: u32, + funding_snapshot_entries: u32, + predicates: u32, + ) -> Weight; + fn scheduler_inner_suspended_head_opening_retry( + opening_snapshot_entries: u32, + opening_predicate_results: u32, + funding_snapshot_entries: u32, + ) -> Weight; + fn scheduler_inner_suspended_head_opening_complete( + opening_snapshot_entries: u32, + opening_predicate_results: u32, + funding_snapshot_entries: u32, + ) -> Weight; + fn scheduler_inner_suspended_head_opening_progress( + opening_snapshot_entries: u32, + opening_predicate_results: u32, + funding_snapshot_entries: u32, + ) -> Weight; fn scheduler_paged_execute_cheap(executions: u32) -> Weight; fn scheduler_paged_execute_cheap_mixed(executions: u32) -> Weight; fn scheduler_actor_state_probe() -> Weight; fn transaction_extension_ingress_base() -> Weight; fn transaction_extension_ingress_notify() -> Weight; fn funding_snapshot_open(assets: u32) -> Weight; - fn continuation_suspend(snapshot_entries: u32) -> Weight; - fn continuation_retry() -> Weight; - fn continuation_complete() -> Weight; - fn continuation_cancel() -> Weight; - fn continuation_suffix_admission(steps: u32) -> Weight; + fn run_progress() -> Weight; + fn run_suspend() -> Weight; + fn run_retry() -> Weight; + fn run_complete() -> Weight; + fn run_cancel() -> Weight; + fn run_suffix_admission(steps: u32) -> Weight; fn update_contract() -> Weight; fn set_global_circuit_breaker() -> Weight; fn clear_crossing_worker_fault() -> Weight; @@ -136,6 +212,10 @@ pub trait WeightInfo { fn set_active_actor_limit() -> Weight; fn permissionless_sweep() -> Weight; fn permissionless_sweep_many(batch: u32) -> Weight; + fn maximum_context_inherent() -> Weight; + fn maximum_xcm_version_discovery() -> Weight; + fn block_resource_meter_extension() -> Weight; + fn block_resource_finalize() -> Weight; } pub struct SubstrateWeight(PhantomData); @@ -193,9 +273,39 @@ impl WeightInfo for Subst } fn manual_trigger() -> Weight { - Weight::from_parts(12_000_000, 1200) - .saturating_add(T::DbWeight::get().reads(1)) - .saturating_add(T::DbWeight::get().writes(2)) + Weight::from_parts(113_494_000, 9_635) + .saturating_add(T::DbWeight::get().reads(13)) + .saturating_add(T::DbWeight::get().writes(5)) + } + + fn address_event_trigger_occurrence() -> Weight { + Weight::from_parts(169_927_000, 8_366) + .saturating_add(T::DbWeight::get().reads(14)) + .saturating_add(T::DbWeight::get().writes(7)) + } + + fn observation_change_trigger_occurrence() -> Weight { + Weight::from_parts(117_615_000, 8_295) + .saturating_add(T::DbWeight::get().reads(13)) + .saturating_add(T::DbWeight::get().writes(7)) + } + + fn observation_crossing_trigger_occurrence() -> Weight { + Weight::from_parts(499_862_000, 164_106) + .saturating_add(T::DbWeight::get().reads(89)) + .saturating_add(T::DbWeight::get().writes(80)) + } + + fn at_time_trigger_occurrence() -> Weight { + Weight::from_parts(157_425_000, 8_317) + .saturating_add(T::DbWeight::get().reads(14)) + .saturating_add(T::DbWeight::get().writes(7)) + } + + fn cadenced_trigger_occurrence() -> Weight { + Weight::from_parts(195_070_000, 8_325) + .saturating_add(T::DbWeight::get().reads(17)) + .saturating_add(T::DbWeight::get().writes(11)) } fn observation_change_ingress() -> Weight { @@ -206,10 +316,51 @@ impl WeightInfo for Subst Weight::from_parts(15_000_000, 4_000) } + fn observation_fanout_branch_probe() -> Weight { + Weight::from_parts(20_000_000, 6_000) + } + fn observation_fanout_page() -> Weight { + Weight::from_parts(150_000_000_000, 750_000) + } + + fn observation_fanout_wakeup_page() -> Weight { + Weight::from_parts(8_000_000_000, 750_000) + } + + fn observation_fanout_coalesced_page() -> Weight { Weight::from_parts(8_000_000_000, 750_000) } + fn observation_fanout_blocked_page() -> Weight { + Weight::from_parts(150_000_000_000, 400_000) + } + + fn observation_fanout_terminal() -> Weight { + Weight::from_parts(8_000_000_000, 750_000) + } + + fn record_crossing_worker_fault() -> Weight { + Weight::from_parts(16_000_000, 1_529) + .saturating_add(T::DbWeight::get().reads_writes(1, 1)) + } + + fn record_observation_fanout_worker_fault() -> Weight { + Weight::from_parts(16_000_000, 1_529) + .saturating_add(T::DbWeight::get().reads_writes(1, 1)) + } + + fn record_wakeup_worker_fault() -> Weight { + Weight::from_parts(16_000_000, 1_529) + .saturating_add(T::DbWeight::get().reads_writes(1, 1)) + } + + fn pipeline_admission_apoptosis() -> Weight { + Weight::from_parts(161_616_000, 5_736) + .saturating_add(T::DbWeight::get().reads(15)) + .saturating_add(T::DbWeight::get().writes(15)) + } + fn close_actor() -> Weight { Weight::from_parts(84_719_000, 8_120) .saturating_add(T::DbWeight::get().reads(8)) @@ -314,6 +465,78 @@ impl WeightInfo for Subst .saturating_add(T::DbWeight::get().reads_writes(64, 12)) } + fn contract_geometry_create(chunks: u32) -> Weight { + Weight::from_parts(100_000_000, 16_000) + .saturating_add(Weight::from_parts(25_000_000, 8_000).saturating_mul(chunks.into())) + .saturating_add(T::DbWeight::get().reads_writes( + u64::from(2u32.saturating_add(chunks)), + u64::from(2u32.saturating_add(chunks)), + )) + } + + fn contract_geometry_close(chunks: u32) -> Weight { + Weight::from_parts(100_000_000, 16_000) + .saturating_add(Weight::from_parts(25_000_000, 8_000).saturating_mul(chunks.into())) + .saturating_add(T::DbWeight::get().reads_writes( + u64::from(2u32.saturating_add(chunks)), + u64::from(2u32.saturating_add(chunks)), + )) + } + + fn contract_geometry_reconstruct(chunks: u32) -> Weight { + Weight::from_parts(75_000_000, 16_000) + .saturating_add(Weight::from_parts(20_000_000, 8_000).saturating_mul(chunks.into())) + .saturating_add(T::DbWeight::get().reads(u64::from( + 2u32.saturating_add(chunks), + ))) + } + + fn current_step_load_head() -> Weight { + Weight::from_parts(30_000_000, 8_000).saturating_add(T::DbWeight::get().reads(2)) + } + + fn current_step_load_tail(steps_in_chunk: u32) -> Weight { + Weight::from_parts(40_000_000, 16_000) + .saturating_add( + Weight::from_parts(1_000_000, 512).saturating_mul(steps_in_chunk.into()), + ) + .saturating_add(T::DbWeight::get().reads(3)) + } + + fn current_step_plan_opening_head() -> Weight { + Weight::from_parts(100_000_000, 24_000).saturating_add(T::DbWeight::get().reads(8)) + } + + fn current_step_plan_suspended_head() -> Weight { + Weight::from_parts(150_000_000, 32_000).saturating_add(T::DbWeight::get().reads(8)) + } + + fn current_step_plan_running_tail(steps_in_chunk: u32) -> Weight { + Weight::from_parts(150_000_000, 32_000) + .saturating_add( + Weight::from_parts(1_000_000, 512).saturating_mul(steps_in_chunk.into()), + ) + .saturating_add(T::DbWeight::get().reads(10)) + } + + fn opening_snapshot_capture(entries: u32) -> Weight { + Weight::from_parts(25_000_000, 4_000) + .saturating_add(Weight::from_parts(15_000_000, 3_000).saturating_mul(entries.into())) + .saturating_add(T::DbWeight::get().reads(u64::from(entries))) + } + + fn opening_predicate_capture(predicates: u32) -> Weight { + Weight::from_parts(25_000_000, 4_000) + .saturating_add(Weight::from_parts(15_000_000, 3_000).saturating_mul(predicates.into())) + .saturating_add(T::DbWeight::get().reads(u64::from(predicates))) + } + + fn scheduler_on_initialize_cutoff() -> Weight { + Weight::from_parts(7_543_000, 1_493) + .saturating_add(T::DbWeight::get().reads(1)) + .saturating_add(T::DbWeight::get().writes(1)) + } + fn scheduler_on_idle_base() -> Weight { Weight::from_parts(25_000_000, 2_500) .saturating_add(T::DbWeight::get().reads(7)) @@ -416,6 +639,98 @@ impl WeightInfo for Subst )) } + fn scheduler_inner_zero_step_complete() -> Weight { + Weight::from_parts(37_645_000, 4_570) + } + + fn scheduler_paged_execute_opening_max() -> Weight { + Weight::from_parts(20_000_000_000, 600_000) + } + + fn scheduler_inner_opening_close_min(_: u32) -> Weight { + Weight::from_parts(20_000_000_000, 600_000) + } + + fn scheduler_inner_opening_close_max(_: u32) -> Weight { + Weight::from_parts(20_000_000_000, 600_000) + } + + fn scheduler_inner_opening_failed_min(_: u32) -> Weight { + Weight::from_parts(20_000_000_000, 600_000) + } + + fn scheduler_inner_opening_failed_max(_: u32) -> Weight { + Weight::from_parts(20_000_000_000, 600_000) + } + + fn scheduler_inner_opening_retry_min(_: u32) -> Weight { + Weight::from_parts(20_000_000_000, 600_000) + } + + fn scheduler_inner_opening_retry_max(_: u32) -> Weight { + Weight::from_parts(20_000_000_000, 600_000) + } + + fn scheduler_inner_opening_complete_min(_: u32) -> Weight { + Weight::from_parts(20_000_000_000, 600_000) + } + + fn scheduler_inner_opening_complete_max(_: u32) -> Weight { + Weight::from_parts(20_000_000_000, 600_000) + } + + fn scheduler_inner_opening_progress_min(_: u32) -> Weight { + Weight::from_parts(20_000_000_000, 600_000) + } + + fn scheduler_inner_opening_progress_max(_: u32) -> Weight { + Weight::from_parts(20_000_000_000, 600_000) + } + + fn scheduler_inner_running_complete(_: u32, _: u32) -> Weight { + Weight::from_parts(20_000_000_000, 600_000) + } + + fn scheduler_inner_running_progress(_: u32, _: u32) -> Weight { + Weight::from_parts(20_000_000_000, 600_000) + } + + fn scheduler_inner_suspended_tail_retry(_: u32, _: u32) -> Weight { + Weight::from_parts(20_000_000_000, 600_000) + } + + fn scheduler_inner_suspended_tail_complete(_: u32, _: u32) -> Weight { + Weight::from_parts(20_000_000_000, 600_000) + } + + fn scheduler_inner_suspended_tail_progress(_: u32, _: u32) -> Weight { + Weight::from_parts(20_000_000_000, 600_000) + } + + fn scheduler_inner_suspended_head_retry(_: u32, _: u32, _: u32, _: u32) -> Weight { + Weight::from_parts(20_000_000_000, 600_000) + } + + fn scheduler_inner_suspended_head_complete(_: u32, _: u32, _: u32, _: u32) -> Weight { + Weight::from_parts(20_000_000_000, 600_000) + } + + fn scheduler_inner_suspended_head_progress(_: u32, _: u32, _: u32, _: u32) -> Weight { + Weight::from_parts(20_000_000_000, 600_000) + } + + fn scheduler_inner_suspended_head_opening_retry(_: u32, _: u32, _: u32) -> Weight { + Weight::from_parts(20_000_000_000, 600_000) + } + + fn scheduler_inner_suspended_head_opening_complete(_: u32, _: u32, _: u32) -> Weight { + Weight::from_parts(20_000_000_000, 600_000) + } + + fn scheduler_inner_suspended_head_opening_progress(_: u32, _: u32, _: u32) -> Weight { + Weight::from_parts(20_000_000_000, 600_000) + } + fn scheduler_paged_execute_cheap(executions: u32) -> Weight { Weight::from_parts(50_000_000, 8_000) .saturating_add(Weight::from_parts(100_000_000, 8_000).saturating_mul(executions.into())) @@ -447,21 +762,24 @@ impl WeightInfo for Subst .saturating_add(T::DbWeight::get().reads_writes(1, 1)) } - fn continuation_suspend(snapshot_entries: u32) -> Weight { - Weight::from_parts(27_920_348, 4_178) - .saturating_add(Weight::from_parts(37_426, 0).saturating_mul(snapshot_entries.into())) + fn run_progress() -> Weight { + Weight::from_parts(30_000_000, 8_000).saturating_add(T::DbWeight::get().reads_writes(6, 2)) + } + + fn run_suspend() -> Weight { + Weight::from_parts(28_668_868, 4_178) .saturating_add(T::DbWeight::get().reads_writes(2, 2)) } - fn continuation_retry() -> Weight { + fn run_retry() -> Weight { Weight::from_parts(22_070_000, 4_266).saturating_add(T::DbWeight::get().reads_writes(1, 1)) } - fn continuation_complete() -> Weight { + fn run_complete() -> Weight { Weight::from_parts(18_019_000, 4_030).saturating_add(T::DbWeight::get().reads_writes(1, 2)) } - fn continuation_cancel() -> Weight { + fn run_cancel() -> Weight { Weight::from_parts(56_782_000, 8_120).saturating_add(T::DbWeight::get().reads_writes(6, 4)) } - fn continuation_suffix_admission(steps: u32) -> Weight { + fn run_suffix_admission(steps: u32) -> Weight { Weight::from_parts(1_438_574, 0) .saturating_add(Weight::from_parts(432, 0).saturating_mul(steps.into())) } @@ -513,6 +831,19 @@ impl WeightInfo for Subst .saturating_add(T::DbWeight::get().reads(1u64.saturating_add(bounded))) .saturating_add(T::DbWeight::get().writes(bounded.saturating_mul(5))) } + + fn maximum_context_inherent() -> Weight { + Weight::MAX + } + fn maximum_xcm_version_discovery() -> Weight { + Weight::MAX + } + fn block_resource_meter_extension() -> Weight { + Weight::MAX + } + fn block_resource_finalize() -> Weight { + Weight::MAX + } } #[cfg(any(test, feature = "runtime-benchmarks"))] @@ -530,10 +861,24 @@ impl WeightInfo for TestWeightInfo { fn deactivate_actor() -> Weight { Weight::from_parts(60_623_000, 8_120) } fn pause_actor() -> Weight { Weight::from_parts(15_000_000, 1200) } fn resume_actor() -> Weight { Weight::from_parts(15_000_000, 1200) } - fn manual_trigger() -> Weight { Weight::from_parts(12_000_000, 1200) } + fn manual_trigger() -> Weight { Weight::from_parts(113_494_000, 9_635) } + fn address_event_trigger_occurrence() -> Weight { Weight::from_parts(169_927_000, 8_366) } + fn observation_change_trigger_occurrence() -> Weight { Weight::from_parts(117_615_000, 8_295) } + fn observation_crossing_trigger_occurrence() -> Weight { Weight::from_parts(499_862_000, 164_106) } + fn at_time_trigger_occurrence() -> Weight { Weight::from_parts(157_425_000, 8_317) } + fn cadenced_trigger_occurrence() -> Weight { Weight::from_parts(195_070_000, 8_325) } fn observation_change_ingress() -> Weight { Weight::from_parts(75_000_000, 24_000) } fn observation_fanout_base() -> Weight { Weight::from_parts(15_000_000, 4_000) } - fn observation_fanout_page() -> Weight { Weight::from_parts(8_000_000_000, 750_000) } + fn observation_fanout_branch_probe() -> Weight { Weight::zero() } + fn observation_fanout_page() -> Weight { Weight::from_parts(150_000_000_000, 750_000) } + fn observation_fanout_wakeup_page() -> Weight { Weight::from_parts(8_000_000_000, 750_000) } + fn observation_fanout_coalesced_page() -> Weight { Weight::from_parts(8_000_000_000, 750_000) } + fn observation_fanout_blocked_page() -> Weight { Weight::from_parts(150_000_000_000, 400_000) } + fn observation_fanout_terminal() -> Weight { Weight::from_parts(8_000_000_000, 750_000) } + fn record_crossing_worker_fault() -> Weight { Weight::from_parts(16_000_000, 1_529) } + fn record_observation_fanout_worker_fault() -> Weight { Weight::from_parts(16_000_000, 1_529) } + fn record_wakeup_worker_fault() -> Weight { Weight::from_parts(16_000_000, 1_529) } + fn pipeline_admission_apoptosis() -> Weight { Weight::from_parts(161_616_000, 5_736) } fn close_actor() -> Weight { Weight::from_parts(84_719_000, 8_120) } fn fee_collection() -> Weight { Weight::from_parts(112_097_000, 8_120) } fn predicate_set_evaluation(predicates: u32) -> Weight { @@ -566,6 +911,38 @@ impl WeightInfo for TestWeightInfo { fn task_unstake() -> Weight { Weight::from_parts(200_000_000, 24_000) } fn task_dex_exact_in() -> Weight { Weight::from_parts(280_000_000, 13_000) } fn task_dex_exact_out() -> Weight { Weight::from_parts(1_500_000_000, 64_000) } + fn contract_geometry_create(chunks: u32) -> Weight { + Weight::from_parts(100_000_000, 16_000) + .saturating_add(Weight::from_parts(25_000_000, 8_000).saturating_mul(chunks.into())) + } + fn contract_geometry_close(chunks: u32) -> Weight { + Weight::from_parts(100_000_000, 16_000) + .saturating_add(Weight::from_parts(25_000_000, 8_000).saturating_mul(chunks.into())) + } + fn contract_geometry_reconstruct(chunks: u32) -> Weight { + Weight::from_parts(75_000_000, 16_000) + .saturating_add(Weight::from_parts(20_000_000, 8_000).saturating_mul(chunks.into())) + } + fn current_step_load_head() -> Weight { Weight::from_parts(30_000_000, 8_000) } + fn current_step_load_tail(steps_in_chunk: u32) -> Weight { + Weight::from_parts(40_000_000, 16_000) + .saturating_add(Weight::from_parts(1_000_000, 512).saturating_mul(steps_in_chunk.into())) + } + fn current_step_plan_opening_head() -> Weight { Weight::from_parts(100_000_000, 24_000) } + fn current_step_plan_suspended_head() -> Weight { Weight::from_parts(150_000_000, 32_000) } + fn current_step_plan_running_tail(steps_in_chunk: u32) -> Weight { + Weight::from_parts(150_000_000, 32_000) + .saturating_add(Weight::from_parts(1_000_000, 512).saturating_mul(steps_in_chunk.into())) + } + fn opening_snapshot_capture(entries: u32) -> Weight { + Weight::from_parts(25_000_000, 4_000) + .saturating_add(Weight::from_parts(15_000_000, 3_000).saturating_mul(entries.into())) + } + fn opening_predicate_capture(predicates: u32) -> Weight { + Weight::from_parts(25_000_000, 4_000) + .saturating_add(Weight::from_parts(15_000_000, 3_000).saturating_mul(predicates.into())) + } + fn scheduler_on_initialize_cutoff() -> Weight { Weight::from_parts(7_543_000, 1_493) } fn scheduler_on_idle_base() -> Weight { Weight::from_parts(25_000_000, 2_500) } fn materialization_coordinator_base() -> Weight { Weight::from_parts(20_000_000, 4_000) } fn scheduler_paged_append_existing_page() -> Weight { Weight::from_parts(80_000_000, 16_000) } @@ -594,6 +971,75 @@ impl WeightInfo for TestWeightInfo { Weight::from_parts(20_000_000, 4_000) .saturating_add(Weight::from_parts(40_000_000, 4_000).saturating_mul(entries.into())) } + fn scheduler_inner_zero_step_complete() -> Weight { + Weight::from_parts(37_645_000, 4_570) + } + fn scheduler_paged_execute_opening_max() -> Weight { + Weight::from_parts(20_000_000_000, 600_000) + } + fn scheduler_inner_opening_close_min(_: u32) -> Weight { + Weight::from_parts(20_000_000_000, 600_000) + } + fn scheduler_inner_opening_close_max(_: u32) -> Weight { + Weight::from_parts(20_000_000_000, 600_000) + } + fn scheduler_inner_opening_failed_min(_: u32) -> Weight { + Weight::from_parts(20_000_000_000, 600_000) + } + fn scheduler_inner_opening_failed_max(_: u32) -> Weight { + Weight::from_parts(20_000_000_000, 600_000) + } + fn scheduler_inner_opening_retry_min(_: u32) -> Weight { + Weight::from_parts(20_000_000_000, 600_000) + } + fn scheduler_inner_opening_retry_max(_: u32) -> Weight { + Weight::from_parts(20_000_000_000, 600_000) + } + fn scheduler_inner_opening_complete_min(_: u32) -> Weight { + Weight::from_parts(20_000_000_000, 600_000) + } + fn scheduler_inner_opening_complete_max(_: u32) -> Weight { + Weight::from_parts(20_000_000_000, 600_000) + } + fn scheduler_inner_opening_progress_min(_: u32) -> Weight { + Weight::from_parts(20_000_000_000, 600_000) + } + fn scheduler_inner_opening_progress_max(_: u32) -> Weight { + Weight::from_parts(20_000_000_000, 600_000) + } + fn scheduler_inner_running_complete(_: u32, _: u32) -> Weight { + Weight::from_parts(20_000_000_000, 600_000) + } + fn scheduler_inner_running_progress(_: u32, _: u32) -> Weight { + Weight::from_parts(20_000_000_000, 600_000) + } + fn scheduler_inner_suspended_tail_retry(_: u32, _: u32) -> Weight { + Weight::from_parts(20_000_000_000, 600_000) + } + fn scheduler_inner_suspended_tail_complete(_: u32, _: u32) -> Weight { + Weight::from_parts(20_000_000_000, 600_000) + } + fn scheduler_inner_suspended_tail_progress(_: u32, _: u32) -> Weight { + Weight::from_parts(20_000_000_000, 600_000) + } + fn scheduler_inner_suspended_head_retry(_: u32, _: u32, _: u32, _: u32) -> Weight { + Weight::from_parts(20_000_000_000, 600_000) + } + fn scheduler_inner_suspended_head_complete(_: u32, _: u32, _: u32, _: u32) -> Weight { + Weight::from_parts(20_000_000_000, 600_000) + } + fn scheduler_inner_suspended_head_progress(_: u32, _: u32, _: u32, _: u32) -> Weight { + Weight::from_parts(20_000_000_000, 600_000) + } + fn scheduler_inner_suspended_head_opening_retry(_: u32, _: u32, _: u32) -> Weight { + Weight::from_parts(20_000_000_000, 600_000) + } + fn scheduler_inner_suspended_head_opening_complete(_: u32, _: u32, _: u32) -> Weight { + Weight::from_parts(20_000_000_000, 600_000) + } + fn scheduler_inner_suspended_head_opening_progress(_: u32, _: u32, _: u32) -> Weight { + Weight::from_parts(20_000_000_000, 600_000) + } fn scheduler_paged_execute_cheap(executions: u32) -> Weight { Weight::from_parts(50_000_000, 8_000) .saturating_add(Weight::from_parts(100_000_000, 8_000).saturating_mul(executions.into())) @@ -609,14 +1055,12 @@ impl WeightInfo for TestWeightInfo { Weight::from_parts(15_653_872, 4_265) .saturating_add(Weight::from_parts(146_253, 0).saturating_mul(assets.into())) } - fn continuation_suspend(snapshot_entries: u32) -> Weight { - Weight::from_parts(27_920_348, 4_178) - .saturating_add(Weight::from_parts(37_426, 0).saturating_mul(snapshot_entries.into())) - } - fn continuation_retry() -> Weight { Weight::from_parts(22_070_000, 4_266) } - fn continuation_complete() -> Weight { Weight::from_parts(18_019_000, 4_030) } - fn continuation_cancel() -> Weight { Weight::from_parts(56_782_000, 8_120) } - fn continuation_suffix_admission(steps: u32) -> Weight { + fn run_progress() -> Weight { Weight::from_parts(30_000_000, 8_000) } + fn run_suspend() -> Weight { Weight::from_parts(28_668_868, 4_178) } + fn run_retry() -> Weight { Weight::from_parts(22_070_000, 4_266) } + fn run_complete() -> Weight { Weight::from_parts(18_019_000, 4_030) } + fn run_cancel() -> Weight { Weight::from_parts(56_782_000, 8_120) } + fn run_suffix_admission(steps: u32) -> Weight { Weight::from_parts(1_438_574, 0) .saturating_add(Weight::from_parts(432, 0).saturating_mul(steps.into())) } @@ -640,4 +1084,8 @@ impl WeightInfo for TestWeightInfo { 1200u64.saturating_add(384u64.saturating_mul(bounded)), ) } + fn maximum_context_inherent() -> Weight { Weight::MAX } + fn maximum_xcm_version_discovery() -> Weight { Weight::MAX } + fn block_resource_meter_extension() -> Weight { Weight::MAX } + fn block_resource_finalize() -> Weight { Weight::from_parts(10_000_000, 1_000) } } diff --git a/template/pallets/governance/docs/architecture.en.md b/template/pallets/governance/docs/architecture.en.md index 4ab2f8e4..b25d47c8 100644 --- a/template/pallets/governance/docs/architecture.en.md +++ b/template/pallets/governance/docs/architecture.en.md @@ -584,7 +584,7 @@ Current runtime policy values: - `ProposalVetoMinimumVetoTurnout = 1%` of eligible protection supply - `ProposalMinimumTurnout = 200` weighted units - `MaxEpochCatchUpPerBlock = 1` -- `MaxMaturingProposalsPerBlock = 3` +- `MaxMaturingProposalsPerBlock = 2` - `MaxPendingEnactmentsPerBlock = 4` - `MaxFinalizedProposalOutcomesPerBlock = 1024` - `MaxExpiringAccountsPerBlock = 512` @@ -592,6 +592,8 @@ Current runtime policy values: - `MaxFinalizedProposalOutcomesPerEpoch = 1024` - `MaxExpiringAccountsPerEpoch = 1024` +The two-proposal maturity bound keeps the generated worst-case hook ProofSize compatible with the runtime fixed envelope while preserving bounded continuation across later blocks; increasing it requires recomputing `FixedBlockWeight` and maximum signed-call fit. + Ordinary and invoice-family resolution consume exactly the fixed `ProposalApprovalThreshold` and `ProposalMinimumTurnout`. No adaptive ceiling, progress, or decay policy is reconstructed in views or resolution. ### Vote weight providers diff --git a/template/pallets/oracle/docs/architecture.en.md b/template/pallets/oracle/docs/architecture.en.md index b1733ee1..b837dae6 100644 --- a/template/pallets/oracle/docs/architecture.en.md +++ b/template/pallets/oracle/docs/architecture.en.md @@ -26,7 +26,7 @@ Concrete DEOS composition belongs to [`docs/oracle.integration.en.md`](../../../ `FeedId` remains the immutable identity key chosen by the host. `FeedConfig` stores the authorized producer, explicit meaning and provenance, scalar scale, aggregation policy, zero policy, and lifecycle. The package never infers semantic equivalence, reverse direction, market meaning, or producer trust. -`RegisterOrigin` controls feed and lifecycle administration. `PublishOrigin` resolves directly to the typed producer identity checked against the immutable feed configuration. `OnObservationChanged` receives `(feed, revision, previous, current)` after a changed scalar and exposes no independent numeric Weight. Hook failure rolls back both the Oracle value and downstream transition admission. +`RegisterOrigin` controls feed and lifecycle administration. `PublishOrigin` resolves directly to the typed producer identity checked against the immutable feed configuration. `OnObservationChanged` receives `(feed, revision, previous, current, cause_provenance)` after a changed scalar; signed `publish` supplies `ExternalPhase`, while `publish_from` and `ObservationSink` supply fail-closed `Deferred`. The hook exposes no independent numeric Weight. Hook failure rolls back both the Oracle value and downstream transition admission. ## Storage Topology diff --git a/template/pallets/oracle/docs/embedding.md b/template/pallets/oracle/docs/embedding.md index 8ef82b5d..c946f4e9 100644 --- a/template/pallets/oracle/docs/embedding.md +++ b/template/pallets/oracle/docs/embedding.md @@ -14,7 +14,7 @@ ## Hooks and Consumers -- `OnObservationChanged` receives the exact revision plus previous/current scalar transition and must remain O(1), bounded, and subscriber-independent. It exposes no separate Weight estimate: host-generated publication weights measure the concrete composed changed path and charge it exactly once. A hook error atomically rejects publication. +- `OnObservationChanged` receives the exact revision, previous/current scalar transition, and closed `ObservationCauseProvenance`; signed `publish` supplies `ExternalPhase`, while `publish_from` and `ObservationSink` fail closed to `Deferred`. It must remain O(1), bounded, and subscriber-independent. It exposes no separate Weight estimate: host-generated publication weights measure the concrete composed changed path and charge it exactly once. A hook error atomically rejects publication. - A required hook failure must propagate and roll back the observation plus the producer's enclosing transaction. - Consumers must author a nonzero maximum age and distinguish Unavailable, Uninitialized, Fresh, and Stale. - History, search, charts, subscriber fanout, and strategy execution remain outside this package. diff --git a/template/pallets/oracle/docs/specification.en.md b/template/pallets/oracle/docs/specification.en.md index ca94db27..9ee27922 100644 --- a/template/pallets/oracle/docs/specification.en.md +++ b/template/pallets/oracle/docs/specification.en.md @@ -128,12 +128,18 @@ The pallet exposes bounded current truth only. Historical revision lookup, chart ## 9. Transactional Change Hook -The package exposes `OnObservationChanged(feed, revision, previous, current) -> DispatchResult`. It calls the hook exactly once after computing a changed published scalar and before committing the transition. `previous` is absent only for revision `1`; every later changed revision carries the exact previously committed value. +The package exposes `OnObservationChanged(feed, revision, previous, current, cause_provenance) -> DispatchResult`. The signed `publish` extrinsic supplies `ExternalPhase`; the public `publish_from` adapter and `ObservationSink` supply fail-closed `Deferred`. It calls the hook exactly once after computing a changed published scalar and before committing the transition. `previous` is absent only for revision `1`; every later changed revision carries the exact previously committed value. Hook work MUST remain O(1), bounded, and independent of subscriber count. The host-generated publication `WeightInfo` MUST measure the concrete changed-publication path with the hook composed and charge that synchronous path exactly once; the hook exposes no parallel numeric Weight authority. Hook failure rolls back observation state, revision, oracle event, and every mutation enclosed by the producer's outer transaction. Equal-output refreshes do not call the hook. Actors integration may use this hook only for bounded dirty-feed marking. It MUST NOT iterate subscribers or execute actors in producer context. +The changed publication's canonical causal block is the current block in which the Oracle transition commits. A host that turns the hook into deferred consumer work MUST durably bind that work to the exact feed revision and causal block, explicitly or through equivalent temporal authority. Consumer readiness caused by publication in block `N` MUST have `eligible_at >= N + 1`; hook execution, dirty marking, bounded detector materialization, or spare block Weight cannot make a consumer effect eligible in block `N`. + +The causal binding is part of the publication transaction. Failure to preserve it rolls back observation state, revision, Oracle event, and hook mutations. Equal-output refresh creates no changed-observation readiness because it creates no changed revision or hook call. + +Per-feed changed revisions retain their exact committed order through deferred processing. Later revision work MUST NOT overtake an earlier incomplete revision for the same feed. This source order is independent from consumer temporal order and the Actors FIFO: revision order authorizes detector progression, temporal order controls earliest eligibility, and FIFO order controls service after readiness. + ## 10. Lifecycle Transitions - `register`: absent to Active/Paused as explicitly admitted; no observation exists. diff --git a/template/pallets/oracle/src/lib.rs b/template/pallets/oracle/src/lib.rs index e11c0e5a..d8b6be07 100644 --- a/template/pallets/oracle/src/lib.rs +++ b/template/pallets/oracle/src/lib.rs @@ -78,12 +78,19 @@ pub enum ObservationState { Stale(Observation), } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ObservationCauseProvenance { + ExternalPhase, + Deferred, +} + pub trait OnObservationChanged { fn on_observation_changed( feed: FeedId, revision: Revision, previous: Option, current: OracleValue, + cause_provenance: ObservationCauseProvenance, ) -> DispatchResult; } @@ -93,6 +100,7 @@ impl OnObservationChanged for () { _: Revision, _: Option, _: OracleValue, + _: ObservationCauseProvenance, ) -> DispatchResult { Ok(()) } @@ -400,7 +408,12 @@ pub mod pallet { )] pub fn publish(origin: OriginFor, feed: T::FeedId, sample: OracleValue) -> DispatchResult { let producer = T::PublishOrigin::ensure_origin(origin)?; - Self::publish_from(producer, feed, sample) + Self::publish_with_provenance( + producer, + feed, + sample, + ObservationCauseProvenance::ExternalPhase, + ) } } @@ -426,6 +439,15 @@ pub mod pallet { producer: T::ProducerId, feed: T::FeedId, sample: OracleValue, + ) -> DispatchResult { + Self::publish_with_provenance(producer, feed, sample, ObservationCauseProvenance::Deferred) + } + + fn publish_with_provenance( + producer: T::ProducerId, + feed: T::FeedId, + sample: OracleValue, + cause_provenance: ObservationCauseProvenance, ) -> DispatchResult { let config = Feeds::::get(feed).ok_or(Error::::FeedNotFound)?; ensure!( @@ -476,6 +498,7 @@ pub mod pallet { revision, previous.map(|observation| observation.value), value, + cause_provenance, )?; } Observations::::insert( diff --git a/template/pallets/oracle/src/mock.rs b/template/pallets/oracle/src/mock.rs index 307dd881..dbd4355a 100644 --- a/template/pallets/oracle/src/mock.rs +++ b/template/pallets/oracle/src/mock.rs @@ -18,6 +18,7 @@ pub type AccountId = u64; std::thread_local! { static HOOK_FAILURE: RefCell = const { RefCell::new(false) }; static HOOK_CALLS: RefCell, u128)>> = const { RefCell::new(Vec::new()) }; + static HOOK_PROVENANCE: RefCell> = const { RefCell::new(Vec::new()) }; } pub struct TestObservationHook; @@ -27,6 +28,7 @@ impl crate::OnObservationChanged for TestObservationHook { revision: crate::Revision, previous: Option, current: crate::OracleValue, + cause_provenance: crate::ObservationCauseProvenance, ) -> polkadot_sdk::sp_runtime::DispatchResult { if HOOK_FAILURE.with(|value| *value.borrow()) { return Err(polkadot_sdk::sp_runtime::DispatchError::Other( @@ -34,10 +36,15 @@ impl crate::OnObservationChanged for TestObservationHook { )); } HOOK_CALLS.with(|calls| calls.borrow_mut().push((feed, revision, previous, current))); + HOOK_PROVENANCE.with(|causes| causes.borrow_mut().push(cause_provenance)); Ok(()) } } +pub fn take_hook_provenance() -> Vec { + HOOK_PROVENANCE.with(|causes| core::mem::take(&mut *causes.borrow_mut())) +} + pub fn set_hook_failure(fail: bool) { HOOK_FAILURE.with(|value| *value.borrow_mut() = fail); } @@ -109,6 +116,7 @@ pub fn new_test_ext() -> polkadot_sdk::sp_io::TestExternalities { let mut ext = polkadot_sdk::sp_io::TestExternalities::new(storage); HOOK_FAILURE.with(|value| *value.borrow_mut() = false); HOOK_CALLS.with(|calls| calls.borrow_mut().clear()); + HOOK_PROVENANCE.with(|causes| causes.borrow_mut().clear()); ext.execute_with(|| System::set_block_number(1)); ext } diff --git a/template/pallets/oracle/src/tests.rs b/template/pallets/oracle/src/tests.rs index a17fa440..00e00e43 100644 --- a/template/pallets/oracle/src/tests.rs +++ b/template/pallets/oracle/src/tests.rs @@ -1,6 +1,10 @@ use crate::{ - Aggregation, Error, FeedLifecycle, Observation, ObservationState, OracleValue, ZeroPolicy, - mock::{Oracle, RuntimeOrigin, System, Test, hook_calls, new_test_ext, set_hook_failure}, + Aggregation, Error, FeedLifecycle, Observation, ObservationCauseProvenance, ObservationState, + OracleValue, ZeroPolicy, + mock::{ + Oracle, RuntimeOrigin, System, Test, hook_calls, new_test_ext, set_hook_failure, + take_hook_provenance, + }, }; use codec::{Decode, Encode}; use polkadot_sdk::frame_support::{assert_noop, assert_ok, traits::StorageInfoTrait}; @@ -295,6 +299,18 @@ fn changed_hook_is_transactional_and_equal_refresh_is_hook_free() { assert_eq!(hook_calls(), vec![(1, 1, None, 10)]); assert_ok!(Oracle::publish(RuntimeOrigin::signed(1), 1, 12)); assert_eq!(hook_calls(), vec![(1, 1, None, 10), (1, 2, Some(10), 12)]); + assert_eq!( + take_hook_provenance(), + vec![ + ObservationCauseProvenance::ExternalPhase, + ObservationCauseProvenance::ExternalPhase, + ] + ); + assert_ok!(Oracle::publish_from(1, 1, 14)); + assert_eq!( + take_hook_provenance(), + vec![ObservationCauseProvenance::Deferred] + ); }); } diff --git a/template/pallets/router/docs/architecture.en.md b/template/pallets/router/docs/architecture.en.md index 239a27a0..cfe03ab2 100644 --- a/template/pallets/router/docs/architecture.en.md +++ b/template/pallets/router/docs/architecture.en.md @@ -49,7 +49,7 @@ graph TD The `swap` extrinsic delegates to `execute_swap_for()`, the shared entry point for both user and system swaps: 1. `Extrinsic Validation`: `amount_in >= MinSwapForeign`, `block <= deadline`. -2. `Core Validation`: `from != to`, `amount_in > 0`. +2. `Core Validation`: Both endpoints are canonical, their `LedgerAssetKey`s differ, and `amount_in > 0`. 3. `Fee Calculation`: Zero for fee-exempt system accounts (Burn Actor, liquidity actors, Router); `Perbill`-based for users. 4. `Gross-Debit Preflight`: Users must be able to pay the full `amount_in` under the selected preservation policy before any state-mutating swap path begins. 5. `Route Selection`: `find_optimal_route()` evaluates all candidates and selects the route with the highest `expected_output` (pure mechanism: best price to the user). @@ -59,12 +59,14 @@ The `swap` extrinsic delegates to `execute_swap_for()`, the shared entry point f 9. `Execution`: Dispatch to XYK adapter or TMC `mint_with_distribution`. System accounts use `keep_alive=false` (can drain balances); users use `keep_alive=true`. 10. `Outcome`: Execution constructs `RouterOutcome` from committed actual facts. `SwapExecuted` carries caller, request endpoints, and that complete outcome directly, so route family, prepared legs, amounts, fee, and Weight class retain one meaning. -`Public API`: `execute_swap_for(...)` and `execute_exact_out_for(...)` return `RouterOutcome` on success and typed `ExecutionError` on failure. The outcome carries selected family, prepared legs, actual total/routed input, fee, recipient output, and route Weight class. +`Public API`: Signed calls expose exact input at index `0` and exact output at index `2`; both delegate to `execute_swap_for(...)` or `execute_exact_out_for(...)` and emit one `RouterOutcome`. Exact output applies `max_amount_in` to Router fee plus routed XYK input. The outcome carries selected family, prepared legs, actual total/routed input, fee, recipient output, and route Weight class. `PreparedRoute` remains a public Rust package type solely for the deterministic conformance-vector example and independent consumer tooling. No call, event, storage item, or runtime API exposes it, so it does not form part of runtime metadata ABI; execution constructs it only inside the transaction. System Actor adapters preserve actual input/output facts in `DexSwapOutcome`. They derive authored slippage bounds and retain System-only reference policy, but they do not call a second Router path validator or own route preparation. The guard first reads its independent Fresh Oracle observation and lazily reads direct reserves only when that observation is absent, stale, uninitialized, or zero; it never treats the selected execution quote as its own reference. +Router owns economic execution Weight and returns the committed route class and actual outcome; it does not own Actor Control, FIFO service, or block-share policy. A host Actors adapter reserves the Router maximum from Shared Economic capacity before dispatch, then reports the canonical generated Router effect Weight as valid actual evidence for transactional reclaim. Failed or inconsistent actual evidence rolls back the Actor attempt. The DEOS equal-thirds split is runtime composition in `docs/actors.integration.en.md`, not Router package semantics. + `Native Exact-Output Boundary`: `AssetConversionApi` exposes one-pool reverse quotes and execution returning `ExactOutputExecution { amount_in, recipient_amount_out }` from measured caller and recipient deltas. `quote_exact_out` evaluates at most direct XYK and one reverse-quoted Native-anchored path, selects minimum required post-fee input, and adds the caller-aware Router fee. Execution enforces total-input and recipient-output bounds against actual deltas. Direct TMC mint remains exact-input only because it cannot promise an exact recipient amount. @@ -266,7 +268,7 @@ Router-local observation storage, tracking calls, metadata, and generated weight | `RouterFee` | `StorageValue` | Current bounded governance fee rate | | `LpPairByTokenId` | `StorageValue>` | Bounded reverse index from LP token ID to canonical pool pair | -LP registration canonicalizes pair order, rejects repeated endpoints, duplicate LP ownership in either direction, and capacity overflow at the ecosystem-owned `MAX_ROUTER_LP_PAIRS` bound. `preflight_register_lp_pair` exposes the same read-only admission contract so a host can reject before an independently dispatched pool mutation. Package `try_state` verifies the fee ceiling, strict pair order, and one-to-one LP/pair ownership. Optional `LpPairIntegrity` host reconciliation additionally validates every binding against concrete pool/LP-asset truth and compares complete pool/index cardinality; `()` retains internal-only checks for independent hosts without that cross-pallet topology. +LP registration canonicalizes pair order through canonical physical-ledger admission and rejects aliases, duplicate LP ownership in either direction, and capacity overflow at `MAX_ROUTER_LP_PAIRS`. `PoolLifecycleApi` owns preflight, underlying creation, actual LP verification, reverse binding, required observation topology, and rollback. Package `try_state` verifies the fee ceiling, strict pair order, and one-to-one LP/pair ownership. Optional `LpPairIntegrity` host reconciliation additionally validates both pool/index directions, LP assets, physical-pair uniqueness, observation topology, and complete cardinality; `()` retains internal-only checks for independent hosts. ## Extrinsics @@ -274,6 +276,8 @@ LP registration canonicalizes pair order, rejects repeated endpoints, duplicate | --- | --- | --- | --- | | `0` | `swap(from, to, amount_in, min_amount_out, recipient, deadline)` | Signed | Benchmarked | | `1` | `update_router_fee(new_fee)` | AdminOrigin (Root) | Benchmarked | +| `2` | `swap_exact_output(from, to, amount_out, max_amount_in, recipient, deadline)` | Signed | Component-wise Router swap maximum | +| `3` | `create_pool(asset_a, asset_b)` | Signed, permissionless | `144,923,000 / 34,255`, 13 reads, 10 writes | ## Events @@ -424,7 +428,7 @@ Located in `runtime/src/tests/deos_router_integration_tests.rs`: ### Benchmarks -`swap` and `update_router_fee` use generated V2 runtime weights. Each XYK route seeds a nearby valid safety reference, then measures changed Oracle publication with combined broad and Crossing ingress on every committed directional pool feed. Exact-output fixtures use a non-dust amount so the reference guard and measured route remain representative. +Router calls use generated V2 runtime weights. The public swap envelope takes the component-wise maximum across exact-input and exact-output route classes. Each XYK route seeds a nearby valid safety reference, then measures changed Oracle publication with combined broad and Crossing ingress on every committed directional pool feed. Canonical `create_pool` measures the complete host pool, LP reverse identity, and two-feed Oracle topology at 50 steps × 20 repeats. Production `50 × 20` generation measures every semantic route class independently: diff --git a/template/pallets/router/docs/embedding.md b/template/pallets/router/docs/embedding.md index 41de1097..0eb786bf 100644 --- a/template/pallets/router/docs/embedding.md +++ b/template/pallets/router/docs/embedding.md @@ -8,13 +8,15 @@ The Router does not require DEOS Actor accounts or concrete DEOS market policy. ## Required Guarantees -- `AssetConversionApi` identifies and executes exactly one canonical pool per call. +- `AssetConversionApi` identifies and executes exactly one canonical physical-ledger pool per call and reports full-balance reserves. +- `PoolLifecycleApi` atomically owns permissionless underlying creation, actual LP verification, reverse binding, required observation topology, and rollback. +- The host call filter denies raw XYK swaps and raw pool creation so Router fee and lifecycle policy cannot be bypassed. - Execution reports measured recipient output and, for exact output, measured caller spend. - `TmcInterface` reports recipient allocation rather than total issuance. - `FeeRoutingAdapter` participates in the Router transaction. - `PriceOracle` validates directional references and publishes pre-execution observations. - `MaxLpPairs` bounds the reverse LP index. -- `LpPairIntegrity` optionally reconciles each Router LP binding and complete index cardinality against host pool/LP-asset storage during try-state; use `()` only when the host has no such cross-pallet topology. +- `LpPairIntegrity` optionally reconciles both pool/index directions, LP assets, physical-pair uniqueness, required observation topology, cardinality, and genesis admission; use `()` only when the host has no such cross-pallet topology. - `WeightInfo` covers the host's worst-case bounded execution. The packaged `SubstrateWeight` and `()` are hand-written placeholders that report zero ProofSize and no database access, while the DEOS reference runtime measures a direct XYK swap at 13998 ProofSize with 25 reads and 12 writes. Generate weights against your own runtime and bind those. ## Evidence diff --git a/template/pallets/router/docs/specification.en.md b/template/pallets/router/docs/specification.en.md index a771e220..9f667e50 100644 --- a/template/pallets/router/docs/specification.en.md +++ b/template/pallets/router/docs/specification.en.md @@ -38,10 +38,13 @@ The Router is a decision and execution mechanism. It does not own treasury polic - Fee routing, publication, Actor ingress, market mutation, balance changes, Router events, and outcome construction commit or roll back together. - Event and outcome route identity, amounts, fees, legs, and Weight class remain identical. - Unknown adapter or execution failures classify as Permanent at the Actor boundary. +- Every accepted route endpoint is the unique canonical `AssetKind` for its `LedgerAssetKey`; semantic aliases of one physical ledger are invalid. +- Public XYK execution enters through Router exact-input or exact-output calls so Router fee collection cannot be bypassed. +- Permissionless pool creation enters through one host `PoolLifecycleApi` transaction that owns the underlying pool, LP identity, required observation topology, and rollback. ## Supported Assets and Route Families -`AssetKind` is the shared asset identity. `NativeAsset` is host configuration. A route contains at most two legs under the current contract. +`AssetKind` is shared semantic identity, while `LedgerAssetKey::{Native, Assets(u32)}` is physical-ledger identity. `Local(id)` and `Foreign(id)` cannot coexist as accepted representations of one `Assets(id)` ledger. `NativeAsset` is host configuration. A route contains at most two legs under the current contract. | Family | Legs | Admission | | --- | --- | --- | @@ -293,7 +296,9 @@ Actor admission uses the maximum Router Weight class reachable by the authored s ## Adapter Contracts -`AssetConversionApi` exposes canonical pool identity, current reserves, exact-input and exact-output one-pool quotes, and execution of one identified pool leg returning actual spend and output. Low-level helpers are package-private unless their names explicitly state single-pool primitive semantics. +`AssetConversionApi` exposes canonical pool identity, current full-balance reserves, exact-input and exact-output one-pool quotes, and execution of one identified pool leg returning actual spend and output. Low-level helpers are package-private unless their names explicitly state single-pool primitive semantics. + +`PoolLifecycleApi` exposes one permissionless atomic creation transition. The host preflights canonical physical distinctness, LP namespace/collision/capacity, and required observation topology before creating the underlying pool; it verifies the actual LP identity and commits LP binding plus observation topology in the same rollback boundary. `TmcInterface` exposes curve existence, collateral support, exact-input recipient quote, and mint execution returning actual recipient output. It does not expose redemption or exact-output mint. @@ -316,7 +321,7 @@ Package `try_state` proves the fee bound, canonical pair ordering, and one-to-on ## Public Calls and Runtime APIs -The signed swap call preserves its existing call index while evolving to return or emit canonical outcome truth. Governance fee update preserves its existing call index and bounded origin contract. +Call `0` executes signed exact-input swaps. Call `1` updates the bounded Router fee through governance. Call `2` executes signed exact-output swaps under a total caller-input cap. Call `3` permissionlessly creates one complete host-owned pool topology. Raw host XYK swaps and raw pool creation are outside the package contract and must be filtered by the embedding runtime. Pallet-facing execution APIs accept caller, recipient, assets, and typed intent. Exact-input and exact-output share preparation, comparator, protection, execution, and outcome owners rather than parallel implementations. diff --git a/template/pallets/router/embedding-runtime/src/lib.rs b/template/pallets/router/embedding-runtime/src/lib.rs index 658ab11e..e8f4c911 100644 --- a/template/pallets/router/embedding-runtime/src/lib.rs +++ b/template/pallets/router/embedding-runtime/src/lib.rs @@ -213,6 +213,7 @@ impl pallet_deos_router::Config for Runtime { type Assets = Assets; type TmcPallet = HostTmc; type AssetConversion = HostXyk; + type PoolLifecycle = (); type AdminOrigin = frame_system::EnsureRoot; type PalletId = RouterPalletId; type NativeAsset = NativeAsset; diff --git a/template/pallets/router/src/benchmarking.rs b/template/pallets/router/src/benchmarking.rs index b25b342f..81286b84 100644 --- a/template/pallets/router/src/benchmarking.rs +++ b/template/pallets/router/src/benchmarking.rs @@ -141,6 +141,17 @@ mod benches { } } + #[benchmark] + fn create_pool() { + let caller: T::AccountId = whitelisted_caller(); + let asset_a = T::NativeAsset::get(); + let asset_b = AssetKind::Local(19); + fund::(&caller, &[asset_a, asset_b]); + + #[extrinsic_call] + create_pool(RawOrigin::Signed(caller), asset_a, asset_b); + } + #[benchmark] fn update_router_fee() { let new_fee = polkadot_sdk::sp_runtime::Perbill::from_percent(1); diff --git a/template/pallets/router/src/lib.rs b/template/pallets/router/src/lib.rs index 526985d5..7961f34c 100644 --- a/template/pallets/router/src/lib.rs +++ b/template/pallets/router/src/lib.rs @@ -409,6 +409,9 @@ pub mod pallet { /// Asset conversion API for XYK pools type AssetConversion: crate::types::AssetConversionApi; + /// Atomic host owner for permissionless pool, LP identity, and Oracle topology creation. + type PoolLifecycle: crate::types::PoolLifecycleApi; + /// Origin that can perform governance operations type AdminOrigin: EnsureOrigin; @@ -644,6 +647,45 @@ pub mod pallet { Ok(()) } + /// Execute an exact-output token swap through the router under a total input cap. + #[pallet::call_index(2)] + #[pallet::weight(T::WeightInfo::swap())] + pub fn swap_exact_output( + origin: OriginFor, + from: AssetKind, + to: AssetKind, + amount_out: Balance, + max_amount_in: Balance, + recipient: T::AccountId, + deadline: BlockNumberFor, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + ensure!(!amount_out.is_zero(), Error::::ZeroAmount); + ensure!( + frame_system::Pallet::::block_number() <= deadline, + Error::::DeadlinePassed + ); + Self::execute_exact_out_for(&who, from, to, amount_out, max_amount_in, &recipient) + .map_err(DispatchError::from)?; + Ok(()) + } + + /// Permissionlessly create one complete canonical XYK pool topology. + #[pallet::call_index(3)] + #[pallet::weight(T::WeightInfo::create_pool())] + pub fn create_pool( + origin: OriginFor, + asset_a: AssetKind, + asset_b: AssetKind, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + ensure!( + asset_a.is_valid_market_pair(asset_b), + Error::::InvalidPoolPair + ); + T::PoolLifecycle::create_pool(&who, asset_a, asset_b) + } + /// Update router fee (governance only) #[pallet::call_index(1)] #[pallet::weight(T::WeightInfo::update_router_fee())] @@ -659,7 +701,7 @@ pub mod pallet { } fn canonical_lp_pair(pair: (AssetKind, AssetKind)) -> Option<(AssetKind, AssetKind)> { - if pair.0 == pair.1 { + if !pair.0.is_valid_market_pair(pair.1) { None } else if pair.0 < pair.1 { Some(pair) @@ -1050,7 +1092,7 @@ pub mod pallet { min_amount_out: Balance, recipient: &T::AccountId, ) -> Result> { - ensure!(from != to, Error::::IdenticalAssets); + ensure!(from.is_valid_market_pair(to), Error::::InvalidPoolPair); ensure!(amount_in > 0, Error::::ZeroAmount); let system_account = Self::is_fee_exempt(who); let fee = if system_account { @@ -1344,7 +1386,7 @@ pub mod pallet { to: AssetKind, amount_out: Balance, ) -> Result<(PreparedRoute, Balance, Balance), Error> { - ensure!(from != to, Error::::IdenticalAssets); + ensure!(from.is_valid_market_pair(to), Error::::InvalidPoolPair); ensure!(!amount_out.is_zero(), Error::::ZeroAmount); let route = Self::find_optimal_exact_output_route(from, to, amount_out) .ok_or(Error::::NoRouteFound)?; @@ -1504,7 +1546,7 @@ pub mod pallet { "LP reverse-index cardinality disagrees with host pool storage", )); } - Ok(()) + T::LpPairIntegrity::validate_complete_topology().map_err(TryRuntimeError::Other) } } @@ -1517,8 +1559,8 @@ pub mod pallet { to: AssetKind, amount_in: Balance, ) -> Result> { - if from == to { - return Err(Error::::IdenticalAssets); + if !from.is_valid_market_pair(to) { + return Err(Error::::InvalidPoolPair); } if amount_in.is_zero() { return Err(Error::::ZeroAmount); @@ -1572,6 +1614,9 @@ pub mod pallet { fn build(&self) { // Ensure pallet account survives zero native balance (ED-free) frame_system::Pallet::::inc_providers(&Pallet::::account_id()); + T::LpPairIntegrity::validate_genesis_topology().unwrap_or_else(|error| { + panic!("DEOS pool genesis topology is invalid: {error}") // deos-bypass: panic-owner — invalid fresh genesis must fail hard before block production + }); } } } diff --git a/template/pallets/router/src/mock.rs b/template/pallets/router/src/mock.rs index ed015486..adfb9e4e 100644 --- a/template/pallets/router/src/mock.rs +++ b/template/pallets/router/src/mock.rs @@ -716,6 +716,7 @@ impl pallet_deos_router::Config for Test { type Assets = Assets; type TmcPallet = MockTmcPallet; type AssetConversion = MockAssetConversionAdapter; + type PoolLifecycle = (); type PalletId = PalletIdStub; type NativeAsset = NativeAsset; type DefaultRouterFee = RouterFeeStub; diff --git a/template/pallets/router/src/tests.rs b/template/pallets/router/src/tests.rs index d0717599..cb767df7 100644 --- a/template/pallets/router/src/tests.rs +++ b/template/pallets/router/src/tests.rs @@ -638,6 +638,28 @@ fn lp_reverse_index_is_canonical_bounded_and_collision_safe() { DeosRouter::register_lp_pair(8, (AssetKind::Native, AssetKind::Native)), Error::::InvalidPoolPair ); + let foreign_id = primitives::TYPE_FOREIGN | 9; + assert_noop!( + DeosRouter::register_lp_pair( + 8, + (AssetKind::Local(foreign_id), AssetKind::Foreign(foreign_id)), + ), + Error::::InvalidPoolPair + ); + assert_noop!( + DeosRouter::register_lp_pair(8, (AssetKind::Native, AssetKind::Local(foreign_id))), + Error::::InvalidPoolPair + ); + assert_noop!( + DeosRouter::register_lp_pair( + 8, + ( + AssetKind::Native, + AssetKind::Foreign(primitives::TYPE_PROTOCOL | 9) + ), + ), + Error::::InvalidPoolPair + ); for index in 1..500u32 { assert_ok!(DeosRouter::register_lp_pair( @@ -1135,7 +1157,20 @@ fn circular_swap_protection_test() { user, u64::MAX ), - Error::::IdenticalAssets + Error::::InvalidPoolPair + ); + let foreign_id = primitives::TYPE_FOREIGN | 1; + assert_noop!( + DeosRouter::swap( + RuntimeOrigin::signed(user), + AssetKind::Local(foreign_id), + AssetKind::Foreign(foreign_id), + amount, + 0, + user, + u64::MAX + ), + Error::::InvalidPoolPair ); }); } diff --git a/template/pallets/router/src/types.rs b/template/pallets/router/src/types.rs index 4bf49b07..ba43c78c 100644 --- a/template/pallets/router/src/types.rs +++ b/template/pallets/router/src/types.rs @@ -1,5 +1,4 @@ use crate::AdapterFailure; -#[cfg(feature = "runtime-benchmarks")] use polkadot_sdk::frame_support::pallet_prelude::DispatchResult; // Re-export AssetKind from primitives as the single source of truth @@ -60,6 +59,18 @@ pub trait TmcInterface { ) -> Result; } +/// Atomic host lifecycle for canonical XYK pool, LP identity, and Oracle topology creation. +pub trait PoolLifecycleApi { + /// Create one permissionless canonical pool as a single transactional state transition. + fn create_pool(who: &AccountId, asset_a: AssetKind, asset_b: AssetKind) -> DispatchResult; +} + +impl PoolLifecycleApi for () { + fn create_pool(_: &AccountId, _: AssetKind, _: AssetKind) -> DispatchResult { + Ok(()) + } +} + /// Asset conversion API for XYK pools pub trait AssetConversionApi { /// Get pool ID for asset pair @@ -119,6 +130,16 @@ pub trait LpPairIntegrity { fn expected_pool_count() -> Result, &'static str> { Ok(None) } + + /// Validate host-owned pool, LP, and observation topology in both directions. + fn validate_complete_topology() -> Result<(), &'static str> { + Ok(()) + } + + /// Validate host topology after all earlier pallet genesis builders have run. + fn validate_genesis_topology() -> Result<(), &'static str> { + Ok(()) + } } impl LpPairIntegrity for () {} diff --git a/template/pallets/router/src/weights.rs b/template/pallets/router/src/weights.rs index 35e2f60a..1474249d 100644 --- a/template/pallets/router/src/weights.rs +++ b/template/pallets/router/src/weights.rs @@ -22,11 +22,25 @@ pub trait WeightInfo { let direct = Self::direct_xyk_exact_input(); let mint = Self::direct_mint_exact_input(); let anchored = Self::native_anchored_exact_input(); + let direct_output = Self::direct_xyk_exact_output(); + let anchored_output = Self::native_anchored_exact_output(); Weight::from_parts( - direct.ref_time().max(mint.ref_time()).max(anchored.ref_time()), - direct.proof_size().max(mint.proof_size()).max(anchored.proof_size()), + direct + .ref_time() + .max(mint.ref_time()) + .max(anchored.ref_time()) + .max(direct_output.ref_time()) + .max(anchored_output.ref_time()), + direct + .proof_size() + .max(mint.proof_size()) + .max(anchored.proof_size()) + .max(direct_output.proof_size()) + .max(anchored_output.proof_size()), ) } + /// Temporary conservative envelope until canonical pool creation is benchmarked on the host. + fn create_pool() -> Weight { Weight::from_parts(1_500_000_000, 100_000) } fn direct_xyk_exact_input() -> Weight; fn direct_mint_exact_input() -> Weight; fn native_anchored_exact_input() -> Weight; diff --git a/template/pallets/session-rotation/Cargo.toml b/template/pallets/session-rotation/Cargo.toml new file mode 100644 index 00000000..5763b294 --- /dev/null +++ b/template/pallets/session-rotation/Cargo.toml @@ -0,0 +1,46 @@ +[package] +name = "pallet-deos-session-rotation" +version.workspace = true +description = "Bounded measured session-rotation hook" +authors.workspace = true +license = "MIT-0" +homepage.workspace = true +repository.workspace = true +edition.workspace = true +publish = false + +[lints] +workspace = true + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { workspace = true } +frame = { workspace = true } +polkadot-sdk = { workspace = true, features = [ + "frame-support", + "frame-system", + "sp-runtime", +] } +scale-info = { workspace = true } + +[dev-dependencies] +polkadot-sdk = { workspace = true, features = [ + "frame-support", + "frame-system", + "sp-io", +] } + +[features] +default = ["std"] +std = [ + "codec/std", + "polkadot-sdk/std", + "scale-info/std", +] +runtime-benchmarks = [ + "frame/runtime-benchmarks", + "polkadot-sdk/runtime-benchmarks", +] +try-runtime = ["polkadot-sdk/try-runtime"] diff --git a/template/pallets/session-rotation/README.md b/template/pallets/session-rotation/README.md new file mode 100644 index 00000000..5b944cdb --- /dev/null +++ b/template/pallets/session-rotation/README.md @@ -0,0 +1,14 @@ +# DEOS Session Rotation + +`pallet-deos-session-rotation` is a small FRAME pallet that gives a host runtime an explicit, benchmarkable Weight owner for session rotation. It contains no calls, storage, events, validator policy, or session implementation. + +- [Specification](docs/specification.en.md) +- [Architecture](docs/architecture.en.md) +- [Embedding](docs/embedding.md) + +Validate with: + +```bash +cargo test -p pallet-deos-session-rotation --features runtime-benchmarks +cargo clippy -p pallet-deos-session-rotation --all-targets --features runtime-benchmarks -- -D warnings +``` diff --git a/template/pallets/session-rotation/docs/architecture.en.md b/template/pallets/session-rotation/docs/architecture.en.md new file mode 100644 index 00000000..f57847c1 --- /dev/null +++ b/template/pallets/session-rotation/docs/architecture.en.md @@ -0,0 +1,17 @@ +# Session Rotation Architecture + +## 1. Package Shape + +`src/lib.rs` defines the FRAME pallet, the `SessionRotation` host boundary, the generated-Weight interface, and focused mock evidence. `src/benchmarking.rs` owns runtime-specific setup, measured rotation, and post-rotation verification. + +## 2. Execution + +`on_initialize(now)` asks the host schedule whether `now` is a rotation boundary. The false branch returns zero without invoking the rotator. The true branch invokes the rotator once and returns the generated rotation Weight. + +## 3. State and Failure Surface + +The package has no storage, calls, events, origins, or cleanup path. Rotation behavior and failure semantics remain those of the host adapter. An invalid host adapter is a runtime-composition defect rather than pallet-managed recoverable state. + +## 4. Weight Boundary + +The package's `WeightInfo` has one owner, `rotate_session`. The benchmark helper can establish host-specific maximum validator/key geometry without moving that policy into the reusable pallet. Database reads and writes performed by the host rotation are captured by generated benchmark evidence. diff --git a/template/pallets/session-rotation/docs/embedding.md b/template/pallets/session-rotation/docs/embedding.md new file mode 100644 index 00000000..8affacc1 --- /dev/null +++ b/template/pallets/session-rotation/docs/embedding.md @@ -0,0 +1,14 @@ +# Embedding DEOS Session Rotation + +## Host Obligations + +- Implement `SessionRotation` with one deterministic bounded schedule and one rotation operation. +- Disable any pre-existing hook that would independently rotate the same session. +- Keep session prediction APIs logically aligned with `should_rotate`. +- Implement `BenchmarkHelper` under `runtime-benchmarks` so setup reaches the host's maximum admitted active and queued session geometry. +- Generate and bind a production `WeightInfo`; `()` is suitable only for tests. +- Include the generated maximum in fixed block-resource accounting. + +## Reference Composition + +The concrete DEOS runtime adapter, periodic cadence, collator/session implementation, benchmark geometry, pallet index, and generated runtime Weight live in the root runtime integration boundary rather than this reusable package. diff --git a/template/pallets/session-rotation/docs/specification.en.md b/template/pallets/session-rotation/docs/specification.en.md new file mode 100644 index 00000000..741421c7 --- /dev/null +++ b/template/pallets/session-rotation/docs/specification.en.md @@ -0,0 +1,18 @@ +# Session Rotation Specification + +## 1. Scope + +The pallet owns one bounded `on_initialize` boundary for a host-defined session rotation. The host owns the rotation schedule, validator policy, session implementation, and production Weight evidence. + +## 2. Invariants + +- A block that is not selected by `SessionRotation::should_rotate` performs no rotation and returns zero Weight. +- A selected block invokes `SessionRotation::rotate` exactly once and returns exactly `WeightInfo::rotate_session`. +- The pallet stores no schedule, session state, validator state, or economic policy. +- A production runtime must bind a generated nonzero Weight owner measured against its maximum admitted session geometry. +- The schedule exposed through client-facing session-rotation prediction must remain logically identical to the schedule used by this hook. +- The underlying session pallet must not independently rotate the same block. + +## 3. Benchmark Contract + +With `runtime-benchmarks`, the host `BenchmarkHelper` prepares its maximum admitted session geometry before measurement and verifies the resulting rotation after measurement. The measured block contains only `SessionRotation::rotate`; setup and verification are excluded. diff --git a/template/pallets/session-rotation/src/benchmarking.rs b/template/pallets/session-rotation/src/benchmarking.rs new file mode 100644 index 00000000..8969dd13 --- /dev/null +++ b/template/pallets/session-rotation/src/benchmarking.rs @@ -0,0 +1,25 @@ +use crate::*; +use polkadot_sdk::frame_benchmarking::v2::*; + +#[benchmarks] +mod benches { + use super::*; + + #[benchmark] + fn rotate_session() { + T::BenchmarkHelper::prepare_rotation(); + + #[block] + { + T::SessionRotation::rotate(); + } + + T::BenchmarkHelper::verify_rotation(); + } + + impl_benchmark_test_suite!( + Pallet, + polkadot_sdk::sp_io::TestExternalities::default(), + crate::tests::Test + ); +} diff --git a/template/pallets/session-rotation/src/lib.rs b/template/pallets/session-rotation/src/lib.rs new file mode 100644 index 00000000..7a62b602 --- /dev/null +++ b/template/pallets/session-rotation/src/lib.rs @@ -0,0 +1,130 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +pub use pallet::*; +use polkadot_sdk::frame_support::weights::Weight; + +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; + +#[cfg(feature = "runtime-benchmarks")] +pub trait BenchmarkHelper { + fn prepare_rotation(); + fn verify_rotation(); +} + +pub trait SessionRotation { + fn should_rotate(now: BlockNumber) -> bool; + fn rotate(); +} + +pub trait WeightInfo { + fn rotate_session() -> Weight; +} + +impl WeightInfo for () { + fn rotate_session() -> Weight { + Weight::zero() + } +} + +#[frame::pallet] +pub mod pallet { + use super::*; + use frame::prelude::*; + + #[pallet::config] + pub trait Config: frame_system::Config { + type SessionRotation: SessionRotation>; + type WeightInfo: WeightInfo; + + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper: BenchmarkHelper; + } + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::hooks] + impl Hooks> for Pallet { + fn on_initialize(now: BlockNumberFor) -> Weight { + if !T::SessionRotation::should_rotate(now) { + return Weight::zero(); + } + T::SessionRotation::rotate(); + T::WeightInfo::rotate_session() + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use core::cell::RefCell; + use polkadot_sdk::{ + frame_support::{construct_runtime, derive_impl, parameter_types, traits::Hooks}, + frame_system, + }; + + type Block = frame_system::mocking::MockBlock; + + construct_runtime!( + pub enum Test { + System: frame_system, + SessionRotationPallet: crate, + } + ); + + #[derive_impl(frame_system::config_preludes::TestDefaultConfig)] + impl frame_system::Config for Test { + type Block = Block; + } + + thread_local! { + static ROTATIONS: RefCell = const { RefCell::new(0) }; + } + + pub struct MockRotation; + impl SessionRotation for MockRotation { + fn should_rotate(now: u64) -> bool { + now > 0 && now.is_multiple_of(10) + } + + fn rotate() { + ROTATIONS.with(|rotations| *rotations.borrow_mut() += 1); + } + } + + parameter_types! { + pub const RotationWeight: Weight = Weight::from_parts(123, 45); + } + + impl WeightInfo for RotationWeight { + fn rotate_session() -> Weight { + RotationWeight::get() + } + } + + #[cfg(feature = "runtime-benchmarks")] + impl BenchmarkHelper for MockRotation { + fn prepare_rotation() {} + fn verify_rotation() { + ROTATIONS.with(|rotations| assert_eq!(*rotations.borrow(), 1)); + } + } + + impl Config for Test { + type SessionRotation = MockRotation; + type WeightInfo = RotationWeight; + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = MockRotation; + } + + #[test] + fn rotates_only_on_schedule_and_returns_owned_weight() { + ROTATIONS.with(|rotations| *rotations.borrow_mut() = 0); + assert_eq!(Pallet::::on_initialize(9), Weight::zero()); + assert_eq!(Pallet::::on_initialize(10), RotationWeight::get()); + assert_eq!(Pallet::::on_initialize(11), Weight::zero()); + ROTATIONS.with(|rotations| assert_eq!(*rotations.borrow(), 1)); + } +} diff --git a/template/pallets/tmc/src/lib.rs b/template/pallets/tmc/src/lib.rs index 86022d28..736d33df 100644 --- a/template/pallets/tmc/src/lib.rs +++ b/template/pallets/tmc/src/lib.rs @@ -460,14 +460,7 @@ pub mod pallet { } fn assets_are_distinct(left: AssetKind, right: AssetKind) -> bool { - match (left, right) { - (AssetKind::Native, AssetKind::Native) => false, - ( - AssetKind::Local(left_id) | AssetKind::Foreign(left_id), - AssetKind::Local(right_id) | AssetKind::Foreign(right_id), - ) => left_id != right_id, - _ => true, - } + left.is_valid_market_pair(right) } fn ensure_asset_exists(asset: AssetKind) -> Result<(), DispatchError> { diff --git a/template/pallets/tmc/src/tests.rs b/template/pallets/tmc/src/tests.rs index 00ea308d..7e031147 100644 --- a/template/pallets/tmc/src/tests.rs +++ b/template/pallets/tmc/src/tests.rs @@ -613,6 +613,16 @@ mod tests { ), crate::Error::::InvalidParameters ); + assert_noop!( + TokenMintingCurve::create_curve( + RuntimeOrigin::root(), + AssetKind::Local(2), + AssetKind::Local(primitives::TYPE_FOREIGN | 1), + 1_000_000_000u128, + 1_000_000_000u128, + ), + crate::Error::::InvalidParameters + ); }); } diff --git a/template/primitives/src/assets.rs b/template/primitives/src/assets.rs index 391e6c22..7426a060 100644 --- a/template/primitives/src/assets.rs +++ b/template/primitives/src/assets.rs @@ -10,11 +10,11 @@ use alloc::vec::Vec; #[cfg(feature = "std")] use std::vec::Vec; -/// This enum serves as the single source of truth for asset types across all pallets, -/// enabling type-safe interactions between DEOS Router, TMC, Burn Actor, and other actors. +/// Semantic asset identity shared by DEOS domains. /// -/// - `Native`: The system's native token (managed by pallet-balances). -/// - `Local(u32)`: Local synthetic assets (managed by pallet-assets). +/// `Local(id)` and `Foreign(id)` are both physically held by `pallet-assets`; their semantic +/// distinction is canonical only when the ID namespace agrees with the variant. Use +/// [`AssetKind::ledger_key`] for physical-ledger comparisons. #[derive( Clone, Copy, @@ -42,10 +42,28 @@ pub enum AssetKind { Foreign(u32), } -impl From for AssetKind { - fn from(asset_id: u32) -> Self { - AssetKind::Local(asset_id) - } +/// Physical ledger identity used wherever economic distinctness matters. +#[derive( + Clone, + Copy, + Debug, + Decode, + DecodeWithMemTracking, + Encode, + Eq, + MaxEncodedLen, + Ord, + PartialEq, + PartialOrd, + TypeInfo, + Serialize, + Deserialize, +)] +pub enum LedgerAssetKey { + /// The native balance ledger. + Native, + /// One `pallet-assets` ledger keyed by its exact asset ID. + Assets(u32), } // Bitmask Architecture for Asset Classification @@ -80,6 +98,35 @@ pub trait AssetInspector { } impl AssetKind { + /// Return the physical ledger that owns balances for this semantic asset. + pub const fn ledger_key(self) -> LedgerAssetKey { + match self { + Self::Native => LedgerAssetKey::Native, + Self::Local(id) | Self::Foreign(id) => LedgerAssetKey::Assets(id), + } + } + + /// Whether this is the unique semantic representation admitted for its physical ledger. + pub const fn is_canonical(self) -> bool { + match self { + Self::Native => true, + Self::Local(id) => id & MASK_TYPE != TYPE_FOREIGN, + Self::Foreign(id) => id & MASK_TYPE == TYPE_FOREIGN, + } + } + + /// Validate two distinct canonical physical ledgers for market topology. + pub const fn is_valid_market_pair(self, other: Self) -> bool { + self.is_canonical() + && other.is_canonical() + && match (self.ledger_key(), other.ledger_key()) { + (LedgerAssetKey::Assets(a), LedgerAssetKey::Assets(b)) => a != b, + (LedgerAssetKey::Native, LedgerAssetKey::Assets(_)) + | (LedgerAssetKey::Assets(_), LedgerAssetKey::Native) => true, + (LedgerAssetKey::Native, LedgerAssetKey::Native) => false, + } + } + /// Deterministically derive the local staking-receipt asset ID for a given base asset. /// /// Native and local assets use the `0x5...` receipt namespace. Foreign assets use the dedicated @@ -139,11 +186,7 @@ impl AssetInspector for AssetKind { } fn is_foreign(&self) -> bool { - match self { - AssetKind::Foreign(_) => true, - AssetKind::Local(id) => (id & MASK_TYPE) == TYPE_FOREIGN, - _ => false, - } + matches!(self, AssetKind::Foreign(_)) } } @@ -257,6 +300,47 @@ mod tests { assert!(!not_lp.is_lp()); } + #[test] + fn local_and_foreign_same_id_share_ledger_identity() { + let id = TYPE_FOREIGN | 42; + assert_eq!( + AssetKind::Local(id).ledger_key(), + AssetKind::Foreign(id).ledger_key() + ); + assert!(!AssetKind::Local(id).is_valid_market_pair(AssetKind::Foreign(id))); + } + + #[test] + fn canonical_asset_representation_is_unique() { + let local_id = TYPE_PROTOCOL | 7; + let foreign_id = TYPE_FOREIGN | 7; + assert!(AssetKind::Local(local_id).is_canonical()); + assert!(!AssetKind::Foreign(local_id).is_canonical()); + assert!(AssetKind::Foreign(foreign_id).is_canonical()); + assert!(!AssetKind::Local(foreign_id).is_canonical()); + } + + #[test] + fn accepted_distinct_assets_have_distinct_ledger_keys() { + let accepted = [ + AssetKind::Native, + AssetKind::Local(TYPE_PROTOCOL | 1), + AssetKind::Foreign(TYPE_FOREIGN | 1), + ]; + assert!(accepted[0].is_valid_market_pair(accepted[1])); + assert!(accepted[0].is_valid_market_pair(accepted[2])); + assert!(accepted[1].is_valid_market_pair(accepted[2])); + assert_ne!(accepted[0].ledger_key(), accepted[1].ledger_key()); + assert_ne!(accepted[0].ledger_key(), accepted[2].ledger_key()); + assert_ne!(accepted[1].ledger_key(), accepted[2].ledger_key()); + } + + #[test] + fn noncanonical_asset_representations_are_rejected() { + assert!(!AssetKind::Foreign(TYPE_PROTOCOL | 9).is_canonical()); + assert!(!AssetKind::Local(TYPE_FOREIGN | 9).is_canonical()); + } + #[test] fn foreign_asset_isolation() { let foreign_asset = AssetKind::Foreign(TYPE_FOREIGN | 12345); diff --git a/template/runtime/Cargo.toml b/template/runtime/Cargo.toml index eaca7932..91f896b8 100644 --- a/template/runtime/Cargo.toml +++ b/template/runtime/Cargo.toml @@ -19,6 +19,10 @@ targets = ["x86_64-unknown-linux-gnu"] docify = { workspace = true } substrate-wasm-builder = { optional = true, workspace = true, default-features = true } +[dev-dependencies] +cumulus-primitives-parachain-inherent = "0.26.0" +cumulus-test-relay-sproof-builder = "0.27.0" + [dependencies] codec = { workspace = true } cumulus-pallet-parachain-system.workspace = true @@ -30,6 +34,7 @@ pallet-asset-registry.workspace = true pallet-deos-router.workspace = true pallet-governance.workspace = true pallet-oracle.workspace = true +pallet-session-rotation.workspace = true pallet-staking.workspace = true pallet-tmc.workspace = true primitives.workspace = true @@ -83,6 +88,7 @@ std = [ "pallet-deos-router/std", "pallet-governance/std", "pallet-oracle/std", + "pallet-session-rotation/std", "pallet-staking/std", "pallet-tmc/std", "primitives/std", @@ -101,6 +107,7 @@ runtime-benchmarks = [ "pallet-deos-router/runtime-benchmarks", "pallet-governance/runtime-benchmarks", "pallet-oracle/runtime-benchmarks", + "pallet-session-rotation/runtime-benchmarks", "pallet-staking/runtime-benchmarks", "pallet-tmc/runtime-benchmarks", "primitives/runtime-benchmarks", @@ -115,6 +122,7 @@ try-runtime = [ "pallet-deos-router/try-runtime", "pallet-governance/try-runtime", "pallet-oracle/try-runtime", + "pallet-session-rotation/try-runtime", "pallet-staking/try-runtime", "pallet-tmc/try-runtime", diff --git a/template/runtime/examples/actor_cost_vectors.rs b/template/runtime/examples/actor_cost_vectors.rs new file mode 100644 index 00000000..02835743 --- /dev/null +++ b/template/runtime/examples/actor_cost_vectors.rs @@ -0,0 +1,379 @@ +use deos_runtime::{AccountId, Actors, Balance, Balances, Oracle, Runtime, RuntimeOrigin, System}; +use pallet_deos_actors::{ + ActorContract, ActorCostQuote, ActorType, AmountResolution, AssetFilter, CompletionPolicy, + ContractSteps, CrossingDirection, FundingSourcePolicy, Mutability, SourceFilter, Step, + StepErrorPolicy, Task, Trigger, TriggerFamily, +}; +use polkadot_sdk::{ + frame_support::{BoundedVec, traits::Currency}, + polkadot_runtime_common::BuildStorage, + sp_io::TestExternalities, + sp_io::hashing::sha2_256, +}; +use primitives::{ + AssetKind, LocalPoolObservationMethod, OracleAggregationId, OracleFeedId, OracleProvenance, +}; +use serde_json::{Value, json}; +use std::{env, fs, path::Path, process}; + +const FORMAT: &str = "deos.actor.cost-vectors"; +const FORMAT_VERSION: u32 = 1; +const RUNTIME_API_VERSION: u32 = 1; +const FIXTURE_BALANCE: Balance = 1u128 << 100; + +type RuntimeSteps = ContractSteps; +type RuntimeTrigger = pallet_deos_actors::TriggerOf; + +fn hex(bytes: &[u8]) -> String { + bytes.iter().map(|byte| format!("{byte:02x}")).collect() // deos-bypass: bounded-iter -- fixed SHA-256 and runtime identity digests +} + +fn file_identity(path: &Path) -> String { + let bytes = fs::read(path).unwrap_or_else(|error| { + panic!( + "Actor cost evidence file {} is unreadable: {error}", + path.display() + ) + }); + hex(&sha2_256(&bytes)) +} + +fn trigger_family_name(family: TriggerFamily) -> &'static str { + match family { + TriggerFamily::Manual => "Manual", + TriggerFamily::AddressEvent => "AddressEvent", + TriggerFamily::ObservationChange => "ObservationChange", + TriggerFamily::ObservationCrossing => "ObservationCrossing", + TriggerFamily::AtTime => "AtTime", + TriggerFamily::Cadenced => "Cadenced", + } +} + +fn actor_type_name(actor_type: ActorType) -> &'static str { + match actor_type { + ActorType::User => "User", + ActorType::System => "System", + } +} + +fn weight_value(weight: polkadot_sdk::sp_weights::Weight) -> Value { + json!({ + "refTime": weight.ref_time().to_string(), + "proofSize": weight.proof_size().to_string(), + }) +} + +fn hold_breakdown_value(breakdown: pallet_deos_actors::ActorStateHoldBreakdown) -> Value { + json!({ + "identity": breakdown.identity.to_string(), + "contractHead": breakdown.contract_head.to_string(), + "contractBody": breakdown.contract_body.to_string(), + "detector": breakdown.detector.to_string(), + "funding": breakdown.funding.to_string(), + "run": breakdown.run.to_string(), + }) +} + +fn quote_value(quote: ActorCostQuote) -> Value { + let trigger = quote.prospective_trigger_fee.map(|trigger| { + json!({ + "family": trigger_family_name(trigger.trigger_family), + "maximumWeight": weight_value(trigger.maximum_weight), + "fee": trigger.fee.to_string(), + "productionWeightIdentity": hex(&trigger.production_weight_identity), + }) + }); + let pipeline = quote.prospective_pipeline_fee.map(|pipeline| { + json!({ + "machineFee": pipeline.pipeline_machine_fee.to_string(), + "cleanupFee": pipeline.cleanup_fee.to_string(), + "totalFee": pipeline.total_fee.to_string(), + "strategy": "UpfrontBounded", + "admissionIdentity": hex(&pipeline.admission_identity), + "productionWeightIdentity": hex(&pipeline.production_weight_identity), + }) + }); + json!({ + "actorType": actor_type_name(quote.actor_type), + "creationFee": quote.creation_fee.to_string(), + "prospectiveTriggerFee": trigger, + "prospectivePipelineFee": pipeline, + "maximumNextActionFee": { + "maximumEffectWeight": weight_value(quote.maximum_next_action_fee.maximum_effect_weight), + "maximumEffectFee": quote.maximum_next_action_fee.maximum_effect_fee.to_string(), + "productionWeightIdentity": hex("e.maximum_next_action_fee.production_weight_identity), + }, + "stateHold": { + "exempt": quote.actor_state_hold.exempt, + "basePerComponent": quote.actor_state_hold.base_per_component.to_string(), + "perEncodedByte": quote.actor_state_hold.per_encoded_byte.to_string(), + "components": hold_breakdown_value(quote.actor_state_hold.breakdown), + "total": quote.actor_state_hold.total.to_string(), + }, + }) +} + +fn transfer_steps(step_count: usize, destination: &AccountId) -> RuntimeSteps { + let step = Step { + precondition: None, + task: Task::Transfer { + to: destination.clone(), + asset: AssetKind::Native, + amount: AmountResolution::Fixed(1), + }, + on_error: StepErrorPolicy::AbortCycle, + }; + BoundedVec::try_from(vec![step; step_count]).expect("cost-vector Contract geometry fits") +} + +fn active_contract( + trigger: RuntimeTrigger, + steps: RuntimeSteps, +) -> pallet_deos_actors::ActorContractOf { + ActorContract { + trigger, + cooldown_blocks: 0, + window: None, + steps, + completion: CompletionPolicy::Persistent, + funding: FundingSourcePolicy::OwnerOnly, + auto_close_at_cycle_nonce: None, + } +} + +fn prefund_user_sovereign(owner: &AccountId, steps: &RuntimeSteps) { + let sovereign = Actors::sovereign_account_id(owner, 0); + let required = Actors::user_pipeline_machine_capacity_requirement(steps) + .expect("cost-vector Pipeline capacity computes"); + let _ = >::deposit_creating(&sovereign, required); +} + +fn create_user_vector( + name: &'static str, + owner_byte: u8, + trigger: RuntimeTrigger, + steps: RuntimeSteps, +) -> Value { + let owner = AccountId::new([owner_byte; 32]); + let family = trigger.family(); + let step_count = steps.len(); + prefund_user_sovereign(&owner, &steps); + let actor_id = Actors::next_actor_id(); + Actors::create_user_actor( + RuntimeOrigin::signed(owner), + Mutability::Mutable, + Some(active_contract(trigger, steps)), + ) + .expect("cost-vector User Actor creation succeeds"); + let quote = Actors::actor_cost_quote(actor_id).expect("cost-vector User quote computes"); + json!({ + "name": name, + "actorId": actor_id.to_string(), + "contractStepCount": step_count, + "triggerFamily": trigger_family_name(family), + "quote": quote_value(quote), + }) +} + +fn create_system_vector(destination: &AccountId) -> Value { + let owner = AccountId::new([240; 32]); + let steps = transfer_steps(1, destination); + let actor_id = Actors::next_actor_id(); + Actors::create_system_actor( + RuntimeOrigin::root(), + owner, + Mutability::Mutable, + Some(ActorContract { + trigger: Trigger::Manual, + cooldown_blocks: 0, + window: None, + steps, + completion: CompletionPolicy::Persistent, + funding: FundingSourcePolicy::RuntimePolicy, + auto_close_at_cycle_nonce: None, + }), + ) + .expect("cost-vector System Actor creation succeeds"); + let quote = Actors::actor_cost_quote(actor_id).expect("cost-vector System quote computes"); + json!({ + "name": "system-manual-1", + "actorId": actor_id.to_string(), + "contractStepCount": 1, + "triggerFamily": "Manual", + "quote": quote_value(quote), + }) +} + +fn create_dormant_vector() -> Value { + let owner = AccountId::new([241; 32]); + let actor_id = Actors::next_actor_id(); + Actors::create_user_actor(RuntimeOrigin::signed(owner), Mutability::Mutable, None) + .expect("cost-vector dormant User Actor creation succeeds"); + let quote = Actors::actor_cost_quote(actor_id).expect("cost-vector dormant quote computes"); + json!({ + "name": "user-dormant", + "actorId": actor_id.to_string(), + "contractStepCount": null, + "triggerFamily": null, + "quote": quote_value(quote), + }) +} + +fn manifest() -> Value { + let destination = AccountId::new([250; 32]); + let owners = (1u8..=20) + .map(|byte| (AccountId::new([byte; 32]), FIXTURE_BALANCE)) + .chain([ + (AccountId::new([240; 32]), FIXTURE_BALANCE), + (AccountId::new([241; 32]), FIXTURE_BALANCE), + (destination.clone(), FIXTURE_BALANCE), + ]) + .collect::>(); + let mut storage = polkadot_sdk::frame_system::GenesisConfig::::default() + .build_storage() + .expect("cost-vector System genesis builds"); + polkadot_sdk::pallet_balances::GenesisConfig:: { + balances: owners, + ..Default::default() + } + .assimilate_storage(&mut storage) + .expect("cost-vector balances genesis assimilates"); + pallet_deos_actors::GenesisConfig::::default() + .assimilate_storage(&mut storage) + .expect("cost-vector Actors genesis assimilates"); + + let mut ext = TestExternalities::new(storage); + let vectors = ext.execute_with(|| { + System::set_block_number(1); + let feed = OracleFeedId::directional_local_pool_price( + AssetKind::Native, + AssetKind::Local(1), + LocalPoolObservationMethod::PreExecutionSpot, + OracleAggregationId::Ema { + half_life_blocks: 1, + }, + 12, + ); + Oracle::register_feed( + RuntimeOrigin::root(), + feed, + destination.clone(), + feed.meaning(), + OracleProvenance::DeosRouterPreExecutionReserves, + 12, + pallet_oracle::Aggregation::LastValue, + pallet_oracle::ZeroPolicy::Reject, + false, + ) + .expect("cost-vector observation feed registers"); + Oracle::publish(RuntimeOrigin::signed(destination.clone()), feed, 100) + .expect("cost-vector observation publishes"); + + let mut vectors = vec![ + create_user_vector( + "user-manual-0", + 1, + Trigger::Manual, + transfer_steps(0, &destination), + ), + create_user_vector( + "user-manual-1", + 2, + Trigger::Manual, + transfer_steps(1, &destination), + ), + create_user_vector( + "user-manual-4", + 3, + Trigger::Manual, + transfer_steps(4, &destination), + ), + create_user_vector( + "user-manual-8", + 4, + Trigger::Manual, + transfer_steps(8, &destination), + ), + create_user_vector( + "user-manual-32", + 5, + Trigger::Manual, + transfer_steps(32, &destination), + ), + create_user_vector( + "user-address-event-1", + 6, + Trigger::AddressEvent { + source_filter: SourceFilter::Any, + asset_filter: AssetFilter::Any, + }, + transfer_steps(1, &destination), + ), + create_user_vector( + "user-observation-change-1", + 7, + Trigger::ObservationChange { feed }, + transfer_steps(1, &destination), + ), + create_user_vector( + "user-observation-crossing-1", + 8, + Trigger::ObservationCrossing { + feed, + direction: CrossingDirection::Rising, + threshold: 200, + rearm_threshold: 100, + }, + transfer_steps(1, &destination), + ), + create_user_vector( + "user-at-time-1", + 9, + Trigger::AtTime { after_ticks: 10 }, + transfer_steps(1, &destination), + ), + create_user_vector( + "user-cadenced-1", + 10, + Trigger::Cadenced { every_ticks: 10 }, + transfer_steps(1, &destination), + ), + ]; + vectors.push(create_system_vector(&destination)); + vectors.push(create_dormant_vector()); + vectors + }); + + json!({ + "format": FORMAT, + "formatVersion": FORMAT_VERSION, + "runtimeApiVersion": RUNTIME_API_VERSION, + "metadataSha256": file_identity(Path::new("../web-client/.papi/metadata/deos.scale")), + "weightSha256": file_identity(Path::new("runtime/src/weights/pallet_deos_actors.rs")), + "vectors": vectors, + }) +} + +fn usage() { + eprintln!( + "Usage: cargo run -p deos-runtime --example actor_cost_vectors -- [PATH | --check PATH]" + ); +} + +fn main() { + let rendered = serde_json::to_string(&manifest()).expect("Actor cost vectors serialize") + "\n"; + let args = env::args().skip(1).collect::>(); + match args.as_slice() { + [] => print!("{rendered}"), + [path] => fs::write(Path::new(path), rendered).expect("Actor cost vector artifact is writable"), + [flag, path] if flag == "--check" => { + let actual = + fs::read_to_string(Path::new(path)).expect("Actor cost vector artifact is readable"); + assert_eq!(actual, rendered, "Actor cost vector artifact is stale"); + } + _ => { + usage(); + process::exit(1); + } + } +} diff --git a/template/runtime/examples/trigger_bond_vectors.rs b/template/runtime/examples/trigger_bond_vectors.rs deleted file mode 100644 index 0d90c120..00000000 --- a/template/runtime/examples/trigger_bond_vectors.rs +++ /dev/null @@ -1,85 +0,0 @@ -use std::{env, fs, path::Path}; - -use deos_runtime::{Balance, Runtime}; -use pallet_deos_actors::{ - TriggerOf, - types::{AssetFilter, CrossingDirection, SourceFilter, Trigger}, -}; -use serde_json::json; - -fn hex(bytes: &[u8]) -> String { - bytes.iter().map(|byte| format!("{byte:02x}")).collect() // deos-bypass: bounded-iter -- fixed SHA-256 digest -} - -fn file_identity(path: &Path) -> String { - let bytes = fs::read(path) - .unwrap_or_else(|error| panic!("failed to read identity input {}: {error}", path.display())); - hex(&polkadot_sdk::sp_io::hashing::sha2_256(&bytes)) -} - -fn amount(trigger: TriggerOf) -> Balance { - deos_runtime::actors_trigger_state_bond(&trigger) -} - -fn main() { - let mut args = env::args().skip(1).collect::>(); - let check = args.first().is_some_and(|arg| arg == "--check"); - if check { - args.remove(0); - } - assert_eq!( - args.len(), - 1, - "usage: trigger_bond_vectors [--check] " - ); - let output = args.remove(0); - let output = Path::new(&output); - let repository = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); - let metadata = repository.join("web-client/.papi/metadata/deos.scale"); - let weights = repository.join("template/runtime/src/weights/pallet_deos_actors.rs"); - let feed = deos_runtime::actors_trigger_bond_vector_feed(); - let vectors = [ - ("Manual", amount(Trigger::manual())), - ( - "AddressEvent", - amount(Trigger::address_event(SourceFilter::Any, AssetFilter::Any)), - ), - ( - "ObservationChange", - amount(Trigger::observation_change(feed)), - ), - ( - "ObservationCrossing", - amount(Trigger::observation_crossing( - feed, - CrossingDirection::Rising, - 2, - 1, - )), - ), - ("Cadenced", amount(Trigger::cadenced(1))), - ]; - let value = json!({ - "formatVersion": 1, - "metadataSha256": file_identity(&metadata), - "actorsWeightSha256": file_identity(&weights), - "vectors": vectors.into_iter().map(|(trigger_family, amount)| json!({ - "triggerFamily": trigger_family, - "amount": amount.to_string(), - })).collect::>(), - }); - let encoded = format!( - "{}\n", - serde_json::to_string_pretty(&value).expect("JSON encoding succeeds") - ); - if check { - let current = fs::read_to_string(output) - .unwrap_or_else(|error| panic!("failed to read {}: {error}", output.display())); - assert_eq!(current, encoded, "Trigger bond vectors are stale"); - } else { - if let Some(parent) = output.parent() { - fs::create_dir_all(parent).expect("output parent creation succeeds"); - } - fs::write(output, encoded).expect("Trigger bond vector write succeeds"); - } -} diff --git a/template/runtime/fixtures/maximum-context-basic.scale b/template/runtime/fixtures/maximum-context-basic.scale new file mode 100644 index 0000000000000000000000000000000000000000..287efefcc5b5df4c9f36681739b968824203b06d GIT binary patch literal 13669 zcmcJUWmHu`*r*TP9n#Vu4T2&_NVkBLbVzq2IdrEWAR*EzsUn@yAR#5Cgra~nO5T0G zxa<3Mzy0U#wH^*@G0(iS=ggk{&It)YALCIJ81q@v?W71YHSc5fCBHD;H zUD24+dyVm-Hp{3p(9i$6kkWr%C|p7EQhv8sEUou<@8nAMhYT208N|JC)oVR!`(8#< zLtTeJFhWg<5!grp9RiXupbsRN{{{5%9~TZb3M#U1R0uQ(Wb^-ShXwtP>;ne^7Xl9g zIWPhULI@%VVh9olQV22#atI0tN{Cw!R1nk=7$KM-m?2moSRvRT z*daI|I3eypa6xcG@IdfF@ImlH2tWuz2tnM15QY$exCe0`LKNZwgcyW4#6t)P2uTPj z2x*8%5Hb+55ONUm5DE~A5K0ir5GoL=5NZ(W5E>AVAv7VhAhaQ#K6d4dDaf3*iUx9Ks(W03r|~2qG9F1Oh-5qpuwB$sBD&Z`Vs_R3;jCJKR;iO{=-M z^f#2A92}iTA-{*NahzH`k(FKKtio#l5b=-0@1qWK2H~|QA(pY5p6paHr1;I*G|_YFPzj$AqL z+HWOc#ro@G!EV1jrXQUdf(o;JmjptC&9Lv?5%RHuGayKaj{W6EWPp5bVG6MvZmTxy z$TbU3K)Kg*6M#90+!5djs3>v6XX)L@M-!$nxc;}Wvktizz!XmV!iIx>BR5`{!kL9f zX@x0rcZ4UP5#We549y|8Fn9tw0^`_f6c4#~!W7h{Sd3df62TXu#Rl<}t&uw#%m`bW zQAHm;kXtuA0pqqnkh#|ua_@vGG$kd8&7dPUcbLMmiGSKlYgX6-=7IG;JVhTx*aFti zT901ZSIn>l9DmySk^x!d%MIQ=-~{*5tNgq{zR6$;15{Xp9<|8E@B~~=hQwyH5H$FL zNYjtofgh2H3Csvde{XhcZzJEb@C3ZD;#f2XToTv>#sP zco8VDFmTHpN|5OpOksfgrs}OKGOvUu&;+JvB!-0{lSX&~D_t66F;fH`Y=MKD2P3^c z1es34i@+gtzWDWDE%FQiPvG3^=Nj_yWq~bl@#Uux2y-E`YIqU2jkIp|nY57UFFZlO zQu)Jp!lzrX1rdD7J*G(|WO5EK!h=e?w#oTmKG=eo;xFMhUVo6s4tNnBZoCwFnH7jU zYrzvFXx}r`Kkwj&FIZO8y?D_f3|o+-5f-UE%R(OW;0;OgcDf|hB#kX>LE*Hd;))0p zd8mOGL9r&HB5`#^3$~y_;D~SQiYfqG(7=o9w^hm`hA#*%2AV(iLmu4V-NVxi1!|ws zLFB0po?tXAVpZl9i#&$H6U=@6bodkrL}3dy;q}6o+*ZhgIJ^k9l{4LkVh*IR1-qx= zUHFw^$Rj$u2(BZgxCDy_$TKlK!L4%n;K{xmK5W6A_qhqV{8KLYf_5`b?btI;*n($A z!_&U;OL5qOR~mn2Ri!kta02h9f!-FK;4B%kA^=Ya4Ksf9mF@>NY#}_A$|Zm{9$5#0 z7a>m0oKz~a8XdNfkV%B*CyA;GTSy)Wlamo_qK7S{N_)xgORrPF7G7*hosIGwslXOq zzCP*rV@iaqKEa!%mzSDv)z!-hU<+A~w$OXoDluUTue#gD`vu65g)4YN%2D7rDjaiU zfGycJCgzj&zJ`g`{tY@x1!<8`DIxd?2b z{<>Oscw~hUw(!x-eeu153L9*p#qfIMqhSlOLJRMvTfFyh(+CNvU<<9rw^YAu&LL~J z@FKL|9z7)BcR?0WVG2UvCr7U>vhoU3$S0fnq5sbew$P_}XY_E!_cm-{=%h4DP1s2Y zwlFg4EJ>AW3SbMhL0Lh3OxK>Ta6D7Jd#mH#|A?84}{ zxFyx3^j`Eva%LBrn1981YVj(4?p@TvhGl7DWg|+ijUgyYH28r zfc}3|{z_31+^0UNtLJgY=<@8xl6$HoL)POisnn#6oQAPGk6pvVPNY?T{X{4ZpPqFG zwC_0AuuO|MG*I1%F-^lkkz&MLk%-fo3YM$*GEVi9>X5goDKA5aAcZMgEs}_>c|CIH z>(7yA$=0)feu|va3SDI+SR5Nm8C}J;;xE4)cw;;D07Xiaz#UJZov#W_dwG=ok^h@? z+J|iXBX{PLZXQc)uE_cH{?9S}(@>A+4~fGzc(OMa`p_pDYnp;R^|=(-wx|E;6DRIF zXokP#_dHMX236+CN5Y7-z)t|WlmF-S043zsSwfbF#8QfOuw8yl$NXr>=4-*ogng#p zN$WY|iAV3+)HSjj956*9U(uQ0Y16M#P>$F7@z+(8u{m>c8^wBiYk_vn*8>ntxu59I zwioSx(jI}UV<+I7gXep$?dYT2Cx&HKTquw1h8gS>G~lkfXG*lBMp4rC1wX2;$@$a! z&fCDCZ%zF`rsOf`oAGwC-eQfuKaG<3%gx8QzFU0s2zv|5D}>o3>FLD<)D1c@nBYGx z`BS_(Sv!NSs%og_b~3BRgcnJ>PH~Jf1;(Eg0nN{Rm(+s2va`h}5m@<$-mLU!PS@*$ z)Ac&OOGRnTp4lh>69CqXSBX!ORY+ZC{6jt2_xebWw`b5yFFq+B&Y}0;IeP$H&)$Ub zH`x|_?92-0df> zjCjucIGoqp92zb*DnMnPy}Q42Yh3**?5e!{AZ_&M7TPD;9_oK=S$M3koS4C2icy$O zpESXY-K$LV55ZU1(v`ov=n0Z#g&k_lX};zFrhBd5rY{yf49XKyu8i#mKm||SS;%X_%Mc_ z$6fr!$;Dp9#-e6J1Gu^%rnEOJdey|~smACl!SxrUgaHqBiFY$AAES8W-2eRL4REVT zj`fwpuDhO@s&~^B(H-WH!kqqiwK8yGn{g1k6%TkY$<66Io7(c49ac)m1c?9otRh<0 zs#A?k_KV0ARrSG#HKwZjW#S{(N|HJ$tw|C=o&M+tFC}HsGoogmWb)aAvm{KmE74xr z6i<--zPw~u_QIgq*7u&FP1)IdZJLud5W_|)8R1_yTDx88KBmp}{Rb{}+JZ~2t6EN# zvp2&a@)^NL0KexSX%BTfj;_n>@%>%DJ2~rbd}XTh3WgB-PGi_s)cffTYhFzF#l@L(}~Lz97I|3y&Z>e zeMjy!OA`=ndZ_Gu6;2-Dd=aceGmmyfQ)QOGi!L#+t<8~jqf!b@Ea^9pWLz!%$64e~ z0S#jGNWI;J#k!y(7ZyPng`|_SIzT@}>z#%NLr88^OV4;`}RkO?P$_G4$KMK&x^e{@<2bNxb z*3uy1JKXu4L%a4VNo%orx;YiNHTz;3*gBY+tV~B%;X3;7Ys2j!Rz}Sr*%#b zBK}AXG4>hP-Zj9vUsE^vzMbfEdv1+16??vYw-KwTJ2WHY2=KmVk8D`;%?LLlHHWxZ z&^!KS`6livPba{pYqwEve;15z_F-3;F_z6@Gx9iE1-mxn=`hAH2m~=Z`BkmirlCSJ zLZJX!>gQLh^CmEb9L;#`!zT_5*USn(vyI|TytH3zOdGHUGn)Q0)-yJ4v}CP0Q>!wj zYm1h}Bp2BkB$RB2mgR?^fa=PmC#L5Cj+Id7&W4kr{4VYb7Wi0cN$R{lazBI*I;T|GO%H8=9S4Ch0O)c2qQX}2<9)k zkbC+eH`kaYqN;GZw6uyy^Ma+|i=K`H-GiCmAnnHk+dpzxk(4CUMBD5YVeO9Z4=Kvn z7x=RA)I3>yE}4Z55FZ5-{F;z7g~Aw{#vB$u6c*vznvS*lI@= zd&{XsJQ5Q`4RD1XPqWs9-qEYJt-Z8Dpl?{%77V)8P_`&Hl41hwMv#LRx{BD|Bg zIstr^*#KOhYIoZ&mV-Q;6szL$=do-qH38N;?z$&C?&i^q9_($N#5Xa4ExI z;Y-nS4?Xo7!=s`}-;0_4d{Bx54fhcOoL+L|@2*8UyKz1XKSiVZLGHzZUY1wqU8`b| zZk3Ri1bzlr);u;yU|USy@Jn!fnbWcAvO?BK^?3bu>zqnAWIA=TSG@VPUf%883Egc5TRFe>RTdO2WV~G({BxIM~C2_&%d#*N;$Yxcnw^g zvWx9>7lP)vc2Xto6>aZpkpR`qw6`t67MkQ&Oy3I+WWWCq-ERgaLU;0Q~+QLuq2O=(In58x8JD>NLocBoVjNOy{4dCrU&* z1BM~{AB=yy?)MaGqT#ltG)Ib{6wlj>^O!$-Xg%%}6bg-0vk=^&zY#2@qd%b!k7}pA zyIJ>#g>S<2?+<>{mXl{Gws@JKZ`ehFV@hJV1uaAc6)nn>Jup9SKd{%1F{lMk^Uc&l&@oN(G|)$$Wbrrjs`eW_D&NnO$S6pvOHB)2$~ z&394=2uku4tx@9?oKUXWt4YQ+ny83~DzLloL3@RT4&eV_`haWr8}G!CNCygyMVHBH z^uylQOolZ=pBoW?rxsiji}<9h1*T^OVkP%SE2$=vOT{G>Z5a{mb0_}cq)h^3I6J`| zEgLn5P~q{~nLwj!a!(30(UI8V3t~n&HX?J84 zYMswQw?ylts%zUP`iN4kLIyX|It}&TgOtaWA7V$;D5{bLbDO0TF3&BT##~?WjS8|V zE90bgs{j<6C)?EN*nJpMnNFxt55+R?u6X3{JBU4GJ zdA5F=KFi}eR5DP{izMDHweshMn4S!nH1Ihl)vm*$jI1y?Zp>X9jh^FNf|6b1HC+qq6d&PZMHK4_&bq@FeUGQxc26^XcPoMVr25BPLj%pvAu+v~rS&}6&K4V+vEmelmPIU$ zD!2h4wW7ihhf8^6yUvyEuY>($bh3mneV&KEMNfkm>j6pyAowiIzMPEQjZQ&Ye6F7t@oY&wi3U4Q(uB9Ps6|DZf*NeZ6M=x)wb$pg70cf=e10#A%&)t+9U)^o7`Td1u3voDHog^Y2~ zqQWtksr5!D$(SU2RtYUO*bUAr4fvYk=+YcmRgg)HK#uVAU1tO7bR| z*xG96Y%(E*lXLDqms|7md-gay6DhtbQy{0jA(4Axc~mm#IJ_j~Pds?X^DFxciO#=I z;vGlP=#tR)`UVa3uTGM4|0E2s4eMyoPVBb*RUXTj#P*<-AxhdtO-UvTq$#8e+bk}6 z;h0u=9VzhWX|~kO?`8Z>RM$p5yS%wNH`?ggS%w!8*3wR-?-! zz5{Ml22k7I9dLAKDfl)6YxLssVs&pW4j(JM9~A9Alx) zr=dIHH&&hRGc8yV(IooWYN4=xGxxeAOBjV5q{@Eh`(DX7fVIykdqTp%X<#pgN~Z5C zm%*a2`P`F)5BgraF#)QED)+!YcBi%E)J1Uf*+`J(F__A{YgI3EcK#~=cMUL1Y^VxX zRnNGO${$0Fh|>9|FdAD(;N#LMGPJ!?m7f4MZ&J*^_`Y-&be;HqVJh@(8+G`eqOY2S zZAsv@H=lGN;K+TEl{364BK84sk>x}rM7+4HaY|dPJ#Sb)dMf6E5Bkn*4F?N-)U>Xi zaS-WtCf!2aaK*<(2$s=TYN0jaHSwZ@&-~!YV{xCpFCW++MfQ4Q{_$sPn;>y0qN{E7 zI;ZpeR+uXZN)sD%nXFt#TP@tZq+c2>^s}O|<*ZaHMW|84C)APN0|5gJV}6ae3<5$# zKhlHC4(8h2-WsAZp9RV^Cp0k#Cv+qWyL=H0YB4g^`yf}1jY+%L_UmF)<5giiA>vR^M8cF%|F|2=Lc}M zzl?8PvQDfI}ROkBHABNy73wQTN%g43OdAI z8U|gUgIX~C zrt>r3ty5=uT8bept3%r*XVO|Qe4-UIlr458BgKD9x!@@n+^k_wxl z{QG;MldjiUcFb{C;raYXr<4WyX2POQmH=-^AaLhL#n4c!p;V6uK zdA4OeW<)Wzl75eQ*Eiyz?TV4Wi5evo6OjUbq_91ij8<&)rcIv#lfRgwO&02p&h}=# z)h8P)4Bw+IH<&cYhsY$-J3a8(Y&InUdz$;_*Nnc zi3_I-L5(^67JElRP=7l}%5v+e72V$-GV<95rHB^^){g53k7!oU+7g^bd{R+jQ4s&% zFD;aOf&$}`Y_c}dax09KL(4!^Y_QVeZD4x3q=kDc?o*F}G43byAO5-~)mu*T{)drt z1lAxYaMvI-ZQ&7h&V0pLj4z|M_b=Dxs3#%mc<$_KEk6bmv0r1FtKEdfxk%~fmiE6I zN0GR>0W|M?zuD^LSLC(?1>)JM@uc{%*PmK-;sH?Q3W&c$?v)@|!UcozBhJIW>>M5zy7v*v(W8&5X z5+7n=K8qPTa37of$R9K7P1|?hXD6Qh{HXO<9ftO&jT+GiybRUj%qqJ%K}ln!@muC{FYl zDk`_vT>4$EyS2)NJc;+x7j~9zD&7IjySL=4S6#~v!>+uGjNX)%Rqu=XVeN8@R@~pu zv1%3vDkd_P8@lrP2#dIH51no1@VsIP>Mge$0zMqbChz8HgX3ihny{sSBE!uDcO5y& zs4a>e9}HsVh^G}=l#GkbeZabfWmG<<{%v{WTiqx^?3NHJdP%7nGKI?`lg+Wp^poU!(gV^<oy#~EK1-i~)kSseT9 z^p|PGak(eBzM<5VtVex~|Nh7Y)xVW7b>bZf?mO&|3UoU&ObVli3*|$b+l5kJmdHwW z8UOnkurtFJbm~klSPbK0tftfLq+S2Ne*(ol1JTMH1Z8h=zO}ZV&S;h2v{gvtaIcc$ z{cPmTJ|h9wP9top5&Kh&eD5&YS6}Ol sp_inherents::CheckInherentsResult { + let mut result = sp_inherents::CheckInherentsResult::new(); + result + .put_error( + CONTEXT_GEOMETRY_INHERENT_IDENTIFIER, + &sp_inherents::MakeFatalError::from(()), + ) + .expect("a fresh fixed-size context-geometry error must encode"); // deos-bypass: panic-owner — CheckInherentsResult is fresh and unit MakeFatalError has fixed infallible SCALE encoding. + result +} + +pub(crate) fn validate_context_inherent_geometry( + data: &sp_inherents::InherentData, +) -> Result<(), pallet_deos_actors::BlockResourceError> { + use frame_support::inherent::ProvideInherent; + + let Some(cumulus_pallet_parachain_system::Call::set_validation_data { + inbound_messages_data, + .. + }) = ::create_inherent(data) + else { + return Ok(()); + }; + validate_inbound_messages_geometry(&inbound_messages_data) +} + +pub(crate) fn validate_inbound_messages_geometry( + inbound_messages_data: &cumulus_pallet_parachain_system::parachain_inherent::InboundMessagesData, +) -> Result<(), pallet_deos_actors::BlockResourceError> { + use pallet_deos_actors::{ContextMessageGeometry, ContextMessageLimits}; + + let (downward_full, downward_hashed) = inbound_messages_data.downward_messages.messages(); + let (horizontal_full, horizontal_hashed) = inbound_messages_data.horizontal_messages.messages(); + let downward_full = u32::try_from(downward_full.len()) + .map_err(|_| pallet_deos_actors::BlockResourceError::ArithmeticOverflow)?; + let downward_hashed = u32::try_from(downward_hashed.len()) + .map_err(|_| pallet_deos_actors::BlockResourceError::ArithmeticOverflow)?; + let horizontal_full_count = u32::try_from(horizontal_full.len()) + .map_err(|_| pallet_deos_actors::BlockResourceError::ArithmeticOverflow)?; + let horizontal_hashed_count = u32::try_from(horizontal_hashed.len()) + .map_err(|_| pallet_deos_actors::BlockResourceError::ArithmeticOverflow)?; + let limits = ContextMessageLimits::new( + MaxInboundDownwardMessagesPerContext::get(), + MaxInboundHorizontalMessagesPerContext::get(), + MaxInboundHorizontalChannelsPerContext::get(), + ); + limits.validate(ContextMessageGeometry::new( + downward_full, + downward_hashed, + horizontal_full_count, + horizontal_hashed_count, + 0, + ))?; + let mut horizontal_channels = BTreeSet::new(); + for sender in horizontal_full + .iter() // deos-bypass: bounded-iter — aggregate HRMP geometry is validated above and the channel set rejects at runtime maximum plus one. + .map(|(sender, _)| *sender) + .chain( + horizontal_hashed + .iter() // deos-bypass: bounded-iter — the same admitted full-plus-hashed HRMP bound owns this traversal. + .map(|(sender, _)| *sender), + ) + { + horizontal_channels.insert(sender); + if horizontal_channels.len() > MaxInboundHorizontalChannelsPerContext::get() as usize { + return Err(pallet_deos_actors::BlockResourceError::ContextGeometryExceeded); + } + } + limits.validate(ContextMessageGeometry::new( + downward_full, + downward_hashed, + horizontal_full_count, + horizontal_hashed_count, + u32::try_from(horizontal_channels.len()) + .map_err(|_| pallet_deos_actors::BlockResourceError::ArithmeticOverflow)?, + )) +} impl Runtime { #[docify::export] @@ -133,13 +217,34 @@ impl_runtime_apis! { } } - impl pallet_deos_actors::ActorEligibilityApi< - Block, - primitives::OracleFeedId, - BlockNumber, - pallet_deos_actors::TriggerOf, - Balance, - > for Runtime { + impl pallet_deos_actors::ActorCostApi for Runtime { + fn actor_cost_quote( + actor_id: pallet_deos_actors::ActorId, + ) -> Result< + pallet_deos_actors::ActorCostQuote, + pallet_deos_actors::ActorCostQuoteError, + > { + Actors::actor_cost_quote(actor_id) + } + } + + impl pallet_deos_actors::ActorResourceApi for Runtime { + fn block_resource_budget() -> pallet_deos_actors::BlockResourceBudget { + crate::configs::BlockResourceBudgetValue::get() + } + + fn current_block_resource_state() -> Option> { + Actors::block_resource_state() + } + + fn finalized_block_resource_snapshot( + ) -> Option> { + Actors::finalized_block_resource_telemetry() + } +} + +impl pallet_deos_actors::ActorEligibilityApi + for Runtime { fn actor_eligibility( actor_id: pallet_deos_actors::ActorId, ) -> Result< @@ -149,34 +254,23 @@ impl_runtime_apis! { Actors::actor_eligibility(actor_id) } - fn materialization_faults() -> ( - Option>, - Option>, - Option>, - ) { - ( - Actors::crossing_worker_fault(), - Actors::observation_fanout_worker_fault(), - Actors::wakeup_worker_fault(), - ) + fn materialization_faults() -> pallet_deos_actors::MaterializationFaults { + pallet_deos_actors::MaterializationFaults { + crossing: Actors::crossing_worker_fault(), + fanout: Actors::observation_fanout_worker_fault(), + wakeup: Actors::wakeup_worker_fault(), + } } - fn crossing_capacity(feed: primitives::OracleFeedId) -> (u32, u32, u32, u32) { - ( - >::get(), - >::get(), - Actors::crossing_user_feed_membership_count(feed), - Actors::crossing_feed_membership_count(feed), - ) + fn crossing_capacity(feed: primitives::OracleFeedId) -> pallet_deos_actors::CrossingCapacity { + pallet_deos_actors::CrossingCapacity { + user_limit: >::get(), + total_limit: >::get(), + user_memberships: Actors::crossing_user_feed_membership_count(feed), + total_memberships: Actors::crossing_feed_membership_count(feed), + } } - fn trigger_state_bond(trigger: pallet_deos_actors::TriggerOf) -> Balance { - , - Balance, - >>::amount(&trigger) - } } impl primitives::TmctolReadModelApi for Runtime { @@ -202,6 +296,9 @@ impl_runtime_apis! { block: ::LazyBlock, data: sp_inherents::InherentData, ) -> sp_inherents::CheckInherentsResult { + if validate_context_inherent_geometry(&data).is_err() { + return context_geometry_fatal_result(); + } data.check_extrinsics(&block) } } diff --git a/template/runtime/src/benchmarks.rs b/template/runtime/src/benchmarks.rs index 84f85d02..3ab413ac 100644 --- a/template/runtime/src/benchmarks.rs +++ b/template/runtime/src/benchmarks.rs @@ -14,6 +14,7 @@ polkadot_sdk::frame_benchmarking::define_benchmarks!( [pallet_asset_registry, AssetRegistry] [pallet_governance, Governance] [pallet_oracle, Oracle] + [pallet_session_rotation, SessionRotation] [pallet_staking, Staking] [pallet_deos_actors, Actors] ); diff --git a/template/runtime/src/configs/actor_config.rs b/template/runtime/src/configs/actor_config.rs index 7850249a..5f1c4829 100644 --- a/template/runtime/src/configs/actor_config.rs +++ b/template/runtime/src/configs/actor_config.rs @@ -11,19 +11,25 @@ use codec::Encode; use primitives::{AssetKind, ecosystem}; use polkadot_sdk::frame_support::traits::{ - Currency, Get, ReservableCurrency, + Currency, Get, fungible::{Inspect as NativeInspect, Mutate as NativeMutate}, fungibles::{Inspect as FungiblesInspect, Mutate as FungiblesMutate}, tokens::{DepositConsequence, Fortitude, Precision, Preservation, Provenance}, }; use polkadot_sdk::pallet_asset_conversion::PoolLocator; use polkadot_sdk::sp_core::U256; -use polkadot_sdk::sp_runtime::{DispatchError, DispatchResult, Perbill, TokenError}; +use polkadot_sdk::{ + sp_runtime::{DispatchError, DispatchResult, Perbill, TokenError}, + sp_weights::Weight, +}; use crate::{AssetConversion, RuntimeOrigin, Timestamp}; use pallet_deos_actors::{ - ActorType, AssetOps, DexOps, DexSwapOutcome, ExecutionContext, FeeCollector, FundingAuthority, - LiquidityOps, TaskFailure, TriggerStateBond, + ActorPrepassContext, ActorType, AdmissionCertificateAuthority, + AdmissionCertificateAuthorityProvider, AssetOps, DexOps, DexSwapOutcome, ExecutionContext, + FeeCollector, FundingAuthority, LiquidityOps, StepControlExecution, StepControlOutcome, + StepControlPhase, StepControlPlacement, StepControlWeightProvider, Task, + TaskEffectWeightProvider, TaskFailure, WeightInfo as ActorsWeightInfo, }; parameter_types! { @@ -31,21 +37,16 @@ parameter_types! { pub const ActorsPalletId: PalletId = PalletId(*ecosystem::pallet_ids::ACTORS_PALLET_ID); pub const ActorFeeNativeAssetId: AssetKind = AssetKind::Native; - pub const ActorManualTriggerBond: Balance = EXISTENTIAL_DEPOSIT; - pub const ActorAddressEventTriggerBond: Balance = 2 * EXISTENTIAL_DEPOSIT; - pub const ActorObservationChangeTriggerBond: Balance = 2 * EXISTENTIAL_DEPOSIT; - pub const ActorObservationCrossingTriggerBond: Balance = 5 * EXISTENTIAL_DEPOSIT; - pub const ActorCadencedTriggerBond: Balance = 2 * EXISTENTIAL_DEPOSIT; pub const ActorCadenceTickMillis: u64 = ecosystem::params::ACTOR_CADENCE_TICK_MILLIS; /// User Actors slot capacity per owner; System Actors is not constrained by this limit pub const ActorMaxOwnerSlots: u8 = 255; // --- Execution-plan and task bounds --- - pub const ActorMaxContractSteps: u32 = 8; - pub const ActorMaxFundingTrackedAssets: u32 = 10; - pub const ActorMaxOpeningSnapshotEntries: u32 = 16; - pub const ActorMaxOpeningPredicateResults: u32 = 32; + pub const ActorMaxContractSteps: u32 = 32; + pub const ActorMaxFundingTrackedAssets: u32 = 40; + pub const ActorMaxOpeningSnapshotEntries: u32 = 64; + pub const ActorMaxOpeningPredicateResults: u32 = 128; pub const ActorMaxPreconditionClauses: u32 = 4; pub const ActorMaxPredicatesPerClause: u32 = 4; pub const ActorMaxPredicatesPerStep: u32 = 4; @@ -55,7 +56,7 @@ parameter_types! { pub const ActorTargetBlockTime: u64 = 6; pub const ActorMaxExecutionDelayBlocks: BlockNumber = 52_596_000; - pub const ActorMaxCadenceDelayTicks: u64 = 631_152_000; + pub const ActorMaxTemporalDelayTicks: u64 = 631_152_000; pub const ActorMinWindowLength: BlockNumber = 100; pub const ActorMaxWhitelistSize: u32 = 16; @@ -68,16 +69,19 @@ parameter_types! { pub const ActorQueuePageSize: u32 = 64; /// Production temporal page granularity selected from 32/64/128 Wasm operation evidence. pub const ActorWakeupPageSize: u32 = 32; - /// Independent observation subscriber/fanout page granularity. + /// Broad ObservationChange subscriber/fanout page granularity. pub const ActorObservationPageSize: u32 = 64; + /// ObservationCrossing membership page granularity selected by EXP-0008/EXP-0009. + pub const ActorCrossingPageSize: u32 = 128; pub const ActorMaxCrossingTransitionsPerFeed: u32 = 64; pub const ActorMaxCrossingMembersPerFeed: u32 = 10_000; pub const ActorMaxUserCrossingMembersPerFeed: u32 = 9_000; pub const ActorMaxCrossingTransitionsPerBlock: u32 = 8; pub const ActorMaxCrossingLeavesPerBlock: u32 = 64; pub const ActorMaxCrossingPagesPerBlock: u32 = 64; - /// Reachable placed-fire ceiling under the generated pair cohort and 10% worker reserve. - pub const ActorMaxCrossingActorsPerBlock: u32 = 4; + /// Accepted tail/preflight ceiling under Actor Control. Exact non-tail compaction is + /// independently clamped to 64 candidates by the pallet. + pub const ActorMaxCrossingActorsPerBlock: u32 = 128; pub const ActorMaxQueueEntriesScannedPerBlock: u32 = 10_000; pub const ActorMaxObservationFanoutPagesPerBlock: u32 = 64; pub const ActorMaxWakeupsPerBlock: u32 = 512; @@ -90,6 +94,9 @@ pub const ActorMaxCrossingTransitionsPerBlock: u32 = 8; pub ActorWakeupWeightLimit: Weight = Perbill::from_percent(14) * MAXIMUM_BLOCK_WEIGHT; pub ActorOnIdleReserve: Weight = MIN_ON_IDLE_RESERVE_RATIO * MAXIMUM_BLOCK_WEIGHT; + /// Mandatory cutoff capture is Actor-specific control work, never fixed-context work. + pub ActorControlInitializationWeight: Weight = + as pallet_deos_actors::WeightInfo>::scheduler_on_initialize_cutoff(); // --- Lifecycle and sweep controls --- pub const ActorMaxConsecutiveFailures: u32 = 10; @@ -111,8 +118,12 @@ pub const ActorMaxCrossingTransitionsPerBlock: u32 = 8; pub const ActorMaxSystemReferenceAgeBlocks: u32 = ecosystem::params::MAX_SYSTEM_REFERENCE_AGE_BLOCKS; - /// Non-refundable opening fee routed to `FeeSink` - pub const ActorCreationFee: Balance = ExistentialDeposit::get(); + /// Non-refundable opening fee routed to `FeeSink`. + pub const ActorCreationFee: Balance = 2 * ExistentialDeposit::get(); + /// Fixed accounting owner for each present Actor-state component. + pub const ActorStateHoldBase: Balance = ExistentialDeposit::get(); + /// Linear refundable price for each retained SCALE byte. + pub const ActorStateHoldPerByte: Balance = MICRO_UNIT; } pub struct ActorMinUserBalanceGuard; @@ -145,70 +156,1152 @@ impl FeeCollector for TmctolFeeCollector { } } -pub struct RuntimeTriggerStateBond; - -#[cfg(test)] -std::thread_local! { - static FAIL_NEXT_TRIGGER_BOND_AFTER_ADJUSTMENT: core::cell::Cell = const { - core::cell::Cell::new(false) - }; +pub struct ActorFeeRecipient; +impl Get for ActorFeeRecipient { + fn get() -> crate::AccountId { + crate::Actors::sovereign_account_id_system(ecosystem::actor_ids::FEE_SINK_ACTORS_ID) + } } -#[cfg(test)] -pub fn fail_next_trigger_bond_after_adjustment() { - FAIL_NEXT_TRIGGER_BOND_AFTER_ADJUSTMENT.with(|flag| flag.set(true)); -} +pub struct RuntimeStepControlWeight; -impl TriggerStateBond, Balance> - for RuntimeTriggerStateBond -{ - fn amount(trigger: &pallet_deos_actors::TriggerOf) -> Balance { - match trigger { - pallet_deos_actors::types::Trigger::Manual => ActorManualTriggerBond::get(), - pallet_deos_actors::types::Trigger::AddressEvent { .. } => { - ActorAddressEventTriggerBond::get() - } - pallet_deos_actors::types::Trigger::ObservationChange { .. } => { - ActorObservationChangeTriggerBond::get() - } - pallet_deos_actors::types::Trigger::ObservationCrossing { .. } => { - ActorObservationCrossingTriggerBond::get() - } - pallet_deos_actors::types::Trigger::Cadenced { .. } => ActorCadencedTriggerBond::get(), +impl RuntimeStepControlWeight { + fn component_max(left: Weight, right: Weight) -> Weight { + Weight::from_parts( + left.ref_time().max(right.ref_time()), + left.proof_size().max(right.proof_size()), + ) + } + + fn nonzero_parameterized(value: u32, weight: impl FnOnce(u32) -> Weight) -> Weight { + if value == 0 { + Weight::zero() + } else { + weight(value) } } +} - fn maximum() -> Balance { - ActorObservationCrossingTriggerBond::get() +impl StepControlWeightProvider> for RuntimeStepControlWeight { + fn production_weight_identity() -> Option<[u8; 32]> { + type ControlWeights = crate::weights::pallet_deos_actors::SubstrateWeight; + let tail_plan = (1..=4) + .map(ControlWeights::current_step_plan_running_tail) + .collect::>(); + let maximum_opening_tail_chunks = ActorMaxContractSteps::get() + .saturating_sub(1) + .div_ceil(pallet_deos_actors::MAX_STEPS_PER_TAIL_CHUNK); + let opening_failed = (0..=maximum_opening_tail_chunks) + .map(|tail_chunks| { + ( + ControlWeights::scheduler_inner_opening_failed_min(tail_chunks), + ControlWeights::scheduler_inner_opening_failed_max(tail_chunks), + ) + }) + .collect::>(); + let opening_retry = (0..=maximum_opening_tail_chunks) + .map(|tail_chunks| { + ( + ControlWeights::scheduler_inner_opening_retry_min(tail_chunks), + ControlWeights::scheduler_inner_opening_retry_max(tail_chunks), + ) + }) + .collect::>(); + let opening_complete = (0..=maximum_opening_tail_chunks) + .map(|tail_chunks| { + ( + ControlWeights::scheduler_inner_opening_complete_min(tail_chunks), + ControlWeights::scheduler_inner_opening_complete_max(tail_chunks), + ) + }) + .collect::>(); + let opening_progress = (1..=maximum_opening_tail_chunks) + .map(|tail_chunks| { + ( + ControlWeights::scheduler_inner_opening_progress_min(tail_chunks), + ControlWeights::scheduler_inner_opening_progress_max(tail_chunks), + ) + }) + .collect::>(); + let running_complete = (1..=4) + .flat_map(|steps| { + (0..=ActorMaxPredicatesPerStep::get()).map(move |predicates| { + ControlWeights::scheduler_inner_running_complete(steps, predicates) + }) + }) + .collect::>(); + let running_progress = (2..=4) + .flat_map(|steps| { + (0..=ActorMaxPredicatesPerStep::get()).map(move |predicates| { + ControlWeights::scheduler_inner_running_progress(steps, predicates) + }) + }) + .collect::>(); + let suspended_tail_retry = (1..=4) + .flat_map(|steps| { + (0..=ActorMaxPredicatesPerStep::get()).map(move |predicates| { + ControlWeights::scheduler_inner_suspended_tail_retry(steps, predicates) + }) + }) + .collect::>(); + let suspended_tail_complete = (1..=4) + .flat_map(|steps| { + (0..=ActorMaxPredicatesPerStep::get()).map(move |predicates| { + ControlWeights::scheduler_inner_suspended_tail_complete(steps, predicates) + }) + }) + .collect::>(); + let suspended_tail_progress = (2..=4) + .flat_map(|steps| { + (0..=ActorMaxPredicatesPerStep::get()).map(move |predicates| { + ControlWeights::scheduler_inner_suspended_tail_progress(steps, predicates) + }) + }) + .collect::>(); + let suspended_head_retry = [ + ControlWeights::scheduler_inner_suspended_head_retry(0, 0, 0, 0), + ControlWeights::scheduler_inner_suspended_head_retry( + ActorMaxOpeningSnapshotEntries::get(), + 0, + 0, + 0, + ), + ControlWeights::scheduler_inner_suspended_head_retry( + 0, + ActorMaxOpeningPredicateResults::get(), + 0, + 0, + ), + ControlWeights::scheduler_inner_suspended_head_retry( + 0, + 0, + ActorMaxFundingTrackedAssets::get(), + 0, + ), + ControlWeights::scheduler_inner_suspended_head_retry( + 0, + 0, + 0, + ActorMaxPredicatesPerStep::get(), + ), + ControlWeights::scheduler_inner_suspended_head_retry( + ActorMaxOpeningSnapshotEntries::get(), + ActorMaxOpeningPredicateResults::get(), + ActorMaxFundingTrackedAssets::get(), + ActorMaxPredicatesPerStep::get(), + ), + ]; + let suspended_head_complete = [ + ControlWeights::scheduler_inner_suspended_head_complete(0, 0, 0, 0), + ControlWeights::scheduler_inner_suspended_head_complete( + ActorMaxOpeningSnapshotEntries::get(), + 0, + 0, + 0, + ), + ControlWeights::scheduler_inner_suspended_head_complete( + 0, + ActorMaxOpeningPredicateResults::get(), + 0, + 0, + ), + ControlWeights::scheduler_inner_suspended_head_complete( + 0, + 0, + ActorMaxFundingTrackedAssets::get(), + 0, + ), + ControlWeights::scheduler_inner_suspended_head_complete( + 0, + 0, + 0, + ActorMaxPredicatesPerStep::get(), + ), + ControlWeights::scheduler_inner_suspended_head_complete( + ActorMaxOpeningSnapshotEntries::get(), + ActorMaxOpeningPredicateResults::get(), + ActorMaxFundingTrackedAssets::get(), + ActorMaxPredicatesPerStep::get(), + ), + ]; + let suspended_head_progress = [ + ControlWeights::scheduler_inner_suspended_head_progress(0, 0, 0, 0), + ControlWeights::scheduler_inner_suspended_head_progress( + ActorMaxOpeningSnapshotEntries::get(), + 0, + 0, + 0, + ), + ControlWeights::scheduler_inner_suspended_head_progress( + 0, + ActorMaxOpeningPredicateResults::get(), + 0, + 0, + ), + ControlWeights::scheduler_inner_suspended_head_progress( + 0, + 0, + ActorMaxFundingTrackedAssets::get(), + 0, + ), + ControlWeights::scheduler_inner_suspended_head_progress( + 0, + 0, + 0, + ActorMaxPredicatesPerStep::get(), + ), + ControlWeights::scheduler_inner_suspended_head_progress( + ActorMaxOpeningSnapshotEntries::get(), + ActorMaxOpeningPredicateResults::get(), + ActorMaxFundingTrackedAssets::get(), + ActorMaxPredicatesPerStep::get(), + ), + ]; + let suspended_head_opening_retry = [ + ControlWeights::scheduler_inner_suspended_head_opening_retry(0, 1, 0), + ControlWeights::scheduler_inner_suspended_head_opening_retry( + ActorMaxOpeningSnapshotEntries::get(), + 1, + 0, + ), + ControlWeights::scheduler_inner_suspended_head_opening_retry( + 0, + ActorMaxOpeningPredicateResults::get(), + 0, + ), + ControlWeights::scheduler_inner_suspended_head_opening_retry( + 0, + 1, + ActorMaxFundingTrackedAssets::get(), + ), + ControlWeights::scheduler_inner_suspended_head_opening_retry( + ActorMaxOpeningSnapshotEntries::get(), + ActorMaxOpeningPredicateResults::get(), + ActorMaxFundingTrackedAssets::get(), + ), + ]; + let suspended_head_opening_complete = [ + ControlWeights::scheduler_inner_suspended_head_opening_complete(0, 1, 0), + ControlWeights::scheduler_inner_suspended_head_opening_complete( + ActorMaxOpeningSnapshotEntries::get(), + 1, + 0, + ), + ControlWeights::scheduler_inner_suspended_head_opening_complete( + 0, + ActorMaxOpeningPredicateResults::get(), + 0, + ), + ControlWeights::scheduler_inner_suspended_head_opening_complete( + 0, + 1, + ActorMaxFundingTrackedAssets::get(), + ), + ControlWeights::scheduler_inner_suspended_head_opening_complete( + ActorMaxOpeningSnapshotEntries::get(), + ActorMaxOpeningPredicateResults::get(), + ActorMaxFundingTrackedAssets::get(), + ), + ]; + let suspended_head_opening_progress = [ + ControlWeights::scheduler_inner_suspended_head_opening_progress(0, 1, 0), + ControlWeights::scheduler_inner_suspended_head_opening_progress( + ActorMaxOpeningSnapshotEntries::get(), + 1, + 0, + ), + ControlWeights::scheduler_inner_suspended_head_opening_progress( + 0, + ActorMaxOpeningPredicateResults::get(), + 0, + ), + ControlWeights::scheduler_inner_suspended_head_opening_progress( + 0, + 1, + ActorMaxFundingTrackedAssets::get(), + ), + ControlWeights::scheduler_inner_suspended_head_opening_progress( + ActorMaxOpeningSnapshotEntries::get(), + ActorMaxOpeningPredicateResults::get(), + ActorMaxFundingTrackedAssets::get(), + ), + ]; + let opening_tail_chunks = (0..=ActorMaxContractSteps::get() + .saturating_sub(1) + .div_ceil(pallet_deos_actors::MAX_STEPS_PER_TAIL_CHUNK)) + .map(ControlWeights::contract_geometry_reconstruct) + .collect::>(); + let opening_snapshot = (1..=ActorMaxOpeningSnapshotEntries::get()) + .map(ControlWeights::opening_snapshot_capture) + .collect::>(); + let opening_predicates = (1..=ActorMaxOpeningPredicateResults::get()) + .map(ControlWeights::opening_predicate_capture) + .collect::>(); + let funding = (1..=ActorMaxFundingTrackedAssets::get()) + .map(ControlWeights::funding_snapshot_open) + .collect::>(); + let current_predicates = (1..=ActorMaxPredicatesPerStep::get()) + .map(ControlWeights::predicate_set_evaluation) + .collect::>(); + Some( + ( + *b"DEOS_ACTOR_STEP_CONTROL_WEIGHT", + *b"DEOS_ACTOR_CONTROL_ACTUAL_V2", + ( + ControlWeights::scheduler_paged_tombstone_drain(1), + ControlWeights::scheduler_actor_state_probe(), + ControlWeights::scheduler_paged_consume_preserve_page(), + ControlWeights::scheduler_paged_consume_delete_page(), + ), + ( + ControlWeights::scheduler_inner_zero_step_complete(), + ControlWeights::scheduler_paged_execute_opening_max(), + opening_failed, + opening_retry, + opening_complete, + opening_progress, + ), + ControlWeights::current_step_plan_opening_head(), + ControlWeights::current_step_plan_suspended_head(), + tail_plan, + ( + running_complete, + running_progress, + suspended_tail_retry, + suspended_tail_complete, + suspended_tail_progress, + suspended_head_retry, + suspended_head_complete, + suspended_head_progress, + suspended_head_opening_retry, + suspended_head_opening_complete, + suspended_head_opening_progress, + ), + opening_tail_chunks, + opening_snapshot, + opening_predicates, + funding, + current_predicates, + ControlWeights::run_progress(), + ControlWeights::run_suspend(), + ControlWeights::run_complete(), + ControlWeights::scheduler_paged_append_new_page(), + ( + ControlWeights::scheduler_wakeup_append_new_page(), + ControlWeights::close_actor(), + ), + ) + .using_encoded(polkadot_sdk::sp_io::hashing::blake2_256), + ) } - fn set_bond(owner: &AccountId, current: Balance, target: Balance) -> DispatchResult { - let result = if target > current { - >::reserve(owner, target - current) - } else if current > target { - let remainder = - >::unreserve(owner, current - target); - if remainder == 0 { - Ok(()) + fn maximum_control_weight( + context: pallet_deos_actors::StepControlWeightContext, + _: &pallet_deos_actors::StepOf, + ) -> Option { + type ControlWeights = crate::weights::pallet_deos_actors::SubstrateWeight; + let maximum_evaluation_units = ActorMaxPredicatesPerStep::get().checked_mul(2)?; + let maximum_opening_tail_chunks = ActorMaxContractSteps::get() + .saturating_sub(1) + .div_ceil(pallet_deos_actors::MAX_STEPS_PER_TAIL_CHUNK); + if context.predicate_evaluation_units > maximum_evaluation_units + || context.opening_tail_chunks > maximum_opening_tail_chunks + || context.cursor > 0 && context.opening_tail_chunks != 0 + || context.opening_snapshot_entries > ActorMaxOpeningSnapshotEntries::get() + || context.opening_predicate_results > ActorMaxOpeningPredicateResults::get() + || context.funding_snapshot_entries > ActorMaxFundingTrackedAssets::get() + { + return None; + } + let maximum_opening = context.cursor == 0 + && context.steps_in_fragment == 1 + && context.opening_tail_chunks == maximum_opening_tail_chunks + && context.predicate_evaluation_units == maximum_evaluation_units + && context.opening_snapshot_entries == ActorMaxOpeningSnapshotEntries::get() + && context.opening_predicate_results == ActorMaxOpeningPredicateResults::get() + && context.funding_snapshot_entries == ActorMaxFundingTrackedAssets::get(); + if maximum_opening { + return Some(Self::component_max( + ControlWeights::scheduler_paged_execute_opening_max(), + ControlWeights::scheduler_inner_opening_progress_max(maximum_opening_tail_chunks), + )); + } + let plan = if context.cursor == 0 { + if context.steps_in_fragment != 1 { + return None; + } + Self::component_max( + ControlWeights::current_step_plan_opening_head(), + ControlWeights::current_step_plan_suspended_head(), + ) + } else { + if context.steps_in_fragment == 0 || context.steps_in_fragment > 4 { + return None; + } + ControlWeights::current_step_plan_running_tail(context.steps_in_fragment) + }; + let opening_tail = if context.opening_tail_chunks == 0 { + Weight::zero() + } else { + ControlWeights::contract_geometry_reconstruct(context.opening_tail_chunks) + .checked_sub(&ControlWeights::contract_geometry_reconstruct(0))? + }; + let opening_snapshot = Self::nonzero_parameterized( + context.opening_snapshot_entries, + ControlWeights::opening_snapshot_capture, + ); + let opening_predicates = Self::nonzero_parameterized( + context.opening_predicate_results, + ControlWeights::opening_predicate_capture, + ); + let funding = Self::nonzero_parameterized( + context.funding_snapshot_entries, + ControlWeights::funding_snapshot_open, + ); + let current_predicates = if context.predicate_evaluation_units == 0 { + Weight::zero() + } else { + ControlWeights::predicate_set_evaluation( + context + .predicate_evaluation_units + .min(ActorMaxPredicatesPerStep::get()), + ) + }; + let commit = Self::component_max( + Self::component_max( + ControlWeights::run_progress(), + ControlWeights::run_suspend(), + ), + ControlWeights::run_complete(), + ); + let placement = Self::component_max( + ControlWeights::scheduler_paged_append_new_page(), + ControlWeights::scheduler_wakeup_append_new_page(), + ); + let composed = plan + .saturating_add(opening_tail) + .saturating_add(opening_snapshot) + .saturating_add(opening_predicates) + .saturating_add(funding) + .saturating_add(current_predicates) + .saturating_add(commit) + .saturating_add(placement); + let opening_geometry_steps = 1u32 + .saturating_add( + context + .opening_tail_chunks + .saturating_mul(pallet_deos_actors::MAX_STEPS_PER_TAIL_CHUNK), + ) + .min(ActorMaxContractSteps::get()); + let minimal_opening = context.cursor == 0 + && context.predicate_evaluation_units == 0 + && context.opening_snapshot_entries == 0 + && context.opening_predicate_results == 0; + let maximal_opening = context.cursor == 0 + && context.opening_tail_chunks > 0 + && context.predicate_evaluation_units == maximum_evaluation_units + && context.opening_snapshot_entries + == opening_geometry_steps + .saturating_mul(2) + .min(ActorMaxOpeningSnapshotEntries::get()) + && context.opening_predicate_results + == opening_geometry_steps + .saturating_mul(ActorMaxPredicatesPerStep::get()) + .min(ActorMaxOpeningPredicateResults::get()) + && context.funding_snapshot_entries == ActorMaxFundingTrackedAssets::get(); + let maximal_opening_completion = context.cursor == 0 + && context.predicate_evaluation_units == maximum_evaluation_units + && context.opening_snapshot_entries + == opening_geometry_steps + .saturating_sub(1) + .saturating_mul(2) + .min(ActorMaxOpeningSnapshotEntries::get()) + && context.opening_predicate_results + == opening_geometry_steps + .saturating_mul(ActorMaxPredicatesPerStep::get()) + .min(ActorMaxOpeningPredicateResults::get()) + && context.funding_snapshot_entries == ActorMaxFundingTrackedAssets::get(); + let composed = if minimal_opening { + let non_progress_min = Self::component_max( + ControlWeights::scheduler_inner_opening_failed_min(context.opening_tail_chunks), + Self::component_max( + ControlWeights::scheduler_inner_opening_retry_min(context.opening_tail_chunks), + ControlWeights::scheduler_inner_opening_complete_min(context.opening_tail_chunks), + ), + ); + let direct = if context.opening_tail_chunks == 0 { + non_progress_min + } else { + Self::component_max( + non_progress_min, + ControlWeights::scheduler_inner_opening_progress_min(context.opening_tail_chunks), + ) + }; + Self::component_max(composed, direct) + } else if maximal_opening { + Self::component_max( + composed, + ControlWeights::scheduler_inner_opening_progress_max(context.opening_tail_chunks), + ) + } else if maximal_opening_completion { + Self::component_max( + composed, + Self::component_max( + ControlWeights::scheduler_inner_opening_failed_max(context.opening_tail_chunks), + Self::component_max( + ControlWeights::scheduler_inner_opening_retry_max(context.opening_tail_chunks), + ControlWeights::scheduler_inner_opening_complete_max(context.opening_tail_chunks), + ), + ), + ) + } else { + composed + }; + if context.cursor == 0 && context.predicate_evaluation_units <= ActorMaxPredicatesPerStep::get() + { + let retry = ControlWeights::scheduler_inner_suspended_head_retry( + context.opening_snapshot_entries, + context.opening_predicate_results, + context.funding_snapshot_entries, + context.predicate_evaluation_units, + ); + let complete = ControlWeights::scheduler_inner_suspended_head_complete( + context.opening_snapshot_entries, + context.opening_predicate_results, + context.funding_snapshot_entries, + context.predicate_evaluation_units, + ); + let progress = ControlWeights::scheduler_inner_suspended_head_progress( + context.opening_snapshot_entries, + context.opening_predicate_results, + context.funding_snapshot_entries, + context.predicate_evaluation_units, + ); + return Some(Self::component_max( + composed, + Self::component_max(retry, Self::component_max(complete, progress)), + )); + } + if context.cursor == 0 + && context.predicate_evaluation_units > ActorMaxPredicatesPerStep::get() + && context.predicate_evaluation_units <= maximum_evaluation_units + { + let opening_retry = ControlWeights::scheduler_inner_suspended_head_opening_retry( + context.opening_snapshot_entries, + context.opening_predicate_results, + context.funding_snapshot_entries, + ); + let opening_complete = ControlWeights::scheduler_inner_suspended_head_opening_complete( + context.opening_snapshot_entries, + context.opening_predicate_results, + context.funding_snapshot_entries, + ); + let opening_progress = ControlWeights::scheduler_inner_suspended_head_opening_progress( + context.opening_snapshot_entries, + context.opening_predicate_results, + context.funding_snapshot_entries, + ); + return Some(Self::component_max( + composed, + Self::component_max( + opening_retry, + Self::component_max(opening_complete, opening_progress), + ), + )); + } + if context.cursor > 0 && context.predicate_evaluation_units <= ActorMaxPredicatesPerStep::get() + { + let atomic_complete = ControlWeights::scheduler_inner_running_complete( + context.steps_in_fragment, + context.predicate_evaluation_units, + ); + let atomic = if context.steps_in_fragment >= 2 { + Self::component_max( + atomic_complete, + ControlWeights::scheduler_inner_running_progress( + context.steps_in_fragment, + context.predicate_evaluation_units, + ), + ) + } else { + atomic_complete + }; + let atomic = Self::component_max( + atomic, + ControlWeights::scheduler_inner_suspended_tail_retry( + context.steps_in_fragment, + context.predicate_evaluation_units, + ), + ); + let atomic = Self::component_max( + atomic, + ControlWeights::scheduler_inner_suspended_tail_complete( + context.steps_in_fragment, + context.predicate_evaluation_units, + ), + ); + let atomic = if context.steps_in_fragment >= 2 { + Self::component_max( + atomic, + ControlWeights::scheduler_inner_suspended_tail_progress( + context.steps_in_fragment, + context.predicate_evaluation_units, + ), + ) } else { - Err(DispatchError::Token(TokenError::FundsUnavailable)) + atomic + }; + return Some(Self::component_max(composed, atomic)); + } + Some(composed) + } + + fn actual_control_weight( + context: pallet_deos_actors::StepControlWeightContext, + step: &pallet_deos_actors::StepOf, + maximum: Weight, + execution: StepControlExecution, + ) -> Option { + type ControlWeights = crate::weights::pallet_deos_actors::SubstrateWeight; + let expected_context_maximum = Self::maximum_control_weight(context, step)?; + if execution.phase == StepControlPhase::Opening && expected_context_maximum != maximum { + return None; + } + let maximum_evaluation_units = ActorMaxPredicatesPerStep::get().checked_mul(2)?; + let maximum_opening_tail_chunks = ActorMaxContractSteps::get() + .saturating_sub(1) + .div_ceil(pallet_deos_actors::MAX_STEPS_PER_TAIL_CHUNK); + let maximum_opening = context.cursor == 0 + && context.steps_in_fragment == 1 + && context.opening_tail_chunks == maximum_opening_tail_chunks + && context.predicate_evaluation_units == maximum_evaluation_units + && context.opening_snapshot_entries == ActorMaxOpeningSnapshotEntries::get() + && context.opening_predicate_results == ActorMaxOpeningPredicateResults::get() + && context.funding_snapshot_entries == ActorMaxFundingTrackedAssets::get(); + if execution.phase == StepControlPhase::Opening + && execution.outcome == StepControlOutcome::Failed + && execution.placement == StepControlPlacement::None + && context.cursor == 0 + && context.predicate_evaluation_units == 0 + && context.opening_snapshot_entries == 0 + && context.opening_predicate_results == 0 + { + return Some(ControlWeights::scheduler_inner_opening_failed_min( + context.opening_tail_chunks, + )); + } + if execution.phase == StepControlPhase::Opening + && execution.outcome == StepControlOutcome::Suspended + && execution.placement == StepControlPlacement::Wakeup + && context.cursor == 0 + && context.predicate_evaluation_units == 0 + && context.opening_snapshot_entries == 0 + && context.opening_predicate_results == 0 + { + return Some(ControlWeights::scheduler_inner_opening_retry_min( + context.opening_tail_chunks, + )); + } + if execution.phase == StepControlPhase::Opening + && execution.outcome == StepControlOutcome::Completed + && execution.placement == StepControlPlacement::None + && context.cursor == 0 + && context.predicate_evaluation_units == 0 + && context.opening_snapshot_entries == 0 + && context.opening_predicate_results == 0 + { + return Some(ControlWeights::scheduler_inner_opening_complete_min( + context.opening_tail_chunks, + )); + } + if execution.phase == StepControlPhase::Opening + && execution.outcome == StepControlOutcome::Continued + && execution.placement == StepControlPlacement::Queue + && context.cursor == 0 + && context.opening_tail_chunks > 0 + { + let opening_geometry_steps = 1u32 + .saturating_add( + context + .opening_tail_chunks + .saturating_mul(pallet_deos_actors::MAX_STEPS_PER_TAIL_CHUNK), + ) + .min(ActorMaxContractSteps::get()); + if context.predicate_evaluation_units == 0 + && context.opening_snapshot_entries == 0 + && context.opening_predicate_results == 0 + { + return Some(ControlWeights::scheduler_inner_opening_progress_min( + context.opening_tail_chunks, + )); + } + if context.predicate_evaluation_units == maximum_evaluation_units + && context.opening_snapshot_entries + == opening_geometry_steps + .saturating_mul(2) + .min(ActorMaxOpeningSnapshotEntries::get()) + && context.opening_predicate_results + == opening_geometry_steps + .saturating_mul(ActorMaxPredicatesPerStep::get()) + .min(ActorMaxOpeningPredicateResults::get()) + && context.funding_snapshot_entries == ActorMaxFundingTrackedAssets::get() + { + return Some(ControlWeights::scheduler_inner_opening_progress_max( + context.opening_tail_chunks, + )); } + } + if execution.phase == StepControlPhase::Opening + && execution.outcome == StepControlOutcome::Failed + && execution.placement == StepControlPlacement::None + && context.cursor == 0 + && context.predicate_evaluation_units == maximum_evaluation_units + { + let opening_geometry_steps = 1u32 + .saturating_add( + context + .opening_tail_chunks + .saturating_mul(pallet_deos_actors::MAX_STEPS_PER_TAIL_CHUNK), + ) + .min(ActorMaxContractSteps::get()); + if context.opening_snapshot_entries + == opening_geometry_steps + .saturating_sub(1) + .saturating_mul(2) + .min(ActorMaxOpeningSnapshotEntries::get()) + && context.opening_predicate_results + == opening_geometry_steps + .saturating_mul(ActorMaxPredicatesPerStep::get()) + .min(ActorMaxOpeningPredicateResults::get()) + && context.funding_snapshot_entries == ActorMaxFundingTrackedAssets::get() + { + return Some(ControlWeights::scheduler_inner_opening_failed_max( + context.opening_tail_chunks, + )); + } + } + if execution.phase == StepControlPhase::Opening + && execution.outcome == StepControlOutcome::Suspended + && execution.placement == StepControlPlacement::Wakeup + && context.cursor == 0 + && context.predicate_evaluation_units == maximum_evaluation_units + { + let opening_geometry_steps = 1u32 + .saturating_add( + context + .opening_tail_chunks + .saturating_mul(pallet_deos_actors::MAX_STEPS_PER_TAIL_CHUNK), + ) + .min(ActorMaxContractSteps::get()); + if context.opening_snapshot_entries + == opening_geometry_steps + .saturating_sub(1) + .saturating_mul(2) + .min(ActorMaxOpeningSnapshotEntries::get()) + && context.opening_predicate_results + == opening_geometry_steps + .saturating_mul(ActorMaxPredicatesPerStep::get()) + .min(ActorMaxOpeningPredicateResults::get()) + && context.funding_snapshot_entries == ActorMaxFundingTrackedAssets::get() + { + return Some(ControlWeights::scheduler_inner_opening_retry_max( + context.opening_tail_chunks, + )); + } + } + if execution.phase == StepControlPhase::Opening + && execution.outcome == StepControlOutcome::Completed + && execution.placement == StepControlPlacement::None + && context.cursor == 0 + && context.predicate_evaluation_units == maximum_evaluation_units + { + let opening_geometry_steps = 1u32 + .saturating_add( + context + .opening_tail_chunks + .saturating_mul(pallet_deos_actors::MAX_STEPS_PER_TAIL_CHUNK), + ) + .min(ActorMaxContractSteps::get()); + if context.opening_snapshot_entries + == opening_geometry_steps + .saturating_sub(1) + .saturating_mul(2) + .min(ActorMaxOpeningSnapshotEntries::get()) + && context.opening_predicate_results + == opening_geometry_steps + .saturating_mul(ActorMaxPredicatesPerStep::get()) + .min(ActorMaxOpeningPredicateResults::get()) + && context.funding_snapshot_entries == ActorMaxFundingTrackedAssets::get() + { + return Some(ControlWeights::scheduler_inner_opening_complete_max( + context.opening_tail_chunks, + )); + } + } + if maximum_opening && execution.phase == StepControlPhase::Opening { + return Some(maximum); + } + if execution.phase == StepControlPhase::Running + && context.cursor > 0 + && context.predicate_evaluation_units <= ActorMaxPredicatesPerStep::get() + { + if execution.outcome == StepControlOutcome::Completed + && execution.placement == StepControlPlacement::None + { + return Some(ControlWeights::scheduler_inner_running_complete( + context.steps_in_fragment, + context.predicate_evaluation_units, + )); + } + if execution.outcome == StepControlOutcome::Continued + && execution.placement == StepControlPlacement::Queue + && context.steps_in_fragment >= 2 + { + return Some(ControlWeights::scheduler_inner_running_progress( + context.steps_in_fragment, + context.predicate_evaluation_units, + )); + } + } + if execution.phase == StepControlPhase::Suspended + && execution.outcome == StepControlOutcome::Continued + && execution.placement == StepControlPlacement::Queue + && context.cursor == 0 + && context.opening_tail_chunks > 0 + && context.predicate_evaluation_units > ActorMaxPredicatesPerStep::get() + && context.predicate_evaluation_units <= maximum_evaluation_units + { + return Some( + ControlWeights::scheduler_inner_suspended_head_opening_progress( + context.opening_snapshot_entries, + context.opening_predicate_results, + context.funding_snapshot_entries, + ), + ); + } + if execution.phase == StepControlPhase::Suspended + && execution.outcome == StepControlOutcome::Completed + && execution.placement == StepControlPlacement::None + && context.cursor == 0 + && context.opening_tail_chunks == 0 + && context.predicate_evaluation_units > ActorMaxPredicatesPerStep::get() + && context.predicate_evaluation_units <= maximum_evaluation_units + { + return Some( + ControlWeights::scheduler_inner_suspended_head_opening_complete( + context.opening_snapshot_entries, + context.opening_predicate_results, + context.funding_snapshot_entries, + ), + ); + } + if execution.phase == StepControlPhase::Suspended + && execution.outcome == StepControlOutcome::Suspended + && execution.placement == StepControlPlacement::Wakeup + && context.cursor == 0 + && context.predicate_evaluation_units > ActorMaxPredicatesPerStep::get() + && context.predicate_evaluation_units <= maximum_evaluation_units + { + return Some( + ControlWeights::scheduler_inner_suspended_head_opening_retry( + context.opening_snapshot_entries, + context.opening_predicate_results, + context.funding_snapshot_entries, + ), + ); + } + if execution.phase == StepControlPhase::Suspended + && execution.outcome == StepControlOutcome::Continued + && execution.placement == StepControlPlacement::Queue + && context.cursor == 0 + && context.opening_tail_chunks > 0 + && context.predicate_evaluation_units <= ActorMaxPredicatesPerStep::get() + { + return Some(ControlWeights::scheduler_inner_suspended_head_progress( + context.opening_snapshot_entries, + context.opening_predicate_results, + context.funding_snapshot_entries, + context.predicate_evaluation_units, + )); + } + if execution.phase == StepControlPhase::Suspended + && execution.outcome == StepControlOutcome::Completed + && execution.placement == StepControlPlacement::None + && context.cursor == 0 + && context.opening_tail_chunks == 0 + && context.predicate_evaluation_units <= ActorMaxPredicatesPerStep::get() + { + return Some(ControlWeights::scheduler_inner_suspended_head_complete( + context.opening_snapshot_entries, + context.opening_predicate_results, + context.funding_snapshot_entries, + context.predicate_evaluation_units, + )); + } + if execution.phase == StepControlPhase::Suspended + && execution.outcome == StepControlOutcome::Suspended + && execution.placement == StepControlPlacement::Wakeup + && context.cursor == 0 + && context.predicate_evaluation_units <= ActorMaxPredicatesPerStep::get() + { + return Some(ControlWeights::scheduler_inner_suspended_head_retry( + context.opening_snapshot_entries, + context.opening_predicate_results, + context.funding_snapshot_entries, + context.predicate_evaluation_units, + )); + } + if execution.phase == StepControlPhase::Suspended + && execution.outcome == StepControlOutcome::Completed + && execution.placement == StepControlPlacement::None + && context.cursor > 0 + && context.predicate_evaluation_units <= ActorMaxPredicatesPerStep::get() + { + return Some(ControlWeights::scheduler_inner_suspended_tail_complete( + context.steps_in_fragment, + context.predicate_evaluation_units, + )); + } + if execution.phase == StepControlPhase::Suspended + && execution.outcome == StepControlOutcome::Continued + && execution.placement == StepControlPlacement::Queue + && context.cursor > 0 + && context.steps_in_fragment >= 2 + && context.predicate_evaluation_units <= ActorMaxPredicatesPerStep::get() + { + return Some(ControlWeights::scheduler_inner_suspended_tail_progress( + context.steps_in_fragment, + context.predicate_evaluation_units, + )); + } + if execution.phase == StepControlPhase::Suspended + && execution.outcome == StepControlOutcome::Suspended + && execution.placement == StepControlPlacement::Wakeup + && context.cursor > 0 + && context.predicate_evaluation_units <= ActorMaxPredicatesPerStep::get() + { + return Some(ControlWeights::scheduler_inner_suspended_tail_retry( + context.steps_in_fragment, + context.predicate_evaluation_units, + )); + } + let plan = match execution.phase { + StepControlPhase::Opening if context.cursor == 0 => { + ControlWeights::current_step_plan_opening_head() + } + StepControlPhase::Suspended if context.cursor == 0 => { + ControlWeights::current_step_plan_suspended_head() + } + StepControlPhase::Running | StepControlPhase::Suspended if context.cursor > 0 => { + ControlWeights::current_step_plan_running_tail(context.steps_in_fragment) + } + _ => return None, + }; + let opening_tail = + if execution.phase != StepControlPhase::Opening || context.opening_tail_chunks == 0 { + Weight::zero() + } else { + ControlWeights::contract_geometry_reconstruct(context.opening_tail_chunks) + .checked_sub(&ControlWeights::contract_geometry_reconstruct(0))? + }; + let opening_snapshot = if execution.phase == StepControlPhase::Opening { + Self::nonzero_parameterized( + context.opening_snapshot_entries, + ControlWeights::opening_snapshot_capture, + ) } else { - Ok(()) + Weight::zero() + }; + let opening_predicates = if execution.phase == StepControlPhase::Opening { + Self::nonzero_parameterized( + context.opening_predicate_results, + ControlWeights::opening_predicate_capture, + ) + } else { + Weight::zero() + }; + let funding = if execution.phase == StepControlPhase::Opening { + Self::nonzero_parameterized( + context.funding_snapshot_entries, + ControlWeights::funding_snapshot_open, + ) + } else { + Weight::zero() }; - result?; - #[cfg(test)] - if FAIL_NEXT_TRIGGER_BOND_AFTER_ADJUSTMENT.with(|flag| flag.replace(false)) { - return Err(DispatchError::Other("injected Trigger-state bond failure")); + let current_predicates = Self::nonzero_parameterized( + context + .predicate_evaluation_units + .min(ActorMaxPredicatesPerStep::get()), + ControlWeights::predicate_set_evaluation, + ); + let commit = match execution.outcome { + StepControlOutcome::Continued => ControlWeights::run_progress(), + StepControlOutcome::Suspended => ControlWeights::run_suspend(), + StepControlOutcome::Completed | StepControlOutcome::Failed => ControlWeights::run_complete(), + }; + let placement = match execution.placement { + StepControlPlacement::None => Weight::zero(), + StepControlPlacement::Queue => ControlWeights::scheduler_paged_append_new_page(), + StepControlPlacement::Wakeup => ControlWeights::scheduler_wakeup_append_new_page(), + }; + Some( + plan + .saturating_add(opening_tail) + .saturating_add(opening_snapshot) + .saturating_add(opening_predicates) + .saturating_add(funding) + .saturating_add(current_predicates) + .saturating_add(commit) + .saturating_add(placement), + ) + } +} + +pub struct TmctolTaskEffectWeight; + +impl + TaskEffectWeightProvider> + for TmctolTaskEffectWeight +where + MaxSplitTransferLegs: Get, +{ + fn production_weight_identity() -> Option<[u8; 32]> { + type EffectWeights = crate::weights::pallet_deos_actors::SubstrateWeight; + let split_transfer = (0..=ActorMaxSplitTransferLegs::get()) + .map(EffectWeights::task_split_transfer) + .collect::>(); + Some( + ( + *b"DEOS_ACTOR_TASK_EFFECT_WEIGHT", + *b"DEOS_ACTOR_EFFECT_ACTUAL_V1", + EffectWeights::task_transfer(), + split_transfer, + EffectWeights::task_dex_exact_in(), + EffectWeights::task_dex_exact_out(), + EffectWeights::task_add_liquidity(), + EffectWeights::task_remove_liquidity(), + EffectWeights::task_donate_liquidity(), + EffectWeights::task_burn(), + EffectWeights::task_mint(), + EffectWeights::task_stake(), + EffectWeights::task_unstake(), + Weight::zero(), + ) + .using_encoded(polkadot_sdk::sp_io::hashing::blake2_256), + ) + } + + fn maximum_effect_weight( + task: &Task, + ) -> Option { + type EffectWeights = crate::weights::pallet_deos_actors::SubstrateWeight; + match task { + Task::Transfer { .. } => Some(EffectWeights::task_transfer()), + Task::SplitTransfer { legs, .. } => Some(EffectWeights::task_split_transfer( + u32::try_from(legs.len()).ok()?, + )), + Task::SwapIn { .. } => Some(EffectWeights::task_dex_exact_in()), + Task::SwapOut { .. } => Some(EffectWeights::task_dex_exact_out()), + Task::AddLiquidity { .. } => Some(EffectWeights::task_add_liquidity()), + Task::RemoveLiquidity { .. } => Some(EffectWeights::task_remove_liquidity()), + Task::Burn { .. } => Some(EffectWeights::task_burn()), + Task::Mint { .. } => Some(EffectWeights::task_mint()), + Task::Stake { .. } => Some(EffectWeights::task_stake()), + Task::DonateLiquidity { .. } => Some(EffectWeights::task_donate_liquidity()), + Task::Unstake { .. } => Some(EffectWeights::task_unstake()), + Task::StopCycle => Some(Weight::zero()), + } + } + + fn actual_effect_weight( + task: &Task, + execution: pallet_deos_actors::TaskEffectExecution, + ) -> Option { + match execution { + pallet_deos_actors::TaskEffectExecution::NotInvoked => Some(Weight::zero()), + pallet_deos_actors::TaskEffectExecution::Invoked => Self::maximum_effect_weight(task), } - Ok(()) } } -pub struct ActorFeeRecipient; -impl Get for ActorFeeRecipient { - fn get() -> crate::AccountId { - crate::Actors::sovereign_account_id_system(ecosystem::actor_ids::FEE_SINK_ACTORS_ID) +pub struct RuntimeAdmissionCertificateAuthority; + +impl AdmissionCertificateAuthorityProvider for RuntimeAdmissionCertificateAuthority { + fn current() -> Option { + type ControlWeights = ::StepControlWeight; + type EffectWeights = ::TaskEffectWeight; + let control_identity = , + >>::production_weight_identity()?; + let effect_identity = , + >>::production_weight_identity()?; + let configured_bounds_commitment = ( + *b"DEOS_ACTOR_CONFIGURED_BOUNDS", + ( + ActorMaxContractSteps::get(), + ActorMaxFundingTrackedAssets::get(), + ActorMaxOpeningSnapshotEntries::get(), + ActorMaxOpeningPredicateResults::get(), + ActorMaxPreconditionClauses::get(), + ActorMaxPredicatesPerClause::get(), + ActorMaxPredicatesPerStep::get(), + ActorMaxSplitTransferLegs::get(), + ActorMaxWhitelistSize::get(), + ), + ( + ActorMaxConsecutiveFailures::get(), + ActorMaxRetryAttempts::get(), + ActorMaxAutoCloseNonceHorizon::get(), + ActorMaxExecutionDelayBlocks::get(), + ActorMaxTemporalDelayTicks::get(), + ActorMinWindowLength::get(), + ActorTargetBlockTime::get(), + ), + ( + ActorMaxExecutionsPerBlock::get(), + ActorMaxQueueLength::get(), + ActorMaxQueueEntriesScannedPerBlock::get(), + ActorQueuePageSize::get(), + ActorWakeupPageSize::get(), + ActorObservationPageSize::get(), + ActorOnIdleReserve::get(), + ), + ) + .using_encoded(polkadot_sdk::sp_io::hashing::blake2_256); + type LifecycleWeights = crate::weights::pallet_deos_actors::SubstrateWeight; + let maximum_lifecycle_weight = [ + LifecycleWeights::create_user_actor(), + LifecycleWeights::create_user_actor_crossing_new_page(), + LifecycleWeights::create_user_actor_at_slot(), + LifecycleWeights::create_system_actor(), + LifecycleWeights::create_system_actor_at_sovereign_id(), + LifecycleWeights::create_dormant_system_actor(), + LifecycleWeights::activate_actor(), + LifecycleWeights::deactivate_actor(), + LifecycleWeights::pause_actor(), + LifecycleWeights::resume_actor(), + LifecycleWeights::update_contract(), + LifecycleWeights::close_actor(), + LifecycleWeights::run_cancel(), + ] + .into_iter() + .fold(Weight::zero(), |maximum, weight| { + Weight::from_parts( + maximum.ref_time().max(weight.ref_time()), + maximum.proof_size().max(weight.proof_size()), + ) + }); + Some(AdmissionCertificateAuthority { + runtime_actor_semantics_version: 1, + production_weight_identity: AdmissionCertificateAuthority::compose_production_weight_identity( + control_identity, + effect_identity, + ), + body_geometry_version: 1, + configured_bounds_commitment, + maximum_lifecycle_weight, + }) } } @@ -688,15 +1781,9 @@ impl LiquidityOps for TmctolLiquidityOps { use alloc::boxed::Box; let result = (|| -> Result<(Balance, Balance, Balance), TaskFailure> { if AssetConversion::get_reserves(asset_a, asset_b).is_err() { - AssetConversion::create_pool( - RuntimeOrigin::signed(who.clone()), - Box::new(asset_a), - Box::new(asset_b), - ) - .map_err(TaskFailure::permanent)?; + crate::DeosRouter::create_pool(RuntimeOrigin::signed(who.clone()), asset_a, asset_b) + .map_err(TaskFailure::permanent)?; } - super::assets_config::register_pool_lp_pair(asset_a, asset_b) - .map_err(TaskFailure::permanent)?; let lp_before = liquidity_lp_balance(who, asset_a, asset_b); let a_before = TmctolAssetOps::balance(who, asset_a); let b_before = TmctolAssetOps::balance(who, asset_b); @@ -1068,7 +2155,7 @@ impl DeosSystemActorContractValidator { let contract = candidate .filter(|(candidate_id, _)| *candidate_id == actor_id) .map(|(_, contract)| contract.clone()) - .or_else(|| pallet_deos_actors::ActorContracts::::get(actor_id)); + .or_else(|| crate::Actors::actor_contract(actor_id)); if let Some(contract) = contract { contracts.push((actor_id, contract)); } @@ -2305,6 +3392,15 @@ impl pallet_deos_actors::ObservationProvider bool { + Timestamp::get() != 0 + && polkadot_sdk::cumulus_pallet_parachain_system::ValidationData::::exists() + } +} + impl pallet_deos_actors::Config for Runtime { type PalletId = ActorsPalletId; type SystemOrigin = EnsureRoot; @@ -2312,6 +3408,9 @@ impl pallet_deos_actors::Config for Runtime { type FeeNativeAssetId = ActorFeeNativeAssetId; type Balance = Balance; type AssetOps = TmctolAssetOps; + type AdmissionCertificateAuthority = RuntimeAdmissionCertificateAuthority; + type StepControlWeight = RuntimeStepControlWeight; + type TaskEffectWeight = TmctolTaskEffectWeight; type ObservationFeedId = primitives::OracleFeedId; type ObservationProvider = TmctolObservationProvider; type FundingAuthority = DeosFundingAuthority; @@ -2323,9 +3422,12 @@ impl pallet_deos_actors::Config for Runtime { type Time = Timestamp; type CadenceTickMillis = ActorCadenceTickMillis; type ActorCreationFee = ActorCreationFee; + type RuntimeHoldReason = RuntimeHoldReason; + type StateHoldCurrency = Balances; + type ActorStateHoldBase = ActorStateHoldBase; + type ActorStateHoldPerByte = ActorStateHoldPerByte; type FeeSink = ActorFeeRecipient; type FeeCollector = TmctolFeeCollector; - type TriggerStateBond = RuntimeTriggerStateBond; type GenesisSystemActors = TmctolGenesisSystemActors; type SystemActorContractValidator = DeosSystemActorContractValidator; type GlobalBreakerOrigin = EnsureRoot; @@ -2341,12 +3443,13 @@ impl pallet_deos_actors::Config for Runtime { type MaxAutoCloseNonceHorizon = ActorMaxAutoCloseNonceHorizon; type TargetBlockTime = ActorTargetBlockTime; type MaxExecutionDelayBlocks = ActorMaxExecutionDelayBlocks; - type MaxCadenceDelayTicks = ActorMaxCadenceDelayTicks; + type MaxTemporalDelayTicks = ActorMaxTemporalDelayTicks; type MaxExecutionsPerBlock = ActorMaxExecutionsPerBlock; type MaxQueueLength = ActorMaxQueueLength; type QueuePageSize = ActorQueuePageSize; type WakeupPageSize = ActorWakeupPageSize; type ObservationPageSize = ActorObservationPageSize; + type CrossingPageSize = ActorCrossingPageSize; type MaxCrossingTransitionsPerFeed = ActorMaxCrossingTransitionsPerFeed; type MaxCrossingMembersPerFeed = ActorMaxCrossingMembersPerFeed; type MaxUserCrossingMembersPerFeed = ActorMaxUserCrossingMembersPerFeed; @@ -2372,6 +3475,8 @@ impl pallet_deos_actors::Config for Runtime { type MinUserBalance = ActorMinUserBalanceGuard; type MinWindowLength = ActorMinWindowLength; type WeightInfo = crate::weights::pallet_deos_actors::SubstrateWeight; + type BlockResourceBudget = crate::configs::BlockResourceBudgetValue; + type PrepassContext = DeosActorPrepassContext; type WeightToFee = crate::WeightToFee; // Runtime binds task upper bounds so fee admission stays chain-specific and auditable #[cfg(feature = "runtime-benchmarks")] @@ -2406,13 +3511,8 @@ impl RuntimeActorsBenchmarkHelper { asset_b: AssetKind, liquidity: Balance, ) -> Result<(), DispatchError> { - AssetConversion::create_pool( - RuntimeOrigin::signed(owner.clone()), - alloc::boxed::Box::new(asset_a), - alloc::boxed::Box::new(asset_b), - ) - .map_err(|_| DispatchError::Other("CreateDirectPoolForBenchmarkFailed"))?; - super::assets_config::register_pool_lp_pair(asset_a, asset_b)?; + crate::DeosRouter::create_pool(RuntimeOrigin::signed(owner.clone()), asset_a, asset_b) + .map_err(|_| DispatchError::Other("CreateDirectPoolForBenchmarkFailed"))?; let pool_account = ::PoolLocator::pool_address(&asset_a, &asset_b) .map_err(|_| DispatchError::Other("DirectPoolAddressUnavailable"))?; @@ -2542,17 +3642,7 @@ impl pallet_deos_actors::BenchmarkHelper::get().unwrap_or(0); - if current_next_lp < lp_namespace_start { - pallet_asset_conversion::NextPoolAssetId::::put(lp_namespace_start); - } - AssetConversion::create_pool( - RuntimeOrigin::signed(owner.clone()), - alloc::boxed::Box::new(asset_a), - alloc::boxed::Box::new(asset_b), - )?; - super::assets_config::register_pool_lp_pair(asset_a, asset_b)?; + crate::DeosRouter::create_pool(RuntimeOrigin::signed(owner.clone()), asset_a, asset_b)?; let pool_id = ::PoolLocator::pool_id(&asset_a, &asset_b) .map_err(|_| DispatchError::Other("PoolIdUnavailable"))?; @@ -2610,16 +3700,11 @@ impl pallet_deos_actors::BenchmarkHelper::PoolLocator::pool_address(&asset_a, &asset_b) .map_err(|_| DispatchError::Other("PoolAddressUnavailable"))?; @@ -2826,4 +3911,160 @@ impl pallet_deos_actors::BenchmarkHelper DispatchResult { crate::configs::xcm_config::benchmark_foreign_asset_deposit(recipient, source, amount) } + + type MaximumContextInherent = ( + cumulus_pallet_parachain_system::parachain_inherent::BasicParachainInherentData, + cumulus_pallet_parachain_system::parachain_inherent::InboundMessagesData, + ); + + fn prepare_maximum_context_inherent() -> Self::MaximumContextInherent { + use polkadot_sdk::frame_support::traits::BuildGenesisConfig; + parachain_info::GenesisConfig:: { + parachain_id: 100u32.into(), + _config: Default::default(), + } + .build(); + use alloc::{collections::BTreeMap, vec}; + use codec::Decode; + use cumulus_pallet_parachain_system::parachain_inherent::{ + InboundDownwardMessages, InboundHrmpMessages, InboundMessagesData, + }; + use polkadot_sdk::cumulus_primitives_core::{ + InboundDownwardMessage, InboundHrmpMessage, ParaId, + }; + + let downward_messages = (0..MaxInboundDownwardMessagesPerContext::get()) + .map(|_| InboundDownwardMessage { + sent_at: 1, + msg: vec![0; 65_536], + }) + .collect::>(); + let mut horizontal_messages = BTreeMap::new(); + let channels = MaxInboundHorizontalChannelsPerContext::get(); + for message_index in 0..MaxInboundHorizontalMessagesPerContext::get() { + let sender = ParaId::from(message_index % channels + 1); + horizontal_messages + .entry(sender) + .or_insert_with(Vec::new) + .push(InboundHrmpMessage { + sent_at: 1, + data: vec![0], + }); + } + let data = + cumulus_pallet_parachain_system::parachain_inherent::BasicParachainInherentData::decode( + &mut &include_bytes!("../../fixtures/maximum-context-basic.scale")[..], + ) + .expect("validated maximum-context BasicParachainInherentData fixture must decode"); // deos-bypass: panic-owner — fixture hash 720062c34020ad38b5c56e7772befd24a38775a7b3dff547c4b5b28dac97ae17 passed the actual maximum-context dispatch test. + let collection_size_limit = (RuntimeBlockWeights::get().max_block.proof_size() / 6) as usize; + let mut size_limit = collection_size_limit; + let downward = InboundDownwardMessages::new(downward_messages).into_abridged(&mut size_limit); + size_limit = size_limit.saturating_add(collection_size_limit); + let horizontal = + InboundHrmpMessages::from_map(horizontal_messages).into_abridged(&mut size_limit); + (data, InboundMessagesData::new(downward, horizontal)) + } + + fn execute_maximum_context_inherent( + (data, inbound): Self::MaximumContextInherent, + ) -> DispatchResult { + crate::ParachainSystem::set_validation_data( + polkadot_sdk::frame_system::RawOrigin::None.into(), + data, + inbound, + ) + } + + fn verify_maximum_context_inherent() { + assert_eq!( + polkadot_sdk::cumulus_pallet_parachain_system::ProcessedDownwardMessages::::get(), + 6 + ); + } + + fn prepare_maximum_xcm_version_discovery() { + use codec::Encode; + use xcm::{VersionedLocation, latest::prelude::*}; + + let queue = (0..::VERSION_DISCOVERY_QUEUE_SIZE) + .map(|index| { + ( + VersionedLocation::from(Location::new(0, [GeneralIndex(u128::from(index))])), + index, + ) + }) + .collect::>(); + let queue_key = polkadot_sdk::frame_support::storage::storage_prefix( + b"PolkadotXcm", + b"VersionDiscoveryQueue", + ); + let migration_key = + polkadot_sdk::frame_support::storage::storage_prefix(b"PolkadotXcm", b"CurrentMigration"); + polkadot_sdk::sp_io::storage::set(&queue_key, &queue.encode()); + polkadot_sdk::sp_io::storage::clear(&migration_key); + } + + fn execute_maximum_xcm_version_discovery() { + use polkadot_sdk::frame_support::traits::Hooks; + let _ = crate::PolkadotXcm::on_initialize(1); + } + + fn verify_maximum_xcm_version_discovery() { + use codec::Decode; + + let queue_key = polkadot_sdk::frame_support::storage::storage_prefix( + b"PolkadotXcm", + b"VersionDiscoveryQueue", + ); + let encoded = polkadot_sdk::sp_io::storage::get(&queue_key) + .expect("XCM discovery queue must be rewritten after traversal"); // deos-bypass: panic-owner — benchmark setup writes the exact storage key and on_initialize always rewrites the taken queue. + let remaining = Vec::<(xcm::VersionedLocation, u32)>::decode(&mut &encoded[..]) + .expect("rewritten XCM discovery queue must decode"); // deos-bypass: panic-owner — pallet_xcm owns the same bounded queue SCALE type used by setup. + assert!(remaining.is_empty()); + } + + fn prepare_block_resource_meter_extension() { + use polkadot_sdk::sp_runtime::traits::One; + + let measured_block = crate::System::block_number().saturating_add(One::one()); + let mut state = pallet_deos_actors::BlockResourceState::new(measured_block); + state + .begin_prepass() + .and_then(|()| state.open_external_phase()) + .expect("benchmark state must enter ExternalPhase"); // deos-bypass: panic-owner — fresh state has no reservations and follows the canonical transition. + pallet_deos_actors::CurrentBlockResourceState::::put(state); + } + + fn execute_block_resource_meter_extension() { + use polkadot_sdk::frame_support::dispatch::{GetDispatchInfo, Pays, PostDispatchInfo}; + use polkadot_sdk::sp_runtime::traits::TransactionExtension; + + let extension = crate::configs::resource_meter::BlockResourceMeterExtension; + let call = crate::RuntimeCall::System(frame_system::Call::remark { remark: Vec::new() }); + let mut info = call.get_dispatch_info(); + info.extension_weight = extension.weight(&call); + let origin = crate::RuntimeOrigin::none(); + let reservation = extension + .prepare((), &origin, &call, &info, 0) + .expect("prepared benchmark resource state must admit one remark"); // deos-bypass: panic-owner — benchmark setup opens the exact current-block ExternalPhase budget. + let post_info = PostDispatchInfo { + actual_weight: Some(info.total_weight()), + pays_fee: Pays::Yes, + }; + let _ = crate::configs::resource_meter::BlockResourceMeterExtension::post_dispatch_details( + reservation, + &info, + &post_info, + 0, + &Ok(()), + ) + .expect("valid benchmark reservation must settle"); // deos-bypass: panic-owner — actual equals the exact reserved maximum. + } + + fn verify_block_resource_meter_extension() { + let state = crate::Actors::block_resource_state() + .expect("resource extension benchmark must retain current state"); // deos-bypass: panic-owner — setup creates and execution mutates the authoritative state. + assert_eq!(state.outstanding_reservations(), 0); + assert!(state.usage().user_dispatch_used() != Weight::zero()); + } } diff --git a/template/runtime/src/configs/address_event_ingress.rs b/template/runtime/src/configs/address_event_ingress.rs index 605f6752..64477f53 100644 --- a/template/runtime/src/configs/address_event_ingress.rs +++ b/template/runtime/src/configs/address_event_ingress.rs @@ -292,7 +292,12 @@ impl AddressEventIngressExtension { } fn notify_weight() -> Weight { - <::WeightInfo as pallet_deos_actors::WeightInfo>::transaction_extension_ingress_notify() + let base = Self::base_weight(); + let notify = <::WeightInfo as pallet_deos_actors::WeightInfo>::transaction_extension_ingress_notify(); + Weight::from_parts( + base.ref_time().max(notify.ref_time()), + base.proof_size().max(notify.proof_size()), + ) } pub(crate) fn post_dispatch_refund(result_is_err: bool, submitted: bool) -> Weight { diff --git a/template/runtime/src/configs/assets_config.rs b/template/runtime/src/configs/assets_config.rs index 2653b677..8cd49f6f 100644 --- a/template/runtime/src/configs/assets_config.rs +++ b/template/runtime/src/configs/assets_config.rs @@ -9,7 +9,7 @@ use polkadot_sdk::{ frame_support::{parameter_types, traits::*}, pallet_asset_conversion::{self, PoolLocator}, pallet_assets, - sp_runtime::traits::AccountIdConversion, + sp_runtime::traits::{AccountIdConversion, TryConvert}, }; use crate::{ @@ -44,6 +44,36 @@ impl polkadot_sdk::frame_support::traits::EnsureOriginWithArg for DeosPoolLocator { + fn address(id: &(AssetKind, AssetKind)) -> Result { + if !id.0.is_valid_market_pair(id.1) { + return Err(()); + } + pallet_asset_conversion::AccountIdConverter::< + AssetConversionPalletId, + (AssetKind, AssetKind), + >::try_convert(id) + .map_err(|_| ()) + } + + fn pool_id(asset1: &AssetKind, asset2: &AssetKind) -> Result<(AssetKind, AssetKind), ()> { + if !asset1.is_valid_market_pair(*asset2) { + return Err(()); + } + if asset1 < asset2 { + Ok((*asset1, *asset2)) + } else { + Ok((*asset2, *asset1)) + } + } +} + impl polkadot_sdk::sp_runtime::traits::Convert< AssetKind, @@ -82,7 +112,9 @@ parameter_types! { // -- Asset Conversion Constants -- pub const AssetConversionPalletId: polkadot_sdk::frame_support::PalletId = polkadot_sdk::frame_support::PalletId(*primitives::ecosystem::pallet_ids::ASSET_CONVERSION_PALLET_ID); - /// Liquidity withdrawal fee (0%) + /// XYK liquidity-provider swap fee (0% for the current launch line). + pub const XykLpFee: polkadot_sdk::sp_runtime::Permill = polkadot_sdk::sp_runtime::Permill::from_percent(0); + /// Independent liquidity withdrawal fee (0% for the current launch line). pub const LiquidityWithdrawalFee: polkadot_sdk::sp_runtime::Permill = polkadot_sdk::sp_runtime::Permill::from_percent(0); /// Minimum liquidity that must be minted when creating a pool pub const MintMinLiquidity: Balance = 100; @@ -168,6 +200,70 @@ impl pallet_asset_registry::Config for Runtime { type WeightInfo = crate::weights::pallet_asset_registry::SubstrateWeight; } +pub struct DeosPoolLifecycle; + +#[cfg(test)] +std::thread_local! { + static FAIL_AFTER_POOL_CREATION: core::cell::Cell = const { core::cell::Cell::new(false) }; + static FORCE_LP_IDENTITY_MISMATCH: core::cell::Cell = const { core::cell::Cell::new(false) }; +} + +#[cfg(test)] +pub(crate) fn set_fail_after_pool_creation(value: bool) { + FAIL_AFTER_POOL_CREATION.with(|flag| flag.set(value)); +} + +#[cfg(test)] +pub(crate) fn set_force_lp_identity_mismatch(value: bool) { + FORCE_LP_IDENTITY_MISMATCH.with(|flag| flag.set(value)); +} + +impl pallet_deos_router::PoolLifecycleApi for DeosPoolLifecycle { + #[polkadot_sdk::frame_support::transactional] + fn create_pool( + who: &AccountId, + asset1: AssetKind, + asset2: AssetKind, + ) -> polkadot_sdk::sp_runtime::DispatchResult { + let pool_id = DeosPoolLocator::pool_id(&asset1, &asset2) + .map_err(|_| polkadot_sdk::sp_runtime::DispatchError::Other("Invalid asset pair"))?; + polkadot_sdk::frame_support::ensure!( + !pallet_asset_conversion::Pools::::contains_key(pool_id), + polkadot_sdk::sp_runtime::DispatchError::Other("Pool already exists") + ); + super::deos_router_config::AssetConversionAdapter::ensure_lp_asset_namespace(); + let expected_lp = pallet_asset_conversion::NextPoolAssetId::::get().ok_or( + polkadot_sdk::sp_runtime::DispatchError::Other("LP namespace unavailable"), + )?; + preflight_register_pool_lp_pair(pool_id.0, pool_id.1, expected_lp)?; + crate::AssetConversion::create_pool( + RuntimeOrigin::signed(who.clone()), + alloc::boxed::Box::new(pool_id.0), + alloc::boxed::Box::new(pool_id.1), + )?; + let actual = pallet_asset_conversion::Pools::::get(pool_id).ok_or( + polkadot_sdk::sp_runtime::DispatchError::Other("Created pool missing"), + )?; + #[cfg(test)] + let expected_lp = if FORCE_LP_IDENTITY_MISMATCH.with(|flag| flag.get()) { + expected_lp.saturating_add(1) + } else { + expected_lp + }; + polkadot_sdk::frame_support::ensure!( + actual.lp_token == expected_lp, + polkadot_sdk::sp_runtime::DispatchError::Other("LP identity mismatch") + ); + #[cfg(test)] + if FAIL_AFTER_POOL_CREATION.with(|flag| flag.get()) { + return Err(polkadot_sdk::sp_runtime::DispatchError::Other( + "Injected post-pool lifecycle failure", + )); + } + register_pool_lp_pair(pool_id.0, pool_id.1) + } +} + pub(crate) fn preflight_register_pool_lp_pair( asset1: AssetKind, asset2: AssetKind, @@ -192,6 +288,19 @@ pub(crate) fn register_pool_lp_pair( super::oracle_config::ensure_deos_router_pool_feeds(pool_id.0, pool_id.1) } +#[cfg(feature = "runtime-benchmarks")] +pub struct AssetConversionBenchmarkHelper; + +#[cfg(feature = "runtime-benchmarks")] +impl pallet_asset_conversion::BenchmarkHelper for AssetConversionBenchmarkHelper { + fn create_pair(seed1: u32, seed2: u32) -> (AssetKind, AssetKind) { + ( + AssetKind::Local(seed1 & primitives::MASK_INDEX), + AssetKind::Local(seed2 & primitives::MASK_INDEX), + ) + } +} + impl pallet_asset_conversion::Config for Runtime { type AssetKind = AssetKind; type Assets = polkadot_sdk::frame_support::traits::fungible::UnionOf< @@ -203,29 +312,17 @@ impl pallet_asset_conversion::Config for Runtime { >; type Balance = Balance; #[cfg(feature = "runtime-benchmarks")] - type BenchmarkHelper = (); + type BenchmarkHelper = AssetConversionBenchmarkHelper; type HigherPrecisionBalance = polkadot_sdk::sp_core::U256; type LiquidityWithdrawalFee = LiquidityWithdrawalFee; - type LPFee = LiquidityWithdrawalFee; + type LPFee = XykLpFee; type MaxSwapPathLength = ConstU32<4>; type MintMinLiquidity = MintMinLiquidity; type PalletId = AssetConversionPalletId; type PoolAssetId = u32; type PoolAssets = pallet_assets::Pallet; type PoolId = (AssetKind, AssetKind); - type PoolLocator = pallet_asset_conversion::Chain< - pallet_asset_conversion::WithFirstAsset< - NativeAssetId, - AccountId, - AssetKind, - pallet_asset_conversion::AccountIdConverter, - >, - pallet_asset_conversion::Ascending< - AccountId, - AssetKind, - pallet_asset_conversion::AccountIdConverter, - >, - >; + type PoolLocator = DeosPoolLocator; type PoolSetupFee = PoolSetupFee; type PoolSetupFeeAsset = NativeAssetId; type PoolSetupFeeTarget = (); @@ -237,6 +334,56 @@ impl pallet_asset_conversion::Config for Runtime { mod tests { use super::*; + #[test] + fn reference_runtime_xyk_and_withdrawal_fees_are_independent_zero_parameters() { + assert_eq!(XykLpFee::get(), polkadot_sdk::sp_runtime::Permill::zero()); + assert_eq!( + LiquidityWithdrawalFee::get(), + polkadot_sdk::sp_runtime::Permill::zero() + ); + assert_eq!( + ::LPFee::get(), + XykLpFee::get() + ); + assert_eq!( + ::LiquidityWithdrawalFee::get(), + LiquidityWithdrawalFee::get() + ); + assert_eq!( + super::super::deos_router_config::DeosRouterFee::get(), + polkadot_sdk::sp_runtime::Perbill::from_rational(5u32, 1_000u32) + ); + } + + #[test] + fn canonical_pool_locator_rejects_same_ledger_and_noncanonical_pairs() { + let id = primitives::TYPE_FOREIGN | 7; + assert!(DeosPoolLocator::pool_id(&AssetKind::Local(id), &AssetKind::Foreign(id)).is_err()); + assert!(DeosPoolLocator::pool_id(&AssetKind::Native, &AssetKind::Local(id)).is_err()); + assert!( + DeosPoolLocator::pool_id( + &AssetKind::Native, + &AssetKind::Foreign(primitives::TYPE_PROTOCOL | 7), + ) + .is_err() + ); + } + + #[test] + fn canonical_pool_locator_orders_distinct_physical_assets_once() { + let foreign = AssetKind::Foreign(primitives::TYPE_FOREIGN | 7); + let expected = (AssetKind::Native, foreign); + assert_eq!( + DeosPoolLocator::pool_id(&foreign, &AssetKind::Native), + Ok(expected) + ); + assert_eq!( + DeosPoolLocator::pool_id(&AssetKind::Native, &foreign), + Ok(expected) + ); + assert!(DeosPoolLocator::address(&expected).is_ok()); + } + #[test] fn genesis_protocol_asset_policy_exposes_well_known_veto_asset() { let assets = genesis_protocol_assets(); diff --git a/template/runtime/src/configs/deos_router_config.rs b/template/runtime/src/configs/deos_router_config.rs index 281bf436..2d7a8a54 100644 --- a/template/runtime/src/configs/deos_router_config.rs +++ b/template/runtime/src/configs/deos_router_config.rs @@ -124,7 +124,6 @@ pub(crate) fn market_execution_failure(error: DispatchError) -> pallet_deos_rout let retryable = [ MarketError::::ReserveLeftLessThanMinimal.into(), MarketError::::AmountOutTooHigh.into(), - MarketError::::PoolNotFound.into(), MarketError::::ProvidedMinimumNotSufficientForSwap.into(), MarketError::::ProvidedMaximumNotSufficientForSwap.into(), MarketError::::BelowMinimum.into(), @@ -249,7 +248,7 @@ impl AssetConversionAdapter { amount2: Balance, max_ratio_error: Perbill, ) -> Result<(), pallet_deos_actors::TaskFailure> { - if amount1.is_zero() || amount2.is_zero() || asset1 == asset2 { + if amount1.is_zero() || amount2.is_zero() || !asset1.is_valid_market_pair(asset2) { return Err(pallet_deos_actors::TaskFailure::permanent( DispatchError::Other("InvalidDonation"), )); @@ -470,14 +469,7 @@ impl AssetConversionAdapter { impl pallet_deos_router::AssetConversionApi for AssetConversionAdapter { fn single_pool_id(asset_a: AssetKind, asset_b: AssetKind) -> Option<(AssetKind, AssetKind)> { - if asset_a == asset_b { - return None; - } - if asset_a < asset_b { - Some((asset_a, asset_b)) - } else { - Some((asset_b, asset_a)) - } + ::PoolLocator::pool_id(&asset_a, &asset_b).ok() } fn single_pool_reserves(pool_id: (AssetKind, AssetKind)) -> Option<(Balance, Balance)> { @@ -793,10 +785,87 @@ impl pallet_deos_router::types::LpPairIntegrity for RuntimeLpPairIntegrity { } fn expected_pool_count() -> Result, &'static str> { - let count = u32::try_from(pallet_asset_conversion::Pools::::iter_keys().count()) - .map_err(|_| "Asset Conversion pool cardinality exceeds u32")?; + let maximum = DeosRouterMaxLpPairs::get(); + let scan_limit = usize::try_from(maximum.saturating_add(1)) + .map_err(|_| "Asset Conversion pool scan bound exceeds usize")?; + let count = u32::try_from( + pallet_asset_conversion::Pools::::iter_keys() + .take(scan_limit) + .count(), + ) + .map_err(|_| "Asset Conversion pool cardinality exceeds u32")?; + if count > maximum { + return Err("Asset Conversion pool cardinality exceeds the lifecycle bound"); + } Ok(Some(count)) } + + fn validate_complete_topology() -> Result<(), &'static str> { + let reverse = pallet_deos_router::LpPairByTokenId::::get(); + let mut physical_pairs = alloc::collections::BTreeSet::new(); + for (lp_token, pair) in &reverse { + let pool = pallet_asset_conversion::Pools::::get(pair) + .ok_or("LP reverse binding references an absent Asset Conversion pool")?; + if pool.lp_token != *lp_token { + return Err("LP reverse binding disagrees with the pool LP token"); + } + } + let maximum = DeosRouterMaxLpPairs::get(); + let scan_limit = usize::try_from(maximum.saturating_add(1)) + .map_err(|_| "Asset Conversion pool scan bound exceeds usize")?; + let mut scanned = 0u32; + let all_pools = pallet_asset_conversion::Pools::::iter(); // deos-bypass: bounded-iter — consumed through MaxLpPairs + 1 + let mut pools = all_pools.take(scan_limit); + while let Some((pair, pool)) = pools.next() { + scanned = scanned + .checked_add(1) + .ok_or("Asset Conversion pool scan count overflow")?; + if scanned > maximum { + return Err("Asset Conversion pool cardinality exceeds the lifecycle bound"); + } + let canonical = + ::PoolLocator::pool_id(&pair.0, &pair.1) + .map_err(|_| "Asset Conversion pool has invalid canonical assets")?; + if canonical != pair { + return Err("Asset Conversion pool key is not canonical"); + } + let physical = (pair.0.ledger_key(), pair.1.ledger_key()); + if !physical_pairs.insert(physical) { + return Err("Asset Conversion contains a duplicate physical pair"); + } + if reverse.get(&pool.lp_token) != Some(&pair) { + return Err("Asset Conversion pool lacks its exact LP reverse binding"); + } + if ! as FungiblesInspect>::asset_exists( + pool.lp_token, + ) { + return Err("Asset Conversion pool LP asset does not exist"); + } + let forward = super::oracle_config::deos_router_pool_feed(pair.0, pair.1); + let backward = super::oracle_config::deos_router_pool_feed(pair.1, pair.0); + if !pallet_oracle::Feeds::::contains_key(forward) + || !pallet_oracle::Feeds::::contains_key(backward) + { + return Err("Asset Conversion pool lacks required Oracle topology"); + } + } + Ok(()) + } + + fn validate_genesis_topology() -> Result<(), &'static str> { + if pallet_asset_conversion::Pools::::iter_keys() + .next() + .is_some() + { + return Err( + "raw Asset Conversion genesis pools are unsupported; create complete pools through the permissionless DEOS lifecycle after genesis", + ); + } + if !pallet_deos_router::LpPairByTokenId::::get().is_empty() { + return Err("genesis contains LP bindings without lifecycle-owned pools"); + } + Ok(()) + } } impl pallet_deos_router::pallet::Config for Runtime { @@ -816,6 +885,7 @@ impl pallet_deos_router::pallet::Config for Runtime { type MinSwapForeign = MinSwapForeign; type NativeAsset = NativeAsset; type PalletId = RouterPalletId; + type PoolLifecycle = super::assets_config::DeosPoolLifecycle; type Precision = DeosRouterPrecision; type PriceOracle = PriceOracleImpl; type TmcPallet = TmcPalletAdapter; @@ -867,13 +937,7 @@ impl pallet_deos_router::types::BenchmarkHelper let creator = BurnActorAccount::get(); let _ = >::deposit_creating(&creator, 1_000_000_000_000_000_000); - AssetConversionAdapter::ensure_lp_asset_namespace(); - AssetConversion::create_pool( - RuntimeOrigin::signed(creator), - Box::new(asset1), - Box::new(asset2), - )?; - super::assets_config::register_pool_lp_pair(asset1, asset2) + crate::DeosRouter::create_pool(RuntimeOrigin::signed(creator), asset1, asset2) } fn prepare_observation_hook( diff --git a/template/runtime/src/configs/governance_config.rs b/template/runtime/src/configs/governance_config.rs index 279401dc..994a929e 100644 --- a/template/runtime/src/configs/governance_config.rs +++ b/template/runtime/src/configs/governance_config.rs @@ -41,7 +41,7 @@ parameter_types! { polkadot_sdk::sp_runtime::Perbill::from_percent(1); pub const ProposalMinimumTurnout: u64 = 200; pub const MaxGovernanceEpochCatchUpPerBlock: u32 = 1; - pub const MaxGovernanceMaturingProposalsPerBlock: u32 = 3; + pub const MaxGovernanceMaturingProposalsPerBlock: u32 = 2; pub const MaxGovernancePendingEnactmentsPerBlock: u32 = 4; pub const MaxGovernanceFinalizedOutcomesPerBlock: u32 = 1024; pub const MaxGovernanceExpiringAccountsPerBlock: u32 = 512; @@ -51,6 +51,35 @@ parameter_types! { pub const MaxExpiringAccountsPerEpoch: u32 = 1024; } +parameter_types! { + /// Worst reachable one-block Governance hook branch: catch-up plus one bounded service phase. + pub GovernanceFixedWeight: Weight = { + type W = crate::weights::pallet_governance::SubstrateWeight; + let phase = [ + ::service_maturing_proposals( + MaxGovernanceMaturingProposalsPerBlock::get(), + ), + ::service_pending_enactments( + MaxGovernancePendingEnactmentsPerBlock::get(), + ), + ::service_finalized_proposal_outcomes( + MaxGovernanceFinalizedOutcomesPerBlock::get(), + ), + ::service_expiring_accounts( + MaxGovernanceExpiringAccountsPerBlock::get(), + ), + ] + .into_iter() + .fold(Weight::zero(), |left, right| { + Weight::from_parts( + left.ref_time().max(right.ref_time()), + left.proof_size().max(right.proof_size()), + ) + }); + ::service_epoch_catch_up().saturating_add(phase) + }; +} + pub struct RuntimeGovernanceEpochProvider; impl pallet_governance::EpochProvider for RuntimeGovernanceEpochProvider { fn current_epoch() -> BlockNumber { diff --git a/template/runtime/src/configs/mod.rs b/template/runtime/src/configs/mod.rs index 16e5a190..75fbfa1f 100644 --- a/template/runtime/src/configs/mod.rs +++ b/template/runtime/src/configs/mod.rs @@ -4,8 +4,8 @@ pub(crate) mod assets_config; pub mod deos_router_config; pub mod governance_config; pub mod oracle_config; -pub mod pool_index; pub mod preimage_config; +pub mod resource_meter; pub mod staking_config; pub(crate) mod tmc_config; mod xcm_config; @@ -47,6 +47,7 @@ use std::cell::Cell; use xcm::latest::prelude::{AssetId, BodyId}; // Local module imports +use self::governance_config::GovernanceFixedWeight; use super::{ AVERAGE_ON_INITIALIZE_RATIO, AccountId, Aura, Balance, Balances, Block, BlockNumber, CollatorSelection, EXISTENTIAL_DEPOSIT, HOURS, Hash, MAXIMUM_BLOCK_WEIGHT, MICRO_UNIT, @@ -102,11 +103,33 @@ parameter_types! { pub const SS58Prefix: u16 = 42; } +/// Fail-closed public Asset Conversion surface. +/// +/// DEOS Router owns every user-initiated XYK swap and its protocol fee. Direct runtime calls keep +/// only reviewed liquidity maintenance operations. Canonical permissionless creation is owned by +/// the DEOS Router lifecycle. New upstream Asset Conversion calls are denied by default. +pub struct RuntimeCallFilter; + +impl frame_support::traits::Contains for RuntimeCallFilter { + fn contains(call: &RuntimeCall) -> bool { + match call { + RuntimeCall::AssetConversion(call) => matches!( + call, + pallet_asset_conversion::Call::add_liquidity { .. } + | pallet_asset_conversion::Call::remove_liquidity { .. } + | pallet_asset_conversion::Call::touch { .. } + ), + _ => true, + } + } +} + /// The default types are being injected by [`derive_impl`](`frame_support::derive_impl`) from /// [`ParaChainDefaultConfig`](`struct@frame_system::config_preludes::ParaChainDefaultConfig`), /// but overridden as needed. #[derive_impl(frame_system::config_preludes::ParaChainDefaultConfig)] impl frame_system::Config for Runtime { + type BaseCallFilter = RuntimeCallFilter; /// The identifier used to distinguish between accounts. type AccountId = AccountId; /// The index type for storing how many extrinsics an account has signed. @@ -207,8 +230,33 @@ impl pallet_transaction_payment::Config for Runtime { } parameter_types! { - pub const ReservedXcmpWeight: Weight = MAXIMUM_BLOCK_WEIGHT.saturating_div(4); - pub const ReservedDmpWeight: Weight = MAXIMUM_BLOCK_WEIGHT.saturating_div(4); + pub MaximumContextMeasuredWeight: Weight = + as pallet_deos_actors::WeightInfo>::maximum_context_inherent(); + /// Relay DMP storage is capped by 32 MiB divided by the sanitized 64 KiB minimum + /// `max_downward_message_size`; this is the declared cap for Prepass inherent checking. + pub const MaxInboundDownwardMessagesPerContext: u32 = 512; + /// Relay configuration admits at most 128 inbound HRMP channels per parachain. + pub const MaxInboundHorizontalChannelsPerContext: u32 = 128; + /// DEOS fail-closed cap across full and hashed HRMP messages in one context inherent. + /// It covers 1,000 messages on every maximally admitted inbound channel. + pub const MaxInboundHorizontalMessagesPerContext: u32 = 131_072; + pub DmpFixedWeight: Weight = + as + cumulus_pallet_parachain_system::WeightInfo>::enqueue_inbound_downward_messages( + MaxInboundDownwardMessagesPerContext::get(), + ); + /// Current SDK enqueue adapters do not consume this value directly; keep the compatibility + /// surface equal to the measured finite owner rather than a fictitious block percentage. + pub ReservedDmpWeight: Weight = DmpFixedWeight::get(); + /// Smallest component-wise XCMP residual whose sum with DMP dominates the complete measured + /// maximum-context inherent. Outer enqueue bookkeeping is added by the fixed owner below. + pub ReservedXcmpWeight: Weight = + MaximumContextMeasuredWeight::get().saturating_sub(DmpFixedWeight::get()); + /// Maximum currently registered by the XCMP enqueue path: handler meter plus its outer + /// two-read/three-write bookkeeping. Metadata traversal remains a separate evidence owner. + pub XcmpRegisteredFixedWeight: Weight = ReservedXcmpWeight::get().saturating_add( + ::DbWeight::get().reads_writes(2, 3), + ); pub const RelayOrigin: AggregateMessageOrigin = AggregateMessageOrigin::Parent; } @@ -294,9 +342,123 @@ impl cumulus_pallet_xcmp_queue::Config for Runtime { type PriceForSiblingDelivery = PriceForSiblingParachainDelivery; } +#[allow(dead_code)] // Referenced by FixedBlockWeightComponentsValue's lazy getter. +pub fn frame_and_other_fixed_weight() -> Weight { + let block_weights = RuntimeBlockWeights::get(); + let mandatory_base = block_weights.get(DispatchClass::Mandatory).base_extrinsic; + block_weights + .base_block + .checked_add(&mandatory_base) + .and_then(|weight| weight.checked_add(&mandatory_base)) + .and_then(|weight| weight.checked_add(&mandatory_base)) + .and_then(|weight| weight.checked_add(&MessageQueueServiceWeight::get())) + .and_then(|weight| weight.checked_add(&XcmpQueue::on_idle_weight())) + .and_then(|weight| weight.checked_add(&GovernanceFixedWeight::get())) + .and_then(|weight| weight.checked_add(&SessionRotationFixedWeight::get())) + .and_then(|weight| weight.checked_add(&AuthorshipFixedWeight::get())) + .and_then(|weight| weight.checked_add(&AuraFixedWeight::get())) + .and_then(|weight| weight.checked_add(&AuraExtFixedWeight::get())) + .and_then(|weight| weight.checked_add(&XcmVersionDiscoveryFixedWeight::get())) + .unwrap_or(Weight::MAX) +} + +const ACTOR_CONTROL_RATIO: (u64, u64) = (1, 3); + parameter_types! { - pub const Period: u32 = 6 * HOURS; - pub const Offset: u32 = 0; + pub AuthorshipFixedWeight: Weight = + <::WeightInfo as pallet_collator_selection::weights::WeightInfo>::note_author(); + pub AuraFixedWeight: Weight = ::DbWeight::get().reads_writes(2, 1); + pub AuraExtFixedWeight: Weight = ::DbWeight::get().reads_writes(2, 1); + /// Upstream XCM lazy migration is explicitly capped at one tenth of max block Weight. + pub XcmMigrationMaximumWeight: Weight = RuntimeBlockWeights::get().max_block / 10; + pub XcmVersionDiscoveryFixedWeight: Weight = + as pallet_deos_actors::WeightInfo>::maximum_xcm_version_discovery(); + pub FixedBlockWeightComponentsValue: pallet_deos_actors::FixedBlockWeightComponents = + pallet_deos_actors::FixedBlockWeightComponents::new( + frame_and_other_fixed_weight(), + as pallet_timestamp::WeightInfo>::set() + .saturating_add( as pallet_timestamp::WeightInfo>::on_finalize()), + Weight::zero(), + DmpFixedWeight::get(), + XcmpRegisteredFixedWeight::get(), + ); + pub FixedBlockWeight: Weight = + FixedBlockWeightComponentsValue::get().total().unwrap_or(Weight::MAX); + pub BlockResourceBudgetValue: pallet_deos_actors::BlockResourceBudget = + pallet_deos_actors::BlockResourceBudget::new_with_control_ratio( + MAXIMUM_BLOCK_WEIGHT, + FixedBlockWeight::get(), + ACTOR_CONTROL_RATIO.0, + ACTOR_CONTROL_RATIO.1, + ) + .unwrap_or_else(|_| pallet_deos_actors::BlockResourceBudget::fail_closed(MAXIMUM_BLOCK_WEIGHT)); + pub const Period: u32 = 6 * HOURS; + pub const Offset: u32 = 0; + pub SessionRotationFixedWeight: Weight = + as pallet_session_rotation::WeightInfo>::rotate_session(); +} + +pub struct NeverEndSession; +impl pallet_session::ShouldEndSession for NeverEndSession { + fn should_end_session(_: BlockNumber) -> bool { + false + } +} + +pub struct RuntimeSessionRotation; +impl pallet_session_rotation::SessionRotation for RuntimeSessionRotation { + fn should_rotate(now: BlockNumber) -> bool { + as pallet_session::ShouldEndSession< + BlockNumber, + >>::should_end_session(now) + } + + fn rotate() { + pallet_session::Pallet::::rotate_session(); + } +} + +#[cfg(feature = "runtime-benchmarks")] +impl pallet_session_rotation::BenchmarkHelper for RuntimeSessionRotation { + fn prepare_rotation() { + use polkadot_sdk::frame_support::traits::Get; + + let max_candidates: u32 = ::MaxCandidates::get(); + let max_invulnerables: u32 = + ::MaxInvulnerables::get(); + let active_bound = max_candidates.saturating_add(max_invulnerables); + let mut queued = Vec::with_capacity(active_bound as usize); + for index in 0..active_bound { + let mut raw = [0u8; 32]; + raw[..4].copy_from_slice(&index.to_le_bytes()); + raw[31] = 1; + let account = AccountId::new(raw); + let keys = crate::SessionKeys { + aura: AuraId::from(sp_core::sr25519::Public::from_raw(raw)), + }; + pallet_session::NextKeys::::insert(&account, &keys); + queued.push((account, keys)); + } + pallet_session::QueuedKeys::::put(queued); + } + + fn verify_rotation() { + let max_candidates: u32 = ::MaxCandidates::get(); + let max_invulnerables: u32 = + ::MaxInvulnerables::get(); + assert_eq!( + pallet_session::QueuedKeys::::decode_len(), + Some(max_candidates.saturating_add(max_invulnerables) as usize) + ); + assert_eq!(pallet_session::CurrentIndex::::get(), 1); + } +} + +impl pallet_session_rotation::Config for Runtime { + type SessionRotation = RuntimeSessionRotation; + type WeightInfo = crate::weights::pallet_session_rotation::SubstrateWeight; + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = RuntimeSessionRotation; } #[cfg(test)] @@ -609,7 +771,7 @@ impl pallet_session::Config for Runtime { type KeyDeposit = (); // We don't have stash and controller, thus we don't need the convert as well. type ValidatorIdOf = pallet_collator_selection::IdentityCollator; - type ShouldEndSession = pallet_session::PeriodicSessions; + type ShouldEndSession = NeverEndSession; type NextSessionRotation = pallet_session::PeriodicSessions; type SessionManager = DelegationWeightedCollatorSessionManager; // Essentially just Aura, but let's be pedantic. diff --git a/template/runtime/src/configs/oracle_config.rs b/template/runtime/src/configs/oracle_config.rs index be241973..19ebe2a9 100644 --- a/template/runtime/src/configs/oracle_config.rs +++ b/template/runtime/src/configs/oracle_config.rs @@ -1,5 +1,7 @@ use crate::{AccountId, Oracle, Runtime, RuntimeOrigin}; -use pallet_deos_actors::{ObservationTransition, ObservationTransitionIngress}; +use pallet_deos_actors::{ + ObservationTransition, ObservationTransitionIngress, TriggerCauseProvenance, +}; use pallet_oracle::{Aggregation, FeedConfig, FeedLifecycle, ZeroPolicy}; use polkadot_sdk::{ frame_support::{ensure, parameter_types, transactional}, @@ -52,8 +54,8 @@ pub(crate) fn preflight_deos_router_pool_feeds( asset_b: AssetKind, ) -> DispatchResult { ensure!( - asset_a != asset_b, - DispatchError::Other("Identical oracle feed assets") + asset_a.is_valid_market_pair(asset_b), + DispatchError::Other("Invalid oracle pool assets") ); let forward = deos_router_pool_feed(asset_a, asset_b); let reverse = forward.reverse(); @@ -150,6 +152,7 @@ impl pallet_oracle::PublicationBenchmarkHelper for OraclePublicati previous: None, current: 1_000_000_000, }, + TriggerCauseProvenance::Deferred, )?; } if topology == pallet_oracle::ChangedHookBenchmarkTopology::SecondaryExisting { @@ -160,6 +163,7 @@ impl pallet_oracle::PublicationBenchmarkHelper for OraclePublicati previous: None, current: 1_000_000_000, }, + TriggerCauseProvenance::Deferred, )?; } Ok(()) @@ -177,6 +181,7 @@ impl pallet_oracle::PublicationBenchmarkHelper for OraclePublicati previous: None, current, }, + TriggerCauseProvenance::Deferred, )?; let capacity = <::MaxCrossingTransitionsPerFeed as polkadot_sdk::frame_support::traits::Get>::get(); for offset in 0..capacity { @@ -191,6 +196,7 @@ impl pallet_oracle::PublicationBenchmarkHelper for OraclePublicati previous: Some(previous), current, }, + TriggerCauseProvenance::Deferred, )?; } Ok((u64::from(capacity).saturating_add(1), current, true)) @@ -211,6 +217,7 @@ impl pallet_oracle::OnObservationChanged for ActorObservationChang revision: pallet_oracle::Revision, previous: Option, current: pallet_oracle::OracleValue, + cause_provenance: pallet_oracle::ObservationCauseProvenance, ) -> DispatchResult { >::note_observation_transition( feed, @@ -219,6 +226,12 @@ impl pallet_oracle::OnObservationChanged for ActorObservationChang previous, current, }, + match cause_provenance { + pallet_oracle::ObservationCauseProvenance::ExternalPhase => { + TriggerCauseProvenance::ExternalPhase + } + pallet_oracle::ObservationCauseProvenance::Deferred => TriggerCauseProvenance::Deferred, + }, ) } } diff --git a/template/runtime/src/configs/pool_index.rs b/template/runtime/src/configs/pool_index.rs deleted file mode 100644 index 46c27b4f..00000000 --- a/template/runtime/src/configs/pool_index.rs +++ /dev/null @@ -1,119 +0,0 @@ -//! Transaction-extension ownership for the Asset Conversion LP reverse index. - -use super::*; - -use codec::{Decode, DecodeWithMemTracking, Encode}; -use polkadot_sdk::sp_runtime::{ - DispatchResult, impl_tx_ext_default, - traits::{DispatchInfoOf, PostDispatchInfoOf, TransactionExtension}, - transaction_validity::{InvalidTransaction, TransactionValidityError}, -}; -use scale_info::TypeInfo; - -#[derive(Clone, Debug, Decode, DecodeWithMemTracking, Encode, Eq, PartialEq, TypeInfo)] -pub struct PoolIndexExtension; - -#[cfg(test)] -std::thread_local! { - static FAIL_POOL_INDEX_POST_DISPATCH: core::cell::Cell = const { core::cell::Cell::new(false) }; -} - -#[cfg(test)] -pub(crate) fn set_fail_pool_index_post_dispatch(value: bool) { - FAIL_POOL_INDEX_POST_DISPATCH.with(|flag| flag.set(value)); -} - -impl PoolIndexExtension { - fn index_weight() -> Weight { - let feed_registration = - as pallet_oracle::WeightInfo>::register_feed_existing_producer() - .max( - as pallet_oracle::WeightInfo>::register_feed_new_producer(), - ); - ::DbWeight::get() - .reads_writes(13, 1) - .saturating_add(feed_registration.saturating_mul(2)) - } - - fn pair(call: &RuntimeCall) -> Option<(AssetKind, AssetKind)> { - match call { - RuntimeCall::AssetConversion( - pallet_asset_conversion::Call::create_pool { asset1, asset2 } - | pallet_asset_conversion::Call::add_liquidity { asset1, asset2, .. }, - ) => Some((**asset1, **asset2)), - _ => None, - } - } - - fn expected_lp_token(call: &RuntimeCall, pair: (AssetKind, AssetKind)) -> Option { - match call { - RuntimeCall::AssetConversion(pallet_asset_conversion::Call::create_pool { .. }) => { - pallet_asset_conversion::NextPoolAssetId::::get() - } - RuntimeCall::AssetConversion(pallet_asset_conversion::Call::add_liquidity { .. }) => { - let pool_id = - ::PoolLocator::pool_id(&pair.0, &pair.1) - .ok()?; - pallet_asset_conversion::Pools::::get(pool_id).map(|pool| pool.lp_token) - } - _ => None, - } - } -} - -impl TransactionExtension for PoolIndexExtension { - const IDENTIFIER: &'static str = "PoolIndex"; - type Implicit = (); - type Val = (); - type Pre = Option<(AssetKind, AssetKind)>; - - fn weight(&self, call: &RuntimeCall) -> Weight { - if Self::pair(call).is_some() { - Self::index_weight() - } else { - Weight::zero() - } - } - - fn prepare( - self, - _val: Self::Val, - _origin: &::RuntimeOrigin, - call: &RuntimeCall, - _info: &DispatchInfoOf, - _len: usize, - ) -> Result { - let Some(pair) = Self::pair(call) else { - return Ok(None); - }; - if let Some(lp_token) = Self::expected_lp_token(call, pair) { - crate::configs::assets_config::preflight_register_pool_lp_pair(pair.0, pair.1, lp_token) - .map_err(|_| InvalidTransaction::Custom(41))?; - } - Ok(Some(pair)) - } - - fn post_dispatch_details( - pre: Self::Pre, - _info: &DispatchInfoOf, - _post_info: &PostDispatchInfoOf, - _len: usize, - result: &DispatchResult, - ) -> Result { - let Some((asset1, asset2)) = pre else { - return Ok(Weight::zero()); - }; - if result.is_err() { - return Ok(Self::index_weight()); - } - #[cfg(test)] - if FAIL_POOL_INDEX_POST_DISPATCH.with(|flag| flag.get()) { - return Err(InvalidTransaction::Custom(41).into()); - } - crate::configs::assets_config::register_pool_lp_pair(asset1, asset2) - .map_err(|_| InvalidTransaction::Custom(41))?; - Ok(Weight::zero()) - } - - impl_tx_ext_default!(RuntimeCall; validate); -} diff --git a/template/runtime/src/configs/resource_meter.rs b/template/runtime/src/configs/resource_meter.rs new file mode 100644 index 00000000..e4aa1645 --- /dev/null +++ b/template/runtime/src/configs/resource_meter.rs @@ -0,0 +1,160 @@ +//! Transaction-extension binding for the shared block-resource meter. + +use super::*; + +use codec::{Decode, DecodeWithMemTracking, Encode}; +use pallet_deos_actors::{BlockResourceDomain, BlockResourceReservation}; +use polkadot_sdk::sp_runtime::{ + DispatchResult, impl_tx_ext_default, + traits::{DispatchInfoOf, PostDispatchInfoOf, TransactionExtension}, + transaction_validity::{InvalidTransaction, TransactionValidityError}, +}; +use scale_info::TypeInfo; + +const MISSING_RESOURCE_STATE: u8 = 42; +const STALE_RESOURCE_STATE: u8 = 43; +const RESOURCE_RESERVATION_REJECTED: u8 = 44; +const RESOURCE_SETTLEMENT_REJECTED: u8 = 45; + +#[derive(Clone, Debug, Decode, DecodeWithMemTracking, Encode, Eq, PartialEq, TypeInfo)] +pub struct BlockResourceMeterExtension; + +impl TransactionExtension for BlockResourceMeterExtension { + const IDENTIFIER: &'static str = "BlockResourceMeter"; + type Implicit = (); + type Val = (); + type Pre = BlockResourceReservation; + + fn weight(&self, _call: &RuntimeCall) -> Weight { + as pallet_deos_actors::WeightInfo>::block_resource_meter_extension() + } + + fn prepare( + self, + _val: Self::Val, + _origin: &::RuntimeOrigin, + _call: &RuntimeCall, + info: &DispatchInfoOf, + _len: usize, + ) -> Result { + let mut state = pallet_deos_actors::CurrentBlockResourceState::::get() + .ok_or(InvalidTransaction::Custom(MISSING_RESOURCE_STATE))?; + state + .ensure_block(System::block_number()) + .map_err(|_| InvalidTransaction::Custom(STALE_RESOURCE_STATE))?; + let reservation = state + .reserve( + BlockResourceBudgetValue::get().limits(), + BlockResourceDomain::UserDispatch, + info.total_weight(), + ) + .map_err(|_| InvalidTransaction::Custom(RESOURCE_RESERVATION_REJECTED))?; + pallet_deos_actors::CurrentBlockResourceState::::put(state); + Ok(reservation) + } + + fn post_dispatch_details( + mut pre: Self::Pre, + info: &DispatchInfoOf, + post_info: &PostDispatchInfoOf, + _len: usize, + _result: &DispatchResult, + ) -> Result { + let mut state = pallet_deos_actors::CurrentBlockResourceState::::get() + .ok_or(InvalidTransaction::Custom(MISSING_RESOURCE_STATE))?; + state + .ensure_block(System::block_number()) + .and_then(|()| state.settle(&mut pre, post_info.calc_actual_weight(info))) + .map_err(|_| InvalidTransaction::Custom(RESOURCE_SETTLEMENT_REJECTED))?; + pallet_deos_actors::CurrentBlockResourceState::::put(state); + Ok(Weight::zero()) + } + + impl_tx_ext_default!(RuntimeCall; validate); +} + +#[cfg(test)] +mod tests { + use super::*; + use polkadot_sdk::frame_support::dispatch::GetDispatchInfo; + + fn remark() -> (RuntimeCall, DispatchInfoOf) { + let call = RuntimeCall::System(frame_system::Call::remark { remark: Vec::new() }); + let mut info = call.get_dispatch_info(); + info.extension_weight = BlockResourceMeterExtension.weight(&call); + (call, info) + } + + fn external_state(block: BlockNumber) -> pallet_deos_actors::BlockResourceState { + let mut state = pallet_deos_actors::BlockResourceState::new(block); + state.begin_prepass().expect("fresh state opens"); // deos-bypass: panic-owner — test constructs a fresh state with no reservations. + state.open_external_phase().expect("empty prepass closes"); // deos-bypass: panic-owner — preceding transition establishes PrepassExecuting without reservations. + state + } + + #[test] + fn prepare_rejects_missing_stale_and_over_capacity_state_distinctly() { + crate::tests::common::seeded_test_ext().execute_with(|| { + System::set_block_number(1); + pallet_deos_actors::CurrentBlockResourceState::::kill(); + let (call, mut info) = remark(); + let origin = RuntimeOrigin::none(); + assert_eq!( + BlockResourceMeterExtension.prepare((), &origin, &call, &info, 0), + Err(InvalidTransaction::Custom(MISSING_RESOURCE_STATE).into()) + ); + + pallet_deos_actors::CurrentBlockResourceState::::put(external_state(0)); + assert_eq!( + BlockResourceMeterExtension.prepare((), &origin, &call, &info, 0), + Err(InvalidTransaction::Custom(STALE_RESOURCE_STATE).into()) + ); + + pallet_deos_actors::CurrentBlockResourceState::::put(external_state(1)); + info.call_weight = Weight::MAX; + assert_eq!( + BlockResourceMeterExtension.prepare((), &origin, &call, &info, 0), + Err(InvalidTransaction::Custom(RESOURCE_RESERVATION_REJECTED).into()) + ); + }); + } + + #[test] + fn settlement_reclaims_valid_actual_and_rejects_lost_reservation_authority() { + crate::tests::common::seeded_test_ext().execute_with(|| { + System::set_block_number(1); + pallet_deos_actors::CurrentBlockResourceState::::put(external_state(1)); + let (call, info) = remark(); + let origin = RuntimeOrigin::none(); + let reservation = BlockResourceMeterExtension + .prepare((), &origin, &call, &info, 0) + .expect("current ExternalPhase must admit one remark"); // deos-bypass: panic-owner — production budget fit is covered by the maximum signed-call integration test. + let reserved = pallet_deos_actors::CurrentBlockResourceState::::get() + .expect("prepare retains state"); // deos-bypass: panic-owner — successful prepare writes authoritative state. + assert_eq!(reserved.outstanding_reservations(), 1); + assert_eq!(reserved.usage().user_dispatch_used(), info.total_weight()); + + let actual = PostDispatchInfoOf:: { + actual_weight: Some(Weight::zero()), + pays_fee: polkadot_sdk::frame_support::dispatch::Pays::Yes, + }; + assert_eq!( + BlockResourceMeterExtension::post_dispatch_details(reservation, &info, &actual, 0, &Ok(())), + Ok(Weight::zero()) + ); + let settled = pallet_deos_actors::CurrentBlockResourceState::::get() + .expect("settlement retains state"); // deos-bypass: panic-owner — successful settlement writes authoritative state. + assert_eq!(settled.outstanding_reservations(), 0); + assert_eq!(settled.usage().user_dispatch_used(), Weight::zero()); + + let lost = BlockResourceMeterExtension + .prepare((), &origin, &call, &info, 0) + .expect("second reservation fits after reclaim"); // deos-bypass: panic-owner — preceding zero-actual settlement restores complete capacity. + pallet_deos_actors::CurrentBlockResourceState::::put(external_state(1)); + assert_eq!( + BlockResourceMeterExtension::post_dispatch_details(lost, &info, &actual, 0, &Ok(())), + Err(InvalidTransaction::Custom(RESOURCE_SETTLEMENT_REJECTED).into()) + ); + }); + } +} diff --git a/template/runtime/src/configs/staking_config.rs b/template/runtime/src/configs/staking_config.rs index 28f1f327..0468ea14 100644 --- a/template/runtime/src/configs/staking_config.rs +++ b/template/runtime/src/configs/staking_config.rs @@ -248,15 +248,13 @@ impl pallet_staking::BenchmarkHelper native_asset_id, mint_amount / 2, )?; - crate::configs::AssetConversionAdapter::ensure_lp_asset_namespace(); let base_asset = primitives::AssetKind::Local(native_asset_id); let staked_asset = primitives::AssetKind::Local(staked_asset_id); - let _ = crate::AssetConversion::create_pool( + crate::DeosRouter::create_pool( crate::RuntimeOrigin::signed(account.clone()), - Box::new(base_asset), - Box::new(staked_asset), - ); - crate::configs::assets_config::register_pool_lp_pair(base_asset, staked_asset)?; + base_asset, + staked_asset, + )?; crate::AssetConversion::add_liquidity( crate::RuntimeOrigin::signed(account.clone()), Box::new(base_asset), diff --git a/template/runtime/src/configs/xcm_config.rs b/template/runtime/src/configs/xcm_config.rs index 47c6cdf7..c67d608a 100644 --- a/template/runtime/src/configs/xcm_config.rs +++ b/template/runtime/src/configs/xcm_config.rs @@ -817,11 +817,9 @@ mod tests { let asset_id = LocationToAssetId::convert(relay_location); // Should return ID directly - let asset_kind = primitives::AssetKind::Local(asset_id); - assert!( - asset_kind.is_foreign(), - "Relay token should be in Foreign namespace (0xF...)" - ); + let asset_kind = primitives::AssetKind::Foreign(asset_id); + assert!(asset_kind.is_canonical()); + assert!(asset_kind.is_foreign()); } #[test] @@ -832,11 +830,9 @@ mod tests { let asset_id = LocationToAssetId::convert(sibling_location); - let asset_kind = primitives::AssetKind::Local(asset_id); - assert!( - asset_kind.is_foreign(), - "Sibling token should be in Foreign namespace (0xF...)" - ); + let asset_kind = primitives::AssetKind::Foreign(asset_id); + assert!(asset_kind.is_canonical()); + assert!(asset_kind.is_foreign()); } #[test] diff --git a/template/runtime/src/lib.rs b/template/runtime/src/lib.rs index 488358ea..299674f7 100644 --- a/template/runtime/src/lib.rs +++ b/template/runtime/src/lib.rs @@ -104,8 +104,10 @@ pub type TxExtension = cumulus_pallet_weight_reclaim::StorageWeightReclaim< frame_system::CheckEra, frame_system::CheckNonce, frame_system::CheckWeight, - configs::address_event_ingress::AddressEventIngressExtension, - configs::pool_index::PoolIndexExtension, + ( + configs::resource_meter::BlockResourceMeterExtension, + configs::address_event_ingress::AddressEventIngressExtension, + ), pallet_transaction_payment::ChargeTransactionPayment, frame_metadata_hash_extension::CheckMetadataHash, ), @@ -214,6 +216,8 @@ mod runtime { pub type CollatorSelection = pallet_collator_selection; #[runtime::pallet_index(22)] pub type Session = pallet_session; + #[runtime::pallet_index(25)] + pub type SessionRotation = pallet_session_rotation; #[runtime::pallet_index(23)] pub type Aura = pallet_aura; #[runtime::pallet_index(24)] @@ -269,23 +273,6 @@ pub type Executive = frame_executive::Executive< pub use genesis_config_presets::{PARACHAIN_ID, template_session_keys}; -#[doc(hidden)] -pub fn actors_trigger_state_bond(trigger: &pallet_deos_actors::TriggerOf) -> Balance { - , - Balance, - >>::amount(trigger) -} - -#[doc(hidden)] -pub fn actors_trigger_bond_vector_feed() -> primitives::OracleFeedId { - configs::oracle_config::deos_router_pool_feed( - primitives::AssetKind::Native, - primitives::AssetKind::Local(9), - ) -} - pub type ConsensusHook = cumulus_pallet_aura_ext::FixedVelocityConsensusHook< Runtime, RELAY_CHAIN_SLOT_DURATION_MILLIS, diff --git a/template/runtime/src/tests/actors_integration_tests.rs b/template/runtime/src/tests/actors_integration_tests.rs index b804ffc7..7c51cc31 100644 --- a/template/runtime/src/tests/actors_integration_tests.rs +++ b/template/runtime/src/tests/actors_integration_tests.rs @@ -12,56 +12,732 @@ macro_rules! update_actor_contract_partial { } use crate::{ - AccountId, Actors, Address, Assets, Balance, Balances, Executive, Oracle, Runtime, RuntimeCall, - RuntimeEvent, RuntimeOrigin, Signature, Staking, System, TxExtension, UncheckedExtrinsic, + AccountId, Actors, Address, Assets, Balance, Balances, Executive, MessageQueue, Oracle, Runtime, + RuntimeCall, RuntimeEvent, RuntimeHoldReason, RuntimeOrigin, Session, SessionRotation, Signature, + Staking, System, TxExtension, UncheckedExtrinsic, XcmpQueue, configs::{ - RuntimeAddressEventIngress, + AuraExtFixedWeight, AuraFixedWeight, AuthorshipFixedWeight, BlockResourceBudgetValue, + DmpFixedWeight, FixedBlockWeight, FixedBlockWeightComponentsValue, + MaxInboundDownwardMessagesPerContext, MaxInboundHorizontalChannelsPerContext, + MaxInboundHorizontalMessagesPerContext, MaximumContextMeasuredWeight, + MessageQueueServiceWeight, ReservedDmpWeight, ReservedXcmpWeight, RuntimeAddressEventIngress, + SessionRotationFixedWeight, XcmMigrationMaximumWeight, XcmVersionDiscoveryFixedWeight, + XcmpRegisteredFixedWeight, actor_config::{ - TmctolAssetOps, TmctolDexOps, TmctolFeeCollector, TmctolGenesisSystemActors, - TmctolLiquidityOps, classify_remove_liquidity_failure, classify_router_execution_failure, + ActorControlInitializationWeight, ActorOnIdleReserve, RuntimeAdmissionCertificateAuthority, + RuntimeStepControlWeight, TmctolAssetOps, TmctolDexOps, TmctolFeeCollector, + TmctolGenesisSystemActors, TmctolLiquidityOps, TmctolTaskEffectWeight, + classify_remove_liquidity_failure, classify_router_execution_failure, classify_router_failure, validate_remove_liquidity_output, }, address_event_ingress::AddressEventIngressExtension, deos_router_config::{AssetConversionAdapter, market_execution_failure}, - pool_index::PoolIndexExtension, + governance_config::GovernanceFixedWeight, + resource_meter::BlockResourceMeterExtension, }, }; -use alloc::boxed::Box; +use alloc::{ + boxed::Box, + collections::{BTreeMap, BTreeSet}, +}; use codec::Encode; use pallet_deos_actors::adapters::SovereignAccountPolicy; use pallet_deos_actors::{ - ActorContract, ActorId, ActorType, AmountResolution, AssetFilter, AssetFilterOf, AssetOps, - AttemptDisposition, CloseReason, CompletionPolicy, ContractSteps, CrossingDirection, CycleResult, - DexOps, Error, Event, ExecutionContext, FeeCollector, FundingSourcePolicy, IdleStarvationPhase, - IdleStarvationState, InputLimit, LiquidityOps, Mutability, OutcomeTotals, RetryClass, - ScheduleWindow, SimulationMode, SourceFilter, SourceFilterOf, SplitLeg, SplitTransferLegsOf, - StakingOps, StepErrorPolicy, StepOf, StepOutcome, StepSkippedReason, Task, TaskOf, Trigger, - WakeupKey, WeightInfo, + ActorContract, ActorId, ActorType, AdmissionCertificateAuthorityProvider, AmountResolution, + AssetFilter, AssetFilterOf, AssetOps, AttemptDisposition, CloseReason, CompletionPolicy, + ContextMessageGeometry, ContextMessageLimits, ContractSteps, CrossingDirection, CycleResult, + DexOps, Error, Event, ExecutionContext, FeeCollector, FundingSourcePolicy, IdleStarvationState, + InputLimit, LiquidityOps, Mutability, OutcomeTotals, RetryClass, ScheduleWindow, SimulationMode, + SourceFilter, SourceFilterOf, SplitLeg, SplitTransferLegsOf, StakingOps, StepControlExecution, + StepControlOutcome, StepControlPhase, StepControlPlacement, StepControlWeightContext, + StepControlWeightProvider, StepErrorPolicy, StepOf, StepOutcome, StepSkippedReason, Task, + TaskEffectExecution, TaskEffectWeightProvider, TaskOf, Trigger, TriggerFamily, WakeupKey, + WeightInfo, }; use pallet_deos_router::{AssetConversionApi, FeeRoutingAdapter}; use polkadot_sdk::frame_support::{ BoundedVec, assert_noop, assert_ok, dispatch::{DispatchClass, GetDispatchInfo}, traits::{ - Currency, ExistenceRequirement, Get, GetStorageVersion, Hooks, ReservableCurrency, - StorageVersion, + Currency, ExistenceRequirement, Get, GetStorageVersion, Hooks, PalletInfoAccess, + ReservableCurrency, StorageVersion, + fungible::InspectHold, fungibles::{Inspect as FungiblesInspect, Mutate as FungiblesMutate}, tokens::imbalance::{ImbalanceAccounting, UnsafeConstructorDestructor, UnsafeManualAccounting}, }, weights::Weight, }; -use polkadot_sdk::sp_core::{Pair, crypto::Ss58Codec, sr25519}; use polkadot_sdk::sp_runtime::traits::{AccountIdConversion, TransactionExtension}; use polkadot_sdk::sp_runtime::{ DispatchError, Perbill, generic, transaction_validity::TransactionSource, }; use polkadot_sdk::sp_weights::{WeightMeter, WeightToFee}; +use polkadot_sdk::{ + cumulus_primitives_core::{InboundDownwardMessage, InboundHrmpMessage, ParaId}, + sp_core::{Pair, crypto::Ss58Codec, sr25519}, +}; use polkadot_sdk::{ staging_xcm as xcm, staging_xcm_executor::{AssetsInHolding, traits::TransactAsset}, }; use primitives::AssetKind; +fn conservative_actor_resource_limits() -> (Weight, Weight) { + let schedulable = ActorOnIdleReserve::get(); + let control = Perbill::from_percent(20) * schedulable; + let shared = schedulable + .checked_sub(&control) + .expect("control share fits conservative schedulable reserve"); + let actor_base_turn = Perbill::from_percent(50) * shared; + (control, actor_base_turn) +} + +#[test] +fn task_effect_weight_provider_owns_all_task_families() { + assert!( + >>::production_weight_identity() + .is_some() + ); + assert!(RuntimeAdmissionCertificateAuthority::current().is_some()); + let (_, actor_base_turn) = conservative_actor_resource_limits(); + let swap = TaskOf::::SwapIn { + asset_in: AssetKind::Native, + amount_in: AmountResolution::Fixed(1), + asset_out: AssetKind::Local(ASSET_A), + slippage_tolerance: Perbill::zero(), + }; + let swap_weight = TmctolTaskEffectWeight::maximum_effect_weight(&swap) + .expect("SwapIn has a production Router envelope"); + assert_eq!( + TmctolTaskEffectWeight::actual_effect_weight(&swap, TaskEffectExecution::NotInvoked), + Some(Weight::zero()), + ); + assert_eq!( + TmctolTaskEffectWeight::actual_effect_weight(&swap, TaskEffectExecution::Invoked), + Some(swap_weight), + ); + assert!(swap_weight.ref_time() > 0); + assert!(swap_weight.proof_size() > 0); + assert!(swap_weight.all_lte(actor_base_turn)); + for task in [ + TaskOf::::Stake { + asset: AssetKind::Native, + amount: AmountResolution::Fixed(1), + }, + TaskOf::::Unstake { + asset: AssetKind::Native, + shares: AmountResolution::Fixed(1), + }, + ] { + let weight = TmctolTaskEffectWeight::maximum_effect_weight(&task) + .expect("staking Task has a production envelope"); + assert!(weight.ref_time() > 0); + assert!(weight.proof_size() > 0); + assert!(weight.all_lte(actor_base_turn)); + } + assert_eq!( + TmctolTaskEffectWeight::maximum_effect_weight(&TaskOf::::StopCycle), + Some(Weight::zero()) + ); + let transfer = TaskOf::::Transfer { + to: BOB, + asset: AssetKind::Native, + amount: AmountResolution::Fixed(1), + }; + let transfer_weight = TmctolTaskEffectWeight::maximum_effect_weight(&transfer) + .expect("Transfer has a measured composite effect envelope"); + assert!(transfer_weight.ref_time() > 0); + assert!(transfer_weight.proof_size() > 0); + assert!(transfer_weight.all_lte(actor_base_turn)); + + let legs = SplitTransferLegsOf::::try_from(alloc::vec![ + SplitLeg { + to: BOB, + share: Perbill::from_percent(50), + }, + SplitLeg { + to: CHARLIE, + share: Perbill::from_percent(50), + }, + ]) + .expect("two split legs fit"); + let remaining = alloc::vec![ + TaskOf::::SplitTransfer { + asset: AssetKind::Native, + amount: AmountResolution::Fixed(2), + legs, + }, + TaskOf::::SwapOut { + asset_out: AssetKind::Local(ASSET_A), + amount_out: AmountResolution::Fixed(1), + asset_in: AssetKind::Native, + input_limit: InputLimit::LiveQuote, + slippage_tolerance: Perbill::zero(), + }, + TaskOf::::AddLiquidity { + asset_a: AssetKind::Native, + asset_b: AssetKind::Local(ASSET_A), + amount_a: AmountResolution::Fixed(1), + amount_b: AmountResolution::Fixed(1), + min_lp_out: 1, + }, + TaskOf::::RemoveLiquidity { + lp_asset: AssetKind::Local(ASSET_A), + asset_a: AssetKind::Native, + asset_b: AssetKind::Local(ASSET_A), + lp_amount: AmountResolution::Fixed(1), + min_amount_a: 1, + min_amount_b: 1, + }, + TaskOf::::Burn { + asset: AssetKind::Native, + amount: AmountResolution::Fixed(1), + }, + TaskOf::::Mint { + asset: AssetKind::Native, + amount: AmountResolution::Fixed(1), + }, + TaskOf::::DonateLiquidity { + asset_a: AssetKind::Native, + asset_b: AssetKind::Local(ASSET_A), + max_amount_a: AmountResolution::Fixed(1), + max_ratio_error: Perbill::zero(), + }, + ]; + for task in remaining { + let weight = TmctolTaskEffectWeight::maximum_effect_weight(&task) + .expect("every runtime Task family has a production envelope"); + assert!(weight.ref_time() > 0); + assert!(weight.proof_size() > 0); + assert!(weight.all_lte(actor_base_turn)); + } +} + +#[test] +fn runtime_step_control_weight_identity_commits_every_staged_production_branch() { + let step = StepOf:: { + precondition: None, + task: TaskOf::::StopCycle, + on_error: StepErrorPolicy::AbortCycle, + }; + assert!(RuntimeStepControlWeight::production_weight_identity().is_some()); + let simple_context = StepControlWeightContext { + cursor: 0, + steps_in_fragment: 1, + opening_tail_chunks: 0, + predicate_evaluation_units: 0, + opening_snapshot_entries: 0, + opening_predicate_results: 0, + funding_snapshot_entries: 0, + }; + let simple = RuntimeStepControlWeight::maximum_control_weight(simple_context, &step) + .expect("the production head branch is bounded"); + let simple_actual = RuntimeStepControlWeight::actual_control_weight( + simple_context, + &step, + simple, + StepControlExecution { + phase: StepControlPhase::Opening, + outcome: StepControlOutcome::Completed, + placement: StepControlPlacement::None, + }, + ) + .expect("simple completed branch has actual control evidence"); + assert!(simple_actual.all_lte(simple)); + assert!(simple_actual.ref_time() < simple.ref_time()); + let suspended_head_retry = RuntimeStepControlWeight::actual_control_weight( + simple_context, + &step, + simple, + StepControlExecution { + phase: StepControlPhase::Suspended, + outcome: StepControlOutcome::Suspended, + placement: StepControlPlacement::Wakeup, + }, + ) + .expect("Suspended-head retry actual evidence exists"); + assert_eq!( + suspended_head_retry, + crate::weights::pallet_deos_actors::SubstrateWeight::::scheduler_inner_suspended_head_retry( + 0, 0, 0, 0, + ), + ); + assert!(suspended_head_retry.all_lte(simple)); + let opening_heavy_retry_context = StepControlWeightContext { + predicate_evaluation_units: 5, + opening_predicate_results: 1, + ..simple_context + }; + let opening_heavy_retry_maximum = + RuntimeStepControlWeight::maximum_control_weight(opening_heavy_retry_context, &step) + .expect("Opening-heavy Suspended retry maximum exists"); + let opening_heavy_retry = RuntimeStepControlWeight::actual_control_weight( + opening_heavy_retry_context, + &step, + opening_heavy_retry_maximum, + StepControlExecution { + phase: StepControlPhase::Suspended, + outcome: StepControlOutcome::Suspended, + placement: StepControlPlacement::Wakeup, + }, + ) + .expect("Opening-heavy Suspended retry actual evidence exists"); + assert_eq!( + opening_heavy_retry, + crate::weights::pallet_deos_actors::SubstrateWeight::::scheduler_inner_suspended_head_opening_retry( + 0, 1, 0, + ), + ); + assert!(opening_heavy_retry.all_lte(opening_heavy_retry_maximum)); + let opening_heavy_complete = RuntimeStepControlWeight::actual_control_weight( + opening_heavy_retry_context, + &step, + opening_heavy_retry_maximum, + StepControlExecution { + phase: StepControlPhase::Suspended, + outcome: StepControlOutcome::Completed, + placement: StepControlPlacement::None, + }, + ) + .expect("Opening-heavy Suspended completion actual evidence exists"); + assert_eq!( + opening_heavy_complete, + crate::weights::pallet_deos_actors::SubstrateWeight::::scheduler_inner_suspended_head_opening_complete( + 0, 1, 0, + ), + ); + assert!(opening_heavy_complete.all_lte(opening_heavy_retry_maximum)); + let opening_heavy_progress_context = StepControlWeightContext { + opening_tail_chunks: 1, + ..opening_heavy_retry_context + }; + let opening_heavy_progress_maximum = + RuntimeStepControlWeight::maximum_control_weight(opening_heavy_progress_context, &step) + .expect("Opening-heavy Suspended progress maximum exists"); + let opening_heavy_progress = RuntimeStepControlWeight::actual_control_weight( + opening_heavy_progress_context, + &step, + opening_heavy_progress_maximum, + StepControlExecution { + phase: StepControlPhase::Suspended, + outcome: StepControlOutcome::Continued, + placement: StepControlPlacement::Queue, + }, + ) + .expect("Opening-heavy Suspended progress actual evidence exists"); + assert_eq!( + opening_heavy_progress, + crate::weights::pallet_deos_actors::SubstrateWeight::::scheduler_inner_suspended_head_opening_progress( + 0, 1, 0, + ), + ); + assert!(opening_heavy_progress.all_lte(opening_heavy_progress_maximum)); + let suspended_head_complete = RuntimeStepControlWeight::actual_control_weight( + simple_context, + &step, + simple, + StepControlExecution { + phase: StepControlPhase::Suspended, + outcome: StepControlOutcome::Completed, + placement: StepControlPlacement::None, + }, + ) + .expect("Suspended-head completion actual evidence exists"); + assert_eq!( + suspended_head_complete, + crate::weights::pallet_deos_actors::SubstrateWeight::::scheduler_inner_suspended_head_complete( + 0, 0, 0, 0, + ), + ); + assert!(suspended_head_complete.all_lte(simple)); + let suspended_head_progress_context = StepControlWeightContext { + opening_tail_chunks: 1, + ..simple_context + }; + let suspended_head_progress_maximum = + RuntimeStepControlWeight::maximum_control_weight(suspended_head_progress_context, &step) + .expect("Suspended-head progress maximum exists"); + let suspended_head_progress = RuntimeStepControlWeight::actual_control_weight( + suspended_head_progress_context, + &step, + suspended_head_progress_maximum, + StepControlExecution { + phase: StepControlPhase::Suspended, + outcome: StepControlOutcome::Continued, + placement: StepControlPlacement::Queue, + }, + ) + .expect("Suspended-head progress actual evidence exists"); + assert_eq!( + suspended_head_progress, + crate::weights::pallet_deos_actors::SubstrateWeight::::scheduler_inner_suspended_head_progress( + 0, 0, 0, 0, + ), + ); + assert!(suspended_head_progress.all_lte(suspended_head_progress_maximum)); + let two_tail_chunks = RuntimeStepControlWeight::maximum_control_weight( + StepControlWeightContext { + cursor: 0, + steps_in_fragment: 1, + opening_tail_chunks: 2, + predicate_evaluation_units: 0, + opening_snapshot_entries: 0, + opening_predicate_results: 0, + funding_snapshot_entries: 0, + }, + &step, + ) + .expect("authored Opening tail reconstruction is bounded"); + assert!(two_tail_chunks.ref_time() > simple.ref_time()); + assert!(two_tail_chunks.proof_size() > simple.proof_size()); + let authored = RuntimeStepControlWeight::maximum_control_weight( + StepControlWeightContext { + cursor: 0, + steps_in_fragment: 1, + opening_tail_chunks: ::MaxContractSteps::get() + .saturating_sub(1) + .div_ceil(pallet_deos_actors::MAX_STEPS_PER_TAIL_CHUNK), + predicate_evaluation_units: + ::MaxPredicatesPerStep::get().saturating_mul(2), + opening_snapshot_entries: + ::MaxOpeningSnapshotEntries::get(), + opening_predicate_results: + ::MaxOpeningPredicateResults::get(), + funding_snapshot_entries: + ::MaxFundingTrackedAssets::get(), + }, + &step, + ) + .expect("maximum authored Opening geometry is bounded"); + let maximum_opening_tail_chunks = + ::MaxContractSteps::get() + .saturating_sub(1) + .div_ceil(pallet_deos_actors::MAX_STEPS_PER_TAIL_CHUNK); + let opening_progress = + crate::weights::pallet_deos_actors::SubstrateWeight::::scheduler_inner_opening_progress_max( + maximum_opening_tail_chunks, + ); + let conservative_opening = + crate::weights::pallet_deos_actors::SubstrateWeight::::scheduler_paged_execute_opening_max(); + assert_eq!( + authored, + Weight::from_parts( + conservative_opening + .ref_time() + .max(opening_progress.ref_time()), + conservative_opening + .proof_size() + .max(opening_progress.proof_size()), + ), + ); + let authored_progress_actual = RuntimeStepControlWeight::actual_control_weight( + StepControlWeightContext { + cursor: 0, + steps_in_fragment: 1, + opening_tail_chunks: maximum_opening_tail_chunks, + predicate_evaluation_units: + ::MaxPredicatesPerStep::get().saturating_mul(2), + opening_snapshot_entries: + ::MaxOpeningSnapshotEntries::get(), + opening_predicate_results: + ::MaxOpeningPredicateResults::get(), + funding_snapshot_entries: + ::MaxFundingTrackedAssets::get(), + }, + &step, + authored, + StepControlExecution { + phase: StepControlPhase::Opening, + outcome: StepControlOutcome::Continued, + placement: StepControlPlacement::Queue, + }, + ) + .expect("maximum Opening progress actual evidence exists"); + assert_eq!(authored_progress_actual, opening_progress); + assert!(authored_progress_actual.all_lte(authored)); + let minimal_opening_context = StepControlWeightContext { + cursor: 0, + steps_in_fragment: 1, + opening_tail_chunks: maximum_opening_tail_chunks, + predicate_evaluation_units: 0, + opening_snapshot_entries: 0, + opening_predicate_results: 0, + funding_snapshot_entries: ::MaxFundingTrackedAssets::get( + ), + }; + let minimal_opening_maximum = + RuntimeStepControlWeight::maximum_control_weight(minimal_opening_context, &step) + .expect("minimal Opening progress maximum exists"); + let minimal_opening_actual = RuntimeStepControlWeight::actual_control_weight( + minimal_opening_context, + &step, + minimal_opening_maximum, + StepControlExecution { + phase: StepControlPhase::Opening, + outcome: StepControlOutcome::Continued, + placement: StepControlPlacement::Queue, + }, + ) + .expect("minimal Opening progress actual evidence exists"); + assert_eq!( + minimal_opening_actual, + crate::weights::pallet_deos_actors::SubstrateWeight::::scheduler_inner_opening_progress_min( + maximum_opening_tail_chunks, + ), + ); + assert!(minimal_opening_actual.all_lte(minimal_opening_maximum)); + let minimal_failed_actual = RuntimeStepControlWeight::actual_control_weight( + minimal_opening_context, + &step, + minimal_opening_maximum, + StepControlExecution { + phase: StepControlPhase::Opening, + outcome: StepControlOutcome::Failed, + placement: StepControlPlacement::None, + }, + ) + .expect("minimal Opening failure actual evidence exists"); + assert_eq!( + minimal_failed_actual, + crate::weights::pallet_deos_actors::SubstrateWeight::::scheduler_inner_opening_failed_min( + maximum_opening_tail_chunks, + ), + ); + assert!(minimal_failed_actual.all_lte(minimal_opening_maximum)); + let minimal_retry_actual = RuntimeStepControlWeight::actual_control_weight( + minimal_opening_context, + &step, + minimal_opening_maximum, + StepControlExecution { + phase: StepControlPhase::Opening, + outcome: StepControlOutcome::Suspended, + placement: StepControlPlacement::Wakeup, + }, + ) + .expect("minimal Opening retry actual evidence exists"); + assert_eq!( + minimal_retry_actual, + crate::weights::pallet_deos_actors::SubstrateWeight::::scheduler_inner_opening_retry_min( + maximum_opening_tail_chunks, + ), + ); + assert!(minimal_retry_actual.all_lte(minimal_opening_maximum)); + let minimal_completion_actual = RuntimeStepControlWeight::actual_control_weight( + minimal_opening_context, + &step, + minimal_opening_maximum, + StepControlExecution { + phase: StepControlPhase::Opening, + outcome: StepControlOutcome::Completed, + placement: StepControlPlacement::None, + }, + ) + .expect("minimal Opening completion actual evidence exists"); + assert_eq!( + minimal_completion_actual, + crate::weights::pallet_deos_actors::SubstrateWeight::::scheduler_inner_opening_complete_min( + maximum_opening_tail_chunks, + ), + ); + assert!(minimal_completion_actual.all_lte(minimal_opening_maximum)); + let maximal_completion_context = StepControlWeightContext { + cursor: 0, + steps_in_fragment: 1, + opening_tail_chunks: maximum_opening_tail_chunks, + predicate_evaluation_units: ::MaxPredicatesPerStep::get( + ) + .saturating_mul(2), + opening_snapshot_entries: ::MaxContractSteps::get() + .saturating_sub(1) + .saturating_mul(2), + opening_predicate_results: + ::MaxOpeningPredicateResults::get(), + funding_snapshot_entries: ::MaxFundingTrackedAssets::get( + ), + }; + let maximal_completion_maximum = + RuntimeStepControlWeight::maximum_control_weight(maximal_completion_context, &step) + .expect("maximum realizable Opening completion is bounded"); + let maximal_failed_actual = RuntimeStepControlWeight::actual_control_weight( + maximal_completion_context, + &step, + maximal_completion_maximum, + StepControlExecution { + phase: StepControlPhase::Opening, + outcome: StepControlOutcome::Failed, + placement: StepControlPlacement::None, + }, + ) + .expect("maximum Opening failure actual evidence exists"); + assert_eq!( + maximal_failed_actual, + crate::weights::pallet_deos_actors::SubstrateWeight::::scheduler_inner_opening_failed_max( + maximum_opening_tail_chunks, + ), + ); + assert!(maximal_failed_actual.all_lte(maximal_completion_maximum)); + let maximal_retry_actual = RuntimeStepControlWeight::actual_control_weight( + maximal_completion_context, + &step, + maximal_completion_maximum, + StepControlExecution { + phase: StepControlPhase::Opening, + outcome: StepControlOutcome::Suspended, + placement: StepControlPlacement::Wakeup, + }, + ) + .expect("maximum Opening retry actual evidence exists"); + assert_eq!( + maximal_retry_actual, + crate::weights::pallet_deos_actors::SubstrateWeight::::scheduler_inner_opening_retry_max( + maximum_opening_tail_chunks, + ), + ); + assert!(maximal_retry_actual.all_lte(maximal_completion_maximum)); + let maximal_completion_actual = RuntimeStepControlWeight::actual_control_weight( + maximal_completion_context, + &step, + maximal_completion_maximum, + StepControlExecution { + phase: StepControlPhase::Opening, + outcome: StepControlOutcome::Completed, + placement: StepControlPlacement::None, + }, + ) + .expect("maximum Opening completion actual evidence exists"); + assert_eq!( + maximal_completion_actual, + crate::weights::pallet_deos_actors::SubstrateWeight::::scheduler_inner_opening_complete_max( + maximum_opening_tail_chunks, + ), + ); + assert!(maximal_completion_actual.all_lte(maximal_completion_maximum)); + assert!(authored.ref_time() > simple.ref_time()); + assert!(authored.proof_size() > simple.proof_size()); + let (control_limit, _) = conservative_actor_resource_limits(); + assert!( + authored.all_lte(control_limit), + "maximum control {authored:?} exceeds conservative limit {control_limit:?}" + ); + assert_eq!( + RuntimeStepControlWeight::maximum_control_weight( + StepControlWeightContext { + cursor: 1, + steps_in_fragment: 5, + opening_tail_chunks: 0, + predicate_evaluation_units: 0, + opening_snapshot_entries: 0, + opening_predicate_results: 0, + funding_snapshot_entries: 0, + }, + &step, + ), + None + ); + let tail_context = StepControlWeightContext { + cursor: 1, + steps_in_fragment: 4, + opening_tail_chunks: 0, + predicate_evaluation_units: 0, + opening_snapshot_entries: 0, + opening_predicate_results: 0, + funding_snapshot_entries: 0, + }; + let tail_maximum = RuntimeStepControlWeight::maximum_control_weight(tail_context, &step) + .expect("tail control maximum exists"); + let running_complete = RuntimeStepControlWeight::actual_control_weight( + tail_context, + &step, + tail_maximum, + StepControlExecution { + phase: StepControlPhase::Running, + outcome: StepControlOutcome::Completed, + placement: StepControlPlacement::None, + }, + ) + .expect("Running completion actual evidence exists"); + assert_eq!( + running_complete, + crate::weights::pallet_deos_actors::SubstrateWeight::::scheduler_inner_running_complete( + 4, 0, + ), + ); + assert!(running_complete.all_lte(tail_maximum)); + let running_progress = RuntimeStepControlWeight::actual_control_weight( + tail_context, + &step, + tail_maximum, + StepControlExecution { + phase: StepControlPhase::Running, + outcome: StepControlOutcome::Continued, + placement: StepControlPlacement::Queue, + }, + ) + .expect("Running progress actual evidence exists"); + assert_eq!( + running_progress, + crate::weights::pallet_deos_actors::SubstrateWeight::::scheduler_inner_running_progress( + 4, 0, + ), + ); + assert!(running_progress.all_lte(tail_maximum)); + let suspended_complete = RuntimeStepControlWeight::actual_control_weight( + tail_context, + &step, + tail_maximum, + StepControlExecution { + phase: StepControlPhase::Suspended, + outcome: StepControlOutcome::Completed, + placement: StepControlPlacement::None, + }, + ) + .expect("Suspended completion actual evidence exists"); + assert_eq!( + suspended_complete, + crate::weights::pallet_deos_actors::SubstrateWeight::::scheduler_inner_suspended_tail_complete( + 4, 0, + ), + ); + assert!(suspended_complete.all_lte(tail_maximum)); + let suspended_progress = RuntimeStepControlWeight::actual_control_weight( + tail_context, + &step, + tail_maximum, + StepControlExecution { + phase: StepControlPhase::Suspended, + outcome: StepControlOutcome::Continued, + placement: StepControlPlacement::Queue, + }, + ) + .expect("Suspended progress actual evidence exists"); + assert_eq!( + suspended_progress, + crate::weights::pallet_deos_actors::SubstrateWeight::::scheduler_inner_suspended_tail_progress( + 4, 0, + ), + ); + assert!(suspended_progress.all_lte(tail_maximum)); + let suspended = RuntimeStepControlWeight::actual_control_weight( + tail_context, + &step, + tail_maximum, + StepControlExecution { + phase: StepControlPhase::Suspended, + outcome: StepControlOutcome::Suspended, + placement: StepControlPlacement::Wakeup, + }, + ) + .expect("Suspended branch actual evidence exists"); + assert_eq!( + suspended, + crate::weights::pallet_deos_actors::SubstrateWeight::::scheduler_inner_suspended_tail_retry( + 4, 0, + ), + ); + assert!(suspended.all_lte(tail_maximum)); + assert!(RuntimeAdmissionCertificateAuthority::current().is_some()); +} + fn system_transfer_steps(target_actor_id: ActorId) -> pallet_deos_actors::ContractSteps { alloc::vec![StepOf:: { precondition: None, @@ -119,8 +795,7 @@ fn host_system_activation_manifest_is_ranked_and_rejects_undeclared_cycle_edges_ System::set_block_number(1); let splitter_before = - pallet_deos_actors::ActorContracts::::get(actor_ids::BLDR_SPLITTER_ACTORS_ID) - .expect("Splitter contract"); + Actors::actor_contract(actor_ids::BLDR_SPLITTER_ACTORS_ID).expect("Splitter contract"); assert_noop!( update_actor_contract_partial( RuntimeOrigin::root(), @@ -133,13 +808,257 @@ fn host_system_activation_manifest_is_ranked_and_rejects_undeclared_cycle_edges_ Error::::SystemActorTopologyInvalid ); assert_eq!( - pallet_deos_actors::ActorContracts::::get(actor_ids::BLDR_SPLITTER_ACTORS_ID), + Actors::actor_contract(actor_ids::BLDR_SPLITTER_ACTORS_ID), Some(splitter_before), "cycle rejection must precede contract mutation" ); }); } +#[test] +#[ignore = "fixed T+1 runtime length-32 stress profile; run by exact test name"] +fn next_block_runtime_mixed_length_herd_includes_maximum_tail() { + seeded_test_ext().execute_with(|| { + let feed = crate::configs::oracle_config::deos_router_pool_feed( + AssetKind::Native, + AssetKind::Local(8_043), + ); + let lengths = [0_u32, 1, 4, 8, 32]; + let actors = (0..10) + .map(|index| { + let steps = BoundedVec::try_from( + (0..lengths[index % lengths.len()]) + .map(|_| { + make_step(Task::Mint { + asset: AssetKind::Native, + amount: AmountResolution::Fixed(1), + }) + }) + .collect::>(), + ) + .expect("runtime W3 contract fits MaxContractSteps"); + create_system(ALICE, observation_schedule(feed), None, steps) + }) + .collect::>(); + + System::set_block_number(1); + Actors::on_initialize(1); + assert_ok!(Actors::note_observation_transition_with_provenance( + feed, + pallet_deos_actors::ObservationTransition { + revision: 1, + previous: None, + current: 10, + }, + pallet_deos_actors::TriggerCauseProvenance::ExternalPhase, + )); + run_idle(Weight::MAX); + + let started_actors = || { + System::events() + .into_iter() + .filter_map(|record| match record.event { + RuntimeEvent::Actors(Event::CycleStarted { actor_id, .. }) => Some(actor_id), + _ => None, + }) + .collect::>() + }; + let source_started = started_actors(); + assert!(source_started.is_empty()); + + let mut started = source_started; + let mut block = 1_u32; + while started.len() < actors.len() { + block += 1; + assert!(block <= 12, "runtime W3 herd failed to drain"); + System::set_block_number(block); + Actors::on_initialize(block); + run_idle(Weight::MAX); + started = started_actors(); + assert_eq!(started, actors[..started.len()]); + } + assert_eq!(started, actors); + }); +} + +fn run_next_block_runtime_herd(requested_population: Option) { + seeded_test_ext().execute_with(|| { + let feed = crate::configs::oracle_config::deos_router_pool_feed( + AssetKind::Native, + AssetKind::Local(8_044), + ); + let active_remaining = ::MaxActiveActors::get() + .saturating_sub(Actors::active_actor_count()); + let identity_remaining = ::MaxActorIdentities::get() + .saturating_sub(Actors::actor_identity_count()); + let sovereign_remaining = ::MaxSystemSovereigns::get() + .saturating_sub(Actors::system_sovereign_count()); + let maximum_population = active_remaining + .min(identity_remaining) + .min(sovereign_remaining) as usize; + if requested_population.is_none() { + assert_eq!(maximum_population, 9_985); + } + let population = requested_population.unwrap_or(maximum_population); + assert!(population <= maximum_population); + let lengths = [0_u32, 1, 4, 8, 32]; + let actors = (0..population) + .map(|index| { + let steps = BoundedVec::try_from( + (0..lengths[index % lengths.len()]) + .map(|_| { + make_step(Task::Mint { + asset: AssetKind::Native, + amount: AmountResolution::Fixed(1), + }) + }) + .collect::>(), + ) + .expect("runtime W3 contract fits MaxContractSteps"); + create_system(ALICE, observation_schedule(feed), None, steps) + }) + .collect::>(); + let actor_set = actors + .iter() + .copied() + .collect::>(); + + System::set_block_number(1); + Actors::on_initialize(1); + assert_ok!(Actors::note_observation_transition_with_provenance( + feed, + pallet_deos_actors::ObservationTransition { + revision: 1, + previous: None, + current: 10, + }, + pallet_deos_actors::TriggerCauseProvenance::ExternalPhase, + )); + + let mut started = Vec::new(); + let mut block = 1_u32; + while started.len() < actors.len() { + System::reset_events(); + run_idle(Weight::MAX); + started.extend( + System::events() + .into_iter() + .filter_map(|record| match record.event { + RuntimeEvent::Actors(Event::CycleStarted { actor_id, .. }) + if actor_set.contains(&actor_id) => + { + Some(actor_id) + } + _ => None, + }), + ); + assert_eq!(started, actors[..started.len()]); + if started.len() == actors.len() { + break; + } + block += 1; + assert!( + block <= population as u32 + 2, + "runtime W3 herd failed to drain" + ); + System::set_block_number(block); + Actors::on_initialize(block); + } + assert_eq!(started, actors); + }); +} + +#[test] +#[ignore = "fixed T+1 1,000-Actor runtime stress profile; run by exact test name"] +fn next_block_thousand_actor_runtime_herd_preserves_fifo_order() { + run_next_block_runtime_herd(Some(1_000)); +} + +#[test] +#[ignore = "fixed T+1 maximum-admissible runtime stress profile; run by exact test name"] +fn next_block_maximum_admissible_runtime_herd_preserves_fifo_order() { + run_next_block_runtime_herd(None); +} + +#[test] +fn actor_produced_address_event_waits_one_additional_block() { + seeded_test_ext().execute_with(|| { + let first_account = Actors::sovereign_account_id(&ALICE, 0); + let second_account = Actors::sovereign_account_id(&ALICE, 1); + let first = create_user( + ALICE, + on_address_event_schedule(SourceFilter::Any, AssetFilter::Any), + None, + transfer_contract_steps(second_account.clone(), AssetKind::Native, 10), + ); + let second = create_user( + ALICE, + on_address_event_schedule(SourceFilter::Any, AssetFilter::Any), + None, + transfer_contract_steps(first_account.clone(), AssetKind::Native, 10), + ); + fund_native(first, 1_000_000_000_000); + fund_native(second, 1_000_000_000_000); + + System::set_block_number(1); + Actors::on_initialize(1); + ensure_actor_prepass_context(); + assert_ok!(Actors::actor_prepass(RuntimeOrigin::none())); + assert_ok!(TmctolAssetOps::transfer( + &ALICE, + &first_account, + AssetKind::Native, + 100, + )); + run_idle(Weight::MAX); + assert_eq!( + Actors::active_actor_state(first) + .expect("first Actor") + .identity + .cycle_nonce, + 0 + ); + assert_eq!( + Actors::active_actor_state(second) + .expect("second Actor") + .identity + .cycle_nonce, + 0 + ); + assert!(Actors::pending_signal(first)); + + System::set_block_number(2); + Actors::on_initialize(2); + run_idle(Weight::MAX); + assert_eq!( + Actors::active_actor_state(first) + .expect("first Actor") + .identity + .cycle_nonce, + 1 + ); + assert_eq!( + Actors::active_actor_state(second) + .expect("second Actor") + .identity + .cycle_nonce, + 0 + ); + assert!(Actors::pending_signal(second)); + + System::set_block_number(3); + Actors::on_initialize(3); + run_idle(Weight::MAX); + assert_eq!( + Actors::active_actor_state(second) + .expect("second Actor") + .identity + .cycle_nonce, + 1 + ); + }); +} + #[test] fn paid_user_two_actor_cycle_is_fifo_bounded_and_cannot_recurse_in_one_block() { seeded_test_ext().execute_with(|| { @@ -279,6 +1198,7 @@ fn externally_closed_user_self_cycle_remains_paid_and_economically_apoptotic() { let balance = Balances::free_balance(&account); let protected_floor = crate::configs::actor_config::ActorMinUserBalance::get(); deplete_user_sovereign(actor_id, balance.saturating_sub(protected_floor)); + fund_native(actor_id, address_event_trigger_fee()); System::set_block_number(4); assert_ok!(Actors::notify_address_event( actor_id, @@ -291,13 +1211,13 @@ fn externally_closed_user_self_cycle_remains_paid_and_economically_apoptotic() { assert_eq!( >::reserved_balance(&ALICE), reserved_before, - "economic apoptosis releases the refundable Trigger-state bond" + "process cleanup never mutates owner reserve custody" ); assert!(System::events().iter().any(|record| matches!( &record.event, RuntimeEvent::Actors(Event::ActorClosed { actor_id: closed, - reason: CloseReason::BalanceExhausted | CloseReason::FeeBudgetExhausted, + reason: CloseReason::CycleAdmissionInsufficient, }) if *closed == actor_id ))); }); @@ -367,6 +1287,14 @@ fn paid_user_long_cycle_preserves_fifo_under_queue_pressure_and_closes_insolvent let balance = Balances::free_balance(actor_account(insolvent)); let protected_floor = crate::configs::actor_config::ActorMinUserBalance::get(); deplete_user_sovereign(insolvent, balance.saturating_sub(protected_floor)); + fund_native(insolvent, address_event_trigger_fee()); + System::set_block_number(10); + assert_ok!(Actors::notify_address_event( + insolvent, + AssetKind::Native, + 1, + &ALICE, + )); for block in 10..=13 { System::set_block_number(block); run_idle(Weight::MAX); @@ -376,7 +1304,7 @@ fn paid_user_long_cycle_preserves_fifo_under_queue_pressure_and_closes_insolvent &record.event, RuntimeEvent::Actors(Event::ActorClosed { actor_id, - reason: CloseReason::BalanceExhausted | CloseReason::FeeBudgetExhausted, + reason: CloseReason::CycleAdmissionInsufficient, }) if *actor_id == insolvent ))); assert!( @@ -552,7 +1480,7 @@ fn oracle_publication_rolls_back_when_actor_change_hook_rejects() { } #[test] -fn insolvent_crossing_fire_releases_bond_and_preserves_peer_cursor_continuity() { +fn underfunded_crossing_fire_advances_without_readiness_or_peer_cursor_loss() { seeded_test_ext().execute_with(|| { System::set_block_number(1); let producer = deos_router_account(); @@ -576,8 +1504,7 @@ fn insolvent_crossing_fire_releases_bond_and_preserves_peer_cursor_continuity() feed, 1_000_000_000_000, )); - let reserved_before = - >::reserved_balance(&ALICE); + let hold_before = actors_owner_hold(&ALICE); let crossing = RuntimeSchedule { trigger: Trigger::observation_crossing( feed, @@ -599,14 +1526,14 @@ fn insolvent_crossing_fire_releases_bond_and_preserves_peer_cursor_continuity() None, transfer_contract_steps(BOB, AssetKind::Native, 1), ); - let crossing_bond = crate::configs::actor_config::ActorObservationCrossingTriggerBond::get(); - assert_eq!( - >::reserved_balance(&ALICE), - reserved_before + crossing_bond * 2 - ); + let state_hold_after_creation = hold_before + .saturating_add(actor_state_hold_total(insolvent)) + .saturating_add(actor_state_hold_total(peer)); + assert_eq!(actors_owner_hold(&ALICE), state_hold_after_creation); let balance = Balances::free_balance(actor_account(insolvent)); let protected_floor = crate::configs::actor_config::ActorMinUserBalance::get(); deplete_user_sovereign(insolvent, balance.saturating_sub(protected_floor)); + fund_native(peer, observation_crossing_trigger_fee()); assert_ok!(Oracle::publish( RuntimeOrigin::signed(producer), @@ -618,21 +1545,25 @@ fn insolvent_crossing_fire_releases_bond_and_preserves_peer_cursor_continuity() run_idle(Weight::MAX); } - assert!(Actors::active_actor_state(insolvent).is_none()); + let underfunded = Actors::active_actor_state(insolvent).expect("underfunded process remains"); + assert!(!underfunded.hot.pending_signal); + assert!(underfunded.hot.queue_ticket.is_none()); assert!(Actors::active_actor_state(peer).is_some()); - assert_eq!( - >::reserved_balance(&ALICE), - reserved_before + crossing_bond - ); - assert_eq!(Actors::crossing_feed_membership_count(feed), 1); + assert_eq!(actors_owner_hold(&ALICE), state_hold_after_creation); + assert_eq!(Actors::crossing_feed_membership_count(feed), 2); assert!(Actors::crossing_membership(peer).is_some()); - assert!(Actors::crossing_membership(insolvent).is_none()); + assert!(Actors::crossing_membership(insolvent).is_some()); assert!(Actors::crossing_worker_fault().is_none()); - assert!(System::events().iter().any(|record| matches!( + assert!(!System::events().iter().any(|record| matches!( &record.event, - RuntimeEvent::Actors(Event::ActorClosed { + RuntimeEvent::Actors(Event::ActorClosed { actor_id, .. }) if *actor_id == insolvent + ))); + assert!(!System::events().iter().any(|record| matches!( + &record.event, + RuntimeEvent::Actors(Event::TriggerOccurrenceProcessed { actor_id, - reason: CloseReason::BalanceExhausted | CloseReason::FeeBudgetExhausted, + trigger_family: TriggerFamily::ObservationCrossing, + .. }) if *actor_id == insolvent ))); }); @@ -690,6 +1621,8 @@ fn oracle_publication_rolls_back_when_crossing_transition_queue_is_full() { revision: observation.revision, previous: observation.value.saturating_sub(1), current: observation.value, + cause_provenance: pallet_deos_actors::TriggerCauseProvenance::Deferred, + cause_block: 0, }; maximum ]) @@ -729,17 +1662,41 @@ fn native_flow_anchor_topology_is_unique_and_funded_with_one_ed() { #[test] fn actor_0_7_storage_schema_is_a_fresh_genesis_baseline() { seeded_test_ext().execute_with(|| { - let baseline = StorageVersion::new(9); + let baseline = StorageVersion::new(15); assert_eq!(Actors::in_code_storage_version(), baseline); assert_eq!(Actors::on_chain_storage_version(), baseline); }); } +fn ensure_actor_prepass_context() { + if crate::Timestamp::get() == 0 { + set_consensus_timestamp(1); + } + if !polkadot_sdk::cumulus_pallet_parachain_system::ValidationData::::exists() { + polkadot_sdk::cumulus_pallet_parachain_system::ValidationData::::put( + polkadot_sdk::cumulus_primitives_core::PersistedValidationData::default(), + ); + } +} + +fn ensure_current_resource_state() { + ensure_actor_prepass_context(); + let now = System::block_number(); + if Actors::block_resource_state().is_some_and(|state| state.ensure_block(now).is_err()) { + pallet_deos_actors::CurrentBlockResourceState::::kill(); + } + if Actors::block_resource_state().is_none() { + let _ = Actors::on_initialize(now); + assert_ok!(Actors::actor_prepass(RuntimeOrigin::none())); + } +} + fn signed_extrinsic( signer: &sr25519::Pair, nonce: crate::Nonce, call: RuntimeCall, ) -> UncheckedExtrinsic { + ensure_current_resource_state(); let tx_ext = TxExtension::new(( polkadot_sdk::frame_system::AuthorizeCall::::new(), polkadot_sdk::frame_system::CheckNonZeroSender::::new(), @@ -749,8 +1706,7 @@ fn signed_extrinsic( polkadot_sdk::frame_system::CheckEra::::from(generic::Era::Immortal), polkadot_sdk::frame_system::CheckNonce::::from(nonce), polkadot_sdk::frame_system::CheckWeight::::new(), - AddressEventIngressExtension, - PoolIndexExtension, + (BlockResourceMeterExtension, AddressEventIngressExtension), polkadot_sdk::pallet_transaction_payment::ChargeTransactionPayment::::from(0), polkadot_sdk::frame_metadata_hash_extension::CheckMetadataHash::::new(false), )); @@ -814,6 +1770,17 @@ fn inert_task() -> RuntimeTask { Task::StopCycle } +fn exp_0002_false_steps(step_count: u32) -> RuntimeContractSteps { + let step = RuntimeStep { + precondition: all_preconditions(vec![pallet_deos_actors::Predicate::BlockNumberBelow { + threshold: 0, + }]), + task: inert_task(), + on_error: StepErrorPolicy::AbortCycle, + }; + BoundedVec::try_from(vec![step; step_count as usize]).expect("profile Contract Steps fit") +} + fn manual_schedule() -> RuntimeSchedule { RuntimeSchedule { trigger: Trigger::manual(), @@ -946,13 +1913,22 @@ fn fund_native(actor_id: ActorId, amount: u128) { let _ = >::deposit_creating(&actor_acc, amount); } -/// User Active prefunding requirement: `MinUserBalance + attempt_fee_envelope(plan, 0, User).total`. +fn address_event_trigger_fee() -> Balance { + let weight = + <::WeightInfo as WeightInfo>::address_event_trigger_occurrence(); + ::WeightToFee::weight_to_fee(&weight) +} + +fn observation_crossing_trigger_fee() -> Balance { + let weight = + <::WeightInfo as WeightInfo>::observation_crossing_trigger_occurrence(); + ::WeightToFee::weight_to_fee(&weight) +} + +/// User Active prefunding requirement: floor plus the maximum opening-Step fee. fn user_prefunding_requirement(plan: &RuntimeContractSteps) -> u128 { - ::MinUserBalance::get().saturating_add( - Actors::attempt_fee_envelope(ActorType::User, plan, 0) - .expect("fixture plan has a checked fee envelope") - .total, - ) + Actors::user_pipeline_machine_capacity_requirement(plan) + .expect("fixture plan has a checked opening-Step fee") } /// Lowest free owner slot for the deterministic prospective User sovereign; mirrors the @@ -1006,6 +1982,30 @@ fn deplete_user_sovereign(actor_id: ActorId, amount: u128) { ); } +#[test] +fn zero_step_pipeline_quote_uses_generated_machine_and_cleanup_owners() { + seeded_test_ext().execute_with(|| { + type ActorWeights = crate::weights::pallet_deos_actors::SubstrateWeight; + let steps = RuntimeContractSteps::default(); + let machine = ::WeightToFee::weight_to_fee( + &ActorWeights::scheduler_inner_zero_step_complete(), + ); + let cleanup = ::WeightToFee::weight_to_fee( + &ActorWeights::close_actor(), + ); + let expected = ::MinUserBalance::get() + .saturating_add(machine) + .saturating_add(cleanup); + assert_eq!( + Actors::user_pipeline_machine_capacity_requirement(&steps) + .expect("zero-Step Pipeline quote fits"), + expected + ); + assert!(machine > 0); + assert!(cleanup > 0); + }); +} + #[test] fn deos_runtime_executes_unconditional_and_dnf_with_fixed_successors() { seeded_test_ext().execute_with(|| { @@ -1120,6 +2120,25 @@ fn native_balance(who: &crate::AccountId) -> u128 { Balances::free_balance(who) } +fn actor_state_hold_total(actor_id: ActorId) -> Balance { + let record = Actors::actor_state_hold(actor_id).expect("User Actor state hold exists"); + record + .breakdown + .identity + .saturating_add(record.breakdown.contract_head) + .saturating_add(record.breakdown.contract_body) + .saturating_add(record.breakdown.detector) + .saturating_add(record.breakdown.funding) + .saturating_add(record.breakdown.run) +} + +fn actors_owner_hold(owner: &AccountId) -> Balance { + Balances::balance_on_hold( + &RuntimeHoldReason::Actors(pallet_deos_actors::HoldReason::ActorState), + owner, + ) +} + fn account_location(who: crate::AccountId) -> xcm::latest::Location { let mut id = [0u8; 32]; id.copy_from_slice(who.as_ref()); @@ -1183,13 +2202,42 @@ fn asset_to_holding(asset: xcm::latest::Asset) -> AssetsInHolding { holding } +fn run_next_idle(weight: Weight) { + let now = System::block_number() + .checked_add(1) + .expect("test block advances"); + System::set_block_number(now); + set_consensus_timestamp( + u64::from(now).saturating_mul(primitives::ecosystem::params::ACTOR_CADENCE_TICK_MILLIS), + ); + Actors::on_initialize(now); + run_idle(weight); +} + fn run_idle(weight: Weight) { - let block_time = u64::from(System::block_number()) - .saturating_mul(primitives::ecosystem::params::ACTOR_CADENCE_TICK_MILLIS); + let mut now = System::block_number(); + let block_time = + u64::from(now).saturating_mul(primitives::ecosystem::params::ACTOR_CADENCE_TICK_MILLIS); if crate::Timestamp::get() < block_time { set_consensus_timestamp(block_time); } - Actors::on_idle(System::block_number(), weight); + ensure_current_resource_state(); + Actors::on_idle(now, weight); + for _ in 1..::MaxContractSteps::get().saturating_mul(2) { + if !pallet_deos_actors::ActorHot::::iter_values() + .any(|hot| hot.cycle_state == pallet_deos_actors::CycleState::Running) + { + break; + } + now = now.checked_add(1).expect("test block advances"); + System::set_block_number(now); + set_consensus_timestamp( + u64::from(now).saturating_mul(primitives::ecosystem::params::ACTOR_CADENCE_TICK_MILLIS), + ); + Actors::on_initialize(now); + ensure_current_resource_state(); + Actors::on_idle(now, weight); + } } fn starvation_observation_weight() -> Weight { @@ -1286,7 +2334,149 @@ fn manual_trigger_executes_transfer_contract_steps() { }, } if *id == actor_id ) - })); + })); + }); +} + +#[test] +fn exp_0002_ceiling_semantics_profile_commits_exactly_one_step_per_block() { + seeded_test_ext().execute_with(|| { + System::set_block_number(1); + let step_count = ::MaxContractSteps::get(); + let actor_id = create_system( + ALICE, + manual_schedule(), + None, + exp_0002_false_steps(step_count), + ); + let expected_tail_chunks = step_count + .saturating_sub(1) + .div_ceil(pallet_deos_actors::MAX_STEPS_PER_TAIL_CHUNK) + as usize; + assert_eq!( + pallet_deos_actors::ActorContractTailChunks::::iter_prefix(actor_id).count(), + expected_tail_chunks + ); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + assert!(Actors::actor_run_state(actor_id).is_none()); + assert_eq!( + Actors::active_actor_state(actor_id) + .expect("profile actor is active") + .identity + .cycle_nonce, + 0 + ); + + let mut now = System::block_number(); + for step_index in 0..step_count { + now = now.checked_add(1).expect("profile block advances"); + System::set_block_number(now); + set_consensus_timestamp( + u64::from(now).saturating_mul(primitives::ecosystem::params::ACTOR_CADENCE_TICK_MILLIS), + ); + assert_eq!(Actors::on_initialize(now), Weight::zero()); + ensure_actor_prepass_context(); + assert_ok!(Actors::actor_prepass(RuntimeOrigin::none())); + Actors::on_idle(now, Weight::MAX); + assert!(has_actor_event(|event| matches!( + event, + Event::StepSkipped { + actor_id: id, + step_index: index, + reason: StepSkippedReason::PreconditionFalse, + .. + } if *id == actor_id && *index == step_index + ))); + let state = Actors::active_actor_state(actor_id).expect("profile actor remains active"); + if step_index + 1 < step_count { + let run = Actors::actor_run_state(actor_id).expect("profile run remains open"); + assert_eq!(run.cursor, step_index + 1); + assert_eq!(run.last_committed_step_block, Some(now)); + assert_eq!(run.eligible_at, now + 1); + assert_eq!(state.identity.cycle_nonce, 0); + } else { + assert!(Actors::actor_run_state(actor_id).is_none()); + assert_eq!(state.identity.cycle_nonce, 1); + } + } + assert!(has_actor_event(|event| matches!( + event, + Event::CycleSummary { + actor_id: id, + cycle_nonce: 1, + result: CycleResult::Completed, + outcomes, + } if *id == actor_id + && outcomes.executed_steps == 0 + && outcomes.precondition_skips == step_count + && outcomes.committed_effectful_tasks == 0 + ))); + }); +} + +#[test] +#[ignore = "deterministic EXP-0002 profile output; run explicitly with --nocapture"] +fn profile_exp_0002_state_footprint() { + seeded_test_ext().execute_with(|| { + System::set_block_number(1); + let step_count = ::MaxContractSteps::get(); + let short_actor = create_system( + ALICE, + manual_schedule(), + None, + exp_0002_false_steps(1), + ); + let max_actor = create_system( + BOB, + manual_schedule(), + None, + exp_0002_false_steps(step_count), + ); + let geometry = |actor_id| { + let head = pallet_deos_actors::ActorContractHeads::::get(actor_id) + .expect("profile head exists"); + let certificate = pallet_deos_actors::ActorAdmissionCertificates::::get(actor_id) + .expect("profile certificate exists"); + let tails = pallet_deos_actors::ActorContractTailChunks::::iter_prefix(actor_id) + .collect::>(); + let tail_bytes = tails + .iter() + .map(|(_, chunk)| chunk.encode().len()) + .sum::(); + ( + head.encode().len(), + certificate.encode().len(), + tails.len(), + tail_bytes, + ) + }; + let short = geometry(short_actor); + let maximum = geometry(max_actor); + assert_eq!(short.2, 0); + assert_eq!( + maximum.2, + step_count + .saturating_sub(1) + .div_ceil(pallet_deos_actors::MAX_STEPS_PER_TAIL_CHUNK) as usize + ); + println!( + "EXP0002_PROFILE steps={} funding={} opening={} opening_predicates={} short_head={} short_certificate={} short_tail_chunks={} short_tail_bytes={} max_head={} max_certificate={} max_tail_chunks={} max_tail_bytes={}", + step_count, + ::MaxFundingTrackedAssets::get(), + ::MaxOpeningSnapshotEntries::get(), + ::MaxOpeningPredicateResults::get(), + short.0, + short.1, + short.2, + short.3, + maximum.0, + maximum.1, + maximum.2, + maximum.3, + ); }); } @@ -1422,49 +2612,6 @@ fn pool_creation_owns_an_exact_lp_reverse_index() { }); } -#[test] -fn pool_index_post_dispatch_failure_rejects_and_restores_exact_root() { - seeded_test_ext().execute_with(|| { - const INDEXED_ASSET: u32 = 901_003; - System::set_block_number(1); - let signer_pair = sr25519::Pair::from_seed(&[54u8; 32]); - let signer = crate::AccountId::from(signer_pair.public()); - let _ = >::deposit_creating( - &signer, - 1_000_000_000_000_000_000, - ); - assert_ok!(create_test_asset(INDEXED_ASSET, &signer)); - let pair = (AssetKind::Native, AssetKind::Local(INDEXED_ASSET)); - let call = - RuntimeCall::AssetConversion(polkadot_sdk::pallet_asset_conversion::Call::create_pool { - asset1: Box::new(pair.0), - asset2: Box::new(pair.1), - }); - crate::configs::pool_index::set_fail_pool_index_post_dispatch(true); - let root_before = - polkadot_sdk::sp_io::storage::root(polkadot_sdk::sp_runtime::StateVersion::V1); - let rejected = polkadot_sdk::frame_support::storage::with_transaction( - || -> polkadot_sdk::frame_support::storage::TransactionOutcome> { - let result = Executive::apply_extrinsic(signed_extrinsic(&signer_pair, 0, call)); - let rejected = result.is_err() || matches!(result, Ok(Err(_))); - polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Ok(rejected)) - }, - ) - .expect("pool-index rejection check"); - crate::configs::pool_index::set_fail_pool_index_post_dispatch(false); - assert!( - rejected, - "post-dispatch index fault must reject the block candidate" - ); - assert_eq!( - polkadot_sdk::sp_io::storage::root(polkadot_sdk::sp_runtime::StateVersion::V1), - root_before, - "rejection restores pool, LP asset/index, Oracle feeds, balances, events, and nonce" - ); - assert!(crate::AssetConversion::get_reserves(pair.0, pair.1).is_err()); - }); -} - #[test] fn remove_liquidity_requires_and_uses_the_exact_lp_reverse_index() { seeded_test_ext().execute_with(|| { @@ -1586,7 +2733,7 @@ fn remove_liquidity_requires_and_uses_the_exact_lp_reverse_index() { } #[test] -fn executive_pool_creation_indexes_the_lp_without_event_scanning() { +fn executive_canonical_pool_creation_commits_complete_topology() { seeded_test_ext().execute_with(|| { const INDEXED_ASSET: u32 = 901_003; System::set_block_number(1); @@ -1599,11 +2746,10 @@ fn executive_pool_creation_indexes_the_lp_without_event_scanning() { assert_ok!(create_test_asset(INDEXED_ASSET, &ALICE)); crate::configs::AssetConversionAdapter::ensure_lp_asset_namespace(); let pair = (AssetKind::Native, AssetKind::Local(INDEXED_ASSET)); - let call = - RuntimeCall::AssetConversion(polkadot_sdk::pallet_asset_conversion::Call::create_pool { - asset1: Box::new(pair.0), - asset2: Box::new(pair.1), - }); + let call = RuntimeCall::DeosRouter(pallet_deos_router::Call::create_pool { + asset_a: pair.0, + asset_b: pair.1, + }); let result = Executive::apply_extrinsic(signed_extrinsic(&signer, 0, call)); assert!(matches!(result, Ok(Ok(_))), "{result:?}"); let pool = polkadot_sdk::pallet_asset_conversion::Pools::::get(pair) @@ -1616,7 +2762,7 @@ fn executive_pool_creation_indexes_the_lp_without_event_scanning() { } #[test] -fn executive_pool_creation_rejects_before_dispatch_when_index_admission_fails() { +fn executive_canonical_pool_creation_rolls_back_when_oracle_admission_fails() { seeded_test_ext().execute_with(|| { System::set_block_number(1); let producer = deos_router_account(); @@ -1650,22 +2796,14 @@ fn executive_pool_creation_rejects_before_dispatch_when_index_admission_fails() AssetConversionAdapter::ensure_lp_asset_namespace(); let next_lp_before = polkadot_sdk::pallet_asset_conversion::NextPoolAssetId::::get(); let pair = (AssetKind::Native, AssetKind::Local(INDEXED_ASSET)); - let call = - RuntimeCall::AssetConversion(polkadot_sdk::pallet_asset_conversion::Call::create_pool { - asset1: Box::new(pair.0), - asset2: Box::new(pair.1), - }); + let call = RuntimeCall::DeosRouter(pallet_deos_router::Call::create_pool { + asset_a: pair.0, + asset_b: pair.1, + }); let result = Executive::apply_extrinsic(signed_extrinsic(&signer, 0, call)); - assert!(matches!( - result, - Err( - polkadot_sdk::sp_runtime::transaction_validity::TransactionValidityError::Invalid( - polkadot_sdk::sp_runtime::transaction_validity::InvalidTransaction::Custom(41) - ) - ) - )); + assert!(matches!(result, Ok(Err(_))), "{result:?}"); assert!(!polkadot_sdk::pallet_asset_conversion::Pools::::contains_key(pair)); assert_eq!( polkadot_sdk::pallet_asset_conversion::NextPoolAssetId::::get(), @@ -1778,7 +2916,7 @@ fn create_user_charges_creation_fee_to_fee_sink() { let fee_sink = ::FeeSink::get(); let sink_before = native_balance(&fee_sink); let alice_before = native_balance(&ALICE); - let _ = create_user( + let actor_id = create_user( ALICE, manual_schedule(), None, @@ -1789,7 +2927,7 @@ fn create_user_charges_creation_fee_to_fee_sink() { native_balance(&ALICE), alice_before .saturating_sub(fee) - .saturating_sub(crate::configs::actor_config::ActorManualTriggerBond::get()) + .saturating_sub(actor_state_hold_total(actor_id)) ); }); } @@ -1819,7 +2957,8 @@ fn actor_fee_collector_routes_the_full_amount_to_fee_sink() { assert!(!Actors::pending_signal(fee_sink_id)); let hot = Actors::actor_hot(fee_sink_id).expect("Fee Sink hot state"); assert!(hot.queue_ticket.is_none()); - assert!(hot.wakeup_pointer.is_some()); + assert!(hot.wakeup_pointer.is_none()); + assert!(hot.trigger_wakeup_pointer.is_some()); assert_eq!( Actors::active_actor_state(fee_sink_id) .expect("Fee Sink remains active") @@ -1931,19 +3070,17 @@ fn fee_sink_processes_ten_percent_and_splits_it_fifty_fifty() { assert_eq!(native_balance(&staking_pool), pool_before); assert_eq!(native_balance(&staking_liquidity_actor), liquidity_before); - let cadence_block = Actors::actor_hot(fee_sink_id) - .and_then(|hot| hot.wakeup_pointer) + let cadence_tick = Actors::actor_hot(fee_sink_id) + .and_then(|hot| hot.trigger_wakeup_pointer) .expect("Fee Sink cadence is armed from the first consensus timestamp") - .block; - let WakeupKey::Tick(cadence_tick) = cadence_block else { - panic!("Fee Sink must own a timestamp-tick wakeup"); - }; + .tick; set_consensus_timestamp( cadence_tick.saturating_mul(primitives::ecosystem::params::ACTOR_CADENCE_TICK_MILLIS), ); System::set_block_number(3); Actors::on_initialize(3); run_idle(Weight::MAX); + run_next_idle(Weight::MAX); // Each cycle processes 10% of the spendable buffer and splits that amount 50/50. let pool_delta = native_balance(&staking_pool).saturating_sub(pool_before); @@ -1965,7 +3102,7 @@ fn fee_sink_processes_ten_percent_and_splits_it_fifty_fifty() { } #[test] -fn permissionless_sweep_many_batches_lifecycle_evaluation() { +fn permissionless_sweep_many_does_not_predict_pipeline_affordability() { seeded_test_ext().execute_with(|| { System::set_block_number(1); let user_a_prefunded = @@ -1998,16 +3135,16 @@ fn permissionless_sweep_many_batches_lifecycle_evaluation() { RuntimeOrigin::signed(CHARLIE), sweep_ids, )); - assert!(Actors::active_actor_state(user_a).is_none()); - assert!(Actors::active_actor_state(user_b).is_none()); + assert!(Actors::active_actor_state(user_a).is_some()); + assert!(Actors::active_actor_state(user_b).is_some()); assert!(Actors::active_actor_state(system_alive).is_some()); assert!(has_actor_event(|event| { matches!( event, Event::SweepBatchProcessed { requested: 3, - closed: 2, - alive: 1, + closed: 0, + alive: 3, missing: 0, } ) @@ -2118,26 +3255,18 @@ fn paged_queue_limits_are_independent_runtime_controls() { } #[test] -fn user_trigger_state_bond_adjusts_and_releases_across_control_lifecycle() { +fn active_trigger_lifecycle_reconciles_only_the_dedicated_state_hold() { seeded_test_ext().execute_with(|| { - let reserved_before = - >::reserved_balance(&ALICE); + let hold_before = actors_owner_hold(&ALICE); let actor_id = create_user( ALICE, manual_schedule(), None, transfer_contract_steps(BOB, AssetKind::Native, 1), ); - let expected = crate::configs::actor_config::ActorManualTriggerBond::get(); assert_eq!( - >::reserved_balance(&ALICE), - reserved_before + expected - ); - assert_eq!( - pallet_deos_actors::ActorFunding::::get(actor_id) - .expect("active User funding partition exists") - .trigger_state_bond, - expected + actors_owner_hold(&ALICE), + hold_before.saturating_add(actor_state_hold_total(actor_id)) ); assert_ok!(update_actor_contract_partial( @@ -2145,16 +3274,9 @@ fn user_trigger_state_bond_adjusts_and_releases_across_control_lifecycle() { actor_id, (Trigger::cadenced(1), 0, None), )); - let cadenced = crate::configs::actor_config::ActorCadencedTriggerBond::get(); assert_eq!( - >::reserved_balance(&ALICE), - reserved_before + cadenced - ); - assert_eq!( - pallet_deos_actors::ActorFunding::::get(actor_id) - .expect("replacement preserves funding partition") - .trigger_state_bond, - cadenced + actors_owner_hold(&ALICE), + hold_before.saturating_add(actor_state_hold_total(actor_id)) ); age_fixture_control_clock(actor_id); @@ -2162,96 +3284,13 @@ fn user_trigger_state_bond_adjusts_and_releases_across_control_lifecycle() { RuntimeOrigin::signed(ALICE), actor_id )); + let dormant = Actors::actor_state_hold(actor_id).expect("Dormant identity hold remains"); + assert!(dormant.breakdown.identity > 0); + assert_eq!(dormant.breakdown.contract_head, 0); assert_eq!( - >::reserved_balance(&ALICE), - reserved_before - ); - assert!(pallet_deos_actors::ActorFunding::::get(actor_id).is_none()); - - let close_id = create_user( - ALICE, - manual_schedule(), - None, - transfer_contract_steps(BOB, AssetKind::Native, 1), - ); - assert_eq!( - >::reserved_balance(&ALICE), - reserved_before + expected - ); - assert_ok!(Actors::close_actor(RuntimeOrigin::signed(ALICE), close_id)); - assert_eq!( - >::reserved_balance(&ALICE), - reserved_before - ); - }); -} - -#[test] -fn insufficient_user_trigger_bond_rejects_active_creation_before_partial_mutation() { - seeded_test_ext().execute_with(|| { - let owner = crate::AccountId::from([42u8; 32]); - let steps = transfer_contract_steps(BOB, AssetKind::Native, 1); - prefund_active_user_creation(&owner, &steps); - let creation_fee = ::ActorCreationFee::get(); - let one_ed = crate::EXISTENTIAL_DEPOSIT; - let _ = - >::deposit_creating(&owner, creation_fee + one_ed); - let actor_id = Actors::next_actor_id(); - let root_before = - polkadot_sdk::sp_io::storage::root(polkadot_sdk::sp_runtime::StateVersion::V1); - - assert!( - Actors::create_user_actor( - RuntimeOrigin::signed(owner.clone()), - Mutability::Mutable, - user_active_contract( - RuntimeSchedule { - trigger: Trigger::cadenced(1), - cooldown_blocks: 0, - }, - None, - steps, - ), - ) - .is_err() - ); - assert_eq!( - polkadot_sdk::sp_io::storage::root(polkadot_sdk::sp_runtime::StateVersion::V1), - root_before - ); - assert_eq!(Actors::next_actor_id(), actor_id); - assert_eq!( - >::reserved_balance(&owner), - 0 - ); - assert!(pallet_deos_actors::ActorIdentities::::get(actor_id).is_none()); - - let _ = >::deposit_creating(&owner, one_ed); - let root_after_top_up = - polkadot_sdk::sp_io::storage::root(polkadot_sdk::sp_runtime::StateVersion::V1); - crate::configs::actor_config::fail_next_trigger_bond_after_adjustment(); - assert!( - Actors::create_user_actor( - RuntimeOrigin::signed(owner.clone()), - Mutability::Mutable, - user_active_contract( - manual_schedule(), - None, - transfer_contract_steps(BOB, AssetKind::Native, 1) - ), - ) - .is_err() - ); - assert_eq!( - polkadot_sdk::sp_io::storage::root(polkadot_sdk::sp_runtime::StateVersion::V1), - root_after_top_up, - "a failure after successful reserve must roll back owner custody and all Actor state" - ); - assert_eq!( - >::reserved_balance(&owner), - 0 + actors_owner_hold(&ALICE), + hold_before.saturating_add(dormant.breakdown.identity) ); - assert_eq!(Actors::next_actor_id(), actor_id); }); } @@ -2332,22 +3371,47 @@ fn crossing_capacity_policy_is_bound_to_measured_minimum_progress_and_explicit_h fn reactive_delivery_envelopes_follow_production_weights_and_topology_bounds() { let base = as pallet_deos_actors::WeightInfo>::observation_fanout_base(); - let unit = + let branch_probe = + as pallet_deos_actors::WeightInfo>::observation_fanout_branch_probe(); + let queue = as pallet_deos_actors::WeightInfo>::observation_fanout_page(); + let wakeup = + as pallet_deos_actors::WeightInfo>::observation_fanout_wakeup_page(); + let coalesced = + as pallet_deos_actors::WeightInfo>::observation_fanout_coalesced_page(); + let blocked = + as pallet_deos_actors::WeightInfo>::observation_fanout_blocked_page(); + let unit = Weight::from_parts( + queue + .ref_time() + .max(wakeup.ref_time()) + .max(coalesced.ref_time()) + .max(blocked.ref_time()), + queue + .proof_size() + .max(wakeup.proof_size()) + .max(coalesced.proof_size()) + .max(blocked.proof_size()), + ); + let fault = + as pallet_deos_actors::WeightInfo>::record_observation_fanout_worker_fault(); let limit = ::ObservationFanoutWeightLimit::get(); let configured_units = u64::from(::MaxObservationFanoutPagesPerBlock::get()); let available = limit.saturating_sub(base); + let admitted_unit = branch_probe.saturating_add(unit).saturating_add(fault); let units_per_block = configured_units - .min(available.ref_time() / unit.ref_time()) - .min(available.proof_size() / unit.proof_size()); + .min(available.ref_time() / admitted_unit.ref_time()) + .min(available.proof_size() / admitted_unit.proof_size()); - assert_eq!(base, Weight::from_parts(56_705_000, 1_543)); - assert_eq!(unit, Weight::from_parts(18_151_008_000, 718_430)); + assert_eq!(base, Weight::from_parts(56_285_000, 1_629)); + assert_eq!(branch_probe, Weight::from_parts(63_340_000, 3_587)); + assert_eq!(unit, Weight::from_parts(150_994_631_000, 304_734)); + assert_eq!(fault, Weight::from_parts(195_464_000, 4_106)); assert_eq!(limit, Weight::from_parts(400_000_000_000, 1_000_000)); assert_eq!( - units_per_block, 1, - "complete actor-state ProofSize is the active fanout limit" + units_per_block, 2, + "fee-charged blocked-fallback RefTime is the active ordinary fanout service limit" ); let max_actors = u64::from(::MaxActiveActors::get()); @@ -2361,10 +3425,10 @@ fn reactive_delivery_envelopes_follow_production_weights_and_topology_bounds() { assert_eq!((max_actors, page_size, max_sources), (10_000, 64, 1)); assert_eq!(subscription_pages, 157); - assert_eq!(dense_single_feed_units.div_ceil(units_per_block), 157); + assert_eq!(dense_single_feed_units.div_ceil(units_per_block), 79); assert_eq!(sparse_high_slot_units.div_ceil(units_per_block), 1); - assert_eq!(compact_four_feed_units.div_ceil(units_per_block), 157); - assert_eq!(quiescent_revision_race_units.div_ceil(units_per_block), 314); + assert_eq!(compact_four_feed_units.div_ceil(units_per_block), 79); + assert_eq!(quiescent_revision_race_units.div_ceil(units_per_block), 157); } #[test] @@ -2392,7 +3456,8 @@ fn sched_workers_static_envelope_leaves_one_actor_unit_inside_guaranteed_budget( // Maximum fanout worker envelope: base plus one page per configured slot, capped by the limit. let fanout_base = W::observation_fanout_base(); - let fanout_page = W::observation_fanout_page(); + let fanout_page = Actors::observation_fanout_ordinary_weight_upper() + .saturating_add(W::record_observation_fanout_worker_fault()); let fanout_ceiling = ::ObservationFanoutWeightLimit::get(); let fanout_per_block = u64::from(::MaxObservationFanoutPagesPerBlock::get()); @@ -2410,7 +3475,9 @@ fn sched_workers_static_envelope_leaves_one_actor_unit_inside_guaranteed_budget( .saturating_add(W::crossing_page_unit()) .saturating_add(W::crossing_actor_unit()); let crossing_ceiling = ::CrossingWorkerWeightLimit::get(); - let crossing_envelope = crossing_base.saturating_add(crossing_unit); + let crossing_envelope = crossing_base + .saturating_add(crossing_unit) + .saturating_add(W::record_crossing_worker_fault()); assert!( crossing_envelope.all_lte(crossing_ceiling), "one complete maximum Crossing worker unit must fit its two-dimensional envelope: base={crossing_base:?}, unit={crossing_unit:?}, combined={crossing_envelope:?}, ceiling={crossing_ceiling:?}" @@ -3199,7 +4266,7 @@ fn remove_liquidity_minimum_failure_preserves_each_error_policy_path() { RuntimeOrigin::signed(ALICE), actor_id )); - run_idle(Weight::MAX); + run_next_idle(Weight::MAX); assert_eq!(Assets::balance(lp_id, &actor), lp_before); assert!(!has_actor_event(|event| matches!( @@ -3221,7 +4288,7 @@ fn remove_liquidity_minimum_failure_preserves_each_error_policy_path() { event, Event::CycleStopped { actor_id: id, .. } if *id == actor_id ))); - assert!(Actors::continuation_state(actor_id).is_none()); + assert!(Actors::actor_run_state(actor_id).is_none()); } StepErrorPolicy::ContinueNextStep => { assert_eq!(Balances::free_balance(&actor), native_before); @@ -3229,7 +4296,7 @@ fn remove_liquidity_minimum_failure_preserves_each_error_policy_path() { event, Event::CycleStopped { actor_id: id, step_index: 1, .. } if *id == actor_id ))); - assert!(Actors::continuation_state(actor_id).is_none()); + assert!(Actors::actor_run_state(actor_id).is_none()); } StepErrorPolicy::RetryLater { .. } => { assert_eq!(Balances::free_balance(&actor), native_before); @@ -3238,7 +4305,7 @@ fn remove_liquidity_minimum_failure_preserves_each_error_policy_path() { Event::CycleStopped { actor_id: id, .. } if *id == actor_id ))); let continuation = - Actors::continuation_state(actor_id).expect("temporary minimum failure suspends"); + Actors::actor_run_state(actor_id).expect("temporary minimum failure suspends"); assert_eq!(continuation.cursor, 0); assert_eq!(continuation.unsuccessful_attempts_at_cursor, 1); } @@ -3318,18 +4385,22 @@ fn market_execution_classifier_uses_the_concrete_cause() { ActorRetryClass::Temporary ); - let permanent = market_execution_failure( - polkadot_sdk::pallet_asset_conversion::Error::::InvalidPath.into(), - ); - assert_eq!( - permanent.failure_class(), - RouterFailureClass::InvariantViolation - ); - assert_eq!(permanent.retry_disposition(), RouterRetryClass::Permanent); - assert_eq!( - classify_router_execution_failure(permanent.into()).retry, - ActorRetryClass::Permanent - ); + for error in [ + polkadot_sdk::pallet_asset_conversion::Error::::InvalidPath, + polkadot_sdk::pallet_asset_conversion::Error::::InvalidAssetPair, + polkadot_sdk::pallet_asset_conversion::Error::::PoolNotFound, + ] { + let permanent = market_execution_failure(error.into()); + assert_eq!( + permanent.failure_class(), + RouterFailureClass::InvariantViolation + ); + assert_eq!(permanent.retry_disposition(), RouterRetryClass::Permanent); + assert_eq!( + classify_router_execution_failure(permanent.into()).retry, + ActorRetryClass::Permanent + ); + } } #[test] @@ -3625,7 +4696,7 @@ fn excessive_system_reference_deviation_suspends_without_fill_and_backs_off() { assert_ok!(Actors::manual_trigger(RuntimeOrigin::root(), actor_id)); run_idle(Weight::MAX); - let continuation = Actors::continuation_state(actor_id).expect("deviation suspends"); + let continuation = Actors::actor_run_state(actor_id).expect("deviation suspends"); assert_eq!(continuation.unsuccessful_attempts_at_cursor, 1); assert_eq!(continuation.cursor, 0); assert_eq!(native_balance(&actor), before); @@ -3635,7 +4706,7 @@ fn excessive_system_reference_deviation_suspends_without_fill_and_backs_off() { System::set_block_number(2); run_idle(Weight::MAX); - let continuation = Actors::continuation_state(actor_id).expect("deviation resuspends"); + let continuation = Actors::actor_run_state(actor_id).expect("deviation resuspends"); assert_eq!(continuation.unsuccessful_attempts_at_cursor, 2); assert_eq!(continuation.cursor, 0); assert_eq!(native_balance(&actor), before); @@ -3681,7 +4752,7 @@ fn temporary_market_failure_opens_the_single_retry_continuation() { )); run_idle(Weight::MAX); - let continuation = Actors::continuation_state(actor_id).expect("Temporary failure suspends"); + let continuation = Actors::actor_run_state(actor_id).expect("Temporary failure suspends"); assert_eq!(continuation.cursor, 0); assert!(has_actor_event(|event| matches!( event, @@ -3767,7 +4838,7 @@ fn temporary_oracle_capacity_failure_rolls_back_economics_and_has_one_retry_owne RuntimeOrigin::signed(ALICE), actor_id )); - run_idle(Weight::MAX); + run_next_idle(Weight::MAX); let input_after_failure = native_balance(&actor); assert!(input_after_failure < input_before); @@ -3815,7 +4886,7 @@ fn temporary_oracle_capacity_failure_rolls_back_economics_and_has_one_retry_owne .count(), 1, ); - let continuation = Actors::continuation_state(actor_id).expect("publication retry suspends"); + let continuation = Actors::actor_run_state(actor_id).expect("publication retry suspends"); assert_eq!(continuation.cursor, 0); let hot = Actors::actor_hot(actor_id).expect("suspended Actor stays hot"); assert!(hot.queue_ticket.is_some()); @@ -3825,7 +4896,7 @@ fn temporary_oracle_capacity_failure_rolls_back_economics_and_has_one_retry_owne System::set_block_number(21); run_idle(Weight::MAX); - assert!(Actors::continuation_state(actor_id).is_none()); + assert!(Actors::actor_run_state(actor_id).is_none()); assert!(native_balance(&actor) < input_after_failure); assert!(Assets::balance(ASSET_A, &actor) > output_before); assert_ne!( @@ -3929,7 +5000,7 @@ fn permanent_publication_invariant_terminates_without_cross_system_mutation_or_r RuntimeOrigin::signed(ALICE), actor_id )); - run_idle(Weight::MAX); + run_next_idle(Weight::MAX); assert!( native_balance(&actor) < actor_input_before, @@ -3939,7 +5010,7 @@ fn permanent_publication_invariant_terminates_without_cross_system_mutation_or_r Actors::actor_contract(actor_id).expect("Actor Contract remains"), actor_contract_before, ); - assert!(Actors::continuation_state(actor_id).is_none()); + assert!(Actors::actor_run_state(actor_id).is_none()); let hot = Actors::actor_hot(actor_id).expect("Actor hot state remains"); assert!(hot.queue_ticket.is_none()); assert!(hot.wakeup_pointer.is_none()); @@ -3983,7 +5054,7 @@ fn permanent_publication_invariant_terminates_without_cross_system_mutation_or_r System::set_block_number(21); run_idle(Weight::MAX); - assert!(Actors::continuation_state(actor_id).is_none()); + assert!(Actors::actor_run_state(actor_id).is_none()); assert_eq!( actor_events() .iter() @@ -4409,7 +5480,7 @@ fn split_transfer_rejects_ed_ineligible_recipient_then_retries_atomically() { event, Event::SplitTransferExecuted { actor_id: id, .. } if *id == actor_id ))); - let continuation = Actors::continuation_state(actor_id).expect("temporary rejection suspends"); + let continuation = Actors::actor_run_state(actor_id).expect("temporary rejection suspends"); assert_eq!(continuation.cursor, 0); assert_eq!(continuation.unsuccessful_attempts_at_cursor, 1); @@ -4422,7 +5493,7 @@ fn split_transfer_rejects_ed_ineligible_recipient_then_retries_atomically() { assert_eq!(native_balance(&actor), actor_before - total); assert_eq!(native_balance(&BOB), bob_before + 50); assert_eq!(native_balance(&unknown), unknown_before + 50); - assert!(Actors::continuation_state(actor_id).is_none()); + assert!(Actors::actor_run_state(actor_id).is_none()); assert!(has_actor_event(|event| matches!( event, Event::SplitTransferExecuted { @@ -4537,7 +5608,7 @@ fn typed_ten_julian_year_horizons_match_runtime_clocks() { block_horizon ); assert_eq!( - crate::configs::actor_config::ActorMaxCadenceDelayTicks::get(), + crate::configs::actor_config::ActorMaxTemporalDelayTicks::get(), cadence_horizon ); } @@ -4546,35 +5617,51 @@ fn typed_ten_julian_year_horizons_match_runtime_clocks() { fn timer_horizon_validation_accepts_exact_runtime_bound() { seeded_test_ext().execute_with(|| { System::set_block_number(1); - let largest_valid_cadence = - ::MaxCadenceDelayTicks::get(); + let largest_valid_temporal_delay = + ::MaxTemporalDelayTicks::get(); assert!( - largest_valid_cadence + largest_valid_temporal_delay > u64::from(::MaxExecutionDelayBlocks::get()) ); - let schedule = |every_ticks| Schedule { - trigger: Trigger::cadenced(every_ticks), - cooldown_blocks: 0, - }; - let valid_plan = transfer_contract_steps(BOB, AssetKind::Native, 1); - prefund_active_user_creation(&ALICE, &valid_plan); - assert_ok!(Actors::create_user_actor( - RuntimeOrigin::signed(ALICE), - Mutability::Mutable, - user_active_contract(schedule(largest_valid_cadence), None, valid_plan), - )); - assert_noop!( - Actors::create_user_actor( + for trigger in [ + Trigger::at_time(largest_valid_temporal_delay), + Trigger::cadenced(largest_valid_temporal_delay), + ] { + let valid_plan = transfer_contract_steps(BOB, AssetKind::Native, 1); + prefund_active_user_creation(&ALICE, &valid_plan); + assert_ok!(Actors::create_user_actor( RuntimeOrigin::signed(ALICE), Mutability::Mutable, user_active_contract( - schedule(largest_valid_cadence.saturating_add(1)), + Schedule { + trigger, + cooldown_blocks: 0, + }, None, - transfer_contract_steps(BOB, AssetKind::Native, 1), + valid_plan, ), - ), - Error::::ExecutionDelayTooLong - ); + )); + } + for trigger in [ + Trigger::at_time(largest_valid_temporal_delay.saturating_add(1)), + Trigger::cadenced(largest_valid_temporal_delay.saturating_add(1)), + ] { + assert_noop!( + Actors::create_user_actor( + RuntimeOrigin::signed(ALICE), + Mutability::Mutable, + user_active_contract( + Schedule { + trigger, + cooldown_blocks: 0, + }, + None, + transfer_contract_steps(BOB, AssetKind::Native, 1), + ), + ), + Error::::ExecutionDelayTooLong + ); + } }); } @@ -4608,6 +5695,8 @@ fn on_address_event_owner_only_respects_source_filter() { &ALICE )); run_idle(Weight::MAX); + System::set_block_number(System::block_number().saturating_add(1)); + run_idle(Weight::MAX); assert_eq!(native_balance(&BOB), bob_before.saturating_add(amount)); }); } @@ -4641,6 +5730,8 @@ fn on_address_event_asset_filter_is_enforced() { &ALICE )); run_idle(Weight::MAX); + System::set_block_number(System::block_number().saturating_add(1)); + run_idle(Weight::MAX); assert_eq!(native_balance(&BOB), bob_before.saturating_add(amount)); }); } @@ -4869,8 +5960,10 @@ fn mint_to_actor_sovereign_notifies_source_less_certified_ingress() { )); assert_eq!( native_balance(&receiver_sovereign), - before.saturating_add(10_000), - "mint creates the value movement" + before + .saturating_add(10_000) + .saturating_sub(address_event_trigger_fee()), + "mint creates value and the sovereign independently pays Trigger materialization" ); assert!( Actors::pending_signal(receiver), @@ -5212,6 +6305,52 @@ fn genesis_system_locator_is_recoverable_after_close_through_reattachment() { }); } +#[test] +fn user_exact_slot_recovery_reuses_residual_runtime_custody() { + seeded_test_ext().execute_with(|| { + System::set_block_number(1); + let slot = 200u8; + let steps = transfer_contract_steps(BOB, AssetKind::Native, 333); + prefund_user_sovereign(&ALICE, slot, &steps); + let first_id = Actors::next_actor_id(); + assert_ok!(Actors::create_user_actor_at_slot( + RuntimeOrigin::signed(ALICE), + slot, + Mutability::Mutable, + user_active_contract(manual_schedule(), None, steps.clone()), + )); + let sovereign = Actors::sovereign_account_id(&ALICE, slot); + let residual = 10_000_000_000_000u128; + let _ = >::deposit_creating(&sovereign, residual); + let preserved = Balances::free_balance(&sovereign); + + assert_ok!(Actors::close_actor(RuntimeOrigin::signed(ALICE), first_id)); + assert_eq!(Balances::free_balance(&sovereign), preserved); + + let fresh_id = Actors::next_actor_id(); + assert_ok!(Actors::create_user_actor_at_slot( + RuntimeOrigin::signed(ALICE), + slot, + Mutability::Mutable, + user_active_contract(manual_schedule(), None, steps), + )); + let fresh = Actors::active_actor_state(fresh_id).expect("fresh User identity exists"); + assert_ne!(fresh_id, first_id); + assert_eq!(fresh.identity.sovereign_account, sovereign); + assert_eq!(fresh.identity.cycle_nonce, 0); + assert_eq!(Balances::free_balance(&sovereign), preserved); + + let bob_before = Balances::free_balance(BOB); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + fresh_id + )); + run_idle(Weight::MAX); + assert_eq!(Balances::free_balance(BOB), bob_before.saturating_add(333)); + assert!(Balances::free_balance(&sovereign) < preserved); + }); +} + #[test] fn ingress_adapter_without_source_matches_any_source_filter() { seeded_test_ext().execute_with(|| { @@ -5311,6 +6450,8 @@ fn transfer_ingress_updates_system_snapshot_without_pause_resume() { sender_id )); run_idle(Weight::MAX); + System::set_block_number(System::block_number().saturating_add(1)); + run_idle(Weight::MAX); assert_eq!( actor_funding(target_id) .funding_accumulated @@ -5607,41 +6748,30 @@ fn xcm_mixed_ingress_single_deposit_triggers_single_cycle() { // --- Actors Platform: Scheduling & Budget --- #[test] -fn cycle_does_not_execute_when_budget_is_too_small() { +fn mandatory_base_pass_executes_independently_of_later_idle_budget() { seeded_test_ext().execute_with(|| { System::set_block_number(1); - let heavy_task = Task::RemoveLiquidity { - lp_asset: AssetKind::Local(ASSET_A), - asset_a: AssetKind::Local(1), - asset_b: AssetKind::Local(2), - lp_amount: AmountResolution::Fixed(1), - min_amount_a: 1, - min_amount_b: 1, - }; - let step = make_step(heavy_task); - let steps = BoundedVec::try_from(vec![step.clone(), step.clone(), step]).expect("steps fits"); - let actor_id = create_user(ALICE, manual_schedule(), None, steps); + let actor_id = create_system( + ALICE, + manual_schedule(), + None, + transfer_contract_steps(BOB, AssetKind::Native, 1), + ); fund_native(actor_id, 1_000_000_000_000); assert_ok!(Actors::manual_trigger( RuntimeOrigin::signed(ALICE), actor_id )); - let instance = Actors::active_actor_state(actor_id).expect("Actors exists"); - let attempt_weight = Actors::compute_cycle_weight_upper( - instance.identity.actor_class.actor_type(), - &instance.contract.steps, - ); System::set_block_number(2); - let target_weight = attempt_weight.saturating_sub(Weight::from_parts(1, 0)); - run_idle(target_weight); + run_idle(Weight::zero()); let instance = Actors::active_actor_state(actor_id).expect("Actors exists"); - assert_eq!(instance.identity.cycle_nonce, 0); - assert!(instance.hot.pending_signal); + assert_eq!(instance.identity.cycle_nonce, 1); + assert!(!instance.hot.pending_signal); }); } #[test] -fn cycle_closes_with_balance_exhausted_before_the_smaller_weight_derived_fee() { +fn underfunded_manual_trigger_preserves_runtime_user_process() { seeded_test_ext().execute_with(|| { System::set_block_number(1); let heavy_task = Task::RemoveLiquidity { @@ -5659,57 +6789,23 @@ fn cycle_closes_with_balance_exhausted_before_the_smaller_weight_derived_fee() { let min_balance = ::MinUserBalance::get(); assert!( fee_envelope.total < min_balance, - "reference Weight-derived fee should remain below MinUserBalance" - ); - let prefunded = user_prefunding_requirement(&steps); - let actor_id = create_user(ALICE, manual_schedule(), None, steps); - deplete_user_sovereign(actor_id, prefunded); - fund_native(actor_id, min_balance.saturating_sub(1)); - assert_ok!(Actors::manual_trigger( - RuntimeOrigin::signed(ALICE), - actor_id - )); - run_idle(Weight::MAX); - assert!(Actors::active_actor_state(actor_id).is_none()); - assert!(has_actor_event(|event| { - matches!( - event, - Event::ActorClosed { - actor_id: id, - reason: CloseReason::BalanceExhausted, - } if *id == actor_id - ) - })); - }); -} - -#[test] -fn fee_insufficiency_is_terminal_without_deferral_guard() { - seeded_test_ext().execute_with(|| { - System::set_block_number(1); - let heavy_task = Task::RemoveLiquidity { - lp_asset: AssetKind::Local(ASSET_A), - asset_a: AssetKind::Local(1), - asset_b: AssetKind::Local(2), - lp_amount: AmountResolution::Fixed(1), - min_amount_a: 1, - min_amount_b: 1, - }; - let step = make_step(heavy_task.clone()); - let steps = BoundedVec::try_from(vec![step.clone(), step.clone(), step]).expect("steps fits"); - let attempt_fee = Actors::attempt_fee_envelope(ActorType::User, &steps, 0) - .expect("runtime plan has a checked fee envelope") - .total; + "reference Weight-derived fee should remain below MinUserBalance" + ); let prefunded = user_prefunding_requirement(&steps); let actor_id = create_user(ALICE, manual_schedule(), None, steps); deplete_user_sovereign(actor_id, prefunded); - fund_native(actor_id, attempt_fee.saturating_sub(1)); - assert_ok!(Actors::manual_trigger( - RuntimeOrigin::signed(ALICE), - actor_id - )); - run_idle(Weight::MAX); - assert!(Actors::active_actor_state(actor_id).is_none()); + fund_native(actor_id, min_balance.saturating_sub(1)); + assert_noop!( + Actors::manual_trigger(RuntimeOrigin::signed(ALICE), actor_id), + Error::::InsufficientFee + ); + let state = Actors::active_actor_state(actor_id).expect("process remains live"); + assert!(!state.hot.pending_signal); + assert!(state.hot.queue_ticket.is_none()); + assert!(!has_actor_event(|event| matches!( + event, + Event::ActorClosed { actor_id: id, .. } if *id == actor_id + ))); }); } @@ -5736,13 +6832,18 @@ fn scheduler_fifo_order_is_deterministic_across_actor_types() { } System::set_block_number(2); run_idle(Weight::MAX); - let actual = actor_events() + System::set_block_number(3); + run_idle(Weight::MAX); + System::set_block_number(4); + run_idle(Weight::MAX); + let mut actual: Vec<_> = actor_events() .into_iter() .filter_map(|event| match event { Event::CycleStarted { actor_id, .. } if tracked.contains(&actor_id) => Some(actor_id), _ => None, }) .collect(); + actual.dedup(); (tracked, actual) }) }; @@ -5758,7 +6859,7 @@ fn scheduler_fifo_order_is_deterministic_across_actor_types() { } #[test] -fn strict_head_of_line_heavy_head_deferral_preserves_follower_order() { +fn mandatory_base_and_drain_preserve_strict_heavy_head_order() { seeded_test_ext().execute_with(|| { System::set_block_number(1); // Heavy head first: three transfer steps make its cycle envelope strictly larger than the @@ -5813,47 +6914,27 @@ fn strict_head_of_line_heavy_head_deferral_preserves_follower_order() { System::reset_events(); run_idle(starvation_blocked_budget(head)); - let head_inst = Actors::active_actor_state(head).expect("head survives deferral"); - assert_eq!( - head_inst.identity.cycle_nonce, 0, - "head attempt is deferred, not admitted" - ); - assert!(head_inst.hot.pending_signal); - assert!(!has_actor_event(|event| matches!( - event, - Event::CycleStarted { actor_id: id, .. } | Event::CycleSummary { actor_id: id, .. } - if *id == head - ))); - for (id, ticket) in [(light_a, 1), (light_b, 2)] { - let inst = Actors::active_actor_state(id).expect("follower survives"); - assert_eq!( - inst.identity.cycle_nonce, 0, - "follower never admitted behind the head" - ); - assert!( - Actors::actor_hot(id).is_some_and(|hot| hot.queue_ticket == Some(ticket)), - "follower retains its exact physical ticket" - ); - } - assert!( - !has_actor_event(|event| matches!( - event, - Event::CycleStarted { actor_id: id, .. } if *id == light_a || *id == light_b - )), - "no follower attempt starts behind an unadmitted head" - ); - - // Conforming full envelope: the head advances first, then followers in exact ticket order. - System::set_block_number(3); - System::reset_events(); - run_idle(Weight::MAX); - let started: Vec<_> = System::events() - .into_iter() + let mut started: Vec<_> = System::events() + .iter() .filter_map(|record| match record.event { RuntimeEvent::Actors(Event::CycleStarted { actor_id, .. }) => Some(actor_id), _ => None, }) .collect(); + assert_eq!(started.first(), Some(&head)); + + System::set_block_number(3); + System::reset_events(); + run_idle(Weight::MAX); + started.extend( + System::events() + .into_iter() + .filter_map(|record| match record.event { + RuntimeEvent::Actors(Event::CycleStarted { actor_id, .. }) => Some(actor_id), + _ => None, + }), + ); + started.dedup(); assert_eq!(started, vec![head, light_a, light_b]); for id in [head, light_a, light_b] { assert_eq!( @@ -6021,15 +7102,29 @@ fn wakeup_drain_admission_uses_generated_runtime_weights() { seeded_test_ext().execute_with(|| { let close = <::WeightInfo as WeightInfo>::close_actor(); + let fault = + <::WeightInfo as WeightInfo>::record_wakeup_worker_fault(); + let at_time = + <::WeightInfo as WeightInfo>::at_time_trigger_occurrence(); + let cadence = + <::WeightInfo as WeightInfo>::cadenced_trigger_occurrence(); + let temporal = Weight::from_parts( + at_time.ref_time().max(cadence.ref_time()), + at_time.proof_size().max(cadence.proof_size()), + ); assert_eq!( Actors::wakeup_cursor_drain_unit_weight_upper(false), <::WeightInfo as WeightInfo>::scheduler_wakeup_cursor_worker_partial() + .saturating_add(temporal) .saturating_add(close) + .saturating_add(fault) ); assert_eq!( Actors::wakeup_cursor_drain_unit_weight_upper(true), <::WeightInfo as WeightInfo>::scheduler_wakeup_cursor_worker_remove() + .saturating_add(temporal) .saturating_add(close) + .saturating_add(fault) ); }); } @@ -6041,8 +7136,12 @@ fn transaction_extension_ingress_uses_generated_runtime_weights() { dest: polkadot_sdk::sp_runtime::MultiAddress::Id(BOB), value: 1, }); - let notify = <::WeightInfo as WeightInfo>::transaction_extension_ingress_notify(); + let measured_notify = <::WeightInfo as WeightInfo>::transaction_extension_ingress_notify(); let base = <::WeightInfo as WeightInfo>::transaction_extension_ingress_base(); + let notify = Weight::from_parts( + base.ref_time().max(measured_notify.ref_time()), + base.proof_size().max(measured_notify.proof_size()), + ); assert_eq!(AddressEventIngressExtension.weight(&call), notify); assert!(base.all_lte(notify)); assert!(base.proof_size() > 0); @@ -6555,19 +7654,26 @@ fn signed_transfer_all_is_rejected_before_dispatch_when_funding_pending_overflow fn signed_transfer_all_records_actual_post_fee_movement_without_event_scan() { seeded_test_ext().execute_with(|| { System::set_block_number(1); - let signer = sr25519::Pair::from_seed(&[49u8; 32]); - let signer_account = crate::AccountId::from(signer.public()); - let _ = >::deposit_creating( - &signer_account, - 1_000_000_000_000_000_000, - ); + let owner_signer = sr25519::Pair::from_seed(&[49u8; 32]); + let owner = crate::AccountId::from(owner_signer.public()); + let donor_signer = sr25519::Pair::from_seed(&[50u8; 32]); + let donor = crate::AccountId::from(donor_signer.public()); + let _ = + >::deposit_creating(&owner, 1_000_000_000_000_000_000); + let _ = + >::deposit_creating(&donor, 1_000_000_000_000_000_000); let steps = BoundedVec::try_from(vec![make_step(Task::Transfer { to: BOB, asset: AssetKind::Native, amount: AmountResolution::PercentageOfLastFunding(Perbill::one()), })]) .expect("execution plan fits"); - let actor_id = create_user(signer_account.clone(), manual_schedule(), None, steps); + let actor_id = create_user(owner.clone(), manual_schedule(), None, steps); + assert_ok!(update_actor_contract_partial( + RuntimeOrigin::signed(owner), + actor_id, + FundingSourcePolicy::AnyVerifiedIngress, + )); let sovereign = actor_account(actor_id); let sovereign_before = native_balance(&sovereign); let call = RuntimeCall::Balances(polkadot_sdk::pallet_balances::Call::transfer_all { @@ -6575,10 +7681,11 @@ fn signed_transfer_all_records_actual_post_fee_movement_without_event_scan() { keep_alive: true, }); - assert!(matches!( - Executive::apply_extrinsic(signed_extrinsic(&signer, 0, call)), - Ok(Ok(_)) - )); + let result = Executive::apply_extrinsic(signed_extrinsic(&donor_signer, 0, call)); + assert!( + matches!(result, Ok(Ok(_))), + "transfer_all result: {result:?}" + ); let actual = native_balance(&sovereign).saturating_sub(sovereign_before); assert!(actual > 0); assert_eq!( @@ -6769,6 +7876,7 @@ fn maximum_user_creation_and_replacement_pass_real_transaction_extensions() { create_extrinsic.clone(), System::block_hash(0), )); + ensure_current_resource_state(); assert_ok!(Executive::apply_extrinsic(create_extrinsic)); assert!(Actors::active_actor_state(actor_id).is_some()); @@ -6797,6 +7905,7 @@ fn maximum_user_creation_and_replacement_pass_real_transaction_extensions() { replacement_extrinsic.clone(), System::block_hash(0), )); + ensure_current_resource_state(); assert_ok!(Executive::apply_extrinsic(replacement_extrinsic)); assert_eq!( Actors::active_actor_state(actor_id) @@ -6809,7 +7918,122 @@ fn maximum_user_creation_and_replacement_pass_real_transaction_extensions() { } #[test] -fn on_initialize_is_a_zero_weight_noop() { +fn actor_resource_runtime_api_backing_projects_budget_current_state_and_finalized_telemetry() { + seeded_test_ext().execute_with(|| { + System::set_block_number(1); + ensure_actor_prepass_context(); + assert_ok!(Actors::actor_prepass(RuntimeOrigin::none())); + + let budget = crate::configs::BlockResourceBudgetValue::get(); + assert_eq!(budget, crate::configs::BlockResourceBudgetValue::get()); + let current = Actors::block_resource_state().expect("current resource state projects"); + assert_eq!(current.block_number(), 1); + assert_eq!( + current.phase(), + pallet_deos_actors::BlockResourcePhase::ExternalPhase + ); + + Actors::on_idle(1, Weight::MAX); + let finalized = + Actors::finalized_block_resource_telemetry().expect("finalized telemetry projects"); + assert_eq!(finalized.block_number(), 1); + }); +} + +#[test] +fn production_full_block_resource_harness_records_actor_and_user_contention() { + seeded_test_ext().execute_with(|| { + System::set_block_number(1); + let actor_id = create_system( + ALICE, + manual_schedule(), + None, + transfer_contract_steps(BOB, AssetKind::Native, 1), + ); + fund_native(actor_id, 100_000_000_000_000); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + + System::set_block_number(2); + ensure_actor_prepass_context(); + assert_ok!(Actors::actor_prepass(RuntimeOrigin::none())); + + let signer_pair = sr25519::Pair::from_seed(&[57u8; 32]); + let signer = AccountId::from(signer_pair.public()); + let _ = >::deposit_creating(&signer, 1_000_000_000_000_000); + let user_call = RuntimeCall::System(polkadot_sdk::frame_system::Call::remark { + remark: vec![1, 2, 3], + }); + let user_extrinsic = signed_extrinsic(&signer_pair, 0, user_call); + assert_ok!(Executive::apply_extrinsic(user_extrinsic)); + + let current = Actors::block_resource_state().expect("resource state remains authoritative"); + let usage = current.usage(); + let limits = crate::configs::BlockResourceBudgetValue::get().limits(); + assert_ne!(usage.actor_effect_used(), Weight::zero()); + assert_ne!(usage.user_dispatch_used(), Weight::zero()); + assert!(usage.actor_effect_used().all_lte(limits.actor_base_turn())); + assert!( + usage + .shared_used() + .expect("shared usage reconciles") + .all_lte(limits.shared_economic()) + ); + + Actors::on_idle(2, Weight::MAX); + let finalized = Actors::finalized_block_resource_telemetry() + .expect("mixed Actor/user block finalizes telemetry"); + let finalized_usage = finalized.usage(); + assert_eq!( + finalized_usage.actor_effect_used(), + usage.actor_effect_used() + ); + assert_eq!( + finalized_usage.user_dispatch_used(), + usage.user_dispatch_used() + ); + assert!( + usage + .actor_control_used() + .all_lte(finalized_usage.actor_control_used()) + ); + }); +} + +#[test] +fn actor_prepass_requires_timestamp_and_parachain_context_before_mutation() { + seeded_test_ext().execute_with(|| { + System::set_block_number(1); + polkadot_sdk::pallet_timestamp::Now::::kill(); + polkadot_sdk::cumulus_pallet_parachain_system::ValidationData::::kill(); + assert_noop!( + Actors::actor_prepass(RuntimeOrigin::none()), + pallet_deos_actors::Error::::PrepassContextIncomplete + ); + assert!(Actors::block_resource_state().is_none()); + + set_consensus_timestamp(1); + assert_noop!( + Actors::actor_prepass(RuntimeOrigin::none()), + pallet_deos_actors::Error::::PrepassContextIncomplete + ); + assert!(Actors::block_resource_state().is_none()); + + polkadot_sdk::cumulus_pallet_parachain_system::ValidationData::::put( + polkadot_sdk::cumulus_primitives_core::PersistedValidationData::default(), + ); + assert_ok!(Actors::actor_prepass(RuntimeOrigin::none())); + assert_eq!( + Actors::block_resource_state().map(|state| state.phase()), + Some(pallet_deos_actors::BlockResourcePhase::ExternalPhase) + ); + }); +} + +#[test] +fn on_initialize_freezes_cutoff_and_executes_mandatory_base_work() { seeded_test_ext().execute_with(|| { System::set_block_number(1); let amount = 1_000u128; @@ -6827,8 +8051,21 @@ fn on_initialize_is_a_zero_weight_noop() { )); System::set_block_number(2); assert_eq!(Actors::on_initialize(2), Weight::zero()); - assert_eq!(native_balance(&BOB), bob_before); - assert!(!has_actor_event(|event| { + ensure_actor_prepass_context(); + let consumed = Actors::actor_prepass(RuntimeOrigin::none()) + .expect("mandatory prepass succeeds") + .actual_weight + .expect("mandatory prepass reports actual Weight"); + assert!( + as WeightInfo>::scheduler_on_initialize_cutoff() + .all_lte(consumed) + ); + assert_eq!( + Actors::prepass_execution_cutoff(), + Some((2, Actors::next_queue_ticket())) + ); + assert_eq!(native_balance(&BOB), bob_before.saturating_add(amount)); + assert!(has_actor_event(|event| { matches!( event, Event::CycleStarted { @@ -6840,6 +8077,555 @@ fn on_initialize_is_a_zero_weight_noop() { }); } +#[test] +fn resource_meter_extension_uses_generated_production_owner() { + use polkadot_sdk::sp_runtime::traits::TransactionExtension; + + let extension = crate::configs::resource_meter::BlockResourceMeterExtension; + let call = RuntimeCall::System(polkadot_sdk::frame_system::Call::remark { remark: Vec::new() }); + assert_eq!( + extension.weight(&call), + as pallet_deos_actors::WeightInfo>::block_resource_meter_extension() + ); +} + +#[test] +fn actor_control_limit_admits_cutoff_and_one_rotating_materialization_family() { + let family_minimum = Actors::materialization_family_minimum(0) + .max(Actors::materialization_family_minimum(1)) + .max(Actors::materialization_family_minimum(2)); + let minimum_control = ActorControlInitializationWeight::get() + .saturating_add(::WeightInfo::scheduler_on_idle_base()) + .saturating_add( + ::WeightInfo::materialization_coordinator_base(), + ) + .saturating_add(family_minimum); + assert!( + minimum_control.all_lte(BlockResourceBudgetValue::get().limits().actor_control()), + "minimum Actor Control {minimum_control:?} rotating families={:?}/{:?}/{:?} must fit {:?}", + Actors::materialization_family_minimum(0), + Actors::materialization_family_minimum(1), + Actors::materialization_family_minimum(2), + BlockResourceBudgetValue::get().limits().actor_control() + ); +} + +#[test] +fn actor_cutoff_capture_is_owned_by_actor_control() { + assert_eq!( + ActorControlInitializationWeight::get(), + as pallet_deos_actors::WeightInfo>::scheduler_on_initialize_cutoff() + ); + assert_ne!(ActorControlInitializationWeight::get(), Weight::zero()); +} + +#[test] +fn session_rotation_hook_uses_the_generated_bounded_owner() { + seeded_test_ext().execute_with(|| { + let period = crate::configs::Period::get(); + System::set_block_number(period); + assert_eq!(Session::on_initialize(period), Weight::zero()); + let generated = as pallet_session_rotation::WeightInfo>::rotate_session(); + assert_eq!(SessionRotation::on_initialize(period), generated); + assert!(generated.all_lte(crate::configs::RuntimeBlockWeights::get().max_block)); + }); +} + +#[test] +fn message_queue_service_is_fixed_initialize_work_not_external_idle_work() { + assert!(MessageQueue::index() < Actors::index()); + assert_eq!( + <::ServiceWeight as Get< + Option, + >>::get(), + Some(MessageQueueServiceWeight::get()) + ); + assert_eq!( + <::IdleMaxServiceWeight as Get< + Option, + >>::get(), + None + ); + assert!(XcmpQueue::index() < Actors::index()); + assert_ne!(XcmpQueue::on_idle_weight(), Weight::zero()); +} + +#[test] +fn shared_parachain_inherent_exposes_complete_context_geometry() { + use polkadot_sdk::cumulus_pallet_parachain_system::parachain_inherent::{ + InboundDownwardMessages, InboundHrmpMessages, InboundMessagesData, + }; + + let downward = InboundDownwardMessages::new(vec![ + InboundDownwardMessage { + sent_at: 1, + msg: vec![1], + }, + InboundDownwardMessage { + sent_at: 2, + msg: vec![2], + }, + ]); + let mut downward_full_bytes = 1usize; + let downward = downward.into_abridged(&mut downward_full_bytes); + + let mut horizontal = BTreeMap::new(); + horizontal.insert( + ParaId::from(2_001u32), + vec![InboundHrmpMessage { + sent_at: 1, + data: vec![1], + }], + ); + horizontal.insert( + ParaId::from(2_002u32), + vec![InboundHrmpMessage { + sent_at: 2, + data: vec![2], + }], + ); + let mut horizontal_full_bytes = 1usize; + let horizontal = + InboundHrmpMessages::from_map(horizontal).into_abridged(&mut horizontal_full_bytes); + let data = InboundMessagesData::new(downward, horizontal); + + let (downward_full, downward_hashed) = data.downward_messages.messages(); + let (horizontal_full, horizontal_hashed) = data.horizontal_messages.messages(); + let channels = horizontal_full + .iter() + .map(|(sender, _)| *sender) + .chain(horizontal_hashed.iter().map(|(sender, _)| *sender)) + .collect::>(); + let geometry = ContextMessageGeometry::new( + u32::try_from(downward_full.len()).unwrap_or(u32::MAX), + u32::try_from(downward_hashed.len()).unwrap_or(u32::MAX), + u32::try_from(horizontal_full.len()).unwrap_or(u32::MAX), + u32::try_from(horizontal_hashed.len()).unwrap_or(u32::MAX), + u32::try_from(channels.len()).unwrap_or(u32::MAX), + ); + let limits = ContextMessageLimits::new(2, 2, 2); + assert_eq!(limits.validate(geometry), Ok(())); + assert_eq!( + crate::apis::validate_inbound_messages_geometry(&data), + Ok(()) + ); +} + +#[test] +fn runtime_context_validator_accepts_exact_dmp_and_rejects_full_mixed_and_channel_overflow() { + use polkadot_sdk::cumulus_pallet_parachain_system::parachain_inherent::{ + InboundDownwardMessages, InboundHrmpMessages, InboundMessagesData, + }; + + let make_downward = |count: u32, size_limit: &mut usize| { + InboundDownwardMessages::new( + (0..count) + .map(|sent_at| InboundDownwardMessage { + sent_at, + msg: vec![1], + }) + .collect(), + ) + .into_abridged(size_limit) + }; + let empty_horizontal = || { + let mut bytes = usize::MAX; + InboundHrmpMessages::from_map(BTreeMap::new()).into_abridged(&mut bytes) + }; + let mut exact_bytes = usize::MAX; + let exact = InboundMessagesData::new(make_downward(512, &mut exact_bytes), empty_horizontal()); + assert_eq!( + crate::apis::validate_inbound_messages_geometry(&exact), + Ok(()) + ); + + let mut full_bytes = usize::MAX; + let full_over = InboundMessagesData::new(make_downward(513, &mut full_bytes), empty_horizontal()); + assert_eq!( + crate::apis::validate_inbound_messages_geometry(&full_over), + Err(pallet_deos_actors::BlockResourceError::ContextGeometryExceeded) + ); + + let mut mixed_bytes = 1usize; + let mixed_over = + InboundMessagesData::new(make_downward(513, &mut mixed_bytes), empty_horizontal()); + assert_eq!( + crate::apis::validate_inbound_messages_geometry(&mixed_over), + Err(pallet_deos_actors::BlockResourceError::ContextGeometryExceeded) + ); + + let mut channels = BTreeMap::new(); + for sender in 1..=129u32 { + channels.insert( + ParaId::from(sender), + vec![InboundHrmpMessage { + sent_at: sender, + data: vec![1], + }], + ); + } + let mut channel_bytes = usize::MAX; + let mut downward_bytes = usize::MAX; + let channel_over = InboundMessagesData::new( + InboundDownwardMessages::new(Vec::new()).into_abridged(&mut downward_bytes), + InboundHrmpMessages::from_map(channels).into_abridged(&mut channel_bytes), + ); + assert_eq!( + crate::apis::validate_inbound_messages_geometry(&channel_over), + Err(pallet_deos_actors::BlockResourceError::ContextGeometryExceeded) + ); +} + +#[test] +fn context_provider_and_checker_agree_at_and_above_the_dmp_bound() { + use cumulus_primitives_parachain_inherent::{ + INHERENT_IDENTIFIER, MessageQueueChain, ParachainInherentData, + }; + use cumulus_test_relay_sproof_builder::RelayStateSproofBuilder; + use polkadot_sdk::{ + cumulus_primitives_core::PersistedValidationData, sp_inherents::InherentData, + }; + + seeded_test_ext().execute_with(|| { + for (count, expected) in [ + (512, Ok(())), + ( + 513, + Err(pallet_deos_actors::BlockResourceError::ContextGeometryExceeded), + ), + ] { + let downward_messages = (0..count) + .map(|_| InboundDownwardMessage { + sent_at: 1, + msg: vec![1], + }) + .collect::>(); + let mut queue = MessageQueueChain::default(); + for message in &downward_messages { + queue.extend_downward(message); + } + let mut proof_builder = RelayStateSproofBuilder::default(); + proof_builder.dmq_mqc_head = Some(queue.head()); + let (relay_parent_storage_root, relay_chain_state) = + proof_builder.into_state_root_and_proof(); + let mut data = InherentData::new(); + data + .put_data( + INHERENT_IDENTIFIER, + &ParachainInherentData { + validation_data: PersistedValidationData { + relay_parent_number: 1, + relay_parent_storage_root, + max_pov_size: 5_000_000, + ..Default::default() + }, + relay_chain_state, + downward_messages, + horizontal_messages: Default::default(), + relay_parent_descendants: Default::default(), + collator_peer_id: None, + }, + ) + .expect("bounded relay-proof fixture encodes"); // deos-bypass: panic-owner — fixed 512/513-message fixtures and upstream proof builder own deterministic encoding. + assert_eq!( + crate::apis::validate_context_inherent_geometry(&data), + expected + ); + } + }); +} + +#[test] +fn context_provider_and_checker_agree_at_and_above_the_hrmp_channel_bound() { + use cumulus_primitives_parachain_inherent::{ + INHERENT_IDENTIFIER, MessageQueueChain, ParachainInherentData, + }; + use cumulus_test_relay_sproof_builder::RelayStateSproofBuilder; + use polkadot_sdk::{ + cumulus_primitives_core::PersistedValidationData, sp_inherents::InherentData, + }; + + seeded_test_ext().execute_with(|| { + for (count, max_pov_size, expected) in [ + (128, 5_000_000, Ok(())), + ( + 129, + 5_000_000, + Err(pallet_deos_actors::BlockResourceError::ContextGeometryExceeded), + ), + (128, 1, Ok(())), + ( + 129, + 1, + Err(pallet_deos_actors::BlockResourceError::ContextGeometryExceeded), + ), + ] { + let mut horizontal_messages = BTreeMap::new(); + let mut proof_builder = RelayStateSproofBuilder::default(); + for raw_sender in 1..=count { + let sender = ParaId::from(raw_sender); + let message = InboundHrmpMessage { + sent_at: 1, + data: vec![1], + }; + horizontal_messages.insert(sender, vec![message.clone()]); + let channel = proof_builder.upsert_inbound_channel(sender); + channel.max_message_size = 1024; + channel.mqc_head = Some(MessageQueueChain::default().extend_hrmp(&message).head()); + } + let (relay_parent_storage_root, relay_chain_state) = + proof_builder.into_state_root_and_proof(); + let mut data = InherentData::new(); + data + .put_data( + INHERENT_IDENTIFIER, + &ParachainInherentData { + validation_data: PersistedValidationData { + relay_parent_number: 1, + relay_parent_storage_root, + max_pov_size, + ..Default::default() + }, + relay_chain_state, + downward_messages: Default::default(), + horizontal_messages, + relay_parent_descendants: Default::default(), + collator_peer_id: None, + }, + ) + .expect("bounded HRMP relay-proof fixture encodes"); // deos-bypass: panic-owner — fixed 128/129-channel fixtures and upstream proof builder own deterministic encoding. + assert_eq!( + crate::apis::validate_context_inherent_geometry(&data), + expected + ); + } + }); +} + +#[cfg(feature = "runtime-benchmarks")] +#[test] +#[ignore = "expensive maximum-context production evidence"] +fn maximum_context_static_fixture_dispatches() { + use codec::Encode; + use cumulus_primitives_parachain_inherent::MessageQueueChain; + use cumulus_test_relay_sproof_builder::RelayStateSproofBuilder; + use polkadot_sdk::{ + cumulus_pallet_parachain_system::parachain_inherent::{ + BasicParachainInherentData, InboundDownwardMessages, InboundHrmpMessages, InboundMessagesData, + }, + cumulus_primitives_core::{ + InboundDownwardMessage, InboundHrmpMessage, ParaId, PersistedValidationData, + relay_chain::HeadData, + }, + frame_system::pallet_prelude::HeaderFor, + sp_runtime::traits::Header as HeaderT, + }; + + seeded_test_ext().execute_with(|| { + let downward_messages = (0..MaxInboundDownwardMessagesPerContext::get()) + .map(|_| InboundDownwardMessage { + sent_at: 1, + msg: vec![0; 65_536], + }) + .collect::>(); + let mut downward_queue = MessageQueueChain::default(); + for message in &downward_messages { + downward_queue.extend_downward(message); + } + let mut horizontal_messages = BTreeMap::new(); + let mut proof_builder = RelayStateSproofBuilder::default(); + proof_builder.para_id = crate::ParachainInfo::parachain_id(); + proof_builder.dmq_mqc_head = Some(downward_queue.head()); + proof_builder.included_para_head = HeadData( + HeaderFor::::new( + 0, + Default::default(), + Default::default(), + Default::default(), + Default::default(), + ) + .encode(), + ) + .into(); + let channels = MaxInboundHorizontalChannelsPerContext::get(); + for message_index in 0..MaxInboundHorizontalMessagesPerContext::get() { + let sender = ParaId::from(message_index % channels + 1); + let message = InboundHrmpMessage { + sent_at: 1, + data: vec![0], + }; + horizontal_messages + .entry(sender) + .or_insert_with(Vec::new) + .push(message.clone()); + let channel = proof_builder.upsert_inbound_channel(sender); + channel.max_message_size = 65_536; + let mut queue = MessageQueueChain::new(channel.mqc_head.unwrap_or_default()); + queue.extend_hrmp(&message); + channel.mqc_head = Some(queue.head()); + } + let (relay_parent_storage_root, relay_chain_state) = proof_builder.into_state_root_and_proof(); + let basic = BasicParachainInherentData { + validation_data: PersistedValidationData { + relay_parent_number: 1, + relay_parent_storage_root, + max_pov_size: 12, + ..Default::default() + }, + relay_chain_state, + relay_parent_descendants: Vec::new(), + collator_peer_id: None, + }; + let collection_size_limit = (crate::configs::RuntimeBlockWeights::get() + .max_block + .proof_size() + / 6) as usize; + let mut size_limit = collection_size_limit; + let downward = InboundDownwardMessages::new(downward_messages).into_abridged(&mut size_limit); + size_limit = size_limit.saturating_add(collection_size_limit); + let horizontal = + InboundHrmpMessages::from_map(horizontal_messages).into_abridged(&mut size_limit); + let inbound = InboundMessagesData::new(downward, horizontal); + let expected_processed = inbound.downward_messages.messages().0.len() as u32; + assert_ok!(crate::ParachainSystem::set_validation_data( + polkadot_sdk::frame_system::RawOrigin::None.into(), + basic.clone(), + inbound, + )); + assert_eq!( + polkadot_sdk::cumulus_pallet_parachain_system::ProcessedDownwardMessages::::get(), + expected_processed + ); + }); +} + +#[test] +fn context_geometry_failure_is_fatal_and_has_one_owned_identifier() { + let result = crate::apis::context_geometry_fatal_result(); + assert!(!result.ok()); + assert!(result.fatal_error()); + assert!(matches!( + result.get_error::<()>(&crate::apis::CONTEXT_GEOMETRY_INHERENT_IDENTIFIER), + Ok(Some(())) + )); + assert_eq!(result.into_errors().count(), 1); +} + +#[test] +fn declared_context_geometry_covers_dmp_and_bounded_hrmp_dimensions() { + let limits = ContextMessageLimits::new( + MaxInboundDownwardMessagesPerContext::get(), + MaxInboundHorizontalMessagesPerContext::get(), + MaxInboundHorizontalChannelsPerContext::get(), + ); + assert_eq!(MaxInboundDownwardMessagesPerContext::get(), 512); + assert_eq!(MaxInboundHorizontalChannelsPerContext::get(), 128); + assert_eq!(MaxInboundHorizontalMessagesPerContext::get(), 131_072); + assert_eq!( + limits.validate(ContextMessageGeometry::new(256, 256, 65_536, 65_536, 128)), + Ok(()) + ); +} + +#[test] +fn configured_dmp_compatibility_reserve_equals_the_measured_owner() { + let reserve = ReservedDmpWeight::get(); + assert_eq!(MaxInboundDownwardMessagesPerContext::get(), 512); + let declared_maximum = as polkadot_sdk::cumulus_pallet_parachain_system::WeightInfo>::enqueue_inbound_downward_messages(512); + assert_eq!(DmpFixedWeight::get(), declared_maximum); + assert_eq!(reserve, declared_maximum); +} + +#[test] +fn fresh_genesis_has_no_active_xcm_lazy_migration() { + seeded_test_ext().execute_with(|| { + let key = + polkadot_sdk::frame_support::storage::storage_prefix(b"PolkadotXcm", b"CurrentMigration"); + assert!(polkadot_sdk::sp_io::storage::get(&key).is_none()); + }); +} + +#[test] +fn configured_inbound_owners_require_complete_fixed_classification() { + assert_eq!( + XcmMigrationMaximumWeight::get(), + crate::configs::RuntimeBlockWeights::get().max_block / 10 + ); + assert_eq!( + ::VERSION_DISCOVERY_QUEUE_SIZE, + 100 + ); + let downward = ReservedDmpWeight::get(); + let horizontal = ReservedXcmpWeight::get(); + assert_eq!(downward, DmpFixedWeight::get()); + assert_eq!( + horizontal, + MaximumContextMeasuredWeight::get().saturating_sub(DmpFixedWeight::get()) + ); + assert_eq!( + XcmpRegisteredFixedWeight::get(), + horizontal.saturating_add( + ::DbWeight::get().reads_writes(2, 3) + ) + ); + + let block_weights = crate::configs::RuntimeBlockWeights::get(); + let mandatory_base = block_weights.get(DispatchClass::Mandatory).base_extrinsic; + let three_mandatory_inherent_bases = mandatory_base + .checked_add(&mandatory_base) + .and_then(|weight| weight.checked_add(&mandatory_base)) + .expect("three Mandatory inherent bases must fit"); // deos-bypass: panic-owner — RuntimeBlockWeights construction and the three fixed inherents prove bounded addition. + let frame_base = block_weights + .base_block + .checked_add(&three_mandatory_inherent_bases) + .expect("configured FRAME base weights must fit"); // deos-bypass: panic-owner — RuntimeBlockWeights construction proves bounded addition. + let timestamp = as polkadot_sdk::pallet_timestamp::WeightInfo>::set() + .saturating_add( + as polkadot_sdk::pallet_timestamp::WeightInfo>::on_finalize(), + ); + let classified_candidate = frame_base + .checked_add(×tamp) + .and_then(|weight| weight.checked_add(&downward)) + .and_then(|weight| weight.checked_add(&XcmpRegisteredFixedWeight::get())) + .and_then(|weight| weight.checked_add(&MessageQueueServiceWeight::get())) + .and_then(|weight| weight.checked_add(&XcmpQueue::on_idle_weight())) + .and_then(|weight| weight.checked_add(&GovernanceFixedWeight::get())) + .and_then(|weight| weight.checked_add(&SessionRotationFixedWeight::get())) + .and_then(|weight| weight.checked_add(&AuthorshipFixedWeight::get())) + .and_then(|weight| weight.checked_add(&AuraFixedWeight::get())) + .and_then(|weight| weight.checked_add(&AuraExtFixedWeight::get())) + .and_then(|weight| weight.checked_add(&XcmVersionDiscoveryFixedWeight::get())); + assert!(classified_candidate.is_some()); + let classified_candidate = classified_candidate.unwrap_or(Weight::MAX); + assert_eq!(FixedBlockWeight::get(), classified_candidate); + assert_eq!( + FixedBlockWeightComponentsValue::get().total(), + Ok(classified_candidate) + ); + assert_eq!( + BlockResourceBudgetValue::get().fixed_envelope(), + classified_candidate + ); + assert!( + classified_candidate.all_lte(crate::MAXIMUM_BLOCK_WEIGHT), + "classified fixed owners must fit before validation loops and remaining hooks are added" + ); + assert!( + horizontal.all_lte(classified_candidate), + "known fixed owners cannot reduce the XCMP envelope" + ); + let registered_context_owner = downward.saturating_add(XcmpRegisteredFixedWeight::get()); + assert!( + MaximumContextMeasuredWeight::get().all_lte(registered_context_owner), + "registered DMP/XCMP owners must dominate complete measured context execution without additive double accounting" + ); + let unclassified_headroom = crate::MAXIMUM_BLOCK_WEIGHT.checked_sub(&classified_candidate); + assert!(unclassified_headroom.is_some()); + let unclassified_headroom = unclassified_headroom.unwrap_or(Weight::zero()); + assert!(unclassified_headroom.ref_time() > 0); + assert!(unclassified_headroom.proof_size() > 0); +} + #[test] fn block_weight_partition_is_50_dispatch_50_on_idle_without_operational_reserve() { let maximum = crate::MAXIMUM_BLOCK_WEIGHT; @@ -6913,7 +8699,7 @@ fn configured_on_idle_reserve_admits_one_scheduler_actor_probe() { } #[test] -fn starvation_emits_observability_event_once_on_threshold_crossing() { +fn mandatory_base_progress_prevents_false_idle_starvation() { seeded_test_ext().execute_with(|| { let threshold = <::MaxIdleStarvationBlocks as Get>::get(); @@ -6930,12 +8716,7 @@ fn starvation_emits_observability_event_once_on_threshold_crossing() { actor_id )); run_idle(starvation_blocked_budget(actor_id)); - assert!(matches!( - IdleStarvationState::::get(), - IdleStarvationPhase::Starving { - consecutive_blocks: 1, - } - )); + assert!(!IdleStarvationState::::exists()); for block in 2..=(threshold + 2) { System::set_block_number(block); run_idle(starvation_blocked_budget(actor_id)); @@ -6949,13 +8730,8 @@ fn starvation_emits_observability_event_once_on_threshold_crossing() { _ => None, }) .collect::>(); - assert_eq!(detections, vec![threshold]); - assert_eq!( - IdleStarvationState::::get(), - IdleStarvationPhase::Alerted { - consecutive_blocks: threshold + 2, - } - ); + assert!(detections.is_empty()); + assert!(!IdleStarvationState::::exists()); }); } @@ -6998,19 +8774,13 @@ fn starvation_recovery_is_observable_and_healthy_idle_stays_sparse() { System::set_block_number(block); run_idle(starvation_blocked_budget(actor_id)); } - assert_eq!( - IdleStarvationState::::get(), - IdleStarvationPhase::Alerted { - consecutive_blocks: threshold, - } - ); + assert!(!IdleStarvationState::::exists()); System::set_block_number(threshold.saturating_add(2)); run_idle(Weight::MAX); assert!(!IdleStarvationState::::exists()); - assert!(has_actor_event(|event| matches!( + assert!(!has_actor_event(|event| matches!( event, - Event::IdleStarvationRecovered { consecutive_blocks } - if *consecutive_blocks == threshold + Event::IdleStarvationRecovered { .. } ))); let recovery_count = System::events() .into_iter() @@ -7180,9 +8950,7 @@ fn user_dca_e2e_lifecycle_with_explicit_close() { )); assert_eq!( Balances::free_balance(&ALICE), - initial_alice_balance - - create_fee - - crate::configs::actor_config::ActorCadencedTriggerBond::get() + initial_alice_balance - create_fee - actor_state_hold_total(id) ); let sov = Actors::sovereign_account_id(&ALICE, 0); let min_user_balance = ::MinUserBalance::get(); @@ -7476,7 +9244,9 @@ fn run_blocks_with_queue_diagnostics( queue_diag.max_queue_occupancy = queue_diag.max_queue_occupancy.max(queue_occupancy); queue_diag.max_wakeup_backlog = queue_diag.max_wakeup_backlog.max(wakeup_backlog); queue_diag.max_wakeup_buckets = queue_diag.max_wakeup_buckets.max(wakeup_buckets); - diag.min_per_block = diag.min_per_block.min(block_executions); + if block > 2 { + diag.min_per_block = diag.min_per_block.min(block_executions); + } diag.max_per_block = diag.max_per_block.max(block_executions); } (diag, queue_diag) @@ -7539,21 +9309,21 @@ fn circular_chain_under_capacity_preserves_progress_and_fairness() { let count = diag.actor_cycle_counts[&id]; assert!(count > 0, "Actor {id} never received service"); } - assert!(diag.min_per_block > 0, "Every block must make progress"); let total_executions: u32 = diag.actor_cycle_counts.values().sum(); assert!( u64::from(total_executions) >= chain_len, "The run must complete at least one full traversal", ); - // Fairness remains bounded despite measured Weight limiting each pass. + // Independent cadence detection remains bounded but may phase-shift Actors when due + // materialization spans blocks; FIFO still orders every readiness that reaches service. let nonces: alloc::vec::Vec = actor_ids .iter() .filter_map(|&id| Actors::active_actor_state(id).map(|state| state.identity.cycle_nonce)) .collect(); let (min_n, max_n) = (*nonces.iter().min().unwrap(), *nonces.iter().max().unwrap()); assert!( - max_n.saturating_sub(min_n) <= 1, - "Fairness: cycle_nonce spread exceeds 1 (min={min_n}, max={max_n})", + max_n.saturating_sub(min_n) <= 10, + "Cadenced detection/service spread exceeds 10 (min={min_n}, max={max_n})", ); assert!(min_n > 0, "Every actor must complete at least one cycle"); assert_core_stability(&actor_ids, &diag); @@ -7672,10 +9442,6 @@ fn circular_chain_respects_execution_ceiling_and_remains_fair() { "Per-block throughput must not exceed MaxExecutionsPerBlock={execution_ceiling} (got {})", diag.max_per_block, ); - assert!( - diag.min_per_block > 0, - "Every block must execute at least some actors", - ); // No starvation: every chain actor must have executed multiple times let min_count = *diag.actor_cycle_counts.values().min().unwrap(); let zero_actors: alloc::vec::Vec = diag @@ -7723,7 +9489,13 @@ fn clear_genesis_system_actors_for_stress_fixture() { let actors: alloc::vec::Vec<_> = pallet_deos_actors::ActorHot::::iter().collect(); for (actor_id, _hot) in actors { pallet_deos_actors::ActorHot::::remove(actor_id); - pallet_deos_actors::ActorContracts::::remove(actor_id); + pallet_deos_actors::ActorContractHeads::::remove(actor_id); + pallet_deos_actors::ActorAdmissionCertificates::::remove(actor_id); + let _ = pallet_deos_actors::ActorContractTailChunks::::clear_prefix( + actor_id, + u32::MAX, + None, + ); pallet_deos_actors::ActorFunding::::remove(actor_id); let identity = Actors::actor_identities(actor_id).expect("actor identity exists"); pallet_deos_actors::SovereignIndex::::remove(&identity.sovereign_account); @@ -7856,16 +9628,16 @@ fn scheduler_fast_fifo_dense_vs_sparse_topology_smoke() { blocks, ); assert!( - dense_max.saturating_sub(dense_min) <= 3, - "Dense fairness spread exceeded bound=3 (actors={}, blocks={}, min={}, max={})", + dense_max.saturating_sub(dense_min) <= 8, + "Dense cadence/service spread exceeded bound=8 (actors={}, blocks={}, min={}, max={})", actors, blocks, dense_min, dense_max, ); assert!( - sparse_max.saturating_sub(sparse_min) <= 3, - "Sparse fairness spread exceeded bound=3 (actors={}, blocks={}, min={}, max={})", + sparse_max.saturating_sub(sparse_min) <= 8, + "Sparse cadence/service spread exceeded bound=8 (actors={}, blocks={}, min={}, max={})", actors, blocks, sparse_min, @@ -7970,11 +9742,8 @@ fn reference_idle_budget_converges_paged_wakeup_and_pure_close_pressure() { Actors::on_initialize(block); run_idle(budget); let retries_done = retry_ids.iter().all(|id| { - Actors::actor_hot(*id).is_some_and(|hot| { - hot - .wakeup_pointer - .is_none_or(|pointer| matches!(pointer.block, WakeupKey::Tick(_))) - }) + Actors::actor_hot(*id) + .is_some_and(|hot| hot.wakeup_pointer.is_none() && hot.trigger_wakeup_pointer.is_some()) }); let closes_done = expired_ids .iter() @@ -7989,11 +9758,8 @@ fn reference_idle_budget_converges_paged_wakeup_and_pure_close_pressure() { assert!( retry_ids.iter().all(|id| { - Actors::actor_hot(*id).is_some_and(|hot| { - hot - .wakeup_pointer - .is_none_or(|pointer| matches!(pointer.block, WakeupKey::Tick(_))) - }) + Actors::actor_hot(*id) + .is_some_and(|hot| hot.wakeup_pointer.is_none() && hot.trigger_wakeup_pointer.is_some()) }), "overdue block wakeups must converge back to cadence ownership" ); @@ -8028,7 +9794,7 @@ fn reference_idle_budget_converges_paged_wakeup_and_pure_close_pressure() { #[ignore] // Heavy: run in the scheduled nightly FIFO stress job (release mode) fn scheduler_stress_fifo_over_capacity_fairness_matrix() { use super::common::new_test_ext; - let scenarios: [(u64, u32); 4] = [(48, 96), (100, 150), (1000, 252), (10_000, 418)]; + let scenarios: [(u64, u32); 4] = [(48, 96), (100, 150), (1000, 252), (10_000, 1_300)]; for (actors, blocks) in scenarios { new_test_ext().execute_with(|| { let _ = run_fairness_matrix_case(actors, blocks); @@ -8040,7 +9806,7 @@ fn scheduler_stress_fifo_over_capacity_fairness_matrix() { #[ignore] // Heavy topology matrix, run in the scheduled nightly FIFO stress job fn scheduler_stress_fifo_dense_vs_sparse_topology_matrix() { use super::common::new_test_ext; - let scenarios: [(u64, u32, u64); 3] = [(100, 200, 8), (1000, 300, 16), (5000, 420, 32)]; + let scenarios: [(u64, u32, u64); 3] = [(100, 200, 8), (1000, 300, 16), (5000, 700, 32)]; for (actors, blocks, stride) in scenarios { let dense_diag = new_test_ext().execute_with(|| { System::set_block_number(1); @@ -8072,16 +9838,16 @@ fn scheduler_stress_fifo_dense_vs_sparse_topology_matrix() { blocks, ); assert!( - dense_max.saturating_sub(dense_min) <= 3, - "Dense fairness spread exceeded bound=3 (actors={}, blocks={}, min={}, max={})", + dense_max.saturating_sub(dense_min) <= 8, + "Dense cadence/service spread exceeded bound=8 (actors={}, blocks={}, min={}, max={})", actors, blocks, dense_min, dense_max, ); assert!( - sparse_max.saturating_sub(sparse_min) <= 3, - "Sparse fairness spread exceeded bound=3 (actors={}, blocks={}, min={}, max={})", + sparse_max.saturating_sub(sparse_min) <= 8, + "Sparse cadence/service spread exceeded bound=8 (actors={}, blocks={}, min={}, max={})", actors, blocks, sparse_min, @@ -8196,8 +9962,8 @@ fn scheduler_512_mixed_clocks_survives_delayed_timestamp() { assert!(tick_ids.iter().all(|actor_id| { Actors::actor_hot(*actor_id).is_some_and(|hot| { hot - .wakeup_pointer - .is_some_and(|pointer| matches!(pointer.block, WakeupKey::Tick(2))) + .trigger_wakeup_pointer + .is_some_and(|pointer| pointer.tick == 2) }) })); @@ -8270,10 +10036,17 @@ fn scheduler_512_mixed_clocks_survives_delayed_timestamp() { tick_last.abs_diff(block_last) <= 1, "mixed clocks must finish service within one block: tick={tick_last}, block={block_last}", ); - for actor_id in tick_ids.iter().chain(block_ids.iter()) { - let hot = Actors::actor_hot(*actor_id).expect("stress actor remains active"); + for actor_id in &tick_ids { + let hot = Actors::actor_hot(*actor_id).expect("Cadenced stress actor remains active"); + assert!(hot.queue_ticket.is_none()); + assert!(hot.wakeup_pointer.is_none()); + assert!(hot.trigger_wakeup_pointer.is_some()); + } + for actor_id in &block_ids { + let hot = Actors::actor_hot(*actor_id).expect("block-clock stress actor remains active"); assert!(hot.queue_ticket.is_none()); assert!(hot.wakeup_pointer.is_some()); + assert!(hot.trigger_wakeup_pointer.is_none()); } }); } @@ -8286,7 +10059,7 @@ fn profile_scheduler_queue_wakeup_occupancy_10k() { System::set_block_number(1); close_genesis_system_actors(); let actors = 10_000u64; - let blocks = 418u32; + let blocks = 1_300u32; let actor_ids = setup_inert_actors(actors, 10_000u128); let (diag, queue_diag) = run_blocks_with_queue_diagnostics(&actor_ids, blocks, Weight::MAX); let min_count = *diag.actor_cycle_counts.values().min().unwrap_or(&0); @@ -8320,7 +10093,7 @@ fn profile_scheduler_queue_wakeup_occupancy_10k() { fn profile_scheduler_wallclock_matrix() { use super::common::new_test_ext; use std::time::Instant; - let scenarios: [(u64, u32); 4] = [(48, 96), (100, 150), (1000, 252), (10_000, 418)]; + let scenarios: [(u64, u32); 4] = [(48, 96), (100, 150), (1000, 252), (10_000, 1_300)]; for (actors, blocks) in scenarios { new_test_ext().execute_with(|| { let started = Instant::now(); @@ -8390,7 +10163,11 @@ fn genesis_sparse_id_space_executes_only_active_actors() { System::reset_events(); Actors::on_initialize(block); run_idle(Weight::from_parts(u64::MAX, u64::MAX)); - let executed_block_2: alloc::vec::Vec<_> = System::events() + System::set_block_number(3); + System::reset_events(); + Actors::on_initialize(3); + run_idle(Weight::from_parts(u64::MAX, u64::MAX)); + let mut executed_block_2: alloc::vec::Vec<_> = System::events() .iter() .filter_map(|evt| { if let RuntimeEvent::Actors(Event::CycleSummary { actor_id, .. }) = &evt.event { @@ -8400,10 +10177,11 @@ fn genesis_sparse_id_space_executes_only_active_actors() { } }) .collect(); + executed_block_2.dedup(); for &id in &all_ids { assert!( executed_block_2.contains(&id), - "Actors {} must execute in first block despite sparse ID gaps \ + "Actors {} must execute in the first eligible block despite sparse ID gaps \ (total_actors={}, id_space=0..{}, executed={:?})", id, all_ids.len(), @@ -8421,7 +10199,8 @@ fn genesis_sparse_id_space_executes_only_active_actors() { System::reset_events(); Actors::on_initialize(block); run_idle(Weight::from_parts(u64::MAX, u64::MAX)); - let executed_block_13: alloc::vec::Vec<_> = System::events() + run_next_idle(Weight::from_parts(u64::MAX, u64::MAX)); + let mut executed_block_13: alloc::vec::Vec<_> = System::events() .iter() .filter_map(|evt| { if let RuntimeEvent::Actors(Event::CycleSummary { actor_id, .. }) = &evt.event { @@ -8431,6 +10210,7 @@ fn genesis_sparse_id_space_executes_only_active_actors() { } }) .collect(); + executed_block_13.dedup(); assert_eq!(executed_block_13, all_ids); }); } @@ -8500,12 +10280,16 @@ fn execution_order_lower_id_executes_before_higher_id() { (contract_steps_b, CompletionPolicy::Persistent,) )); let charlie_before = Balances::free_balance(CHARLIE); - // Run one block + // Materialize in block 2 and execute in the first eligible block. let block = 2u32; System::set_block_number(block); System::reset_events(); Actors::on_initialize(block); run_idle(Weight::from_parts(u64::MAX, u64::MAX)); + System::set_block_number(3); + System::reset_events(); + Actors::on_initialize(3); + run_idle(Weight::from_parts(u64::MAX, u64::MAX)); // If A executed before B: A transferred 10% to B, then B has initial + A's transfer, // and B transfers 10% of that total to CHARLIE. // If B executed before A: B transfers 10% of initial only, then A transfers to B. @@ -8591,13 +10375,308 @@ fn runtime_simulation_core_rolls_back_deos_adapter_effects() { }); } +#[test] +#[ignore = "10,000 effectful Actor production profile; run through actors-assurance"] +fn transfer_10k_manual_and_reactive_first_traversal() { + use super::common::new_test_ext; + new_test_ext().execute_with(|| { + System::set_block_number(1); + close_genesis_system_actors(); + let actor_count = ::MaxActiveActors::get(); + let initial_balance = 1_000u128.saturating_mul(crate::EXISTENTIAL_DEPOSIT); + let mut actor_ids = Vec::with_capacity(actor_count as usize); + for index in 0..actor_count { + let trigger = if index % 2 == 0 { + Trigger::manual() + } else { + Trigger::cadenced(1) + }; + let actor_id = create_system( + ALICE, + RuntimeSchedule { + trigger, + cooldown_blocks: 0, + }, + None, + transfer_contract_steps(BOB, AssetKind::Native, 1), + ); + fund_native(actor_id, initial_balance); + if index % 2 == 0 { + assert_ok!(Actors::manual_trigger(RuntimeOrigin::root(), actor_id)); + } + actor_ids.push(actor_id); + } + assert_eq!( + pallet_deos_actors::ActiveActorCount::::get(), + actor_count + ); + + let mut progressed = alloc::collections::BTreeSet::new(); + let mut completion_block = None; + for block in 2..=1_301u32 { + System::set_block_number(block); + System::reset_events(); + System::set_block_consumed_resources(Weight::zero(), 0); + set_consensus_timestamp( + u64::from(block).saturating_mul(primitives::ecosystem::params::ACTOR_CADENCE_TICK_MILLIS), + ); + assert_eq!(Actors::on_initialize(block), Weight::zero()); + ensure_current_resource_state(); + Actors::on_idle(block, Weight::MAX); + for event in System::events() { + if let RuntimeEvent::Actors(Event::CycleSummary { + actor_id, outcomes, .. + }) = event.event + { + assert_eq!(outcomes.failed_steps, 0); + progressed.insert(actor_id); + } + } + if progressed.len() == actor_ids.len() { + completion_block = Some(block); + break; + } + } + assert_eq!(progressed.len(), actor_ids.len()); + let limits = crate::configs::BlockResourceBudgetValue::get().limits(); + println!( + "CONTROL_SWEEP_W1 control={:?} shared={:?} actor_base={:?} user_base={:?} completion_block={:?}", + limits.actor_control(), + limits.shared_economic(), + limits.actor_base_turn(), + limits.user_base_turn(), + completion_block, + ); + }); +} + +#[test] +#[ignore = "10,000 effectful Actors with continuous user demand; run through actors-assurance"] +fn transfer_10k_first_traversal_with_continuous_user_dispatch() { + use super::common::new_test_ext; + new_test_ext().execute_with(|| { + System::set_block_number(1); + close_genesis_system_actors(); + let actor_count = ::MaxActiveActors::get(); + let initial_balance = 1_000u128.saturating_mul(crate::EXISTENTIAL_DEPOSIT); + let mut actor_ids = Vec::with_capacity(actor_count as usize); + for index in 0..actor_count { + let actor_id = create_system( + ALICE, + RuntimeSchedule { + trigger: if index % 2 == 0 { + Trigger::manual() + } else { + Trigger::cadenced(1) + }, + cooldown_blocks: 0, + }, + None, + transfer_contract_steps(BOB, AssetKind::Native, 1), + ); + fund_native(actor_id, initial_balance); + if index % 2 == 0 { + assert_ok!(Actors::manual_trigger(RuntimeOrigin::root(), actor_id)); + } + actor_ids.push(actor_id); + } + + let signer_pair = sr25519::Pair::from_seed(&[58u8; 32]); + let signer = AccountId::from(signer_pair.public()); + let _ = >::deposit_creating(&signer, 1_000_000_000_000_000_000); + let mut progressed = alloc::collections::BTreeSet::new(); + let mut completion_block = None; + for block in 2..=1_301u32 { + System::set_block_number(block); + System::reset_events(); + System::set_block_consumed_resources(Weight::zero(), 0); + set_consensus_timestamp( + u64::from(block).saturating_mul(primitives::ecosystem::params::ACTOR_CADENCE_TICK_MILLIS), + ); + assert_eq!(Actors::on_initialize(block), Weight::zero()); + ensure_current_resource_state(); + let call = RuntimeCall::System(polkadot_sdk::frame_system::Call::remark { + remark: block.to_le_bytes().to_vec(), + }); + let extrinsic = signed_extrinsic(&signer_pair, crate::Nonce::from(block - 2), call); + let before_user_state = Actors::block_resource_state().expect("prepass state exists"); + let before_user_phase = before_user_state.phase(); + let before_user_outstanding = before_user_state.outstanding_reservations(); + let before_user_halted = before_user_state.optional_actor_work_halted(); + let before_user = before_user_state.usage(); + let declared = extrinsic.get_dispatch_info().total_weight(); + let applied = Executive::apply_extrinsic(extrinsic); + assert!( + applied.is_ok(), + "user dispatch rejected in block {block}: phase={before_user_phase:?}, outstanding={before_user_outstanding}, halted={before_user_halted}, before={before_user:?}, declared={declared:?}, user_base={:?}, result={applied:?}", + crate::configs::BlockResourceBudgetValue::get().limits().user_base_turn(), + ); + let after_user = Actors::block_resource_state().expect("mixed block state exists"); + assert_ne!(after_user.usage().user_dispatch_used(), Weight::zero()); + Actors::on_idle(block, Weight::MAX); + for event in System::events() { + if let RuntimeEvent::Actors(Event::CycleSummary { + actor_id, outcomes, .. + }) = event.event + { + assert_eq!(outcomes.failed_steps, 0); + progressed.insert(actor_id); + } + } + if progressed.len() == actor_ids.len() { + completion_block = Some(block); + break; + } + } + let missing_manual = actor_ids + .iter() + .step_by(2) + .filter(|actor_id| !progressed.contains(actor_id)) + .count(); + let missing_reactive = actor_ids + .iter() + .skip(1) + .step_by(2) + .filter(|actor_id| !progressed.contains(actor_id)) + .count(); + assert_eq!( + progressed.len(), + actor_ids.len(), + "missing Manual={missing_manual}, reactive={missing_reactive}" + ); + let limits = crate::configs::BlockResourceBudgetValue::get().limits(); + println!( + "CONTROL_SWEEP control={:?} shared={:?} actor_base={:?} user_base={:?} completion_block={:?}", + limits.actor_control(), + limits.shared_economic(), + limits.actor_base_turn(), + limits.user_base_turn(), + completion_block, + ); + }); +} + +#[test] +#[ignore = "mixed 10,000-Actor production throughput profile"] +fn mixed_9500_transfer_400_swapout_100_control_first_traversal() { + seeded_test_ext().execute_with(|| { + System::set_block_number(1); + close_genesis_system_actors(); + let dormant = pallet_deos_actors::ActorIdentities::::iter() + .filter(|(actor_id, _)| !pallet_deos_actors::ActorHot::::contains_key(actor_id)) + .collect::>(); + for (actor_id, identity) in &dormant { + pallet_deos_actors::ActorIdentities::::remove(actor_id); + pallet_deos_actors::SovereignIndex::::remove(&identity.sovereign_account); + } + pallet_deos_actors::ActorIdentityCount::::mutate(|count| { + *count = count.saturating_sub(dormant.len() as u32); + }); + assert_ok!(super::common::setup_deos_router_infrastructure()); + let actor_count = ::MaxActiveActors::get(); + let initial_balance = 10_000u128.saturating_mul(crate::EXISTENTIAL_DEPOSIT); + let swap_steps = BoundedVec::try_from(vec![make_step(Task::SwapOut { + asset_out: AssetKind::Local(ASSET_A), + amount_out: AmountResolution::Fixed(crate::EXISTENTIAL_DEPOSIT), + asset_in: AssetKind::Native, + input_limit: InputLimit::Absolute(initial_balance), + slippage_tolerance: Perbill::from_percent(5), + })]) + .expect("one SwapOut Step fits"); + let control_steps = BoundedVec::try_from(vec![make_step(Task::StopCycle)]) + .expect("one control Step fits"); + let mut actor_ids = Vec::with_capacity(actor_count as usize); + for index in 0..actor_count { + let steps = if index < 9_500 { + transfer_contract_steps(BOB, AssetKind::Native, 1) + } else if index < 9_900 { + swap_steps.clone() + } else { + control_steps.clone() + }; + let actor_id = create_system( + ALICE, + RuntimeSchedule { + trigger: if index % 2 == 0 { + Trigger::manual() + } else { + Trigger::cadenced(1) + }, + cooldown_blocks: 0, + }, + None, + steps, + ); + fund_native(actor_id, initial_balance); + if index % 2 == 0 { + assert_ok!(Actors::manual_trigger(RuntimeOrigin::root(), actor_id)); + } + actor_ids.push(actor_id); + } + + let signer_pair = sr25519::Pair::from_seed(&[59u8; 32]); + let signer = AccountId::from(signer_pair.public()); + let _ = >::deposit_creating( + &signer, + 1_000_000_000_000_000_000, + ); + let mut progressed = alloc::collections::BTreeSet::new(); + let mut failed_steps = 0u32; + let mut completion_block = None; + for block in 2..=1_301u32 { + System::set_block_number(block); + System::reset_events(); + System::set_block_consumed_resources(Weight::zero(), 0); + set_consensus_timestamp( + u64::from(block).saturating_mul(primitives::ecosystem::params::ACTOR_CADENCE_TICK_MILLIS), + ); + assert_eq!(Actors::on_initialize(block), Weight::zero()); + ensure_current_resource_state(); + let call = RuntimeCall::System(polkadot_sdk::frame_system::Call::remark { + remark: block.to_le_bytes().to_vec(), + }); + let extrinsic = signed_extrinsic(&signer_pair, crate::Nonce::from(block - 2), call); + assert!(Executive::apply_extrinsic(extrinsic).is_ok()); + Actors::on_idle(block, Weight::MAX); + for event in System::events() { + if let RuntimeEvent::Actors(Event::CycleSummary { + actor_id, outcomes, .. + }) = event.event + { + failed_steps = failed_steps.saturating_add(outcomes.failed_steps); + progressed.insert(actor_id); + } + } + if progressed.len() == actor_ids.len() { + completion_block = Some(block); + break; + } + } + assert_eq!(progressed.len(), actor_ids.len()); + assert!( + completion_block.is_some_and(|block| block <= 1_301), + "all 10,000 Actors must complete first traversal within the measured horizon" + ); + assert_eq!(failed_steps, 0, "mixed traversal must lose no Step effect"); + let limits = crate::configs::BlockResourceBudgetValue::get().limits(); + println!( + "CONTROL_SWEEP_W3 control={:?} shared={:?} actor_base={:?} user_base={:?} completion_block={:?} failed_steps={failed_steps}", + limits.actor_control(), + limits.shared_economic(), + limits.actor_base_turn(), + limits.user_base_turn(), + completion_block, + ); + }); +} + #[test] #[ignore] // ~30s wall-clock; run manually: cargo test --release stress_10k_actors_queue_scheduler -- --ignored fn stress_10k_actors_queue_scheduler() { use super::common::new_test_ext; new_test_ext().execute_with(|| { System::set_block_number(1); - let num_blocks = 1_000u32; + let num_blocks = 1_300u32; let initial_balance: u128 = 1_000 * crate::EXISTENTIAL_DEPOSIT; let max_active = ::MaxActiveActors::get() as u64; // Retain paused active genesis actors to validate mixed ready/non-ready fairness. @@ -8818,9 +10897,10 @@ fn fee_collector_charge_leaves_the_single_cadence_placement_unchanged() { let hot = Actors::actor_hot(fee_sink_id).expect("Fee Sink hot state"); assert!(!hot.pending_signal); assert!(hot.queue_ticket.is_none()); + assert!(hot.wakeup_pointer.is_none()); assert!( - hot.wakeup_pointer.is_some(), - "cadence remains the sole placement" + hot.trigger_wakeup_pointer.is_some(), + "cadence remains the sole Trigger placement" ); }); } @@ -8846,7 +10926,93 @@ fn fee_collector_noop_zero_emits_no_ingress() { let hot = Actors::actor_hot(fee_sink_id).expect("Fee Sink remains active"); assert!(!hot.pending_signal); assert!(hot.queue_ticket.is_none()); - assert!(hot.wakeup_pointer.is_some(), "cadence remains armed"); + assert!(hot.wakeup_pointer.is_none()); + assert!( + hot.trigger_wakeup_pointer.is_some(), + "cadence remains armed" + ); + }); +} + +#[test] +fn user_actor_state_uses_the_dedicated_runtime_hold_reason_and_releases_exactly() { + seeded_test_ext().execute_with(|| { + System::set_block_number(1); + let actor_id = create_user( + ALICE, + manual_schedule(), + None, + transfer_contract_steps(BOB, AssetKind::Native, 1), + ); + let record = Actors::actor_state_hold(actor_id).expect("User state hold exists"); + let expected = record + .breakdown + .identity + .saturating_add(record.breakdown.contract_head) + .saturating_add(record.breakdown.contract_body) + .saturating_add(record.breakdown.detector) + .saturating_add(record.breakdown.funding) + .saturating_add(record.breakdown.run); + let reason = RuntimeHoldReason::Actors(pallet_deos_actors::HoldReason::ActorState); + assert_eq!(Balances::balance_on_hold(&reason, &ALICE), expected); + + System::set_block_number(2); + assert_ok!(Actors::close_actor(RuntimeOrigin::signed(ALICE), actor_id)); + assert!(Actors::actor_state_hold(actor_id).is_none()); + assert_eq!(Balances::balance_on_hold(&reason, &ALICE), 0); + }); +} + +#[test] +fn actor_cost_runtime_api_exposes_named_deos_fee_and_hold_provenance() { + seeded_test_ext().execute_with(|| { + System::set_block_number(1); + let actor_id = create_user( + ALICE, + manual_schedule(), + None, + transfer_contract_steps(BOB, AssetKind::Native, 1), + ); + let quote = Actors::actor_cost_quote(actor_id).expect("runtime cost quote exists"); + assert_eq!(quote.actor_type, pallet_deos_actors::ActorType::User); + assert_eq!( + quote.creation_fee, + ::ActorCreationFee::get() + ); + let trigger = quote + .prospective_trigger_fee + .expect("Manual Trigger quote exists"); + assert_eq!( + trigger.trigger_family, + pallet_deos_actors::TriggerFamily::Manual + ); + assert!(trigger.maximum_weight.ref_time() > 0); + assert!(trigger.fee > 0); + let pipeline = quote + .prospective_pipeline_fee + .expect("Pipeline quote exists"); + assert_eq!( + pipeline.strategy, + pallet_deos_actors::PipelineMachineFeeStrategy::UpfrontBounded + ); + assert_eq!( + pipeline.total_fee, + pipeline.pipeline_machine_fee + pipeline.cleanup_fee + ); + assert!(quote.maximum_next_action_fee.maximum_effect_fee > 0); + assert!(!quote.actor_state_hold.exempt); + assert_eq!( + quote.actor_state_hold.base_per_component, + ::ActorStateHoldBase::get() + ); + assert_eq!( + quote.actor_state_hold.per_encoded_byte, + ::ActorStateHoldPerByte::get() + ); + assert_eq!( + quote.actor_state_hold.total, + actor_state_hold_total(actor_id) + ); }); } diff --git a/template/runtime/src/tests/asset_conversion_integration_tests.rs b/template/runtime/src/tests/asset_conversion_integration_tests.rs index 08a427f9..a57e78cb 100644 --- a/template/runtime/src/tests/asset_conversion_integration_tests.rs +++ b/template/runtime/src/tests/asset_conversion_integration_tests.rs @@ -18,7 +18,10 @@ use crate::{ System, configs::AssetKind, }; use alloc::{boxed::Box, vec::Vec}; -use polkadot_sdk::frame_support::{assert_ok, dispatch::DispatchResult}; +use polkadot_sdk::{ + frame_support::{assert_ok, dispatch::DispatchResult, traits::fungibles::Inspect}, + pallet_asset_conversion::PoolLocator, +}; /// Helper to swap exact tokens for tokens along a specified path. fn swap_exact_tokens_for_tokens( @@ -67,7 +70,7 @@ fn test_pool_creation_success() { assert_ok!(create_pool( RuntimeOrigin::signed(admin.clone()), AssetKind::Native, - AssetKind::Local(asset_id) + AssetKind::Foreign(asset_id) )); // Verify that AssetConversion events were emitted (simplified check) assert!( @@ -210,18 +213,24 @@ fn test_token_swap_success() { )); // Mint swap tokens to trader assert_ok!(mint_tokens(asset_id, &admin, &trader, swap_amount)); - // Perform swap + // Quote and execution consume the same full physical reserve source. + let quoted = AssetConversion::quote_price_exact_tokens_for_tokens( + local_asset, + native_asset, + swap_amount, + true, + ) + .expect("seeded full-balance reserves are quotable"); let initial_native_balance = Balances::free_balance(&trader); assert_ok!(swap_exact_tokens_for_tokens( RuntimeOrigin::signed(trader.clone()), vec![local_asset, native_asset], swap_amount, - 1, + quoted, trader.clone(), )); - // Verify swap was successful let final_native_balance = Balances::free_balance(&trader); - assert!(final_native_balance > initial_native_balance); + assert_eq!(final_native_balance - initial_native_balance, quoted); // Verify that AssetConversion events were emitted (simplified check) assert!( System::events() @@ -363,17 +372,43 @@ fn test_liquidity_removal_success() { 1, &lp )); - // Remove some liquidity + // Removal math uses the same full reserves reported by the runtime API. + let burn = safe_liquidity_amount / 4; + let pool_id = + ::PoolLocator::pool_id( + &native_asset, + &local_asset, + ) + .expect("canonical pool identity"); + let pool = polkadot_sdk::pallet_asset_conversion::Pools::::get(pool_id) + .expect("pool exists"); + let (native_reserve, asset_reserve) = + AssetConversion::get_reserves(native_asset, local_asset).expect("full reserves exist"); + let total_lp = Assets::total_issuance(pool.lp_token); + let expected_native = burn + .checked_mul(native_reserve) + .expect("fixture product fits") + / total_lp; + let expected_asset = burn + .checked_mul(asset_reserve) + .expect("fixture product fits") + / total_lp; + let native_before = Balances::free_balance(&lp); + let asset_before = Assets::balance(asset_id, &lp); assert_ok!(AssetConversion::remove_liquidity( RuntimeOrigin::signed(lp.clone()), Box::new(native_asset), Box::new(local_asset), - safe_liquidity_amount / 4, // Remove 25% of liquidity - 1, - 1, + burn, + expected_native, + expected_asset, lp.clone(), )); - // Verify removal was successful (balances may not change in simplified test environment) + assert_eq!(Balances::free_balance(&lp) - native_before, expected_native); + assert_eq!( + Assets::balance(asset_id, &lp) - asset_before, + expected_asset + ); // Verify that AssetConversion events were emitted (simplified check) assert!( System::events() diff --git a/template/runtime/src/tests/common.rs b/template/runtime/src/tests/common.rs index 0f91f712..88aa3782 100644 --- a/template/runtime/src/tests/common.rs +++ b/template/runtime/src/tests/common.rs @@ -4,8 +4,8 @@ //! ensuring consistent test environment initialization and reducing code duplication. use crate::{ - AccountId, AssetConversion, Assets, Balance, Balances, EXISTENTIAL_DEPOSIT, Oracle, Runtime, - RuntimeOrigin, SLOT_DURATION, Staking, System, Timestamp, configs::AssetKind, + AccountId, Assets, Balance, Balances, EXISTENTIAL_DEPOSIT, Oracle, Runtime, RuntimeOrigin, + SLOT_DURATION, Staking, System, Timestamp, configs::AssetKind, }; use alloc::vec; use polkadot_sdk::frame_support::{ @@ -34,7 +34,7 @@ pub fn update_actor_contract_partial( actor_id: pallet_deos_actors::ActorId, replacement: impl ActorContractReplacement, ) -> polkadot_sdk::sp_runtime::DispatchResult { - let mut contract = pallet_deos_actors::ActorContracts::::get(actor_id) + let mut contract = crate::Actors::actor_contract(actor_id) .ok_or(pallet_deos_actors::Error::::ActorNotFound)?; replacement.apply(&mut contract); crate::Actors::update_contract(origin, actor_id, contract) @@ -401,51 +401,27 @@ pub fn add_liquidity( ) } -/// Ensure an AssetConversion pool exists, ignoring `PoolExists` and Assets `InUse` errors. +/// Ensure one complete canonical pool topology exists through its sole lifecycle owner. pub fn ensure_asset_conversion_pool(asset1: AssetKind, asset2: AssetKind) { let (canonical_asset1, canonical_asset2) = canonical_asset_pair(&asset1, &asset2); - let result = AssetConversion::create_pool( + let result = crate::DeosRouter::create_pool( RuntimeOrigin::signed(ALICE), - Box::new(canonical_asset1), - Box::new(canonical_asset2), + canonical_asset1, + canonical_asset2, ); if let Err(error) = result { - // Handle Assets pallet "InUse" error (index 12) - asset already in use - if let DispatchError::Module(ModuleError { - index: 12, - error: [3, 0, 0, 0], - .. - }) = &error - { - crate::configs::assets_config::register_pool_lp_pair(canonical_asset1, canonical_asset2) - .expect("existing pool must be indexed"); - return; - } if let DispatchError::Module(ModuleError { - index: 12, - message: Some("InUse"), + message: Some("PoolExists"), .. }) = &error { - crate::configs::assets_config::register_pool_lp_pair(canonical_asset1, canonical_asset2) - .expect("existing pool must be indexed"); return; } - // Handle AssetConversion pallet "PoolExists" error (index 13) - if let DispatchError::Module(ModuleError { - index: 13, - message: Some("PoolExists"), - .. - }) = &error - { - crate::configs::assets_config::register_pool_lp_pair(canonical_asset1, canonical_asset2) - .expect("existing pool must be indexed"); + if matches!(error, DispatchError::Other("Pool already exists")) { return; } - panic!("Unexpected AssetConversion pool creation error: {error:?}"); + panic!("Unexpected canonical pool creation error: {error:?}"); } - crate::configs::assets_config::register_pool_lp_pair(canonical_asset1, canonical_asset2) - .expect("created pool must be indexed"); } /// Creates the BLDR protocol token; runtime-topology sovereigns already hold native ED anchors. diff --git a/template/runtime/src/tests/deos_router_integration_tests.rs b/template/runtime/src/tests/deos_router_integration_tests.rs index a4eadbc8..434f4fc0 100644 --- a/template/runtime/src/tests/deos_router_integration_tests.rs +++ b/template/runtime/src/tests/deos_router_integration_tests.rs @@ -19,7 +19,11 @@ use pallet_deos_actors::{ ActorContract, CompletionPolicy, FundingSourcePolicy, Mutability, Step, StepErrorPolicy, Task, Trigger, }; -use polkadot_sdk::frame_support::{BoundedVec, assert_noop, assert_ok}; +use polkadot_sdk::{ + frame_support::{BoundedVec, assert_noop, assert_ok, traits::Contains}, + pallet_asset_conversion::PoolLocator, + sp_runtime::traits::Dispatchable, +}; use primitives::AssetKind; /// Setup test environment with pools and liquidity @@ -44,6 +48,72 @@ fn check_router_host_pool_registry() -> Result<(), &'static str> { Ok(()) } +#[test] +fn complete_pool_topology_integrity_rejects_every_corruption_class() { + use pallet_deos_router::LpPairIntegrity; + + #[derive(Clone, Copy)] + enum Corruption { + MissingReverse, + OrphanReverse, + MissingOracle, + PhysicalAlias, + } + + for corruption in [ + Corruption::MissingReverse, + Corruption::OrphanReverse, + Corruption::MissingOracle, + Corruption::PhysicalAlias, + ] { + seeded_test_ext().execute_with(|| { + assert_ok!(setup_test_environment()); + assert!( + ::validate_complete_topology() + .is_ok() + ); + let (pair, pool) = polkadot_sdk::pallet_asset_conversion::Pools::::iter() + .next() + .expect("one pool exists"); + match corruption { + Corruption::MissingReverse => { + pallet_deos_router::LpPairByTokenId::::mutate(|pairs| { + pairs.remove(&pool.lp_token); + }); + } + Corruption::OrphanReverse => { + pallet_deos_router::LpPairByTokenId::::mutate(|pairs| { + pairs + .try_insert( + pool.lp_token.saturating_add(10_000), + (AssetKind::Local(800_001), AssetKind::Local(800_002)), + ) + .expect("orphan entry fits"); + }); + } + Corruption::MissingOracle => { + pallet_oracle::Feeds::::remove( + crate::configs::oracle_config::deos_router_pool_feed(pair.0, pair.1), + ); + } + Corruption::PhysicalAlias => { + let id = primitives::TYPE_FOREIGN | 99; + polkadot_sdk::pallet_asset_conversion::Pools::::insert( + (AssetKind::Local(id), AssetKind::Foreign(id)), + polkadot_sdk::pallet_asset_conversion::PoolInfo { + lp_token: pool.lp_token.saturating_add(10_000), + }, + ); + } + } + assert!( + ::validate_complete_topology() + .is_err() + ); + }); + } +} + #[test] fn router_host_pool_registry_corruption_matrix_is_deterministic() { #[derive(Clone, Copy)] @@ -106,6 +176,355 @@ fn router_host_pool_registry_corruption_matrix_is_deterministic() { } } +#[test] +fn raw_asset_conversion_genesis_topology_is_rejected_fail_closed() { + use pallet_deos_router::LpPairIntegrity; + + seeded_test_ext().execute_with(|| { + assert!( + ::validate_genesis_topology() + .is_ok() + ); + polkadot_sdk::pallet_asset_conversion::Pools::::insert( + (AssetKind::Native, AssetKind::Local(ASSET_A)), + polkadot_sdk::pallet_asset_conversion::PoolInfo { lp_token: 700_001 }, + ); + assert_eq!( + ::validate_genesis_topology(), + Err( + "raw Asset Conversion genesis pools are unsupported; create complete pools through the permissionless DEOS lifecycle after genesis" + ) + ); + }); +} + +#[test] +fn canonical_pool_creation_rolls_back_when_lp_index_is_full() { + seeded_test_ext().execute_with(|| { + pallet_deos_router::LpPairByTokenId::::mutate(|pairs| { + for index in 0..500u32 { + pairs + .try_insert( + 100_000 + index, + ( + AssetKind::Local(200_000 + index * 2), + AssetKind::Local(200_001 + index * 2), + ), + ) + .expect("production LP index capacity"); + } + }); + let pair = (AssetKind::Native, AssetKind::Local(ASSET_A)); + let root_before = + polkadot_sdk::sp_io::storage::root(polkadot_sdk::sp_runtime::StateVersion::V1); + assert_noop!( + DeosRouter::create_pool(RuntimeOrigin::signed(ALICE), pair.0, pair.1), + pallet_deos_router::Error::::LpPairCapacityExceeded + ); + assert_eq!( + polkadot_sdk::sp_io::storage::root(polkadot_sdk::sp_runtime::StateVersion::V1), + root_before + ); + let pool_id = ::PoolLocator::pool_id( + &pair.0, &pair.1, + ) + .expect("canonical pool identity"); + assert!(!polkadot_sdk::pallet_asset_conversion::Pools::::contains_key(pool_id)); + assert!(!pallet_oracle::Feeds::::contains_key( + crate::configs::oracle_config::deos_router_pool_feed(pool_id.0, pool_id.1) + )); + }); +} + +#[test] +fn canonical_pool_creation_rolls_back_on_lp_identity_collision() { + seeded_test_ext().execute_with(|| { + let pair = (AssetKind::Native, AssetKind::Local(ASSET_A)); + crate::configs::AssetConversionAdapter::ensure_lp_asset_namespace(); + let expected_lp = polkadot_sdk::pallet_asset_conversion::NextPoolAssetId::::get() + .expect("LP namespace initialized"); + assert_ok!(Assets::force_create( + RuntimeOrigin::root(), + expected_lp, + ALICE.into(), + true, + 1, + )); + let root_before = + polkadot_sdk::sp_io::storage::root(polkadot_sdk::sp_runtime::StateVersion::V1); + assert!(DeosRouter::create_pool(RuntimeOrigin::signed(ALICE), pair.0, pair.1).is_err()); + assert_eq!( + polkadot_sdk::sp_io::storage::root(polkadot_sdk::sp_runtime::StateVersion::V1), + root_before + ); + let pool_id = ::PoolLocator::pool_id( + &pair.0, &pair.1, + ) + .expect("canonical pool identity"); + assert!(!polkadot_sdk::pallet_asset_conversion::Pools::::contains_key(pool_id)); + assert!(pallet_deos_router::LpPairByTokenId::::get().is_empty()); + assert!(!pallet_oracle::Feeds::::contains_key( + crate::configs::oracle_config::deos_router_pool_feed(pool_id.0, pool_id.1) + )); + }); +} + +#[test] +fn canonical_pool_creation_rolls_back_on_lp_identity_mismatch() { + seeded_test_ext().execute_with(|| { + let pair = (AssetKind::Native, AssetKind::Local(ASSET_A)); + crate::configs::assets_config::set_force_lp_identity_mismatch(true); + let root_before = + polkadot_sdk::sp_io::storage::root(polkadot_sdk::sp_runtime::StateVersion::V1); + assert_noop!( + DeosRouter::create_pool(RuntimeOrigin::signed(ALICE), pair.0, pair.1), + polkadot_sdk::sp_runtime::DispatchError::Other("LP identity mismatch") + ); + crate::configs::assets_config::set_force_lp_identity_mismatch(false); + assert_eq!( + polkadot_sdk::sp_io::storage::root(polkadot_sdk::sp_runtime::StateVersion::V1), + root_before + ); + let pool_id = ::PoolLocator::pool_id( + &pair.0, &pair.1, + ) + .expect("canonical pool identity"); + assert!(!polkadot_sdk::pallet_asset_conversion::Pools::::contains_key(pool_id)); + assert!(pallet_deos_router::LpPairByTokenId::::get().is_empty()); + }); +} + +#[test] +fn canonical_pool_creation_rolls_back_complete_topology_after_underlying_mutation() { + seeded_test_ext().execute_with(|| { + let pair = (AssetKind::Native, AssetKind::Local(ASSET_A)); + crate::configs::assets_config::set_fail_after_pool_creation(true); + let root_before = + polkadot_sdk::sp_io::storage::root(polkadot_sdk::sp_runtime::StateVersion::V1); + assert_noop!( + DeosRouter::create_pool(RuntimeOrigin::signed(ALICE), pair.0, pair.1), + polkadot_sdk::sp_runtime::DispatchError::Other("Injected post-pool lifecycle failure") + ); + crate::configs::assets_config::set_fail_after_pool_creation(false); + assert_eq!( + polkadot_sdk::sp_io::storage::root(polkadot_sdk::sp_runtime::StateVersion::V1), + root_before + ); + let pool_id = ::PoolLocator::pool_id( + &pair.0, &pair.1, + ) + .expect("canonical pool identity"); + assert!(!polkadot_sdk::pallet_asset_conversion::Pools::::contains_key(pool_id)); + assert!(pallet_deos_router::LpPairByTokenId::::get().is_empty()); + assert!(!pallet_oracle::Feeds::::contains_key( + crate::configs::oracle_config::deos_router_pool_feed(pool_id.0, pool_id.1) + )); + }); +} + +#[test] +fn canonical_pool_creation_builds_complete_topology_and_closes_raw_bypass() { + seeded_test_ext().execute_with(|| { + let asset_a = AssetKind::Native; + let asset_b = AssetKind::Local(ASSET_A); + let raw = crate::RuntimeCall::AssetConversion( + polkadot_sdk::pallet_asset_conversion::Call::create_pool { + asset1: Box::new(asset_a), + asset2: Box::new(asset_b), + }, + ); + assert!(!crate::configs::RuntimeCallFilter::contains(&raw)); + assert_noop!( + raw.dispatch(RuntimeOrigin::signed(ALICE)), + polkadot_sdk::frame_system::Error::::CallFiltered + ); + + assert_ok!(DeosRouter::create_pool( + RuntimeOrigin::signed(ALICE), + asset_a, + asset_b, + )); + let pool_id = ::PoolLocator::pool_id( + &asset_a, &asset_b, + ) + .expect("canonical pool identity"); + let pool = polkadot_sdk::pallet_asset_conversion::Pools::::get(pool_id) + .expect("underlying pool exists"); + assert_eq!( + DeosRouter::lp_pair_by_token_id(pool.lp_token), + Some(pool_id) + ); + let forward = crate::configs::oracle_config::deos_router_pool_feed(pool_id.0, pool_id.1); + let reverse = crate::configs::oracle_config::deos_router_pool_feed(pool_id.1, pool_id.0); + assert!(pallet_oracle::Feeds::::contains_key(forward)); + assert!(pallet_oracle::Feeds::::contains_key(reverse)); + }); +} + +#[test] +fn raw_asset_conversion_swaps_are_call_filtered_fail_closed() { + let path = vec![ + Box::new(AssetKind::Native), + Box::new(AssetKind::Local(ASSET_A)), + ]; + let exact_input = crate::RuntimeCall::AssetConversion( + polkadot_sdk::pallet_asset_conversion::Call::swap_exact_tokens_for_tokens { + path: path.clone(), + amount_in: SWAP_AMOUNT, + amount_out_min: MIN_AMOUNT_OUT, + send_to: ALICE, + keep_alive: true, + }, + ); + let exact_output = crate::RuntimeCall::AssetConversion( + polkadot_sdk::pallet_asset_conversion::Call::swap_tokens_for_exact_tokens { + path, + amount_out: MIN_AMOUNT_OUT, + amount_in_max: SWAP_AMOUNT, + send_to: ALICE, + keep_alive: true, + }, + ); + assert!(!crate::configs::RuntimeCallFilter::contains(&exact_input)); + assert!(!crate::configs::RuntimeCallFilter::contains(&exact_output)); + seeded_test_ext().execute_with(|| { + assert_noop!( + exact_input.clone().dispatch(RuntimeOrigin::signed(ALICE)), + polkadot_sdk::frame_system::Error::::CallFiltered + ); + assert_noop!( + exact_output.clone().dispatch(RuntimeOrigin::signed(ALICE)), + polkadot_sdk::frame_system::Error::::CallFiltered + ); + }); + + let router = crate::RuntimeCall::DeosRouter(pallet_deos_router::Call::swap { + from: AssetKind::Native, + to: AssetKind::Local(ASSET_A), + amount_in: SWAP_AMOUNT, + min_amount_out: MIN_AMOUNT_OUT, + recipient: ALICE, + deadline: 10, + }); + assert!(crate::configs::RuntimeCallFilter::contains(&router)); + let router_exact_output = + crate::RuntimeCall::DeosRouter(pallet_deos_router::Call::swap_exact_output { + from: AssetKind::Native, + to: AssetKind::Local(ASSET_A), + amount_out: MIN_AMOUNT_OUT, + max_amount_in: SWAP_AMOUNT, + recipient: ALICE, + deadline: 10, + }); + assert!(crate::configs::RuntimeCallFilter::contains( + &router_exact_output + )); +} + +#[test] +fn protected_pool_balance_is_included_in_reserves() { + use polkadot_sdk::frame_support::traits::{ + fungible::Inspect, + tokens::{Fortitude::Polite, Preservation::Preserve}, + }; + + seeded_test_ext().execute_with(|| { + assert_ok!(setup_test_environment()); + let pair = (AssetKind::Native, AssetKind::Local(ASSET_A)); + let pool_id = ::PoolLocator::pool_id( + &pair.0, &pair.1, + ) + .expect("canonical pool identity"); + let pool_account = + ::PoolLocator::address(&pool_id) + .expect("canonical pool account"); + let full_native = Balances::balance(&pool_account); + let preserve_reducible = Balances::reducible_balance(&pool_account, Preserve, Polite); + let (native_reserve, _) = + crate::AssetConversion::get_reserves(pair.0, pair.1).expect("pool reserves exist"); + assert_eq!(native_reserve, full_native); + assert!(native_reserve > preserve_reducible); + }); +} + +#[test] +fn unrelated_non_sufficient_asset_does_not_change_quote() { + seeded_test_ext().execute_with(|| { + assert_ok!(setup_test_environment()); + let asset_in = AssetKind::Local(ASSET_A); + let asset_out = AssetKind::Native; + let amount_in = SWAP_AMOUNT; + let quote_before = crate::AssetConversion::quote_price_exact_tokens_for_tokens( + asset_in, asset_out, amount_in, true, + ); + let pool_id = ::PoolLocator::pool_id( + &asset_in, &asset_out, + ) + .expect("canonical pool identity"); + let pool_account = + ::PoolLocator::address(&pool_id) + .expect("canonical pool account"); + const UNRELATED: u32 = 900_001; + assert_ok!(Assets::force_create( + RuntimeOrigin::root(), + UNRELATED, + ALICE.into(), + false, + 1, + )); + assert_ok!(Assets::mint( + RuntimeOrigin::signed(ALICE), + UNRELATED, + ALICE.into(), + 1_000, + )); + assert_ok!(Assets::transfer( + RuntimeOrigin::signed(ALICE), + UNRELATED, + pool_account.into(), + 1, + )); + assert_eq!( + crate::AssetConversion::quote_price_exact_tokens_for_tokens( + asset_in, asset_out, amount_in, true, + ), + quote_before + ); + }); +} + +#[test] +fn router_exact_output_swap_enforces_fee_conservation() { + seeded_test_ext().execute_with(|| { + assert_ok!(setup_test_environment()); + let from = AssetKind::Local(ASSET_A); + let to = AssetKind::Native; + let amount_out = MIN_AMOUNT_OUT; + let quote = DeosRouter::quote_exact_out(ALICE, from, to, amount_out) + .expect("exact-output quote must exist for seeded direct pool"); + let sender_before = Assets::balance(ASSET_A, ALICE); + let recipient_before = Balances::free_balance(ALICE); + let burn_before = Assets::balance(ASSET_A, burn_actor_account()); + + assert_ok!(DeosRouter::swap_exact_output( + RuntimeOrigin::signed(ALICE), + from, + to, + amount_out, + quote.amount_in, + ALICE, + 1_000, + )); + + let sender_input = sender_before - Assets::balance(ASSET_A, ALICE); + let burn_credit = Assets::balance(ASSET_A, burn_actor_account()) - burn_before; + assert_eq!(sender_input, quote.amount_in); + assert_eq!(sender_input, quote.router_fee + quote.amount_after_fee); + assert_eq!(burn_credit, quote.router_fee); + assert!(Balances::free_balance(ALICE) - recipient_before >= amount_out); + }); +} + #[test] fn test_deos_router_basic_swap_functionality() { seeded_test_ext().execute_with(|| { @@ -519,7 +938,7 @@ fn test_deos_router_error_handling() { ALICE, 1000, ), - pallet_deos_router::pallet::Error::::IdenticalAssets + pallet_deos_router::pallet::Error::::InvalidPoolPair ); // Test zero amount error (caught by MinSwapForeign check) assert_noop!( diff --git a/template/runtime/src/tests/dispatchability_matrix_tests.rs b/template/runtime/src/tests/dispatchability_matrix_tests.rs index d0e4326f..94b1c666 100644 --- a/template/runtime/src/tests/dispatchability_matrix_tests.rs +++ b/template/runtime/src/tests/dispatchability_matrix_tests.rs @@ -1,5 +1,8 @@ use crate::{Runtime, configs::RuntimeBlockWeights}; -use polkadot_sdk::frame_support::{dispatch::DispatchClass, weights::Weight}; +use polkadot_sdk::frame_support::{ + dispatch::{DispatchClass, Pays}, + weights::Weight, +}; type ActorsWeights = crate::weights::pallet_deos_actors::SubstrateWeight; type AssetRegistryWeights = crate::weights::pallet_asset_registry::SubstrateWeight; @@ -9,13 +12,14 @@ type RouterWeights = crate::weights::pallet_deos_router::SubstrateWeight; type TmcWeights = crate::weights::pallet_tmc::SubstrateWeight; -const EXPECTED_CUSTOM_CALL_FAMILIES: usize = 63; +const EXPECTED_CUSTOM_CALL_FAMILIES: usize = 65; #[derive(Clone, Copy)] struct DispatchabilityRow { family: &'static str, class: DispatchClass, maximum_weight: Weight, + success_payment: Pays, } fn normal(family: &'static str, maximum_weight: Weight) -> DispatchabilityRow { @@ -23,6 +27,16 @@ fn normal(family: &'static str, maximum_weight: Weight) -> DispatchabilityRow { family, class: DispatchClass::Normal, maximum_weight, + success_payment: Pays::Yes, + } +} + +fn actor_funded_success(family: &'static str, maximum_weight: Weight) -> DispatchabilityRow { + DispatchabilityRow { + family, + class: DispatchClass::Normal, + maximum_weight, + success_payment: Pays::No, } } @@ -76,7 +90,7 @@ fn every_custom_runtime_call_family_fits_its_dispatch_envelope_at_maximum_input( "Actors.resume_actor", ActorsWeights::resume_actor().saturating_add(close), ), - normal( + actor_funded_success( "Actors.manual_trigger", ActorsWeights::manual_trigger().saturating_add(close), ), @@ -116,10 +130,7 @@ fn every_custom_runtime_call_family_fits_its_dispatch_envelope_at_maximum_input( ), normal("Actors.activate_actor", ActorsWeights::activate_actor()), normal("Actors.deactivate_actor", ActorsWeights::deactivate_actor()), - normal( - "Actors.cancel_continuation", - ActorsWeights::continuation_cancel(), - ), + normal("Actors.cancel_run", ActorsWeights::run_cancel()), normal( "AssetRegistry.register_foreign_asset", AssetRegistryWeights::register_foreign_asset(), @@ -195,6 +206,8 @@ fn every_custom_runtime_call_family_fits_its_dispatch_envelope_at_maximum_input( "DEOS Router.update_router_fee", RouterWeights::update_router_fee(), ), + normal("DEOS Router.swap_exact_output", RouterWeights::swap()), + normal("DEOS Router.create_pool", RouterWeights::create_pool()), normal( "DEOS Staking.register_staking_asset", StakingWeights::register_staking_asset(), @@ -276,6 +289,11 @@ fn every_custom_runtime_call_family_fits_its_dispatch_envelope_at_maximum_input( ); let block_weights = RuntimeBlockWeights::get(); for row in rows { + if row.family == "Actors.manual_trigger" { + assert_eq!(row.success_payment, Pays::No); + } else { + assert_eq!(row.success_payment, Pays::Yes); + } let class_limits = block_weights.get(row.class); let max_extrinsic = class_limits.max_extrinsic.unwrap_or( class_limits diff --git a/template/runtime/src/tests/fee_integration_tests.rs b/template/runtime/src/tests/fee_integration_tests.rs index 7c9b7662..6cbd8e44 100644 --- a/template/runtime/src/tests/fee_integration_tests.rs +++ b/template/runtime/src/tests/fee_integration_tests.rs @@ -44,6 +44,19 @@ fn run_at_cadence_tick(key: WakeupKey) { crate::System::set_block_number(block); let _ = Actors::on_initialize(block); let _ = Actors::on_idle(block, Weight::MAX); + let eligible_block = block.saturating_add(1); + crate::System::set_block_number(eligible_block); + let _ = Actors::on_initialize(eligible_block); + let _ = Actors::on_idle(eligible_block, Weight::MAX); +} + +fn cadence_key(actor_id: pallet_deos_actors::ActorId) -> WakeupKey { + WakeupKey::Tick( + Actors::actor_hot(actor_id) + .and_then(|hot| hot.trigger_wakeup_pointer) + .expect("Cadenced Trigger deadline exists") + .tick, + ) } fn initialize_genesis_fee_sink_cadence(timestamp_millis: u64) -> WakeupKey { @@ -53,10 +66,7 @@ fn initialize_genesis_fee_sink_cadence(timestamp_millis: u64) -> WakeupKey::put( + polkadot_sdk::cumulus_primitives_core::PersistedValidationData::default(), + ); + assert_ok!(Actors::actor_prepass(RuntimeOrigin::none())); let _ = Actors::on_idle(10_000, Weight::MAX); assert_eq!( Actors::active_actor_state(fee_sink_id) @@ -166,7 +174,22 @@ fn fee_sink_cadence_anchors_at_first_consensus_timestamp_and_never_executes_earl set_consensus_timestamp(61_000); crate::System::set_block_number(10_001); let _ = Actors::on_initialize(10_001); + pallet_deos_actors::CurrentBlockResourceState::::kill(); + assert_ok!(Actors::actor_prepass(RuntimeOrigin::none())); let _ = Actors::on_idle(10_001, Weight::MAX); + assert_eq!( + Actors::active_actor_state(fee_sink_id) + .expect("Fee Sink remains active") + .identity + .cycle_nonce, + 0, + ); + + crate::System::set_block_number(10_002); + let _ = Actors::on_initialize(10_002); + pallet_deos_actors::CurrentBlockResourceState::::kill(); + assert_ok!(Actors::actor_prepass(RuntimeOrigin::none())); + let _ = Actors::on_idle(10_002, Weight::MAX); assert_eq!( Actors::active_actor_state(fee_sink_id) .expect("Fee Sink remains active") @@ -197,10 +220,7 @@ fn repeated_low_volume_fee_sink_distributions_preserve_anchors_without_failures( AssetKind::Native, 2, )); - let cadence_block = Actors::actor_hot(primitives::ecosystem::actor_ids::FEE_SINK_ACTORS_ID) - .and_then(|hot| hot.wakeup_pointer) - .expect("Fee Sink cadence remains armed") - .block; + let cadence_block = cadence_key(primitives::ecosystem::actor_ids::FEE_SINK_ACTORS_ID); run_at_cadence_tick(cadence_block); } @@ -231,10 +251,7 @@ fn fee_sink_threshold_admits_exactly_one_ed_per_permissioned_leg() { AssetKind::Native, amount, )); - let cadence_block = Actors::actor_hot(fee_sink_id) - .and_then(|hot| hot.wakeup_pointer) - .expect("Fee Sink cadence remains armed") - .block; + let cadence_block = cadence_key(fee_sink_id); run_at_cadence_tick(cadence_block); assert_eq!(Balances::free_balance(&staking_pool), anchor * 2); @@ -316,10 +333,7 @@ fn fee_sink_actor_splits_trusted_set_native_flow_to_staking_and_lp_ingress() { AssetKind, crate::Balance, >>::mint(&fee_sink, AssetKind::Native, amount)); - let cadence = Actors::actor_hot(primitives::ecosystem::actor_ids::FEE_SINK_ACTORS_ID) - .and_then(|hot| hot.wakeup_pointer) - .expect("Fee Sink cadence remains armed") - .block; + let cadence = cadence_key(primitives::ecosystem::actor_ids::FEE_SINK_ACTORS_ID); run_at_cadence_tick(cadence); let followup_block = crate::System::block_number().saturating_add(1); crate::System::set_block_number(followup_block); diff --git a/template/runtime/src/tests/governance_integration_tests.rs b/template/runtime/src/tests/governance_integration_tests.rs index 89e7d124..af83ad5c 100644 --- a/template/runtime/src/tests/governance_integration_tests.rs +++ b/template/runtime/src/tests/governance_integration_tests.rs @@ -136,6 +136,10 @@ fn signed_extrinsic( nonce: crate::Nonce, call: RuntimeCall, ) -> UncheckedExtrinsic { + let now = System::block_number(); + if !Actors::block_resource_state().is_some_and(|state| state.ensure_block(now).is_ok()) { + let _ = Actors::on_initialize(now); + } let tx_ext = TxExtension::new(( polkadot_sdk::frame_system::AuthorizeCall::::new(), polkadot_sdk::frame_system::CheckNonZeroSender::::new(), @@ -145,8 +149,10 @@ fn signed_extrinsic( polkadot_sdk::frame_system::CheckEra::::from(generic::Era::Immortal), polkadot_sdk::frame_system::CheckNonce::::from(nonce), polkadot_sdk::frame_system::CheckWeight::::new(), - crate::configs::address_event_ingress::AddressEventIngressExtension, - crate::configs::pool_index::PoolIndexExtension, + ( + crate::configs::resource_meter::BlockResourceMeterExtension, + crate::configs::address_event_ingress::AddressEventIngressExtension, + ), polkadot_sdk::pallet_transaction_payment::ChargeTransactionPayment::::from(0), polkadot_sdk::frame_metadata_hash_extension::CheckMetadataHash::::new(false), )); @@ -1519,12 +1525,52 @@ fn maximum_signed_governance_proposal_passes_real_check_weight_validation() { .expect("signed Governance class must define max_extrinsic"); assert!(dispatch_info.call_weight.all_lte(max_extrinsic)); let extrinsic = signed_extrinsic(&signer_pair, 0, call); + let extrinsic_weight = extrinsic.get_dispatch_info().total_weight(); + assert!( + extrinsic_weight.all_lte( + crate::configs::BlockResourceBudgetValue::get() + .limits() + .shared_economic() + ), + "extrinsic {extrinsic_weight:?} must fit shared {:?}", + crate::configs::BlockResourceBudgetValue::get() + .limits() + .shared_economic() + ); + let now = System::block_number(); + let validation_block = now.saturating_add(1); + let mut validation_state = pallet_deos_actors::BlockResourceState::new(validation_block); + validation_state + .begin_prepass() + .expect("validation phase opens"); + validation_state + .open_external_phase() + .expect("validation external phase opens"); + pallet_deos_actors::CurrentBlockResourceState::::put(validation_state); assert_ok!(Executive::validate_transaction( TransactionSource::External, extrinsic.clone(), System::block_hash(0), )); + pallet_deos_actors::CurrentBlockResourceState::::kill(); + polkadot_sdk::pallet_timestamp::Now::::put(1); + polkadot_sdk::cumulus_pallet_parachain_system::ValidationData::::put( + polkadot_sdk::cumulus_primitives_core::PersistedValidationData::default(), + ); + let _ = Actors::on_initialize(now); + assert_ok!(Actors::actor_prepass(RuntimeOrigin::none())); assert_ok!(Executive::apply_extrinsic(extrinsic)); + let resource_state = + Actors::block_resource_state().expect("resource meter remains authoritative"); + assert_eq!(resource_state.outstanding_reservations(), 0); + assert!( + resource_state.usage().user_dispatch_used() + != polkadot_sdk::frame_support::weights::Weight::zero() + ); + assert_eq!( + resource_state.phase(), + pallet_deos_actors::BlockResourcePhase::ExternalPhase + ); assert_eq!( Governance::proposal_author(PROTOCOL_GOVERNANCE_DOMAIN, 113), Some(signer), diff --git a/template/runtime/src/tests/oracle_integration_tests.rs b/template/runtime/src/tests/oracle_integration_tests.rs index 8911d899..0c73b805 100644 --- a/template/runtime/src/tests/oracle_integration_tests.rs +++ b/template/runtime/src/tests/oracle_integration_tests.rs @@ -11,14 +11,11 @@ use pallet_deos_actors::{ ActorContract, FundingSourcePolicy, Mutability, StepErrorPolicy, Task, Trigger, }; use pallet_oracle::{Aggregation, ObservationState, WeightInfo as _, ZeroPolicy}; -use polkadot_sdk::{ - frame_support::{ - BoundedVec, assert_noop, assert_ok, - dispatch::GetDispatchInfo, - traits::{Currency, Hooks, fungibles::Inspect as FungiblesInspect}, - weights::Weight, - }, - sp_runtime::traits::TransactionExtension, +use polkadot_sdk::frame_support::{ + BoundedVec, assert_noop, assert_ok, + dispatch::GetDispatchInfo, + traits::{Currency, Hooks, fungibles::Inspect as FungiblesInspect}, + weights::Weight, }; use primitives::{AssetKind, OracleAggregationId, OracleFeedId, OracleMeaning, OracleProvenance}; @@ -500,22 +497,6 @@ fn pool_feed_cardinality_is_explicitly_bounded() { ); } -#[test] -fn pool_index_extension_declares_two_worst_case_feed_registrations() { - let call = RuntimeCall::AssetConversion(crate::pallet_asset_conversion::Call::create_pool { - asset1: Box::new(AssetKind::Native), - asset2: Box::new(AssetKind::Local(7)), - }); - let declared = crate::configs::pool_index::PoolIndexExtension.weight(&call); - let registration = - crate::weights::pallet_oracle::SubstrateWeight::::register_feed_existing_producer() - .max(crate::weights::pallet_oracle::SubstrateWeight::::register_feed_new_producer()) - .saturating_mul(2); - let index_work = - ::DbWeight::get().reads_writes(13, 1); - assert_eq!(declared, registration.saturating_add(index_work)); -} - #[test] fn pair_registration_rejects_before_partial_mutation_when_capacity_is_full() { new_test_ext().execute_with(|| { diff --git a/template/runtime/src/tests/tmctol_integration_tests.rs b/template/runtime/src/tests/tmctol_integration_tests.rs index e7141e16..88aae6f7 100644 --- a/template/runtime/src/tests/tmctol_integration_tests.rs +++ b/template/runtime/src/tests/tmctol_integration_tests.rs @@ -191,19 +191,18 @@ fn tmctol_guarantee_state_reports_bldr_buyback_liveness_when_configured() { #[test] fn tmctol_guarantee_state_flags_broken_native_burn_plan_as_violation() { new_test_ext().execute_with(|| { - pallet_deos_actors::ActorContracts::::mutate(actor_ids::BURN_ACTOR_ID, |maybe| { - let contract = maybe.as_mut().expect("Burn Actor contract exists"); - contract.steps = alloc::vec![pallet_deos_actors::Step { - precondition: None, - task: pallet_deos_actors::Task::Transfer { - to: ALICE, - asset: AssetKind::Native, - amount: AmountResolution::Fixed(0), - }, - on_error: pallet_deos_actors::StepErrorPolicy::AbortCycle, - }] - .try_into() - .expect("malformed burn plan fits"); + pallet_deos_actors::ActorContractHeads::::mutate(actor_ids::BURN_ACTOR_ID, |maybe| { + maybe + .as_mut() + .expect("Burn Actor Contract head exists") + .first_step + .as_mut() + .expect("Burn Actor has inline Step 0") + .task = pallet_deos_actors::Task::Transfer { + to: ALICE, + asset: AssetKind::Native, + amount: AmountResolution::Fixed(0), + }; }); let state = crate::tmctol_read_model::TmctolReadModel::tmctol_guarantee_state(); @@ -566,7 +565,7 @@ fn router_oracle_burn_success_path_commits_once_without_scheduler_or_reward_resi let hot = Actors::actor_hot(burn_actor_id).expect("Burn Actor hot state exists"); assert!(hot.queue_ticket.is_none()); assert!(hot.wakeup_pointer.is_none()); - assert!(Actors::continuation_state(burn_actor_id).is_none()); + assert!(Actors::actor_run_state(burn_actor_id).is_none()); assert_eq!(Actors::dirty_observation_feed_count(), 0); assert_eq!( crate::Staking::native_security_reward_liability(), @@ -850,6 +849,14 @@ fn swap_with_slippage_tolerance_succeeds_under_fair_conditions() { System::set_block_number(11); Actors::on_initialize(11); Actors::on_idle(11, Weight::from_parts(u64::MAX, u64::MAX)); + assert_eq!( + crate::Assets::balance(super::common::ASSET_A, &bm), + balance_before, + "stale Contract authority is replaced without same-block execution" + ); + System::set_block_number(12); + Actors::on_initialize(12); + Actors::on_idle(12, Weight::from_parts(u64::MAX, u64::MAX)); let balance_after = crate::Assets::balance(super::common::ASSET_A, &bm); assert!( balance_after > balance_before, @@ -1922,6 +1929,17 @@ fn treasury_b_buyback_burns_bldr() { Actors::on_initialize(block); Actors::on_idle(block, Weight::from_parts(u64::MAX, u64::MAX)); } + for block in 101..=108 { + if Actors::actor_run_state(treasury_b_id).is_none() { + break; + } + System::set_block_number(block); + set_consensus_timestamp(u64::from(block).saturating_mul( + primitives::ecosystem::params::ACTOR_CADENCE_TICK_MILLIS, + )); + Actors::on_initialize(block); + Actors::on_idle(block, Weight::from_parts(u64::MAX, u64::MAX)); + } // Verify at least one cycle executed assert!( has_actor_event(|event| { diff --git a/template/runtime/src/tmctol_read_model.rs b/template/runtime/src/tmctol_read_model.rs index ffcebb4f..0a12f7d8 100644 --- a/template/runtime/src/tmctol_read_model.rs +++ b/template/runtime/src/tmctol_read_model.rs @@ -251,7 +251,7 @@ impl TmctolReadModel { let dust_threshold = ecosystem::params::BURN_ACTOR_DUST_THRESHOLD; let maybe_actor = pallet_deos_actors::ActorIdentities::::get(actor_id) .zip(pallet_deos_actors::ActorHot::::get(actor_id)) - .zip(pallet_deos_actors::ActorContracts::::get(actor_id)); + .zip(crate::Actors::actor_contract(actor_id)); let ( actor_exists, is_system, @@ -316,7 +316,7 @@ impl TmctolReadModel { let sovereign_account = crate::Actors::sovereign_account_id_system(actor_id); let maybe_actor = pallet_deos_actors::ActorIdentities::::get(actor_id) .zip(pallet_deos_actors::ActorHot::::get(actor_id)) - .zip(pallet_deos_actors::ActorContracts::::get(actor_id)); + .zip(crate::Actors::actor_contract(actor_id)); let Some(((identity, hot), program)) = maybe_actor else { let status = if pallet_deos_actors::ActorIdentities::::contains_key(actor_id) { GuaranteeStatus::NotInitialized diff --git a/template/runtime/src/weights/mod.rs b/template/runtime/src/weights/mod.rs index ddfe87dd..009a3f7c 100644 --- a/template/runtime/src/weights/mod.rs +++ b/template/runtime/src/weights/mod.rs @@ -27,6 +27,7 @@ pub mod pallet_deos_actors; pub mod pallet_deos_router; pub mod pallet_governance; pub mod pallet_oracle; +pub mod pallet_session_rotation; pub mod pallet_staking; pub mod pallet_xcm; diff --git a/template/runtime/src/weights/pallet_deos_actors.rs b/template/runtime/src/weights/pallet_deos_actors.rs index acf00ac6..c8181a93 100644 --- a/template/runtime/src/weights/pallet_deos_actors.rs +++ b/template/runtime/src/weights/pallet_deos_actors.rs @@ -2,7 +2,7 @@ //! Autogenerated weights for `pallet_deos_actors` //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 58.0.0 -//! DATE: 2026-08-23, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2026-08-31, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` //! HOSTNAME: `fedora`, CPU: `AMD Ryzen 7 4800H with Radeon Graphics` //! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 @@ -47,7 +47,27 @@ // --exclude-extrinsics // pallet_deos_actors::precondition_observation // --exclude-extrinsics -// pallet_deos_actors::observation_fanout_blocked_page +// pallet_deos_actors::benchmark_monolithic_create +// --exclude-extrinsics +// pallet_deos_actors::benchmark_chunked_create +// --exclude-extrinsics +// pallet_deos_actors::benchmark_monolithic_close +// --exclude-extrinsics +// pallet_deos_actors::benchmark_chunked_close +// --exclude-extrinsics +// pallet_deos_actors::benchmark_monolithic_update +// --exclude-extrinsics +// pallet_deos_actors::benchmark_monolithic_reconstruct +// --exclude-extrinsics +// pallet_deos_actors::benchmark_chunked_reconstruct +// --exclude-extrinsics +// pallet_deos_actors::benchmark_monolithic_load_first +// --exclude-extrinsics +// pallet_deos_actors::benchmark_monolithic_load_tail +// --exclude-extrinsics +// pallet_deos_actors::benchmark_chunked_load_first +// --exclude-extrinsics +// pallet_deos_actors::benchmark_chunked_load_tail // --exclude-extrinsics // pallet_deos_actors::circular_chain_stress // --exclude-extrinsics @@ -91,18 +111,18 @@ impl pallet_deos_actors::WeightInfo for S /// Storage: `Actors::ActorIdentities` (r:1 w:1) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorHot` (r:1 w:1) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorContract` (r:1 w:1) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:1 w:1) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorFunding` (r:1 w:1) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `MaxEncodedLen`) - /// Storage: `Actors::ContinuationState` (r:1 w:0) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:1 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) /// Storage: `Actors::OwnerSlotBitmaps` (r:1 w:1) /// Proof: `Actors::OwnerSlotBitmaps` (`max_values`: None, `max_size`: Some(80), added: 2555, mode: `MaxEncodedLen`) /// Storage: `Actors::SovereignIndex` (r:1 w:1) /// Proof: `Actors::SovereignIndex` (`max_values`: None, `max_size`: Some(56), added: 2531, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:2 w:1) + /// Storage: `System::Account` (r:1 w:1) /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingMemberships` (r:1 w:1) /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) @@ -112,6 +132,8 @@ impl pallet_deos_actors::WeightInfo for S /// Proof: `Oracle::Observations` (`max_values`: None, `max_size`: Some(61), added: 2536, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorObservationFeeds` (r:1 w:0) /// Proof: `Actors::ActorObservationFeeds` (`max_values`: None, `max_size`: Some(42), added: 2517, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:1) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingFeedMembershipCount` (r:1 w:1) /// Proof: `Actors::CrossingFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingUserFeedMembershipCount` (r:1 w:1) @@ -119,18 +141,30 @@ impl pallet_deos_actors::WeightInfo for S /// Storage: `Actors::CrossingLeafStates` (r:1 w:1) /// Proof: `Actors::CrossingLeafStates` (`max_values`: None, `max_size`: Some(62), added: 2537, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingMemberPages` (r:1 w:1) - /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(1096), added: 3571, mode: `MaxEncodedLen`) + /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(8264), added: 10739, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingRadixNodes` (r:32 w:32) /// Proof: `Actors::CrossingRadixNodes` (`max_values`: None, `max_size`: Some(53), added: 2528, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractTailChunk` (r:8 w:8) + /// Proof: `Actors::ActorContractTailChunk` (`max_values`: None, `max_size`: Some(4070), added: 6545, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorStateHolds` (r:1 w:1) + /// Proof: `Actors::ActorStateHolds` (`max_values`: None, `max_size`: Some(152), added: 2627, mode: `MaxEncodedLen`) + /// Storage: `Actors::ObservationSubscriptionSlot` (r:1 w:0) + /// Proof: `Actors::ObservationSubscriptionSlot` (`max_values`: None, `max_size`: Some(28), added: 2503, mode: `MaxEncodedLen`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(121), added: 2596, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorActivationAuthority` (r:0 w:1) + /// Proof: `Actors::ActorActivationAuthority` (`max_values`: None, `max_size`: Some(159), added: 2634, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:0 w:1) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) fn create_user_actor() -> Weight { // Proof Size summary in bytes: - // Measured: `2185` + // Measured: `2506` // Estimated: `81886` - // Minimum execution time: 254_506_000 picoseconds. - Weight::from_parts(261_839_000, 0) + // Minimum execution time: 464_243_000 picoseconds. + Weight::from_parts(489_874_000, 0) .saturating_add(Weight::from_parts(0, 81886)) - .saturating_add(T::DbWeight::get().reads(54)) - .saturating_add(T::DbWeight::get().writes(47)) + .saturating_add(T::DbWeight::get().reads(65)) + .saturating_add(T::DbWeight::get().writes(60)) } /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) @@ -145,18 +179,18 @@ impl pallet_deos_actors::WeightInfo for S /// Storage: `Actors::ActorIdentities` (r:1 w:1) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorHot` (r:1 w:1) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorContract` (r:1 w:1) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:1 w:1) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorFunding` (r:1 w:1) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `MaxEncodedLen`) - /// Storage: `Actors::ContinuationState` (r:1 w:0) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:1 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) /// Storage: `Actors::OwnerSlotBitmaps` (r:1 w:1) /// Proof: `Actors::OwnerSlotBitmaps` (`max_values`: None, `max_size`: Some(80), added: 2555, mode: `MaxEncodedLen`) /// Storage: `Actors::SovereignIndex` (r:1 w:1) /// Proof: `Actors::SovereignIndex` (`max_values`: None, `max_size`: Some(56), added: 2531, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:2 w:1) + /// Storage: `System::Account` (r:1 w:1) /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingMemberships` (r:1 w:1) /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) @@ -166,6 +200,8 @@ impl pallet_deos_actors::WeightInfo for S /// Proof: `Oracle::Observations` (`max_values`: None, `max_size`: Some(61), added: 2536, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorObservationFeeds` (r:1 w:0) /// Proof: `Actors::ActorObservationFeeds` (`max_values`: None, `max_size`: Some(42), added: 2517, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:1) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingFeedMembershipCount` (r:1 w:1) /// Proof: `Actors::CrossingFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingUserFeedMembershipCount` (r:1 w:1) @@ -173,18 +209,30 @@ impl pallet_deos_actors::WeightInfo for S /// Storage: `Actors::CrossingLeafStates` (r:1 w:1) /// Proof: `Actors::CrossingLeafStates` (`max_values`: None, `max_size`: Some(62), added: 2537, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingMemberPages` (r:1 w:1) - /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(1096), added: 3571, mode: `MaxEncodedLen`) + /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(8264), added: 10739, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingRadixNodes` (r:32 w:32) /// Proof: `Actors::CrossingRadixNodes` (`max_values`: None, `max_size`: Some(53), added: 2528, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractTailChunk` (r:8 w:8) + /// Proof: `Actors::ActorContractTailChunk` (`max_values`: None, `max_size`: Some(4070), added: 6545, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorStateHolds` (r:1 w:1) + /// Proof: `Actors::ActorStateHolds` (`max_values`: None, `max_size`: Some(152), added: 2627, mode: `MaxEncodedLen`) + /// Storage: `Actors::ObservationSubscriptionSlot` (r:1 w:0) + /// Proof: `Actors::ObservationSubscriptionSlot` (`max_values`: None, `max_size`: Some(28), added: 2503, mode: `MaxEncodedLen`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(121), added: 2596, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorActivationAuthority` (r:0 w:1) + /// Proof: `Actors::ActorActivationAuthority` (`max_values`: None, `max_size`: Some(159), added: 2634, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:0 w:1) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) fn create_user_actor_at_slot() -> Weight { // Proof Size summary in bytes: - // Measured: `2083` + // Measured: `2404` // Estimated: `81886` - // Minimum execution time: 251_153_000 picoseconds. - Weight::from_parts(258_137_000, 0) + // Minimum execution time: 461_868_000 picoseconds. + Weight::from_parts(481_145_000, 0) .saturating_add(Weight::from_parts(0, 81886)) - .saturating_add(T::DbWeight::get().reads(54)) - .saturating_add(T::DbWeight::get().writes(47)) + .saturating_add(T::DbWeight::get().reads(65)) + .saturating_add(T::DbWeight::get().writes(60)) } /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) @@ -199,13 +247,13 @@ impl pallet_deos_actors::WeightInfo for S /// Storage: `Actors::ActorIdentities` (r:1 w:1) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorHot` (r:1 w:1) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorContract` (r:1 w:1) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:1 w:1) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorFunding` (r:1 w:1) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `MaxEncodedLen`) - /// Storage: `Actors::ContinuationState` (r:1 w:0) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:1 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) /// Storage: `Actors::SystemSovereigns` (r:1 w:1) /// Proof: `Actors::SystemSovereigns` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `MaxEncodedLen`) /// Storage: `Actors::SystemSovereignCount` (r:1 w:1) @@ -220,25 +268,35 @@ impl pallet_deos_actors::WeightInfo for S /// Proof: `Oracle::Observations` (`max_values`: None, `max_size`: Some(61), added: 2536, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorObservationFeeds` (r:1 w:0) /// Proof: `Actors::ActorObservationFeeds` (`max_values`: None, `max_size`: Some(42), added: 2517, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:1) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingFeedMembershipCount` (r:1 w:1) /// Proof: `Actors::CrossingFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingLeafStates` (r:1 w:1) /// Proof: `Actors::CrossingLeafStates` (`max_values`: None, `max_size`: Some(62), added: 2537, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingMemberPages` (r:1 w:1) - /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(1096), added: 3571, mode: `MaxEncodedLen`) + /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(8264), added: 10739, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingRadixNodes` (r:32 w:32) /// Proof: `Actors::CrossingRadixNodes` (`max_values`: None, `max_size`: Some(53), added: 2528, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractTailChunk` (r:8 w:8) + /// Proof: `Actors::ActorContractTailChunk` (`max_values`: None, `max_size`: Some(4070), added: 6545, mode: `MaxEncodedLen`) /// Storage: `System::Account` (r:1 w:1) /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorStateHolds` (r:1 w:0) + /// Proof: `Actors::ActorStateHolds` (`max_values`: None, `max_size`: Some(152), added: 2627, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorActivationAuthority` (r:0 w:1) + /// Proof: `Actors::ActorActivationAuthority` (`max_values`: None, `max_size`: Some(159), added: 2634, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:0 w:1) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) fn create_system_actor() -> Weight { // Proof Size summary in bytes: - // Measured: `2169` + // Measured: `2308` // Estimated: `81886` - // Minimum execution time: 204_778_000 picoseconds. - Weight::from_parts(210_295_000, 0) + // Minimum execution time: 520_396_000 picoseconds. + Weight::from_parts(541_837_000, 0) .saturating_add(Weight::from_parts(0, 81886)) - .saturating_add(T::DbWeight::get().reads(53)) - .saturating_add(T::DbWeight::get().writes(47)) + .saturating_add(T::DbWeight::get().reads(63)) + .saturating_add(T::DbWeight::get().writes(58)) } /// Storage: `Actors::SystemSovereigns` (r:1 w:1) /// Proof: `Actors::SystemSovereigns` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `MaxEncodedLen`) @@ -255,13 +313,13 @@ impl pallet_deos_actors::WeightInfo for S /// Storage: `Actors::ActorIdentities` (r:1 w:1) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorHot` (r:1 w:1) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorContract` (r:1 w:1) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:1 w:1) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorFunding` (r:1 w:1) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `MaxEncodedLen`) - /// Storage: `Actors::ContinuationState` (r:1 w:0) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:1 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) /// Storage: `Actors::SovereignIndex` (r:1 w:1) /// Proof: `Actors::SovereignIndex` (`max_values`: None, `max_size`: Some(56), added: 2531, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingMemberships` (r:1 w:1) @@ -272,23 +330,33 @@ impl pallet_deos_actors::WeightInfo for S /// Proof: `Oracle::Observations` (`max_values`: None, `max_size`: Some(61), added: 2536, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorObservationFeeds` (r:1 w:0) /// Proof: `Actors::ActorObservationFeeds` (`max_values`: None, `max_size`: Some(42), added: 2517, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:1) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingFeedMembershipCount` (r:1 w:1) /// Proof: `Actors::CrossingFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingLeafStates` (r:1 w:1) /// Proof: `Actors::CrossingLeafStates` (`max_values`: None, `max_size`: Some(62), added: 2537, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingMemberPages` (r:1 w:1) - /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(1096), added: 3571, mode: `MaxEncodedLen`) + /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(8264), added: 10739, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingRadixNodes` (r:32 w:32) /// Proof: `Actors::CrossingRadixNodes` (`max_values`: None, `max_size`: Some(53), added: 2528, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractTailChunk` (r:8 w:8) + /// Proof: `Actors::ActorContractTailChunk` (`max_values`: None, `max_size`: Some(4070), added: 6545, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorStateHolds` (r:1 w:0) + /// Proof: `Actors::ActorStateHolds` (`max_values`: None, `max_size`: Some(152), added: 2627, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorActivationAuthority` (r:0 w:1) + /// Proof: `Actors::ActorActivationAuthority` (`max_values`: None, `max_size`: Some(159), added: 2634, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:0 w:1) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) fn create_system_actor_at_sovereign_id() -> Weight { // Proof Size summary in bytes: - // Measured: `2219` + // Measured: `2372` // Estimated: `81886` - // Minimum execution time: 194_860_000 picoseconds. - Weight::from_parts(199_540_000, 0) + // Minimum execution time: 509_500_000 picoseconds. + Weight::from_parts(528_846_000, 0) .saturating_add(Weight::from_parts(0, 81886)) - .saturating_add(T::DbWeight::get().reads(51)) - .saturating_add(T::DbWeight::get().writes(45)) + .saturating_add(T::DbWeight::get().reads(61)) + .saturating_add(T::DbWeight::get().writes(56)) } /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) @@ -303,18 +371,18 @@ impl pallet_deos_actors::WeightInfo for S /// Storage: `Actors::ActorIdentities` (r:1 w:1) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorHot` (r:1 w:1) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorContract` (r:1 w:1) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:1 w:1) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorFunding` (r:1 w:1) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `MaxEncodedLen`) - /// Storage: `Actors::ContinuationState` (r:1 w:0) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:1 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) /// Storage: `Actors::OwnerSlotBitmaps` (r:1 w:1) /// Proof: `Actors::OwnerSlotBitmaps` (`max_values`: None, `max_size`: Some(80), added: 2555, mode: `MaxEncodedLen`) /// Storage: `Actors::SovereignIndex` (r:1 w:1) /// Proof: `Actors::SovereignIndex` (`max_values`: None, `max_size`: Some(56), added: 2531, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:2 w:1) + /// Storage: `System::Account` (r:1 w:1) /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingMemberships` (r:1 w:1) /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) @@ -324,6 +392,8 @@ impl pallet_deos_actors::WeightInfo for S /// Proof: `Oracle::Observations` (`max_values`: None, `max_size`: Some(61), added: 2536, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorObservationFeeds` (r:1 w:0) /// Proof: `Actors::ActorObservationFeeds` (`max_values`: None, `max_size`: Some(42), added: 2517, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:1) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingFeedMembershipCount` (r:1 w:1) /// Proof: `Actors::CrossingFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingUserFeedMembershipCount` (r:1 w:1) @@ -331,16 +401,28 @@ impl pallet_deos_actors::WeightInfo for S /// Storage: `Actors::CrossingLeafStates` (r:1 w:1) /// Proof: `Actors::CrossingLeafStates` (`max_values`: None, `max_size`: Some(62), added: 2537, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingMemberPages` (r:1 w:1) - /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(1096), added: 3571, mode: `MaxEncodedLen`) + /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(8264), added: 10739, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractTailChunk` (r:8 w:8) + /// Proof: `Actors::ActorContractTailChunk` (`max_values`: None, `max_size`: Some(4070), added: 6545, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorStateHolds` (r:1 w:1) + /// Proof: `Actors::ActorStateHolds` (`max_values`: None, `max_size`: Some(152), added: 2627, mode: `MaxEncodedLen`) + /// Storage: `Actors::ObservationSubscriptionSlot` (r:1 w:0) + /// Proof: `Actors::ObservationSubscriptionSlot` (`max_values`: None, `max_size`: Some(28), added: 2503, mode: `MaxEncodedLen`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(121), added: 2596, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorActivationAuthority` (r:0 w:1) + /// Proof: `Actors::ActorActivationAuthority` (`max_values`: None, `max_size`: Some(159), added: 2634, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:0 w:1) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) fn create_user_actor_crossing_new_page() -> Weight { // Proof Size summary in bytes: - // Measured: `6535` - // Estimated: `12200` - // Minimum execution time: 237_395_000 picoseconds. - Weight::from_parts(254_785_000, 0) - .saturating_add(Weight::from_parts(0, 12200)) - .saturating_add(T::DbWeight::get().reads(22)) - .saturating_add(T::DbWeight::get().writes(15)) + // Measured: `15158` + // Estimated: `53350` + // Minimum execution time: 515_646_000 picoseconds. + Weight::from_parts(540_999_000, 0) + .saturating_add(Weight::from_parts(0, 53350)) + .saturating_add(T::DbWeight::get().reads(33)) + .saturating_add(T::DbWeight::get().writes(28)) } /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) @@ -351,13 +433,13 @@ impl pallet_deos_actors::WeightInfo for S /// Storage: `Actors::ActorIdentities` (r:1 w:1) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorHot` (r:1 w:0) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorContract` (r:1 w:0) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorFunding` (r:1 w:0) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `MaxEncodedLen`) - /// Storage: `Actors::ContinuationState` (r:1 w:0) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:1 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) /// Storage: `Actors::SystemSovereigns` (r:1 w:1) /// Proof: `Actors::SystemSovereigns` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `MaxEncodedLen`) /// Storage: `Actors::SystemSovereignCount` (r:1 w:1) @@ -366,26 +448,28 @@ impl pallet_deos_actors::WeightInfo for S /// Proof: `Actors::SovereignIndex` (`max_values`: None, `max_size`: Some(56), added: 2531, mode: `MaxEncodedLen`) /// Storage: `System::Account` (r:1 w:1) /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorStateHolds` (r:1 w:0) + /// Proof: `Actors::ActorStateHolds` (`max_values`: None, `max_size`: Some(152), added: 2627, mode: `MaxEncodedLen`) fn create_dormant_system_actor() -> Weight { // Proof Size summary in bytes: - // Measured: `1687` - // Estimated: `12200` - // Minimum execution time: 63_417_000 picoseconds. - Weight::from_parts(65_861_000, 0) - .saturating_add(Weight::from_parts(0, 12200)) - .saturating_add(T::DbWeight::get().reads(12)) + // Measured: `1699` + // Estimated: `5736` + // Minimum execution time: 68_166_000 picoseconds. + Weight::from_parts(70_751_000, 0) + .saturating_add(Weight::from_parts(0, 5736)) + .saturating_add(T::DbWeight::get().reads(13)) .saturating_add(T::DbWeight::get().writes(7)) } /// Storage: `Actors::ActorIdentities` (r:1 w:1) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorHot` (r:1 w:1) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorContract` (r:1 w:1) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:1 w:1) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorFunding` (r:1 w:1) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `MaxEncodedLen`) - /// Storage: `Actors::ContinuationState` (r:1 w:0) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:1 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) /// Storage: `Actors::ActiveActorCount` (r:1 w:1) @@ -400,48 +484,66 @@ impl pallet_deos_actors::WeightInfo for S /// Proof: `Oracle::Observations` (`max_values`: None, `max_size`: Some(61), added: 2536, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorObservationFeeds` (r:1 w:0) /// Proof: `Actors::ActorObservationFeeds` (`max_values`: None, `max_size`: Some(42), added: 2517, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:1) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingFeedMembershipCount` (r:1 w:1) /// Proof: `Actors::CrossingFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingLeafStates` (r:1 w:1) /// Proof: `Actors::CrossingLeafStates` (`max_values`: None, `max_size`: Some(62), added: 2537, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingMemberPages` (r:1 w:1) - /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(1096), added: 3571, mode: `MaxEncodedLen`) + /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(8264), added: 10739, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingRadixNodes` (r:32 w:32) /// Proof: `Actors::CrossingRadixNodes` (`max_values`: None, `max_size`: Some(53), added: 2528, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractTailChunk` (r:8 w:8) + /// Proof: `Actors::ActorContractTailChunk` (`max_values`: None, `max_size`: Some(4070), added: 6545, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorStateHolds` (r:1 w:0) + /// Proof: `Actors::ActorStateHolds` (`max_values`: None, `max_size`: Some(152), added: 2627, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorActivationAuthority` (r:0 w:1) + /// Proof: `Actors::ActorActivationAuthority` (`max_values`: None, `max_size`: Some(159), added: 2634, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:0 w:1) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) fn activate_actor() -> Weight { // Proof Size summary in bytes: - // Measured: `1890` + // Measured: `2347` // Estimated: `81886` - // Minimum execution time: 185_012_000 picoseconds. - Weight::from_parts(190_181_000, 0) + // Minimum execution time: 499_932_000 picoseconds. + Weight::from_parts(520_675_000, 0) .saturating_add(Weight::from_parts(0, 81886)) - .saturating_add(T::DbWeight::get().reads(47)) - .saturating_add(T::DbWeight::get().writes(41)) + .saturating_add(T::DbWeight::get().reads(57)) + .saturating_add(T::DbWeight::get().writes(52)) } /// Storage: `Actors::ActorIdentities` (r:1 w:1) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorHot` (r:1 w:1) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorContract` (r:1 w:1) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:1 w:1) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:1) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractTailChunk` (r:8 w:8) + /// Proof: `Actors::ActorContractTailChunk` (`max_values`: None, `max_size`: Some(4070), added: 6545, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorFunding` (r:1 w:1) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `MaxEncodedLen`) - /// Storage: `Actors::ContinuationState` (r:1 w:1) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:1 w:1) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunPayload` (r:1 w:1) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(2533), added: 5008, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorObservationFeeds` (r:1 w:0) /// Proof: `Actors::ActorObservationFeeds` (`max_values`: None, `max_size`: Some(42), added: 2517, mode: `MaxEncodedLen`) /// Storage: `Actors::ObservationSubscriptionSlot` (r:1 w:0) /// Proof: `Actors::ObservationSubscriptionSlot` (`max_values`: None, `max_size`: Some(28), added: 2503, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingMemberships` (r:1 w:1) /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorStateHolds` (r:1 w:0) + /// Proof: `Actors::ActorStateHolds` (`max_values`: None, `max_size`: Some(152), added: 2627, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingLeafStates` (r:1 w:1) /// Proof: `Actors::CrossingLeafStates` (`max_values`: None, `max_size`: Some(62), added: 2537, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingMemberPages` (r:1 w:1) - /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(1096), added: 3571, mode: `MaxEncodedLen`) + /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(8264), added: 10739, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingFeedMembershipCount` (r:1 w:1) /// Proof: `Actors::CrossingFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingTransitionQueues` (r:1 w:0) - /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(2595), added: 5070, mode: `MaxEncodedLen`) + /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(3171), added: 5646, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingPendingFeeds` (r:1 w:0) /// Proof: `Actors::CrossingPendingFeeds` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingRadixNodes` (r:32 w:32) @@ -450,78 +552,84 @@ impl pallet_deos_actors::WeightInfo for S /// Proof: `Actors::ActiveActorCount` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingRangeCursors` (r:0 w:1) /// Proof: `Actors::CrossingRangeCursors` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorActivationAuthority` (r:0 w:1) + /// Proof: `Actors::ActorActivationAuthority` (`max_values`: None, `max_size`: Some(159), added: 2634, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:0 w:1) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) fn deactivate_actor() -> Weight { // Proof Size summary in bytes: - // Measured: `3961` + // Measured: `8967` // Estimated: `81886` - // Minimum execution time: 303_326_000 picoseconds. - Weight::from_parts(312_475_000, 0) + // Minimum execution time: 543_094_000 picoseconds. + Weight::from_parts(561_742_000, 0) .saturating_add(Weight::from_parts(0, 81886)) - .saturating_add(T::DbWeight::get().reads(46)) - .saturating_add(T::DbWeight::get().writes(43)) + .saturating_add(T::DbWeight::get().reads(57)) + .saturating_add(T::DbWeight::get().writes(55)) } /// Storage: `Actors::ActorIdentities` (r:1 w:1) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorHot` (r:1 w:1) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorContract` (r:1 w:0) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorFunding` (r:1 w:0) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `MaxEncodedLen`) - /// Storage: `Actors::ContinuationState` (r:1 w:0) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:1 w:0) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:1 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) fn pause_actor() -> Weight { // Proof Size summary in bytes: - // Measured: `1490` - // Estimated: `12200` - // Minimum execution time: 57_201_000 picoseconds. - Weight::from_parts(59_506_000, 0) - .saturating_add(Weight::from_parts(0, 12200)) + // Measured: `1825` + // Estimated: `5736` + // Minimum execution time: 67_537_000 picoseconds. + Weight::from_parts(70_541_000, 0) + .saturating_add(Weight::from_parts(0, 5736)) .saturating_add(T::DbWeight::get().reads(7)) .saturating_add(T::DbWeight::get().writes(2)) } /// Storage: `Actors::ActorIdentities` (r:1 w:1) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorHot` (r:1 w:1) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorContract` (r:1 w:0) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorFunding` (r:1 w:0) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `MaxEncodedLen`) - /// Storage: `Actors::ContinuationState` (r:1 w:0) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:1 w:0) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:1 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) fn resume_actor() -> Weight { // Proof Size summary in bytes: - // Measured: `1490` - // Estimated: `12200` - // Minimum execution time: 57_481_000 picoseconds. - Weight::from_parts(59_506_000, 0) - .saturating_add(Weight::from_parts(0, 12200)) + // Measured: `1825` + // Estimated: `5736` + // Minimum execution time: 67_188_000 picoseconds. + Weight::from_parts(69_214_000, 0) + .saturating_add(Weight::from_parts(0, 5736)) .saturating_add(T::DbWeight::get().reads(7)) .saturating_add(T::DbWeight::get().writes(2)) } /// Storage: `Actors::ActorIdentities` (r:1 w:0) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorHot` (r:1 w:1) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorContract` (r:1 w:0) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorFunding` (r:1 w:0) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `MaxEncodedLen`) - /// Storage: `Actors::ContinuationState` (r:1 w:0) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:1 w:0) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:1 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) /// Storage: `Actors::QueueHead` (r:1 w:0) /// Proof: `Actors::QueueHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `Actors::QueueTail` (r:1 w:1) @@ -529,29 +637,117 @@ impl pallet_deos_actors::WeightInfo for S /// Storage: `Actors::QueueOccupancy` (r:1 w:1) /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `Actors::QueuePages` (r:1 w:1) - /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(1050), added: 3525, mode: `MaxEncodedLen`) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `MaxEncodedLen`) /// Storage: `Actors::NextQueueTicket` (r:1 w:1) /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) fn manual_trigger() -> Weight { // Proof Size summary in bytes: - // Measured: `1743` - // Estimated: `12200` - // Minimum execution time: 80_598_000 picoseconds. - Weight::from_parts(82_764_000, 0) - .saturating_add(Weight::from_parts(0, 12200)) - .saturating_add(T::DbWeight::get().reads(12)) - .saturating_add(T::DbWeight::get().writes(5)) + // Measured: `2425` + // Estimated: `9635` + // Minimum execution time: 150_231_000 picoseconds. + Weight::from_parts(155_120_000, 0) + .saturating_add(Weight::from_parts(0, 9635)) + .saturating_add(T::DbWeight::get().reads(14)) + .saturating_add(T::DbWeight::get().writes(7)) + } + /// Storage: `Actors::ActorIdentities` (r:1 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `Measured`) + /// Storage: `Actors::ActorHot` (r:1 w:1) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `Measured`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `Measured`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `Measured`) + /// Storage: `Actors::ActorFunding` (r:1 w:0) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `Measured`) + /// Storage: `Actors::ActorRunHead` (r:1 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `Measured`) + /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) + /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `Measured`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `Measured`) + /// Storage: `Actors::QueueHead` (r:1 w:0) + /// Proof: `Actors::QueueHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::QueueTail` (r:1 w:1) + /// Proof: `Actors::QueueTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::QueueOccupancy` (r:1 w:1) + /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `Measured`) + /// Storage: `Actors::QueuePages` (r:1 w:1) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `Measured`) + /// Storage: `Actors::NextQueueTicket` (r:1 w:1) + /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + fn address_event_trigger_occurrence() -> Weight { + // Proof Size summary in bytes: + // Measured: `2427` + // Estimated: `8367` + // Minimum execution time: 164_129_000 picoseconds. + Weight::from_parts(171_114_000, 0) + .saturating_add(Weight::from_parts(0, 8367)) + .saturating_add(T::DbWeight::get().reads(14)) + .saturating_add(T::DbWeight::get().writes(7)) + } + /// Storage: `Actors::ActorIdentities` (r:1 w:1) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorHot` (r:1 w:1) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:1 w:1) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:1) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorFunding` (r:1 w:1) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:1 w:1) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActiveActorCount` (r:1 w:1) + /// Proof: `Actors::ActiveActorCount` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorIdentityCount` (r:1 w:1) + /// Proof: `Actors::ActorIdentityCount` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Actors::SovereignIndex` (r:1 w:1) + /// Proof: `Actors::SovereignIndex` (`max_values`: None, `max_size`: Some(56), added: 2531, mode: `MaxEncodedLen`) + /// Storage: `Actors::OwnerSlotBitmaps` (r:1 w:1) + /// Proof: `Actors::OwnerSlotBitmaps` (`max_values`: None, `max_size`: Some(80), added: 2555, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorObservationFeeds` (r:1 w:0) + /// Proof: `Actors::ActorObservationFeeds` (`max_values`: None, `max_size`: Some(42), added: 2517, mode: `MaxEncodedLen`) + /// Storage: `Actors::ObservationSubscriptionSlot` (r:1 w:0) + /// Proof: `Actors::ObservationSubscriptionSlot` (`max_values`: None, `max_size`: Some(28), added: 2503, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingMemberships` (r:1 w:0) + /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorStateHolds` (r:1 w:1) + /// Proof: `Actors::ActorStateHolds` (`max_values`: None, `max_size`: Some(152), added: 2627, mode: `MaxEncodedLen`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(121), added: 2596, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunPayload` (r:0 w:1) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(2533), added: 5008, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorActivationAuthority` (r:0 w:1) + /// Proof: `Actors::ActorActivationAuthority` (`max_values`: None, `max_size`: Some(159), added: 2634, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:0 w:1) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) + fn pipeline_admission_apoptosis() -> Weight { + // Proof Size summary in bytes: + // Measured: `2534` + // Estimated: `5736` + // Minimum execution time: 136_542_000 picoseconds. + Weight::from_parts(140_872_000, 0) + .saturating_add(Weight::from_parts(0, 5736)) + .saturating_add(T::DbWeight::get().reads(15)) + .saturating_add(T::DbWeight::get().writes(15)) } /// Storage: `Actors::ActorIdentities` (r:1 w:1) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorHot` (r:1 w:1) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorContract` (r:1 w:1) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:1 w:1) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:1) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractTailChunk` (r:8 w:8) + /// Proof: `Actors::ActorContractTailChunk` (`max_values`: None, `max_size`: Some(4070), added: 6545, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorFunding` (r:1 w:1) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `MaxEncodedLen`) - /// Storage: `Actors::ContinuationState` (r:1 w:1) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:1 w:1) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunPayload` (r:1 w:1) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(2533), added: 5008, mode: `MaxEncodedLen`) /// Storage: `Actors::ActiveActorCount` (r:1 w:1) /// Proof: `Actors::ActiveActorCount` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorIdentityCount` (r:1 w:1) @@ -566,16 +762,22 @@ impl pallet_deos_actors::WeightInfo for S /// Proof: `Actors::ObservationSubscriptionSlot` (`max_values`: None, `max_size`: Some(28), added: 2503, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingMemberships` (r:1 w:1) /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorStateHolds` (r:1 w:1) + /// Proof: `Actors::ActorStateHolds` (`max_values`: None, `max_size`: Some(152), added: 2627, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorActivationAuthority` (r:1 w:1) + /// Proof: `Actors::ActorActivationAuthority` (`max_values`: None, `max_size`: Some(159), added: 2634, mode: `MaxEncodedLen`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(121), added: 2596, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingLeafStates` (r:1 w:1) /// Proof: `Actors::CrossingLeafStates` (`max_values`: None, `max_size`: Some(62), added: 2537, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingMemberPages` (r:1 w:1) - /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(1096), added: 3571, mode: `MaxEncodedLen`) + /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(8264), added: 10739, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingFeedMembershipCount` (r:1 w:1) /// Proof: `Actors::CrossingFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingUserFeedMembershipCount` (r:1 w:1) /// Proof: `Actors::CrossingUserFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingTransitionQueues` (r:1 w:1) - /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(2595), added: 5070, mode: `MaxEncodedLen`) + /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(3171), added: 5646, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingPendingFeeds` (r:1 w:1) /// Proof: `Actors::CrossingPendingFeeds` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:1) @@ -584,28 +786,34 @@ impl pallet_deos_actors::WeightInfo for S /// Proof: `Actors::CrossingRadixNodes` (`max_values`: None, `max_size`: Some(53), added: 2528, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingRangeCursors` (r:0 w:1) /// Proof: `Actors::CrossingRangeCursors` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:0 w:1) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) fn close_actor() -> Weight { // Proof Size summary in bytes: - // Measured: `4627` + // Measured: `9691` // Estimated: `81886` - // Minimum execution time: 345_581_000 picoseconds. - Weight::from_parts(355_707_000, 0) + // Minimum execution time: 683_757_000 picoseconds. + Weight::from_parts(708_551_000, 0) .saturating_add(Weight::from_parts(0, 81886)) - .saturating_add(T::DbWeight::get().reads(51)) - .saturating_add(T::DbWeight::get().writes(50)) + .saturating_add(T::DbWeight::get().reads(64)) + .saturating_add(T::DbWeight::get().writes(64)) } /// Storage: `Actors::ActorIdentities` (r:1 w:1) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorHot` (r:1 w:1) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorContract` (r:1 w:1) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:1 w:1) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:1) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractTailChunk` (r:8 w:8) + /// Proof: `Actors::ActorContractTailChunk` (`max_values`: None, `max_size`: Some(4070), added: 6545, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorFunding` (r:1 w:1) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `MaxEncodedLen`) - /// Storage: `Actors::ContinuationState` (r:1 w:1) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:1 w:0) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:1 w:1) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunPayload` (r:1 w:1) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(2533), added: 5008, mode: `MaxEncodedLen`) /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingMemberships` (r:1 w:1) @@ -616,16 +824,24 @@ impl pallet_deos_actors::WeightInfo for S /// Proof: `Oracle::Observations` (`max_values`: None, `max_size`: Some(61), added: 2536, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorObservationFeeds` (r:1 w:0) /// Proof: `Actors::ActorObservationFeeds` (`max_values`: None, `max_size`: Some(42), added: 2517, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorStateHolds` (r:1 w:1) + /// Proof: `Actors::ActorStateHolds` (`max_values`: None, `max_size`: Some(152), added: 2627, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorActivationAuthority` (r:1 w:1) + /// Proof: `Actors::ActorActivationAuthority` (`max_values`: None, `max_size`: Some(159), added: 2634, mode: `MaxEncodedLen`) + /// Storage: `Actors::ObservationSubscriptionSlot` (r:1 w:0) + /// Proof: `Actors::ObservationSubscriptionSlot` (`max_values`: None, `max_size`: Some(28), added: 2503, mode: `MaxEncodedLen`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(121), added: 2596, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingLeafStates` (r:2 w:2) /// Proof: `Actors::CrossingLeafStates` (`max_values`: None, `max_size`: Some(62), added: 2537, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingMemberPages` (r:2 w:2) - /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(1096), added: 3571, mode: `MaxEncodedLen`) + /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(8264), added: 10739, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingFeedMembershipCount` (r:1 w:1) /// Proof: `Actors::CrossingFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingUserFeedMembershipCount` (r:1 w:1) /// Proof: `Actors::CrossingUserFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingTransitionQueues` (r:1 w:0) - /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(2595), added: 5070, mode: `MaxEncodedLen`) + /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(3171), added: 5646, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingPendingFeeds` (r:1 w:0) /// Proof: `Actors::CrossingPendingFeeds` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingRadixNodes` (r:32 w:32) @@ -640,15 +856,17 @@ impl pallet_deos_actors::WeightInfo for S /// Proof: `Actors::WakeupPages` (`max_values`: None, `max_size`: Some(348), added: 2823, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingRangeCursors` (r:0 w:1) /// Proof: `Actors::CrossingRangeCursors` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:0 w:1) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) fn update_contract() -> Weight { // Proof Size summary in bytes: - // Measured: `4939` + // Measured: `10450` // Estimated: `81886` - // Minimum execution time: 439_169_000 picoseconds. - Weight::from_parts(457_049_000, 0) + // Minimum execution time: 949_927_000 picoseconds. + Weight::from_parts(984_638_000, 0) .saturating_add(Weight::from_parts(0, 81886)) - .saturating_add(T::DbWeight::get().reads(54)) - .saturating_add(T::DbWeight::get().writes(49)) + .saturating_add(T::DbWeight::get().reads(67)) + .saturating_add(T::DbWeight::get().writes(63)) } /// Storage: `Actors::GlobalCircuitBreaker` (r:0 w:1) /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) @@ -656,32 +874,72 @@ impl pallet_deos_actors::WeightInfo for S // Proof Size summary in bytes: // Measured: `0` // Estimated: `0` - // Minimum execution time: 5_587_000 picoseconds. - Weight::from_parts(7_124_000, 0) + // Minimum execution time: 5_238_000 picoseconds. + Weight::from_parts(6_845_000, 0) .saturating_add(Weight::from_parts(0, 0)) .saturating_add(T::DbWeight::get().writes(1)) } /// Storage: `Actors::CrossingWorkerFaultState` (r:1 w:1) /// Proof: `Actors::CrossingWorkerFaultState` (`max_values`: Some(1), `max_size`: Some(44), added: 539, mode: `MaxEncodedLen`) + fn record_crossing_worker_fault() -> Weight { + // Proof Size summary in bytes: + // Measured: `421` + // Estimated: `1529` + // Minimum execution time: 9_499_000 picoseconds. + Weight::from_parts(11_105_000, 0) + .saturating_add(Weight::from_parts(0, 1529)) + .saturating_add(T::DbWeight::get().reads(1)) + .saturating_add(T::DbWeight::get().writes(1)) + } + /// Storage: `Actors::ObservationSubscriberPages` (r:1 w:0) + /// Proof: `Actors::ObservationSubscriberPages` (`max_values`: None, `max_size`: Some(641), added: 3116, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorActivationAuthority` (r:1 w:0) + /// Proof: `Actors::ActorActivationAuthority` (`max_values`: None, `max_size`: Some(159), added: 2634, mode: `MaxEncodedLen`) + /// Storage: `Actors::ObservationFanoutWorkerFaultState` (r:1 w:1) + /// Proof: `Actors::ObservationFanoutWorkerFaultState` (`max_values`: Some(1), `max_size`: Some(144), added: 639, mode: `MaxEncodedLen`) + fn record_observation_fanout_worker_fault() -> Weight { + // Proof Size summary in bytes: + // Measured: `709` + // Estimated: `4106` + // Minimum execution time: 18_648_000 picoseconds. + Weight::from_parts(20_464_000, 0) + .saturating_add(Weight::from_parts(0, 4106)) + .saturating_add(T::DbWeight::get().reads(3)) + .saturating_add(T::DbWeight::get().writes(1)) + } + /// Storage: `Actors::WakeupWorkerFaultState` (r:1 w:1) + /// Proof: `Actors::WakeupWorkerFaultState` (`max_values`: Some(1), `max_size`: Some(18), added: 513, mode: `MaxEncodedLen`) + fn record_wakeup_worker_fault() -> Weight { + // Proof Size summary in bytes: + // Measured: `438` + // Estimated: `1503` + // Minimum execution time: 8_451_000 picoseconds. + Weight::from_parts(10_057_000, 0) + .saturating_add(Weight::from_parts(0, 1503)) + .saturating_add(T::DbWeight::get().reads(1)) + .saturating_add(T::DbWeight::get().writes(1)) + } + /// Storage: `Actors::CrossingWorkerFaultState` (r:1 w:1) + /// Proof: `Actors::CrossingWorkerFaultState` (`max_values`: Some(1), `max_size`: Some(44), added: 539, mode: `MaxEncodedLen`) fn clear_crossing_worker_fault() -> Weight { // Proof Size summary in bytes: - // Measured: `448` + // Measured: `459` // Estimated: `1529` - // Minimum execution time: 12_502_000 picoseconds. - Weight::from_parts(14_248_000, 0) + // Minimum execution time: 11_943_000 picoseconds. + Weight::from_parts(13_759_000, 0) .saturating_add(Weight::from_parts(0, 1529)) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) } /// Storage: `Actors::ObservationFanoutWorkerFaultState` (r:1 w:1) - /// Proof: `Actors::ObservationFanoutWorkerFaultState` (`max_values`: Some(1), `max_size`: Some(31), added: 526, mode: `MaxEncodedLen`) + /// Proof: `Actors::ObservationFanoutWorkerFaultState` (`max_values`: Some(1), `max_size`: Some(144), added: 639, mode: `MaxEncodedLen`) fn clear_observation_fanout_worker_fault() -> Weight { // Proof Size summary in bytes: - // Measured: `433` - // Estimated: `1516` - // Minimum execution time: 12_082_000 picoseconds. - Weight::from_parts(13_828_000, 0) - .saturating_add(Weight::from_parts(0, 1516)) + // Measured: `454` + // Estimated: `1629` + // Minimum execution time: 12_222_000 picoseconds. + Weight::from_parts(13_968_000, 0) + .saturating_add(Weight::from_parts(0, 1629)) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) } @@ -689,10 +947,10 @@ impl pallet_deos_actors::WeightInfo for S /// Proof: `Actors::WakeupWorkerFaultState` (`max_values`: Some(1), `max_size`: Some(18), added: 513, mode: `MaxEncodedLen`) fn clear_wakeup_worker_fault() -> Weight { // Proof Size summary in bytes: - // Measured: `454` + // Measured: `431` // Estimated: `1503` - // Minimum execution time: 11_315_000 picoseconds. - Weight::from_parts(12_991_000, 0) + // Minimum execution time: 10_826_000 picoseconds. + Weight::from_parts(12_501_000, 0) .saturating_add(Weight::from_parts(0, 1503)) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) @@ -703,48 +961,48 @@ impl pallet_deos_actors::WeightInfo for S /// Proof: `Actors::ActiveActorLimit` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) fn set_active_actor_limit() -> Weight { // Proof Size summary in bytes: - // Measured: `383` + // Measured: `421` // Estimated: `1489` - // Minimum execution time: 7_683_000 picoseconds. - Weight::from_parts(9_429_000, 0) + // Minimum execution time: 8_032_000 picoseconds. + Weight::from_parts(9_918_000, 0) .saturating_add(Weight::from_parts(0, 1489)) .saturating_add(T::DbWeight::get().reads(2)) } /// Storage: `Actors::ActorIdentities` (r:1 w:0) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorHot` (r:1 w:0) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorContract` (r:1 w:0) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorFunding` (r:1 w:0) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `MaxEncodedLen`) - /// Storage: `Actors::ContinuationState` (r:1 w:0) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:1 w:0) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:1 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) fn permissionless_sweep() -> Weight { // Proof Size summary in bytes: - // Measured: `1490` - // Estimated: `12200` - // Minimum execution time: 40_509_000 picoseconds. - Weight::from_parts(42_465_000, 0) - .saturating_add(Weight::from_parts(0, 12200)) + // Measured: `1825` + // Estimated: `5736` + // Minimum execution time: 46_305_000 picoseconds. + Weight::from_parts(48_610_000, 0) + .saturating_add(Weight::from_parts(0, 5736)) .saturating_add(T::DbWeight::get().reads(7)) } /// Storage: `Actors::ActorIdentities` (r:5 w:5) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorHot` (r:5 w:5) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorContract` (r:5 w:5) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:5 w:5) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:5 w:5) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorFunding` (r:5 w:5) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `MaxEncodedLen`) - /// Storage: `Actors::ContinuationState` (r:5 w:5) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:10 w:5) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:5 w:5) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) /// Storage: `Actors::ActiveActorCount` (r:1 w:1) @@ -761,21 +1019,33 @@ impl pallet_deos_actors::WeightInfo for S /// Proof: `Actors::ObservationSubscriptionSlot` (`max_values`: None, `max_size`: Some(28), added: 2503, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingMemberships` (r:5 w:0) /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorStateHolds` (r:5 w:5) + /// Proof: `Actors::ActorStateHolds` (`max_values`: None, `max_size`: Some(152), added: 2627, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:5 w:5) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Balances::Holds` (r:5 w:5) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(121), added: 2596, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunPayload` (r:0 w:5) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(2533), added: 5008, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorActivationAuthority` (r:0 w:5) + /// Proof: `Actors::ActorActivationAuthority` (`max_values`: None, `max_size`: Some(159), added: 2634, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:0 w:5) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) /// The range of component `n` is `[1, 5]`. fn permissionless_sweep_many(n: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `1127 + n * (706 ±0)` - // Estimated: `1489 + n * (11210 ±0)` - // Minimum execution time: 104_484_000 picoseconds. - Weight::from_parts(20_599_986, 0) + // Measured: `1291 + n * (1222 ±0)` + // Estimated: `1489 + n * (4746 ±0)` + // Minimum execution time: 158_752_000 picoseconds. + Weight::from_parts(26_061_500, 0) .saturating_add(Weight::from_parts(0, 1489)) - // Standard Error: 44_918 - .saturating_add(Weight::from_parts(87_291_781, 0).saturating_mul(n.into())) + // Standard Error: 74_200 + .saturating_add(Weight::from_parts(140_666_863, 0).saturating_mul(n.into())) .saturating_add(T::DbWeight::get().reads(3)) - .saturating_add(T::DbWeight::get().reads((12_u64).saturating_mul(n.into()))) + .saturating_add(T::DbWeight::get().reads((14_u64).saturating_mul(n.into()))) .saturating_add(T::DbWeight::get().writes(2)) - .saturating_add(T::DbWeight::get().writes((8_u64).saturating_mul(n.into()))) - .saturating_add(Weight::from_parts(0, 11210).saturating_mul(n.into())) + .saturating_add(T::DbWeight::get().writes((14_u64).saturating_mul(n.into()))) + .saturating_add(Weight::from_parts(0, 4746).saturating_mul(n.into())) } /// Storage: `System::Account` (r:1 w:1) /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) @@ -783,30 +1053,34 @@ impl pallet_deos_actors::WeightInfo for S // Proof Size summary in bytes: // Measured: `207` // Estimated: `3593` - // Minimum execution time: 41_067_000 picoseconds. - Weight::from_parts(43_022_000, 0) + // Minimum execution time: 41_557_000 picoseconds. + Weight::from_parts(43_512_000, 0) .saturating_add(Weight::from_parts(0, 3593)) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) } /// Storage: `Actors::SovereignIndex` (r:1 w:0) /// Proof: `Actors::SovereignIndex` (`max_values`: None, `max_size`: Some(56), added: 2531, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorIdentities` (r:1 w:0) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorHot` (r:1 w:1) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorContract` (r:1 w:0) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorFunding` (r:1 w:1) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `MaxEncodedLen`) - /// Storage: `Actors::ContinuationState` (r:1 w:0) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:1 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:2 w:2) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) /// Storage: `Assets::Asset` (r:1 w:0) /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorStateHolds` (r:1 w:0) + /// Proof: `Actors::ActorStateHolds` (`max_values`: None, `max_size`: Some(152), added: 2627, mode: `MaxEncodedLen`) /// Storage: `Actors::QueueHead` (r:1 w:0) /// Proof: `Actors::QueueHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `Actors::QueueTail` (r:1 w:0) @@ -814,7 +1088,9 @@ impl pallet_deos_actors::WeightInfo for S /// Storage: `Actors::QueueOccupancy` (r:1 w:0) /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `Actors::QueuePages` (r:2 w:0) - /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(1050), added: 3525, mode: `MaxEncodedLen`) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `MaxEncodedLen`) + /// Storage: `Actors::NextQueueTicket` (r:1 w:0) + /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `Actors::WakeupBuckets` (r:1 w:1) /// Proof: `Actors::WakeupBuckets` (`max_values`: None, `max_size`: Some(58), added: 2533, mode: `MaxEncodedLen`) /// Storage: `Actors::WakeupCursorLen` (r:1 w:1) @@ -825,12 +1101,12 @@ impl pallet_deos_actors::WeightInfo for S /// Proof: `Actors::WakeupPages` (`max_values`: None, `max_size`: Some(348), added: 2823, mode: `MaxEncodedLen`) fn task_transfer() -> Weight { // Proof Size summary in bytes: - // Measured: `5350` - // Estimated: `12200` - // Minimum execution time: 215_324_000 picoseconds. - Weight::from_parts(226_429_000, 0) - .saturating_add(Weight::from_parts(0, 12200)) - .saturating_add(T::DbWeight::get().reads(18)) + // Measured: `12551` + // Estimated: `18280` + // Minimum execution time: 324_628_000 picoseconds. + Weight::from_parts(338_037_000, 0) + .saturating_add(Weight::from_parts(0, 18280)) + .saturating_add(T::DbWeight::get().reads(21)) .saturating_add(T::DbWeight::get().writes(8)) } /// Storage: `System::Account` (r:1 w:1) @@ -847,20 +1123,22 @@ impl pallet_deos_actors::WeightInfo for S } /// Storage: `Actors::SovereignIndex` (r:1 w:0) /// Proof: `Actors::SovereignIndex` (`max_values`: None, `max_size`: Some(56), added: 2531, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorIdentities` (r:1 w:0) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorHot` (r:1 w:1) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorContract` (r:1 w:0) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorFunding` (r:1 w:0) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `MaxEncodedLen`) - /// Storage: `Actors::ContinuationState` (r:1 w:0) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:1 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) /// Storage: `Actors::QueueHead` (r:1 w:0) /// Proof: `Actors::QueueHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `Actors::QueueTail` (r:1 w:0) @@ -868,7 +1146,9 @@ impl pallet_deos_actors::WeightInfo for S /// Storage: `Actors::QueueOccupancy` (r:1 w:0) /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `Actors::QueuePages` (r:2 w:0) - /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(1050), added: 3525, mode: `MaxEncodedLen`) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `MaxEncodedLen`) + /// Storage: `Actors::NextQueueTicket` (r:1 w:0) + /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `Actors::WakeupBuckets` (r:1 w:1) /// Proof: `Actors::WakeupBuckets` (`max_values`: None, `max_size`: Some(58), added: 2533, mode: `MaxEncodedLen`) /// Storage: `Actors::WakeupCursorLen` (r:1 w:1) @@ -879,12 +1159,12 @@ impl pallet_deos_actors::WeightInfo for S /// Proof: `Actors::WakeupPages` (`max_values`: None, `max_size`: Some(348), added: 2823, mode: `MaxEncodedLen`) fn task_mint() -> Weight { // Proof Size summary in bytes: - // Measured: `4441` - // Estimated: `12200` - // Minimum execution time: 181_032_000 picoseconds. - Weight::from_parts(191_089_000, 0) - .saturating_add(Weight::from_parts(0, 12200)) - .saturating_add(T::DbWeight::get().reads(16)) + // Measured: `11325` + // Estimated: `18280` + // Minimum execution time: 274_411_000 picoseconds. + Weight::from_parts(292_011_000, 0) + .saturating_add(Weight::from_parts(0, 18280)) + .saturating_add(T::DbWeight::get().reads(18)) .saturating_add(T::DbWeight::get().writes(6)) } /// Storage: `System::Account` (r:1 w:0) @@ -898,11 +1178,11 @@ impl pallet_deos_actors::WeightInfo for S // Proof Size summary in bytes: // Measured: `0 + c * (488 ±0)` // Estimated: `3675 + c * (674 ±18)` - // Minimum execution time: 6_215_000 picoseconds. - Weight::from_parts(7_543_000, 0) + // Minimum execution time: 6_146_000 picoseconds. + Weight::from_parts(7_264_000, 0) .saturating_add(Weight::from_parts(0, 3675)) - // Standard Error: 13_559 - .saturating_add(Weight::from_parts(6_543_854, 0).saturating_mul(c.into())) + // Standard Error: 13_269 + .saturating_add(Weight::from_parts(6_569_170, 0).saturating_mul(c.into())) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(c.into()))) .saturating_add(Weight::from_parts(0, 674).saturating_mul(c.into())) @@ -912,27 +1192,31 @@ impl pallet_deos_actors::WeightInfo for S // Measured: `0` // Estimated: `0` // Minimum execution time: 2_793_000 picoseconds. - Weight::from_parts(4_330_000, 0) + Weight::from_parts(4_260_000, 0) .saturating_add(Weight::from_parts(0, 0)) } /// Storage: `Actors::SovereignIndex` (r:8 w:0) /// Proof: `Actors::SovereignIndex` (`max_values`: None, `max_size`: Some(56), added: 2531, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:8 w:8) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorIdentities` (r:8 w:0) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorHot` (r:8 w:8) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorContract` (r:8 w:0) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:8 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:8 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorFunding` (r:8 w:8) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `MaxEncodedLen`) - /// Storage: `Actors::ContinuationState` (r:8 w:0) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:8 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:8 w:8) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) /// Storage: `Assets::Asset` (r:1 w:0) /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorStateHolds` (r:8 w:0) + /// Proof: `Actors::ActorStateHolds` (`max_values`: None, `max_size`: Some(152), added: 2627, mode: `MaxEncodedLen`) /// Storage: `Actors::QueueHead` (r:1 w:0) /// Proof: `Actors::QueueHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `Actors::QueueTail` (r:1 w:0) @@ -940,7 +1224,9 @@ impl pallet_deos_actors::WeightInfo for S /// Storage: `Actors::QueueOccupancy` (r:1 w:0) /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `Actors::QueuePages` (r:2 w:0) - /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(1050), added: 3525, mode: `MaxEncodedLen`) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `MaxEncodedLen`) + /// Storage: `Actors::NextQueueTicket` (r:1 w:0) + /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `Actors::WakeupBuckets` (r:1 w:1) /// Proof: `Actors::WakeupBuckets` (`max_values`: None, `max_size`: Some(58), added: 2533, mode: `MaxEncodedLen`) /// Storage: `Actors::WakeupCursorLen` (r:1 w:1) @@ -952,33 +1238,39 @@ impl pallet_deos_actors::WeightInfo for S /// The range of component `l` is `[2, 8]`. fn task_split_transfer(l: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `4215 + l * (1061 ±0)` - // Estimated: `8040 + l * (11210 ±0)` - // Minimum execution time: 366_673_000 picoseconds. - Weight::from_parts(78_936_673, 0) - .saturating_add(Weight::from_parts(0, 8040)) - // Standard Error: 95_573 - .saturating_add(Weight::from_parts(152_943_219, 0).saturating_mul(l.into())) - .saturating_add(T::DbWeight::get().reads(10)) - .saturating_add(T::DbWeight::get().reads((7_u64).saturating_mul(l.into()))) + // Measured: `11059 + l * (1408 ±0)` + // Estimated: `18280 + l * (4746 ±0)` + // Minimum execution time: 515_926_000 picoseconds. + Weight::from_parts(157_986_014, 0) + .saturating_add(Weight::from_parts(0, 18280)) + // Standard Error: 173_124 + .saturating_add(Weight::from_parts(196_105_670, 0).saturating_mul(l.into())) + .saturating_add(T::DbWeight::get().reads(11)) + .saturating_add(T::DbWeight::get().reads((9_u64).saturating_mul(l.into()))) .saturating_add(T::DbWeight::get().writes(4)) .saturating_add(T::DbWeight::get().writes((3_u64).saturating_mul(l.into()))) - .saturating_add(Weight::from_parts(0, 11210).saturating_mul(l.into())) + .saturating_add(Weight::from_parts(0, 4746).saturating_mul(l.into())) } /// Storage: `AssetRegistry::ForeignAssetMapping` (r:1 w:0) /// Proof: `AssetRegistry::ForeignAssetMapping` (`max_values`: None, `max_size`: Some(622), added: 3097, mode: `MaxEncodedLen`) /// Storage: `Actors::SovereignIndex` (r:1 w:0) /// Proof: `Actors::SovereignIndex` (`max_values`: None, `max_size`: Some(56), added: 2531, mode: `MaxEncodedLen`) + /// Storage: `Assets::Asset` (r:1 w:1) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) + /// Storage: `Assets::Account` (r:1 w:1) + /// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorIdentities` (r:1 w:0) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorHot` (r:1 w:1) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorContract` (r:1 w:0) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorFunding` (r:1 w:0) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `MaxEncodedLen`) - /// Storage: `Actors::ContinuationState` (r:1 w:0) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:1 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) /// Storage: `Actors::QueueHead` (r:1 w:0) @@ -988,49 +1280,47 @@ impl pallet_deos_actors::WeightInfo for S /// Storage: `Actors::QueueOccupancy` (r:1 w:0) /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `Actors::QueuePages` (r:2 w:0) - /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(1050), added: 3525, mode: `MaxEncodedLen`) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `MaxEncodedLen`) + /// Storage: `Actors::NextQueueTicket` (r:1 w:0) + /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `Actors::WakeupBuckets` (r:1 w:1) /// Proof: `Actors::WakeupBuckets` (`max_values`: None, `max_size`: Some(58), added: 2533, mode: `MaxEncodedLen`) /// Storage: `Actors::WakeupCursorLen` (r:1 w:1) /// Proof: `Actors::WakeupCursorLen` (`max_values`: None, `max_size`: Some(21), added: 2496, mode: `MaxEncodedLen`) /// Storage: `Actors::WakeupCursorPages` (r:1 w:1) /// Proof: `Actors::WakeupCursorPages` (`max_values`: None, `max_size`: Some(314), added: 2789, mode: `MaxEncodedLen`) - /// Storage: `Assets::Asset` (r:1 w:1) - /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) - /// Storage: `Assets::Account` (r:1 w:1) - /// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`) /// Storage: `System::Account` (r:1 w:1) /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) /// Storage: `Actors::WakeupPages` (r:0 w:1) /// Proof: `Actors::WakeupPages` (`max_values`: None, `max_size`: Some(348), added: 2823, mode: `MaxEncodedLen`) fn xcm_asset_deposit() -> Weight { // Proof Size summary in bytes: - // Measured: `5657` - // Estimated: `12200` - // Minimum execution time: 201_775_000 picoseconds. - Weight::from_parts(215_464_000, 0) - .saturating_add(Weight::from_parts(0, 12200)) - .saturating_add(T::DbWeight::get().reads(19)) + // Measured: `12540` + // Estimated: `18280` + // Minimum execution time: 308_983_000 picoseconds. + Weight::from_parts(326_303_000, 0) + .saturating_add(Weight::from_parts(0, 18280)) + .saturating_add(T::DbWeight::get().reads(21)) .saturating_add(T::DbWeight::get().writes(8)) } /// Storage: `System::Account` (r:1 w:1) /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - /// Storage: `Assets::Asset` (r:2 w:2) - /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) /// Storage: `Assets::Account` (r:4 w:4) /// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`) /// Storage: `AssetConversion::Pools` (r:1 w:1) /// Proof: `AssetConversion::Pools` (`max_values`: None, `max_size`: Some(30), added: 2505, mode: `MaxEncodedLen`) /// Storage: `AssetConversion::NextPoolAssetId` (r:1 w:1) /// Proof: `AssetConversion::NextPoolAssetId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) - /// Storage: `Assets::NextAssetId` (r:1 w:0) - /// Proof: `Assets::NextAssetId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `DeosRouter::LpPairByTokenId` (r:1 w:1) /// Proof: `DeosRouter::LpPairByTokenId` (`max_values`: Some(1), `max_size`: Some(7002), added: 7497, mode: `MaxEncodedLen`) /// Storage: `Oracle::ProducerFeeds` (r:1 w:1) /// Proof: `Oracle::ProducerFeeds` (`max_values`: None, `max_size`: Some(17067), added: 19542, mode: `MaxEncodedLen`) /// Storage: `Oracle::Feeds` (r:2 w:2) /// Proof: `Oracle::Feeds` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) + /// Storage: `Assets::Asset` (r:2 w:2) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) + /// Storage: `Assets::NextAssetId` (r:1 w:0) + /// Proof: `Assets::NextAssetId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `Oracle::FeedIds` (r:1 w:1) /// Proof: `Oracle::FeedIds` (`max_values`: Some(1), `max_size`: Some(17410), added: 17905, mode: `MaxEncodedLen`) /// Storage: `Oracle::ProducerIds` (r:1 w:1) @@ -1039,8 +1329,8 @@ impl pallet_deos_actors::WeightInfo for S // Proof Size summary in bytes: // Measured: `938` // Estimated: `34255` - // Minimum execution time: 261_280_000 picoseconds. - Weight::from_parts(266_588_000, 0) + // Minimum execution time: 273_503_000 picoseconds. + Weight::from_parts(281_953_000, 0) .saturating_add(Weight::from_parts(0, 34255)) .saturating_add(T::DbWeight::get().reads(16)) .saturating_add(T::DbWeight::get().writes(15)) @@ -1055,10 +1345,10 @@ impl pallet_deos_actors::WeightInfo for S /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) fn task_donate_liquidity() -> Weight { // Proof Size summary in bytes: - // Measured: `1525` + // Measured: `1559` // Estimated: `14035` - // Minimum execution time: 158_752_000 picoseconds. - Weight::from_parts(161_755_000, 0) + // Minimum execution time: 160_009_000 picoseconds. + Weight::from_parts(164_968_000, 0) .saturating_add(Weight::from_parts(0, 14035)) .saturating_add(T::DbWeight::get().reads(9)) .saturating_add(T::DbWeight::get().writes(8)) @@ -1077,8 +1367,8 @@ impl pallet_deos_actors::WeightInfo for S // Proof Size summary in bytes: // Measured: `1541` // Estimated: `8817` - // Minimum execution time: 148_625_000 picoseconds. - Weight::from_parts(151_767_000, 0) + // Minimum execution time: 147_926_000 picoseconds. + Weight::from_parts(152_675_000, 0) .saturating_add(Weight::from_parts(0, 8817)) .saturating_add(T::DbWeight::get().reads(8)) .saturating_add(T::DbWeight::get().writes(6)) @@ -1093,10 +1383,10 @@ impl pallet_deos_actors::WeightInfo for S /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) fn task_stake() -> Weight { // Proof Size summary in bytes: - // Measured: `1103` + // Measured: `1137` // Estimated: `8817` - // Minimum execution time: 82_414_000 picoseconds. - Weight::from_parts(84_649_000, 0) + // Minimum execution time: 83_532_000 picoseconds. + Weight::from_parts(85_976_000, 0) .saturating_add(Weight::from_parts(0, 8817)) .saturating_add(T::DbWeight::get().reads(7)) .saturating_add(T::DbWeight::get().writes(7)) @@ -1111,20 +1401,20 @@ impl pallet_deos_actors::WeightInfo for S /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) fn task_unstake() -> Weight { // Proof Size summary in bytes: - // Measured: `1375` + // Measured: `1409` // Estimated: `8817` - // Minimum execution time: 98_199_000 picoseconds. - Weight::from_parts(100_503_000, 0) + // Minimum execution time: 99_176_000 picoseconds. + Weight::from_parts(101_900_000, 0) .saturating_add(Weight::from_parts(0, 8817)) .saturating_add(T::DbWeight::get().reads(7)) .saturating_add(T::DbWeight::get().writes(7)) } /// Storage: `DeosRouter::RouterFee` (r:1 w:0) /// Proof: `DeosRouter::RouterFee` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) - /// Storage: `Assets::Asset` (r:2 w:2) - /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) /// Storage: `Assets::Account` (r:7 w:5) /// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`) + /// Storage: `Assets::Asset` (r:2 w:2) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) /// Storage: `TokenMintingCurve::TokenCurves` (r:1 w:0) /// Proof: `TokenMintingCurve::TokenCurves` (`max_values`: None, `max_size`: Some(79), added: 2554, mode: `MaxEncodedLen`) /// Storage: `System::Account` (r:3 w:3) @@ -1138,13 +1428,15 @@ impl pallet_deos_actors::WeightInfo for S /// Storage: `Actors::ActorIdentities` (r:1 w:0) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorHot` (r:1 w:1) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorContract` (r:1 w:0) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorFunding` (r:1 w:0) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `MaxEncodedLen`) - /// Storage: `Actors::ContinuationState` (r:1 w:0) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:1 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) /// Storage: `Actors::QueueHead` (r:1 w:0) @@ -1154,7 +1446,7 @@ impl pallet_deos_actors::WeightInfo for S /// Storage: `Actors::QueueOccupancy` (r:1 w:1) /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `Actors::QueuePages` (r:1 w:1) - /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(1050), added: 3525, mode: `MaxEncodedLen`) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `MaxEncodedLen`) /// Storage: `Actors::NextQueueTicket` (r:1 w:1) /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `Actors::ObservationSubscriberCount` (r:2 w:0) @@ -1162,23 +1454,23 @@ impl pallet_deos_actors::WeightInfo for S /// Storage: `Actors::ObservationIngressRevisions` (r:2 w:0) /// Proof: `Actors::ObservationIngressRevisions` (`max_values`: None, `max_size`: Some(41), added: 2516, mode: `MaxEncodedLen`) /// Storage: `Actors::DirtyObservationFeeds` (r:2 w:0) - /// Proof: `Actors::DirtyObservationFeeds` (`max_values`: None, `max_size`: Some(94), added: 2569, mode: `MaxEncodedLen`) + /// Proof: `Actors::DirtyObservationFeeds` (`max_values`: None, `max_size`: Some(122), added: 2597, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingFeedMembershipCount` (r:2 w:0) /// Proof: `Actors::CrossingFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) fn task_dex_exact_in() -> Weight { // Proof Size summary in bytes: - // Measured: `4322` + // Measured: `4765` // Estimated: `19253` - // Minimum execution time: 484_427_000 picoseconds. - Weight::from_parts(502_376_000, 0) + // Minimum execution time: 513_132_000 picoseconds. + Weight::from_parts(536_949_000, 0) .saturating_add(Weight::from_parts(0, 19253)) - .saturating_add(T::DbWeight::get().reads(40)) + .saturating_add(T::DbWeight::get().reads(41)) .saturating_add(T::DbWeight::get().writes(17)) } - /// Storage: `Assets::Asset` (r:2 w:2) - /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) /// Storage: `Assets::Account` (r:7 w:5) /// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`) + /// Storage: `Assets::Asset` (r:2 w:2) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) /// Storage: `System::Account` (r:3 w:3) /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) /// Storage: `DeosRouter::RouterFee` (r:1 w:0) @@ -1192,13 +1484,15 @@ impl pallet_deos_actors::WeightInfo for S /// Storage: `Actors::ActorIdentities` (r:1 w:0) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorHot` (r:1 w:1) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorContract` (r:1 w:0) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorFunding` (r:1 w:0) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `MaxEncodedLen`) - /// Storage: `Actors::ContinuationState` (r:1 w:0) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:1 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) /// Storage: `Actors::QueueHead` (r:1 w:0) @@ -1208,7 +1502,7 @@ impl pallet_deos_actors::WeightInfo for S /// Storage: `Actors::QueueOccupancy` (r:1 w:1) /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `Actors::QueuePages` (r:1 w:1) - /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(1050), added: 3525, mode: `MaxEncodedLen`) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `MaxEncodedLen`) /// Storage: `Actors::NextQueueTicket` (r:1 w:1) /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `Actors::ObservationSubscriberCount` (r:2 w:0) @@ -1216,19 +1510,37 @@ impl pallet_deos_actors::WeightInfo for S /// Storage: `Actors::ObservationIngressRevisions` (r:2 w:0) /// Proof: `Actors::ObservationIngressRevisions` (`max_values`: None, `max_size`: Some(41), added: 2516, mode: `MaxEncodedLen`) /// Storage: `Actors::DirtyObservationFeeds` (r:2 w:0) - /// Proof: `Actors::DirtyObservationFeeds` (`max_values`: None, `max_size`: Some(94), added: 2569, mode: `MaxEncodedLen`) + /// Proof: `Actors::DirtyObservationFeeds` (`max_values`: None, `max_size`: Some(122), added: 2597, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingFeedMembershipCount` (r:2 w:0) /// Proof: `Actors::CrossingFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) fn task_dex_exact_out() -> Weight { // Proof Size summary in bytes: - // Measured: `4103` + // Measured: `4546` // Estimated: `19253` - // Minimum execution time: 477_861_000 picoseconds. - Weight::from_parts(495_532_000, 0) + // Minimum execution time: 505_310_000 picoseconds. + Weight::from_parts(530_243_000, 0) .saturating_add(Weight::from_parts(0, 19253)) - .saturating_add(T::DbWeight::get().reads(39)) + .saturating_add(T::DbWeight::get().reads(40)) .saturating_add(T::DbWeight::get().writes(17)) } + /// Storage: `Actors::CurrentBlockResourceState` (r:1 w:1) + /// Proof: `Actors::CurrentBlockResourceState` (`max_values`: Some(1), `max_size`: Some(75), added: 570, mode: `MaxEncodedLen`) + /// Storage: `Actors::NextQueueTicket` (r:1 w:0) + /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::PrepassExecutionCutoff` (r:0 w:1) + /// Proof: `Actors::PrepassExecutionCutoff` (`max_values`: Some(1), `max_size`: Some(12), added: 507, mode: `MaxEncodedLen`) + fn scheduler_on_initialize_cutoff() -> Weight { + // Proof Size summary in bytes: + // Measured: `512` + // Estimated: `1560` + // Minimum execution time: 8_940_000 picoseconds. + Weight::from_parts(10_616_000, 0) + .saturating_add(Weight::from_parts(0, 1560)) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(2)) + } + /// Storage: `Actors::CurrentBlockResourceState` (r:1 w:1) + /// Proof: `Actors::CurrentBlockResourceState` (`max_values`: Some(1), `max_size`: Some(75), added: 570, mode: `MaxEncodedLen`) /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) /// Storage: `Actors::QueueHead` (r:1 w:0) @@ -1243,101 +1555,333 @@ impl pallet_deos_actors::WeightInfo for S /// Proof: `Actors::IdleStarvationState` (`max_values`: Some(1), `max_size`: Some(5), added: 500, mode: `MaxEncodedLen`) fn scheduler_on_idle_base() -> Weight { // Proof Size summary in bytes: - // Measured: `556` - // Estimated: `1543` - // Minimum execution time: 12_782_000 picoseconds. - Weight::from_parts(14_667_000, 0) - .saturating_add(Weight::from_parts(0, 1543)) - .saturating_add(T::DbWeight::get().reads(6)) - .saturating_add(T::DbWeight::get().writes(1)) + // Measured: `485` + // Estimated: `1560` + // Minimum execution time: 15_086_000 picoseconds. + Weight::from_parts(16_832_000, 0) + .saturating_add(Weight::from_parts(0, 1560)) + .saturating_add(T::DbWeight::get().reads(7)) + .saturating_add(T::DbWeight::get().writes(2)) } /// Storage: `Actors::WakeupWorkerFaultState` (r:1 w:0) + /// Proof: `Actors::WakeupWorkerFaultState` (`max_values`: Some(1), `max_size`: Some(18), added: 513, mode: `MaxEncodedLen`) /// Storage: `Timestamp::Now` (r:1 w:0) + /// Proof: `Timestamp::Now` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `Actors::WakeupCursorLen` (r:2 w:0) + /// Proof: `Actors::WakeupCursorLen` (`max_values`: None, `max_size`: Some(21), added: 2496, mode: `MaxEncodedLen`) /// Storage: `Actors::WakeupCursorPages` (r:1 w:0) + /// Proof: `Actors::WakeupCursorPages` (`max_values`: None, `max_size`: Some(314), added: 2789, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingWorkerFaultState` (r:1 w:0) + /// Proof: `Actors::CrossingWorkerFaultState` (`max_values`: Some(1), `max_size`: Some(44), added: 539, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:0) + /// Proof: `Actors::CrossingPendingFeedListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) /// Storage: `Actors::ObservationFanoutWorkerFaultState` (r:1 w:0) + /// Proof: `Actors::ObservationFanoutWorkerFaultState` (`max_values`: Some(1), `max_size`: Some(144), added: 639, mode: `MaxEncodedLen`) /// Storage: `Actors::DirtyObservationListState` (r:1 w:0) + /// Proof: `Actors::DirtyObservationListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) /// Storage: `Actors::MaterializationFamilyCursor` (r:1 w:1) + /// Proof: `Actors::MaterializationFamilyCursor` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) fn materialization_coordinator_base() -> Weight { - Weight::from_parts(25_283_000, 0) + // Proof Size summary in bytes: + // Measured: `635` + // Estimated: `5982` + // Minimum execution time: 24_724_000 picoseconds. + Weight::from_parts(26_680_000, 0) .saturating_add(Weight::from_parts(0, 5982)) .saturating_add(T::DbWeight::get().reads(10)) .saturating_add(T::DbWeight::get().writes(1)) } + /// Storage: `Actors::ActorContractHead` (r:1 w:1) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `Measured`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:1) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `Measured`) + /// Storage: `Actors::ActorIdentities` (r:1 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `Measured`) + /// Storage: `Actors::ActorContractTailChunk` (r:8 w:8) + /// Proof: `Actors::ActorContractTailChunk` (`max_values`: None, `max_size`: Some(4070), added: 6545, mode: `Measured`) + /// The range of component `c` is `[0, 8]`. + fn contract_geometry_create(c: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1046` + // Estimated: `4494 + c * (2475 ±0)` + // Minimum execution time: 26_261_000 picoseconds. + Weight::from_parts(29_462_359, 0) + .saturating_add(Weight::from_parts(0, 4494)) + // Standard Error: 3_052 + .saturating_add(Weight::from_parts(5_346_654, 0).saturating_mul(c.into())) + .saturating_add(T::DbWeight::get().reads(3)) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(c.into()))) + .saturating_add(T::DbWeight::get().writes(2)) + .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(c.into()))) + .saturating_add(Weight::from_parts(0, 2475).saturating_mul(c.into())) + } + /// Storage: `Actors::ActorContractHead` (r:1 w:1) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `Measured`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:1) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `Measured`) + /// Storage: `Actors::ActorContractTailChunk` (r:8 w:8) + /// Proof: `Actors::ActorContractTailChunk` (`max_values`: None, `max_size`: Some(4070), added: 6545, mode: `Measured`) + /// Storage: `Actors::ActorActivationAuthority` (r:0 w:1) + /// Proof: `Actors::ActorActivationAuthority` (`max_values`: None, `max_size`: Some(159), added: 2634, mode: `Measured`) + /// The range of component `c` is `[0, 8]`. + fn contract_geometry_close(c: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1124 + c * (195 ±0)` + // Estimated: `4557 + c * (2670 ±0)` + // Minimum execution time: 24_375_000 picoseconds. + Weight::from_parts(27_029_761, 0) + .saturating_add(Weight::from_parts(0, 4557)) + // Standard Error: 4_136 + .saturating_add(Weight::from_parts(6_953_989, 0).saturating_mul(c.into())) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(c.into()))) + .saturating_add(T::DbWeight::get().writes(3)) + .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(c.into()))) + .saturating_add(Weight::from_parts(0, 2670).saturating_mul(c.into())) + } + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `Measured`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `Measured`) + /// Storage: `Actors::ActorContractTailChunk` (r:8 w:0) + /// Proof: `Actors::ActorContractTailChunk` (`max_values`: None, `max_size`: Some(4070), added: 6545, mode: `Measured`) + /// The range of component `c` is `[0, 8]`. + fn contract_geometry_reconstruct(c: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1124 + c * (195 ±0)` + // Estimated: `4557 + c * (2669 ±0)` + // Minimum execution time: 20_953_000 picoseconds. + Weight::from_parts(23_313_535, 0) + .saturating_add(Weight::from_parts(0, 4557)) + // Standard Error: 3_856 + .saturating_add(Weight::from_parts(5_451_102, 0).saturating_mul(c.into())) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(c.into()))) + .saturating_add(Weight::from_parts(0, 2669).saturating_mul(c.into())) + } + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `Measured`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `Measured`) + fn current_step_load_head() -> Weight { + // Proof Size summary in bytes: + // Measured: `1048` + // Estimated: `4513` + // Minimum execution time: 18_438_000 picoseconds. + Weight::from_parts(20_254_000, 0) + .saturating_add(Weight::from_parts(0, 4513)) + .saturating_add(T::DbWeight::get().reads(2)) + } + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `Measured`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `Measured`) + /// Storage: `Actors::ActorContractTailChunk` (r:1 w:0) + /// Proof: `Actors::ActorContractTailChunk` (`max_values`: None, `max_size`: Some(4070), added: 6545, mode: `Measured`) + /// The range of component `s` is `[1, 4]`. + fn current_step_load_tail(s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1264 + s * (14 ±0)` + // Estimated: `4729 + s * (14 ±0)` + // Minimum execution time: 24_305_000 picoseconds. + Weight::from_parts(25_950_252, 0) + .saturating_add(Weight::from_parts(0, 4729)) + // Standard Error: 2_069 + .saturating_add(Weight::from_parts(79_315, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(3)) + .saturating_add(Weight::from_parts(0, 14).saturating_mul(s.into())) + } + /// Storage: `Actors::ActorIdentities` (r:1 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `Measured`) + /// Storage: `Actors::ActorHot` (r:1 w:0) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `Measured`) + /// Storage: `Actors::ActorRunHead` (r:1 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `Measured`) + /// Storage: `Actors::ActorFunding` (r:1 w:0) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `Measured`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `Measured`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `Measured`) + fn current_step_plan_opening_head() -> Weight { + // Proof Size summary in bytes: + // Measured: `1758` + // Estimated: `5223` + // Minimum execution time: 41_556_000 picoseconds. + Weight::from_parts(43_442_000, 0) + .saturating_add(Weight::from_parts(0, 5223)) + .saturating_add(T::DbWeight::get().reads(6)) + } + /// Storage: `Actors::ActorIdentities` (r:1 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `Measured`) + /// Storage: `Actors::ActorHot` (r:1 w:0) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `Measured`) + /// Storage: `Actors::ActorRunHead` (r:1 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `Measured`) + /// Storage: `Actors::ActorRunPayload` (r:1 w:0) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(2533), added: 5008, mode: `Measured`) + /// Storage: `Actors::ActorFunding` (r:1 w:0) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `Measured`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `Measured`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `Measured`) + fn current_step_plan_suspended_head() -> Weight { + // Proof Size summary in bytes: + // Measured: `4593` + // Estimated: `8058` + // Minimum execution time: 69_074_000 picoseconds. + Weight::from_parts(71_030_000, 0) + .saturating_add(Weight::from_parts(0, 8058)) + .saturating_add(T::DbWeight::get().reads(7)) + } + /// Storage: `Actors::ActorIdentities` (r:1 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `Measured`) + /// Storage: `Actors::ActorHot` (r:1 w:0) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `Measured`) + /// Storage: `Actors::ActorRunHead` (r:1 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `Measured`) + /// Storage: `Actors::ActorRunPayload` (r:1 w:0) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(2533), added: 5008, mode: `Measured`) + /// Storage: `Actors::ActorFunding` (r:1 w:0) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `Measured`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `Measured`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `Measured`) + /// Storage: `Actors::ActorContractTailChunk` (r:1 w:0) + /// Proof: `Actors::ActorContractTailChunk` (`max_values`: None, `max_size`: Some(4070), added: 6545, mode: `Measured`) + /// The range of component `s` is `[1, 4]`. + fn current_step_plan_running_tail(s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `4773 + s * (14 ±0)` + // Estimated: `8238 + s * (14 ±0)` + // Minimum execution time: 73_753_000 picoseconds. + Weight::from_parts(76_192_916, 0) + .saturating_add(Weight::from_parts(0, 8238)) + // Standard Error: 8_028 + .saturating_add(Weight::from_parts(131_564, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(8)) + .saturating_add(Weight::from_parts(0, 14).saturating_mul(s.into())) + } + /// Storage: `System::Account` (r:1 w:0) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `Measured`) + /// Storage: `Assets::Asset` (r:15 w:0) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `Measured`) + /// Storage: `Assets::Account` (r:15 w:0) + /// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `Measured`) + /// The range of component `e` is `[1, 16]`. + fn opening_snapshot_capture(e: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `288 + e * (311 ±0)` + // Estimated: `4373 + e * (2260 ±1)` + // Minimum execution time: 4_191_000 picoseconds. + Weight::from_parts(2_403_773, 0) + .saturating_add(Weight::from_parts(0, 4373)) + // Standard Error: 5_434 + .saturating_add(Weight::from_parts(9_035_928, 0).saturating_mul(e.into())) + .saturating_add(T::DbWeight::get().reads(1)) + .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(e.into()))) + .saturating_add(Weight::from_parts(0, 2260).saturating_mul(e.into())) + } + /// Storage: `System::Account` (r:1 w:0) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `Measured`) + /// Storage: `Assets::Asset` (r:31 w:0) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `Measured`) + /// Storage: `Assets::Account` (r:31 w:0) + /// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `Measured`) + /// The range of component `p` is `[1, 32]`. + fn opening_predicate_capture(p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `147 + p * (311 ±0)` + // Estimated: `4235 + p * (2524 ±0)` + // Minimum execution time: 3_282_000 picoseconds. + Weight::from_parts(3_046_526, 0) + .saturating_add(Weight::from_parts(0, 4235)) + // Standard Error: 3_006 + .saturating_add(Weight::from_parts(7_474_282, 0).saturating_mul(p.into())) + .saturating_add(T::DbWeight::get().reads(1)) + .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(p.into()))) + .saturating_add(Weight::from_parts(0, 2524).saturating_mul(p.into())) + } /// Storage: `Actors::ActorIdentities` (r:1 w:0) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorHot` (r:1 w:0) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorContract` (r:1 w:0) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorFunding` (r:1 w:0) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `MaxEncodedLen`) - /// Storage: `Actors::ContinuationState` (r:1 w:0) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:1 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunPayload` (r:1 w:0) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(2533), added: 5008, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) fn scheduler_actor_state_probe() -> Weight { // Proof Size summary in bytes: - // Measured: `1973` - // Estimated: `12200` - // Minimum execution time: 34_153_000 picoseconds. - Weight::from_parts(36_109_000, 0) - .saturating_add(Weight::from_parts(0, 12200)) - .saturating_add(T::DbWeight::get().reads(5)) + // Measured: `4529` + // Estimated: `5998` + // Minimum execution time: 65_791_000 picoseconds. + Weight::from_parts(68_586_000, 0) + .saturating_add(Weight::from_parts(0, 5998)) + .saturating_add(T::DbWeight::get().reads(7)) } + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorFunding` (r:1 w:1) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorHot` (r:1 w:1) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorIdentities` (r:1 w:1) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorHot` (r:1 w:1) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorContract` (r:1 w:0) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `MaxEncodedLen`) - /// Storage: `Actors::ContinuationState` (r:1 w:0) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `MaxEncodedLen`) fn cycle_orchestration() -> Weight { // Proof Size summary in bytes: - // Measured: `1288` - // Estimated: `12200` - // Minimum execution time: 46_725_000 picoseconds. - Weight::from_parts(48_750_000, 0) - .saturating_add(Weight::from_parts(0, 12200)) + // Measured: `1758` + // Estimated: `5736` + // Minimum execution time: 51_404_000 picoseconds. + Weight::from_parts(53_988_000, 0) + .saturating_add(Weight::from_parts(0, 5736)) .saturating_add(T::DbWeight::get().reads(5)) .saturating_add(T::DbWeight::get().writes(3)) } + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorFunding` (r:1 w:1) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorHot` (r:1 w:1) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorIdentities` (r:1 w:1) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorHot` (r:1 w:1) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorContract` (r:1 w:0) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `MaxEncodedLen`) - /// Storage: `Actors::ContinuationState` (r:1 w:0) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `MaxEncodedLen`) /// The range of component `n` is `[1, 8]`. fn step_orchestration(n: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `1285 + n * (3 ±0)` - // Estimated: `12200` - // Minimum execution time: 47_074_000 picoseconds. - Weight::from_parts(48_660_474, 0) - .saturating_add(Weight::from_parts(0, 12200)) - // Standard Error: 1_411 - .saturating_add(Weight::from_parts(183_073, 0).saturating_mul(n.into())) + // Measured: `1758` + // Estimated: `5736` + // Minimum execution time: 51_962_000 picoseconds. + Weight::from_parts(54_387_403, 0) + .saturating_add(Weight::from_parts(0, 5736)) + // Standard Error: 3_857 + .saturating_add(Weight::from_parts(73_523, 0).saturating_mul(n.into())) .saturating_add(T::DbWeight::get().reads(5)) .saturating_add(T::DbWeight::get().writes(3)) } /// Storage: `Actors::ActorIdentities` (r:1 w:0) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `Measured`) /// Storage: `Actors::ActorHot` (r:1 w:1) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `Measured`) - /// Storage: `Actors::ActorContract` (r:1 w:0) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `Measured`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `Measured`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `Measured`) /// Storage: `Actors::ActorFunding` (r:1 w:0) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `Measured`) - /// Storage: `Actors::ContinuationState` (r:1 w:0) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `Measured`) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `Measured`) + /// Storage: `Actors::ActorRunHead` (r:1 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `Measured`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `Measured`) /// Storage: `Actors::QueueHead` (r:1 w:0) /// Proof: `Actors::QueueHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) /// Storage: `Actors::QueueTail` (r:1 w:1) @@ -1345,29 +1889,31 @@ impl pallet_deos_actors::WeightInfo for S /// Storage: `Actors::QueueOccupancy` (r:1 w:1) /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `Measured`) /// Storage: `Actors::QueuePages` (r:1 w:1) - /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(1050), added: 3525, mode: `Measured`) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `Measured`) /// Storage: `Actors::NextQueueTicket` (r:1 w:1) /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) fn scheduler_paged_append_existing_page() -> Weight { // Proof Size summary in bytes: - // Measured: `4514` - // Estimated: `7979` - // Minimum execution time: 67_817_000 picoseconds. - Weight::from_parts(74_172_000, 0) - .saturating_add(Weight::from_parts(0, 7979)) - .saturating_add(T::DbWeight::get().reads(10)) + // Measured: `10583` + // Estimated: `14048` + // Minimum execution time: 105_951_000 picoseconds. + Weight::from_parts(118_313_000, 0) + .saturating_add(Weight::from_parts(0, 14048)) + .saturating_add(T::DbWeight::get().reads(11)) .saturating_add(T::DbWeight::get().writes(5)) } /// Storage: `Actors::ActorIdentities` (r:1 w:0) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `Measured`) /// Storage: `Actors::ActorHot` (r:1 w:1) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `Measured`) - /// Storage: `Actors::ActorContract` (r:1 w:0) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `Measured`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `Measured`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `Measured`) /// Storage: `Actors::ActorFunding` (r:1 w:0) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `Measured`) - /// Storage: `Actors::ContinuationState` (r:1 w:0) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `Measured`) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `Measured`) + /// Storage: `Actors::ActorRunHead` (r:1 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `Measured`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `Measured`) /// Storage: `Actors::QueueHead` (r:1 w:0) /// Proof: `Actors::QueueHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) /// Storage: `Actors::QueueTail` (r:1 w:1) @@ -1375,29 +1921,31 @@ impl pallet_deos_actors::WeightInfo for S /// Storage: `Actors::QueueOccupancy` (r:1 w:1) /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `Measured`) /// Storage: `Actors::QueuePages` (r:2 w:1) - /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(1050), added: 3525, mode: `Measured`) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `Measured`) /// Storage: `Actors::NextQueueTicket` (r:1 w:1) /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) fn scheduler_paged_append_new_page() -> Weight { // Proof Size summary in bytes: - // Measured: `4379` - // Estimated: `10319` - // Minimum execution time: 63_487_000 picoseconds. - Weight::from_parts(72_008_000, 0) - .saturating_add(Weight::from_parts(0, 10319)) - .saturating_add(T::DbWeight::get().reads(11)) + // Measured: `10495` + // Estimated: `16435` + // Minimum execution time: 101_970_000 picoseconds. + Weight::from_parts(111_538_000, 0) + .saturating_add(Weight::from_parts(0, 16435)) + .saturating_add(T::DbWeight::get().reads(12)) .saturating_add(T::DbWeight::get().writes(5)) } /// Storage: `Actors::ActorIdentities` (r:1 w:0) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `Measured`) /// Storage: `Actors::ActorHot` (r:1 w:1) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `Measured`) - /// Storage: `Actors::ActorContract` (r:1 w:0) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `Measured`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `Measured`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `Measured`) /// Storage: `Actors::ActorFunding` (r:1 w:0) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `Measured`) - /// Storage: `Actors::ContinuationState` (r:1 w:0) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `Measured`) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `Measured`) + /// Storage: `Actors::ActorRunHead` (r:1 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `Measured`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `Measured`) /// Storage: `Actors::WakeupBuckets` (r:1 w:1) /// Proof: `Actors::WakeupBuckets` (`max_values`: None, `max_size`: Some(58), added: 2533, mode: `Measured`) /// Storage: `Actors::WakeupCursorPages` (r:1 w:0) @@ -1406,24 +1954,26 @@ impl pallet_deos_actors::WeightInfo for S /// Proof: `Actors::WakeupPages` (`max_values`: None, `max_size`: Some(348), added: 2823, mode: `Measured`) fn scheduler_wakeup_append_existing_page() -> Weight { // Proof Size summary in bytes: - // Measured: `3271` - // Estimated: `6736` - // Minimum execution time: 70_541_000 picoseconds. - Weight::from_parts(76_268_000, 0) - .saturating_add(Weight::from_parts(0, 6736)) - .saturating_add(T::DbWeight::get().reads(8)) + // Measured: `4101` + // Estimated: `7566` + // Minimum execution time: 81_995_000 picoseconds. + Weight::from_parts(93_309_000, 0) + .saturating_add(Weight::from_parts(0, 7566)) + .saturating_add(T::DbWeight::get().reads(9)) .saturating_add(T::DbWeight::get().writes(3)) } /// Storage: `Actors::ActorIdentities` (r:1 w:0) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `Measured`) /// Storage: `Actors::ActorHot` (r:1 w:1) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `Measured`) - /// Storage: `Actors::ActorContract` (r:1 w:0) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `Measured`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `Measured`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `Measured`) /// Storage: `Actors::ActorFunding` (r:1 w:0) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `Measured`) - /// Storage: `Actors::ContinuationState` (r:1 w:0) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `Measured`) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `Measured`) + /// Storage: `Actors::ActorRunHead` (r:1 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `Measured`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `Measured`) /// Storage: `Actors::WakeupBuckets` (r:1 w:1) /// Proof: `Actors::WakeupBuckets` (`max_values`: None, `max_size`: Some(58), added: 2533, mode: `Measured`) /// Storage: `Actors::WakeupCursorPages` (r:1 w:0) @@ -1432,24 +1982,26 @@ impl pallet_deos_actors::WeightInfo for S /// Proof: `Actors::WakeupPages` (`max_values`: None, `max_size`: Some(348), added: 2823, mode: `Measured`) fn scheduler_wakeup_append_new_page() -> Weight { // Proof Size summary in bytes: - // Measured: `3415` - // Estimated: `6880` - // Minimum execution time: 72_776_000 picoseconds. - Weight::from_parts(78_573_000, 0) - .saturating_add(Weight::from_parts(0, 6880)) - .saturating_add(T::DbWeight::get().reads(8)) + // Measured: `4274` + // Estimated: `7739` + // Minimum execution time: 82_623_000 picoseconds. + Weight::from_parts(98_688_000, 0) + .saturating_add(Weight::from_parts(0, 7739)) + .saturating_add(T::DbWeight::get().reads(9)) .saturating_add(T::DbWeight::get().writes(4)) } /// Storage: `Actors::ActorIdentities` (r:1 w:0) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `Measured`) /// Storage: `Actors::ActorHot` (r:1 w:1) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `Measured`) - /// Storage: `Actors::ActorContract` (r:1 w:0) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `Measured`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `Measured`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `Measured`) /// Storage: `Actors::ActorFunding` (r:1 w:0) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `Measured`) - /// Storage: `Actors::ContinuationState` (r:1 w:0) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `Measured`) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `Measured`) + /// Storage: `Actors::ActorRunHead` (r:1 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `Measured`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `Measured`) /// Storage: `Actors::WakeupPages` (r:1 w:2) /// Proof: `Actors::WakeupPages` (`max_values`: None, `max_size`: Some(348), added: 2823, mode: `Measured`) /// Storage: `Actors::WakeupBuckets` (r:2 w:2) @@ -1460,24 +2012,26 @@ impl pallet_deos_actors::WeightInfo for S /// Proof: `Actors::WakeupCursorLen` (`max_values`: None, `max_size`: Some(21), added: 2496, mode: `Measured`) fn scheduler_wakeup_replace_exact() -> Weight { // Proof Size summary in bytes: - // Measured: `1581` - // Estimated: `7521` - // Minimum execution time: 94_427_000 picoseconds. - Weight::from_parts(97_151_000, 0) - .saturating_add(Weight::from_parts(0, 7521)) - .saturating_add(T::DbWeight::get().reads(10)) + // Measured: `2061` + // Estimated: `8001` + // Minimum execution time: 101_271_000 picoseconds. + Weight::from_parts(105_043_000, 0) + .saturating_add(Weight::from_parts(0, 8001)) + .saturating_add(T::DbWeight::get().reads(11)) .saturating_add(T::DbWeight::get().writes(7)) } /// Storage: `Actors::ActorIdentities` (r:1 w:0) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `Measured`) /// Storage: `Actors::ActorHot` (r:1 w:1) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `Measured`) - /// Storage: `Actors::ActorContract` (r:1 w:0) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `Measured`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `Measured`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `Measured`) /// Storage: `Actors::ActorFunding` (r:1 w:0) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `Measured`) - /// Storage: `Actors::ContinuationState` (r:1 w:0) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `Measured`) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `Measured`) + /// Storage: `Actors::ActorRunHead` (r:1 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `Measured`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `Measured`) /// Storage: `Actors::WakeupPages` (r:3 w:3) /// Proof: `Actors::WakeupPages` (`max_values`: None, `max_size`: Some(348), added: 2823, mode: `Measured`) /// Storage: `Actors::WakeupBuckets` (r:1 w:1) @@ -1486,12 +2040,12 @@ impl pallet_deos_actors::WeightInfo for S /// Proof: `Actors::WakeupCursorPages` (`max_values`: None, `max_size`: Some(314), added: 2789, mode: `Measured`) fn scheduler_wakeup_invalidate_middle_page() -> Weight { // Proof Size summary in bytes: - // Measured: `4121` - // Estimated: `12536` - // Minimum execution time: 85_138_000 picoseconds. - Weight::from_parts(91_843_000, 0) - .saturating_add(Weight::from_parts(0, 12536)) - .saturating_add(T::DbWeight::get().reads(10)) + // Measured: `5123` + // Estimated: `13538` + // Minimum execution time: 110_351_000 picoseconds. + Weight::from_parts(132_631_000, 0) + .saturating_add(Weight::from_parts(0, 13538)) + .saturating_add(T::DbWeight::get().reads(11)) .saturating_add(T::DbWeight::get().writes(5)) } /// Storage: `Actors::WakeupBuckets` (r:1 w:1) @@ -1503,21 +2057,23 @@ impl pallet_deos_actors::WeightInfo for S /// Storage: `Actors::ActorIdentities` (r:16 w:0) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `Measured`) /// Storage: `Actors::ActorHot` (r:16 w:16) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `Measured`) - /// Storage: `Actors::ActorContract` (r:16 w:0) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `Measured`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `Measured`) + /// Storage: `Actors::ActorContractHead` (r:16 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `Measured`) /// Storage: `Actors::ActorFunding` (r:16 w:0) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `Measured`) - /// Storage: `Actors::ContinuationState` (r:16 w:0) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `Measured`) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `Measured`) + /// Storage: `Actors::ActorRunHead` (r:16 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `Measured`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:16 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `Measured`) fn scheduler_wakeup_drain_partial_page() -> Weight { // Proof Size summary in bytes: - // Measured: `7485` - // Estimated: `48075` - // Minimum execution time: 337_408_000 picoseconds. - Weight::from_parts(354_939_000, 0) - .saturating_add(Weight::from_parts(0, 48075)) - .saturating_add(T::DbWeight::get().reads(83)) + // Measured: `13381` + // Estimated: `53971` + // Minimum execution time: 429_531_000 picoseconds. + Weight::from_parts(453_347_000, 0) + .saturating_add(Weight::from_parts(0, 53971)) + .saturating_add(T::DbWeight::get().reads(99)) .saturating_add(T::DbWeight::get().writes(18)) } /// Storage: `Actors::WakeupBuckets` (r:1 w:1) @@ -1529,23 +2085,25 @@ impl pallet_deos_actors::WeightInfo for S /// Storage: `Actors::ActorIdentities` (r:32 w:0) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `Measured`) /// Storage: `Actors::ActorHot` (r:32 w:32) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `Measured`) - /// Storage: `Actors::ActorContract` (r:32 w:0) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `Measured`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `Measured`) + /// Storage: `Actors::ActorContractHead` (r:32 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `Measured`) /// Storage: `Actors::ActorFunding` (r:32 w:0) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `Measured`) - /// Storage: `Actors::ContinuationState` (r:32 w:0) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `Measured`) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `Measured`) + /// Storage: `Actors::ActorRunHead` (r:32 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `Measured`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:32 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `Measured`) /// Storage: `Actors::WakeupCursorLen` (r:1 w:1) /// Proof: `Actors::WakeupCursorLen` (`max_values`: None, `max_size`: Some(21), added: 2496, mode: `Measured`) fn scheduler_wakeup_drain_full_page() -> Weight { // Proof Size summary in bytes: - // Measured: `10397` - // Estimated: `90587` - // Minimum execution time: 632_911_000 picoseconds. - Weight::from_parts(658_684_000, 0) - .saturating_add(Weight::from_parts(0, 90587)) - .saturating_add(T::DbWeight::get().reads(164)) + // Measured: `21447` + // Estimated: `101637` + // Minimum execution time: 791_873_000 picoseconds. + Weight::from_parts(833_220_000, 0) + .saturating_add(Weight::from_parts(0, 101637)) + .saturating_add(T::DbWeight::get().reads(196)) .saturating_add(T::DbWeight::get().writes(36)) } /// Storage: `Actors::WakeupBuckets` (r:1 w:1) @@ -1557,23 +2115,25 @@ impl pallet_deos_actors::WeightInfo for S /// Storage: `Actors::ActorIdentities` (r:33 w:0) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `Measured`) /// Storage: `Actors::ActorHot` (r:33 w:33) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `Measured`) - /// Storage: `Actors::ActorContract` (r:33 w:0) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `Measured`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `Measured`) + /// Storage: `Actors::ActorContractHead` (r:33 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `Measured`) /// Storage: `Actors::ActorFunding` (r:33 w:0) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `Measured`) - /// Storage: `Actors::ContinuationState` (r:33 w:0) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `Measured`) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `Measured`) + /// Storage: `Actors::ActorRunHead` (r:33 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `Measured`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:33 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `Measured`) /// Storage: `Actors::WakeupCursorLen` (r:1 w:1) /// Proof: `Actors::WakeupCursorLen` (`max_values`: None, `max_size`: Some(21), added: 2496, mode: `Measured`) fn scheduler_wakeup_drain_dense_boundary() -> Weight { // Proof Size summary in bytes: - // Measured: `10729` - // Estimated: `93394` - // Minimum execution time: 658_265_000 picoseconds. - Weight::from_parts(686_551_000, 0) - .saturating_add(Weight::from_parts(0, 93394)) - .saturating_add(T::DbWeight::get().reads(170)) + // Measured: `22119` + // Estimated: `104784` + // Minimum execution time: 843_556_000 picoseconds. + Weight::from_parts(882_669_000, 0) + .saturating_add(Weight::from_parts(0, 104784)) + .saturating_add(T::DbWeight::get().reads(203)) .saturating_add(T::DbWeight::get().writes(38)) } /// Storage: `Actors::WakeupBuckets` (r:1 w:1) @@ -1585,23 +2145,25 @@ impl pallet_deos_actors::WeightInfo for S /// Storage: `Actors::ActorIdentities` (r:32 w:0) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `Measured`) /// Storage: `Actors::ActorHot` (r:32 w:0) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `Measured`) - /// Storage: `Actors::ActorContract` (r:32 w:0) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `Measured`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `Measured`) + /// Storage: `Actors::ActorContractHead` (r:32 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `Measured`) /// Storage: `Actors::ActorFunding` (r:32 w:0) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `Measured`) - /// Storage: `Actors::ContinuationState` (r:32 w:0) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `Measured`) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `Measured`) + /// Storage: `Actors::ActorRunHead` (r:32 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `Measured`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:32 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `Measured`) /// Storage: `Actors::WakeupCursorLen` (r:1 w:1) /// Proof: `Actors::WakeupCursorLen` (`max_values`: None, `max_size`: Some(21), added: 2496, mode: `Measured`) fn scheduler_wakeup_drain_stale_page() -> Weight { // Proof Size summary in bytes: - // Measured: `9789` - // Estimated: `89979` - // Minimum execution time: 574_873_000 picoseconds. - Weight::from_parts(602_530_000, 0) - .saturating_add(Weight::from_parts(0, 89979)) - .saturating_add(T::DbWeight::get().reads(164)) + // Measured: `20839` + // Estimated: `101029` + // Minimum execution time: 727_828_000 picoseconds. + Weight::from_parts(771_200_000, 0) + .saturating_add(Weight::from_parts(0, 101029)) + .saturating_add(T::DbWeight::get().reads(196)) .saturating_add(T::DbWeight::get().writes(4)) } /// Storage: `Actors::WakeupBuckets` (r:14 w:14) @@ -1612,11 +2174,11 @@ impl pallet_deos_actors::WeightInfo for S /// Proof: `Actors::WakeupCursorPages` (`max_values`: None, `max_size`: Some(314), added: 2789, mode: `Measured`) fn scheduler_wakeup_cursor_insert() -> Weight { // Proof Size summary in bytes: - // Measured: `7093` - // Estimated: `42733` - // Minimum execution time: 334_265_000 picoseconds. - Weight::from_parts(347_047_000, 0) - .saturating_add(Weight::from_parts(0, 42733)) + // Measured: `7066` + // Estimated: `42706` + // Minimum execution time: 329_027_000 picoseconds. + Weight::from_parts(345_650_000, 0) + .saturating_add(Weight::from_parts(0, 42706)) .saturating_add(T::DbWeight::get().reads(25)) .saturating_add(T::DbWeight::get().writes(25)) } @@ -1628,11 +2190,11 @@ impl pallet_deos_actors::WeightInfo for S /// Proof: `Actors::WakeupCursorLen` (`max_values`: None, `max_size`: Some(21), added: 2496, mode: `Measured`) fn scheduler_wakeup_cursor_pop_min() -> Weight { // Proof Size summary in bytes: - // Measured: `9719` - // Estimated: `55259` - // Minimum execution time: 426_248_000 picoseconds. - Weight::from_parts(448_249_000, 0) - .saturating_add(Weight::from_parts(0, 55259)) + // Measured: `9692` + // Estimated: `55232` + // Minimum execution time: 424_851_000 picoseconds. + Weight::from_parts(450_902_000, 0) + .saturating_add(Weight::from_parts(0, 55232)) .saturating_add(T::DbWeight::get().reads(34)) .saturating_add(T::DbWeight::get().writes(26)) } @@ -1644,11 +2206,11 @@ impl pallet_deos_actors::WeightInfo for S /// Proof: `Actors::WakeupCursorPages` (`max_values`: None, `max_size`: Some(314), added: 2789, mode: `Measured`) fn scheduler_wakeup_cursor_remove_exact() -> Weight { // Proof Size summary in bytes: - // Measured: `9186` - // Estimated: `54726` - // Minimum execution time: 397_962_000 picoseconds. - Weight::from_parts(419_753_000, 0) - .saturating_add(Weight::from_parts(0, 54726)) + // Measured: `9159` + // Estimated: `54699` + // Minimum execution time: 396_495_000 picoseconds. + Weight::from_parts(420_102_000, 0) + .saturating_add(Weight::from_parts(0, 54699)) .saturating_add(T::DbWeight::get().reads(33)) .saturating_add(T::DbWeight::get().writes(25)) } @@ -1671,23 +2233,127 @@ impl pallet_deos_actors::WeightInfo for S /// Storage: `Actors::ActorIdentities` (r:1 w:0) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `Measured`) /// Storage: `Actors::ActorHot` (r:1 w:1) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `Measured`) - /// Storage: `Actors::ActorContract` (r:1 w:0) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `Measured`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `Measured`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `Measured`) /// Storage: `Actors::ActorFunding` (r:1 w:0) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `Measured`) - /// Storage: `Actors::ContinuationState` (r:1 w:0) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `Measured`) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `Measured`) + /// Storage: `Actors::ActorRunHead` (r:1 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `Measured`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `Measured`) fn scheduler_wakeup_cursor_worker_partial() -> Weight { // Proof Size summary in bytes: - // Measured: `1584` - // Estimated: `7524` - // Minimum execution time: 113_005_000 picoseconds. - Weight::from_parts(116_428_000, 0) - .saturating_add(Weight::from_parts(0, 7524)) - .saturating_add(T::DbWeight::get().reads(15)) + // Measured: `2036` + // Estimated: `7976` + // Minimum execution time: 123_272_000 picoseconds. + Weight::from_parts(127_323_000, 0) + .saturating_add(Weight::from_parts(0, 7976)) + .saturating_add(T::DbWeight::get().reads(16)) .saturating_add(T::DbWeight::get().writes(8)) } + /// Storage: `Actors::ActorIdentities` (r:1 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `Measured`) + /// Storage: `Actors::ActorHot` (r:1 w:1) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `Measured`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `Measured`) + /// Storage: `Actors::ActorFunding` (r:1 w:0) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `Measured`) + /// Storage: `Actors::ActorRunHead` (r:1 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `Measured`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `Measured`) + /// Storage: `Actors::ActorStateHolds` (r:1 w:1) + /// Proof: `Actors::ActorStateHolds` (`max_values`: None, `max_size`: Some(152), added: 2627, mode: `Measured`) + /// Storage: `Actors::ActorActivationAuthority` (r:1 w:0) + /// Proof: `Actors::ActorActivationAuthority` (`max_values`: None, `max_size`: Some(159), added: 2634, mode: `Measured`) + /// Storage: `Actors::ActorObservationFeeds` (r:1 w:0) + /// Proof: `Actors::ActorObservationFeeds` (`max_values`: None, `max_size`: Some(42), added: 2517, mode: `Measured`) + /// Storage: `Actors::ObservationSubscriptionSlot` (r:1 w:0) + /// Proof: `Actors::ObservationSubscriptionSlot` (`max_values`: None, `max_size`: Some(28), added: 2503, mode: `Measured`) + /// Storage: `Actors::CrossingMemberships` (r:1 w:0) + /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `Measured`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(121), added: 2596, mode: `Measured`) + /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) + /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `Measured`) + /// Storage: `Actors::QueueHead` (r:1 w:0) + /// Proof: `Actors::QueueHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::QueueTail` (r:1 w:1) + /// Proof: `Actors::QueueTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::QueueOccupancy` (r:1 w:1) + /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `Measured`) + /// Storage: `Actors::QueuePages` (r:1 w:1) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `Measured`) + /// Storage: `Actors::NextQueueTicket` (r:1 w:1) + /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `Measured`) + fn at_time_trigger_occurrence() -> Weight { + // Proof Size summary in bytes: + // Measured: `2612` + // Estimated: `8552` + // Minimum execution time: 204_778_000 picoseconds. + Weight::from_parts(214_136_000, 0) + .saturating_add(Weight::from_parts(0, 8552)) + .saturating_add(T::DbWeight::get().reads(20)) + .saturating_add(T::DbWeight::get().writes(9)) + } + /// Storage: `Actors::ActorIdentities` (r:1 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `Measured`) + /// Storage: `Actors::ActorHot` (r:1 w:1) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `Measured`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `Measured`) + /// Storage: `Actors::ActorFunding` (r:1 w:0) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `Measured`) + /// Storage: `Actors::ActorRunHead` (r:1 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `Measured`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `Measured`) + /// Storage: `Actors::WakeupBuckets` (r:1 w:1) + /// Proof: `Actors::WakeupBuckets` (`max_values`: None, `max_size`: Some(58), added: 2533, mode: `Measured`) + /// Storage: `Actors::WakeupCursorLen` (r:1 w:1) + /// Proof: `Actors::WakeupCursorLen` (`max_values`: None, `max_size`: Some(21), added: 2496, mode: `Measured`) + /// Storage: `Actors::WakeupCursorPages` (r:1 w:1) + /// Proof: `Actors::WakeupCursorPages` (`max_values`: None, `max_size`: Some(314), added: 2789, mode: `Measured`) + /// Storage: `Actors::ActorStateHolds` (r:1 w:0) + /// Proof: `Actors::ActorStateHolds` (`max_values`: None, `max_size`: Some(152), added: 2627, mode: `Measured`) + /// Storage: `Actors::ActorActivationAuthority` (r:1 w:0) + /// Proof: `Actors::ActorActivationAuthority` (`max_values`: None, `max_size`: Some(159), added: 2634, mode: `Measured`) + /// Storage: `Actors::ActorObservationFeeds` (r:1 w:0) + /// Proof: `Actors::ActorObservationFeeds` (`max_values`: None, `max_size`: Some(42), added: 2517, mode: `Measured`) + /// Storage: `Actors::ObservationSubscriptionSlot` (r:1 w:0) + /// Proof: `Actors::ObservationSubscriptionSlot` (`max_values`: None, `max_size`: Some(28), added: 2503, mode: `Measured`) + /// Storage: `Actors::CrossingMemberships` (r:1 w:0) + /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `Measured`) + /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) + /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `Measured`) + /// Storage: `Actors::QueueHead` (r:1 w:0) + /// Proof: `Actors::QueueHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::QueueTail` (r:1 w:1) + /// Proof: `Actors::QueueTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::QueueOccupancy` (r:1 w:1) + /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `Measured`) + /// Storage: `Actors::QueuePages` (r:1 w:1) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `Measured`) + /// Storage: `Actors::NextQueueTicket` (r:1 w:1) + /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `Measured`) + /// Storage: `Actors::WakeupPages` (r:0 w:1) + /// Proof: `Actors::WakeupPages` (`max_values`: None, `max_size`: Some(348), added: 2823, mode: `Measured`) + fn cadenced_trigger_occurrence() -> Weight { + // Proof Size summary in bytes: + // Measured: `2565` + // Estimated: `8505` + // Minimum execution time: 229_502_000 picoseconds. + Weight::from_parts(241_445_000, 0) + .saturating_add(Weight::from_parts(0, 8505)) + .saturating_add(T::DbWeight::get().reads(22)) + .saturating_add(T::DbWeight::get().writes(11)) + } /// Storage: `Timestamp::Now` (r:1 w:0) /// Proof: `Timestamp::Now` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) /// Storage: `Actors::NextWakeupClock` (r:1 w:1) @@ -1707,29 +2373,31 @@ impl pallet_deos_actors::WeightInfo for S /// Storage: `Actors::ActorIdentities` (r:1 w:0) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `Measured`) /// Storage: `Actors::ActorHot` (r:1 w:1) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `Measured`) - /// Storage: `Actors::ActorContract` (r:1 w:0) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `Measured`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `Measured`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `Measured`) /// Storage: `Actors::ActorFunding` (r:1 w:0) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `Measured`) - /// Storage: `Actors::ContinuationState` (r:1 w:0) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `Measured`) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `Measured`) + /// Storage: `Actors::ActorRunHead` (r:1 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `Measured`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `Measured`) /// Storage: `Actors::QueueHead` (r:1 w:0) /// Proof: `Actors::QueueHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) /// Storage: `Actors::QueueTail` (r:1 w:1) /// Proof: `Actors::QueueTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) /// Storage: `Actors::QueuePages` (r:1 w:1) - /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(1050), added: 3525, mode: `Measured`) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `Measured`) /// Storage: `Actors::NextQueueTicket` (r:1 w:1) /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) fn scheduler_wakeup_cursor_worker_remove() -> Weight { // Proof Size summary in bytes: - // Measured: `10923` - // Estimated: `56463` - // Minimum execution time: 506_497_000 picoseconds. - Weight::from_parts(535_621_000, 0) - .saturating_add(Weight::from_parts(0, 56463)) - .saturating_add(T::DbWeight::get().reads(49)) + // Measured: `11372` + // Estimated: `56912` + // Minimum execution time: 544_352_000 picoseconds. + Weight::from_parts(575_780_000, 0) + .saturating_add(Weight::from_parts(0, 56912)) + .saturating_add(T::DbWeight::get().reads(50)) .saturating_add(T::DbWeight::get().writes(33)) } /// Storage: `Timestamp::Now` (r:1 w:0) @@ -1744,11 +2412,11 @@ impl pallet_deos_actors::WeightInfo for S /// Proof: `Actors::WakeupCursorPages` (`max_values`: None, `max_size`: Some(314), added: 2789, mode: `Measured`) fn scheduler_wakeup_cursor_worker_future() -> Weight { // Proof Size summary in bytes: - // Measured: `622` - // Estimated: `6562` + // Measured: `668` + // Estimated: `6608` // Minimum execution time: 23_397_000 picoseconds. Weight::from_parts(25_283_000, 0) - .saturating_add(Weight::from_parts(0, 6562)) + .saturating_add(Weight::from_parts(0, 6608)) .saturating_add(T::DbWeight::get().reads(6)) } /// Storage: `Actors::QueueHead` (r:1 w:1) @@ -1756,27 +2424,29 @@ impl pallet_deos_actors::WeightInfo for S /// Storage: `Actors::QueueTail` (r:1 w:0) /// Proof: `Actors::QueueTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) /// Storage: `Actors::QueuePages` (r:1 w:0) - /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(1050), added: 3525, mode: `Measured`) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `Measured`) /// Storage: `Actors::QueueOccupancy` (r:1 w:1) /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `Measured`) /// Storage: `Actors::ActorIdentities` (r:1 w:0) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `Measured`) /// Storage: `Actors::ActorHot` (r:1 w:1) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `Measured`) - /// Storage: `Actors::ActorContract` (r:1 w:0) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `Measured`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `Measured`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `Measured`) /// Storage: `Actors::ActorFunding` (r:1 w:0) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `Measured`) - /// Storage: `Actors::ContinuationState` (r:1 w:0) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `Measured`) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `Measured`) + /// Storage: `Actors::ActorRunHead` (r:1 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `Measured`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `Measured`) fn scheduler_paged_consume_preserve_page() -> Weight { // Proof Size summary in bytes: - // Measured: `1428` - // Estimated: `4893` - // Minimum execution time: 45_607_000 picoseconds. - Weight::from_parts(47_702_000, 0) - .saturating_add(Weight::from_parts(0, 4893)) - .saturating_add(T::DbWeight::get().reads(9)) + // Measured: `2063` + // Estimated: `5528` + // Minimum execution time: 54_896_000 picoseconds. + Weight::from_parts(57_620_000, 0) + .saturating_add(Weight::from_parts(0, 5528)) + .saturating_add(T::DbWeight::get().reads(10)) .saturating_add(T::DbWeight::get().writes(3)) } /// Storage: `Actors::QueueHead` (r:1 w:1) @@ -1784,27 +2454,29 @@ impl pallet_deos_actors::WeightInfo for S /// Storage: `Actors::QueueTail` (r:1 w:1) /// Proof: `Actors::QueueTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) /// Storage: `Actors::QueuePages` (r:1 w:1) - /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(1050), added: 3525, mode: `Measured`) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `Measured`) /// Storage: `Actors::QueueOccupancy` (r:1 w:1) /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `Measured`) /// Storage: `Actors::ActorIdentities` (r:1 w:0) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `Measured`) /// Storage: `Actors::ActorHot` (r:1 w:1) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `Measured`) - /// Storage: `Actors::ActorContract` (r:1 w:0) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `Measured`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `Measured`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `Measured`) /// Storage: `Actors::ActorFunding` (r:1 w:0) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `Measured`) - /// Storage: `Actors::ContinuationState` (r:1 w:0) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `Measured`) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `Measured`) + /// Storage: `Actors::ActorRunHead` (r:1 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `Measured`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `Measured`) fn scheduler_paged_consume_delete_page() -> Weight { // Proof Size summary in bytes: - // Measured: `1410` - // Estimated: `4875` - // Minimum execution time: 46_515_000 picoseconds. - Weight::from_parts(48_750_000, 0) - .saturating_add(Weight::from_parts(0, 4875)) - .saturating_add(T::DbWeight::get().reads(9)) + // Measured: `1963` + // Estimated: `5428` + // Minimum execution time: 56_293_000 picoseconds. + Weight::from_parts(59_086_000, 0) + .saturating_add(Weight::from_parts(0, 5428)) + .saturating_add(T::DbWeight::get().reads(10)) .saturating_add(T::DbWeight::get().writes(5)) } /// Storage: `Actors::QueueHead` (r:1 w:1) @@ -1814,33 +2486,27 @@ impl pallet_deos_actors::WeightInfo for S /// Storage: `Actors::QueueOccupancy` (r:1 w:1) /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `Measured`) /// Storage: `Actors::QueuePages` (r:157 w:157) - /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(1050), added: 3525, mode: `Measured`) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `Measured`) /// Storage: `Actors::NextQueueTicket` (r:1 w:0) /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) /// Storage: `Actors::ActorIdentities` (r:10000 w:0) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `Measured`) /// Storage: `Actors::ActorHot` (r:10000 w:0) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `Measured`) - /// Storage: `Actors::ActorContract` (r:10000 w:0) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `Measured`) - /// Storage: `Actors::ActorFunding` (r:10000 w:0) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `Measured`) - /// Storage: `Actors::ContinuationState` (r:10000 w:0) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `Measured`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `Measured`) /// The range of component `n` is `[1, 10000]`. fn scheduler_paged_tombstone_drain(n: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `1824 + n * (16 ±0)` - // Estimated: `4096 + n * (2492 ±0)` - // Minimum execution time: 41_417_000 picoseconds. - Weight::from_parts(41_975_000, 0) - .saturating_add(Weight::from_parts(0, 4096)) - // Standard Error: 7_012 - .saturating_add(Weight::from_parts(9_491_017, 0).saturating_mul(n.into())) + // Measured: `1497 + n * (96 ±0)` + // Estimated: `3778 + n * (2572 ±0)` + // Minimum execution time: 33_803_000 picoseconds. + Weight::from_parts(33_874_000, 0) + .saturating_add(Weight::from_parts(0, 3778)) + // Standard Error: 4_745 + .saturating_add(Weight::from_parts(3_778_028, 0).saturating_mul(n.into())) .saturating_add(T::DbWeight::get().reads(5)) - .saturating_add(T::DbWeight::get().reads((5_u64).saturating_mul(n.into()))) + .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(n.into()))) .saturating_add(T::DbWeight::get().writes(4)) - .saturating_add(Weight::from_parts(0, 2492).saturating_mul(n.into())) + .saturating_add(Weight::from_parts(0, 2572).saturating_mul(n.into())) } /// Storage: `Actors::QueueHead` (r:1 w:1) /// Proof: `Actors::QueueHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) @@ -1849,74 +2515,62 @@ impl pallet_deos_actors::WeightInfo for S /// Storage: `Actors::QueueOccupancy` (r:1 w:1) /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `Measured`) /// Storage: `Actors::QueuePages` (r:157 w:157) - /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(1050), added: 3525, mode: `Measured`) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `Measured`) /// Storage: `Actors::NextQueueTicket` (r:1 w:0) /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) /// Storage: `Actors::ActorIdentities` (r:10000 w:0) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `Measured`) /// Storage: `Actors::ActorHot` (r:10000 w:5000) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `Measured`) - /// Storage: `Actors::ActorContract` (r:10000 w:0) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `Measured`) - /// Storage: `Actors::ActorFunding` (r:10000 w:0) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `Measured`) - /// Storage: `Actors::ContinuationState` (r:10000 w:0) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `Measured`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `Measured`) + /// Storage: `Actors::ActorContractHead` (r:5000 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `Measured`) + /// Storage: `Actors::ActorFunding` (r:5000 w:0) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `Measured`) + /// Storage: `Actors::ActorRunHead` (r:5000 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `Measured`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:5000 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `Measured`) /// The range of component `n` is `[1, 10000]`. fn scheduler_paged_mixed_scan(n: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `2546 + n * (141 ±0)` - // Estimated: `4846 + n * (2616 ±0)` - // Minimum execution time: 45_607_000 picoseconds. - Weight::from_parts(46_096_000, 0) - .saturating_add(Weight::from_parts(0, 4846)) - // Standard Error: 72_128 - .saturating_add(Weight::from_parts(39_857_795, 0).saturating_mul(n.into())) - .saturating_add(T::DbWeight::get().reads(5)) - .saturating_add(T::DbWeight::get().reads((5_u64).saturating_mul(n.into()))) + // Measured: `2895 + n * (391 ±0)` + // Estimated: `5121 + n * (2866 ±0)` + // Minimum execution time: 36_527_000 picoseconds. + Weight::from_parts(37_156_000, 0) + .saturating_add(Weight::from_parts(0, 5121)) + // Standard Error: 83_394 + .saturating_add(Weight::from_parts(47_657_638, 0).saturating_mul(n.into())) + .saturating_add(T::DbWeight::get().reads(3)) + .saturating_add(T::DbWeight::get().reads((4_u64).saturating_mul(n.into()))) .saturating_add(T::DbWeight::get().writes(3)) .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(n.into()))) - .saturating_add(Weight::from_parts(0, 2616).saturating_mul(n.into())) + .saturating_add(Weight::from_parts(0, 2866).saturating_mul(n.into())) } - /// Storage: `Actors::NextQueueTicket` (r:1 w:0) - /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) - /// Storage: `Actors::QueueHead` (r:1 w:1) - /// Proof: `Actors::QueueHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) - /// Storage: `Actors::QueueTail` (r:1 w:1) - /// Proof: `Actors::QueueTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) - /// Storage: `Actors::QueueOccupancy` (r:1 w:1) - /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `Measured`) - /// Storage: `Actors::QueuePages` (r:16 w:16) - /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(1050), added: 3525, mode: `Measured`) - /// Storage: `Actors::ActorIdentities` (r:999 w:999) + /// Storage: `Actors::ActorRunHead` (r:1 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `Measured`) + /// Storage: `Actors::ActorHot` (r:1 w:1) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `Measured`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `Measured`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `Measured`) + /// Storage: `Actors::ActorIdentities` (r:1 w:1) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `Measured`) - /// Storage: `Actors::ActorHot` (r:999 w:999) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `Measured`) - /// Storage: `Actors::ActorContract` (r:999 w:0) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `Measured`) - /// Storage: `Actors::ActorFunding` (r:999 w:999) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `Measured`) - /// Storage: `Actors::ContinuationState` (r:999 w:0) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `Measured`) - /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) - /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `Measured`) - /// The range of component `n` is `[1, 1000]`. - fn scheduler_paged_execute_cheap(n: u32, ) -> Weight { + /// Storage: `Actors::ActorFunding` (r:1 w:1) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `Measured`) + /// Storage: `Actors::ActorStateHolds` (r:1 w:0) + /// Proof: `Actors::ActorStateHolds` (`max_values`: None, `max_size`: Some(152), added: 2627, mode: `Measured`) + fn scheduler_inner_zero_step_complete() -> Weight { // Proof Size summary in bytes: - // Measured: `1503 + n * (275 ±0)` - // Estimated: `3735 + n * (2750 ±0)` - // Minimum execution time: 114_332_000 picoseconds. - Weight::from_parts(115_379_000, 0) - .saturating_add(Weight::from_parts(0, 3735)) - // Standard Error: 95_548 - .saturating_add(Weight::from_parts(93_865_117, 0).saturating_mul(n.into())) - .saturating_add(T::DbWeight::get().reads(6)) - .saturating_add(T::DbWeight::get().reads((5_u64).saturating_mul(n.into()))) - .saturating_add(T::DbWeight::get().writes(4)) - .saturating_add(T::DbWeight::get().writes((3_u64).saturating_mul(n.into()))) - .saturating_add(Weight::from_parts(0, 2750).saturating_mul(n.into())) + // Measured: `2072` + // Estimated: `5537` + // Minimum execution time: 58_667_000 picoseconds. + Weight::from_parts(60_973_000, 0) + .saturating_add(Weight::from_parts(0, 5537)) + .saturating_add(T::DbWeight::get().reads(7)) + .saturating_add(T::DbWeight::get().writes(3)) } - /// Storage: `Actors::NextQueueTicket` (r:1 w:0) + /// Storage: `Actors::NextQueueTicket` (r:1 w:1) /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) /// Storage: `Actors::QueueHead` (r:1 w:1) /// Proof: `Actors::QueueHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) @@ -1924,193 +2578,1335 @@ impl pallet_deos_actors::WeightInfo for S /// Proof: `Actors::QueueTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) /// Storage: `Actors::QueueOccupancy` (r:1 w:1) /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `Measured`) - /// Storage: `Actors::QueuePages` (r:16 w:16) - /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(1050), added: 3525, mode: `Measured`) - /// Storage: `Actors::ActorIdentities` (r:1000 w:1000) + /// Storage: `Actors::QueuePages` (r:2 w:2) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `Measured`) + /// Storage: `Actors::ActorIdentities` (r:1 w:0) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `Measured`) - /// Storage: `Actors::ActorHot` (r:1000 w:1000) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `Measured`) - /// Storage: `Actors::ActorContract` (r:1000 w:0) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `Measured`) - /// Storage: `Actors::ActorFunding` (r:1000 w:1000) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `Measured`) - /// Storage: `Actors::ContinuationState` (r:1000 w:0) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `Measured`) + /// Storage: `Actors::ActorHot` (r:1 w:1) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `Measured`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `Measured`) + /// Storage: `Actors::ActorFunding` (r:1 w:1) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `Measured`) + /// Storage: `Actors::ActorRunHead` (r:1 w:1) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `Measured`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `Measured`) /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `Measured`) - /// Storage: `System::Account` (r:501 w:501) + /// Storage: `System::Account` (r:3 w:3) /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `Measured`) - /// The range of component `n` is `[2, 1000]`. - fn scheduler_paged_execute_cheap_mixed(n: u32, ) -> Weight { + /// Storage: `Actors::ActorContractTailChunk` (r:8 w:0) + /// Proof: `Actors::ActorContractTailChunk` (`max_values`: None, `max_size`: Some(4070), added: 6545, mode: `Measured`) + /// Storage: `Actors::ActorStateHolds` (r:1 w:1) + /// Proof: `Actors::ActorStateHolds` (`max_values`: None, `max_size`: Some(152), added: 2627, mode: `Measured`) + /// Storage: `Actors::ActorActivationAuthority` (r:1 w:0) + /// Proof: `Actors::ActorActivationAuthority` (`max_values`: None, `max_size`: Some(159), added: 2634, mode: `Measured`) + /// Storage: `Actors::ActorObservationFeeds` (r:1 w:0) + /// Proof: `Actors::ActorObservationFeeds` (`max_values`: None, `max_size`: Some(42), added: 2517, mode: `Measured`) + /// Storage: `Actors::ObservationSubscriptionSlot` (r:1 w:0) + /// Proof: `Actors::ObservationSubscriptionSlot` (`max_values`: None, `max_size`: Some(28), added: 2503, mode: `Measured`) + /// Storage: `Actors::CrossingMemberships` (r:1 w:0) + /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `Measured`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(121), added: 2596, mode: `Measured`) + /// Storage: `Actors::ActorRunPayload` (r:0 w:1) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(2533), added: 5008, mode: `Measured`) + fn scheduler_paged_execute_opening_max() -> Weight { // Proof Size summary in bytes: - // Measured: `1746 + n * (354 ±0)` - // Estimated: `4729 + n * (2829 ±0)` - // Minimum execution time: 247_521_000 picoseconds. - Weight::from_parts(248_569_000, 0) - .saturating_add(Weight::from_parts(0, 4729)) - // Standard Error: 129_811 - .saturating_add(Weight::from_parts(119_818_614, 0).saturating_mul(n.into())) + // Measured: `7034` + // Estimated: `27824` + // Minimum execution time: 524_656_000 picoseconds. + Weight::from_parts(549_800_000, 0) + .saturating_add(Weight::from_parts(0, 27824)) + .saturating_add(T::DbWeight::get().reads(30)) + .saturating_add(T::DbWeight::get().writes(15)) + } + /// Storage: `Actors::ActorHot` (r:1 w:1) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `Measured`) + /// Storage: `Actors::ActorContractHead` (r:1 w:1) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `Measured`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:1) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `Measured`) + /// Storage: `Actors::ActorContractTailChunk` (r:8 w:8) + /// Proof: `Actors::ActorContractTailChunk` (`max_values`: None, `max_size`: Some(4070), added: 6545, mode: `Measured`) + /// Storage: `Actors::ActorFunding` (r:1 w:1) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `Measured`) + /// Storage: `Actors::ActorIdentities` (r:1 w:1) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `Measured`) + /// Storage: `Actors::ActorRunHead` (r:1 w:1) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `Measured`) + /// Storage: `Actors::ActiveActorCount` (r:1 w:1) + /// Proof: `Actors::ActiveActorCount` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `Measured`) + /// Storage: `Actors::ActorIdentityCount` (r:1 w:1) + /// Proof: `Actors::ActorIdentityCount` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `Measured`) + /// Storage: `Actors::SovereignIndex` (r:1 w:1) + /// Proof: `Actors::SovereignIndex` (`max_values`: None, `max_size`: Some(56), added: 2531, mode: `Measured`) + /// Storage: `Actors::SystemSovereigns` (r:1 w:1) + /// Proof: `Actors::SystemSovereigns` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `Measured`) + /// Storage: `Actors::ActorObservationFeeds` (r:1 w:0) + /// Proof: `Actors::ActorObservationFeeds` (`max_values`: None, `max_size`: Some(42), added: 2517, mode: `Measured`) + /// Storage: `Actors::ObservationSubscriptionSlot` (r:1 w:0) + /// Proof: `Actors::ObservationSubscriptionSlot` (`max_values`: None, `max_size`: Some(28), added: 2503, mode: `Measured`) + /// Storage: `Actors::CrossingMemberships` (r:1 w:0) + /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `Measured`) + /// Storage: `Actors::ActorStateHolds` (r:1 w:0) + /// Proof: `Actors::ActorStateHolds` (`max_values`: None, `max_size`: Some(152), added: 2627, mode: `Measured`) + /// Storage: `Actors::ActorRunPayload` (r:0 w:1) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(2533), added: 5008, mode: `Measured`) + /// Storage: `Actors::ActorActivationAuthority` (r:0 w:1) + /// Proof: `Actors::ActorActivationAuthority` (`max_values`: None, `max_size`: Some(159), added: 2634, mode: `Measured`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:0 w:1) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `Measured`) + /// The range of component `t` is `[0, 8]`. + fn scheduler_inner_opening_close_min(t: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `3542 + t * (195 ±0)` + // Estimated: `6975 + t * (2670 ±0)` + // Minimum execution time: 154_002_000 picoseconds. + Weight::from_parts(165_491_631, 0) + .saturating_add(Weight::from_parts(0, 6975)) + // Standard Error: 33_131 + .saturating_add(Weight::from_parts(14_400_135, 0).saturating_mul(t.into())) + .saturating_add(T::DbWeight::get().reads(14)) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(t.into()))) + .saturating_add(T::DbWeight::get().writes(13)) + .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(t.into()))) + .saturating_add(Weight::from_parts(0, 2670).saturating_mul(t.into())) + } + /// Storage: `Actors::ActorHot` (r:1 w:1) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `Measured`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `Measured`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `Measured`) + /// Storage: `Actors::ActorContractTailChunk` (r:8 w:0) + /// Proof: `Actors::ActorContractTailChunk` (`max_values`: None, `max_size`: Some(4070), added: 6545, mode: `Measured`) + /// Storage: `Actors::ActorFunding` (r:1 w:1) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `Measured`) + /// Storage: `Actors::ActorIdentities` (r:1 w:1) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `Measured`) + /// Storage: `Actors::ActorStateHolds` (r:1 w:0) + /// Proof: `Actors::ActorStateHolds` (`max_values`: None, `max_size`: Some(152), added: 2627, mode: `Measured`) + /// The range of component `t` is `[0, 8]`. + fn scheduler_inner_opening_failed_min(t: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `3296 + t * (195 ±0)` + // Estimated: `6729 + t * (2670 ±0)` + // Minimum execution time: 83_532_000 picoseconds. + Weight::from_parts(89_459_955, 0) + .saturating_add(Weight::from_parts(0, 6729)) + // Standard Error: 18_296 + .saturating_add(Weight::from_parts(6_187_462, 0).saturating_mul(t.into())) .saturating_add(T::DbWeight::get().reads(6)) - .saturating_add(T::DbWeight::get().reads((6_u64).saturating_mul(n.into()))) - .saturating_add(T::DbWeight::get().writes(4)) - .saturating_add(T::DbWeight::get().writes((4_u64).saturating_mul(n.into()))) - .saturating_add(Weight::from_parts(0, 2829).saturating_mul(n.into())) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(t.into()))) + .saturating_add(T::DbWeight::get().writes(3)) + .saturating_add(Weight::from_parts(0, 2670).saturating_mul(t.into())) } + /// Storage: `Actors::ActorHot` (r:1 w:1) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `Measured`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `Measured`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `Measured`) + /// Storage: `Actors::ActorContractTailChunk` (r:8 w:0) + /// Proof: `Actors::ActorContractTailChunk` (`max_values`: None, `max_size`: Some(4070), added: 6545, mode: `Measured`) + /// Storage: `Actors::ActorFunding` (r:1 w:1) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `Measured`) + /// Storage: `System::Account` (r:1 w:0) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `Measured`) + /// Storage: `Actors::ActorRunHead` (r:1 w:1) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `Measured`) + /// Storage: `Actors::ActorStateHolds` (r:1 w:0) + /// Proof: `Actors::ActorStateHolds` (`max_values`: None, `max_size`: Some(152), added: 2627, mode: `Measured`) /// Storage: `Actors::ActorIdentities` (r:1 w:0) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `Measured`) - /// Storage: `Actors::ActorHot` (r:1 w:1) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `Measured`) - /// Storage: `Actors::ActorContract` (r:1 w:0) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `Measured`) - /// Storage: `Actors::ActorFunding` (r:1 w:0) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `Measured`) - /// Storage: `Actors::ContinuationState` (r:1 w:1) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `Measured`) - /// The range of component `s` is `[0, 20]`. - fn continuation_suspend(s: u32, ) -> Weight { - // Proof Size summary in bytes: - // Measured: `1292` - // Estimated: `4757` - // Minimum execution time: 37_156_000 picoseconds. - Weight::from_parts(38_981_444, 0) - .saturating_add(Weight::from_parts(0, 4757)) - // Standard Error: 375 - .saturating_add(Weight::from_parts(36_264, 0).saturating_mul(s.into())) - .saturating_add(T::DbWeight::get().reads(5)) - .saturating_add(T::DbWeight::get().writes(2)) + /// Storage: `Actors::WakeupBuckets` (r:1 w:1) + /// Proof: `Actors::WakeupBuckets` (`max_values`: None, `max_size`: Some(58), added: 2533, mode: `Measured`) + /// Storage: `Actors::WakeupCursorLen` (r:1 w:1) + /// Proof: `Actors::WakeupCursorLen` (`max_values`: None, `max_size`: Some(21), added: 2496, mode: `Measured`) + /// Storage: `Actors::WakeupCursorPages` (r:1 w:1) + /// Proof: `Actors::WakeupCursorPages` (`max_values`: None, `max_size`: Some(314), added: 2789, mode: `Measured`) + /// Storage: `Actors::WakeupPages` (r:0 w:1) + /// Proof: `Actors::WakeupPages` (`max_values`: None, `max_size`: Some(348), added: 2823, mode: `Measured`) + /// Storage: `Actors::ActorRunPayload` (r:0 w:1) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(2533), added: 5008, mode: `Measured`) + /// The range of component `t` is `[0, 8]`. + fn scheduler_inner_opening_retry_min(t: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `3331 + t * (195 ±0)` + // Estimated: `6780 + t * (2669 ±0)` + // Minimum execution time: 129_697_000 picoseconds. + Weight::from_parts(135_268_177, 0) + .saturating_add(Weight::from_parts(0, 6780)) + // Standard Error: 23_024 + .saturating_add(Weight::from_parts(6_539_368, 0).saturating_mul(t.into())) + .saturating_add(T::DbWeight::get().reads(11)) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(t.into()))) + .saturating_add(T::DbWeight::get().writes(8)) + .saturating_add(Weight::from_parts(0, 2669).saturating_mul(t.into())) } - /// Storage: `Actors::ContinuationState` (r:1 w:1) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `Measured`) - fn continuation_retry() -> Weight { + /// Storage: `Actors::ActorHot` (r:1 w:1) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `Measured`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `Measured`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `Measured`) + /// Storage: `Actors::ActorContractTailChunk` (r:8 w:0) + /// Proof: `Actors::ActorContractTailChunk` (`max_values`: None, `max_size`: Some(4070), added: 6545, mode: `Measured`) + /// Storage: `Actors::ActorFunding` (r:1 w:1) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `Measured`) + /// Storage: `Assets::Asset` (r:63 w:0) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `Measured`) + /// Storage: `Assets::Account` (r:63 w:0) + /// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `Measured`) + /// Storage: `System::Account` (r:1 w:0) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `Measured`) + /// Storage: `Actors::ActorIdentities` (r:1 w:1) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `Measured`) + /// Storage: `Actors::ActorStateHolds` (r:1 w:0) + /// Proof: `Actors::ActorStateHolds` (`max_values`: None, `max_size`: Some(152), added: 2627, mode: `Measured`) + /// The range of component `t` is `[0, 8]`. + fn scheduler_inner_opening_failed_max(t: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `818` - // Estimated: `4283` - // Minimum execution time: 18_159_000 picoseconds. - Weight::from_parts(19_975_000, 0) - .saturating_add(Weight::from_parts(0, 4283)) - .saturating_add(T::DbWeight::get().reads(1)) - .saturating_add(T::DbWeight::get().writes(1)) + // Measured: `4750 + t * (2407 ±0)` + // Estimated: `8347 + t * (22129 ±4)` + // Minimum execution time: 113_145_000 picoseconds. + Weight::from_parts(129_307_530, 0) + .saturating_add(Weight::from_parts(0, 8347)) + // Standard Error: 85_602 + .saturating_add(Weight::from_parts(129_086_543, 0).saturating_mul(t.into())) + .saturating_add(T::DbWeight::get().reads(9)) + .saturating_add(T::DbWeight::get().reads((17_u64).saturating_mul(t.into()))) + .saturating_add(T::DbWeight::get().writes(3)) + .saturating_add(Weight::from_parts(0, 22129).saturating_mul(t.into())) } + /// Storage: `Actors::ActorHot` (r:1 w:1) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `Measured`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `Measured`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `Measured`) + /// Storage: `Actors::ActorContractTailChunk` (r:8 w:0) + /// Proof: `Actors::ActorContractTailChunk` (`max_values`: None, `max_size`: Some(4070), added: 6545, mode: `Measured`) + /// Storage: `Actors::ActorFunding` (r:1 w:1) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `Measured`) + /// Storage: `Assets::Asset` (r:63 w:0) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `Measured`) + /// Storage: `Assets::Account` (r:63 w:0) + /// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `Measured`) + /// Storage: `System::Account` (r:1 w:0) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `Measured`) + /// Storage: `Actors::ActorRunHead` (r:1 w:1) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `Measured`) + /// Storage: `Actors::ActorStateHolds` (r:1 w:0) + /// Proof: `Actors::ActorStateHolds` (`max_values`: None, `max_size`: Some(152), added: 2627, mode: `Measured`) /// Storage: `Actors::ActorIdentities` (r:1 w:0) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `Measured`) - /// Storage: `Actors::ActorHot` (r:1 w:1) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `Measured`) - /// Storage: `Actors::ActorContract` (r:1 w:0) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `Measured`) - /// Storage: `Actors::ActorFunding` (r:1 w:0) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `Measured`) - /// Storage: `Actors::ContinuationState` (r:1 w:1) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `Measured`) - fn continuation_complete() -> Weight { - // Proof Size summary in bytes: - // Measured: `1718` - // Estimated: `5183` - // Minimum execution time: 37_435_000 picoseconds. - Weight::from_parts(39_601_000, 0) - .saturating_add(Weight::from_parts(0, 5183)) - .saturating_add(T::DbWeight::get().reads(5)) - .saturating_add(T::DbWeight::get().writes(2)) + /// Storage: `Actors::WakeupBuckets` (r:1 w:1) + /// Proof: `Actors::WakeupBuckets` (`max_values`: None, `max_size`: Some(58), added: 2533, mode: `Measured`) + /// Storage: `Actors::WakeupCursorLen` (r:1 w:1) + /// Proof: `Actors::WakeupCursorLen` (`max_values`: None, `max_size`: Some(21), added: 2496, mode: `Measured`) + /// Storage: `Actors::WakeupCursorPages` (r:1 w:1) + /// Proof: `Actors::WakeupCursorPages` (`max_values`: None, `max_size`: Some(314), added: 2789, mode: `Measured`) + /// Storage: `Actors::WakeupPages` (r:0 w:1) + /// Proof: `Actors::WakeupPages` (`max_values`: None, `max_size`: Some(348), added: 2823, mode: `Measured`) + /// Storage: `Actors::ActorRunPayload` (r:0 w:1) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(2533), added: 5008, mode: `Measured`) + /// The range of component `t` is `[0, 8]`. + fn scheduler_inner_opening_retry_max(t: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `4564 + t * (2407 ±0)` + // Estimated: `8179 + t * (22125 ±28)` + // Minimum execution time: 156_377_000 picoseconds. + Weight::from_parts(174_001_916, 0) + .saturating_add(Weight::from_parts(0, 8179)) + // Standard Error: 77_698 + .saturating_add(Weight::from_parts(131_385_472, 0).saturating_mul(t.into())) + .saturating_add(T::DbWeight::get().reads(13)) + .saturating_add(T::DbWeight::get().reads((17_u64).saturating_mul(t.into()))) + .saturating_add(T::DbWeight::get().writes(8)) + .saturating_add(Weight::from_parts(0, 22125).saturating_mul(t.into())) } + /// Storage: `Actors::ActorHot` (r:1 w:1) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `Measured`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `Measured`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `Measured`) + /// Storage: `Actors::ActorContractTailChunk` (r:8 w:0) + /// Proof: `Actors::ActorContractTailChunk` (`max_values`: None, `max_size`: Some(4070), added: 6545, mode: `Measured`) + /// Storage: `Actors::ActorFunding` (r:1 w:1) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `Measured`) /// Storage: `Actors::ActorIdentities` (r:1 w:1) - /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `Measured`) + /// Storage: `Actors::ActorStateHolds` (r:1 w:0) + /// Proof: `Actors::ActorStateHolds` (`max_values`: None, `max_size`: Some(152), added: 2627, mode: `Measured`) + /// The range of component `t` is `[0, 8]`. + fn scheduler_inner_opening_complete_min(t: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `3261 + t * (195 ±0)` + // Estimated: `6694 + t * (2670 ±0)` + // Minimum execution time: 84_859_000 picoseconds. + Weight::from_parts(89_448_696, 0) + .saturating_add(Weight::from_parts(0, 6694)) + // Standard Error: 13_886 + .saturating_add(Weight::from_parts(6_148_376, 0).saturating_mul(t.into())) + .saturating_add(T::DbWeight::get().reads(6)) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(t.into()))) + .saturating_add(T::DbWeight::get().writes(3)) + .saturating_add(Weight::from_parts(0, 2670).saturating_mul(t.into())) + } /// Storage: `Actors::ActorHot` (r:1 w:1) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorContract` (r:1 w:0) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorFunding` (r:1 w:0) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `MaxEncodedLen`) - /// Storage: `Actors::ContinuationState` (r:1 w:1) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `MaxEncodedLen`) - /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) - /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) - /// Storage: `Actors::WakeupPages` (r:3 w:3) - /// Proof: `Actors::WakeupPages` (`max_values`: None, `max_size`: Some(348), added: 2823, mode: `MaxEncodedLen`) - /// Storage: `Actors::WakeupBuckets` (r:1 w:1) - /// Proof: `Actors::WakeupBuckets` (`max_values`: None, `max_size`: Some(58), added: 2533, mode: `MaxEncodedLen`) - /// Storage: `Actors::WakeupCursorPages` (r:1 w:0) - /// Proof: `Actors::WakeupCursorPages` (`max_values`: None, `max_size`: Some(314), added: 2789, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `Measured`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `Measured`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `Measured`) + /// Storage: `Actors::ActorContractTailChunk` (r:8 w:0) + /// Proof: `Actors::ActorContractTailChunk` (`max_values`: None, `max_size`: Some(4070), added: 6545, mode: `Measured`) + /// Storage: `Actors::ActorFunding` (r:1 w:1) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `Measured`) + /// Storage: `System::Account` (r:1 w:0) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `Measured`) + /// Storage: `Actors::ActorRunHead` (r:1 w:1) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `Measured`) + /// Storage: `Actors::ActorStateHolds` (r:1 w:0) + /// Proof: `Actors::ActorStateHolds` (`max_values`: None, `max_size`: Some(152), added: 2627, mode: `Measured`) + /// Storage: `Actors::ActorIdentities` (r:1 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `Measured`) /// Storage: `Actors::QueueHead` (r:1 w:0) - /// Proof: `Actors::QueueHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Proof: `Actors::QueueHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) /// Storage: `Actors::QueueTail` (r:1 w:1) - /// Proof: `Actors::QueueTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Proof: `Actors::QueueTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) /// Storage: `Actors::QueueOccupancy` (r:1 w:1) - /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `Measured`) /// Storage: `Actors::QueuePages` (r:1 w:1) - /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(1050), added: 3525, mode: `MaxEncodedLen`) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `Measured`) /// Storage: `Actors::NextQueueTicket` (r:1 w:1) - /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - fn continuation_cancel() -> Weight { + /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::ActorRunPayload` (r:0 w:1) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(2533), added: 5008, mode: `Measured`) + /// The range of component `t` is `[1, 8]`. + fn scheduler_inner_opening_progress_min(t: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `5040` - // Estimated: `12200` - // Minimum execution time: 163_082_000 picoseconds. - Weight::from_parts(175_514_000, 0) - .saturating_add(Weight::from_parts(0, 12200)) - .saturating_add(T::DbWeight::get().reads(16)) - .saturating_add(T::DbWeight::get().writes(11)) + // Measured: `3578 + t * (195 ±0)` + // Estimated: `7045 + t * (2670 ±0)` + // Minimum execution time: 152_047_000 picoseconds. + Weight::from_parts(151_658_315, 0) + .saturating_add(Weight::from_parts(0, 7045)) + // Standard Error: 20_704 + .saturating_add(Weight::from_parts(6_196_436, 0).saturating_mul(t.into())) + .saturating_add(T::DbWeight::get().reads(13)) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(t.into()))) + .saturating_add(T::DbWeight::get().writes(8)) + .saturating_add(Weight::from_parts(0, 2670).saturating_mul(t.into())) } - /// The range of component `n` is `[1, 10]`. - fn continuation_suffix_admission(n: u32, ) -> Weight { + /// Storage: `Actors::ActorHot` (r:1 w:1) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `Measured`) + /// Storage: `Actors::ActorContractHead` (r:1 w:1) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `Measured`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:1) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `Measured`) + /// Storage: `Actors::ActorContractTailChunk` (r:8 w:8) + /// Proof: `Actors::ActorContractTailChunk` (`max_values`: None, `max_size`: Some(4070), added: 6545, mode: `Measured`) + /// Storage: `Actors::ActorFunding` (r:1 w:1) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `Measured`) + /// Storage: `Assets::Asset` (r:63 w:0) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `Measured`) + /// Storage: `Assets::Account` (r:63 w:0) + /// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `Measured`) + /// Storage: `System::Account` (r:1 w:0) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `Measured`) + /// Storage: `Actors::ActorIdentities` (r:1 w:1) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `Measured`) + /// Storage: `Actors::ActorRunHead` (r:1 w:1) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `Measured`) + /// Storage: `Actors::ActiveActorCount` (r:1 w:1) + /// Proof: `Actors::ActiveActorCount` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `Measured`) + /// Storage: `Actors::ActorIdentityCount` (r:1 w:1) + /// Proof: `Actors::ActorIdentityCount` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `Measured`) + /// Storage: `Actors::SovereignIndex` (r:1 w:1) + /// Proof: `Actors::SovereignIndex` (`max_values`: None, `max_size`: Some(56), added: 2531, mode: `Measured`) + /// Storage: `Actors::SystemSovereigns` (r:1 w:1) + /// Proof: `Actors::SystemSovereigns` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `Measured`) + /// Storage: `Actors::ActorObservationFeeds` (r:1 w:0) + /// Proof: `Actors::ActorObservationFeeds` (`max_values`: None, `max_size`: Some(42), added: 2517, mode: `Measured`) + /// Storage: `Actors::ObservationSubscriptionSlot` (r:1 w:0) + /// Proof: `Actors::ObservationSubscriptionSlot` (`max_values`: None, `max_size`: Some(28), added: 2503, mode: `Measured`) + /// Storage: `Actors::CrossingMemberships` (r:1 w:0) + /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `Measured`) + /// Storage: `Actors::ActorStateHolds` (r:1 w:0) + /// Proof: `Actors::ActorStateHolds` (`max_values`: None, `max_size`: Some(152), added: 2627, mode: `Measured`) + /// Storage: `Actors::ActorRunPayload` (r:0 w:1) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(2533), added: 5008, mode: `Measured`) + /// Storage: `Actors::ActorActivationAuthority` (r:0 w:1) + /// Proof: `Actors::ActorActivationAuthority` (`max_values`: None, `max_size`: Some(159), added: 2634, mode: `Measured`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:0 w:1) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `Measured`) + /// The range of component `t` is `[0, 8]`. + fn scheduler_inner_opening_close_max(t: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `0` - // Estimated: `0` - // Minimum execution time: 838_000 picoseconds. - Weight::from_parts(1_432_976, 0) - .saturating_add(Weight::from_parts(0, 0)) - // Standard Error: 70 - .saturating_add(Weight::from_parts(494, 0).saturating_mul(n.into())) + // Measured: `4996 + t * (2407 ±0)` + // Estimated: `8593 + t * (22129 ±4)` + // Minimum execution time: 190_879_000 picoseconds. + Weight::from_parts(212_625_448, 0) + .saturating_add(Weight::from_parts(0, 8593)) + // Standard Error: 119_259 + .saturating_add(Weight::from_parts(153_030_720, 0).saturating_mul(t.into())) + .saturating_add(T::DbWeight::get().reads(17)) + .saturating_add(T::DbWeight::get().reads((17_u64).saturating_mul(t.into()))) + .saturating_add(T::DbWeight::get().writes(13)) + .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(t.into()))) + .saturating_add(Weight::from_parts(0, 22129).saturating_mul(t.into())) } - /// Storage: `Actors::ObservationSubscriberCount` (r:1 w:0) - /// Proof: `Actors::ObservationSubscriberCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) - /// Storage: `Actors::ObservationIngressRevisions` (r:1 w:1) - /// Proof: `Actors::ObservationIngressRevisions` (`max_values`: None, `max_size`: Some(41), added: 2516, mode: `MaxEncodedLen`) - /// Storage: `Actors::DirtyObservationFeeds` (r:2 w:2) - /// Proof: `Actors::DirtyObservationFeeds` (`max_values`: None, `max_size`: Some(94), added: 2569, mode: `MaxEncodedLen`) - /// Storage: `Actors::DirtyObservationListState` (r:1 w:1) - /// Proof: `Actors::DirtyObservationListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) - fn observation_change_ingress() -> Weight { + /// Storage: `Actors::ActorHot` (r:1 w:1) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `Measured`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `Measured`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `Measured`) + /// Storage: `Actors::ActorContractTailChunk` (r:8 w:0) + /// Proof: `Actors::ActorContractTailChunk` (`max_values`: None, `max_size`: Some(4070), added: 6545, mode: `Measured`) + /// Storage: `Actors::ActorFunding` (r:1 w:1) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `Measured`) + /// Storage: `Assets::Asset` (r:63 w:0) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `Measured`) + /// Storage: `Assets::Account` (r:63 w:0) + /// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `Measured`) + /// Storage: `System::Account` (r:1 w:0) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `Measured`) + /// Storage: `Actors::ActorIdentities` (r:1 w:1) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `Measured`) + /// Storage: `Actors::ActorStateHolds` (r:1 w:0) + /// Proof: `Actors::ActorStateHolds` (`max_values`: None, `max_size`: Some(152), added: 2627, mode: `Measured`) + /// The range of component `t` is `[0, 8]`. + fn scheduler_inner_opening_complete_max(t: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `745` - // Estimated: `6128` - // Minimum execution time: 31_010_000 picoseconds. - Weight::from_parts(32_896_000, 0) - .saturating_add(Weight::from_parts(0, 6128)) - .saturating_add(T::DbWeight::get().reads(5)) - .saturating_add(T::DbWeight::get().writes(4)) + // Measured: `4715 + t * (2407 ±0)` + // Estimated: `8312 + t * (22129 ±4)` + // Minimum execution time: 113_005_000 picoseconds. + Weight::from_parts(126_799_350, 0) + .saturating_add(Weight::from_parts(0, 8312)) + // Standard Error: 81_571 + .saturating_add(Weight::from_parts(127_843_620, 0).saturating_mul(t.into())) + .saturating_add(T::DbWeight::get().reads(9)) + .saturating_add(T::DbWeight::get().reads((17_u64).saturating_mul(t.into()))) + .saturating_add(T::DbWeight::get().writes(3)) + .saturating_add(Weight::from_parts(0, 22129).saturating_mul(t.into())) } - /// Storage: `Actors::ObservationFanoutWorkerFaultState` (r:1 w:0) - /// Proof: `Actors::ObservationFanoutWorkerFaultState` (`max_values`: Some(1), `max_size`: Some(31), added: 526, mode: `MaxEncodedLen`) - /// Storage: `Actors::DirtyObservationListState` (r:1 w:0) - /// Proof: `Actors::DirtyObservationListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) - fn observation_fanout_base() -> Weight { - // Proof Size summary in bytes: - // Measured: `383` - // Estimated: `1543` - // Minimum execution time: 5_168_000 picoseconds. - Weight::from_parts(6_705_000, 0) - .saturating_add(Weight::from_parts(0, 1543)) + /// Storage: `Actors::ActorHot` (r:1 w:1) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `Measured`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `Measured`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `Measured`) + /// Storage: `Actors::ActorContractTailChunk` (r:8 w:0) + /// Proof: `Actors::ActorContractTailChunk` (`max_values`: None, `max_size`: Some(4070), added: 6545, mode: `Measured`) + /// Storage: `Actors::ActorFunding` (r:1 w:1) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `Measured`) + /// Storage: `System::Account` (r:1 w:0) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `Measured`) + /// Storage: `Assets::Asset` (r:63 w:0) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `Measured`) + /// Storage: `Assets::Account` (r:63 w:0) + /// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `Measured`) + /// Storage: `Actors::ActorRunHead` (r:1 w:1) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `Measured`) + /// Storage: `Actors::ActorStateHolds` (r:1 w:0) + /// Proof: `Actors::ActorStateHolds` (`max_values`: None, `max_size`: Some(152), added: 2627, mode: `Measured`) + /// Storage: `Actors::ActorIdentities` (r:1 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `Measured`) + /// Storage: `Actors::QueueHead` (r:1 w:0) + /// Proof: `Actors::QueueHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::QueueTail` (r:1 w:1) + /// Proof: `Actors::QueueTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::QueueOccupancy` (r:1 w:1) + /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `Measured`) + /// Storage: `Actors::QueuePages` (r:1 w:1) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `Measured`) + /// Storage: `Actors::NextQueueTicket` (r:1 w:1) + /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::ActorRunPayload` (r:0 w:1) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(2533), added: 5008, mode: `Measured`) + /// The range of component `t` is `[1, 8]`. + fn scheduler_inner_opening_progress_max(t: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `4841 + t * (2401 ±0)` + // Estimated: `8640 + t * (22090 ±31)` + // Minimum execution time: 315_269_000 picoseconds. + Weight::from_parts(202_040_691, 0) + .saturating_add(Weight::from_parts(0, 8640)) + // Standard Error: 117_028 + .saturating_add(Weight::from_parts(132_879_336, 0).saturating_mul(t.into())) + .saturating_add(T::DbWeight::get().reads(15)) + .saturating_add(T::DbWeight::get().reads((17_u64).saturating_mul(t.into()))) + .saturating_add(T::DbWeight::get().writes(8)) + .saturating_add(Weight::from_parts(0, 22090).saturating_mul(t.into())) + } + /// Storage: `System::Account` (r:1 w:0) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `Measured`) + /// Storage: `Assets::Asset` (r:3 w:0) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `Measured`) + /// Storage: `Assets::Account` (r:3 w:0) + /// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `Measured`) + /// Storage: `Actors::ActorIdentities` (r:1 w:1) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `Measured`) + /// Storage: `Actors::ActorHot` (r:1 w:1) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `Measured`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `Measured`) + /// Storage: `Actors::ActorFunding` (r:1 w:0) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `Measured`) + /// Storage: `Actors::ActorRunHead` (r:1 w:1) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `Measured`) + /// Storage: `Actors::ActorRunPayload` (r:1 w:1) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(2533), added: 5008, mode: `Measured`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `Measured`) + /// Storage: `Actors::ActorContractTailChunk` (r:1 w:0) + /// Proof: `Actors::ActorContractTailChunk` (`max_values`: None, `max_size`: Some(4070), added: 6545, mode: `Measured`) + /// Storage: `Actors::ActorStateHolds` (r:1 w:0) + /// Proof: `Actors::ActorStateHolds` (`max_values`: None, `max_size`: Some(152), added: 2627, mode: `Measured`) + /// The range of component `s` is `[1, 4]`. + /// The range of component `p` is `[0, 4]`. + fn scheduler_inner_running_complete(s: u32, p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `3580 + p * (356 ±0) + s * (14 ±0)` + // Estimated: `7016 + p * (2718 ±0) + s * (49 ±0)` + // Minimum execution time: 100_224_000 picoseconds. + Weight::from_parts(95_782_916, 0) + .saturating_add(Weight::from_parts(0, 7016)) + // Standard Error: 70_838 + .saturating_add(Weight::from_parts(2_552_077, 0).saturating_mul(s.into())) + // Standard Error: 55_208 + .saturating_add(Weight::from_parts(11_013_042, 0).saturating_mul(p.into())) + .saturating_add(T::DbWeight::get().reads(9)) + .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(p.into()))) + .saturating_add(T::DbWeight::get().writes(4)) + .saturating_add(Weight::from_parts(0, 2718).saturating_mul(p.into())) + .saturating_add(Weight::from_parts(0, 49).saturating_mul(s.into())) + } + /// Storage: `System::Account` (r:1 w:0) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `Measured`) + /// Storage: `Assets::Asset` (r:3 w:0) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `Measured`) + /// Storage: `Assets::Account` (r:3 w:0) + /// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `Measured`) + /// Storage: `Actors::ActorHot` (r:1 w:1) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `Measured`) + /// Storage: `Actors::ActorRunHead` (r:1 w:1) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `Measured`) + /// Storage: `Actors::ActorStateHolds` (r:1 w:0) + /// Proof: `Actors::ActorStateHolds` (`max_values`: None, `max_size`: Some(152), added: 2627, mode: `Measured`) + /// Storage: `Actors::ActorIdentities` (r:1 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `Measured`) + /// Storage: `Actors::ActorRunPayload` (r:1 w:0) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(2533), added: 5008, mode: `Measured`) + /// Storage: `Actors::QueueHead` (r:1 w:0) + /// Proof: `Actors::QueueHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::QueueTail` (r:1 w:1) + /// Proof: `Actors::QueueTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::QueueOccupancy` (r:1 w:1) + /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `Measured`) + /// Storage: `Actors::QueuePages` (r:1 w:1) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `Measured`) + /// Storage: `Actors::NextQueueTicket` (r:1 w:1) + /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `Measured`) + /// The range of component `s` is `[2, 4]`. + /// The range of component `p` is `[0, 4]`. + fn scheduler_inner_running_progress(s: u32, p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `3417 + p * (264 ±0)` + // Estimated: `6762 + p * (2693 ±5) + s * (20 ±9)` + // Minimum execution time: 117_894_000 picoseconds. + Weight::from_parts(112_950_315, 0) + .saturating_add(Weight::from_parts(0, 6762)) + // Standard Error: 83_978 + .saturating_add(Weight::from_parts(1_651_829, 0).saturating_mul(s.into())) + // Standard Error: 46_874 + .saturating_add(Weight::from_parts(8_886_496, 0).saturating_mul(p.into())) + .saturating_add(T::DbWeight::get().reads(13)) + .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(p.into()))) + .saturating_add(T::DbWeight::get().writes(6)) + .saturating_add(Weight::from_parts(0, 2693).saturating_mul(p.into())) + .saturating_add(Weight::from_parts(0, 20).saturating_mul(s.into())) + } + /// Storage: `Actors::ActorRunHead` (r:1 w:1) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `Measured`) + /// Storage: `Actors::ActorRunPayload` (r:1 w:0) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(2533), added: 5008, mode: `Measured`) + /// Storage: `System::Account` (r:1 w:0) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `Measured`) + /// Storage: `Assets::Asset` (r:3 w:0) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `Measured`) + /// Storage: `Assets::Account` (r:3 w:0) + /// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `Measured`) + /// Storage: `Actors::ActorHot` (r:1 w:1) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `Measured`) + /// Storage: `Actors::ActorStateHolds` (r:1 w:0) + /// Proof: `Actors::ActorStateHolds` (`max_values`: None, `max_size`: Some(152), added: 2627, mode: `Measured`) + /// Storage: `Actors::ActorIdentities` (r:1 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `Measured`) + /// Storage: `Actors::WakeupBuckets` (r:1 w:1) + /// Proof: `Actors::WakeupBuckets` (`max_values`: None, `max_size`: Some(58), added: 2533, mode: `Measured`) + /// Storage: `Actors::WakeupCursorLen` (r:1 w:1) + /// Proof: `Actors::WakeupCursorLen` (`max_values`: None, `max_size`: Some(21), added: 2496, mode: `Measured`) + /// Storage: `Actors::WakeupCursorPages` (r:1 w:1) + /// Proof: `Actors::WakeupCursorPages` (`max_values`: None, `max_size`: Some(314), added: 2789, mode: `Measured`) + /// Storage: `Actors::WakeupPages` (r:0 w:1) + /// Proof: `Actors::WakeupPages` (`max_values`: None, `max_size`: Some(348), added: 2823, mode: `Measured`) + /// The range of component `s` is `[1, 4]`. + /// The range of component `p` is `[0, 4]`. + fn scheduler_inner_suspended_tail_retry(s: u32, p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `2868 + p * (264 ±0)` + // Estimated: `6244 + p * (2693 ±4) + s * (12 ±6)` + // Minimum execution time: 129_209_000 picoseconds. + Weight::from_parts(127_671_072, 0) + .saturating_add(Weight::from_parts(0, 6244)) + // Standard Error: 59_498 + .saturating_add(Weight::from_parts(1_427_388, 0).saturating_mul(s.into())) + // Standard Error: 46_370 + .saturating_add(Weight::from_parts(8_247_122, 0).saturating_mul(p.into())) + .saturating_add(T::DbWeight::get().reads(9)) + .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(p.into()))) + .saturating_add(T::DbWeight::get().writes(6)) + .saturating_add(Weight::from_parts(0, 2693).saturating_mul(p.into())) + .saturating_add(Weight::from_parts(0, 12).saturating_mul(s.into())) + } + /// Storage: `Actors::ActorRunHead` (r:1 w:1) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `Measured`) + /// Storage: `Actors::ActorRunPayload` (r:1 w:1) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(2533), added: 5008, mode: `Measured`) + /// Storage: `System::Account` (r:1 w:0) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `Measured`) + /// Storage: `Assets::Asset` (r:3 w:0) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `Measured`) + /// Storage: `Assets::Account` (r:3 w:0) + /// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `Measured`) + /// Storage: `Actors::ActorIdentities` (r:1 w:1) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `Measured`) + /// Storage: `Actors::ActorHot` (r:1 w:1) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `Measured`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `Measured`) + /// Storage: `Actors::ActorFunding` (r:1 w:0) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `Measured`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `Measured`) + /// Storage: `Actors::ActorContractTailChunk` (r:1 w:0) + /// Proof: `Actors::ActorContractTailChunk` (`max_values`: None, `max_size`: Some(4070), added: 6545, mode: `Measured`) + /// Storage: `Actors::ActorStateHolds` (r:1 w:0) + /// Proof: `Actors::ActorStateHolds` (`max_values`: None, `max_size`: Some(152), added: 2627, mode: `Measured`) + /// The range of component `s` is `[1, 4]`. + /// The range of component `p` is `[0, 4]`. + fn scheduler_inner_suspended_tail_complete(s: u32, p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `3919 + p * (287 ±0) + s * (14 ±0)` + // Estimated: `7294 + p * (2718 ±0) + s * (26 ±0)` + // Minimum execution time: 138_497_000 picoseconds. + Weight::from_parts(135_401_769, 0) + .saturating_add(Weight::from_parts(0, 7294)) + // Standard Error: 76_325 + .saturating_add(Weight::from_parts(1_926_065, 0).saturating_mul(s.into())) + // Standard Error: 59_485 + .saturating_add(Weight::from_parts(9_564_179, 0).saturating_mul(p.into())) + .saturating_add(T::DbWeight::get().reads(10)) + .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(p.into()))) + .saturating_add(T::DbWeight::get().writes(4)) + .saturating_add(Weight::from_parts(0, 2718).saturating_mul(p.into())) + .saturating_add(Weight::from_parts(0, 26).saturating_mul(s.into())) + } + /// Storage: `Actors::ActorRunHead` (r:1 w:1) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `Measured`) + /// Storage: `Actors::ActorRunPayload` (r:1 w:0) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(2533), added: 5008, mode: `Measured`) + /// Storage: `System::Account` (r:1 w:0) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `Measured`) + /// Storage: `Assets::Asset` (r:3 w:0) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `Measured`) + /// Storage: `Assets::Account` (r:3 w:0) + /// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `Measured`) + /// Storage: `Actors::ActorHot` (r:1 w:1) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `Measured`) + /// Storage: `Actors::ActorStateHolds` (r:1 w:0) + /// Proof: `Actors::ActorStateHolds` (`max_values`: None, `max_size`: Some(152), added: 2627, mode: `Measured`) + /// Storage: `Actors::ActorIdentities` (r:1 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `Measured`) + /// Storage: `Actors::QueueHead` (r:1 w:0) + /// Proof: `Actors::QueueHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::QueueTail` (r:1 w:1) + /// Proof: `Actors::QueueTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::QueueOccupancy` (r:1 w:1) + /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `Measured`) + /// Storage: `Actors::QueuePages` (r:1 w:1) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `Measured`) + /// Storage: `Actors::NextQueueTicket` (r:1 w:1) + /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `Measured`) + /// The range of component `s` is `[2, 4]`. + /// The range of component `p` is `[0, 4]`. + fn scheduler_inner_suspended_tail_progress(s: u32, p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `3419 + p * (264 ±0)` + // Estimated: `6764 + p * (2693 ±5) + s * (20 ±9)` + // Minimum execution time: 149_952_000 picoseconds. + Weight::from_parts(145_948_500, 0) + .saturating_add(Weight::from_parts(0, 6764)) + // Standard Error: 108_983 + .saturating_add(Weight::from_parts(2_112_826, 0).saturating_mul(s.into())) + // Standard Error: 60_831 + .saturating_add(Weight::from_parts(9_053_092, 0).saturating_mul(p.into())) + .saturating_add(T::DbWeight::get().reads(13)) + .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(p.into()))) + .saturating_add(T::DbWeight::get().writes(6)) + .saturating_add(Weight::from_parts(0, 2693).saturating_mul(p.into())) + .saturating_add(Weight::from_parts(0, 20).saturating_mul(s.into())) + } + /// Storage: `Actors::ActorRunHead` (r:1 w:1) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `Measured`) + /// Storage: `Actors::ActorRunPayload` (r:1 w:0) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(2533), added: 5008, mode: `Measured`) + /// Storage: `System::Account` (r:1 w:0) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `Measured`) + /// Storage: `Assets::Asset` (r:3 w:0) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `Measured`) + /// Storage: `Assets::Account` (r:3 w:0) + /// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `Measured`) + /// Storage: `Actors::ActorHot` (r:1 w:1) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `Measured`) + /// Storage: `Actors::ActorStateHolds` (r:1 w:0) + /// Proof: `Actors::ActorStateHolds` (`max_values`: None, `max_size`: Some(152), added: 2627, mode: `Measured`) + /// Storage: `Actors::ActorIdentities` (r:1 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `Measured`) + /// Storage: `Actors::WakeupBuckets` (r:1 w:1) + /// Proof: `Actors::WakeupBuckets` (`max_values`: None, `max_size`: Some(58), added: 2533, mode: `Measured`) + /// Storage: `Actors::WakeupCursorLen` (r:1 w:1) + /// Proof: `Actors::WakeupCursorLen` (`max_values`: None, `max_size`: Some(21), added: 2496, mode: `Measured`) + /// Storage: `Actors::WakeupCursorPages` (r:1 w:1) + /// Proof: `Actors::WakeupCursorPages` (`max_values`: None, `max_size`: Some(314), added: 2789, mode: `Measured`) + /// Storage: `Actors::WakeupPages` (r:0 w:1) + /// Proof: `Actors::WakeupPages` (`max_values`: None, `max_size`: Some(348), added: 2823, mode: `Measured`) + /// The range of component `n` is `[0, 64]`. + /// The range of component `r` is `[0, 128]`. + /// The range of component `f` is `[0, 40]`. + /// The range of component `p` is `[0, 4]`. + fn scheduler_inner_suspended_head_retry(n: u32, r: u32, f: u32, p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1722 + f * (21 ±0) + n * (22 ±0) + p * (264 ±0) + r * (2 ±0)` + // Estimated: `5133 + f * (21 ±0) + n * (22 ±0) + p * (2693 ±2) + r * (2 ±0)` + // Minimum execution time: 155_399_000 picoseconds. + Weight::from_parts(101_216_218, 0) + .saturating_add(Weight::from_parts(0, 5133)) + // Standard Error: 4_580 + .saturating_add(Weight::from_parts(624_373, 0).saturating_mul(n.into())) + // Standard Error: 2_301 + .saturating_add(Weight::from_parts(77_816, 0).saturating_mul(r.into())) + // Standard Error: 7_282 + .saturating_add(Weight::from_parts(605_547, 0).saturating_mul(f.into())) + // Standard Error: 64_024 + .saturating_add(Weight::from_parts(7_436_390, 0).saturating_mul(p.into())) + .saturating_add(T::DbWeight::get().reads(9)) + .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(p.into()))) + .saturating_add(T::DbWeight::get().writes(6)) + .saturating_add(Weight::from_parts(0, 21).saturating_mul(f.into())) + .saturating_add(Weight::from_parts(0, 22).saturating_mul(n.into())) + .saturating_add(Weight::from_parts(0, 2693).saturating_mul(p.into())) + .saturating_add(Weight::from_parts(0, 2).saturating_mul(r.into())) + } + /// Storage: `Actors::ActorRunHead` (r:1 w:1) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `Measured`) + /// Storage: `Actors::ActorRunPayload` (r:1 w:1) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(2533), added: 5008, mode: `Measured`) + /// Storage: `System::Account` (r:1 w:0) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `Measured`) + /// Storage: `Assets::Asset` (r:3 w:0) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `Measured`) + /// Storage: `Assets::Account` (r:3 w:0) + /// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `Measured`) + /// Storage: `Actors::ActorIdentities` (r:1 w:1) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `Measured`) + /// Storage: `Actors::ActorHot` (r:1 w:1) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `Measured`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `Measured`) + /// Storage: `Actors::ActorFunding` (r:1 w:0) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `Measured`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `Measured`) + /// Storage: `Actors::ActorStateHolds` (r:1 w:0) + /// Proof: `Actors::ActorStateHolds` (`max_values`: None, `max_size`: Some(152), added: 2627, mode: `Measured`) + /// The range of component `n` is `[0, 64]`. + /// The range of component `r` is `[0, 128]`. + /// The range of component `f` is `[0, 40]`. + /// The range of component `p` is `[0, 4]`. + fn scheduler_inner_suspended_head_complete(n: u32, r: u32, f: u32, p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `2628 + f * (21 ±0) + n * (22 ±0) + p * (286 ±0) + r * (2 ±0)` + // Estimated: `6038 + f * (21 ±0) + n * (22 ±0) + p * (2716 ±2) + r * (2 ±0)` + // Minimum execution time: 160_079_000 picoseconds. + Weight::from_parts(109_542_628, 0) + .saturating_add(Weight::from_parts(0, 6038)) + // Standard Error: 4_517 + .saturating_add(Weight::from_parts(544_051, 0).saturating_mul(n.into())) + // Standard Error: 2_270 + .saturating_add(Weight::from_parts(34_686, 0).saturating_mul(r.into())) + // Standard Error: 7_182 + .saturating_add(Weight::from_parts(520_243, 0).saturating_mul(f.into())) + // Standard Error: 63_144 + .saturating_add(Weight::from_parts(8_244_070, 0).saturating_mul(p.into())) + .saturating_add(T::DbWeight::get().reads(9)) + .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(p.into()))) + .saturating_add(T::DbWeight::get().writes(4)) + .saturating_add(Weight::from_parts(0, 21).saturating_mul(f.into())) + .saturating_add(Weight::from_parts(0, 22).saturating_mul(n.into())) + .saturating_add(Weight::from_parts(0, 2716).saturating_mul(p.into())) + .saturating_add(Weight::from_parts(0, 2).saturating_mul(r.into())) + } + /// Storage: `Actors::ActorRunHead` (r:1 w:1) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `Measured`) + /// Storage: `Actors::ActorRunPayload` (r:1 w:0) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(2533), added: 5008, mode: `Measured`) + /// Storage: `System::Account` (r:1 w:0) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `Measured`) + /// Storage: `Assets::Asset` (r:3 w:0) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `Measured`) + /// Storage: `Assets::Account` (r:3 w:0) + /// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `Measured`) + /// Storage: `Actors::ActorHot` (r:1 w:1) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `Measured`) + /// Storage: `Actors::ActorStateHolds` (r:1 w:0) + /// Proof: `Actors::ActorStateHolds` (`max_values`: None, `max_size`: Some(152), added: 2627, mode: `Measured`) + /// Storage: `Actors::ActorIdentities` (r:1 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `Measured`) + /// Storage: `Actors::QueueHead` (r:1 w:0) + /// Proof: `Actors::QueueHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::QueueTail` (r:1 w:1) + /// Proof: `Actors::QueueTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::QueueOccupancy` (r:1 w:1) + /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `Measured`) + /// Storage: `Actors::QueuePages` (r:1 w:1) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `Measured`) + /// Storage: `Actors::NextQueueTicket` (r:1 w:1) + /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `Measured`) + /// The range of component `n` is `[0, 64]`. + /// The range of component `r` is `[0, 128]`. + /// The range of component `f` is `[0, 40]`. + /// The range of component `p` is `[0, 4]`. + fn scheduler_inner_suspended_head_progress(n: u32, r: u32, f: u32, p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `2310 + f * (21 ±0) + n * (22 ±0) + p * (264 ±0) + r * (2 ±0)` + // Estimated: `5721 + f * (21 ±0) + n * (22 ±0) + p * (2693 ±0) + r * (2 ±0)` + // Minimum execution time: 177_330_000 picoseconds. + Weight::from_parts(111_036_829, 0) + .saturating_add(Weight::from_parts(0, 5721)) + // Standard Error: 4_669 + .saturating_add(Weight::from_parts(803_981, 0).saturating_mul(n.into())) + // Standard Error: 2_346 + .saturating_add(Weight::from_parts(91_680, 0).saturating_mul(r.into())) + // Standard Error: 7_423 + .saturating_add(Weight::from_parts(827_461, 0).saturating_mul(f.into())) + // Standard Error: 65_265 + .saturating_add(Weight::from_parts(7_824_145, 0).saturating_mul(p.into())) + .saturating_add(T::DbWeight::get().reads(12)) + .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(p.into()))) + .saturating_add(T::DbWeight::get().writes(6)) + .saturating_add(Weight::from_parts(0, 21).saturating_mul(f.into())) + .saturating_add(Weight::from_parts(0, 22).saturating_mul(n.into())) + .saturating_add(Weight::from_parts(0, 2693).saturating_mul(p.into())) + .saturating_add(Weight::from_parts(0, 2).saturating_mul(r.into())) + } + /// Storage: `Actors::ActorRunHead` (r:1 w:1) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `Measured`) + /// Storage: `Actors::ActorRunPayload` (r:1 w:0) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(2533), added: 5008, mode: `Measured`) + /// Storage: `Assets::Asset` (r:3 w:0) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `Measured`) + /// Storage: `Assets::Account` (r:3 w:0) + /// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `Measured`) + /// Storage: `System::Account` (r:1 w:0) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `Measured`) + /// Storage: `Actors::ActorHot` (r:1 w:1) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `Measured`) + /// Storage: `Actors::ActorStateHolds` (r:1 w:0) + /// Proof: `Actors::ActorStateHolds` (`max_values`: None, `max_size`: Some(152), added: 2627, mode: `Measured`) + /// Storage: `Actors::ActorIdentities` (r:1 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `Measured`) + /// Storage: `Actors::WakeupBuckets` (r:1 w:1) + /// Proof: `Actors::WakeupBuckets` (`max_values`: None, `max_size`: Some(58), added: 2533, mode: `Measured`) + /// Storage: `Actors::WakeupCursorLen` (r:1 w:1) + /// Proof: `Actors::WakeupCursorLen` (`max_values`: None, `max_size`: Some(21), added: 2496, mode: `Measured`) + /// Storage: `Actors::WakeupCursorPages` (r:1 w:1) + /// Proof: `Actors::WakeupCursorPages` (`max_values`: None, `max_size`: Some(314), added: 2789, mode: `Measured`) + /// Storage: `Actors::WakeupPages` (r:0 w:1) + /// Proof: `Actors::WakeupPages` (`max_values`: None, `max_size`: Some(348), added: 2823, mode: `Measured`) + /// The range of component `n` is `[0, 64]`. + /// The range of component `r` is `[0, 128]`. + /// The range of component `f` is `[0, 40]`. + fn scheduler_inner_suspended_head_opening_retry(n: u32, r: u32, f: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `2781 + f * (21 ±0) + n * (22 ±0) + r * (2 ±0)` + // Estimated: `11197 + f * (21 ±0) + n * (22 ±0) + r * (2 ±0)` + // Minimum execution time: 152_675_000 picoseconds. + Weight::from_parts(122_805_125, 0) + .saturating_add(Weight::from_parts(0, 11197)) + // Standard Error: 4_163 + .saturating_add(Weight::from_parts(662_864, 0).saturating_mul(n.into())) + // Standard Error: 2_092 + .saturating_add(Weight::from_parts(77_292, 0).saturating_mul(r.into())) + // Standard Error: 6_621 + .saturating_add(Weight::from_parts(715_051, 0).saturating_mul(f.into())) + .saturating_add(T::DbWeight::get().reads(15)) + .saturating_add(T::DbWeight::get().writes(6)) + .saturating_add(Weight::from_parts(0, 21).saturating_mul(f.into())) + .saturating_add(Weight::from_parts(0, 22).saturating_mul(n.into())) + .saturating_add(Weight::from_parts(0, 2).saturating_mul(r.into())) + } + /// Storage: `Actors::ActorRunHead` (r:1 w:1) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `Measured`) + /// Storage: `Actors::ActorRunPayload` (r:1 w:1) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(2533), added: 5008, mode: `Measured`) + /// Storage: `Assets::Asset` (r:3 w:0) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `Measured`) + /// Storage: `Assets::Account` (r:3 w:0) + /// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `Measured`) + /// Storage: `Actors::ActorIdentities` (r:1 w:1) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `Measured`) + /// Storage: `Actors::ActorHot` (r:1 w:1) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `Measured`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `Measured`) + /// Storage: `Actors::ActorFunding` (r:1 w:0) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `Measured`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `Measured`) + /// Storage: `Actors::ActorStateHolds` (r:1 w:0) + /// Proof: `Actors::ActorStateHolds` (`max_values`: None, `max_size`: Some(152), added: 2627, mode: `Measured`) + /// The range of component `n` is `[0, 64]`. + /// The range of component `r` is `[0, 128]`. + /// The range of component `f` is `[0, 40]`. + fn scheduler_inner_suspended_head_opening_complete(n: u32, r: u32, f: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `3569 + f * (21 ±0) + n * (22 ±0) + r * (2 ±0)` + // Estimated: `11985 + f * (21 ±0) + n * (22 ±0) + r * (2 ±0)` + // Minimum execution time: 154_561_000 picoseconds. + Weight::from_parts(128_627_138, 0) + .saturating_add(Weight::from_parts(0, 11985)) + // Standard Error: 3_585 + .saturating_add(Weight::from_parts(574_669, 0).saturating_mul(n.into())) + // Standard Error: 1_801 + .saturating_add(Weight::from_parts(64_436, 0).saturating_mul(r.into())) + // Standard Error: 5_701 + .saturating_add(Weight::from_parts(599_401, 0).saturating_mul(f.into())) + .saturating_add(T::DbWeight::get().reads(14)) + .saturating_add(T::DbWeight::get().writes(4)) + .saturating_add(Weight::from_parts(0, 21).saturating_mul(f.into())) + .saturating_add(Weight::from_parts(0, 22).saturating_mul(n.into())) + .saturating_add(Weight::from_parts(0, 2).saturating_mul(r.into())) + } + /// Storage: `Actors::ActorRunHead` (r:1 w:1) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `Measured`) + /// Storage: `Actors::ActorRunPayload` (r:1 w:0) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(2533), added: 5008, mode: `Measured`) + /// Storage: `Assets::Asset` (r:3 w:0) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `Measured`) + /// Storage: `Assets::Account` (r:3 w:0) + /// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `Measured`) + /// Storage: `Actors::ActorHot` (r:1 w:1) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `Measured`) + /// Storage: `Actors::ActorStateHolds` (r:1 w:0) + /// Proof: `Actors::ActorStateHolds` (`max_values`: None, `max_size`: Some(152), added: 2627, mode: `Measured`) + /// Storage: `Actors::ActorIdentities` (r:1 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `Measured`) + /// Storage: `Actors::QueueHead` (r:1 w:0) + /// Proof: `Actors::QueueHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::QueueTail` (r:1 w:1) + /// Proof: `Actors::QueueTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::QueueOccupancy` (r:1 w:1) + /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `Measured`) + /// Storage: `Actors::QueuePages` (r:1 w:1) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `Measured`) + /// Storage: `Actors::NextQueueTicket` (r:1 w:1) + /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `Measured`) + /// The range of component `n` is `[0, 64]`. + /// The range of component `r` is `[0, 128]`. + /// The range of component `f` is `[0, 40]`. + fn scheduler_inner_suspended_head_opening_progress(n: u32, r: u32, f: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `3162 + f * (21 ±0) + n * (22 ±0) + r * (2 ±0)` + // Estimated: `11578 + f * (21 ±0) + n * (22 ±0) + r * (2 ±0)` + // Minimum execution time: 172_721_000 picoseconds. + Weight::from_parts(135_507_223, 0) + .saturating_add(Weight::from_parts(0, 11578)) + // Standard Error: 4_921 + .saturating_add(Weight::from_parts(806_693, 0).saturating_mul(n.into())) + // Standard Error: 2_472 + .saturating_add(Weight::from_parts(104_938, 0).saturating_mul(r.into())) + // Standard Error: 7_825 + .saturating_add(Weight::from_parts(820_306, 0).saturating_mul(f.into())) + .saturating_add(T::DbWeight::get().reads(17)) + .saturating_add(T::DbWeight::get().writes(6)) + .saturating_add(Weight::from_parts(0, 21).saturating_mul(f.into())) + .saturating_add(Weight::from_parts(0, 22).saturating_mul(n.into())) + .saturating_add(Weight::from_parts(0, 2).saturating_mul(r.into())) + } + /// Storage: `Actors::NextQueueTicket` (r:1 w:0) + /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::QueueHead` (r:1 w:1) + /// Proof: `Actors::QueueHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::QueueTail` (r:1 w:1) + /// Proof: `Actors::QueueTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::QueueOccupancy` (r:1 w:1) + /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `Measured`) + /// Storage: `Actors::QueuePages` (r:16 w:16) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `Measured`) + /// Storage: `Actors::ActorIdentities` (r:999 w:999) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `Measured`) + /// Storage: `Actors::ActorHot` (r:999 w:999) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `Measured`) + /// Storage: `Actors::ActorContractHead` (r:999 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `Measured`) + /// Storage: `Actors::ActorFunding` (r:999 w:999) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `Measured`) + /// Storage: `Actors::ActorRunHead` (r:999 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `Measured`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:999 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `Measured`) + /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) + /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `Measured`) + /// Storage: `Actors::ActorStateHolds` (r:999 w:0) + /// Proof: `Actors::ActorStateHolds` (`max_values`: None, `max_size`: Some(152), added: 2627, mode: `Measured`) + /// The range of component `n` is `[1, 1000]`. + fn scheduler_paged_execute_cheap(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1975 + n * (695 ±0)` + // Estimated: `4210 + n * (3171 ±0)` + // Minimum execution time: 135_983_000 picoseconds. + Weight::from_parts(139_405_000, 0) + .saturating_add(Weight::from_parts(0, 4210)) + // Standard Error: 141_147 + .saturating_add(Weight::from_parts(122_349_209, 0).saturating_mul(n.into())) + .saturating_add(T::DbWeight::get().reads(6)) + .saturating_add(T::DbWeight::get().reads((7_u64).saturating_mul(n.into()))) + .saturating_add(T::DbWeight::get().writes(4)) + .saturating_add(T::DbWeight::get().writes((3_u64).saturating_mul(n.into()))) + .saturating_add(Weight::from_parts(0, 3171).saturating_mul(n.into())) + } + /// Storage: `Actors::NextQueueTicket` (r:1 w:0) + /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::QueueHead` (r:1 w:1) + /// Proof: `Actors::QueueHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::QueueTail` (r:1 w:1) + /// Proof: `Actors::QueueTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::QueueOccupancy` (r:1 w:1) + /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `Measured`) + /// Storage: `Actors::QueuePages` (r:16 w:16) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `Measured`) + /// Storage: `Actors::ActorIdentities` (r:1000 w:1000) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `Measured`) + /// Storage: `Actors::ActorHot` (r:1000 w:1000) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `Measured`) + /// Storage: `Actors::ActorContractHead` (r:1000 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `Measured`) + /// Storage: `Actors::ActorFunding` (r:1000 w:1000) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `Measured`) + /// Storage: `Actors::ActorRunHead` (r:1000 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `Measured`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1000 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `Measured`) + /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) + /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `Measured`) + /// Storage: `Actors::ActorStateHolds` (r:1000 w:500) + /// Proof: `Actors::ActorStateHolds` (`max_values`: None, `max_size`: Some(152), added: 2627, mode: `Measured`) + /// Storage: `System::Account` (r:1001 w:1001) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `Measured`) + /// Storage: `Actors::ActorActivationAuthority` (r:500 w:0) + /// Proof: `Actors::ActorActivationAuthority` (`max_values`: None, `max_size`: Some(159), added: 2634, mode: `Measured`) + /// Storage: `Actors::ActorObservationFeeds` (r:500 w:0) + /// Proof: `Actors::ActorObservationFeeds` (`max_values`: None, `max_size`: Some(42), added: 2517, mode: `Measured`) + /// Storage: `Actors::ObservationSubscriptionSlot` (r:500 w:0) + /// Proof: `Actors::ObservationSubscriptionSlot` (`max_values`: None, `max_size`: Some(28), added: 2503, mode: `Measured`) + /// Storage: `Actors::CrossingMemberships` (r:500 w:0) + /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `Measured`) + /// Storage: `Balances::Holds` (r:500 w:500) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(121), added: 2596, mode: `Measured`) + /// The range of component `n` is `[2, 1000]`. + fn scheduler_paged_execute_cheap_mixed(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `2214 + n * (885 ±0)` + // Estimated: `4772 + n * (3361 ±0)` + // Minimum execution time: 355_708_000 picoseconds. + Weight::from_parts(357_034_000, 0) + .saturating_add(Weight::from_parts(0, 4772)) + // Standard Error: 206_593 + .saturating_add(Weight::from_parts(190_370_288, 0).saturating_mul(n.into())) + .saturating_add(T::DbWeight::get().reads(5)) + .saturating_add(T::DbWeight::get().reads((11_u64).saturating_mul(n.into()))) + .saturating_add(T::DbWeight::get().writes(4)) + .saturating_add(T::DbWeight::get().writes((5_u64).saturating_mul(n.into()))) + .saturating_add(Weight::from_parts(0, 3361).saturating_mul(n.into())) + } + /// Storage: `Actors::ActorIdentities` (r:1 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `Measured`) + /// Storage: `Actors::ActorHot` (r:1 w:1) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `Measured`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `Measured`) + /// Storage: `Actors::ActorFunding` (r:1 w:0) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `Measured`) + /// Storage: `Actors::ActorRunHead` (r:1 w:1) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `Measured`) + /// Storage: `Actors::ActorRunPayload` (r:1 w:0) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(2533), added: 5008, mode: `Measured`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `Measured`) + /// Storage: `Actors::ActorContractTailChunk` (r:1 w:0) + /// Proof: `Actors::ActorContractTailChunk` (`max_values`: None, `max_size`: Some(4070), added: 6545, mode: `Measured`) + /// Storage: `Actors::ActorStateHolds` (r:1 w:0) + /// Proof: `Actors::ActorStateHolds` (`max_values`: None, `max_size`: Some(152), added: 2627, mode: `Measured`) + fn run_progress() -> Weight { + // Proof Size summary in bytes: + // Measured: `5102` + // Estimated: `8567` + // Minimum execution time: 101_411_000 picoseconds. + Weight::from_parts(104_904_000, 0) + .saturating_add(Weight::from_parts(0, 8567)) + .saturating_add(T::DbWeight::get().reads(9)) + .saturating_add(T::DbWeight::get().writes(2)) + } + /// Storage: `Actors::ActorIdentities` (r:1 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `Measured`) + /// Storage: `Actors::ActorHot` (r:1 w:1) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `Measured`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `Measured`) + /// Storage: `Actors::ActorFunding` (r:1 w:0) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `Measured`) + /// Storage: `Actors::ActorRunHead` (r:1 w:1) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `Measured`) + /// Storage: `Actors::ActorRunPayload` (r:1 w:0) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(2533), added: 5008, mode: `Measured`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `Measured`) + /// Storage: `Actors::ActorStateHolds` (r:1 w:0) + /// Proof: `Actors::ActorStateHolds` (`max_values`: None, `max_size`: Some(152), added: 2627, mode: `Measured`) + fn run_suspend() -> Weight { + // Proof Size summary in bytes: + // Measured: `4911` + // Estimated: `8376` + // Minimum execution time: 100_014_000 picoseconds. + Weight::from_parts(103_646_000, 0) + .saturating_add(Weight::from_parts(0, 8376)) + .saturating_add(T::DbWeight::get().reads(8)) + .saturating_add(T::DbWeight::get().writes(2)) + } + /// Storage: `Actors::ActorRunHead` (r:1 w:1) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `Measured`) + /// Storage: `Actors::ActorRunPayload` (r:1 w:0) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(2533), added: 5008, mode: `Measured`) + fn run_retry() -> Weight { + // Proof Size summary in bytes: + // Measured: `2124` + // Estimated: `5589` + // Minimum execution time: 50_915_000 picoseconds. + Weight::from_parts(53_290_000, 0) + .saturating_add(Weight::from_parts(0, 5589)) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(1)) + } + /// Storage: `Actors::ActorIdentities` (r:1 w:1) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `Measured`) + /// Storage: `Actors::ActorHot` (r:1 w:1) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `Measured`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `Measured`) + /// Storage: `Actors::ActorFunding` (r:1 w:0) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `Measured`) + /// Storage: `Actors::ActorRunHead` (r:1 w:1) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `Measured`) + /// Storage: `Actors::ActorRunPayload` (r:1 w:1) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(2533), added: 5008, mode: `Measured`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `Measured`) + /// Storage: `Actors::ActorStateHolds` (r:1 w:0) + /// Proof: `Actors::ActorStateHolds` (`max_values`: None, `max_size`: Some(152), added: 2627, mode: `Measured`) + fn run_complete() -> Weight { + // Proof Size summary in bytes: + // Measured: `3811` + // Estimated: `7276` + // Minimum execution time: 72_567_000 picoseconds. + Weight::from_parts(75_429_000, 0) + .saturating_add(Weight::from_parts(0, 7276)) + .saturating_add(T::DbWeight::get().reads(8)) + .saturating_add(T::DbWeight::get().writes(4)) + } + /// Storage: `Actors::ActorIdentities` (r:1 w:1) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorHot` (r:1 w:1) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorFunding` (r:1 w:0) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:1 w:1) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunPayload` (r:1 w:1) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(2533), added: 5008, mode: `MaxEncodedLen`) + /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) + /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + /// Storage: `Actors::WakeupPages` (r:3 w:3) + /// Proof: `Actors::WakeupPages` (`max_values`: None, `max_size`: Some(348), added: 2823, mode: `MaxEncodedLen`) + /// Storage: `Actors::WakeupBuckets` (r:1 w:1) + /// Proof: `Actors::WakeupBuckets` (`max_values`: None, `max_size`: Some(58), added: 2533, mode: `MaxEncodedLen`) + /// Storage: `Actors::WakeupCursorPages` (r:1 w:0) + /// Proof: `Actors::WakeupCursorPages` (`max_values`: None, `max_size`: Some(314), added: 2789, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorStateHolds` (r:1 w:0) + /// Proof: `Actors::ActorStateHolds` (`max_values`: None, `max_size`: Some(152), added: 2627, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueHead` (r:1 w:0) + /// Proof: `Actors::QueueHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueTail` (r:1 w:1) + /// Proof: `Actors::QueueTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueOccupancy` (r:1 w:1) + /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueuePages` (r:1 w:1) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `MaxEncodedLen`) + /// Storage: `Actors::NextQueueTicket` (r:1 w:1) + /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + fn run_cancel() -> Weight { + // Proof Size summary in bytes: + // Measured: `7854` + // Estimated: `9635` + // Minimum execution time: 271_547_000 picoseconds. + Weight::from_parts(288_868_000, 0) + .saturating_add(Weight::from_parts(0, 9635)) + .saturating_add(T::DbWeight::get().reads(19)) + .saturating_add(T::DbWeight::get().writes(12)) + } + /// The range of component `n` is `[1, 10]`. + fn run_suffix_admission(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 489_000 picoseconds. + Weight::from_parts(1_426_855, 0) + .saturating_add(Weight::from_parts(0, 0)) + // Standard Error: 70 + .saturating_add(Weight::from_parts(605, 0).saturating_mul(n.into())) + } + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:1 w:1) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `Measured`) + /// Storage: `Actors::ActorIdentities` (r:1 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `Measured`) + /// Storage: `Actors::ActorHot` (r:1 w:1) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `Measured`) + /// Storage: `Actors::ActorActivationAuthority` (r:1 w:0) + /// Proof: `Actors::ActorActivationAuthority` (`max_values`: None, `max_size`: Some(159), added: 2634, mode: `Measured`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `Measured`) + /// Storage: `Actors::ActorRunHead` (r:1 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `Measured`) + /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) + /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `Measured`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `Measured`) + /// Storage: `Actors::QueueHead` (r:1 w:0) + /// Proof: `Actors::QueueHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::QueueTail` (r:1 w:1) + /// Proof: `Actors::QueueTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::QueueOccupancy` (r:1 w:1) + /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `Measured`) + /// Storage: `Actors::QueuePages` (r:1 w:1) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `Measured`) + /// Storage: `Actors::NextQueueTicket` (r:1 w:1) + /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + fn observation_change_trigger_occurrence() -> Weight { + // Proof Size summary in bytes: + // Measured: `2356` + // Estimated: `8296` + // Minimum execution time: 113_075_000 picoseconds. + Weight::from_parts(116_846_000, 0) + .saturating_add(Weight::from_parts(0, 8296)) + .saturating_add(T::DbWeight::get().reads(14)) + .saturating_add(T::DbWeight::get().writes(8)) + } + /// Storage: `Actors::ObservationSubscriberCount` (r:1 w:0) + /// Proof: `Actors::ObservationSubscriberCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) + /// Storage: `Actors::ObservationIngressRevisions` (r:1 w:1) + /// Proof: `Actors::ObservationIngressRevisions` (`max_values`: None, `max_size`: Some(41), added: 2516, mode: `MaxEncodedLen`) + /// Storage: `Actors::DirtyObservationFeeds` (r:2 w:2) + /// Proof: `Actors::DirtyObservationFeeds` (`max_values`: None, `max_size`: Some(122), added: 2597, mode: `MaxEncodedLen`) + /// Storage: `Actors::DirtyObservationListState` (r:1 w:1) + /// Proof: `Actors::DirtyObservationListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) + fn observation_change_ingress() -> Weight { + // Proof Size summary in bytes: + // Measured: `817` + // Estimated: `6184` + // Minimum execution time: 32_756_000 picoseconds. + Weight::from_parts(34_852_000, 0) + .saturating_add(Weight::from_parts(0, 6184)) + .saturating_add(T::DbWeight::get().reads(5)) + .saturating_add(T::DbWeight::get().writes(4)) + } + /// Storage: `Actors::ObservationFanoutWorkerFaultState` (r:1 w:0) + /// Proof: `Actors::ObservationFanoutWorkerFaultState` (`max_values`: Some(1), `max_size`: Some(144), added: 639, mode: `MaxEncodedLen`) + /// Storage: `Actors::DirtyObservationListState` (r:1 w:0) + /// Proof: `Actors::DirtyObservationListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) + fn observation_fanout_base() -> Weight { + // Proof Size summary in bytes: + // Measured: `393` + // Estimated: `1629` + // Minimum execution time: 4_679_000 picoseconds. + Weight::from_parts(6_285_000, 0) + .saturating_add(Weight::from_parts(0, 1629)) + .saturating_add(T::DbWeight::get().reads(2)) + } + /// Storage: `Actors::DirtyObservationListState` (r:1 w:0) + /// Proof: `Actors::DirtyObservationListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) + /// Storage: `Actors::DirtyObservationFeeds` (r:1 w:0) + /// Proof: `Actors::DirtyObservationFeeds` (`max_values`: None, `max_size`: Some(122), added: 2597, mode: `MaxEncodedLen`) + fn observation_fanout_branch_probe() -> Weight { + // Proof Size summary in bytes: + // Measured: `698` + // Estimated: `3587` + // Minimum execution time: 11_803_000 picoseconds. + Weight::from_parts(13_340_000, 0) + .saturating_add(Weight::from_parts(0, 3587)) .saturating_add(T::DbWeight::get().reads(2)) } /// Storage: `Actors::DirtyObservationListState` (r:1 w:1) /// Proof: `Actors::DirtyObservationListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) /// Storage: `Actors::DirtyObservationFeeds` (r:3 w:3) - /// Proof: `Actors::DirtyObservationFeeds` (`max_values`: None, `max_size`: Some(94), added: 2569, mode: `MaxEncodedLen`) + /// Proof: `Actors::DirtyObservationFeeds` (`max_values`: None, `max_size`: Some(122), added: 2597, mode: `MaxEncodedLen`) /// Storage: `Actors::ObservationSubscriberPageLists` (r:1 w:0) /// Proof: `Actors::ObservationSubscriberPageLists` (`max_values`: None, `max_size`: Some(45), added: 2520, mode: `MaxEncodedLen`) /// Storage: `Actors::ObservationSubscriberPages` (r:1 w:0) /// Proof: `Actors::ObservationSubscriberPages` (`max_values`: None, `max_size`: Some(641), added: 3116, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:64 w:64) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorIdentities` (r:64 w:0) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorHot` (r:64 w:64) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorContract` (r:64 w:0) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorFunding` (r:64 w:0) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `MaxEncodedLen`) - /// Storage: `Actors::ContinuationState` (r:64 w:0) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorActivationAuthority` (r:64 w:0) + /// Proof: `Actors::ActorActivationAuthority` (`max_values`: None, `max_size`: Some(159), added: 2634, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:64 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:64 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:65 w:65) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) /// Storage: `Actors::QueueHead` (r:1 w:0) /// Proof: `Actors::QueueHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `Actors::QueueTail` (r:1 w:1) @@ -2118,18 +3914,216 @@ impl pallet_deos_actors::WeightInfo for S /// Storage: `Actors::QueueOccupancy` (r:1 w:1) /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `Actors::QueuePages` (r:1 w:1) - /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(1050), added: 3525, mode: `MaxEncodedLen`) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `MaxEncodedLen`) /// Storage: `Actors::NextQueueTicket` (r:1 w:1) /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) fn observation_fanout_page() -> Weight { // Proof Size summary in bytes: - // Measured: `20291` - // Estimated: `718430` - // Minimum execution time: 2_592_689_000 picoseconds. - Weight::from_parts(2_651_008_000, 0) - .saturating_add(Weight::from_parts(0, 718430)) - .saturating_add(T::DbWeight::get().reads(332)) - .saturating_add(T::DbWeight::get().writes(72)) + // Measured: `46609` + // Estimated: `304734` + // Minimum execution time: 4_559_661_000 picoseconds. + Weight::from_parts(4_651_713_000, 0) + .saturating_add(Weight::from_parts(0, 304734)) + .saturating_add(T::DbWeight::get().reads(461)) + .saturating_add(T::DbWeight::get().writes(201)) + } + /// Storage: `Actors::DirtyObservationListState` (r:1 w:1) + /// Proof: `Actors::DirtyObservationListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) + /// Storage: `Actors::DirtyObservationFeeds` (r:1 w:1) + /// Proof: `Actors::DirtyObservationFeeds` (`max_values`: None, `max_size`: Some(122), added: 2597, mode: `MaxEncodedLen`) + /// Storage: `Actors::ObservationSubscriberPageLists` (r:1 w:0) + /// Proof: `Actors::ObservationSubscriberPageLists` (`max_values`: None, `max_size`: Some(45), added: 2520, mode: `MaxEncodedLen`) + /// Storage: `Actors::ObservationSubscriberPages` (r:1 w:0) + /// Proof: `Actors::ObservationSubscriberPages` (`max_values`: None, `max_size`: Some(641), added: 3116, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:64 w:64) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorIdentities` (r:64 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorHot` (r:64 w:64) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorActivationAuthority` (r:64 w:0) + /// Proof: `Actors::ActorActivationAuthority` (`max_values`: None, `max_size`: Some(159), added: 2634, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:64 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:64 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) + /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:65 w:65) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Actors::WakeupBuckets` (r:1 w:1) + /// Proof: `Actors::WakeupBuckets` (`max_values`: None, `max_size`: Some(58), added: 2533, mode: `MaxEncodedLen`) + /// Storage: `Actors::WakeupCursorLen` (r:1 w:1) + /// Proof: `Actors::WakeupCursorLen` (`max_values`: None, `max_size`: Some(21), added: 2496, mode: `MaxEncodedLen`) + /// Storage: `Actors::WakeupCursorPages` (r:1 w:1) + /// Proof: `Actors::WakeupCursorPages` (`max_values`: None, `max_size`: Some(314), added: 2789, mode: `MaxEncodedLen`) + /// Storage: `Actors::WakeupPages` (r:0 w:2) + /// Proof: `Actors::WakeupPages` (`max_values`: None, `max_size`: Some(348), added: 2823, mode: `MaxEncodedLen`) + fn observation_fanout_wakeup_page() -> Weight { + // Proof Size summary in bytes: + // Measured: `46460` + // Estimated: `304734` + // Minimum execution time: 4_927_521_000 picoseconds. + Weight::from_parts(5_061_897_000, 0) + .saturating_add(Weight::from_parts(0, 304734)) + .saturating_add(T::DbWeight::get().reads(457)) + .saturating_add(T::DbWeight::get().writes(200)) + } + /// Storage: `Actors::DirtyObservationListState` (r:1 w:1) + /// Proof: `Actors::DirtyObservationListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) + /// Storage: `Actors::DirtyObservationFeeds` (r:1 w:1) + /// Proof: `Actors::DirtyObservationFeeds` (`max_values`: None, `max_size`: Some(122), added: 2597, mode: `MaxEncodedLen`) + /// Storage: `Actors::ObservationSubscriberPageLists` (r:1 w:0) + /// Proof: `Actors::ObservationSubscriberPageLists` (`max_values`: None, `max_size`: Some(45), added: 2520, mode: `MaxEncodedLen`) + /// Storage: `Actors::ObservationSubscriberPages` (r:1 w:0) + /// Proof: `Actors::ObservationSubscriberPages` (`max_values`: None, `max_size`: Some(641), added: 3116, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:64 w:0) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorIdentities` (r:64 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorHot` (r:64 w:0) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorActivationAuthority` (r:64 w:0) + /// Proof: `Actors::ActorActivationAuthority` (`max_values`: None, `max_size`: Some(159), added: 2634, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:64 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:64 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) + /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + fn observation_fanout_coalesced_page() -> Weight { + // Proof Size summary in bytes: + // Measured: `35213` + // Estimated: `304734` + // Minimum execution time: 1_907_535_000 picoseconds. + Weight::from_parts(1_961_593_000, 0) + .saturating_add(Weight::from_parts(0, 304734)) + .saturating_add(T::DbWeight::get().reads(389)) + .saturating_add(T::DbWeight::get().writes(2)) + } + /// Storage: `Actors::DirtyObservationListState` (r:1 w:1) + /// Proof: `Actors::DirtyObservationListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) + /// Storage: `Actors::DirtyObservationFeeds` (r:1 w:1) + /// Proof: `Actors::DirtyObservationFeeds` (`max_values`: None, `max_size`: Some(122), added: 2597, mode: `MaxEncodedLen`) + /// Storage: `Actors::ObservationSubscriberPageLists` (r:1 w:1) + /// Proof: `Actors::ObservationSubscriberPageLists` (`max_values`: None, `max_size`: Some(45), added: 2520, mode: `MaxEncodedLen`) + /// Storage: `Actors::ObservationSubscriberPages` (r:1 w:1) + /// Proof: `Actors::ObservationSubscriberPages` (`max_values`: None, `max_size`: Some(641), added: 3116, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:1 w:1) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorIdentities` (r:1 w:1) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorHot` (r:1 w:1) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorActivationAuthority` (r:1 w:1) + /// Proof: `Actors::ActorActivationAuthority` (`max_values`: None, `max_size`: Some(159), added: 2634, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:1 w:1) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:1 w:1) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) + /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:1) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorFunding` (r:1 w:1) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActiveActorCount` (r:1 w:1) + /// Proof: `Actors::ActiveActorCount` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorIdentityCount` (r:1 w:1) + /// Proof: `Actors::ActorIdentityCount` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Actors::SovereignIndex` (r:1 w:1) + /// Proof: `Actors::SovereignIndex` (`max_values`: None, `max_size`: Some(56), added: 2531, mode: `MaxEncodedLen`) + /// Storage: `Actors::SystemSovereigns` (r:1 w:1) + /// Proof: `Actors::SystemSovereigns` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorObservationFeeds` (r:1 w:1) + /// Proof: `Actors::ActorObservationFeeds` (`max_values`: None, `max_size`: Some(42), added: 2517, mode: `MaxEncodedLen`) + /// Storage: `Actors::ObservationSubscriptionSlot` (r:1 w:1) + /// Proof: `Actors::ObservationSubscriptionSlot` (`max_values`: None, `max_size`: Some(28), added: 2503, mode: `MaxEncodedLen`) + /// Storage: `Actors::ObservationSubscriptionSlotOwner` (r:1 w:1) + /// Proof: `Actors::ObservationSubscriptionSlotOwner` (`max_values`: None, `max_size`: Some(28), added: 2503, mode: `MaxEncodedLen`) + /// Storage: `Actors::ObservationSubscriberCount` (r:1 w:1) + /// Proof: `Actors::ObservationSubscriberCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) + /// Storage: `Actors::ObservationSubscriptionCount` (r:1 w:1) + /// Proof: `Actors::ObservationSubscriptionCount` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ObservationFreeSlotLen` (r:1 w:1) + /// Proof: `Actors::ObservationFreeSlotLen` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Actors::NextObservationSubscriptionSlot` (r:1 w:0) + /// Proof: `Actors::NextObservationSubscriptionSlot` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ObservationFreeSlotPages` (r:1 w:1) + /// Proof: `Actors::ObservationFreeSlotPages` (`max_values`: None, `max_size`: Some(278), added: 2753, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingMemberships` (r:1 w:0) + /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorStateHolds` (r:1 w:0) + /// Proof: `Actors::ActorStateHolds` (`max_values`: None, `max_size`: Some(152), added: 2627, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunPayload` (r:0 w:1) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(2533), added: 5008, mode: `MaxEncodedLen`) + /// Storage: `Actors::ObservationIngressRevisions` (r:0 w:1) + /// Proof: `Actors::ObservationIngressRevisions` (`max_values`: None, `max_size`: Some(41), added: 2516, mode: `MaxEncodedLen`) + fn observation_fanout_terminal() -> Weight { + // Proof Size summary in bytes: + // Measured: `3233` + // Estimated: `5736` + // Minimum execution time: 220_422_000 picoseconds. + Weight::from_parts(228_035_000, 0) + .saturating_add(Weight::from_parts(0, 5736)) + .saturating_add(T::DbWeight::get().reads(27)) + .saturating_add(T::DbWeight::get().writes(25)) + } + /// Storage: `Actors::DirtyObservationListState` (r:1 w:1) + /// Proof: `Actors::DirtyObservationListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) + /// Storage: `Actors::DirtyObservationFeeds` (r:1 w:1) + /// Proof: `Actors::DirtyObservationFeeds` (`max_values`: None, `max_size`: Some(122), added: 2597, mode: `MaxEncodedLen`) + /// Storage: `Actors::ObservationSubscriberPageLists` (r:1 w:0) + /// Proof: `Actors::ObservationSubscriberPageLists` (`max_values`: None, `max_size`: Some(45), added: 2520, mode: `MaxEncodedLen`) + /// Storage: `Actors::ObservationSubscriberPages` (r:1 w:0) + /// Proof: `Actors::ObservationSubscriberPages` (`max_values`: None, `max_size`: Some(641), added: 3116, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:64 w:64) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorIdentities` (r:64 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorHot` (r:64 w:64) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorActivationAuthority` (r:64 w:0) + /// Proof: `Actors::ActorActivationAuthority` (`max_values`: None, `max_size`: Some(159), added: 2634, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:64 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:64 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) + /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:65 w:65) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueHead` (r:1 w:0) + /// Proof: `Actors::QueueHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueTail` (r:1 w:0) + /// Proof: `Actors::QueueTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueOccupancy` (r:1 w:0) + /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueuePages` (r:2 w:0) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `MaxEncodedLen`) + /// Storage: `Actors::NextQueueTicket` (r:1 w:0) + /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:64 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorFunding` (r:64 w:0) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `MaxEncodedLen`) + /// Storage: `Actors::WakeupBuckets` (r:1 w:1) + /// Proof: `Actors::WakeupBuckets` (`max_values`: None, `max_size`: Some(58), added: 2533, mode: `MaxEncodedLen`) + /// Storage: `Actors::WakeupCursorLen` (r:1 w:1) + /// Proof: `Actors::WakeupCursorLen` (`max_values`: None, `max_size`: Some(21), added: 2496, mode: `MaxEncodedLen`) + /// Storage: `Actors::WakeupCursorPages` (r:1 w:1) + /// Proof: `Actors::WakeupCursorPages` (`max_values`: None, `max_size`: Some(314), added: 2789, mode: `MaxEncodedLen`) + /// Storage: `Actors::WakeupPages` (r:0 w:2) + /// Proof: `Actors::WakeupPages` (`max_values`: None, `max_size`: Some(348), added: 2823, mode: `MaxEncodedLen`) + fn observation_fanout_blocked_page() -> Weight { + // Proof Size summary in bytes: + // Measured: `70130` + // Estimated: `304734` + // Minimum execution time: 115_118_776_000 picoseconds. + Weight::from_parts(116_219_631_000, 0) + .saturating_add(Weight::from_parts(0, 304734)) + .saturating_add(T::DbWeight::get().reads(591)) + .saturating_add(T::DbWeight::get().writes(200)) } /// Storage: `Actors::CrossingWorkerFaultState` (r:1 w:0) /// Proof: `Actors::CrossingWorkerFaultState` (`max_values`: Some(1), `max_size`: Some(44), added: 539, mode: `MaxEncodedLen`) @@ -2137,115 +4131,178 @@ impl pallet_deos_actors::WeightInfo for S /// Proof: `Actors::CrossingPendingFeedListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) fn crossing_worker_base() -> Weight { // Proof Size summary in bytes: - // Measured: `383` + // Measured: `421` // Estimated: `1543` - // Minimum execution time: 5_168_000 picoseconds. - Weight::from_parts(6_705_000, 0) + // Minimum execution time: 5_797_000 picoseconds. + Weight::from_parts(7_543_000, 0) .saturating_add(Weight::from_parts(0, 1543)) .saturating_add(T::DbWeight::get().reads(2)) } /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:0) /// Proof: `Actors::CrossingPendingFeedListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingTransitionQueues` (r:1 w:0) - /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(2595), added: 5070, mode: `MaxEncodedLen`) + /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(3171), added: 5646, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingRangeCursors` (r:1 w:0) /// Proof: `Actors::CrossingRangeCursors` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingLeafStates` (r:1 w:0) /// Proof: `Actors::CrossingLeafStates` (`max_values`: None, `max_size`: Some(62), added: 2537, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingMemberPages` (r:1 w:0) - /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(1096), added: 3571, mode: `MaxEncodedLen`) + /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(8264), added: 10739, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingMemberships` (r:1 w:0) /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:1 w:0) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorIdentities` (r:1 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorHot` (r:1 w:0) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorContract` (r:1 w:0) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:1 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorActivationAuthority` (r:1 w:0) + /// Proof: `Actors::ActorActivationAuthority` (`max_values`: None, `max_size`: Some(159), added: 2634, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) fn crossing_work_probe() -> Weight { // Proof Size summary in bytes: - // Measured: `1624` - // Estimated: `12200` - // Minimum execution time: 44_071_000 picoseconds. - Weight::from_parts(46_305_000, 0) - .saturating_add(Weight::from_parts(0, 12200)) - .saturating_add(T::DbWeight::get().reads(8)) + // Measured: `2405` + // Estimated: `11729` + // Minimum execution time: 59_576_000 picoseconds. + Weight::from_parts(62_090_000, 0) + .saturating_add(Weight::from_parts(0, 11729)) + .saturating_add(T::DbWeight::get().reads(12)) + } + /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:1) + /// Proof: `Actors::CrossingPendingFeedListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `Measured`) + /// Storage: `Actors::CrossingTransitionQueues` (r:1 w:0) + /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(3171), added: 5646, mode: `Measured`) + /// Storage: `Actors::CrossingRangeCursors` (r:1 w:1) + /// Proof: `Actors::CrossingRangeCursors` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `Measured`) + /// Storage: `Actors::CrossingLeafStates` (r:2 w:2) + /// Proof: `Actors::CrossingLeafStates` (`max_values`: None, `max_size`: Some(62), added: 2537, mode: `Measured`) + /// Storage: `Actors::CrossingMemberPages` (r:2 w:2) + /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(8264), added: 10739, mode: `Measured`) + /// Storage: `Actors::CrossingMemberships` (r:1 w:1) + /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `Measured`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:1 w:1) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `Measured`) + /// Storage: `Actors::ActorHot` (r:1 w:1) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `Measured`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `Measured`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `Measured`) + /// Storage: `Actors::ActorIdentities` (r:1 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `Measured`) + /// Storage: `Actors::ActorFunding` (r:1 w:0) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `Measured`) + /// Storage: `Actors::ActorRunHead` (r:1 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `Measured`) + /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) + /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `Measured`) + /// Storage: `Actors::NextQueueTicket` (r:1 w:1) + /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::CrossingFeedMembershipCount` (r:1 w:1) + /// Proof: `Actors::CrossingFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `Measured`) + /// Storage: `Actors::CrossingUserFeedMembershipCount` (r:1 w:1) + /// Proof: `Actors::CrossingUserFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `Measured`) + /// Storage: `Actors::CrossingRadixNodes` (r:64 w:64) + /// Proof: `Actors::CrossingRadixNodes` (`max_values`: None, `max_size`: Some(53), added: 2528, mode: `Measured`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `Measured`) + /// Storage: `Actors::QueueHead` (r:1 w:0) + /// Proof: `Actors::QueueHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::QueueTail` (r:1 w:1) + /// Proof: `Actors::QueueTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::QueueOccupancy` (r:1 w:1) + /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `Measured`) + /// Storage: `Actors::QueuePages` (r:1 w:1) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `Measured`) + /// Storage: `Actors::CrossingPendingFeeds` (r:1 w:0) + /// Proof: `Actors::CrossingPendingFeeds` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `Measured`) + fn observation_crossing_trigger_occurrence() -> Weight { + // Proof Size summary in bytes: + // Measured: `4814` + // Estimated: `164204` + // Minimum execution time: 490_294_000 picoseconds. + Weight::from_parts(508_662_000, 0) + .saturating_add(Weight::from_parts(0, 164204)) + .saturating_add(T::DbWeight::get().reads(90)) + .saturating_add(T::DbWeight::get().writes(81)) } - /// Fixed-depth radix search charged only when no source threshold is retained. + /// Storage: `Actors::CrossingRadixNodes` (r:32 w:0) + /// Proof: `Actors::CrossingRadixNodes` (`max_values`: None, `max_size`: Some(53), added: 2528, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingLeafStates` (r:1 w:0) + /// Proof: `Actors::CrossingLeafStates` (`max_values`: None, `max_size`: Some(62), added: 2537, mode: `MaxEncodedLen`) fn crossing_search_probe() -> Weight { - Weight::from_parts(127_253_000, 81_886) + // Proof Size summary in bytes: + // Measured: `2415` + // Estimated: `81886` + // Minimum execution time: 123_202_000 picoseconds. + Weight::from_parts(125_926_000, 0) + .saturating_add(Weight::from_parts(0, 81886)) .saturating_add(T::DbWeight::get().reads(33)) } /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:0) /// Proof: `Actors::CrossingPendingFeedListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingTransitionQueues` (r:1 w:0) - /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(2595), added: 5070, mode: `MaxEncodedLen`) + /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(3171), added: 5646, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingRangeCursors` (r:1 w:0) /// Proof: `Actors::CrossingRangeCursors` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingLeafStates` (r:1 w:0) /// Proof: `Actors::CrossingLeafStates` (`max_values`: None, `max_size`: Some(62), added: 2537, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingMemberPages` (r:1 w:0) - /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(1096), added: 3571, mode: `MaxEncodedLen`) + /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(8264), added: 10739, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingMemberships` (r:1 w:0) /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorHot` (r:1 w:0) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorContract` (r:1 w:0) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:1 w:0) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorIdentities` (r:1 w:0) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorFunding` (r:1 w:0) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `MaxEncodedLen`) - /// Storage: `Actors::ContinuationState` (r:1 w:0) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:1 w:0) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorHot` (r:1 w:0) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:1 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorActivationAuthority` (r:1 w:0) + /// Proof: `Actors::ActorActivationAuthority` (`max_values`: None, `max_size`: Some(159), added: 2634, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) /// Storage: `Actors::NextQueueTicket` (r:1 w:0) /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) fn crossing_fire_probe() -> Weight { // Proof Size summary in bytes: - // Measured: `2266` - // Estimated: `12200` - // Minimum execution time: 69_563_000 picoseconds. - Weight::from_parts(72_427_000, 0) - .saturating_add(Weight::from_parts(0, 12200)) + // Measured: `2307` + // Estimated: `11729` + // Minimum execution time: 63_906_000 picoseconds. + Weight::from_parts(66_770_000, 0) + .saturating_add(Weight::from_parts(0, 11729)) .saturating_add(T::DbWeight::get().reads(14)) } - /// Storage: `Actors::CrossingMemberPages` (r:1 w:0) - /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(1096), added: 3571, mode: `MaxEncodedLen`) - fn crossing_tail_refill_probe() -> Weight { - // Proof Size summary in bytes: - // Measured: `795` - // Estimated: `4561` - // Minimum execution time: 14_597_000 picoseconds. - Weight::from_parts(17_530_000, 0) - .saturating_add(Weight::from_parts(0, 4561)) - .saturating_add(T::DbWeight::get().reads(1)) - } /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:0) /// Proof: `Actors::CrossingPendingFeedListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingTransitionQueues` (r:1 w:0) - /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(2595), added: 5070, mode: `MaxEncodedLen`) + /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(3171), added: 5646, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingRangeCursors` (r:1 w:0) /// Proof: `Actors::CrossingRangeCursors` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingLeafStates` (r:1 w:0) /// Proof: `Actors::CrossingLeafStates` (`max_values`: None, `max_size`: Some(62), added: 2537, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingMemberPages` (r:1 w:0) - /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(1096), added: 3571, mode: `MaxEncodedLen`) + /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(8264), added: 10739, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingMemberships` (r:2 w:0) /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorHot` (r:2 w:0) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorContract` (r:2 w:0) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:2 w:0) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorIdentities` (r:2 w:0) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorFunding` (r:2 w:0) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `MaxEncodedLen`) - /// Storage: `Actors::ContinuationState` (r:2 w:0) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:2 w:0) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorHot` (r:2 w:0) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:2 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorActivationAuthority` (r:2 w:0) + /// Proof: `Actors::ActorActivationAuthority` (`max_values`: None, `max_size`: Some(159), added: 2634, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:2 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) /// Storage: `Actors::NextQueueTicket` (r:1 w:0) @@ -2257,200 +4314,247 @@ impl pallet_deos_actors::WeightInfo for S /// Storage: `Actors::QueueOccupancy` (r:1 w:0) /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `Actors::QueuePages` (r:1 w:0) - /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(1050), added: 3525, mode: `MaxEncodedLen`) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `MaxEncodedLen`) fn crossing_fire_pair_probe() -> Weight { // Proof Size summary in bytes: - // Measured: `5585` - // Estimated: `23410` - // Minimum execution time: 203_102_000 picoseconds. - Weight::from_parts(241_375_000, 0) - .saturating_add(Weight::from_parts(0, 23410)) + // Measured: `7431` + // Estimated: `11729` + // Minimum execution time: 196_816_000 picoseconds. + Weight::from_parts(216_651_000, 0) + .saturating_add(Weight::from_parts(0, 11729)) .saturating_add(T::DbWeight::get().reads(25)) } - /// Storage: `Actors::CrossingMemberships` (r:4 w:0) + /// Storage: `Actors::CrossingMemberPages` (r:1 w:0) + /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(8264), added: 10739, mode: `MaxEncodedLen`) + fn crossing_tail_refill_probe() -> Weight { + // Proof Size summary in bytes: + // Measured: `996` + // Estimated: `11729` + // Minimum execution time: 17_670_000 picoseconds. + Weight::from_parts(20_114_000, 0) + .saturating_add(Weight::from_parts(0, 11729)) + .saturating_add(T::DbWeight::get().reads(1)) + } + /// Storage: `Actors::CrossingMemberships` (r:128 w:0) /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorHot` (r:4 w:0) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorContract` (r:4 w:0) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorIdentities` (r:4 w:0) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:128 w:0) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorIdentities` (r:128 w:0) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorFunding` (r:4 w:0) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `MaxEncodedLen`) - /// Storage: `Actors::ContinuationState` (r:4 w:0) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:4 w:0) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorHot` (r:128 w:0) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:128 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorActivationAuthority` (r:128 w:0) + /// Proof: `Actors::ActorActivationAuthority` (`max_values`: None, `max_size`: Some(159), added: 2634, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:128 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) /// Storage: `Actors::NextQueueTicket` (r:1 w:0) /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - /// The range of component `c` is `[1, 4]`. - fn crossing_fire_cohort_preflight(c: u32) -> Weight { + /// The range of component `c` is `[1, 128]`. + fn crossing_fire_cohort_preflight(c: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `1295 + c * (583 ±0)` - // Estimated: `1493 + c * (11210 ±0)` - // Minimum execution time: 51_473_000 picoseconds. - Weight::from_parts(21_192_911, 0) + // Measured: `1416 + c * (598 ±0)` + // Estimated: `1493 + c * (2699 ±0)` + // Minimum execution time: 41_696_000 picoseconds. + Weight::from_parts(41_975_000, 0) .saturating_add(Weight::from_parts(0, 1493)) - // Standard Error: 26_660 - .saturating_add(Weight::from_parts(33_046_169, 0).saturating_mul(c.into())) + // Standard Error: 24_685 + .saturating_add(Weight::from_parts(23_857_544, 0).saturating_mul(c.into())) .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().reads((7_u64).saturating_mul(c.into()))) - .saturating_add(Weight::from_parts(0, 11210).saturating_mul(c.into())) + .saturating_add(Weight::from_parts(0, 2699).saturating_mul(c.into())) } - /// Storage: `Actors::CrossingMemberships` (r:4 w:0) + /// Storage: `Actors::CrossingMemberships` (r:128 w:0) /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorHot` (r:4 w:0) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorContract` (r:4 w:0) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorIdentities` (r:4 w:0) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:128 w:0) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorIdentities` (r:128 w:0) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorFunding` (r:4 w:0) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `MaxEncodedLen`) - /// Storage: `Actors::ContinuationState` (r:4 w:0) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:4 w:0) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorHot` (r:128 w:0) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:128 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorActivationAuthority` (r:128 w:0) + /// Proof: `Actors::ActorActivationAuthority` (`max_values`: None, `max_size`: Some(159), added: 2634, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:128 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) /// Storage: `Actors::NextQueueTicket` (r:1 w:0) /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - /// The range of component `c` is `[1, 4]`. - fn crossing_coalesced_cohort_preflight(c: u32) -> Weight { + /// The range of component `c` is `[1, 128]`. + fn crossing_coalesced_cohort_preflight(c: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `1330 + c * (643 ±0)` - // Estimated: `1493 + c * (11210 ±0)` - // Minimum execution time: 53_151_000 picoseconds. - Weight::from_parts(23_064_782, 0) + // Measured: `1511 + c * (608 ±0)` + // Estimated: `1493 + c * (2699 ±0)` + // Minimum execution time: 43_302_000 picoseconds. + Weight::from_parts(44_280_000, 0) .saturating_add(Weight::from_parts(0, 1493)) - // Standard Error: 33_837 - .saturating_add(Weight::from_parts(33_354_579, 0).saturating_mul(c.into())) + // Standard Error: 31_643 + .saturating_add(Weight::from_parts(24_157_448, 0).saturating_mul(c.into())) .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().reads((7_u64).saturating_mul(c.into()))) - .saturating_add(Weight::from_parts(0, 11210).saturating_mul(c.into())) + .saturating_add(Weight::from_parts(0, 2699).saturating_mul(c.into())) } - /// Storage: `Actors::CrossingMemberships` (r:4 w:0) + /// Storage: `Actors::CrossingMemberships` (r:128 w:0) /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorHot` (r:4 w:0) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorContract` (r:4 w:0) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorIdentities` (r:4 w:0) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:128 w:0) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorIdentities` (r:128 w:0) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorFunding` (r:4 w:0) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `MaxEncodedLen`) - /// Storage: `Actors::ContinuationState` (r:4 w:0) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:4 w:0) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorHot` (r:128 w:0) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:128 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorActivationAuthority` (r:128 w:0) + /// Proof: `Actors::ActorActivationAuthority` (`max_values`: None, `max_size`: Some(159), added: 2634, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:128 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) /// Storage: `Actors::NextQueueTicket` (r:1 w:0) /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - /// The range of component `c` is `[1, 4]`. - fn crossing_terminal_cohort_preflight(c: u32) -> Weight { + /// The range of component `c` is `[1, 128]`. + fn crossing_terminal_cohort_preflight(c: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `1304 + c * (599 ±0)` - // Estimated: `1493 + c * (11210 ±0)` - // Minimum execution time: 51_614_000 picoseconds. - Weight::from_parts(21_646_700, 0) + // Measured: `1442 + c * (598 ±0)` + // Estimated: `1493 + c * (2699 ±0)` + // Minimum execution time: 42_394_000 picoseconds. + Weight::from_parts(43_721_000, 0) .saturating_add(Weight::from_parts(0, 1493)) - // Standard Error: 26_929 - .saturating_add(Weight::from_parts(33_085_920, 0).saturating_mul(c.into())) + // Standard Error: 24_923 + .saturating_add(Weight::from_parts(23_827_571, 0).saturating_mul(c.into())) .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().reads((7_u64).saturating_mul(c.into()))) - .saturating_add(Weight::from_parts(0, 11210).saturating_mul(c.into())) + .saturating_add(Weight::from_parts(0, 2699).saturating_mul(c.into())) } - /// Storage: `Actors::CrossingMemberships` (r:4 w:0) + /// Storage: `Actors::CrossingMemberships` (r:128 w:0) /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorHot` (r:4 w:0) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) - /// The range of component `c` is `[1, 4]`. - fn crossing_skip_cohort_preflight(c: u32) -> Weight { + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:128 w:0) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorIdentities` (r:128 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorHot` (r:128 w:0) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:128 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorActivationAuthority` (r:128 w:0) + /// Proof: `Actors::ActorActivationAuthority` (`max_values`: None, `max_size`: Some(159), added: 2634, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:128 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) + /// The range of component `c` is `[1, 128]`. + fn crossing_skip_cohort_preflight(c: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `654 + c * (134 ±0)` - // Estimated: `990 + c * (2561 ±0)` - // Minimum execution time: 13_759_000 picoseconds. - Weight::from_parts(6_986_537, 0) + // Measured: `1389 + c * (598 ±0)` + // Estimated: `990 + c * (2699 ±0)` + // Minimum execution time: 38_762_000 picoseconds. + Weight::from_parts(40_718_000, 0) .saturating_add(Weight::from_parts(0, 990)) - // Standard Error: 9_318 - .saturating_add(Weight::from_parts(8_569_450, 0).saturating_mul(c.into())) - .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(c.into()))) - .saturating_add(Weight::from_parts(0, 2561).saturating_mul(c.into())) + // Standard Error: 23_555 + .saturating_add(Weight::from_parts(22_509_580, 0).saturating_mul(c.into())) + .saturating_add(T::DbWeight::get().reads((7_u64).saturating_mul(c.into()))) + .saturating_add(Weight::from_parts(0, 2699).saturating_mul(c.into())) } - /// Storage: `Actors::CrossingMemberships` (r:4 w:0) + /// Storage: `Actors::CrossingMemberships` (r:128 w:0) /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorHot` (r:4 w:0) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorContract` (r:4 w:0) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `MaxEncodedLen`) - /// The range of component `c` is `[1, 4]`. - fn crossing_rearm_cohort_preflight(c: u32) -> Weight { + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:128 w:0) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorIdentities` (r:128 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorHot` (r:128 w:0) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:128 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorActivationAuthority` (r:128 w:0) + /// Proof: `Actors::ActorActivationAuthority` (`max_values`: None, `max_size`: Some(159), added: 2634, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:128 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) + /// The range of component `c` is `[1, 128]`. + fn crossing_rearm_cohort_preflight(c: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `869 + c * (272 ±0)` - // Estimated: `990 + c * (11210 ±0)` - // Minimum execution time: 22_489_000 picoseconds. - Weight::from_parts(12_893_611, 0) + // Measured: `1484 + c * (608 ±0)` + // Estimated: `990 + c * (2699 ±0)` + // Minimum execution time: 40_439_000 picoseconds. + Weight::from_parts(40_928_000, 0) .saturating_add(Weight::from_parts(0, 990)) - // Standard Error: 15_426 - .saturating_add(Weight::from_parts(11_773_740, 0).saturating_mul(c.into())) - .saturating_add(T::DbWeight::get().reads((3_u64).saturating_mul(c.into()))) - .saturating_add(Weight::from_parts(0, 11210).saturating_mul(c.into())) + // Standard Error: 32_080 + .saturating_add(Weight::from_parts(22_797_266, 0).saturating_mul(c.into())) + .saturating_add(T::DbWeight::get().reads((7_u64).saturating_mul(c.into()))) + .saturating_add(Weight::from_parts(0, 2699).saturating_mul(c.into())) } /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:0) /// Proof: `Actors::CrossingPendingFeedListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingTransitionQueues` (r:1 w:0) - /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(2595), added: 5070, mode: `MaxEncodedLen`) + /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(3171), added: 5646, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingRangeCursors` (r:1 w:0) /// Proof: `Actors::CrossingRangeCursors` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingLeafStates` (r:1 w:0) /// Proof: `Actors::CrossingLeafStates` (`max_values`: None, `max_size`: Some(62), added: 2537, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingMemberPages` (r:1 w:0) - /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(1096), added: 3571, mode: `MaxEncodedLen`) + /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(8264), added: 10739, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingMemberships` (r:2 w:0) /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:2 w:0) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorIdentities` (r:2 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorHot` (r:2 w:0) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorContract` (r:2 w:0) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:2 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorActivationAuthority` (r:2 w:0) + /// Proof: `Actors::ActorActivationAuthority` (`max_values`: None, `max_size`: Some(159), added: 2634, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:2 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) fn crossing_rearm_pair_probe() -> Weight { // Proof Size summary in bytes: - // Measured: `1917` - // Estimated: `23410` - // Minimum execution time: 55_874_000 picoseconds. - Weight::from_parts(58_249_000, 0) - .saturating_add(Weight::from_parts(0, 23410)) - .saturating_add(T::DbWeight::get().reads(11)) + // Measured: `3095` + // Estimated: `11729` + // Minimum execution time: 89_678_000 picoseconds. + Weight::from_parts(93_449_000, 0) + .saturating_add(Weight::from_parts(0, 11729)) + .saturating_add(T::DbWeight::get().reads(19)) } /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:0) /// Proof: `Actors::CrossingPendingFeedListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingTransitionQueues` (r:1 w:0) - /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(2595), added: 5070, mode: `MaxEncodedLen`) + /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(3171), added: 5646, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingRangeCursors` (r:1 w:0) /// Proof: `Actors::CrossingRangeCursors` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingLeafStates` (r:1 w:0) /// Proof: `Actors::CrossingLeafStates` (`max_values`: None, `max_size`: Some(62), added: 2537, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingMemberPages` (r:1 w:0) - /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(1096), added: 3571, mode: `MaxEncodedLen`) + /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(8264), added: 10739, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingMemberships` (r:2 w:0) /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:2 w:0) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorIdentities` (r:2 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorHot` (r:2 w:0) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:2 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorActivationAuthority` (r:2 w:0) + /// Proof: `Actors::ActorActivationAuthority` (`max_values`: None, `max_size`: Some(159), added: 2634, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:2 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) fn crossing_skip_pair_probe() -> Weight { // Proof Size summary in bytes: - // Measured: `1444` - // Estimated: `6112` - // Minimum execution time: 44_350_000 picoseconds. - Weight::from_parts(46_515_000, 0) - .saturating_add(Weight::from_parts(0, 6112)) - .saturating_add(T::DbWeight::get().reads(9)) + // Measured: `2960` + // Estimated: `11729` + // Minimum execution time: 86_116_000 picoseconds. + Weight::from_parts(89_188_000, 0) + .saturating_add(Weight::from_parts(0, 11729)) + .saturating_add(T::DbWeight::get().reads(19)) } /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:1) /// Proof: `Actors::CrossingPendingFeedListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingTransitionQueues` (r:1 w:0) - /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(2595), added: 5070, mode: `MaxEncodedLen`) + /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(3171), added: 5646, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingRangeCursors` (r:1 w:1) /// Proof: `Actors::CrossingRangeCursors` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingRadixNodes` (r:1 w:0) @@ -2459,18 +4563,18 @@ impl pallet_deos_actors::WeightInfo for S /// Proof: `Actors::CrossingPendingFeeds` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) fn crossing_transition_unit() -> Weight { // Proof Size summary in bytes: - // Measured: `1211` - // Estimated: `6060` - // Minimum execution time: 30_102_000 picoseconds. - Weight::from_parts(31_918_000, 0) - .saturating_add(Weight::from_parts(0, 6060)) + // Measured: `1307` + // Estimated: `6636` + // Minimum execution time: 31_289_000 picoseconds. + Weight::from_parts(33_105_000, 0) + .saturating_add(Weight::from_parts(0, 6636)) .saturating_add(T::DbWeight::get().reads(5)) .saturating_add(T::DbWeight::get().writes(2)) } /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:1) /// Proof: `Actors::CrossingPendingFeedListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingTransitionQueues` (r:1 w:0) - /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(2595), added: 5070, mode: `MaxEncodedLen`) + /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(3171), added: 5646, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingRangeCursors` (r:1 w:1) /// Proof: `Actors::CrossingRangeCursors` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingRadixNodes` (r:64 w:64) @@ -2478,27 +4582,33 @@ impl pallet_deos_actors::WeightInfo for S /// Storage: `Actors::CrossingLeafStates` (r:2 w:2) /// Proof: `Actors::CrossingLeafStates` (`max_values`: None, `max_size`: Some(62), added: 2537, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingMemberPages` (r:2 w:2) - /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(1096), added: 3571, mode: `MaxEncodedLen`) + /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(8264), added: 10739, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingMemberships` (r:1 w:1) /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:1 w:1) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorHot` (r:1 w:1) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorContract` (r:1 w:0) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorIdentities` (r:1 w:0) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorFunding` (r:1 w:0) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:1 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) + /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + /// Storage: `Actors::NextQueueTicket` (r:1 w:1) + /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingFeedMembershipCount` (r:1 w:1) /// Proof: `Actors::CrossingFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingUserFeedMembershipCount` (r:1 w:1) /// Proof: `Actors::CrossingUserFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorFunding` (r:1 w:0) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `MaxEncodedLen`) - /// Storage: `Actors::ContinuationState` (r:1 w:0) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:1 w:0) + /// Storage: `System::Account` (r:2 w:2) /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) - /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) /// Storage: `Actors::QueueHead` (r:1 w:0) /// Proof: `Actors::QueueHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `Actors::QueueTail` (r:1 w:1) @@ -2506,25 +4616,23 @@ impl pallet_deos_actors::WeightInfo for S /// Storage: `Actors::QueueOccupancy` (r:1 w:1) /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `Actors::QueuePages` (r:1 w:1) - /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(1050), added: 3525, mode: `MaxEncodedLen`) - /// Storage: `Actors::NextQueueTicket` (r:1 w:1) - /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingPendingFeeds` (r:1 w:0) /// Proof: `Actors::CrossingPendingFeeds` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) fn crossing_leaf_unit() -> Weight { // Proof Size summary in bytes: - // Measured: `3958` + // Measured: `4751` // Estimated: `162782` - // Minimum execution time: 424_153_000 picoseconds. - Weight::from_parts(439_029_000, 0) + // Minimum execution time: 530_872_000 picoseconds. + Weight::from_parts(553_082_000, 0) .saturating_add(Weight::from_parts(0, 162782)) - .saturating_add(T::DbWeight::get().reads(87)) - .saturating_add(T::DbWeight::get().writes(78)) + .saturating_add(T::DbWeight::get().reads(90)) + .saturating_add(T::DbWeight::get().writes(81)) } /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:1) /// Proof: `Actors::CrossingPendingFeedListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingTransitionQueues` (r:1 w:0) - /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(2595), added: 5070, mode: `MaxEncodedLen`) + /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(3171), added: 5646, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingRangeCursors` (r:1 w:1) /// Proof: `Actors::CrossingRangeCursors` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingRadixNodes` (r:64 w:64) @@ -2532,27 +4640,33 @@ impl pallet_deos_actors::WeightInfo for S /// Storage: `Actors::CrossingLeafStates` (r:2 w:2) /// Proof: `Actors::CrossingLeafStates` (`max_values`: None, `max_size`: Some(62), added: 2537, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingMemberPages` (r:2 w:2) - /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(1096), added: 3571, mode: `MaxEncodedLen`) + /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(8264), added: 10739, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingMemberships` (r:1 w:1) /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:1 w:1) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorHot` (r:1 w:1) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorContract` (r:1 w:0) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorIdentities` (r:1 w:0) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorFunding` (r:1 w:0) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:1 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) + /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + /// Storage: `Actors::NextQueueTicket` (r:1 w:1) + /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingFeedMembershipCount` (r:1 w:1) /// Proof: `Actors::CrossingFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingUserFeedMembershipCount` (r:1 w:1) /// Proof: `Actors::CrossingUserFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorFunding` (r:1 w:0) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `MaxEncodedLen`) - /// Storage: `Actors::ContinuationState` (r:1 w:0) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:1 w:0) + /// Storage: `System::Account` (r:2 w:2) /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) - /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) /// Storage: `Actors::QueueHead` (r:1 w:0) /// Proof: `Actors::QueueHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `Actors::QueueTail` (r:1 w:1) @@ -2560,25 +4674,23 @@ impl pallet_deos_actors::WeightInfo for S /// Storage: `Actors::QueueOccupancy` (r:1 w:1) /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `Actors::QueuePages` (r:1 w:1) - /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(1050), added: 3525, mode: `MaxEncodedLen`) - /// Storage: `Actors::NextQueueTicket` (r:1 w:1) - /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingPendingFeeds` (r:1 w:0) /// Proof: `Actors::CrossingPendingFeeds` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) fn crossing_page_unit() -> Weight { // Proof Size summary in bytes: - // Measured: `3958` + // Measured: `4751` // Estimated: `162782` - // Minimum execution time: 423_873_000 picoseconds. - Weight::from_parts(440_426_000, 0) + // Minimum execution time: 531_710_000 picoseconds. + Weight::from_parts(553_710_000, 0) .saturating_add(Weight::from_parts(0, 162782)) - .saturating_add(T::DbWeight::get().reads(87)) - .saturating_add(T::DbWeight::get().writes(78)) + .saturating_add(T::DbWeight::get().reads(90)) + .saturating_add(T::DbWeight::get().writes(81)) } /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:1) /// Proof: `Actors::CrossingPendingFeedListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingTransitionQueues` (r:1 w:0) - /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(2595), added: 5070, mode: `MaxEncodedLen`) + /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(3171), added: 5646, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingRangeCursors` (r:1 w:1) /// Proof: `Actors::CrossingRangeCursors` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingRadixNodes` (r:64 w:64) @@ -2586,13 +4698,17 @@ impl pallet_deos_actors::WeightInfo for S /// Storage: `Actors::CrossingLeafStates` (r:2 w:2) /// Proof: `Actors::CrossingLeafStates` (`max_values`: None, `max_size`: Some(62), added: 2537, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingMemberPages` (r:2 w:2) - /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(1096), added: 3571, mode: `MaxEncodedLen`) + /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(8264), added: 10739, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingMemberships` (r:1 w:1) /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:1 w:0) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorHot` (r:1 w:1) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorContract` (r:1 w:0) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorIdentities` (r:1 w:0) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingFeedMembershipCount` (r:1 w:1) @@ -2603,30 +4719,34 @@ impl pallet_deos_actors::WeightInfo for S /// Proof: `Actors::CrossingPendingFeeds` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) fn crossing_rearm_unit() -> Weight { // Proof Size summary in bytes: - // Measured: `3812` + // Measured: `4436` // Estimated: `162782` - // Minimum execution time: 383_575_000 picoseconds. - Weight::from_parts(397_683_000, 0) + // Minimum execution time: 405_016_000 picoseconds. + Weight::from_parts(420_591_000, 0) .saturating_add(Weight::from_parts(0, 162782)) - .saturating_add(T::DbWeight::get().reads(78)) + .saturating_add(T::DbWeight::get().reads(80)) .saturating_add(T::DbWeight::get().writes(74)) } /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:1) /// Proof: `Actors::CrossingPendingFeedListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingTransitionQueues` (r:1 w:0) - /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(2595), added: 5070, mode: `MaxEncodedLen`) + /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(3171), added: 5646, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingRangeCursors` (r:1 w:1) /// Proof: `Actors::CrossingRangeCursors` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingLeafStates` (r:2 w:2) /// Proof: `Actors::CrossingLeafStates` (`max_values`: None, `max_size`: Some(62), added: 2537, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingMemberPages` (r:2 w:2) - /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(1096), added: 3571, mode: `MaxEncodedLen`) + /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(8264), added: 10739, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingMemberships` (r:2 w:2) /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:2 w:0) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorHot` (r:2 w:2) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorContract` (r:2 w:0) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:2 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:2 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorIdentities` (r:2 w:0) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingFeedMembershipCount` (r:1 w:1) @@ -2639,18 +4759,18 @@ impl pallet_deos_actors::WeightInfo for S /// Proof: `Actors::CrossingPendingFeeds` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) fn crossing_rearm_pair_unit() -> Weight { // Proof Size summary in bytes: - // Measured: `4322` + // Measured: `5317` // Estimated: `162782` - // Minimum execution time: 414_375_000 picoseconds. - Weight::from_parts(430_439_000, 0) + // Minimum execution time: 456_420_000 picoseconds. + Weight::from_parts(472_623_000, 0) .saturating_add(Weight::from_parts(0, 162782)) - .saturating_add(T::DbWeight::get().reads(82)) + .saturating_add(T::DbWeight::get().reads(86)) .saturating_add(T::DbWeight::get().writes(76)) } /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:1) /// Proof: `Actors::CrossingPendingFeedListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingTransitionQueues` (r:1 w:0) - /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(2595), added: 5070, mode: `MaxEncodedLen`) + /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(3171), added: 5646, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingRangeCursors` (r:1 w:1) /// Proof: `Actors::CrossingRangeCursors` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingRadixNodes` (r:64 w:64) @@ -2658,87 +4778,95 @@ impl pallet_deos_actors::WeightInfo for S /// Storage: `Actors::CrossingLeafStates` (r:2 w:2) /// Proof: `Actors::CrossingLeafStates` (`max_values`: None, `max_size`: Some(62), added: 2537, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingMemberPages` (r:2 w:2) - /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(1096), added: 3571, mode: `MaxEncodedLen`) + /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(8264), added: 10739, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingMemberships` (r:1 w:1) /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:1 w:0) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorHot` (r:1 w:1) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorContract` (r:1 w:0) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorIdentities` (r:1 w:0) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorFunding` (r:1 w:0) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:1 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) + /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + /// Storage: `Actors::NextQueueTicket` (r:1 w:0) + /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingFeedMembershipCount` (r:1 w:1) /// Proof: `Actors::CrossingFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingUserFeedMembershipCount` (r:1 w:1) /// Proof: `Actors::CrossingUserFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorFunding` (r:1 w:0) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `MaxEncodedLen`) - /// Storage: `Actors::ContinuationState` (r:1 w:0) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:1 w:0) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) - /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingPendingFeeds` (r:1 w:0) /// Proof: `Actors::CrossingPendingFeeds` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) fn crossing_coalesced_unit() -> Weight { // Proof Size summary in bytes: - // Measured: `4083` + // Measured: `4529` // Estimated: `162782` - // Minimum execution time: 404_178_000 picoseconds. - Weight::from_parts(419_403_000, 0) + // Minimum execution time: 423_943_000 picoseconds. + Weight::from_parts(439_728_000, 0) .saturating_add(Weight::from_parts(0, 162782)) - .saturating_add(T::DbWeight::get().reads(82)) + .saturating_add(T::DbWeight::get().reads(84)) .saturating_add(T::DbWeight::get().writes(74)) } /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:1) /// Proof: `Actors::CrossingPendingFeedListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingTransitionQueues` (r:1 w:0) - /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(2595), added: 5070, mode: `MaxEncodedLen`) + /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(3171), added: 5646, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingRangeCursors` (r:1 w:1) /// Proof: `Actors::CrossingRangeCursors` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingLeafStates` (r:2 w:2) /// Proof: `Actors::CrossingLeafStates` (`max_values`: None, `max_size`: Some(62), added: 2537, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingMemberPages` (r:2 w:2) - /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(1096), added: 3571, mode: `MaxEncodedLen`) + /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(8264), added: 10739, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingMemberships` (r:2 w:2) /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:2 w:0) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorHot` (r:2 w:2) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorContract` (r:2 w:0) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:2 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:2 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorIdentities` (r:2 w:0) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorFunding` (r:2 w:0) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:2 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) + /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + /// Storage: `Actors::NextQueueTicket` (r:1 w:0) + /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingFeedMembershipCount` (r:1 w:1) /// Proof: `Actors::CrossingFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingUserFeedMembershipCount` (r:1 w:1) /// Proof: `Actors::CrossingUserFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingRadixNodes` (r:64 w:64) /// Proof: `Actors::CrossingRadixNodes` (`max_values`: None, `max_size`: Some(53), added: 2528, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorFunding` (r:2 w:0) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `MaxEncodedLen`) - /// Storage: `Actors::ContinuationState` (r:2 w:0) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:2 w:0) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) - /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingPendingFeeds` (r:1 w:0) /// Proof: `Actors::CrossingPendingFeeds` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) fn crossing_coalesced_pair_unit() -> Weight { // Proof Size summary in bytes: - // Measured: `4746` + // Measured: `5442` // Estimated: `162782` - // Minimum execution time: 457_957_000 picoseconds. - Weight::from_parts(478_630_000, 0) + // Minimum execution time: 499_862_000 picoseconds. + Weight::from_parts(521_862_000, 0) .saturating_add(Weight::from_parts(0, 162782)) - .saturating_add(T::DbWeight::get().reads(89)) + .saturating_add(T::DbWeight::get().reads(92)) .saturating_add(T::DbWeight::get().writes(76)) } /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:1) /// Proof: `Actors::CrossingPendingFeedListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingTransitionQueues` (r:1 w:0) - /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(2595), added: 5070, mode: `MaxEncodedLen`) + /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(3171), added: 5646, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingRangeCursors` (r:1 w:1) /// Proof: `Actors::CrossingRangeCursors` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingRadixNodes` (r:64 w:64) @@ -2746,27 +4874,33 @@ impl pallet_deos_actors::WeightInfo for S /// Storage: `Actors::CrossingLeafStates` (r:2 w:2) /// Proof: `Actors::CrossingLeafStates` (`max_values`: None, `max_size`: Some(62), added: 2537, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingMemberPages` (r:2 w:2) - /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(1096), added: 3571, mode: `MaxEncodedLen`) + /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(8264), added: 10739, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingMemberships` (r:1 w:1) /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:1 w:1) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorHot` (r:1 w:1) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorContract` (r:1 w:0) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorIdentities` (r:1 w:0) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorFunding` (r:1 w:0) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:1 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) + /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + /// Storage: `Actors::NextQueueTicket` (r:1 w:1) + /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingFeedMembershipCount` (r:1 w:1) /// Proof: `Actors::CrossingFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingUserFeedMembershipCount` (r:1 w:1) /// Proof: `Actors::CrossingUserFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorFunding` (r:1 w:0) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `MaxEncodedLen`) - /// Storage: `Actors::ContinuationState` (r:1 w:0) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:1 w:0) + /// Storage: `System::Account` (r:2 w:2) /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) - /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) /// Storage: `Actors::QueueHead` (r:1 w:0) /// Proof: `Actors::QueueHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `Actors::QueueTail` (r:1 w:1) @@ -2774,53 +4908,47 @@ impl pallet_deos_actors::WeightInfo for S /// Storage: `Actors::QueueOccupancy` (r:1 w:1) /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `Actors::QueuePages` (r:1 w:1) - /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(1050), added: 3525, mode: `MaxEncodedLen`) - /// Storage: `Actors::NextQueueTicket` (r:1 w:1) - /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingPendingFeeds` (r:1 w:0) /// Proof: `Actors::CrossingPendingFeeds` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) fn crossing_placed_unit() -> Weight { // Proof Size summary in bytes: - // Measured: `3958` + // Measured: `4751` // Estimated: `162782` - // Minimum execution time: 424_502_000 picoseconds. - Weight::from_parts(439_379_000, 0) + // Minimum execution time: 534_154_000 picoseconds. + Weight::from_parts(555_946_000, 0) .saturating_add(Weight::from_parts(0, 162782)) - .saturating_add(T::DbWeight::get().reads(87)) - .saturating_add(T::DbWeight::get().writes(78)) + .saturating_add(T::DbWeight::get().reads(90)) + .saturating_add(T::DbWeight::get().writes(81)) } /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:1) /// Proof: `Actors::CrossingPendingFeedListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingTransitionQueues` (r:1 w:0) - /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(2595), added: 5070, mode: `MaxEncodedLen`) + /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(3171), added: 5646, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingRangeCursors` (r:1 w:1) /// Proof: `Actors::CrossingRangeCursors` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingLeafStates` (r:2 w:2) /// Proof: `Actors::CrossingLeafStates` (`max_values`: None, `max_size`: Some(62), added: 2537, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingMemberPages` (r:2 w:2) - /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(1096), added: 3571, mode: `MaxEncodedLen`) + /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(8264), added: 10739, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingMemberships` (r:2 w:2) /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorHot` (r:2 w:2) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorContract` (r:2 w:0) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:2 w:2) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorIdentities` (r:2 w:0) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) - /// Storage: `Actors::CrossingFeedMembershipCount` (r:1 w:1) - /// Proof: `Actors::CrossingFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) - /// Storage: `Actors::CrossingUserFeedMembershipCount` (r:1 w:1) - /// Proof: `Actors::CrossingUserFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) - /// Storage: `Actors::CrossingRadixNodes` (r:64 w:64) - /// Proof: `Actors::CrossingRadixNodes` (`max_values`: None, `max_size`: Some(53), added: 2528, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorFunding` (r:2 w:0) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `MaxEncodedLen`) - /// Storage: `Actors::ContinuationState` (r:2 w:0) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:2 w:0) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorHot` (r:2 w:2) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:2 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorActivationAuthority` (r:2 w:0) + /// Proof: `Actors::ActorActivationAuthority` (`max_values`: None, `max_size`: Some(159), added: 2634, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:2 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + /// Storage: `Actors::NextQueueTicket` (r:1 w:1) + /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `Actors::QueueHead` (r:1 w:0) /// Proof: `Actors::QueueHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `Actors::QueueTail` (r:1 w:1) @@ -2828,48 +4956,199 @@ impl pallet_deos_actors::WeightInfo for S /// Storage: `Actors::QueueOccupancy` (r:1 w:1) /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `Actors::QueuePages` (r:1 w:1) - /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(1050), added: 3525, mode: `MaxEncodedLen`) - /// Storage: `Actors::NextQueueTicket` (r:1 w:1) - /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:3 w:3) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingFeedMembershipCount` (r:1 w:1) + /// Proof: `Actors::CrossingFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingUserFeedMembershipCount` (r:1 w:1) + /// Proof: `Actors::CrossingUserFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingRadixNodes` (r:64 w:64) + /// Proof: `Actors::CrossingRadixNodes` (`max_values`: None, `max_size`: Some(53), added: 2528, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingPendingFeeds` (r:1 w:0) /// Proof: `Actors::CrossingPendingFeeds` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) fn crossing_placed_pair_unit() -> Weight { // Proof Size summary in bytes: - // Measured: `4611` + // Measured: `5231` // Estimated: `162782` - // Minimum execution time: 464_312_000 picoseconds. - Weight::from_parts(510_967_000, 0) + // Minimum execution time: 562_860_000 picoseconds. + Weight::from_parts(586_537_000, 0) .saturating_add(Weight::from_parts(0, 162782)) - .saturating_add(T::DbWeight::get().reads(94)) - .saturating_add(T::DbWeight::get().writes(80)) + .saturating_add(T::DbWeight::get().reads(97)) + .saturating_add(T::DbWeight::get().writes(85)) } - /// Maximum homogeneous four-candidate placed batch. + /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:1) + /// Proof: `Actors::CrossingPendingFeedListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingTransitionQueues` (r:1 w:0) + /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(3171), added: 5646, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingRangeCursors` (r:1 w:1) + /// Proof: `Actors::CrossingRangeCursors` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingLeafStates` (r:2 w:2) + /// Proof: `Actors::CrossingLeafStates` (`max_values`: None, `max_size`: Some(62), added: 2537, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingMemberPages` (r:2 w:2) + /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(8264), added: 10739, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingMemberships` (r:128 w:128) + /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:128 w:128) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorIdentities` (r:128 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorHot` (r:128 w:128) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:128 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorActivationAuthority` (r:128 w:0) + /// Proof: `Actors::ActorActivationAuthority` (`max_values`: None, `max_size`: Some(159), added: 2634, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:128 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) + /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) + /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + /// Storage: `Actors::NextQueueTicket` (r:1 w:1) + /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueHead` (r:1 w:0) + /// Proof: `Actors::QueueHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueTail` (r:1 w:1) + /// Proof: `Actors::QueueTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueOccupancy` (r:1 w:1) + /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueuePages` (r:2 w:2) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:5 w:5) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingFeedMembershipCount` (r:1 w:1) + /// Proof: `Actors::CrossingFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingRadixNodes` (r:64 w:64) + /// Proof: `Actors::CrossingRadixNodes` (`max_values`: None, `max_size`: Some(53), added: 2528, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingUserFeedMembershipCount` (r:1 w:1) + /// Proof: `Actors::CrossingUserFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingPendingFeeds` (r:1 w:0) + /// Proof: `Actors::CrossingPendingFeeds` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) fn crossing_placed_maximum_unit() -> Weight { // Proof Size summary in bytes: - // Measured: `10977` - // Estimated: `162782` - // Minimum execution time: 627_255_000 picoseconds. - Weight::from_parts(712_602_000, 0) - .saturating_add(Weight::from_parts(0, 162782)) - .saturating_add(T::DbWeight::get().reads(108)) - .saturating_add(T::DbWeight::get().writes(84)) + // Measured: `90773` + // Estimated: `346462` + // Minimum execution time: 9_816_978_000 picoseconds. + Weight::from_parts(9_904_350_000, 0) + .saturating_add(Weight::from_parts(0, 346462)) + .saturating_add(T::DbWeight::get().reads(982)) + .saturating_add(T::DbWeight::get().writes(466)) } - /// Non-tail four-candidate placed batch that deletes the physical tail page. + /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:1) + /// Proof: `Actors::CrossingPendingFeedListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingTransitionQueues` (r:1 w:0) + /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(3171), added: 5646, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingRangeCursors` (r:1 w:1) + /// Proof: `Actors::CrossingRangeCursors` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingLeafStates` (r:2 w:2) + /// Proof: `Actors::CrossingLeafStates` (`max_values`: None, `max_size`: Some(62), added: 2537, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingMemberPages` (r:3 w:3) + /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(8264), added: 10739, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingMemberships` (r:128 w:192) + /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:64 w:64) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorIdentities` (r:64 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorHot` (r:64 w:64) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:64 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorActivationAuthority` (r:64 w:0) + /// Proof: `Actors::ActorActivationAuthority` (`max_values`: None, `max_size`: Some(159), added: 2634, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:64 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) + /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) + /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + /// Storage: `Actors::NextQueueTicket` (r:1 w:1) + /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueHead` (r:1 w:0) + /// Proof: `Actors::QueueHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueTail` (r:1 w:1) + /// Proof: `Actors::QueueTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueOccupancy` (r:1 w:1) + /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueuePages` (r:1 w:1) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingFeedMembershipCount` (r:1 w:1) + /// Proof: `Actors::CrossingFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingUserFeedMembershipCount` (r:1 w:1) + /// Proof: `Actors::CrossingUserFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingRadixNodes` (r:32 w:32) + /// Proof: `Actors::CrossingRadixNodes` (`max_values`: None, `max_size`: Some(53), added: 2528, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingPendingFeeds` (r:1 w:0) + /// Proof: `Actors::CrossingPendingFeeds` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) fn crossing_placed_non_tail_emptied_unit() -> Weight { - Weight::from_parts(601_343_000, 81_886) - .saturating_add(T::DbWeight::get().reads(78)) - .saturating_add(T::DbWeight::get().writes(117)) + // Proof Size summary in bytes: + // Measured: `76353` + // Estimated: `327262` + // Minimum execution time: 3_982_833_000 picoseconds. + Weight::from_parts(4_094_371_000, 0) + .saturating_add(Weight::from_parts(0, 327262)) + .saturating_add(T::DbWeight::get().reads(563)) + .saturating_add(T::DbWeight::get().writes(367)) } - /// Non-tail four-candidate placed batch that trims the physical tail page. + /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:1) + /// Proof: `Actors::CrossingPendingFeedListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingTransitionQueues` (r:1 w:0) + /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(3171), added: 5646, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingRangeCursors` (r:1 w:1) + /// Proof: `Actors::CrossingRangeCursors` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingLeafStates` (r:2 w:2) + /// Proof: `Actors::CrossingLeafStates` (`max_values`: None, `max_size`: Some(62), added: 2537, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingMemberPages` (r:3 w:3) + /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(8264), added: 10739, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingMemberships` (r:128 w:194) + /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:64 w:64) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorIdentities` (r:64 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorHot` (r:64 w:64) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:64 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorActivationAuthority` (r:64 w:0) + /// Proof: `Actors::ActorActivationAuthority` (`max_values`: None, `max_size`: Some(159), added: 2634, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:64 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) + /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) + /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + /// Storage: `Actors::NextQueueTicket` (r:1 w:1) + /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueHead` (r:1 w:0) + /// Proof: `Actors::QueueHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueTail` (r:1 w:1) + /// Proof: `Actors::QueueTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueueOccupancy` (r:1 w:1) + /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Actors::QueuePages` (r:1 w:1) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingFeedMembershipCount` (r:1 w:1) + /// Proof: `Actors::CrossingFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingUserFeedMembershipCount` (r:1 w:1) + /// Proof: `Actors::CrossingUserFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingRadixNodes` (r:32 w:32) + /// Proof: `Actors::CrossingRadixNodes` (`max_values`: None, `max_size`: Some(53), added: 2528, mode: `MaxEncodedLen`) + /// Storage: `Actors::CrossingPendingFeeds` (r:1 w:0) + /// Proof: `Actors::CrossingPendingFeeds` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) fn crossing_placed_non_tail_trimmed_unit() -> Weight { - Weight::from_parts(603_089_000, 81_886) - .saturating_add(T::DbWeight::get().reads(78)) - .saturating_add(T::DbWeight::get().writes(119)) + // Proof Size summary in bytes: + // Measured: `76648` + // Estimated: `327262` + // Minimum execution time: 3_979_340_000 picoseconds. + Weight::from_parts(4_067_622_000, 0) + .saturating_add(Weight::from_parts(0, 327262)) + .saturating_add(T::DbWeight::get().reads(563)) + .saturating_add(T::DbWeight::get().writes(369)) } /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:1) /// Proof: `Actors::CrossingPendingFeedListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingTransitionQueues` (r:1 w:0) - /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(2595), added: 5070, mode: `MaxEncodedLen`) + /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(3171), added: 5646, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingRangeCursors` (r:1 w:1) /// Proof: `Actors::CrossingRangeCursors` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingRadixNodes` (r:32 w:0) @@ -2877,53 +5156,57 @@ impl pallet_deos_actors::WeightInfo for S /// Storage: `Actors::CrossingLeafStates` (r:1 w:0) /// Proof: `Actors::CrossingLeafStates` (`max_values`: None, `max_size`: Some(62), added: 2537, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingMemberPages` (r:1 w:0) - /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(1096), added: 3571, mode: `MaxEncodedLen`) + /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(8264), added: 10739, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingMemberships` (r:1 w:0) /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:1 w:0) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorHot` (r:1 w:0) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingPendingFeeds` (r:1 w:0) /// Proof: `Actors::CrossingPendingFeeds` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) fn crossing_skip_unit() -> Weight { // Proof Size summary in bytes: - // Measured: `2968` + // Measured: `3116` // Estimated: `81886` - // Minimum execution time: 160_637_000 picoseconds. - Weight::from_parts(165_038_000, 0) + // Minimum execution time: 164_409_000 picoseconds. + Weight::from_parts(168_390_000, 0) .saturating_add(Weight::from_parts(0, 81886)) - .saturating_add(T::DbWeight::get().reads(40)) + .saturating_add(T::DbWeight::get().reads(41)) .saturating_add(T::DbWeight::get().writes(2)) } /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:1) /// Proof: `Actors::CrossingPendingFeedListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingTransitionQueues` (r:1 w:0) - /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(2595), added: 5070, mode: `MaxEncodedLen`) + /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(3171), added: 5646, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingRangeCursors` (r:1 w:1) /// Proof: `Actors::CrossingRangeCursors` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingLeafStates` (r:1 w:0) /// Proof: `Actors::CrossingLeafStates` (`max_values`: None, `max_size`: Some(62), added: 2537, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingMemberPages` (r:1 w:0) - /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(1096), added: 3571, mode: `MaxEncodedLen`) + /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(8264), added: 10739, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingMemberships` (r:2 w:0) /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:2 w:0) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorHot` (r:2 w:0) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingPendingFeeds` (r:1 w:0) /// Proof: `Actors::CrossingPendingFeeds` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) fn crossing_skip_pair_unit() -> Weight { // Proof Size summary in bytes: - // Measured: `1463` - // Estimated: `6112` - // Minimum execution time: 68_026_000 picoseconds. - Weight::from_parts(70_960_000, 0) - .saturating_add(Weight::from_parts(0, 6112)) - .saturating_add(T::DbWeight::get().reads(10)) + // Measured: `1618` + // Estimated: `11729` + // Minimum execution time: 73_334_000 picoseconds. + Weight::from_parts(77_665_000, 0) + .saturating_add(Weight::from_parts(0, 11729)) + .saturating_add(T::DbWeight::get().reads(12)) .saturating_add(T::DbWeight::get().writes(2)) } /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:1) /// Proof: `Actors::CrossingPendingFeedListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingTransitionQueues` (r:1 w:1) - /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(2595), added: 5070, mode: `MaxEncodedLen`) + /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(3171), added: 5646, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingRangeCursors` (r:1 w:1) /// Proof: `Actors::CrossingRangeCursors` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingRadixNodes` (r:64 w:64) @@ -2931,27 +5214,31 @@ impl pallet_deos_actors::WeightInfo for S /// Storage: `Actors::CrossingLeafStates` (r:2 w:2) /// Proof: `Actors::CrossingLeafStates` (`max_values`: None, `max_size`: Some(62), added: 2537, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingMemberPages` (r:2 w:2) - /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(1096), added: 3571, mode: `MaxEncodedLen`) + /// Proof: `Actors::CrossingMemberPages` (`max_values`: None, `max_size`: Some(8264), added: 10739, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingMemberships` (r:1 w:1) /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Actors::IndexedTriggerDetectionDisabled` (r:1 w:1) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorHot` (r:1 w:1) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorContract` (r:1 w:1) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:1 w:1) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:1) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorIdentities` (r:1 w:1) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorFunding` (r:1 w:1) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:1 w:1) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) + /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + /// Storage: `Actors::NextQueueTicket` (r:1 w:0) + /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingFeedMembershipCount` (r:1 w:1) /// Proof: `Actors::CrossingFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingUserFeedMembershipCount` (r:1 w:1) /// Proof: `Actors::CrossingUserFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorFunding` (r:1 w:1) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `MaxEncodedLen`) - /// Storage: `Actors::ContinuationState` (r:1 w:1) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:2 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) - /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) /// Storage: `Actors::QueueHead` (r:1 w:0) /// Proof: `Actors::QueueHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `Actors::QueueTail` (r:1 w:0) @@ -2959,9 +5246,7 @@ impl pallet_deos_actors::WeightInfo for S /// Storage: `Actors::QueueOccupancy` (r:1 w:0) /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `Actors::QueuePages` (r:1 w:0) - /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(1050), added: 3525, mode: `MaxEncodedLen`) - /// Storage: `Actors::NextQueueTicket` (r:1 w:0) - /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `MaxEncodedLen`) /// Storage: `Actors::ActiveActorCount` (r:1 w:1) /// Proof: `Actors::ActiveActorCount` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorIdentityCount` (r:1 w:1) @@ -2976,24 +5261,34 @@ impl pallet_deos_actors::WeightInfo for S /// Proof: `Actors::ObservationSubscriptionSlot` (`max_values`: None, `max_size`: Some(28), added: 2503, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingPendingFeeds` (r:1 w:1) /// Proof: `Actors::CrossingPendingFeeds` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorStateHolds` (r:1 w:1) + /// Proof: `Actors::ActorStateHolds` (`max_values`: None, `max_size`: Some(152), added: 2627, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(121), added: 2596, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunPayload` (r:0 w:1) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(2533), added: 5008, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorActivationAuthority` (r:0 w:1) + /// Proof: `Actors::ActorActivationAuthority` (`max_values`: None, `max_size`: Some(159), added: 2634, mode: `MaxEncodedLen`) fn crossing_actor_unit() -> Weight { // Proof Size summary in bytes: - // Measured: `4511` + // Measured: `5191` // Estimated: `162782` - // Minimum execution time: 610_841_000 picoseconds. - Weight::from_parts(631_585_000, 0) + // Minimum execution time: 706_246_000 picoseconds. + Weight::from_parts(730_831_000, 0) .saturating_add(Weight::from_parts(0, 162782)) - .saturating_add(T::DbWeight::get().reads(94)) - .saturating_add(T::DbWeight::get().writes(85)) + .saturating_add(T::DbWeight::get().reads(97)) + .saturating_add(T::DbWeight::get().writes(91)) } /// Storage: `Actors::SovereignIndex` (r:2 w:0) /// Proof: `Actors::SovereignIndex` (`max_values`: None, `max_size`: Some(56), added: 2531, mode: `MaxEncodedLen`) fn transaction_extension_ingress_base() -> Weight { // Proof Size summary in bytes: - // Measured: `769` + // Measured: `779` // Estimated: `6052` - // Minimum execution time: 11_943_000 picoseconds. - Weight::from_parts(13_619_000, 0) + // Minimum execution time: 11_804_000 picoseconds. + Weight::from_parts(13_410_000, 0) .saturating_add(Weight::from_parts(0, 6052)) .saturating_add(T::DbWeight::get().reads(2)) } @@ -3002,15 +5297,21 @@ impl pallet_deos_actors::WeightInfo for S /// Storage: `Actors::ActorIdentities` (r:1 w:0) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorHot` (r:1 w:1) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorContract` (r:1 w:0) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorFunding` (r:1 w:1) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `MaxEncodedLen`) - /// Storage: `Actors::ContinuationState` (r:1 w:0) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:1 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunPayload` (r:1 w:0) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(2533), added: 5008, mode: `MaxEncodedLen`) /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorStateHolds` (r:1 w:0) + /// Proof: `Actors::ActorStateHolds` (`max_values`: None, `max_size`: Some(152), added: 2627, mode: `MaxEncodedLen`) /// Storage: `Actors::WakeupBuckets` (r:1 w:1) /// Proof: `Actors::WakeupBuckets` (`max_values`: None, `max_size`: Some(58), added: 2533, mode: `MaxEncodedLen`) /// Storage: `Actors::WakeupCursorLen` (r:1 w:1) @@ -3021,27 +5322,125 @@ impl pallet_deos_actors::WeightInfo for S /// Proof: `Actors::WakeupPages` (`max_values`: None, `max_size`: Some(348), added: 2823, mode: `MaxEncodedLen`) fn transaction_extension_ingress_notify() -> Weight { // Proof Size summary in bytes: - // Measured: `2851` - // Estimated: `12200` - // Minimum execution time: 142_897_000 picoseconds. - Weight::from_parts(153_584_000, 0) - .saturating_add(Weight::from_parts(0, 12200)) - .saturating_add(T::DbWeight::get().reads(10)) + // Measured: `4914` + // Estimated: `5998` + // Minimum execution time: 274_899_000 picoseconds. + Weight::from_parts(290_055_000, 0) + .saturating_add(Weight::from_parts(0, 5998)) + .saturating_add(T::DbWeight::get().reads(13)) .saturating_add(T::DbWeight::get().writes(6)) } /// Storage: `Actors::ActorFunding` (r:1 w:1) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(302), added: 2777, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `MaxEncodedLen`) /// The range of component `a` is `[1, 10]`. fn funding_snapshot_open(a: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `586 + a * (21 ±0)` - // Estimated: `3767` - // Minimum execution time: 10_337_000 picoseconds. - Weight::from_parts(11_872_324, 0) - .saturating_add(Weight::from_parts(0, 3767)) - // Standard Error: 550 - .saturating_add(Weight::from_parts(105_543, 0).saturating_mul(a.into())) + // Measured: `566 + a * (21 ±0)` + // Estimated: `4531` + // Minimum execution time: 11_593_000 picoseconds. + Weight::from_parts(13_240_086, 0) + .saturating_add(Weight::from_parts(0, 4531)) + // Standard Error: 850 + .saturating_add(Weight::from_parts(122_803, 0).saturating_mul(a.into())) + .saturating_add(T::DbWeight::get().reads(1)) + .saturating_add(T::DbWeight::get().writes(1)) + } + /// Storage: `ParachainSystem::ValidationData` (r:1 w:1) + /// Proof: `ParachainSystem::ValidationData` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainSystem::LastRelayChainBlockNumber` (r:1 w:0) + /// Proof: `ParachainSystem::LastRelayChainBlockNumber` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainInfo::ParachainId` (r:1 w:0) + /// Proof: `ParachainInfo::ParachainId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `AuraExt::RelaySlotInfo` (r:1 w:1) + /// Proof: `AuraExt::RelaySlotInfo` (`max_values`: Some(1), `max_size`: Some(12), added: 507, mode: `MaxEncodedLen`) + /// Storage: `Aura::CurrentSlot` (r:1 w:0) + /// Proof: `Aura::CurrentSlot` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `ParachainSystem::UnincludedSegment` (r:1 w:1) + /// Proof: `ParachainSystem::UnincludedSegment` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainSystem::AggregatedUnincludedSegment` (r:1 w:0) + /// Proof: `ParachainSystem::AggregatedUnincludedSegment` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainSystem::LastDmqMqcHead` (r:1 w:1) + /// Proof: `ParachainSystem::LastDmqMqcHead` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `MessageQueue::BookStateFor` (r:1 w:1) + /// Proof: `MessageQueue::BookStateFor` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`) + /// Storage: `MessageQueue::ServiceHead` (r:1 w:1) + /// Proof: `MessageQueue::ServiceHead` (`max_values`: Some(1), `max_size`: Some(5), added: 500, mode: `MaxEncodedLen`) + /// Storage: `ParachainSystem::LastHrmpMqcHeads` (r:1 w:1) + /// Proof: `ParachainSystem::LastHrmpMqcHeads` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainSystem::HrmpWatermark` (r:1 w:1) + /// Proof: `ParachainSystem::HrmpWatermark` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainSystem::ReservedXcmpWeightOverride` (r:1 w:0) + /// Proof: `ParachainSystem::ReservedXcmpWeightOverride` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainSystem::LastProcessedDownwardMessage` (r:0 w:1) + /// Proof: `ParachainSystem::LastProcessedDownwardMessage` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainSystem::RelevantMessagingState` (r:0 w:1) + /// Proof: `ParachainSystem::RelevantMessagingState` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainSystem::PendingApprovedPeer` (r:0 w:1) + /// Proof: `ParachainSystem::PendingApprovedPeer` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainSystem::LastProcessedHrmpMessage` (r:0 w:1) + /// Proof: `ParachainSystem::LastProcessedHrmpMessage` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainSystem::RelayStateProof` (r:0 w:1) + /// Proof: `ParachainSystem::RelayStateProof` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainSystem::HostConfiguration` (r:0 w:1) + /// Proof: `ParachainSystem::HostConfiguration` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainSystem::ProcessedDownwardMessages` (r:0 w:1) + /// Proof: `ParachainSystem::ProcessedDownwardMessages` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainSystem::UpgradeGoAhead` (r:0 w:1) + /// Proof: `ParachainSystem::UpgradeGoAhead` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ParachainSystem::UpgradeRestrictionSignal` (r:0 w:1) + /// Proof: `ParachainSystem::UpgradeRestrictionSignal` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `MessageQueue::Pages` (r:0 w:6) + /// Proof: `MessageQueue::Pages` (`max_values`: None, `max_size`: Some(105521), added: 107996, mode: `MaxEncodedLen`) + fn maximum_context_inherent() -> Weight { + // Proof Size summary in bytes: + // Measured: `230` + // Estimated: `3517` + // Minimum execution time: 189_958_814_000 picoseconds. + Weight::from_parts(191_559_950_000, 0) + .saturating_add(Weight::from_parts(0, 3517)) + .saturating_add(T::DbWeight::get().reads(13)) + .saturating_add(T::DbWeight::get().writes(23)) + } + /// Storage: `Actors::CurrentBlockResourceState` (r:1 w:1) + /// Proof: `Actors::CurrentBlockResourceState` (`max_values`: Some(1), `max_size`: Some(75), added: 570, mode: `MaxEncodedLen`) + /// Storage: `Actors::FinalizedBlockResourceTelemetry` (r:0 w:1) + /// Proof: `Actors::FinalizedBlockResourceTelemetry` (`max_values`: Some(1), `max_size`: Some(69), added: 564, mode: `MaxEncodedLen`) + fn block_resource_finalize() -> Weight { + // Proof Size summary in bytes: + // Measured: `444` + // Estimated: `1560` + // Minimum execution time: 6_775_000 picoseconds. + Weight::from_parts(8_590_000, 0) + .saturating_add(Weight::from_parts(0, 1560)) .saturating_add(T::DbWeight::get().reads(1)) + .saturating_add(T::DbWeight::get().writes(2)) + } + /// Storage: `Actors::CurrentBlockResourceState` (r:1 w:1) + /// Proof: `Actors::CurrentBlockResourceState` (`max_values`: Some(1), `max_size`: Some(75), added: 570, mode: `MaxEncodedLen`) + fn block_resource_meter_extension() -> Weight { + // Proof Size summary in bytes: + // Measured: `434` + // Estimated: `1560` + // Minimum execution time: 8_940_000 picoseconds. + Weight::from_parts(10_616_000, 0) + .saturating_add(Weight::from_parts(0, 1560)) + .saturating_add(T::DbWeight::get().reads(1)) + .saturating_add(T::DbWeight::get().writes(1)) + } + /// Storage: `PolkadotXcm::CurrentMigration` (r:1 w:0) + /// Proof: `PolkadotXcm::CurrentMigration` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `PolkadotXcm::VersionNotifiers` (r:100 w:0) + /// Proof: `PolkadotXcm::VersionNotifiers` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `PolkadotXcm::QueryCounter` (r:1 w:1) + /// Proof: `PolkadotXcm::QueryCounter` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn maximum_xcm_version_discovery() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `248490` + // Minimum execution time: 431_905_000 picoseconds. + Weight::from_parts(446_572_000, 0) + .saturating_add(Weight::from_parts(0, 248490)) + .saturating_add(T::DbWeight::get().reads(102)) .saturating_add(T::DbWeight::get().writes(1)) } } diff --git a/template/runtime/src/weights/pallet_deos_router.rs b/template/runtime/src/weights/pallet_deos_router.rs index 21353a7b..49edd02e 100644 --- a/template/runtime/src/weights/pallet_deos_router.rs +++ b/template/runtime/src/weights/pallet_deos_router.rs @@ -2,7 +2,7 @@ //! Autogenerated weights for `pallet_deos_router` //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 58.0.0 -//! DATE: 2026-08-23, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2026-08-30, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` //! HOSTNAME: `fedora`, CPU: `AMD Ryzen 7 4800H with Radeon Graphics` //! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 @@ -57,13 +57,15 @@ impl pallet_deos_router::WeightInfo for S /// Storage: `Actors::ActorIdentities` (r:1 w:0) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorHot` (r:1 w:1) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorContract` (r:1 w:0) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorFunding` (r:1 w:0) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(286), added: 2761, mode: `MaxEncodedLen`) - /// Storage: `Actors::ContinuationState` (r:1 w:0) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:1 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) /// Storage: `Actors::QueueHead` (r:1 w:0) @@ -73,7 +75,7 @@ impl pallet_deos_router::WeightInfo for S /// Storage: `Actors::QueueOccupancy` (r:1 w:1) /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `Actors::QueuePages` (r:1 w:1) - /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(1050), added: 3525, mode: `MaxEncodedLen`) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `MaxEncodedLen`) /// Storage: `Actors::NextQueueTicket` (r:1 w:1) /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `Actors::ObservationSubscriberCount` (r:1 w:0) @@ -81,25 +83,25 @@ impl pallet_deos_router::WeightInfo for S /// Storage: `Actors::ObservationIngressRevisions` (r:1 w:1) /// Proof: `Actors::ObservationIngressRevisions` (`max_values`: None, `max_size`: Some(41), added: 2516, mode: `MaxEncodedLen`) /// Storage: `Actors::DirtyObservationFeeds` (r:1 w:1) - /// Proof: `Actors::DirtyObservationFeeds` (`max_values`: None, `max_size`: Some(94), added: 2569, mode: `MaxEncodedLen`) + /// Proof: `Actors::DirtyObservationFeeds` (`max_values`: None, `max_size`: Some(122), added: 2597, mode: `MaxEncodedLen`) /// Storage: `Actors::DirtyObservationListState` (r:1 w:1) /// Proof: `Actors::DirtyObservationListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingFeedMembershipCount` (r:1 w:0) /// Proof: `Actors::CrossingFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingTransitionQueues` (r:1 w:1) - /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(2595), added: 5070, mode: `MaxEncodedLen`) + /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(3171), added: 5646, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingPendingFeeds` (r:1 w:1) /// Proof: `Actors::CrossingPendingFeeds` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:1) /// Proof: `Actors::CrossingPendingFeedListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) fn direct_xyk_exact_input() -> Weight { // Proof Size summary in bytes: - // Measured: `3288` - // Estimated: `12200` - // Minimum execution time: 317_923_000 picoseconds. - Weight::from_parts(343_205_000, 0) - .saturating_add(Weight::from_parts(0, 12200)) - .saturating_add(T::DbWeight::get().reads(30)) + // Measured: `3756` + // Estimated: `9635` + // Minimum execution time: 358_641_000 picoseconds. + Weight::from_parts(389_092_000, 0) + .saturating_add(Weight::from_parts(0, 9635)) + .saturating_add(T::DbWeight::get().reads(31)) .saturating_add(T::DbWeight::get().writes(18)) } /// Storage: `DeosRouter::RouterFee` (r:1 w:0) @@ -117,13 +119,15 @@ impl pallet_deos_router::WeightInfo for S /// Storage: `Actors::ActorIdentities` (r:2 w:0) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorHot` (r:2 w:1) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorContract` (r:2 w:0) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:2 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorFunding` (r:2 w:0) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(286), added: 2761, mode: `MaxEncodedLen`) - /// Storage: `Actors::ContinuationState` (r:2 w:0) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:2 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) /// Storage: `Actors::QueueHead` (r:1 w:0) @@ -133,17 +137,17 @@ impl pallet_deos_router::WeightInfo for S /// Storage: `Actors::QueueOccupancy` (r:1 w:1) /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `Actors::QueuePages` (r:1 w:1) - /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(1050), added: 3525, mode: `MaxEncodedLen`) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `MaxEncodedLen`) /// Storage: `Actors::NextQueueTicket` (r:1 w:1) /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) fn direct_mint_exact_input() -> Weight { // Proof Size summary in bytes: - // Measured: `3247` - // Estimated: `23410` - // Minimum execution time: 311_498_000 picoseconds. - Weight::from_parts(321_275_000, 0) - .saturating_add(Weight::from_parts(0, 23410)) - .saturating_add(T::DbWeight::get().reads(33)) + // Measured: `3715` + // Estimated: `21862` + // Minimum execution time: 368_558_000 picoseconds. + Weight::from_parts(390_349_000, 0) + .saturating_add(Weight::from_parts(0, 21862)) + .saturating_add(T::DbWeight::get().reads(34)) .saturating_add(T::DbWeight::get().writes(14)) } /// Storage: `DeosRouter::RouterFee` (r:1 w:0) @@ -165,13 +169,15 @@ impl pallet_deos_router::WeightInfo for S /// Storage: `Actors::ActorIdentities` (r:1 w:0) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorHot` (r:1 w:1) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorContract` (r:1 w:0) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorFunding` (r:1 w:0) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(286), added: 2761, mode: `MaxEncodedLen`) - /// Storage: `Actors::ContinuationState` (r:1 w:0) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:1 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) /// Storage: `Actors::QueueHead` (r:1 w:0) @@ -181,7 +187,7 @@ impl pallet_deos_router::WeightInfo for S /// Storage: `Actors::QueueOccupancy` (r:1 w:1) /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `Actors::QueuePages` (r:1 w:1) - /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(1050), added: 3525, mode: `MaxEncodedLen`) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `MaxEncodedLen`) /// Storage: `Actors::NextQueueTicket` (r:1 w:1) /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `Actors::ObservationSubscriberCount` (r:2 w:0) @@ -189,35 +195,35 @@ impl pallet_deos_router::WeightInfo for S /// Storage: `Actors::ObservationIngressRevisions` (r:2 w:2) /// Proof: `Actors::ObservationIngressRevisions` (`max_values`: None, `max_size`: Some(41), added: 2516, mode: `MaxEncodedLen`) /// Storage: `Actors::DirtyObservationFeeds` (r:2 w:2) - /// Proof: `Actors::DirtyObservationFeeds` (`max_values`: None, `max_size`: Some(94), added: 2569, mode: `MaxEncodedLen`) + /// Proof: `Actors::DirtyObservationFeeds` (`max_values`: None, `max_size`: Some(122), added: 2597, mode: `MaxEncodedLen`) /// Storage: `Actors::DirtyObservationListState` (r:1 w:1) /// Proof: `Actors::DirtyObservationListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingFeedMembershipCount` (r:2 w:0) /// Proof: `Actors::CrossingFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingTransitionQueues` (r:2 w:2) - /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(2595), added: 5070, mode: `MaxEncodedLen`) + /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(3171), added: 5646, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingPendingFeeds` (r:2 w:2) /// Proof: `Actors::CrossingPendingFeeds` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:1) /// Proof: `Actors::CrossingPendingFeedListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) fn native_anchored_exact_input() -> Weight { // Proof Size summary in bytes: - // Measured: `4260` + // Measured: `4728` // Estimated: `19253` - // Minimum execution time: 479_468_000 picoseconds. - Weight::from_parts(500_770_000, 0) + // Minimum execution time: 508_942_000 picoseconds. + Weight::from_parts(562_930_000, 0) .saturating_add(Weight::from_parts(0, 19253)) - .saturating_add(T::DbWeight::get().reads(44)) + .saturating_add(T::DbWeight::get().reads(45)) .saturating_add(T::DbWeight::get().writes(27)) } - /// Storage: `Assets::Asset` (r:1 w:1) - /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) /// Storage: `Assets::Account` (r:3 w:3) /// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`) /// Storage: `System::Account` (r:2 w:2) /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) /// Storage: `DeosRouter::RouterFee` (r:1 w:0) /// Proof: `DeosRouter::RouterFee` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Assets::Asset` (r:1 w:1) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) /// Storage: `Oracle::Feeds` (r:1 w:0) /// Proof: `Oracle::Feeds` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) /// Storage: `Oracle::Observations` (r:1 w:1) @@ -227,13 +233,15 @@ impl pallet_deos_router::WeightInfo for S /// Storage: `Actors::ActorIdentities` (r:1 w:0) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorHot` (r:1 w:1) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorContract` (r:1 w:0) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorFunding` (r:1 w:0) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(286), added: 2761, mode: `MaxEncodedLen`) - /// Storage: `Actors::ContinuationState` (r:1 w:0) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:1 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) /// Storage: `Actors::QueueHead` (r:1 w:0) @@ -243,7 +251,7 @@ impl pallet_deos_router::WeightInfo for S /// Storage: `Actors::QueueOccupancy` (r:1 w:1) /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `Actors::QueuePages` (r:1 w:1) - /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(1050), added: 3525, mode: `MaxEncodedLen`) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `MaxEncodedLen`) /// Storage: `Actors::NextQueueTicket` (r:1 w:1) /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `Actors::ObservationSubscriberCount` (r:1 w:0) @@ -251,33 +259,33 @@ impl pallet_deos_router::WeightInfo for S /// Storage: `Actors::ObservationIngressRevisions` (r:1 w:1) /// Proof: `Actors::ObservationIngressRevisions` (`max_values`: None, `max_size`: Some(41), added: 2516, mode: `MaxEncodedLen`) /// Storage: `Actors::DirtyObservationFeeds` (r:1 w:1) - /// Proof: `Actors::DirtyObservationFeeds` (`max_values`: None, `max_size`: Some(94), added: 2569, mode: `MaxEncodedLen`) + /// Proof: `Actors::DirtyObservationFeeds` (`max_values`: None, `max_size`: Some(122), added: 2597, mode: `MaxEncodedLen`) /// Storage: `Actors::DirtyObservationListState` (r:1 w:1) /// Proof: `Actors::DirtyObservationListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingFeedMembershipCount` (r:1 w:0) /// Proof: `Actors::CrossingFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingTransitionQueues` (r:1 w:1) - /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(2595), added: 5070, mode: `MaxEncodedLen`) + /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(3171), added: 5646, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingPendingFeeds` (r:1 w:1) /// Proof: `Actors::CrossingPendingFeeds` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:1) /// Proof: `Actors::CrossingPendingFeedListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) fn direct_xyk_exact_output() -> Weight { // Proof Size summary in bytes: - // Measured: `3114` - // Estimated: `12200` - // Minimum execution time: 315_129_000 picoseconds. - Weight::from_parts(332_170_000, 0) - .saturating_add(Weight::from_parts(0, 12200)) - .saturating_add(T::DbWeight::get().reads(29)) + // Measured: `3582` + // Estimated: `9635` + // Minimum execution time: 356_545_000 picoseconds. + Weight::from_parts(387_486_000, 0) + .saturating_add(Weight::from_parts(0, 9635)) + .saturating_add(T::DbWeight::get().reads(30)) .saturating_add(T::DbWeight::get().writes(18)) } - /// Storage: `Assets::Asset` (r:2 w:2) - /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) /// Storage: `Assets::Account` (r:7 w:5) /// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`) /// Storage: `System::Account` (r:3 w:3) /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Assets::Asset` (r:2 w:2) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) /// Storage: `DeosRouter::RouterFee` (r:1 w:0) /// Proof: `DeosRouter::RouterFee` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `Oracle::Feeds` (r:2 w:0) @@ -289,13 +297,15 @@ impl pallet_deos_router::WeightInfo for S /// Storage: `Actors::ActorIdentities` (r:1 w:0) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorHot` (r:1 w:1) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorContract` (r:1 w:0) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorAdmissionCertificate` (r:1 w:0) + /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorFunding` (r:1 w:0) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(286), added: 2761, mode: `MaxEncodedLen`) - /// Storage: `Actors::ContinuationState` (r:1 w:0) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:1 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) /// Storage: `Actors::QueueHead` (r:1 w:0) @@ -305,7 +315,7 @@ impl pallet_deos_router::WeightInfo for S /// Storage: `Actors::QueueOccupancy` (r:1 w:1) /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `Actors::QueuePages` (r:1 w:1) - /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(1050), added: 3525, mode: `MaxEncodedLen`) + /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `MaxEncodedLen`) /// Storage: `Actors::NextQueueTicket` (r:1 w:1) /// Proof: `Actors::NextQueueTicket` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `Actors::ObservationSubscriberCount` (r:2 w:0) @@ -313,34 +323,66 @@ impl pallet_deos_router::WeightInfo for S /// Storage: `Actors::ObservationIngressRevisions` (r:2 w:2) /// Proof: `Actors::ObservationIngressRevisions` (`max_values`: None, `max_size`: Some(41), added: 2516, mode: `MaxEncodedLen`) /// Storage: `Actors::DirtyObservationFeeds` (r:2 w:2) - /// Proof: `Actors::DirtyObservationFeeds` (`max_values`: None, `max_size`: Some(94), added: 2569, mode: `MaxEncodedLen`) + /// Proof: `Actors::DirtyObservationFeeds` (`max_values`: None, `max_size`: Some(122), added: 2597, mode: `MaxEncodedLen`) /// Storage: `Actors::DirtyObservationListState` (r:1 w:1) /// Proof: `Actors::DirtyObservationListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingFeedMembershipCount` (r:2 w:0) /// Proof: `Actors::CrossingFeedMembershipCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingTransitionQueues` (r:2 w:2) - /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(2595), added: 5070, mode: `MaxEncodedLen`) + /// Proof: `Actors::CrossingTransitionQueues` (`max_values`: None, `max_size`: Some(3171), added: 5646, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingPendingFeeds` (r:2 w:2) /// Proof: `Actors::CrossingPendingFeeds` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:1) /// Proof: `Actors::CrossingPendingFeedListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) fn native_anchored_exact_output() -> Weight { // Proof Size summary in bytes: - // Measured: `3973` + // Measured: `4441` // Estimated: `19253` - // Minimum execution time: 482_541_000 picoseconds. - Weight::from_parts(506_427_000, 0) + // Minimum execution time: 510_268_000 picoseconds. + Weight::from_parts(556_714_000, 0) .saturating_add(Weight::from_parts(0, 19253)) - .saturating_add(T::DbWeight::get().reads(43)) + .saturating_add(T::DbWeight::get().reads(44)) .saturating_add(T::DbWeight::get().writes(27)) } + /// Storage: `AssetConversion::Pools` (r:1 w:1) + /// Proof: `AssetConversion::Pools` (`max_values`: None, `max_size`: Some(30), added: 2505, mode: `MaxEncodedLen`) + /// Storage: `AssetConversion::NextPoolAssetId` (r:1 w:1) + /// Proof: `AssetConversion::NextPoolAssetId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `DeosRouter::LpPairByTokenId` (r:1 w:1) + /// Proof: `DeosRouter::LpPairByTokenId` (`max_values`: Some(1), `max_size`: Some(7002), added: 7497, mode: `MaxEncodedLen`) + /// Storage: `Oracle::ProducerFeeds` (r:1 w:1) + /// Proof: `Oracle::ProducerFeeds` (`max_values`: None, `max_size`: Some(17067), added: 19542, mode: `MaxEncodedLen`) + /// Storage: `Oracle::Feeds` (r:2 w:2) + /// Proof: `Oracle::Feeds` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) + /// Storage: `Assets::Asset` (r:2 w:1) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) + /// Storage: `Assets::NextAssetId` (r:1 w:0) + /// Proof: `Assets::NextAssetId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:0) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Assets::Account` (r:1 w:1) + /// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`) + /// Storage: `Oracle::FeedIds` (r:1 w:1) + /// Proof: `Oracle::FeedIds` (`max_values`: Some(1), `max_size`: Some(17410), added: 17905, mode: `MaxEncodedLen`) + /// Storage: `Oracle::ProducerIds` (r:1 w:1) + /// Proof: `Oracle::ProducerIds` (`max_values`: Some(1), `max_size`: Some(32770), added: 33265, mode: `MaxEncodedLen`) + fn create_pool() -> Weight { + // Proof Size summary in bytes: + // Measured: `812` + // Estimated: `34255` + // Minimum execution time: 140_313_000 picoseconds. + Weight::from_parts(144_923_000, 0) + .saturating_add(Weight::from_parts(0, 34255)) + .saturating_add(T::DbWeight::get().reads(13)) + .saturating_add(T::DbWeight::get().writes(10)) + } /// Storage: `DeosRouter::RouterFee` (r:1 w:1) /// Proof: `DeosRouter::RouterFee` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) fn update_router_fee() -> Weight { // Proof Size summary in bytes: // Measured: `4` // Estimated: `1489` - // Minimum execution time: 6_984_000 picoseconds. + // Minimum execution time: 7_054_000 picoseconds. Weight::from_parts(8_591_000, 0) .saturating_add(Weight::from_parts(0, 1489)) .saturating_add(T::DbWeight::get().reads(1)) diff --git a/template/runtime/src/weights/pallet_session_rotation.rs b/template/runtime/src/weights/pallet_session_rotation.rs new file mode 100644 index 00000000..d243c60c --- /dev/null +++ b/template/runtime/src/weights/pallet_session_rotation.rs @@ -0,0 +1,73 @@ + +//! Autogenerated weights for `pallet_session_rotation` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 58.0.0 +//! DATE: 2026-08-29, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `fedora`, CPU: `AMD Ryzen 7 4800H with Radeon Graphics` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 + +// Executed Command: +// frame-omni-bencher +// v1 +// benchmark +// pallet +// --runtime +// template/target/benchmarks/release/wbuild/deos-runtime/deos_runtime.compact.compressed.wasm +// --pallet +// pallet_session_rotation +// --extrinsic +// rotate_session +// --steps +// 50 +// --repeat +// 20 +// --heap-pages +// 4096 +// --output +// template/runtime/src/weights/pallet_session_rotation.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use polkadot_sdk::frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions for `pallet_session_rotation`. +pub struct SubstrateWeight(PhantomData); +impl pallet_session_rotation::WeightInfo for SubstrateWeight { + /// Storage: `Session::CurrentIndex` (r:1 w:1) + /// Proof: `Session::CurrentIndex` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `Session::QueuedChanged` (r:1 w:1) + /// Proof: `Session::QueuedChanged` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `Session::QueuedKeys` (r:1 w:1) + /// Proof: `Session::QueuedKeys` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `Staking::NativeSecurityRewardPots` (r:1 w:0) + /// Proof: `Staking::NativeSecurityRewardPots` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`) + /// Storage: `Staking::NativeSecurityEpochSnapshots` (r:1 w:0) + /// Proof: `Staking::NativeSecurityEpochSnapshots` (`max_values`: None, `max_size`: Some(12844), added: 15319, mode: `MaxEncodedLen`) + /// Storage: `CollatorSelection::Invulnerables` (r:1 w:0) + /// Proof: `CollatorSelection::Invulnerables` (`max_values`: Some(1), `max_size`: Some(641), added: 1136, mode: `MaxEncodedLen`) + /// Storage: `Staking::Pools` (r:1 w:0) + /// Proof: `Staking::Pools` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`) + /// Storage: `AssetConversion::Pools` (r:1 w:0) + /// Proof: `AssetConversion::Pools` (`max_values`: None, `max_size`: Some(30), added: 2505, mode: `MaxEncodedLen`) + /// Storage: `Session::NextKeys` (r:120 w:0) + /// Proof: `Session::NextKeys` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `Staking::LastNativeSecurityBoundaryDiagnostic` (r:0 w:1) + /// Proof: `Staking::LastNativeSecurityBoundaryDiagnostic` (`max_values`: Some(1), `max_size`: Some(6), added: 501, mode: `MaxEncodedLen`) + /// Storage: `Session::Validators` (r:0 w:1) + /// Proof: `Session::Validators` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn rotate_session() -> Weight { + // Proof Size summary in bytes: + // Measured: `17845` + // Estimated: `315835` + // Minimum execution time: 467_735_000 picoseconds. + Weight::from_parts(492_598_000, 0) + .saturating_add(Weight::from_parts(0, 315835)) + .saturating_add(T::DbWeight::get().reads(128)) + .saturating_add(T::DbWeight::get().writes(5)) + } +} diff --git a/template/runtime/src/weights/pallet_tmc.rs b/template/runtime/src/weights/pallet_tmc.rs index f5b228ff..b182a2f8 100644 --- a/template/runtime/src/weights/pallet_tmc.rs +++ b/template/runtime/src/weights/pallet_tmc.rs @@ -2,7 +2,7 @@ //! Autogenerated weights for `pallet_tmc` //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 58.0.0 -//! DATE: 2026-08-21, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2026-08-26, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` //! HOSTNAME: `fedora`, CPU: `AMD Ryzen 7 4800H with Radeon Graphics` //! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 @@ -46,7 +46,7 @@ impl pallet_tmc::WeightInfo for Substrate // Proof Size summary in bytes: // Measured: `852` // Estimated: `6360` - // Minimum execution time: 24_794_000 picoseconds. + // Minimum execution time: 24_655_000 picoseconds. Weight::from_parts(26_680_000, 0) .saturating_add(Weight::from_parts(0, 6360)) .saturating_add(T::DbWeight::get().reads(3)) @@ -61,24 +61,24 @@ impl pallet_tmc::WeightInfo for Substrate /// Storage: `Actors::ActorIdentities` (r:1 w:0) /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorHot` (r:1 w:0) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(85), added: 2560, mode: `MaxEncodedLen`) - /// Storage: `Actors::ActorContract` (r:1 w:0) - /// Proof: `Actors::ActorContract` (`max_values`: None, `max_size`: Some(8735), added: 11210, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2237), added: 4712, mode: `MaxEncodedLen`) /// Storage: `Actors::ActorFunding` (r:1 w:0) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(286), added: 2761, mode: `MaxEncodedLen`) - /// Storage: `Actors::ContinuationState` (r:1 w:0) - /// Proof: `Actors::ContinuationState` (`max_values`: None, `max_size`: Some(689), added: 3164, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1082), added: 3557, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:1 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) /// Storage: `System::Account` (r:1 w:1) /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) /// Storage: `Assets::Account` (r:2 w:2) /// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`) fn mint_with_distribution() -> Weight { // Proof Size summary in bytes: - // Measured: `2362` - // Estimated: `12200` - // Minimum execution time: 159_799_000 picoseconds. - Weight::from_parts(164_130_000, 0) - .saturating_add(Weight::from_parts(0, 12200)) + // Measured: `2365` + // Estimated: `6208` + // Minimum execution time: 159_031_000 picoseconds. + Weight::from_parts(164_758_000, 0) + .saturating_add(Weight::from_parts(0, 6208)) .saturating_add(T::DbWeight::get().reads(11)) .saturating_add(T::DbWeight::get().writes(4)) } diff --git a/template/vendor/pallet-asset-conversion/Cargo.lock b/template/vendor/pallet-asset-conversion/Cargo.lock new file mode 100644 index 00000000..4ef38991 --- /dev/null +++ b/template/vendor/pallet-asset-conversion/Cargo.lock @@ -0,0 +1,4042 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +dependencies = [ + "lazy_static", + "regex", +] + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6748e8def348ed4d14996fa801f4122cd763fff530258cdc03f64b25f89d3a5a" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "anyhow" +version = "1.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" + +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + +[[package]] +name = "aquamarine" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21cc1548309245035eb18aa7f0967da6bc65587005170c56e6ef2788a4cf3f4e" +dependencies = [ + "include_dir", + "itertools 0.10.5", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "ark-bls12-377" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb00293ba84f51ce3bd026bd0de55899c4e68f0a39a5728cebae3a73ffdc0a4f" +dependencies = [ + "ark-ec 0.4.2", + "ark-ff 0.4.2", + "ark-std 0.4.0", +] + +[[package]] +name = "ark-bls12-381" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c775f0d12169cba7aae4caeb547bb6a50781c7449a8aa53793827c9ec4abf488" +dependencies = [ + "ark-ec 0.4.2", + "ark-ff 0.4.2", + "ark-serialize 0.4.2", + "ark-std 0.4.0", +] + +[[package]] +name = "ark-bls12-381" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3df4dcc01ff89867cd86b0da835f23c3f02738353aaee7dde7495af71363b8d5" +dependencies = [ + "ark-ec 0.5.0", + "ark-ff 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", +] + +[[package]] +name = "ark-ec" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defd9a439d56ac24968cca0571f598a61bc8c55f71d50a89cda591cb750670ba" +dependencies = [ + "ark-ff 0.4.2", + "ark-poly 0.4.2", + "ark-serialize 0.4.2", + "ark-std 0.4.0", + "derivative", + "hashbrown 0.13.2", + "itertools 0.10.5", + "num-traits", + "zeroize", +] + +[[package]] +name = "ark-ec" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d68f2d516162846c1238e755a7c4d131b892b70cc70c471a8e3ca3ed818fce" +dependencies = [ + "ahash", + "ark-ff 0.5.0", + "ark-poly 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "educe", + "fnv", + "hashbrown 0.15.3", + "itertools 0.13.0", + "num-bigint", + "num-integer", + "num-traits", + "zeroize", +] + +[[package]] +name = "ark-ed-on-bls12-381-bandersnatch" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1786b2e3832f6f0f7c8d62d5d5a282f6952a1ab99981c54cd52b6ac1d8f02df5" +dependencies = [ + "ark-bls12-381 0.5.0", + "ark-ec 0.5.0", + "ark-ff 0.5.0", + "ark-std 0.5.0", +] + +[[package]] +name = "ark-ff" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec847af850f44ad29048935519032c33da8aa03340876d351dfab5660d2966ba" +dependencies = [ + "ark-ff-asm 0.4.2", + "ark-ff-macros 0.4.2", + "ark-serialize 0.4.2", + "ark-std 0.4.0", + "derivative", + "digest 0.10.7", + "itertools 0.10.5", + "num-bigint", + "num-traits", + "paste", + "rustc_version", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a177aba0ed1e0fbb62aa9f6d0502e9b46dad8c2eab04c14258a1212d2557ea70" +dependencies = [ + "ark-ff-asm 0.5.0", + "ark-ff-macros 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "arrayvec", + "digest 0.10.7", + "educe", + "itertools 0.13.0", + "num-bigint", + "num-traits", + "paste", + "zeroize", +] + +[[package]] +name = "ark-ff-asm" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed4aa4fe255d0bc6d79373f7e31d2ea147bcf486cba1be5ba7ea85abdb92348" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-asm" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60" +dependencies = [ + "quote", + "syn 2.0.98", +] + +[[package]] +name = "ark-ff-macros" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7abe79b0e4288889c4574159ab790824d0033b9fdcb2a112a3182fac2e514565" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09be120733ee33f7693ceaa202ca41accd5653b779563608f1234f78ae07c4b3" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "ark-poly" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d320bfc44ee185d899ccbadfa8bc31aab923ce1558716e1997a1e74057fe86bf" +dependencies = [ + "ark-ff 0.4.2", + "ark-serialize 0.4.2", + "ark-std 0.4.0", + "derivative", + "hashbrown 0.13.2", +] + +[[package]] +name = "ark-poly" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579305839da207f02b89cd1679e50e67b4331e2f9294a57693e5051b7703fe27" +dependencies = [ + "ahash", + "ark-ff 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "educe", + "fnv", + "hashbrown 0.15.3", +] + +[[package]] +name = "ark-serialize" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" +dependencies = [ + "ark-serialize-derive 0.4.2", + "ark-std 0.4.0", + "digest 0.10.7", + "num-bigint", +] + +[[package]] +name = "ark-serialize" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f4d068aaf107ebcd7dfb52bc748f8030e0fc930ac8e360146ca54c1203088f7" +dependencies = [ + "ark-serialize-derive 0.5.0", + "ark-std 0.5.0", + "arrayvec", + "digest 0.10.7", + "num-bigint", +] + +[[package]] +name = "ark-serialize-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae3281bc6d0fd7e549af32b52511e1302185bd688fd3359fa36423346ff682ea" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-serialize-derive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213888f660fddcca0d257e88e54ac05bca01885f258ccdf695bafd77031bb69d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "ark-std" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" +dependencies = [ + "num-traits", + "rand", +] + +[[package]] +name = "ark-std" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a" +dependencies = [ + "num-traits", + "rand", +] + +[[package]] +name = "ark-transcript" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47c1c928edb9d8ff24cb5dcb7651d3a98494fff3099eee95c2404cd813a9139f" +dependencies = [ + "ark-ff 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "digest 0.10.7", + "rand_core", + "sha3", +] + +[[package]] +name = "ark-vrf" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f69f34cb5a7d5d4627792c7a6343508becc19deab59a87ef73367cc112dfd02c" +dependencies = [ + "ark-bls12-381 0.5.0", + "ark-ec 0.5.0", + "ark-ed-on-bls12-381-bandersnatch", + "ark-ff 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "digest 0.10.7", + "generic-array", + "sha2 0.10.9", + "w3f-ring-proof", + "zeroize", +] + +[[package]] +name = "array-bytes" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f840fb7195bcfc5e17ea40c26e5ce6d5b9ce5d584466e17703209657e459ae0" + +[[package]] +name = "arrayref" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "async-trait" +version = "0.1.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "atomic-waker" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1181e1e0d1fce796a03db1ae795d67167da795f9cf4a39c37589e85ef57f26d3" + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "binary-merkle-tree" +version = "16.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6d867f1ffee8b07e7bee466f4f33a043b91f868f5c7b1d22d8a02f86e92bee8" +dependencies = [ + "hash-db", + "log", + "parity-scale-codec", +] + +[[package]] +name = "bip39" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d193de1f7487df1914d3a568b772458861d33f9c54249612cc2893d6915054" +dependencies = [ + "bitcoin_hashes", + "rand", + "rand_core", + "serde", + "unicode-normalization", +] + +[[package]] +name = "bitcoin-internals" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9425c3bf7089c983facbae04de54513cce73b41c7f9ff8c845b54e7bc64ebbfb" + +[[package]] +name = "bitcoin_hashes" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1930a4dabfebb8d7d9992db18ebe3ae2876f0a305fab206fd168df931ede293b" +dependencies = [ + "bitcoin-internals", + "hex-conservative", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "blake2b_simd" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23285ad32269793932e830392f2fe2f83e26488fd3ec778883a93c8323735780" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bounded-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee8eddd066a8825ec5570528e6880471210fd5d88cb6abbe1cfdd51ca249c33" +dependencies = [ + "jam-codec", + "log", + "parity-scale-codec", + "scale-info", + "serde", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "byte-slice-cast" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" + +[[package]] +name = "bytemuck" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-expr" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03915af431787e6ffdcc74c645077518c6b6e01f80b761e0fbbfa288536311b3" +dependencies = [ + "smallvec", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "common-path" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2382f75942f4b3be3690fe4f86365e9c853c1587d6ee58212cebf6e2a9ccd101" + +[[package]] +name = "const-oid" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" + +[[package]] +name = "const-random" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368a7a772ead6ce7e1de82bfb04c485f3db8ec744f72925af5735e29a22cc18e" +dependencies = [ + "const-random-macro", + "proc-macro-hack", +] + +[[package]] +name = "const-random-macro" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d7d6ab3c3a2282db210df5f02c4dab6e0a7057af0fb7ebd4070f30fe05c0ddb" +dependencies = [ + "getrandom", + "once_cell", + "proc-macro-hack", + "tiny-keccak", +] + +[[package]] +name = "const_format" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "126f97965c8ad46d6d9163268ff28432e8f6a1196a55578867832e3049df63dd" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "constant_time_eq" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" + +[[package]] +name = "cpufeatures" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +dependencies = [ + "libc", +] + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "crypto-bigint" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4c2f4e1afd912bc40bfd6fed5d9dc1f288e0ba01bfcc835cc5bc3eb13efe15" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "rand_core", + "typenum", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest 0.10.7", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fdaf97f4804dcebfa5862639bc9ce4121e82140bec2a987ac5140294865b5b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "der" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive-syn-parse" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d65d7ce8132b7c0e54497a4d9a55a1c2a0912a0d786cf894472ba818fba45762" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "derive-where" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef941ded77d15ca19b40374869ac6000af1c9f2a4c0f3d4c70926287e6364a8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "derive_more" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "docify" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a772b62b1837c8f060432ddcc10b17aae1453ef17617a99bc07789252d2a5896" +dependencies = [ + "docify_macros", +] + +[[package]] +name = "docify_macros" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60e6be249b0a462a14784a99b19bf35a667bb5e09de611738bb7362fa4c95ff7" +dependencies = [ + "common-path", + "derive-syn-parse", + "once_cell", + "proc-macro2", + "quote", + "regex", + "syn 2.0.98", + "termcolor", + "toml", + "walkdir", +] + +[[package]] +name = "dyn-clone" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" + +[[package]] +name = "ecdsa" +version = "0.16.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4b1e0c257a9e9f25f90ff76d7a68360ed497ee519c8e428d1825ef0000799d4" +dependencies = [ + "der", + "digest 0.10.7", + "elliptic-curve", + "rfc6979", + "serdect", + "signature", + "spki", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2 0.10.9", + "subtle", + "zeroize", +] + +[[package]] +name = "ed25519-zebra" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0017d969298eec91e3db7a2985a8cab4df6341d86e6f3a6f5878b13fb7846bc9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "hashbrown 0.15.3", + "pkcs8", + "rand_core", + "sha2 0.10.9", + "subtle", + "zeroize", +] + +[[package]] +name = "educe" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest 0.10.7", + "ff", + "generic-array", + "group", + "pkcs8", + "rand_core", + "sec1", + "serdect", + "subtle", + "zeroize", +] + +[[package]] +name = "enum-ordinalize" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea0dcfa4e54eeb516fe454635a95753ddd39acda650ce703031c6973e315dd5" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d28318a75d4aead5c4db25382e8ef717932d0346600cacae6357eb5941bc5ff" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "environmental" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48c92028aaa870e83d51c64e5d4e0b6981b360c522198c23959f219a4e1b15b" + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "expander" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2c470c71d91ecbd179935b24170459e926382eaaa86b590b78814e180d8a8e2" +dependencies = [ + "blake2", + "file-guard", + "fs-err", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "ff" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +dependencies = [ + "rand_core", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27573eac26f4dd11e2b1916c3fe1baa56407c83c71a773a8ba17ec0bca03b6b7" + +[[package]] +name = "file-guard" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21ef72acf95ec3d7dbf61275be556299490a245f017cf084bd23b4f68cf9407c" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fixed-hash" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" +dependencies = [ + "byteorder", + "rand", + "rustc-hex", + "static_assertions", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "frame-benchmarking" +version = "49.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b995d8f0f6237d62d6e8854a7c3ea5660dab488776ab5667dae0dbf1a0f55b" +dependencies = [ + "anyhow", + "frame-support", + "frame-support-procedural", + "frame-system", + "linregress", + "log", + "parity-scale-codec", + "paste", + "scale-info", + "serde", + "sp-api", + "sp-application-crypto", + "sp-core", + "sp-io", + "sp-runtime", + "sp-runtime-interface", + "sp-storage", + "static_assertions", +] + +[[package]] +name = "frame-metadata" +version = "23.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ba5be0edbdb824843a0f9c6f0906ecfc66c5316218d74457003218b24909ed0" +dependencies = [ + "cfg-if", + "parity-scale-codec", + "scale-info", + "serde", +] + +[[package]] +name = "frame-support" +version = "48.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819d92d637b76fb08c4e0c084c5d84a82c3d6c6e2bc2e52dbdb1cd740c3f67ed" +dependencies = [ + "aquamarine", + "array-bytes", + "binary-merkle-tree", + "bitflags", + "derive-where", + "docify", + "environmental", + "frame-metadata", + "frame-support-procedural", + "impl-trait-for-tuples", + "k256", + "log", + "macro_magic", + "parity-scale-codec", + "paste", + "scale-info", + "serde", + "serde_json", + "sp-api", + "sp-arithmetic", + "sp-core", + "sp-crypto-hashing-proc-macro", + "sp-debug-derive", + "sp-genesis-builder", + "sp-inherents", + "sp-io", + "sp-metadata-ir", + "sp-runtime", + "sp-staking", + "sp-state-machine", + "sp-std", + "sp-tracing", + "sp-trie", + "sp-weights", + "tt-call", +] + +[[package]] +name = "frame-support-procedural" +version = "40.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cde8332190b6f5b1855905ef5aadc965223ce5050fa5ecadcb482a46173073a4" +dependencies = [ + "Inflector", + "cfg-expr", + "derive-syn-parse", + "docify", + "expander", + "frame-support-procedural-tools", + "itertools 0.11.0", + "macro_magic", + "proc-macro-warning", + "proc-macro2", + "quote", + "sp-crypto-hashing", + "syn 2.0.98", +] + +[[package]] +name = "frame-support-procedural-tools" +version = "13.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81a088fd6fda5f53ff0c17fc7551ce8bd0ead14ba742228443c8196296a7369b" +dependencies = [ + "frame-support-procedural-tools-derive", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "frame-support-procedural-tools-derive" +version = "12.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed971c6435503a099bdac99fe4c5bea08981709e5b5a0a8535a1856f48561191" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "frame-system" +version = "48.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48fb841230390aa696e2092202f87b768aa04e6d5c94cdfefb5738261b841d1" +dependencies = [ + "cfg-if", + "docify", + "frame-support", + "log", + "parity-scale-codec", + "scale-info", + "serde", + "sp-core", + "sp-io", + "sp-runtime", + "sp-version", + "sp-weights", +] + +[[package]] +name = "fs-err" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0845fa252299212f0389d64ba26f34fa32cfe41588355f21ed507c59a0f64541" + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", + "num_cpus", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom_or_panic" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea1015b5a70616b688dc230cfe50c8af89d972cb132d5a622814d29773b10b9" +dependencies = [ + "rand", + "rand_core", +] + +[[package]] +name = "gimli" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + +[[package]] +name = "h2" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hash-db" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e7d7786361d7425ae2fe4f9e407eb0efaa0840f5212d109cc018c40c35c6ab4" + +[[package]] +name = "hash256-std-hasher" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92c171d55b98633f4ed3860808f004099b36c1cc29c42cfc53aa8591b21efcf2" +dependencies = [ + "crunchy", +] + +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-conservative" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ed443af458ccb6d81c1e7e661545f94d3176752fb1df2f543b902a1e0f51e2" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "impl-codec" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d40b9d5e17727407e55028eafc22b2dc68781786e6d7eb8a21103f5058e3a14" +dependencies = [ + "parity-scale-codec", +] + +[[package]] +name = "impl-num-traits" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "803d15461ab0dcc56706adf266158acbc44ccf719bf7d0af30705f58b90a4b8c" +dependencies = [ + "integer-sqrt", + "num-traits", + "uint", +] + +[[package]] +name = "impl-serde" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a143eada6a1ec4aefa5049037a26a6d597bfd64f8c026d07b77133e02b7dd0b" +dependencies = [ + "serde", +] + +[[package]] +name = "impl-trait-for-tuples" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "include_dir" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18762faeff7122e89e0857b02f7ce6fcc0d101d5e9ad2ad7846cc01d61b7f19e" +dependencies = [ + "include_dir_macros", +] + +[[package]] +name = "include_dir_macros" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b139284b5cf57ecfa712bcc66950bb635b31aff41c188e8a4cfc758eca374a3f" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "indexmap" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +dependencies = [ + "equivalent", + "hashbrown 0.15.3", +] + +[[package]] +name = "integer-sqrt" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "276ec31bcb4a9ee45f58bec6f9ec700ae4cf4f4f8f2fa7e06cb406bd5ffdd770" +dependencies = [ + "num-traits", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "jam-codec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d72f2fb8cfd27f6c52ea7d0528df594f7f2ed006feac153e9393ec567aafea98" +dependencies = [ + "arrayvec", + "bitvec", + "byte-slice-cast", + "const_format", + "impl-trait-for-tuples", + "jam-codec-derive", + "rustversion", + "serde", +] + +[[package]] +name = "jam-codec-derive" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09985146f40378e13af626964ac9c206d9d9b67c40c70805898d9954f709bcf5" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "once_cell", + "serdect", + "sha2 0.10.9", +] + +[[package]] +name = "keccak" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f6d5ed8676d904364de097082f4e7d240b571b67989ced0240f08b7f966f940" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.172" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" + +[[package]] +name = "libsecp256k1" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95b09eff1b35ed3b33b877ced3a691fc7a481919c7e29c53c906226fcf55e2a1" +dependencies = [ + "arrayref", + "base64", + "digest 0.9.0", + "libsecp256k1-core", + "libsecp256k1-gen-ecmult", + "libsecp256k1-gen-genmult", + "rand", + "serde", + "sha2 0.9.9", +] + +[[package]] +name = "libsecp256k1-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be9b9bb642d8522a44d533eab56c16c738301965504753b03ad1de3425d5451" +dependencies = [ + "crunchy", + "digest 0.9.0", + "subtle", +] + +[[package]] +name = "libsecp256k1-gen-ecmult" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3038c808c55c87e8a172643a7d87187fc6c4174468159cb3090659d55bcb4809" +dependencies = [ + "libsecp256k1-core", +] + +[[package]] +name = "libsecp256k1-gen-genmult" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db8d6ba2cec9eacc40e6e8ccc98931840301f1006e95647ceb2dd5c3aa06f7c" +dependencies = [ + "libsecp256k1-core", +] + +[[package]] +name = "linregress" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4de0b5f52a9f84544d268f5fabb71b38962d6aa3c6600b8bcd27d44ccf9c9c45" +dependencies = [ + "nalgebra", +] + +[[package]] +name = "lock_api" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "macro_magic" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc33f9f0351468d26fbc53d9ce00a096c8522ecb42f19b50f34f2c422f76d21d" +dependencies = [ + "macro_magic_core", + "macro_magic_macros", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "macro_magic_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1687dc887e42f352865a393acae7cf79d98fab6351cde1f58e9e057da89bf150" +dependencies = [ + "const-random", + "derive-syn-parse", + "macro_magic_core_macros", + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "macro_magic_core_macros" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b02abfe41815b5bd98dbd4260173db2c116dda171dc0fe7838cb206333b83308" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "macro_magic_macros" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ea28ee64b88876bf45277ed9a5817c1817df061a74f2b988971a12570e5869" +dependencies = [ + "macro_magic_core", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "matrixmultiply" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "090126dc04f95dc0d1c1c91f61bdd474b3930ca064c1edc8a849da2c6cbe1e77" +dependencies = [ + "autocfg", + "rawpointer", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memory-db" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e300c54e3239a86f9c61cc63ab0f03862eb40b1c6e065dc6fd6ceaeff6da93d" +dependencies = [ + "foldhash", + "hash-db", + "hashbrown 0.15.3", +] + +[[package]] +name = "merlin" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58c38e2799fc0978b65dfff8023ec7843e2330bb462f19198840b34b6582397d" +dependencies = [ + "byteorder", + "keccak", + "rand_core", + "zeroize", +] + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi", + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "nalgebra" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "307ed9b18cc2423f29e83f84fd23a8e73628727990181f18641a8b5dc2ab1caa" +dependencies = [ + "approx", + "matrixmultiply", + "nalgebra-macros", + "num-complex", + "num-rational", + "num-traits", + "simba", + "typenum", +] + +[[package]] +name = "nalgebra-macros" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91761aed67d03ad966ef783ae962ef9bbaca728d2dd7ceb7939ec110fffad998" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ba157ca0885411de85d6ca030ba7e2a83a28636056c7c699b07c8b6f7383214" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + +[[package]] +name = "num-format" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a652d9771a63711fd3c3deb670acfbe5c30a4072e664d7a3bf5a9e1056ac72c3" +dependencies = [ + "arrayvec", + "itoa", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "pallet-asset-conversion" +version = "31.0.0" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "parity-scale-codec", + "primitive-types", + "scale-info", + "sp-api", + "sp-arithmetic", + "sp-core", + "sp-io", + "sp-runtime", +] + +[[package]] +name = "parity-scale-codec" +version = "3.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799781ae679d79a948e13d4824a40970bfa500058d245760dd857301059810fa" +dependencies = [ + "arrayvec", + "bitvec", + "byte-slice-cast", + "bytes", + "const_format", + "impl-trait-for-tuples", + "parity-scale-codec-derive", + "rustversion", + "serde", +] + +[[package]] +name = "parity-scale-codec-derive" +version = "3.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b4653168b563151153c9e4c08ebed57fb8262bebfa79711552fa983c623e7a" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "parity-wasm" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1ad0aff30c1da14b1254fcb2af73e1fa9a28670e584a626f53a369d0e157304" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.48.5", +] + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest 0.10.7", + "password-hash", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "picosimd" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af35c838647fef3d6d052e27006ef88ea162336eee33063c50a63f163c18cdeb" + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "polkavm-common" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e44a9487003cf5b9fc4462bbcf105cc37d5d9b18b40edf5ed50dd20ed1fdb27" +dependencies = [ + "picosimd", +] + +[[package]] +name = "polkavm-derive" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ef966bc8518a66ce12d4edb73f2c4094cae72bb23258bc9e9b2802cc9d6cd79" +dependencies = [ + "polkavm-derive-impl-macro", +] + +[[package]] +name = "polkavm-derive-impl" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c2166ad71dd7f51dcdd0d91b70d408a8b3610fa6e94d8202dd4b7185607181" +dependencies = [ + "polkavm-common", + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "polkavm-derive-impl-macro" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7ac2ac8ec5b938e249fa97b5ebb1e6fa47000c81a25eba6bf0f13edb8d430e4" +dependencies = [ + "polkavm-derive-impl", + "syn 2.0.98", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "prettyplease" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c64d9ba0963cdcea2e1b2230fbae2bab30eb25a174be395c41e764bfb65dd62" +dependencies = [ + "proc-macro2", + "syn 2.0.98", +] + +[[package]] +name = "primitive-types" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d15600a7d856470b7d278b3fe0e311fe28c2526348549f8ef2ff7db3299c87f5" +dependencies = [ + "fixed-hash", + "impl-codec", + "impl-num-traits", + "impl-serde", + "scale-info", + "uint", +] + +[[package]] +name = "proc-macro-crate" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" +dependencies = [ + "toml_edit 0.21.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + +[[package]] +name = "proc-macro-warning" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b698b0b09d40e9b7c1a47b132d66a8b54bcd20583d9b6d06e4535e383b4405c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prometheus" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "449811d15fbdf5ceb5c1144416066429cf82316e2ec8ce0c1f6f8a02e7bbcf8c" +dependencies = [ + "cfg-if", + "fnv", + "lazy_static", + "memchr", + "parking_lot", + "thiserror", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags", +] + +[[package]] +name = "ref-cast" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf0a6f84d5f1d581da8b41b47ec8600871962f2a528115b542b362d4b744931" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc303e793d3734489387d205e9b186fac9c6cfacedd98cbb2e8a5943595f3e6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.8", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustc-hex" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" + +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + +[[package]] +name = "safe_arch" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f398075ce1e6a179b46f51bd88d0598b92b00d3551f1a2d4ac49e771b56ac354" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scale-info" +version = "2.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346a3b32eba2640d17a9cb5927056b08f3de90f65b72fe09402c2ad07d684d0b" +dependencies = [ + "bitvec", + "cfg-if", + "derive_more", + "parity-scale-codec", + "scale-info-derive", + "serde", +] + +[[package]] +name = "scale-info-derive" +version = "2.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6630024bf739e2179b91fb424b28898baf819414262c5d376677dbff1fe7ebf" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "schnellru" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9a8ef13a93c54d20580de1e5c413e624e53121d42fc7e2c11d10ef7f8b02367" +dependencies = [ + "ahash", + "cfg-if", + "hashbrown 0.13.2", +] + +[[package]] +name = "schnorrkel" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de18f6d8ba0aad7045f5feae07ec29899c1112584a38509a84ad7b04451eaa0" +dependencies = [ + "aead", + "arrayref", + "arrayvec", + "curve25519-dalek", + "getrandom_or_panic", + "merlin", + "rand_core", + "serde_bytes", + "sha2 0.10.9", + "subtle", + "zeroize", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "serdect", + "subtle", + "zeroize", +] + +[[package]] +name = "secp256k1" +version = "0.28.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24b59d129cdadea20aea4fb2352fa053712e5d713eee47d700cd4b2bc002f10" +dependencies = [ + "secp256k1-sys", +] + +[[package]] +name = "secp256k1-sys" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d1746aae42c19d583c3c1a8c646bfad910498e2051c551a7f2e3c0c9fbb7eb" +dependencies = [ + "cc", +] + +[[package]] +name = "secrecy" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e" +dependencies = [ + "zeroize", +] + +[[package]] +name = "semver" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_bytes" +version = "0.11.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab33ec92f677585af6d88c65593ae2375adde54efdbf16d597f2cbc7a6d368ff" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "serde_json" +version = "1.0.132" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" +dependencies = [ + "serde", +] + +[[package]] +name = "serdect" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" +dependencies = [ + "base16ct", + "serde", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest 0.10.7", + "keccak", +] + +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signature" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" +dependencies = [ + "digest 0.10.7", + "rand_core", +] + +[[package]] +name = "simba" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "061507c94fc6ab4ba1c9a0305018408e312e17c041eb63bef8aa726fa33aceae" +dependencies = [ + "approx", + "num-complex", + "num-traits", + "paste", + "wide", +] + +[[package]] +name = "simple-mermaid" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "620a1d43d70e142b1d46a929af51d44f383db9c7a2ec122de2cd992ccfcf3c18" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" + +[[package]] +name = "socket2" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "sp-api" +version = "43.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0e3d2391cbfaf3f5a00544763b1d9c803f9820ff97303ac8bf811f2b7c975e" +dependencies = [ + "docify", + "hash-db", + "log", + "parity-scale-codec", + "scale-info", + "sp-api-proc-macro", + "sp-core", + "sp-externalities", + "sp-metadata-ir", + "sp-runtime", + "sp-runtime-interface", + "sp-state-machine", + "sp-trie", + "sp-version", + "thiserror", +] + +[[package]] +name = "sp-api-proc-macro" +version = "29.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f941938a76ef6f65554d5eb5647844e50c368e79f1af2d042902ab5632ea4656" +dependencies = [ + "Inflector", + "blake2", + "expander", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "sp-application-crypto" +version = "48.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e6d6166fc63c14f158f7092c2b1ae1c23f446591699252f5a6063d86c628bd1" +dependencies = [ + "parity-scale-codec", + "scale-info", + "serde", + "sp-core", + "sp-io", +] + +[[package]] +name = "sp-arithmetic" +version = "28.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e06588d1c43f60b9bb7f989785689842cc4cc8ce0e19d1c47686b1ff5fe9548" +dependencies = [ + "docify", + "integer-sqrt", + "num-traits", + "parity-scale-codec", + "scale-info", + "serde", + "static_assertions", +] + +[[package]] +name = "sp-core" +version = "43.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41314ae10e80f9f0c5f68bb23f26f2d21f105bb6d867810b719c29c684aa0458" +dependencies = [ + "ark-vrf", + "array-bytes", + "bip39", + "bitflags", + "blake2", + "bounded-collections", + "bs58", + "dyn-clone", + "ed25519-zebra", + "futures", + "hash-db", + "hash256-std-hasher", + "impl-serde", + "itertools 0.11.0", + "k256", + "libsecp256k1", + "log", + "merlin", + "parity-scale-codec", + "parking_lot", + "paste", + "primitive-types", + "rand", + "scale-info", + "schnorrkel", + "secp256k1", + "secrecy", + "serde", + "sha2 0.10.9", + "sp-crypto-hashing", + "sp-debug-derive", + "sp-externalities", + "sp-std", + "sp-storage", + "ss58-registry", + "substrate-bip39", + "thiserror", + "tracing", + "w3f-bls", + "zeroize", +] + +[[package]] +name = "sp-crypto-hashing" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc9927a7f81334ed5b8a98a4a978c81324d12bd9713ec76b5c68fd410174c5eb" +dependencies = [ + "blake2b_simd", + "byteorder", + "digest 0.10.7", + "sha2 0.10.9", + "sha3", + "twox-hash", +] + +[[package]] +name = "sp-crypto-hashing-proc-macro" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b85d0f1f1e44bd8617eb2a48203ee854981229e3e79e6f468c7175d5fd37489b" +dependencies = [ + "quote", + "sp-crypto-hashing", + "syn 2.0.98", +] + +[[package]] +name = "sp-debug-derive" +version = "15.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d61809bf52be994e4d0a0485bb18a78509ed185e1418736c1ff9011bd1528999" +dependencies = [ + "proc-macro-warning", + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "sp-externalities" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa3b0eef9f045e705a34987920f538b0730f7e1a11798e3b32642ccf3dafb979" +dependencies = [ + "environmental", + "parity-scale-codec", + "sp-storage", +] + +[[package]] +name = "sp-genesis-builder" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "483f915e6f303af420e0fed4a07b0ed7bd1f0a74c7c5c36acb413de9998412d5" +dependencies = [ + "parity-scale-codec", + "scale-info", + "serde_json", + "sp-api", + "sp-runtime", +] + +[[package]] +name = "sp-inherents" +version = "43.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e4c30e3ff24fe69140b0b6cff47aa20c5c87b3985673aa01cfd3ec365b83c2a" +dependencies = [ + "async-trait", + "impl-trait-for-tuples", + "parity-scale-codec", + "scale-info", + "sp-runtime", + "thiserror", +] + +[[package]] +name = "sp-io" +version = "48.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "288c325303adfbe4c570be8dccd0fbb599e245bf7d78bbdab2acfbaa326168c9" +dependencies = [ + "bytes", + "docify", + "ed25519-dalek", + "libsecp256k1", + "log", + "parity-scale-codec", + "polkavm-derive", + "rustversion", + "secp256k1", + "sp-core", + "sp-crypto-hashing", + "sp-externalities", + "sp-keystore", + "sp-runtime-interface", + "sp-state-machine", + "sp-tracing", + "sp-trie", + "tracing", + "tracing-core", +] + +[[package]] +name = "sp-keystore" +version = "0.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20bb3c3354b6d43720d95b9da9188a302f796e2a62c50aa0c7dcc8d2df32c7d3" +dependencies = [ + "parity-scale-codec", + "parking_lot", + "sp-core", + "sp-externalities", +] + +[[package]] +name = "sp-metadata-ir" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d4e7855b9d8b356ffd264ed089bc1063675880742bb4535f206831a87fc725e" +dependencies = [ + "derive-where", + "frame-metadata", + "parity-scale-codec", + "scale-info", +] + +[[package]] +name = "sp-panic-handler" +version = "13.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8b52e69a577cbfdea62bfaf16f59eb884422ce98f78b5cd8d9bf668776bced1" +dependencies = [ + "backtrace", + "regex", +] + +[[package]] +name = "sp-runtime" +version = "48.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "068ec7054022b0867b9b3ae2b871a03e9b21b62fbb675c12768e89b3166d8cdd" +dependencies = [ + "binary-merkle-tree", + "bytes", + "docify", + "either", + "hash256-std-hasher", + "impl-trait-for-tuples", + "log", + "num-traits", + "parity-scale-codec", + "paste", + "rand", + "scale-info", + "serde", + "simple-mermaid", + "sp-application-crypto", + "sp-arithmetic", + "sp-core", + "sp-io", + "sp-std", + "sp-trie", + "sp-weights", + "strum", + "tracing", + "tuplex", +] + +[[package]] +name = "sp-runtime-interface" +version = "37.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14b5a271292ac99198015ace377351e3b39e355b647dedbbc743dc1206dd4265" +dependencies = [ + "bytes", + "impl-trait-for-tuples", + "parity-scale-codec", + "polkavm-derive", + "sp-externalities", + "sp-runtime-interface-proc-macro", + "sp-std", + "sp-storage", + "sp-tracing", + "sp-wasm-interface", + "static_assertions", +] + +[[package]] +name = "sp-runtime-interface-proc-macro" +version = "21.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7d9650b5186fd32bf5198adfe08d8fecc76f2ebe8a8927f28aaf2a537d75b2e" +dependencies = [ + "Inflector", + "expander", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "sp-staking" +version = "45.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291d6f6da1cec243394d9ef505b41fc83cf966390f9105848303df3c881b5cd8" +dependencies = [ + "impl-trait-for-tuples", + "parity-scale-codec", + "scale-info", + "serde", + "sp-core", + "sp-runtime", +] + +[[package]] +name = "sp-state-machine" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4baaee9d7756bef1e2c1973ab1befb15b7d0b4105a65d0b649da662225ae1692" +dependencies = [ + "hash-db", + "log", + "parity-scale-codec", + "parking_lot", + "rand", + "smallvec", + "sp-core", + "sp-externalities", + "sp-panic-handler", + "sp-trie", + "thiserror", + "tracing", + "trie-db", +] + +[[package]] +name = "sp-std" +version = "14.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f8ee986414b0a9ad741776762f4083cd3a5128449b982a3919c4df36874834" + +[[package]] +name = "sp-storage" +version = "25.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a6b7ba48e82f36b2c4b037059866c858f3f3bfe79bfa71345fee61bd45d69b" +dependencies = [ + "impl-serde", + "parity-scale-codec", + "ref-cast", + "serde", + "sp-debug-derive", +] + +[[package]] +name = "sp-tracing" +version = "19.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2c7372456c39cc81e15befe54d0caab8378f2b30fd34d1bcb5f0f56631c6b6e" +dependencies = [ + "parity-scale-codec", + "regex", + "tracing", + "tracing-core", + "tracing-subscriber", +] + +[[package]] +name = "sp-trie" +version = "46.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3ffd357cd03519b56f5fa274a90dde587f80914fb0f5d1f6cfae9a40aa38bf7" +dependencies = [ + "ahash", + "foldhash", + "hash-db", + "hashbrown 0.15.3", + "memory-db", + "nohash-hasher", + "parity-scale-codec", + "parking_lot", + "rand", + "scale-info", + "schnellru", + "sp-core", + "sp-externalities", + "substrate-prometheus-endpoint", + "thiserror", + "tracing", + "trie-db", + "trie-root", +] + +[[package]] +name = "sp-version" +version = "46.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bce4baa3e414068c3a874be104e8cbbea00e300ced3332ca5fec587adc427ab" +dependencies = [ + "impl-serde", + "parity-scale-codec", + "parity-wasm", + "scale-info", + "serde", + "sp-core", + "sp-crypto-hashing-proc-macro", + "sp-runtime", + "sp-std", + "sp-version-proc-macro", + "thiserror", +] + +[[package]] +name = "sp-version-proc-macro" +version = "15.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54cabc8279e835cd9c608d70cb00e693bddec94fe8478e9f3104dad1da5f93ca" +dependencies = [ + "parity-scale-codec", + "proc-macro-warning", + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "sp-wasm-interface" +version = "27.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d0d8ca13712d195299005601abc5d5062d5bbb11d69f0582f961b86dd6052a8" +dependencies = [ + "anyhow", + "impl-trait-for-tuples", + "log", + "parity-scale-codec", +] + +[[package]] +name = "sp-weights" +version = "36.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ca43a748480ec03bedd4b67b8e7144310afebfb004b3e49ba977565fbdd4e2" +dependencies = [ + "bounded-collections", + "parity-scale-codec", + "scale-info", + "serde", + "smallvec", + "sp-arithmetic", + "sp-debug-derive", +] + +[[package]] +name = "spki" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "ss58-registry" +version = "1.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6915280e2d0db8911e5032a5c275571af6bdded2916abd691a659be25d3439" +dependencies = [ + "Inflector", + "num-format", + "proc-macro2", + "quote", + "serde", + "serde_json", + "unicode-xid", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.98", +] + +[[package]] +name = "substrate-bip39" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d93affb0135879b1b67cbcf6370a256e1772f9eaaece3899ec20966d67ad0492" +dependencies = [ + "hmac", + "pbkdf2", + "schnorrkel", + "sha2 0.10.9", + "zeroize", +] + +[[package]] +name = "substrate-prometheus-endpoint" +version = "0.17.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d23e4bc8e910a312820d589047ab683928b761242dbe31dee081fbdb37cbe0be" +dependencies = [ + "http-body-util", + "hyper", + "hyper-util", + "log", + "prometheus", + "thiserror", + "tokio", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "termcolor" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "socket2", + "windows-sys", +] + +[[package]] +name = "tokio-util" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.22.22", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow 0.5.15", +] + +[[package]] +name = "toml_edit" +version = "0.22.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow 0.6.18", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "time", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "trie-db" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7795f2df2ef744e4ffb2125f09325e60a21d305cc3ecece0adeef03f7a9e560" +dependencies = [ + "hash-db", + "log", + "rustc-hex", + "smallvec", +] + +[[package]] +name = "trie-root" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4ed310ef5ab98f5fa467900ed906cb9232dd5376597e00fd4cba2a449d06c0b" +dependencies = [ + "hash-db", +] + +[[package]] +name = "tt-call" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f195fd851901624eee5a58c4bb2b4f06399148fcd0ed336e6f1cb60a9881df" + +[[package]] +name = "tuplex" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "676ac81d5454c4dcf37955d34fa8626ede3490f744b86ca14a7b90168d2a08aa" + +[[package]] +name = "twox-hash" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" +dependencies = [ + "cfg-if", + "digest 0.10.7", + "rand", + "static_assertions", +] + +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "uint" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "909988d098b2f738727b161a106cfc7cab00c539c2687a8836f8e565976fb53e" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + +[[package]] +name = "unicode-ident" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-xid" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "w3f-bls" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6bfb937b3d12077654a9e43e32a4e9c20177dd9fea0f3aba673e7840bb54f32" +dependencies = [ + "ark-bls12-377", + "ark-bls12-381 0.4.0", + "ark-ec 0.4.2", + "ark-ff 0.4.2", + "ark-serialize 0.4.2", + "ark-serialize-derive 0.4.2", + "arrayref", + "digest 0.10.7", + "rand", + "rand_chacha", + "rand_core", + "sha2 0.10.9", + "sha3", + "zeroize", +] + +[[package]] +name = "w3f-pcs" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ea1046a1deb6d26c34ba2d1f1bab4222d695d126502ee765f80b021753cb674" +dependencies = [ + "ark-ec 0.5.0", + "ark-ff 0.5.0", + "ark-poly 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "merlin", +] + +[[package]] +name = "w3f-plonk-common" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30408cda37b81bd7257319942584c794c5784d00d749757bc664656749a1472a" +dependencies = [ + "ark-ec 0.5.0", + "ark-ff 0.5.0", + "ark-poly 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "getrandom_or_panic", + "rand_core", + "w3f-pcs", +] + +[[package]] +name = "w3f-ring-proof" +version = "0.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cbfc4cb881a934e6f33c25927bf955d0cb18e52b94528bbc5fa28dddedb4cd1" +dependencies = [ + "ark-ec 0.5.0", + "ark-ff 0.5.0", + "ark-poly 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "ark-transcript", + "w3f-pcs", + "w3f-plonk-common", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wide" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa469ffa65ef7e0ba0f164183697b89b854253fd31aeb92358b7b6155177d62f" +dependencies = [ + "bytemuck", + "safe_arch", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c2e3184b9c4e92ad5167ca73039d0c42476302ab603e2fec4487511f38ccefc" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" +dependencies = [ + "memchr", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "zerocopy" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] diff --git a/template/vendor/pallet-asset-conversion/Cargo.toml b/template/vendor/pallet-asset-conversion/Cargo.toml new file mode 100644 index 00000000..59619d35 --- /dev/null +++ b/template/vendor/pallet-asset-conversion/Cargo.toml @@ -0,0 +1,231 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +name = "pallet-asset-conversion" +version = "31.0.0" +authors = ["Parity Technologies "] +build = false +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "FRAME asset conversion pallet" +homepage = "https://paritytech.github.io/polkadot-sdk/" +readme = "README.md" +license = "Apache-2.0" +repository = "https://github.com/paritytech/polkadot-sdk.git" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[features] +default = ["std"] +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", +] +std = [ + "codec/std", + "frame-benchmarking?/std", + "frame-support/std", + "frame-system/std", + "log/std", + "scale-info/std", + "sp-api/std", + "sp-arithmetic/std", + "sp-core/std", + "sp-io/std", + "sp-runtime/std", +] +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", + "sp-runtime/try-runtime", +] + +[lib] +name = "pallet_asset_conversion" +path = "src/lib.rs" + +[dependencies.codec] +version = "3.7.5" +default-features = false +package = "parity-scale-codec" + +[dependencies.frame-benchmarking] +version = "49.0.0" +optional = true +default-features = false + +[dependencies.frame-support] +version = "48.0.0" +default-features = false + +[dependencies.frame-system] +version = "48.0.0" +default-features = false + +[dependencies.log] +version = "0.4.22" +default-features = false + +[dependencies.scale-info] +version = "2.11.6" +features = ["derive"] +default-features = false + +[dependencies.sp-api] +version = "43.0.0" +default-features = false + +[dependencies.sp-arithmetic] +version = "28.0.1" +default-features = false + +[dependencies.sp-core] +version = "43.0.0" +default-features = false + +[dependencies.sp-io] +version = "48.0.0" +default-features = false + +[dependencies.sp-runtime] +version = "48.0.0" +default-features = false + +[dev-dependencies.primitive-types] +version = "0.13.1" +features = [ + "num-traits", + "codec", + "num-traits", + "scale-info", +] +default-features = false + +[lints.clippy.all] +level = "allow" +priority = 0 + +[lints.clippy.bind_instead_of_map] +level = "allow" +priority = 2 + +[lints.clippy.borrowed-box] +level = "allow" +priority = 2 + +[lints.clippy.complexity] +level = "warn" +priority = 1 + +[lints.clippy.correctness] +level = "warn" +priority = 1 + +[lints.clippy.default_constructed_unit_structs] +level = "allow" +priority = 2 + +[lints.clippy.derivable_impls] +level = "allow" +priority = 2 + +[lints.clippy.eq_op] +level = "allow" +priority = 2 + +[lints.clippy.erasing_op] +level = "allow" +priority = 2 + +[lints.clippy.extra-unused-type-parameters] +level = "allow" +priority = 2 + +[lints.clippy.identity-op] +level = "allow" +priority = 2 + +[lints.clippy.if-same-then-else] +level = "allow" +priority = 2 + +[lints.clippy.needless-lifetimes] +level = "allow" +priority = 2 + +[lints.clippy.needless_option_as_deref] +level = "allow" +priority = 2 + +[lints.clippy.nonminimal-bool] +level = "allow" +priority = 2 + +[lints.clippy.option-map-unit-fn] +level = "allow" +priority = 2 + +[lints.clippy.stable_sort_primitive] +level = "allow" +priority = 2 + +[lints.clippy.too-many-arguments] +level = "allow" +priority = 2 + +[lints.clippy.type_complexity] +level = "allow" +priority = 2 + +[lints.clippy.unit_arg] +level = "allow" +priority = 2 + +[lints.clippy.unnecessary_cast] +level = "allow" +priority = 2 + +[lints.clippy.useless_conversion] +level = "allow" +priority = 2 + +[lints.clippy.while_immutable_condition] +level = "allow" +priority = 2 + +[lints.clippy.zero-prefixed-literal] +level = "allow" +priority = 2 + +[lints.rust.suspicious_double_ref_op] +level = "allow" +priority = 2 + +[lints.rust.unexpected_cfgs] +level = "warn" +priority = 0 +check-cfg = [ + 'cfg(build_opt_level, values("3"))', + 'cfg(build_profile, values("debug", "release"))', + "cfg(enable_alloc_error_handler)", + "cfg(fuzzing)", + "cfg(ignore_flaky_test)", + "cfg(revive_debug)", + "cfg(substrate_runtime)", +] diff --git a/template/vendor/pallet-asset-conversion/Cargo.toml.orig b/template/vendor/pallet-asset-conversion/Cargo.toml.orig new file mode 100644 index 00000000..abc22303 --- /dev/null +++ b/template/vendor/pallet-asset-conversion/Cargo.toml.orig @@ -0,0 +1,61 @@ +[package] +name = "pallet-asset-conversion" +version = "31.0.0" +authors.workspace = true +edition.workspace = true +license = "Apache-2.0" +homepage.workspace = true +repository.workspace = true +description = "FRAME asset conversion pallet" +readme = "README.md" + +[lints] +workspace = true + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { workspace = true } +frame-benchmarking = { optional = true, workspace = true } +frame-support.workspace = true +frame-system.workspace = true +log = { workspace = true } +scale-info = { features = ["derive"], workspace = true } +sp-api.workspace = true +sp-arithmetic.workspace = true +sp-core.workspace = true +sp-io.workspace = true +sp-runtime.workspace = true + +[dev-dependencies] +pallet-assets = { default-features = true, path = "../assets" } +pallet-balances = { default-features = true, path = "../balances" } +primitive-types = { features = ["codec", "num-traits", "scale-info"], workspace = true } + +[features] +default = ["std"] +std = [ + "codec/std", + "frame-benchmarking?/std", + "frame-support/std", + "frame-system/std", + "log/std", + "scale-info/std", + "sp-api/std", + "sp-arithmetic/std", + "sp-core/std", + "sp-io/std", + "sp-runtime/std", +] +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", +] +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", + "sp-runtime/try-runtime", +] diff --git a/template/vendor/pallet-asset-conversion/DEOS-PATCH.md b/template/vendor/pallet-asset-conversion/DEOS-PATCH.md new file mode 100644 index 00000000..654844a8 --- /dev/null +++ b/template/vendor/pallet-asset-conversion/DEOS-PATCH.md @@ -0,0 +1,28 @@ +# DEOS Asset Conversion Full-Balance Backport + +## Authority + +- Upstream crate: `pallet-asset-conversion 31.0.0`. +- Original crates.io checksum: `9372e01886c4a9b625929c2549ac50d67cfe0f8e4daf232d884aecc843fe2557`. +- Upstream pull request: `paritytech/polkadot-sdk#12408`. +- Upstream merge commit: `408895c27a5aff4bac99e956df5426983566f8cb`. +- Parent upstream `src/lib.rs` SHA-256: `060af0ccea54c6d970a67b6f65af94c952d995f22885ed27861f9095ec54a010`. +- Fixed upstream and retained vendored `src/lib.rs` SHA-256: `cc2cd2d15e190d03b19b564c529ee06f0fe6987979c2c08fd532ca40d734c2f0`. + +## Exact Patch + +The retained source applies only the upstream production changes from PR #12408 to the crates.io 31.0.0 implementation: + +- pool reserve reads use full `Assets::balance` rather than `reducible_balance`; +- liquidity withdrawal obtains both reserves through `get_reserves`; +- exact-input and exact-output quote paths obtain both reserves through `get_reserves`. + +The upstream post-merge `src/lib.rs` and retained `src/lib.rs` have the same SHA-256 above. The upstream regression is realized at the DEOS runtime integration boundary because the crates.io package does not ship a complete independently buildable dev-dependency set. + +## Reachability and Removal + +- `template/Cargo.toml` patches crates.io to this directory. +- `cargo tree -p deos-runtime -i pallet-asset-conversion --edges normal` must resolve exactly one package and show this path. +- `template/Cargo.lock` must contain exactly one `pallet-asset-conversion` package and no registry source for it. +- Remove this directory and `[patch.crates-io]` entry when the selected compatible Polkadot SDK release contains PR #12408 with equivalent or stronger full-balance reserve semantics. +- Review expiry: before DEOS 0.7.25 or any Polkadot SDK dependency update, whichever occurs first. diff --git a/template/vendor/pallet-asset-conversion/README.md b/template/vendor/pallet-asset-conversion/README.md new file mode 100644 index 00000000..e895db5e --- /dev/null +++ b/template/vendor/pallet-asset-conversion/README.md @@ -0,0 +1,25 @@ +# asset-conversion + +## A swap pallet + +This pallet allows assets to be converted from one type to another by means of a constant product formula. +The pallet based is based on [Uniswap V2](https://github.com/Uniswap/v2-core) logic. + +### Overview + +This pallet allows you to: + + - create a liquidity pool for 2 assets + - provide the liquidity and receive back an LP token + - exchange the LP token back to assets + - swap 2 assets if there is a pool created + - query for an exchange price via a new runtime call endpoint + - query the size of a liquidity pool. + +Please see the rust module documentation for full details: + +`cargo doc -p pallet-asset-conversion --open` + +### License + +License: Apache-2.0 diff --git a/template/vendor/pallet-asset-conversion/src/benchmarking.rs b/template/vendor/pallet-asset-conversion/src/benchmarking.rs new file mode 100644 index 00000000..2897ef72 --- /dev/null +++ b/template/vendor/pallet-asset-conversion/src/benchmarking.rs @@ -0,0 +1,439 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Asset Conversion pallet benchmarking. + +use super::*; +use crate::Pallet as AssetConversion; +use alloc::vec; +use core::marker::PhantomData; +use frame_benchmarking::{v2::*, whitelisted_caller}; +use frame_support::{ + assert_ok, + traits::{ + fungible::NativeOrWithId, + fungibles::{Create, Inspect, Mutate, Refund}, + }, +}; +use frame_system::RawOrigin as SystemOrigin; +use sp_core::Get; + +/// Benchmark Helper +pub trait BenchmarkHelper { + /// Returns a valid assets pair for the pool creation. + /// + /// When a specific asset, such as the native asset, is required in every pool, it should be + /// returned for each odd-numbered seed. + fn create_pair(seed1: u32, seed2: u32) -> (AssetKind, AssetKind); +} + +impl BenchmarkHelper for () +where + AssetKind: From, +{ + fn create_pair(seed1: u32, seed2: u32) -> (AssetKind, AssetKind) { + (seed1.into(), seed2.into()) + } +} + +/// Factory for creating a valid asset pairs with [`NativeOrWithId::Native`] always leading in the +/// pair. +pub struct NativeOrWithIdFactory(PhantomData); +impl + Ord> BenchmarkHelper> + for NativeOrWithIdFactory +{ + fn create_pair(seed1: u32, seed2: u32) -> (NativeOrWithId, NativeOrWithId) { + if seed1.is_multiple_of(2) { + (NativeOrWithId::WithId(seed2.into()), NativeOrWithId::Native) + } else { + (NativeOrWithId::Native, NativeOrWithId::WithId(seed2.into())) + } + } +} + +/// Provides a pair of amounts expected to serve as sufficient initial liquidity for a pool. +fn valid_liquidity_amount(ed1: T::Balance, ed2: T::Balance) -> (T::Balance, T::Balance) +where + T::Assets: Inspect, +{ + let l = + ed1.max(ed2) + T::MintMinLiquidity::get() + T::MintMinLiquidity::get() + T::Balance::one(); + (l, l) +} + +/// Create the `asset` and mint the `amount` for the `caller`. +fn create_asset( + caller: &T::AccountId, + asset: &T::AssetKind, + amount: T::Balance, + is_sufficient: bool, +) where + T::Assets: Create + Mutate, +{ + if !T::Assets::asset_exists(asset.clone()) { + assert_ok!(T::Assets::create( + asset.clone(), + caller.clone(), + is_sufficient, + T::Balance::one() + )); + } + assert_ok!(T::Assets::mint_into( + asset.clone(), + &caller, + amount + T::Assets::minimum_balance(asset.clone()) + )); +} + +/// Create the designated fee asset for pool creation. +fn create_fee_asset(caller: &T::AccountId) +where + T::Assets: Create + Mutate, +{ + let fee_asset = T::PoolSetupFeeAsset::get(); + if !T::Assets::asset_exists(fee_asset.clone()) { + assert_ok!(T::Assets::create(fee_asset.clone(), caller.clone(), true, T::Balance::one())); + } + assert_ok!(T::Assets::mint_into( + fee_asset.clone(), + &caller, + T::Assets::minimum_balance(fee_asset) + )); +} + +/// Mint the fee asset for the `caller` sufficient to cover the fee for creating a new pool. +fn mint_setup_fee_asset( + caller: &T::AccountId, + asset1: &T::AssetKind, + asset2: &T::AssetKind, + lp_token: &T::PoolAssetId, +) where + T::Assets: Create + Mutate, +{ + assert_ok!(T::Assets::mint_into( + T::PoolSetupFeeAsset::get(), + &caller, + T::PoolSetupFee::get() + + T::Assets::deposit_required(asset1.clone()) + + T::Assets::deposit_required(asset2.clone()) + + T::PoolAssets::deposit_required(lp_token.clone()) + )); +} + +/// Creates a pool for a given asset pair. +/// +/// This action mints the necessary amounts of the given assets for the `caller` to provide initial +/// liquidity. It returns the LP token ID along with a pair of amounts sufficient for the pool's +/// initial liquidity. +fn create_asset_and_pool( + caller: &T::AccountId, + asset1: &T::AssetKind, + asset2: &T::AssetKind, +) -> (T::PoolAssetId, T::Balance, T::Balance) +where + T::Assets: Create + Mutate, +{ + let (liquidity1, liquidity2) = valid_liquidity_amount::( + T::Assets::minimum_balance(asset1.clone()), + T::Assets::minimum_balance(asset2.clone()), + ); + create_asset::(caller, asset1, liquidity1, true); + create_asset::(caller, asset2, liquidity2, true); + let lp_token = AssetConversion::::get_next_pool_asset_id(); + + mint_setup_fee_asset::(caller, asset1, asset2, &lp_token); + + assert_ok!(AssetConversion::::create_pool( + SystemOrigin::Signed(caller.clone()).into(), + Box::new(asset1.clone()), + Box::new(asset2.clone()) + )); + + (lp_token, liquidity1, liquidity2) +} + +fn assert_last_event(generic_event: ::RuntimeEvent) { + let events = frame_system::Pallet::::events(); + let system_event: ::RuntimeEvent = generic_event.into(); + // compare to the last event record + let frame_system::EventRecord { event, .. } = &events[events.len() - 1]; + assert_eq!(event, &system_event); +} + +#[benchmarks(where T::Assets: Create + Mutate, T::PoolAssetId: Into,)] +mod benchmarks { + use super::*; + + #[benchmark] + fn create_pool() { + let caller: T::AccountId = whitelisted_caller(); + let (asset1, asset2) = T::BenchmarkHelper::create_pair(0, 1); + create_asset::(&caller, &asset1, T::Assets::minimum_balance(asset1.clone()), true); + create_asset::(&caller, &asset2, T::Assets::minimum_balance(asset2.clone()), true); + + let lp_token = AssetConversion::::get_next_pool_asset_id(); + create_fee_asset::(&caller); + mint_setup_fee_asset::(&caller, &asset1, &asset2, &lp_token); + + #[extrinsic_call] + _(SystemOrigin::Signed(caller.clone()), Box::new(asset1.clone()), Box::new(asset2.clone())); + + let pool_id = T::PoolLocator::pool_id(&asset1, &asset2).unwrap(); + let pool_account = T::PoolLocator::address(&pool_id).unwrap(); + assert_last_event::( + Event::PoolCreated { creator: caller, pool_account, pool_id, lp_token }.into(), + ); + } + + #[benchmark] + fn add_liquidity() { + let caller: T::AccountId = whitelisted_caller(); + let (asset1, asset2) = T::BenchmarkHelper::create_pair(0, 1); + + create_fee_asset::(&caller); + let (lp_token, liquidity1, liquidity2) = + create_asset_and_pool::(&caller, &asset1, &asset2); + + #[extrinsic_call] + _( + SystemOrigin::Signed(caller.clone()), + Box::new(asset1.clone()), + Box::new(asset2.clone()), + liquidity1, + liquidity2, + T::Balance::one(), + T::Balance::zero(), + caller.clone(), + ); + + let pool_account = T::PoolLocator::pool_address(&asset1, &asset2).unwrap(); + let lp_minted = + AssetConversion::::calc_lp_amount_for_zero_supply(&liquidity1, &liquidity2).unwrap(); + assert_eq!(T::PoolAssets::balance(lp_token, &caller), lp_minted); + assert_eq!(T::Assets::balance(asset1, &pool_account), liquidity1); + assert_eq!(T::Assets::balance(asset2, &pool_account), liquidity2); + } + + #[benchmark] + fn remove_liquidity() { + let caller: T::AccountId = whitelisted_caller(); + let (asset1, asset2) = T::BenchmarkHelper::create_pair(0, 1); + + create_fee_asset::(&caller); + let (lp_token, liquidity1, liquidity2) = + create_asset_and_pool::(&caller, &asset1, &asset2); + + let remove_lp_amount = T::Balance::one(); + + assert_ok!(AssetConversion::::add_liquidity( + SystemOrigin::Signed(caller.clone()).into(), + Box::new(asset1.clone()), + Box::new(asset2.clone()), + liquidity1, + liquidity2, + T::Balance::one(), + T::Balance::zero(), + caller.clone(), + )); + let total_supply = + >::total_issuance(lp_token.clone()); + + #[extrinsic_call] + _( + SystemOrigin::Signed(caller.clone()), + Box::new(asset1), + Box::new(asset2), + remove_lp_amount, + T::Balance::zero(), + T::Balance::zero(), + caller.clone(), + ); + + let new_total_supply = >::total_issuance(lp_token); + assert_eq!(new_total_supply, total_supply - remove_lp_amount); + } + + #[benchmark] + fn swap_exact_tokens_for_tokens(n: Linear<2, { T::MaxSwapPathLength::get() }>) { + let mut swap_amount = T::Balance::one(); + let mut path = vec![]; + + let caller: T::AccountId = whitelisted_caller(); + create_fee_asset::(&caller); + for n in 1..n { + let (asset1, asset2) = T::BenchmarkHelper::create_pair(n - 1, n); + swap_amount = swap_amount + T::Balance::one(); + if path.len() == 0 { + path = vec![Box::new(asset1.clone()), Box::new(asset2.clone())]; + } else { + path.push(Box::new(asset2.clone())); + } + + let (_, liquidity1, liquidity2) = create_asset_and_pool::(&caller, &asset1, &asset2); + + assert_ok!(AssetConversion::::add_liquidity( + SystemOrigin::Signed(caller.clone()).into(), + Box::new(asset1.clone()), + Box::new(asset2.clone()), + liquidity1, + liquidity2, + T::Balance::one(), + T::Balance::zero(), + caller.clone(), + )); + } + + let asset_in = *path.first().unwrap().clone(); + assert_ok!(T::Assets::mint_into( + asset_in.clone(), + &caller, + swap_amount + T::Balance::one() + )); + let init_caller_balance = T::Assets::balance(asset_in.clone(), &caller); + + #[extrinsic_call] + _( + SystemOrigin::Signed(caller.clone()), + path, + swap_amount, + T::Balance::one(), + caller.clone(), + true, + ); + + let actual_balance = T::Assets::balance(asset_in, &caller); + assert_eq!(actual_balance, init_caller_balance - swap_amount); + } + + #[benchmark] + fn swap_tokens_for_exact_tokens(n: Linear<2, { T::MaxSwapPathLength::get() }>) { + let mut max_swap_amount = T::Balance::one(); + let mut path = vec![]; + + let caller: T::AccountId = whitelisted_caller(); + create_fee_asset::(&caller); + for n in 1..n { + let (asset1, asset2) = T::BenchmarkHelper::create_pair(n - 1, n); + max_swap_amount = max_swap_amount + T::Balance::one() + T::Balance::one(); + if path.len() == 0 { + path = vec![Box::new(asset1.clone()), Box::new(asset2.clone())]; + } else { + path.push(Box::new(asset2.clone())); + } + + let (_, liquidity1, liquidity2) = create_asset_and_pool::(&caller, &asset1, &asset2); + + assert_ok!(AssetConversion::::add_liquidity( + SystemOrigin::Signed(caller.clone()).into(), + Box::new(asset1.clone()), + Box::new(asset2.clone()), + liquidity1, + liquidity2, + T::Balance::one(), + T::Balance::zero(), + caller.clone(), + )); + } + + let asset_in = *path.first().unwrap().clone(); + let asset_out = *path.last().unwrap().clone(); + assert_ok!(T::Assets::mint_into(asset_in, &caller, max_swap_amount)); + let init_caller_balance = T::Assets::balance(asset_out.clone(), &caller); + + #[extrinsic_call] + _( + SystemOrigin::Signed(caller.clone()), + path, + T::Balance::one(), + max_swap_amount, + caller.clone(), + true, + ); + + let actual_balance = T::Assets::balance(asset_out, &caller); + assert_eq!(actual_balance, init_caller_balance + T::Balance::one()); + } + + #[benchmark] + fn touch(n: Linear<0, 3>) { + let caller: T::AccountId = whitelisted_caller(); + let (asset1, asset2) = T::BenchmarkHelper::create_pair(0, 1); + let pool_id = T::PoolLocator::pool_id(&asset1, &asset2).unwrap(); + let pool_account = T::PoolLocator::address(&pool_id).unwrap(); + + create_fee_asset::(&caller); + create_asset::(&caller, &asset1, ::Balance::one(), false); + create_asset::(&caller, &asset2, ::Balance::one(), false); + let lp_token = AssetConversion::::get_next_pool_asset_id(); + mint_setup_fee_asset::(&caller, &asset1, &asset2, &lp_token); + + assert_ok!(AssetConversion::::create_pool( + SystemOrigin::Signed(caller.clone()).into(), + Box::new(asset1.clone()), + Box::new(asset2.clone()) + )); + + if n > 0 && + ::Assets::deposit_held(asset1.clone(), pool_account.clone()).is_some() + { + let _ = ::Assets::refund(asset1.clone(), pool_account.clone()); + } + if n > 1 && + ::Assets::deposit_held(asset2.clone(), pool_account.clone()).is_some() + { + let _ = ::Assets::refund(asset2.clone(), pool_account.clone()); + } + if n > 2 && + ::PoolAssets::deposit_held(lp_token.clone(), pool_account.clone()) + .is_some() + { + let _ = ::PoolAssets::refund(lp_token, pool_account); + } + + #[extrinsic_call] + _(SystemOrigin::Signed(caller.clone()), Box::new(asset1.clone()), Box::new(asset2.clone())); + + assert_last_event::(Event::Touched { pool_id, who: caller }.into()); + } + + #[benchmark] + fn get_reserves() { + let caller: T::AccountId = whitelisted_caller(); + let (asset1, asset2) = T::BenchmarkHelper::create_pair(0, 1); + + create_fee_asset::(&caller); + let (_, liquidity1, liquidity2) = create_asset_and_pool::(&caller, &asset1, &asset2); + + assert_ok!(AssetConversion::::add_liquidity( + SystemOrigin::Signed(caller.clone()).into(), + Box::new(asset1.clone()), + Box::new(asset2.clone()), + liquidity1, + liquidity2, + T::Balance::one(), + T::Balance::zero(), + caller.clone(), + )); + + #[block] + { + AssetConversion::::get_reserves(asset1, asset2).expect("pool has liquidity; qed"); + } + } + + impl_benchmark_test_suite!(AssetConversion, crate::mock::new_test_ext(), crate::mock::Test); +} diff --git a/template/vendor/pallet-asset-conversion/src/lib.rs b/template/vendor/pallet-asset-conversion/src/lib.rs new file mode 100644 index 00000000..27bfb631 --- /dev/null +++ b/template/vendor/pallet-asset-conversion/src/lib.rs @@ -0,0 +1,1527 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! # Substrate Asset Conversion pallet +//! +//! Substrate Asset Conversion pallet based on the [Uniswap V2](https://github.com/Uniswap/v2-core) logic. +//! +//! ## Overview +//! +//! This pallet allows you to: +//! +//! - [create a liquidity pool](`Pallet::create_pool()`) for 2 assets +//! - [provide the liquidity](`Pallet::add_liquidity()`) and receive back an LP token +//! - [exchange the LP token back to assets](`Pallet::remove_liquidity()`) +//! - [swap a specific amount of assets for another](`Pallet::swap_exact_tokens_for_tokens()`) if +//! there is a pool created, or +//! - [swap some assets for a specific amount of +//! another](`Pallet::swap_tokens_for_exact_tokens()`). +//! - [query for an exchange price](`AssetConversionApi::quote_price_exact_tokens_for_tokens`) via +//! a runtime call endpoint +//! - [query the size of a liquidity pool](`AssetConversionApi::get_reserves`) via a runtime api +//! endpoint. +//! +//! The `quote_price_exact_tokens_for_tokens` and `quote_price_tokens_for_exact_tokens` functions +//! both take a path parameter of the route to take. If you want to swap from native asset to +//! non-native asset 1, you would pass in a path of `[DOT, 1]` or `[1, DOT]`. If you want to swap +//! from non-native asset 1 to non-native asset 2, you would pass in a path of `[1, DOT, 2]`. +//! +//! (For an example of configuring this pallet to use `Location` as an asset id, see the +//! cumulus repo). +//! +//! Here is an example `state_call` that asks for a quote of a pool of native versus asset 1: +//! +//! ```text +//! curl -sS -H "Content-Type: application/json" -d \ +//! '{"id":1, "jsonrpc":"2.0", "method": "state_call", "params": ["AssetConversionApi_quote_price_tokens_for_exact_tokens", "0x0101000000000000000000000011000000000000000000"]}' \ +//! http://localhost:9933/ +//! ``` +//! (This can be run against the kitchen sync node in the `node` folder of this repo.) +#![deny(missing_docs)] +#![cfg_attr(not(feature = "std"), no_std)] + +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; +mod liquidity; +#[cfg(test)] +mod mock; +mod swap; +#[cfg(test)] +mod tests; +mod types; +pub mod weights; +#[cfg(feature = "runtime-benchmarks")] +pub use benchmarking::{BenchmarkHelper, NativeOrWithIdFactory}; +pub use liquidity::*; +pub use pallet::*; +pub use swap::*; +pub use types::*; +pub use weights::WeightInfo; + +extern crate alloc; + +use alloc::{boxed::Box, collections::btree_set::BTreeSet, vec::Vec}; +use codec::Codec; +use frame_support::{ + traits::{ + fungibles::{Balanced, Create, Credit, Inspect, Mutate}, + tokens::{ + AssetId, Balance, + Fortitude::Polite, + Precision::Exact, + Preservation::{Expendable, Preserve}, + }, + AccountTouch, Incrementable, OnUnbalanced, + }, + PalletId, +}; +use sp_core::Get; +use sp_runtime::{ + traits::{ + CheckedAdd, CheckedDiv, CheckedMul, CheckedSub, Ensure, IntegerSquareRoot, MaybeDisplay, + MaybeSerializeDeserialize, One, TrailingZeroInput, Zero, + }, + DispatchError, Saturating, TokenError, TransactionOutcome, +}; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_support::{pallet_prelude::*, traits::fungibles::Refund}; + use frame_system::pallet_prelude::*; + use sp_arithmetic::{traits::Unsigned, PerThing, Permill}; + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config { + /// Overarching event type. + #[allow(deprecated)] + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + /// The type in which the assets for swapping are measured. + type Balance: Balance; + + /// A type used for calculations concerning the `Balance` type to avoid possible overflows. + type HigherPrecisionBalance: IntegerSquareRoot + + One + + Ensure + + Unsigned + + From + + From + + TryInto; + + /// Type of asset class, sourced from [`Config::Assets`], utilized to offer liquidity to a + /// pool. + type AssetKind: Parameter + MaxEncodedLen + MaybeSerializeDeserialize; + + /// Registry of assets utilized for providing liquidity to pools. + type Assets: Inspect + + Mutate + + AccountTouch + + Balanced + + Refund; + + /// Liquidity pool identifier. + type PoolId: Parameter + MaxEncodedLen + Ord; + + /// Provides means to resolve the [`Config::PoolId`] and it's `AccountId` from a pair + /// of [`Config::AssetKind`]s. + /// + /// Examples: [`crate::types::WithFirstAsset`], [`crate::types::Ascending`]. + type PoolLocator: PoolLocator; + + /// Asset class for the lp tokens from [`Self::PoolAssets`]. + type PoolAssetId: AssetId + PartialOrd + Incrementable + From; + + /// Registry for the lp tokens. Ideally only this pallet should have create permissions on + /// the assets. + type PoolAssets: Inspect + + Create + + Mutate + + AccountTouch + + Refund; + + /// The fraction of every swap that the liquidity providers take as a fee. + #[pallet::constant] + type LPFee: Get; + + /// A one-time fee to setup the pool. + #[pallet::constant] + type PoolSetupFee: Get; + + /// Asset class from [`Config::Assets`] used to pay the [`Config::PoolSetupFee`]. + #[pallet::constant] + type PoolSetupFeeAsset: Get; + + /// Handler for the [`Config::PoolSetupFee`]. + type PoolSetupFeeTarget: OnUnbalanced>; + + /// A fee to withdraw the liquidity. + #[pallet::constant] + type LiquidityWithdrawalFee: Get; + + /// The minimum LP token amount that could be minted. Ameliorates rounding errors. + #[pallet::constant] + type MintMinLiquidity: Get; + + /// The max number of hops in a swap. + #[pallet::constant] + type MaxSwapPathLength: Get; + + /// The pallet's id, used for deriving its sovereign account ID. + #[pallet::constant] + type PalletId: Get; + + /// Weight information for extrinsics in this pallet. + type WeightInfo: WeightInfo; + + /// The benchmarks need a way to create asset ids from u32s. + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper: BenchmarkHelper; + } + + /// Map from `PoolAssetId` to `PoolInfo`. This establishes whether a pool has been officially + /// created rather than people sending tokens directly to a pool's public account. + #[pallet::storage] + pub type Pools = + StorageMap<_, Blake2_128Concat, T::PoolId, PoolInfo, OptionQuery>; + + /// Stores the `PoolAssetId` that is going to be used for the next lp token. + /// This gets incremented whenever a new lp pool is created. + #[pallet::storage] + pub type NextPoolAssetId = StorageValue<_, T::PoolAssetId, OptionQuery>; + + /// Genesis config for the asset conversion pallet. + #[pallet::genesis_config] + #[derive(frame_support::DefaultNoBound)] + pub struct GenesisConfig { + /// Pools to create at genesis with initial liquidity. + /// + /// Each entry is `(asset1, asset2, liquidity_provider, amount1, amount2)`. + /// The `liquidity_provider` must hold sufficient balances of both assets + /// (e.g. via `pallet_balances` / `pallet_assets` genesis configs). + /// Set both amounts to zero to create a pool without initial liquidity. + /// No pool setup fee is charged at genesis. + pub pools: Vec<(T::AssetKind, T::AssetKind, T::AccountId, T::Balance, T::Balance)>, + } + + #[pallet::genesis_build] + impl BuildGenesisConfig for GenesisConfig { + fn build(&self) { + for (asset1, asset2, lp_provider, amount1, amount2) in &self.pools { + Pallet::::setup_pool_from_genesis( + asset1, + asset2, + lp_provider, + *amount1, + *amount2, + ) + .unwrap_or_else(|e| { + panic!("Genesis pool ({asset1:?}, {asset2:?}) setup failed: {e:?}") + }); + } + } + } + + // Pallet's events. + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// A successful call of the `CreatePool` extrinsic will create this event. + PoolCreated { + /// The account that created the pool. + creator: T::AccountId, + /// The pool id associated with the pool. Note that the order of the assets may not be + /// the same as the order specified in the create pool extrinsic. + pool_id: T::PoolId, + /// The account ID of the pool. + pool_account: T::AccountId, + /// The id of the liquidity tokens that will be minted when assets are added to this + /// pool. + lp_token: T::PoolAssetId, + }, + + /// A successful call of the `AddLiquidity` extrinsic will create this event. + LiquidityAdded { + /// The account that the liquidity was taken from. + who: T::AccountId, + /// The account that the liquidity tokens were minted to. + mint_to: T::AccountId, + /// The pool id of the pool that the liquidity was added to. + pool_id: T::PoolId, + /// The amount of the first asset that was added to the pool. + amount1_provided: T::Balance, + /// The amount of the second asset that was added to the pool. + amount2_provided: T::Balance, + /// The id of the lp token that was minted. + lp_token: T::PoolAssetId, + /// The amount of lp tokens that were minted of that id. + lp_token_minted: T::Balance, + }, + + /// A successful call of the `RemoveLiquidity` extrinsic will create this event. + LiquidityRemoved { + /// The account that the liquidity tokens were burned from. + who: T::AccountId, + /// The account that the assets were transferred to. + withdraw_to: T::AccountId, + /// The pool id that the liquidity was removed from. + pool_id: T::PoolId, + /// The amount of the first asset that was removed from the pool. + amount1: T::Balance, + /// The amount of the second asset that was removed from the pool. + amount2: T::Balance, + /// The id of the lp token that was burned. + lp_token: T::PoolAssetId, + /// The amount of lp tokens that were burned of that id. + lp_token_burned: T::Balance, + /// Liquidity withdrawal fee (%). + withdrawal_fee: Permill, + }, + /// Assets have been converted from one to another. Both `SwapExactTokenForToken` + /// and `SwapTokenForExactToken` will generate this event. + SwapExecuted { + /// Which account was the instigator of the swap. + who: T::AccountId, + /// The account that the assets were transferred to. + send_to: T::AccountId, + /// The amount of the first asset that was swapped. + amount_in: T::Balance, + /// The amount of the second asset that was received. + amount_out: T::Balance, + /// The route of asset IDs with amounts that the swap went through. + /// E.g. (A, amount_in) -> (Dot, amount_out) -> (B, amount_out) + path: BalancePath, + }, + /// Assets have been converted from one to another. + SwapCreditExecuted { + /// The amount of the first asset that was swapped. + amount_in: T::Balance, + /// The amount of the second asset that was received. + amount_out: T::Balance, + /// The route of asset IDs with amounts that the swap went through. + /// E.g. (A, amount_in) -> (Dot, amount_out) -> (B, amount_out) + path: BalancePath, + }, + /// Pool has been touched in order to fulfill operational requirements. + Touched { + /// The ID of the pool. + pool_id: T::PoolId, + /// The account initiating the touch. + who: T::AccountId, + }, + } + + #[pallet::error] + pub enum Error { + /// Provided asset pair is not supported for pool. + InvalidAssetPair, + /// Pool already exists. + PoolExists, + /// Desired amount can't be zero. + WrongDesiredAmount, + /// Provided amount should be greater than or equal to the existential deposit/asset's + /// minimal amount. + AmountOneLessThanMinimal, + /// Provided amount should be greater than or equal to the existential deposit/asset's + /// minimal amount. + AmountTwoLessThanMinimal, + /// Reserve needs to always be greater than or equal to the existential deposit/asset's + /// minimal amount. + ReserveLeftLessThanMinimal, + /// Desired amount can't be equal to the pool reserve. + AmountOutTooHigh, + /// The pool doesn't exist. + PoolNotFound, + /// An overflow happened. + Overflow, + /// The minimal amount requirement for the first token in the pair wasn't met. + AssetOneDepositDidNotMeetMinimum, + /// The minimal amount requirement for the second token in the pair wasn't met. + AssetTwoDepositDidNotMeetMinimum, + /// The minimal amount requirement for the first token in the pair wasn't met. + AssetOneWithdrawalDidNotMeetMinimum, + /// The minimal amount requirement for the second token in the pair wasn't met. + AssetTwoWithdrawalDidNotMeetMinimum, + /// Optimal calculated amount is less than desired. + OptimalAmountLessThanDesired, + /// Insufficient liquidity minted. + InsufficientLiquidityMinted, + /// Requested liquidity can't be zero. + ZeroLiquidity, + /// Amount can't be zero. + ZeroAmount, + /// Calculated amount out is less than provided minimum amount. + ProvidedMinimumNotSufficientForSwap, + /// Provided maximum amount is not sufficient for swap. + ProvidedMaximumNotSufficientForSwap, + /// The provided path must consists of 2 assets at least. + InvalidPath, + /// The provided path must consists of unique assets. + NonUniquePath, + /// It was not possible to get or increment the Id of the pool. + IncorrectPoolAssetId, + /// The destination account cannot exist with the swapped funds. + BelowMinimum, + /// The pool exists but has no liquidity (at least one of the reserves is zero). + PoolEmpty, + } + + #[pallet::hooks] + impl Hooks> for Pallet { + fn integrity_test() { + assert!( + T::MaxSwapPathLength::get() > 1, + "the `MaxSwapPathLength` should be greater than 1", + ); + } + } + + /// Pallet's callable functions. + #[pallet::call] + impl Pallet { + /// Creates an empty liquidity pool and an associated new `lp_token` asset + /// (the id of which is returned in the `Event::PoolCreated` event). + /// + /// Once a pool is created, someone may [`Pallet::add_liquidity`] to it. + #[pallet::call_index(0)] + #[pallet::weight(T::WeightInfo::create_pool())] + pub fn create_pool( + origin: OriginFor, + asset1: Box, + asset2: Box, + ) -> DispatchResult { + let sender = ensure_signed(origin)?; + Self::do_create_pool(&sender, *asset1, *asset2)?; + Ok(()) + } + + /// Provide liquidity into the pool of `asset1` and `asset2`. + /// NOTE: an optimal amount of asset1 and asset2 will be calculated and + /// might be different than the provided `amount1_desired`/`amount2_desired` + /// thus you should provide the min amount you're happy to provide. + /// Params `amount1_min`/`amount2_min` represent that. + /// `mint_to` will be sent the liquidity tokens that represent this share of the pool. + /// + /// NOTE: when encountering an incorrect exchange rate and non-withdrawable pool liquidity, + /// batch an atomic call with [`Pallet::add_liquidity`] and + /// [`Pallet::swap_exact_tokens_for_tokens`] or [`Pallet::swap_tokens_for_exact_tokens`] + /// calls to render the liquidity withdrawable and rectify the exchange rate. + /// + /// Once liquidity is added, someone may successfully call + /// [`Pallet::swap_exact_tokens_for_tokens`]. + #[pallet::call_index(1)] + #[pallet::weight(T::WeightInfo::add_liquidity())] + pub fn add_liquidity( + origin: OriginFor, + asset1: Box, + asset2: Box, + amount1_desired: T::Balance, + amount2_desired: T::Balance, + amount1_min: T::Balance, + amount2_min: T::Balance, + mint_to: T::AccountId, + ) -> DispatchResult { + let sender = ensure_signed(origin)?; + Self::do_add_liquidity( + &sender, + *asset1, + *asset2, + amount1_desired, + amount2_desired, + amount1_min, + amount2_min, + &mint_to, + )?; + Ok(()) + } + + /// Allows you to remove liquidity by providing the `lp_token_burn` tokens that will be + /// burned in the process. With the usage of `amount1_min_receive`/`amount2_min_receive` + /// it's possible to control the min amount of returned tokens you're happy with. + #[pallet::call_index(2)] + #[pallet::weight(T::WeightInfo::remove_liquidity())] + pub fn remove_liquidity( + origin: OriginFor, + asset1: Box, + asset2: Box, + lp_token_burn: T::Balance, + amount1_min_receive: T::Balance, + amount2_min_receive: T::Balance, + withdraw_to: T::AccountId, + ) -> DispatchResult { + let sender = ensure_signed(origin)?; + Self::do_remove_liquidity( + &sender, + *asset1, + *asset2, + lp_token_burn, + amount1_min_receive, + amount2_min_receive, + &withdraw_to, + )?; + Ok(()) + } + + /// Swap the exact amount of `asset1` into `asset2`. + /// `amount_out_min` param allows you to specify the min amount of the `asset2` + /// you're happy to receive. + /// + /// [`AssetConversionApi::quote_price_exact_tokens_for_tokens`] runtime call can be called + /// for a quote. + #[pallet::call_index(3)] + #[pallet::weight(T::WeightInfo::swap_exact_tokens_for_tokens(path.len() as u32))] + pub fn swap_exact_tokens_for_tokens( + origin: OriginFor, + path: Vec>, + amount_in: T::Balance, + amount_out_min: T::Balance, + send_to: T::AccountId, + keep_alive: bool, + ) -> DispatchResult { + let sender = ensure_signed(origin)?; + Self::do_swap_exact_tokens_for_tokens( + sender, + path.into_iter().map(|a| *a).collect(), + amount_in, + Some(amount_out_min), + send_to, + keep_alive, + )?; + Ok(()) + } + + /// Swap any amount of `asset1` to get the exact amount of `asset2`. + /// `amount_in_max` param allows to specify the max amount of the `asset1` + /// you're happy to provide. + /// + /// [`AssetConversionApi::quote_price_tokens_for_exact_tokens`] runtime call can be called + /// for a quote. + #[pallet::call_index(4)] + #[pallet::weight(T::WeightInfo::swap_tokens_for_exact_tokens(path.len() as u32))] + pub fn swap_tokens_for_exact_tokens( + origin: OriginFor, + path: Vec>, + amount_out: T::Balance, + amount_in_max: T::Balance, + send_to: T::AccountId, + keep_alive: bool, + ) -> DispatchResult { + let sender = ensure_signed(origin)?; + Self::do_swap_tokens_for_exact_tokens( + sender, + path.into_iter().map(|a| *a).collect(), + amount_out, + Some(amount_in_max), + send_to, + keep_alive, + )?; + Ok(()) + } + + /// Touch an existing pool to fulfill prerequisites before providing liquidity, such as + /// ensuring that the pool's accounts are in place. It is typically useful when a pool + /// creator removes the pool's accounts and does not provide a liquidity. This action may + /// involve holding assets from the caller as a deposit for creating the pool's accounts. + /// + /// The origin must be Signed. + /// + /// - `asset1`: The asset ID of an existing pool with a pair (asset1, asset2). + /// - `asset2`: The asset ID of an existing pool with a pair (asset1, asset2). + /// + /// Emits `Touched` event when successful. + #[pallet::call_index(5)] + #[pallet::weight(T::WeightInfo::touch(3))] + pub fn touch( + origin: OriginFor, + asset1: Box, + asset2: Box, + ) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + + let pool_id = T::PoolLocator::pool_id(&asset1, &asset2) + .map_err(|_| Error::::InvalidAssetPair)?; + let pool = Pools::::get(&pool_id).ok_or(Error::::PoolNotFound)?; + let pool_account = + T::PoolLocator::address(&pool_id).map_err(|_| Error::::InvalidAssetPair)?; + + let mut refunds_number: u32 = 0; + if T::Assets::should_touch(*asset1.clone(), &pool_account) { + T::Assets::touch(*asset1, &pool_account, &who)?; + refunds_number += 1; + } + if T::Assets::should_touch(*asset2.clone(), &pool_account) { + T::Assets::touch(*asset2, &pool_account, &who)?; + refunds_number += 1; + } + if T::PoolAssets::should_touch(pool.lp_token.clone(), &pool_account) { + T::PoolAssets::touch(pool.lp_token, &pool_account, &who)?; + refunds_number += 1; + } + Self::deposit_event(Event::Touched { pool_id, who }); + Ok(Some(T::WeightInfo::touch(refunds_number)).into()) + } + } + + impl Pallet { + /// Create a pool at genesis, bypassing the setup fee. + /// + /// The `lp_provider` must already hold sufficient balances of both assets. + /// If both `amount1` and `amount2` are non-zero, initial liquidity is added. + /// Returns the LP token amount minted to `lp_provider` (zero if no liquidity). + pub(crate) fn setup_pool_from_genesis( + asset1: &T::AssetKind, + asset2: &T::AssetKind, + lp_provider: &T::AccountId, + amount1: T::Balance, + amount2: T::Balance, + ) -> Result { + ensure!(asset1 != asset2, Error::::InvalidAssetPair); + + let pool_id = T::PoolLocator::pool_id(asset1, asset2) + .map_err(|_| Error::::InvalidAssetPair)?; + ensure!(!Pools::::contains_key(&pool_id), Error::::PoolExists); + + let pool_account = + T::PoolLocator::address(&pool_id).map_err(|_| Error::::InvalidAssetPair)?; + + // Allocate LP token ID. + let lp_token = NextPoolAssetId::::get() + .or(T::PoolAssetId::initial_value()) + .ok_or(Error::::IncorrectPoolAssetId)?; + let next_lp_token_id = lp_token.increment().ok_or(Error::::IncorrectPoolAssetId)?; + NextPoolAssetId::::set(Some(next_lp_token_id)); + + // Create LP token asset. + T::PoolAssets::create(lp_token.clone(), pool_account.clone(), false, 1u32.into())?; + + // Touch asset accounts for the pool account. + if T::Assets::should_touch(asset1.clone(), &pool_account) { + T::Assets::touch(asset1.clone(), &pool_account, lp_provider)?; + } + if T::Assets::should_touch(asset2.clone(), &pool_account) { + T::Assets::touch(asset2.clone(), &pool_account, lp_provider)?; + } + if T::PoolAssets::should_touch(lp_token.clone(), &pool_account) { + T::PoolAssets::touch(lp_token.clone(), &pool_account, lp_provider)?; + } + + // Register pool. + Pools::::insert(pool_id, PoolInfo { lp_token: lp_token.clone() }); + + // Add initial liquidity if amounts are non-zero. + if !amount1.is_zero() && !amount2.is_zero() { + T::Assets::transfer(asset1.clone(), lp_provider, &pool_account, amount1, Preserve)?; + T::Assets::transfer(asset2.clone(), lp_provider, &pool_account, amount2, Preserve)?; + + let lp_token_amount = Self::calc_lp_amount_for_zero_supply(&amount1, &amount2)?; + T::PoolAssets::mint_into( + lp_token.clone(), + &pool_account, + T::MintMinLiquidity::get(), + )?; + T::PoolAssets::mint_into(lp_token, lp_provider, lp_token_amount)?; + + Ok(lp_token_amount) + } else { + Ok(Zero::zero()) + } + } + + /// Create a new liquidity pool. + /// + /// **Warning**: The storage must be rolled back on error. + pub(crate) fn do_create_pool( + creator: &T::AccountId, + asset1: T::AssetKind, + asset2: T::AssetKind, + ) -> Result { + ensure!(asset1 != asset2, Error::::InvalidAssetPair); + + // prepare pool_id + let pool_id = T::PoolLocator::pool_id(&asset1, &asset2) + .map_err(|_| Error::::InvalidAssetPair)?; + ensure!(!Pools::::contains_key(&pool_id), Error::::PoolExists); + + let pool_account = + T::PoolLocator::address(&pool_id).map_err(|_| Error::::InvalidAssetPair)?; + + // pay the setup fee + let fee = + Self::withdraw(T::PoolSetupFeeAsset::get(), creator, T::PoolSetupFee::get(), true)?; + T::PoolSetupFeeTarget::on_unbalanced(fee); + + if T::Assets::should_touch(asset1.clone(), &pool_account) { + T::Assets::touch(asset1.clone(), &pool_account, creator)? + }; + + if T::Assets::should_touch(asset2.clone(), &pool_account) { + T::Assets::touch(asset2.clone(), &pool_account, creator)? + }; + + let lp_token = NextPoolAssetId::::get() + .or(T::PoolAssetId::initial_value()) + .ok_or(Error::::IncorrectPoolAssetId)?; + let next_lp_token_id = lp_token.increment().ok_or(Error::::IncorrectPoolAssetId)?; + NextPoolAssetId::::set(Some(next_lp_token_id)); + + T::PoolAssets::create(lp_token.clone(), pool_account.clone(), false, 1u32.into())?; + if T::PoolAssets::should_touch(lp_token.clone(), &pool_account) { + T::PoolAssets::touch(lp_token.clone(), &pool_account, creator)? + }; + + let pool_info = PoolInfo { lp_token: lp_token.clone() }; + Pools::::insert(pool_id.clone(), pool_info); + + Self::deposit_event(Event::PoolCreated { + creator: creator.clone(), + pool_id: pool_id.clone(), + pool_account, + lp_token, + }); + + Ok(pool_id) + } + + /// Add liquidity to a pool. + pub(crate) fn do_add_liquidity( + who: &T::AccountId, + asset1: T::AssetKind, + asset2: T::AssetKind, + amount1_desired: T::Balance, + amount2_desired: T::Balance, + amount1_min: T::Balance, + amount2_min: T::Balance, + mint_to: &T::AccountId, + ) -> Result { + let pool_id = T::PoolLocator::pool_id(&asset1, &asset2) + .map_err(|_| Error::::InvalidAssetPair)?; + + ensure!( + amount1_desired > Zero::zero() && amount2_desired > Zero::zero(), + Error::::WrongDesiredAmount + ); + + let pool = Pools::::get(&pool_id).ok_or(Error::::PoolNotFound)?; + let pool_account = + T::PoolLocator::address(&pool_id).map_err(|_| Error::::InvalidAssetPair)?; + + let reserve1 = Self::get_balance(&pool_account, asset1.clone()); + let reserve2 = Self::get_balance(&pool_account, asset2.clone()); + + let amount1: T::Balance; + let amount2: T::Balance; + if reserve1.is_zero() || reserve2.is_zero() { + amount1 = amount1_desired; + amount2 = amount2_desired; + } else { + let amount2_optimal = Self::quote(&amount1_desired, &reserve1, &reserve2)?; + + if amount2_optimal <= amount2_desired { + ensure!( + amount2_optimal >= amount2_min, + Error::::AssetTwoDepositDidNotMeetMinimum + ); + amount1 = amount1_desired; + amount2 = amount2_optimal; + } else { + let amount1_optimal = Self::quote(&amount2_desired, &reserve2, &reserve1)?; + ensure!( + amount1_optimal <= amount1_desired, + Error::::OptimalAmountLessThanDesired + ); + ensure!( + amount1_optimal >= amount1_min, + Error::::AssetOneDepositDidNotMeetMinimum + ); + amount1 = amount1_optimal; + amount2 = amount2_desired; + } + } + + ensure!( + amount1.saturating_add(reserve1) >= T::Assets::minimum_balance(asset1.clone()), + Error::::AmountOneLessThanMinimal + ); + ensure!( + amount2.saturating_add(reserve2) >= T::Assets::minimum_balance(asset2.clone()), + Error::::AmountTwoLessThanMinimal + ); + + T::Assets::transfer(asset1, who, &pool_account, amount1, Preserve)?; + T::Assets::transfer(asset2, who, &pool_account, amount2, Preserve)?; + + let total_supply = T::PoolAssets::total_issuance(pool.lp_token.clone()); + + let lp_token_amount: T::Balance; + if total_supply.is_zero() { + lp_token_amount = Self::calc_lp_amount_for_zero_supply(&amount1, &amount2)?; + T::PoolAssets::mint_into( + pool.lp_token.clone(), + &pool_account, + T::MintMinLiquidity::get(), + )?; + } else { + let side1 = Self::mul_div(&amount1, &total_supply, &reserve1)?; + let side2 = Self::mul_div(&amount2, &total_supply, &reserve2)?; + lp_token_amount = side1.min(side2); + } + + ensure!( + lp_token_amount > T::MintMinLiquidity::get(), + Error::::InsufficientLiquidityMinted + ); + + T::PoolAssets::mint_into(pool.lp_token.clone(), mint_to, lp_token_amount)?; + + Self::deposit_event(Event::LiquidityAdded { + who: who.clone(), + mint_to: mint_to.clone(), + pool_id, + amount1_provided: amount1, + amount2_provided: amount2, + lp_token: pool.lp_token, + lp_token_minted: lp_token_amount, + }); + + Ok(lp_token_amount) + } + + /// Remove liquidity from a pool. + pub(crate) fn do_remove_liquidity( + who: &T::AccountId, + asset1: T::AssetKind, + asset2: T::AssetKind, + lp_token_burn: T::Balance, + amount1_min_receive: T::Balance, + amount2_min_receive: T::Balance, + withdraw_to: &T::AccountId, + ) -> Result<(T::Balance, T::Balance), DispatchError> { + let pool_id = T::PoolLocator::pool_id(&asset1, &asset2) + .map_err(|_| Error::::InvalidAssetPair)?; + + ensure!(lp_token_burn > Zero::zero(), Error::::ZeroLiquidity); + + let pool = Pools::::get(&pool_id).ok_or(Error::::PoolNotFound)?; + + let pool_account = + T::PoolLocator::address(&pool_id).map_err(|_| Error::::InvalidAssetPair)?; + let (reserve1, reserve2) = Self::get_reserves(asset1.clone(), asset2.clone())?; + + let total_supply = T::PoolAssets::total_issuance(pool.lp_token.clone()); + let withdrawal_fee_amount = T::LiquidityWithdrawalFee::get() * lp_token_burn; + let lp_redeem_amount = lp_token_burn.saturating_sub(withdrawal_fee_amount); + + let amount1 = Self::mul_div(&lp_redeem_amount, &reserve1, &total_supply)?; + let amount2 = Self::mul_div(&lp_redeem_amount, &reserve2, &total_supply)?; + + ensure!( + !amount1.is_zero() && amount1 >= amount1_min_receive, + Error::::AssetOneWithdrawalDidNotMeetMinimum + ); + ensure!( + !amount2.is_zero() && amount2 >= amount2_min_receive, + Error::::AssetTwoWithdrawalDidNotMeetMinimum + ); + let reserve1_left = reserve1.saturating_sub(amount1); + let reserve2_left = reserve2.saturating_sub(amount2); + ensure!( + reserve1_left >= T::Assets::minimum_balance(asset1.clone()), + Error::::ReserveLeftLessThanMinimal + ); + ensure!( + reserve2_left >= T::Assets::minimum_balance(asset2.clone()), + Error::::ReserveLeftLessThanMinimal + ); + + // burn the provided lp token amount that includes the fee + T::PoolAssets::burn_from( + pool.lp_token.clone(), + who, + lp_token_burn, + Expendable, + Exact, + Polite, + )?; + + T::Assets::transfer(asset1, &pool_account, withdraw_to, amount1, Expendable)?; + T::Assets::transfer(asset2, &pool_account, withdraw_to, amount2, Expendable)?; + + Self::deposit_event(Event::LiquidityRemoved { + who: who.clone(), + withdraw_to: withdraw_to.clone(), + pool_id, + amount1, + amount2, + lp_token: pool.lp_token, + lp_token_burned: lp_token_burn, + withdrawal_fee: T::LiquidityWithdrawalFee::get(), + }); + + Ok((amount1, amount2)) + } + + /// Swap exactly `amount_in` of asset `path[0]` for asset `path[1]`. + /// If an `amount_out_min` is specified, it will return an error if it is unable to acquire + /// the amount desired. + /// + /// Withdraws the `path[0]` asset from `sender`, deposits the `path[1]` asset to `send_to`, + /// respecting `keep_alive`. + /// + /// If successful, returns the amount of `path[1]` acquired for the `amount_in`. + /// + /// WARNING: This may return an error after a partial storage mutation. It should be used + /// only inside a transactional storage context and an Err result must imply a storage + /// rollback. + pub(crate) fn do_swap_exact_tokens_for_tokens( + sender: T::AccountId, + path: Vec, + amount_in: T::Balance, + amount_out_min: Option, + send_to: T::AccountId, + keep_alive: bool, + ) -> Result { + ensure!(amount_in > Zero::zero(), Error::::ZeroAmount); + if let Some(amount_out_min) = amount_out_min { + ensure!(amount_out_min > Zero::zero(), Error::::ZeroAmount); + } + + Self::validate_swap_path(&path)?; + let path = Self::balance_path_from_amount_in(amount_in, path)?; + + let amount_out = path.last().map(|(_, a)| *a).ok_or(Error::::InvalidPath)?; + if let Some(amount_out_min) = amount_out_min { + ensure!( + amount_out >= amount_out_min, + Error::::ProvidedMinimumNotSufficientForSwap + ); + } + + Self::swap(&sender, &path, &send_to, keep_alive)?; + + Self::deposit_event(Event::SwapExecuted { + who: sender, + send_to, + amount_in, + amount_out, + path, + }); + Ok(amount_out) + } + + /// Take the `path[0]` asset and swap some amount for `amount_out` of the `path[1]`. If an + /// `amount_in_max` is specified, it will return an error if acquiring `amount_out` would be + /// too costly. + /// + /// Withdraws `path[0]` asset from `sender`, deposits the `path[1]` asset to `send_to`, + /// respecting `keep_alive`. + /// + /// If successful returns the amount of the `path[0]` taken to provide `path[1]`. + /// + /// WARNING: This may return an error after a partial storage mutation. It should be used + /// only inside a transactional storage context and an Err result must imply a storage + /// rollback. + pub(crate) fn do_swap_tokens_for_exact_tokens( + sender: T::AccountId, + path: Vec, + amount_out: T::Balance, + amount_in_max: Option, + send_to: T::AccountId, + keep_alive: bool, + ) -> Result { + ensure!(amount_out > Zero::zero(), Error::::ZeroAmount); + if let Some(amount_in_max) = amount_in_max { + ensure!(amount_in_max > Zero::zero(), Error::::ZeroAmount); + } + + Self::validate_swap_path(&path)?; + let path = Self::balance_path_from_amount_out(amount_out, path)?; + + let amount_in = path.first().map(|(_, a)| *a).ok_or(Error::::InvalidPath)?; + if let Some(amount_in_max) = amount_in_max { + ensure!( + amount_in <= amount_in_max, + Error::::ProvidedMaximumNotSufficientForSwap + ); + } + + Self::swap(&sender, &path, &send_to, keep_alive)?; + + Self::deposit_event(Event::SwapExecuted { + who: sender, + send_to, + amount_in, + amount_out, + path, + }); + + Ok(amount_in) + } + + /// Swap exactly `credit_in` of asset `path[0]` for asset `path[last]`. If `amount_out_min` + /// is provided and the swap can't achieve at least this amount, an error is returned. + /// + /// On a successful swap, the function returns the `credit_out` of `path[last]` obtained + /// from the `credit_in`. On failure, it returns an `Err` containing the original + /// `credit_in` and the associated error code. + /// + /// WARNING: This may return an error after a partial storage mutation. It should be used + /// only inside a transactional storage context and an Err result must imply a storage + /// rollback. + pub(crate) fn do_swap_exact_credit_tokens_for_tokens( + path: Vec, + credit_in: CreditOf, + amount_out_min: Option, + ) -> Result, (CreditOf, DispatchError)> { + let amount_in = credit_in.peek(); + let inspect_path = |credit_asset| { + ensure!( + path.first().map_or(false, |a| *a == credit_asset), + Error::::InvalidPath + ); + ensure!(!amount_in.is_zero(), Error::::ZeroAmount); + ensure!(amount_out_min.map_or(true, |a| !a.is_zero()), Error::::ZeroAmount); + + Self::validate_swap_path(&path)?; + let path = Self::balance_path_from_amount_in(amount_in, path)?; + + let amount_out = path.last().map(|(_, a)| *a).ok_or(Error::::InvalidPath)?; + ensure!( + amount_out_min.map_or(true, |a| amount_out >= a), + Error::::ProvidedMinimumNotSufficientForSwap + ); + Ok((path, amount_out)) + }; + let (path, amount_out) = match inspect_path(credit_in.asset()) { + Ok((p, a)) => (p, a), + Err(e) => return Err((credit_in, e)), + }; + + let credit_out = Self::credit_swap(credit_in, &path)?; + + Self::deposit_event(Event::SwapCreditExecuted { amount_in, amount_out, path }); + + Ok(credit_out) + } + + /// Swaps a portion of `credit_in` of `path[0]` asset to obtain the desired `amount_out` of + /// the `path[last]` asset. The provided `credit_in` must be adequate to achieve the target + /// `amount_out`, or an error will occur. + /// + /// On success, the function returns a (`credit_out`, `credit_change`) tuple, where + /// `credit_out` represents the acquired amount of the `path[last]` asset, and + /// `credit_change` is the remaining portion from the `credit_in`. On failure, an `Err` with + /// the initial `credit_in` and error code is returned. + /// + /// WARNING: This may return an error after a partial storage mutation. It should be used + /// only inside a transactional storage context and an Err result must imply a storage + /// rollback. + pub(crate) fn do_swap_credit_tokens_for_exact_tokens( + path: Vec, + credit_in: CreditOf, + amount_out: T::Balance, + ) -> Result<(CreditOf, CreditOf), (CreditOf, DispatchError)> { + let amount_in_max = credit_in.peek(); + let inspect_path = |credit_asset| { + ensure!( + path.first().map_or(false, |a| a == &credit_asset), + Error::::InvalidPath + ); + ensure!(amount_in_max > Zero::zero(), Error::::ZeroAmount); + ensure!(amount_out > Zero::zero(), Error::::ZeroAmount); + + Self::validate_swap_path(&path)?; + let path = Self::balance_path_from_amount_out(amount_out, path)?; + + let amount_in = path.first().map(|(_, a)| *a).ok_or(Error::::InvalidPath)?; + ensure!( + amount_in <= amount_in_max, + Error::::ProvidedMaximumNotSufficientForSwap + ); + + Ok((path, amount_in)) + }; + let (path, amount_in) = match inspect_path(credit_in.asset()) { + Ok((p, a)) => (p, a), + Err(e) => return Err((credit_in, e)), + }; + + let (credit_in, credit_change) = credit_in.split(amount_in); + let credit_out = Self::credit_swap(credit_in, &path)?; + + Self::deposit_event(Event::SwapCreditExecuted { amount_in, amount_out, path }); + + Ok((credit_out, credit_change)) + } + + /// Swap assets along the `path`, withdrawing from `sender` and depositing in `send_to`. + /// + /// Note: It's assumed that the provided `path` is valid. + /// + /// WARNING: This may return an error after a partial storage mutation. It should be used + /// only inside a transactional storage context and an Err result must imply a storage + /// rollback. + fn swap( + sender: &T::AccountId, + path: &BalancePath, + send_to: &T::AccountId, + keep_alive: bool, + ) -> Result<(), DispatchError> { + let (asset_in, amount_in) = path.first().ok_or(Error::::InvalidPath)?; + let credit_in = Self::withdraw(asset_in.clone(), sender, *amount_in, keep_alive)?; + + let credit_out = Self::credit_swap(credit_in, path).map_err(|(_, e)| e)?; + T::Assets::resolve(send_to, credit_out).map_err(|_| Error::::BelowMinimum)?; + + Ok(()) + } + + /// Swap assets along the specified `path`, consuming `credit_in` and producing + /// `credit_out`. + /// + /// If an error occurs, `credit_in` is returned back. + /// + /// Note: It's assumed that the provided `path` is valid and `credit_in` corresponds to the + /// first asset in the `path`. + /// + /// WARNING: This may return an error after a partial storage mutation. It should be used + /// only inside a transactional storage context and an Err result must imply a storage + /// rollback. + fn credit_swap( + credit_in: CreditOf, + path: &BalancePath, + ) -> Result, (CreditOf, DispatchError)> { + let resolve_path = || -> Result, DispatchError> { + for pos in 0..=path.len() { + if let Some([(asset1, _), (asset2, amount_out)]) = path.get(pos..=pos + 1) { + let pool_from = T::PoolLocator::pool_address(asset1, asset2) + .map_err(|_| Error::::InvalidAssetPair)?; + + if let Some((asset3, _)) = path.get(pos + 2) { + let pool_to = T::PoolLocator::pool_address(asset2, asset3) + .map_err(|_| Error::::InvalidAssetPair)?; + + T::Assets::transfer( + asset2.clone(), + &pool_from, + &pool_to, + *amount_out, + Preserve, + )?; + } else { + let credit_out = + Self::withdraw(asset2.clone(), &pool_from, *amount_out, true)?; + return Ok(credit_out); + } + } + } + Err(Error::::InvalidPath.into()) + }; + + let credit_out = match resolve_path() { + Ok(c) => c, + Err(e) => return Err((credit_in, e)), + }; + + let pool_to = if let Some([(asset1, _), (asset2, _)]) = path.get(0..2) { + match T::PoolLocator::pool_address(asset1, asset2) { + Ok(address) => address, + Err(_) => return Err((credit_in, Error::::InvalidAssetPair.into())), + } + } else { + return Err((credit_in, Error::::InvalidPath.into())); + }; + + T::Assets::resolve(&pool_to, credit_in) + .map_err(|c| (c, Error::::BelowMinimum.into()))?; + + Ok(credit_out) + } + + /// Removes `value` balance of `asset` from `who` account if possible. + fn withdraw( + asset: T::AssetKind, + who: &T::AccountId, + value: T::Balance, + keep_alive: bool, + ) -> Result, DispatchError> { + let preservation = match keep_alive { + true => Preserve, + false => Expendable, + }; + if preservation == Preserve { + // TODO drop the ensure! when this issue addressed + // https://github.com/paritytech/polkadot-sdk/issues/1698 + let free = T::Assets::reducible_balance(asset.clone(), who, preservation, Polite); + ensure!(free >= value, TokenError::NotExpendable); + } + T::Assets::withdraw(asset, who, value, Exact, preservation, Polite) + } + + /// Get the `owner`'s balance of `asset`, which could be the chain's native asset or another + /// fungible. Returns a value in the form of an `Balance`. + pub(crate) fn get_balance(owner: &T::AccountId, asset: T::AssetKind) -> T::Balance { + T::Assets::balance(asset, owner) + } + + /// Leading to an amount at the end of a `path`, get the required amounts in. + pub(crate) fn balance_path_from_amount_out( + amount_out: T::Balance, + path: Vec, + ) -> Result, DispatchError> { + let mut balance_path: BalancePath = Vec::with_capacity(path.len()); + let mut amount_in: T::Balance = amount_out; + + let mut iter = path.into_iter().rev().peekable(); + while let Some(asset2) = iter.next() { + let asset1 = match iter.peek() { + Some(a) => a, + None => { + balance_path.push((asset2, amount_in)); + break; + }, + }; + let (reserve_in, reserve_out) = Self::get_reserves(asset1.clone(), asset2.clone())?; + balance_path.push((asset2, amount_in)); + amount_in = Self::get_amount_in(&amount_in, &reserve_in, &reserve_out)?; + } + balance_path.reverse(); + + Ok(balance_path) + } + + /// Following an amount into a `path`, get the corresponding amounts out. + pub(crate) fn balance_path_from_amount_in( + amount_in: T::Balance, + path: Vec, + ) -> Result, DispatchError> { + let mut balance_path: BalancePath = Vec::with_capacity(path.len()); + let mut amount_out: T::Balance = amount_in; + + let mut iter = path.into_iter().peekable(); + while let Some(asset1) = iter.next() { + let asset2 = match iter.peek() { + Some(a) => a, + None => { + balance_path.push((asset1, amount_out)); + break; + }, + }; + let (reserve_in, reserve_out) = Self::get_reserves(asset1.clone(), asset2.clone())?; + balance_path.push((asset1, amount_out)); + amount_out = Self::get_amount_out(&amount_out, &reserve_in, &reserve_out)?; + } + Ok(balance_path) + } + + /// Calculates the optimal amount from the reserves. + pub fn quote( + amount: &T::Balance, + reserve1: &T::Balance, + reserve2: &T::Balance, + ) -> Result> { + // (amount * reserve2) / reserve1 + Self::mul_div(amount, reserve2, reserve1) + } + + pub(super) fn calc_lp_amount_for_zero_supply( + amount1: &T::Balance, + amount2: &T::Balance, + ) -> Result> { + let amount1 = T::HigherPrecisionBalance::from(*amount1); + let amount2 = T::HigherPrecisionBalance::from(*amount2); + + let result = amount1 + .checked_mul(&amount2) + .ok_or(Error::::Overflow)? + .integer_sqrt() + .checked_sub(&T::MintMinLiquidity::get().into()) + .ok_or(Error::::InsufficientLiquidityMinted)?; + + result.try_into().map_err(|_| Error::::Overflow) + } + + fn mul_div(a: &T::Balance, b: &T::Balance, c: &T::Balance) -> Result> { + let a = T::HigherPrecisionBalance::from(*a); + let b = T::HigherPrecisionBalance::from(*b); + let c = T::HigherPrecisionBalance::from(*c); + + let result = a + .checked_mul(&b) + .ok_or(Error::::Overflow)? + .checked_div(&c) + .ok_or(Error::::Overflow)?; + + result.try_into().map_err(|_| Error::::Overflow) + } + + /// Calculates amount out. + /// + /// Given an input amount of an asset and pair reserves, returns the maximum output amount + /// of the other asset. + pub fn get_amount_out( + amount_in: &T::Balance, + reserve_in: &T::Balance, + reserve_out: &T::Balance, + ) -> Result> { + let amount_in = T::HigherPrecisionBalance::from(*amount_in); + let reserve_in = T::HigherPrecisionBalance::from(*reserve_in); + let reserve_out = T::HigherPrecisionBalance::from(*reserve_out); + + if reserve_in.is_zero() || reserve_out.is_zero() { + return Err(Error::::ZeroLiquidity); + } + + let fee_complement = T::LPFee::get().left_from_one().deconstruct(); + let amount_in_with_fee = amount_in + .checked_mul(&T::HigherPrecisionBalance::from(fee_complement)) + .ok_or(Error::::Overflow)?; + + let numerator = + amount_in_with_fee.checked_mul(&reserve_out).ok_or(Error::::Overflow)?; + + let denominator = reserve_in + .checked_mul(&T::HigherPrecisionBalance::from(Permill::ACCURACY)) + .ok_or(Error::::Overflow)? + .checked_add(&amount_in_with_fee) + .ok_or(Error::::Overflow)?; + + let result = numerator.checked_div(&denominator).ok_or(Error::::Overflow)?; + + result.try_into().map_err(|_| Error::::Overflow) + } + + /// Calculates amount in. + /// + /// Given an output amount of an asset and pair reserves, returns a required input amount + /// of the other asset. + pub fn get_amount_in( + amount_out: &T::Balance, + reserve_in: &T::Balance, + reserve_out: &T::Balance, + ) -> Result> { + let amount_out = T::HigherPrecisionBalance::from(*amount_out); + let reserve_in = T::HigherPrecisionBalance::from(*reserve_in); + let reserve_out = T::HigherPrecisionBalance::from(*reserve_out); + + if reserve_in.is_zero() || reserve_out.is_zero() { + Err(Error::::ZeroLiquidity)? + } + + if amount_out >= reserve_out { + Err(Error::::AmountOutTooHigh)? + } + + let fee_complement = T::LPFee::get().left_from_one().deconstruct(); + let numerator = reserve_in + .checked_mul(&amount_out) + .ok_or(Error::::Overflow)? + .checked_mul(&T::HigherPrecisionBalance::from(Permill::ACCURACY)) + .ok_or(Error::::Overflow)?; + + let denominator = reserve_out + .checked_sub(&amount_out) + .ok_or(Error::::Overflow)? + .checked_mul(&T::HigherPrecisionBalance::from(fee_complement)) + .ok_or(Error::::Overflow)?; + + let result = numerator + .checked_div(&denominator) + .ok_or(Error::::Overflow)? + .checked_add(&One::one()) + .ok_or(Error::::Overflow)?; + + result.try_into().map_err(|_| Error::::Overflow) + } + + /// Ensure that a path is valid. + fn validate_swap_path(path: &Vec) -> Result<(), DispatchError> { + ensure!(path.len() >= 2, Error::::InvalidPath); + ensure!(path.len() as u32 <= T::MaxSwapPathLength::get(), Error::::InvalidPath); + + // validate all the pools in the path are unique + let mut pools = BTreeSet::::new(); + for assets_pair in path.windows(2) { + if let [asset1, asset2] = assets_pair { + let pool_id = T::PoolLocator::pool_id(asset1, asset2) + .map_err(|_| Error::::InvalidAssetPair)?; + + let new_element = pools.insert(pool_id); + if !new_element { + return Err(Error::::NonUniquePath.into()); + } + } + } + Ok(()) + } + + /// Returns the next pool asset id for benchmark purposes only. + #[cfg(any(test, feature = "runtime-benchmarks"))] + pub fn get_next_pool_asset_id() -> T::PoolAssetId { + NextPoolAssetId::::get() + .or(T::PoolAssetId::initial_value()) + .expect("Next pool asset ID can not be None") + } + } + + #[pallet::view_functions] + impl Pallet { + /// Returns the balance of each asset in the pool. + /// The tuple result is in the order requested (not necessarily the same as pool order). + pub fn get_reserves( + asset1: T::AssetKind, + asset2: T::AssetKind, + ) -> Result<(T::Balance, T::Balance), Error> { + let pool_account = T::PoolLocator::pool_address(&asset1, &asset2) + .map_err(|_| Error::::InvalidAssetPair)?; + + let balance1 = Self::get_balance(&pool_account, asset1); + let balance2 = Self::get_balance(&pool_account, asset2); + + if balance1.is_zero() || balance2.is_zero() { + Err(Error::::PoolEmpty)?; + } + + Ok((balance1, balance2)) + } + + /// Gets a quote for swapping an exact amount of `asset1` for `asset2`. + /// + /// If `include_fee` is true, the quote will include the liquidity provider fee. + /// If the pool does not exist or has no liquidity, `None` is returned. + /// Note that the price may have changed by the time the transaction is executed. + /// (Use `amount_out_min` to control slippage.) + /// Returns `Some(quoted_amount)` on success. + pub fn quote_price_exact_tokens_for_tokens( + asset1: T::AssetKind, + asset2: T::AssetKind, + amount: T::Balance, + include_fee: bool, + ) -> Option { + // Swaps reject zero amounts, match that behavior. + if amount.is_zero() { + return None; + } + + let pool_account = T::PoolLocator::pool_address(&asset1, &asset2).ok()?; + + let (balance1, balance2) = Self::get_reserves(asset1.clone(), asset2.clone()).ok()?; + + if balance1.is_zero() { + return None; + } + + let amount_out = if include_fee { + Self::get_amount_out(&amount, &balance1, &balance2).ok()? + } else { + Self::quote(&amount, &balance1, &balance2).ok()? + }; + + // Small inputs can round output to zero due to integer division. + if amount_out.is_zero() { + return None; + } + + // Swap withdrawals from pools use `keep_alive=true` (Preserve). Use the same + // preservation level to determine the actual withdrawable amount. + let max_output = T::Assets::reducible_balance(asset2, &pool_account, Preserve, Polite); + if amount_out > max_output { + return None; + } + + Some(amount_out) + } + + /// Gets a quote for swapping `amount` of `asset1` for an exact amount of `asset2`. + /// + /// If `include_fee` is true, the quote will include the liquidity provider fee. + /// If the pool does not exist or has no liquidity, `None` is returned. + /// Note that the price may have changed by the time the transaction is executed. + /// (Use `amount_in_max` to control slippage.) + /// Returns `Some(quoted_amount)` on success. + pub fn quote_price_tokens_for_exact_tokens( + asset1: T::AssetKind, + asset2: T::AssetKind, + amount: T::Balance, + include_fee: bool, + ) -> Option { + // Swaps reject zero amounts, match that behavior. + if amount.is_zero() { + return None; + } + let pool_account = T::PoolLocator::pool_address(&asset1, &asset2).ok()?; + + let (balance1, balance2) = Self::get_reserves(asset1.clone(), asset2.clone()).ok()?; + + if balance1.is_zero() { + return None; + } + + // Swap withdrawals from pools use `keep_alive=true` (Preserve). Use the same + // preservation level to determine the actual withdrawable amount. + let max_output = T::Assets::reducible_balance(asset2, &pool_account, Preserve, Polite); + if amount > max_output { + return None; + } + + if include_fee { + Self::get_amount_in(&amount, &balance1, &balance2).ok() + } else { + Self::quote(&amount, &balance2, &balance1).ok() + } + } + } +} + +sp_api::decl_runtime_apis! { + /// This runtime api allows people to query the size of the liquidity pools + /// and quote prices for swaps. + pub trait AssetConversionApi + where + Balance: frame_support::traits::tokens::Balance + MaybeDisplay, + AssetId: Codec, + { + /// Provides a quote for [`Pallet::swap_tokens_for_exact_tokens`]. + /// + /// Note that the price may have changed by the time the transaction is executed. + /// (Use `amount_in_max` to control slippage.) + fn quote_price_tokens_for_exact_tokens( + asset1: AssetId, + asset2: AssetId, + amount: Balance, + include_fee: bool, + ) -> Option; + + /// Provides a quote for [`Pallet::swap_exact_tokens_for_tokens`]. + /// + /// Note that the price may have changed by the time the transaction is executed. + /// (Use `amount_out_min` to control slippage.) + fn quote_price_exact_tokens_for_tokens( + asset1: AssetId, + asset2: AssetId, + amount: Balance, + include_fee: bool, + ) -> Option; + + /// Returns the size of the liquidity pool for the given asset pair. + fn get_reserves(asset1: AssetId, asset2: AssetId) -> Option<(Balance, Balance)>; + } +} + +sp_core::generate_feature_enabled_macro!(runtime_benchmarks_enabled, feature = "runtime-benchmarks", $); diff --git a/template/vendor/pallet-asset-conversion/src/liquidity.rs b/template/vendor/pallet-asset-conversion/src/liquidity.rs new file mode 100644 index 00000000..06131fe7 --- /dev/null +++ b/template/vendor/pallet-asset-conversion/src/liquidity.rs @@ -0,0 +1,138 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Trait for providing methods to mutate liquidity pools. + +use frame_support::{traits::tokens::Balance, transactional}; +use sp_runtime::DispatchError; + +use crate::{Config, Pallet}; + +/// A struct to represent an asset and its desired and minimum amounts for adding liquidity. +pub struct AddLiquidityAsset { + /// The kind of asset. + pub asset: AssetKind, + /// The desired amount of the asset to add. + pub amount_desired: Balance, + /// The minimum amount of the asset to add. + pub amount_min: Balance, +} + +/// Trait for providing methods to mutate liquidity pools. This includes creating pools, +/// adding liquidity, and removing liquidity. +pub trait MutateLiquidity { + /// The balance type for assets. + type Balance: Balance; + /// The type used to identify assets. + type AssetKind; + /// The type used to identify a liquidity pool. + type PoolId; + + /// Creates a new liquidity pool for the given assets. + /// + /// Mints LP tokens to the `creator` account. + /// + /// Returns the ID of the newly created pool. + fn create_pool( + creator: &AccountId, + asset1: Self::AssetKind, + asset2: Self::AssetKind, + ) -> Result; + + /// Adds liquidity to an existing pool. + /// + /// Mints LP tokens to the `mint_to` account. + /// + /// Returns the amount of LP tokens minted. + fn add_liquidity( + who: &AccountId, + asset1: AddLiquidityAsset, + asset2: AddLiquidityAsset, + mint_to: &AccountId, + ) -> Result; + + /// Removes liquidity from a pool. + /// + /// Burns LP tokens from the `who` account and transfers the withdrawn assets to the + /// `withdraw_to` account. + /// + /// Returns the amounts of assets withdrawn. + fn remove_liquidity( + who: &AccountId, + asset1: Self::AssetKind, + asset2: Self::AssetKind, + lp_token_burn: Self::Balance, + amount1_min_receive: Self::Balance, + amount2_min_receive: Self::Balance, + withdraw_to: &AccountId, + ) -> Result<(Self::Balance, Self::Balance), DispatchError>; +} + +impl MutateLiquidity for Pallet { + type Balance = T::Balance; + type AssetKind = T::AssetKind; + type PoolId = T::PoolId; + + #[transactional] + fn create_pool( + creator: &T::AccountId, + asset1: T::AssetKind, + asset2: T::AssetKind, + ) -> Result { + Self::do_create_pool(creator, asset1, asset2) + } + + #[transactional] + fn add_liquidity( + who: &T::AccountId, + asset1: AddLiquidityAsset, + asset2: AddLiquidityAsset, + mint_to: &T::AccountId, + ) -> Result { + Self::do_add_liquidity( + who, + asset1.asset, + asset2.asset, + asset1.amount_desired, + asset2.amount_desired, + asset1.amount_min, + asset2.amount_min, + mint_to, + ) + } + + #[transactional] + fn remove_liquidity( + who: &T::AccountId, + asset1: T::AssetKind, + asset2: T::AssetKind, + lp_token_burn: T::Balance, + amount1_min_receive: T::Balance, + amount2_min_receive: T::Balance, + withdraw_to: &T::AccountId, + ) -> Result<(T::Balance, T::Balance), DispatchError> { + Self::do_remove_liquidity( + who, + asset1, + asset2, + lp_token_burn, + amount1_min_receive, + amount2_min_receive, + withdraw_to, + ) + } +} diff --git a/template/vendor/pallet-asset-conversion/src/mock.rs b/template/vendor/pallet-asset-conversion/src/mock.rs new file mode 100644 index 00000000..a297225d --- /dev/null +++ b/template/vendor/pallet-asset-conversion/src/mock.rs @@ -0,0 +1,178 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Test environment for Asset Conversion pallet. + +use super::*; +use crate as pallet_asset_conversion; +use core::default::Default; +use frame_support::{ + construct_runtime, derive_impl, + instances::{Instance1, Instance2}, + ord_parameter_types, parameter_types, + traits::{ + tokens::{ + fungible::{NativeFromLeft, NativeOrWithId, UnionOf}, + imbalance::ResolveAssetTo, + }, + AsEnsureOriginWithArg, ConstU128, ConstU32, + }, + PalletId, +}; +use frame_system::{EnsureSigned, EnsureSignedBy}; +use sp_arithmetic::Permill; +use sp_runtime::{ + traits::{AccountIdConversion, IdentityLookup}, + BuildStorage, +}; + +type Block = frame_system::mocking::MockBlock; + +construct_runtime!( + pub enum Test + { + System: frame_system, + Balances: pallet_balances, + Assets: pallet_assets::, + PoolAssets: pallet_assets::, + AssetConversion: pallet_asset_conversion, + } +); + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig)] +impl frame_system::Config for Test { + type AccountId = u128; + type Lookup = IdentityLookup; + type Block = Block; + type AccountData = pallet_balances::AccountData; +} + +#[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)] +impl pallet_balances::Config for Test { + type Balance = u128; + type ExistentialDeposit = ConstU128<100>; + type AccountStore = System; +} + +impl pallet_assets::Config for Test { + type RuntimeEvent = RuntimeEvent; + type Balance = u128; + type RemoveItemsLimit = ConstU32<1000>; + type AssetId = u32; + type AssetIdParameter = u32; + type ReserveData = (); + type Currency = Balances; + type CreateOrigin = AsEnsureOriginWithArg>; + type ForceOrigin = frame_system::EnsureRoot; + type AssetDeposit = ConstU128<1>; + type AssetAccountDeposit = ConstU128<10>; + type MetadataDepositBase = ConstU128<1>; + type MetadataDepositPerByte = ConstU128<1>; + type ApprovalDeposit = ConstU128<1>; + type StringLimit = ConstU32<50>; + type Holder = (); + type Freezer = (); + type Extra = (); + type WeightInfo = (); + type CallbackHandle = (); + pallet_assets::runtime_benchmarks_enabled! { + type BenchmarkHelper = (); + } +} + +impl pallet_assets::Config for Test { + type RuntimeEvent = RuntimeEvent; + type Balance = u128; + type RemoveItemsLimit = ConstU32<1000>; + type AssetId = u32; + type AssetIdParameter = u32; + type ReserveData = (); + type Currency = Balances; + type CreateOrigin = + AsEnsureOriginWithArg>; + type ForceOrigin = frame_system::EnsureRoot; + type AssetDeposit = ConstU128<0>; + type AssetAccountDeposit = ConstU128<0>; + type MetadataDepositBase = ConstU128<0>; + type MetadataDepositPerByte = ConstU128<0>; + type ApprovalDeposit = ConstU128<0>; + type StringLimit = ConstU32<50>; + type Holder = (); + type Freezer = (); + type Extra = (); + type WeightInfo = (); + type CallbackHandle = (); + pallet_assets::runtime_benchmarks_enabled! { + type BenchmarkHelper = (); + } +} + +parameter_types! { + pub const AssetConversionPalletId: PalletId = PalletId(*b"py/ascon"); + pub const Native: NativeOrWithId = NativeOrWithId::Native; + pub LpFee: Permill = Permill::from_rational(3u32, 1_000u32); // 0.3% + pub storage LiquidityWithdrawalFee: Permill = Permill::from_percent(0); +} + +ord_parameter_types! { + pub const AssetConversionOrigin: u128 = AccountIdConversion::::into_account_truncating(&AssetConversionPalletId::get()); +} + +pub type NativeAndAssets = UnionOf, u128>; +pub type PoolIdToAccountId = + AccountIdConverter, NativeOrWithId)>; +pub type AscendingLocator = Ascending, PoolIdToAccountId>; +pub type WithFirstAssetLocator = + WithFirstAsset, PoolIdToAccountId>; + +impl Config for Test { + type RuntimeEvent = RuntimeEvent; + type Balance = ::Balance; + type HigherPrecisionBalance = sp_core::U256; + type AssetKind = NativeOrWithId; + type Assets = NativeAndAssets; + type PoolId = (Self::AssetKind, Self::AssetKind); + type PoolLocator = Chain; + type PoolAssetId = u32; + type PoolAssets = PoolAssets; + type PoolSetupFee = ConstU128<100>; // should be more or equal to the existential deposit + type PoolSetupFeeAsset = Native; + type PoolSetupFeeTarget = ResolveAssetTo; + type PalletId = AssetConversionPalletId; + type WeightInfo = (); + type LPFee = LpFee; + type LiquidityWithdrawalFee = LiquidityWithdrawalFee; + type MaxSwapPathLength = ConstU32<4>; + type MintMinLiquidity = ConstU128<100>; // 100 is good enough when the main currency has 12 decimals. + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = (); +} + +pub(crate) fn new_test_ext() -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); + + pallet_balances::GenesisConfig:: { + balances: vec![(1, 10000), (2, 20000), (3, 30000), (4, 40000)], + ..Default::default() + } + .assimilate_storage(&mut t) + .unwrap(); + + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext +} diff --git a/template/vendor/pallet-asset-conversion/src/swap.rs b/template/vendor/pallet-asset-conversion/src/swap.rs new file mode 100644 index 00000000..8067859d --- /dev/null +++ b/template/vendor/pallet-asset-conversion/src/swap.rs @@ -0,0 +1,261 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Traits and implementations for swap between the various asset classes. + +use super::*; +use frame_support::{storage::with_transaction, transactional}; + +/// Trait for providing methods to swap between the various asset classes. +pub trait Swap { + /// Measure units of the asset classes for swapping. + type Balance: Balance; + /// Kind of assets that are going to be swapped. + type AssetKind; + + /// Returns the upper limit on the length of the swap path. + fn max_path_len() -> u32; + + /// Swap exactly `amount_in` of asset `path[0]` for asset `path[last]`. + /// If an `amount_out_min` is specified, it will return an error if it is unable to acquire + /// the amount desired. + /// + /// Withdraws the `path[0]` asset from `sender`, deposits the `path[last]` asset to `send_to`, + /// respecting `keep_alive`. + /// + /// If successful, returns the amount of `path[last]` acquired for the `amount_in`. + /// + /// This operation is expected to be atomic. + fn swap_exact_tokens_for_tokens( + sender: AccountId, + path: Vec, + amount_in: Self::Balance, + amount_out_min: Option, + send_to: AccountId, + keep_alive: bool, + ) -> Result; + + /// Take the `path[0]` asset and swap some amount for `amount_out` of the `path[last]`. If an + /// `amount_in_max` is specified, it will return an error if acquiring `amount_out` would be + /// too costly. + /// + /// Withdraws `path[0]` asset from `sender`, deposits `path[last]` asset to `send_to`, + /// respecting `keep_alive`. + /// + /// If successful returns the amount of the `path[0]` taken to provide `path[last]`. + /// + /// This operation is expected to be atomic. + fn swap_tokens_for_exact_tokens( + sender: AccountId, + path: Vec, + amount_out: Self::Balance, + amount_in_max: Option, + send_to: AccountId, + keep_alive: bool, + ) -> Result; +} + +/// Trait providing methods to swap between the various asset classes. +pub trait SwapCredit { + /// Measure units of the asset classes for swapping. + type Balance: Balance; + /// Kind of assets that are going to be swapped. + type AssetKind; + /// Credit implying a negative imbalance in the system that can be placed into an account or + /// alter the total supply. + type Credit; + + /// Returns the upper limit on the length of the swap path. + fn max_path_len() -> u32; + + /// Swap exactly `credit_in` of asset `path[0]` for asset `path[last]`. If `amount_out_min` is + /// provided and the swap can't achieve at least this amount, an error is returned. + /// + /// On a successful swap, the function returns the `credit_out` of `path[last]` obtained from + /// the `credit_in`. On failure, it returns an `Err` containing the original `credit_in` and the + /// associated error code. + /// + /// This operation is expected to be atomic. + fn swap_exact_tokens_for_tokens( + path: Vec, + credit_in: Self::Credit, + amount_out_min: Option, + ) -> Result; + + /// Swaps a portion of `credit_in` of `path[0]` asset to obtain the desired `amount_out` of + /// the `path[last]` asset. The provided `credit_in` must be adequate to achieve the target + /// `amount_out`, or an error will occur. + /// + /// On success, the function returns a (`credit_out`, `credit_change`) tuple, where `credit_out` + /// represents the acquired amount of the `path[last]` asset, and `credit_change` is the + /// remaining portion from the `credit_in`. On failure, an `Err` with the initial `credit_in` + /// and error code is returned. + /// + /// This operation is expected to be atomic. + fn swap_tokens_for_exact_tokens( + path: Vec, + credit_in: Self::Credit, + amount_out: Self::Balance, + ) -> Result<(Self::Credit, Self::Credit), (Self::Credit, DispatchError)>; +} + +/// Trait providing methods to quote swap prices between asset classes. +/// +/// The quoted price is only guaranteed if no other swaps are made after the price is quoted and +/// before the target swap (e.g., the swap is made immediately within the same transaction). +pub trait QuotePrice { + /// Measurement units of the asset classes for pricing. + type Balance: Balance; + /// Type representing the kind of assets for which the price is being quoted. + type AssetKind; + /// Quotes the amount of `asset1` required to obtain the exact `amount` of `asset2`. + /// + /// If `include_fee` is set to `true`, the price will include the pool's fee. + /// If the pool does not exist or the swap cannot be made, `None` is returned. + fn quote_price_tokens_for_exact_tokens( + asset1: Self::AssetKind, + asset2: Self::AssetKind, + amount: Self::Balance, + include_fee: bool, + ) -> Option; + /// Quotes the amount of `asset2` resulting from swapping the exact `amount` of `asset1`. + /// + /// If `include_fee` is set to `true`, the price will include the pool's fee. + /// If the pool does not exist or the swap cannot be made, `None` is returned. + fn quote_price_exact_tokens_for_tokens( + asset1: Self::AssetKind, + asset2: Self::AssetKind, + amount: Self::Balance, + include_fee: bool, + ) -> Option; +} + +impl Swap for Pallet { + type Balance = T::Balance; + type AssetKind = T::AssetKind; + + fn max_path_len() -> u32 { + T::MaxSwapPathLength::get() + } + + #[transactional] + fn swap_exact_tokens_for_tokens( + sender: T::AccountId, + path: Vec, + amount_in: Self::Balance, + amount_out_min: Option, + send_to: T::AccountId, + keep_alive: bool, + ) -> Result { + Self::do_swap_exact_tokens_for_tokens( + sender, + path, + amount_in, + amount_out_min, + send_to, + keep_alive, + ) + } + + #[transactional] + fn swap_tokens_for_exact_tokens( + sender: T::AccountId, + path: Vec, + amount_out: Self::Balance, + amount_in_max: Option, + send_to: T::AccountId, + keep_alive: bool, + ) -> Result { + Self::do_swap_tokens_for_exact_tokens( + sender, + path, + amount_out, + amount_in_max, + send_to, + keep_alive, + ) + } +} + +impl SwapCredit for Pallet { + type Balance = T::Balance; + type AssetKind = T::AssetKind; + type Credit = CreditOf; + + fn max_path_len() -> u32 { + T::MaxSwapPathLength::get() + } + + fn swap_exact_tokens_for_tokens( + path: Vec, + credit_in: Self::Credit, + amount_out_min: Option, + ) -> Result { + let credit_asset = credit_in.asset(); + with_transaction(|| -> TransactionOutcome> { + let res = Self::do_swap_exact_credit_tokens_for_tokens(path, credit_in, amount_out_min); + match &res { + Ok(_) => TransactionOutcome::Commit(Ok(res)), + // wrapping `res` with `Ok`, since our `Err` doesn't satisfy the + // `From` bound of the `with_transaction` function. + Err(_) => TransactionOutcome::Rollback(Ok(res)), + } + }) + // should never map an error since `with_transaction` above never returns it. + .map_err(|_| (Self::Credit::zero(credit_asset), DispatchError::Corruption))? + } + + fn swap_tokens_for_exact_tokens( + path: Vec, + credit_in: Self::Credit, + amount_out: Self::Balance, + ) -> Result<(Self::Credit, Self::Credit), (Self::Credit, DispatchError)> { + let credit_asset = credit_in.asset(); + with_transaction(|| -> TransactionOutcome> { + let res = Self::do_swap_credit_tokens_for_exact_tokens(path, credit_in, amount_out); + match &res { + Ok(_) => TransactionOutcome::Commit(Ok(res)), + // wrapping `res` with `Ok`, since our `Err` doesn't satisfy the + // `From` bound of the `with_transaction` function. + Err(_) => TransactionOutcome::Rollback(Ok(res)), + } + }) + // should never map an error since `with_transaction` above never returns it. + .map_err(|_| (Self::Credit::zero(credit_asset), DispatchError::Corruption))? + } +} + +impl QuotePrice for Pallet { + type Balance = T::Balance; + type AssetKind = T::AssetKind; + fn quote_price_exact_tokens_for_tokens( + asset1: Self::AssetKind, + asset2: Self::AssetKind, + amount: Self::Balance, + include_fee: bool, + ) -> Option { + Self::quote_price_exact_tokens_for_tokens(asset1, asset2, amount, include_fee) + } + fn quote_price_tokens_for_exact_tokens( + asset1: Self::AssetKind, + asset2: Self::AssetKind, + amount: Self::Balance, + include_fee: bool, + ) -> Option { + Self::quote_price_tokens_for_exact_tokens(asset1, asset2, amount, include_fee) + } +} diff --git a/template/vendor/pallet-asset-conversion/src/tests.rs b/template/vendor/pallet-asset-conversion/src/tests.rs new file mode 100644 index 00000000..f5e9ddbd --- /dev/null +++ b/template/vendor/pallet-asset-conversion/src/tests.rs @@ -0,0 +1,2824 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::{mock::*, *}; +use frame_support::{ + assert_noop, assert_ok, assert_storage_noop, + instances::Instance1, + traits::{ + fungible, + fungible::{Inspect as FungibleInspect, NativeOrWithId}, + fungibles, + fungibles::{Inspect, InspectEnumerable}, + Get, + }, +}; +use sp_arithmetic::Permill; +use sp_runtime::{BuildStorage, DispatchError, TokenError}; + +fn events() -> Vec> { + let result = System::events() + .into_iter() + .map(|r| r.event) + .filter_map(|e| { + if let mock::RuntimeEvent::AssetConversion(inner) = e { + Some(inner) + } else { + None + } + }) + .collect(); + + System::reset_events(); + + result +} + +fn pools() -> Vec<::PoolId> { + let mut s: Vec<_> = Pools::::iter().map(|x| x.0).collect(); + s.sort(); + s +} + +fn assets() -> Vec> { + let mut s: Vec<_> = Assets::asset_ids().map(|id| NativeOrWithId::WithId(id)).collect(); + s.sort(); + s +} + +fn pool_assets() -> Vec { + let mut s: Vec<_> = <::PoolAssets>::asset_ids().collect(); + s.sort(); + s +} + +fn create_tokens(owner: u128, tokens: Vec>) { + create_tokens_with_ed(owner, tokens, 1) +} + +fn create_tokens_with_ed(owner: u128, tokens: Vec>, ed: u128) { + for token_id in tokens { + let asset_id = match token_id { + NativeOrWithId::WithId(id) => id, + _ => unreachable!("invalid token"), + }; + assert_ok!(Assets::force_create(RuntimeOrigin::root(), asset_id, owner, false, ed)); + } +} + +fn balance(owner: u128, token_id: NativeOrWithId) -> u128 { + <::Assets>::balance(token_id, &owner) +} + +fn pool_balance(owner: u128, token_id: u32) -> u128 { + <::PoolAssets>::balance(token_id, owner) +} + +fn get_native_ed() -> u128 { + <::Assets>::minimum_balance(NativeOrWithId::Native) +} + +macro_rules! bvec { + ($($x:expr),+ $(,)?) => ( + vec![$( Box::new( $x ), )*] + ) +} + +#[test] +fn validate_with_first_asset_pool_id_locator() { + new_test_ext().execute_with(|| { + use NativeOrWithId::{Native, WithId}; + assert_eq!(WithFirstAssetLocator::pool_id(&Native, &WithId(2)), Ok((Native, WithId(2)))); + assert_eq!(WithFirstAssetLocator::pool_id(&WithId(2), &Native), Ok((Native, WithId(2)))); + assert_noop!(WithFirstAssetLocator::pool_id(&Native, &Native), ()); + assert_noop!(WithFirstAssetLocator::pool_id(&WithId(2), &WithId(1)), ()); + }); +} + +#[test] +fn validate_ascending_pool_id_locator() { + new_test_ext().execute_with(|| { + use NativeOrWithId::{Native, WithId}; + assert_eq!(AscendingLocator::pool_id(&Native, &WithId(2)), Ok((Native, WithId(2)))); + assert_eq!(AscendingLocator::pool_id(&WithId(2), &Native), Ok((Native, WithId(2)))); + assert_eq!(AscendingLocator::pool_id(&WithId(2), &WithId(1)), Ok((WithId(1), WithId(2)))); + assert_eq!(AscendingLocator::pool_id(&Native, &Native), Err(())); + assert_eq!(AscendingLocator::pool_id(&WithId(1), &WithId(1)), Err(())); + }); +} + +#[test] +fn validate_native_or_with_id_sorting() { + new_test_ext().execute_with(|| { + use NativeOrWithId::{Native, WithId}; + assert!(WithId(2) > WithId(1)); + assert!(WithId(1) <= WithId(1)); + assert_eq!(WithId(1), WithId(1)); + assert_eq!(Native::, Native::); + assert!(Native < WithId(1)); + }); +} + +#[test] +fn check_pool_accounts_dont_collide() { + use std::collections::HashSet; + let mut map = HashSet::new(); + + for i in 0..1_000_000u32 { + let account: u128 = ::PoolLocator::address(&( + NativeOrWithId::Native, + NativeOrWithId::WithId(i), + )) + .unwrap(); + if map.contains(&account) { + panic!("Collision at {}", i); + } + map.insert(account); + } +} + +#[test] +fn check_max_numbers() { + new_test_ext().execute_with(|| { + assert_eq!(AssetConversion::quote(&3u128, &u128::MAX, &u128::MAX).ok().unwrap(), 3); + assert!(AssetConversion::quote(&u128::MAX, &3u128, &u128::MAX).is_err()); + assert_eq!(AssetConversion::quote(&u128::MAX, &u128::MAX, &1u128).ok().unwrap(), 1); + + assert_eq!( + AssetConversion::get_amount_out(&100u128, &u128::MAX, &u128::MAX).ok().unwrap(), + 99 + ); + assert_eq!( + AssetConversion::get_amount_in(&100u128, &u128::MAX, &u128::MAX).ok().unwrap(), + 101 + ); + }); +} + +#[test] +fn can_create_pool() { + new_test_ext().execute_with(|| { + let asset_account_deposit: u128 = + >::AssetAccountDeposit::get(); + let user = 1; + let token_1 = NativeOrWithId::Native; + let token_2 = NativeOrWithId::WithId(2); + let pool_id = (token_1.clone(), token_2.clone()); + + create_tokens(user, vec![token_2.clone()]); + + let lp_token = AssetConversion::get_next_pool_asset_id(); + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user, 1000)); + assert_ok!(AssetConversion::create_pool( + RuntimeOrigin::signed(user), + Box::new(token_2.clone()), + Box::new(token_1.clone()) + )); + + let setup_fee = <::PoolSetupFee as Get<::Balance>>::get(); + let pool_account = AssetConversionOrigin::get(); + assert_eq!( + balance(user, NativeOrWithId::Native), + 1000 - (setup_fee + asset_account_deposit) + ); + assert_eq!(balance(pool_account, NativeOrWithId::Native), setup_fee); + assert_eq!(lp_token + 1, AssetConversion::get_next_pool_asset_id()); + + assert_eq!( + events(), + [Event::::PoolCreated { + creator: user, + pool_id: pool_id.clone(), + pool_account: ::PoolLocator::address(&pool_id).unwrap(), + lp_token + }] + ); + assert_eq!(pools(), vec![pool_id]); + assert_eq!(assets(), vec![token_2.clone()]); + assert_eq!(pool_assets(), vec![lp_token]); + + assert_noop!( + AssetConversion::create_pool( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_1.clone()) + ), + Error::::InvalidAssetPair + ); + assert_noop!( + AssetConversion::create_pool( + RuntimeOrigin::signed(user), + Box::new(token_2.clone()), + Box::new(token_2.clone()) + ), + Error::::InvalidAssetPair + ); + + // validate we cannot create WithId(1)/WithId(2) pool + let token_1 = NativeOrWithId::WithId(1); + create_tokens(user, vec![token_1.clone()]); + assert_ok!(AssetConversion::create_pool( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_2.clone()) + )); + }); +} + +#[test] +fn create_same_pool_twice_should_fail() { + new_test_ext().execute_with(|| { + let user = 1; + let token_1 = NativeOrWithId::Native; + let token_2 = NativeOrWithId::WithId(2); + + create_tokens(user, vec![token_2.clone()]); + + let lp_token = AssetConversion::get_next_pool_asset_id(); + assert_ok!(AssetConversion::create_pool( + RuntimeOrigin::signed(user), + Box::new(token_2.clone()), + Box::new(token_1.clone()) + )); + let expected_free = lp_token + 1; + assert_eq!(expected_free, AssetConversion::get_next_pool_asset_id()); + + assert_noop!( + AssetConversion::create_pool( + RuntimeOrigin::signed(user), + Box::new(token_2.clone()), + Box::new(token_1.clone()) + ), + Error::::PoolExists + ); + assert_eq!(expected_free, AssetConversion::get_next_pool_asset_id()); + + // Try switching the same tokens around: + assert_noop!( + AssetConversion::create_pool( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_2.clone()) + ), + Error::::PoolExists + ); + assert_eq!(expected_free, AssetConversion::get_next_pool_asset_id()); + }); +} + +#[test] +fn different_pools_should_have_different_lp_tokens() { + new_test_ext().execute_with(|| { + let user = 1; + let token_1 = NativeOrWithId::Native; + let token_2 = NativeOrWithId::WithId(2); + let token_3 = NativeOrWithId::WithId(3); + let pool_id_1_2 = (token_1.clone(), token_2.clone()); + let pool_id_1_3 = (token_1.clone(), token_3.clone()); + + create_tokens(user, vec![token_2.clone(), token_3.clone()]); + + let lp_token2_1 = AssetConversion::get_next_pool_asset_id(); + assert_ok!(AssetConversion::create_pool( + RuntimeOrigin::signed(user), + Box::new(token_2.clone()), + Box::new(token_1.clone()) + )); + let lp_token3_1 = AssetConversion::get_next_pool_asset_id(); + + assert_eq!( + events(), + [Event::::PoolCreated { + creator: user, + pool_id: pool_id_1_2.clone(), + pool_account: ::PoolLocator::address(&pool_id_1_2).unwrap(), + lp_token: lp_token2_1 + }] + ); + + assert_ok!(AssetConversion::create_pool( + RuntimeOrigin::signed(user), + Box::new(token_3.clone()), + Box::new(token_1.clone()) + )); + assert_eq!( + events(), + [Event::::PoolCreated { + creator: user, + pool_id: pool_id_1_3.clone(), + pool_account: ::PoolLocator::address(&pool_id_1_3).unwrap(), + lp_token: lp_token3_1, + }] + ); + + assert_ne!(lp_token2_1, lp_token3_1); + }); +} + +#[test] +fn can_add_liquidity() { + new_test_ext().execute_with(|| { + let user = 1; + let token_1 = NativeOrWithId::Native; + let token_2 = NativeOrWithId::WithId(2); + let token_3 = NativeOrWithId::WithId(3); + + create_tokens(user, vec![token_2.clone(), token_3.clone()]); + let lp_token1 = AssetConversion::get_next_pool_asset_id(); + assert_ok!(AssetConversion::create_pool( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_2.clone()) + )); + let lp_token2 = AssetConversion::get_next_pool_asset_id(); + assert_ok!(AssetConversion::create_pool( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_3.clone()) + )); + + let ed = get_native_ed(); + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user, 10000 * 2 + ed)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, user, 1000)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 3, user, 1000)); + + assert_ok!(AssetConversion::add_liquidity( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_2.clone()), + 10000, + 10, + 10000, + 10, + user, + )); + + let pool_id = (token_1.clone(), token_2.clone()); + assert!(events().contains(&Event::::LiquidityAdded { + who: user, + mint_to: user, + pool_id: pool_id.clone(), + amount1_provided: 10000, + amount2_provided: 10, + lp_token: lp_token1, + lp_token_minted: 216, + })); + let pallet_account = ::PoolLocator::address(&pool_id).unwrap(); + assert_eq!(balance(pallet_account, token_1.clone()), 10000); + assert_eq!(balance(pallet_account, token_2.clone()), 10); + assert_eq!(balance(user, token_1.clone()), 10000 + ed); + assert_eq!(balance(user, token_2.clone()), 1000 - 10); + assert_eq!(pool_balance(user, lp_token1), 216); + + // try to pass the non-native - native assets, the result should be the same + assert_ok!(AssetConversion::add_liquidity( + RuntimeOrigin::signed(user), + Box::new(token_3.clone()), + Box::new(token_1.clone()), + 10, + 10000, + 10, + 10000, + user, + )); + + let pool_id = (token_1.clone(), token_3.clone()); + assert!(events().contains(&Event::::LiquidityAdded { + who: user, + mint_to: user, + pool_id: pool_id.clone(), + amount1_provided: 10, + amount2_provided: 10000, + lp_token: lp_token2, + lp_token_minted: 216, + })); + let pallet_account = ::PoolLocator::address(&pool_id).unwrap(); + assert_eq!(balance(pallet_account, token_1.clone()), 10000); + assert_eq!(balance(pallet_account, token_3.clone()), 10); + assert_eq!(balance(user, token_1.clone()), ed); + assert_eq!(balance(user, token_3.clone()), 1000 - 10); + assert_eq!(pool_balance(user, lp_token2), 216); + }); +} + +#[test] +fn add_tiny_liquidity_leads_to_insufficient_liquidity_minted_error() { + new_test_ext().execute_with(|| { + let user = 1; + let token_1 = NativeOrWithId::Native; + let token_2 = NativeOrWithId::WithId(2); + + create_tokens(user, vec![token_2.clone()]); + assert_ok!(AssetConversion::create_pool( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_2.clone()) + )); + + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user, 1000)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, user, 1000)); + + assert_noop!( + AssetConversion::add_liquidity( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_2.clone()), + 1, + 1, + 1, + 1, + user + ), + Error::::AmountOneLessThanMinimal + ); + + assert_noop!( + AssetConversion::add_liquidity( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_2.clone()), + get_native_ed(), + 1, + 1, + 1, + user + ), + Error::::InsufficientLiquidityMinted + ); + }); +} + +#[test] +fn add_tiny_liquidity_directly_to_pool_address() { + new_test_ext().execute_with(|| { + let user = 1; + let token_1 = NativeOrWithId::Native; + let token_2 = NativeOrWithId::WithId(2); + let token_3 = NativeOrWithId::WithId(3); + + create_tokens(user, vec![token_2.clone(), token_3.clone()]); + assert_ok!(AssetConversion::create_pool( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_2.clone()) + )); + assert_ok!(AssetConversion::create_pool( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_3.clone()) + )); + + let ed = get_native_ed(); + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user, 10000 * 2 + ed)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, user, 1000)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 3, user, 1000)); + + // check we're still able to add the liquidity even when the pool already has some + // token_1.clone() + let pallet_account = + ::PoolLocator::address(&(token_1.clone(), token_2.clone())).unwrap(); + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), pallet_account, 1000)); + + assert_ok!(AssetConversion::add_liquidity( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_2.clone()), + 10000, + 10, + 10000, + 10, + user, + )); + + // check the same but for token_3.clone() (non-native token) + assert_ok!(AssetConversion::add_liquidity( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_3.clone()), + 10000, + 10, + 10000, + 10, + user, + )); + }); +} + +#[test] +fn can_remove_liquidity() { + new_test_ext().execute_with(|| { + let user = 1; + let token_1 = NativeOrWithId::Native; + let token_2 = NativeOrWithId::WithId(2); + let pool_id = (token_1.clone(), token_2.clone()); + + create_tokens(user, vec![token_2.clone()]); + let lp_token = AssetConversion::get_next_pool_asset_id(); + assert_ok!(AssetConversion::create_pool( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_2.clone()) + )); + + let ed_token_1 = >::minimum_balance(); + let ed_token_2 = >::minimum_balance(2); + assert_ok!(Balances::force_set_balance( + RuntimeOrigin::root(), + user, + 10000000000 + ed_token_1 + )); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, user, 100000 + ed_token_2)); + + assert_ok!(AssetConversion::add_liquidity( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_2.clone()), + 1000000000, + 100000, + 1000000000, + 100000, + user, + )); + + let total_lp_received = pool_balance(user, lp_token); + LiquidityWithdrawalFee::set(&Permill::from_percent(10)); + + assert_ok!(AssetConversion::remove_liquidity( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_2.clone()), + total_lp_received, + 0, + 0, + user, + )); + + assert!(events().contains(&Event::::LiquidityRemoved { + who: user, + withdraw_to: user, + pool_id: pool_id.clone(), + amount1: 899991000, + amount2: 89999, + lp_token, + lp_token_burned: total_lp_received, + withdrawal_fee: ::LiquidityWithdrawalFee::get() + })); + + let pool_account = ::PoolLocator::address(&pool_id).unwrap(); + assert_eq!(balance(pool_account, token_1.clone()), 100009000); + assert_eq!(balance(pool_account, token_2.clone()), 10001); + assert_eq!(pool_balance(pool_account, lp_token), 100); + + assert_eq!( + balance(user, token_1.clone()), + 10000000000 - 1000000000 + 899991000 + ed_token_1 + ); + assert_eq!(balance(user, token_2.clone()), 89999 + ed_token_2); + assert_eq!(pool_balance(user, lp_token), 0); + }); +} + +#[test] +fn can_not_redeem_more_lp_tokens_than_were_minted() { + new_test_ext().execute_with(|| { + let user = 1; + let token_1 = NativeOrWithId::Native; + let token_2 = NativeOrWithId::WithId(2); + let lp_token = AssetConversion::get_next_pool_asset_id(); + + create_tokens(user, vec![token_2.clone()]); + assert_ok!(AssetConversion::create_pool( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_2.clone()) + )); + + assert_ok!(Balances::force_set_balance( + RuntimeOrigin::root(), + user, + 10000 + get_native_ed() + )); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, user, 1000)); + + assert_ok!(AssetConversion::add_liquidity( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_2.clone()), + 10000, + 10, + 10000, + 10, + user, + )); + + // Only 216 lp_tokens_minted + assert_eq!(pool_balance(user, lp_token), 216); + + assert_noop!( + AssetConversion::remove_liquidity( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_2.clone()), + 216 + 1, // Try and redeem 10 lp tokens while only 9 minted. + 0, + 0, + user, + ), + DispatchError::Token(TokenError::FundsUnavailable) + ); + }); +} + +#[test] +fn quote_price_returns_none_for_zero_amount() { + new_test_ext().execute_with(|| { + let user = 1; + let token_1 = NativeOrWithId::Native; + let token_2 = NativeOrWithId::WithId(2); + + create_tokens(user, vec![token_2.clone()]); + assert_ok!(AssetConversion::create_pool( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_2.clone()) + )); + + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user, 100000)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, user, 1000)); + + assert_ok!(AssetConversion::add_liquidity( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_2.clone()), + 10000, + 200, + 1, + 1, + user, + )); + + assert_eq!( + AssetConversion::quote_price_exact_tokens_for_tokens( + token_1.clone(), + token_2.clone(), + 0, + true, + ), + None + ); + assert_eq!( + AssetConversion::quote_price_tokens_for_exact_tokens( + token_1.clone(), + token_2.clone(), + 0, + true, + ), + None + ); + }); +} + +#[test] +fn quote_price_returns_none_for_zero_output() { + new_test_ext().execute_with(|| { + let user = 1; + let token_1 = NativeOrWithId::Native; + let token_2 = NativeOrWithId::WithId(2); + + create_tokens(user, vec![token_2.clone()]); + assert_ok!(AssetConversion::create_pool( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_2.clone()) + )); + + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user, 10_000_000)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, user, 1000)); + + // Create a heavily skewed pool: lots of asset1, very little asset2. + assert_ok!(AssetConversion::add_liquidity( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_2.clone()), + 1_000_000, + 200, + 1, + 1, + user, + )); + + // Tiny input into a skewed pool rounds output to zero. + // get_amount_out(1, 1_000_000, 200) = 1*997*200 / (1_000_000*1000 + 997) = 0 + assert_eq!( + AssetConversion::quote_price_exact_tokens_for_tokens( + token_1.clone(), + token_2.clone(), + 1, + true, + ), + None + ); + // Without fees: quote(1, 1_000_000, 200) = 1*200/1_000_000 = 0 + assert_eq!( + AssetConversion::quote_price_exact_tokens_for_tokens( + token_1.clone(), + token_2.clone(), + 1, + false, + ), + None + ); + }); +} + +#[test] +fn can_quote_price() { + new_test_ext().execute_with(|| { + let user = 1; + let token_1 = NativeOrWithId::Native; + let token_2 = NativeOrWithId::WithId(2); + + create_tokens(user, vec![token_2.clone()]); + assert_ok!(AssetConversion::create_pool( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_2.clone()) + )); + + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user, 100000)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, user, 1000)); + + assert_ok!(AssetConversion::add_liquidity( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_2.clone()), + 10000, + 200, + 1, + 1, + user, + )); + + assert_eq!( + AssetConversion::quote_price_exact_tokens_for_tokens( + NativeOrWithId::Native, + NativeOrWithId::WithId(2), + 3000, + false, + ), + Some(60) + ); + // including fee so should get less out... + assert_eq!( + AssetConversion::quote_price_exact_tokens_for_tokens( + NativeOrWithId::Native, + NativeOrWithId::WithId(2), + 3000, + true, + ), + Some(46) + ); + // Check it still gives same price: + // (if the above accidentally exchanged then it would not give same quote as before) + assert_eq!( + AssetConversion::quote_price_exact_tokens_for_tokens( + NativeOrWithId::Native, + NativeOrWithId::WithId(2), + 3000, + false, + ), + Some(60) + ); + // including fee so should get less out... + assert_eq!( + AssetConversion::quote_price_exact_tokens_for_tokens( + NativeOrWithId::Native, + NativeOrWithId::WithId(2), + 3000, + true, + ), + Some(46) + ); + + // Check inverse: + assert_eq!( + AssetConversion::quote_price_exact_tokens_for_tokens( + NativeOrWithId::WithId(2), + NativeOrWithId::Native, + 60, + false, + ), + Some(3000) + ); + // including fee so should get less out... + assert_eq!( + AssetConversion::quote_price_exact_tokens_for_tokens( + NativeOrWithId::WithId(2), + NativeOrWithId::Native, + 60, + true, + ), + Some(2302) + ); + + // same tests as above but for quote_price_tokens_for_exact_tokens: + // + assert_eq!( + AssetConversion::quote_price_tokens_for_exact_tokens( + NativeOrWithId::Native, + NativeOrWithId::WithId(2), + 60, + false, + ), + Some(3000) + ); + // including fee so should need to put more in... + assert_eq!( + AssetConversion::quote_price_tokens_for_exact_tokens( + NativeOrWithId::Native, + NativeOrWithId::WithId(2), + 60, + true, + ), + Some(4299) + ); + // Check it still gives same price: + // (if the above accidentally exchanged then it would not give same quote as before) + assert_eq!( + AssetConversion::quote_price_tokens_for_exact_tokens( + NativeOrWithId::Native, + NativeOrWithId::WithId(2), + 60, + false, + ), + Some(3000) + ); + // including fee so should need to put more in... + assert_eq!( + AssetConversion::quote_price_tokens_for_exact_tokens( + NativeOrWithId::Native, + NativeOrWithId::WithId(2), + 60, + true, + ), + Some(4299) + ); + + // Check inverse: + assert_eq!( + AssetConversion::quote_price_tokens_for_exact_tokens( + NativeOrWithId::WithId(2), + NativeOrWithId::Native, + 3000, + false, + ), + Some(60) + ); + // including fee so should need to put more in... + assert_eq!( + AssetConversion::quote_price_tokens_for_exact_tokens( + NativeOrWithId::WithId(2), + NativeOrWithId::Native, + 3000, + true, + ), + Some(86) + ); + + // roundtrip: Without fees one should get the original number + // + let amount_in = 100; + + assert_eq!( + AssetConversion::quote_price_exact_tokens_for_tokens( + NativeOrWithId::WithId(2), + NativeOrWithId::Native, + amount_in, + false, + ) + .and_then(|amount| AssetConversion::quote_price_exact_tokens_for_tokens( + NativeOrWithId::Native, + NativeOrWithId::WithId(2), + amount, + false, + )), + Some(amount_in) + ); + assert_eq!( + AssetConversion::quote_price_exact_tokens_for_tokens( + NativeOrWithId::Native, + NativeOrWithId::WithId(2), + amount_in, + false, + ) + .and_then(|amount| AssetConversion::quote_price_exact_tokens_for_tokens( + NativeOrWithId::WithId(2), + NativeOrWithId::Native, + amount, + false, + )), + Some(amount_in) + ); + + assert_eq!( + AssetConversion::quote_price_tokens_for_exact_tokens( + NativeOrWithId::WithId(2), + NativeOrWithId::Native, + amount_in, + false, + ) + .and_then(|amount| AssetConversion::quote_price_tokens_for_exact_tokens( + NativeOrWithId::Native, + NativeOrWithId::WithId(2), + amount, + false, + )), + Some(amount_in) + ); + assert_eq!( + AssetConversion::quote_price_tokens_for_exact_tokens( + NativeOrWithId::Native, + NativeOrWithId::WithId(2), + amount_in, + false, + ) + .and_then(|amount| AssetConversion::quote_price_tokens_for_exact_tokens( + NativeOrWithId::WithId(2), + NativeOrWithId::Native, + amount, + false, + )), + Some(amount_in) + ); + }); +} + +#[test] +fn quote_price_exact_tokens_for_tokens_matches_execution() { + new_test_ext().execute_with(|| { + let user = 1; + let user2 = 2; + let token_1 = NativeOrWithId::Native; + let token_2 = NativeOrWithId::WithId(2); + + create_tokens(user, vec![token_2.clone()]); + assert_ok!(AssetConversion::create_pool( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_2.clone()) + )); + + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user, 100000)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, user, 1000)); + + assert_ok!(AssetConversion::add_liquidity( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_2.clone()), + 10000, + 200, + 1, + 1, + user, + )); + + let amount = 1; + let quoted_price = 49; + assert_eq!( + AssetConversion::quote_price_exact_tokens_for_tokens( + token_2.clone(), + token_1.clone(), + amount, + true, + ), + Some(quoted_price) + ); + + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, user2, amount)); + let prior_dot_balance = 20000; + assert_eq!(prior_dot_balance, balance(user2, token_1.clone())); + assert_ok!(AssetConversion::swap_exact_tokens_for_tokens( + RuntimeOrigin::signed(user2), + bvec![token_2.clone(), token_1.clone()], + amount, + 1, + user2, + false, + )); + + assert_eq!(prior_dot_balance + quoted_price, balance(user2, token_1.clone())); + }); +} + +#[test] +fn quote_price_tokens_for_exact_tokens_matches_execution() { + new_test_ext().execute_with(|| { + let user = 1; + let user2 = 2; + let token_1 = NativeOrWithId::Native; + let token_2 = NativeOrWithId::WithId(2); + + create_tokens(user, vec![token_2.clone()]); + assert_ok!(AssetConversion::create_pool( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_2.clone()) + )); + + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user, 100000)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, user, 1000)); + + assert_ok!(AssetConversion::add_liquidity( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_2.clone()), + 10000, + 200, + 1, + 1, + user, + )); + + let amount = 49; + let quoted_price = 1; + assert_eq!( + AssetConversion::quote_price_tokens_for_exact_tokens( + token_2.clone(), + token_1.clone(), + amount, + true, + ), + Some(quoted_price) + ); + + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, user2, amount)); + let prior_dot_balance = 20000; + assert_eq!(prior_dot_balance, balance(user2, token_1.clone())); + let prior_asset_balance = 49; + assert_eq!(prior_asset_balance, balance(user2, token_2.clone())); + assert_ok!(AssetConversion::swap_tokens_for_exact_tokens( + RuntimeOrigin::signed(user2), + bvec![token_2.clone(), token_1.clone()], + amount, + 1, + user2, + false, + )); + + assert_eq!(prior_dot_balance + amount, balance(user2, token_1.clone())); + assert_eq!(prior_asset_balance - quoted_price, balance(user2, token_2.clone())); + }); +} + +#[test] +fn quote_price_returns_none_when_output_exceeds_pool_withdrawable() { + new_test_ext().execute_with(|| { + let user = 1; + let token_1 = NativeOrWithId::Native; + let token_2 = NativeOrWithId::WithId(2); + let ed = 1000; + + create_tokens_with_ed(user, vec![token_2.clone()], ed); + assert_ok!(AssetConversion::create_pool( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_2.clone()) + )); + + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user, 10_000_000)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, user, 10_000)); + + assert_ok!(AssetConversion::add_liquidity( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_2.clone()), + 1_000_000, + 2_000, + 1, + 1, + user, + )); + // Pool has 1_000_000 native and 2_000 of asset2 (min_balance=1000). + // Pool can only send out up to 2_000 - 1_000 = 1_000 of asset2 while staying alive. + + // quote_price_tokens_for_exact_tokens: requesting exact output. + // Requesting 1001 of asset2 exceeds available (1000), must return None. + assert_eq!( + AssetConversion::quote_price_tokens_for_exact_tokens( + token_1.clone(), + token_2.clone(), + 1001, + true, + ), + None + ); + // Also without fees. + assert_eq!( + AssetConversion::quote_price_tokens_for_exact_tokens( + token_1.clone(), + token_2.clone(), + 1001, + false, + ), + None + ); + // Requesting exactly 1000 should succeed (at the boundary). + assert!(AssetConversion::quote_price_tokens_for_exact_tokens( + token_1.clone(), + token_2.clone(), + 1000, + true, + ) + .is_some()); + + // quote_price_exact_tokens_for_tokens: given input, computed output must fit. + // With reserves (1_000_000, 2_000), fee=0.3%, and max_output=1000: + // input=1_005_018, output=1001 > 1000, must return None. + assert_eq!( + AssetConversion::quote_price_exact_tokens_for_tokens( + token_1.clone(), + token_2.clone(), + 1_005_018, + true, + ), + None + ); + // Also without fees. + assert_eq!( + AssetConversion::quote_price_exact_tokens_for_tokens( + token_1.clone(), + token_2.clone(), + 1_005_018, + false, + ), + None + ); + // input=1_005_017, output=1000 ≤ 1000, must return Some. + assert!(AssetConversion::quote_price_exact_tokens_for_tokens( + token_1.clone(), + token_2.clone(), + 1_005_017, + true, + ) + .is_some()); + }); +} + +#[test] +fn quote_price_returns_none_swap_fails() { + new_test_ext().execute_with(|| { + let user = 1; + let token_1 = NativeOrWithId::Native; + let token_2 = NativeOrWithId::WithId(2); + let ed = 1000; + + create_tokens_with_ed(user, vec![token_2.clone()], ed); + assert_ok!(AssetConversion::create_pool( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_2.clone()) + )); + + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user, 10_000_000)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, user, 10_000)); + + assert_ok!(AssetConversion::add_liquidity( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_2.clone()), + 1_000_000, + 2_000, + 1, + 1, + user, + )); + + // The AMM formula can compute a price for 1001 output (it's below reserve of 2000), + // but the pool can only deliver 1000 (2000 - 1000 min_balance). + // Verify the raw AMM math would produce a result... + let (reserve_in, reserve_out) = + AssetConversion::get_reserves(token_1.clone(), token_2.clone()).unwrap(); + assert!(Pallet::::get_amount_in(&1001, &reserve_in, &reserve_out).is_ok()); + // ...but the quote correctly returns None. + assert_eq!( + AssetConversion::quote_price_tokens_for_exact_tokens( + token_1.clone(), + token_2.clone(), + 1001, + true, + ), + None + ); + + // And verify the actual swap would indeed fail. + let swapper = 3; + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), swapper, 10_000_000)); + assert_noop!( + AssetConversion::swap_tokens_for_exact_tokens( + RuntimeOrigin::signed(swapper), + bvec![token_1.clone(), token_2.clone()], + 1001, + 10_000_000, + swapper, + false, + ), + TokenError::NotExpendable + ); + }); +} + +#[test] +fn can_swap_with_native() { + new_test_ext().execute_with(|| { + let user = 1; + let token_1 = NativeOrWithId::Native; + let token_2 = NativeOrWithId::WithId(2); + let pool_id = (token_1.clone(), token_2.clone()); + + create_tokens(user, vec![token_2.clone()]); + assert_ok!(AssetConversion::create_pool( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_2.clone()) + )); + + let ed = get_native_ed(); + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user, 10000 + ed)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, user, 1000)); + + let liquidity1 = 10000; + let liquidity2 = 200; + + assert_ok!(AssetConversion::add_liquidity( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_2.clone()), + liquidity1, + liquidity2, + 1, + 1, + user, + )); + + let input_amount = 100; + let expect_receive = + AssetConversion::get_amount_out(&input_amount, &liquidity2, &liquidity1) + .ok() + .unwrap(); + + assert_ok!(AssetConversion::swap_exact_tokens_for_tokens( + RuntimeOrigin::signed(user), + bvec![token_2.clone(), token_1.clone()], + input_amount, + 1, + user, + false, + )); + + let pallet_account = ::PoolLocator::address(&pool_id).unwrap(); + assert_eq!(balance(user, token_1.clone()), expect_receive + ed); + assert_eq!(balance(user, token_2.clone()), 1000 - liquidity2 - input_amount); + assert_eq!(balance(pallet_account, token_1.clone()), liquidity1 - expect_receive); + assert_eq!(balance(pallet_account, token_2.clone()), liquidity2 + input_amount); + }); +} + +#[test] +fn can_swap_with_realistic_values() { + new_test_ext().execute_with(|| { + let user = 1; + let dot = NativeOrWithId::Native; + let usd = NativeOrWithId::WithId(2); + create_tokens(user, vec![usd.clone()]); + assert_ok!(AssetConversion::create_pool( + RuntimeOrigin::signed(user), + Box::new(dot.clone()), + Box::new(usd.clone()) + )); + + const UNIT: u128 = 1_000_000_000; + + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user, 300_000 * UNIT)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, user, 1_100_000 * UNIT)); + + let liquidity_dot = 200_000 * UNIT; // ratio for a 5$ price + let liquidity_usd = 1_000_000 * UNIT; + assert_ok!(AssetConversion::add_liquidity( + RuntimeOrigin::signed(user), + Box::new(dot.clone()), + Box::new(usd.clone()), + liquidity_dot, + liquidity_usd, + 1, + 1, + user, + )); + + let input_amount = 10 * UNIT; // usd + + assert_ok!(AssetConversion::swap_exact_tokens_for_tokens( + RuntimeOrigin::signed(user), + bvec![usd.clone(), dot.clone()], + input_amount, + 1, + user, + false, + )); + + assert!(events().contains(&Event::::SwapExecuted { + who: user, + send_to: user, + amount_in: 10 * UNIT, // usd + amount_out: 1_993_980_120, // About 2 dot after div by UNIT. + path: vec![(usd, 10 * UNIT), (dot, 1_993_980_120)], + })); + }); +} + +#[test] +fn can_not_swap_in_pool_with_no_liquidity_added_yet() { + new_test_ext().execute_with(|| { + let user = 1; + let token_1 = NativeOrWithId::Native; + let token_2 = NativeOrWithId::WithId(2); + + create_tokens(user, vec![token_2.clone()]); + assert_ok!(AssetConversion::create_pool( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_2.clone()) + )); + + // Check can't swap an empty pool + assert_noop!( + AssetConversion::swap_exact_tokens_for_tokens( + RuntimeOrigin::signed(user), + bvec![token_2.clone(), token_1.clone()], + 10, + 1, + user, + false, + ), + Error::::PoolEmpty + ); + }); +} + +#[test] +fn check_no_panic_when_try_swap_close_to_empty_pool() { + new_test_ext().execute_with(|| { + let user = 1; + let token_1 = NativeOrWithId::Native; + let token_2 = NativeOrWithId::WithId(2); + let pool_id = (token_1.clone(), token_2.clone()); + let lp_token = AssetConversion::get_next_pool_asset_id(); + + create_tokens(user, vec![token_2.clone()]); + assert_ok!(AssetConversion::create_pool( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_2.clone()) + )); + + let ed = get_native_ed(); + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user, 10000 + ed)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, user, 1000)); + + let liquidity1 = 10000; + let liquidity2 = 200; + + assert_ok!(AssetConversion::add_liquidity( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_2.clone()), + liquidity1, + liquidity2, + 1, + 1, + user, + )); + + let lp_token_minted = pool_balance(user, lp_token); + assert!(events().contains(&Event::::LiquidityAdded { + who: user, + mint_to: user, + pool_id: pool_id.clone(), + amount1_provided: liquidity1, + amount2_provided: liquidity2, + lp_token, + lp_token_minted, + })); + + let pallet_account = ::PoolLocator::address(&pool_id).unwrap(); + assert_eq!(balance(pallet_account, token_1.clone()), liquidity1); + assert_eq!(balance(pallet_account, token_2.clone()), liquidity2); + + assert_ok!(AssetConversion::remove_liquidity( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_2.clone()), + lp_token_minted, + 1, + 1, + user, + )); + + // Now, the pool should exist but be almost empty. + // Let's try and drain it. + assert_eq!(balance(pallet_account, token_1.clone()), 708); + assert_eq!(balance(pallet_account, token_2.clone()), 15); + + // validate the reserve should always stay above the ED + assert_noop!( + AssetConversion::swap_tokens_for_exact_tokens( + RuntimeOrigin::signed(user), + bvec![token_2.clone(), token_1.clone()], + 708 - ed + 1, // amount_out + 500, // amount_in_max + user, + false, + ), + TokenError::NotExpendable, + ); + + assert_ok!(AssetConversion::swap_tokens_for_exact_tokens( + RuntimeOrigin::signed(user), + bvec![token_2.clone(), token_1.clone()], + 608, // amount_out + 500, // amount_in_max + user, + false, + )); + + let token_1_left = balance(pallet_account, token_1.clone()); + let token_2_left = balance(pallet_account, token_2.clone()); + assert_eq!(token_1_left, 708 - 608); + + // The price for the last tokens should be very high + assert_eq!( + AssetConversion::get_amount_in(&(token_1_left - 1), &token_2_left, &token_1_left) + .ok() + .unwrap(), + 10625 + ); + + assert_noop!( + AssetConversion::swap_tokens_for_exact_tokens( + RuntimeOrigin::signed(user), + bvec![token_2.clone(), token_1.clone()], + token_1_left - 1, // amount_out + 1000, // amount_in_max + user, + false, + ), + Error::::ProvidedMaximumNotSufficientForSwap + ); + + // Try to swap what's left in the pool + assert_noop!( + AssetConversion::swap_tokens_for_exact_tokens( + RuntimeOrigin::signed(user), + bvec![token_2.clone(), token_1.clone()], + token_1_left, // amount_out + 1000, // amount_in_max + user, + false, + ), + Error::::AmountOutTooHigh + ); + }); +} + +#[test] +fn swap_should_not_work_if_too_much_slippage() { + new_test_ext().execute_with(|| { + let user = 1; + let token_1 = NativeOrWithId::Native; + let token_2 = NativeOrWithId::WithId(2); + + create_tokens(user, vec![token_2.clone()]); + assert_ok!(AssetConversion::create_pool( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_2.clone()) + )); + + assert_ok!(Balances::force_set_balance( + RuntimeOrigin::root(), + user, + 10000 + get_native_ed() + )); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, user, 1000)); + + let liquidity1 = 10000; + let liquidity2 = 200; + + assert_ok!(AssetConversion::add_liquidity( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_2.clone()), + liquidity1, + liquidity2, + 1, + 1, + user, + )); + + let exchange_amount = 100; + + assert_noop!( + AssetConversion::swap_exact_tokens_for_tokens( + RuntimeOrigin::signed(user), + bvec![token_2.clone(), token_1.clone()], + exchange_amount, // amount_in + 4000, // amount_out_min + user, + false, + ), + Error::::ProvidedMinimumNotSufficientForSwap + ); + }); +} + +#[test] +fn can_swap_tokens_for_exact_tokens() { + new_test_ext().execute_with(|| { + let user = 1; + let token_1 = NativeOrWithId::Native; + let token_2 = NativeOrWithId::WithId(2); + let pool_id = (token_1.clone(), token_2.clone()); + + create_tokens(user, vec![token_2.clone()]); + assert_ok!(AssetConversion::create_pool( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_2.clone()) + )); + + let ed = get_native_ed(); + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user, 20000 + ed)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, user, 1000)); + + let pallet_account = ::PoolLocator::address(&pool_id).unwrap(); + let before1 = balance(pallet_account, token_1.clone()) + balance(user, token_1.clone()); + let before2 = balance(pallet_account, token_2.clone()) + balance(user, token_2.clone()); + + let liquidity1 = 10000; + let liquidity2 = 200; + + assert_ok!(AssetConversion::add_liquidity( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_2.clone()), + liquidity1, + liquidity2, + 1, + 1, + user, + )); + + let exchange_out = 50; + let expect_in = AssetConversion::get_amount_in(&exchange_out, &liquidity1, &liquidity2) + .ok() + .unwrap(); + + assert_ok!(AssetConversion::swap_tokens_for_exact_tokens( + RuntimeOrigin::signed(user), + bvec![token_1.clone(), token_2.clone()], + exchange_out, // amount_out + 3500, // amount_in_max + user, + true, + )); + + assert_eq!(balance(user, token_1.clone()), 10000 + ed - expect_in); + assert_eq!(balance(user, token_2.clone()), 1000 - liquidity2 + exchange_out); + assert_eq!(balance(pallet_account, token_1.clone()), liquidity1 + expect_in); + assert_eq!(balance(pallet_account, token_2.clone()), liquidity2 - exchange_out); + + // check invariants: + + // native and asset totals should be preserved. + assert_eq!( + before1, + balance(pallet_account, token_1.clone()) + balance(user, token_1.clone()) + ); + assert_eq!( + before2, + balance(pallet_account, token_2.clone()) + balance(user, token_2.clone()) + ); + }); +} + +#[test] +fn can_swap_tokens_for_exact_tokens_when_not_liquidity_provider() { + new_test_ext().execute_with(|| { + let user = 1; + let user2 = 2; + let token_1 = NativeOrWithId::Native; + let token_2 = NativeOrWithId::WithId(2); + let pool_id = (token_1.clone(), token_2.clone()); + let lp_token = AssetConversion::get_next_pool_asset_id(); + + create_tokens(user2, vec![token_2.clone()]); + assert_ok!(AssetConversion::create_pool( + RuntimeOrigin::signed(user2), + Box::new(token_1.clone()), + Box::new(token_2.clone()) + )); + + let ed = get_native_ed(); + let base1 = 10000; + let base2 = 1000; + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user, base1 + ed)); + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user2, base1 + ed)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user2), 2, user2, base2)); + + let pallet_account = ::PoolLocator::address(&pool_id).unwrap(); + let before1 = balance(pallet_account, token_1.clone()) + + balance(user, token_1.clone()) + + balance(user2, token_1.clone()); + let before2 = balance(pallet_account, token_2.clone()) + + balance(user, token_2.clone()) + + balance(user2, token_2.clone()); + + let liquidity1 = 10000; + let liquidity2 = 200; + + assert_ok!(AssetConversion::add_liquidity( + RuntimeOrigin::signed(user2), + Box::new(token_1.clone()), + Box::new(token_2.clone()), + liquidity1, + liquidity2, + 1, + 1, + user2, + )); + + assert_eq!(balance(user, token_1.clone()), base1 + ed); + assert_eq!(balance(user, token_2.clone()), 0); + + let exchange_out = 50; + let expect_in = AssetConversion::get_amount_in(&exchange_out, &liquidity1, &liquidity2) + .ok() + .unwrap(); + + assert_ok!(AssetConversion::swap_tokens_for_exact_tokens( + RuntimeOrigin::signed(user), + bvec![token_1.clone(), token_2.clone()], + exchange_out, // amount_out + 3500, // amount_in_max + user, + true, + )); + + assert_eq!(balance(user, token_1.clone()), base1 + ed - expect_in); + assert_eq!(balance(pallet_account, token_1.clone()), liquidity1 + expect_in); + assert_eq!(balance(user, token_2.clone()), exchange_out); + assert_eq!(balance(pallet_account, token_2.clone()), liquidity2 - exchange_out); + + // check invariants: + + // native and asset totals should be preserved. + assert_eq!( + before1, + balance(pallet_account, token_1.clone()) + + balance(user, token_1.clone()) + + balance(user2, token_1.clone()) + ); + assert_eq!( + before2, + balance(pallet_account, token_2.clone()) + + balance(user, token_2.clone()) + + balance(user2, token_2.clone()) + ); + + let lp_token_minted = pool_balance(user2, lp_token); + assert_eq!(lp_token_minted, 1314); + + assert_ok!(AssetConversion::remove_liquidity( + RuntimeOrigin::signed(user2), + Box::new(token_1.clone()), + Box::new(token_2.clone()), + lp_token_minted, + 0, + 0, + user2, + )); + }); +} + +#[test] +fn swap_when_existential_deposit_would_cause_reaping_but_keep_alive_set() { + new_test_ext().execute_with(|| { + let user = 1; + let user2 = 2; + let token_1 = NativeOrWithId::Native; + let token_2 = NativeOrWithId::WithId(2); + + create_tokens(user2, vec![token_2.clone()]); + assert_ok!(AssetConversion::create_pool( + RuntimeOrigin::signed(user2), + Box::new(token_1.clone()), + Box::new(token_2.clone()) + )); + + let ed = get_native_ed(); + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user, 101)); + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user2, 10000 + ed)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user2), 2, user2, 1000)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user2), 2, user, 2)); + + assert_ok!(AssetConversion::add_liquidity( + RuntimeOrigin::signed(user2), + Box::new(token_1.clone()), + Box::new(token_2.clone()), + 10000, + 200, + 1, + 1, + user2, + )); + + assert_noop!( + AssetConversion::swap_tokens_for_exact_tokens( + RuntimeOrigin::signed(user), + bvec![token_1.clone(), token_2.clone()], + 1, // amount_out + 101, // amount_in_max + user, + true, + ), + DispatchError::Token(TokenError::NotExpendable) + ); + + assert_noop!( + AssetConversion::swap_exact_tokens_for_tokens( + RuntimeOrigin::signed(user), + bvec![token_1.clone(), token_2.clone()], + 51, // amount_in + 1, // amount_out_min + user, + true, + ), + DispatchError::Token(TokenError::NotExpendable) + ); + + assert_noop!( + AssetConversion::swap_tokens_for_exact_tokens( + RuntimeOrigin::signed(user), + bvec![token_2.clone(), token_1.clone()], + 51, // amount_out + 2, // amount_in_max + user, + true, + ), + DispatchError::Token(TokenError::NotExpendable) + ); + + assert_noop!( + AssetConversion::swap_exact_tokens_for_tokens( + RuntimeOrigin::signed(user), + bvec![token_2.clone(), token_1.clone()], + 2, // amount_in + 1, // amount_out_min + user, + true, + ), + DispatchError::Token(TokenError::NotExpendable) + ); + }); +} + +#[test] +fn swap_when_existential_deposit_would_cause_reaping_pool_account() { + new_test_ext().execute_with(|| { + let user = 1; + let user2 = 2; + let token_1 = NativeOrWithId::Native; + let token_2 = NativeOrWithId::WithId(2); + let token_3 = NativeOrWithId::WithId(3); + + let ed_assets = 100; + create_tokens_with_ed(user2, vec![token_2.clone(), token_3.clone()], ed_assets); + assert_ok!(AssetConversion::create_pool( + RuntimeOrigin::signed(user2), + Box::new(token_1.clone()), + Box::new(token_2.clone()) + )); + assert_ok!(AssetConversion::create_pool( + RuntimeOrigin::signed(user2), + Box::new(token_1.clone()), + Box::new(token_3.clone()) + )); + assert_ok!(AssetConversion::create_pool( + RuntimeOrigin::signed(user2), + Box::new(token_2.clone()), + Box::new(token_3.clone()) + )); + + let ed = get_native_ed(); + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user, 20000 + ed)); + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user2, 20000 + ed)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user2), 2, user2, 400 + ed_assets)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user2), 3, user2, 20000 + ed_assets)); + + assert_ok!(Assets::mint(RuntimeOrigin::signed(user2), 2, user, 400 + ed_assets)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user2), 3, user, 20000 + ed_assets)); + + assert_ok!(AssetConversion::add_liquidity( + RuntimeOrigin::signed(user2), + Box::new(token_1.clone()), + Box::new(token_2.clone()), + 10000, + 200, + 1, + 1, + user2, + )); + + assert_ok!(AssetConversion::add_liquidity( + RuntimeOrigin::signed(user2), + Box::new(token_1.clone()), + Box::new(token_3.clone()), + 200, + 10000, + 1, + 1, + user2, + )); + + assert_ok!(AssetConversion::add_liquidity( + RuntimeOrigin::signed(user2), + Box::new(token_2.clone()), + Box::new(token_3.clone()), + 200, + 10000, + 1, + 1, + user2, + )); + + // causes an account removal for asset token 2 + assert_noop!( + AssetConversion::swap_tokens_for_exact_tokens( + RuntimeOrigin::signed(user), + bvec![token_1.clone(), token_2.clone()], + 110, // amount_out + 20000, // amount_in_max + user, + true, + ), + DispatchError::Token(TokenError::NotExpendable) + ); + + // causes an account removal for asset token 2 + assert_noop!( + AssetConversion::swap_exact_tokens_for_tokens( + RuntimeOrigin::signed(user), + bvec![token_1.clone(), token_2.clone()], + 15000, // amount_in + 110, // amount_out_min + user, + true, + ), + DispatchError::Token(TokenError::NotExpendable) + ); + + // causes an account removal for native token 1 + assert_noop!( + AssetConversion::swap_tokens_for_exact_tokens( + RuntimeOrigin::signed(user), + bvec![token_3.clone(), token_1.clone()], + 110, // amount_out + 20000, // amount_in_max + user, + true, + ), + DispatchError::Token(TokenError::NotExpendable) + ); + + // causes an account removal for native token 1 + assert_noop!( + AssetConversion::swap_exact_tokens_for_tokens( + RuntimeOrigin::signed(user), + bvec![token_3.clone(), token_1.clone()], + 15000, // amount_in + 110, // amount_out_min + user, + true, + ), + DispatchError::Token(TokenError::NotExpendable) + ); + + // causes an account removal for native token 1 locate in the middle of a swap path + let amount_in = AssetConversion::balance_path_from_amount_out( + 110, + vec![token_3.clone(), token_1.clone()], + ) + .unwrap() + .first() + .map(|(_, a)| *a) + .unwrap(); + + assert_noop!( + AssetConversion::swap_exact_tokens_for_tokens( + RuntimeOrigin::signed(user), + bvec![token_3.clone(), token_1.clone(), token_2.clone()], + amount_in, // amount_in + 1, // amount_out_min + user, + true, + ), + DispatchError::Token(TokenError::NotExpendable) + ); + + // causes an account removal for asset token 2 locate in the middle of a swap path + let amount_in = AssetConversion::balance_path_from_amount_out( + 110, + vec![token_1.clone(), token_2.clone()], + ) + .unwrap() + .first() + .map(|(_, a)| *a) + .unwrap(); + + assert_noop!( + AssetConversion::swap_exact_tokens_for_tokens( + RuntimeOrigin::signed(user), + bvec![token_1.clone(), token_2.clone(), token_3.clone()], + amount_in, // amount_in + 1, // amount_out_min + user, + true, + ), + DispatchError::Token(TokenError::NotExpendable) + ); + }); +} + +#[test] +fn swap_tokens_for_exact_tokens_should_not_work_if_too_much_slippage() { + new_test_ext().execute_with(|| { + let user = 1; + let token_1 = NativeOrWithId::Native; + let token_2 = NativeOrWithId::WithId(2); + + create_tokens(user, vec![token_2.clone()]); + assert_ok!(AssetConversion::create_pool( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_2.clone()) + )); + + assert_ok!(Balances::force_set_balance( + RuntimeOrigin::root(), + user, + 20000 + get_native_ed() + )); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, user, 1000)); + + let liquidity1 = 10000; + let liquidity2 = 200; + + assert_ok!(AssetConversion::add_liquidity( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_2.clone()), + liquidity1, + liquidity2, + 1, + 1, + user, + )); + + let exchange_out = 1; + + assert_noop!( + AssetConversion::swap_tokens_for_exact_tokens( + RuntimeOrigin::signed(user), + bvec![token_1.clone(), token_2.clone()], + exchange_out, // amount_out + 50, // amount_in_max just greater than slippage. + user, + true + ), + Error::::ProvidedMaximumNotSufficientForSwap + ); + }); +} + +#[test] +fn swap_exact_tokens_for_tokens_in_multi_hops() { + new_test_ext().execute_with(|| { + let user = 1; + let token_1 = NativeOrWithId::Native; + let token_2 = NativeOrWithId::WithId(2); + let token_3 = NativeOrWithId::WithId(3); + + create_tokens(user, vec![token_2.clone(), token_3.clone()]); + assert_ok!(AssetConversion::create_pool( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_2.clone()) + )); + assert_ok!(AssetConversion::create_pool( + RuntimeOrigin::signed(user), + Box::new(token_2.clone()), + Box::new(token_3.clone()) + )); + + let ed = get_native_ed(); + let base1 = 10000; + let base2 = 10000; + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user, base1 * 2 + ed)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, user, base2)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 3, user, base2)); + + let liquidity1 = 10000; + let liquidity2 = 200; + let liquidity3 = 2000; + + assert_ok!(AssetConversion::add_liquidity( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_2.clone()), + liquidity1, + liquidity2, + 1, + 1, + user, + )); + assert_ok!(AssetConversion::add_liquidity( + RuntimeOrigin::signed(user), + Box::new(token_2.clone()), + Box::new(token_3.clone()), + liquidity2, + liquidity3, + 1, + 1, + user, + )); + + let input_amount = 500; + let expect_out2 = AssetConversion::get_amount_out(&input_amount, &liquidity1, &liquidity2) + .ok() + .unwrap(); + let expect_out3 = AssetConversion::get_amount_out(&expect_out2, &liquidity2, &liquidity3) + .ok() + .unwrap(); + + assert_noop!( + AssetConversion::swap_exact_tokens_for_tokens( + RuntimeOrigin::signed(user), + bvec![token_1.clone()], + input_amount, + 80, + user, + true, + ), + Error::::InvalidPath + ); + + assert_noop!( + AssetConversion::swap_exact_tokens_for_tokens( + RuntimeOrigin::signed(user), + bvec![token_1.clone(), token_2.clone(), token_3.clone(), token_2.clone()], + input_amount, + 80, + user, + true, + ), + Error::::NonUniquePath + ); + + assert_ok!(AssetConversion::swap_exact_tokens_for_tokens( + RuntimeOrigin::signed(user), + bvec![token_1.clone(), token_2.clone(), token_3.clone()], + input_amount, // amount_in + 80, // amount_out_min + user, + true, + )); + + let pool_id1 = (token_1.clone(), token_2.clone()); + let pool_id2 = (token_2.clone(), token_3.clone()); + let pallet_account1 = ::PoolLocator::address(&pool_id1).unwrap(); + let pallet_account2 = ::PoolLocator::address(&pool_id2).unwrap(); + + assert_eq!(balance(user, token_1.clone()), base1 + ed - input_amount); + assert_eq!(balance(pallet_account1, token_1.clone()), liquidity1 + input_amount); + assert_eq!(balance(pallet_account1, token_2.clone()), liquidity2 - expect_out2); + assert_eq!(balance(pallet_account2, token_2.clone()), liquidity2 + expect_out2); + assert_eq!(balance(pallet_account2, token_3.clone()), liquidity3 - expect_out3); + assert_eq!(balance(user, token_3.clone()), 10000 - liquidity3 + expect_out3); + }); +} + +#[test] +fn swap_tokens_for_exact_tokens_in_multi_hops() { + new_test_ext().execute_with(|| { + let user = 1; + let token_1 = NativeOrWithId::Native; + let token_2 = NativeOrWithId::WithId(2); + let token_3 = NativeOrWithId::WithId(3); + + create_tokens(user, vec![token_2.clone(), token_3.clone()]); + assert_ok!(AssetConversion::create_pool( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_2.clone()) + )); + assert_ok!(AssetConversion::create_pool( + RuntimeOrigin::signed(user), + Box::new(token_2.clone()), + Box::new(token_3.clone()) + )); + + let ed = get_native_ed(); + let base1 = 10000; + let base2 = 10000; + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user, base1 * 2 + ed)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, user, base2)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 3, user, base2)); + + let liquidity1 = 10000; + let liquidity2 = 200; + let liquidity3 = 2000; + + assert_ok!(AssetConversion::add_liquidity( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_2.clone()), + liquidity1, + liquidity2, + 1, + 1, + user, + )); + assert_ok!(AssetConversion::add_liquidity( + RuntimeOrigin::signed(user), + Box::new(token_2.clone()), + Box::new(token_3.clone()), + liquidity2, + liquidity3, + 1, + 1, + user, + )); + + let exchange_out3 = 100; + let expect_in2 = AssetConversion::get_amount_in(&exchange_out3, &liquidity2, &liquidity3) + .ok() + .unwrap(); + let expect_in1 = AssetConversion::get_amount_in(&expect_in2, &liquidity1, &liquidity2) + .ok() + .unwrap(); + + assert_ok!(AssetConversion::swap_tokens_for_exact_tokens( + RuntimeOrigin::signed(user), + bvec![token_1.clone(), token_2.clone(), token_3.clone()], + exchange_out3, // amount_out + 1000, // amount_in_max + user, + true, + )); + + let pool_id1 = (token_1.clone(), token_2.clone()); + let pool_id2 = (token_2.clone(), token_3.clone()); + let pallet_account1 = ::PoolLocator::address(&pool_id1).unwrap(); + let pallet_account2 = ::PoolLocator::address(&pool_id2).unwrap(); + + assert_eq!(balance(user, token_1.clone()), base1 + ed - expect_in1); + assert_eq!(balance(pallet_account1, token_1.clone()), liquidity1 + expect_in1); + assert_eq!(balance(pallet_account1, token_2.clone()), liquidity2 - expect_in2); + assert_eq!(balance(pallet_account2, token_2.clone()), liquidity2 + expect_in2); + assert_eq!(balance(pallet_account2, token_3.clone()), liquidity3 - exchange_out3); + assert_eq!(balance(user, token_3.clone()), 10000 - liquidity3 + exchange_out3); + }); +} + +#[test] +fn can_not_swap_same_asset() { + new_test_ext().execute_with(|| { + let user = 1; + let token_1 = NativeOrWithId::WithId(1); + let token_2 = NativeOrWithId::Native; + + create_tokens(user, vec![token_1.clone()]); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 1, user, 1000)); + + let liquidity1 = 1000; + let liquidity2 = 20; + assert_noop!( + AssetConversion::add_liquidity( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_1.clone()), + liquidity1, + liquidity2, + 1, + 1, + user, + ), + Error::::InvalidAssetPair + ); + + let exchange_amount = 10; + assert_noop!( + AssetConversion::swap_exact_tokens_for_tokens( + RuntimeOrigin::signed(user), + bvec![token_1.clone(), token_1.clone()], + exchange_amount, + 1, + user, + true, + ), + Error::::InvalidAssetPair + ); + + assert_noop!( + AssetConversion::swap_exact_tokens_for_tokens( + RuntimeOrigin::signed(user), + bvec![token_2.clone(), token_2.clone()], + exchange_amount, + 1, + user, + true, + ), + Error::::InvalidAssetPair + ); + }); +} + +#[test] +fn cannot_block_pool_creation() { + new_test_ext().execute_with(|| { + // User 1 is the pool creator + let user = 1; + // User 2 is the attacker + let attacker = 2; + + let ed = get_native_ed(); + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), attacker, 10000 + ed)); + + // The target pool the user wants to create is Native <=> WithId(2) + let token_1 = NativeOrWithId::Native; + let token_2 = NativeOrWithId::WithId(2); + + // Attacker computes the still non-existing pool account for the target pair + let pool_account = + ::PoolLocator::address(&(token_1.clone(), token_2.clone())).unwrap(); + // And transfers the ED to that pool account + assert_ok!(Balances::transfer_allow_death( + RuntimeOrigin::signed(attacker), + pool_account, + ed + )); + // Then, the attacker creates 14 tokens and sends one of each to the pool account + for i in 10..25 { + create_tokens(attacker, vec![NativeOrWithId::WithId(i)]); + assert_ok!(Assets::mint(RuntimeOrigin::signed(attacker), i, attacker, 1000)); + assert_ok!(Assets::transfer(RuntimeOrigin::signed(attacker), i, pool_account, 1)); + } + + // User can still create the pool + create_tokens(user, vec![token_2.clone()]); + assert_ok!(AssetConversion::create_pool( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_2.clone()) + )); + + // User has to transfer one WithId(2) token to the pool account (otherwise add_liquidity + // will fail with `AssetTwoDepositDidNotMeetMinimum`) + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user, 10000 + ed)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, user, 10000)); + assert_ok!(Assets::transfer(RuntimeOrigin::signed(user), 2, pool_account, 1)); + + // add_liquidity shouldn't fail because of the number of consumers + assert_ok!(AssetConversion::add_liquidity( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_2.clone()), + 10000, + 100, + 10000, + 10, + user, + )); + }); +} + +#[test] +fn swap_transactional() { + new_test_ext().execute_with(|| { + let user = 1; + let user2 = 2; + let token_1 = NativeOrWithId::Native; + let token_2 = NativeOrWithId::WithId(2); + let token_3 = NativeOrWithId::WithId(3); + + let asset_ed = 150; + create_tokens_with_ed(user, vec![token_2.clone(), token_3.clone()], asset_ed); + assert_ok!(AssetConversion::create_pool( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_2.clone()) + )); + assert_ok!(AssetConversion::create_pool( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_3.clone()) + )); + + let ed = get_native_ed(); + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user, 20000 + ed)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, user, 1000)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 3, user, 1000)); + + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user2, 20000 + ed)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, user2, 1000)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 3, user2, 1000)); + + let liquidity1 = 10000; + let liquidity2 = 200; + + assert_ok!(AssetConversion::add_liquidity( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_2.clone()), + liquidity1, + liquidity2, + 1, + 1, + user, + )); + + assert_ok!(AssetConversion::add_liquidity( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_3.clone()), + liquidity1, + liquidity2, + 1, + 1, + user, + )); + + let pool_1 = + ::PoolLocator::address(&(token_1.clone(), token_2.clone())).unwrap(); + let pool_2 = + ::PoolLocator::address(&(token_1.clone(), token_3.clone())).unwrap(); + + assert_eq!(Balances::balance(&pool_1), liquidity1); + assert_eq!(Assets::balance(2, pool_1), liquidity2); + assert_eq!(Balances::balance(&pool_2), liquidity1); + assert_eq!(Assets::balance(3, pool_2), liquidity2); + + // the amount that would cause a transfer from the last pool in the path to fail + let expected_out = liquidity2 - asset_ed + 1; + let amount_in = AssetConversion::balance_path_from_amount_out( + expected_out, + vec![token_2.clone(), token_1.clone(), token_3.clone()], + ) + .unwrap() + .first() + .map(|(_, a)| *a) + .unwrap(); + + // swap credit with `swap_tokens_for_exact_tokens` transactional + let credit_in = NativeAndAssets::issue(token_2.clone(), amount_in); + let credit_in_err_expected = NativeAndAssets::issue(token_2.clone(), amount_in); + // avoiding drop of any credit, to assert any storage mutation from an actual call. + let error; + assert_storage_noop!( + error = >::swap_tokens_for_exact_tokens( + vec![token_2.clone(), token_1.clone(), token_3.clone()], + credit_in, + expected_out, + ) + .unwrap_err() + ); + assert_eq!(error, (credit_in_err_expected, TokenError::NotExpendable.into())); + + // swap credit with `swap_exact_tokens_for_tokens` transactional + let credit_in = NativeAndAssets::issue(token_2.clone(), amount_in); + let credit_in_err_expected = NativeAndAssets::issue(token_2.clone(), amount_in); + // avoiding drop of any credit, to assert any storage mutation from an actual call. + let error; + assert_storage_noop!( + error = >::swap_exact_tokens_for_tokens( + vec![token_2.clone(), token_1.clone(), token_3.clone()], + credit_in, + Some(expected_out), + ) + .unwrap_err() + ); + assert_eq!(error, (credit_in_err_expected, TokenError::NotExpendable.into())); + + // swap with `swap_exact_tokens_for_tokens` transactional + assert_noop!( + >::swap_exact_tokens_for_tokens( + user2, + vec![token_2.clone(), token_1.clone(), token_3.clone()], + amount_in, + Some(expected_out), + user2, + true, + ), + TokenError::NotExpendable + ); + + // swap with `swap_exact_tokens_for_tokens` transactional + assert_noop!( + >::swap_tokens_for_exact_tokens( + user2, + vec![token_2.clone(), token_1.clone(), token_3.clone()], + expected_out, + Some(amount_in), + user2, + true, + ), + TokenError::NotExpendable + ); + + assert_eq!(Balances::balance(&pool_1), liquidity1); + assert_eq!(Assets::balance(2, pool_1), liquidity2); + assert_eq!(Balances::balance(&pool_2), liquidity1); + assert_eq!(Assets::balance(3, pool_2), liquidity2); + }) +} + +#[test] +fn swap_credit_returns_change() { + new_test_ext().execute_with(|| { + let user = 1; + let token_1 = NativeOrWithId::Native; + let token_2 = NativeOrWithId::WithId(2); + + create_tokens(user, vec![token_2.clone()]); + assert_ok!(AssetConversion::create_pool( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_2.clone()) + )); + + let ed = get_native_ed(); + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user, 20000 + ed)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, user, 1000)); + + let liquidity1 = 10000; + let liquidity2 = 200; + + assert_ok!(AssetConversion::add_liquidity( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_2.clone()), + liquidity1, + liquidity2, + 1, + 1, + user, + )); + + let expected_change = NativeAndAssets::issue(token_1.clone(), 100); + let expected_credit_out = NativeAndAssets::issue(token_2.clone(), 20); + + let amount_in_max = + AssetConversion::get_amount_in(&expected_credit_out.peek(), &liquidity1, &liquidity2) + .unwrap(); + + let credit_in = + NativeAndAssets::issue(token_1.clone(), amount_in_max + expected_change.peek()); + assert_ok!( + >::swap_tokens_for_exact_tokens( + vec![token_1.clone(), token_2.clone()], + credit_in, + expected_credit_out.peek(), + ), + (expected_credit_out, expected_change) + ); + }) +} + +#[test] +fn swap_credit_insufficient_amount_bounds() { + new_test_ext().execute_with(|| { + let user = 1; + let user2 = 2; + let token_1 = NativeOrWithId::Native; + let token_2 = NativeOrWithId::WithId(2); + + create_tokens(user, vec![token_2.clone()]); + assert_ok!(AssetConversion::create_pool( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_2.clone()) + )); + + let ed = get_native_ed(); + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user, 20000 + ed)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, user, 1000)); + + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user2, 20000 + ed)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, user2, 1000)); + + let liquidity1 = 10000; + let liquidity2 = 200; + + assert_ok!(AssetConversion::add_liquidity( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_2.clone()), + liquidity1, + liquidity2, + 1, + 1, + user, + )); + + // provided `credit_in` is not sufficient to swap for desired `amount_out_min` + let amount_out_min = 20; + let amount_in = + AssetConversion::get_amount_in(&(amount_out_min - 1), &liquidity2, &liquidity1) + .unwrap(); + let credit_in = NativeAndAssets::issue(token_1.clone(), amount_in); + let expected_credit_in = NativeAndAssets::issue(token_1.clone(), amount_in); + let error = >::swap_exact_tokens_for_tokens( + vec![token_1.clone(), token_2.clone()], + credit_in, + Some(amount_out_min), + ) + .unwrap_err(); + assert_eq!( + error, + (expected_credit_in, Error::::ProvidedMinimumNotSufficientForSwap.into()) + ); + + // provided `credit_in` is not sufficient to swap for desired `amount_out` + let amount_out = 20; + let amount_in_max = + AssetConversion::get_amount_in(&(amount_out - 1), &liquidity2, &liquidity1).unwrap(); + let credit_in = NativeAndAssets::issue(token_1.clone(), amount_in_max); + let expected_credit_in = NativeAndAssets::issue(token_1.clone(), amount_in_max); + let error = >::swap_tokens_for_exact_tokens( + vec![token_1.clone(), token_2.clone()], + credit_in, + amount_out, + ) + .unwrap_err(); + assert_eq!( + error, + (expected_credit_in, Error::::ProvidedMaximumNotSufficientForSwap.into()) + ); + }) +} + +#[test] +fn swap_credit_zero_amount() { + new_test_ext().execute_with(|| { + let user = 1; + let user2 = 2; + let token_1 = NativeOrWithId::Native; + let token_2 = NativeOrWithId::WithId(2); + + create_tokens(user, vec![token_2.clone()]); + assert_ok!(AssetConversion::create_pool( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_2.clone()) + )); + + let ed = get_native_ed(); + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user, 20000 + ed)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, user, 1000)); + + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user2, 20000 + ed)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, user2, 1000)); + + let liquidity1 = 10000; + let liquidity2 = 200; + + assert_ok!(AssetConversion::add_liquidity( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_2.clone()), + liquidity1, + liquidity2, + 1, + 1, + user, + )); + + // swap with zero credit fails for `swap_exact_tokens_for_tokens` + let credit_in = CreditOf::::zero(token_1.clone()); + let expected_credit_in = CreditOf::::zero(token_1.clone()); + let error = >::swap_exact_tokens_for_tokens( + vec![token_1.clone(), token_2.clone()], + credit_in, + None, + ) + .unwrap_err(); + assert_eq!(error, (expected_credit_in, Error::::ZeroAmount.into())); + + // swap with zero credit fails for `swap_tokens_for_exact_tokens` + let credit_in = CreditOf::::zero(token_1.clone()); + let expected_credit_in = CreditOf::::zero(token_1.clone()); + let error = >::swap_tokens_for_exact_tokens( + vec![token_1.clone(), token_2.clone()], + credit_in, + 10, + ) + .unwrap_err(); + assert_eq!(error, (expected_credit_in, Error::::ZeroAmount.into())); + + // swap with zero amount_out_min fails for `swap_exact_tokens_for_tokens` + let credit_in = NativeAndAssets::issue(token_1.clone(), 10); + let expected_credit_in = NativeAndAssets::issue(token_1.clone(), 10); + let error = >::swap_exact_tokens_for_tokens( + vec![token_1.clone(), token_2.clone()], + credit_in, + Some(0), + ) + .unwrap_err(); + assert_eq!(error, (expected_credit_in, Error::::ZeroAmount.into())); + + // swap with zero amount_out fails with `swap_tokens_for_exact_tokens` fails + let credit_in = NativeAndAssets::issue(token_1.clone(), 10); + let expected_credit_in = NativeAndAssets::issue(token_1.clone(), 10); + let error = >::swap_tokens_for_exact_tokens( + vec![token_1.clone(), token_2.clone()], + credit_in, + 0, + ) + .unwrap_err(); + assert_eq!(error, (expected_credit_in, Error::::ZeroAmount.into())); + }); +} + +#[test] +fn swap_credit_invalid_path() { + new_test_ext().execute_with(|| { + let user = 1; + let user2 = 2; + let token_1 = NativeOrWithId::Native; + let token_2 = NativeOrWithId::WithId(2); + + create_tokens(user, vec![token_2.clone()]); + assert_ok!(AssetConversion::create_pool( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_2.clone()) + )); + + let ed = get_native_ed(); + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user, 20000 + ed)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, user, 1000)); + + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), user2, 20000 + ed)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(user), 2, user2, 1000)); + + let liquidity1 = 10000; + let liquidity2 = 200; + + assert_ok!(AssetConversion::add_liquidity( + RuntimeOrigin::signed(user), + Box::new(token_1.clone()), + Box::new(token_2.clone()), + liquidity1, + liquidity2, + 1, + 1, + user, + )); + + // swap with credit_in.asset different from path[0] asset fails + let credit_in = NativeAndAssets::issue(token_1.clone(), 10); + let expected_credit_in = NativeAndAssets::issue(token_1.clone(), 10); + let error = >::swap_exact_tokens_for_tokens( + vec![token_2.clone(), token_1.clone()], + credit_in, + None, + ) + .unwrap_err(); + assert_eq!(error, (expected_credit_in, Error::::InvalidPath.into())); + + // swap with credit_in.asset different from path[0] asset fails + let credit_in = NativeAndAssets::issue(token_2.clone(), 10); + let expected_credit_in = NativeAndAssets::issue(token_2.clone(), 10); + let error = >::swap_tokens_for_exact_tokens( + vec![token_1.clone(), token_2.clone()], + credit_in, + 10, + ) + .unwrap_err(); + assert_eq!(error, (expected_credit_in, Error::::InvalidPath.into())); + + // swap with path.len < 2 fails + let credit_in = NativeAndAssets::issue(token_1.clone(), 10); + let expected_credit_in = NativeAndAssets::issue(token_1.clone(), 10); + let error = >::swap_exact_tokens_for_tokens( + vec![token_2.clone()], + credit_in, + None, + ) + .unwrap_err(); + assert_eq!(error, (expected_credit_in, Error::::InvalidPath.into())); + + // swap with path.len < 2 fails + let credit_in = NativeAndAssets::issue(token_2.clone(), 10); + let expected_credit_in = NativeAndAssets::issue(token_2.clone(), 10); + let error = + >::swap_tokens_for_exact_tokens(vec![], credit_in, 10) + .unwrap_err(); + assert_eq!(error, (expected_credit_in, Error::::InvalidPath.into())); + }); +} + +fn new_test_ext_with_genesis_pools( + pools: Vec<(NativeOrWithId, NativeOrWithId, u128, u128, u128)>, +) -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); + + pallet_balances::GenesisConfig:: { + balances: vec![(1, 10000), (2, 20000)], + ..Default::default() + } + .assimilate_storage(&mut t) + .unwrap(); + + pallet_assets::GenesisConfig:: { + assets: vec![(1, 0, true, 1)], + accounts: vec![(1, 1, 10000), (1, 2, 10000)], + ..Default::default() + } + .assimilate_storage(&mut t) + .unwrap(); + + crate::GenesisConfig:: { pools }.assimilate_storage(&mut t).unwrap(); + + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext +} + +#[test] +fn genesis_pool_with_liquidity() { + use NativeOrWithId::{Native, WithId}; + new_test_ext_with_genesis_pools(vec![(Native, WithId(1), 1, 1000, 2000)]).execute_with(|| { + assert_eq!(pools(), vec![(Native, WithId(1))]); + assert_eq!(pool_assets(), vec![0]); + // Provider's native balance decreased by pool contribution. + assert_eq!(balance(1, Native), 10000 - 1000); + // Provider's asset balance decreased by pool contribution. + assert_eq!(balance(1, WithId(1)), 10000 - 2000); + // Provider received LP tokens. + assert!(pool_balance(1, 0) > 0); + // No pool setup fee charged (balance is exactly 10000 - 1000). + }); +} + +#[test] +fn genesis_pool_without_liquidity() { + use NativeOrWithId::{Native, WithId}; + new_test_ext_with_genesis_pools(vec![(Native, WithId(1), 1, 0, 0)]).execute_with(|| { + assert_eq!(pools(), vec![(Native, WithId(1))]); + assert_eq!(pool_assets(), vec![0]); + assert_eq!(balance(1, Native), 10000); + assert_eq!(balance(1, WithId(1)), 10000); + assert_eq!(pool_balance(1, 0), 0); + }); +} + +#[test] +#[should_panic(expected = "PoolExists")] +fn genesis_duplicate_pool_panics() { + use NativeOrWithId::{Native, WithId}; + new_test_ext_with_genesis_pools(vec![ + (Native, WithId(1), 1, 1000, 2000), + (Native, WithId(1), 1, 1000, 2000), + ]); +} + +#[test] +#[should_panic(expected = "InvalidAssetPair")] +fn genesis_same_assets_panics() { + use NativeOrWithId::WithId; + new_test_ext_with_genesis_pools(vec![(WithId(1), WithId(1), 1, 1000, 2000)]); +} diff --git a/template/vendor/pallet-asset-conversion/src/types.rs b/template/vendor/pallet-asset-conversion/src/types.rs new file mode 100644 index 00000000..1fc989e7 --- /dev/null +++ b/template/vendor/pallet-asset-conversion/src/types.rs @@ -0,0 +1,171 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::*; +use codec::{Decode, Encode, MaxEncodedLen}; +use core::marker::PhantomData; +use scale_info::TypeInfo; +use sp_runtime::traits::TryConvert; + +/// Represents a swap path with associated asset amounts indicating how much of the asset needs to +/// be deposited to get the following asset's amount withdrawn (this is inclusive of fees). +/// +/// Example: +/// Given path [(asset1, amount_in), (asset2, amount_out2), (asset3, amount_out3)], can be resolved: +/// 1. `asset(asset1, amount_in)` take from `user` and move to the pool(asset1, asset2); +/// 2. `asset(asset2, amount_out2)` transfer from pool(asset1, asset2) to pool(asset2, asset3); +/// 3. `asset(asset3, amount_out3)` move from pool(asset2, asset3) to `user`. +pub type BalancePath = Vec<(::AssetKind, ::Balance)>; + +/// Credit of [Config::Assets]. +pub type CreditOf = Credit<::AccountId, ::Assets>; + +/// Stores the lp_token asset id a particular pool has been assigned. +#[derive(Decode, Encode, Default, PartialEq, Eq, MaxEncodedLen, TypeInfo)] +pub struct PoolInfo { + /// Liquidity pool asset + pub lp_token: PoolAssetId, +} + +/// Provides means to resolve the `PoolId` and `AccountId` from a pair of assets. +/// +/// Resulting `PoolId` remains consistent whether the asset pair is presented as (asset1, asset2) +/// or (asset2, asset1). The derived `AccountId` may serve as an address for liquidity provider +/// tokens. +pub trait PoolLocator { + /// Retrieves the account address associated with a valid `PoolId`. + fn address(id: &PoolId) -> Result; + /// Identifies the `PoolId` for a given pair of assets. + /// + /// Returns an error if the asset pair isn't supported. + fn pool_id(asset1: &AssetKind, asset2: &AssetKind) -> Result; + /// Retrieves the account address associated with a given asset pair. + /// + /// Returns an error if the asset pair isn't supported. + fn pool_address(asset1: &AssetKind, asset2: &AssetKind) -> Result { + if let Ok(id) = Self::pool_id(asset1, asset2) { + Self::address(&id) + } else { + Err(()) + } + } +} + +/// Pool locator that mandates the inclusion of the specified `FirstAsset` in every asset pair. +/// +/// The `PoolId` is represented as a tuple of `AssetKind`s with `FirstAsset` always positioned as +/// the first element. +pub struct WithFirstAsset( + PhantomData<(FirstAsset, AccountId, AssetKind, AccountIdConverter)>, +); +impl + PoolLocator + for WithFirstAsset +where + AssetKind: Eq + Clone + Encode, + AccountId: Decode, + FirstAsset: Get, + AccountIdConverter: for<'a> TryConvert<&'a (AssetKind, AssetKind), AccountId>, +{ + fn pool_id(asset1: &AssetKind, asset2: &AssetKind) -> Result<(AssetKind, AssetKind), ()> { + if asset1 == asset2 { + return Err(()); + } + let first = FirstAsset::get(); + if first == *asset1 { + Ok((first, asset2.clone())) + } else if first == *asset2 { + Ok((first, asset1.clone())) + } else { + Err(()) + } + } + fn address(id: &(AssetKind, AssetKind)) -> Result { + AccountIdConverter::try_convert(id).map_err(|_| ()) + } +} + +/// Pool locator where the `PoolId` is a tuple of `AssetKind`s arranged in ascending order. +pub struct Ascending( + PhantomData<(AccountId, AssetKind, AccountIdConverter)>, +); +impl + PoolLocator + for Ascending +where + AssetKind: Ord + Clone + Encode, + AccountId: Decode, + AccountIdConverter: for<'a> TryConvert<&'a (AssetKind, AssetKind), AccountId>, +{ + fn pool_id(asset1: &AssetKind, asset2: &AssetKind) -> Result<(AssetKind, AssetKind), ()> { + if asset1 > asset2 { + Ok((asset2.clone(), asset1.clone())) + } else if asset1 < asset2 { + Ok((asset1.clone(), asset2.clone())) + } else { + Err(()) + } + } + fn address(id: &(AssetKind, AssetKind)) -> Result { + AccountIdConverter::try_convert(id).map_err(|_| ()) + } +} + +/// Pool locator that chains the `First` and `Second` implementations of [`PoolLocator`]. +/// +/// If the `First` implementation fails, it falls back to the `Second`. +pub struct Chain(PhantomData<(First, Second)>); +impl PoolLocator + for Chain +where + First: PoolLocator, + Second: PoolLocator, +{ + fn pool_id(asset1: &AssetKind, asset2: &AssetKind) -> Result<(AssetKind, AssetKind), ()> { + First::pool_id(asset1, asset2).or(Second::pool_id(asset1, asset2)) + } + fn address(id: &(AssetKind, AssetKind)) -> Result { + First::address(id).or(Second::address(id)) + } +} + +/// `PoolId` to `AccountId` conversion. +pub struct AccountIdConverter(PhantomData<(Seed, PoolId)>); +impl TryConvert<&PoolId, AccountId> for AccountIdConverter +where + PoolId: Encode, + AccountId: Decode, + Seed: Get, +{ + fn try_convert(id: &PoolId) -> Result { + sp_io::hashing::blake2_256(&Encode::encode(&(Seed::get(), id))[..]) + .using_encoded(|e| Decode::decode(&mut TrailingZeroInput::new(e)).map_err(|_| id)) + } +} + +/// `PoolId` to `AccountId` conversion without an addition arguments to the seed. +pub struct AccountIdConverterNoSeed(PhantomData); +impl TryConvert<&PoolId, AccountId> for AccountIdConverterNoSeed +where + PoolId: Encode, + AccountId: Decode, +{ + fn try_convert(id: &PoolId) -> Result { + sp_io::hashing::blake2_256(&Encode::encode(id)[..]) + .using_encoded(|e| Decode::decode(&mut TrailingZeroInput::new(e)).map_err(|_| id)) + } +} diff --git a/template/vendor/pallet-asset-conversion/src/weights.rs b/template/vendor/pallet-asset-conversion/src/weights.rs new file mode 100644 index 00000000..f431459a --- /dev/null +++ b/template/vendor/pallet-asset-conversion/src/weights.rs @@ -0,0 +1,356 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Autogenerated weights for `pallet_asset_conversion` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 32.0.0 +//! DATE: 2026-04-21, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `2e2073941e5c`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: `1024` + +// Executed Command: +// frame-omni-bencher +// v1 +// benchmark +// pallet +// --extrinsic=* +// --runtime=target/production/wbuild/kitchensink-runtime/kitchensink_runtime.wasm +// --pallet=pallet_asset_conversion +// --header=/__w/polkadot-sdk/polkadot-sdk/substrate/HEADER-APACHE2 +// --output=/__w/polkadot-sdk/polkadot-sdk/substrate/frame/asset-conversion/src/weights.rs +// --wasm-execution=compiled +// --steps=50 +// --repeat=20 +// --heap-pages=4096 +// --template=substrate/.maintain/frame-weight-template.hbs +// --no-storage-info +// --no-min-squares +// --no-median-slopes +// --exclude-pallets=pallet_xcm,pallet_xcm_benchmarks::fungible,pallet_xcm_benchmarks::generic,pallet_nomination_pools,pallet_remark,pallet_transaction_storage + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] +#![allow(dead_code)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use core::marker::PhantomData; + +/// Weight functions needed for `pallet_asset_conversion`. +pub trait WeightInfo { + fn create_pool() -> Weight; + fn add_liquidity() -> Weight; + fn remove_liquidity() -> Weight; + fn swap_exact_tokens_for_tokens(n: u32, ) -> Weight; + fn swap_tokens_for_exact_tokens(n: u32, ) -> Weight; + fn touch(n: u32, ) -> Weight; + fn get_reserves() -> Weight; +} + +/// Weights for `pallet_asset_conversion` using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + /// Storage: `AssetConversion::Pools` (r:1 w:1) + /// Proof: `AssetConversion::Pools` (`max_values`: None, `max_size`: Some(30), added: 2505, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Assets::Asset` (r:2 w:0) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) + /// Storage: `AssetConversion::NextPoolAssetId` (r:1 w:1) + /// Proof: `AssetConversion::NextPoolAssetId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `PoolAssets::Asset` (r:1 w:1) + /// Proof: `PoolAssets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) + /// Storage: `PoolAssets::NextAssetId` (r:1 w:0) + /// Proof: `PoolAssets::NextAssetId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `PoolAssets::Account` (r:1 w:1) + /// Proof: `PoolAssets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`) + fn create_pool() -> Weight { + // Proof Size summary in bytes: + // Measured: `1015` + // Estimated: `6360` + // Minimum execution time: 103_128_000 picoseconds. + Weight::from_parts(107_814_000, 6360) + .saturating_add(T::DbWeight::get().reads(9_u64)) + .saturating_add(T::DbWeight::get().writes(5_u64)) + } + /// Storage: `AssetConversion::Pools` (r:1 w:0) + /// Proof: `AssetConversion::Pools` (`max_values`: None, `max_size`: Some(30), added: 2505, mode: `MaxEncodedLen`) + /// Storage: `Assets::Asset` (r:2 w:2) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) + /// Storage: `Assets::Account` (r:4 w:4) + /// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `PoolAssets::Asset` (r:1 w:1) + /// Proof: `PoolAssets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) + /// Storage: `PoolAssets::Account` (r:2 w:2) + /// Proof: `PoolAssets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`) + fn add_liquidity() -> Weight { + // Proof Size summary in bytes: + // Measured: `1612` + // Estimated: `11426` + // Minimum execution time: 158_805_000 picoseconds. + Weight::from_parts(166_146_000, 11426) + .saturating_add(T::DbWeight::get().reads(11_u64)) + .saturating_add(T::DbWeight::get().writes(10_u64)) + } + /// Storage: `AssetConversion::Pools` (r:1 w:0) + /// Proof: `AssetConversion::Pools` (`max_values`: None, `max_size`: Some(30), added: 2505, mode: `MaxEncodedLen`) + /// Storage: `Assets::Asset` (r:2 w:2) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) + /// Storage: `Assets::Account` (r:4 w:4) + /// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`) + /// Storage: `PoolAssets::Asset` (r:1 w:1) + /// Proof: `PoolAssets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) + /// Storage: `PoolAssets::Account` (r:1 w:1) + /// Proof: `PoolAssets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`) + fn remove_liquidity() -> Weight { + // Proof Size summary in bytes: + // Measured: `1716` + // Estimated: `11426` + // Minimum execution time: 142_436_000 picoseconds. + Weight::from_parts(148_314_000, 11426) + .saturating_add(T::DbWeight::get().reads(9_u64)) + .saturating_add(T::DbWeight::get().writes(8_u64)) + } + /// Storage: `Assets::Asset` (r:4 w:4) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) + /// Storage: `Assets::Account` (r:8 w:8) + /// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`) + /// The range of component `n` is `[2, 4]`. + fn swap_exact_tokens_for_tokens(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `155 + n * (419 ±0)` + // Estimated: `990 + n * (5218 ±0)` + // Minimum execution time: 97_583_000 picoseconds. + Weight::from_parts(100_654_000, 990) + // Standard Error: 119_944 + .saturating_add(Weight::from_parts(13_196_089, 0).saturating_mul(n.into())) + .saturating_add(T::DbWeight::get().reads((3_u64).saturating_mul(n.into()))) + .saturating_add(T::DbWeight::get().writes((3_u64).saturating_mul(n.into()))) + .saturating_add(Weight::from_parts(0, 5218).saturating_mul(n.into())) + } + /// Storage: `Assets::Asset` (r:4 w:4) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) + /// Storage: `Assets::Account` (r:8 w:8) + /// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`) + /// The range of component `n` is `[2, 4]`. + fn swap_tokens_for_exact_tokens(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `155 + n * (419 ±0)` + // Estimated: `990 + n * (5218 ±0)` + // Minimum execution time: 97_799_000 picoseconds. + Weight::from_parts(100_557_000, 990) + // Standard Error: 119_435 + .saturating_add(Weight::from_parts(13_110_353, 0).saturating_mul(n.into())) + .saturating_add(T::DbWeight::get().reads((3_u64).saturating_mul(n.into()))) + .saturating_add(T::DbWeight::get().writes((3_u64).saturating_mul(n.into()))) + .saturating_add(Weight::from_parts(0, 5218).saturating_mul(n.into())) + } + /// Storage: `AssetConversion::Pools` (r:1 w:0) + /// Proof: `AssetConversion::Pools` (`max_values`: None, `max_size`: Some(30), added: 2505, mode: `MaxEncodedLen`) + /// Storage: `Assets::Asset` (r:2 w:2) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:0) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Assets::Account` (r:2 w:2) + /// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`) + /// Storage: `PoolAssets::Asset` (r:1 w:1) + /// Proof: `PoolAssets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) + /// Storage: `PoolAssets::Account` (r:1 w:1) + /// Proof: `PoolAssets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`) + /// The range of component `n` is `[0, 3]`. + fn touch(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1676` + // Estimated: `6360` + // Minimum execution time: 53_689_000 picoseconds. + Weight::from_parts(57_471_161, 6360) + // Standard Error: 21_268 + .saturating_add(Weight::from_parts(21_853_452, 0).saturating_mul(n.into())) + .saturating_add(T::DbWeight::get().reads(8_u64)) + .saturating_add(T::DbWeight::get().writes((2_u64).saturating_mul(n.into()))) + } + /// Storage: `Assets::Asset` (r:2 w:0) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) + /// Storage: `Assets::Account` (r:2 w:0) + /// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`) + fn get_reserves() -> Weight { + // Proof Size summary in bytes: + // Measured: `915` + // Estimated: `6360` + // Minimum execution time: 23_562_000 picoseconds. + Weight::from_parts(25_131_000, 6360) + .saturating_add(T::DbWeight::get().reads(4_u64)) + } +} + +// For backwards compatibility and tests. +impl WeightInfo for () { + /// Storage: `AssetConversion::Pools` (r:1 w:1) + /// Proof: `AssetConversion::Pools` (`max_values`: None, `max_size`: Some(30), added: 2505, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Assets::Asset` (r:2 w:0) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) + /// Storage: `AssetConversion::NextPoolAssetId` (r:1 w:1) + /// Proof: `AssetConversion::NextPoolAssetId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `PoolAssets::Asset` (r:1 w:1) + /// Proof: `PoolAssets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) + /// Storage: `PoolAssets::NextAssetId` (r:1 w:0) + /// Proof: `PoolAssets::NextAssetId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `PoolAssets::Account` (r:1 w:1) + /// Proof: `PoolAssets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`) + fn create_pool() -> Weight { + // Proof Size summary in bytes: + // Measured: `1015` + // Estimated: `6360` + // Minimum execution time: 103_128_000 picoseconds. + Weight::from_parts(107_814_000, 6360) + .saturating_add(RocksDbWeight::get().reads(9_u64)) + .saturating_add(RocksDbWeight::get().writes(5_u64)) + } + /// Storage: `AssetConversion::Pools` (r:1 w:0) + /// Proof: `AssetConversion::Pools` (`max_values`: None, `max_size`: Some(30), added: 2505, mode: `MaxEncodedLen`) + /// Storage: `Assets::Asset` (r:2 w:2) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) + /// Storage: `Assets::Account` (r:4 w:4) + /// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `PoolAssets::Asset` (r:1 w:1) + /// Proof: `PoolAssets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) + /// Storage: `PoolAssets::Account` (r:2 w:2) + /// Proof: `PoolAssets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`) + fn add_liquidity() -> Weight { + // Proof Size summary in bytes: + // Measured: `1612` + // Estimated: `11426` + // Minimum execution time: 158_805_000 picoseconds. + Weight::from_parts(166_146_000, 11426) + .saturating_add(RocksDbWeight::get().reads(11_u64)) + .saturating_add(RocksDbWeight::get().writes(10_u64)) + } + /// Storage: `AssetConversion::Pools` (r:1 w:0) + /// Proof: `AssetConversion::Pools` (`max_values`: None, `max_size`: Some(30), added: 2505, mode: `MaxEncodedLen`) + /// Storage: `Assets::Asset` (r:2 w:2) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) + /// Storage: `Assets::Account` (r:4 w:4) + /// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`) + /// Storage: `PoolAssets::Asset` (r:1 w:1) + /// Proof: `PoolAssets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) + /// Storage: `PoolAssets::Account` (r:1 w:1) + /// Proof: `PoolAssets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`) + fn remove_liquidity() -> Weight { + // Proof Size summary in bytes: + // Measured: `1716` + // Estimated: `11426` + // Minimum execution time: 142_436_000 picoseconds. + Weight::from_parts(148_314_000, 11426) + .saturating_add(RocksDbWeight::get().reads(9_u64)) + .saturating_add(RocksDbWeight::get().writes(8_u64)) + } + /// Storage: `Assets::Asset` (r:4 w:4) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) + /// Storage: `Assets::Account` (r:8 w:8) + /// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`) + /// The range of component `n` is `[2, 4]`. + fn swap_exact_tokens_for_tokens(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `155 + n * (419 ±0)` + // Estimated: `990 + n * (5218 ±0)` + // Minimum execution time: 97_583_000 picoseconds. + Weight::from_parts(100_654_000, 990) + // Standard Error: 119_944 + .saturating_add(Weight::from_parts(13_196_089, 0).saturating_mul(n.into())) + .saturating_add(RocksDbWeight::get().reads((3_u64).saturating_mul(n.into()))) + .saturating_add(RocksDbWeight::get().writes((3_u64).saturating_mul(n.into()))) + .saturating_add(Weight::from_parts(0, 5218).saturating_mul(n.into())) + } + /// Storage: `Assets::Asset` (r:4 w:4) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) + /// Storage: `Assets::Account` (r:8 w:8) + /// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`) + /// The range of component `n` is `[2, 4]`. + fn swap_tokens_for_exact_tokens(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `155 + n * (419 ±0)` + // Estimated: `990 + n * (5218 ±0)` + // Minimum execution time: 97_799_000 picoseconds. + Weight::from_parts(100_557_000, 990) + // Standard Error: 119_435 + .saturating_add(Weight::from_parts(13_110_353, 0).saturating_mul(n.into())) + .saturating_add(RocksDbWeight::get().reads((3_u64).saturating_mul(n.into()))) + .saturating_add(RocksDbWeight::get().writes((3_u64).saturating_mul(n.into()))) + .saturating_add(Weight::from_parts(0, 5218).saturating_mul(n.into())) + } + /// Storage: `AssetConversion::Pools` (r:1 w:0) + /// Proof: `AssetConversion::Pools` (`max_values`: None, `max_size`: Some(30), added: 2505, mode: `MaxEncodedLen`) + /// Storage: `Assets::Asset` (r:2 w:2) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:0) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Assets::Account` (r:2 w:2) + /// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`) + /// Storage: `PoolAssets::Asset` (r:1 w:1) + /// Proof: `PoolAssets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) + /// Storage: `PoolAssets::Account` (r:1 w:1) + /// Proof: `PoolAssets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`) + /// The range of component `n` is `[0, 3]`. + fn touch(n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1676` + // Estimated: `6360` + // Minimum execution time: 53_689_000 picoseconds. + Weight::from_parts(57_471_161, 6360) + // Standard Error: 21_268 + .saturating_add(Weight::from_parts(21_853_452, 0).saturating_mul(n.into())) + .saturating_add(RocksDbWeight::get().reads(8_u64)) + .saturating_add(RocksDbWeight::get().writes((2_u64).saturating_mul(n.into()))) + } + /// Storage: `Assets::Asset` (r:2 w:0) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) + /// Storage: `Assets::Account` (r:2 w:0) + /// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`) + fn get_reserves() -> Weight { + // Proof Size summary in bytes: + // Measured: `915` + // Estimated: `6360` + // Minimum execution time: 23_562_000 picoseconds. + Weight::from_parts(25_131_000, 6360) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + } +} diff --git a/web-client/.papi/descriptors/package.json b/web-client/.papi/descriptors/package.json index 1ec9988d..54801a33 100644 --- a/web-client/.papi/descriptors/package.json +++ b/web-client/.papi/descriptors/package.json @@ -1,5 +1,5 @@ { - "version": "0.1.0-autogenerated.6754844343475179793", + "version": "0.1.0-autogenerated.16302145885248040146", "name": "@polkadot-api/descriptors", "files": [ "dist" diff --git a/web-client/.papi/metadata/deos.scale b/web-client/.papi/metadata/deos.scale index 921bdef4fc606b2e9d566274df9a5006bb7f33c7..2cc742b5bee65b186122bcf81397873635a17553 100644 GIT binary patch delta 23910 zcmb_^4}4U`wf~)&yUA{Lg9&6IAqi|C2}w*K0fGttOb9_D2?PiV3WgFE@TyC;^m1vGa^Ywh;?JFV0-Q@EBo1Jv<5>S2{VPTdV+G!^!-3H*pk=NiQr z>gr>o*-`pRA1mhrDy?in*;{>VjCx#U9aR1#%O>ZO>p5Z+WkxBOe@kGFAYHg6#VO_fBuqP&kw8QYY;h) zg$rAc4nPWtQRIVr|WqBr*f5d9pDGGeV9t0Qd zXW2(+@!44x;AhnBoBlJGw)~mh$IeDZf6Pv*>@0ouS4QkyaTO;o~_*K!j0d|4@GK|0IxS%SE zYIO>yP%hF7SMW(Am_<>z#iHO(uQ{aHo=fBHjHgm)+E>cQMyBL&J7;;3tHQhpQ5 zqM9zb9XzKcq+vtfh@S@CUcCn1tvI_Y; zj{a?cXHw5ZzKQOegz;rh;$c=TduoNmo}0u!krKP`JkNcg0-Udptg7JojMdYZCi97` zf%Z-2PS!-fn9T1TwZg(`mPl=Mz2;rp5vuEy3tefTt|@#OZ?bG3c)Z-X*rF~4mz8A} zmU*4lHnU~6qAVucR6ZHAICCmbXSQW>Iu=`$6?9!Cf10g|Bv0dhM!D5IEppFQoS4uo z4nxMLRP*6jdGo6IHOx!js0Ptp^yg}Rf~}{Qrt>Y?VGAp*>uKxMpc}%SA%(d>A2ga4 z?4iyXyp3(7V>5U$>!H8R;NN1KXy;76pKYc^vv?=l5_x_WuTNe&UtdJ?+(58IYx4&@ znkS99+U0wz*R$2aa`d_ekHXT{L8Wf9u&Ti(x{_th?cP8z)EY3`(<7NYdoIgq^ZP>D zhR_ZRdqc-jXS~mBnLd5dnxGcA#T|lDo~LP^22ZI=*K!NPU2C+!JbwVZo2_I&P|v&A zZn|%j$Pqcp*Xff9B8T?Y^KP>c;&IhH?qd6B`8+-=S~!NDna7L9j8!^X0^YT2wZNjb zws0VzL2EY${B76-G>?PM&Eq2*t3n1uYrDJ4+u5_qJjBM+;%oRamQRme!>3|FynPL?2SX^fC@r=0!v^kUVM)sbs=F4e zqDPWfKo4EZe@xFT0MWNE;8hS*z4XKaz9?h8l<`h~5HhZB0zy$X8`bVcex2A7T`Wf% zfo`j;$#Y!nkqnsFZ`StE~TDaS+fkNM=BWW~{ z3=#E=94W;Ced>~6W=m$B%M&(Vs zk?k2Qw2uukY2E%KDZ2>QqGEB3eSBicWOr0r{73PA_!&LHm&T}?% zQMas`sq2#l&uoP?2emG@54KUOR2~7q0&<1QYD~x*!&Ng}Qzowh=*%_XKES%VyrC{g z@F3LcnQ>tA-BED|CV+WEJ%f<(fSGFouRJ@= zczy0b4<^#pB27`ByAwjA^vUKqbXO8Mdi8Ko1SR%#5;*wf;UWhte`L6*K+zz~qfJ=I z`}IDl<{oBCzf@CY(hXBYDOWi8Ey9@%5j1U1$e^pvjls9+3sE^he`(^&h98t$QNKmO zA1s5aD&;V(Sjbm6k675;I{&RRCj?!qgUpmHNB2fUfLgMfsR!j~kNEW5B?U z0>VdOLg{!vXyzHPqb@e{8ThGQ%-t|bA6m?3!gP9TF)tZ+St1pd?}a_z>T`E#M<{;@ zcFc2h_Yyw4_?$&`U`(po!p>X35B#U-|M3eJ1%F_`E>@*nv}{jzIG)~e2=CUvo@IAx`^UTsj7EIgt&B{zu% zegwm-U^&la!}Cy@1Y4*;))prz#YQd7zJZURudKjMSVntS@H|#aFRy^1R8NtWyol;n z@$nMI4ZG-(8+ehcB1v`4X$fdrle^ohE?^I<*Re`TWi=2hmGM|}ZsapZ$YMS#Nx>fo zd>Ej$wC+Y;P*R_yW*SRVvbG_KS<|5SH6Te_3PVIy8qfm5-jKBY&!aNwwHrBX#CY_R zRzh;h=(?4BwMlKs;sl(_hT>di;9O?l%v;6B@f9Y{Z`}kV{`OU{Z&%UpSMh3`K5}k? zZK(eg3T4$GT;~Y49F>-&;7^Xqo1}Otx0Qc4qc(|^n+j*XJJ^A_?b7>-oNeVkmc2fS z&94pi_}b>U+h7u{ZFUFTT`&gMC#7xwc+@D`>V^>;rjOjbadOXPI31Rp-iS{1|M;dP z1%HwPRBSc~-0sU6LEl=#Yi(O3kz13Lt@Obf3~L)LYU5MX9ZA+Z=)Nb|SlZLZ_mAG0 z#7d<&N=l?2Rd&i&4=cNLY$cIzdH7u%N@ceXYxd3_p6}d~WSFXbNrR^99(s2z?B#pb z^2T9%lURH@ z0w0b8?x#{OzsYu3!u2O9M+~$_O~~VMkVhqCMaDo<)VaV8DOP6ndIpkcy_aXP0jj#0 zPe?f~Q5{WEP9#0>GEUx`ZsxT@oJ_KkzX6){-7YMUQ^b6{5uXH%|lC0!RB`cYxg?S0jO{QbVSXSirFmGV6 z<_0$Mikx?i=_*JboUQ_i^IfGlneHnR8B};HPq&xp*ou>@lnU8yB>f{|#8=zJ>NX!2=O9G^Do;2%{foAAK#wwa3VC*$#T97piNAfqAEic~>0fojT^UL&`3R8E?A{*gYof-Z zH}O(f9&JZVo0Zha(fe#247u5xVVb{Bd?9GXCp) zlzH=547k2l*-=%Zqy3Wfs^ZpXT= z+y*jF>GW-PJwBYDPL__B$f0fgYl5ALH1B}*F#IeQfbJfYZlf@C+gU=zNP6}GUY>GJ zM_L8*Ko;gu3gL)#fmRbV%tgA5_|ADh zL4F;#nJ=|F`6M;fs$|fxo$!O)yOSs7q*~e3Iq;j}#JyBn5o;Be8SLp=k- zY1|dr{(b&i29v}81pEz^)cid7{Ff)-BdVsFK6q#9p23OlD}69*W>No7z?S|#tkGE# zupl!0Nj{cAP3W!WKgDlT>#b-w@oD&c-hB#sdNE!3G!M{-XZd7jgVoSdP1Zs6*kCmM z+td7O*fsCj$BS%DppZ9MrCtGFoy*KtE8UwIA;Fhfl~p=LH0gC%>OcD- z^pBhJo&o)fo`)dc@C=`mqUl4&L@Mh>7*~^Yc#BiLyV~=42;=r@~?vx&* zS(UO;FN}EhbC0EbQv%yJOSW%`vVAjJMA^Ot&zQT-2F1gE!hdM+@f*-A;F`90Yr^br z8)`^xgQ5q9Z3%EYhDyl}qo*AP>EjRbU$dRYbLI>Dp^>{Rv6GzXCV>UA%K&)q1^)Ks z0d^Z>KL1mAbNC)BrDenGcuzmSQ{8LDgtZ=q;adJPz7*#y$-EJL=4|aBoUI3bfm!(Q zXE=iFqr3hC3ud1VN)P>tk4V`MqIsWHIbfnb{&U{K51KF69^yCge)HwXA^z{|Fs(cc zzsg}9!^k6MX9I(sDMw6jy~CqMheu-_I%Ri9tjci{YTPg2kU3$#Z1@Gglb)hf^yM+n(g^+99z8YUM*-8q`2JO-2wkC(RX_O2)CNR*34V9kUv(_^-G>?)GXo( zv>#YC^0Z0)nKug{CajiHnb7Y zdOF^VCSGP6TxJ)|*3P56*~V-@dC{s^X!l8;NB17(4cumn8HPqop$S9k$Wx6xa#VAYtO;<>N#InaMVUXCHfUtCqDS2-(ehPJAV zV`GIeyf@z9HzZd|E>_s6;&;%A&Byr-&RHl?tut$F3jU-4IZG(@MvLDa=Z{^^XFrCH zzlr_2L5GTr|21D^fh$|*;r6%rFZg1EmSrX_E8=KbCc#43-bTK|r>YjoH_YTH(%#|u zDJvv#%WTRjlea&32aY#4z4H!iH2o(>C~mW^@pt@=#ahB-xra`c8yGQJ-UPU=p>SOW zT$eGT>UZH9E?%F|C_J=L*k}|sM*i_BzSa&B?r4KMLYqFFPQAmYTv{-i4*Z_q3E$*x ze}JE|^$&cs@my5glhE77p}lQ1dfRC9_NPDaK=G!8Hk*gG*=)4gY_$2_ANhmu&0l>Q zA__f@-$L5@9v{DWO9Gy}p?LBPJX;gswhe{bX25MD_CAg|JE;DBUSqDpY`ds!X&JHvop7pPR(B=9?H)?bZiAfNrpQ=6$Iui<@W$;m(QisF3P z+78gVkNFa~*IxLTPlrqG!pD5xf5IzAp8`PXGG9Pj zAJ(K79@~KoJ8PlS^x`LcHjcXg{Dd#yr)*T_h1KFc$7jzvZS-}@rkqLW?DWviBq%yN ztGAe0rJR#0Wez(ZneZ3>AOe^4;-|c3;RV&ub{B1|D(Z8ZxuDc_9gKvFwnRTv75q>L zVX6vCfte%!vo4aoGC0{FKxyPVqKlu+F%p){f*Dr@k240Y+q&>6Ym1QY|`)?WgNyY}@&fAeMu7&9=QRvp_Ed9}WIc&8%E{NNBeF|;3iswfD zory-q8Xg!X3P=@VOlm31hnZp^ZlXy-xL}4i3sHdM`7J_h!ioGdAtp>)j`NHRytUq{ z2UidT@FEl#&{9~p=~pvjO-7WW*{G%*RRma*-rcjRD5BS_qOiP34<{K>e&$GP@W?SK zIZMJmpMGXi@|T8#-I_c`$*9IMy?LWWj6{w>n+1`H71TUcl-r`-a4WrJ5xFB6az7CN z>e4(fv9vYF_kiF0phXl}5Mf%8Lh0KOVoOaD6N^`2(g$6fZb;cBXDGKJV{UY;yYnZ( z4236uezBI&(mS-J*J$aDwzO3=Uyd&jW|k%wZV{Whw5>z)ggdo>Jf15ohw{=zCjBu< zWbiJ7>fX6}I7JUrA$~Gnb9=0r@_#MB)~BQ`4Qbu1Pe>nqhhDfI!ems#dt(H0p)0z3 zQer+(*+j->1}hQjH8NC!8&lZb=9KE)E)9*+x^ZaK^H=J9w`iU4{bQxCH;8jK4k1np zWKh?gEGvKNJe-B4JJsLmZR=s@#h|kr?_@VodM|75Wkr%q1b~ov6b&w2 zHhg4ddh?+XqgcgdVnnezPq!KAZS8Wmbs*x~s_COD>DIz2?5=1WxXGxDj(FzaK`3la z%gQeRRR@O&PfW*Wn~X8Ed(1f=I>icT*8~0tjUt4-JO-%Cp}#X4(#|x3-2iFPL0a6wb@h7u z+k{Xy8-0ylt=Ho}uaPHE8xD2&0|?(a=+$0WyoF9N%oY`l9J=}|NCx=Pooqfe^|F%d zE+0vZ&FZDW!&t??5Cvt6IgqiNnjaN?c$QpWM1w0Dp1hn$##P>d z46_h&#*AdB)@W$7H`sa$Ty=3V)ef8$Jz+h=z|10u#X7Al4q=NPGF_`7G7}2)T#BU| z3bEb07GZr)>l*2o3dTiONqnX*v3wH+Wc)Ik6eCF35JI3^JDOOi|KUzB?)oyP<*fW-=Nd6Ow35 zxzuGua9B;8_R`(c&>59_L^?z2k>HLLW^|^oDqXR}RNPK!8ASD+jONRYlpcdY056Dq z9^o^Qi&W*VXLDs;o;JflBGuM4;%MEeFhAh0y7X}rW#efjxSPk z{|~#a(x0#~9UeV&XF6=`Oot_Tbk9UlZX~uj=~oj)9zT*oA5RpS;z){_OqV-JR8?=W zu%abW^_kgwM-zJ4IP?&^(Ky5o$W96kl>#0#8={kuqX!b8t(2{Wg zs#%sIgDD(zuhpEF)XveC$zp8mP#nP!Qgfu915ME+&0ti=t!|h3OWxa{x!S!O@V^11 zdFT=bnP^^7hgHM;dQuUnCD*V^IkgreTZdE zvyowM+#)cJe^ZfzqXj^e(UL1g^FUL_+<@EbLk?8wJcD5O8k`%EjOVncLHBod(()@s zhVKjbZg2xA(AE)`&Z%TwGVl#1X__Z- z(oKbN>Aa`?>v^wAoR7GU6DNJz;9UZ9(*GxumP^~RMTIlZZm6eX`=AFRPY%mSWV_1H zk#esw!pGM5jf_>zmy;^9`s!k7d#u2&Wa_E2gtKxi*f?D2ww$%(4&hL(D>7 zIMdCF>*$U&QDCc;v{c&>Fc_rZ)im(~dxK6~lf6i3imUp5y2yi9N79kIFPhLEN5?XW zZlBCx*Cpa|UZhoel^%w(yY)`o2-It`lWaYW^cy3@8t%2zUq*;4v2{p9v?xPZon3Jq zb;*v-b7lQtyX_g`T4Ywm@K~IpJY_1IDUDU6oscU>xLQ`4Z*53D#?>GhM)S!VJ3V$5 zGX4Ihm!(cZASK)cfK`~#RM)b`6&+gT6eTSy+x6_**8dIWgr@PBH#@~#_i7&!=dhBBNfs`*wsJ)J40A_Lqkud;8`1#gSJ z3MaN0@XcA`O1{%hlqE`VdXs`Te3!Y{cE@Coem7N&(FJwVXps&-V(n;Al*(FkQ$-7O zy8|H_yNQh{`EO6WyQK{8vMYO{Bsl3iqs28a|6(K!&xRdn(C?(R*>G6)87pg_N!R`u zUGhK`Yxj?ATp7!tWA%uw*5rukOIXeSLM}^y%slrdtenVG%-fq4*Prc3`|tmqa;?1pJS zBGb4xmVsOpe*igb3TueWb-~o8pXNc84WmuwVgm$=VOFLbYs5Axw%+b4K~9JMUqq$ ziFsyFN|fdnzrV@t>xpjR((ni>S1OH5O&e;N&wUH7s_4dFsnTJ3n1R6XaQFj@Kv+9i zrj*gCd@+hmq<=IZ;5&&j3WRG$xzeGd_IEZxWaLaQ!A{fY#gzjvqe5A&+b4DIZg(3T zlkuHRCVzn_s-2?W{P72HY6O4hNw1nRRf(OZ<@^S`xbUD<%Ih^{GzQVYcxQLQ{o>3_q$%lbl*E~YDY8E@T%B8APM+Y3c5 zn@Kwh#dstL9V`?%SIt(I$K7#=r~7JUbwZ8Thua&5!LQU%+63t2TB?`;)iZ|{PY_wG zj@C^OW7u5!>I8&=>gj3uHjiGFKiANoCx{8Q`HIJg^AIZ%BXc{9nb#$;#k^x{3c7MF zEiMvSy$h7)*vT=Tx<+MwtbCAnP0HL@X^e9Vm6hgUVQv_!OL}J)1ptOqz#?V2T)Ogx zEjIsk?u?=6B;Y=B-Ug0eDi1R!FY zT`aO}$EEH(VOLJjtYT4yd!p-#v0a^_=ZZyf@#zE;?$l5d?i5;M<2*yw60vgPS%8C3 zIFMxiU8X#wqltb9+(4NIFZVzcflNkeK_$9aONO3OyrZTR7~W!c~xC1t`X9;Bt2PH+EEUsa3%&V@GU05_$d~di?_PV*4_@Hv_J#3+dranR@c7UkZnNkr)T1hw z-U-90&gRhbaZ(+|@gyD5t>^{JRK3?i%AF`CXCH>^G0qc)$S_96Qyq%aq;2~oQNyzw zbbONdt^CyqezRP_!L%eyX%(WNBG(Z=EqRh@wNa+!#WAhcU|ODmq}T9u*ZG5?#bG}} z1#w}YOjd+55pG3=db+jqcIg$tTMJ$kR<7Sxhvt#STAVw9t%PL@Z=H;fXstLz=~(O`gXqGBK>vDFUN zty6&iy#BfJqGuuj3abG$h8={~(^@-l)q7U7O>99YG@5wtv{XOU<_h#p6G?K)AzGK4YzJ$J zPURO~rk#HY#3d}ebb^Nt#VRiz;(Q%XQlgl~&sqk{)HY~c-Jz)8i_KpH4_qq(xiaR^ zjc}|l6hBurvIckb{77VIUDFdvnBXt)jh1QBw|W`pD9g}iJ`TT98%K9hObzKqGtynA z>gy?HqbQ1@t|+}ijL+*ZVzV*dfBcq=C{4XXrb}e^2Fy*hF~OZRC+t}(?-9V67M%h8 zv=Q&1GJRJ@WW8I9ugdI{)>g2=!4lExMi`>34GzPfr-dkM8yoQjL`0#}{)IWL_(^vdetRWN2EA@o^LR`3ZEPqfD+8 z%|?#QXj~{&(4$@~*EsF21c6-+dc9Ii;Oiaqd8NoN=}O=Zw*O0Y>Utwi1NbPW7kL;j zO}ubqOB0kCjBGfqb=z9M3>~Cwrx-ob0dmc&;XMxab#uqlFJR|JFIHol%ru-g8y)QF zCcG+$<_eGYaPeY*zUgRRvfo9}SY`dRoy%)x5m zh`qd1Eu7Jp)S}z)86#Jd|o95mF(0S8sq z!K-u7L1*WRJo<7SsOrb(0O}BHJ?x-Y>X06QcT|p}l)2)REW}GU(qC_?$GPw*-CYll z+fjO|USwtt$R6Z>{J2BGAL5m}SSHR)zpEEZGLC?W4ZaRI41A%v?vNYltY)|E^Tf3B zQwgM>P9XiXM4U+aY5LVXaSTVIUDt>kXPiw~Ea!$=XXk(cCms32&pQI zLR;vEy|h8T9W~TNZUS-%c!?cr!sVe4j^~0@Dt`jkOeZypG%%>9QTpzZ=o?Sq8ztXt zghS^9J=Z9zQS}Lac(M?a{`#-leiDBE%B-uHxz5Sy} zOjU93%=`vb(?aY=Wwd^wsKobe9$N^0G}y@Z6dOsOEX3kHZ=>B$!7rM(NaS)`iu@kc z3?$Oh#zpwVLOp$Nktl^wzP<>4*3QVqMPj0b9gLJN6Rn&bj(q)k@ghU+chd?nlMO_^ zxGw(!>my%UCt5ge!v4BTtYxe#as$rh0S-0KK`n>-*Ol!-K*)9ts*n>$k#-!3MRgAFOnee zIjsA{W^O-%?() z^r(0f!@l=Bn6H7zZ@wd}76|bl9!Czusz~!*afIQv-{>ER75EALK(vYV$zl4<55#U< zBDv!UaUQb}=o1ZMYw|XFp-)`Tc1K1&DV`OqFY@v}aRA~L+58M5fmjmu{o-YO{o>dA zMHPmb_MG^xIFWpkBG19GbS858Igx5X^_Wn}52dC0~FN+1ZFiIz17PsKFSuF1&$2R1dk1iw({mYglj13WUN^ElOsn$-P0xME`MIJPWmQ z`6z{3H0;Vm%(4+rUURE_D^dzVgg3=1Aed?H8B-@?H9Eyt<(1pIaVwElz`C;BZX z=z$Yr%$R=5Nu$-E)81r<%GI;<+6j>_daUQ@0v_P_8}l0s{sPthM&zdVTP!xotgl47 zqGJa=%>hUz_NaN=QmU^5sl3ijMwK zOvCi*prcM>g{~*}X&~8T6xrSr3+V0l@m++D_t18$QTpV2;%XXk2BqwM>;t=u(%Sbi zZF`L8hu_C^B5hGp@X7mF@%xSEnh&sPVU67W0Vq6Rl)e4|VEc{d31`G?>??Y^o-<&< zQTd!fFQ38ogIkZ*53&6WP{oH>P{{525Jqg@ha!E%S=%{-MGaulEV({#@A5-@`sV_P zKZ)-QtF{-Gt8s!iLNyoDA!!b*wa5#x)qCP3y_5mYtYMlWXx2FsrgYCgbXgw}u_?AT{421Gq zw8)RoBM``Q?ezM8in(f;y~0k}|0Ncx`z-Z#>iRFCsSWnUc6#%_gqt_nsrob6ANc*s zXJQrav(WpWiKlz-Xpo;k(LAPy+W0~o%#B#+PA&^ll=Y&J}mi#&|+FYNFB;mS2&OA6WBVd5b0)F7FArJ164Xd(zJyx2R*vplF3X z^6Ui!%Pp(GR*2Z=V$`r^D)zeqq@z$!%ofr5NpI1`ySf=57936+H)gg{s|Sr`gUk) z7dx=yb+sD?=Uk?CK(F>OHOO~bXdG7`zT&W?PGK9Nel&}6h>mjgx`GQy#@FP6<>kl? z#3mk933?p3D$Ru^3f0;~mZ(ZAB9Bn>SPk7H)N#po$tqevB zx5`ci{~31xV)|na{K1soFGlp*m}{QQro?b`IhE50r%9WpYY+{0i|~ptxW0Id*)}ys z%#|FSKOhMH@U0koGHP(7O7)`Jqc~=Oq zLL~-+w9BD3j$WQq8xFW@yS>(_L8KkDLZWqrq#PxYs&3*1$#hSu`b}xwj~k};rQs`0 z$Hu6$u(1vst5#uSy=JWX98PNg7^~K+r|oIqCFW8`Mpn7h@3NFL_6+^Y(V1t-K29ZU zwBH%0miJz;UxaTJsiDS(AwAsD{50~*$&>9-UyhwYmD0JcY0_q`I*OhB8c>bf840;WO* zA&wSZL(4o{O%ys@{nm{~i%HZcC+u+YJNo};%CMGdT`)Fc~ZD?x}UqTaJLlTpiyfgPMvOMzr4Z~9WVud+{D_{ z$NlU_Mz%~%y_Y>DBA3-TJvFZSjWu3Rv)dDJHoLtZY$|{;Z1lmRNsH9tAFxDVk=VDI zk5#|7ixo4xx|@v`LQxLj*#}sXnz)CJn}WNaV>9ut=U8-9N~70XztQcfb!}fw-Okolig{#-CtutQTjUV7aUh8&9x8Fc|QJpO3=}CkUGb z|9*mHvoIXf$)>Y>bkGY54Y;$JkHtqjSrV(lQ=Kf8RpZ|}Svqs7;s4J5HJY79^)+^x z*>UgdY!4`%`0npnG_HJuwc@CYtdgG|pkuh;z*lRJ{$78Up1x)I(H`xrq zE~wP!Q^qd}`=Q-5)PtX}QOLX551>n6_Ffo`*v%4HxB4I5BpP;2{na1Y-J9KDaIp?)oeq zAOD0s!fvQj``9%BHw2D<2*zN-XY4-KtG0i}(ndlb$^747B7SUu^{iiAW`qSm?*nEy zB;P4EIKbFV^+h`zXK;%c4u}2f7b9Tiz*iURM;-xZ?ESP9ggJ^wapfjnihyaOZ&3{} zgF=tia!xU*5u;!W;}(M*yHuLD%NIyaZKL5q3(ZYJJp3n%z+GuD0r$QOBUlvTyO5yH z9}jk5@oISjY&Plu3)Igi!$QUq)j27U%~&!vra~!8#b2kwZ*-7lEJJ-W4Hht#jT`?7 zv6z_-KV!N0$8+Xuz^TbOru5in@q@}#(!o)ra6Y#HV7piQ>Vcc zmM8%nSU!!Wu)9@1T zH`y>3@(pPHn()L=hibkl#&4i2XkQ_I1C57 zp3NFtY7i@FK9mXri(c)jDOfc}QA!5@r_F$Pn)cEe5W#E}BmgKiD3t@P-=7Ijuqrh= z54;Sj4Y)qQV%4AK!-EVT$cJ#eR1QX5FdIg(hjGnpSj1fT)NI(u-1zNmxWXFnN&)PP zZK8q9Z>gzwt#J7R_00-Ppi$L$8|Vvn6haMa!Ja}$WdXF!fuHal28KOlv{uf{fv4Cm z+%y;JS*v<+E|i#99ri4SD7?A|RFq32Z;cIiOIl9FN-_ zG@;}1@)DYt30SfeDp>+Pu@rJ>mM<@bT$re!vzyF~3$s^49L`%#^Syi-(Qt=UytWL| z@TbdQGL9>S#M$$To%Pa&mTa$c2K>#gT7@ag=6gMUS52VVeGg5WyFTD^`4v!d){t5B zZY$Z|==KpD$EQnSj9n;;7rI=5&mdEK${>&J z2&o`Rl|?p43DruF(?9yR2mD6;z;+b-qoPDw?7C)&3JARYTEqG%wMBwKu zVK#}2nv!wE8Yp82ux|}ij%t&O;jlrWhm<)lPdS2X*FsfJyMZm3@7-1~#otVuMooHx zzu8ACs@)*$R4v3RcGN(tjs7XxKLaSo@cLS^HXYctj?B^8Dp<@qvAqf=L#F{FI#}jR zgJNgV9N#43fQdmCLoZCAQomDJ$$)H<8Jz7`!uIjJLDunrz(r(`*e?DyB} zU^blA@MhqaJK+v?79YP83fOsk>rP0U_}%=oBzHq&{YH z29_r^uw;9)&+YNMYe;U?HpK}}+jWUNns8q?;6W$M&b=WkZjdlYd8w?3tVEG4QbmTy z7P+EO6p0ey5T&9*D81B7M3g=Qeo_mGYQ|<#>QVhvYb2X?Q?}kRD7UoM9DJmP^n+n% z4J48k{6h_?Mhw2Z1yZQ8sU9Zb&KeknRkg638SvR!syAR)EsVBVjKooxk$AL(04BMh zV6?1-2%|y|3FJwFj6%N)l2c=hBD$$pVnIr5;QnqkGl` z^GF>BIheTxChL5T#-=S~>~$_vlZT*89gZ?pN0};7#$f7JveA=jX&T+#-qoa&0OCMRm zVyC~3rm{$DRr}rGVX-Afwz$aO;;AWi*4*v(Y%O>CoDI@M8^aHNJa#O$-A$X213$eR zma$SzzVQwz-wGn8{ew!QLXV_O4ONo4do0zGC8tqw;wSaAuexwe1Kh#gMl<%+(q_}q z04HM_j4WNQGD*=YKwZe`J z>4+Uh7N;8(;(do~h(6?liPl{bXNys3HOf6EYF`k|{tz_#By_WNzl7XpR1WBnZ9&Mx zA&_m@Ny zIykNIfS32MQE*l(N1qS!b0LJE^TrS_u+nltl00uzE@DRuq=~N3R+IWw3tSV)-4f=C zQK5$>SiGZLlNCKig&wNNyKYpj8}Zgo$hF_lYI=>)O0N;;`~cFR&xk$0CvCXZ0v7e* zACTHWzs~75_kk(uW(aFHCE9nDTSgqW3zF^3B(dKz<|zgf&07?1+65Wu786S^UgdMS zR=S!s@4#Xb2?G^UG*6Ob=?Ox zdq`PQb)YMIU<1!E=`Qy9Ct#Gi<$hQreZPo(NIZh2>p6CO`C4r8S^YPL?$OpHE zeo8|SeNzbf1`U1qIi*RWr>}s{N=p!SM+j_-27B%!Wru{NuU+`egOD+%)nqR%TFdsC zNQn}h^=_x%<@4{y_a20_uvY57%3X7}t65Q6G5#S)Coe)1@P>yV2KMXg;XsgDTL`rS z`0IzjYiX0T4w#g~S}%3U!!Xmrj^OS#c$OVeryhc_oVT0Ic;X4Ny6Xr)o>%Ty!y<6h2=TY!{#e}c?3)~IYOqlZwg1P9d~?>?WZwm)$KCW+r2x*eOC4 z%1s@KtP}UZpTC4s9${v9rwS85iO_4` zc@ak9T`GAkQMgy7I4?>iXo}=w^!Bto9<)n|w<$bC+hyHLkYY(DRw2=>r0Tsgg1xdsdS&2WUV`1 zrr?oZflK6@Su-B^g3<`_r$Mr$jjP}^6k7{58PF^j$uY;{(bG`QN;E3kCPMqYM&3I8t%7N=pD&QXqD>1m!Z~>Rb>vHfoeGePTJ?R8K|Zn12f>HGHF7!&dDd|;NMA% z&zz^2$SJFman0{wUWJRW4SHg(;ckXAfww4b(Z>7b zEAYgaUK1+_`bEL*nOu;74)E%$@XmJwwCDqW>jE?~*kQ)Rc*+UXy$(CYE;B_WMVBB2 zw_PTgPJ07pP*5o4IHol?bNfSPt~J>AjW;OD*oRRU$-wT@#a=dzkGAe7?xEGJ90w`0@Ok@c+nE&ACKD;}M-;dywF<5Q6QY1dr+jkA@IDCJDBil@2p@ zeMUZ-p^MbJGuUZe7wm+S!H=6=;Gj6>+dn|LG2G*9@#|$Dsb!9;Qj3%$sLH5svuzy-(D+R}P!(G<1lGoE_<$RFW z=euDVTrgur7Ns~o?WXGMBAx%G;~+HF>@5u&%i4LxS; z=p#@2&u_sfd#{-pSa7{yR~pfYd>fJ^jJ5=--iAE5Zbo+>MXoQr4OX~e#uwivy$U9I zCSQSK37|pmy8=rkfc8O?X~A!m{TzTiS+_@0A%mw<{(|yD$p(tTZw7g} z=NiQ?xBma~VixIGjrlH78nmV;i$%9>VU|JLX2JHq(2DxypJAOP%tAu8SjeJwenGZ; z`d?sOR20<-b3u$np@&L&g`5H{>hp@8F$>MQmC|S=Hr3DrWd_QxX{ZD5!7GrebC40_ zAUlME3?1y5_hA)fBENVaa>O+$x2CsXEH3&0k}TPhuM7*BP>rwsAAn59p80^ZSbN2R zlCL*q-hl6y6j)26-`6PMHq(7 zUj$_%X#N62;97>W)uOZ2qOXnj|4G@19eSCPu{pE@-2W+L@>UDOm-}h0z40lm`|h1( z*X>>w+|W-xS*u<_-Lg~n4Pj&da5nbqZ0y%Zo&OnlCLb8mr)_wjHrWR+_rWBU#Q7LY zJv^kxk>Nd#=v0nK&PrK3e)|QbPL2-g)iZpCd-NGThNU+t^6J2+Z^A4JaIfBkh0{9- zBL$Y{l+GboI)>AhkTfADapjkgJMI(#q*r=;uBL!X<}}HsQ<9JT|I_#{U&0*9rS*IX zOKD@t_zKqBPU}=oS;$#__A{7bJFis~Q|=#ezJ2QOlzXGIxvtm3lC)fV zIepwt!~aSqEz8(&U`GkDg{=ZB@RByv}D)X5kT#uLa-pB~1O zX%k%=#&?m)_){34g0n_&FKsl`CmAn|;3)*9mE%Y9uxN9*qgHaJB&`hi8(nmkDD&$| zh7RHz$;Z$k-M*1Ll`PNuYj_sMh4T~vijplMVzF^7CAjOud1h*^b;y=t9cGg9Nj!u) zUv`Yczl8H?ln);n!51^i_ua-77RyHw$*v_ZTCgH(IkH3^Ig&Z7Az3IpmdDb0Sdo=! zaQMAg9wQu9rU6GKvc;75-MZD~+bDDAu8o_$p4xM4pubEYlH|w8q>Qd5_+c!!N0nHI zt49gi<9M`D9=ItEz1O@ro;j-2`oB^+rFdT)kD=X4N_I3Q?gvkMWt{7ZM(4v6E&T4+ zFamER@FgSTSaeOm=lA;ZD1&pyblm0O6Y+FBpE_zDW#m1S$=vAoI2-+S-sVbNl*nC_ zrRYfHcT#%AGLg^0pF4O|WWd03%JegyRo-T2Jw?+L*$f$5VwIIlz&U-fR)5D3t3qV| z+C<)xU2Pp|5_AM)5}Y)D+COkv6?(`dR9Vj{Zb>n_xWTH^2!tpE)rA;|SII^+(X!BN z#H)lWXd;^QbPCMd&DAXtQEsI&Wod8Y_i|gRHcNutIeqVd0VC+Hpje!V#U& z#!McLMHzg&rCsuJ*s2^2;b4CTf8KUXW3I!RqIBHulSccJlt%pi;9=y+5Ef2KO#2NK zr3{XRFqyD=H62mx#~-YwgV%kiuBL)DQad=n7$|OET0CN_R-#!&!U@9nKFTvt{!vx*AZB%~SEEY@UMo z(|PfjWsN@9J#KHnzp>WsYi`-Nh3xD#E86bh6USVmRUV9Vr1{ly^VHUU2cJx*`Gc50 zzJsR#HJq5m$Hsq0rvVKmkkTA7uGd5Qu3H7EXk1Bos;5>#zA3Us9t`UTrJ&ph0c^|R z>CkJ%-{#P`dP5p=b9p}WS+Qw0Z3wNod;;CC(0DtO%kymgnqYgad35GK$Z^^XJ{4|R zam@^#Mq!Q=EFC8YHzHebBf?9`e1d-Z`0)%Lfovxmi(_Z=lw;A*0@l=Sg$*5^mRFG240Hry2B89G%XuT}?Gy*%O=JWMd zB}USTvdJJw(iu1j&63j2B56kqlQu3-)0qy#`)BdA|I4&c2qlRm%;sf5wjDsJxiUF9 zZ_lPpCiONz6n?ydG7oy|L|W_eU>j(&BMiEkz>EUk2-!9~TTa>SpBKM5rQ|GD;)VuVva3vZO=kl?X;me=P$B!AO zEC_f8?eT8&C{yr}d3+p8!RP1k*jcGc z?QMu)u6Vw;zTWMp!%!tn*+gEd%Xg1!7?3jcP!S&+ovtifIk3@socGXW0y#mnCZ-kf zc$SH!1Qa(-SwA3FpLXAB@G?Rc-d9A%AsgEz{B(S|h(vh@UM=D&({q&aP)~q_v``ax zuCgk$Ue1Wm?esJ&Gmw??F_|-!1=?bxS*{`9#VyZul{}@q)JZqU>Q{OLz8cp8w_h?^ zOa4?-F(1!n;UmRl6=q|{L=j^fL|K4;DJI4W@m4XP#O7e)d>+T<;+*+BCVrl>scf^9 zj&=c{lDrNgMa%16LR*B*^J!eg*fyU}h@G#jo$vGd{p6Qu;~&Jo058oa@+J6>`8+vs zp;A8pr!63j^+Di83TCf>Nc(L`ELI-D3Q|IBT)-`qx4m}(akm6<0gsPes;n)lZEzD& zuSZ*`WwZ{q&{9;EVfK44iTv9KN_ZyZ+vM^8IP5Cn69(=8Oe(aoa(Oyi*65@VEA%En zp)^iKHX2~=LY_g4*DNH{?7$Zm^3>E)Tj)0Junlu!9n_ng*os4o`1;IBf*uTlqRGFh zq8^%mBd-_){hKPiOBHpQfX5c`SqT+3x}e~8ZCe=dkZtgK9Mb2GobRo5)zB>#$}p2X zO2(AMB!8z~_AKU;7cZM1@cGD4gsvUQjB5xRZX5B@I>3jUhz~H*sxv|<2#NlS#XOTY z*ci@TLfgO`2Tu)KMyqY`mg18R9*uh(yd-wGpUEOgGaLO)HvF4|&yd*)iBe*kAD%Sd zONmsMys4uo(eio@l~`zi0ZO!Xji#2)JBMGXY0|fwKybU+HMrgMWYKnWW*Jy<#8RF! zvc<;Ir5z5sz&MvSbUZYH`~~ecQxv{4fhWQa8-6>1N6R-E^?00-z^BEu8d%B-7x{w0 z8(q6dG!U@S;hQ#)T}$~q*k{AvF6CEgo6zdsT+5C4+A^LJHbi;xn-zRgwz7Z792}5y zuy0@v4us6XK79@j=r|KAc?GoDu(^^VE0{r>(3wiUKpeKQ?fB+uic!X|=IP>yjXjE& z*3xWjS9ux&v|o?W8gJhqJ#w4#a`NvcRFNHM*Q+kCAivLD z#izkh8$Mper@%29Z52;SJvu}KXnKa7OX!)UW493*arQd?;tx7#WWwHQJ83&*Q#yxW zNgb|7sk$6a4uQKq+%3LNju0*JQ$yfR4Tn25S_eEWxd_jrKjo|qn>O>&7*WksHNk;hGKa*+8D>h7Ej7@S^~( z+wd0~_;k2v!w)u)&7rqtBTt!l(Z&iE1?W16r;1bLeh2i}@T(ev(rDg+ zWn1`0JXy=%kPRBtAs5eK2)bPoxv+)&M0$U^g}*DurvcvB%B!+_Y32rP zWS>-w{sG14qsbaDPW}3V=(C}}j=bD{8$Mmf7r{*%-l!ujzGXwZo3xm|sXh@Gx%ppZ zrS`GxE?!27nJ4d}+=l^wa~JJ{2JF3yM@L)iL_q$7FuOtz#jHnIv=V{wck>manVn@h zJayzdno7e_E65_CElQY4{{!_cTKP7FHnZ&YCXRb zf3cjWMG)-_yFw4qj_x&5;-sm8AEeETUXw7zL%Nr5#|jV6PR%EZgJ*|@_F=kLNLWZ4 z3iZBkdU$PG(U3lDc>0p*>B|!P!J;o?FyQ5-bQ9vYUYbyc9nWl`;}m-%-=5|ef~Ryi z?NXg~sU2Tv-Zx-?Vtuc?x%EHC0_UQY?_&|&3rmVrAz1qJr;RWeKuwH z@TF$TjK$;Mn|V5!(TD(Lyz1210iJ1~JN{ro=ubIRO4zl(3;=M7CbVmFPk9B1t2n=H=!_jW0l_!k1z-TWnT$NhWw zKS%(Z@8_?`i;PoS`6G0ZQ9a#C&Ir&AL(2pFbLcl9?By@PO@sRKUQX8o64l5DIncS0 zI`+qsl^FHyNBAnBE7=(b`Buh?)V4=?G2;&7edrAE1pKm%ziKNr3MJgZ5?0l@aKs@V zqrP{DZ(wvazvMCAL$4`^`D3hFJ#(1v0OnHXJV~AbbE^$caT8-rYT#)eZe{`e;j{cJ zO6%`F#*dK<(x2m*B$7Ly<3)mXs7GGlc1E{Ko;%LhGZ&gpkS=y&#tDi&PGZRk64Y7s zPbYZ3pxi>_NlIj$RU1$8RX+K(iB`&mt5dx z1Eo?|zRBxZwtD7Geu0s<_~2z;MX$eK<~6*;VLNs@p4J$G=Y$%UDJ z;A>bw{ox<@FF4z!&VGxZVl=Y;w<%3TtM_|<;;&OOV$oHeM_clJSNTtQhp7{%yhDs$ zS1aG)VFp@IH9dSTiR+mjp2E+YF5sIzyi9bP=yFTqU-?ab%~XyR+j*2a{cn6H&`%FK zuX7(KEl`th@E(90CNzIUsThiP>*c%8gqJ_!e-Qnm-=zMmm#<*9KB2@#E-R-P^QJ03 z=8^Q91dREFzsD{f>f=kLB9->>d+4VK=lVzkZkaIozj;3)RO9~1|C`A|ct7s|qe1Gq z0e|~B`6U+o;B#J1IfeW$c&aJJER&~=IQ107n*0hAkQajweL+4;E*}4aS4}!^3YQQ$ z%P9!C$F;0+nYZR{f-?gYnv1l~b(nb;VuYV5CAjz|QE*_xP5vA&G?(JIFDd>m!on{p z)l`b^FDb`UfscI2w?|c&OSRrCnJdguk{4G!773K@*rUKcpAAL)M@50V+d3?lS!!@nz-T_kBlLe8H#VrmBp@UR! zY7lj}VIoAgO%Nt_xNVAD<`)y`e6+WM@hcln8gB8 zxGswrjjJsphqO*B|HLAu(}JM#S_)OQszR%nK{KnBAFv7s&3c!WkW}eaEjBu?a_Z%` zZPeSPm&e=1T$(klTa#U+)2wObPPLhss?#u1c~6NEJxz(kzzLVnsNI`k;2F+9w}Ck!J}6y#)pfD z(dR7}bP+j7MEO!pE@Dx*$fkMohl^)M6dIKDgmPy+1x#cC)uafqmTUV`q*wvhE%^IL z@dWf)&>JNl06LBOB1#-GRY^Q_B-J)X(C^NeRZdo3`sE_>tn=1b{T&jBT~u|wRXCWN2x0rfyykH zROgg^T$0by8YrTzr5u4)PZLP>!_Pvr#tQ5lC&rN)y)jNq#L8Ha4Lhv3H&)EcFJuYJ zUE7-zJT8j&GXj2BZGwI_nn1rxNT5>_>8@pLt|vP|U(MbvTN3tW7bY9`X&e`bXc)_f+&^mqy$k7r>wXqL7c!d zi6RzGTh(ic!o^HywSm;(=}98Ss%!9h>jf*;PZ9y>wyJ-dB=Q(sv0{9(@Ia3h_a}>b zxNgO-l0`h+u;RGM;!e?Ty=ldk$zn6_wOa7clf@PuW{Xi5O(7AJKK&>~G?2u;m1;Sr@d^X`7{KKO;qFQ9z z6gwJoMH-gmif56}5RqXeB0TB#PY!YH46$rnd0bJz=PYV;n{)h>>)TjMUs}2<7kA7M zyWpnr&`j~T+?t=9DLTUG*C9(5lk()_-o+%e592Wk2NrO2s(nu;H~*5jROji{EirDB^+#i2M0D^{Y}biIGSrl!*+Wm~wfU z$P_(>UK`$5CN2qP?=|4ca&eb6-xf|kn{hIG1in%(K8cL7o13@y7*e#Ay0MOayigFM zzFr|-0bXrNRNGdG1p0xU`pZg@%F=oa_Ij5mFPl8%1(dMzX-66xYw5?P?s~u39@yR- zgrIxXYs5}+QTMJ9_fm42uN76=M!ca)*kl2zFa_UOPCn-I>%`MgWXIAw#dTweJ^aap r`FM4`n5=GCFYabgVOL+T7JZCY+N;%=2.0.0" } diff --git a/web-client/package.json b/web-client/package.json index d2f3b867..1ad7a60d 100644 --- a/web-client/package.json +++ b/web-client/package.json @@ -1,7 +1,7 @@ { "name": "web-client", "private": true, - "version": "0.7.23", + "version": "0.7.24", "type": "module", "packageManager": "npm@11.7.0", "volta": { @@ -17,8 +17,6 @@ "generate:actors-abi": "node scripts/generate-actors-abi-manifest.mjs", "check:actors-abi": "node scripts/generate-actors-abi-manifest.mjs --check", "check:actors-normative-drift": "node scripts/check-actors-normative-drift.mjs", - "generate:actors-bonds": "cd ../template && cargo run -q --locked -p deos-runtime --example trigger_bond_vectors -- ../web-client/src/lib/automation/actors-trigger-bond-vectors.json", - "check:actors-bonds": "cd ../template && cargo run -q --locked -p deos-runtime --example trigger_bond_vectors -- --check ../web-client/src/lib/automation/actors-trigger-bond-vectors.json", "generate:observation-evidence": "node scripts/generate-observation-runtime-evidence.mjs", "check:observation-evidence": "node scripts/generate-observation-runtime-evidence.mjs --check", "generate:ingress-evidence": "node scripts/generate-ingress-runtime-evidence.mjs", @@ -27,7 +25,7 @@ "format:check": "prettier --check .", "test:automation": "node --test scripts/test-actors-*.mjs", "test:wiki-relations": "node --test scripts/test-wiki-relations.mjs", - "validate": "npm run prepare && npm run papi:generate && npm run format:check && npm run check:actors-abi && npm run check:actors-bonds && npm run check:observation-evidence && npm run check:ingress-evidence && npm run test:automation && npm run test:wiki-relations && npm run check && npm run build", + "validate": "npm run prepare && npm run papi:generate && npm run format:check && npm run check:actors-abi && npm run check:observation-evidence && npm run check:ingress-evidence && npm run test:automation && npm run test:wiki-relations && npm run check && npm run build", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" }, diff --git a/web-client/scripts/check-actors-normative-drift.mjs b/web-client/scripts/check-actors-normative-drift.mjs index 3c0919a8..15598809 100644 --- a/web-client/scripts/check-actors-normative-drift.mjs +++ b/web-client/scripts/check-actors-normative-drift.mjs @@ -46,26 +46,14 @@ function rustVariantNames(block) { } function specEvents() { - const block = specSection( - '## 8. Events and Ordering', - '## 9. ABI, Errors, Storage, and Upgrades', - ); - return block - .split('\n') - .map((line) => line.match(/^([A-Z][A-Za-z0-9_]*)\s*\{([^}]*)\}$/)) - .filter(Boolean) - .map((match) => ({ - name: match[1], - fields: match[2] - .split(',') - .map((field) => field.trim()) - .filter(Boolean) - .map((field) => field.split(':')[0].trim()), - })); + return specTypeSurface('Event'); } function specErrors() { - const block = specSection('### 9.2 Errors', '### 9.3 Storage Contract'); + const block = specSection( + '### 12.4 Errors and projections', + '## 13. Storage, upgrades, configuration, and conformance', + ); const body = block.match(/enum Error\s*\{([\s\S]*?)\}/)?.[1]; assert.ok(body, 'spec Error enum body is missing'); return splitTopLevel(body) @@ -149,7 +137,10 @@ function specStructFields(name) { } function specCalls() { - const block = specSection('### 7.1 Calls', '### 7.2 Simulation'); + const block = specSection( + '### 12.1 Calls and authorization', + '### 12.2 Events', + ); const contract = block.match(/```text\n([\s\S]*?)```/); assert.ok(contract, 'calls contract block not found'); return [...contract[1].matchAll(/\b[a-z][a-z0-9_]+\b/g)].map( @@ -215,7 +206,7 @@ const headingNumbers = new Set( (match) => match[1], ), ); -for (const match of spec.matchAll(/\bSections? ([0-9]+(?:\.[0-9]+)?)/g)) { +for (const match of spec.matchAll(/(?:\bSections? |§)([0-9]+(?:\.[0-9]+)?)/g)) { if (!headingNumbers.has(match[1])) { failures.push(`section reference: missing Section ${match[1]}`); } diff --git a/web-client/scripts/generate-actors-abi-manifest.mjs b/web-client/scripts/generate-actors-abi-manifest.mjs index 5f8fe24f..bdddfd68 100644 --- a/web-client/scripts/generate-actors-abi-manifest.mjs +++ b/web-client/scripts/generate-actors-abi-manifest.mjs @@ -196,7 +196,19 @@ function decodeUnsignedConstant(manifest, name, bytes) { ); } const encoded = Buffer.from(constant.value.slice(2), 'hex'); - return bytes === 1 ? encoded.readUInt8(0) : encoded.readUInt32LE(0); + const decoded = + bytes === 1 + ? BigInt(encoded.readUInt8(0)) + : bytes === 4 + ? BigInt(encoded.readUInt32LE(0)) + : encoded.readBigUInt64LE(0); + const value = Number(decoded); + if (!Number.isSafeInteger(value)) { + throw new Error( + `Actors constant ${name} exceeds browser-safe integer range`, + ); + } + return value; } const manifest = await buildManifest(); @@ -205,6 +217,7 @@ const generatedBounds = await format( `/* Generated from Actors runtime metadata ${manifest.metadata.sha256}; do not edit. */\n` + `export const ACTORS_MAX_CONTRACT_STEPS = ${decodeUnsignedConstant(manifest, 'MaxContractSteps', 4)};\n` + `export const ACTORS_MAX_EXECUTION_DELAY_BLOCKS = ${decodeUnsignedConstant(manifest, 'MaxExecutionDelayBlocks', 4)};\n` + + `export const ACTORS_MAX_TEMPORAL_DELAY_TICKS = ${decodeUnsignedConstant(manifest, 'MaxTemporalDelayTicks', 8)};\n` + `export const ACTORS_MAX_RETRY_ATTEMPTS = ${decodeUnsignedConstant(manifest, 'MaxRetryAttempts', 4)};\n` + `export const ACTORS_MAX_OPENING_SNAPSHOT_ENTRIES = ${decodeUnsignedConstant(manifest, 'MaxOpeningSnapshotEntries', 4)};\n` + `export const ACTORS_MAX_OPENING_PREDICATE_RESULTS = ${decodeUnsignedConstant(manifest, 'MaxOpeningPredicateResults', 4)};\n` + diff --git a/web-client/scripts/test-actors-abi-manifest.mjs b/web-client/scripts/test-actors-abi-manifest.mjs index d54afdbb..5972cb5f 100644 --- a/web-client/scripts/test-actors-abi-manifest.mjs +++ b/web-client/scripts/test-actors-abi-manifest.mjs @@ -19,12 +19,12 @@ function graphNode(path) { ); } -test('metadata exposes the current eight-step execution-plan baseline', () => { +test('metadata exposes the current thirty-two-step execution-plan baseline', () => { const bound = manifest.pallet.constants.find( (constant) => constant.name === 'MaxContractSteps', ); assert(bound, 'MaxContractSteps must remain public metadata'); - assert.equal(bound.value, '0x08000000'); + assert.equal(bound.value, '0x20000000'); }); test('metadata exposes the protocol-fixed retry-attempt bound', () => { diff --git a/web-client/scripts/test-actors-analysis.mjs b/web-client/scripts/test-actors-analysis.mjs index 4a135a5d..0d37fb12 100644 --- a/web-client/scripts/test-actors-analysis.mjs +++ b/web-client/scripts/test-actors-analysis.mjs @@ -877,6 +877,7 @@ test('trigger analysis projects one exact scalar trigger without runtime proof', assert.equal(observation.completionPolicy, 'Persistent'); assert.deepEqual(observation.trigger, { kind: 'ObservationChange', + afterTicks: null, everyTicks: null, sourceKinds: ['ObservationChange'], observationFeeds: [ @@ -919,6 +920,7 @@ test('trigger analysis projects one exact scalar trigger without runtime proof', ); assert.deepEqual(crossing.trigger, { kind: 'ObservationCrossing', + afterTicks: null, everyTicks: null, sourceKinds: ['ObservationCrossing'], observationFeeds: observation.trigger.observationFeeds, @@ -956,6 +958,29 @@ test('trigger analysis projects one exact scalar trigger without runtime proof', ), ); + const oneShot = analyze( + artifactFor({ + contract: contractWithTrigger({ + type: 'AtTime', + value: { after_ticks: 10n }, + }), + }), + ); + assert.deepEqual(oneShot.trigger, { + kind: 'AtTime', + afterTicks: 10, + everyTicks: null, + sourceKinds: [], + observationFeeds: [], + }); + assert( + oneShot.findings.some( + (finding) => + finding.kind === 'OneShotTemporalAdmission' && + finding.afterTicks === 10, + ), + ); + const periodic = analyze( artifactFor({ contract: contractWithTrigger({ @@ -966,6 +991,7 @@ test('trigger analysis projects one exact scalar trigger without runtime proof', ); assert.deepEqual(periodic.trigger, { kind: 'Cadenced', + afterTicks: null, everyTicks: 10, sourceKinds: [], observationFeeds: [], diff --git a/web-client/scripts/test-actors-authoring.mjs b/web-client/scripts/test-actors-authoring.mjs index 24cb98e0..8d77f530 100644 --- a/web-client/scripts/test-actors-authoring.mjs +++ b/web-client/scripts/test-actors-authoring.mjs @@ -157,14 +157,19 @@ test('authoring controls cover every current task and predicate variant', () => assert.equal(ACTORS_AUTHORING_CONDITION_TYPES.length, 10); }); -test('one metadata-aligned eight-step baseline applies to both actor classes', () => { - const steps = Array.from({ length: 9 }, (_, index) => +test('one metadata-aligned zero-to-thirty-two-step baseline applies to both actor classes', () => { + const steps = Array.from({ length: 33 }, (_, index) => authoringStep(`step-${index}`), ); for (const actorType of ['User', 'System']) { assert.equal( - validateActorAuthoringContract(contract(steps.slice(0, 8), { actorType })) - .valid, + validateActorAuthoringContract(contract([], { actorType })).valid, + true, + ); + assert.equal( + validateActorAuthoringContract( + contract(steps.slice(0, 32), { actorType }), + ).valid, true, ); const tooLong = validateActorAuthoringContract( @@ -175,7 +180,7 @@ test('one metadata-aligned eight-step baseline applies to both actor classes', ( assert( tooLong.issues.some( (issue) => - issue.path === 'steps' && /1\.\.8 steps/.test(issue.message), + issue.path === 'steps' && /0\.\.32 steps/.test(issue.message), ), ); } @@ -765,13 +770,12 @@ test('typed validation rejects control-flow-adjacent and runtime-invalid drafts' splitValidation.issues.some((issue) => /exceed/.test(issue.message)), ); } - assert.equal(validateActorAuthoringContract(contract([])).valid, false); for (const overrides of [ { trigger: { type: 'Cadenced', everyTicks: 0 } }, { trigger: { type: 'Cadenced', - everyTicks: DEOS_ACTORS_AUTHORING_LIMITS.maxCadenceTicks + 1, + everyTicks: DEOS_ACTORS_AUTHORING_LIMITS.maxTemporalTicks + 1, }, }, { @@ -986,6 +990,15 @@ test('trigger, completion, and funding policy variants lower as typed ActorContr trigger: { type: 'Manual' }, fundingPolicy: { type: 'OwnerOnly' }, }), + contract(undefined, { + trigger: { type: 'AtTime', afterTicks: 10 }, + cooldownBlocks: 0, + scheduleWindow: null, + steps: [], + mutability: 'Immutable', + autoCloseAtCycleNonce: 1n, + fundingPolicy: { type: 'OwnerOnly' }, + }), contract(undefined, { trigger: { type: 'Cadenced', everyTicks: 10 }, fundingPolicy: { type: 'RuntimePolicy' }, @@ -1021,7 +1034,7 @@ test('trigger, completion, and funding policy variants lower as typed ActorContr true, ); } - const lowered = lowerActorAuthoringContract(drafts[2]); + const lowered = lowerActorAuthoringContract(drafts[3]); assert.equal(lowered.trigger.type, 'AddressEvent'); assert.deepEqual( lowered.trigger.value.asset_filter.value.map((asset) => asset.type), diff --git a/web-client/scripts/test-actors-client-storage.mjs b/web-client/scripts/test-actors-client-storage.mjs new file mode 100644 index 00000000..a912a0e9 --- /dev/null +++ b/web-client/scripts/test-actors-client-storage.mjs @@ -0,0 +1,37 @@ +/* +Domain: Actors browser storage boundaries +Owns: Static regression evidence that browser summaries consume compact canonical heads. +Excludes: Runtime API execution, full Contract reconstruction, archive history, and transport behavior. +Zone: Web-client validation entrypoint; prevents retired monolithic storage and physical tail/payload reads. +*/ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import test from 'node:test'; + +const adapter = await readFile( + new URL('../src/lib/adapters/blockchain/index.ts', import.meta.url), + 'utf8', +); +const upgradeEvidence = await readFile( + new URL('./upgrade-state-evidence.mjs', import.meta.url), + 'utf8', +); + +test('browser Actor summaries consume compact Contract and Run heads only', () => { + assert.match(adapter, /Actors\.ActorContractHead\.getValue/); + assert.match(adapter, /Actors\.ActorRunHead\.getValue/); + assert.doesNotMatch(adapter, /Actors\.ActorContract\.getValue/); + assert.doesNotMatch(adapter, /Actors\.ActorRunState\.getValue/); + assert.doesNotMatch(adapter, /Actors\.ActorContractTailChunk\.getValue/); + assert.doesNotMatch(adapter, /Actors\.ActorRunPayload\.getValue/); +}); + +test('upgrade evidence records the compact Contract head without reconstructing tails', () => { + assert.match(upgradeEvidence, /Actors\.ActorContractHead\.getValue/); + assert.match(upgradeEvidence, /actor_contract_head: actorContractHead/); + assert.doesNotMatch(upgradeEvidence, /Actors\.ActorContract\.getValue/); + assert.doesNotMatch( + upgradeEvidence, + /Actors\.ActorContractTailChunk\.getValue/, + ); +}); diff --git a/web-client/scripts/test-actors-cost-projection.mjs b/web-client/scripts/test-actors-cost-projection.mjs new file mode 100644 index 00000000..e81bd10b --- /dev/null +++ b/web-client/scripts/test-actors-cost-projection.mjs @@ -0,0 +1,345 @@ +/* +Domain: Actors cost projection validation +Owns: Fail-closed fixtures for independent runtime cost owners and actual Action receipts. +Excludes: Metadata generation, chain transport, fee policy, and historical event indexing. +Zone: Web-client validation entrypoint; imports only the automation cost contract. +*/ +import { + decAnyMetadata, + unifyMetadata, +} from '@polkadot-api/substrate-bindings'; +import assert from 'node:assert/strict'; +import { createHash } from 'node:crypto'; +import { readFile } from 'node:fs/promises'; +import test from 'node:test'; + +import { readActorCost } from '../src/lib/adapters/blockchain/actor-cost.ts'; +import { + ACTORS_COST_VECTORS, + parseActorCostVectors, +} from '../src/lib/automation/cost-vectors.ts'; +import { + ACTORS_COST_RUNTIME_API, + ACTORS_COST_RUNTIME_API_VERSION, + projectActorActionFeeReceipt, + projectActorCostQuote, +} from '../src/lib/automation/cost.ts'; + +const metadataBytes = new Uint8Array( + await readFile(new URL('../.papi/metadata/deos.scale', import.meta.url)), +); +const costVectorArtifact = JSON.parse( + await readFile( + new URL('../src/lib/automation/actors-cost-vectors.json', import.meta.url), + 'utf8', + ), +); + +const hash = (byte) => new Uint8Array(32).fill(byte); +const weight = (refTime, proofSize) => ({ + ref_time: BigInt(refTime), + proof_size: BigInt(proofSize), +}); + +function activeUserQuote() { + return { + success: true, + value: { + actor_type: { type: 'User', value: undefined }, + creation_fee: 20n, + prospective_trigger_fee: { + trigger_family: { type: 'Manual', value: undefined }, + maximum_weight: weight(113_494_000, 9_635), + fee: 114n, + production_weight_identity: hash(1), + }, + prospective_pipeline_fee: { + pipeline_machine_fee: 1_000n, + cleanup_fee: 600n, + total_fee: 1_600n, + strategy: { type: 'UpfrontBounded', value: undefined }, + admission_identity: hash(2), + production_weight_identity: hash(3), + }, + maximum_next_action_fee: { + maximum_effect_weight: weight(280_000_000, 13_000), + maximum_effect_fee: 280n, + production_weight_identity: hash(4), + }, + actor_state_hold: { + exempt: false, + base_per_component: 10n, + per_encoded_byte: 1n, + breakdown: { + identity: 11n, + contract_head: 12n, + contract_body: 0n, + detector: 13n, + funding: 14n, + run: 0n, + }, + total: 50n, + }, + }, + }; +} + +test('metadata exposes one versioned Actor cost quote with canonical input', () => { + assert.equal(ACTORS_COST_RUNTIME_API, 'ActorCostApi_actor_cost_quote'); + const metadata = unifyMetadata(decAnyMetadata(metadataBytes)); + const apis = metadata.apis.filter( + (candidate) => candidate.name === 'ActorCostApi', + ); + assert.equal(apis.length, 1); + assert.equal(apis[0].version, ACTORS_COST_RUNTIME_API_VERSION); + const methods = apis[0].methods.filter( + (candidate) => candidate.name === 'actor_cost_quote', + ); + assert.equal(methods.length, 1); + assert.deepEqual( + methods[0].inputs.map((input) => input.name), + ['actor_id'], + ); +}); + +test('cost projection keeps every economic owner and provenance separate', () => { + const quote = projectActorCostQuote(activeUserQuote()); + assert.equal(quote.actorType, 'User'); + assert.equal(quote.creationFee, 20n); + assert.deepEqual(quote.prospectiveTriggerFee, { + family: 'Manual', + maximumWeight: { refTime: 113_494_000n, proofSize: 9_635n }, + fee: 114n, + productionWeightIdentity: `0x${'01'.repeat(32)}`, + }); + assert.deepEqual(quote.prospectivePipelineFee, { + machineFee: 1_000n, + cleanupFee: 600n, + totalFee: 1_600n, + strategy: 'UpfrontBounded', + admissionIdentity: `0x${'02'.repeat(32)}`, + productionWeightIdentity: `0x${'03'.repeat(32)}`, + }); + assert.equal(quote.maximumNextActionFee.maximumEffectFee, 280n); + assert.equal(quote.stateHold.total, 50n); + assert.equal('activationTotal' in quote, false); + assert.equal('remainingMachineBudget' in quote, false); +}); + +test('dormant and System quotes preserve absence and explicit exemption', () => { + const dormant = activeUserQuote(); + dormant.value.prospective_trigger_fee = undefined; + dormant.value.prospective_pipeline_fee = undefined; + dormant.value.maximum_next_action_fee.maximum_effect_weight = weight(0, 0); + dormant.value.maximum_next_action_fee.maximum_effect_fee = 0n; + const dormantView = projectActorCostQuote(dormant); + assert.equal(dormantView.prospectiveTriggerFee, null); + assert.equal(dormantView.prospectivePipelineFee, null); + assert.equal(dormantView.maximumNextActionFee.maximumEffectFee, 0n); + + const system = activeUserQuote(); + system.value.actor_type = { type: 'System', value: undefined }; + system.value.creation_fee = 0n; + system.value.prospective_trigger_fee.fee = 0n; + system.value.prospective_pipeline_fee.pipeline_machine_fee = 0n; + system.value.prospective_pipeline_fee.cleanup_fee = 0n; + system.value.prospective_pipeline_fee.total_fee = 0n; + system.value.maximum_next_action_fee.maximum_effect_fee = 0n; + system.value.actor_state_hold.exempt = true; + for (const component of Object.keys( + system.value.actor_state_hold.breakdown, + )) { + system.value.actor_state_hold.breakdown[component] = 0n; + } + system.value.actor_state_hold.total = 0n; + const systemView = projectActorCostQuote(system); + assert.equal(systemView.actorType, 'System'); + assert.equal(systemView.stateHold.exempt, true); + assert.equal(systemView.stateHold.total, 0n); +}); + +test('cost projection rejects unknown variants and inconsistent named totals', () => { + assert.throws( + () => + projectActorCostQuote({ + success: false, + value: { type: 'FutureFailure', value: undefined }, + }), + /Unsupported runtime Actor cost error FutureFailure/, + ); + const unknownStrategy = activeUserQuote(); + unknownStrategy.value.prospective_pipeline_fee.strategy = { + type: 'RefundAfterRun', + value: undefined, + }; + assert.throws( + () => projectActorCostQuote(unknownStrategy), + /Unsupported runtime Pipeline strategy RefundAfterRun/, + ); + const badPipeline = activeUserQuote(); + badPipeline.value.prospective_pipeline_fee.total_fee = 1_601n; + assert.throws( + () => projectActorCostQuote(badPipeline), + /Pipeline total must equal Machine plus cleanup fees/, + ); + const badHold = activeUserQuote(); + badHold.value.actor_state_hold.total = 51n; + assert.throws( + () => projectActorCostQuote(badHold), + /state hold total must equal its named components/, + ); +}); + +test('finalized cost transport invokes the typed API once at the requested block', async () => { + const calls = []; + const typedApi = { + apis: { + ActorCostApi: { + actor_cost_quote: async (...args) => { + calls.push(args); + return activeUserQuote(); + }, + }, + }, + }; + const result = await readActorCost(typedApi, '0x1234', 7); + assert.deepEqual(calls, [[7n, { at: '0x1234' }]]); + assert.equal(result.projection?.actorType, 'User'); + assert.equal(result.unavailableReason, null); + + typedApi.apis.ActorCostApi.actor_cost_quote = async () => { + throw new Error('unavailable'); + }; + assert.deepEqual(await readActorCost(typedApi, '0x1234', 7), { + projection: null, + unavailableReason: 'unavailable', + }); +}); + +test('ActionFeeCharged projects exact coordinates, actual Weight, and zero fee', () => { + assert.deepEqual( + projectActorActionFeeReceipt({ + actor_id: 7n, + cycle_nonce: 9n, + step_index: 3, + actual_effect_weight: weight(0, 0), + fee: 0n, + }), + { + actorId: 7n, + cycleNonce: 9n, + stepIndex: 3, + actualEffectWeight: { refTime: 0n, proofSize: 0n }, + fee: 0n, + }, + ); +}); + +test('runtime-generated cost vectors bind metadata, Weight, geometry, and Trigger families', async () => { + const metadataSha256 = createHash('sha256') + .update(metadataBytes) + .digest('hex'); + const weights = await readFile( + new URL( + '../../template/runtime/src/weights/pallet_deos_actors.rs', + import.meta.url, + ), + ); + assert.equal(ACTORS_COST_VECTORS.metadataSha256, metadataSha256); + assert.equal( + ACTORS_COST_VECTORS.weightSha256, + createHash('sha256').update(weights).digest('hex'), + ); + assert.equal(ACTORS_COST_VECTORS.runtimeApiVersion, 1); + assert.equal(ACTORS_COST_VECTORS.vectors.length, 12); + + const manual = ACTORS_COST_VECTORS.vectors + .filter( + (vector) => + vector.quote.actorType === 'User' && vector.triggerFamily === 'Manual', + ) + .sort((left, right) => left.contractStepCount - right.contractStepCount); + assert.deepEqual( + manual.map((vector) => vector.contractStepCount), + [0, 1, 4, 8, 32], + ); + assert.equal(manual[0].quote.maximumNextActionFee.maximumEffectFee, 0n); + assert.equal(manual[1].quote.stateHold.components.contractBody, 0n); + assert.ok(manual[2].quote.stateHold.components.contractBody > 0n); + assert.deepEqual( + manual + .slice(1) + .map((vector) => vector.quote.maximumNextActionFee.maximumEffectFee), + Array(4).fill(manual[1].quote.maximumNextActionFee.maximumEffectFee), + 'unreachable tails must not inflate the current Action maximum', + ); + assert.ok( + manual[4].quote.prospectivePipelineFee.totalFee > + manual[1].quote.prospectivePipelineFee.totalFee, + 'complete upfront Pipeline Machine authority grows with admitted geometry', + ); + + const families = new Set( + ACTORS_COST_VECTORS.vectors + .filter( + (vector) => + vector.quote.actorType === 'User' && vector.contractStepCount === 1, + ) + .map((vector) => vector.triggerFamily), + ); + assert.deepEqual( + families, + new Set([ + 'Manual', + 'AddressEvent', + 'ObservationChange', + 'ObservationCrossing', + 'AtTime', + 'Cadenced', + ]), + ); +}); + +test('generated cost vectors preserve explicit System exemption and dormant absence', () => { + const system = ACTORS_COST_VECTORS.vectors.find( + (vector) => vector.name === 'system-manual-1', + ).quote; + assert.equal(system.creationFee, 0n); + assert.equal(system.prospectiveTriggerFee.fee, 0n); + assert.equal(system.prospectivePipelineFee.totalFee, 0n); + assert.equal(system.maximumNextActionFee.maximumEffectFee, 0n); + assert.equal(system.stateHold.exempt, true); + assert.equal(system.stateHold.total, 0n); + + const dormant = ACTORS_COST_VECTORS.vectors.find( + (vector) => vector.name === 'user-dormant', + ).quote; + assert.equal(dormant.prospectiveTriggerFee, null); + assert.equal(dormant.prospectivePipelineFee, null); + assert.equal(dormant.maximumNextActionFee.maximumEffectFee, 0n); + assert.ok(dormant.stateHold.total > 0n); +}); + +test('generated cost vector parser fails closed on drift and malformed ownership', () => { + const unsupported = structuredClone(costVectorArtifact); + unsupported.formatVersion = 2; + assert.throws(() => parseActorCostVectors(unsupported), /unsupported/); + + const malformedTotal = structuredClone(costVectorArtifact); + malformedTotal.vectors.find( + (vector) => vector.name === 'user-manual-1', + ).quote.prospectivePipelineFee.totalFee = '1'; + assert.throws( + () => parseActorCostVectors(malformedTotal), + /total is inconsistent/, + ); + + const missingGeometry = structuredClone(costVectorArtifact); + missingGeometry.vectors = missingGeometry.vectors.filter( + (vector) => vector.name !== 'user-manual-32', + ); + assert.throws( + () => parseActorCostVectors(missingGeometry), + /0\/1\/4\/8\/32 geometry/, + ); +}); diff --git a/web-client/scripts/test-actors-eligibility.mjs b/web-client/scripts/test-actors-eligibility.mjs index 0ef66ee3..5a46b98a 100644 --- a/web-client/scripts/test-actors-eligibility.mjs +++ b/web-client/scripts/test-actors-eligibility.mjs @@ -19,7 +19,6 @@ import test from 'node:test'; import { ACTORS_ELIGIBILITY_RUNTIME_API, ACTORS_ELIGIBILITY_RUNTIME_API_VERSION, - ACTORS_TRIGGER_STATE_BOND_RUNTIME_API, ACTOR_CLOSE_REASONS, projectActorEligibility, } from '../src/lib/automation/eligibility.ts'; @@ -72,23 +71,14 @@ function encodeProjection(value) { return decoded; } -test('runtime metadata binds the canonical eligibility API signature', () => { +test('runtime metadata omits the retired Trigger-state bond quote', () => { const { metadata } = eligibilityMethod(); const api = metadata.apis.find( (candidate) => candidate.name === 'ActorEligibilityApi', ); - const bondMethod = api.methods.filter( - (candidate) => candidate.name === 'trigger_state_bond', - ); - assert.equal(bondMethod.length, 1, 'trigger_state_bond must appear once'); - assert.deepEqual( - bondMethod[0].inputs.map((input) => input.name), - ['trigger'], - 'bond quote must consume exactly one authored Trigger', - ); assert.equal( - ACTORS_TRIGGER_STATE_BOND_RUNTIME_API, - 'ActorEligibilityApi_trigger_state_bond', + api.methods.some((candidate) => candidate.name === 'trigger_state_bond'), + false, ); }); @@ -131,6 +121,35 @@ test('projectActorEligibility projects canonical active classification', () => { }); }); +test('projectActorEligibility preserves AtTime one-shot consumption', () => { + const decoded = encodeProjection({ + success: true, + value: { + type: 'Active', + value: { + trigger: { + type: 'AtTime', + value: { after_ticks: 20n, consumed: true }, + }, + pending_signal: false, + placement: { type: 'Unplaced', value: undefined }, + eligibility: { + terminal_reason: undefined, + execution_phase: { type: 'WaitingSignal', value: undefined }, + }, + }, + }, + }); + assert.deepEqual(projectActorEligibility(decoded), { + type: 'Active', + trigger: { type: 'AtTime', afterTicks: 20n, consumed: true }, + pendingSignal: false, + placement: { type: 'Unplaced' }, + terminalReason: null, + executionPhase: { type: 'WaitingSignal' }, + }); +}); + test('projectActorEligibility preserves absence, dormancy, terminal reason, and execution phase', () => { for (const type of ['NotRegistered', 'Dormant']) { const decoded = encodeProjection({ @@ -225,12 +244,9 @@ test('projectActorEligibility preserves semantic Crossing activation state', () test('projectActorEligibility rejects a typed runtime failure honestly', () => { const decoded = encodeProjection({ success: false, - value: { type: 'ContinuationInvariant' }, + value: { type: 'RunInvariant' }, }); - assert.throws( - () => projectActorEligibility(decoded), - /ContinuationInvariant/, - ); + assert.throws(() => projectActorEligibility(decoded), /RunInvariant/); }); test('projectActorEligibility rejects unknown runtime variants and malformed results', () => { diff --git a/web-client/scripts/test-actors-matching-wasm.mjs b/web-client/scripts/test-actors-matching-wasm.mjs index 297a86b9..90400fd3 100644 --- a/web-client/scripts/test-actors-matching-wasm.mjs +++ b/web-client/scripts/test-actors-matching-wasm.mjs @@ -82,7 +82,7 @@ const artifact = createActorContractArtifact({ const base = { artifact, actorId: 14n, - mode: 'CurrentContinuation', + mode: 'CurrentRun', metadataBytes, runtime, runtimeCodeBytes: Uint8Array.of(1, 2, 3), @@ -102,7 +102,7 @@ const suspendedRuntimeValue = { status: { type: 'Suspended', value: undefined }, cycle_nonce: 7n, start_cursor: 0, - continuation_cursor: 1, + run_cursor: 1, unsuccessful_attempts_at_cursor: 2, cumulative_outcomes: { executed_steps: 3, @@ -129,7 +129,7 @@ const suspendedOutcome = { closeReason: null, cycleNonce: 7n, startCursor: 0, - continuationCursor: 1, + runCursor: 1, unsuccessfulAttemptsAtCursor: 2, cumulativeOutcomes: { executedSteps: 3, @@ -168,7 +168,7 @@ test('runtime API result codec discovers metadata and preserves bounded evidence status: { type: 'Completed', value: undefined }, cycle_nonce: 8n, start_cursor: 0, - continuation_cursor: undefined, + run_cursor: undefined, cumulative_outcomes: { executed_steps: 1, committed_effectful_tasks: 0, @@ -203,7 +203,7 @@ test('runtime API result codec discovers metadata and preserves bounded evidence type: 'Closed', value: { type: 'ProductiveCycleCompleted', value: undefined }, }, - continuation_cursor: undefined, + run_cursor: undefined, unsuccessful_attempts_at_cursor: undefined, }, }); @@ -212,7 +212,7 @@ test('runtime API result codec discovers metadata and preserves bounded evidence if (closed.success) { assert.equal(closed.outcome.status, 'Closed'); assert.equal(closed.outcome.closeReason, 'ProductiveCycleCompleted'); - assert.equal(closed.outcome.continuationCursor, null); + assert.equal(closed.outcome.runCursor, null); assert.equal(closed.outcome.unsuccessfulAttemptsAtCursor, null); } const rejectedScale = encodeActorRuntimeSimulationResult(metadataBytes, { @@ -323,12 +323,12 @@ test('finalized transport pins state and invokes the typed runtime API at one bl const result = await runDeosActorFinalizedSimulation(connection, { artifact, actorId: 14n, - mode: 'CurrentContinuation', + mode: 'CurrentRun', finalizedBlock: { hash: at, number: 42 }, }); assert.equal(result.outcome.status, 'Suspended'); - assert.equal(result.outcome.continuationCursor, 1); + assert.equal(result.outcome.runCursor, 1); assert.equal(observedArguments[0], 14n); assert.deepEqual(observedArguments[1], { type: 'System', @@ -339,7 +339,7 @@ test('finalized transport pins state and invokes the typed runtime API at one bl value: undefined, }); assert.deepEqual(observedArguments[4], { - type: 'CurrentContinuation', + type: 'CurrentRun', value: undefined, }); assert.deepEqual(observedArguments[5], { at }); @@ -369,9 +369,9 @@ test('matching-Wasm gate binds runtime code, metadata, state, API, and Actor Con assert.equal(result.pin.metadataHash, artifact.metadataHash); assert.equal(result.pin.stateRoot, base.snapshot.stateRoot); assert.equal(observedRequest.actorId, 14n); - assert.equal(observedRequest.mode, 'CurrentContinuation'); + assert.equal(observedRequest.mode, 'CurrentRun'); assert.equal(observedRequest.contractScale, artifact.contractScale); - assert.equal(result.outcome.continuationCursor, 1); + assert.equal(result.outcome.runCursor, 1); }); test('provider cannot change any requested runtime or state dependency', async () => { @@ -419,7 +419,7 @@ test('provider summary must match canonical runtime SCALE bytes', async () => { ); }); -test('local projections and malformed Continuation outcomes fail closed', async () => { +test('local projections and malformed Actor run outcomes fail closed', async () => { await assert.rejects( runActorMatchingWasmSimulation({ ...base, @@ -445,12 +445,12 @@ test('local projections and malformed Continuation outcomes fail closed', async pin: request.pin, outcome: { ...suspendedOutcome, - continuationCursor: null, + runCursor: null, }, }; }, }, }), - /require a Continuation cursor/, + /require an Actor run cursor/, ); }); diff --git a/web-client/scripts/test-actors-materialization.mjs b/web-client/scripts/test-actors-materialization.mjs new file mode 100644 index 00000000..861b27a5 --- /dev/null +++ b/web-client/scripts/test-actors-materialization.mjs @@ -0,0 +1,138 @@ +/* +Domain: Actors materialization projection validation +Owns: Named capacity/fault lowering, absent-state behavior, and finalized transport call evidence. +Excludes: Runtime execution, fault repair, historical indexing, and UI rendering. +Zone: Web-client validation entrypoint for ActorEligibilityApi v6 materialization projections. +*/ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { + projectActorMaterialization, + readActorMaterializationProjection, +} from '../src/lib/adapters/blockchain/actor-materialization.ts'; + +const capacity = { + user_limit: 10_000, + total_limit: 10_000, + user_memberships: 128, + total_memberships: 129, +}; + +const noFaults = { + crossing: undefined, + fanout: undefined, + wakeup: undefined, +}; + +test('named Crossing capacity and absent current faults remain distinct', () => { + assert.deepEqual(projectActorMaterialization(capacity, noFaults), { + crossingCapacity: { + userLimit: 10_000, + totalLimit: 10_000, + userMemberships: 128, + totalMemberships: 129, + }, + faults: { crossing: null, fanout: null, wakeup: null }, + }); +}); + +test('all materialization fault families preserve canonical coordinates', () => { + const hash = new Uint8Array(32).fill(7); + const projection = projectActorMaterialization(capacity, { + crossing: { + feed: { id: 'crossing' }, + revision: 4n, + threshold: 99n, + class: { type: 'Capacity' }, + }, + fanout: { + feed: { id: 'fanout' }, + revision: 8n, + subscriber_page: 2, + subscriber_position: 3, + actor_id: 11n, + semantic_contract_id: hash, + body_commitment: undefined, + admission_identity: { asBytes: () => hash }, + branch: { type: 'Placed' }, + class: { type: 'SchedulerExhausted' }, + }, + wakeup: { + key: { type: 'Tick', value: 42n }, + page: 5, + class: { type: 'Invariant' }, + }, + }); + + assert.deepEqual(projection.faults.crossing, { + feed: { id: 'crossing' }, + revision: 4n, + threshold: 99n, + class: 'Capacity', + }); + assert.deepEqual(projection.faults.fanout, { + feed: { id: 'fanout' }, + revision: 8n, + subscriberPage: 2, + subscriberPosition: 3, + actorId: 11n, + semanticContractId: hash, + bodyCommitment: null, + admissionIdentity: hash, + branch: 'Placed', + class: 'SchedulerExhausted', + }); + assert.deepEqual(projection.faults.wakeup, { + key: { type: 'Tick', tick: 42n }, + page: 5, + class: 'Invariant', + }); +}); + +test('materialization projection fails closed on unknown fault semantics', () => { + assert.throws( + () => + projectActorMaterialization(capacity, { + ...noFaults, + crossing: { + feed: {}, + revision: undefined, + threshold: undefined, + class: { type: 'FutureClass' }, + }, + }), + /Unsupported materialization fault class FutureClass/, + ); +}); + +test('materialization transport pins both API reads to one finalized block', async () => { + const calls = []; + const at = Symbol('finalized block'); + const feed = { runtime: 'feed' }; + const typedApi = { + apis: { + ActorEligibilityApi: { + crossing_capacity: async (requestedFeed, options) => { + calls.push(['capacity', requestedFeed, options.at]); + return capacity; + }, + materialization_faults: async (options) => { + calls.push(['faults', options.at]); + return noFaults; + }, + }, + }, + }; + + const projection = await readActorMaterializationProjection( + typedApi, + at, + feed, + ); + assert.equal(projection.crossingCapacity.totalMemberships, 129); + assert.deepEqual(calls, [ + ['capacity', feed, at], + ['faults', at], + ]); +}); diff --git a/web-client/scripts/test-actors-normative-drift.mjs b/web-client/scripts/test-actors-normative-drift.mjs index 16c4eb4d..5fd03826 100644 --- a/web-client/scripts/test-actors-normative-drift.mjs +++ b/web-client/scripts/test-actors-normative-drift.mjs @@ -16,9 +16,12 @@ const scriptSource = await readFile( ); test('drift gate script exists and parses the canonical spec markers', () => { - assert.match(scriptSource, /## 8\. Events and Ordering/); - assert.match(scriptSource, /## 9\. ABI, Errors, Storage, and Upgrades/); - assert.match(scriptSource, /### 9\.2 Errors/); + assert.match(scriptSource, /specTypeSurface\('Event'\)/); + assert.match(scriptSource, /### 12\.4 Errors and projections/); + assert.match( + scriptSource, + /## 13\. Storage, upgrades, configuration, and conformance/, + ); assert.match(scriptSource, /'ActorContract'/); assert.match(scriptSource, /'Task'/); assert.match(scriptSource, /'Predicate'/); @@ -151,7 +154,7 @@ test('drift gate passes on the aligned surface and fails closed on drift', async const missingCallManifest = JSON.parse(manifestSource); missingCallManifest.pallet.calls = missingCallManifest.pallet.calls.filter( - (entry) => entry.name !== 'cancel_continuation', + (entry) => entry.name !== 'cancel_run', ); await writeFile(manifestPath, JSON.stringify(missingCallManifest)); await writeFile(sandboxSpecPath, specSource); @@ -160,15 +163,12 @@ test('drift gate passes on the aligned surface and fails closed on drift', async webClient, ); assert.equal(missingCall.code, 1); - assert.match(missingCall.output, /calls: missing: cancel_continuation/); + assert.match(missingCall.output, /calls: missing: cancel_run/); await writeFile(manifestPath, manifestSource); await writeFile( sandboxSpecPath, - specSource.replace( - ' Manual,\n AddressEvent { source_filter: SourceFilter, asset_filter: AssetFilter },', - ' AddressEvent { source_filter: SourceFilter, asset_filter: AssetFilter },\n Manual,', - ), + specSource.replace(' Manual,\n AddressEvent {', ' AddressEvent {'), ); const structFieldDrift = await run( ['scripts/check-actors-normative-drift.mjs'], @@ -178,10 +178,7 @@ test('drift gate passes on the aligned surface and fails closed on drift', async assert.match(structFieldDrift.output, /Trigger variants: ordered drift/); await writeFile(manifestPath, manifestSource); - await writeFile( - sandboxSpecPath, - specSource.replace('Section 4.4.', 'Section 99.9.'), - ); + await writeFile(sandboxSpecPath, specSource.replace('§4.4', '§99.9')); const staleReference = await run( ['scripts/check-actors-normative-drift.mjs'], webClient, @@ -251,8 +248,8 @@ test('drift gate passes on the aligned surface and fails closed on drift', async await writeFile( sandboxSpecPath, specSource.replace( - 'ActorCreated { actor_id: ActorId, owner: AccountId, actor_class: ActorClass, mutability: Mutability, sovereign_account: AccountId, initial_lifecycle: InitialLifecycle }\nActorActivated { actor_id: ActorId }', - 'ActorActivated { actor_id: ActorId }\nActorCreated { actor_id: ActorId, owner: AccountId, actor_class: ActorClass, mutability: Mutability, sovereign_account: AccountId, initial_lifecycle: InitialLifecycle }', + ' ActorCreated { actor_id: ActorId, owner: AccountId, actor_class: ActorClass, mutability: Mutability, sovereign_account: AccountId, initial_lifecycle: InitialLifecycle },\n ActorActivated { actor_id: ActorId },', + ' ActorActivated { actor_id: ActorId },\n ActorCreated { actor_id: ActorId, owner: AccountId, actor_class: ActorClass, mutability: Mutability, sovereign_account: AccountId, initial_lifecycle: InitialLifecycle },', ), ); const orderDrift = await run( diff --git a/web-client/scripts/test-actors-reactive-authoring.mjs b/web-client/scripts/test-actors-reactive-authoring.mjs index cefe0dfd..827efa69 100644 --- a/web-client/scripts/test-actors-reactive-authoring.mjs +++ b/web-client/scripts/test-actors-reactive-authoring.mjs @@ -121,7 +121,7 @@ function matchingOutcome(resultScale) { closeReason: 'ProductiveCycleCompleted', cycleNonce: 1n, startCursor: 0, - continuationCursor: null, + runCursor: null, unsuccessfulAttemptsAtCursor: null, cumulativeOutcomes: { executedSteps: 1, @@ -264,7 +264,7 @@ test('local projection preserves one-shot readiness, retry, and productive closu }); assert.equal(suspended.status, 'Suspended'); assert.equal(suspended.state.nativeBalance, 100n); - assert.equal(suspended.continuationCursor, 0); + assert.equal(suspended.runCursor, 0); assert.equal(suspended.unsuccessfulAttemptsAtCursor, 1); const closed = simulateActorLocally({ @@ -297,7 +297,7 @@ test('matching-Wasm contract accepts canonical productive closure for the fixtur }, cycle_nonce: 1n, start_cursor: 0, - continuation_cursor: undefined, + run_cursor: undefined, unsuccessful_attempts_at_cursor: undefined, cumulative_outcomes: { executed_steps: 1, @@ -319,7 +319,7 @@ test('matching-Wasm contract accepts canonical productive closure for the fixtur const response = await runActorMatchingWasmSimulation({ artifact, actorId: 9n, - mode: 'CurrentContinuation', + mode: 'CurrentRun', metadataBytes, runtime, runtimeCodeBytes: Uint8Array.of(1, 2, 3), @@ -370,15 +370,16 @@ test('reactive authoring UI exposes every canonical fixture control', async () = ); const source = sources.join('\n'); assert(sources[1].includes('ACTORS_AUTHORING_CONDITION_TYPES')); + assert(!sources[3].includes('disabled={total === 1}')); for (const control of [ 'ObservationChange', 'SwapIn', 'RetryLater', 'Close after productive cycle', + 'Opening-only Contract', 'Persistent', 'CrossingUserCapacityExceeded', 'CrossingIndexCapacityExceeded', - 'Refundable bond', 'Service cost scales boundedly with subscribed pages', 'Repeated fires while already latched coalesce', 'backpressure may defer service', diff --git a/web-client/scripts/test-actors-risk-warnings.mjs b/web-client/scripts/test-actors-risk-warnings.mjs index 36601b4d..5c22b5d8 100644 --- a/web-client/scripts/test-actors-risk-warnings.mjs +++ b/web-client/scripts/test-actors-risk-warnings.mjs @@ -120,10 +120,10 @@ function activeContract(steps) { }; } -function artifactFor({ steps, mutability = 'Mutable' } = {}) { +function artifactFor({ steps, mutability = 'Mutable', contract } = {}) { const contractScale = encodeActorContractValue( metadataBytes, - activeContract(steps), + contract ?? activeContract(steps), ); return createActorContractArtifact({ metadataBytes, @@ -159,6 +159,21 @@ test('Immutable actor without a terminal step produces a critical warning', () = assert.match(hit.message, /custody permanently/); }); +test('Immutable zero-Step AtTime with auto-close nonce has an authored terminal', () => { + const contract = activeContract([]); + contract.trigger = { type: 'AtTime', value: { after_ticks: 20n } }; + contract.cooldown_blocks = 0; + contract.auto_close_at_cycle_nonce = 1n; + const artifact = artifactFor({ contract, mutability: 'Immutable' }); + const analysis = analyze(artifact); + const warnings = projectActorCompositionWarnings({ artifact, analysis }); + assert.equal(analysis.autoCloseAtCycleNonce, 1n); + assert.ok( + !warnings.some((w) => w.kind === 'ImmutableWithoutReachableTerminal'), + 'authored auto-close nonce is a reachable immutable terminal', + ); +}); + test('Mutable actor with a terminal step does not warn about permanence', () => { const artifact = artifactFor({ steps: [step()], mutability: 'Mutable' }); const analysis = analyze(artifact); diff --git a/web-client/scripts/test-actors-simulation.mjs b/web-client/scripts/test-actors-simulation.mjs index 38ba7796..460c1a0f 100644 --- a/web-client/scripts/test-actors-simulation.mjs +++ b/web-client/scripts/test-actors-simulation.mjs @@ -72,7 +72,7 @@ test('local projection commits successful tasks and rolls back one failed task', assert.equal(result.provenance.truth, 'AdapterLocalProjection'); assert.equal(result.status, 'Completed'); assert.equal(result.state.balance, 95n); - assert.equal(result.continuationCursor, null); + assert.equal(result.runCursor, null); assert.deepEqual( result.journal.map(({ outcome, stateCommitted }) => [ outcome.kind, @@ -142,12 +142,12 @@ test('temporary RetryLater preserves the prefix and resumes from one scalar curs assert.equal(suspended.status, 'Suspended'); assert.equal(suspended.state.balance, 90n); - assert.equal(suspended.continuationCursor, 1); + assert.equal(suspended.runCursor, 1); assert.equal(suspended.unsuccessfulAttemptsAtCursor, 1); const resumed = simulateActorLocally({ ...provenance, - startCursor: suspended.continuationCursor, + startCursor: suspended.runCursor, unsuccessfulAttemptsAtCursor: suspended.unsuccessfulAttemptsAtCursor, initialState: suspended.state, initialCounts: suspended.cumulative, @@ -184,7 +184,7 @@ test('RetryLater closes exactly on its local bound and counts funding unavailabi }); assert.equal(closedImmediately.status, 'Closed'); assert.equal(closedImmediately.closeReason, 'RetryAttemptsExhausted'); - assert.equal(closedImmediately.continuationCursor, null); + assert.equal(closedImmediately.runCursor, null); const suspended = simulateActorLocally({ ...provenance, @@ -239,7 +239,7 @@ test('cursor advancement resets the local unsuccessful-attempt count', () => { }, }); assert.equal(result.status, 'Suspended'); - assert.equal(result.continuationCursor, 1); + assert.equal(result.runCursor, 1); assert.equal(result.unsuccessfulAttemptsAtCursor, 1); }); @@ -254,7 +254,7 @@ test('permanent RetryLater aborts, and Immutable Actor Contracts reject retry po }, }); assert.equal(aborted.status, 'Failed'); - assert.equal(aborted.continuationCursor, null); + assert.equal(aborted.runCursor, null); assert.equal(aborted.state.balance, 1n); assert.throws( diff --git a/web-client/scripts/test-actors-trigger-bond-vectors.mjs b/web-client/scripts/test-actors-trigger-bond-vectors.mjs deleted file mode 100644 index 1eda01c5..00000000 --- a/web-client/scripts/test-actors-trigger-bond-vectors.mjs +++ /dev/null @@ -1,58 +0,0 @@ -/* -Domain: Actors Trigger-state bond vector validation -Owns: Runtime-generated policy identity, exhaustive family coverage, and browser quote parity. -Excludes: Live runtime transport and reserve mutation. -*/ -import assert from 'node:assert/strict'; -import { createHash } from 'node:crypto'; -import { readFile } from 'node:fs/promises'; -import test from 'node:test'; - -import { - ACTORS_TRIGGER_BOND_VECTORS, - actorTriggerStateBond, -} from '../src/lib/automation/trigger-bond-vectors.ts'; - -const metadata = await readFile( - new URL('../.papi/metadata/deos.scale', import.meta.url), -); -const weights = await readFile( - new URL( - '../../template/runtime/src/weights/pallet_deos_actors.rs', - import.meta.url, - ), -); - -function sha256(bytes) { - return createHash('sha256').update(bytes).digest('hex'); -} - -test('runtime-generated Trigger bond vectors bind metadata and Actors weights', () => { - assert.equal(ACTORS_TRIGGER_BOND_VECTORS.formatVersion, 1); - assert.equal(ACTORS_TRIGGER_BOND_VECTORS.metadataSha256, sha256(metadata)); - assert.equal(ACTORS_TRIGGER_BOND_VECTORS.actorsWeightSha256, sha256(weights)); -}); - -test('Trigger bond vectors are exhaustive, nonzero, and projected without a client formula', () => { - const expected = { - Manual: 1_000_000_000n, - AddressEvent: 2_000_000_000n, - ObservationChange: 2_000_000_000n, - ObservationCrossing: 5_000_000_000n, - Cadenced: 2_000_000_000n, - }; - assert.deepEqual( - ACTORS_TRIGGER_BOND_VECTORS.vectors.map((vector) => vector.triggerFamily), - Object.keys(expected), - ); - for (const [family, amount] of Object.entries(expected)) { - assert.equal(actorTriggerStateBond(family), amount); - } -}); - -test('Trigger bond projection fails closed when generated family evidence is absent', () => { - assert.throws( - () => actorTriggerStateBond('Unknown'), - /Missing canonical Trigger-state bond vector/, - ); -}); diff --git a/web-client/scripts/upgrade-state-evidence.mjs b/web-client/scripts/upgrade-state-evidence.mjs index 19ba7dba..be595b8c 100644 --- a/web-client/scripts/upgrade-state-evidence.mjs +++ b/web-client/scripts/upgrade-state-evidence.mjs @@ -101,7 +101,7 @@ async function chainSnapshot(client, api) { lpPairs, observation, actorHot, - actorContract, + actorContractHead, actorFunding, ] = await Promise.all([ api.query.System.Account.getValue(burnAccount, { at: block.hash }), @@ -114,7 +114,9 @@ async function chainSnapshot(client, api) { api.query.DeosRouter.LpPairByTokenId.getValue({ at: block.hash }), api.query.Oracle.Observations.getValue(feed, { at: block.hash }), api.query.Actors.ActorHot.getValue(burnActorId, { at: block.hash }), - api.query.Actors.ActorContract.getValue(burnActorId, { at: block.hash }), + api.query.Actors.ActorContractHead.getValue(burnActorId, { + at: block.hash, + }), api.query.Actors.ActorFunding.getValue(burnActorId, { at: block.hash }), ]); if (!assetDetails) @@ -122,7 +124,7 @@ async function chainSnapshot(client, api) { if (!pool || !reserves.success) throw new Error('Native/foreign pool state is unavailable'); if (!observation) throw new Error('Oracle observation is unavailable'); - if (!actorHot || !actorContract || !actorFunding) + if (!actorHot || !actorContractHead || !actorFunding) throw new Error('Burn Actor state is incomplete'); return jsonValue({ @@ -154,7 +156,7 @@ async function chainSnapshot(client, api) { oracle_observation: observation, actor_identity: actorIdentity, actor_hot: actorHot, - actor_contract: actorContract, + actor_contract_head: actorContractHead, actor_funding: actorFunding, }, }); diff --git a/web-client/src/lib/adapters/blockchain/actor-cost.ts b/web-client/src/lib/adapters/blockchain/actor-cost.ts new file mode 100644 index 00000000..f323ff5d --- /dev/null +++ b/web-client/src/lib/adapters/blockchain/actor-cost.ts @@ -0,0 +1,44 @@ +/* +Domain: Actors cost transport +Owns: Read-only ActorCostApi invocation at one finalized block and canonical cost projection. +Excludes: Fee estimation, storage assembly, event history, and presentation. +Zone: Blockchain adapter capability; imports only the automation cost contract. +*/ +import type { DeosTypedApi } from './deos.ts'; + +import { + type ActorCostView, + projectActorCostQuote, +} from '../../automation/cost.ts'; + +export type ActorCostRead = { + projection: ActorCostView | null; + unavailableReason: string | null; +}; + +type ActorCostAt = NonNullable< + Parameters[1] +>['at']; + +export async function readActorCost( + typedApi: DeosTypedApi, + at: ActorCostAt, + actorId: number, +): Promise { + try { + const result = await typedApi.apis.ActorCostApi.actor_cost_quote( + BigInt(actorId), + { at }, + ); + return { + projection: projectActorCostQuote(result), + unavailableReason: null, + }; + } catch (error) { + return { + projection: null, + unavailableReason: + error instanceof Error ? error.message : 'Actor cost API unavailable', + }; + } +} diff --git a/web-client/src/lib/adapters/blockchain/actor-materialization.ts b/web-client/src/lib/adapters/blockchain/actor-materialization.ts new file mode 100644 index 00000000..05d2d9a0 --- /dev/null +++ b/web-client/src/lib/adapters/blockchain/actor-materialization.ts @@ -0,0 +1,182 @@ +/* +Domain: Actors materialization transport +Owns: Finalized ActorEligibilityApi capacity/fault reads and lossless lowering into automation contracts. +Excludes: Fault repair, scheduler policy, historical indexing, and UI presentation. +Zone: Blockchain adapter capability; imports only the automation materialization contract. +*/ +import type { DeosTypedApi } from './deos.ts'; + +import type { + ActorMaterializationProjection, + MaterializationFaultClass, +} from '../../automation/materialization.ts'; + +type ActorEligibilityAt = NonNullable< + Parameters< + DeosTypedApi['apis']['ActorEligibilityApi']['materialization_faults'] + >[0] +>['at']; + +type RecordValue = Record; + +function record(value: unknown, field: string): RecordValue { + if (value == null || typeof value !== 'object' || Array.isArray(value)) { + throw new Error(`${field} must be a runtime object`); + } + return value as RecordValue; +} + +function count(value: unknown, field: string): number { + if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) { + throw new Error(`${field} must be a non-negative safe integer`); + } + return value; +} + +function integer(value: unknown, field: string): bigint { + if (typeof value === 'bigint' && value >= 0n) return value; + if (typeof value === 'number' && Number.isSafeInteger(value) && value >= 0) { + return BigInt(value); + } + throw new Error(`${field} must be an unsigned runtime integer`); +} + +function optional(value: unknown, project: (value: unknown) => T): T | null { + return value === undefined ? null : project(value); +} + +function variant( + value: unknown, + field: string, +): { type: string; value?: unknown } { + const decoded = record(value, field); + if (typeof decoded.type !== 'string') { + throw new Error(`${field} must carry a runtime variant type`); + } + return decoded as { type: string; value?: unknown }; +} + +function faultClass(value: unknown): MaterializationFaultClass { + const type = variant(value, 'materialization fault class').type; + if ( + type !== 'Invariant' && + type !== 'Capacity' && + type !== 'SchedulerExhausted' && + type !== 'Other' + ) { + throw new Error(`Unsupported materialization fault class ${type}`); + } + return type; +} + +function bytes(value: unknown, field: string): Uint8Array { + if (value instanceof Uint8Array) return value; + const asBytes = record(value, field).asBytes; + if (typeof asBytes === 'function') { + const decoded = asBytes.call(value); + if (decoded instanceof Uint8Array) return decoded; + } + throw new Error(`${field} must carry runtime bytes`); +} + +export function projectActorMaterialization( + runtimeCapacity: unknown, + runtimeFaults: unknown, +): ActorMaterializationProjection { + const capacity = record(runtimeCapacity, 'Crossing capacity'); + const faults = record(runtimeFaults, 'materialization faults'); + + return { + crossingCapacity: { + userLimit: count(capacity.user_limit, 'Crossing user limit'), + totalLimit: count(capacity.total_limit, 'Crossing total limit'), + userMemberships: count( + capacity.user_memberships, + 'Crossing user memberships', + ), + totalMemberships: count( + capacity.total_memberships, + 'Crossing total memberships', + ), + }, + faults: { + crossing: optional(faults.crossing, (value) => { + const fault = record(value, 'Crossing fault'); + return { + feed: fault.feed, + revision: optional(fault.revision, (item) => + integer(item, 'revision'), + ), + threshold: optional(fault.threshold, (item) => + integer(item, 'threshold'), + ), + class: faultClass(fault.class), + }; + }), + fanout: optional(faults.fanout, (value) => { + const fault = record(value, 'fanout fault'); + return { + feed: fault.feed, + revision: integer(fault.revision, 'fanout revision'), + subscriberPage: optional(fault.subscriber_page, (item) => + count(item, 'subscriber page'), + ), + subscriberPosition: count( + fault.subscriber_position, + 'subscriber position', + ), + actorId: optional(fault.actor_id, (item) => + integer(item, 'actor id'), + ), + semanticContractId: optional(fault.semantic_contract_id, (item) => + bytes(item, 'semantic contract id'), + ), + bodyCommitment: optional(fault.body_commitment, (item) => + bytes(item, 'body commitment'), + ), + admissionIdentity: optional(fault.admission_identity, (item) => + bytes(item, 'admission identity'), + ), + branch: variant(fault.branch, 'fanout branch').type, + class: faultClass(fault.class), + }; + }), + wakeup: optional(faults.wakeup, (value) => { + const fault = record(value, 'wakeup fault'); + const key = variant(fault.key, 'wakeup key'); + return { + key: + key.type === 'Block' + ? { + type: 'Block' as const, + block: count(key.value, 'wakeup block'), + } + : key.type === 'Tick' + ? { + type: 'Tick' as const, + tick: integer(key.value, 'wakeup tick'), + } + : (() => { + throw new Error(`Unsupported wakeup key ${key.type}`); + })(), + page: count(fault.page, 'wakeup page'), + class: faultClass(fault.class), + }; + }), + }, + }; +} + +export async function readActorMaterializationProjection( + typedApi: DeosTypedApi, + at: ActorEligibilityAt, + feed: Parameters< + DeosTypedApi['apis']['ActorEligibilityApi']['crossing_capacity'] + >[0], +): Promise { + const [capacity, faults] = await Promise.all([ + typedApi.apis.ActorEligibilityApi.crossing_capacity(feed, { at }), + typedApi.apis.ActorEligibilityApi.materialization_faults({ at }), + ]); + return projectActorMaterialization(capacity, faults); +} diff --git a/web-client/src/lib/adapters/blockchain/actor-resource.ts b/web-client/src/lib/adapters/blockchain/actor-resource.ts new file mode 100644 index 00000000..37aba880 --- /dev/null +++ b/web-client/src/lib/adapters/blockchain/actor-resource.ts @@ -0,0 +1,85 @@ +/* +Domain: Actors block-resource transport +Owns: Finalized ActorResourceApi reads and lossless lowering into automation resource contracts. +Excludes: Scheduler policy, resource arithmetic, archive history, and UI presentation. +Zone: Blockchain adapter capability; imports only the automation resource contract. +*/ +import type { DeosTypedApi } from './deos.ts'; + +import type { + ActorResourceBudgetView, + ActorResourceProjection, + ActorResourceUsageView, + ActorResourceWeight, + CurrentActorResourceView, + FinalizedActorResourceView, +} from '../../automation/resource.ts'; + +type ActorResourceAt = NonNullable< + Parameters< + DeosTypedApi['apis']['ActorResourceApi']['block_resource_budget'] + >[0] +>['at']; + +type RuntimeWeight = { ref_time: bigint; proof_size: bigint }; +type RuntimeUsage = { + actor_control: RuntimeWeight; + actor_effect: RuntimeWeight; + user_dispatch: RuntimeWeight; +}; + +function weight(value: RuntimeWeight): ActorResourceWeight { + return { refTime: value.ref_time, proofSize: value.proof_size }; +} + +function usage(value: RuntimeUsage): ActorResourceUsageView { + return { + actorControl: weight(value.actor_control), + actorEffect: weight(value.actor_effect), + userDispatch: weight(value.user_dispatch), + }; +} + +export async function readActorResourceProjection( + typedApi: DeosTypedApi, + at: ActorResourceAt, +): Promise { + const [runtimeBudget, runtimeCurrent, runtimeFinalized] = await Promise.all([ + typedApi.apis.ActorResourceApi.block_resource_budget({ at }), + typedApi.apis.ActorResourceApi.current_block_resource_state({ at }), + typedApi.apis.ActorResourceApi.finalized_block_resource_snapshot({ at }), + ]); + + const budget: ActorResourceBudgetView = { + maximumBlock: weight(runtimeBudget.maximum_block), + fixedEnvelope: weight(runtimeBudget.fixed_envelope), + limits: { + actorControl: weight(runtimeBudget.limits.actor_control), + sharedEconomic: weight(runtimeBudget.limits.shared_economic), + actorBaseTurn: weight(runtimeBudget.limits.actor_base_turn), + userBaseTurn: weight(runtimeBudget.limits.user_base_turn), + }, + }; + const current: CurrentActorResourceView | null = runtimeCurrent + ? { + blockNumber: runtimeCurrent.block_number, + phase: runtimeCurrent.phase.type, + usage: usage(runtimeCurrent.usage), + outstandingReservations: runtimeCurrent.outstanding_reservations, + finalizedFixedReserved: runtimeCurrent.finalized_fixed_reserved + ? weight(runtimeCurrent.finalized_fixed_reserved) + : null, + optionalActorWorkHalted: runtimeCurrent.optional_actor_work_halted, + } + : null; + const finalized: FinalizedActorResourceView | null = runtimeFinalized + ? { + blockNumber: runtimeFinalized.block_number, + fixedReserved: weight(runtimeFinalized.fixed_reserved), + usage: usage(runtimeFinalized.usage), + optionalActorWorkHalted: runtimeFinalized.optional_actor_work_halted, + } + : null; + + return { budget, current, finalized }; +} diff --git a/web-client/src/lib/adapters/blockchain/actor-simulation.ts b/web-client/src/lib/adapters/blockchain/actor-simulation.ts index b76d48c2..98954e49 100644 --- a/web-client/src/lib/adapters/blockchain/actor-simulation.ts +++ b/web-client/src/lib/adapters/blockchain/actor-simulation.ts @@ -31,9 +31,7 @@ const METADATA_VERSION = 16; const HEX_PATTERN = /^0x(?:[0-9a-fA-F]{2})+$/; const PLAN_HEX_PATTERN = /^0x(?:[0-9a-f]{2})+$/; -export type ActorFinalizedSimulationMode = - | 'FreshCurrentPlan' - | 'CurrentContinuation'; +export type ActorFinalizedSimulationMode = 'FreshCurrentPlan' | 'CurrentRun'; export type ActorFinalizedSimulationInput = { artifact: ActorContractArtifact; diff --git a/web-client/src/lib/adapters/blockchain/index.ts b/web-client/src/lib/adapters/blockchain/index.ts index b1d5348d..83f19f1c 100644 --- a/web-client/src/lib/adapters/blockchain/index.ts +++ b/web-client/src/lib/adapters/blockchain/index.ts @@ -5,6 +5,8 @@ Excludes: Wallet store ownership, system composition wiring, UI Kit presentation Zone: Transport adapter boundary; consumes adapter contracts and runtime helpers without importing widgets. */ import type { Adapter, AdapterRuntimeContext } from '$lib/adapters/contract'; +import type { ActorMaterializationProjection } from '$lib/automation/materialization'; +import type { ActorResourceProjection } from '$lib/automation/resource'; import type { AutomationActorSnapshot, AutomationAuthoringContext, @@ -31,6 +33,8 @@ import type { SystemConfig, SystemSnapshot } from '$lib/system/types'; import { DEFAULT_DEOS_DAPP_NAME } from '$lib/wallet/signer'; import { readActorEligibility } from './actor-eligibility'; +import { readActorMaterializationProjection } from './actor-materialization'; +import { readActorResourceProjection } from './actor-resource'; import { getDeosActorFinalizedAuthoringContext } from './actor-simulation'; import { BlockchainConnectionSession } from './connection'; import type { @@ -44,7 +48,7 @@ import { formatChainEventMessage, unwrapEventRecord, } from './events'; -import { BlockchainObservationReader } from './observations'; +import { BlockchainObservationReader, runtimeFeed } from './observations'; import { quoteBuyAtSnapshot, quoteSellAtSnapshot, @@ -146,10 +150,9 @@ function automationFundingAccumulated( }); } -function automationFundingSourcePolicy(funding: unknown): string | null { - const value = triggerRecord(funding); - const policy = triggerRecord(value?.funding_source_policy); - return typeof policy?.type === 'string' ? policy.type : null; +function automationFundingSourcePolicy(policy: unknown): string | null { + const value = triggerRecord(policy); + return typeof value?.type === 'string' ? value.type : null; } function automationTriggerLabel(trigger?: { @@ -160,6 +163,14 @@ function automationTriggerLabel(trigger?: { if (trigger.type === 'AddressEvent') return 'Address event'; if (trigger.type === 'ObservationChange') return 'Observation change'; if (trigger.type === 'ObservationCrossing') return 'Observation crossing'; + if (trigger.type === 'AtTime') { + const afterTicks = triggerRecord(trigger.value)?.after_ticks; + const delay = + typeof afterTicks === 'number' || typeof afterTicks === 'bigint' + ? afterTicks.toString() + : 'unknown'; + return `At time · after ${delay} ticks`; + } if (trigger.type !== 'Cadenced') return trigger.type; const everyTicks = triggerRecord(trigger.value)?.every_ticks; const cadence = @@ -434,12 +445,28 @@ export class BlockchainAdapter implements Adapter { ); } + async getActorResourceProjection(): Promise { + const snapshot = await (await this.ensurePapi()).snapshot(); + return readActorResourceProjection(snapshot.typedApi, snapshot.at); + } + + async getActorMaterializationProjection( + feed: ObservationFeedIdentity, + ): Promise { + const snapshot = await (await this.ensurePapi()).snapshot(); + return readActorMaterializationProjection( + snapshot.typedApi, + snapshot.at, + runtimeFeed(feed), + ); + } + async getAutomationActors(): Promise { try { const snapshot = await (await this.ensurePapi()).snapshot(); return await Promise.all( KNOWN_SYSTEM_ACTORS.map(async (actor) => { - const [identity, hot, program, continuation, funding] = + const [identity, hot, contractHead, runHead, funding] = await Promise.all([ snapshot.typedApi.query.Actors.ActorIdentities.getValue( BigInt(actor.actorId), @@ -451,11 +478,11 @@ export class BlockchainAdapter implements Adapter { at: snapshot.at, }, ), - snapshot.typedApi.query.Actors.ActorContract.getValue( + snapshot.typedApi.query.Actors.ActorContractHead.getValue( BigInt(actor.actorId), { at: snapshot.at }, ), - snapshot.typedApi.query.Actors.ContinuationState.getValue( + snapshot.typedApi.query.Actors.ActorRunHead.getValue( BigInt(actor.actorId), { at: snapshot.at }, ), @@ -469,7 +496,8 @@ export class BlockchainAdapter implements Adapter { snapshot.at, actor.actorId, ); - const exists = identity != null && hot != null && program != null; + const exists = + identity != null && hot != null && contractHead != null; const sovereignAccount = identity?.sovereign_account ?? deriveSystemActorSovereignAccount(actor.actorId); @@ -491,14 +519,16 @@ export class BlockchainAdapter implements Adapter { paused: automationActorPaused(hot), runState: automationActorRunState(hot), cycleNonce: identity?.cycle_nonce ?? 0n, - continuation: automationContinuationSnapshot(continuation), + continuation: automationContinuationSnapshot(runHead), lastCycleBlock: hot?.last_cycle_block ?? null, - completionPolicy: program?.completion.type ?? null, - triggerLabel: automationTriggerLabel(program?.trigger), + completionPolicy: contractHead?.header.completion.type ?? null, + triggerLabel: automationTriggerLabel(contractHead?.header.trigger), nativeBalance: account?.data?.free ?? 0n, queueTicket: automationQueueTicket(hot), fundingAccumulated: automationFundingAccumulated(funding), - fundingSourcePolicy: automationFundingSourcePolicy(funding), + fundingSourcePolicy: automationFundingSourcePolicy( + contractHead?.header.funding, + ), eligibility: eligibility.projection, } satisfies AutomationActorSnapshot; }), diff --git a/web-client/src/lib/adapters/blockchain/observations.ts b/web-client/src/lib/adapters/blockchain/observations.ts index 033edba7..9bb5d2eb 100644 --- a/web-client/src/lib/adapters/blockchain/observations.ts +++ b/web-client/src/lib/adapters/blockchain/observations.ts @@ -82,7 +82,7 @@ function feedIdentity(feed: { }; } -function runtimeFeed(feed: ObservationFeedIdentity) { +export function runtimeFeed(feed: ObservationFeedIdentity) { return { asset_in: runtimeAsset(feed.assetIn), asset_out: runtimeAsset(feed.assetOut), diff --git a/web-client/src/lib/adapters/contract.ts b/web-client/src/lib/adapters/contract.ts index 6ed53e99..db2c5579 100644 --- a/web-client/src/lib/adapters/contract.ts +++ b/web-client/src/lib/adapters/contract.ts @@ -5,13 +5,18 @@ Excludes: Concrete provider implementations, widget composition, and domain stor Zone: Adapter public contract; may reference domain projection types but must not import concrete adapters/widgets. */ import type { DeosChainConnectionState } from '$lib/adapters/blockchain/deos'; +import type { ActorMaterializationProjection } from '$lib/automation/materialization'; +import type { ActorResourceProjection } from '$lib/automation/resource'; import type { AutomationActorSnapshot, AutomationAuthoringContext, } from '$lib/automation/types'; import type { LogEntry, TransactionProgress } from '$lib/log/types'; import type { PricePoint, Quote, SwapResult } from '$lib/market/types'; -import type { ObservationInspectionProvider } from '$lib/observation/types'; +import type { + ObservationFeedIdentity, + ObservationInspectionProvider, +} from '$lib/observation/types'; import type { AssetBalanceProjection, TransferAssetKey, @@ -68,6 +73,12 @@ export type AutomationAdapter = { getAutomationAuthoringContext?(): | AutomationAuthoringContext | Promise; + getActorResourceProjection?(): + | ActorResourceProjection + | Promise; + getActorMaterializationProjection?( + feed: ObservationFeedIdentity, + ): ActorMaterializationProjection | Promise; }; export type MarketAdapter = { diff --git a/web-client/src/lib/automation/AutomationStepEditor.svelte b/web-client/src/lib/automation/AutomationStepEditor.svelte index cb9c8bc1..8b4f66e0 100644 --- a/web-client/src/lib/automation/AutomationStepEditor.svelte +++ b/web-client/src/lib/automation/AutomationStepEditor.svelte @@ -227,7 +227,6 @@ Zone: Automation presentation helper; composes typed authoring fields without su diff --git a/web-client/src/lib/automation/AutomationTriggerEditor.svelte b/web-client/src/lib/automation/AutomationTriggerEditor.svelte index dfb47d98..7ae778d5 100644 --- a/web-client/src/lib/automation/AutomationTriggerEditor.svelte +++ b/web-client/src/lib/automation/AutomationTriggerEditor.svelte @@ -1,6 +1,6 @@ @@ -14,7 +14,6 @@ Zone: Automation presentation helper; edits the canonical trigger draft without DEOS_ACTORS_AUTHORING_LIMITS, } from '$lib/automation/authoring'; import { actorReactiveCapacityFailureMessage } from '$lib/automation/capacity-failure'; - import { actorTriggerStateBond } from '$lib/automation/trigger-bond-vectors'; import { Badge, Button, @@ -33,7 +32,6 @@ Zone: Automation presentation helper; edits the canonical trigger draft without }; let { trigger = $bindable(), compact = false }: Props = $props(); - const triggerStateBond = $derived(actorTriggerStateBond(trigger.type)); const crossingCapacityCopy = [ actorReactiveCapacityFailureMessage('CrossingUserCapacityExceeded'), actorReactiveCapacityFailureMessage('CrossingIndexCapacityExceeded'), @@ -76,6 +74,9 @@ Zone: Automation presentation helper; edits the canonical trigger draft without rearmThreshold: '900000000000', }; return; + case 'AtTime': + trigger = { type, afterTicks: 1 }; + return; case 'Cadenced': trigger = { type, everyTicks: 1 }; } @@ -226,13 +227,23 @@ Zone: Automation presentation helper; edits the canonical trigger draft without + - {#if trigger.type === 'Cadenced'} + {#if trigger.type === 'AtTime'} + + {:else if trigger.type === 'Cadenced'} -

- Refundable bond - {triggerStateBond.toLocaleString()} native base units - · finalized runtime quote remains authoritative -

- {#if trigger.type === 'Manual'}

Readiness is latched only by the authorized manual trigger call. diff --git a/web-client/src/lib/automation/actors-abi-manifest.json b/web-client/src/lib/automation/actors-abi-manifest.json index 029ec98c..e953a46a 100644 --- a/web-client/src/lib/automation/actors-abi-manifest.json +++ b/web-client/src/lib/automation/actors-abi-manifest.json @@ -2,14 +2,15 @@ "format": "deos.actor.abi-manifest", "formatVersion": 1, "metadata": { - "sha256": "3a5402c7b7f4a1f58dc8569d67638221961bde9e8efe7587b7dc09fef49f9873", + "sha256": "27984891721c42acbce79d4e458e9b40dd6b9a046228438a072f4f2c1bd0f74e", "version": 16 }, "pallet": { "associatedTypes": [ { "name": "AssetId", "type": 197 }, { "name": "Balance", "type": 49 }, - { "name": "ObservationFeedId", "type": 216 } + { "name": "ObservationFeedId", "type": 216 }, + { "name": "RuntimeHoldReason", "type": 299 } ], "calls": [ { @@ -145,8 +146,9 @@ { "fields": [{ "name": "actor_id", "type": 10, "typeName": "ActorId" }], "index": 19, - "name": "cancel_continuation" - } + "name": "cancel_run" + }, + { "fields": [], "index": 23, "name": "actor_prepass" } ], "constants": [ { "name": "FeeNativeAssetId", "type": 197, "value": "0x00" }, @@ -156,18 +158,18 @@ "value": "0xf401000000000000" }, { "name": "MinWindowLength", "type": 14, "value": "0x64000000" }, - { "name": "PalletId", "type": 435, "value": "0x6163746f72733030" }, - { "name": "MaxContractSteps", "type": 14, "value": "0x08000000" }, - { "name": "MaxFundingTrackedAssets", "type": 14, "value": "0x0a000000" }, + { "name": "PalletId", "type": 446, "value": "0x6163746f72733030" }, + { "name": "MaxContractSteps", "type": 14, "value": "0x20000000" }, + { "name": "MaxFundingTrackedAssets", "type": 14, "value": "0x28000000" }, { "name": "MaxOpeningSnapshotEntries", "type": 14, - "value": "0x10000000" + "value": "0x40000000" }, { "name": "MaxOpeningPredicateResults", "type": 14, - "value": "0x20000000" + "value": "0x80000000" }, { "name": "MaxPreconditionClauses", "type": 14, "value": "0x04000000" }, { "name": "MaxPredicatesPerClause", "type": 14, "value": "0x04000000" }, @@ -178,6 +180,7 @@ { "name": "QueuePageSize", "type": 14, "value": "0x40000000" }, { "name": "WakeupPageSize", "type": 14, "value": "0x20000000" }, { "name": "ObservationPageSize", "type": 14, "value": "0x40000000" }, + { "name": "CrossingPageSize", "type": 14, "value": "0x80000000" }, { "name": "MaxCrossingMembersPerFeed", "type": 14, @@ -207,7 +210,7 @@ { "name": "MaxCrossingActorsPerBlock", "type": 14, - "value": "0x04000000" + "value": "0x80000000" }, { "name": "CrossingWorkerWeightLimit", @@ -241,7 +244,7 @@ { "name": "TargetBlockTime", "type": 10, "value": "0x0600000000000000" }, { "name": "MaxExecutionDelayBlocks", "type": 14, "value": "0x208d2203" }, { - "name": "MaxCadenceDelayTicks", + "name": "MaxTemporalDelayTicks", "type": 10, "value": "0x809d9e2500000000" }, @@ -262,8 +265,18 @@ { "name": "ActorCreationFee", "type": 49, + "value": "0x00943577000000000000000000000000" + }, + { + "name": "ActorStateHoldBase", + "type": 49, "value": "0x00ca9a3b000000000000000000000000" }, + { + "name": "ActorStateHoldPerByte", + "type": 49, + "value": "0x40420f00000000000000000000000000" + }, { "name": "MaxConsecutiveFailures", "type": 14, "value": "0x0a000000" }, { "name": "MaxRetryAttempts", "type": 14, "value": "0x0a000000" }, { @@ -291,92 +304,97 @@ { "fields": [], "index": 11, "name": "ActiveActorLimitTooLow" }, { "fields": [], "index": 12, "name": "ActiveActorLimitBelowCurrent" }, { "fields": [], "index": 13, "name": "ActorPaused" }, - { "fields": [], "index": 14, "name": "EmptyContractSteps" }, - { "fields": [], "index": 15, "name": "ContractStepsExceedOnIdleBudget" }, - { "fields": [], "index": 16, "name": "ExecutionDelayTooLong" }, - { "fields": [], "index": 17, "name": "GlobalCircuitBreakerActive" }, - { "fields": [], "index": 18, "name": "ImmutableActor" }, - { "fields": [], "index": 19, "name": "InsufficientBalance" }, - { "fields": [], "index": 20, "name": "InsufficientFee" }, - { "fields": [], "index": 21, "name": "InvalidAmountResolution" }, - { "fields": [], "index": 22, "name": "InvalidPredicate" }, - { "fields": [], "index": 23, "name": "InvalidAutoCloseNonce" }, - { "fields": [], "index": 24, "name": "InvalidScheduleWindow" }, - { "fields": [], "index": 25, "name": "InvalidSplitTransfer" }, - { "fields": [], "index": 26, "name": "InvalidTriggerConfiguration" }, - { "fields": [], "index": 27, "name": "InvalidTradeBound" }, - { "fields": [], "index": 28, "name": "InvalidRetryAttemptLimit" }, - { "fields": [], "index": 29, "name": "InvalidObservationMaxAge" }, - { "fields": [], "index": 30, "name": "SelfTransferNotAllowed" }, - { "fields": [], "index": 31, "name": "MintNotAllowedForUserActor" }, - { "fields": [], "index": 32, "name": "NotGovernance" }, - { "fields": [], "index": 33, "name": "NotOwner" }, - { "fields": [], "index": 34, "name": "OwnerSlotCapacityExceeded" }, - { "fields": [], "index": 35, "name": "OwnerSlotOccupied" }, - { "fields": [], "index": 36, "name": "InvalidOwnerSlot" }, - { "fields": [], "index": 37, "name": "ActorIdOccupied" }, - { "fields": [], "index": 38, "name": "SystemSovereignCapacityExceeded" }, - { "fields": [], "index": 39, "name": "SystemSovereignUnknown" }, - { "fields": [], "index": 40, "name": "SystemSovereignOccupied" }, - { "fields": [], "index": 41, "name": "SystemSovereignInvariant" }, - { "fields": [], "index": 42, "name": "SovereignAccountCollision" }, - { "fields": [], "index": 43, "name": "ReservedSovereignAccount" }, - { "fields": [], "index": 44, "name": "TooManyContractSteps" }, - { "fields": [], "index": 45, "name": "SnapshotUnavailable" }, - { "fields": [], "index": 46, "name": "FundingAccumulatorOverflow" }, - { "fields": [], "index": 47, "name": "QueueTicketExhausted" }, - { "fields": [], "index": 48, "name": "SchedulerIndexExhausted" }, - { "fields": [], "index": 49, "name": "AutoCloseNonceHorizonExceeded" }, - { "fields": [], "index": 50, "name": "ControlMutationRateLimited" }, - { "fields": [], "index": 51, "name": "QueueCapacityUnavailable" }, + { "fields": [], "index": 14, "name": "ContractStepsExceedOnIdleBudget" }, + { "fields": [], "index": 15, "name": "ExecutionDelayTooLong" }, + { "fields": [], "index": 16, "name": "GlobalCircuitBreakerActive" }, + { "fields": [], "index": 17, "name": "ImmutableActor" }, + { "fields": [], "index": 18, "name": "InsufficientBalance" }, + { "fields": [], "index": 19, "name": "InsufficientFee" }, + { "fields": [], "index": 20, "name": "InvalidAmountResolution" }, + { "fields": [], "index": 21, "name": "InvalidPredicate" }, + { "fields": [], "index": 22, "name": "InvalidAutoCloseNonce" }, + { "fields": [], "index": 23, "name": "InvalidScheduleWindow" }, + { "fields": [], "index": 24, "name": "InvalidSplitTransfer" }, + { "fields": [], "index": 25, "name": "InvalidTriggerConfiguration" }, + { "fields": [], "index": 26, "name": "InvalidTradeBound" }, + { "fields": [], "index": 27, "name": "InvalidRetryAttemptLimit" }, + { "fields": [], "index": 28, "name": "InvalidObservationMaxAge" }, + { "fields": [], "index": 29, "name": "SelfTransferNotAllowed" }, + { "fields": [], "index": 30, "name": "MintNotAllowedForUserActor" }, + { "fields": [], "index": 31, "name": "NotGovernance" }, + { "fields": [], "index": 32, "name": "NotOwner" }, + { "fields": [], "index": 33, "name": "OwnerSlotCapacityExceeded" }, + { "fields": [], "index": 34, "name": "OwnerSlotOccupied" }, + { "fields": [], "index": 35, "name": "InvalidOwnerSlot" }, + { "fields": [], "index": 36, "name": "ActorIdOccupied" }, + { "fields": [], "index": 37, "name": "SystemSovereignCapacityExceeded" }, + { "fields": [], "index": 38, "name": "SystemSovereignUnknown" }, + { "fields": [], "index": 39, "name": "SystemSovereignOccupied" }, + { "fields": [], "index": 40, "name": "SystemSovereignInvariant" }, + { "fields": [], "index": 41, "name": "SovereignAccountCollision" }, + { "fields": [], "index": 42, "name": "ReservedSovereignAccount" }, + { "fields": [], "index": 43, "name": "TooManyContractSteps" }, + { "fields": [], "index": 44, "name": "SnapshotUnavailable" }, + { "fields": [], "index": 45, "name": "FundingAccumulatorOverflow" }, + { "fields": [], "index": 46, "name": "QueueTicketExhausted" }, + { "fields": [], "index": 47, "name": "SchedulerIndexExhausted" }, + { "fields": [], "index": 48, "name": "AutoCloseNonceHorizonExceeded" }, + { "fields": [], "index": 49, "name": "ControlMutationRateLimited" }, + { "fields": [], "index": 50, "name": "QueueCapacityUnavailable" }, { "fields": [], - "index": 52, + "index": 51, "name": "RetryLaterNotAllowedForImmutableActor" }, - { "fields": [], "index": 53, "name": "ContinuationNotFound" }, - { "fields": [], "index": 54, "name": "ContinuationInvariant" }, - { "fields": [], "index": 55, "name": "ComputationOverflow" }, - { "fields": [], "index": 56, "name": "EmptyPrecondition" }, - { "fields": [], "index": 57, "name": "ManualSourceDisabled" }, - { "fields": [], "index": 58, "name": "RecipientDepositUnavailable" }, + { "fields": [], "index": 52, "name": "ActorRunNotFound" }, + { "fields": [], "index": 53, "name": "ActorRunInvariant" }, + { "fields": [], "index": 54, "name": "ComputationOverflow" }, + { "fields": [], "index": 55, "name": "EmptyPrecondition" }, + { "fields": [], "index": 56, "name": "ManualSourceDisabled" }, + { "fields": [], "index": 57, "name": "RecipientDepositUnavailable" }, { "fields": [], - "index": 59, + "index": 58, "name": "ObservationSubscriptionCapacityExceeded" }, - { "fields": [], "index": 60, "name": "ObservationSubscriptionInvariant" }, - { "fields": [], "index": 61, "name": "InvalidObservationRevision" }, - { "fields": [], "index": 62, "name": "DirtyObservationCapacityExceeded" }, - { "fields": [], "index": 63, "name": "DirtyObservationInvariant" }, - { "fields": [], "index": 64, "name": "ObservationUnavailable" }, - { "fields": [], "index": 65, "name": "ObservationUninitialized" }, - { "fields": [], "index": 66, "name": "CrossingIndexCapacityExceeded" }, - { "fields": [], "index": 67, "name": "CrossingUserCapacityExceeded" }, - { "fields": [], "index": 68, "name": "CrossingIndexInvariant" }, - { "fields": [], "index": 69, "name": "CrossingGenerationExhausted" }, + { "fields": [], "index": 59, "name": "ObservationSubscriptionInvariant" }, + { "fields": [], "index": 60, "name": "InvalidObservationRevision" }, + { "fields": [], "index": 61, "name": "DirtyObservationCapacityExceeded" }, + { "fields": [], "index": 62, "name": "DirtyObservationInvariant" }, + { "fields": [], "index": 63, "name": "ObservationUnavailable" }, + { "fields": [], "index": 64, "name": "ObservationUninitialized" }, + { "fields": [], "index": 65, "name": "CrossingIndexCapacityExceeded" }, + { "fields": [], "index": 66, "name": "CrossingUserCapacityExceeded" }, + { "fields": [], "index": 67, "name": "CrossingIndexInvariant" }, + { "fields": [], "index": 68, "name": "CrossingGenerationExhausted" }, { "fields": [], - "index": 70, + "index": 69, "name": "CrossingTransitionCapacityExceeded" }, - { "fields": [], "index": 71, "name": "CrossingTransitionInvariant" }, - { "fields": [], "index": 72, "name": "CrossingWorkerFaultNotFound" }, + { "fields": [], "index": 70, "name": "CrossingTransitionInvariant" }, + { "fields": [], "index": 71, "name": "CrossingWorkerFaultNotFound" }, { "fields": [], - "index": 73, + "index": 72, "name": "ObservationFanoutWorkerFaultNotFound" }, - { "fields": [], "index": 74, "name": "WakeupWorkerFaultNotFound" }, - { "fields": [], "index": 75, "name": "SystemActorTopologyInvalid" }, - { "fields": [], "index": 76, "name": "AdmissionBoundOverflow" } + { "fields": [], "index": 73, "name": "WakeupWorkerFaultNotFound" }, + { "fields": [], "index": 74, "name": "SystemActorTopologyInvalid" }, + { "fields": [], "index": 75, "name": "AdmissionBoundOverflow" }, + { "fields": [], "index": 76, "name": "StateHoldUnavailable" }, + { "fields": [], "index": 77, "name": "StateHoldInvariant" }, + { "fields": [], "index": 78, "name": "StateHoldOverflow" }, + { "fields": [], "index": 79, "name": "PrepassDuplicateOrStale" }, + { "fields": [], "index": 80, "name": "ResourceProtocolFailed" }, + { "fields": [], "index": 81, "name": "PrepassContextIncomplete" } ], "events": [ { "fields": [ { "name": "actor_id", "type": 10, "typeName": "ActorId" }, { "name": "owner", "type": 1, "typeName": "T::AccountId" }, - { "name": "actor_class", "type": 345, "typeName": "ActorClass" }, + { "name": "actor_class", "type": 346, "typeName": "ActorClass" }, { "name": "mutability", "type": 224, "typeName": "Mutability" }, { "name": "sovereign_account", @@ -385,7 +403,7 @@ }, { "name": "initial_lifecycle", - "type": 346, + "type": 347, "typeName": "InitialLifecycle" } ], @@ -415,7 +433,7 @@ { "fields": [ { "name": "actor_id", "type": 10, "typeName": "ActorId" }, - { "name": "reason", "type": 347, "typeName": "CloseReason" } + { "name": "reason", "type": 348, "typeName": "CloseReason" } ], "index": 5, "name": "ActorClosed" @@ -432,8 +450,8 @@ "fields": [ { "name": "actor_id", "type": 10, "typeName": "ActorId" }, { "name": "cycle_nonce", "type": 10, "typeName": "u64" }, - { "name": "result", "type": 348, "typeName": "CycleResult" }, - { "name": "outcomes", "type": 349, "typeName": "OutcomeTotals" } + { "name": "result", "type": 349, "typeName": "CycleResult" }, + { "name": "outcomes", "type": 350, "typeName": "OutcomeTotals" } ], "index": 7, "name": "CycleSummary" @@ -443,10 +461,10 @@ { "name": "actor_id", "type": 10, "typeName": "ActorId" }, { "name": "cycle_nonce", "type": 10, "typeName": "u64" }, { "name": "cursor", "type": 14, "typeName": "u32" }, - { "name": "reason", "type": 350, "typeName": "SuspensionReason" }, + { "name": "reason", "type": 351, "typeName": "SuspensionReason" }, { "name": "cumulative_outcomes", - "type": 349, + "type": 350, "typeName": "OutcomeTotals" } ], @@ -466,7 +484,7 @@ "fields": [ { "name": "actor_id", "type": 10, "typeName": "ActorId" }, { "name": "cycle_nonce", "type": 10, "typeName": "u64" }, - { "name": "reason", "type": 351, "typeName": "CancellationReason" } + { "name": "reason", "type": 352, "typeName": "CancellationReason" } ], "index": 10, "name": "CycleCancelled" @@ -485,7 +503,7 @@ { "name": "actor_id", "type": 10, "typeName": "ActorId" }, { "name": "cycle_nonce", "type": 10, "typeName": "u64" }, { "name": "step_index", "type": 14, "typeName": "u32" }, - { "name": "reason", "type": 352, "typeName": "StepSkippedReason" } + { "name": "reason", "type": 353, "typeName": "StepSkippedReason" } ], "index": 12, "name": "StepSkipped" @@ -495,7 +513,7 @@ { "name": "actor_id", "type": 10, "typeName": "ActorId" }, { "name": "cycle_nonce", "type": 10, "typeName": "u64" }, { "name": "step_index", "type": 14, "typeName": "u32" }, - { "name": "retry_class", "type": 353, "typeName": "RetryClass" }, + { "name": "retry_class", "type": 354, "typeName": "RetryClass" }, { "name": "error", "type": 290, "typeName": "DispatchError" } ], "index": 13, @@ -647,6 +665,24 @@ "index": 26, "name": "GlobalCircuitBreakerSet" }, + { + "fields": [ + { "name": "fault_id", "type": 355, "typeName": "FaultId" }, + { "name": "kind", "type": 356, "typeName": "ActorFaultKind" }, + { + "name": "first_recorded_block", + "type": 14, + "typeName": "BlockNumberFor" + }, + { + "name": "context", + "type": 357, + "typeName": "FaultContext>" + } + ], + "index": 27, + "name": "ActorFaultRecorded" + }, { "fields": [ { "name": "feed", "type": 216, "typeName": "T::ObservationFeedId" }, @@ -657,11 +693,11 @@ }, { "name": "class", - "type": 354, + "type": 360, "typeName": "CrossingWorkerFaultClass" } ], - "index": 27, + "index": 28, "name": "CrossingWorkerFaultCleared" }, { @@ -671,35 +707,67 @@ { "name": "subscriber_page", "type": 186, "typeName": "Option" }, { "name": "class", - "type": 354, + "type": 360, "typeName": "CrossingWorkerFaultClass" } ], - "index": 28, + "index": 29, "name": "ObservationFanoutWorkerFaultCleared" }, { "fields": [ { "name": "key", - "type": 355, + "type": 364, "typeName": "WakeupKey>" }, { "name": "page", "type": 10, "typeName": "WakeupPageId" }, { "name": "class", - "type": 354, + "type": 360, "typeName": "CrossingWorkerFaultClass" } ], - "index": 29, + "index": 30, "name": "WakeupWorkerFaultCleared" }, { "fields": [{ "name": "actor_id", "type": 10, "typeName": "ActorId" }], - "index": 30, + "index": 31, "name": "ManualTriggerSet" }, + { + "fields": [ + { "name": "actor_id", "type": 10, "typeName": "ActorId" }, + { + "name": "trigger_family", + "type": 365, + "typeName": "TriggerFamily" + }, + { "name": "fee", "type": 49, "typeName": "BalanceOf" } + ], + "index": 32, + "name": "TriggerOccurrenceProcessed" + }, + { + "fields": [ + { "name": "actor_id", "type": 10, "typeName": "ActorId" }, + { "name": "fee", "type": 49, "typeName": "BalanceOf" } + ], + "index": 33, + "name": "PipelineFeeCharged" + }, + { + "fields": [ + { "name": "actor_id", "type": 10, "typeName": "ActorId" }, + { "name": "cycle_nonce", "type": 10, "typeName": "u64" }, + { "name": "step_index", "type": 14, "typeName": "u32" }, + { "name": "actual_effect_weight", "type": 109, "typeName": "Weight" }, + { "name": "fee", "type": 49, "typeName": "BalanceOf" } + ], + "index": 34, + "name": "ActionFeeCharged" + }, { "fields": [ { "name": "actor_id", "type": 10, "typeName": "ActorId" }, @@ -707,7 +775,7 @@ { "name": "added", "type": 49, "typeName": "BalanceOf" }, { "name": "accumulated", "type": 49, "typeName": "BalanceOf" } ], - "index": 31, + "index": 35, "name": "FundingAccumulated" }, { @@ -717,21 +785,21 @@ { "name": "alive", "type": 14, "typeName": "u32" }, { "name": "missing", "type": 14, "typeName": "u32" } ], - "index": 32, + "index": 36, "name": "SweepBatchProcessed" }, { "fields": [ { "name": "consecutive_blocks", "type": 14, "typeName": "u32" } ], - "index": 33, + "index": 37, "name": "IdleStarvationDetected" }, { "fields": [ { "name": "consecutive_blocks", "type": 14, "typeName": "u32" } ], - "index": 34, + "index": 38, "name": "IdleStarvationRecovered" } ], @@ -751,19 +819,58 @@ "value": { "hashers": [{ "tag": "Blake2128Concat" }], "key": 10, - "value": 648 + "value": 658 + } + } + }, + { + "modifier": 0, + "name": "ActorContractHead", + "type": { + "tag": "map", + "value": { + "hashers": [{ "tag": "Blake2128Concat" }], + "key": 10, + "value": 667 } } }, { "modifier": 0, - "name": "ActorContract", + "name": "ActorActivationAuthority", "type": { "tag": "map", "value": { "hashers": [{ "tag": "Blake2128Concat" }], "key": 10, - "value": 226 + "value": 673 + } + } + }, + { + "modifier": 0, + "name": "ActorAdmissionCertificate", + "type": { + "tag": "map", + "value": { + "hashers": [{ "tag": "Blake2128Concat" }], + "key": 10, + "value": 674 + } + } + }, + { + "modifier": 0, + "name": "ActorContractTailChunk", + "type": { + "tag": "map", + "value": { + "hashers": [ + { "tag": "Blake2128Concat" }, + { "tag": "Blake2128Concat" } + ], + "key": 675, + "value": 676 } } }, @@ -775,19 +882,31 @@ "value": { "hashers": [{ "tag": "Blake2128Concat" }], "key": 10, - "value": 655 + "value": 681 + } + } + }, + { + "modifier": 0, + "name": "ActorRunHead", + "type": { + "tag": "map", + "value": { + "hashers": [{ "tag": "Blake2128Concat" }], + "key": 10, + "value": 686 } } }, { "modifier": 0, - "name": "ContinuationState", + "name": "ActorRunPayload", "type": { "tag": "map", "value": { "hashers": [{ "tag": "Blake2128Concat" }], "key": 10, - "value": 660 + "value": 692 } } }, @@ -799,7 +918,7 @@ "value": { "hashers": [{ "tag": "Blake2128Concat" }], "key": 10, - "value": 670 + "value": 702 } } }, @@ -808,6 +927,18 @@ "name": "ActorIdentityCount", "type": { "tag": "plain", "value": 14 } }, + { + "modifier": 0, + "name": "ActorStateHolds", + "type": { + "tag": "map", + "value": { + "hashers": [{ "tag": "Blake2128Concat" }], + "key": 10, + "value": 703 + } + } + }, { "modifier": 1, "name": "ActiveActorCount", @@ -821,7 +952,7 @@ "value": { "hashers": [{ "tag": "Blake2128Concat" }], "key": 10, - "value": 671 + "value": 705 } } }, @@ -835,6 +966,21 @@ "name": "NextQueueTicket", "type": { "tag": "plain", "value": 10 } }, + { + "modifier": 0, + "name": "PrepassExecutionCutoff", + "type": { "tag": "plain", "value": 706 } + }, + { + "modifier": 0, + "name": "CurrentBlockResourceState", + "type": { "tag": "plain", "value": 707 } + }, + { + "modifier": 0, + "name": "FinalizedBlockResourceTelemetry", + "type": { "tag": "plain", "value": 710 } + }, { "modifier": 1, "name": "QueueHead", @@ -858,7 +1004,7 @@ "value": { "hashers": [{ "tag": "Blake2128Concat" }], "key": 10, - "value": 672 + "value": 711 } } }, @@ -869,8 +1015,8 @@ "tag": "map", "value": { "hashers": [{ "tag": "Blake2128Concat" }], - "key": 675, - "value": 676 + "key": 715, + "value": 716 } } }, @@ -881,8 +1027,8 @@ "tag": "map", "value": { "hashers": [{ "tag": "Blake2128Concat" }], - "key": 355, - "value": 681 + "key": 364, + "value": 721 } } }, @@ -893,8 +1039,8 @@ "tag": "map", "value": { "hashers": [{ "tag": "Blake2128Concat" }], - "key": 682, - "value": 684 + "key": 722, + "value": 724 } } }, @@ -905,7 +1051,7 @@ "tag": "map", "value": { "hashers": [{ "tag": "Blake2128Concat" }], - "key": 683, + "key": 723, "value": 14 } } @@ -913,12 +1059,12 @@ { "modifier": 1, "name": "NextWakeupClock", - "type": { "tag": "plain", "value": 683 } + "type": { "tag": "plain", "value": 723 } }, { "modifier": 0, "name": "WakeupWorkerFaultState", - "type": { "tag": "plain", "value": 686 } + "type": { "tag": "plain", "value": 363 } }, { "modifier": 1, @@ -949,6 +1095,18 @@ "name": "ActiveActorLimit", "type": { "tag": "plain", "value": 14 } }, + { + "modifier": 0, + "name": "IndexedTriggerDetectionDisabled", + "type": { + "tag": "map", + "value": { + "hashers": [{ "tag": "Blake2128Concat" }], + "key": 10, + "value": 4 + } + } + }, { "modifier": 0, "name": "ActorObservationFeeds", @@ -957,7 +1115,7 @@ "value": { "hashers": [{ "tag": "Blake2128Concat" }], "key": 10, - "value": 687 + "value": 726 } } }, @@ -1003,7 +1161,7 @@ "value": { "hashers": [{ "tag": "Blake2128Concat" }], "key": 14, - "value": 688 + "value": 727 } } }, @@ -1017,8 +1175,8 @@ { "tag": "Blake2128Concat" }, { "tag": "Blake2128Concat" } ], - "key": 689, - "value": 690 + "key": 728, + "value": 729 } } }, @@ -1030,7 +1188,7 @@ "value": { "hashers": [{ "tag": "Blake2128Concat" }], "key": 216, - "value": 693 + "value": 732 } } }, @@ -1071,19 +1229,19 @@ "value": { "hashers": [{ "tag": "Blake2128Concat" }], "key": 216, - "value": 694 + "value": 733 } } }, { "modifier": 1, "name": "DirtyObservationListState", - "type": { "tag": "plain", "value": 696 } + "type": { "tag": "plain", "value": 736 } }, { "modifier": 0, "name": "ObservationFanoutWorkerFaultState", - "type": { "tag": "plain", "value": 697 } + "type": { "tag": "plain", "value": 361 } }, { "modifier": 0, @@ -1093,7 +1251,7 @@ "value": { "hashers": [{ "tag": "Blake2128Concat" }], "key": 10, - "value": 698 + "value": 737 } } }, @@ -1107,8 +1265,8 @@ { "tag": "Blake2128Concat" }, { "tag": "Blake2128Concat" } ], - "key": 701, - "value": 702 + "key": 740, + "value": 741 } } }, @@ -1119,8 +1277,8 @@ "tag": "map", "value": { "hashers": [{ "tag": "Blake2128Concat" }], - "key": 699, - "value": 706 + "key": 738, + "value": 745 } } }, @@ -1131,8 +1289,8 @@ "tag": "map", "value": { "hashers": [{ "tag": "Blake2128Concat" }], - "key": 707, - "value": 326 + "key": 746, + "value": 327 } } }, @@ -1168,7 +1326,7 @@ "value": { "hashers": [{ "tag": "Blake2128Concat" }], "key": 216, - "value": 708 + "value": 747 } } }, @@ -1180,14 +1338,14 @@ "value": { "hashers": [{ "tag": "Blake2128Concat" }], "key": 216, - "value": 711 + "value": 750 } } }, { "modifier": 1, "name": "CrossingPendingFeedListState", - "type": { "tag": "plain", "value": 712 } + "type": { "tag": "plain", "value": 751 } }, { "modifier": 0, @@ -1197,14 +1355,14 @@ "value": { "hashers": [{ "tag": "Blake2128Concat" }], "key": 216, - "value": 713 + "value": 752 } } }, { "modifier": 0, "name": "CrossingWorkerFaultState", - "type": { "tag": "plain", "value": 714 } + "type": { "tag": "plain", "value": 358 } }, { "modifier": 1, @@ -1219,7 +1377,7 @@ { "modifier": 1, "name": "IdleStarvationState", - "type": { "tag": "plain", "value": 715 } + "type": { "tag": "plain", "value": 753 } } ] }, @@ -1245,6 +1403,12 @@ "params": [], "path": [] }, + { + "def": { "tag": "tuple", "value": [] }, + "id": 4, + "params": [], + "path": [] + }, { "def": { "tag": "primitive", "value": { "tag": "u64" } }, "id": 10, @@ -1305,6 +1469,23 @@ "params": [], "path": ["sp_weights", "weight_v2", "Weight"] }, + { + "def": { + "tag": "variant", + "value": [ + { "docs": [], "fields": [], "index": 0, "name": "None" }, + { + "docs": [], + "fields": [{ "docs": [], "type": 109 }], + "index": 1, + "name": "Some" + } + ] + }, + "id": 161, + "params": [{ "name": "T", "type": 109 }], + "path": ["Option"] + }, { "def": { "tag": "variant", @@ -1720,8 +1901,9 @@ } ], "index": 19, - "name": "cancel_continuation" - } + "name": "cancel_run" + }, + { "docs": [], "fields": [], "index": 23, "name": "actor_prepass" } ] }, "id": 223, @@ -1877,12 +2059,25 @@ "fields": [ { "docs": [], - "name": "every_ticks", + "name": "after_ticks", "type": 10, "typeName": "u64" } ], "index": 4, + "name": "AtTime" + }, + { + "docs": [], + "fields": [ + { + "docs": [], + "name": "every_ticks", + "type": 10, + "typeName": "u64" + } + ], + "index": 5, "name": "Cadenced" } ] @@ -2878,6 +3073,23 @@ "params": [], "path": [] }, + { + "def": { + "tag": "variant", + "value": [ + { "docs": [], "fields": [], "index": 0, "name": "None" }, + { + "docs": [], + "fields": [{ "docs": [], "type": 2 }], + "index": 1, + "name": "Some" + } + ] + }, + "id": 278, + "params": [{ "name": "T", "type": 2 }], + "path": ["Option"] + }, { "def": { "tag": "variant", @@ -3039,20 +3251,114 @@ "path": ["sp_runtime", "proving_trie", "TrieError"] }, { - "def": { "tag": "sequence", "value": 320 }, - "id": 319, + "def": { + "tag": "variant", + "value": [ + { + "docs": [], + "fields": [ + { + "docs": [], + "type": 300, + "typeName": "pallet_session::HoldReason" + } + ], + "index": 22, + "name": "Session" + }, + { + "docs": [], + "fields": [ + { "docs": [], "type": 301, "typeName": "pallet_xcm::HoldReason" } + ], + "index": 31, + "name": "PolkadotXcm" + }, + { + "docs": [], + "fields": [ + { + "docs": [], + "type": 302, + "typeName": "pallet_preimage::HoldReason" + } + ], + "index": 45, + "name": "Preimage" + }, + { + "docs": [], + "fields": [ + { + "docs": [], + "type": 303, + "typeName": "pallet_deos_actors::HoldReason" + } + ], + "index": 55, + "name": "Actors" + } + ] + }, + "id": 299, + "params": [], + "path": ["deos_runtime", "RuntimeHoldReason"] + }, + { + "def": { + "tag": "variant", + "value": [{ "docs": [], "fields": [], "index": 0, "name": "Keys" }] + }, + "id": 300, + "params": [], + "path": ["pallet_session", "pallet", "HoldReason"] + }, + { + "def": { + "tag": "variant", + "value": [ + { "docs": [], "fields": [], "index": 0, "name": "AuthorizeAlias" } + ] + }, + "id": 301, + "params": [], + "path": ["pallet_xcm", "pallet", "HoldReason"] + }, + { + "def": { + "tag": "variant", + "value": [{ "docs": [], "fields": [], "index": 0, "name": "Preimage" }] + }, + "id": 302, + "params": [], + "path": ["pallet_preimage", "pallet", "HoldReason"] + }, + { + "def": { + "tag": "variant", + "value": [ + { "docs": [], "fields": [], "index": 0, "name": "ActorState" } + ] + }, + "id": 303, + "params": [], + "path": ["pallet_deos_actors", "pallet", "HoldReason"] + }, + { + "def": { "tag": "sequence", "value": 321 }, + "id": 320, "params": [], "path": [] }, { "def": { "tag": "tuple", "value": [197, 49] }, - "id": 320, + "id": 321, "params": [], "path": [] }, { "def": { "tag": "primitive", "value": { "tag": "u16" } }, - "id": 326, + "id": 327, "params": [], "path": [] }, @@ -3078,7 +3384,7 @@ { "docs": [], "name": "actor_class", - "type": 345, + "type": 346, "typeName": "ActorClass" }, { @@ -3096,7 +3402,7 @@ { "docs": [], "name": "initial_lifecycle", - "type": 346, + "type": 347, "typeName": "InitialLifecycle" } ], @@ -3167,7 +3473,7 @@ { "docs": [], "name": "reason", - "type": 347, + "type": 348, "typeName": "CloseReason" } ], @@ -3211,13 +3517,13 @@ { "docs": [], "name": "result", - "type": 348, + "type": 349, "typeName": "CycleResult" }, { "docs": [], "name": "outcomes", - "type": 349, + "type": 350, "typeName": "OutcomeTotals" } ], @@ -3243,13 +3549,13 @@ { "docs": [], "name": "reason", - "type": 350, + "type": 351, "typeName": "SuspensionReason" }, { "docs": [], "name": "cumulative_outcomes", - "type": 349, + "type": 350, "typeName": "OutcomeTotals" } ], @@ -3294,7 +3600,7 @@ { "docs": [], "name": "reason", - "type": 351, + "type": 352, "typeName": "CancellationReason" } ], @@ -3350,7 +3656,7 @@ { "docs": [], "name": "reason", - "type": 352, + "type": 353, "typeName": "StepSkippedReason" } ], @@ -3381,7 +3687,7 @@ { "docs": [], "name": "retry_class", - "type": 353, + "type": 354, "typeName": "RetryClass" }, { @@ -3902,6 +4208,37 @@ "index": 26, "name": "GlobalCircuitBreakerSet" }, + { + "docs": [], + "fields": [ + { + "docs": [], + "name": "fault_id", + "type": 355, + "typeName": "FaultId" + }, + { + "docs": [], + "name": "kind", + "type": 356, + "typeName": "ActorFaultKind" + }, + { + "docs": [], + "name": "first_recorded_block", + "type": 14, + "typeName": "BlockNumberFor" + }, + { + "docs": [], + "name": "context", + "type": 357, + "typeName": "FaultContext>" + } + ], + "index": 27, + "name": "ActorFaultRecorded" + }, { "docs": [], "fields": [ @@ -3920,11 +4257,11 @@ { "docs": [], "name": "class", - "type": 354, + "type": 360, "typeName": "CrossingWorkerFaultClass" } ], - "index": 27, + "index": 28, "name": "CrossingWorkerFaultCleared" }, { @@ -3951,11 +4288,11 @@ { "docs": [], "name": "class", - "type": 354, + "type": 360, "typeName": "CrossingWorkerFaultClass" } ], - "index": 28, + "index": 29, "name": "ObservationFanoutWorkerFaultCleared" }, { @@ -3964,7 +4301,7 @@ { "docs": [], "name": "key", - "type": 355, + "type": 364, "typeName": "WakeupKey>" }, { @@ -3976,11 +4313,11 @@ { "docs": [], "name": "class", - "type": 354, + "type": 360, "typeName": "CrossingWorkerFaultClass" } ], - "index": 29, + "index": 30, "name": "WakeupWorkerFaultCleared" }, { @@ -3993,9 +4330,90 @@ "typeName": "ActorId" } ], - "index": 30, + "index": 31, "name": "ManualTriggerSet" }, + { + "docs": [], + "fields": [ + { + "docs": [], + "name": "actor_id", + "type": 10, + "typeName": "ActorId" + }, + { + "docs": [], + "name": "trigger_family", + "type": 365, + "typeName": "TriggerFamily" + }, + { + "docs": [], + "name": "fee", + "type": 49, + "typeName": "BalanceOf" + } + ], + "index": 32, + "name": "TriggerOccurrenceProcessed" + }, + { + "docs": [], + "fields": [ + { + "docs": [], + "name": "actor_id", + "type": 10, + "typeName": "ActorId" + }, + { + "docs": [], + "name": "fee", + "type": 49, + "typeName": "BalanceOf" + } + ], + "index": 33, + "name": "PipelineFeeCharged" + }, + { + "docs": [], + "fields": [ + { + "docs": [], + "name": "actor_id", + "type": 10, + "typeName": "ActorId" + }, + { + "docs": [], + "name": "cycle_nonce", + "type": 10, + "typeName": "u64" + }, + { + "docs": [], + "name": "step_index", + "type": 14, + "typeName": "u32" + }, + { + "docs": [], + "name": "actual_effect_weight", + "type": 109, + "typeName": "Weight" + }, + { + "docs": [], + "name": "fee", + "type": 49, + "typeName": "BalanceOf" + } + ], + "index": 34, + "name": "ActionFeeCharged" + }, { "docs": [], "fields": [ @@ -4024,7 +4442,7 @@ "typeName": "BalanceOf" } ], - "index": 31, + "index": 35, "name": "FundingAccumulated" }, { @@ -4040,7 +4458,7 @@ { "docs": [], "name": "alive", "type": 14, "typeName": "u32" }, { "docs": [], "name": "missing", "type": 14, "typeName": "u32" } ], - "index": 32, + "index": 36, "name": "SweepBatchProcessed" }, { @@ -4053,7 +4471,7 @@ "typeName": "u32" } ], - "index": 33, + "index": 37, "name": "IdleStarvationDetected" }, { @@ -4066,12 +4484,12 @@ "typeName": "u32" } ], - "index": 34, + "index": 38, "name": "IdleStarvationRecovered" } ] }, - "id": 344, + "id": 345, "params": [{ "name": "T" }], "path": ["pallet_deos_actors", "pallet", "Event"] }, @@ -4102,7 +4520,7 @@ } ] }, - "id": 345, + "id": 346, "params": [], "path": ["pallet_deos_actors", "types", "lifecycle", "ActorClass"] }, @@ -4114,7 +4532,7 @@ { "docs": [], "fields": [], "index": 1, "name": "Active" } ] }, - "id": 346, + "id": 347, "params": [], "path": ["pallet_deos_actors", "types", "lifecycle", "InitialLifecycle"] }, @@ -4123,25 +4541,30 @@ "tag": "variant", "value": [ { "docs": [], "fields": [], "index": 0, "name": "OwnerInitiated" }, - { "docs": [], "fields": [], "index": 1, "name": "BalanceExhausted" }, + { + "docs": [], + "fields": [], + "index": 1, + "name": "CycleAdmissionInsufficient" + }, { "docs": [], "fields": [], "index": 2, - "name": "ConsecutiveFailures" + "name": "TriggerAdmissionInsufficient" }, - { "docs": [], "fields": [], "index": 3, "name": "WindowExpired" }, { "docs": [], "fields": [], - "index": 4, - "name": "CycleNonceExhausted" + "index": 3, + "name": "ConsecutiveFailures" }, + { "docs": [], "fields": [], "index": 4, "name": "WindowExpired" }, { "docs": [], "fields": [], "index": 5, - "name": "FeeBudgetExhausted" + "name": "CycleNonceExhausted" }, { "docs": [], @@ -4169,7 +4592,7 @@ } ] }, - "id": 347, + "id": 348, "params": [], "path": ["pallet_deos_actors", "types", "lifecycle", "CloseReason"] }, @@ -4182,7 +4605,7 @@ { "docs": [], "fields": [], "index": 2, "name": "Cancelled" } ] }, - "id": 348, + "id": 349, "params": [], "path": ["pallet_deos_actors", "types", "lifecycle", "CycleResult"] }, @@ -4223,7 +4646,7 @@ { "docs": [], "name": "failed_steps", "type": 14, "typeName": "u32" } ] }, - "id": 349, + "id": 350, "params": [], "path": ["pallet_deos_actors", "types", "lifecycle", "OutcomeTotals"] }, @@ -4240,7 +4663,7 @@ { "docs": [], "fields": [], "index": 1, "name": "Temporary" } ] }, - "id": 350, + "id": 351, "params": [], "path": ["pallet_deos_actors", "types", "lifecycle", "SuspensionReason"] }, @@ -4253,13 +4676,13 @@ { "docs": [], "fields": [], "index": 2, "name": "Deactivated" }, { "docs": [], - "fields": [{ "docs": [], "type": 347, "typeName": "CloseReason" }], + "fields": [{ "docs": [], "type": 348, "typeName": "CloseReason" }], "index": 3, "name": "Closing" } ] }, - "id": 351, + "id": 352, "params": [], "path": ["pallet_deos_actors", "types", "lifecycle", "CancellationReason"] }, @@ -4272,7 +4695,7 @@ { "docs": [], "fields": [], "index": 2, "name": "FundingUnavailable" } ] }, - "id": 352, + "id": 353, "params": [], "path": ["pallet_deos_actors", "types", "lifecycle", "StepSkippedReason"] }, @@ -4284,7 +4707,7 @@ { "docs": [], "fields": [], "index": 1, "name": "Temporary" } ] }, - "id": 353, + "id": 354, "params": [], "path": ["pallet_deos_actors", "adapters", "RetryClass"] }, @@ -4292,25 +4715,34 @@ "def": { "tag": "variant", "value": [ - { "docs": [], "fields": [], "index": 0, "name": "Invariant" }, - { "docs": [], "fields": [], "index": 1, "name": "Capacity" }, + { "docs": [], "fields": [], "index": 0, "name": "CrossingWorker" }, { "docs": [], "fields": [], - "index": 2, - "name": "SchedulerExhausted" + "index": 1, + "name": "ObservationFanoutWorker" }, - { "docs": [], "fields": [], "index": 3, "name": "Other" } + { "docs": [], "fields": [], "index": 2, "name": "WakeupWorker" } ] }, - "id": 354, + "id": 355, "params": [], - "path": [ - "pallet_deos_actors", - "types", - "observation", - "CrossingWorkerFaultClass" - ] + "path": ["pallet_deos_actors", "types", "fault", "FaultId"] + }, + { + "def": { + "tag": "variant", + "value": [ + { "docs": [], "fields": [], "index": 0, "name": "Control" }, + { "docs": [], "fields": [], "index": 1, "name": "Body" }, + { "docs": [], "fields": [], "index": 2, "name": "Detector" }, + { "docs": [], "fields": [], "index": 3, "name": "Wakeup" }, + { "docs": [], "fields": [], "index": 4, "name": "Queue" } + ] + }, + "id": 356, + "params": [], + "path": ["pallet_deos_actors", "types", "fault", "ActorFaultKind"] }, { "def": { @@ -4318,30 +4750,82 @@ "value": [ { "docs": [], - "fields": [{ "docs": [], "type": 14, "typeName": "BlockNumber" }], + "fields": [ + { + "docs": [], + "type": 358, + "typeName": "CrossingWorkerFault" + } + ], "index": 0, - "name": "Block" + "name": "Crossing" }, { "docs": [], - "fields": [{ "docs": [], "type": 10, "typeName": "SchedulerTick" }], + "fields": [ + { + "docs": [], + "type": 361, + "typeName": "ObservationFanoutWorkerFault" + } + ], "index": 1, - "name": "Tick" + "name": "ObservationFanout" + }, + { + "docs": [], + "fields": [ + { + "docs": [], + "type": 363, + "typeName": "WakeupWorkerFault" + } + ], + "index": 2, + "name": "Wakeup" } ] }, - "id": 355, - "params": [{ "name": "BlockNumber", "type": 14 }], - "path": ["pallet_deos_actors", "types", "scheduler", "WakeupKey"] + "id": 357, + "params": [ + { "name": "FeedId", "type": 216 }, + { "name": "BlockNumber", "type": 14 } + ], + "path": ["pallet_deos_actors", "types", "fault", "FaultContext"] }, { "def": { "tag": "composite", - "value": [{ "docs": [], "type": 103, "typeName": "[u8; 8]" }] + "value": [ + { "docs": [], "name": "feed", "type": 216, "typeName": "FeedId" }, + { + "docs": [], + "name": "revision", + "type": 189, + "typeName": "Option" + }, + { + "docs": [], + "name": "threshold", + "type": 359, + "typeName": "Option" + }, + { + "docs": [], + "name": "class", + "type": 360, + "typeName": "CrossingWorkerFaultClass" + } + ] }, - "id": 435, - "params": [], - "path": ["frame_support", "PalletId"] + "id": 358, + "params": [{ "name": "FeedId", "type": 216 }], + "path": [ + "pallet_deos_actors", + "types", + "observation", + "CrossingWorkerFault" + ] }, { "def": { @@ -4356,15 +4840,206 @@ } ] }, - "id": 507, + "id": 359, "params": [{ "name": "T", "type": 49 }], "path": ["Option"] }, { - "def": { "tag": "sequence", "value": 216 }, - "id": 641, + "def": { + "tag": "variant", + "value": [ + { "docs": [], "fields": [], "index": 0, "name": "Invariant" }, + { "docs": [], "fields": [], "index": 1, "name": "Capacity" }, + { + "docs": [], + "fields": [], + "index": 2, + "name": "SchedulerExhausted" + }, + { "docs": [], "fields": [], "index": 3, "name": "Other" } + ] + }, + "id": 360, "params": [], - "path": [] + "path": [ + "pallet_deos_actors", + "types", + "observation", + "CrossingWorkerFaultClass" + ] + }, + { + "def": { + "tag": "composite", + "value": [ + { "docs": [], "name": "feed", "type": 216, "typeName": "FeedId" }, + { + "docs": [], + "name": "revision", + "type": 10, + "typeName": "ObservationRevision" + }, + { + "docs": [], + "name": "subscriber_page", + "type": 186, + "typeName": "Option" + }, + { + "docs": [], + "name": "subscriber_position", + "type": 14, + "typeName": "u32" + }, + { + "docs": [], + "name": "actor_id", + "type": 189, + "typeName": "Option" + }, + { + "docs": [], + "name": "semantic_contract_id", + "type": 278, + "typeName": "Option<[u8; 32]>" + }, + { + "docs": [], + "name": "body_commitment", + "type": 278, + "typeName": "Option<[u8; 32]>" + }, + { + "docs": [], + "name": "admission_identity", + "type": 278, + "typeName": "Option<[u8; 32]>" + }, + { + "docs": [], + "name": "branch", + "type": 362, + "typeName": "ObservationFanoutBranch" + }, + { + "docs": [], + "name": "class", + "type": 360, + "typeName": "CrossingWorkerFaultClass" + } + ] + }, + "id": 361, + "params": [{ "name": "FeedId", "type": 216 }], + "path": [ + "pallet_deos_actors", + "types", + "observation", + "ObservationFanoutWorkerFault" + ] + }, + { + "def": { + "tag": "variant", + "value": [ + { "docs": [], "fields": [], "index": 0, "name": "Ordinary" }, + { "docs": [], "fields": [], "index": 1, "name": "Terminal" } + ] + }, + "id": 362, + "params": [], + "path": [ + "pallet_deos_actors", + "types", + "observation", + "ObservationFanoutBranch" + ] + }, + { + "def": { + "tag": "composite", + "value": [ + { + "docs": [], + "name": "key", + "type": 364, + "typeName": "WakeupKey" + }, + { + "docs": [], + "name": "page", + "type": 10, + "typeName": "WakeupPageId" + }, + { + "docs": [], + "name": "class", + "type": 360, + "typeName": "super::observation::CrossingWorkerFaultClass" + } + ] + }, + "id": 363, + "params": [{ "name": "BlockNumber", "type": 14 }], + "path": ["pallet_deos_actors", "types", "scheduler", "WakeupWorkerFault"] + }, + { + "def": { + "tag": "variant", + "value": [ + { + "docs": [], + "fields": [{ "docs": [], "type": 14, "typeName": "BlockNumber" }], + "index": 0, + "name": "Block" + }, + { + "docs": [], + "fields": [{ "docs": [], "type": 10, "typeName": "SchedulerTick" }], + "index": 1, + "name": "Tick" + } + ] + }, + "id": 364, + "params": [{ "name": "BlockNumber", "type": 14 }], + "path": ["pallet_deos_actors", "types", "scheduler", "WakeupKey"] + }, + { + "def": { + "tag": "variant", + "value": [ + { "docs": [], "fields": [], "index": 0, "name": "Manual" }, + { "docs": [], "fields": [], "index": 1, "name": "AddressEvent" }, + { "docs": [], "fields": [], "index": 2, "name": "ObservationChange" }, + { + "docs": [], + "fields": [], + "index": 3, + "name": "ObservationCrossing" + }, + { "docs": [], "fields": [], "index": 4, "name": "AtTime" }, + { "docs": [], "fields": [], "index": 5, "name": "Cadenced" } + ] + }, + "id": 365, + "params": [], + "path": ["pallet_deos_actors", "types", "contract", "TriggerFamily"] + }, + { + "def": { + "tag": "composite", + "value": [{ "docs": [], "type": 103, "typeName": "[u8; 8]" }] + }, + "id": 446, + "params": [], + "path": ["frame_support", "PalletId"] + }, + { + "def": { "tag": "sequence", "value": 216 }, + "id": 651, + "params": [], + "path": [] }, { "def": { @@ -4373,19 +5048,19 @@ { "docs": [], "name": "lifecycle", - "type": 649, + "type": 659, "typeName": "ActiveLifecycle" }, { "docs": [], "name": "cycle_state", - "type": 650, + "type": 660, "typeName": "CycleState" }, { "docs": [], "name": "trigger_runtime_state", - "type": 651, + "type": 661, "typeName": "TriggerRuntimeState" }, { @@ -4409,9 +5084,15 @@ { "docs": [], "name": "wakeup_pointer", - "type": 653, + "type": 663, "typeName": "Option>" }, + { + "docs": [], + "name": "trigger_wakeup_pointer", + "type": 665, + "typeName": "Option" + }, { "docs": [], "name": "terminal_at", @@ -4432,7 +5113,7 @@ } ] }, - "id": 648, + "id": 658, "params": [{ "name": "BlockNumber", "type": 14 }], "path": ["pallet_deos_actors", "types", "lifecycle", "ActorHotState"] }, @@ -4444,7 +5125,7 @@ { "docs": [], "fields": [], "index": 1, "name": "Paused" } ] }, - "id": 649, + "id": 659, "params": [], "path": ["pallet_deos_actors", "types", "lifecycle", "ActiveLifecycle"] }, @@ -4453,10 +5134,11 @@ "tag": "variant", "value": [ { "docs": [], "fields": [], "index": 0, "name": "Idle" }, - { "docs": [], "fields": [], "index": 1, "name": "Suspended" } + { "docs": [], "fields": [], "index": 1, "name": "Running" }, + { "docs": [], "fields": [], "index": 2, "name": "Suspended" } ] }, - "id": 650, + "id": 660, "params": [], "path": ["pallet_deos_actors", "types", "lifecycle", "CycleState"] }, @@ -4471,7 +5153,7 @@ { "docs": [], "name": "phase", - "type": 652, + "type": 662, "typeName": "CrossingPhase" }, { @@ -4492,14 +5174,28 @@ "name": "anchor_tick", "type": 189, "typeName": "Option" - } + }, + { "docs": [], "name": "consumed", "type": 50, "typeName": "bool" } ], "index": 2, + "name": "AtTime" + }, + { + "docs": [], + "fields": [ + { + "docs": [], + "name": "anchor_tick", + "type": 189, + "typeName": "Option" + } + ], + "index": 3, "name": "Cadenced" } ] }, - "id": 651, + "id": 661, "params": [], "path": [ "pallet_deos_actors", @@ -4516,7 +5212,7 @@ { "docs": [], "fields": [], "index": 1, "name": "WaitingForRearm" } ] }, - "id": 652, + "id": 662, "params": [], "path": ["pallet_deos_actors", "types", "contract", "CrossingPhase"] }, @@ -4527,14 +5223,14 @@ { "docs": [], "fields": [], "index": 0, "name": "None" }, { "docs": [], - "fields": [{ "docs": [], "type": 654 }], + "fields": [{ "docs": [], "type": 664 }], "index": 1, "name": "Some" } ] }, - "id": 653, - "params": [{ "name": "T", "type": 654 }], + "id": 663, + "params": [{ "name": "T", "type": 664 }], "path": ["Option"] }, { @@ -4544,7 +5240,7 @@ { "docs": [], "name": "block", - "type": 355, + "type": 364, "typeName": "WakeupKey" }, { @@ -4556,141 +5252,756 @@ { "docs": [], "name": "slot", "type": 14, "typeName": "WakeupSlot" } ] }, - "id": 654, + "id": 664, "params": [{ "name": "BlockNumber", "type": 14 }], "path": ["pallet_deos_actors", "types", "scheduler", "WakeupPointer"] }, { "def": { - "tag": "composite", + "tag": "variant", "value": [ + { "docs": [], "fields": [], "index": 0, "name": "None" }, { "docs": [], - "name": "funding_accumulated", - "type": 656, - "typeName": "FundingAccumulated" - }, + "fields": [{ "docs": [], "type": 666 }], + "index": 1, + "name": "Some" + } + ] + }, + "id": 665, + "params": [{ "name": "T", "type": 666 }], + "path": ["Option"] + }, + { + "def": { + "tag": "composite", + "value": [ { "docs": [], - "name": "funding_tracked_assets", - "type": 658, - "typeName": "FundingTrackedAssets" + "name": "tick", + "type": 10, + "typeName": "SchedulerTick" }, { "docs": [], - "name": "trigger_state_bond", - "type": 49, - "typeName": "Balance" - } + "name": "page_id", + "type": 10, + "typeName": "WakeupPageId" + }, + { "docs": [], "name": "slot", "type": 14, "typeName": "WakeupSlot" } ] }, - "id": 655, - "params": [ - { "name": "FundingAccumulated", "type": 656 }, - { "name": "FundingTrackedAssets", "type": 658 }, - { "name": "Balance", "type": 49 } - ], - "path": ["pallet_deos_actors", "types", "contract", "ActorFundingState"] + "id": 666, + "params": [], + "path": [ + "pallet_deos_actors", + "types", + "scheduler", + "TriggerWakeupPointer" + ] }, { "def": { "tag": "composite", - "value": [{ "docs": [], "type": 657, "typeName": "BTreeMap" }] + "value": [ + { "docs": [], "name": "header", "type": 668, "typeName": "Header" }, + { + "docs": [], + "name": "first_step", + "type": 670, + "typeName": "Option" + }, + { + "docs": [], + "name": "first_step_resources", + "type": 671, + "typeName": "Option" + } + ] }, - "id": 656, - "params": [ - { "name": "K", "type": 197 }, - { "name": "V", "type": 49 }, - { "name": "S" } - ], - "path": ["bounded_collections", "bounded_btree_map", "BoundedBTreeMap"] - }, - { - "def": { "tag": "composite", "value": [{ "docs": [], "type": 319 }] }, - "id": 657, + "id": 667, "params": [ - { "name": "K", "type": 197 }, - { "name": "V", "type": 49 } + { "name": "Header", "type": 668 }, + { "name": "Step", "type": 235 } ], - "path": ["BTreeMap"] - }, - { - "def": { - "tag": "composite", - "value": [{ "docs": [], "type": 659, "typeName": "BTreeSet" }] - }, - "id": 658, - "params": [{ "name": "T", "type": 197 }, { "name": "S" }], - "path": ["bounded_collections", "bounded_btree_set", "BoundedBTreeSet"] - }, - { - "def": { "tag": "composite", "value": [{ "docs": [], "type": 232 }] }, - "id": 659, - "params": [{ "name": "T", "type": 197 }], - "path": ["BTreeSet"] + "path": ["pallet_deos_actors", "types", "contract", "ActorContractHead"] }, { "def": { "tag": "composite", "value": [ - { "docs": [], "name": "cursor", "type": 14, "typeName": "u32" }, + { "docs": [], "name": "trigger", "type": 227, "typeName": "Trigger" }, { "docs": [], - "name": "unsuccessful_attempts_at_cursor", + "name": "cooldown_blocks", "type": 14, "typeName": "u32" }, { "docs": [], - "name": "last_attempt_block", - "type": 14, - "typeName": "BlockNumber" + "name": "window", + "type": 256, + "typeName": "Option>" }, { "docs": [], - "name": "opening_snapshot", - "type": 661, - "typeName": "BoundedBTreeMap, Balance, MaxSnapshotEntries>" - }, - { + "name": "funding", + "type": 253, + "typeName": "FundingPolicy" + }, + { "docs": [], - "name": "opening_predicate_results", - "type": 666, - "typeName": "BoundedVec, MaxOpeningPredicateResults>" + "name": "completion", + "type": 258, + "typeName": "CompletionPolicy" }, { "docs": [], - "name": "funding_snapshot", - "type": 656, - "typeName": "BoundedBTreeMap" + "name": "auto_close_at_cycle_nonce", + "type": 189, + "typeName": "Option" + }, + { "docs": [], "name": "step_count", "type": 14, "typeName": "u32" }, + { + "docs": [], + "name": "semantic_contract_id", + "type": 2, + "typeName": "Hash" + }, + { + "docs": [], + "name": "body_commitment", + "type": 2, + "typeName": "Hash" + }, + { + "docs": [], + "name": "admission_identity", + "type": 2, + "typeName": "Hash" + }, + { + "docs": [], + "name": "pipeline_machine_envelope", + "type": 669, + "typeName": "PipelineMachineEnvelope" + } + ] + }, + "id": 668, + "params": [ + { "name": "Trigger", "type": 227 }, + { "name": "BlockNumber", "type": 14 }, + { "name": "FundingPolicy", "type": 253 }, + { "name": "Balance", "type": 49 }, + { "name": "Hash", "type": 2 } + ], + "path": ["pallet_deos_actors", "types", "contract", "ActorContractHeader"] + }, + { + "def": { + "tag": "composite", + "value": [ + { + "docs": [], + "name": "pipeline_machine_fee_upper", + "type": 49, + "typeName": "Balance" + }, + { + "docs": [], + "name": "cleanup_fee_upper", + "type": 49, + "typeName": "Balance" + } + ] + }, + "id": 669, + "params": [{ "name": "Balance", "type": 49 }], + "path": [ + "pallet_deos_actors", + "types", + "contract", + "PipelineMachineEnvelope" + ] + }, + { + "def": { + "tag": "variant", + "value": [ + { "docs": [], "fields": [], "index": 0, "name": "None" }, + { + "docs": [], + "fields": [{ "docs": [], "type": 235 }], + "index": 1, + "name": "Some" + } + ] + }, + "id": 670, + "params": [{ "name": "T", "type": 235 }], + "path": ["Option"] + }, + { + "def": { + "tag": "variant", + "value": [ + { "docs": [], "fields": [], "index": 0, "name": "None" }, + { + "docs": [], + "fields": [{ "docs": [], "type": 672 }], + "index": 1, + "name": "Some" + } + ] + }, + "id": 671, + "params": [{ "name": "T", "type": 672 }], + "path": ["Option"] + }, + { + "def": { + "tag": "composite", + "value": [ + { + "docs": [], + "name": "control", + "type": 109, + "typeName": "ActorStepControlWeight" + }, + { + "docs": [], + "name": "effect", + "type": 109, + "typeName": "ActorTaskEffectWeight" + } + ] + }, + "id": 672, + "params": [], + "path": [ + "pallet_deos_actors", + "types", + "contract", + "ActorStepResourceEnvelope" + ] + }, + { + "def": { + "tag": "composite", + "value": [ + { + "docs": [], + "name": "feed", + "type": 216, + "typeName": "ObservationFeedId" + }, + { + "docs": [], + "name": "cooldown_blocks", + "type": 14, + "typeName": "u32" + }, + { + "docs": [], + "name": "window", + "type": 256, + "typeName": "Option>" + }, + { + "docs": [], + "name": "auto_close_at_cycle_nonce", + "type": 189, + "typeName": "Option" + }, + { + "docs": [], + "name": "semantic_contract_id", + "type": 2, + "typeName": "Hash" + }, + { + "docs": [], + "name": "body_commitment", + "type": 2, + "typeName": "Hash" + }, + { + "docs": [], + "name": "admission_identity", + "type": 2, + "typeName": "Hash" + } + ] + }, + "id": 673, + "params": [ + { "name": "ObservationFeedId", "type": 216 }, + { "name": "BlockNumber", "type": 14 }, + { "name": "Hash", "type": 2 } + ], + "path": [ + "pallet_deos_actors", + "types", + "contract", + "ActorActivationAuthority" + ] + }, + { + "def": { + "tag": "composite", + "value": [ + { + "docs": [], + "name": "semantic_contract_id", + "type": 2, + "typeName": "[u8; 32]" + }, + { + "docs": [], + "name": "body_commitment", + "type": 2, + "typeName": "[u8; 32]" + }, + { + "docs": [], + "name": "runtime_actor_semantics_version", + "type": 14, + "typeName": "u32" + }, + { + "docs": [], + "name": "production_weight_identity", + "type": 2, + "typeName": "[u8; 32]" + }, + { + "docs": [], + "name": "body_geometry_version", + "type": 14, + "typeName": "u32" + }, + { + "docs": [], + "name": "configured_bounds_commitment", + "type": 2, + "typeName": "[u8; 32]" + }, + { + "docs": [], + "name": "maximum_lifecycle_weight", + "type": 109, + "typeName": "Weight" + }, + { + "docs": [], + "name": "admission_identity", + "type": 2, + "typeName": "[u8; 32]" + } + ] + }, + "id": 674, + "params": [{ "name": "Resources" }], + "path": [ + "pallet_deos_actors", + "types", + "contract", + "ActorAdmissionCertificate" + ] + }, + { + "def": { "tag": "tuple", "value": [10, 14] }, + "id": 675, + "params": [], + "path": [] + }, + { + "def": { + "tag": "composite", + "value": [ + { + "docs": [], + "name": "authority", + "type": 680, + "typeName": "ActorBodyAuthority" + }, + { + "docs": [], + "name": "first_step_index", + "type": 14, + "typeName": "u32" + }, + { "docs": [], "name": "steps", "type": 677, "typeName": "Steps" }, + { + "docs": [], + "name": "step_resources", + "type": 678, + "typeName": "Resources" + } + ] + }, + "id": 676, + "params": [ + { "name": "ActorId", "type": 10 }, + { "name": "Hash", "type": 2 }, + { "name": "Steps", "type": 677 }, + { "name": "Resources", "type": 678 } + ], + "path": ["pallet_deos_actors", "types", "contract", "ActorStepChunk"] + }, + { + "def": { + "tag": "composite", + "value": [{ "docs": [], "type": 252, "typeName": "Vec" }] + }, + "id": 677, + "params": [{ "name": "T", "type": 235 }, { "name": "S" }], + "path": ["bounded_collections", "bounded_vec", "BoundedVec"] + }, + { + "def": { + "tag": "composite", + "value": [{ "docs": [], "type": 679, "typeName": "Vec" }] + }, + "id": 678, + "params": [{ "name": "T", "type": 672 }, { "name": "S" }], + "path": ["bounded_collections", "bounded_vec", "BoundedVec"] + }, + { + "def": { "tag": "sequence", "value": 672 }, + "id": 679, + "params": [], + "path": [] + }, + { + "def": { + "tag": "composite", + "value": [ + { "docs": [], "name": "actor_id", "type": 10, "typeName": "ActorId" }, + { + "docs": [], + "name": "semantic_contract_id", + "type": 2, + "typeName": "Hash" + }, + { + "docs": [], + "name": "body_commitment", + "type": 2, + "typeName": "Hash" + }, + { + "docs": [], + "name": "admission_identity", + "type": 2, + "typeName": "Hash" + } + ] + }, + "id": 680, + "params": [ + { "name": "ActorId", "type": 10 }, + { "name": "Hash", "type": 2 } + ], + "path": ["pallet_deos_actors", "types", "contract", "ActorBodyAuthority"] + }, + { + "def": { + "tag": "composite", + "value": [ + { + "docs": [], + "name": "funding_accumulated", + "type": 682, + "typeName": "FundingAccumulated" + }, + { + "docs": [], + "name": "funding_tracked_assets", + "type": 684, + "typeName": "FundingTrackedAssets" + } + ] + }, + "id": 681, + "params": [ + { "name": "FundingAccumulated", "type": 682 }, + { "name": "FundingTrackedAssets", "type": 684 } + ], + "path": ["pallet_deos_actors", "types", "contract", "ActorFundingState"] + }, + { + "def": { + "tag": "composite", + "value": [{ "docs": [], "type": 683, "typeName": "BTreeMap" }] + }, + "id": 682, + "params": [ + { "name": "K", "type": 197 }, + { "name": "V", "type": 49 }, + { "name": "S" } + ], + "path": ["bounded_collections", "bounded_btree_map", "BoundedBTreeMap"] + }, + { + "def": { "tag": "composite", "value": [{ "docs": [], "type": 320 }] }, + "id": 683, + "params": [ + { "name": "K", "type": 197 }, + { "name": "V", "type": 49 } + ], + "path": ["BTreeMap"] + }, + { + "def": { + "tag": "composite", + "value": [{ "docs": [], "type": 685, "typeName": "BTreeSet" }] + }, + "id": 684, + "params": [{ "name": "T", "type": 197 }, { "name": "S" }], + "path": ["bounded_collections", "bounded_btree_set", "BoundedBTreeSet"] + }, + { + "def": { "tag": "composite", "value": [{ "docs": [], "type": 232 }] }, + "id": 685, + "params": [{ "name": "T", "type": 197 }], + "path": ["BTreeSet"] + }, + { + "def": { + "tag": "composite", + "value": [ + { + "docs": [], + "name": "contract_authority", + "type": 687, + "typeName": "ActorRunAuthority<[u8; 32]>" + }, + { + "docs": [], + "name": "payload_commitment", + "type": 2, + "typeName": "[u8; 32]" + }, + { "docs": [], "name": "cycle_nonce", "type": 10, "typeName": "u64" }, + { "docs": [], "name": "cursor", "type": 14, "typeName": "u32" }, + { + "docs": [], + "name": "opening_predicate_cursor", + "type": 14, + "typeName": "u32" + }, + { + "docs": [], + "name": "opening_predicate_result_count", + "type": 14, + "typeName": "u32" + }, + { + "docs": [], + "name": "unsuccessful_attempts_at_cursor", + "type": 14, + "typeName": "u32" + }, + { + "docs": [], + "name": "last_attempt_block", + "type": 14, + "typeName": "BlockNumber" + }, + { + "docs": [], + "name": "last_committed_step_block", + "type": 186, + "typeName": "Option" + }, + { + "docs": [], + "name": "eligible_at", + "type": 14, + "typeName": "BlockNumber" }, { "docs": [], "name": "cumulative_outcomes", - "type": 349, + "type": 350, "typeName": "OutcomeTotals" + }, + { + "docs": [], + "name": "last_step_outcome", + "type": 688, + "typeName": "Option" + }, + { + "docs": [], + "name": "suspension", + "type": 691, + "typeName": "Option" } ] }, - "id": 660, + "id": 686, + "params": [{ "name": "BlockNumber", "type": 14 }], + "path": ["pallet_deos_actors", "types", "lifecycle", "ActorRunHead"] + }, + { + "def": { + "tag": "composite", + "value": [ + { + "docs": [], + "name": "semantic_contract_id", + "type": 2, + "typeName": "Hash" + }, + { + "docs": [], + "name": "body_commitment", + "type": 2, + "typeName": "Hash" + }, + { + "docs": [], + "name": "admission_identity", + "type": 2, + "typeName": "Hash" + } + ] + }, + "id": 687, + "params": [{ "name": "Hash", "type": 2 }], + "path": ["pallet_deos_actors", "types", "lifecycle", "ActorRunAuthority"] + }, + { + "def": { + "tag": "variant", + "value": [ + { "docs": [], "fields": [], "index": 0, "name": "None" }, + { + "docs": [], + "fields": [{ "docs": [], "type": 689 }], + "index": 1, + "name": "Some" + } + ] + }, + "id": 688, + "params": [{ "name": "T", "type": 689 }], + "path": ["Option"] + }, + { + "def": { + "tag": "variant", + "value": [ + { "docs": [], "fields": [], "index": 0, "name": "Executed" }, + { "docs": [], "fields": [], "index": 1, "name": "Stopped" }, + { + "docs": [], + "fields": [ + { "docs": [], "type": 353, "typeName": "StepSkippedReason" } + ], + "index": 2, + "name": "Skipped" + }, + { + "docs": [], + "fields": [], + "index": 3, + "name": "FundingUnavailable" + }, + { + "docs": [], + "fields": [ + { "docs": [], "type": 690, "typeName": "crate::TaskFailure" } + ], + "index": 4, + "name": "Failed" + } + ] + }, + "id": 689, + "params": [], + "path": ["pallet_deos_actors", "types", "lifecycle", "StepOutcome"] + }, + { + "def": { + "tag": "composite", + "value": [ + { + "docs": [], + "name": "error", + "type": 290, + "typeName": "DispatchError" + }, + { "docs": [], "name": "retry", "type": 354, "typeName": "RetryClass" } + ] + }, + "id": 690, + "params": [], + "path": ["pallet_deos_actors", "adapters", "TaskFailure"] + }, + { + "def": { + "tag": "variant", + "value": [ + { "docs": [], "fields": [], "index": 0, "name": "None" }, + { + "docs": [], + "fields": [{ "docs": [], "type": 351 }], + "index": 1, + "name": "Some" + } + ] + }, + "id": 691, + "params": [{ "name": "T", "type": 351 }], + "path": ["Option"] + }, + { + "def": { + "tag": "composite", + "value": [ + { + "docs": [], + "name": "opening_snapshot", + "type": 693, + "typeName": "BoundedBTreeMap, Balance, MaxSnapshotEntries>" + }, + { + "docs": [], + "name": "opening_predicate_results", + "type": 698, + "typeName": "BoundedVec, MaxOpeningPredicateResults>" + }, + { + "docs": [], + "name": "funding_snapshot", + "type": 682, + "typeName": "BoundedBTreeMap" + } + ] + }, + "id": 692, "params": [ { "name": "AssetId", "type": 197 }, { "name": "Balance", "type": 49 }, - { "name": "BlockNumber", "type": 14 }, { "name": "MaxSnapshotEntries" }, { "name": "MaxFundingTrackedAssets" }, { "name": "MaxOpeningPredicateResults" } ], - "path": ["pallet_deos_actors", "types", "lifecycle", "ContinuationState"] + "path": ["pallet_deos_actors", "types", "lifecycle", "ActorRunPayload"] }, { "def": { "tag": "composite", - "value": [{ "docs": [], "type": 663, "typeName": "BTreeMap" }] + "value": [{ "docs": [], "type": 695, "typeName": "BTreeMap" }] }, - "id": 661, + "id": 693, "params": [ - { "name": "K", "type": 662 }, + { "name": "K", "type": 694 }, { "name": "V", "type": 49 }, { "name": "S" } ], @@ -4720,38 +6031,38 @@ } ] }, - "id": 662, + "id": 694, "params": [{ "name": "AssetId", "type": 197 }], "path": ["pallet_deos_actors", "types", "contract", "OpeningSurface"] }, { - "def": { "tag": "composite", "value": [{ "docs": [], "type": 664 }] }, - "id": 663, + "def": { "tag": "composite", "value": [{ "docs": [], "type": 696 }] }, + "id": 695, "params": [ - { "name": "K", "type": 662 }, + { "name": "K", "type": 694 }, { "name": "V", "type": 49 } ], "path": ["BTreeMap"] }, { - "def": { "tag": "sequence", "value": 665 }, - "id": 664, + "def": { "tag": "sequence", "value": 697 }, + "id": 696, "params": [], "path": [] }, { - "def": { "tag": "tuple", "value": [662, 49] }, - "id": 665, + "def": { "tag": "tuple", "value": [694, 49] }, + "id": 697, "params": [], "path": [] }, { "def": { "tag": "composite", - "value": [{ "docs": [], "type": 669, "typeName": "Vec" }] + "value": [{ "docs": [], "type": 701, "typeName": "Vec" }] }, - "id": 666, - "params": [{ "name": "T", "type": 667 }, { "name": "S" }], + "id": 698, + "params": [{ "name": "T", "type": 699 }, { "name": "S" }], "path": ["bounded_collections", "bounded_vec", "BoundedVec"] }, { @@ -4766,16 +6077,16 @@ }, { "docs": [], - "fields": [{ "docs": [], "type": 668 }], + "fields": [{ "docs": [], "type": 700 }], "index": 1, "name": "Err" } ] }, - "id": 667, + "id": 699, "params": [ { "name": "T", "type": 50 }, - { "name": "E", "type": 668 } + { "name": "E", "type": 700 } ], "path": ["Result"] }, @@ -4786,13 +6097,13 @@ { "docs": [], "fields": [], "index": 0, "name": "InvalidObservation" } ] }, - "id": 668, + "id": 700, "params": [], "path": ["pallet_deos_actors", "types", "contract", "PredicateError"] }, { - "def": { "tag": "sequence", "value": 667 }, - "id": 669, + "def": { "tag": "sequence", "value": 699 }, + "id": 701, "params": [], "path": [] }, @@ -4810,7 +6121,7 @@ { "docs": [], "name": "actor_class", - "type": 345, + "type": 346, "typeName": "ActorClass" }, { @@ -4828,13 +6139,69 @@ } ] }, - "id": 670, + "id": 702, "params": [ { "name": "AccountId", "type": 1 }, { "name": "BlockNumber", "type": 14 } ], "path": ["pallet_deos_actors", "types", "lifecycle", "ActorIdentity"] }, + { + "def": { + "tag": "composite", + "value": [ + { "docs": [], "name": "owner", "type": 1, "typeName": "AccountId" }, + { + "docs": [], + "name": "breakdown", + "type": 704, + "typeName": "ActorStateHoldBreakdown" + } + ] + }, + "id": 703, + "params": [ + { "name": "AccountId", "type": 1 }, + { "name": "Balance", "type": 49 } + ], + "path": [ + "pallet_deos_actors", + "types", + "lifecycle", + "ActorStateHoldRecord" + ] + }, + { + "def": { + "tag": "composite", + "value": [ + { "docs": [], "name": "identity", "type": 49, "typeName": "Balance" }, + { + "docs": [], + "name": "contract_head", + "type": 49, + "typeName": "Balance" + }, + { + "docs": [], + "name": "contract_body", + "type": 49, + "typeName": "Balance" + }, + { "docs": [], "name": "detector", "type": 49, "typeName": "Balance" }, + { "docs": [], "name": "funding", "type": 49, "typeName": "Balance" }, + { "docs": [], "name": "run", "type": 49, "typeName": "Balance" } + ] + }, + "id": 704, + "params": [{ "name": "Balance", "type": 49 }], + "path": [ + "pallet_deos_actors", + "types", + "lifecycle", + "ActorStateHoldBreakdown" + ] + }, { "def": { "tag": "variant", @@ -4848,7 +6215,7 @@ } ] }, - "id": 671, + "id": 705, "params": [], "path": [ "pallet_deos_actors", @@ -4857,41 +6224,219 @@ "SystemSovereignState" ] }, + { + "def": { "tag": "tuple", "value": [14, 10] }, + "id": 706, + "params": [], + "path": [] + }, { "def": { "tag": "composite", - "value": [{ "docs": [], "type": 674, "typeName": "Vec" }] + "value": [ + { + "docs": [], + "name": "block_number", + "type": 14, + "typeName": "BlockNumber" + }, + { + "docs": [], + "name": "phase", + "type": 708, + "typeName": "BlockResourcePhase" + }, + { + "docs": [], + "name": "usage", + "type": 709, + "typeName": "BlockResourceUsage" + }, + { + "docs": [], + "name": "outstanding_reservations", + "type": 14, + "typeName": "u32" + }, + { + "docs": [], + "name": "finalized_fixed_reserved", + "type": 161, + "typeName": "Option" + }, + { + "docs": [], + "name": "optional_actor_work_halted", + "type": 50, + "typeName": "bool" + } + ] }, - "id": 672, - "params": [{ "name": "T", "type": 673 }, { "name": "S" }], + "id": 707, + "params": [{ "name": "BlockNumber", "type": 14 }], + "path": ["pallet_deos_actors", "types", "resource", "BlockResourceState"] + }, + { + "def": { + "tag": "variant", + "value": [ + { "docs": [], "fields": [], "index": 0, "name": "ContextIncomplete" }, + { "docs": [], "fields": [], "index": 1, "name": "PrepassExecuting" }, + { "docs": [], "fields": [], "index": 2, "name": "ExternalPhase" }, + { "docs": [], "fields": [], "index": 3, "name": "FreshDrain" }, + { "docs": [], "fields": [], "index": 4, "name": "Finalizable" } + ] + }, + "id": 708, + "params": [], + "path": ["pallet_deos_actors", "types", "resource", "BlockResourcePhase"] + }, + { + "def": { + "tag": "composite", + "value": [ + { + "docs": [], + "name": "actor_control", + "type": 109, + "typeName": "Weight" + }, + { + "docs": [], + "name": "actor_effect", + "type": 109, + "typeName": "Weight" + }, + { + "docs": [], + "name": "user_dispatch", + "type": 109, + "typeName": "Weight" + } + ] + }, + "id": 709, + "params": [], + "path": ["pallet_deos_actors", "types", "resource", "BlockResourceUsage"] + }, + { + "def": { + "tag": "composite", + "value": [ + { + "docs": [], + "name": "block_number", + "type": 14, + "typeName": "BlockNumber" + }, + { + "docs": [], + "name": "fixed_reserved", + "type": 109, + "typeName": "Weight" + }, + { + "docs": [], + "name": "usage", + "type": 709, + "typeName": "BlockResourceUsage" + }, + { + "docs": [], + "name": "optional_actor_work_halted", + "type": 50, + "typeName": "bool" + } + ] + }, + "id": 710, + "params": [{ "name": "BlockNumber", "type": 14 }], + "path": [ + "pallet_deos_actors", + "types", + "resource", + "FinalizedBlockResourceSnapshot" + ] + }, + { + "def": { + "tag": "composite", + "value": [{ "docs": [], "type": 714, "typeName": "Vec" }] + }, + "id": 711, + "params": [{ "name": "T", "type": 712 }, { "name": "S" }], "path": ["bounded_collections", "bounded_vec", "BoundedVec"] }, { "def": { "tag": "composite", "value": [ + { "docs": [], "name": "actor_id", "type": 10, "typeName": "ActorId" }, + { "docs": [], "name": "cycle_nonce", "type": 10, "typeName": "u64" }, + { "docs": [], "name": "cursor", "type": 14, "typeName": "u32" }, { "docs": [], "name": "ticket", "type": 10, "typeName": "QueueTicket" }, - { "docs": [], "name": "actor_id", "type": 10, "typeName": "ActorId" } + { + "docs": [], + "name": "eligible_at", + "type": 14, + "typeName": "BlockNumber" + }, + { + "docs": [], + "name": "contract_commitment", + "type": 713, + "typeName": "ContractCommitment" + } ] }, - "id": 673, - "params": [], - "path": ["pallet_deos_actors", "types", "scheduler", "QueueEntry"] + "id": 712, + "params": [ + { "name": "BlockNumber", "type": 14 }, + { "name": "ContractCommitment", "type": 713 } + ], + "path": ["pallet_deos_actors", "types", "scheduler", "ActorStepTicket"] }, { - "def": { "tag": "sequence", "value": 673 }, - "id": 674, + "def": { + "tag": "composite", + "value": [ + { + "docs": [], + "name": "semantic_contract_id", + "type": 2, + "typeName": "Hash" + }, + { + "docs": [], + "name": "body_commitment", + "type": 2, + "typeName": "Hash" + } + ] + }, + "id": 713, + "params": [{ "name": "Hash", "type": 2 }], + "path": [ + "pallet_deos_actors", + "types", + "contract", + "ActorContractCommitment" + ] + }, + { + "def": { "tag": "sequence", "value": 712 }, + "id": 714, "params": [], "path": [] }, { - "def": { "tag": "tuple", "value": [355, 10] }, - "id": 675, + "def": { "tag": "tuple", "value": [364, 10] }, + "id": 715, "params": [], "path": [] }, @@ -4899,7 +6444,7 @@ "def": { "tag": "composite", "value": [ - { "docs": [], "name": "entries", "type": 677, "typeName": "Entries" }, + { "docs": [], "name": "entries", "type": 717, "typeName": "Entries" }, { "docs": [], "name": "live_entries", "type": 14, "typeName": "u32" }, { "docs": [], @@ -4921,17 +6466,17 @@ } ] }, - "id": 676, - "params": [{ "name": "Entries", "type": 677 }], + "id": 716, + "params": [{ "name": "Entries", "type": 717 }], "path": ["pallet_deos_actors", "types", "scheduler", "WakeupPage"] }, { "def": { "tag": "composite", - "value": [{ "docs": [], "type": 680, "typeName": "Vec" }] + "value": [{ "docs": [], "type": 720, "typeName": "Vec" }] }, - "id": 677, - "params": [{ "name": "T", "type": 678 }, { "name": "S" }], + "id": 717, + "params": [{ "name": "T", "type": 718 }, { "name": "S" }], "path": ["bounded_collections", "bounded_vec", "BoundedVec"] }, { @@ -4941,14 +6486,14 @@ { "docs": [], "fields": [], "index": 0, "name": "None" }, { "docs": [], - "fields": [{ "docs": [], "type": 679 }], + "fields": [{ "docs": [], "type": 719 }], "index": 1, "name": "Some" } ] }, - "id": 678, - "params": [{ "name": "T", "type": 679 }], + "id": 718, + "params": [{ "name": "T", "type": 719 }], "path": ["Option"] }, { @@ -4958,13 +6503,13 @@ { "docs": [], "name": "actor_id", "type": 10, "typeName": "ActorId" } ] }, - "id": 679, + "id": 719, "params": [], "path": ["pallet_deos_actors", "types", "scheduler", "WakeupEntry"] }, { - "def": { "tag": "sequence", "value": 678 }, - "id": 680, + "def": { "tag": "sequence", "value": 718 }, + "id": 720, "params": [], "path": [] }, @@ -4999,13 +6544,13 @@ } ] }, - "id": 681, + "id": 721, "params": [], "path": ["pallet_deos_actors", "types", "scheduler", "WakeupBucketState"] }, { - "def": { "tag": "tuple", "value": [683, 10] }, - "id": 682, + "def": { "tag": "tuple", "value": [723, 10] }, + "id": 722, "params": [], "path": [] }, @@ -5017,59 +6562,31 @@ { "docs": [], "fields": [], "index": 1, "name": "Tick" } ] }, - "id": 683, + "id": 723, "params": [], "path": ["pallet_deos_actors", "types", "scheduler", "WakeupClock"] }, { "def": { "tag": "composite", - "value": [{ "docs": [], "type": 685, "typeName": "Vec" }] + "value": [{ "docs": [], "type": 725, "typeName": "Vec" }] }, - "id": 684, - "params": [{ "name": "T", "type": 355 }, { "name": "S" }], + "id": 724, + "params": [{ "name": "T", "type": 364 }, { "name": "S" }], "path": ["bounded_collections", "bounded_vec", "BoundedVec"] }, { - "def": { "tag": "sequence", "value": 355 }, - "id": 685, + "def": { "tag": "sequence", "value": 364 }, + "id": 725, "params": [], "path": [] }, { "def": { "tag": "composite", - "value": [ - { - "docs": [], - "name": "key", - "type": 355, - "typeName": "WakeupKey" - }, - { - "docs": [], - "name": "page", - "type": 10, - "typeName": "WakeupPageId" - }, - { - "docs": [], - "name": "class", - "type": 354, - "typeName": "super::observation::CrossingWorkerFaultClass" - } - ] - }, - "id": 686, - "params": [{ "name": "BlockNumber", "type": 14 }], - "path": ["pallet_deos_actors", "types", "scheduler", "WakeupWorkerFault"] - }, - { - "def": { - "tag": "composite", - "value": [{ "docs": [], "type": 641, "typeName": "Vec" }] + "value": [{ "docs": [], "type": 651, "typeName": "Vec" }] }, - "id": 687, + "id": 726, "params": [{ "name": "T", "type": 216 }, { "name": "S" }], "path": ["bounded_collections", "bounded_vec", "BoundedVec"] }, @@ -5078,13 +6595,13 @@ "tag": "composite", "value": [{ "docs": [], "type": 203, "typeName": "Vec" }] }, - "id": 688, + "id": 727, "params": [{ "name": "T", "type": 14 }, { "name": "S" }], "path": ["bounded_collections", "bounded_vec", "BoundedVec"] }, { "def": { "tag": "tuple", "value": [216, 14] }, - "id": 689, + "id": 728, "params": [], "path": [] }, @@ -5107,12 +6624,12 @@ { "docs": [], "name": "entries", - "type": 691, + "type": 730, "typeName": "BoundedVec, MaxEntries>" } ] }, - "id": 690, + "id": 729, "params": [{ "name": "MaxEntries" }], "path": [ "pallet_deos_actors", @@ -5124,15 +6641,15 @@ { "def": { "tag": "composite", - "value": [{ "docs": [], "type": 692, "typeName": "Vec" }] + "value": [{ "docs": [], "type": 731, "typeName": "Vec" }] }, - "id": 691, + "id": 730, "params": [{ "name": "T", "type": 189 }, { "name": "S" }], "path": ["bounded_collections", "bounded_vec", "BoundedVec"] }, { "def": { "tag": "sequence", "value": 189 }, - "id": 692, + "id": 731, "params": [], "path": [] }, @@ -5145,7 +6662,7 @@ { "docs": [], "name": "count", "type": 14, "typeName": "u32" } ] }, - "id": 693, + "id": 732, "params": [], "path": [ "pallet_deos_actors", @@ -5166,9 +6683,33 @@ }, { "docs": [], - "name": "fanout_revision", + "name": "latest_cause_provenance", + "type": 734, + "typeName": "crate::TriggerCauseProvenance" + }, + { + "docs": [], + "name": "latest_cause_block", + "type": 10, + "typeName": "u64" + }, + { + "docs": [], + "name": "fanout_revision", + "type": 10, + "typeName": "ObservationRevision" + }, + { + "docs": [], + "name": "fanout_cause_provenance", + "type": 734, + "typeName": "crate::TriggerCauseProvenance" + }, + { + "docs": [], + "name": "fanout_cause_block", "type": 10, - "typeName": "ObservationRevision" + "typeName": "u64" }, { "docs": [], @@ -5182,21 +6723,39 @@ "type": 186, "typeName": "Option" }, + { + "docs": [], + "name": "next_subscriber_position", + "type": 14, + "typeName": "u32" + }, + { + "docs": [], + "name": "next_subscriber_branch", + "type": 362, + "typeName": "ObservationFanoutBranch" + }, + { + "docs": [], + "name": "retry_after", + "type": 186, + "typeName": "Option" + }, { "docs": [], "name": "previous_dirty_feed", - "type": 695, + "type": 735, "typeName": "Option" }, { "docs": [], "name": "next_dirty_feed", - "type": 695, + "type": 735, "typeName": "Option" } ] }, - "id": 694, + "id": 733, "params": [ { "name": "FeedId", "type": 216 }, { "name": "BlockNumber", "type": 14 } @@ -5208,6 +6767,23 @@ "DirtyObservationState" ] }, + { + "def": { + "tag": "variant", + "value": [ + { "docs": [], "fields": [], "index": 0, "name": "ExternalPhase" }, + { "docs": [], "fields": [], "index": 1, "name": "Deferred" } + ] + }, + "id": 734, + "params": [], + "path": [ + "pallet_deos_actors", + "types", + "lifecycle", + "TriggerCauseProvenance" + ] + }, { "def": { "tag": "variant", @@ -5221,7 +6797,7 @@ } ] }, - "id": 695, + "id": 735, "params": [{ "name": "T", "type": 216 }], "path": ["Option"] }, @@ -5232,25 +6808,25 @@ { "docs": [], "name": "head", - "type": 695, + "type": 735, "typeName": "Option" }, { "docs": [], "name": "tail", - "type": 695, + "type": 735, "typeName": "Option" }, { "docs": [], "name": "cursor", - "type": 695, + "type": 735, "typeName": "Option" }, { "docs": [], "name": "count", "type": 14, "typeName": "u32" } ] }, - "id": 696, + "id": 736, "params": [{ "name": "FeedId", "type": 216 }], "path": [ "pallet_deos_actors", @@ -5259,40 +6835,6 @@ "DirtyObservationList" ] }, - { - "def": { - "tag": "composite", - "value": [ - { "docs": [], "name": "feed", "type": 216, "typeName": "FeedId" }, - { - "docs": [], - "name": "revision", - "type": 10, - "typeName": "ObservationRevision" - }, - { - "docs": [], - "name": "subscriber_page", - "type": 186, - "typeName": "Option" - }, - { - "docs": [], - "name": "class", - "type": 354, - "typeName": "CrossingWorkerFaultClass" - } - ] - }, - "id": 697, - "params": [{ "name": "FeedId", "type": 216 }], - "path": [ - "pallet_deos_actors", - "types", - "observation", - "ObservationFanoutWorkerFault" - ] - }, { "def": { "tag": "composite", @@ -5300,7 +6842,7 @@ { "docs": [], "name": "key", - "type": 699, + "type": 738, "typeName": "CrossingLeafKey" }, { "docs": [], "name": "page", "type": 14, "typeName": "u32" }, @@ -5308,7 +6850,7 @@ { "docs": [], "name": "generation", "type": 10, "typeName": "u64" } ] }, - "id": 698, + "id": 737, "params": [{ "name": "FeedId", "type": 216 }], "path": [ "pallet_deos_actors", @@ -5325,7 +6867,7 @@ { "docs": [], "name": "traversal", - "type": 700, + "type": 739, "typeName": "CrossingTraversal" }, { @@ -5336,7 +6878,7 @@ } ] }, - "id": 699, + "id": 738, "params": [{ "name": "FeedId", "type": 216 }], "path": ["pallet_deos_actors", "types", "observation", "CrossingLeafKey"] }, @@ -5348,7 +6890,7 @@ { "docs": [], "fields": [], "index": 1, "name": "Downward" } ] }, - "id": 700, + "id": 739, "params": [], "path": [ "pallet_deos_actors", @@ -5358,8 +6900,8 @@ ] }, { - "def": { "tag": "tuple", "value": [699, 14] }, - "id": 701, + "def": { "tag": "tuple", "value": [738, 14] }, + "id": 740, "params": [], "path": [] }, @@ -5370,12 +6912,12 @@ { "docs": [], "name": "entries", - "type": 703, + "type": 742, "typeName": "BoundedVec" } ] }, - "id": 702, + "id": 741, "params": [{ "name": "MaxEntries" }], "path": [ "pallet_deos_actors", @@ -5387,10 +6929,10 @@ { "def": { "tag": "composite", - "value": [{ "docs": [], "type": 705, "typeName": "Vec" }] + "value": [{ "docs": [], "type": 744, "typeName": "Vec" }] }, - "id": 703, - "params": [{ "name": "T", "type": 704 }, { "name": "S" }], + "id": 742, + "params": [{ "name": "T", "type": 743 }, { "name": "S" }], "path": ["bounded_collections", "bounded_vec", "BoundedVec"] }, { @@ -5398,16 +6940,28 @@ "tag": "composite", "value": [ { "docs": [], "name": "actor_id", "type": 10, "typeName": "ActorId" }, - { "docs": [], "name": "generation", "type": 10, "typeName": "u64" } + { "docs": [], "name": "generation", "type": 10, "typeName": "u64" }, + { + "docs": [], + "name": "counterpart_threshold", + "type": 49, + "typeName": "ObservationValue" + }, + { + "docs": [], + "name": "admission_identity", + "type": 2, + "typeName": "[u8; 32]" + } ] }, - "id": 704, + "id": 743, "params": [], "path": ["pallet_deos_actors", "types", "observation", "CrossingMember"] }, { - "def": { "tag": "sequence", "value": 704 }, - "id": 705, + "def": { "tag": "sequence", "value": 743 }, + "id": 744, "params": [], "path": [] }, @@ -5420,7 +6974,7 @@ { "docs": [], "name": "member_count", "type": 14, "typeName": "u32" } ] }, - "id": 706, + "id": 745, "params": [], "path": [ "pallet_deos_actors", @@ -5437,7 +6991,7 @@ { "docs": [], "name": "traversal", - "type": 700, + "type": 739, "typeName": "CrossingTraversal" }, { "docs": [], "name": "depth", "type": 3, "typeName": "u8" }, @@ -5449,7 +7003,7 @@ } ] }, - "id": 707, + "id": 746, "params": [{ "name": "FeedId", "type": 216 }], "path": [ "pallet_deos_actors", @@ -5461,10 +7015,10 @@ { "def": { "tag": "composite", - "value": [{ "docs": [], "type": 710, "typeName": "Vec" }] + "value": [{ "docs": [], "type": 749, "typeName": "Vec" }] }, - "id": 708, - "params": [{ "name": "T", "type": 709 }, { "name": "S" }], + "id": 747, + "params": [{ "name": "T", "type": 748 }, { "name": "S" }], "path": ["bounded_collections", "bounded_vec", "BoundedVec"] }, { @@ -5488,10 +7042,17 @@ "name": "current", "type": 49, "typeName": "ObservationValue" - } + }, + { + "docs": [], + "name": "cause_provenance", + "type": 734, + "typeName": "crate::TriggerCauseProvenance" + }, + { "docs": [], "name": "cause_block", "type": 10, "typeName": "u64" } ] }, - "id": 709, + "id": 748, "params": [], "path": [ "pallet_deos_actors", @@ -5501,8 +7062,8 @@ ] }, { - "def": { "tag": "sequence", "value": 709 }, - "id": 710, + "def": { "tag": "sequence", "value": 748 }, + "id": 749, "params": [], "path": [] }, @@ -5513,18 +7074,18 @@ { "docs": [], "name": "previous", - "type": 695, + "type": 735, "typeName": "Option" }, { "docs": [], "name": "next", - "type": 695, + "type": 735, "typeName": "Option" } ] }, - "id": 711, + "id": 750, "params": [{ "name": "FeedId", "type": 216 }], "path": [ "pallet_deos_actors", @@ -5540,25 +7101,25 @@ { "docs": [], "name": "head", - "type": 695, + "type": 735, "typeName": "Option" }, { "docs": [], "name": "tail", - "type": 695, + "type": 735, "typeName": "Option" }, { "docs": [], "name": "cursor", - "type": 695, + "type": 735, "typeName": "Option" }, { "docs": [], "name": "count", "type": 14, "typeName": "u32" } ] }, - "id": 712, + "id": 751, "params": [{ "name": "FeedId", "type": 216 }], "path": [ "pallet_deos_actors", @@ -5580,7 +7141,7 @@ { "docs": [], "name": "traversal", - "type": 700, + "type": 739, "typeName": "CrossingTraversal" }, { @@ -5592,7 +7153,7 @@ { "docs": [], "name": "current_threshold", - "type": 507, + "type": 359, "typeName": "Option" }, { "docs": [], "name": "page", "type": 14, "typeName": "u32" }, @@ -5600,7 +7161,7 @@ { "docs": [], "name": "exhausted", "type": 50, "typeName": "bool" } ] }, - "id": 713, + "id": 752, "params": [], "path": [ "pallet_deos_actors", @@ -5609,40 +7170,6 @@ "CrossingRangeCursor" ] }, - { - "def": { - "tag": "composite", - "value": [ - { "docs": [], "name": "feed", "type": 216, "typeName": "FeedId" }, - { - "docs": [], - "name": "revision", - "type": 189, - "typeName": "Option" - }, - { - "docs": [], - "name": "threshold", - "type": 507, - "typeName": "Option" - }, - { - "docs": [], - "name": "class", - "type": 354, - "typeName": "CrossingWorkerFaultClass" - } - ] - }, - "id": 714, - "params": [{ "name": "FeedId", "type": 216 }], - "path": [ - "pallet_deos_actors", - "types", - "observation", - "CrossingWorkerFault" - ] - }, { "def": { "tag": "variant", @@ -5676,7 +7203,7 @@ } ] }, - "id": 715, + "id": 753, "params": [], "path": [ "pallet_deos_actors", @@ -5752,348 +7279,373 @@ "docs": [], "fields": [], "index": 14, - "name": "EmptyContractSteps" - }, - { - "docs": [], - "fields": [], - "index": 15, "name": "ContractStepsExceedOnIdleBudget" }, { "docs": [], "fields": [], - "index": 16, + "index": 15, "name": "ExecutionDelayTooLong" }, { "docs": [], "fields": [], - "index": 17, + "index": 16, "name": "GlobalCircuitBreakerActive" }, - { "docs": [], "fields": [], "index": 18, "name": "ImmutableActor" }, + { "docs": [], "fields": [], "index": 17, "name": "ImmutableActor" }, { "docs": [], "fields": [], - "index": 19, + "index": 18, "name": "InsufficientBalance" }, - { "docs": [], "fields": [], "index": 20, "name": "InsufficientFee" }, + { "docs": [], "fields": [], "index": 19, "name": "InsufficientFee" }, { "docs": [], "fields": [], - "index": 21, + "index": 20, "name": "InvalidAmountResolution" }, - { "docs": [], "fields": [], "index": 22, "name": "InvalidPredicate" }, + { "docs": [], "fields": [], "index": 21, "name": "InvalidPredicate" }, { "docs": [], "fields": [], - "index": 23, + "index": 22, "name": "InvalidAutoCloseNonce" }, { "docs": [], "fields": [], - "index": 24, + "index": 23, "name": "InvalidScheduleWindow" }, { "docs": [], "fields": [], - "index": 25, + "index": 24, "name": "InvalidSplitTransfer" }, { "docs": [], "fields": [], - "index": 26, + "index": 25, "name": "InvalidTriggerConfiguration" }, { "docs": [], "fields": [], - "index": 27, + "index": 26, "name": "InvalidTradeBound" }, { "docs": [], "fields": [], - "index": 28, + "index": 27, "name": "InvalidRetryAttemptLimit" }, { "docs": [], "fields": [], - "index": 29, + "index": 28, "name": "InvalidObservationMaxAge" }, { "docs": [], "fields": [], - "index": 30, + "index": 29, "name": "SelfTransferNotAllowed" }, { "docs": [], "fields": [], - "index": 31, + "index": 30, "name": "MintNotAllowedForUserActor" }, - { "docs": [], "fields": [], "index": 32, "name": "NotGovernance" }, - { "docs": [], "fields": [], "index": 33, "name": "NotOwner" }, + { "docs": [], "fields": [], "index": 31, "name": "NotGovernance" }, + { "docs": [], "fields": [], "index": 32, "name": "NotOwner" }, { "docs": [], "fields": [], - "index": 34, + "index": 33, "name": "OwnerSlotCapacityExceeded" }, { "docs": [], "fields": [], - "index": 35, + "index": 34, "name": "OwnerSlotOccupied" }, - { "docs": [], "fields": [], "index": 36, "name": "InvalidOwnerSlot" }, - { "docs": [], "fields": [], "index": 37, "name": "ActorIdOccupied" }, + { "docs": [], "fields": [], "index": 35, "name": "InvalidOwnerSlot" }, + { "docs": [], "fields": [], "index": 36, "name": "ActorIdOccupied" }, { "docs": [], "fields": [], - "index": 38, + "index": 37, "name": "SystemSovereignCapacityExceeded" }, { "docs": [], "fields": [], - "index": 39, + "index": 38, "name": "SystemSovereignUnknown" }, { "docs": [], "fields": [], - "index": 40, + "index": 39, "name": "SystemSovereignOccupied" }, { "docs": [], "fields": [], - "index": 41, + "index": 40, "name": "SystemSovereignInvariant" }, { "docs": [], "fields": [], - "index": 42, + "index": 41, "name": "SovereignAccountCollision" }, { "docs": [], "fields": [], - "index": 43, + "index": 42, "name": "ReservedSovereignAccount" }, { "docs": [], "fields": [], - "index": 44, + "index": 43, "name": "TooManyContractSteps" }, { "docs": [], "fields": [], - "index": 45, + "index": 44, "name": "SnapshotUnavailable" }, { "docs": [], "fields": [], - "index": 46, + "index": 45, "name": "FundingAccumulatorOverflow" }, { "docs": [], "fields": [], - "index": 47, + "index": 46, "name": "QueueTicketExhausted" }, { "docs": [], "fields": [], - "index": 48, + "index": 47, "name": "SchedulerIndexExhausted" }, { "docs": [], "fields": [], - "index": 49, + "index": 48, "name": "AutoCloseNonceHorizonExceeded" }, { "docs": [], "fields": [], - "index": 50, + "index": 49, "name": "ControlMutationRateLimited" }, { "docs": [], "fields": [], - "index": 51, + "index": 50, "name": "QueueCapacityUnavailable" }, { "docs": [], "fields": [], - "index": 52, + "index": 51, "name": "RetryLaterNotAllowedForImmutableActor" }, + { "docs": [], "fields": [], "index": 52, "name": "ActorRunNotFound" }, { "docs": [], "fields": [], "index": 53, - "name": "ContinuationNotFound" + "name": "ActorRunInvariant" }, { "docs": [], "fields": [], "index": 54, - "name": "ContinuationInvariant" + "name": "ComputationOverflow" }, { "docs": [], "fields": [], "index": 55, - "name": "ComputationOverflow" + "name": "EmptyPrecondition" }, { "docs": [], "fields": [], "index": 56, - "name": "EmptyPrecondition" + "name": "ManualSourceDisabled" }, { "docs": [], "fields": [], "index": 57, - "name": "ManualSourceDisabled" + "name": "RecipientDepositUnavailable" }, { "docs": [], "fields": [], "index": 58, - "name": "RecipientDepositUnavailable" + "name": "ObservationSubscriptionCapacityExceeded" }, { "docs": [], "fields": [], "index": 59, - "name": "ObservationSubscriptionCapacityExceeded" + "name": "ObservationSubscriptionInvariant" }, { "docs": [], "fields": [], "index": 60, - "name": "ObservationSubscriptionInvariant" + "name": "InvalidObservationRevision" }, { "docs": [], "fields": [], "index": 61, - "name": "InvalidObservationRevision" + "name": "DirtyObservationCapacityExceeded" }, { "docs": [], "fields": [], "index": 62, - "name": "DirtyObservationCapacityExceeded" + "name": "DirtyObservationInvariant" }, { "docs": [], "fields": [], "index": 63, - "name": "DirtyObservationInvariant" + "name": "ObservationUnavailable" }, { "docs": [], "fields": [], "index": 64, - "name": "ObservationUnavailable" + "name": "ObservationUninitialized" }, { "docs": [], "fields": [], "index": 65, - "name": "ObservationUninitialized" + "name": "CrossingIndexCapacityExceeded" }, { "docs": [], "fields": [], "index": 66, - "name": "CrossingIndexCapacityExceeded" + "name": "CrossingUserCapacityExceeded" }, { "docs": [], "fields": [], "index": 67, - "name": "CrossingUserCapacityExceeded" + "name": "CrossingIndexInvariant" }, { "docs": [], "fields": [], "index": 68, - "name": "CrossingIndexInvariant" + "name": "CrossingGenerationExhausted" }, { "docs": [], "fields": [], "index": 69, - "name": "CrossingGenerationExhausted" + "name": "CrossingTransitionCapacityExceeded" }, { "docs": [], "fields": [], "index": 70, - "name": "CrossingTransitionCapacityExceeded" + "name": "CrossingTransitionInvariant" }, { "docs": [], "fields": [], "index": 71, - "name": "CrossingTransitionInvariant" + "name": "CrossingWorkerFaultNotFound" }, { "docs": [], "fields": [], "index": 72, - "name": "CrossingWorkerFaultNotFound" + "name": "ObservationFanoutWorkerFaultNotFound" }, { "docs": [], "fields": [], "index": 73, - "name": "ObservationFanoutWorkerFaultNotFound" + "name": "WakeupWorkerFaultNotFound" }, { "docs": [], "fields": [], "index": 74, - "name": "WakeupWorkerFaultNotFound" + "name": "SystemActorTopologyInvalid" }, { "docs": [], "fields": [], "index": 75, - "name": "SystemActorTopologyInvalid" + "name": "AdmissionBoundOverflow" }, { "docs": [], "fields": [], "index": 76, - "name": "AdmissionBoundOverflow" + "name": "StateHoldUnavailable" + }, + { + "docs": [], + "fields": [], + "index": 77, + "name": "StateHoldInvariant" + }, + { + "docs": [], + "fields": [], + "index": 78, + "name": "StateHoldOverflow" + }, + { + "docs": [], + "fields": [], + "index": 79, + "name": "PrepassDuplicateOrStale" + }, + { + "docs": [], + "fields": [], + "index": 80, + "name": "ResourceProtocolFailed" + }, + { + "docs": [], + "fields": [], + "index": 81, + "name": "PrepassContextIncomplete" } ] }, - "id": 716, + "id": 754, "params": [{ "name": "T" }], "path": ["pallet_deos_actors", "pallet", "Error"] } diff --git a/web-client/src/lib/automation/actors-cost-vectors.json b/web-client/src/lib/automation/actors-cost-vectors.json new file mode 100644 index 00000000..755b3805 --- /dev/null +++ b/web-client/src/lib/automation/actors-cost-vectors.json @@ -0,0 +1 @@ +{"format":"deos.actor.cost-vectors","formatVersion":1,"metadataSha256":"27984891721c42acbce79d4e458e9b40dd6b9a046228438a072f4f2c1bd0f74e","runtimeApiVersion":1,"vectors":[{"actorId":"15","contractStepCount":0,"name":"user-manual-0","quote":{"actorType":"User","creationFee":"2000000000","maximumNextActionFee":{"maximumEffectFee":"0","maximumEffectWeight":{"proofSize":"0","refTime":"0"},"productionWeightIdentity":"c05d77bd59c8cde9080e8bcb52abedac5e6a239efdbcdc5b4c34603abf647fbd"},"prospectivePipelineFee":{"admissionIdentity":"19385e78787f1fc5fdc055e82dcd489a74636291363a053fffdc0319573aceb5","cleanupFee":"69668408","machineFee":"4287784","productionWeightIdentity":"c8e40855fb83874618d1fece453f97977497b5c7cbc249d8ad0f3a310ac15d1d","strategy":"UpfrontBounded","totalFee":"73956192"},"prospectiveTriggerFee":{"family":"Manual","fee":"9640960","maximumWeight":{"proofSize":"9635","refTime":"1205120000"},"productionWeightIdentity":"7dcab72e9e3b1eca64c197a0d55d547402f1fd640937bc3857b97b46b4be8ee8"},"stateHold":{"basePerComponent":"1000000000","components":{"contractBody":"0","contractHead":"1405000000","detector":"0","funding":"1002000000","identity":"1119000000","run":"3673000000"},"exempt":false,"perEncodedByte":"1000000","total":"7199000000"}},"triggerFamily":"Manual"},{"actorId":"16","contractStepCount":1,"name":"user-manual-1","quote":{"actorType":"User","creationFee":"2000000000","maximumNextActionFee":{"maximumEffectFee":"13304296","maximumEffectWeight":{"proofSize":"18280","refTime":"1663037000"},"productionWeightIdentity":"c05d77bd59c8cde9080e8bcb52abedac5e6a239efdbcdc5b4c34603abf647fbd"},"prospectivePipelineFee":{"admissionIdentity":"56f479b1547464201c991624bd782a1e7d05558813e5eb1b8a32ecbd8f17bf29","cleanupFee":"69668408","machineFee":"15809194","productionWeightIdentity":"c8e40855fb83874618d1fece453f97977497b5c7cbc249d8ad0f3a310ac15d1d","strategy":"UpfrontBounded","totalFee":"85477602"},"prospectiveTriggerFee":{"family":"Manual","fee":"9640960","maximumWeight":{"proofSize":"9635","refTime":"1205120000"},"productionWeightIdentity":"7dcab72e9e3b1eca64c197a0d55d547402f1fd640937bc3857b97b46b4be8ee8"},"stateHold":{"basePerComponent":"1000000000","components":{"contractBody":"0","contractHead":"1476000000","detector":"0","funding":"1002000000","identity":"1119000000","run":"3673000000"},"exempt":false,"perEncodedByte":"1000000","total":"7270000000"}},"triggerFamily":"Manual"},{"actorId":"17","contractStepCount":4,"name":"user-manual-4","quote":{"actorType":"User","creationFee":"2000000000","maximumNextActionFee":{"maximumEffectFee":"13304296","maximumEffectWeight":{"proofSize":"18280","refTime":"1663037000"},"productionWeightIdentity":"c05d77bd59c8cde9080e8bcb52abedac5e6a239efdbcdc5b4c34603abf647fbd"},"prospectivePipelineFee":{"admissionIdentity":"1a5b67588a2bd306a5db42015b59c3518e2a78964566a153868672b51828196b","cleanupFee":"69668408","machineFee":"60778113","productionWeightIdentity":"c8e40855fb83874618d1fece453f97977497b5c7cbc249d8ad0f3a310ac15d1d","strategy":"UpfrontBounded","totalFee":"130446521"},"prospectiveTriggerFee":{"family":"Manual","fee":"9640960","maximumWeight":{"proofSize":"9635","refTime":"1205120000"},"productionWeightIdentity":"7dcab72e9e3b1eca64c197a0d55d547402f1fd640937bc3857b97b46b4be8ee8"},"stateHold":{"basePerComponent":"1000000000","components":{"contractBody":"1323000000","contractHead":"1476000000","detector":"0","funding":"1002000000","identity":"1119000000","run":"3673000000"},"exempt":false,"perEncodedByte":"1000000","total":"8593000000"}},"triggerFamily":"Manual"},{"actorId":"18","contractStepCount":8,"name":"user-manual-8","quote":{"actorType":"User","creationFee":"2000000000","maximumNextActionFee":{"maximumEffectFee":"13304296","maximumEffectWeight":{"proofSize":"18280","refTime":"1663037000"},"productionWeightIdentity":"c05d77bd59c8cde9080e8bcb52abedac5e6a239efdbcdc5b4c34603abf647fbd"},"prospectivePipelineFee":{"admissionIdentity":"dc262745adce0a968a096751c70a740e79730bfa8bc4f72e06d3dbbe7eeff52f","cleanupFee":"69668408","machineFee":"120659678","productionWeightIdentity":"c8e40855fb83874618d1fece453f97977497b5c7cbc249d8ad0f3a310ac15d1d","strategy":"UpfrontBounded","totalFee":"190328086"},"prospectiveTriggerFee":{"family":"Manual","fee":"9640960","maximumWeight":{"proofSize":"9635","refTime":"1205120000"},"productionWeightIdentity":"7dcab72e9e3b1eca64c197a0d55d547402f1fd640937bc3857b97b46b4be8ee8"},"stateHold":{"basePerComponent":"1000000000","components":{"contractBody":"1717000000","contractHead":"1476000000","detector":"0","funding":"1002000000","identity":"1119000000","run":"3673000000"},"exempt":false,"perEncodedByte":"1000000","total":"8987000000"}},"triggerFamily":"Manual"},{"actorId":"19","contractStepCount":32,"name":"user-manual-32","quote":{"actorType":"User","creationFee":"2000000000","maximumNextActionFee":{"maximumEffectFee":"13304296","maximumEffectWeight":{"proofSize":"18280","refTime":"1663037000"},"productionWeightIdentity":"c05d77bd59c8cde9080e8bcb52abedac5e6a239efdbcdc5b4c34603abf647fbd"},"prospectivePipelineFee":{"admissionIdentity":"bbd4bb62c199e3904149f0c9b38c0fb0ce22c50739079eaad6161406151b3724","cleanupFee":"69668408","machineFee":"479949067","productionWeightIdentity":"c8e40855fb83874618d1fece453f97977497b5c7cbc249d8ad0f3a310ac15d1d","strategy":"UpfrontBounded","totalFee":"549617475"},"prospectiveTriggerFee":{"family":"Manual","fee":"9640960","maximumWeight":{"proofSize":"9635","refTime":"1205120000"},"productionWeightIdentity":"7dcab72e9e3b1eca64c197a0d55d547402f1fd640937bc3857b97b46b4be8ee8"},"stateHold":{"basePerComponent":"1000000000","components":{"contractBody":"4081000000","contractHead":"1476000000","detector":"0","funding":"1002000000","identity":"1119000000","run":"3673000000"},"exempt":false,"perEncodedByte":"1000000","total":"11351000000"}},"triggerFamily":"Manual"},{"actorId":"20","contractStepCount":1,"name":"user-address-event-1","quote":{"actorType":"User","creationFee":"2000000000","maximumNextActionFee":{"maximumEffectFee":"13304296","maximumEffectWeight":{"proofSize":"18280","refTime":"1663037000"},"productionWeightIdentity":"c05d77bd59c8cde9080e8bcb52abedac5e6a239efdbcdc5b4c34603abf647fbd"},"prospectivePipelineFee":{"admissionIdentity":"58437e07d2dbaa40db4e6a6cc684b7530bfef1a565720db868efe9178231799c","cleanupFee":"69668408","machineFee":"15809194","productionWeightIdentity":"c8e40855fb83874618d1fece453f97977497b5c7cbc249d8ad0f3a310ac15d1d","strategy":"UpfrontBounded","totalFee":"85477602"},"prospectiveTriggerFee":{"family":"AddressEvent","fee":"9768912","maximumWeight":{"proofSize":"8367","refTime":"1221114000"},"productionWeightIdentity":"7dcab72e9e3b1eca64c197a0d55d547402f1fd640937bc3857b97b46b4be8ee8"},"stateHold":{"basePerComponent":"1000000000","components":{"contractBody":"0","contractHead":"1478000000","detector":"0","funding":"1002000000","identity":"1119000000","run":"3673000000"},"exempt":false,"perEncodedByte":"1000000","total":"7272000000"}},"triggerFamily":"AddressEvent"},{"actorId":"21","contractStepCount":1,"name":"user-observation-change-1","quote":{"actorType":"User","creationFee":"2000000000","maximumNextActionFee":{"maximumEffectFee":"13304296","maximumEffectWeight":{"proofSize":"18280","refTime":"1663037000"},"productionWeightIdentity":"c05d77bd59c8cde9080e8bcb52abedac5e6a239efdbcdc5b4c34603abf647fbd"},"prospectivePipelineFee":{"admissionIdentity":"2fc1c5a1ce22799f64248151b75af89af570ac0423e2c42aeaddce36de3c5a6e","cleanupFee":"69668408","machineFee":"15809194","productionWeightIdentity":"c8e40855fb83874618d1fece453f97977497b5c7cbc249d8ad0f3a310ac15d1d","strategy":"UpfrontBounded","totalFee":"85477602"},"prospectiveTriggerFee":{"family":"ObservationChange","fee":"10134768","maximumWeight":{"proofSize":"8296","refTime":"1266846000"},"productionWeightIdentity":"7dcab72e9e3b1eca64c197a0d55d547402f1fd640937bc3857b97b46b4be8ee8"},"stateHold":{"basePerComponent":"1000000000","components":{"contractBody":"0","contractHead":"1489000000","detector":"1133000000","funding":"1002000000","identity":"1119000000","run":"3673000000"},"exempt":false,"perEncodedByte":"1000000","total":"8416000000"}},"triggerFamily":"ObservationChange"},{"actorId":"22","contractStepCount":1,"name":"user-observation-crossing-1","quote":{"actorType":"User","creationFee":"2000000000","maximumNextActionFee":{"maximumEffectFee":"13304296","maximumEffectWeight":{"proofSize":"18280","refTime":"1663037000"},"productionWeightIdentity":"c05d77bd59c8cde9080e8bcb52abedac5e6a239efdbcdc5b4c34603abf647fbd"},"prospectivePipelineFee":{"admissionIdentity":"412f19cb343dfd95f8229540d953c052bf8f1fb4c01e2aee98d92b8fc3aa3ade","cleanupFee":"69668408","machineFee":"15809194","productionWeightIdentity":"c8e40855fb83874618d1fece453f97977497b5c7cbc249d8ad0f3a310ac15d1d","strategy":"UpfrontBounded","totalFee":"85477602"},"prospectiveTriggerFee":{"family":"ObservationCrossing","fee":"86869296","maximumWeight":{"proofSize":"164204","refTime":"10858662000"},"productionWeightIdentity":"7dcab72e9e3b1eca64c197a0d55d547402f1fd640937bc3857b97b46b4be8ee8"},"stateHold":{"basePerComponent":"1000000000","components":{"contractBody":"0","contractHead":"1522000000","detector":"1161000000","funding":"1002000000","identity":"1119000000","run":"3673000000"},"exempt":false,"perEncodedByte":"1000000","total":"8477000000"}},"triggerFamily":"ObservationCrossing"},{"actorId":"23","contractStepCount":1,"name":"user-at-time-1","quote":{"actorType":"User","creationFee":"2000000000","maximumNextActionFee":{"maximumEffectFee":"13304296","maximumEffectWeight":{"proofSize":"18280","refTime":"1663037000"},"productionWeightIdentity":"c05d77bd59c8cde9080e8bcb52abedac5e6a239efdbcdc5b4c34603abf647fbd"},"prospectivePipelineFee":{"admissionIdentity":"4b5beba69bb223c5ea7d0fa94ea64000c979a2b331b7b77768420abf38694523","cleanupFee":"69668408","machineFee":"15809194","productionWeightIdentity":"c8e40855fb83874618d1fece453f97977497b5c7cbc249d8ad0f3a310ac15d1d","strategy":"UpfrontBounded","totalFee":"85477602"},"prospectiveTriggerFee":{"family":"AtTime","fee":"12913088","maximumWeight":{"proofSize":"8552","refTime":"1614136000"},"productionWeightIdentity":"7dcab72e9e3b1eca64c197a0d55d547402f1fd640937bc3857b97b46b4be8ee8"},"stateHold":{"basePerComponent":"1000000000","components":{"contractBody":"0","contractHead":"1484000000","detector":"1020000000","funding":"1002000000","identity":"1119000000","run":"3673000000"},"exempt":false,"perEncodedByte":"1000000","total":"8298000000"}},"triggerFamily":"AtTime"},{"actorId":"24","contractStepCount":1,"name":"user-cadenced-1","quote":{"actorType":"User","creationFee":"2000000000","maximumNextActionFee":{"maximumEffectFee":"13304296","maximumEffectWeight":{"proofSize":"18280","refTime":"1663037000"},"productionWeightIdentity":"c05d77bd59c8cde9080e8bcb52abedac5e6a239efdbcdc5b4c34603abf647fbd"},"prospectivePipelineFee":{"admissionIdentity":"772a0be7d5493b86a15d2d8ce321bbeda605499eb407d8033350b08d44cc1411","cleanupFee":"69668408","machineFee":"15809194","productionWeightIdentity":"c8e40855fb83874618d1fece453f97977497b5c7cbc249d8ad0f3a310ac15d1d","strategy":"UpfrontBounded","totalFee":"85477602"},"prospectiveTriggerFee":{"family":"Cadenced","fee":"15131560","maximumWeight":{"proofSize":"8505","refTime":"1891445000"},"productionWeightIdentity":"7dcab72e9e3b1eca64c197a0d55d547402f1fd640937bc3857b97b46b4be8ee8"},"stateHold":{"basePerComponent":"1000000000","components":{"contractBody":"0","contractHead":"1484000000","detector":"1020000000","funding":"1002000000","identity":"1119000000","run":"3673000000"},"exempt":false,"perEncodedByte":"1000000","total":"8298000000"}},"triggerFamily":"Cadenced"},{"actorId":"25","contractStepCount":1,"name":"system-manual-1","quote":{"actorType":"System","creationFee":"0","maximumNextActionFee":{"maximumEffectFee":"0","maximumEffectWeight":{"proofSize":"18280","refTime":"1663037000"},"productionWeightIdentity":"c05d77bd59c8cde9080e8bcb52abedac5e6a239efdbcdc5b4c34603abf647fbd"},"prospectivePipelineFee":{"admissionIdentity":"e3129a71897f41a49a18ae045a8280c574914a4ce7b4e8b2aaf3bc0fabe8da6c","cleanupFee":"0","machineFee":"0","productionWeightIdentity":"c8e40855fb83874618d1fece453f97977497b5c7cbc249d8ad0f3a310ac15d1d","strategy":"UpfrontBounded","totalFee":"0"},"prospectiveTriggerFee":{"family":"Manual","fee":"0","maximumWeight":{"proofSize":"9635","refTime":"1205120000"},"productionWeightIdentity":"7dcab72e9e3b1eca64c197a0d55d547402f1fd640937bc3857b97b46b4be8ee8"},"stateHold":{"basePerComponent":"1000000000","components":{"contractBody":"0","contractHead":"0","detector":"0","funding":"0","identity":"0","run":"0"},"exempt":true,"perEncodedByte":"1000000","total":"0"}},"triggerFamily":"Manual"},{"actorId":"26","contractStepCount":null,"name":"user-dormant","quote":{"actorType":"User","creationFee":"2000000000","maximumNextActionFee":{"maximumEffectFee":"0","maximumEffectWeight":{"proofSize":"0","refTime":"0"},"productionWeightIdentity":"c05d77bd59c8cde9080e8bcb52abedac5e6a239efdbcdc5b4c34603abf647fbd"},"prospectivePipelineFee":null,"prospectiveTriggerFee":null,"stateHold":{"basePerComponent":"1000000000","components":{"contractBody":"0","contractHead":"0","detector":"0","funding":"0","identity":"1119000000","run":"0"},"exempt":false,"perEncodedByte":"1000000","total":"1119000000"}},"triggerFamily":null}],"weightSha256":"ac206ec06b3f2c2789da23540ca1ae87d343c8d2196f77b1c12c43569c0d3b9e"} diff --git a/web-client/src/lib/automation/actors-fee-envelope-vectors.json b/web-client/src/lib/automation/actors-fee-envelope-vectors.json index ff0e98c1..8125aa25 100644 --- a/web-client/src/lib/automation/actors-fee-envelope-vectors.json +++ b/web-client/src/lib/automation/actors-fee-envelope-vectors.json @@ -1 +1 @@ -{"format":"deos.actor.fee-envelope-vectors","formatVersion":2,"metadataSha256":"3a5402c7b7f4a1f58dc8569d67638221961bde9e8efe7587b7dc09fef49f9873","weightSha256":"26afb6f472d0471082fc2f92c1c8f1639a2c4d9a10c7dffede4dae9f6e8c6b01","vectors":[{"actorType":"User","cursor":0,"inputs":[{"evaluation":"2","execution":"100"},{"evaluation":"5","execution":"7"},{"evaluation":"0","execution":"33"}],"steps":[{"evaluation":"2","execution":"100","total":"102"},{"evaluation":"5","execution":"7","total":"12"},{"evaluation":"0","execution":"33","total":"33"}],"total":"147"},{"actorType":"User","cursor":1,"inputs":[{"evaluation":"2","execution":"100"},{"evaluation":"5","execution":"7"},{"evaluation":"0","execution":"33"}],"steps":[{"evaluation":"5","execution":"7","total":"12"},{"evaluation":"0","execution":"33","total":"33"}],"total":"45"},{"actorType":"User","cursor":3,"inputs":[{"evaluation":"2","execution":"100"},{"evaluation":"5","execution":"7"},{"evaluation":"0","execution":"33"}],"steps":[],"total":"0"},{"actorType":"System","cursor":0,"inputs":[{"evaluation":"2","execution":"100"},{"evaluation":"5","execution":"7"},{"evaluation":"0","execution":"33"}],"steps":[{"evaluation":"0","execution":"0","total":"0"},{"evaluation":"0","execution":"0","total":"0"},{"evaluation":"0","execution":"0","total":"0"}],"total":"0"}],"settlementCases":[{"name":"releaseToZero","actorType":"User","cursor":0,"inputs":[{"evaluation":"2","execution":"100"},{"evaluation":"5","execution":"7"},{"evaluation":"0","execution":"33"}],"initialReservation":"147","chargeKinds":["EvaluationOnly","Attempted","EvaluationOnly"],"charges":["2","12","0"],"reservationRemaining":["45","33","0"]},{"name":"attemptPricedRollback","actorType":"User","cursor":0,"inputs":[{"evaluation":"2","execution":"100"}],"initialReservation":"102","chargeKinds":["Attempted"],"charges":["102"],"reservationRemaining":["0"]},{"name":"systemFeeExemption","actorType":"System","cursor":0,"inputs":[{"evaluation":"2","execution":"100"}],"initialReservation":"0","chargeKinds":["Attempted"],"charges":["0"],"reservationRemaining":["0"]}],"floorCases":[{"name":"userFeeNative","actorType":"User","isFeeNative":true,"assetMinimum":"1","minUserBalance":"50","protectedMinimum":"50"},{"name":"userNonFeeNative","actorType":"User","isFeeNative":false,"assetMinimum":"1","minUserBalance":"50","protectedMinimum":"1"},{"name":"systemFeeNative","actorType":"System","isFeeNative":true,"assetMinimum":"100","minUserBalance":"50","protectedMinimum":"100"}]} +{"format":"deos.actor.fee-envelope-vectors","formatVersion":2,"metadataSha256":"27984891721c42acbce79d4e458e9b40dd6b9a046228438a072f4f2c1bd0f74e","weightSha256":"ac206ec06b3f2c2789da23540ca1ae87d343c8d2196f77b1c12c43569c0d3b9e","vectors":[{"actorType":"User","cursor":0,"inputs":[{"evaluation":"2","execution":"100"},{"evaluation":"5","execution":"7"},{"evaluation":"0","execution":"33"}],"steps":[{"evaluation":"2","execution":"100","total":"102"},{"evaluation":"5","execution":"7","total":"12"},{"evaluation":"0","execution":"33","total":"33"}],"total":"147"},{"actorType":"User","cursor":1,"inputs":[{"evaluation":"2","execution":"100"},{"evaluation":"5","execution":"7"},{"evaluation":"0","execution":"33"}],"steps":[{"evaluation":"5","execution":"7","total":"12"},{"evaluation":"0","execution":"33","total":"33"}],"total":"45"},{"actorType":"User","cursor":3,"inputs":[{"evaluation":"2","execution":"100"},{"evaluation":"5","execution":"7"},{"evaluation":"0","execution":"33"}],"steps":[],"total":"0"},{"actorType":"System","cursor":0,"inputs":[{"evaluation":"2","execution":"100"},{"evaluation":"5","execution":"7"},{"evaluation":"0","execution":"33"}],"steps":[{"evaluation":"0","execution":"0","total":"0"},{"evaluation":"0","execution":"0","total":"0"},{"evaluation":"0","execution":"0","total":"0"}],"total":"0"}],"settlementCases":[{"name":"releaseToZero","actorType":"User","cursor":0,"inputs":[{"evaluation":"2","execution":"100"},{"evaluation":"5","execution":"7"},{"evaluation":"0","execution":"33"}],"initialReservation":"147","chargeKinds":["EvaluationOnly","Attempted","EvaluationOnly"],"charges":["2","12","0"],"reservationRemaining":["45","33","0"]},{"name":"attemptPricedRollback","actorType":"User","cursor":0,"inputs":[{"evaluation":"2","execution":"100"}],"initialReservation":"102","chargeKinds":["Attempted"],"charges":["102"],"reservationRemaining":["0"]},{"name":"systemFeeExemption","actorType":"System","cursor":0,"inputs":[{"evaluation":"2","execution":"100"}],"initialReservation":"0","chargeKinds":["Attempted"],"charges":["0"],"reservationRemaining":["0"]}],"floorCases":[{"name":"userFeeNative","actorType":"User","isFeeNative":true,"assetMinimum":"1","minUserBalance":"50","protectedMinimum":"50"},{"name":"userNonFeeNative","actorType":"User","isFeeNative":false,"assetMinimum":"1","minUserBalance":"50","protectedMinimum":"1"},{"name":"systemFeeNative","actorType":"System","isFeeNative":true,"assetMinimum":"100","minUserBalance":"50","protectedMinimum":"100"}]} diff --git a/web-client/src/lib/automation/actors-protocol-bounds.generated.ts b/web-client/src/lib/automation/actors-protocol-bounds.generated.ts index 8a74a9c0..e7299945 100644 --- a/web-client/src/lib/automation/actors-protocol-bounds.generated.ts +++ b/web-client/src/lib/automation/actors-protocol-bounds.generated.ts @@ -1,9 +1,10 @@ -/* Generated from Actors runtime metadata 3a5402c7b7f4a1f58dc8569d67638221961bde9e8efe7587b7dc09fef49f9873; do not edit. */ -export const ACTORS_MAX_CONTRACT_STEPS = 8; +/* Generated from Actors runtime metadata 27984891721c42acbce79d4e458e9b40dd6b9a046228438a072f4f2c1bd0f74e; do not edit. */ +export const ACTORS_MAX_CONTRACT_STEPS = 32; export const ACTORS_MAX_EXECUTION_DELAY_BLOCKS = 52596000; +export const ACTORS_MAX_TEMPORAL_DELAY_TICKS = 631152000; export const ACTORS_MAX_RETRY_ATTEMPTS = 10; -export const ACTORS_MAX_OPENING_SNAPSHOT_ENTRIES = 16; -export const ACTORS_MAX_OPENING_PREDICATE_RESULTS = 32; +export const ACTORS_MAX_OPENING_SNAPSHOT_ENTRIES = 64; +export const ACTORS_MAX_OPENING_PREDICATE_RESULTS = 128; export const ACTORS_MAX_PRECONDITION_CLAUSES = 4; export const ACTORS_MAX_PREDICATES_PER_CLAUSE = 4; export const ACTORS_MAX_PREDICATES_PER_STEP = 4; diff --git a/web-client/src/lib/automation/actors-protocol-bounds.ts b/web-client/src/lib/automation/actors-protocol-bounds.ts index cfaaeba8..f86a17ed 100644 --- a/web-client/src/lib/automation/actors-protocol-bounds.ts +++ b/web-client/src/lib/automation/actors-protocol-bounds.ts @@ -8,4 +8,5 @@ export { ACTORS_MAX_PREDICATES_PER_CLAUSE, ACTORS_MAX_PREDICATES_PER_STEP, ACTORS_MAX_RETRY_ATTEMPTS, + ACTORS_MAX_TEMPORAL_DELAY_TICKS, } from './actors-protocol-bounds.generated.ts'; diff --git a/web-client/src/lib/automation/actors-trigger-bond-vectors.json b/web-client/src/lib/automation/actors-trigger-bond-vectors.json deleted file mode 100644 index 7f51543d..00000000 --- a/web-client/src/lib/automation/actors-trigger-bond-vectors.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "actorsWeightSha256": "26afb6f472d0471082fc2f92c1c8f1639a2c4d9a10c7dffede4dae9f6e8c6b01", - "formatVersion": 1, - "metadataSha256": "3a5402c7b7f4a1f58dc8569d67638221961bde9e8efe7587b7dc09fef49f9873", - "vectors": [ - { - "amount": "1000000000", - "triggerFamily": "Manual" - }, - { - "amount": "2000000000", - "triggerFamily": "AddressEvent" - }, - { - "amount": "2000000000", - "triggerFamily": "ObservationChange" - }, - { - "amount": "5000000000", - "triggerFamily": "ObservationCrossing" - }, - { - "amount": "2000000000", - "triggerFamily": "Cadenced" - } - ] -} diff --git a/web-client/src/lib/automation/analysis.ts b/web-client/src/lib/automation/analysis.ts index 7fbaa9ff..7ddd6121 100644 --- a/web-client/src/lib/automation/analysis.ts +++ b/web-client/src/lib/automation/analysis.ts @@ -235,7 +235,9 @@ export type ActorStaticTriggerAnalysis = { | 'AddressEvent' | 'ObservationChange' | 'ObservationCrossing' + | 'AtTime' | 'Cadenced'; + afterTicks: number | null; everyTicks: number | null; sourceKinds: Array< 'Manual' | 'AddressEvent' | 'ObservationChange' | 'ObservationCrossing' @@ -255,6 +257,7 @@ export type ActorStaticFinding = 'Manual' | 'AddressEvent' | 'ObservationChange' | 'ObservationCrossing' >; } + | { kind: 'OneShotTemporalAdmission'; afterTicks: number } | { kind: 'PeriodicAdmission'; everyTicks: number } | { kind: 'TriggerAmountCompatibilityViolation'; @@ -352,6 +355,7 @@ export type ActorContractStaticAnalysis = { actorType: ActorContractArtifact['actorType']; mutability: ActorContractArtifact['mutability']; completionPolicy: 'Persistent' | 'CloseAfterProductiveCycle'; + autoCloseAtCycleNonce: bigint | null; cooldownBlocks: number; trigger: ActorStaticTriggerAnalysis; steps: ActorStaticStepAnalysis[]; @@ -1158,6 +1162,7 @@ function parseTrigger( case 'Manual': return { kind: 'Manual', + afterTicks: null, everyTicks: null, sourceKinds: ['Manual'], observationFeeds: [], @@ -1165,6 +1170,7 @@ function parseTrigger( case 'AddressEvent': return { kind: 'AddressEvent', + afterTicks: null, everyTicks: null, sourceKinds: ['AddressEvent'], observationFeeds: [], @@ -1172,6 +1178,7 @@ function parseTrigger( case 'ObservationChange': return { kind: 'ObservationChange', + afterTicks: null, everyTicks: null, sourceKinds: ['ObservationChange'], observationFeeds: [ @@ -1204,6 +1211,7 @@ function parseTrigger( } return { kind: 'ObservationCrossing', + afterTicks: null, everyTicks: null, sourceKinds: ['ObservationCrossing'], observationFeeds: [ @@ -1211,6 +1219,22 @@ function parseTrigger( ], }; } + case 'AtTime': { + const afterTicks = safeInteger( + member(trigger.value, 'after_ticks', 'Trigger.AtTime'), + 'Trigger.AtTime.after_ticks', + ); + if (afterTicks < 1) { + throw new Error('Trigger.AtTime.after_ticks must be positive'); + } + return { + kind: 'AtTime', + afterTicks, + everyTicks: null, + sourceKinds: [], + observationFeeds: [], + }; + } case 'Cadenced': { const everyTicks = safeInteger( member(trigger.value, 'every_ticks', 'Trigger.Cadenced'), @@ -1221,6 +1245,7 @@ function parseTrigger( } return { kind: 'Cadenced', + afterTicks: null, everyTicks, sourceKinds: [], observationFeeds: [], @@ -1260,7 +1285,12 @@ function findings( reason: 'AddressEventOnlyRequired', }); } - if (trigger?.kind === 'Cadenced') { + if (trigger?.kind === 'AtTime') { + results.push({ + kind: 'OneShotTemporalAdmission', + afterTicks: trigger.afterTicks as number, + }); + } else if (trigger?.kind === 'Cadenced') { results.push({ kind: 'PeriodicAdmission', everyTicks: trigger.everyTicks as number, @@ -1499,6 +1529,18 @@ export function analyzeActorContract(input: { member(contract, 'cooldown_blocks', 'ActorContract'), 'ActorContract.cooldown_blocks', ); + const projectedAutoClose = member( + contract, + 'auto_close_at_cycle_nonce', + 'ActorContract', + ); + const autoCloseAtCycleNonce = + projectedAutoClose == null || isNoneProjection(projectedAutoClose) + ? null + : unsignedBigInt( + projectedAutoClose, + 'ActorContract.auto_close_at_cycle_nonce', + ); let steps: ActorStaticStepAnalysis[] = []; let forecastInputs: ActorStepCostInput[] = []; const projectedPolicy = variant( @@ -1546,6 +1588,7 @@ export function analyzeActorContract(input: { actorType: input.artifact.actorType, mutability: input.artifact.mutability, completionPolicy, + autoCloseAtCycleNonce, cooldownBlocks, trigger, steps, diff --git a/web-client/src/lib/automation/authoring.ts b/web-client/src/lib/automation/authoring.ts index 466da644..f831ca7a 100644 --- a/web-client/src/lib/automation/authoring.ts +++ b/web-client/src/lib/automation/authoring.ts @@ -13,6 +13,7 @@ import { ACTORS_MAX_PREDICATES_PER_CLAUSE, ACTORS_MAX_PREDICATES_PER_STEP, ACTORS_MAX_RETRY_ATTEMPTS, + ACTORS_MAX_TEMPORAL_DELAY_TICKS, } from './actors-protocol-bounds.ts'; import { type ActorContractArtifact, @@ -212,6 +213,7 @@ export type ActorAuthoringTrigger = threshold: string; rearmThreshold: string; } + | { type: 'AtTime'; afterTicks: number } | { type: 'Cadenced'; everyTicks: number }; export type ActorAuthoringCompletionPolicy = @@ -233,7 +235,7 @@ export type ActorAuthoringContract = { export type ActorAuthoringLimits = { maxContractSteps: number; maxExecutionDelayBlocks: number; - maxCadenceTicks: number; + maxTemporalTicks: number; maxRetryAttempts: number; maxPreconditionClauses: number; maxPredicatesPerClause: number; @@ -245,7 +247,7 @@ export type ActorAuthoringLimits = { export const DEOS_ACTORS_AUTHORING_LIMITS: ActorAuthoringLimits = { maxContractSteps: ACTORS_MAX_CONTRACT_STEPS, maxExecutionDelayBlocks: ACTORS_MAX_EXECUTION_DELAY_BLOCKS, - maxCadenceTicks: ACTORS_MAX_EXECUTION_DELAY_BLOCKS, + maxTemporalTicks: ACTORS_MAX_TEMPORAL_DELAY_TICKS, maxRetryAttempts: ACTORS_MAX_RETRY_ATTEMPTS, maxPreconditionClauses: ACTORS_MAX_PRECONDITION_CLAUSES, maxPredicatesPerClause: ACTORS_MAX_PREDICATES_PER_CLAUSE, @@ -880,15 +882,27 @@ function validateTrigger( } return; } + case 'AtTime': + if ( + !Number.isSafeInteger(trigger.afterTicks) || + trigger.afterTicks <= 0 || + trigger.afterTicks > limits.maxTemporalTicks + ) { + issues.push({ + path: 'trigger.afterTicks', + message: `AtTime delay must be within 1..${limits.maxTemporalTicks} timestamp ticks`, + }); + } + return; case 'Cadenced': if ( !Number.isSafeInteger(trigger.everyTicks) || trigger.everyTicks <= 0 || - trigger.everyTicks > limits.maxCadenceTicks + trigger.everyTicks > limits.maxTemporalTicks ) { issues.push({ path: 'trigger.everyTicks', - message: `Cadence must be within 1..${limits.maxCadenceTicks} timestamp ticks`, + message: `Cadence must be within 1..${limits.maxTemporalTicks} timestamp ticks`, }); } } @@ -920,10 +934,10 @@ export function validateActorAuthoringContract( message: 'Auto-close target must be a nonzero u64 logical-cycle nonce', }); } - if (contract.steps.length === 0 || contract.steps.length > maxSteps) { + if (contract.steps.length > maxSteps) { issues.push({ path: 'steps', - message: `Active ${contract.actorType} Actor Contract requires 1..${maxSteps} steps`, + message: `Active ${contract.actorType} Actor Contract allows 0..${maxSteps} steps`, }); } if ( @@ -935,16 +949,24 @@ export function validateActorAuthoringContract( message: `Cooldown must be within 0..${limits.maxExecutionDelayBlocks} blocks`, }); } - if (contract.trigger.type === 'Cadenced' && contract.cooldownBlocks !== 0) { + if ( + (contract.trigger.type === 'AtTime' || + contract.trigger.type === 'Cadenced') && + contract.cooldownBlocks !== 0 + ) { issues.push({ path: 'cooldownBlocks', - message: 'Cadenced triggers require zero block cooldown', + message: 'Temporal triggers require zero block cooldown', }); } - if (contract.trigger.type === 'Cadenced' && contract.scheduleWindow != null) { + if ( + (contract.trigger.type === 'AtTime' || + contract.trigger.type === 'Cadenced') && + contract.scheduleWindow != null + ) { issues.push({ path: 'scheduleWindow', - message: 'Cadenced triggers cannot use a block schedule window', + message: 'Temporal triggers cannot use a block schedule window', }); } if ( @@ -1342,6 +1364,10 @@ function lowerTrigger(trigger: ActorAuthoringTrigger) { threshold: BigInt(trigger.threshold), rearm_threshold: BigInt(trigger.rearmThreshold), }); + case 'AtTime': + return runtimeVariant('AtTime', { + after_ticks: BigInt(trigger.afterTicks), + }); case 'Cadenced': return runtimeVariant('Cadenced', { every_ticks: BigInt(trigger.everyTicks), diff --git a/web-client/src/lib/automation/cost-vectors.ts b/web-client/src/lib/automation/cost-vectors.ts new file mode 100644 index 00000000..6c493ed6 --- /dev/null +++ b/web-client/src/lib/automation/cost-vectors.ts @@ -0,0 +1,346 @@ +/* +Domain: Actors generated cost vectors +Owns: Fail-closed projection of production-runtime ActorCostApi fixtures across geometry and Trigger families. +Excludes: Runtime API transport, live fee estimation, Action receipts, and historical indexing. +Zone: Automation domain contract; runtime-generated vectors bind browser cost names to metadata and Weight identities. +*/ +import vectorsJson from './actors-cost-vectors.json' with { type: 'json' }; +import type { + ActorCostView, + ActorCostWeight, + ActorTriggerFeeView, +} from './cost.ts'; + +export type ActorCostVector = { + name: string; + actorId: bigint; + contractStepCount: number | null; + triggerFamily: ActorTriggerFeeView['family'] | null; + quote: ActorCostView; +}; + +export type ActorCostVectors = { + format: 'deos.actor.cost-vectors'; + formatVersion: 1; + runtimeApiVersion: 1; + metadataSha256: string; + weightSha256: string; + vectors: ActorCostVector[]; +}; + +const TRIGGER_FAMILIES = new Set([ + 'Manual', + 'AddressEvent', + 'ObservationChange', + 'ObservationCrossing', + 'AtTime', + 'Cadenced', +]); + +function record(value: unknown, label: string): Record { + if (value == null || typeof value !== 'object' || Array.isArray(value)) { + throw new Error(`${label} must be an object`); + } + return value as Record; +} + +function decimal(value: unknown, label: string): bigint { + if (typeof value !== 'string' || !/^(0|[1-9]\d*)$/.test(value)) { + throw new Error(`${label} must be a non-negative decimal string`); + } + return BigInt(value); +} + +function digest(value: unknown, label: string): string { + if (typeof value !== 'string' || !/^[0-9a-f]{64}$/.test(value)) { + throw new Error(`${label} must be a lowercase SHA-256 identity`); + } + return value; +} + +function identity(value: unknown, label: string): string { + return `0x${digest(value, label)}`; +} + +function weight(value: unknown, label: string): ActorCostWeight { + const projected = record(value, label); + return { + refTime: decimal(projected.refTime, `${label}.refTime`), + proofSize: decimal(projected.proofSize, `${label}.proofSize`), + }; +} + +function triggerFamily( + value: unknown, + label: string, +): ActorTriggerFeeView['family'] { + if (typeof value === 'string' && TRIGGER_FAMILIES.has(value as never)) { + return value as ActorTriggerFeeView['family']; + } + throw new Error(`${label} must be a supported Trigger family`); +} + +function quote(value: unknown, label: string): ActorCostView { + const source = record(value, label); + if (source.actorType !== 'User' && source.actorType !== 'System') { + throw new Error(`${label}.actorType must be User or System`); + } + const trigger = + source.prospectiveTriggerFee == null + ? null + : (() => { + const candidate = record( + source.prospectiveTriggerFee, + `${label}.prospectiveTriggerFee`, + ); + return { + family: triggerFamily( + candidate.family, + `${label}.prospectiveTriggerFee.family`, + ), + maximumWeight: weight( + candidate.maximumWeight, + `${label}.prospectiveTriggerFee.maximumWeight`, + ), + fee: decimal(candidate.fee, `${label}.prospectiveTriggerFee.fee`), + productionWeightIdentity: identity( + candidate.productionWeightIdentity, + `${label}.prospectiveTriggerFee.productionWeightIdentity`, + ), + } satisfies ActorCostView['prospectiveTriggerFee']; + })(); + const pipeline = + source.prospectivePipelineFee == null + ? null + : (() => { + const candidate = record( + source.prospectivePipelineFee, + `${label}.prospectivePipelineFee`, + ); + if (candidate.strategy !== 'UpfrontBounded') { + throw new Error( + `${label}.prospectivePipelineFee strategy is unsupported`, + ); + } + const machineFee = decimal( + candidate.machineFee, + `${label}.prospectivePipelineFee.machineFee`, + ); + const cleanupFee = decimal( + candidate.cleanupFee, + `${label}.prospectivePipelineFee.cleanupFee`, + ); + const totalFee = decimal( + candidate.totalFee, + `${label}.prospectivePipelineFee.totalFee`, + ); + if (machineFee + cleanupFee !== totalFee) { + throw new Error( + `${label}.prospectivePipelineFee total is inconsistent`, + ); + } + return { + machineFee, + cleanupFee, + totalFee, + strategy: 'UpfrontBounded' as const, + admissionIdentity: identity( + candidate.admissionIdentity, + `${label}.prospectivePipelineFee.admissionIdentity`, + ), + productionWeightIdentity: identity( + candidate.productionWeightIdentity, + `${label}.prospectivePipelineFee.productionWeightIdentity`, + ), + }; + })(); + const actionSource = record( + source.maximumNextActionFee, + `${label}.maximumNextActionFee`, + ); + const holdSource = record(source.stateHold, `${label}.stateHold`); + if (typeof holdSource.exempt !== 'boolean') { + throw new Error(`${label}.stateHold.exempt must be boolean`); + } + const componentsSource = record( + holdSource.components, + `${label}.stateHold.components`, + ); + const components = { + identity: decimal(componentsSource.identity, `${label}.stateHold.identity`), + contractHead: decimal( + componentsSource.contractHead, + `${label}.stateHold.contractHead`, + ), + contractBody: decimal( + componentsSource.contractBody, + `${label}.stateHold.contractBody`, + ), + detector: decimal(componentsSource.detector, `${label}.stateHold.detector`), + funding: decimal(componentsSource.funding, `${label}.stateHold.funding`), + run: decimal(componentsSource.run, `${label}.stateHold.run`), + }; + const holdTotal = decimal(holdSource.total, `${label}.stateHold.total`); + if ( + Object.values(components).reduce( + (total, component) => total + component, + 0n, + ) !== holdTotal + ) { + throw new Error(`${label}.stateHold total is inconsistent`); + } + const projected: ActorCostView = { + actorType: source.actorType, + creationFee: decimal(source.creationFee, `${label}.creationFee`), + prospectiveTriggerFee: trigger, + prospectivePipelineFee: pipeline, + maximumNextActionFee: { + maximumEffectWeight: weight( + actionSource.maximumEffectWeight, + `${label}.maximumNextActionFee.maximumEffectWeight`, + ), + maximumEffectFee: decimal( + actionSource.maximumEffectFee, + `${label}.maximumNextActionFee.maximumEffectFee`, + ), + productionWeightIdentity: identity( + actionSource.productionWeightIdentity, + `${label}.maximumNextActionFee.productionWeightIdentity`, + ), + }, + stateHold: { + exempt: holdSource.exempt, + basePerComponent: decimal( + holdSource.basePerComponent, + `${label}.stateHold.basePerComponent`, + ), + perEncodedByte: decimal( + holdSource.perEncodedByte, + `${label}.stateHold.perEncodedByte`, + ), + components, + total: holdTotal, + }, + }; + if (projected.actorType === 'System') { + const systemFees = [ + projected.creationFee, + projected.prospectiveTriggerFee?.fee ?? 0n, + projected.prospectivePipelineFee?.totalFee ?? 0n, + projected.maximumNextActionFee.maximumEffectFee, + projected.stateHold.total, + ]; + if (!projected.stateHold.exempt || systemFees.some((fee) => fee !== 0n)) { + throw new Error(`${label} violates System fee and hold exemption`); + } + } else if (projected.stateHold.exempt) { + throw new Error(`${label} cannot exempt a User state hold`); + } + return projected; +} + +function optionalStepCount(value: unknown, label: string): number | null { + if (value === null) return null; + if ( + typeof value !== 'number' || + !Number.isSafeInteger(value) || + value < 0 || + value > 32 + ) { + throw new Error(`${label} must be null or an admitted Step count`); + } + return value; +} + +function vector(value: unknown, index: number): ActorCostVector { + const label = `Actor cost vectors[${index}]`; + const source = record(value, label); + if (typeof source.name !== 'string' || source.name.length === 0) { + throw new Error(`${label}.name must be non-empty`); + } + const family = + source.triggerFamily == null + ? null + : triggerFamily(source.triggerFamily, `${label}.triggerFamily`); + const projected = quote(source.quote, `${label}.quote`); + if ((projected.prospectiveTriggerFee?.family ?? null) !== family) { + throw new Error(`${label} Trigger family disagrees with its quote`); + } + return { + name: source.name, + actorId: decimal(source.actorId, `${label}.actorId`), + contractStepCount: optionalStepCount( + source.contractStepCount, + `${label}.contractStepCount`, + ), + triggerFamily: family, + quote: projected, + }; +} + +export function parseActorCostVectors(value: unknown): ActorCostVectors { + const source = record(value, 'Actor cost vector artifact'); + if ( + source.format !== 'deos.actor.cost-vectors' || + source.formatVersion !== 1 || + source.runtimeApiVersion !== 1 + ) { + throw new Error('Actor cost vector identity or version is unsupported'); + } + if (!Array.isArray(source.vectors)) { + throw new Error('Actor cost vectors must be an array'); + } + const vectors = source.vectors.map(vector); + if (new Set(vectors.map((entry) => entry.name)).size !== vectors.length) { + throw new Error('Actor cost vector names must be unique'); + } + const geometry = vectors + .filter( + (entry) => + entry.quote.actorType === 'User' && entry.triggerFamily === 'Manual', + ) + .map((entry) => entry.contractStepCount) + .filter((count): count is number => count !== null) + .sort((left, right) => left - right); + if (geometry.join(',') !== '0,1,4,8,32') { + throw new Error('Actor cost vectors must cover Manual 0/1/4/8/32 geometry'); + } + const families = new Set( + vectors + .filter( + (entry) => + entry.quote.actorType === 'User' && entry.contractStepCount === 1, + ) + .map((entry) => entry.triggerFamily) + .filter( + (family): family is ActorTriggerFeeView['family'] => family !== null, + ), + ); + if ([...TRIGGER_FAMILIES].some((family) => !families.has(family))) { + throw new Error('Actor cost vectors must cover every Trigger family'); + } + if ( + !vectors.some( + (entry) => + entry.quote.actorType === 'System' && entry.contractStepCount === 1, + ) || + !vectors.some( + (entry) => + entry.quote.actorType === 'User' && entry.contractStepCount === null, + ) + ) { + throw new Error( + 'Actor cost vectors must cover System and dormant User semantics', + ); + } + return { + format: source.format, + formatVersion: source.formatVersion, + runtimeApiVersion: source.runtimeApiVersion, + metadataSha256: digest(source.metadataSha256, 'metadata identity'), + weightSha256: digest(source.weightSha256, 'Weight identity'), + vectors, + }; +} + +export const ACTORS_COST_VECTORS = parseActorCostVectors(vectorsJson); diff --git a/web-client/src/lib/automation/cost.ts b/web-client/src/lib/automation/cost.ts new file mode 100644 index 00000000..fc863d74 --- /dev/null +++ b/web-client/src/lib/automation/cost.ts @@ -0,0 +1,313 @@ +/* +Domain: Actors cost projection +Owns: Fail-closed browser projection of named ActorCostApi quotes and ActionFeeCharged receipts. +Excludes: Chain transport, fee estimation, transaction submission, and historical event indexing. +Zone: Automation public contract; adapters and widgets consume these names without recombining fee owners. +*/ +export const ACTORS_COST_RUNTIME_API = 'ActorCostApi_actor_cost_quote' as const; +export const ACTORS_COST_RUNTIME_API_VERSION = 1 as const; + +export type ActorCostFailure = + | 'ActorNotFound' + | 'ActorInvariant' + | 'ComputationOverflow' + | 'WeightAuthorityUnavailable'; + +export type ActorCostWeight = { + refTime: bigint; + proofSize: bigint; +}; + +export type ActorTriggerFeeView = { + family: + | 'Manual' + | 'AddressEvent' + | 'ObservationChange' + | 'ObservationCrossing' + | 'AtTime' + | 'Cadenced'; + maximumWeight: ActorCostWeight; + fee: bigint; + productionWeightIdentity: string; +}; + +export type ActorPipelineFeeView = { + machineFee: bigint; + cleanupFee: bigint; + totalFee: bigint; + strategy: 'UpfrontBounded'; + admissionIdentity: string; + productionWeightIdentity: string; +}; + +export type ActorActionFeeView = { + maximumEffectWeight: ActorCostWeight; + maximumEffectFee: bigint; + productionWeightIdentity: string; +}; + +export type ActorStateHoldView = { + exempt: boolean; + basePerComponent: bigint; + perEncodedByte: bigint; + components: { + identity: bigint; + contractHead: bigint; + contractBody: bigint; + detector: bigint; + funding: bigint; + run: bigint; + }; + total: bigint; +}; + +export type ActorCostView = { + actorType: 'User' | 'System'; + creationFee: bigint; + prospectiveTriggerFee: ActorTriggerFeeView | null; + prospectivePipelineFee: ActorPipelineFeeView | null; + maximumNextActionFee: ActorActionFeeView; + stateHold: ActorStateHoldView; +}; + +export type ActorActionFeeReceipt = { + actorId: bigint; + cycleNonce: bigint; + stepIndex: number; + actualEffectWeight: ActorCostWeight; + fee: bigint; +}; + +const COST_FAILURES: ReadonlySet = new Set([ + 'ActorNotFound', + 'ActorInvariant', + 'ComputationOverflow', + 'WeightAuthorityUnavailable', +]); + +const TRIGGER_FAMILIES: ReadonlySet = new Set([ + 'Manual', + 'AddressEvent', + 'ObservationChange', + 'ObservationCrossing', + 'AtTime', + 'Cadenced', +]); + +function asRecord(value: unknown, field: string): Record { + if (value == null || typeof value !== 'object' || Array.isArray(value)) { + throw new Error(`${field} must be a runtime object`); + } + return value as Record; +} + +function asVariant(value: unknown, field: string) { + const variant = asRecord(value, field); + if (typeof variant.type !== 'string' || variant.type.length === 0) { + throw new Error(`${field} must carry a runtime variant type`); + } + return variant as Record & { type: string; value?: unknown }; +} + +function asUnsigned(value: unknown, field: string): bigint { + if (typeof value === 'bigint' && value >= 0n) return value; + if (typeof value === 'number' && Number.isSafeInteger(value) && value >= 0) { + return BigInt(value); + } + throw new Error(`${field} must be an unsigned runtime integer`); +} + +function asStepIndex(value: unknown): number { + const index = asUnsigned(value, 'Action fee step index'); + const projected = Number(index); + if (!Number.isSafeInteger(projected) || index >= 1n << 32n) { + throw new Error('Action fee step index must fit u32'); + } + return projected; +} + +function asWeight(value: unknown, field: string): ActorCostWeight { + const weight = asRecord(value, field); + return { + refTime: asUnsigned(weight.ref_time, `${field}.ref_time`), + proofSize: asUnsigned(weight.proof_size, `${field}.proof_size`), + }; +} + +function bytesOf(value: unknown, field: string): Uint8Array { + if (value instanceof Uint8Array) return value; + if (Array.isArray(value) && value.every((byte) => Number.isInteger(byte))) { + return Uint8Array.from(value as number[]); + } + if (value != null && typeof value === 'object') { + const asBytes = (value as { asBytes?: unknown }).asBytes; + if (typeof asBytes === 'function') { + const bytes = asBytes.call(value); + if (bytes instanceof Uint8Array) return bytes; + } + } + throw new Error(`${field} must be runtime bytes`); +} + +function asHash(value: unknown, field: string): string { + const bytes = bytesOf(value, field); + if (bytes.length !== 32) throw new Error(`${field} must contain 32 bytes`); + return `0x${Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('')}`; +} + +function projectTrigger(value: unknown): ActorTriggerFeeView { + const trigger = asRecord(value, 'prospective Trigger fee'); + const family = asVariant(trigger.trigger_family, 'Trigger family').type; + if (!TRIGGER_FAMILIES.has(family)) { + throw new Error(`Unsupported runtime Trigger family ${family}`); + } + return { + family: family as ActorTriggerFeeView['family'], + maximumWeight: asWeight(trigger.maximum_weight, 'Trigger maximum Weight'), + fee: asUnsigned(trigger.fee, 'Trigger fee'), + productionWeightIdentity: asHash( + trigger.production_weight_identity, + 'Trigger production Weight identity', + ), + }; +} + +function projectPipeline(value: unknown): ActorPipelineFeeView { + const pipeline = asRecord(value, 'prospective Pipeline fee'); + const strategy = asVariant(pipeline.strategy, 'Pipeline strategy').type; + if (strategy !== 'UpfrontBounded') { + throw new Error(`Unsupported runtime Pipeline strategy ${strategy}`); + } + const machineFee = asUnsigned( + pipeline.pipeline_machine_fee, + 'Pipeline Machine fee', + ); + const cleanupFee = asUnsigned(pipeline.cleanup_fee, 'Pipeline cleanup fee'); + const totalFee = asUnsigned(pipeline.total_fee, 'Pipeline total fee'); + if (machineFee + cleanupFee !== totalFee) { + throw new Error('Pipeline total must equal Machine plus cleanup fees'); + } + return { + machineFee, + cleanupFee, + totalFee, + strategy, + admissionIdentity: asHash( + pipeline.admission_identity, + 'Pipeline admission identity', + ), + productionWeightIdentity: asHash( + pipeline.production_weight_identity, + 'Pipeline production Weight identity', + ), + }; +} + +function projectAction(value: unknown): ActorActionFeeView { + const action = asRecord(value, 'maximum next Action fee'); + return { + maximumEffectWeight: asWeight( + action.maximum_effect_weight, + 'Action maximum effect Weight', + ), + maximumEffectFee: asUnsigned( + action.maximum_effect_fee, + 'Action maximum effect fee', + ), + productionWeightIdentity: asHash( + action.production_weight_identity, + 'Action production Weight identity', + ), + }; +} + +function projectStateHold(value: unknown): ActorStateHoldView { + const hold = asRecord(value, 'Actor state hold'); + if (typeof hold.exempt !== 'boolean') { + throw new Error('Actor state hold exemption must be boolean'); + } + const breakdown = asRecord(hold.breakdown, 'Actor state hold breakdown'); + const components = { + identity: asUnsigned(breakdown.identity, 'identity hold'), + contractHead: asUnsigned(breakdown.contract_head, 'Contract head hold'), + contractBody: asUnsigned(breakdown.contract_body, 'Contract body hold'), + detector: asUnsigned(breakdown.detector, 'detector hold'), + funding: asUnsigned(breakdown.funding, 'funding hold'), + run: asUnsigned(breakdown.run, 'run hold'), + }; + const total = asUnsigned(hold.total, 'Actor state hold total'); + const componentTotal = Object.values(components).reduce( + (sum, component) => sum + component, + 0n, + ); + if (componentTotal !== total) { + throw new Error('Actor state hold total must equal its named components'); + } + if (hold.exempt && total !== 0n) { + throw new Error('State-hold-exempt Actor must report zero held total'); + } + return { + exempt: hold.exempt, + basePerComponent: asUnsigned( + hold.base_per_component, + 'state hold base per component', + ), + perEncodedByte: asUnsigned( + hold.per_encoded_byte, + 'state hold price per encoded byte', + ), + components, + total, + }; +} + +export function projectActorCostQuote(value: unknown): ActorCostView { + const result = asRecord(value, 'runtime Result'); + if (result.success === false) { + const failure = asVariant(result.value, 'Actor cost error').type; + if (!COST_FAILURES.has(failure)) { + throw new Error(`Unsupported runtime Actor cost error ${failure}`); + } + throw new Error( + `Runtime Actor cost quote rejected: ${failure as ActorCostFailure}`, + ); + } + if (result.success !== true) { + throw new Error('Runtime Actor cost output must be a SCALE Result'); + } + const quote = asRecord(result.value, 'Actor cost quote'); + const actorType = asVariant(quote.actor_type, 'Actor type').type; + if (actorType !== 'User' && actorType !== 'System') { + throw new Error(`Unsupported runtime Actor type ${actorType}`); + } + return { + actorType, + creationFee: asUnsigned(quote.creation_fee, 'Actor Creation Fee'), + prospectiveTriggerFee: + quote.prospective_trigger_fee === undefined + ? null + : projectTrigger(quote.prospective_trigger_fee), + prospectivePipelineFee: + quote.prospective_pipeline_fee === undefined + ? null + : projectPipeline(quote.prospective_pipeline_fee), + maximumNextActionFee: projectAction(quote.maximum_next_action_fee), + stateHold: projectStateHold(quote.actor_state_hold), + }; +} + +export function projectActorActionFeeReceipt( + value: unknown, +): ActorActionFeeReceipt { + const receipt = asRecord(value, 'ActionFeeCharged event'); + return { + actorId: asUnsigned(receipt.actor_id, 'Action fee actor id'), + cycleNonce: asUnsigned(receipt.cycle_nonce, 'Action fee cycle nonce'), + stepIndex: asStepIndex(receipt.step_index), + actualEffectWeight: asWeight( + receipt.actual_effect_weight, + 'Action actual effect Weight', + ), + fee: asUnsigned(receipt.fee, 'actual Action fee'), + }; +} diff --git a/web-client/src/lib/automation/eligibility.ts b/web-client/src/lib/automation/eligibility.ts index 00c56550..77d438ee 100644 --- a/web-client/src/lib/automation/eligibility.ts +++ b/web-client/src/lib/automation/eligibility.ts @@ -7,22 +7,20 @@ cooldown, window, retry backoff, breaker, or latch arithmetic. */ export const ACTORS_ELIGIBILITY_RUNTIME_API = 'ActorEligibilityApi_actor_eligibility' as const; -export const ACTORS_ELIGIBILITY_RUNTIME_API_VERSION = 4 as const; -export const ACTORS_TRIGGER_STATE_BOND_RUNTIME_API = - 'ActorEligibilityApi_trigger_state_bond' as const; +export const ACTORS_ELIGIBILITY_RUNTIME_API_VERSION = 6 as const; export type ActorEligibilityFailure = | 'ActorInvariant' - | 'ContinuationInvariant' + | 'RunInvariant' | 'ComputationOverflow'; export const ACTOR_CLOSE_REASONS = [ 'OwnerInitiated', - 'BalanceExhausted', + 'CycleAdmissionInsufficient', + 'TriggerAdmissionInsufficient', 'ConsecutiveFailures', 'WindowExpired', 'CycleNonceExhausted', - 'FeeBudgetExhausted', 'AutoCloseNonceReached', 'RetryAttemptsExhausted', 'ProductiveCycleCompleted', @@ -66,6 +64,7 @@ export type ActorTriggerActivation = pendingRevisions: number; processingRevision: bigint | null; } + | { type: 'AtTime'; afterTicks: bigint; consumed: boolean } | { type: 'Cadenced'; everyTicks: bigint }; export type ActorEligibilityView = @@ -82,7 +81,7 @@ export type ActorEligibilityView = const ELIGIBILITY_FAILURES: ReadonlySet = new Set([ 'ActorInvariant', - 'ContinuationInvariant', + 'RunInvariant', 'ComputationOverflow', ]); @@ -222,6 +221,16 @@ function projectTriggerActivation(value: unknown): ActorTriggerActivation { ), }; } + if (trigger.type === 'AtTime') { + if (typeof fields.consumed !== 'boolean') { + throw new Error('AtTime consumed state must be boolean'); + } + return { + type: trigger.type, + afterTicks: asU64(fields.after_ticks, 'AtTime delay ticks'), + consumed: fields.consumed, + }; + } if (trigger.type === 'Cadenced') { return { type: trigger.type, diff --git a/web-client/src/lib/automation/ingress-runtime-evidence.generated.ts b/web-client/src/lib/automation/ingress-runtime-evidence.generated.ts index 904761b6..c6aa5180 100644 --- a/web-client/src/lib/automation/ingress-runtime-evidence.generated.ts +++ b/web-client/src/lib/automation/ingress-runtime-evidence.generated.ts @@ -15,7 +15,7 @@ export const DEOS_INGRESS_RUNTIME_EVIDENCE = { transactionVersion: 1, }, inventorySha256: - 'c475cd18d522736f9cbd36a8a56098ba7385743bf84deb3038b3c413cef155f2', + 'e92869720bad326c9400f73e59b12d95fccfb3511366bb399e147e1c13654fb1', certifiedProducers: [ { id: 'AddressEventIngressExtension::signed_transfer', diff --git a/web-client/src/lib/automation/matching-wasm.ts b/web-client/src/lib/automation/matching-wasm.ts index b5cb29d7..802ae47c 100644 --- a/web-client/src/lib/automation/matching-wasm.ts +++ b/web-client/src/lib/automation/matching-wasm.ts @@ -47,7 +47,7 @@ export type ActorMatchingWasmProvider = { simulate(request: { pin: ActorMatchingWasmPin; actorId: bigint; - mode: 'FreshCurrentPlan' | 'CurrentContinuation'; + mode: 'FreshCurrentPlan' | 'CurrentRun'; contractScale: ActorContractHex; actorType: ActorContractArtifact['actorType']; mutability: ActorContractArtifact['mutability']; @@ -78,7 +78,7 @@ function samePin(left: ActorMatchingWasmPin, right: ActorMatchingWasmPin) { export async function runActorMatchingWasmSimulation(input: { artifact: ActorContractArtifact; actorId: bigint; - mode: 'FreshCurrentPlan' | 'CurrentContinuation'; + mode: 'FreshCurrentPlan' | 'CurrentRun'; metadataBytes: Uint8Array; runtime: ActorContractRuntimeIdentity; runtimeCodeBytes: Uint8Array; @@ -106,7 +106,7 @@ export async function runActorMatchingWasmSimulation(input: { if (input.actorId < 0n) { throw new Error('actorId must be non-negative'); } - if (!['FreshCurrentPlan', 'CurrentContinuation'].includes(input.mode)) { + if (!['FreshCurrentPlan', 'CurrentRun'].includes(input.mode)) { throw new Error('mode must identify one runtime simulation path'); } validateHash(input.snapshot.blockHash, 'blockHash'); @@ -185,8 +185,8 @@ function contractStepsLength(runtimeValue: unknown) { throw new Error('Runtime simulation requires an ActorContract'); } const value = runtimeValue as Record; - if (!Array.isArray(value.steps) || value.steps.length === 0) { - throw new Error('Runtime simulation requires non-empty Contract Steps'); + if (!Array.isArray(value.steps)) { + throw new Error('Runtime simulation requires bounded Contract Steps'); } return value.steps.length; } @@ -242,23 +242,19 @@ function validateOutcome( previousStep = step.stepIndex; } if (outcome.status === 'Suspended') { - if (outcome.continuationCursor == null) { - throw new Error( - 'Suspended runtime outcomes require a Continuation cursor', - ); + if (outcome.runCursor == null) { + throw new Error('Suspended runtime outcomes require an Actor run cursor'); } - validateIndex(outcome.continuationCursor, 'outcome.continuationCursor'); - if (outcome.continuationCursor >= maxSteps) { - throw new Error('Continuation cursor exceeds admitted Contract Steps'); + validateIndex(outcome.runCursor, 'outcome.runCursor'); + if (outcome.runCursor >= maxSteps) { + throw new Error('Actor run cursor exceeds admitted Contract Steps'); } - if (outcome.continuationCursor < outcome.startCursor) { - throw new Error( - 'Continuation cursor cannot precede the attempted suffix', - ); + if (outcome.runCursor < outcome.startCursor) { + throw new Error('Actor run cursor cannot precede the attempted suffix'); } - } else if (outcome.continuationCursor != null) { + } else if (outcome.runCursor != null) { throw new Error( - 'Only Suspended runtime outcomes may expose a Continuation cursor', + 'Only Suspended runtime outcomes may expose an Actor run cursor', ); } } diff --git a/web-client/src/lib/automation/materialization.ts b/web-client/src/lib/automation/materialization.ts new file mode 100644 index 00000000..9572283a --- /dev/null +++ b/web-client/src/lib/automation/materialization.ts @@ -0,0 +1,56 @@ +/* +Domain: Actors materialization projection +Owns: Named browser contracts for canonical Crossing capacity and current materialization faults. +Excludes: Runtime transport, fault repair, scheduler reconstruction, and historical indexing. +Zone: Automation domain; finalized adapters lower runtime API values into these contracts. +*/ + +export type CrossingCapacityView = { + userLimit: number; + totalLimit: number; + userMemberships: number; + totalMemberships: number; +}; + +export type MaterializationFaultClass = + | 'Invariant' + | 'Capacity' + | 'SchedulerExhausted' + | 'Other'; + +export type CrossingMaterializationFault = { + feed: unknown; + revision: bigint | null; + threshold: bigint | null; + class: MaterializationFaultClass; +}; + +export type FanoutMaterializationFault = { + feed: unknown; + revision: bigint; + subscriberPage: number | null; + subscriberPosition: number; + actorId: bigint | null; + semanticContractId: Uint8Array | null; + bodyCommitment: Uint8Array | null; + admissionIdentity: Uint8Array | null; + branch: string; + class: MaterializationFaultClass; +}; + +export type WakeupMaterializationFault = { + key: { type: 'Block'; block: number } | { type: 'Tick'; tick: bigint }; + page: number; + class: MaterializationFaultClass; +}; + +export type MaterializationFaultsView = { + crossing: CrossingMaterializationFault | null; + fanout: FanoutMaterializationFault | null; + wakeup: WakeupMaterializationFault | null; +}; + +export type ActorMaterializationProjection = { + crossingCapacity: CrossingCapacityView; + faults: MaterializationFaultsView; +}; diff --git a/web-client/src/lib/automation/resource.ts b/web-client/src/lib/automation/resource.ts new file mode 100644 index 00000000..3f934580 --- /dev/null +++ b/web-client/src/lib/automation/resource.ts @@ -0,0 +1,59 @@ +/* +Domain: Actors block-resource projection +Owns: Named browser contracts for configured, current, and finalized canonical block-resource facts. +Excludes: Runtime transport, scheduler reconstruction, archive history, and admission decisions. +Zone: Automation domain; transport adapters lower finalized runtime API values into these contracts. +*/ + +export type ActorResourceWeight = { + refTime: bigint; + proofSize: bigint; +}; + +export type ActorResourceLimits = { + actorControl: ActorResourceWeight; + sharedEconomic: ActorResourceWeight; + actorBaseTurn: ActorResourceWeight; + userBaseTurn: ActorResourceWeight; +}; + +export type ActorResourceBudgetView = { + maximumBlock: ActorResourceWeight; + fixedEnvelope: ActorResourceWeight; + limits: ActorResourceLimits; +}; + +export type ActorResourceUsageView = { + actorControl: ActorResourceWeight; + actorEffect: ActorResourceWeight; + userDispatch: ActorResourceWeight; +}; + +export type ActorResourcePhase = + | 'ContextIncomplete' + | 'PrepassExecuting' + | 'ExternalPhase' + | 'FreshDrain' + | 'Finalizable'; + +export type CurrentActorResourceView = { + blockNumber: number; + phase: ActorResourcePhase; + usage: ActorResourceUsageView; + outstandingReservations: number; + finalizedFixedReserved: ActorResourceWeight | null; + optionalActorWorkHalted: boolean; +}; + +export type FinalizedActorResourceView = { + blockNumber: number; + fixedReserved: ActorResourceWeight; + usage: ActorResourceUsageView; + optionalActorWorkHalted: boolean; +}; + +export type ActorResourceProjection = { + budget: ActorResourceBudgetView; + current: CurrentActorResourceView | null; + finalized: FinalizedActorResourceView | null; +}; diff --git a/web-client/src/lib/automation/risk-warnings.ts b/web-client/src/lib/automation/risk-warnings.ts index 000c5c9f..14c53744 100644 --- a/web-client/src/lib/automation/risk-warnings.ts +++ b/web-client/src/lib/automation/risk-warnings.ts @@ -57,14 +57,16 @@ export function projectActorCompositionWarnings( const { artifact, analysis } = input; if (artifact.mutability === 'Immutable') { - const reachableTerminal = analysis.steps.some(isTerminalStep); + const reachableTerminal = + analysis.autoCloseAtCycleNonce !== null || + analysis.steps.some(isTerminalStep); if (!reachableTerminal) { warnings.push({ kind: 'ImmutableWithoutReachableTerminal', severity: 'critical', message: 'Immutable actor has no reachable terminal condition and will keep custody permanently.', - evidence: `mutability=Immutable, ${analysis.steps.length} step(s), no AbortCycle/zero-retry terminal step`, + evidence: `mutability=Immutable, ${analysis.steps.length} step(s), no auto-close nonce or AbortCycle/zero-retry terminal step`, }); } } diff --git a/web-client/src/lib/automation/runtime-simulation-codec.ts b/web-client/src/lib/automation/runtime-simulation-codec.ts index 3dbf3b01..2d0e1659 100644 --- a/web-client/src/lib/automation/runtime-simulation-codec.ts +++ b/web-client/src/lib/automation/runtime-simulation-codec.ts @@ -35,7 +35,7 @@ export type ActorDecodedRuntimeSimulationOutcome = { closeReason: string | null; cycleNonce: bigint; startCursor: number; - continuationCursor: number | null; + runCursor: number | null; unsuccessfulAttemptsAtCursor: number | null; cumulativeOutcomes: { executedSteps: number; @@ -190,10 +190,7 @@ function projectOutcome(value: unknown): ActorDecodedRuntimeSimulationOutcome { closeReason, cycleNonce: outcome.cycle_nonce, startCursor: asIndex(outcome.start_cursor, 'start_cursor'), - continuationCursor: asOptionalIndex( - outcome.continuation_cursor, - 'continuation_cursor', - ), + runCursor: asOptionalIndex(outcome.run_cursor, 'run_cursor'), unsuccessfulAttemptsAtCursor: asOptionalIndex( outcome.unsuccessful_attempts_at_cursor, 'unsuccessful_attempts_at_cursor', diff --git a/web-client/src/lib/automation/simulation.ts b/web-client/src/lib/automation/simulation.ts index c51a0f98..2ef89158 100644 --- a/web-client/src/lib/automation/simulation.ts +++ b/web-client/src/lib/automation/simulation.ts @@ -73,7 +73,7 @@ export type ActorLocalSimulationResult = { closeReason: 'RetryAttemptsExhausted' | 'ProductiveCycleCompleted' | null; cycleNonce: bigint; startCursor: number; - continuationCursor: number | null; + runCursor: number | null; unsuccessfulAttemptsAtCursor: number | null; state: State; cumulative: ActorLocalSimulationCounts; @@ -287,7 +287,7 @@ export function simulateActorLocally(input: { closeReason: productiveClose ? 'ProductiveCycleCompleted' : null, cycleNonce: input.cycleNonce, startCursor: input.startCursor, - continuationCursor: null, + runCursor: null, unsuccessfulAttemptsAtCursor: null, state, cumulative, @@ -310,7 +310,7 @@ export function simulateActorLocally(input: { closeReason: exhausted ? 'RetryAttemptsExhausted' : null, cycleNonce: input.cycleNonce, startCursor: input.startCursor, - continuationCursor: exhausted ? null : index, + runCursor: exhausted ? null : index, unsuccessfulAttemptsAtCursor: exhausted ? null : nextUnsuccessfulAttempts, @@ -326,7 +326,7 @@ export function simulateActorLocally(input: { closeReason: null, cycleNonce: input.cycleNonce, startCursor: input.startCursor, - continuationCursor: null, + runCursor: null, unsuccessfulAttemptsAtCursor: null, state, cumulative, @@ -344,7 +344,7 @@ export function simulateActorLocally(input: { closeReason: productiveClose ? 'ProductiveCycleCompleted' : null, cycleNonce: input.cycleNonce, startCursor: input.startCursor, - continuationCursor: null, + runCursor: null, unsuccessfulAttemptsAtCursor: null, state, cumulative, diff --git a/web-client/src/lib/automation/trigger-bond-vectors.ts b/web-client/src/lib/automation/trigger-bond-vectors.ts deleted file mode 100644 index 24e769af..00000000 --- a/web-client/src/lib/automation/trigger-bond-vectors.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* -Domain: Actors Trigger-state bond vectors -Owns: Runtime-generated reference-policy bond quotes for typed Trigger authoring. -Excludes: Live chain transport, reserve mutation, token formatting policy, and physical detector occupancy. -Zone: Automation domain contract; the runtime API remains canonical at a finalized block. -*/ -import vectorsJson from './actors-trigger-bond-vectors.json' with { type: 'json' }; - -export type ActorTriggerFamily = - | 'Manual' - | 'AddressEvent' - | 'ObservationChange' - | 'ObservationCrossing' - | 'Cadenced'; - -type TriggerBondVectors = { - formatVersion: number; - metadataSha256: string; - actorsWeightSha256: string; - vectors: { triggerFamily: ActorTriggerFamily; amount: string }[]; -}; - -export const ACTORS_TRIGGER_BOND_VECTORS = vectorsJson as TriggerBondVectors; - -export function actorTriggerStateBond( - triggerFamily: ActorTriggerFamily, -): bigint { - const vector = ACTORS_TRIGGER_BOND_VECTORS.vectors.find( - (candidate) => candidate.triggerFamily === triggerFamily, - ); - if (!vector || !/^(0|[1-9]\d*)$/.test(vector.amount)) { - throw new Error( - `Missing canonical Trigger-state bond vector for ${triggerFamily}`, - ); - } - return BigInt(vector.amount); -} diff --git a/web-client/src/lib/observation/ObservationInspector.svelte b/web-client/src/lib/observation/ObservationInspector.svelte index 1f0b189c..81c5aaeb 100644 --- a/web-client/src/lib/observation/ObservationInspector.svelte +++ b/web-client/src/lib/observation/ObservationInspector.svelte @@ -5,6 +5,7 @@ Excludes: Runtime queries, history, plan authoring, and trading recommendations. Zone: Observation domain UI; receives provider capabilities from its composition root. -->