diff --git a/.agents/skills/README.md b/.agents/skills/README.md index 8bb5ff65..e729fc72 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` | -| `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` | +| `architecture-experiments` | Leaf/Synthesis proof graphs, frozen obligations, controlled production-Weight evidence, progressive fission, artifact-bound lineage and bounded 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/hallucinations.jsonl b/.agents/skills/alignment/ledgers/hallucinations.jsonl index e827c7dd..f49e0265 100644 --- a/.agents/skills/alignment/ledgers/hallucinations.jsonl +++ b/.agents/skills/alignment/ledgers/hallucinations.jsonl @@ -63,3 +63,53 @@ {"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"} +{"timestamp":"2026-09-01T05:16:55Z","title":"Lifecycle envelope used as FIFO stop owner","summary":"The complete lifecycle admission envelope was treated as the next live FIFO head requirement, overstating control Weight and making stop classification non-falsifiable against the scheduler admission point.","scope":"actors/performance-evidence","status":"resolved","files":"template/runtime/src/tests/actors_integration_tests.rs,template/pallets/actors/docs/architecture.en.md","tags":"weight,admission,fifo,attribution","remedy":"Classify a post-probe stop with queue-consume maximum plus stored Step control plus reachable cleanup; include scan/probe only when measuring before those costs are consumed, and keep lifecycle admission separate.","related":""} +{"timestamp":"2026-09-01T07:08:00Z","title":"Transient Cargo Wasm mistaken for canonical evidence mutation","summary":"Release-profile assurance rebuilt the repository-local Wasm side artifact and then reported that transient Cargo output as mutation of the caller-selected canonical production Wasm, despite every behavioral gate passing.","scope":"actors/assurance","status":"resolved","files":"scripts/actors-assurance.sh","tags":"wasm,identity,assurance,artifact","remedy":"Snapshot the canonical Wasm before Cargo validation, restore it atomically on success or failure, and compare the restored bytes while source, Weight, and metadata identities remain independently checked.","related":""} +{"timestamp":"2026-09-01T14:30:00Z","title":"Partial control cell mistaken for complete frame authority","summary":"The 171-byte admission strip and then the 198-byte identity/hot cell were each treated as complete authority before tracing all reconstruction inputs; the latter still omitted the immutable admission certificate, dual coexisting process/Trigger wakeup pointers, and an explicit unsignaled placement.","scope":"actors/EXP-0025","status":"resolved","files":"template/pallets/actors/src/lib.rs,template/pallets/actors/src/tests/candidate_geometry.rs,.agents/skills/architecture-experiments/tracks/actors/EXP-0025.md","tags":"geometry,authority,proofsize,counterfactual","remedy":"Before naming a compact cell complete, prove round-trip reconstruction for identity, hot state, admission, ready/waiting/unsignaled placement, and coexisting memberships; derive only fields whose source remains uniquely available at every transition.","related":"EXP-0025"} +{"timestamp":"2026-09-01T16:20:00Z","title":"Step atom mistaken for complete Pipeline Opening","summary":"An EXP-0025 benchmark called execute_current_step_and_place directly with the useful-signal latch still set, then treated Idle/pending output and cycle_nonce 0 as a complete User Opening branch. It bypassed canonical Pipeline Opening consumption and made the 5,652-proof projection invalid.","scope":"actors/EXP-0025","status":"resolved","files":"template/pallets/actors/src/benchmarking.rs,.agents/skills/architecture-experiments/tracks/actors/EXP-0025.md","tags":"benchmark,opening,latch,cycle-nonce,complete-path","remedy":"A complete-path benchmark must traverse the canonical FIFO and Pipeline Opening transition, then assert nonce advancement, latch consumption, run state, final placement, fees, and events; direct Step atoms remain branch components only.","related":"EXP-0025"} +{"timestamp":"2026-09-01T17:05:00Z","title":"Placement-sized state hold introduced semantic event drift","summary":"The direct C32 branch initially priced the actual encoded eligible_at value, so Ready-to-Unsignaled released eight held units and emitted a Balances event absent from A0 despite otherwise exact state and fee equivalence.","scope":"actors/EXP-0025","status":"resolved","files":"template/pallets/actors/src/lib.rs,template/pallets/actors/src/tests/candidate_geometry.rs","tags":"state-hold,event-equivalence,placement,frame-geometry","remedy":"Price the maximum bounded placement envelope for frame-resident control fields, as A0 prices maximum hot/run geometry, and require exact differential event tests across every physical placement transition.","related":"EXP-0025"} +{"timestamp":"2026-09-01T17:57:00Z","title":"Concurrent production-Wasm probes confounded RefTime","summary":"The direct-prefix and rejected-head frame-omni-bencher processes were first launched in parallel, making both CPU timing models non-comparable even though proof and storage ownership remained deterministic.","scope":"actors/EXP-0025","status":"resolved","files":".agents/skills/architecture-experiments/tracks/actors/EXP-0025.md","tags":"benchmark,ref-time,concurrency,controlled-variables","remedy":"Run decision-bearing production-Wasm benchmarks sequentially on the same host; retain parallel runs only as discarded rehearsals and bind recorded artifacts to sequential reruns.","related":"EXP-0025"} +{"timestamp":"2026-09-01T18:49:17Z","title":"Capacity-violating sparse fixture mistaken for scale evidence","summary":"The first EXP-0025 sparse-read fixture staged 10,000 future cells plus 100 due cells even though the runtime admits at most 10,000 active Actors, so its 10,100-cell population could not represent a realizable scheduler state.","scope":"actors/EXP-0025","status":"resolved","files":"template/pallets/actors/src/tests/candidate_geometry.rs","tags":"configured-bound,fixture,read-scaling,capacity","remedy":"Keep sparse scheduler fixtures within MaxActiveActors; the corrected population is 9,900 future plus 100 due, and acceptance claims must bind total registered authority as well as the due subset.","related":"EXP-0025"} +{"timestamp":"2026-09-01T19:25:15Z","title":"Benchmark unit-test geometry did not admit the production benchmark range","summary":"Actors Crossing benchmarks declared 128-member/page production ranges, but the runtime-benchmarks test runtime retained 8 User members, 16-member pages, and 16-Actor worker limits. Full benchmark tests therefore failed in setup or exercised a different topology even though production-Wasm benchmarks used the larger runtime constants.","scope":"actors/benchmark-fixtures","status":"resolved","files":"template/pallets/actors/src/mock.rs,template/pallets/actors/src/tests.rs,template/pallets/actors/src/tests/crossing.rs,template/pallets/actors/src/tests/scheduling.rs","tags":"benchmark-range,configured-bound,mock-runtime,fixture","remedy":"Under runtime-benchmarks, bind mock Crossing page/User/worker bounds to the declared 128-member geometry and keep ordinary tests dynamic in configured bounds; require the complete benchmark-feature suite, not only focused benchmark tests.","related":"EXP-0025"} +{"timestamp":"2026-09-01T19:43:24Z","title":"Source-preserving intent was mistaken for retained benchmark identity","summary":"After the focused EXP-0025 benchmarks, later source and validation-fixture edits were assumed not to affect the benchmark runtime because the intended production delta appeared comment-only. An exact rebuild produced a different compressed Wasm identity, so the prior generated Weight artifacts could no longer attest the current source state.","scope":"actors/EXP-0025","status":"resolved","files":"template/pallets/actors/src/lib.rs,template/pallets/actors/src/mock.rs,template/pallets/actors/src/tests.rs","tags":"exact-identity,benchmark-wasm,source-drift,evidence","remedy":"After any source-tree mutation, rebuild and hash the benchmark runtime before retaining decision-bearing artifacts; if the Wasm identity changes, rerun every coupled benchmark sequentially even when the edit was believed semantically inert.","related":"EXP-0025"} +{"timestamp":"2026-09-01T20:19:48Z","title":"Additive FRAME model hid an Actor-count by Predicate-count interaction","summary":"The first EXP-0025 Transfer benchmark varied C32 Actor count and per-Actor Predicate count as independent linear components even though Predicate reads scale with their product. The generated additive n+p model attributed only a fixed p term and could not soundly charge the n×p workload.","scope":"actors/EXP-0025","status":"resolved","files":"template/pallets/actors/src/benchmarking.rs","tags":"benchmark-model,interaction-term,predicate,weight-soundness","remedy":"Hold per-Actor geometry fixed whenever its cost repeats across a population component; benchmark Predicate 0/2/4 as separate n-scaled branches rather than fitting an additive two-component model without an interaction term.","related":"EXP-0025"} +{"timestamp":"2026-09-01T19:27:25Z","severity":"FATAL","rule":"O(N) Iteration Anti-Pattern","files":"/home/llb/Repos/deos/template/pallets/actors/src/lib.rs"} +{"timestamp":"2026-09-01T19:28:46Z","severity":"FATAL","rule":"O(N) Iteration Anti-Pattern","files":"/home/llb/Repos/deos/template/pallets/actors/src/lib.rs"} +{"timestamp":"2026-09-01T23:34:09Z","title":"Detached frame service mistaken for atomic successor placement","summary":"The first EXP-0025 retry path took the Ready chunk out of storage, committed execution and a same-page Continuation inside a per-Actor transaction, then rewrote the stale local source frame and aggregate occupancy after the transaction. This erased the committed Continuation while leaving its tail and locator published.","scope":"actors/EXP-0025","status":"resolved","files":"template/pallets/actors/src/scheduler.rs,template/pallets/actors/src/tests/candidate_geometry.rs","tags":"atomicity,continuation,frame,occupancy,stale-write","remedy":"Keep the source chunk resident; consume the exact source slot and decrement occupancy inside the same per-Actor storage transaction that executes and places the successor. Let successor append restore occupancy, retain open partial ranges, and advance the Ready head across a fully closed empty ticket range.","related":"EXP-0025"} +{"timestamp":"2026-09-02T00:16:21Z","title":"Ordinary Running mistaken for retry continuation telemetry","summary":"The first EXP-0025 Running branch emitted CycleContinued for every ordinary persisted next Step because the event name appeared to describe all Continuations. A0 emits that event only when a Suspended retry begins; ordinary Running advances without it, so the candidate introduced event drift despite matching state and effects.","scope":"actors/EXP-0025","status":"resolved","files":"template/pallets/actors/src/execution.rs,template/pallets/actors/src/tests/candidate_geometry.rs","tags":"event-semantics,running,retry,differential-test","remedy":"Treat event ownership as executable semantics rather than lexical implication. Differential-test each Running and Suspended transition against A0 event sequences; emit CycleContinued only on the existing Suspended retry path.","related":"EXP-0025"} +{"timestamp":"2026-09-02T01:08:00Z","title":"Terminal Running parity mistaken for middle-Step parity","summary":"The two-Step EXP-0025 differential covered Opening and terminal Running only, so both paths updated last_cycle_block at a completion boundary and concealed that the candidate also advanced it during an ordinary nonterminal continuation. A three-Step differential exposed the one-block drift.","scope":"actors/EXP-0025","status":"resolved","files":"template/pallets/actors/src/execution.rs,template/pallets/actors/src/tests/candidate_geometry.rs","tags":"differential-test,running,middle-step,timestamp","remedy":"For multi-Step state machines, compare Opening, at least one nonterminal middle transition, and terminal completion independently; do not infer middle-state parity from matching terminal snapshots.","related":"EXP-0025"} +{"timestamp":"2026-09-02T01:42:00Z","title":"Frame geometry mistaken for the middle-Step proof owner","summary":"The first continuation follow-up treated source and successor C32 frames as the likely owner of the 100-Actor middle-Step miss. The generated storage ledger showed a 6,553-byte per-Actor slope and a 6,545-byte ActorContractTailChunk proof addition, while frame chunks were cohort-level keys.","scope":"actors/EXP-0025","status":"resolved","files":".agents/skills/architecture-experiments/tracks/actors/EXP-0025.md,.agents/skills/architecture-experiments/tracks/actors/EXP-0004.md","tags":"proofsize,storage-ledger,frame,contract-tail,bottleneck-attribution","remedy":"Read the generated per-storage proof ledger before changing the visually salient geometry; attribute the slope to key cardinality and maximum value proof, then route optimization to the owning representation experiment.","related":"EXP-0004"} +{"timestamp":"2026-09-02T03:11:00Z","title":"Maximum storage annotation mistaken for actual workload proof","summary":"EXP-0004 initially inferred that reducing ActorContractTailChunk capacity from four Steps to two would reduce the 6,545-byte annotated maximum proof. The three-Step workload stores two actual tail Steps under both capacities, so its encoded value and generated 6,553-byte slope remained identical; one-Step splitting added a key and regressed proof.","scope":"actors/EXP-0004","status":"resolved","files":"template/pallets/actors/src/types/contract.rs,.agents/skills/architecture-experiments/tracks/actors/EXP-0004.md","tags":"proofsize,max-encoded-len,actual-value,fragmentation,benchmark-attribution","remedy":"Distinguish storage metadata maxima from the actual values materialized by a measured workload. Run the smallest capacity control before planning lifecycle work, and count all keys loaded by complete-context reconstruction.","related":"EXP-0004"} +{"timestamp":"2026-09-02T04:02:00Z","title":"Compact execution payload mistaken for removal of the canonical tail owner","summary":"EXP-0004 C3 removed ActorContractTailChunk from Running control-context reconstruction and added one compact current payload, but complete-path Weight still read canonical tail geometry during exact User state-hold reconciliation. The candidate therefore added a key and regressed grouped proof to 7,103.24 instead of removing the owner.","scope":"actors/EXP-0004","status":"resolved","files":"template/pallets/actors/src/lib.rs,template/pallets/actors/src/scheduler.rs,.agents/skills/architecture-experiments/tracks/actors/EXP-0004.md","tags":"complete-path,state-hold,contract-tail,compact-payload,non-additive-counterfactual","remedy":"Trace every downstream owner after a representation substitution, including economic state-hold reconciliation. Do not combine a separately accepted hold optimization with a Step-layout candidate; reject the candidate or open a separately controlled mechanism experiment.","related":"EXP-0004,EXP-0024"} +{"timestamp":"2026-09-02T08:42:00Z","title":"State-hold tail scan mistaken for the sole full-Contract execution owner","summary":"EXP-0027 removed immutable Contract-body scans from per-Step state-hold reconciliation, but the maximum 12-Step path still read all three tail chunks because execution-plan construction independently requested full Contract reconstruction. Candidate and baseline therefore retained identical 11 reads/Actor and 9,882 estimated proof/Actor slopes.","scope":"actors/EXP-0027","status":"resolved","files":"template/pallets/actors/src/lib.rs,template/pallets/actors/src/scheduler.rs,template/pallets/actors/src/benchmarking.rs","tags":"complete-path,state-hold,full-contract,reconstruction,overlapping-owner","remedy":"Before changing one apparent scan owner, enumerate every call site that requests the same storage keys and measure an exact baseline owner. A cache or scan elision is material only if no independently retained path reproduces the same read/proof set.","related":"EXP-0004,EXP-0027"} +{"timestamp":"2026-09-02T11:18:31Z","title":"Trigger fee-collection failure mistaken for one family-uniform rollback rule","summary":"The staged C1 temporal latch initially rolled back both AtTime and Cadenced fee-collection failure. A0 evidence distinguishes them: AtTime preserves its one-shot due reference for retry, while Cadenced consumes the failed occurrence and advances to the next deadline without readiness or apoptosis.","scope":"actors/EXP-0025","status":"resolved","files":"template/pallets/actors/src/scheduler.rs,template/pallets/actors/src/tests/candidate_geometry.rs,template/pallets/actors/src/tests/scheduling.rs","tags":"trigger-fee,cadence,at-time,rollback,differential-test","remedy":"Treat failure semantics as Trigger-family contract, not a shared helper default. Differentially test retained detector authority after failure: AtTime retries the same due occurrence; Cadenced advances its deadline and remains unlatched.","related":"EXP-0025"} +{"timestamp":"2026-09-02T12:53:32Z","title":"try_mutate inventory mistaken for complete hot-mutation ownership","summary":"The first C1 cutover inventory centralized ActorHot::try_mutate, insert, remove, and ordinary reads but omitted seven production ActorHot::mutate call sites in zero-Step, Opening, continuation, failure-streak, paused-head, and stale-ticket paths. Claims that every production hot mutation was behind seams were therefore too broad.","scope":"actors/EXP-0025","status":"resolved","files":"template/pallets/actors/src/lib.rs,template/pallets/actors/src/execution.rs,template/pallets/actors/src/scheduler.rs","tags":"storage-mutation,cutover-inventory,actor-hot,seam,zero-step","remedy":"Inventory every StorageMap mutation form (`mutate`, `try_mutate`, `mutate_exists`, `try_mutate_exists`, `insert`, `remove`, `set`, `take`) before declaring single-owner cutover readiness. Keep infallible fallback semantics in a dedicated owner seam rather than treating only fallible mutations as authority.","related":"EXP-0025"} +{"timestamp":"2026-09-03T00:01:01Z","title":"Pre-consumption Actor view mistaken for post-Opening hot state","summary":"Cadenced rearm initially copied the Ready source ticket from the frozen ActiveActorView into the post-Opening transition. The scalar transition had already cleared that ticket, so the next recurrence rolled back even though the Trigger and admission authority were correct.","scope":"actors/EXP-0025","status":"resolved","files":"template/pallets/actors/src/scheduler.rs,template/pallets/actors/src/tests/scheduling.rs","tags":"opening,source-consumption,frozen-authority,queue-ticket,cadenced","remedy":"Freeze semantic identity, Contract, admission, and Trigger state before source consumption, but explicitly normalize transition-owned hot fields to their post-Opening values. For Cadenced rearm retain the consumed latch context needed by recurrence placement while clearing the consumed Ready ticket before publishing the next reference.","related":"EXP-0025"} +{"timestamp":"2026-09-03T00:26:45Z","title":"Scalar active-existence check mistaken for collision safety","summary":"ActorId admission still classified occupancy through scalar active state after canonical identity moved to frame control. If scalar identity/hot/admission differentials were absent, an existing frame-owned Actor could appear vacant and permit attempted identity reuse.","scope":"actors/EXP-0025","status":"resolved","files":"template/pallets/actors/src/lib.rs,template/pallets/actors/src/tests/lifecycle.rs","tags":"actor-id,collision,frame-authority,admission,scalar-drift","remedy":"Every identity-allocation and collision boundary must query the canonical active owner, not a differential projection. Prove an active frame primary remains occupied after all scalar control partitions are removed and rejects creation at the same ActorId without mutation.","related":"EXP-0025"} +{"timestamp":"2026-09-03T00:32:44Z","title":"Frame service loader mistaken for drop-in placement authority","summary":"Replacing the scalar loader inside prime_actor_schedule with the current frame service loader passed an Unsignaled smoke test but broke paused Cadenced resume, suspended-run placement, paused Crossing coalescing, and exact QueueTicket exhaustion rollback after Contract replacement or run cancellation. Frame primary hot state still preserves source ticket or physical wakeup authority that the transitional scalar post-consumption projection has already cleared, so the same planner input is not yet equivalent.","scope":"actors/EXP-0025","status":"resolved","files":"template/pallets/actors/src/scheduler.rs,template/pallets/actors/src/tests/scheduling.rs,template/pallets/actors/src/tests/crossing.rs,template/pallets/actors/src/tests/execution.rs","tags":"placement,wakeup-pointer,queue-ticket,frame-authority,scalar-bridge,staged-cutover","remedy":"Do not globally substitute frame loading at placement boundaries until each caller passes its explicit normalized post-consumption hot snapshot plus source-consumed and destination-publication facts. Preserve the scalar bridge for prime_actor_schedule, and convert paused, retry, cancellation, replacement, and temporal transitions atomically before removing it.","related":"EXP-0025"} +{"timestamp":"2026-09-03T01:11:00Z","title":"Post-take frame loading mistaken for wakeup source authority","summary":"A global Block-wakeup switch loaded canonical frame service state only after taking the Actor-bearing WakeupPages slot. Unsignaled terminal/reference validation intentionally requires that physical pointer to remain live, so the loader rejected valid source authority; low-level arbitrary Idle/unlatched Block-wakeup fixtures additionally exposed geometry that canonical C1 does not admit.","scope":"actors/EXP-0025","status":"resolved","files":"template/pallets/actors/src/scheduler.rs,template/pallets/actors/src/tests/wakeups.rs","tags":"wakeup,source-consumption,frozen-authority,physical-reference,representation-honesty","remedy":"Freeze and validate frame authority while the selected WakeupPages reference is still physically present, then consume source and publish a normalized destination in one transaction. Convert or retire arbitrary Idle/unlatched service-wakeup fixtures instead of weakening C1 restoration or silently falling back to scalar authority.","related":"EXP-0025"} +{"timestamp":"2026-09-03T03:24:00Z","title":"Strict active projection mistaken for drop-in scalar replacement","summary":"Switching the legacy active_actor_view helper globally to strict frame loading made the new admissionless completion witness pass but initially broke scalar-oriented fixture mutations, exhaustion setup, observation activation, and simulation expectations across eleven tests. The one-line substitution was unsafe; the later retained cutover required migrating the complete caller/fixture cohort and separating simulation-only post-effect reconstruction from live frame projection.","scope":"actors/EXP-0025","status":"resolved","files":"template/pallets/actors/src/lib.rs,template/pallets/actors/src/tests/execution.rs","tags":"projection,frame-authority,scalar-fixture,staged-cutover,active-view","remedy":"Keep strict frame consumers explicit through load_actor_state_for_frame_control until every caller and fixture has representation-honest authority. Migrate the projection boundary with its complete caller set, normalize terminal Failed and window-expiry destinations, coalesce busy frame primaries in place, and reconstruct simulation-only post-effect state from its loaded hot snapshot rather than using one witness to justify a global loader swap.","related":"EXP-0025"} +{"timestamp":"2026-09-03T04:53:44Z","title":"Run source-consumed flag mistaken for all placement consumption","summary":"Cancellation can invalidate a live wakeup and consume its Waiting frame primary even when the caller-level source_consumed flag is false; treating that flag as complete placement provenance removes required Unsignaled restoration and makes deactivation fail closed.","scope":"actors/C1-control-cutover","status":"resolved","files":"template/pallets/actors/src/execution.rs,template/pallets/actors/src/lib.rs","tags":"cancellation,wakeup,frame-authority,transition-fact","remedy":"Carry one closed cancellation mode—retained scalar, retained frame, consumed scalar, or consumed frame—across nested wakeup invalidation. Restoration and nonce publication derive from that mode instead of one overloaded source-consumed flag or an order-sensitive boolean pair.","related":""} +{"timestamp":"2026-09-03T06:23:39Z","title":"Selected control mode mistaken for commit-time authority availability","summary":"Activation commit initially reloaded frame admission solely because the preflight-selected mode was frame, while scalar-fixture terminal activation was routed through a strict frame-validating close wrapper. Mode selects semantics but does not prove a primary remains readable across every placement-changing commit, and direct scalar transitions still require their supplied loaded snapshot.","scope":"actors/EXP-0025","status":"resolved","files":"template/pallets/actors/src/scheduler.rs,template/pallets/actors/src/tests/observations.rs","tags":"activation,frame-authority,frozen-admission,control-mode,terminal-close","remedy":"Carry the selected mode and any required frame admission in ActivationPlan from preflight to commit. Frame close consumes the frozen certificate; explicit scalar close consumes its loaded scalar snapshot directly instead of reopening the global strict projection.","related":"EXP-0025"} +{"timestamp":"2026-09-03T06:57:31Z","title":"Activation source mode mistaken for destination publication mode","summary":"Passing one scalar/frame flag through unified activation made ordinary direct scalar fixtures stop publishing their required C1 mirror, diverging from compact ObservationChange even though both returned Latched. Source authority selection and destination publication coincide in production and combined isolation but intentionally differ in ordinary differential tests.","scope":"actors/EXP-0025","status":"resolved","files":"template/pallets/actors/src/scheduler.rs,template/pallets/actors/src/tests/observations.rs","tags":"activation,transition-fact,source-authority,destination-publication,differential-test","remedy":"Capture source loading and destination publication as separate facts at the activation wrapper. Let ActivationPlan own publication and frozen frame admission; direct scalar atoms may load scalar state while ordinary tests still publish C1, and combined benchmark isolation selects scalar for both.","related":"EXP-0025"} +{"timestamp":"2026-09-03T12:08:06Z","title":"Frame Waiting coordinates mistaken for physical wakeup-pointer coordinates","summary":"C1 Waiting publication overwrote the already allocated physical WakeupPages pointer with the independent frame chunk page/slot. Small package fixtures accidentally aligned both namespaces, while the 512-Actor runtime mixed-clock test diverged after genesis tombstones and faulted every Tick occurrence.","scope":"actors/EXP-0025","status":"resolved","files":"template/pallets/actors/src/lib.rs,template/pallets/actors/src/scheduler.rs,template/runtime/src/tests/actors_integration_tests.rs","tags":"frame-authority,wakeup-pointer,address-space,runtime-integration,mixed-clock","remedy":"Treat ActorControlLocation as the frame address and ActorHot wakeup pointers as lightweight physical WakeupPages references. Preserve the allocated physical pointer through Waiting publication, match source consumption by validated clock/key, and test with populations where the two page/slot namespaces necessarily diverge.","related":"EXP-0025"} +{"timestamp":"2026-09-03T12:41:52Z","title":"Transaction-local scalar projection mistaken for retained differential authority","summary":"Frame-only temporal execution correctly used a temporary scalar hot projection after Ready source consumption, but cycle-nonce commit unconditionally recreated scalar identity and successful destination publication retained the temporary hot partition. Functional execution passed while the supposedly absent scalar control geometry silently returned.","scope":"actors/EXP-0025","status":"resolved","files":"template/pallets/actors/src/execution.rs,template/pallets/actors/src/scheduler.rs,template/pallets/actors/src/tests/wakeups.rs","tags":"frame-authority,scalar-differential,source-consumption,nonce,destination-publication","remedy":"Stable identity commit updates a surviving primary and an already existing differential only; source-consumed frame execution may create only the retained execution core's transaction-local hot bridge when no primary survives; frame-destination nonce commit never creates an absent scalar identity differential. The placement transaction must freeze pre-transition identity/hot/admission differential presence independently before source consumption, pass that typed fact explicitly instead of inferring from the temporary bridge, and remove transaction-local bridges after successful frame destination publication and state-hold reconciliation across every Trigger family. Loaded User execution must defer state-hold reconciliation while only a transaction-local hot projection exists, then reconcile from the surviving frame destination. Suspended retry now removes the transaction-local bridge after frame Waiting publication; exact window expiry freezes and consumes that complete Waiting source, then closes directly from supplied authority without transient Ready publication.","related":"EXP-0025"} +{"timestamp":"2026-09-03T14:01:04Z","title":"Multi-block test helper mistaken for a single-block Q1 witness","summary":"The partial-differential matrix initially used run_idle to assert a middle-Step state. That helper intentionally advances additional blocks while any scalar ActorHot remains Running, so hot-retained cases completed Step 1 before the assertion and appeared to violate one-Step-per-block execution while frame-only cases stopped after Step 0.","scope":"actors/EXP-0025","status":"resolved","files":"template/pallets/actors/src/tests.rs,template/pallets/actors/src/tests/scheduling.rs,template/pallets/actors/src/scheduler.rs","tags":"test-helper,q1,multi-step,scalar-differential,causal-order","remedy":"Use one explicit on_idle call for a single-block assertion, then advance block/on_initialize/prepass/on_idle deliberately. Independently guard service against a loaded Running head whose last_committed_step_block already equals now so Q1 does not depend only on test helpers or queue-cutoff geometry.","related":"EXP-0025"} +{"timestamp":"2026-09-03T16:59:39Z","title":"Exact-next retry mistaken for Block Waiting","summary":"A frame-only RetryLater witness initially expected cooldown-one retry to own a Block wakeup. Canonical scheduling intentionally keeps an exact N+1 retry in Ready with eligible_at=N+1; only later backoff or earlier terminal-window substitution belongs to Block Waiting.","scope":"actors/EXP-0025","status":"resolved","files":"template/pallets/actors/src/tests/execution.rs,template/pallets/actors/src/scheduler.rs","tags":"retry,ready,wakeup,q1,eligibility","remedy":"Assert no Block wakeup for exact-next retry, preserve the immutable Ready ticket and frame-owned Run authority, snapshot the next block cutoff in prepass, then execute at the eligible block. Test later backoff and terminal-window substitution separately through Block Waiting.","related":"EXP-0025"} +{"timestamp":"2026-09-03T17:53:56Z","title":"Retained Ready close mistaken for consumed-source finalization","summary":"Ready admission selected underfunded User apoptosis before queue consumption, but loaded finalization labeled every frame close as source-consumed and supplied no source state. Frame-only retry therefore could not cancel its live Run and left the Actor active while scalar-backed tests passed.","scope":"actors/EXP-0025","status":"resolved","files":"template/pallets/actors/src/lib.rs,template/pallets/actors/src/scheduler.rs,template/pallets/actors/src/tests/execution.rs","tags":"close,cancellation,ready,minimal-apoptosis,frame-authority","remedy":"Carry the closed cancellation mode into finalization. A Ready pre-consumption close must use RetainedFrame with complete loaded state/admission; a physically consumed Waiting close uses ConsumedFrame. Perform queue-owner removal only inside the retained close transaction and prove custody neutrality, fee absence, hold release, and scalar absence.","related":"EXP-0025"} +{"timestamp":"2026-09-03T18:07:35Z","title":"Due temporal placement failure mistaken for retained-source exhaustion close","summary":"The shared scheduler-index close was correctly changed to retain a live primary, then was incorrectly reused after due Tick consumption. Cadenced rearm exhaustion had already consumed its physical Waiting source, so retained reload could not close and left scalar identity behind.","scope":"actors/EXP-0025","status":"resolved","files":"template/pallets/actors/src/scheduler.rs,template/pallets/actors/src/tests/wakeups.rs","tags":"temporal,wakeup,close,scheduler-exhaustion,frame-authority","remedy":"Choose cancellation mode from actual source transition, never close reason. General placement exhaustion with a live primary uses the retained frame/scalar owner; due temporal placement or rearm failure uses ConsumedFrame with the frozen post-source state. Keep the scalar benchmark branch explicit.","related":"EXP-0025"} +{"timestamp":"2026-09-03T18:47:21Z","title":"Supplied frame wakeup transition reopened scalar invalidation","summary":"Observation future activation carried complete frame identity, hot state, admission, cursor, and resource authority but the authority-native wakeup helper still called the scalar-loading invalidator when replacing an existing terminal pointer. With scalar control removed, Unsignaled Block-201 to Waiting Block-10 placement failed as corrupted topology.","scope":"actors/EXP-0025","status":"resolved","files":"template/pallets/actors/src/lib.rs,template/pallets/actors/src/scheduler.rs,template/pallets/actors/src/tests/observations.rs","tags":"observation,wakeup,pointer,scalar-bridge,frame-authority","remedy":"Invalidate the validated physical pointer directly from supplied hot authority, clear the selected clock locally, commit that cleared hot state to any retained primary, then perform source-to-destination placement with supplied identity, cursor, admission, and resources. Preserve the compact scalar fixture by making admission optional outside frame mode.","related":"EXP-0025"} +{"timestamp":"2026-09-03T19:00:32Z","title":"Cadenced Opening rearm mistaken for a standalone frame destination","summary":"Replacing the loaded Cadenced Opening rearm bridge with immediate authority-native Waiting publication passed simple cadence tests but rolled back busy Running preservation. Rearm occurs inside the same consumed-Ready execution transaction whose successor must carry both service and Trigger authority; publishing an independent early destination changed that coupled persistence geometry.","scope":"actors/EXP-0025","status":"resolved","files":"template/pallets/actors/src/scheduler.rs,template/pallets/actors/src/tests/scheduling.rs","tags":"cadenced,rearm,execution,persistence,scalar-bridge","remedy":"Keep the transitional rearm bridge until direct frame execution owns the entire source-consume, Trigger-pointer update, Step commit, and final successor publication as one state transition. Do not promote rearm to a standalone destination before replacing the retained execution core.","related":"EXP-0025"} +{"timestamp":"2026-09-03T19:24:37Z","title":"Final scalar absence mistaken for no transient scalar materialization","summary":"Earlier frame-only execution witnesses checked that scalar hot was absent after completion, but Ready consumption still created a transaction-local hot bridge and cleanup removed it later. Final storage geometry alone did not prove single-owner mutation throughout the transaction.","scope":"actors/EXP-0025","status":"resolved","files":"template/pallets/actors/src/scheduler.rs,template/pallets/actors/src/tests/scheduling.rs","tags":"execution,zero-step,scalar-bridge,instrumentation,single-owner","remedy":"Instrument the exact Ready-consume materialization seam. Suppress materialization before direct frame execution, mutate frozen state locally, publish the sole destination, and require the counter to remain zero in addition to final scalar absence. Apply this proof separately to each execution family before deleting the bridge globally.","related":"EXP-0025"} +{"timestamp":"2026-09-03T19:41:22Z","title":"Coupled Trigger rearm mistaken for requiring a scalar execution bridge","summary":"Standalone early Cadenced Waiting publication was correctly rejected, but that did not imply Cadenced or ObservationCrossing zero-Step Opening required scalar hot. Both can mutate their physical Trigger topology and return a local successor hot projection inside the consumed-Ready transaction, leaving publication to the single final execution destination.","scope":"actors/EXP-0025","status":"resolved","files":"template/pallets/actors/src/scheduler.rs,template/pallets/actors/src/crossing.rs,template/pallets/actors/src/tests/wakeups.rs,template/pallets/actors/src/tests/crossing.rs","tags":"cadenced,crossing,rearm,zero-step,scalar-bridge,atomicity","remedy":"Separate Trigger-topology mutation from owner publication. Apply wakeup or membership replacement transaction-locally to frozen authority, return the updated hot projection, and publish only the final execution successor. Prove the materialization counter remains zero and try-state validates the retained topology.","related":"EXP-0025"} +{"timestamp":"2026-09-03T19:56:25Z","title":"Current-Step admission fee mistaken for resource-only maximum fee","summary":"The first direct StopCycle path initially compared the loaded plan fee with maximum_current_step_fee, but canonical execution validates the exact Step-aware maximum_current_action_fee. The resource-only envelope is not the admitted action-fee identity even when StopCycle has no economic effect.","scope":"actors/EXP-0025","status":"resolved","files":"template/pallets/actors/src/scheduler.rs,template/pallets/actors/src/execution.rs","tags":"execution,fees,step-plan,stop-cycle,admission","remedy":"Direct current-Step implementations must replay the canonical Step-aware admission check against the loaded Step and its resource certificate. Use resource-only maximums for broad reservation, never as a substitute for the admitted action-fee breakdown.","related":"EXP-0025"} +{"timestamp":"2026-09-03T20:06:08Z","title":"Running direct execution tempted to recapture Predicate state","summary":"A Running Step already owns its Opening predicate vector and cursor in the persisted Run. Re-evaluating against current chain state during direct frame execution would violate snapshot semantics even though the Step has no economic effect.","scope":"actors/EXP-0025","status":"resolved","files":"template/pallets/actors/src/scheduler.rs,template/pallets/actors/src/execution.rs","tags":"execution,running,predicate,snapshot,stop-cycle","remedy":"Opening direct execution captures bounded Predicate results once. Running and Suspended direct slices must consume the Run-owned snapshot and cursor, advance bounded outcomes, and never recapture current Predicate state.","related":"EXP-0025"} +{"timestamp":"2026-09-03T20:20:07Z","title":"One-Step Suspended core mistaken for a general continuation core","summary":"Promoting the candidate loaded-Step core and routing every Suspended RetryLater Step through it left a multi-Step continuation suspended: the Suspended branch intentionally admits only a one-Step Contract at cursor zero, while Running supports general cursors. The broad dispatcher selected a shape the core correctly rejected.","scope":"actors/EXP-0025","status":"resolved","files":"template/pallets/actors/src/scheduler.rs,template/pallets/actors/src/execution.rs,template/pallets/actors/src/tests/execution.rs","tags":"execution,suspended,retry,continuation,dispatch","remedy":"Route direct execution only across the core's proven semantic domain. Before widening Suspended support, generalize cursor validation, event indices, Predicate cursor advancement, continuation Run publication, and terminal behavior together, then add a multi-Step witness; never infer generality from a shared result type.","related":"EXP-0025"} +{"timestamp":"2026-09-03T20:46:19Z","title":"Non-execution Ready consumption mistaken for requiring scalar hot","summary":"Paused dequeue, stale Step-ticket repair, and admission Skip historically materialized scalar hot because they reused an execution-era consume helper. Each transition already carried complete frozen identity, hot, Run, admission, and resource authority and performed no Step effect.","scope":"actors/EXP-0025","status":"resolved","files":"template/pallets/actors/src/scheduler.rs,template/pallets/actors/src/tests/scheduling.rs","tags":"ready,paused,stale-ticket,admission,scalar-bridge","remedy":"Classify source consumption independently from execution. For non-execution Ready transitions, consume the immutable ticket without scalar materialization, publish the classified frame successor from supplied authority, reconcile the surviving User hold, and prove zero bridge writes plus no cycle events.","related":"EXP-0025"} +{"timestamp":"2026-09-03T21:09:35Z","title":"Opening outcome mistaken for derivable from Predicate truth","summary":"A direct Opening Step previously inferred the continuation's last outcome from Predicate truth after execution. ContinueNextStep proves that a true Predicate can still yield Executed, Failed, ResolutionSkipped, or FundingUnavailable, so Predicate truth is not an outcome owner.","scope":"actors/EXP-0025","status":"resolved","files":"template/pallets/actors/src/execution.rs,template/pallets/actors/src/tests/execution.rs","tags":"execution,opening,outcome,continue-next-step,predicate","remedy":"Carry the typed StepOutcome from the exact prepare/effect branch into Run publication. Derive counters and last_step_outcome from that branch, never from Predicate truth or effect-invocation alone, and witness frame-only failure-to-continuation with zero scalar materialization.","related":"EXP-0025"} +{"timestamp":"2026-09-03T21:34:14Z","title":"Retry terminal failure mistaken for resuspension invariant failure","summary":"The direct RetryLater core treated permanent adapter failure and a next attempt reaching max_attempts as impossible resuspensions. Returning an invariant would roll back consumed frame execution instead of publishing Failed/Idle or closing with RetryAttemptsExhausted.","scope":"actors/EXP-0025","status":"resolved","files":"template/pallets/actors/src/execution.rs,template/pallets/actors/src/scheduler.rs,template/pallets/actors/src/tests/execution.rs","tags":"retry,exhaustion,permanent-failure,close-provenance,frame-control","remedy":"Classify terminal retry before successor placement. Permanent failure commits Failed/Idle below the global threshold; local max-attempt exhaustion closes from consumed authority as RetryAttemptsExhausted; a simultaneous global streak limit closes as ConsecutiveFailures. Never route a terminal retry through suspension eligibility.","related":"EXP-0025"} +{"timestamp":"2026-09-03T21:42:35Z","title":"Frame direct execution mistaken for an incremental scalar-fixture cutover","summary":"Allowing direct loaded-Step execution whenever a frame primary existed, even when a retained scalar hot differential also existed, switched twenty scalar-oracle and combined-fixture tests to Q1 frame semantics before their whole retained-tree substitution. Snapshot cardinality, same-pass suffix execution, FundingUnavailable events, and lifecycle timing diverged together.","scope":"actors/EXP-0025","status":"resolved","files":"template/pallets/actors/src/scheduler.rs,template/pallets/actors/src/tests/execution.rs,template/pallets/actors/src/tests/fees_and_funding.rs","tags":"cutover,scalar-fixtures,q1,atomicity,differential","remedy":"Keep direct frame execution gated on absent scalar hot while combined fixtures remain intentional scalar authority. Remove that gate only in the same retained-tree substitution that rewrites scalar-oracle fixtures and deletes stale coexisting differentials; do not incrementally reinterpret mixed representation as frame authority.","related":"EXP-0025"} +{"timestamp":"2026-09-03T22:05:00Z","title":"Mutation authority mistaken for retained cancellation authority","summary":"Contract replacement classified mixed scalar-differential fixtures as scalar for in-place mutation, then reused that mode for retained Run cancellation. Four semantic-control tests failed because the same Actors still had canonical frame primaries and cancellation had historically loaded that retained frame authority.","scope":"actors/EXP-0025","status":"resolved","files":"template/pallets/actors/src/lib.rs,template/pallets/actors/src/tests/execution.rs,template/pallets/actors/src/tests/lifecycle.rs,template/pallets/actors/src/tests/scheduling.rs","tags":"contract-replacement,cancellation,mixed-representation,authority-mode,atomic-cutover","remedy":"Keep Trigger/mutation authority and retained cancellation selection separate while scalar differentials coexist with frame primaries. Unify them only in the atomic retained-tree fixture substitution that removes mixed authority; do not infer cancellation source from a narrower in-place mutation gate.","related":"EXP-0025"} +{"timestamp":"2026-09-04T01:40:30Z","title":"Frame locator presence mistaken for valid surviving primary authority","summary":"A typed state-hold helper classified frame authority from ActorControlLocators presence alone. Terminal Ready, wakeup-expiry, and Contract-replacement paths can retain a locator while the primary is consumed, malformed, or no longer readable, so four close paths failed or left the Actor active.","scope":"actors/EXP-0025","status":"resolved","files":"template/pallets/actors/src/scheduler.rs,template/pallets/actors/src/execution.rs,template/pallets/actors/src/lib.rs","tags":"state-hold,frame-authority,locator,primary-validation,finalization","remedy":"Select frame state-hold authority only when load_frame_control_authority validates a surviving primary. Locator presence is topology input, not proof of readable authority; consumed or invalid destinations must fall back to the explicit scalar/closed cleanup semantics.","related":"EXP-0025"} +{"timestamp":"2026-09-04T04:20:00Z","title":"Candidate wakeup coordinate compatibility removed before fixture cutover","summary":"Removing the ScalarFixtureIsolation coordinate-alignment capability made two combined runtime-benchmark candidate transitions accept stale pre-positioned physical pointers instead of rejecting them. Due-page and Crossing rollback witnesses changed from typed invariant failure to successful movement.","scope":"actors/EXP-0025","status":"resolved","files":"template/pallets/actors/src/lib.rs,template/pallets/actors/src/scheduler.rs,template/pallets/actors/src/tests/candidate_geometry.rs","tags":"wakeup,coordinates,scalar-fixtures,atomic-cutover,rollback","remedy":"Retain the explicit aligned-coordinate capability for combined scalar fixtures until their stale primaries and placement differentials are atomically rewritten. Frame and physical address spaces remain independent, but a fixture-supplied physical pointer must still identify its actual physical slot; do not broaden acceptance as an incremental cutover.","related":"EXP-0025"} +{"timestamp":"2026-09-04T22:12:10Z","severity":"FATAL","rule":"O(N) Iteration Anti-Pattern","files":"/home/llb/Repos/deos/template/pallets/actors/src/scheduler.rs"} diff --git a/.agents/skills/alignment/scripts/audit-protocol-coherence-regressions.sh b/.agents/skills/alignment/scripts/audit-protocol-coherence-regressions.sh index 8956f072..bc7c76bf 100755 --- a/.agents/skills/alignment/scripts/audit-protocol-coherence-regressions.sh +++ b/.agents/skills/alignment/scripts/audit-protocol-coherence-regressions.sh @@ -215,7 +215,7 @@ run_normative_drift_fixtures() { exec {FIXTURE_LOCK_FD}>"$PROJECT_ROOT/.git/deos-protocol-coherence-mutation.lock" acquire_fixture_lock - local plural_path condition_path duplicate_path soft_path phase_path staking_path retry_path near_miss_path + local plural_path condition_path duplicate_path soft_path phase_path staking_path retry_path near_miss_path bldr_anchor_path new_fixture "$TEMPLATE_DIR/pallets/actors/src" plural rs; plural_path="$NEW_FIXTURE_PATH" new_fixture "$TEMPLATE_DIR/pallets/actors/docs" condition md; condition_path="$NEW_FIXTURE_PATH" new_fixture "$TEMPLATE_DIR/primitives/src" duplicate rs; duplicate_path="$NEW_FIXTURE_PATH" @@ -224,6 +224,7 @@ run_normative_drift_fixtures() { new_fixture "$TEMPLATE_DIR/pallets/staking/src" staking-view rs; staking_path="$NEW_FIXTURE_PATH" new_fixture "$PROJECT_ROOT/web-client/scripts" router-retry mjs; retry_path="$NEW_FIXTURE_PATH" new_fixture "$PROJECT_ROOT/web-client/scripts" plural-near-miss mjs; near_miss_path="$NEW_FIXTURE_PATH" + new_fixture "$TEMPLATE_DIR/primitives/src" bldr-anchor rs; bldr_anchor_path="$NEW_FIXTURE_PATH" pause_fixture_test_if_requested post-lock require_legitimate_fixture_pass "$plural_path" "One optional Precondition gates this Step." "$output" @@ -234,6 +235,7 @@ run_normative_drift_fixtures() { require_legitimate_fixture_pass "$staking_path" "fn native_security_view() -> NativeSecurityView;" "$output" require_legitimate_fixture_pass "$retry_path" "classifyRouterRetry(failure.retryDisposition, error);" "$output" require_legitimate_fixture_pass "$near_miss_path" "One optional Precondition remains singular." "$output" + require_legitimate_fixture_pass "$bldr_anchor_path" "pub const BLDR_ANCHOR_ACTORS_ID: u64 = 12;" "$output" require_mutation_failure "Plural Precondition" "$plural_path" \ "Optional Preconditions gate this Step." \ @@ -259,12 +261,15 @@ run_normative_drift_fixtures() { require_mutation_failure "Plural Precondition near miss" "$near_miss_path" \ "const entrySummary = 'Unrelated Preconditions prose';" \ "Plural Preconditions terminology reintroduced" "$output" + require_mutation_failure "Retired BLDR bucket-qualified anchor" "$bldr_anchor_path" \ + "pub const BLDR_BUCKET_A_ACTORS_ID: u64 = 12;" \ + "Retired BLDR bucket-qualified anchor identity reintroduced" "$output" remove_fixture "$output" cleanup_fixtures flock -u "$FIXTURE_LOCK_FD" trap - EXIT INT TERM HUP - log_success "Seven repository-behavior mutation families and the plural-exception near miss failed closed; exact detector lines, legitimate fixtures, and cleanup passed" + log_success "Eight repository-behavior mutation families and the plural-exception near miss failed closed; exact detector lines, legitimate fixtures, and cleanup passed" } reject_pattern() { @@ -722,13 +727,13 @@ main() { "Actors Step failure no longer preserves concrete cause and retry disposition" require_anchor 'pub enum AttemptDisposition' "$actors_lifecycle_types" \ "Actors canonical attempt disposition is missing" - require_anchor 'fn resolve_step_control\(outcome: &StepOutcome' \ + require_anchor 'fn execute_loaded_single_step_core\(' \ "$TEMPLATE_DIR/pallets/actors/src/execution.rs" \ "Actors Step policy interpreter no longer consumes the canonical outcome" - require_anchor 'status: attempt.disposition' "$TEMPLATE_DIR/pallets/actors/src/execution.rs" \ + require_anchor 'status: attempt.status' "$TEMPLATE_DIR/pallets/actors/src/scheduler.rs" \ "Actors simulation no longer returns production finalization disposition" - require_anchor 'cumulative_outcomes: attempt.outcomes' \ - "$TEMPLATE_DIR/pallets/actors/src/execution.rs" \ + require_anchor 'cumulative_outcomes: attempt.cumulative_outcomes' \ + "$TEMPLATE_DIR/pallets/actors/src/scheduler.rs" \ "Actors simulation reconstructed counters outside production finalization" require_anchor 'run_simulation_preserves_retry_position_and_committed_state' \ "$actors_tests" "Actors shared failure-cause simulation evidence is missing" @@ -782,13 +787,8 @@ main() { reject_pattern '\bconsecutive_failures\b' \ "Retired ambiguous Actors failure-streak field reintroduced" \ "$TEMPLATE_DIR" "$PROJECT_ROOT/docs" "$PROJECT_ROOT/wiki" "$PROJECT_ROOT/web-client" - require_anchor 'fn predicate_evaluation_weight' "$TEMPLATE_DIR/pallets/actors/src/execution.rs" \ - "Actors benchmark-domain predicate Weight chunking is missing" - require_anchor 'generated_predicate_weight_scales_and_chunks_opening_plus_step_visits' "$actors_tests" \ - "Actors dual-phase predicate Weight evidence is missing" - 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 'generated_predicate_weight_scales_with_bounded_step_visits' "$actors_tests" \ + "Actors bounded predicate Weight evidence is missing" reject_pattern 'BalanceExhausted|FeeBudgetExhausted' \ "Actors active runtime/client surfaces revived retired mid-pipeline economic close reasons" \ "$TEMPLATE_DIR/pallets/actors/src" \ @@ -834,9 +834,9 @@ main() { 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' \ + require_anchor 'latched_at_time_replacement_does_not_retrigger_running_pipeline' \ "$TEMPLATE_DIR/pallets/actors/src/tests/scheduling.rs" \ - "Actors busy AtTime occurrence evidence is missing" + "Actors public AtTime replacement and no-retrigger 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" @@ -900,7 +900,7 @@ main() { "$actors_tests" "Actors single global FIFO evidence is missing" require_anchor 'on_idle_fanout_feeds_the_existing_scheduler_without_direct_execution' \ "$actors_tests" "Actors fanout-to-canonical-scheduler evidence is missing" - require_anchor '`resolve_step_control` remains the only runtime transition owner' \ + require_anchor 'Production and simulation share current-Step transition owners' \ "$TEMPLATE_DIR/pallets/actors/docs/architecture.en.md" \ "Actors single step/lifecycle transition owner is missing" reject_pattern '\b(?:PriorityQueue|PriorityLane|ReadinessCache|RetryQueue|AlternateRetryOwner|SecondaryLifecycleClassifier)\b' \ @@ -947,8 +947,35 @@ main() { reject_pattern '\b(?:fn|pub fn)\s+[a-z][a-z0-9_]+\s*\(' \ "Exhaustive implementation symbol reintroduced into core composition architecture" \ "$core_architecture" - local specification_docs=("$PROJECT_ROOT/docs/tmctol.specification.en.md") + local tmctol_spec="$PROJECT_ROOT/docs/tmctol.specification.en.md" + local builder_contract="$PROJECT_ROOT/docs/builder-economy.contract.en.md" + local specification_docs=("$tmctol_spec") local architecture_docs=("$core_architecture") + require_anchor 'tmctol\.specification\.en\.md' "$builder_contract" \ + "Builder Economy contract no longer depends on the TMCTOL specification" + require_anchor 'template/pallets/governance/docs/specification\.en\.md' "$builder_contract" \ + "Builder Economy contract no longer depends on the Governance specification" + require_anchor 'owns the cross-system composition' "$builder_contract" \ + "Builder Economy contract no longer declares its composition ownership" + require_anchor 'Governance specification.s `BaseFloorCapped` invoice-settlement policy' \ + "$builder_contract" "Builder Economy contract lost its BaseFloorCapped policy selection" + require_anchor 'Mutable System Actor treasury' "$builder_contract" \ + "Builder Economy contract lost Mutable System Actor treasury ownership" + require_anchor 'canonical CIDv1 string' \ + "$TEMPLATE_DIR/pallets/governance/docs/specification.en.md" \ + "Governance specification lost content-addressed invoice identity" + require_anchor 'spendable amount < minimum payable amount' \ + "$TEMPLATE_DIR/pallets/governance/docs/specification.en.md" \ + "Governance specification lost below-floor capacity rejection" + require_anchor 'actual amount = min\(target amount, spendable amount\)' \ + "$TEMPLATE_DIR/pallets/governance/docs/specification.en.md" \ + "Governance specification lost bounded above-base clipping" + require_anchor 'canonical invoice settlement policy is `BaseFloorCapped`' \ + "$TEMPLATE_DIR/pallets/governance/docs/specification.en.md" \ + "Governance specification lost the canonical invoice settlement policy owner" + require_anchor 'builder-economy\.contract\.en\.md' \ + "$PROJECT_ROOT/docs/framework-instance.contract.en.md" \ + "Framework/instance contract no longer routes Builder composition to its owner" local subsystem for subsystem in governance staking actors router oracle; do specification_docs+=("$TEMPLATE_DIR/pallets/$subsystem/docs/specification.en.md") @@ -976,6 +1003,9 @@ main() { reject_pattern '(?:\*\*Status\*\*:\s*Target Contract|Implementation status|shipped-runtime divergence)' \ "Implementation-status narration reintroduced into a normative specification" \ "${specification_docs[@]}" + reject_pattern '(?:\$[A-Z][A-Z0-9]*\b|\b(?:DEOS|BLDR|NTVE|Actors?)\b|[Ii]nvoice)' \ + "Project-specific token, Actor, or invoice policy reintroduced into the TMCTOL standard" \ + "$tmctol_spec" local integration_doc for integration_doc in "$PROJECT_ROOT"/docs/*.integration.en.md; do architecture_docs+=("$integration_doc") @@ -1017,6 +1047,10 @@ main() { reject_pattern '(?i)\b(?:actor[- ]programs?|execution[- ]plans?|cycle[- ]plans?)\b' \ "Parallel current Actor Contract or Step terminology reintroduced" \ "${terminology_surfaces[@]}" + reject_pattern 'BLDR_BUCKET_A_ACTORS_ID|BLDR Bucket A|BLDR `Bucket A`|bldr_bucket_a|AnchorBucketState|anchor_bucket_state' \ + "Retired BLDR bucket-qualified anchor identity reintroduced" \ + "$TEMPLATE_DIR/primitives" "$TEMPLATE_DIR/runtime/src" "$PROJECT_ROOT/docs" \ + "$PROJECT_ROOT/simulator" "$PROJECT_ROOT/web-client/src" "$PROJECT_ROOT/wiki" reject_pattern_except "$plural_preconditions_pattern" "$plural_detector_exception_pattern" \ "Plural Preconditions terminology reintroduced where singular Precondition owns the optional DNF" \ "${required_family_surfaces[@]}" diff --git a/.agents/skills/alignment/scripts/completion-gate.sh b/.agents/skills/alignment/scripts/completion-gate.sh index 3ddbd601..17f86bf7 100755 --- a/.agents/skills/alignment/scripts/completion-gate.sh +++ b/.agents/skills/alignment/scripts/completion-gate.sh @@ -22,6 +22,7 @@ Diff-aware DEOS completion gate for local delivery slices. It validates the smallest meaningful scope for the current pass and blocks the next loop until the touched layer is green. Options: + --self-test Check audit routing without running project validation --all-rust Run architecture audit against the full pallet tree --skip-simulator Do not run simulator validation --skip-cargo-check Do not run cargo check validation @@ -186,6 +187,7 @@ should_run_markdown_table_audit() { should_run_architecture_readability_audit() { has_changed_path '^docs/.*\.architecture\.en\.md$' || \ + has_changed_path '^template/pallets/[^/]+/docs/architecture\.en\.md$' || \ has_changed_path '^AGENTS\.md$' || \ has_changed_path '^\.agents/skills/alignment/scripts/audit-architecture-readability\.sh$' } @@ -195,8 +197,41 @@ should_run_release_line_audit() { } should_run_economic_claim_audit() { - has_changed_path '^\.agents/skills/alignment/economic-claims\.json$' || has_changed_path '^docs/.*\.architecture\.en\.md$' -} + has_changed_path '^\.agents/skills/alignment/economic-claims\.json$' || \ + has_changed_path '^docs/.*\.architecture\.en\.md$' || \ + has_changed_path '^template/pallets/[^/]+/docs/architecture\.en\.md$' +} + +run_self_test() ( + local path expected_readability expected_economic readability economic + while IFS='|' read -r path expected_readability expected_economic; do + CHANGED_PATHS=("$path") + readability=0 + economic=0 + if should_run_architecture_readability_audit; then readability=1; fi + if should_run_economic_claim_audit; then economic=1; fi + if [[ "$readability" != "$expected_readability" || "$economic" != "$expected_economic" ]]; then + log_error "Audit routing mismatch for $path: readability=$readability economic=$economic" + return 1 + fi + done <<'CASES' +docs/core.architecture.en.md|1|1 +template/pallets/actors/docs/architecture.en.md|1|1 +template/pallets/router/docs/architecture.en.md|1|1 +AGENTS.md|1|0 +.agents/skills/alignment/scripts/audit-architecture-readability.sh|1|0 +.agents/skills/alignment/economic-claims.json|0|1 +template/pallets/actors/docs/specification.en.md|0|0 +template/pallets/actors/src/execution.rs|0|0 +docs/README.md|0|0 +CASES + CHANGED_PATHS=() + if should_run_architecture_readability_audit || should_run_economic_claim_audit; then + log_error "Empty diff unexpectedly schedules architecture audits" + return 1 + fi + log_success "Completion audit routing self-test passed" +) should_run_backlog_audit() { has_changed_path '^BACKLOG\.md$' @@ -448,6 +483,7 @@ run_knowledge_sync() { main() { parse_args "$@" + run_self_test check_prerequisites plan run_architecture_audit @@ -469,6 +505,10 @@ main() { } run_entrypoint() { + if [[ $# == 1 && "$1" == "--self-test" ]]; then + run_self_test + return + fi if [[ "${1:-}" == "--internal" ]]; then shift main "$@" diff --git a/.agents/skills/architecture-experiments/SKILL.md b/.agents/skills/architecture-experiments/SKILL.md index d47da1e9..d24d33db 100644 --- a/.agents/skills/architecture-experiments/SKILL.md +++ b/.agents/skills/architecture-experiments/SKILL.md @@ -28,7 +28,7 @@ It does not own: 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. +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/`. Numeric IDs are globally unique across tracks; track-qualified paths retain explicit evidence ownership. 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. @@ -65,7 +65,11 @@ A faster candidate that changes externally observable semantics, deterministic b ## 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. +Use globally sequential IDs `EXP-NNNN`, zero-padded to four digits, across every track. A numeric ID has exactly one live owner and never encodes a track or release; the canonical ownership path remains `/EXP-NNNN`. Ordinary allocation takes one plus the highest current ID or unqualified reserved former ID across all track indexes and records, including index-only Proposed questions. Never restart numbering for a new track or fill gaps through ordinary allocation. Resolve concurrent allocation collisions before merge; the track indexes remain the sole navigation owners, with no duplicate global registry. + +Identity seals on the first Accepted, Rejected, Inconclusive, Superseded, or Invalidated decision. Sealed IDs MUST NOT be renumbered, including an Inconclusive record later reopened as Proposed. Proposed, Prepared, Measuring, and Measured identities may be normalized only through an explicit graph migration that improves proof/dependency ordering, atomically rewrites every repository reference, preserves former identity in `Former IDs`, prevents ambiguous reuse of vacated numbers, and retains an inspectable Git baseline. Interpreted records are not eligible for renumbering. An explicit provisional-number compaction may reuse a vacated navigation number only when every former occurrence is qualified as `/EXP-NNNN@`, the manifest binds that historical identity to its current owner, and all live references resolve to the new canonical record. Unqualified former numbers remain reserved globally across tracks. Sealed identities are never eligible for this compaction. A renumber changes navigation, never evidence identity, semantics, numbers, or historical decisions. No duplicate alias files remain. Cross-track identity moves obey the same restrictions. + +Before migration, record exact content hashes and modes of every existing file to be structurally migrated, its status and the Git baseline. During migration, a Skill-private manifest owns this provenance, not another graph or evidence archive. Record the former-to-current mapping and evidence extraction coverage. The validator checks the baseline identities, sealed-ID stability and absence of live former references; only explicit `Former IDs` fields or clearly labelled former-ID historical prose may retain those names. After the migration and manifest are committed and validated, retire the completed working-tree manifest when its exact full Git commit and repository path remain cited in the affected records or owning index, so hashes, mapping and extraction coverage stay retrievable. Git then owns the completed migration provenance; no live manifest or migration-only validator machinery is required for ordinary record maintenance. Preserve primary experimental evidence in its live record regardless of manifest retirement. ```text .agents/skills/architecture-experiments/ @@ -88,16 +92,64 @@ Use track-local monotonic IDs `EXP-NNNN`, zero-padded to four digits. The canoni └ 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. +Every experiment record is copied from `templates/EXP-NNNN.md`. A Proposed question MAY exist only as a row in its track index; create the record before 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 its Leaf. CSV and TSV are tabular evidence, not standalone artifacts: convert rows into Markdown tables in the owning Leaf and delete the temporary delimited file. Create a sibling `EXP-NNNN/` directory only for non-tabular raw evidence whose fidelity, machine consumption, or reviewability prevents faithful inline retention; filenames describe the 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 +Each track's `experiments.md` is its sole graph projection and entrypoint, including its conditional portfolio. Do not create separate global maps, measurement archives, or duplicated full tables to conceal a compound record. + +## Proof Graph Nodes + +Every Prepared-or-later record declares `Record kind` as exactly `Leaf` or `Synthesis`; declare it for Proposed records when their boundary is known. Records are proof graph nodes, not research notebooks. + +- `Leaf`: Own exactly one materially distinct falsifiable claim, one physical mechanism/owner, changed variable, evidence domain and acceptance criterion. Multiple workloads are permitted only as bounded witnesses of that same claim. Own primary measurements, preserve their limitations, and terminate when decided. +- `Synthesis`: Own a broader decision through a finite child-obligation map, child decisions, composition logic and final interpretation/decision. Never introduce a new candidate implementation, raw benchmark sweep, independently measured branch, or chronological debugging transcript. Measurements contains child-decision pointers only; no primary raw benchmark tables anywhere in the record. +- `Independence test`: Does new work test the same claim under the same changed mechanism, owner, domain and acceptance criterion? If yes, continue the Leaf; otherwise find or allocate the owning Leaf and transfer the question. A stricter witness stays only when all five boundaries are unchanged. +- `Mandatory fission`: A separately decidable or reusable mechanism, Weight owner, production selector, reachable-state domain, host assumption, parameter domain, evidence class, acceptance criterion, changed variable or artifact-identity question requires its own Leaf. Split when that result could independently be Accepted, Rejected, Inconclusive or Invalidated without deciding the parent. A result reused by multiple downstream decisions normally needs its own identity. +- `No speculative nodes`: An ordinary correctness bug does not automatically earn an EXP. Allocate only when it exposes a physical choice, invalidates a load-bearing assumption, or creates a reusable independent proof question. + +## Proof Obligation Freeze + +Before Prepared, enumerate a finite `Proof Obligations` table: Obligation ID, Claim, Smallest falsifier, Evidence class, Owning Experiment, Required/conditional, Downstream consequence, Status. A Leaf has one owning claim with bounded witnesses; a Synthesis has the complete finite child-obligation map and every mandatory row names its experiment owner. Conditional rows name the condition that would make them necessary; they are not silent release gates. Satisfied is an evidence decision with scope, not a synonym for a passing fixture. + +Record `Freeze`, `Review triggers`, and `Decomposition review` in that section. Freeze the obligation set at Measuring. Migration may reconstruct the previously implicit set only with explicit pre-fission provenance; it must not pretend the historical freeze occurred earlier. Discovering a materially independent obligation after freeze permits only the smallest evidence needed to classify independence, then find/create the child, add the graph edge, transfer the question and stop investigating it in the parent. Updating a child/status pointer preserves the frozen claim; adding an independent claim does not. + +Mandatory decomposition review triggers are more than six independent third-level Measurement proof subsections, a new mandatory obligation after Measuring began, measuring a new Weight owner, investigating a new reachable-state domain beyond its smallest falsifier, introducing a new production selector, or reusing a result in multiple downstream decisions. Declare any such event in `Review triggers`, including events not inferable mechanically. `Decomposition review` records the boundary decision and transfer destination or why bounded witnesses still share exactly one claim. A review cannot waive mandatory fission for an independent claim. Large byte/line counts emit nonfatal diagnostics only; size never decides whether to split. + +## Active Record Fission + +For an active compound Measuring record, preserve exact evidence before editing, then apply this sequence: + +1. Retain the original broad question as Synthesis and freeze its finite obligations with migration provenance. +2. Partition evidence by independently decidable claim and actual owner, not mechanically by subsection or benchmark name. +3. Allocate Leaf owners for those questions and move detailed decision evidence into them, without copying full tables into both parent and child. +4. Preserve every original measurement number, command, source/Wasm/output hash, rejection and evidence limitation. Each Leaf explicitly names the pre-fission parent subsection and baseline; exact extraction coverage stays inspectable. +5. Replace parent detail with compact provenance and child decisions. Add reciprocal decomposition, obligation ownership, hard dependencies where actually required, and evidence-flow relations. +6. Normalize eligible IDs only after partitioning, retain former-ID provenance and atomically rewrite repository navigation, then validate all three graph projections. +7. Continue only in the Leaf owning the current unanswered proof. Reuse extracted evidence without measurement when the measured implementation and exact identity remain unchanged and its evidence class still applies; otherwise mark the affected proof as requiring refresh. + +Evidence relocation generates no new runtime artifact, production acceptance, or historical continuity. Do not reopen a frozen architectural decision or regenerate a frozen semantic oracle merely because the graph changes. + +## Correction Boundary + +Experiments preserve causal decision evidence; Git owns ordinary implementation chronology, tests own regression behavior, and BACKLOG owns unfinished work. Classify invalid setup, test expectation defects, clock mistakes, unsupported host assumptions and ordinary implementation bugs. If no reusable architectural knowledge results, fix code/test, rerun the smallest affected proof and keep only a compact note: `Invalid probe: assumption X failed because Y. No measurement retained. Corrected fixture uses Z.` Preserve any decision-relevant rejection, exact artifact identity and causal limitation during migration; compress routine repair chronology through inspectable Git/test provenance, not silent evidence deletion. A reusable failed host assumption may deserve a Leaf; an ordinary repair transcript does not. + +## 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. +Relations follow the template's ordered fields and expose three distinct graphs: + +- `Decomposition`: `Parent question` metadata and reciprocal `Decomposes into` relation connect a broad question to child proofs; `Satisfies obligation` identifies the parent's obligation ID. This graph has no self/cyclic decomposition and does not imply prerequisite ordering. +- `Hard dependencies`: `Depends on` names required accepted input to a downstream decision and remains a DAG. A child need not depend on its parent; a parent may depend on the children's completed proofs. +- `Evidence flow`: `Uses evidence from`, `Produces input for`, `Confirms` and `Invalidates` express scoped reuse or impact, not prerequisite ordering. `Refines`, `Supersedes` and `Contradicts` preserve decision scope; `Transfers question to` names the next owner. None of these silently creates a hard dependency. + +Declare `Reopen trigger` explicitly. Every important claim leads through its owning experiment to exact evidence and source/artifact identity, and every decision exposes downstream consequences. Use None when absent rather than inventing causality. + +The validator checks shape/kinds/obligation owners, reciprocal parent-child/obligation relations, hard and decomposition cycles, global numeric uniqueness (including index-only Proposed questions), sealed and former identity rules, prohibited Synthesis primary evidence and Leaf review conditions. It verifies visually separate decomposition, hard-dependency and evidence-flow projections inside the track index. After relation changes use `--write-index`, then ordinary validation; `--self-test` exercises positive and negative methodology fixtures. It remains Skill-private, never a runtime/project build dependency. Index-only Proposed IDs remain valid references without an inferred decision. + ## 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. @@ -114,7 +166,7 @@ Each track owns one `tracks//experiments.md` charter/index and 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. +Cross-track hard dependencies must be directional and acyclic. Split a cyclic question at its evidence boundary or choose the track owning the changed mechanism. Update the index when track boundary, baseline, portfolio, lifecycle, status or relations change. Cross-track moves change identity and obey the provisional/sealed migration rules; sealed questions transfer through a new related record. ## Lifecycle @@ -133,6 +185,8 @@ Cross-track dependencies must be directional and acyclic. When a proposed experi 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. +Accepted may be scoped to physical architecture: state `Decision scope: physical architecture only` and distinguish Architecture Freeze from later production/release Geometry Freeze. One materially distinct hypothesis has one experiment owner. A scoped acceptance neither proves Weight soundness nor meets a throughput target. Frozen architecture may reopen only under its declared evidence-backed invariant/impossibility trigger; a cost miss, stale artifact or unreachable fixture transfers to the relevant successor. Corrections preserving frozen invariants belong to that successor, and micro-optimizations require a measured production owner plus a dedicated falsifiable question. + ## When to Open an Experiment Open one when all are true: @@ -156,7 +210,7 @@ Before opening, search the index by affected domain, mechanism, question, candid ### 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. +State one decision, record kind, owning implementation phase, governing specification sections, finite proof obligations and semantic invariants. Separate physical variables from semantic constants. A Synthesis declares its child decisions instead of preparing benchmark candidates; measurement protocol steps apply to the owning Leaves. 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. @@ -219,7 +273,7 @@ A host timing, count ceiling, diagnostic run, or `Weight::MAX` test does not est ### 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. +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 evidence stays with its owning track and record. 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. @@ -301,6 +355,10 @@ Domain Architecture may close only when every significant physical decision is t Stop the experiment loop when any applies: - The acceptance target is met. +- The named proof obligation is satisfied: stop measuring it. +- A new independent question is discovered: transfer it and stop the parent investigation. +- The next useful work belongs to another owner/domain: stop this experiment. +- A child result is sufficient for synthesis: do not seek more branch coverage for completeness. - No candidate has a plausible decision-relevant advantage. - Remaining delta is below declared materiality. - The bottleneck moved to another architecture domain. @@ -308,7 +366,7 @@ Stop the experiment loop when any applies: - 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. +“There might be another corner case” is insufficient. A new corner case must name an uncovered owner, domain or acceptance claim. Proof complexity grows by adding earned graph nodes, not by extending one notebook indefinitely. Performance never outranks correctness, deterministic semantics, atomicity, FIFO, causal speed, ownership, rollback, runtime safety, or production Weight soundness. ## Track Registry @@ -320,7 +378,7 @@ Track indexes are the canonical navigation surfaces for experiment status and li | 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. +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 register globally allocated IDs, own statuses and relations, and preserve rejected or invalidated records. ## Handoff diff --git a/.agents/skills/architecture-experiments/scripts/record-normalization.mjs b/.agents/skills/architecture-experiments/scripts/record-normalization.mjs new file mode 100644 index 00000000..920d4600 --- /dev/null +++ b/.agents/skills/architecture-experiments/scripts/record-normalization.mjs @@ -0,0 +1,367 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { createHash } from 'node:crypto'; +import { execFileSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; + +const sealed = new Set(['Accepted', 'Rejected', 'Inconclusive', 'Superseded', 'Invalidated']); +const provisional = new Set(['Proposed', 'Prepared', 'Measuring', 'Measured']); +const statuses = new Set([...sealed, ...provisional, 'Interpreted']); +const proofFields = ['Obligation ID', 'Claim', 'Smallest falsifier', 'Evidence class', 'Owning Experiment', 'Required/conditional', 'Downstream consequence', 'Status']; +const graphPattern = /[\s\S]*?/; +const hash = (value) => createHash('sha256').update(value).digest('hex'); +const cells = (line) => line.split(/(? s.trim()); +const fields = (s) => [...(s.match(/^\| Field \| Value \|\n\| --- \| --- \|\n((?:\|.*\n)+)/m)?.[1] ?? '').matchAll(/^\| ([^|]+) \| (.*) \|$/gm)].map((m) => [m[1].trim(), m[2].trim()]); +const section = (s, h) => s.split(`\n## ${h}\n`)[1]?.split(/\n## /)[0]?.trim() ?? ''; +const relations = (s) => [...section(s, 'Relations').matchAll(/^- `([^`]+)`: *(.*)$/gm)].map((m) => [m[1], m[2].trim()]); +const headings = (s) => [...s.matchAll(/^## ([^#].*)$/gm)].map((m) => m[1].trim()); +const note = (s, name) => s.split('\n').find((l) => l.startsWith(`- \`${name}\`:`))?.split('`:').slice(1).join('`:').trim() ?? ''; +function walk(dir) { + if (!fs.existsSync(dir)) return []; + return fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name)).flatMap((e) => { + const p = path.join(dir, e.name); + return e.isDirectory() ? walk(p) : e.isFile() ? [p] : []; + }); +} + +export function validate(skillDir, { writeIndex = false, repoFiles, gitRead } = {}) { + const errors = [], warnings = [], nodes = new Map(), indexes = new Map(); + const template = fs.readFileSync(path.join(skillDir, 'templates/EXP-NNNN.md'), 'utf8'); + const root = path.resolve(skillDir, '../../..'); + const fail = (key, message) => errors.push(`${key}: ${message}`); + const compare = (key, label, a, b) => { + if (JSON.stringify(a) !== JSON.stringify(b)) fail(key, `${label} differ from template (${b.join(' | ')})`); + }; + const files = walk(path.join(skillDir, 'tracks')); + for (const file of files) { + if (/\.(csv|tsv)$/i.test(file)) fail(file, 'CSV/TSV evidence belongs inline in its Leaf'); + if (path.basename(file) === 'experiments.md') indexes.set(path.basename(path.dirname(file)), { file, source: fs.readFileSync(file, 'utf8') }); + } + for (const file of files.filter((p) => /\/[^/]+\/EXP-\d{4}\.md$/.test(p))) { + const source = fs.readFileSync(file, 'utf8'), track = path.basename(path.dirname(file)), id = path.basename(file, '.md'); + const meta = new Map(fields(source)), rel = new Map(relations(source)), key = `${track}/${id}`; + const proofs = section(source, 'Proof Obligations'), rows = proofs.split('\n').filter((l) => l.startsWith('|')); + const obligations = rows.slice(2).map(cells), kind = meta.get('Record kind'); + const node = { key, track, id, file, source, meta, rel, obligations }; + nodes.set(key, node); + compare(key, 'metadata fields', fields(template).map(([k]) => k), fields(source).map(([k]) => k)); + compare(key, 'sections', headings(template), headings(source)); + compare(key, 'relation fields', relations(template).map(([k]) => k), relations(source).map(([k]) => k)); + if (meta.get('Primary track') !== `[${track}](./experiments.md)`) fail(key, 'Primary track must link its sibling index'); + if (!statuses.has(meta.get('Status'))) fail(key, 'invalid Status'); + if (!['Leaf', 'Synthesis'].includes(kind)) fail(key, 'Record kind must be Leaf or Synthesis'); + const indexRow = indexes.get(track)?.source.split('\n').find((l) => l.startsWith(`| [${id}](`)); + if (!indexRow || cells(indexRow)[2] !== meta.get('Status')) fail(key, 'missing index row or status mismatch'); + compare(key, 'proof columns', proofFields, rows.length ? cells(rows[0]) : []); + if (!obligations.length) fail(key, 'finite Proof Obligations required'); + if (kind === 'Leaf' && obligations.length !== 1) fail(key, 'Leaf must own one proof claim'); + if (new Set(obligations.map((r) => r[0])).size !== obligations.length) fail(key, 'duplicate obligation ID'); + for (const row of obligations) { + if (row.length !== 8 || row.some((v) => !v)) fail(key, 'incomplete proof obligation'); + if (!/^O\d+$/.test(row[0])) fail(key, 'invalid obligation ID'); + if (!/^(Required|Conditional: .+)/.test(row[5] ?? '')) fail(key, `${row[0]} must be Required or name its condition`); + } + for (const n of ['Freeze', 'Review triggers', 'Decomposition review']) if (!note(proofs, n)) fail(key, `missing ${n}`); + if (!['Proposed', 'Prepared'].includes(meta.get('Status')) && !/^Frozen\b/.test(note(proofs, 'Freeze'))) fail(key, 'Measuring-or-later obligations must be frozen'); + const count = [...section(source, 'Measurements').matchAll(/^### /gm)].length; + const triggers = note(proofs, 'Review triggers'), review = note(proofs, 'Decomposition review'); + if (kind === 'Leaf' && count > 6) { + warnings.push(`${key}: ${count} Measurement proof subsections require decomposition review`); + if (/^None\b/.test(review)) fail(key, 'more than six proof subsections without decomposition review'); + } + if (triggers && !/^None\b/.test(triggers)) { + warnings.push(`${key}: declared decomposition trigger: ${triggers}`); + if (/^None\b/.test(review)) fail(key, 'declared decomposition trigger without review'); + } + if (Buffer.byteLength(source) > 40000 || source.split('\n').length > 500) warnings.push(`${key}: large record (${Buffer.byteLength(source)} bytes); size is diagnostic`); + if (kind === 'Synthesis') { + const sourceLines = source.split('\n'); + for (const [i, line] of sourceLines.entries()) { + // Metric names in a workload/child claim are legitimate composition inputs. + // Primary evidence exposes metric columns, or numeric metric/value rows. + const columns = line.startsWith('|') ? cells(line) : []; + const metric = /\b(?:RefTime|ProofSize|Reads|Writes|Samples|median|p95|Recorded proof|Generated model SHA|Time sample|DB sample)\b/i; + if ((/^\|\s*:?-/.test(sourceLines[i + 1] ?? '') && columns.some((c) => metric.test(c))) || + (metric.test(columns[0] ?? '') && columns.slice(1).some((c) => /\d/.test(c)))) fail(key, 'Synthesis contains primary benchmark table'); + if (/`(?:\.\/)?(?:scripts\/benchmarks\.sh|bin\/frame-omni-bencher)\b/.test(line)) fail(key, 'Synthesis contains primary benchmark command'); + } + if (/^### /m.test(section(source, 'Measurements'))) fail(key, 'Synthesis contains independently measured subsections'); + if (/\|\s*(?:Candidate [A-Z]|[AB]\d?)\s*\|/m.test(section(source, 'Baseline and Candidates'))) fail(key, 'Synthesis contains primary benchmark candidate'); + } + } + const indexWrites = new Map(); + for (const [track, index] of indexes) { + for (const line of index.source.split('\n')) { + const m = line.match(/^\| (EXP-\d{4}) \|/); + if (!m || cells(line)[2] !== 'Proposed') continue; + const key = `${track}/${m[1]}`; + if (!nodes.has(key)) nodes.set(key, { key, track, id: m[1], file: index.file, source: '', meta: new Map([['Status', 'Proposed']]), rel: new Map(), obligations: [] }); + } + const table = index.source.match(/^\| ID \| Depends on \| Uses evidence from \|.*\n\|[^\n]+\n((?:\| EXP-\d{4} \|[^\n]+\n)+)/m); + if (!table) continue; + const header = cells(table[0].split('\n')[0]); + for (const line of table[1].trimEnd().split('\n')) { + const row = cells(line), key = `${track}/${row[0]}`, node = nodes.get(key); + if (!node || node.source) { fail(key, 'index-only relations missing Proposed row or duplicate record'); continue; } + node.rel = new Map(header.slice(1).map((name, i) => [name, row[i + 1] ?? ''])); + } + } + const globalIds = new Map(); + for (const [key, node] of nodes) { + if (globalIds.has(node.id)) fail(key, `global experiment ID ${node.id} already owned by ${globalIds.get(node.id)}`); + else globalIds.set(node.id, key); + } + function targets(node, value, check = true) { + const found = new Set(); + const plain = (value ?? '').replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label, url) => { + if (/^\.{1,2}\//.test(url)) { + const resolved = path.resolve(path.dirname(node.file), url.split('#')[0]); + if (check && !fs.existsSync(resolved)) fail(node.key, `broken link ${url}`); + if (/\/EXP-\d{4}\.md$/.test(resolved)) found.add(`${path.basename(path.dirname(resolved))}/${path.basename(resolved, '.md')}`); + else for (const id of label.match(/EXP-\d{4}/g) ?? []) found.add(`${node.track}/${id}`); + } + return ''; + }); + for (const m of plain.matchAll(/(?:([a-z][a-z0-9-]*)\/)?(EXP-\d{4})/g)) found.add(`${m[1] ?? node.track}/${m[2]}`); + return found; + } + const hard = new Map(), decomposition = new Map(), flow = [], former = new Map(); + for (const [key, node] of nodes) { + hard.set(key, targets(node, node.rel.get('Depends on'))); + decomposition.set(key, targets(node, node.rel.get('Decomposes into'))); + for (const [relation, value] of node.rel) { + if (!value) fail(key, `empty ${relation}; use None`); + for (const target of targets(node, value)) { + if (!nodes.has(target)) fail(key, `${relation} references unknown ${target}`); + if (relation === 'Uses evidence from') flow.push([target, key, 'uses']); + if (relation === 'Produces input for') flow.push([key, target, 'produces']); + if (['Confirms', 'Invalidates'].includes(relation)) flow.push([key, target, relation.toLowerCase()]); + } + } + if (!node.source) continue; + const parents = targets(node, node.meta.get('Parent question')), satisfies = targets(node, node.rel.get('Satisfies obligation')); + for (const match of (node.rel.get('Satisfies obligation') ?? '').matchAll(/\]\(([^)]*EXP-\d{4}\.md)(?:#[^)]*)?\)\s+(O\d+)\b/g)) { + const resolved = path.resolve(path.dirname(node.file), match[1]); + const owner = nodes.get(`${path.basename(path.dirname(resolved))}/${path.basename(resolved, '.md')}`); + if (!owner?.obligations.some((r) => r[0] === match[2] && targets(owner, r[4]).has(key))) fail(key, `Satisfies obligation names unknown or differently owned ${match[2]}`); + } + for (const parent of parents) { + if (nodes.get(parent)?.meta.get('Record kind') !== 'Synthesis') fail(key, `Parent question ${parent} is not Synthesis`); + if (!targets(nodes.get(parent) ?? node, nodes.get(parent)?.rel.get('Decomposes into')).has(key)) fail(key, `Parent question ${parent} is not reciprocal`); + if (!satisfies.has(parent)) fail(key, `missing Satisfies obligation for ${parent}`); + } + for (const target of satisfies) if (!parents.has(target)) fail(key, `Satisfies obligation ${target} is not a parent`); + if (node.meta.get('Record kind') === 'Leaf' && decomposition.get(key).size) fail(key, 'Leaf cannot decompose; use Synthesis'); + for (const child of decomposition.get(key)) { + const n = nodes.get(child); + if (!n?.source || !targets(n, n.meta.get('Parent question')).has(key)) fail(key, `Decomposes into ${child} is not reciprocal`); + } + for (const row of node.obligations) { + const owners = row[4] === 'Self' ? new Set([key]) : targets(node, row[4]); + if (owners.size !== 1 || [...owners].some((o) => !nodes.get(o)?.source)) fail(key, `${row[0]} requires one owning experiment`); + if (node.meta.get('Record kind') === 'Leaf' && !owners.has(key)) fail(key, `${row[0]} Leaf claim must be self-owned`); + if (node.meta.get('Record kind') === 'Synthesis') for (const owner of owners) { + if (owner === key || !decomposition.get(key).has(owner)) fail(key, `${row[0]} owner must be a declared child`); + const binding = new RegExp(`\\]\\([^)]*${node.id}\\.md(?:#[^)]*)?\\)\\s+${row[0]}\\b`); + if (!binding.test(nodes.get(owner)?.rel.get('Satisfies obligation') ?? '')) fail(key, `${row[0]} child ${owner} does not satisfy obligation`); + } + } + const priorIds = [...(node.meta.get('Former IDs') ?? '').matchAll(/(?:([a-z][a-z0-9-]*)\/)?(EXP-\d{4})(?:@([a-f0-9]{40}))?/g)].map((m) => `${m[1] ?? node.track}/${m[2]}${m[3] ? `@${m[3]}` : ''}`); + for (const prior of priorIds) { + if (former.has(prior) && former.get(prior) !== key) fail(key, `ambiguous Former ID ${prior}`); + if (!prior.includes('@') && globalIds.has(prior.split('/')[1])) fail(key, `Former ID reused as canonical without baseline qualification: ${prior}`); + if (!prior.includes('@') && [...former.keys()].some((id) => !id.includes('@') && id.split('/')[1] === prior.split('/')[1] && id !== prior)) fail(key, `ambiguous global Former ID ${prior}`); + former.set(prior, key); + } + const consumers = targets(node, node.rel.get('Produces input for')); + if (node.meta.get('Record kind') === 'Leaf' && consumers.size > 1) { + warnings.push(`${key}: reused by ${consumers.size} downstream decisions; decomposition review required`); + if (/^None\b/.test(note(section(node.source, 'Proof Obligations'), 'Decomposition review'))) fail(key, 'multiple consumers without decomposition review'); + } + } + function acyclic(graph, label) { + const done = new Set(), active = new Set(); + function visit(key, chain) { + if (active.has(key)) { fail(label, `cycle ${[...chain, key].join(' -> ')}`); return; } + if (done.has(key)) return; + active.add(key); + for (const next of graph.get(key) ?? []) visit(next, [...chain, key]); + active.delete(key); done.add(key); + } + for (const key of graph.keys()) visit(key, []); + } + acyclic(hard, 'hard dependency DAG'); acyclic(decomposition, 'decomposition graph'); + + const manifests = walk(path.join(skillDir, 'migrations')).filter((p) => p.endsWith('.json')), mappings = new Map(); + for (const file of manifests) { + const manifest = JSON.parse(fs.readFileSync(file, 'utf8')); + const baselineFiles = new Map(); + for (const item of manifest.renumberings ?? []) { + mappings.set(item.former, item.current); + if (former.get(item.former) !== item.current) fail(file, `renumbering ${item.former} lacks Former IDs`); + if (item.former.includes('@') && item.former.split('@')[1] !== manifest.baseline_commit) fail(file, `Former ID baseline qualifier mismatch: ${item.former}`); + } + for (const item of manifest.files ?? []) { + const m = item.path.match(/\/tracks\/([^/]+)\/(EXP-\d{4})\.md$/), key = m ? `${m[1]}/${m[2]}` : null; + const renamed = (manifest.renumberings ?? []).find((r) => r.former.split('@')[0] === key)?.current; + if (key && sealed.has(item.status) && (!nodes.has(key) || renamed)) fail(file, `sealed ID renumbered or removed: ${key}`); + if (key && renamed && !provisional.has(item.status)) fail(file, `non-provisional ID renumbered: ${key}`); + try { + const bytes = gitRead ? gitRead(manifest.baseline_commit, item.path) : execFileSync('git', ['show', `${manifest.baseline_commit}:${item.path}`], { cwd: root, maxBuffer: 32 * 1024 * 1024 }); + baselineFiles.set(item.path, bytes.toString()); + if (hash(bytes) !== item.sha256 || bytes.length !== item.bytes) fail(file, `baseline identity mismatch: ${item.path}`); + if (key && new Map(fields(bytes.toString())).get('Status') !== item.status) fail(file, `baseline status mismatch: ${item.path}`); + if (key && renamed && !gitRead) { + const decisions = execFileSync('git', ['log', manifest.baseline_commit, '--format=%H', '-G', '^\\| Status \\| (Accepted|Rejected|Inconclusive|Superseded|Invalidated) \\|', '--', item.path], { cwd: root }).toString().trim(); + if (decisions) fail(file, `historically sealed ID renumbered: ${key}`); + } + } catch { fail(file, `unavailable Git baseline: ${item.path}`); } + const current = renamed ? path.join(skillDir, 'tracks', `${renamed}.md`) : path.join(root, item.path); + if (fs.existsSync(current) && (fs.statSync(current).mode & 0o777).toString(8).padStart(4, '0') !== item.mode) fail(file, `mode changed: ${item.path}`); + } + const coverage = new Map(), primaryRows = new Map(); + // Navigation may change; literal measurements, commands, hashes and conclusions may not. + const evidenceLine = (line) => line.trim() + .replace(/EXP-\d{4}(?:\s*[–—/-]\s*(?:EXP-)?\d{4})*(?:\.md(?:#[^\s)]*)?)?/g, 'EXP-NAV'); + for (const item of manifest.extractions ?? []) { + const source = baselineFiles.get(item.source); + if (source === undefined) { fail(file, `extraction lacks captured baseline: ${item.source}`); continue; } + const lines = source.match(/[^\n]*\n|[^\n]+$/g) ?? []; + const raw = lines.slice(item.start_line - 1, item.end_line).join(''); + if (hash(raw) !== item.sha256) fail(file, `extraction source identity mismatch: ${item.source}:${item.start_line}`); + const owned = item.retained_source_lines ?? Array.from({ length: item.end_line - item.start_line + 1 }, (_, i) => item.start_line + i); + if (!coverage.has(item.source)) coverage.set(item.source, new Set()); + for (const line of owned) coverage.get(item.source).add(line); + if (!item.owner) { + if (item.classification !== 'compact-provenance' || !item.reason) fail(file, 'unowned extraction requires explicit compact regression/composition provenance'); + if (/^\|[^\n]*\b(?:RefTime|ProofSize|Time sample|DB sample)\b[^\n]*\|\n\|\s*---/m.test(raw)) fail(file, 'primary benchmark table cannot be discarded as compact chronology'); + continue; + } + const node = nodes.get(item.owner); + if (node?.meta.get('Record kind') !== 'Leaf') { fail(file, `primary extraction owner is not Leaf: ${item.owner}`); continue; } + const marker = item.marker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const payloads = [...node.source.matchAll(new RegExp(`${marker}\\n([\\s\\S]*?)\\n`, 'g'))]; + if (payloads.length !== 1) { fail(file, `extraction marker must occur once: ${item.marker}`); continue; } + const payload = payloads[0][1]; + if (hash(payload) !== item.payload_sha256) fail(file, `relocated payload identity mismatch: ${item.marker}`); + const retained = new Set(payload.split('\n').map(evidenceLine)); + for (const line of owned) { + const originalLine = lines[line - 1]?.trim() ?? ''; + if (!originalLine || originalLine.startsWith('#')) continue; + if (!retained.has(evidenceLine(originalLine))) fail(file, `extracted decision evidence changed or missing: ${item.source}:${line} -> ${item.owner}`); + if (originalLine.startsWith('|') && !originalLine.startsWith('| ---') && !['shared-context', 'partitioned-context'].includes(item.classification)) { + const key = `${item.source}:${line}`; + // Table headers can be shared; primary rows belong to exactly one Leaf. + const next = lines[line]?.trim() ?? ''; + if (!next.startsWith('| ---')) { + if (primaryRows.has(key) && primaryRows.get(key) !== item.owner) fail(file, `primary table row duplicated across leaves: ${key}`); + primaryRows.set(key, item.owner); + } + } + } + } + for (const [id, scope] of Object.entries(manifest.coverage ?? {})) { + const sourcePath = [...baselineFiles.keys()].find((p) => p.endsWith(`/EXP-${id.padStart(4, '0')}.md`)); + if (!sourcePath) { fail(file, `coverage missing baseline ${id}`); continue; } + const lines = baselineFiles.get(sourcePath).split('\n'), [start, end] = scope.measurement_and_derived_lines; + for (let n = start; n <= end; n++) if (lines[n - 1]?.trim() && !lines[n - 1].startsWith('#') && !coverage.get(sourcePath)?.has(n)) fail(file, `unclassified pre-fission evidence: ${sourcePath}:${n}`); + } + } + for (const [prior, current] of former) { + if (mappings.get(prior) === current) continue; + const [identity, baseline] = prior.split('@'); + if (!baseline) { fail(current, `Former ID ${prior} lacks migration baseline`); continue; } + const sourcePath = path.relative(root, path.join(skillDir, 'tracks', `${identity}.md`)); + try { + const bytes = gitRead ? gitRead(baseline, sourcePath) : execFileSync('git', ['show', `${baseline}:${sourcePath}`], { cwd: root, maxBuffer: 32 * 1024 * 1024 }); + if (!provisional.has(new Map(fields(bytes.toString())).get('Status'))) fail(current, `non-provisional ID renumbered: ${identity}`); + if (!gitRead) { + const decisions = execFileSync('git', ['log', baseline, '--format=%H', '-G', '^\\| Status \\| (Accepted|Rejected|Inconclusive|Superseded|Invalidated) \\|', '--', sourcePath], { cwd: root }).toString().trim(); + if (decisions) fail(current, `historically sealed ID renumbered: ${identity}`); + } + } catch { fail(current, `unavailable Git baseline for Former ID ${prior}`); } + } + if (former.size) { + let allFiles = repoFiles; + if (!allFiles) { + try { allFiles = execFileSync('git', ['ls-files', '-z', '--cached', '--others', '--exclude-standard'], { cwd: root, maxBuffer: 32 * 1024 * 1024 }).toString().split('\0').filter(Boolean).map((p) => path.join(root, p)); } + catch { fail(root, 'cannot enumerate former-ID references'); allFiles = []; } + } + for (const file of new Set(allFiles)) { + if (manifests.includes(file) || !fs.existsSync(file) || !fs.statSync(file).isFile()) continue; + const bytes = fs.readFileSync(file); + if (bytes.includes(0)) continue; + for (const [prior] of former) { + const [basePrior, qualifier] = prior.split('@'), [track, id] = basePrior.split('/'); + const otherTrack = file.match(/\/tracks\/([^/]+)\//)?.[1]; + const suffix = qualifier && nodes.has(basePrior) ? `@${qualifier}\\b` : '\\b'; + const pattern = new RegExp(otherTrack && otherTrack !== track ? `${track}/${id}${suffix}` : `(?:${track}/)?${id}${suffix}`); + for (const [i, line] of bytes.toString('utf8').split('\n').entries()) { + if (pattern.test(line) && !/^\| Former IDs \|/.test(line) && !/\bformer(?:[- ]ID| provisional| identity| navigation| IDs?)?\b/i.test(line)) fail(file, `live former-ID ${prior} at line ${i + 1}`); + } + } + } + } + // Evidence reuse, including Uses-evidence edges declared by a consumer, is a review trigger. + for (const [key, node] of nodes) { + if (node.meta.get('Record kind') !== 'Leaf') continue; + const consumers = new Set(flow.filter(([from, , type]) => from === key && ['uses', 'produces'].includes(type)).map(([, to]) => to)); + if (consumers.size > 1 && /^None\b/.test(note(section(node.source, 'Proof Obligations'), 'Decomposition review'))) fail(key, 'multiple evidence consumers without decomposition review'); + } + // Record-body links are part of the atomic migration, not merely Relations syntax. + for (const node of nodes.values()) for (const m of node.source.matchAll(/\[[^\]]+\]\((\.{1,2}\/[^)]+)\)/g)) { + const [relative, anchor] = m[1].split('#'), target = path.resolve(path.dirname(node.file), relative); + if (!fs.existsSync(target)) { fail(node.key, `broken body link ${m[1]}`); continue; } + if (anchor && target.endsWith('.md')) { + const slugs = [...fs.readFileSync(target, 'utf8').matchAll(/^#{1,6} (.+)$/gm)].map((h) => h[1].toLowerCase().replace(/[^\p{L}\p{N}_\- ]/gu, '').replaceAll(' ', '-')); + if (!slugs.includes(anchor)) fail(node.key, `broken body anchor ${m[1]}`); + } + } + for (const [track, index] of indexes) { + const campaign = index.source.match(//)?.[1]; + if (!campaign && !graphPattern.test(index.source)) continue; + if (!campaign) { fail(index.file, 'graph requires experiment-graph-campaign scope'); continue; } + const selected = new Set([...nodes.values()].filter((n) => n.track === track && n.meta.get('Architecture release / experiment campaign') === campaign).map((n) => n.key)); + if (!selected.size) { fail(index.file, 'graph campaign has no records'); continue; } + const graph = renderGraph(nodes, selected, hard, decomposition, flow); + if (!graphPattern.test(index.source)) fail(index.file, 'missing graph markers'); + else if (!errors.length && writeIndex) indexWrites.set(index.file, index.source.replace(graphPattern, graph)); + else if (index.source.match(graphPattern)?.[0] !== graph) fail(index.file, 'stale graph projection; use --write-index'); + } + if (!errors.length) for (const [file, source] of indexWrites) fs.writeFileSync(file, source); + return { errors: [...new Set(errors)], warnings: [...new Set(warnings)], recordCount: [...nodes.values()].filter((n) => n.source).length }; +} + +function renderGraph(nodes, selected, hard, decomposition, flow) { + const name = (key) => key.replaceAll(/[^a-zA-Z0-9]/g, '_'); + const lines = ['', 'Scope: the declared campaign and directly related external inputs/consumers. Decomposition means constituent proofs; hard dependencies mean required input; dotted evidence flow means scoped reuse, not ordering.']; + const hardEdges = [...hard].flatMap(([to, inputs]) => [...inputs].map((from) => [from, to, 'requires'])); + const children = [...decomposition].flatMap(([from, outputs]) => [...outputs].map((to) => [from, to, 'contains'])); + for (const [title, edges, dotted] of [['Decomposition', children, false], ['Hard dependencies', hardEdges, false], ['Evidence flow', flow, true]]) { + const filtered = [...new Map(edges.filter(([a, b]) => selected.has(a) || selected.has(b)).map((e) => [e.join('|'), e])).values()].sort((a, b) => a.join('|').localeCompare(b.join('|'))); + const keys = new Set(filtered.flatMap(([a, b]) => [a, b])); + lines.push('', `**${title}**`, '', '```mermaid', 'flowchart TD'); + for (const key of [...keys].sort()) { + const n = nodes.get(key), label = selected.has(key) ? `${n?.meta.get('Record kind')} / ${n?.meta.get('Status')}` : 'external'; + lines.push(` ${name(key)}["${key}: ${label}"]`); + } + for (const [a, b, kind] of filtered) lines.push(` ${name(a)} ${dotted ? `-. ${kind} .->` : '-->'} ${name(b)}`); + if (!filtered.length) lines.push(' none["No declared edges"]'); + lines.push('```'); + } + lines.push('', 'Conditional residual experiments remain unallocated until a measured owner and changed assumption earn a distinct proof question.', ''); + return lines.join('\n'); +} + +if (process.argv[1] === fileURLToPath(import.meta.url)) { + const [skillDir, mode] = process.argv.slice(2); + if (mode === '--self-test') { + execFileSync(process.execPath, [path.join(skillDir, 'scripts/record-normalization.test.mjs'), skillDir], { stdio: 'inherit' }); + } else { + const result = validate(path.resolve(skillDir), { writeIndex: mode === '--write-index' }); + for (const w of result.warnings) console.error(`warning: ${w}`); + for (const e of result.errors) console.error(`error: ${e}`); + if (result.errors.length) process.exitCode = 1; + else console.log(`Experiment normalization passed: ${result.recordCount} records; obligations, identities and three graph projections valid`); + } +} diff --git a/.agents/skills/architecture-experiments/scripts/record-normalization.test.mjs b/.agents/skills/architecture-experiments/scripts/record-normalization.test.mjs new file mode 100644 index 00000000..79da38fc --- /dev/null +++ b/.agents/skills/architecture-experiments/scripts/record-normalization.test.mjs @@ -0,0 +1,171 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import assert from 'node:assert/strict'; +import { createHash } from 'node:crypto'; +import { validate } from './record-normalization.mjs'; +import { fileURLToPath } from 'node:url'; + +export function selfTest(realSkillDir) { + const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'deos-proof-graph-')); + const skill = path.join(temp, '.agents/skills/architecture-experiments'); + const track = path.join(skill, 'tracks/actors'); + fs.mkdirSync(track, { recursive: true }); + fs.mkdirSync(path.join(skill, 'templates')); + const template = fs.readFileSync(path.join(realSkillDir, 'templates/EXP-NNNN.md'), 'utf8'); + fs.writeFileSync(path.join(skill, 'templates/EXP-NNNN.md'), template); + const sectionNames = [...template.matchAll(/^## (.*)$/gm)].map((m) => m[1]); + const metaNames = [...template.matchAll(/^\| ([^|]+) \| .* \|$/gm)].map((m) => m[1]); + const firstSection = template.indexOf('\n## '); + const metadata = metaNames.filter((name) => template.slice(0, firstSection).includes(`| ${name} |`) && !['Field', '---'].includes(name)); + const relationNames = [...template.split('\n## Relations\n')[1].matchAll(/^- `([^`]+)`:/gm)].map((m) => m[1]); + const link = (id) => `[${id}](./${id}.md)`; + function record(id, kind, parent = '', child = '') { + const values = { Status: 'Measuring', 'Record kind': kind, 'Former IDs': 'None', 'Parent question': parent ? link(parent) : 'None', 'Primary track': '[actors](./experiments.md)', 'Architecture release / experiment campaign': 'fixture' }; + const rel = { 'Depends on': child ? link(child) : 'None', 'Decomposes into': child ? link(child) : 'None', 'Satisfies obligation': parent ? `${link(parent)} O1` : 'None' }; + const proof = ['- `Freeze`: Frozen at Measuring on fixture source.', '- `Review triggers`: None.', '- `Decomposition review`: None.', '', '| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status |', '| --- | --- | --- | --- | --- | --- | --- | --- |', `| O1 | A bounded proof | One legal witness | Native construction | ${child ? link(child) : 'Self'} | Required | Decides admission | Open |`].join('\n'); + return `# ${id} — Fixture\n\n| Field | Value |\n| --- | --- |\n${metadata.map((n) => `| ${n} | ${values[n] ?? 'None'} |`).join('\n')}\n\n${sectionNames.map((name) => `## ${name}\n\n${name === 'Proof Obligations' ? proof : name === 'Relations' ? relationNames.map((n) => `- \`${n}\`: ${rel[n] ?? 'None'}.`.replace('None..', 'None.')).join('\n') : 'Bounded fixture evidence.'}`).join('\n\n')}\n`; + } + const parentId = 'EXP-0090', childId = 'EXP-0091', parentFile = path.join(track, `${parentId}.md`), childFile = path.join(track, `${childId}.md`); + const indexFile = path.join(track, 'experiments.md'); + const parent = record(parentId, 'Synthesis', '', childId), child = record(childId, 'Leaf', parentId); + const initialIndex = `# Test index\n\n| ID | Release | Status |\n| --- | --- | --- |\n| ${link(parentId)} | fixture | Measuring |\n| ${link(childId)} | fixture | Measuring |\n\n\n\n\n`; + let count = 0; + const options = { repoFiles: [parentFile, childFile, indexFile] }; + function reset() { + fs.writeFileSync(parentFile, parent); fs.writeFileSync(childFile, child); fs.writeFileSync(indexFile, initialIndex); + fs.rmSync(path.join(skill, 'migrations'), { force: true, recursive: true }); + fs.rmSync(path.join(skill, 'tracks/router'), { force: true, recursive: true }); + const result = validate(skill, { ...options, writeIndex: true }); + assert.deepEqual(result.errors, [], result.errors.join('\n')); + } + function rejects(name, mutate, pattern) { + reset(); mutate(); + const errors = validate(skill, options).errors.join('\n'); + assert.match(errors, pattern, `${name}: mutation was not rejected\n${errors}`); count++; + } + const change = (file, from, to) => fs.writeFileSync(file, fs.readFileSync(file, 'utf8').replace(from, to)); + function router(id, materialized = false) { + const directory = path.join(skill, 'tracks/router'); + fs.mkdirSync(directory, { recursive: true }); + fs.writeFileSync(path.join(directory, 'experiments.md'), `| ID | Release | Status |\n| --- | --- | --- |\n| ${materialized ? link(id) : id} | fixture | ${materialized ? 'Measuring' : 'Proposed'} |\n`); + if (materialized) fs.writeFileSync(path.join(directory, `${id}.md`), record(id, 'Leaf').replace('[actors](./experiments.md)', '[router](./experiments.md)')); + } + try { + reset(); assert.deepEqual(validate(skill, options).errors, []); count++; + rejects('cross-track record collision', () => router(childId, true), /global experiment ID/); + rejects('cross-track index-only collision', () => router(childId), /global experiment ID/); + rejects('two index-only tracks collide', () => { + fs.appendFileSync(indexFile, '\n| EXP-0092 | fixture | Proposed |\n'); + router('EXP-0092'); + }, /global experiment ID/); + reset(); router('EXP-0092'); + assert.deepEqual(validate(skill, options).errors, []); count++; + rejects('missing kind', () => change(childFile, '| Record kind | Leaf |\n', ''), /Record kind/); + rejects('invalid kind', () => change(childFile, '| Record kind | Leaf |', '| Record kind | Notebook |'), /Record kind/); + rejects('unowned synthesis obligation', () => change(parentFile, `| ${link(childId)} | Required |`, '| None | Required |'), /owning experiment/); + rejects('missing parent backlink', () => change(childFile, `| Parent question | ${link(parentId)} |`, '| Parent question | None |'), /not reciprocal/); + rejects('missing child edge', () => change(parentFile, `- \`Decomposes into\`: ${link(childId)}.`, '- `Decomposes into`: None.'), /not reciprocal/); + rejects('wrong obligation backlink', () => change(childFile, `${link(parentId)} O1`, `${link(parentId)} O2`), /does not satisfy obligation/); + rejects('extra unknown obligation', () => change(childFile, `${link(parentId)} O1`, `${link(parentId)} O1; ${link(parentId)} O2`), /unknown or differently owned O2/); + rejects('hard cycle', () => change(childFile, '- `Depends on`: None.', `- \`Depends on\`: ${link(parentId)}.`), /hard dependency DAG: cycle/); + rejects('decomposition self cycle', () => change(parentFile, `- \`Decomposes into\`: ${link(childId)}.`, `- \`Decomposes into\`: ${link(parentId)}.`), /decomposition graph: cycle/); + rejects('stale projection', () => change(indexFile, '**Evidence flow**', '**Changed graph**'), /stale graph projection/); + rejects('raw synthesis table', () => change(parentFile, '## Measurements\n\nBounded fixture evidence.', '## Measurements\n\n| RefTime | Reads |\n| --- | --- |\n| 100 | 2 |'), /primary benchmark table/); + rejects('raw timing sample table', () => change(parentFile, '## Measurements\n\nBounded fixture evidence.', '## Measurements\n\n| Branch | Time sample / storage-root ns |\n| --- | --- |\n| A | 100 / 2 |'), /primary benchmark table/); + rejects('synthesis command', () => change(parentFile, '## Measurements\n\nBounded fixture evidence.', '## Measurements\n\nRun `scripts/benchmarks.sh --extrinsic foo`.'), /primary benchmark command/); + rejects('synthesis candidate', () => change(parentFile, '## Baseline and Candidates\n\nBounded fixture evidence.', '## Baseline and Candidates\n\n| A | new candidate |'), /primary benchmark candidate/); + rejects('unreviewed branch growth', () => change(childFile, '## Measurements\n\nBounded fixture evidence.', `## Measurements\n\n${Array.from({ length: 7 }, (_, i) => `### Proof ${i}\n\nWitness.`).join('\n\n')}`), /six proof subsections/); + for (const trigger of ['new mandatory obligation', 'new Weight owner', 'new reachable domain', 'new production selector']) rejects(trigger, () => change(childFile, '- `Review triggers`: None.', `- \`Review triggers\`: ${trigger}.`), /trigger without review/); + rejects('unfrozen measuring', () => change(childFile, 'Frozen at Measuring on fixture source.', 'Not frozen.'), /must be frozen/); + rejects('multiple leaf claims', () => change(childFile, '| O1 | A bounded proof', '| O2 | Another claim | One witness | Native | Self | Required | Consumer | Open |\n| O1 | A bounded proof'), /one proof claim/); + rejects('broken body anchor', () => change(childFile, '## Measurements\n\nBounded fixture evidence.', `## Measurements\n\n[Parent](./${parentId}.md#missing-proof).`), /broken body anchor/); + reset(); + change(parentFile, '## Measurements\n\nBounded fixture evidence.', '## Measurements\n\n| Phase | Child claim |\n| --- | --- |\n| Admission | Independent RefTime and ProofSize refusal |'); + assert.deepEqual(validate(skill, options).errors, []); count++; + reset(); + change(childFile, '## Measurements\n\nBounded fixture evidence.', `## Measurements\n\n${'Retained bounded evidence. '.repeat(2000)}`); + const large = validate(skill, options); assert.equal(large.errors.length, 0); assert(large.warnings.some((w) => w.includes('large record'))); count++; + + // Former identity checks use an actual frozen baseline buffer, never the live file. + const formerId = 'EXP-0089'; + function migrated(status = 'Proposed') { + reset(); + change(childFile, '| Former IDs | None |', `| Former IDs | actors/${formerId} |`); + const baseline = Buffer.from(child.replaceAll(childId, formerId).replace('| Status | Measuring |', `| Status | ${status} |`)); + fs.mkdirSync(path.join(skill, 'migrations')); + fs.writeFileSync(path.join(skill, 'migrations/fixture.json'), JSON.stringify({ baseline_commit: 'fixture', renumberings: [{ former: `actors/${formerId}`, current: `actors/${childId}` }], files: [{ path: `.agents/skills/architecture-experiments/tracks/actors/${formerId}.md`, status, mode: '0644', bytes: baseline.length, sha256: createHash('sha256').update(baseline).digest('hex') }] })); + return { ...options, gitRead: () => baseline }; + } + const good = migrated(); assert.deepEqual(validate(skill, good).errors, []); count++; + router(formerId); + assert(validate(skill, good).errors.some((e) => e.includes('without baseline qualification'))); count++; + fs.rmSync(path.join(skill, 'tracks/router'), { recursive: true }); + fs.appendFileSync(indexFile, `\nFormer provisional identity actors/${formerId} now names ${childId}.\n`); + assert.deepEqual(validate(skill, good).errors, []); count++; + fs.appendFileSync(indexFile, `\nContinue actors/${formerId}.\n`); + assert(validate(skill, good).errors.some((e) => e.includes('live former-ID'))); count++; + for (const state of ['Accepted', 'Rejected', 'Inconclusive', 'Superseded', 'Invalidated', 'Interpreted']) { + const opts = migrated(state); + assert(validate(skill, opts).errors.some((e) => e.includes('non-provisional ID')), state); count++; + } + // Explicit compaction may reuse only a baseline-qualified provisional identity. + const qualifiedOptions = migrated(); + const fixtureManifest = path.join(skill, 'migrations/fixture.json'); + const qualified = JSON.parse(fs.readFileSync(fixtureManifest, 'utf8')); + const epoch = '1'.repeat(40); + qualified.baseline_commit = epoch; + qualified.renumberings[0].former += `@${epoch}`; + fs.writeFileSync(fixtureManifest, JSON.stringify(qualified)); + change(childFile, `actors/${formerId} |`, `actors/${formerId}@${epoch} |`); + const reusedFile = path.join(track, `${formerId}.md`); + fs.writeFileSync(reusedFile, record(formerId, 'Leaf')); + fs.appendFileSync(indexFile, `\n| ${link(formerId)} | fixture | Measuring |\n`); + assert.deepEqual(validate(skill, qualifiedOptions).errors, []); count++; + fs.rmSync(path.join(skill, 'migrations'), { recursive: true }); + assert.deepEqual(validate(skill, qualifiedOptions).errors, []); count++; + assert(validate(skill, { ...qualifiedOptions, gitRead: () => { throw new Error('Missing baseline'); } }).errors.some((e) => e.includes('unavailable Git baseline'))); count++; + for (const status of ['Accepted', 'Rejected', 'Inconclusive', 'Superseded', 'Invalidated', 'Interpreted']) { + const bytes = qualifiedOptions.gitRead().toString().replace('| Status | Proposed |', `| Status | ${status} |`); + assert(validate(skill, { ...qualifiedOptions, gitRead: () => Buffer.from(bytes) }).errors.some((e) => e.includes('non-provisional ID')), status); count++; + } + fs.mkdirSync(path.join(skill, 'migrations')); + fs.writeFileSync(fixtureManifest, JSON.stringify(qualified)); + change(childFile, `actors/${formerId}@${epoch} |`, `actors/${formerId} |`); + assert(validate(skill, qualifiedOptions).errors.some((e) => e.includes('without baseline qualification'))); count++; + change(childFile, `actors/${formerId} |`, `actors/${formerId}@${epoch} |`); + qualified.baseline_commit = '2'.repeat(40); + fs.writeFileSync(fixtureManifest, JSON.stringify(qualified)); + assert(validate(skill, qualifiedOptions).errors.some((e) => e.includes('qualifier mismatch'))); count++; + fs.rmSync(reusedFile); + reset(); + const table = '| RefTime | Reads |\n| --- | --- |\n| 100 | 2 |'; + const baselineSource = child.replace('## Measurements\n\nBounded fixture evidence.', `## Measurements\n\n${table}`); + const marker = ''; + fs.writeFileSync(childFile, baselineSource.replace(table, `${marker}\n${table}\n`)); + const sourcePath = `.agents/skills/architecture-experiments/tracks/actors/${childId}.md`; + const start = baselineSource.split('\n').indexOf('| RefTime | Reads |') + 1; + const digest = (s) => createHash('sha256').update(s).digest('hex'); + const manifest = { baseline_commit: 'fixture', files: [{ path: sourcePath, status: 'Measuring', mode: '0644', bytes: Buffer.byteLength(baselineSource), sha256: digest(baselineSource) }], extractions: [{ source: sourcePath, start_line: start, end_line: start + 2, sha256: digest(table + '\n'), owner: `actors/${childId}`, marker, classification: 'primary-evidence', payload_sha256: digest(table) }], coverage: { 91: { measurement_and_derived_lines: [start, start + 2] } } }; + fs.mkdirSync(path.join(skill, 'migrations')); + const manifestFile = path.join(skill, 'migrations/extraction.json'); + const save = () => fs.writeFileSync(manifestFile, JSON.stringify(manifest)); + save(); + const extractionOptions = { ...options, gitRead: () => Buffer.from(baselineSource) }; + assert.deepEqual(validate(skill, extractionOptions).errors, []); count++; + change(childFile, '| 100 | 2 |', '| 101 | 2 |'); + assert(validate(skill, extractionOptions).errors.some((e) => e.includes('payload identity mismatch'))); count++; + manifest.extractions[0].payload_sha256 = digest(table.replace('100', '101')); save(); + assert(validate(skill, extractionOptions).errors.some((e) => e.includes('decision evidence changed or missing'))); count++; + manifest.files[0].status = 'Proposed'; save(); + assert(validate(skill, extractionOptions).errors.some((e) => e.includes('baseline status mismatch'))); count++; + manifest.files[0].status = 'Measuring'; + delete manifest.extractions[0].owner; + manifest.extractions[0].classification = 'compact-provenance'; + manifest.extractions[0].reason = 'Ordinary chronology'; save(); + assert(validate(skill, extractionOptions).errors.some((e) => e.includes('cannot be discarded'))); count++; + console.log(`Proof-graph validator self-test passed: ${count} positive/negative cases`); + } finally { fs.rmSync(temp, { recursive: true, force: true }); } +} + +if (process.argv[1] === fileURLToPath(import.meta.url)) selfTest(process.argv[2]); diff --git a/.agents/skills/architecture-experiments/scripts/validate-record-normalization.sh b/.agents/skills/architecture-experiments/scripts/validate-record-normalization.sh index 7eea7ca5..8f802fec 100755 --- a/.agents/skills/architecture-experiments/scripts/validate-record-normalization.sh +++ b/.agents/skills/architecture-experiments/scripts/validate-record-normalization.sh @@ -1,74 +1,15 @@ #!/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 +if [[ "$#" -gt 1 ]]; then + printf 'error: expected at most one argument\n' >&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 +case "${1:-}" in + ''|--write-index|--self-test) ;; + --help) + printf 'Usage: %s [--write-index|--self-test]\nValidates proof records, obligations, identities and three graph projections.\n--write-index refreshes declared track projections after validation.\n--self-test exercises isolated positive and negative methodology fixtures.\nSkill-private; never a project build dependency.\n' "${0##*/}" + exit 0 ;; + *) printf 'error: unknown argument: %s\n' "$1" >&2; exit 1 ;; +esac +exec node "$skill_dir/scripts/record-normalization.mjs" "$skill_dir" "${1:-}" diff --git a/.agents/skills/architecture-experiments/templates/EXP-NNNN.md b/.agents/skills/architecture-experiments/templates/EXP-NNNN.md index a6ab1b18..b3d2512a 100644 --- a/.agents/skills/architecture-experiments/templates/EXP-NNNN.md +++ b/.agents/skills/architecture-experiments/templates/EXP-NNNN.md @@ -3,6 +3,9 @@ | Field | Value | | --- | --- | | Status | Proposed / Prepared / Measuring / Measured / Interpreted / Accepted / Rejected / Inconclusive / Superseded / Invalidated | +| Record kind | Leaf / Synthesis | +| Former IDs | None or track-qualified former provisional IDs; qualify with @full-Git-baseline when explicit compaction reuses a number | +| Parent question | None or relative links to owning Synthesis records | | Architecture release / experiment campaign | Measured X.Y.Z baseline / campaign X.Y.Z | | Date | YYYY-MM-DD or Not recorded | | Affected domain | Domain | @@ -14,7 +17,7 @@ ## Decision Question -State the single implementation choice this evidence can decide. +State the single implementation choice this evidence can decide. Allocate the globally unique `EXP-NNNN` under the architecture-experiments Skill’s Record Identity and Layout rule, checking every track; the containing track owns the record. ## Governing Specification @@ -70,8 +73,22 @@ State the single implementation choice this evidence can decide. | 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 | +## Proof Obligations + +- `Freeze`: Not frozen until Measuring; record the source/date or explicit migration reconstruction at freeze. +- `Review triggers`: None, or new mandatory obligation / new Weight owner / new reachable domain / new production selector / multiple consumers / more than six proof subsections. +- `Decomposition review`: None, or the boundary decision with transfers and exact same-claim justification. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | One falsifiable claim | Smallest deciding witness | Exact class | Self for Leaf, child experiment link for Synthesis | Required, or Conditional: named activation condition | Decision consuming this result | Open / Satisfied with scope / Rejected / Requires refresh / Conditional | + +A Leaf owns one claim; bounded workload witnesses must share its mechanism, changed variable, evidence domain and acceptance criterion. A Synthesis enumerates every finite child obligation. Freeze before measurements; independent later findings transfer to their own nodes. + ## Measurements +Leaf only: retain primary evidence below. Synthesis: replace this table with compact child-decision links; no primary benchmark tables, candidate implementation, branch sweep or debugging transcript anywhere in the record. Extracted evidence explicitly names the pre-fission parent subsection, Git baseline and exact artifact identity; moving it does not remeasure it. + | Candidate | Workload | RefTime | ProofSize | Reads | Writes | Other decision metrics | | --- | --- | ---: | ---: | ---: | ---: | --- | | Baseline | W1 | Not measured | Not measured | Not measured | Not measured | Not measured | @@ -131,6 +148,8 @@ target → measured gap → binding dimension → dominant contributor ## Relations +- `Decomposes into`: Child experiment links, or None for a Leaf. +- `Satisfies obligation`: Parent link and its obligation ID, or None. - `Replaces`: None. - `Supersedes`: None. - `Refines`: None. @@ -139,3 +158,7 @@ target → measured gap → binding dimension → dominant contributor - `Baseline of`: None. - `Confirms`: None. - `Invalidates`: None. +- `Uses evidence from`: Exact historical experiment/evidence links; distinguish borrowed observations from required accepted inputs. +- `Transfers question to`: Experiment links and the specific transferred question, or None. +- `Produces input for`: Downstream experiment links and deliverable, or None. +- `Reopen trigger`: Exact failed assumption/invariant and required evidence; cost alone does not reopen a frozen physical baseline. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0001.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0001.md index fcf2fc3d..c79ff209 100644 --- a/.agents/skills/architecture-experiments/tracks/actors/EXP-0001.md +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0001.md @@ -3,6 +3,9 @@ | Field | Value | | --- | --- | | Status | Rejected | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | None | | Architecture release / experiment campaign | DEOS 0.7.23 baseline / 0.7.24 optimization campaign | | Date | 2026-08-25 | | Affected domain | DEOS Actors runtime composition | @@ -71,6 +74,16 @@ B0 is tested against the release envelope rather than another implementation. Re | 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 | +## Proof Obligations + +- `Freeze`: Frozen by explicit migration reconstruction from the pre-fission decision and retained witnesses at a60de7748132211d9e58cecd5be82b41f76ce230. No new measurement or historical decision is implied. +- `Review triggers`: None. +- `Decomposition review`: Boundary reviewed during provenance-preserving fission. The historical decision boundary and all original evidence remain unchanged. Metadata normalization neither fissions nor reinterprets this sealed result; downstream reuse consumes its original validity scope. Any newly earned question receives a new node. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | 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? | One legal counterexample to the declared owner/domain or component-wise containment | Source-derived and bounded native/Wasm evidence as separately identified below | Self | Required | Named parent decision and declared downstream consumers | Rejected; retained original decision and validity scope | + ## Measurements | Actors | Blocks | Cycles | Cycles/block | ms/block min | ms/block mean | ms/block max | Sample SD | CV | @@ -166,8 +179,18 @@ target 100 cycles/block → measured 32.24 at 10k → gap 67.76% ## Relations -- `Baseline of`: EXP-0013 and later throughput candidates. +- `Decomposes into`: None. +- `Satisfies obligation`: None. + +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: Any claim that the configured execution ceiling is current measured throughput. - `Depends on`: Accepted 0.7.23 Actors architecture and the separate 0.7.24 release target. +- `Baseline of`: EXP-0013 and later throughput candidates. - `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. +- `Uses evidence from`: See Baseline and Candidates and Environment and Artifact Identity; historical inputs are not automatically acceptance dependencies. +- `Transfers question to`: See Follow-Up / Next Gradient; no additional transfer declared. +- `Produces input for`: See Baseline of and Successor experiment; no additional consumer declared. +- `Reopen trigger`: The specific conditions in Governing Specification and Validity; preserve historical evidence and create a successor for a materially different question. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0002.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0002.md index 730f0ee2..d6c38a44 100644 --- a/.agents/skills/architecture-experiments/tracks/actors/EXP-0002.md +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0002.md @@ -3,6 +3,9 @@ | Field | Value | | --- | --- | | Status | Invalidated | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | None | | 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 | @@ -168,6 +171,16 @@ cd .. - `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. +## Proof Obligations + +- `Freeze`: Frozen by explicit migration reconstruction from the pre-fission decision and retained witnesses at a60de7748132211d9e58cecd5be82b41f76ce230. No new measurement or historical decision is implied. +- `Review triggers`: None. +- `Decomposition review`: Boundary reviewed during provenance-preserving fission. The historical decision boundary and all original evidence remain unchanged. Metadata normalization neither fissions nor reinterprets this sealed result; downstream reuse consumes its original validity scope. Any newly earned question receives a new node. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | 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? | One legal counterexample to the declared owner/domain or component-wise containment | Source-derived and bounded native/Wasm evidence as separately identified below | Self | Required | Named parent decision and declared downstream consumers | Invalidated; retained original decision and validity scope | + ## Measurements | Candidate | Workload | RefTime | ProofSize | Reads | Writes | Other decision metrics | @@ -248,6 +261,9 @@ production ceiling 32 → FIFO short path and lifecycle pass ## Relations +- `Decomposes into`: None. +- `Satisfies obligation`: None. + - `Replaces`: None. - `Supersedes`: None. - `Refines`: The Step-ceiling and maximum-body boundary left outside [EXP-0001](./EXP-0001.md). @@ -256,4 +272,7 @@ production ceiling 32 → FIFO short path and lifecycle pass - `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. +- `Uses evidence from`: 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. +- `Transfers question to`: See Follow-Up / Next Gradient; no additional transfer declared. +- `Produces input for`: See Baseline of and Successor experiment; no additional consumer declared. +- `Reopen trigger`: The specific conditions in Governing Specification and Validity; preserve historical evidence and create a successor for a materially different question. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0003.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0003.md index e1f0992f..9cc2721b 100644 --- a/.agents/skills/architecture-experiments/tracks/actors/EXP-0003.md +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0003.md @@ -3,6 +3,9 @@ | Field | Value | | --- | --- | | Status | Rejected | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | None | | 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 | @@ -93,6 +96,16 @@ sha256sum "$out"/* template/target/benchmarks/release/wbuild/deos-runtime/deos_r The first B0 command builds the matching benchmark Wasm; every later command uses `--skip-build`. Any source/manifest drift invalidates the matrix before Measuring. +## Proof Obligations + +- `Freeze`: Frozen by explicit migration reconstruction from the pre-fission decision and retained witnesses at a60de7748132211d9e58cecd5be82b41f76ce230. No new measurement or historical decision is implied. +- `Review triggers`: None. +- `Decomposition review`: Boundary reviewed during provenance-preserving fission. The historical decision boundary and all original evidence remain unchanged. Metadata normalization neither fissions nor reinterprets this sealed result; downstream reuse consumes its original validity scope. Any newly earned question receives a new node. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | 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? | One legal counterexample to the declared owner/domain or component-wise containment | Source-derived and bounded native/Wasm evidence as separately identified below | Self | Required | Named parent decision and declared downstream consumers | Rejected; retained original decision and validity scope | + ## Measurements The complete Prepared matrix ran once from manifest `45c43626` and matching benchmark Wasm `23c83b9f`. Models exclude database Weight additions shown separately. @@ -163,6 +176,18 @@ None from rejected C1. `Exp0003*` stores and benchmark functions are temporary ` ## 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. +- `Decomposes into`: None. +- `Satisfies obligation`: None. + +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: None. +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: See Baseline and Candidates and Environment and Artifact Identity; historical inputs are not automatically acceptance dependencies. +- `Transfers question to`: Successor: EXP-0016 owns the inline-Step-0 plus lazy-tail geometry. EXP-0004 remains conditional Step-representation work after body ownership converges. +- `Produces input for`: Gates: I2 Physical Geometry Prototype and Selected Geometry. Related: EXP-0002 consumes the selected body geometry for 8/16/32 ceiling comparison. +- `Reopen trigger`: The specific conditions in Governing Specification and Validity; preserve historical evidence and create a successor for a materially different question. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0004.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0004.md new file mode 100644 index 00000000..648ee543 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0004.md @@ -0,0 +1,192 @@ +# EXP-0004 — Actor Step Representation + +| Field | Value | +| --- | --- | +| Status | Invalidated | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | None | +| Architecture release / experiment campaign | DEOS 0.7.25 baseline / Canonical Actor Control Geometry campaign | +| Date | 2026-09-02 | +| Affected domain | DEOS Actors Contract tail storage, current-Step execution, lifecycle, and projection | +| Physical mechanism | Unique-owner immutable Step-tail representation | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0025](./EXP-0025.md) | +| Successor experiment | None | + +## Decision Question + +Which unique-owner Step-tail representation reduces nonterminal current-Step ProofSize below the EXP-0025 allowance without making maximum Contract lifecycle, reconstruction, or projection non-dispatchable? + +## Governing Specification + +- `Exact section`: `template/pallets/actors/docs/specification.en.md` → `Hot/Cold Physical Contract`, `Complete Projection`, and `Active Execution State Geometry`. +- `Invariant`: One semantic ordered Contract has one immutable physical body owner per Step, reconstructs exactly, loads only the current execution representation, fails closed on stale or missing fragments, and preserves atomic create/update/close. +- `Reopen trigger`: A semantic defect or impossibility shared by all bounded unique-owner representations reopens the Specification; a performance loss rejects only the candidate. + +## Context and Hypothesis + +- `Current architecture`: Step 0 is uniquely inline in `ActorContractHead`; Steps 1..31 and their resource envelopes are stored in four-Step `ActorContractTailChunk` values. +- `Reason`: EXP-0025's semantically exact 100-Actor middle Transfer projection is `6,876.52` estimated proof/Actor against the `6,361` allowance. Its generated slope is `6,553`; the initial tail maximum annotation is `6,545`, while C1/C2 controls now identify complete current-plus-successor authority as the actual owner. +- `Hypothesis`: Capacity-only and one-Step fragmentation were falsified first. C3 then tested whether one current payload owner plus shifted Contract-bound successor resources could remove at least `515.52` proof/Actor; exact state-hold reconciliation falsified that residual hypothesis. +- `Materiality`: The primary gate is exact: grouped `3×32 + 4` middle Transfer must be at most `636,100` estimated proof. Every semantic divergence, added hot mutable owner, or non-dispatchable maximum lifecycle branch rejects a candidate. Among passing candidates, compare component-wise RefTime, ProofSize, reads/writes, retained bytes, and Wasm size without an arbitrary percentage floor. + +## Baseline and Candidates + +| ID | Architecture | Source tree | Runtime/config identity | +| --- | --- | --- | --- | +| B0 | Inline Step 0 plus unique-owner four-Step tail chunks | EXP-0025 manifest `77d79973`; full Actors-source manifest `fbbcbae9` | 32-Step benchmark geometry; fixed C32 control frames; benchmark Wasm `b3a11cf4` | +| C1 | Inline Step 0 plus unique-owner two-Step tail chunks | Full Actors-source manifest `8fedf45e`; measured then reverted | Same semantics, runtime limits, and EXP-0025 scheduler geometry; benchmark Wasm `44c3d8c3` | +| C2 | Inline Step 0 plus unique-owner one-Step tail cells | Full Actors-source manifest `45a93afa`; measured then reverted | Same semantics, runtime limits, and EXP-0025 scheduler geometry; benchmark Wasm `28de711e` | +| C3 | One-Step current payload carrying shifted successor resources | Full Actors-source manifest `a00c4ab9`; measured then deleted | Same semantics/config; candidate Wasm `7fee8271`; canonical tail remains required by exact state-hold reconciliation | + +## Controls + +- `Controlled variables`: Contract semantics and bytes, 32-Step ceiling, inline Step 0, C32 Actor control frames, FIFO/Q1 behavior, Actor identity and admission, Task/Predicate/effect profile, runtime limits, database/cache, toolchain, benchmark method, repeats, and correctness postconditions. +- `Changed variables`: Tail ownership unit and, for C3 only, the unique physical split between compact Step header and payload/resource authority. +- `Confounders`: EXP-0025 remains benchmark-only and unselected. Candidate code may alter benchmark Wasm; each complete matrix must rebuild and bind one matching Wasm. C3 adds a read and must not be compared as if payload loading were free. + +## Benchmark Class, Workloads, and Criteria + +- `Benchmark class`: Production-Wasm FRAME benchmark, exact retained-state profile, and deterministic differential/fault tests. + +| Workload | State/population geometry | Hot/cold assumptions | Purpose | +| --- | --- | --- | --- | +| W1 | EXP-0025 three-Step Running cohort, Step 1 Transfer, `n=1..32`, grouped `3×32 + 4` | Current tail cold; Ready source and successor chunks distinct | Primary `6,361` proof gate | +| W2 | 32-Step create/update/close and state-hold reconciliation | Maximum bounded body cold | Lifecycle dispatchability and write amplification | +| W3 | Current-Step loads at first and last slot of every tail unit | Selected tail cold | Boundary-independent proof/read set | +| W4 | Exact 0/1/2/3/32-Step reconstruction and projection | All owned fragments cold | Semantic completeness and reconstruction cost | +| W5 | Missing, swapped, stale-hash, and duplicate-fragment fixtures | Adversarial bounded state | Fail-closed ownership evidence | + +- `Metrics`: Generated RefTime and estimated/measured ProofSize; reads/writes; grouped 100-Actor cost; retained encoded bytes at 1/3/32 Steps; lifecycle dispatch fit; benchmark and production Wasm size; exact differential state/events/fees. +- `Statistical method`: Sequential compiled-Wasm RocksDB runs with 1,024 MiB cache, 50 steps, 20 repeats, and no sample removal. Retain generated coefficient standard errors. Deterministic encoded size and semantic postconditions have no noise band. +- `Acceptance criteria`: Exact A0/C1 semantics; grouped W1 estimated proof `<=636,100`; every maximum W2 call fits its production component-wise envelope; W3 has no unreachable-tail reads; W4 reconstructs exact bytes; W5 fails closed; and no duplicate mutable or semantic owner exists. Select from the conforming nondominated set by W1 proof, then lifecycle RefTime, retained bytes, Wasm size, and simplicity. +- `Rejection criteria`: W1 above allowance, semantic/event/fee drift, unbounded scan, stale or duplicated authority, current execution reading unreachable tails, or any maximum lifecycle/projection branch exceeding its dispatch envelope. + +## Environment and Artifact Identity + +| Artifact | Identity | +| --- | --- | +| Source commit | `7f767924e780a4ab032e2cc6799d8148f9abb9a2` | +| Source tree | B0 EXP-0025 code manifest `77d7997307135f298d8e648cbe89f61ee5bf33812dab4aca1e68c02bdd9e9d18`; full B0 Actors-source manifest `fbbcbae90f3c79b4883ed8656899d9cd99373cf1acd9f92a2becd4b6ceeae525`; C1 `8fedf45e370d793dc818357e9cd5e0b178976c61500547cc6f46bf4d67515e94`; C2 `45a93afa9239bc38dcab3e36d34b34c759b0efe3f9eb146374af284de17437ac`; C3 `a00c4ab934674e14c3f8d8e7a8bfa1ead6e9e5e1968d16009c7d2b7a225c71a1`; all candidates reverted/deleted after measurement | +| Rust toolchain | `rustc 1.98.0 (88d9e12ae 2026-08-18)`; `cargo 1.98.0 (797e8a9bc 2026-08-05)` | +| Benchmark runtime Wasm | B0 `b3a11cf4137b10d1dadafbef103a9a5104076c47f2704c1bc306ae954644ae8a`; C1 `44c3d8c3fb344f91455e548a633606999e04ab208a7d7ba4e508f6715d9fd530`; C2 `28de711eacea700b6884140605ce4328b760c2d51535c736a8439c704d800d64`; C3 `7fee827129fd3ae75bbb2397e9abe0fae9e1b9f11b6f31c627d66c59930c7e2b`; current source/Wasm restored to B0 | +| Post-generation production runtime Wasm | Not measured; candidate remains benchmark-only | +| Final release production runtime Wasm | Not measured | +| Generated Weight | B0 middle Transfer `/tmp/exp0025-c1-running-middle-transfer-batch32-v1.rs` SHA-256 `6c216bbe3a640c72387008ae57cc9eda397f81a8f0b3c67ecd9c277f2f378493`; C1 `/tmp/exp0004-c1-tail2-running-middle-transfer-batch32-v1.rs` `d95d27392cc75ace59d34ab9c8c1d882af0db43baed70b532f84e012ae5e9ae3`; C2 `/tmp/exp0004-c2-tail1-running-middle-transfer-batch32-v1.rs` `68c70e76023fd001660523407ce2ad216c11c215b23403b979730223e57779c9`; C3 `/tmp/exp0004-c3-compact-running-middle-transfer-batch32-v1.rs` `4e31ffcb00dcea5c2fa384542bf1e2263e34c8c1e3ed0549fce736b62f6af465` | +| Database/backend | RocksDB, 1,024 MiB cache, compiled Wasm, AMD Ryzen 7 4800H | +| Benchmark command/config | `./scripts/benchmarks.sh --extrinsic --output pallet_deos_actors`; first candidate branch rebuilds, later exact-manifest branches use `--skip-build`; 50 steps × 20 repeats | +| Raw evidence | B0 inline below and EXP-0025; candidate outputs pending. `Cargo.lock` `a1610860`; harness `f8b987db` | + +## Proof Obligations + +- `Freeze`: Frozen by explicit migration reconstruction from the pre-fission decision and retained witnesses at a60de7748132211d9e58cecd5be82b41f76ce230. No new measurement or historical decision is implied. +- `Review triggers`: None. +- `Decomposition review`: Boundary reviewed during provenance-preserving fission. The historical decision boundary and all original evidence remain unchanged. Metadata normalization neither fissions nor reinterprets this sealed result; downstream reuse consumes its original validity scope. Any newly earned question receives a new node. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | Which unique-owner Step-tail representation reduces nonterminal current-Step ProofSize below the EXP-0025 allowance without making maximum Contract lifecycle, reconstruction, or projection non-dispatchable? | One legal counterexample to the declared owner/domain or component-wise containment | Source-derived and bounded native/Wasm evidence as separately identified below | Self | Required | Named parent decision and declared downstream consumers | Invalidated; retained original decision and validity scope | + +## Measurements + +| Candidate | Workload | RefTime | ProofSize | Reads | Writes | Other decision metrics | +| --- | --- | ---: | ---: | ---: | ---: | --- | +| B0 | W1 n=32 | `35,772,854,048` | `217,784` estimated; `53,353` measured | 327 | 198 | Model `8,088 + 6,553n`; storage annotation adds `6,545` for a maximum tail value | +| B0 | W1 grouped 100 | `112,705,653,400` | `687,652` estimated; `168,724` measured | 1,028 | 624 | `6,876.52` estimated proof/Actor; misses by `515.52` | +| C1 | W1 n=32 | `35,848,217,584` | `217,784` estimated; `53,353` measured | 327 | 198 | Tail maximum shrinks `4,070 → 2,112` bytes, but the actual two-Step value and proof are unchanged | +| C1 | W1 grouped 100 | `112,944,708,200` | `687,652` estimated; `168,724` measured | 1,028 | 624 | `6,876.52` estimated proof/Actor; still misses by `515.52` | +| C2 | W1 n=32 | `36,922,238,499` | `222,296` estimated; `57,865` measured | 359 | 198 | Two one-Step tail keys per Actor replace one two-Step key | +| C2 | W1 grouped 100 | `116,064,688,932` | `701,752` estimated; `182,824` measured | 1,128 | 624 | `7,017.52` estimated proof/Actor; misses by `656.52` | +| C3 | W1 n=32 | `35,630,885,448` | `225,034` estimated; `60,603` measured | 359 | 198 | Compact payload is read, but exact state-hold reconciliation still reads canonical tail geometry | +| C3 | W1 grouped 100 | `112,249,839,900` | `710,324` estimated; `191,396` measured | 1,128 | 624 | `7,103.24` estimated proof/Actor; misses by `742.24` | + +## Derived Metrics + +| Candidate | Workload | RefTime delta | ProofSize delta | Throughput / latency | Marginal or amortized cost | Pareto class | +| --- | --- | ---: | ---: | --- | --- | --- | +| B0 | W1 grouped 100 | Baseline | `+51,552` above allowance | 100 semantic commits fit RefTime but not ProofSize | One actual two-Step tail value carries current payload plus successor payload/resources | Binding baseline | +| C1 | W1 grouped 100 | `+0.21%` | `0` | Fails target | Same actual two-Step tail value; maximum-capacity reduction is irrelevant to this workload | Dominated by B0 | +| C2 | W1 grouped 100 | `+2.76%` | `+141.00` proof/Actor | Fails target by `656.52` | `+1.00` read and `+141.00` estimated proof/Actor | Dominated by B0 | +| C3 | W1 grouped 100 | `-0.40%` | `+226.72` proof/Actor | Fails target by `742.24` | Adds one read and retains canonical tail proof for exact state hold | RefTime/proof tradeoff, rejected by gate | + +## Statistical / Variance Notes + +B0, C1, C2, and C3 each use a separately rebuilt matching benchmark Wasm with sequential 50-step/20-repeat runs and no sample removal. C1 keeps the exact B0 proof model `8,088 + 6,553n` because a three-Step Contract encodes Steps 1 and 2 into one actual two-Step tail value under both capacities. C2 changes the model to `8,088 + 6,694n` and adds one read per Actor because full Running reconstruction loads two one-Step values. C3 changes it to `8,106 + 6,779n`: it reads one compact payload, but `exp0025_reconcile_single_step_state_hold` independently reads the canonical tail to reproduce exact immutable contract-body hold bytes. Generated coefficient standard errors remain below the 2% materiality floor; all ProofSize failures are exact model outputs, not timing noise. + +## Result + +C1, C2, and C3 preserve the focused three-Step A0 differential but all fail the primary W1 gate. C1 is proof-identical to B0 and slightly slower; C2 raises grouped estimated proof to `7,017.52`; C3 raises it to `7,103.24` because exact state-hold reconciliation retains the canonical tail read alongside the compact payload. No candidate is selected. + +## Interpretation + +The initial maximum-value attribution was incomplete. The three-Step workload stores only two tail Steps, so B0 and C1 encode the same actual value and proof despite different `MaxEncodedLen`; C2 splits that value but full reconstruction reads both keys. C3 removes tail loading from Running control-context reconstruction, yet the exact User state-hold owner still reads canonical tail bytes and makes the composed path worse. Therefore Step representation alone cannot clear the gate under the retained hold algorithm. This does not implicate FIFO, Q1, or frame ownership, and combining a separately controlled state-hold optimization would violate EXP-0004's changed-variable boundary. + +## Decision + +Reject C1, C2, and C3 at W1 before lifecycle measurement. Retain B0 and mark EXP-0004 Rejected: no Step representation candidate clears `6,361` without changing the separately owned exact state-hold reconciliation mechanism. Candidate code is deleted and EXP-0025 B0 source/Wasm are restored. + +## Rejected Alternatives and Tradeoffs + +| Candidate | Decision | Evidence-backed reason | +| --- | --- | --- | +| Duplicate compact execution mirror | Excluded | Creates a second semantic owner and stale-authority risk | +| Load only a partial SCALE value from the existing storage item | Excluded | Trie values are proven and loaded as complete values; this does not reduce the storage proof bound | +| Change C32 FIFO frames first | Rejected as current gradient | W1 slope is owned by `ActorContractTailChunk`, not frame proof | +| Accept measured proof instead of estimated proof | Rejected | Production Weight must reserve the declared worst-case storage proof | +| C1 two-Step tail | Rejected | Same actual two-Step value and proof as B0, slightly higher RefTime, and unchanged target miss | +| C2 one-Step tail | Rejected | Full reconstruction loads two keys, adding one read and `141.00` proof/Actor | +| C3 compact payload | Rejected | Adds compact payload proof while exact state-hold reconciliation still reads canonical tail bytes; `7,103.24` exceeds B0 | +| Combine C3 with cached contract-body hold | Deferred outside EXP-0004 | Changes a separately accepted state-hold mechanism and would make the counterfactual non-additive | +| Build C3 before C1/C2 | Avoided | C3 was constructed only after bounded chunk-size controls failed | + +## Validity + +- `Establishes`: Exact B0 proof miss; C1 proof equivalence; C2 regression; C3 regression under exact state-hold reconciliation; and rejection of all admitted Step representation candidates for W1. +- `Does not establish`: A separately controlled state-hold reconciliation optimization, longer-Contract current-Step behavior, production schema selection, or EXP-0025 acceptance. +- `Known limitations`: C1/C2/C3 stop at the W1 three-Step Transfer Contract because each failed the primary rejection gate; W2–W5 were correctly not run. +- `Invalidation triggers`: Contract/Step/resource encoding, 32-Step ceiling, tail chunk size, storage proof model, runtime limits, benchmark method, C32 scheduler geometry, or task/effect profile changes. +- `Later evidence`: The governing assurance audit established that `6,361` is a W1 one-Step allowance rather than an independent middle-Step rejection ceiling, and the DEOS runtime subsequently changed the ceiling-dependent profile from `32/64/128` to `12/24/48`. These changes invalidate the selection claim while preserving the P32 representation measurements as historical constraint evidence. +- `Residual qualitative insight`: Current-Step locality is bounded by the maximum proven storage value, not only by the number of logical Steps consumed. + +## Follow-Up / Next Gradient + +```text +6,361 target → 6,876.52 middle-Step proof → 515.52 miss +→ two-Step capacity keeps the same actual value and proof +→ one-Step cells add one key/read and regress to 7,017.52 +→ complete current-plus-successor payload/resource authority +→ C3 compact payload removes control-context tail load +→ exact state hold independently retains canonical tail proof +→ C3 regresses to 7,103.24 and fails W1 +→ reject EXP-0004; retain B0 and named state-hold boundary +``` + +- `Eliminated hypotheses`: Frame chunk size is the current W1 slope owner; measured proof may replace estimated production proof; partial decode of one trie value reduces proof; two-Step capacity clears W1; one-Step fragmentation clears W1; compact current payload clears W1 while exact state-hold reconciliation is retained. +- `Remaining hypotheses`: None inside Step representation. A cached immutable contract-body hold component is a separate mechanism and requires its own controlled reopening before it can be composed. +- `Stop condition`: Met. All C1–C3 candidates fail grouped proof `<=636,100`; retain B0 and record `6,876.52` as the exact EXP-0025 blocker under current state-hold semantics. + +## Architecture Impact + +- `Affected physical truth`: Contract tail key/value geometry, current-Step loading, lifecycle mutation, reconstruction, projection, and state-hold accounting. +- `Architecture update`: None. B0 remains production truth; all benchmark-only candidate instrumentation is deleted. +- `Provenance citation`: Cite EXP-0004 only after an interpreted decision. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: None. + +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: EXP-0003's deferred Step-shape question under the accepted inline-Step-0/lazy-tail architecture. +- `Contradicts`: None. +- `Depends on`: [EXP-0025](./EXP-0025.md) middle-Step evidence and the governing Actor specification. +- `Baseline of`: Candidate C1/C2 Step-tail matrix. +- `Confirms`: EXP-0003 correctly deferred payload separation until an exact current-Step proof owner became material. +- `Invalidates`: None. +- `Uses evidence from`: Invalidated by: Corrected EXP-0025 W1 criterion ownership and the P12 DEOS reference profile; EXP-0026 owns the new physical baseline. +- `Transfers question to`: See Follow-Up / Next Gradient; no additional transfer declared. +- `Produces input for`: See Baseline of and Successor experiment; no additional consumer declared. +- `Reopen trigger`: The specific conditions in Governing Specification and Validity; preserve historical evidence and create a successor for a materially different question. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0005.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0005.md index 068d172b..6d30bade 100644 --- a/.agents/skills/architecture-experiments/tracks/actors/EXP-0005.md +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0005.md @@ -3,6 +3,9 @@ | Field | Value | | --- | --- | | Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | None | | 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 | @@ -76,6 +79,16 @@ Which single-owner representation minimizes per-block current-Step control proof | 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` | +## Proof Obligations + +- `Freeze`: Frozen by explicit migration reconstruction from the pre-fission decision and retained witnesses at a60de7748132211d9e58cecd5be82b41f76ce230. No new measurement or historical decision is implied. +- `Review triggers`: None. +- `Decomposition review`: Boundary reviewed during provenance-preserving fission. The historical decision boundary and all original evidence remain unchanged. Metadata normalization neither fissions nor reinterprets this sealed result; downstream reuse consumes its original validity scope. Any newly earned question receives a new node. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | Which single-owner representation minimizes per-block current-Step control proof and mutation cost without duplicating Step semantics or weakening lifecycle authority? | One legal counterexample to the declared owner/domain or component-wise containment | Source-derived and bounded native/Wasm evidence as separately identified below | Self | Required | Named parent decision and declared downstream consumers | Accepted; retained original decision and validity scope | + ## Measurements | Candidate | Workload | RefTime | ProofSize | Reads | Writes | Other decision metrics | @@ -205,6 +218,9 @@ about 14.7 committed distinct Actors/block → decompose authority proofs ## Relations +- `Decomposes into`: None. +- `Satisfies obligation`: None. + - `Replaces`: None. - `Supersedes`: None. - `Refines`: EXP-0013 loaded-state reuse. @@ -213,3 +229,7 @@ about 14.7 committed distinct Actors/block → decompose authority proofs - `Baseline of`: None. - `Confirms`: None. - `Invalidates`: None. +- `Uses evidence from`: See Baseline and Candidates and Environment and Artifact Identity; historical inputs are not automatically acceptance dependencies. +- `Transfers question to`: See Follow-Up / Next Gradient; no additional transfer declared. +- `Produces input for`: See Baseline of and Successor experiment; no additional consumer declared. +- `Reopen trigger`: The specific conditions in Governing Specification and Validity; preserve historical evidence and create a successor for a materially different question. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0006.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0006.md index 3e3cdf5e..b831efdd 100644 --- a/.agents/skills/architecture-experiments/tracks/actors/EXP-0006.md +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0006.md @@ -3,6 +3,9 @@ | Field | Value | | --- | --- | | Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | None | | Architecture release / experiment campaign | DEOS 0.7.24 optimization campaign | | Date | 2026-08-30 | | Affected domain | Actor Control and Shared Economic block allocation | @@ -74,6 +77,16 @@ Does `20/80`, `25/75`, or `30/70` maximize useful Actor progress while retaining | 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` | +## Proof Obligations + +- `Freeze`: Frozen by explicit migration reconstruction from the pre-fission decision and retained witnesses at a60de7748132211d9e58cecd5be82b41f76ce230. No new measurement or historical decision is implied. +- `Review triggers`: None. +- `Decomposition review`: Boundary reviewed during provenance-preserving fission. The historical decision boundary and all original evidence remain unchanged. Metadata normalization neither fissions nor reinterprets this sealed result; downstream reuse consumes its original validity scope. Any newly earned question receives a new node. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | Does `20/80`, `25/75`, or `30/70` maximize useful Actor progress while retaining explicit user and Actor effect capacity? | One legal counterexample to the declared owner/domain or component-wise containment | Source-derived and bounded native/Wasm evidence as separately identified below | Self | Required | Named parent decision and declared downstream consumers | Accepted; retained original decision and validity scope | + ## Measurements | Candidate | Workload | RefTime | ProofSize | Reads | Writes | Other decision metrics | @@ -176,6 +189,9 @@ Accept A2 as the DEOS reference allocation: 30% Actor Control, 70% Shared Econom ## Relations +- `Decomposes into`: None. +- `Satisfies obligation`: None. + - `Replaces`: None. - `Supersedes`: None. - `Refines`: EXP-0013 production throughput pressure. @@ -184,3 +200,7 @@ Accept A2 as the DEOS reference allocation: 30% Actor Control, 70% Shared Econom - `Baseline of`: None. - `Confirms`: None. - `Invalidates`: None. +- `Uses evidence from`: See Baseline and Candidates and Environment and Artifact Identity; historical inputs are not automatically acceptance dependencies. +- `Transfers question to`: See Follow-Up / Next Gradient; no additional transfer declared. +- `Produces input for`: See Baseline of and Successor experiment; no additional consumer declared. +- `Reopen trigger`: The specific conditions in Governing Specification and Validity; preserve historical evidence and create a successor for a materially different question. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0008.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0008.md index 17efd045..f6659c9a 100644 --- a/.agents/skills/architecture-experiments/tracks/actors/EXP-0008.md +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0008.md @@ -3,6 +3,9 @@ | Field | Value | | --- | --- | | Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | None | | Architecture release / experiment campaign | DEOS 0.7.24 | | Date | 2026-08-29 | | Affected domain | DEOS Actors ObservationCrossing membership topology | @@ -75,6 +78,16 @@ Which production `ObservationPageSize` among 64, 128, and 256 minimizes complete | 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 | +## Proof Obligations + +- `Freeze`: Frozen by explicit migration reconstruction from the pre-fission decision and retained witnesses at a60de7748132211d9e58cecd5be82b41f76ce230. No new measurement or historical decision is implied. +- `Review triggers`: None. +- `Decomposition review`: Boundary reviewed during provenance-preserving fission. The historical decision boundary and all original evidence remain unchanged. Metadata normalization neither fissions nor reinterprets this sealed result; downstream reuse consumes its original validity scope. Any newly earned question receives a new node. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | 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? | One legal counterexample to the declared owner/domain or component-wise containment | Source-derived and bounded native/Wasm evidence as separately identified below | Self | Required | Named parent decision and declared downstream consumers | Accepted; retained original decision and validity scope | + ## Measurements | Candidate | Workload | RefTime | ProofSize | Reads | Writes | Other decision metrics | @@ -150,6 +163,9 @@ four-candidate production ceiling → page/refill pressure ## Relations +- `Decomposes into`: None. +- `Satisfies obligation`: None. + - `Replaces`: None. - `Supersedes`: None. - `Refines`: EXP-0010 compact Observation activation authority. @@ -158,3 +174,7 @@ four-candidate production ceiling → page/refill pressure - `Baseline of`: EXP-0009. - `Confirms`: None yet. - `Invalidates`: None. +- `Uses evidence from`: See Baseline and Candidates and Environment and Artifact Identity; historical inputs are not automatically acceptance dependencies. +- `Transfers question to`: See Follow-Up / Next Gradient; no additional transfer declared. +- `Produces input for`: See Baseline of and Successor experiment; no additional consumer declared. +- `Reopen trigger`: The specific conditions in Governing Specification and Validity; preserve historical evidence and create a successor for a materially different question. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0009.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0009.md index ce769deb..7fd21628 100644 --- a/.agents/skills/architecture-experiments/tracks/actors/EXP-0009.md +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0009.md @@ -3,6 +3,9 @@ | Field | Value | | --- | --- | | Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | None | | Architecture release / experiment campaign | DEOS 0.7.24 | | Date | 2026-08-29 | | Affected domain | DEOS Actors ObservationCrossing materialization | @@ -79,6 +82,16 @@ Does a P128 page with a generated 128-candidate Crossing cohort fit every comple | 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` | +## Proof Obligations + +- `Freeze`: Frozen by explicit migration reconstruction from the pre-fission decision and retained witnesses at a60de7748132211d9e58cecd5be82b41f76ce230. No new measurement or historical decision is implied. +- `Review triggers`: None. +- `Decomposition review`: Boundary reviewed during provenance-preserving fission. The historical decision boundary and all original evidence remain unchanged. Metadata normalization neither fissions nor reinterprets this sealed result; downstream reuse consumes its original validity scope. Any newly earned question receives a new node. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | 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? | One legal counterexample to the declared owner/domain or component-wise containment | Source-derived and bounded native/Wasm evidence as separately identified below | Self | Required | Named parent decision and declared downstream consumers | Accepted; retained original decision and validity scope | + ## Measurements | Candidate | Workload | RefTime | ProofSize | Reads | Writes | Other decision metrics | @@ -169,6 +182,9 @@ P128 target-enabling page → C128 W10 ProofSize failure ## Relations +- `Decomposes into`: None. +- `Satisfies obligation`: None. + - `Replaces`: None. - `Supersedes`: None. - `Refines`: EXP-0010 compact activation and EXP-0008 page geometry. @@ -177,3 +193,7 @@ P128 target-enabling page → C128 W10 ProofSize failure - `Baseline of`: Crossing production implementation and T8 acceptance. - `Confirms`: None yet. - `Invalidates`: None. +- `Uses evidence from`: See Baseline and Candidates and Environment and Artifact Identity; historical inputs are not automatically acceptance dependencies. +- `Transfers question to`: See Follow-Up / Next Gradient; no additional transfer declared. +- `Produces input for`: See Baseline of and Successor experiment; no additional consumer declared. +- `Reopen trigger`: The specific conditions in Governing Specification and Validity; preserve historical evidence and create a successor for a materially different question. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0010.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0010.md index 07a15420..e71929e6 100644 --- a/.agents/skills/architecture-experiments/tracks/actors/EXP-0010.md +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0010.md @@ -3,6 +3,9 @@ | Field | Value | | --- | --- | | Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | None | | 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 | @@ -90,6 +93,16 @@ A candidate is conforming only if every duplicated field remains an explicitly d | 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 | +## Proof Obligations + +- `Freeze`: Frozen by explicit migration reconstruction from the pre-fission decision and retained witnesses at a60de7748132211d9e58cecd5be82b41f76ce230. No new measurement or historical decision is implied. +- `Review triggers`: None. +- `Decomposition review`: Boundary reviewed during provenance-preserving fission. The historical decision boundary and all original evidence remain unchanged. Metadata normalization neither fissions nor reinterprets this sealed result; downstream reuse consumes its original validity scope. Any newly earned question receives a new node. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | 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? | One legal counterexample to the declared owner/domain or component-wise containment | Source-derived and bounded native/Wasm evidence as separately identified below | Self | Required | Named parent decision and declared downstream consumers | Accepted; retained original decision and validity scope | + ## Measurements | Candidate | Workload | RefTime | ProofSize | Reads | Writes | Other decision metrics | @@ -160,6 +173,7 @@ Accepted and integrated. Candidate A is the physical compact Observation activat ## Validity +- `Historical qualification 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. This records that earlier checkpoint; current disposition is the metadata Status and Decision above. - `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. @@ -187,6 +201,9 @@ P32 ceiling → fanout proof 300,510 → 331,998 → service 3 → 2 ## Relations +- `Decomposes into`: None. +- `Satisfies obligation`: None. + - `Replaces`: None. - `Supersedes`: None. - `Refines`: [EXP-0002](./EXP-0002.md) omitted reactive short-path evidence. @@ -195,4 +212,7 @@ P32 ceiling → fanout proof 300,510 → 331,998 → service 3 → 2 - `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. +- `Uses evidence from`: See Baseline and Candidates and Environment and Artifact Identity; historical inputs are not automatically acceptance dependencies. +- `Transfers question to`: See Follow-Up / Next Gradient; no additional transfer declared. +- `Produces input for`: See Baseline of and Successor experiment; no additional consumer declared. +- `Reopen trigger`: The specific conditions in Governing Specification and Validity; preserve historical evidence and create a successor for a materially different question. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0013.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0013.md index 44db595f..db2765e2 100644 --- a/.agents/skills/architecture-experiments/tracks/actors/EXP-0013.md +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0013.md @@ -3,6 +3,9 @@ | Field | Value | | --- | --- | | Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | None | | 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 | @@ -77,6 +80,16 @@ B0/D1 identified the question. B1/R1 is the controlled production-Wasm decision | 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/` | +## Proof Obligations + +- `Freeze`: Frozen by explicit migration reconstruction from the pre-fission decision and retained witnesses at a60de7748132211d9e58cecd5be82b41f76ce230. No new measurement or historical decision is implied. +- `Review triggers`: None. +- `Decomposition review`: Boundary reviewed during provenance-preserving fission. The historical decision boundary and all original evidence remain unchanged. Metadata normalization neither fissions nor reinterprets this sealed result; downstream reuse consumes its original validity scope. Any newly earned question receives a new node. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | 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? | One legal counterexample to the declared owner/domain or component-wise containment | Source-derived and bounded native/Wasm evidence as separately identified below | Self | Required | Named parent decision and declared downstream consumers | Accepted; retained original decision and validity scope | + ## Measurements | Measured owner | RefTime model | Measured / estimated ProofSize | Reads | Writes | Scope | @@ -147,6 +160,7 @@ Accept R1 as the production loaded-state reuse baseline. Its `17.87%` marginal R ## Validity +- `Historical qualification 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. This records that earlier checkpoint; current disposition is the metadata Status and Decision above. - `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. @@ -165,9 +179,18 @@ R1 is production architecture: scheduler service reuses the canonical state and ## 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. +- `Decomposes into`: None. +- `Satisfies obligation`: None. + +- `Replaces`: None. - `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. +- `Refines`: EXP-0016 C6 implementation by reusing its canonical loaded owners without changing geometry. +- `Contradicts`: None. +- `Depends on`: None. +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: Baseline: EXP-0001 throughput and fairness result. +- `Transfers question to`: See Follow-Up / Next Gradient; no additional transfer declared. +- `Produces input for`: 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. +- `Reopen trigger`: The specific conditions in Governing Specification and Validity; preserve historical evidence and create a successor for a materially different question. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0016.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0016.md index da3b455e..2ba9202f 100644 --- a/.agents/skills/architecture-experiments/tracks/actors/EXP-0016.md +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0016.md @@ -3,6 +3,9 @@ | Field | Value | | --- | --- | | Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | None | | 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 | @@ -107,6 +110,16 @@ sha256sum "$out"/* template/target/benchmarks/release/wbuild/deos-runtime/deos_r The first C6 B0 command builds the matching Wasm; all later C6 commands reuse it. Source-manifest drift invalidates the matrix before Measuring. +## Proof Obligations + +- `Freeze`: Frozen by explicit migration reconstruction from the pre-fission decision and retained witnesses at a60de7748132211d9e58cecd5be82b41f76ce230. No new measurement or historical decision is implied. +- `Review triggers`: None. +- `Decomposition review`: Boundary reviewed during provenance-preserving fission. The historical decision boundary and all original evidence remain unchanged. Metadata normalization neither fissions nor reinterprets this sealed result; downstream reuse consumes its original validity scope. Any newly earned question receives a new node. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | 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? | One legal counterexample to the declared owner/domain or component-wise containment | Source-derived and bounded native/Wasm evidence as separately identified below | Self | Required | Named parent decision and declared downstream consumers | Accepted; retained original decision and validity scope | + ## Measurements The complete Prepared matrix ran once from manifest `4cc1dd50` and matching benchmark Wasm `c9c09e3f`. Models exclude database Weight additions shown separately. @@ -171,6 +184,7 @@ Accepted. C6 is the selected I2 production geometry: one uniquely owned inline S ## Validity +- `Historical qualification 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. This records that earlier checkpoint; current disposition is the metadata Status and Decision above. - `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. @@ -186,8 +200,18 @@ No architecture-document edit yet. The interpreted selection constrains I2 imple ## 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. +- `Decomposes into`: None. +- `Satisfies obligation`: None. + +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: None. +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: Predecessor: EXP-0003 rejects naive C1 and supplies B0/C1 evidence. +- `Transfers question to`: Conditional relation: EXP-0004 only if payload/Step-shape proof remains binding after body ownership converges. +- `Produces input for`: Constrains: EXP-0002 ceiling comparison and I2 Selected Geometry. 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. +- `Reopen trigger`: The specific conditions in Governing Specification and Validity; preserve historical evidence and create a successor for a materially different question. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0017.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0017.md index bfd73831..4af22fb9 100644 --- a/.agents/skills/architecture-experiments/tracks/actors/EXP-0017.md +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0017.md @@ -3,6 +3,9 @@ | Field | Value | | --- | --- | | Status | Proposed | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | None | | 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 | @@ -18,6 +21,7 @@ After the converged Q1 path is fully optimized and measured, does remaining cont ## Governing Specification +- `Current C1 boundary`: [EXP-0025](./EXP-0025.md) freezes Q1. The historical cost/target-based trigger below no longer authorizes Q2/Q4 preparation in 0.7.25; a new experiment must first prove one of C1's qualifying invariant/impossibility failures. This record remains dormant and preserves the old inquiry for provenance. - `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. @@ -80,6 +84,16 @@ After the converged Q1 path is fully optimized and measured, does remaining cont | 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 | +## Proof Obligations + +- `Freeze`: Not frozen; finite required inputs declared before preparation. +- `Review triggers`: None. +- `Decomposition review`: Boundary reviewed during provenance-preserving fission. The historical decision boundary and all original evidence remain unchanged. Metadata normalization neither fissions nor reinterprets this sealed result; downstream reuse consumes its original validity scope. Any newly earned question receives a new node. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | 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? | One legal counterexample to the declared owner/domain or component-wise containment | Source-derived and bounded native/Wasm evidence as separately identified below | Self | Required | Named parent decision and declared downstream consumers | Proposed; retained original decision and validity scope | + ## Measurements | Candidate | Workload | RefTime | ProofSize | Reads | Writes | Other decision metrics | @@ -127,6 +141,7 @@ Proposed and explicitly dormant. No physical baseline changes; Q1/T+1 remains au ## Validity +- `Historical qualification under EXP-0019`: Current Category A dormant question. Any future reopen uses the post-apoptosis Q1 baseline; no pre-deletion rerun is warranted now. This records that earlier checkpoint; current disposition is the metadata Status and Decision above. - `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. @@ -154,6 +169,9 @@ Proposed and explicitly dormant. No physical baseline changes; Q1/T+1 remains au ## Relations +- `Decomposes into`: None. +- `Satisfies obligation`: None. + - `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. @@ -162,4 +180,7 @@ Proposed and explicitly dormant. No physical baseline changes; Q1/T+1 remains au - `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. +- `Uses evidence from`: See Baseline and Candidates and Environment and Artifact Identity; historical inputs are not automatically acceptance dependencies. +- `Transfers question to`: See Follow-Up / Next Gradient; no additional transfer declared. +- `Produces input for`: See Baseline of and Successor experiment; no additional consumer declared. +- `Reopen trigger`: The specific conditions in Governing Specification and Validity; preserve historical evidence and create a successor for a materially different question. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0018.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0018.md index 08d9d231..f493a1cb 100644 --- a/.agents/skills/architecture-experiments/tracks/actors/EXP-0018.md +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0018.md @@ -3,6 +3,9 @@ | Field | Value | | --- | --- | | Status | Rejected | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | None | | 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 | @@ -77,6 +80,16 @@ Can one bounded same-block Fresh Step-0 opportunity reduce Trigger-to-first-Step | 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 | +## Proof Obligations + +- `Freeze`: Frozen by explicit migration reconstruction from the pre-fission decision and retained witnesses at a60de7748132211d9e58cecd5be82b41f76ce230. No new measurement or historical decision is implied. +- `Review triggers`: None. +- `Decomposition review`: Boundary reviewed during provenance-preserving fission. The historical decision boundary and all original evidence remain unchanged. Metadata normalization neither fissions nor reinterprets this sealed result; downstream reuse consumes its original validity scope. Any newly earned question receives a new node. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | 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? | One legal counterexample to the declared owner/domain or component-wise containment | Source-derived and bounded native/Wasm evidence as separately identified below | Self | Required | Named parent decision and declared downstream consumers | Rejected; retained original decision and validity scope | + ## Measurements | Candidate | Workload | RefTime | ProofSize | Reads | Writes | Other decision metrics | @@ -137,6 +150,7 @@ Rejected. Production retains one protocol-fixed next-block eligibility floor; N+ ## Validity +- `Historical qualification under EXP-0019`: Current Category A proposal, but no candidate may be constructed against the pre-deletion User lifecycle baseline. This records that earlier checkpoint; current disposition is the metadata Status and Decision above. - `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. @@ -163,6 +177,9 @@ low-latency causal reaction target → T+1 baseline latency ## Relations +- `Decomposes into`: None. +- `Satisfies obligation`: None. + - `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. @@ -171,4 +188,7 @@ low-latency causal reaction target → T+1 baseline latency - `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. +- `Uses evidence from`: See Baseline and Candidates and Environment and Artifact Identity; historical inputs are not automatically acceptance dependencies. +- `Transfers question to`: See Follow-Up / Next Gradient; no additional transfer declared. +- `Produces input for`: See Baseline of and Successor experiment; no additional consumer declared. +- `Reopen trigger`: The specific conditions in Governing Specification and Validity; preserve historical evidence and create a successor for a materially different question. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0019.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0019.md index 9e34fc44..3da41046 100644 --- a/.agents/skills/architecture-experiments/tracks/actors/EXP-0019.md +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0019.md @@ -3,6 +3,9 @@ | Field | Value | | --- | --- | | Status | Invalidated | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | None | | Architecture release / experiment campaign | DEOS 0.7.24 | | Date | 2026-08-28 | | Affected domain | DEOS Actors User lifecycle, scheduler admission, custody, and state economics | @@ -80,6 +83,16 @@ Which Pipeline Machine pricing strategy best preserves one paid whole-pipeline c | 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` | +## Proof Obligations + +- `Freeze`: Frozen by explicit migration reconstruction from the pre-fission decision and retained witnesses at a60de7748132211d9e58cecd5be82b41f76ce230. No new measurement or historical decision is implied. +- `Review triggers`: None. +- `Decomposition review`: Boundary reviewed during provenance-preserving fission. The historical decision boundary and all original evidence remain unchanged. Metadata normalization neither fissions nor reinterprets this sealed result; downstream reuse consumes its original validity scope. Any newly earned question receives a new node. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | 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? | One legal counterexample to the declared owner/domain or component-wise containment | Source-derived and bounded native/Wasm evidence as separately identified below | Self | Required | Named parent decision and declared downstream consumers | Invalidated; retained original decision and validity scope | + ## Measurements | Candidate | Workload | RefTime | ProofSize | Reads | Writes | Other decision metrics | @@ -228,6 +241,9 @@ current-Step death → admitted-pipeline safety defect → split Trigger occurre ## Relations +- `Decomposes into`: None. +- `Satisfies obligation`: None. + - `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. @@ -236,3 +252,7 @@ current-Step death → admitted-pipeline safety defect → split Trigger occurre - `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. +- `Uses evidence from`: See Baseline and Candidates and Environment and Artifact Identity; historical inputs are not automatically acceptance dependencies. +- `Transfers question to`: See Follow-Up / Next Gradient; no additional transfer declared. +- `Produces input for`: See Baseline of and Successor experiment; no additional consumer declared. +- `Reopen trigger`: The specific conditions in Governing Specification and Validity; preserve historical evidence and create a successor for a materially different question. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0020.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0020.md index b1d377fb..72273dd7 100644 --- a/.agents/skills/architecture-experiments/tracks/actors/EXP-0020.md +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0020.md @@ -3,6 +3,9 @@ | Field | Value | | --- | --- | | Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | None | | Architecture release / experiment campaign | DEOS 0.7.24 | | Date | 2026-08-28 | | Affected domain | DEOS Actors Trigger detection, readiness, fees, and Pipeline Opening | @@ -77,6 +80,16 @@ Which bounded detector disable/re-arm topology enforces zero Actor-specific Trig | 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 | +## Proof Obligations + +- `Freeze`: Frozen by explicit migration reconstruction from the pre-fission decision and retained witnesses at a60de7748132211d9e58cecd5be82b41f76ce230. No new measurement or historical decision is implied. +- `Review triggers`: None. +- `Decomposition review`: Boundary reviewed during provenance-preserving fission. The historical decision boundary and all original evidence remain unchanged. Metadata normalization neither fissions nor reinterprets this sealed result; downstream reuse consumes its original validity scope. Any newly earned question receives a new node. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | 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? | One legal counterexample to the declared owner/domain or component-wise containment | Source-derived and bounded native/Wasm evidence as separately identified below | Self | Required | Named parent decision and declared downstream consumers | Accepted; retained original decision and validity scope | + ## Measurements | Candidate | Workload | RefTime | ProofSize | Reads | Writes | Other decision metrics | @@ -153,6 +166,9 @@ paid coalescence → fee-DoS risk → useful-transition latch ## Relations +- `Decomposes into`: None. +- `Satisfies obligation`: None. + - `Replaces`: EXP-0019 paid-coalescence Trigger boundary only. - `Supersedes`: None. - `Refines`: EXP-0010 compact Observation activation and EXP-0016 C6 Opening geometry. @@ -161,3 +177,7 @@ paid coalescence → fee-DoS risk → useful-transition latch - `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. +- `Uses evidence from`: See Baseline and Candidates and Environment and Artifact Identity; historical inputs are not automatically acceptance dependencies. +- `Transfers question to`: See Follow-Up / Next Gradient; no additional transfer declared. +- `Produces input for`: See Baseline of and Successor experiment; no additional consumer declared. +- `Reopen trigger`: The specific conditions in Governing Specification and Validity; preserve historical evidence and create a successor for a materially different question. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0021.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0021.md index d697dcdb..b4bd9c87 100644 --- a/.agents/skills/architecture-experiments/tracks/actors/EXP-0021.md +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0021.md @@ -2,7 +2,10 @@ | Field | Value | | --- | --- | -| Status | Accepted | +| Status | Superseded | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | None | | Architecture release / experiment campaign | DEOS 0.7.24 optimization campaign | | Date | 2026-08-30 | | Affected domain | Actor Contract maximum geometry | @@ -10,7 +13,7 @@ | Primary track | [actors](./experiments.md) | | Related tracks | None | | Baseline experiment | EXP-0002 | -| Successor experiment | None | +| Successor experiment | EXP-0026 proposed | ## Decision Question @@ -75,6 +78,16 @@ Do ceilings of 24 or 16 Steps produce material physical savings over 32 without | 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` | +## Proof Obligations + +- `Freeze`: Frozen by explicit migration reconstruction from the pre-fission decision and retained witnesses at a60de7748132211d9e58cecd5be82b41f76ce230. No new measurement or historical decision is implied. +- `Review triggers`: None. +- `Decomposition review`: Boundary reviewed during provenance-preserving fission. The historical decision boundary and all original evidence remain unchanged. Metadata normalization neither fissions nor reinterprets this sealed result; downstream reuse consumes its original validity scope. Any newly earned question receives a new node. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | Do ceilings of 24 or 16 Steps produce material physical savings over 32 without an unjustified loss of Contract expressiveness? | One legal counterexample to the declared owner/domain or component-wise containment | Source-derived and bounded native/Wasm evidence as separately identified below | Self | Required | Named parent decision and declared downstream consumers | Superseded; retained original decision and validity scope | + ## Measurements | Candidate | Workload | RefTime | ProofSize | Reads | Writes | Other decision metrics | @@ -129,8 +142,9 @@ Retain `MaxContractSteps = 32`. Reject 24 and 16 as materially cheaper maximum p - `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. +- `Invalidation triggers`: Changed chunk size, Contract layout, Q1, metadata representation, lifecycle owners, or a concrete DEOS reference-product decision that establishes a lower sufficient ceiling. +- `Later decision`: The DEOS reference profile now selects 12 Steps as sufficient local linear-program expressiveness, with larger composition represented by Actor graphs. This product/runtime policy supersedes the former absence-of-sufficiency premise without invalidating the measured 32/24/16 maximum-geometry evidence. EXP-0026 must measure the P12 physical consequences under current EXP-0025 geometry. +- `Residual qualitative insight`: A ceiling reduction is a storage/lifecycle choice and may also shrink dependent maximum-proven values; no ordinary-path gain may be claimed until measured. ## Follow-Up / Next Gradient @@ -152,6 +166,9 @@ Retain `MaxContractSteps = 32`. Reject 24 and 16 as materially cheaper maximum p ## Relations +- `Decomposes into`: None. +- `Satisfies obligation`: None. + - `Replaces`: None. - `Supersedes`: None. - `Refines`: EXP-0002 under the final C6/resource baseline. @@ -160,3 +177,7 @@ Retain `MaxContractSteps = 32`. Reject 24 and 16 as materially cheaper maximum p - `Baseline of`: None. - `Confirms`: None. - `Invalidates`: None. +- `Uses evidence from`: Superseded by: EXP-0026 and the selected 12-Step DEOS reference profile. +- `Transfers question to`: See Follow-Up / Next Gradient; no additional transfer declared. +- `Produces input for`: See Baseline of and Successor experiment; no additional consumer declared. +- `Reopen trigger`: The specific conditions in Governing Specification and Validity; preserve historical evidence and create a successor for a materially different question. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0022.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0022.md index f7396054..dbea3ed7 100644 --- a/.agents/skills/architecture-experiments/tracks/actors/EXP-0022.md +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0022.md @@ -2,7 +2,10 @@ | Field | Value | | --- | --- | -| Status | Accepted | +| Status | Invalidated | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | None | | Architecture release / experiment campaign | DEOS 0.7.24 optimization campaign | | Date | 2026-08-30 | | Affected domain | Actor Control and Shared Economic block allocation | @@ -10,7 +13,7 @@ | Primary track | [actors](./experiments.md) | | Related tracks | None | | Baseline experiment | EXP-0006 | -| Successor experiment | None | +| Successor experiment | EXP-0025 after saturated resource-finalization repair | ## Decision Question @@ -73,6 +76,16 @@ Does exact one-third Actor Control with the Shared remainder split equally betwe | 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` | +## Proof Obligations + +- `Freeze`: Frozen by explicit migration reconstruction from the pre-fission decision and retained witnesses at a60de7748132211d9e58cecd5be82b41f76ce230. No new measurement or historical decision is implied. +- `Review triggers`: None. +- `Decomposition review`: Boundary reviewed during provenance-preserving fission. The historical decision boundary and all original evidence remain unchanged. Metadata normalization neither fissions nor reinterprets this sealed result; downstream reuse consumes its original validity scope. Any newly earned question receives a new node. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | 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? | One legal counterexample to the declared owner/domain or component-wise containment | Source-derived and bounded native/Wasm evidence as separately identified below | Self | Required | Named parent decision and declared downstream consumers | Invalidated; retained original decision and validity scope | + ## Measurements | Candidate | Workload | RefTime | ProofSize | Reads | Writes | Other decision metrics | @@ -122,11 +135,11 @@ Accept A1 as the production resource partition. Actor Control is floor one third ## 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. +- `Invalidation finding`: The 0.7.25 W1/W2 per-block collector reproduced first-population coverage at block `679` but found `optional_actor_work_halted = true` and no current-block `FinalizedBlockResourceTelemetry` in every measured saturated block; the runtime `on_finalize` contract would therefore reject these blocks. The historical completion counters are not valid production-block throughput evidence. +- `Retained observation`: Blocks `2..=679` contain 10,004 committed one-Step dispositions over 678 measured blocks, mean `14.7552`, because four Actors recur before complete population coverage; the old `10,000 / 679 = 14.73` quotient was neither exact committed-Step throughput nor an eligible-block mean. +- `Residual qualitative insight`: W4 still proves the equal-thirds arithmetic, and the normative resource policy independently fixes one third as the maximum Actor Control share. The A0/A1 completion comparison may guide diagnosis but no longer supports a production-performance acceptance claim. +- `Successor evidence`: The 0.7.25 repair adds a dedicated component-wise control meter, withholds generated Drain/finalization control before Prepass service, and proves current-block telemetry plus successful `on_finalize`. Repaired W1/W2 cover 10,000 Actors at block `1099` over 1,098 measured blocks with exactly 10,000 committed Steps/Openings/completions, mean `9.1075`, p50 `7`, p95/p99 `21`, range `0..=21`, zero failed Steps/closures/Drain Steps/halts, and Actor Control as the exact stop reason in all 1,098 measured blocks. The stable report includes the complete Step histogram and finalized domain counters. A 10,000-Actor control-only `StopCycle` run has the exact Transfer histogram with zero effect Weight and Actor Control as all 1,098 stop reasons, directly falsifying effect capacity as the baseline binding owner. Proof-saturated Transfer, zero-effect `StopCycle`, and Router-backed `SwapOut` W2 legs each submit 77 successful signed remarks per block, consume `673,442 / 676,150` User ProofSize with 2,708 remaining, and leave every Actor counter and histogram bin unchanged; RefTime saturation remains pending. This valid successor baseline does not retroactively restore the invalidated comparison. Regenerated Weight is `ac0ff2ca56fef3177b8aa73f914d021559afa059e3edc3026fcee19550671c71`, production Wasm is `5bd3271940df297201a40ce5d25db25c827d4fb1d3c1841bce050c220ee7042d`, and metadata is `78e21e8ed661ffac621115214786b416d9bc030e443456531956ef1f27120b0e`; exact commit/tree binding remains pending. +- `Required renewal`: Bind the repaired source, Weight, Wasm, and metadata identities; complete the user RefTime, control-only, and Router-backed saturation legs before EXP-0025 preparation. ## Follow-Up / Next Gradient @@ -147,6 +160,9 @@ accepted 30/35/35 → one-third ceiling proposed → run retained W1-W4 ## Relations +- `Decomposes into`: None. +- `Satisfies obligation`: None. + - `Replaces`: None. - `Supersedes`: None. - `Refines`: EXP-0006. @@ -155,3 +171,7 @@ accepted 30/35/35 → one-third ceiling proposed → run retained W1-W4 - `Baseline of`: Future authority work inside the selected ceiling. - `Confirms`: None. - `Invalidates`: None. +- `Uses evidence from`: See Baseline and Candidates and Environment and Artifact Identity; historical inputs are not automatically acceptance dependencies. +- `Transfers question to`: See Follow-Up / Next Gradient; no additional transfer declared. +- `Produces input for`: See Baseline of and Successor experiment; no additional consumer declared. +- `Reopen trigger`: The specific conditions in Governing Specification and Validity; preserve historical evidence and create a successor for a materially different question. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0023.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0023.md index c3743a9a..811ce192 100644 --- a/.agents/skills/architecture-experiments/tracks/actors/EXP-0023.md +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0023.md @@ -3,6 +3,9 @@ | Field | Value | | --- | --- | | Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | None | | 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 | @@ -79,6 +82,16 @@ No additional candidate is admitted unless measurements expose one specific inde | 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` | +## Proof Obligations + +- `Freeze`: Frozen by explicit migration reconstruction from the pre-fission decision and retained witnesses at a60de7748132211d9e58cecd5be82b41f76ce230. No new measurement or historical decision is implied. +- `Review triggers`: None. +- `Decomposition review`: Boundary reviewed during provenance-preserving fission. The historical decision boundary and all original evidence remain unchanged. Metadata normalization neither fissions nor reinterprets this sealed result; downstream reuse consumes its original validity scope. Any newly earned question receives a new node. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | 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? | One legal counterexample to the declared owner/domain or component-wise containment | Source-derived and bounded native/Wasm evidence as separately identified below | Self | Required | Named parent decision and declared downstream consumers | Accepted; retained original decision and validity scope | + ## Measurements | Candidate | Workload | RefTime | ProofSize | Reads | Writes | Other decision metrics | @@ -242,11 +255,18 @@ A0 retained → A1 rejected below complete-path materiality ## Relations +- `Decomposes into`: None. +- `Satisfies obligation`: None. + - `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. +- `Depends on`: EXP-0005, EXP-0013, Actors specification and resource policy; the dormant quantum question is not a prerequisite for retained Q1 authority. - `Baseline of`: Potential canonical FIFO-internal physical-topology experiments; external successor staging is closed. - `Confirms`: None. - `Invalidates`: None. +- `Uses evidence from`: [EXP-0017](./EXP-0017.md) historical quantum boundary and [EXP-0022](./EXP-0022.md) corrected resource/baseline evidence, with the exact identities above; neither implies accepting the rejected or invalidated performance claim. +- `Transfers question to`: See Follow-Up / Next Gradient; no additional transfer declared. +- `Produces input for`: [EXP-0024](./EXP-0024.md) Actor-centric hold baseline and [EXP-0025](./EXP-0025.md) semantic sole-owner constraints; C1 supersedes physical control placement only. +- `Reopen trigger`: The specific conditions in Governing Specification and Validity; preserve historical evidence and create a successor for a materially different question. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0024.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0024.md index 6dc46421..4c8183ec 100644 --- a/.agents/skills/architecture-experiments/tracks/actors/EXP-0024.md +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0024.md @@ -3,6 +3,9 @@ | Field | Value | | --- | --- | | Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | None | | Architecture release / experiment campaign | 0.7.24 topology scaling | | Date | 2026-08-30 | | Affected domain | Actors economics and lifecycle | @@ -76,6 +79,16 @@ Should a User Actor reserve run-state hold only while a Cycle exists, or reserve | 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 | +## Proof Obligations + +- `Freeze`: Frozen by explicit migration reconstruction from the pre-fission decision and retained witnesses at a60de7748132211d9e58cecd5be82b41f76ce230. No new measurement or historical decision is implied. +- `Review triggers`: None. +- `Decomposition review`: Boundary reviewed during provenance-preserving fission. The historical decision boundary and all original evidence remain unchanged. Metadata normalization neither fissions nor reinterprets this sealed result; downstream reuse consumes its original validity scope. Any newly earned question receives a new node. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | 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? | One legal counterexample to the declared owner/domain or component-wise containment | Source-derived and bounded native/Wasm evidence as separately identified below | Self | Required | Named parent decision and declared downstream consumers | Accepted; retained original decision and validity scope | + ## Measurements | Candidate | Workload | RefTime | ProofSize | Reads | Writes | Other decision metrics | @@ -174,11 +187,18 @@ Cycle-local run hold → owner-balance liveness dependency ## Relations +- `Decomposes into`: None. +- `Satisfies obligation`: None. + - `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. +- `Depends on`: [EXP-0023](./EXP-0023.md) Actor-centric baseline, accepted Actor state-hold semantics, C6 run topology, P32, strict FIFO/Q1, and equal-thirds resource policy. - `Baseline of`: None. - `Confirms`: None. - `Invalidates`: None. +- `Uses evidence from`: See Baseline and Candidates and Environment and Artifact Identity; historical inputs are not automatically acceptance dependencies. +- `Transfers question to`: See Follow-Up / Next Gradient; no additional transfer declared. +- `Produces input for`: [EXP-0025](./EXP-0025.md) active-lifetime hold semantics, [EXP-0028](./EXP-0028.md) state-hold phase accounting and [EXP-0029](./EXP-0029.md) qualified Run geometry. +- `Reopen trigger`: The specific conditions in Governing Specification and Validity; preserve historical evidence and create a successor for a materially different question. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0025.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0025.md new file mode 100644 index 00000000..291a1326 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0025.md @@ -0,0 +1,168 @@ +# EXP-0025 — Canonical Actor Control Physical Geometry + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | None | +| Architecture release / experiment campaign | 0.7.25 Actor Control geometry | +| Date | 2026-09-05 | +| Affected domain | Actors scheduler, hot control state, FIFO, and temporal readiness | +| Physical mechanism | Ticket-addressed frame-owned Actor Control cells | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0023](./EXP-0023.md) | +| Successor experiment | [EXP-0028](./EXP-0028.md), [EXP-0029](./EXP-0029.md), [EXP-0064](./EXP-0064.md), [EXP-0063](./EXP-0063.md), [EXP-0065](./EXP-0065.md), [EXP-0066](./EXP-0066.md) | + +## Decision Question + +Did ticket-addressed frame-owned Actor Control geometry preserve DEOS semantics and bounded ownership sufficiently to replace the prior scalar Actor Control geometry? + +## Governing Specification + +- `Exact sections`: Actors specification → sole-owner rule, canonical partitions, Trigger occurrence, Pipeline/Cycle, attempt transaction, placement/temporal readiness, Prepass/cutoff/FIFO service and liveness; resource policy → admission/actual Weight and canonical block phases; performance assurance → evidence contract and ten-thousand Actor targets. Owners: `template/pallets/actors/docs/specification.en.md`, `docs/actors-resource-policy.specification.en.md`, `docs/actors-performance-assurance.specification.en.md`. +- `Invariant`: Strict class-neutral FIFO, Q1, immutable global ticket chronology/cutoff, one Step per Actor per block, `N -> N+1-or-later`, one active plus one deferred Pipeline, independent committed prefixes, atomic effects, exact Trigger/Pipeline/Action fees and custody neutrality. +- `Reopen trigger`: Only a new experiment proving semantic unsoundness, unenforceable required boundedness, unavoidable duplicate mutable authority, unavoidable FIFO/Q1/causal-floor violation, or physical impossibility of any sound production Weight envelope can reopen C1. A proposal must identify the frozen invariant → exact evidence → why repair inside C1 is impossible → why macrogeometry must change. + +## Context and Hypothesis + +- `Original pressure`: Scalar control exposed the need for canonical physical ownership. The accepted decision and necessary invariants remain here; successor records own unresolved questions. +- `Current architecture`: Retained production source implements C1. Architecture selection is closed; resource/admission/binding/throughput acceptance transfers to successors. +- `Hypothesis decided`: One process-primary plus bounded physical locators/references preserves the semantic machine without a second execution owner. +- `Decision scope`: Physical architecture only. C1 Architecture Freeze removes macrogeometry from active optimization; it is not the later 0.7.25 Geometry Freeze release gate. + +## Baseline and Candidates + +| ID | Architecture | Source tree | Runtime/config identity | +| --- | --- | --- | --- | +| A0 | Scalar-reference FIFO and separate hot control | Commit `5c35415c5f818be2c96d4b640859b82380cdaf92`; tree `c662e9480935b2817f0583f15e5c894b29f91e0a` | Historical equal-thirds Q1, P32 | +| C1 | Canonical Unsignaled/Ready/Waiting primary and bounded references | Retained commit `5b46b19004b62a723d6c45a610dfe1e5c79982b0`; template tree `98af72ffbb55fe8840b5bb44b5b01d65c8e775e1` | Reference `12/24/48`; production envelope unresolved | + +## Controls + +- `Semantic controls`: Immutable A0 oracle, ordinary FIFO differential, all-Trigger/lifecycle/custody/retry tests and TryRuntime reconciliation; historical candidate-only execution remains separately classified. +- `Changed variables`: Physical control placement/addressing and references. Historical P32/P12 and pre-cutover Wasm measurements retain their original identities. +- `Confounders`: Focused direct paths do not cover Executive/inherents/finalization, continuous user demand or every production branch. + +## Benchmark Class, Workloads, and Criteria + +- `Architecture-selection evidence`: Retained canonical stores, ordinary service, immutable differential oracle, G1 bounded topology and G2 sole-owner cutover on the source identity below. +- `Acceptance criteria`: Physical semantic preservation and bounded sole ownership. The user-authorized 2026-09-05 decision narrows the original compound question; it does not retrospectively declare the original production-performance criteria passed. +- `Transferred criteria`: Resource soundness → EXP-0028, EXP-0029, EXP-0063 and EXP-0064; exact bindings → EXP-0065; W0–W9 and 100/100 → EXP-0066. +- `Rejection criteria`: The invariant/impossibility reopening test above, not cost alone. + +## Environment and Artifact Identity + +| Artifact | Identity | +| --- | --- | +| Retained commit / template tree | `5b46b19004b62a723d6c45a610dfe1e5c79982b0` / `98af72ffbb55fe8840b5bb44b5b01d65c8e775e1` | +| Final production Weight / Wasm / metadata | Not established; EXP-0065 owns closure | +| Immutable A0 oracle | `template/pallets/actors/src/tests/fixtures/actor_control_transition_oracle_v1.tsv`; SHA-256 `8746e423168309975e509be91e4eb06fdf0eb0b43369d75f1ee62202a4341678` | +| Oracle provenance | 25 logical-transition Blake2-256 digests; original six-file manifest `2ada4b51`, pre-instrumentation module `8fd92143`; later preflight `60907fb6` is not the oracle source | + +## Proof Obligations + +- `Freeze`: Frozen by explicit migration reconstruction from the pre-fission decision and retained witnesses at a60de7748132211d9e58cecd5be82b41f76ce230. No new measurement or historical decision is implied. +- `Review triggers`: None. +- `Decomposition review`: Boundary reviewed during provenance-preserving fission. The historical decision boundary and all original evidence remain unchanged. Metadata normalization neither fissions nor reinterprets this sealed result; downstream reuse consumes its original validity scope. Any newly earned question receives a new node. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | Did ticket-addressed frame-owned Actor Control geometry preserve DEOS semantics and bounded ownership sufficiently to replace the prior scalar Actor Control geometry? | One legal counterexample to the declared owner/domain or component-wise containment | Source-derived and bounded native/Wasm evidence as separately identified below | Self | Required | Named parent decision and declared downstream consumers | Accepted; retained original decision and validity scope | + +## Measurements + +No production measurement is accepted by this physical-architecture decision. EXP-0066 owns integrated throughput; successor records retain only evidence necessary for their questions. + +## Derived Metrics + +No new numerical metric follows from architecture selection. + +## Statistical / Variance Notes + +Native correctness is not production-Wasm evidence; branch arithmetic is not integrated throughput. + +## Result + +- `Physical evidence`: At the retained source identity, `src/lib.rs` owns canonical Ready/Waiting/Unsignaled stores and `do_try_state` reconciliation; `src/scheduler.rs::service_live_queue_entry` uses sole process authority and bounded source/successor transitions. +- `Correctness evidence`: The immutable oracle and retained package tests cover source/successor, Trigger, retry/terminal, cancellation and reference semantics; `mixed_waiting_pages_reclaim_both_entry_kinds_through_real_close` supplies the mixed-container reclamation witness. This decomposition changes no runtime source and makes no new test-run claim. +- `Open boundary`: G3 storage/cardinality reconciliation and Suspended pre-cold admission remain unfinished; EXP-0029 / EXP-0063 own state/topology-bound evidence, EXP-0064 owns admission. + +## Interpretation + +YES for physical architecture selection. Canonical source implements C1 and retained differential/boundedness evidence supports selection. The historical claims that all C1 stores are benchmark-only or cutover is pending are superseded. This establishes no conclusion that every production Weight branch, maximum Contract or artifact binding is sound. + +## Decision + +**C1 PHYSICAL GEOMETRY: FROZEN. Status: Accepted. Decision scope: physical architecture only.** + +Established physical basis: + +- One canonical mutable process-primary per Active Actor in Unsignaled/Ready/Waiting, bounded `ActorControlLocator` lookup and frame-owned lifecycle/hot/admission authority. +- Ticket-addressed Ready ordering, immutable global chronology, strict FIFO/Q1/causal floor, bounded physical span and no live-cell compaction. +- Waiting primary/reference distinction; at most one Block and one Tick reference in addition to the primary; references own no process state. +- Linked nonempty Waiting pages with bounded unlink/reclaim, shared deadline ordering, mixed-clock fail-closed arbitration and no second scheduler. +- Exact source consumption/successor publication, lifecycle and Trigger semantic equivalence, no production scalar compatibility owner and no second execution algorithm. + +Not established: final production Weight, reachable maximum admission, complete Waiting/heap envelope, final Wasm/metadata, W0–W9, 100/100, residual proof optimality or release acceptance. + +High coefficients, expensive Contract/heap branches, conservative ProofSize, stale Weight, unreachable fixtures, missed throughput, possible page/bitmap/shared-body/key-locality improvements or simplification preference do not reopen C1. Correctness repairs, dead-code deletion, redundant-read removal, fixture repair, Weight ownership and artifact regeneration may proceed within frozen invariants under successor ownership. No successor implementation begins in this decomposition. + +## Rejected Alternatives and Tradeoffs + +| Candidate / assumption | Disposition | Evidence | +| --- | --- | --- | +| Scalar topology as production control | Replaced by C1 | G2 sole-owner cutover; no scalar fallback | +| Second scheduler/executor or priority/bin packing | Excluded | Sole order/authority and FIFO | +| Primary-only Waiting | Invalidated | Block/Tick references must coexist with Ready/Unsignaled primary | +| Aligned frame/wakeup coordinates | Invalidated | Exact pointer/locator authority determines consumption | +| Live-cell compaction | Excluded by frozen C1 | Ticket chronology/address invariants | +| Cleanup-only or one temporal leaf | Rejected | Historical counterfactual does not remove macro owner | +| C2 cells behind scalar FIFO | Dormant, no automatic fallback | Requires qualifying C1 reopen experiment | +| P12 alone / state-hold-only cache | Rejected performance hypotheses | [EXP-0026](./EXP-0026.md) / [EXP-0027](./EXP-0027.md); no one-/middle-Step proof removal | + +## Validity + +- `Establishes`: Physical selection/freeze, bounded ownership model and semantic preservation within retained evidence. +- `Does not establish`: Weight soundness, every maximum Contract fitting, final generated identity, throughput or 0.7.25 acceptance. +- `Known limitations`: G3 audits/corrections and successor resource questions remain mandatory. Old direct-executor results cannot certify ordinary production service. +- `Invalidation triggers`: Numeric evidence expires with relevant source/configuration/artifact change; physical freeze reopens only for the five qualifying failures in Governing Specification. +- `Residual qualitative insight`: Ticket order is physical order; references never acquire process authority; no silent second execution owner. + +## Follow-Up / Next Gradient + +| Transferred question | Sole owner | Required output | +| --- | --- | --- | +| Phase/control/collection, source/successor, cleanup, no-double-charge | [EXP-0028](./EXP-0028.md) | Four-owner phase matrix | +| Authored maxima, Opening/funding/DNF/head-tail and fixtures | [EXP-0029](./EXP-0029.md) | Mechanically usable reachable domain | +| Running/Idle/Suspended, liability/terminal, maximum/actual admission | [EXP-0064](./EXP-0064.md) | Component-wise envelope and exact 12/24/48 product contract | +| Waiting primary/reference, placement/removal and deadline heap | [EXP-0063](./EXP-0063.md) | Conservative envelope or branch owners | +| Actors/cross-pallet Weight, Wasm, metadata, ABI/client and bounds | [EXP-0065](./EXP-0065.md) | Exact identity set | +| W0–W9, continuous valid user demand, ranked bottlenecks | [EXP-0066](./EXP-0066.md) | Integrated production evidence | + +No micro-optimization starts without a measured owner from EXP-0066 or a prerequisite production experiment and a dedicated falsifiable record. No speculative residual IDs are allocated. + +## Architecture Impact + +- `Affected physical truth`: C1 is canonical production control in `src/lib.rs` stores and `src/scheduler.rs::service_live_queue_entry`; package architecture describes that implementation. +- `Architecture update`: Reconciles provenance with retained source, replacing stale “A0 remains current production truth”; final correctness, resource, binding and release gates stay open. +- `Provenance citation`: `actors/EXP-0025 — Accepted, physical architecture only; C1 frozen`. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: None. + +- `Replaces`: A0 scalar Actor Control; the immutable oracle remains unchanged. +- `Supersedes`: [EXP-0023](./EXP-0023.md) only for physical control placement, retaining Actor-centric semantic authority. +- `Refines`: [EXP-0023](./EXP-0023.md) sole authority and [EXP-0024](./EXP-0024.md) active-lifetime holds. +- `Contradicts`: Primary-only Waiting and frame/wakeup coordinate equivalence. +- `Depends on`: [EXP-0023](./EXP-0023.md), [EXP-0024](./EXP-0024.md) and governing specifications. +- `Baseline of`: EXP-0028, EXP-0029 and EXP-0063 through EXP-0066; frozen physical input, not accepted numeric Weight. +- `Confirms`: Actor-owned continuation/lifecycle and scheduler-owned order/cutoff/admission. +- `Invalidates`: Scalar compatibility as current owner; second execution owner; primary-only Waiting; aligned frame/wakeup coordinate assumption. +- `Uses evidence from`: [EXP-0022](./EXP-0022.md) corrected A0; [EXP-0026](./EXP-0026.md) and [EXP-0027](./EXP-0027.md) negative results. +- `Transfers question to`: [EXP-0028](./EXP-0028.md), [EXP-0029](./EXP-0029.md), [EXP-0064](./EXP-0064.md), [EXP-0063](./EXP-0063.md), [EXP-0065](./EXP-0065.md), [EXP-0066](./EXP-0066.md) as mapped above. +- `Produces input for`: [EXP-0028](./EXP-0028.md), [EXP-0029](./EXP-0029.md), [EXP-0064](./EXP-0064.md), [EXP-0063](./EXP-0063.md), [EXP-0065](./EXP-0065.md), [EXP-0066](./EXP-0066.md). +- `Reopen trigger`: New evidence proves a qualifying invariant failure and impossibility of repair inside C1; cost/target misses are insufficient. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0026.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0026.md new file mode 100644 index 00000000..232aa463 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0026.md @@ -0,0 +1,181 @@ +# EXP-0026 — Twelve-Step Reference-Profile Physical Consequences + +| Field | Value | +| --- | --- | +| Status | Rejected | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | None | +| Architecture release / experiment campaign | 0.7.25 Actor Control geometry | +| Date | 2026-09-02 | +| Affected domain | Actor Contract, run-state, state-hold, and lifecycle bounds | +| Physical mechanism | Runtime maximum Contract and lockstep Opening bounds | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0021](./EXP-0021.md) and [EXP-0025](./EXP-0025.md) | +| Successor experiment | [EXP-0027](./EXP-0027.md) | + +## Decision Question + +How much do the selected 12-Step DEOS reference ceiling and lockstep `24/48` Opening bounds reduce EXP-0025 nonterminal middle-Step proof and maximum lifecycle geometry, and does that changed baseline clear the W1-derived `6,361` comparison allowance or expose a narrower next physical owner? + +## Governing Specification + +- `Exact section`: `template/pallets/actors/docs/specification.en.md` → `13.3 Runtime configuration`; `docs/actors-performance-assurance.specification.en.md` → `4 Contract Complexity Matrix`. +- `Invariant`: Preserve Q1, one class-neutral FIFO, exact Step semantics, atomic independent committed prefixes, current Contract/Run/state-hold ownership, host configurability up to 255 Steps, and complete dispatchability of every DEOS genesis System Actor Contract. +- `Reopen trigger`: Any candidate requiring changed Step semantics, Task behavior, FIFO order, causal timing, duplicated Contract authority, or a non-generic pallet ceiling must reopen Specification rather than remain a physical consequence measurement. + +## Context and Hypothesis + +- `Current architecture`: EXP-0025 C1 uses ticket-addressed C32 control frames and C6 four-Step tail chunks. Its P32 middle Transfer is semantically exact at `1,127.1m / 6,876.52` per Actor for 100, while Transfer P0 is `902.4m / 6,187.96`. +- `Reason`: DEOS selected 12 Steps as sufficient local linear-program expressiveness, with larger compositions represented by Actor graphs. The previous `32/64/128` maximum profile is now `12/24/48`; EXP-0004 and EXP-0021 selection claims are invalidated or superseded, but their measurements remain historical baselines. +- `Hypothesis`: Smaller maximum-proven Contract/run values reduce P12 middle-Step estimated ProofSize by at least 5%, potentially below `6,361`, while one-Step P0 remains non-regressing and maximum create/close/reconstruct geometry falls materially. +- `Materiality`: Report every dimension independently. A `>=5%` middle complete-path ProofSize reduction or removal of one maximum-bound proof owner is material; clearing `6,361` is useful comparative headroom, not a universal middle-Step acceptance promise. + +## Baseline and Candidates + +| ID | Architecture | Source tree | Runtime/config identity | +| --- | --- | --- | --- | +| B0 | EXP-0025 C1 under P32 | Commit `5c35415`; C1 manifest `77d79973`; Wasm `b3a11cf4` | `MaxContractSteps/OpeningSnapshots/OpeningPredicateResults = 32/64/128` | +| C1 | Same EXP-0025 C1 under selected P12 | HEAD `7f767924`; corrected five-file worktree manifest `3fd62d1f364103861d19774f76c928f362abbe54b8e66cecf147168eed8ffcfe` | `12/24/48`; geometry ranges `c=0..3`; all other runtime policy retained | + +## Controls + +- `Controlled variables`: EXP-0025 C1 source outside runtime bounds, Rust/SDK/Cargo graph, Q1/FIFO semantics, C32 frames, C6 Chunk4 body, Tasks, Contracts, actors, balances, fees, database, cache, compiled-Wasm execution, benchmark steps/repeats, and postconditions. +- `Changed variables`: Only `MaxContractSteps 32→12`, `MaxOpeningSnapshotEntries 64→24`, and `MaxOpeningPredicateResults 128→48`, plus generated runtime/Wasm/metadata consequences. +- `Confounders`: The P32 artifact predates current uncommitted documentation/tests and uses its retained source/Wasm identity. Compare generated models and exact workload geometry rather than wall-clock timing. A generated storage annotation may retain a maximum value that the measured path does not physically populate; report estimated and measured ProofSize separately. + +## Benchmark Class, Workloads, and Criteria + +- `Benchmark class`: Production-Wasm focused pallet benchmark, deterministic encoded-bound inspection, runtime integration, and later full-runtime EXP-0025 W1 composition. + +| Workload | State/population geometry | Hot/cold assumptions | Purpose | +| --- | --- | --- | --- | +| W1 | Three-Step Running cohort, Step 1 Transfer, `n=1..32`, grouped `3×32 + 4` | Current and successor tail authority cold | Primary middle complete-path delta | +| W2 | One-Step Ready Transfer P0, `n=1..32`, grouped `3×32 + 4` | Opening head cold; no Predicate | No-regression and W1 headroom witness | +| W3 | Maximum 12-Step create, close, and reconstruct | Complete admitted P12 tail cold | Maximum lifecycle/state geometry | +| W4 | All configured genesis System Actor Contracts | Genesis state authoritative | Sufficiency and dispatchability | + +- `Metrics`: Generated RefTime; estimated/measured ProofSize; reads/writes; encoded maximum values; grouped per-Actor cost; maximum lifecycle Weight; Wasm bytes/hash; source, Weight, metadata, and runtime identities. +- `Statistical method`: FRAME Weight uses compiled Wasm, RocksDB, 1,024 MiB cache, 50 steps × 20 repeats, and generated model review. Deterministic runtime tests and encoded lengths have no sampling claim. No sample is removed. +- `Acceptance criteria`: P12 preserves semantics and System Contract fit; W2 does not regress component-wise beyond generated-model noise; and W1 reduces estimated ProofSize by at least 5%, clears `6,361`, or removes one named maximum-bound proof owner. The selected P12 runtime policy remains valid even if it earns no throughput claim. +- `Rejection criteria`: Reject the optimization claim on semantic drift, non-dispatchable System Contract/lifecycle maximum, W2 regression without a measured owner, unverifiable artifact identity, or sub-material W1 change with no owner removal. Do not reject P12 product policy from a performance null result. + +## Environment and Artifact Identity + +| Artifact | Identity | +| --- | --- | +| Source commit | HEAD `7f767924e780a4ab032e2cc6799d8148f9abb9a2`; dirty-tree candidate manifest recorded above | +| Source tree | HEAD tree `b3c0873df8fa56d01d63de1db038488c252160e9`; candidate differs by declared worktree files | +| Rust toolchain | `rustc 1.98.0 (88d9e12ae 2026-08-18)`; `cargo 1.98.0 (797e8a9bc 2026-08-05)` | +| Benchmark runtime Wasm | P32 `b3a11cf4137b10d1dadafbef103a9a5104076c47f2704c1bc306ae954644ae8a`; corrected P12 `d555298a9299436e1fe2bae2314c7d9db2add1d8b26763aef24778eb0db1c90a`, 6,133,725 bytes | +| Post-generation production runtime Wasm | Pending | +| Final release production runtime Wasm | Pending | +| Generated Weight | Focused outputs pending; production handoff pending | +| Database/backend | Compiled Wasm; RocksDB; 1,024 MiB cache | +| Benchmark command/config | `./scripts/benchmarks.sh --check`; then focused `--steps 50 --repeat 20 --extrinsic --output /tmp/.rs pallet_deos_actors` with one fresh P12 benchmark build and `--skip-build` only for unchanged subsequent owners | +| Raw evidence | P32 middle `6c216bbe`; corrected P12 middle `/tmp/exp0026-p12-corrected-running-middle-transfer-batch32-v2.rs` `72d93770`; P0 `/tmp/exp0026-p12-corrected-ready-transfer-p0-direct-batch32-v2.rs` `02ffff65`; create `e59123ee`; close `6bd98a94`; reconstruct `d44c7ebf` | + +## Proof Obligations + +- `Freeze`: Frozen by explicit migration reconstruction from the pre-fission decision and retained witnesses at a60de7748132211d9e58cecd5be82b41f76ce230. No new measurement or historical decision is implied. +- `Review triggers`: None. +- `Decomposition review`: Boundary reviewed during provenance-preserving fission. The historical decision boundary and all original evidence remain unchanged. Metadata normalization neither fissions nor reinterprets this sealed result; downstream reuse consumes its original validity scope. Any newly earned question receives a new node. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | How much do the selected 12-Step DEOS reference ceiling and lockstep `24/48` Opening bounds reduce EXP-0025 nonterminal middle-Step proof and maximum lifecycle geometry, and does that changed baseline clear the W1-derived `6,361` comparison allowance or expose a narrower next physical owner? | One legal counterexample to the declared owner/domain or component-wise containment | Source-derived and bounded native/Wasm evidence as separately identified below | Self | Required | Named parent decision and declared downstream consumers | Rejected; retained original decision and validity scope | + +## Measurements + +| Candidate | Workload | RefTime | ProofSize | Reads | Writes | Other decision metrics | +| --- | --- | ---: | ---: | ---: | ---: | --- | +| B0 P32 | W1 middle Transfer at 100 | `1,127.1m`/Actor | `6,876.52` estimated; `1,687.24` measured | `10.28`/Actor | `6.24`/Actor | Exact EXP-0025 baseline | +| B0 P32 | W2 Transfer P0 at 100 | `902.4m`/Actor | `6,187.96` estimated; `1,098.68` measured | `7.24`/Actor | `5.16`/Actor | Exact EXP-0025 baseline | +| C1 P12 | W1 middle Transfer at n=32 | `35,490,160,384` generated call Weight | `217,784` estimated; `53,353` measured | `327` | `198` | Slope `226,400,012 / 6,553`; run payload max/proof `1,491 / 3,966` | +| C1 P12 | W1 middle Transfer at 100 | `1,118.2m`/Actor | `6,876.52` estimated; `1,687.24` measured | `10.28`/Actor | `6.24`/Actor | Grouped `3×32 + 4`; exact proof unchanged from P32 | +| C1 P12 | W2 Transfer P0 at n=32 | `28,416,501,226` | `196,489` estimated; `34,593` measured | `230` | `164` | Same proof model and I/O as P32 | +| C1 P12 | W2 Transfer P0 at 100 | `893.1m`/Actor | `6,187.96` estimated; `1,098.68` measured | `7.24`/Actor | `5.16`/Actor | No component-wise regression | +| C1 P12 | W3 maximum create at c=3 | `697,132,993` | `11,904` estimated; `1,064` measured | `6` | `5` | Three exact tail chunks; slope `6,024,683 / 2,475` | +| C1 P12 | W3 maximum close at c=3 | `775,355,735` | `12,537` estimated; `1,753` measured | `5` | `6` | State-hold storage maximum remains 152 bytes outside Contract-tail owner | +| C1 P12 | W3 maximum reconstruct at c=3 | `165,817,266` | `12,537` estimated; `1,753` measured | `5` | `0` | Exact cold reconstruction of all three tail chunks | +| Invalid P12 probe | W3 geometry `c=0..8` clamped to `c<=3` | Invalid linear model | Invalid linear model | Plateaued | Plateaued | Superseded diagnostic; artifacts excluded | + +## Derived Metrics + +| Candidate | Workload | RefTime delta | ProofSize delta | Throughput / latency | Marginal or amortized cost | Pareto class | +| --- | --- | ---: | ---: | --- | --- | --- | +| C1 P12 | W1 benchmark slope | `-3.7438%` | `0%` | Same C32 grouped geometry | Run-payload annotation `5,008→3,966` (`-20.8067%`) | RefTime-only improvement | +| C1 P12 | W1 grouped complete path | `-0.7845%` | `0%` | No additional middle-Step capacity | DB I/O and tail proof unchanged | Sub-material tradeoff | +| C1 P12 | W2 grouped P0 | `-1.0336%` | `0%` | Same one-Step proof capacity | Reads/writes unchanged | No-regression | +| C1 P12 | W3 maximum create | `-48.2438%` | `-51.0002%` | Lifecycle maximum only | Three tails instead of eight | Material maximum-bound gain | +| C1 P12 | W3 maximum close | `-45.8625%` | `-51.6263%` | Lifecycle maximum only | State-hold owner retained | Material maximum-bound gain | +| C1 P12 | W3 maximum reconstruct | `-47.7051%` | `-51.6263%` | Lifecycle maximum only | Five reads instead of ten | Material maximum-bound gain | + +## Statistical / Variance Notes + +Generated Weight evidence is deterministic model output under the declared 50×20 method. Corrected P12 middle uses coefficient standard error `549,709`, measured proof `2,281 + 1,596n`, and estimated proof `8,088 + 6,553n`. Grouped RefTime applies production RocksDB Weight to four calls (`3×32 + 4`), 1,028 reads, and 624 writes. Corrected lifecycle ranges are exactly `c=0..3`; no clamped observations enter their linear models. Runtime test duration is not decision evidence. + +## Result + +Corrected P12 leaves middle estimated and measured ProofSize exactly unchanged at `217,784 / 53,353` for n=32 and `6,876.52 / 1,687.24` per Actor when grouped to 100. It shrinks the `ActorRunPayload` maximum from 2,533 to 1,491 bytes, but `ActorContractTailChunk` remains `4,070 / 6,545`; grouped RefTime improves only `0.7845%`. P0 keeps `6,187.96` proof and improves RefTime `1.0336%`. Maximum P12 create/close/reconstruct, however, reduce estimated proof about `51%` and RefTime `45.9–48.2%` against P32 because complete tail count falls from eight to three. `ActorStateHolds` remains a separate 152-byte maximum owner. W4 passes through the runtime suite and explicit maximum-tail genesis witness. The corrected benchmark Wasm is `d555298a`, and every retained artifact uses its reachable `c=0..3` source. + +## Interpretation + +P12 is a material maximum-lifecycle simplification and a throughput-path null result. It removes five maximum tail chunks from lifecycle operations and halves their proof, while preserving P0 and middle semantics without component-wise regression. It does not change the middle current-plus-successor `ActorContractTailChunk` owner, does not lower `6,876.52`, and does not clear `6,361`. The independently selected P12 product policy is physically cheaper at its maximum, but the next throughput experiment must change exactly one retained tail/state-hold mechanism rather than claim that the ceiling alone solved W1. + +## Decision + +Reject the P12-only middle optimization hypothesis: W1 remains `6,876.52` and does not clear `6,361`. Retain P12 as the independently selected DEOS product policy and as the measured baseline because it preserves W2/W4 while materially reducing maximum lifecycle cost. Open EXP-0027 with only immutable contract-body state-hold sourcing changed. + +## Rejected Alternatives and Tradeoffs + +| Candidate | Decision | Evidence-backed reason | +| --- | --- | --- | +| Change P12 plus Step representation or state-hold mechanism | Excluded from first measurement | Would stack controlled variables and prevent attribution | +| Reduce funding tracked assets with P12 | Excluded | Funding asset diversity is not derived from Contract length and has separate semantics | +| Treat `6,361` as a universal middle-Step gate | Rejected | Normative target is exact W1 one-Step throughput only | + +## Validity + +- `Establishes`: Exact P12 W1/W2 null-result geometry, material maximum lifecycle savings, W4 dispatchability, and the retained tail/state-hold owner. +- `Does not establish`: EXP-0025 acceptance, integrated W1 throughput, universal middle-Step capacity, or benefit in another host runtime. +- `Known limitations`: P32 and P12 use different runtime-config identities by design. Invalid clamped W3 artifacts are retained only as diagnosis and excluded. Production metadata/client regeneration follows the accepted runtime handoff. +- `Invalidation triggers`: Contract/run/storage layout, C32/C6 geometry, P12 bounds, Task/effect profile, toolchain, benchmark method, block Weight, or proof model changes. +- `Residual qualitative insight`: Product-level limits can release maximum proof/state headroom without forcing a generic pallet ceiling, but actual short-path benefit must be measured. + +## Follow-Up / Next Gradient + +```text +P32 middle 6,876.52 → selected P12 bound profile → maximum-proven value reduction +→ rerun exact middle/P0/lifecycle owners → name retained binding proof +→ integrated EXP-0025 W1 or one separately controlled successor +``` + +- `Eliminated hypotheses`: P12 alone lowers the middle-Step proof slope; the reduced run-payload maximum is the binding middle proof owner. +- `Remaining hypotheses`: Separately cached immutable state-hold component or compact current/successor tail authority against the measured P12 baseline; only one mechanism may change in the successor. +- `Stop condition`: Record W1–W4 with exact artifacts and either identify a material P12 gain/new baseline or retain a falsified optimization claim with one named next owner. + +## Architecture Impact + +- `Affected physical truth`: DEOS runtime Contract/run bounds, maximum storage proof, lifecycle Weight, state holds, metadata, and client authoring bounds. +- `Architecture update`: Update current architecture only after runtime code, tests, generated Weight/Wasm/metadata, and EXP-0025 integration converge. +- `Provenance citation`: `actors/EXP-0026`. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: None. + +- `Replaces`: None. +- `Supersedes`: [EXP-0021](./EXP-0021.md) only for the current DEOS reference-profile decision, not its P32/P24/P16 measurements. +- `Refines`: [EXP-0025](./EXP-0025.md) under the selected P12 runtime profile. +- `Contradicts`: None. +- `Depends on`: [EXP-0021](./EXP-0021.md), [EXP-0025](./EXP-0025.md), and the governing specifications. +- `Baseline of`: [EXP-0027](./EXP-0027.md). +- `Confirms`: None. +- `Invalidates`: [EXP-0004](./EXP-0004.md) as a current selection claim while preserving its P32 measurements; P12 alone reduces one-Step or middle-Step proof. +- `Uses evidence from`: [EXP-0025](./EXP-0025.md) historical direct C1 measurements, scoped to original artifact identities. +- `Transfers question to`: [EXP-0029](./EXP-0029.md) reachable geometry, [EXP-0064](./EXP-0064.md) admissibility, [EXP-0066](./EXP-0066.md) integrated performance; no new layout candidate authorized. +- `Produces input for`: [EXP-0028](./EXP-0028.md), [EXP-0029](./EXP-0029.md), [EXP-0064](./EXP-0064.md), [EXP-0065](./EXP-0065.md), [EXP-0066](./EXP-0066.md) as negative evidence, not acceptance prerequisites. +- `Reopen trigger`: A controlled changed assumption that defeats the recorded negative result. Cost pressure alone cannot reopen frozen C1 in [EXP-0025](./EXP-0025.md). diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0027.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0027.md new file mode 100644 index 00000000..9a3cdb8d --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0027.md @@ -0,0 +1,178 @@ +# EXP-0027 — Immutable Contract-Body State-Hold Cache + +| Field | Value | +| --- | --- | +| Status | Rejected | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | None | +| Architecture release / experiment campaign | 0.7.25 Actor Control geometry | +| Date | 2026-09-02 | +| Affected domain | User Actor state-hold reconciliation and immutable Contract-body access | +| Physical mechanism | Existing state-hold record as the cached immutable Contract-body charge owner | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0026](./EXP-0026.md) and [EXP-0004](./EXP-0004.md) | +| Successor experiment | None | + +## Decision Question + +Can per-Step User state-hold reconciliation reuse the exact `contract_body` component already owned by `ActorStateHolds`, instead of rescanning immutable canonical tail chunks, while preserving hold amounts, lifecycle atomicity, fail-closed ownership, Q1/FIFO semantics, and the measured P12 middle and lifecycle baseline? + +## Governing Specification + +- `Exact section`: `template/pallets/actors/docs/specification.en.md` → `Active User State Hold`, `Hot/Cold Physical Contract`, and lifecycle atomicity; `docs/actors-resource-policy.specification.en.md` → User state-cost custody. +- `Invariant`: Creation and every Contract mutation compute the exact immutable body charge before commit; ordinary Step execution changes no Contract body; the existing User hold record remains the sole monetary owner; missing or wrong-owner records fail closed; close and replacement reconcile atomically; System Actors remain exempt. +- `Reopen trigger`: Any need for a second Contract mirror, duplicated mutable authority, weaker exact-release behavior, owner-funded repair, off-chain correctness, or changed Step/FIFO semantics reopens Specification rather than remaining a physical experiment. + +## Context and Hypothesis + +- `Current architecture`: EXP-0025 C1 reconstructs `contract_body` hold bytes by scanning every canonical `ActorContractTailChunk` during each User Step reconciliation even though the body is immutable and the exact charged component already exists in `ActorStateHolds`. +- `Reason`: EXP-0004 C3 showed that compact current-Step authority could not help while state-hold reconciliation independently retained the canonical tail read. EXP-0026 fixed P12 at three maximum tail chunks and left three-Step middle proof at `6,876.52`. +- `Hypothesis`: Reusing the existing immutable hold component removes only redundant whole-body scans. Three-Step W1 may remain proof-identical because execution still needs its one tail, while a maximum 12-Step middle Step should stop reading unreachable tail chunks and materially reduce reads/proof without schema growth. +- `Materiality`: Primary evidence is exact owner/read removal at maximum P12 middle geometry with no component-wise regression at three-Step W1/P0. RefTime-only gains are reported but do not substitute for removed storage authority. + +## Baseline and Candidates + +| ID | Architecture | Source tree | Runtime/config identity | +| --- | --- | --- | --- | +| B0 | Exact per-Step full Contract-body rescan | EXP-0026 corrected manifest `3fd62d1f`; Wasm `d555298a` | P12 `12/24/48`; Chunk4 tail; C32 control frames | +| C1 | Existing `ActorStateHolds.breakdown.contract_body` reused during Step reconciliation | Five-file worktree manifest `fc98776b681c9b11fea898edfc48f9809fcbbd6fc49fedfd3cfa27df0ea7baba` | Same P12 geometry and semantics; no storage field or encoded-bound change | + +## Controls + +- `Controlled variables`: P12 runtime bounds, C32/C6 geometry, Chunk4 representation, Contract/Step/resource bytes, state-hold rates and totals, Actor population, Tasks, balances, fees, Q1/FIFO behavior, database/cache, benchmark method, and all lifecycle semantics. +- `Changed variables`: Only the source of the immutable `contract_body` component during per-Step User state-hold reconciliation: canonical tail rescan versus the existing exact hold record. +- `Confounders`: B0 focused artifacts use the corrected P12 Wasm. Candidate source changes require a fresh benchmark build. Storage annotations report maximum proof, so compare exact reads, generated proof, and deterministic postconditions together. + +## Benchmark Class, Workloads, and Criteria + +- `Benchmark class`: Production-Wasm focused FRAME benchmark plus deterministic differential, mutation, rollback, and corruption tests. + +| Workload | State/population geometry | Hot/cold assumptions | Purpose | +| --- | --- | --- | --- | +| W1 | Existing three-Step Running Transfer, `n=1..32`, grouped `3×32 + 4` | Required current/successor tail cold | Exact EXP-0026 no-regression witness | +| W2 | Maximum 12-Step Running Transfer at same-chunk and cross-chunk cursors | Complete body cold; unrelated tails unreachable to execution | Primary redundant-scan/read-removal witness | +| W3 | Ready Opening, middle continuation, terminal completion, Temporary suspension/backoff, and P0 User paths | Existing hold cold | Exact hold/event/fee/state parity | +| W4 | Create, replace, deactivate/reactivate, close, reconstruct, and rollback at 0/1/4/8/12 Steps | Complete lifecycle cold | Cache establishment, invalidation, and exact release | +| W5 | Missing hold, wrong owner, stale pre-mutation record, and failed currency hold/release | Adversarial bounded state | Fail-closed and atomicity evidence | + +- `Metrics`: Generated RefTime; estimated/measured ProofSize; reads/writes; grouped per-Actor cost; exact hold breakdown/total; lifecycle Weight; retained bytes; Wasm/hash; differential state/events/fees; corruption outcome. +- `Statistical method`: Compiled Wasm, RocksDB, 1,024 MiB cache, 50 steps × 20 repeats, no sample removal. Deterministic parity and fault tests have no sampling claim. +- `Acceptance criteria`: W1/P0 do not regress component-wise; W2 removes every tail read not needed for current/successor execution and materially lowers its complete proof or read set; W3–W5 preserve exact totals, authority, events, fees, rollback, close/release, and fail-closed outcomes; no schema field, duplicate owner, or lifecycle scan is added. +- `Rejection criteria`: Any hold undercharge/overcharge, stale cache after Contract mutation, owner mismatch acceptance, release drift, semantic/event/fee drift, hidden second authority, no W2 read removal, or component-wise W1/P0 regression without a measured owner. + +## Environment and Artifact Identity + +| Artifact | Identity | +| --- | --- | +| Source commit | HEAD `7f767924e780a4ab032e2cc6799d8148f9abb9a2`; dirty-tree baseline declared by EXP-0026 | +| Source tree | B0 five-file manifest `3fd62d1f364103861d19774f76c928f362abbe54b8e66cecf147168eed8ffcfe`; C1 `fc98776b681c9b11fea898edfc48f9809fcbbd6fc49fedfd3cfa27df0ea7baba` | +| Rust toolchain | `rustc 1.98.0 (88d9e12ae 2026-08-18)`; `cargo 1.98.0 (797e8a9bc 2026-08-05)` | +| Benchmark runtime Wasm | EXP-0026 B0 `d555298a`; instrumented W2 B0 `6dbc8dd99bcd7dc8a3c6e0a293812eecb688dc5ccc8b1e942acea05851bae848`; C1 `67accb9a31f60fdb3e960f4c6fd2c61251e87a202e745ae8fb16b467399a0b1c` | +| Post-generation production runtime Wasm | Pending | +| Final release production runtime Wasm | Pending | +| Generated Weight | B0 middle `72d93770`; B0 P0 `02ffff65`; B0 maximum W2 `670fc4b9`; C1 middle `831a3117`; C1 P0 `79034de2`; C1 maximum W2 `d7af8208` | +| Database/backend | Compiled Wasm; RocksDB; 1,024 MiB cache | +| Benchmark command/config | `./scripts/benchmarks.sh --steps 50 --repeat 20 --extrinsic --output /tmp/.rs pallet_deos_actors`; rebuild after candidate mutation | +| Raw evidence | `/tmp/exp0027-{b0,c1}-running-max12-middle-transfer-batch32-v1.rs`; C1 W1/P0 sibling artifacts; benchmark compilation/storage audit, focused differential, and `939 + 5 ignored` pallet suite pass | + +## Proof Obligations + +- `Freeze`: Frozen by explicit migration reconstruction from the pre-fission decision and retained witnesses at a60de7748132211d9e58cecd5be82b41f76ce230. No new measurement or historical decision is implied. +- `Review triggers`: None. +- `Decomposition review`: Boundary reviewed during provenance-preserving fission. The historical decision boundary and all original evidence remain unchanged. Metadata normalization neither fissions nor reinterprets this sealed result; downstream reuse consumes its original validity scope. Any newly earned question receives a new node. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | Can per-Step User state-hold reconciliation reuse the exact `contract_body` component already owned by `ActorStateHolds`, instead of rescanning immutable canonical tail chunks, while preserving hold amounts, lifecycle atomicity, fail-closed ownership, Q1/FIFO semantics, and the measured P12 middle and lifecycle baseline? | One legal counterexample to the declared owner/domain or component-wise containment | Source-derived and bounded native/Wasm evidence as separately identified below | Self | Required | Named parent decision and declared downstream consumers | Rejected; retained original decision and validity scope | + +## Measurements + +| Candidate | Workload | RefTime | ProofSize | Reads | Writes | Other decision metrics | +| --- | --- | ---: | ---: | ---: | ---: | --- | +| B0 | W1 grouped middle at 100 | `1,118.2m`/Actor | `6,876.52` estimated; `1,687.24` measured | `10.28`/Actor | `6.24`/Actor | Exact corrected P12 baseline | +| B0 | W2 maximum 12-Step middle at 100 | `929.0m`/Actor | `10,195.84` estimated; `2,533.16` measured | `11.28`/Actor | `4.24`/Actor | All three tail chunks read; model `7,846 + 9,882n` | +| C1 | W1 three-Step middle at 100 | `1,111.9m`/Actor | `6,876.52` estimated; `1,687.24` measured | `10.28`/Actor | `6.24`/Actor | Proof/I/O unchanged; RefTime `-0.5603%` | +| C1 | W1 P0 at 100 | `901.0m`/Actor | `6,187.96` estimated; `1,098.68` measured | `7.24`/Actor | `5.16`/Actor | Proof/I/O unchanged; RefTime `+0.8895%` | +| C1 | W2 maximum 12-Step middle at 100 | `929.1m`/Actor | `10,195.84` estimated; `2,533.16` measured | `11.28`/Actor | `4.24`/Actor | Exact proof/read null result; RefTime `+0.0088%` | + +## Derived Metrics + +| Candidate | Workload | RefTime delta | ProofSize delta | Throughput / latency | Marginal or amortized cost | Pareto class | +| --- | --- | ---: | ---: | --- | --- | --- | +| C1 | W1 three-Step middle | `-0.5603%` | `0%` | Same capacity | Full Contract reconstruction retained | RefTime-only noise | +| C1 | W1 P0 | `+0.8895%` | `0%` | Same capacity | No tail body in P0 | Sub-material regression | +| C1 | W2 maximum middle | `+0.0088%` | `0%` | Same capacity | `11.28/4.24` I/O unchanged | Exact physical null result | + +## Statistical / Variance Notes + +All focused models use compiled Wasm, RocksDB, 1,024 MiB cache, 50 steps × 20 repeats, and no removed samples. W2 B0 and C1 use the same dedicated maximum-Contract benchmark source and differ only by the state-hold sourcing mechanism. Their estimated/measured proof, reads, and writes are exactly identical; the `0.0088%` RefTime difference is non-material. + +## Result + +C1 preserves focused semantics and does not regress W1/P0 materially, but it removes no complete-path storage owner. Maximum W2 remains three tail reads per Actor because `exp0025_execute_ready_single_step` independently requests full Contract reconstruction before state-hold reconciliation. B0 and C1 therefore share exact `9,882` estimated proof slope, `2,456` measured slope, 11 reads, and 4 writes per Actor. Candidate code and benchmark instrumentation are deleted; the five-file source manifest is restored exactly to `3fd62d1f`. + +## Interpretation + +The state-hold rescan was real but not the sole full-body reader. Execution-plan construction passes `full_contract = true` for the shared Ready/Running/Suspended path, so removing only state-hold scans cannot change the composed proof/read set. Changing that reconstruction is a different mechanism and may affect Opening control context; combining it here would violate the one-variable boundary. W3–W5 correctly stop because the primary W2 owner-removal rejection criterion already fails. + +## Decision + +Reject C1 at W2. Exact complete-path reads and proof are unchanged, so the cached hold source does not earn its stale-cache risk. Restore B0 and do not open an automatic second successor; any later full-Contract reconstruction experiment must separately own Opening-context and shared-path semantics. + +## Rejected Alternatives and Tradeoffs + +| Candidate | Decision | Evidence-backed reason | +| --- | --- | --- | +| Add a second cached Contract-body field | Excluded | Duplicates the value already owned by the state-hold record and adds schema/proof pressure | +| Combine C1 with EXP-0004 compact Step payload | Excluded | Stacks state-hold and Step-representation mechanisms before C1 is independently established | +| Skip current/successor Contract authority | Excluded | Would weaken execution semantics and committed-successor validity | +| Trust off-chain hold reconstruction | Excluded | Consensus custody and exact release must remain on-chain | +| Rescan tails only periodically | Excluded | Adds temporal inconsistency without an owner or correctness benefit | + +## Validity + +- `Establishes`: State-hold scan elision alone preserves focused parity but removes no complete-path tail read or proof owner under current full-Contract execution-plan reconstruction. +- `Does not establish`: Compact Step representation, EXP-0025 acceptance, integrated W1 throughput, or correctness in another host runtime. +- `Known limitations`: W3–W5 stop at the explicit W2 rejection criterion; the candidate is not production code and all instrumentation is deleted. +- `Invalidation triggers`: State-hold schema/rates, Contract mutation semantics, tail representation, P12 bounds, C32 scheduler geometry, benchmark method, or proof model changes. +- `Residual qualitative insight`: An already charged immutable monetary component may be a valid cache only when lifecycle boundaries remain its exact establishment and invalidation owner. + +## Follow-Up / Next Gradient + +```text +P12 middle 6,876.52 → canonical current/successor tail remains required +→ exact state hold rescans all immutable tails +→ reuse existing contract_body charge without schema growth +→ measure maximum 12-Step read/proof removal +→ accept/reject before any compact Step composition +``` + +- `Eliminated hypotheses`: The state-hold rescan is the sole maximum-middle full-body tail owner; its removal alone improves complete-path proof or reads. +- `Remaining hypotheses`: Shared full-Contract execution reconstruction is the retained maximum-middle owner, but it is outside this experiment and not admitted as an automatic successor. +- `Stop condition`: Accept or reject C1 after W1–W5, retain exact artifacts, and either permit one later representation composition or restore B0 with no second successor. + +## Architecture Impact + +- `Affected physical truth`: Per-Step User hold reconciliation, Contract-tail reads, maximum middle-Step proof, and lifecycle cache establishment. +- `Architecture update`: Update package architecture only if C1 is accepted and becomes shipped implementation truth. +- `Provenance citation`: `actors/EXP-0027` after an interpreted decision. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: None. + +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: EXP-0004's deferred state-hold boundary under the measured P12 profile. +- `Contradicts`: None. +- `Depends on`: [EXP-0004](./EXP-0004.md), [EXP-0025](./EXP-0025.md), and [EXP-0026](./EXP-0026.md). +- `Baseline of`: Any later independently controlled compact-Step composition, only if C1 is accepted. +- `Confirms`: None. +- `Invalidates`: State-hold body caching alone removes the middle-Step proof owner; execution-plan construction independently retains full Contract reconstruction. +- `Uses evidence from`: [EXP-0025](./EXP-0025.md) historical direct C1 measurements, scoped to original artifact identities. +- `Transfers question to`: [EXP-0029](./EXP-0029.md) reachable geometry, [EXP-0064](./EXP-0064.md) admissibility, [EXP-0066](./EXP-0066.md) integrated performance; no new layout candidate authorized. +- `Produces input for`: [EXP-0028](./EXP-0028.md), [EXP-0029](./EXP-0029.md), [EXP-0064](./EXP-0064.md), [EXP-0065](./EXP-0065.md), [EXP-0066](./EXP-0066.md) as negative evidence, not acceptance prerequisites. +- `Reopen trigger`: A controlled changed assumption that defeats the recorded negative result. Cost pressure alone cannot reopen frozen C1 in [EXP-0025](./EXP-0025.md). diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0028.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0028.md new file mode 100644 index 00000000..3d4c19ea --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0028.md @@ -0,0 +1,187 @@ +# EXP-0028 — Actor Control Weight Ownership and Phase Accounting + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Synthesis | +| Former IDs | None | +| Parent question | [EXP-0064](./EXP-0064.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-05 | +| Affected domain | Production phase, resource and monetary accounting | +| Physical mechanism | Control phase reservation and actual settlement | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0025](./EXP-0025.md) — Accepted, physical architecture only | +| Successor experiment | [EXP-0064](./EXP-0064.md), [EXP-0063](./EXP-0063.md), [EXP-0065](./EXP-0065.md) | + +## Decision Question + +Does every production Actor Control phase have exactly one sound Weight owner, with no omission and no double charge? + +## Governing Specification + +- `Owners`: `template/pallets/actors/docs/specification.en.md` (sole ownership, lifecycle/Trigger/Pipeline/Action semantics, placement and scheduler); `docs/actors-resource-policy.specification.en.md` (component-wise admission/actual settlement, equal-thirds block policy); `docs/actors-performance-assurance.specification.en.md` (evidence and target). +- `Frozen physical input`: [EXP-0025](./EXP-0025.md) C1 Architecture Freeze. No change to FIFO, ticket chronology, Q1, causal floor, primary/reference ontology, source/successor atomicity or sole execution owner. +- `Semantic oracle`: A0 SHA-256 `8746e423168309975e509be91e4eb06fdf0eb0b43369d75f1ee62202a4341678` remains frozen; use only for relevant semantic equivalence, never as a Weight oracle. +- `Scope`: + +- Own Trigger/Pipeline/Action control ownership, Action fee-collection Control cost, invocation events and User state-hold work. +- Own source consumption, successor publication, cleanup, lifecycle terminal work, Waiting placement/removal and independent Cadenced rearm phase boundaries. +- Distinguish semantically required User/System differences, actual versus maximum control settlement and no-double-charge. +- Keep four separate columns: semantic owner, physical storage owner, Weight owner and monetary fee owner. A semantic operation may touch several stores but has one complete charging boundary; storage ownership alone does not establish Weight coverage. + +## Context and Hypothesis + +This broad decision composes the finite obligations below. Detailed evidence was extracted from pre-fission records at `a60de7748132211d9e58cecd5be82b41f76ce230` without measurement or promotion of scope. C1 remains frozen. Newly independent proof questions must be transferred before further investigation. + +## Baseline and Candidates + +Composition of the retained frozen C1 baseline and declared child decisions. This Synthesis introduces no benchmark implementation or candidate sweep. + +## Controls + +- `Controlled variables`: Freeze source/configuration, host bounds, toolchain/SDK, database/cache, legal state and workload for each comparison; name any changed input and refresh dependent evidence. +- `Changed variable`: Only the question-specific model, fixture, accounting or binding under test; no macrogeometry or authored semantics. +- `Confounders`: Historical native, direct-path, partial-heap and old generated evidence are narrower than production acceptance. [EXP-0026](./EXP-0026.md) and [EXP-0027](./EXP-0027.md) reject ceiling-only and state-hold-cache-only proof improvements; do not reintroduce them without changed evidence. + +## Benchmark Class, Workloads, and Criteria + +- `Smallest falsifier`: Trace one nonzero User Action collection across Opening, Running and Suspended on the same legal Task and compare maximum reservation with actual settlement. A missing collection read/write or duplicate base charge rejects that owner assignment. +- `Evidence class`: Call-path ownership audit, legal native witnesses and production-Wasm FRAME branch evidence. + +| Phase family | Required branch partition | Deciding witness | +| --- | --- | --- | +| Trigger readiness | All six families, useful/redundant, collection success/failure, detector rearm | One useful transition charge, no paid redundant occurrence | +| Pipeline Opening | Zero/nonzero Steps, User/System, solvent/insolvent, captured Opening state | Complete prepaid machine boundary, no effect surcharge | +| Action attempt | Opening/Running/Suspended; success/skip/permanent/Temporary; StopCycle | Collection Control versus effect Weight and monetary fee | +| Source/successor | Ready consume, Unsignaled/Ready/Waiting destination, capacity fallback | One physical publication owner and correct actual selector | +| Temporal/lifecycle | Cadenced rearm, reference replacement, terminal/close/cancel, cleanup | No omitted or double-counted heap, state-hold or lifecycle work | + +- `Acceptance criteria`: Publish a complete matrix with phase/branch, semantic owner, physical storage owner, maximum Weight method, actual Weight selector, monetary fee owner, caller and exact evidence identity. Every production call path maps to one charged owner; reservations dominate valid actuals component-wise; cross-phase shared work is included exactly once. +- `Rejection criteria`: Any uncovered branch, double charge, mismatched actual selector or collection-funded base undercount; classification by Task name alone is insufficient. + +## Environment and Artifact Identity + +Migration provenance: Git baseline `a60de7748132211d9e58cecd5be82b41f76ce230` and the content/mode manifest archived at Git commit `6c650695568a6e5d05437351ef014e8ee950c484`, repository path `.agents/skills/architecture-experiments/migrations/actors-proof-graph-2026-09-06.json`. Each Leaf retains its measured artifact identity. No final production artifact is created or certified by this migration. + +## Proof Obligations + +- `Freeze`: Frozen by explicit migration reconstruction from the pre-fission decision and retained witnesses at a60de7748132211d9e58cecd5be82b41f76ce230. No new measurement or historical decision is implied. +- `Review triggers`: None. +- `Decomposition review`: Synthesis owns only the frozen finite composition; independent primary evidence is owned by the reciprocal child map. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | Every general-path invoked Action receipt has exactly one Control owner, including inclusive CompleteMax and zero-fee System invocation. | One general-path System invocation and one inclusive CompleteMax receipt; a missing or doubled event owner falsifies the selector | Child decision composition | [EXP-0030](./EXP-0030.md) | Required | This synthesis decision | Satisfied with scope by Accepted EXP-0030; exact complete artifact identity remains G5 | +| O2 | Pipeline Opening collection has one complete inclusive Control owner for admitted User service. | One admitted large User head collecting Pipeline fees compared with the empty-head primitive, including both native ledger accounts | Child decision composition | [EXP-0031](./EXP-0031.md) | Required | This synthesis decision | Satisfied with scope by Accepted EXP-0031; complete production Weight identity remains G5 | +| O3 | The Current predicate component and composed selector dominate the declared canonical predicate and host-state domain. | One canonical mixed Current predicate Step at the reference four-occurrence bound whose selected component misses either charged resource | Child decision composition | [EXP-0032](./EXP-0032.md) | Required | This synthesis decision | Satisfied with scope by Accepted EXP-0032; complete production Weight identity remains G5 | +| O4 | The nonzero Opening predicate capture bound dominates the declared full-Contract predicate allocation and host-state domain. | One full-Contract nonzero capture allocation whose typed host reads or measured model exceed the selected predicate bound | Child decision composition | [EXP-0033](./EXP-0033.md) | Required | This synthesis decision | Satisfied with scope by Accepted EXP-0033; complete production Weight identity remains G5 | +| O5 | The nonzero Opening amount capture bound dominates the declared typed amount allocation and host-ledger domain. | One admitted typed amount surface or fixed-total mixture whose host-ledger capture exceeds the selected amount bound | Child decision composition | [EXP-0034](./EXP-0034.md) | Required | This synthesis decision | Satisfied with scope by Accepted EXP-0034; complete production Weight identity remains G5 | +| O6 | Zero-result predicate traversal selects one generated owner without adding it to inclusive or nonzero capture paths. | A full Contract with all predicates Current returns zero Opening results but still performs traversal | Child decision composition | [EXP-0035](./EXP-0035.md) | Required | This synthesis decision | Satisfied with scope by Accepted EXP-0035; complete production Weight identity remains G5 | +| O7 | Zero-result amount traversal selects one generated owner without adding it to inclusive or nonzero capture paths. | A full Contract with only current/fixed amounts returns zero Opening surfaces but still performs traversal | Child decision composition | [EXP-0036](./EXP-0036.md) | Required | This synthesis decision | Satisfied with scope by Accepted EXP-0036; complete production Weight identity remains G5 | +| O8 | The User/System zero-Step base boundary includes its admitted head, collection and hold work without borrowing a System-only bound for User execution. | An ordinary Manual User zero-Step Opening whose inclusive cost exceeds the System-only bound, then the attained maximum empty head | Child decision composition | [EXP-0037](./EXP-0037.md) | Required | This synthesis decision | Satisfied with scope by Accepted EXP-0037; exact complete artifact identity remains G5 | +| O9 | Temporal zero-Step Opening accounts for the admitted Cadenced/AtTime persistence and authored-close branches exactly once. | One ordinary Cadenced zero-Step nonce-one close that rearms and removes its Tick reference within Opening | Child decision composition | [EXP-0038](./EXP-0038.md) | Required | This synthesis decision | Satisfied with scope by Accepted EXP-0038; exact complete artifact identity remains G5 | +| O10 | Crossing Opening rearm has a sound inclusive bound for its declared reachable membership, cursor and availability domain. | One admitted Crossing Opening removing a target before a live cursor and rearming into a populated leaf | Child decision composition | [EXP-0039](./EXP-0039.md) | Required | This synthesis decision | Satisfied with scope by Accepted EXP-0039; exact source/refusal composition remains EXP-0058 and complete artifact identity remains G5 | +| O11 | Observation subscription cleanup has a sound bound for admitted dense/sparse membership, unlink, dirty traversal and free-slot publication. | One ordinary last-subscriber close that unlinks the middle dirty feed and allocates the next free-slot page | Child decision composition | [EXP-0040](./EXP-0040.md) | Required | This synthesis decision | Satisfied with scope by Accepted EXP-0040; exact complete artifact identity remains G5 | +| O12 | Pipeline admission insolvency has one custody-neutral minimal-apoptosis owner covering its admitted detector, cold-tail and window domain. | One paid useful Trigger leaving the Actor one balance unit short of Pipeline admission with retained detector/window state | Child decision composition | [EXP-0041](./EXP-0041.md) | Required | This synthesis decision | Satisfied with scope by Accepted EXP-0041 and EXP-0058 whole-owner replacement; exact complete artifact identity remains G5 | +| O13 | Public close cleanup has one sound bound covering admitted Contract/Run, live detector cursor and owner-ledger release. | One public owner close after real Temporary suspension with retained Run captures and owner hold | Child decision composition | [EXP-0042](./EXP-0042.md) | Required | This synthesis decision | Satisfied with scope: EXP-0042 owns the admitted joint state and EXP-0058 selects EXP-0056 whole for populated Waiting removal; exact regeneration remains G5 | +| O14 | Window expiry source consumption and custody-neutral close have one complete terminal Control owner. | One unsignaled Immutable zero-Step User expiring through its Block primary before Pipeline admission | Child decision composition | [EXP-0043](./EXP-0043.md) | Required | This synthesis decision | Satisfied with scope by Accepted EXP-0043 and EXP-0058's inclusive due-worker/complete-removal partition; exact regeneration remains G5 | +| O15 | User state-hold reconciliation is included exactly once and bounded over admitted live geometry and the host hold ledger. | One admitted User Run transition changing live geometry and requiring host hold reconciliation after publication | Child decision composition | [EXP-0044](./EXP-0044.md) | Required | This synthesis decision | Satisfied with scope by Accepted EXP-0044; inclusive complete owner and exact regeneration remain G5 inputs | +| O16 | The funding snapshot consumption component covers the legal authored tracked-asset domain with certified ingress. | One admitted tracked asset funded by certified Signed ingress, then consumed by funding_snapshot_open | Child decision composition | [EXP-0047](./EXP-0047.md) | Required | This synthesis decision | Satisfied with scope by Accepted EXP-0047; configured-capacity composition and complete regeneration remain G4/G5 | +| O17 | Each useful Trigger transition and deferred family detector has one sound charged owner, with redundant latched work excluded. | One useful versus already-latched Trigger occurrence with an omitted detector owner or duplicate Actor-specific evaluation/charge | Child decision composition | [EXP-0057](./EXP-0057.md) | Required | This synthesis decision | Satisfied with scope by Accepted EXP-0057 and EXP-0058 placement composition; exact complete artifact identity remains G5 | +| O18 | Scheduler discovery, source consumption and destination publication compose with exactly one charged physical transition owner. | One consumed FIFO source followed by retry publication with omitted or duplicate source/destination work | Child decision composition | [EXP-0058](./EXP-0058.md) | Required | This synthesis decision | Satisfied for the admitted reference ownership domain by Accepted EXP-0058; Accepted EXP-0063 supplies numeric maximal-cell qualification without reopening placement | +| O19 | Running and Suspended control includes reconstruction, retry, persistence and completion exactly once over admitted geometry. | One admitted Running/Suspended transition whose reconstruction, retry, persistence or reference work lacks a charged Control owner | Child decision composition | [EXP-0059](./EXP-0059.md) | Required | This synthesis decision | Satisfied by Accepted EXP-0059: exact complete successors cover the admitted partition, and low-capture Continued→Wakeup component-wise contains dense `S=3` under the pinned DB schedule | +| O20 | The control/effect settlement selector remains component-wise contained by its matching reservation and charges no effect for NotInvoked. | One typed NotInvoked or Invoked actual exceeding its matching control/effect reservation in either resource dimension | Child decision composition | [EXP-0060](./EXP-0060.md) | Required | This synthesis decision | Satisfied by Accepted EXP-0060: the current provider is total, actuals are component-wise contained and one-unit overruns roll back atomically | +| O21 | Nonzero Action fee collection has one complete Control owner, selected independently of invocation receipts and Pipeline collection. | One counterexample to Nonzero Action fee collection has one complete Control owner, selected independently of invocation receipts and Pipeline collection. | Child decision composition | [EXP-0062](./EXP-0062.md) | Required | This synthesis decision | Satisfied with scope by Accepted EXP-0062; complete production Weight identity remains G5 | + +## Measurements + +No primary measurements. This matrix composes child decisions; it does not turn a diagnostic or isolated component into complete production acceptance. + +| Phase / semantic owner | Physical owner | Weight owner | Monetary owner | Inclusive/separate | Child proof | Status | Downstream consumer | +| --- | --- | --- | --- | --- | --- | --- | --- | +| Creation | Identity/Contract/placement/hold | create/activate/update dispatch | Creation Fee and refundable hold | Separate dispatch | [EXP-0064](./EXP-0064.md) | Open exact generated binding | [EXP-0064](./EXP-0064.md) then [EXP-0065](./EXP-0065.md) | +| Useful Trigger latch and deferred detection | Family detector and pending transition | Useful transition/family worker | Trigger Fee only on useful latch | Separate from Pipeline | [EXP-0057](./EXP-0057.md) | Six-family useful occurrence matrix and redundant exclusion accepted; source/destination composition and regeneration remain | [EXP-0064](./EXP-0064.md) then [EXP-0065](./EXP-0065.md) | +| FIFO discovery and source/successor atom | Ready/Waiting primary, locator, cutoff | Outer source/placement owners or per-entry complete replacement | Prepaid Pipeline Machine | Generic source plus one exact successor, or one complete-owner replacement | [EXP-0058](./EXP-0058.md), [EXP-0063](./EXP-0063.md) | Ownership accepted: direct successors remain inclusive, unmatched Waiting publishes once, due workers own removal, populated close selects EXP-0056 whole; Accepted EXP-0063 supplies numeric publication qualification | [EXP-0064](./EXP-0064.md) then [EXP-0065](./EXP-0065.md) | +| Pipeline Opening collection | Payer/Fee Sink and admitted head | charge_pipeline_opening | Pipeline Machine and cleanup capacity | Inclusive in complete User Opening | [EXP-0031](./EXP-0031.md) | Scoped owner accepted; complete file regeneration remains | [EXP-0064](./EXP-0064.md) then [EXP-0065](./EXP-0065.md) | +| Action collection | Payer and Fee Sink ledger | fee_collection | Pay-as-attempted Action effect fee | Separate from receipt; inclusive only when demonstrated | [EXP-0062](./EXP-0062.md) | Scoped owner accepted; complete file regeneration remains | [EXP-0064](./EXP-0064.md) then [EXP-0065](./EXP-0065.md) | +| Invocation receipt | System event storage | action_invocation_receipt and inclusive CompleteMax | No independent fee | Separate or inclusive by typed invocation selector | [EXP-0030](./EXP-0030.md) | Scoped V5 boundary retained | [EXP-0064](./EXP-0064.md) then [EXP-0065](./EXP-0065.md) | +| Current predicate | Current typed host reads and plan | Current component/selector | Prepaid Pipeline Machine | Composed component; direct paths remain inclusive | [EXP-0032](./EXP-0032.md) | Scoped reference component and selector accepted; complete file regeneration remains | [EXP-0064](./EXP-0064.md) then [EXP-0065](./EXP-0065.md) | +| Opening predicate capture | Contract traversal and typed predicate host reads | opening_predicate_capture | Prepaid Pipeline Machine | Nonzero capture alternative | [EXP-0033](./EXP-0033.md) | Scoped reference component and selector accepted; complete file regeneration remains | [EXP-0064](./EXP-0064.md) then [EXP-0065](./EXP-0065.md) | +| Opening amount capture | Typed amount surfaces and host ledger | opening_snapshot_capture and qualified variants | Prepaid Pipeline Machine | Nonzero capture alternative | [EXP-0034](./EXP-0034.md) | Scoped reference component and selector accepted; complete file regeneration remains | [EXP-0064](./EXP-0064.md) then [EXP-0065](./EXP-0065.md) | +| Zero-result traversal | Contract predicate/amount traversal | Two distinct traversal methods | Prepaid Pipeline Machine | Alternative to nonzero capture; never additive to direct inclusive paths | [EXP-0035](./EXP-0035.md), [EXP-0036](./EXP-0036.md) | Scoped fixed owners and mutually exclusive selectors accepted; complete file regeneration remains | [EXP-0064](./EXP-0064.md) then [EXP-0065](./EXP-0065.md) | +| Funding consumption | ActorFunding accumulation/snapshot | funding_snapshot_open | Prepaid Pipeline Machine | Nonzero actual 1..24 component; zero absent; excludes holds and whole Opening | [EXP-0047](./EXP-0047.md) | Scoped authored-domain component accepted; configured-capacity composition and complete regeneration remain | [EXP-0064](./EXP-0064.md) then [EXP-0065](./EXP-0065.md) | +| Manual zero-Step base | Admitted head/control and User ledger | Complete System/User zero-Step owner | Pipeline Machine; refundable hold distinct | Inclusive collection/hold | [EXP-0037](./EXP-0037.md) | Scoped actor-class partition and maximum-head User owner accepted; complete file regeneration remains | [EXP-0064](./EXP-0064.md) then [EXP-0065](./EXP-0065.md) | +| Temporal Opening | Tick reference, consumed AtTime state, control | Temporal zero-Step complete owner | No second Trigger Fee | Inclusive rearm or authored close | [EXP-0038](./EXP-0038.md) | Scoped AtTime/Cadenced × persistence/close partition accepted; EXP-0058 forbids a second Waiting owner and Accepted EXP-0063 supplies numeric qualification | [EXP-0064](./EXP-0064.md) then [EXP-0065](./EXP-0065.md) | +| Crossing Opening | Crossing membership/radix/cursor | Crossing rearm complete owner or unavailable refusal | No second Trigger Fee; no Pipeline collection on refusal | Inclusive available persistence/close alternatives; unavailable refusal separate | [EXP-0039](./EXP-0039.md) | Scoped reference membership/cursor/availability partition accepted; source composition and complete regeneration remain | [EXP-0064](./EXP-0064.md) then [EXP-0065](./EXP-0065.md) | +| Observation close | Subscription pages, dirty fanout, free slots | Subscription cleanup | Prepaid cleanup; refundable hold release | Inclusive terminal atom | [EXP-0040](./EXP-0040.md) | Scoped component-wise dense/sparse unlink, cursor and free-page envelope accepted; complete regeneration remains | [EXP-0064](./EXP-0064.md) then [EXP-0065](./EXP-0065.md) | +| Admission insolvency | Contract/detector/window/hold cleanup | Complete full-FIFO pipeline apoptosis | No Pipeline collection; paid Trigger final | Inclusive terminal atom; generic source not added again | [EXP-0041](./EXP-0041.md) | Scoped maximum-Step Crossing/future-window owner accepted; populated heap and regeneration remain | [EXP-0064](./EXP-0064.md) then [EXP-0065](./EXP-0065.md) | +| Public close | Contract/Run/Waiting/detector/owner ledger | Inclusive admitted joint retry/Crossing close owner | No new fee; hold release | Complete public terminal atom; populated removal selects EXP-0056 whole | [EXP-0042](./EXP-0042.md), [EXP-0058](./EXP-0058.md) | Joint state owner accepted; populated Waiting branch replaces the narrower close rather than adding heap work; regeneration remains | [EXP-0064](./EXP-0064.md) then [EXP-0065](./EXP-0065.md) | +| Window expiry | Block source and terminal process cleanup | Inclusive singleton-Block expiry close owner | No new fee or custody movement | Source plus terminal owner exactly once; populated due/removal branch stays inclusive | [EXP-0043](./EXP-0043.md), [EXP-0058](./EXP-0058.md) | Scoped singleton expiry owner and outer due/removal partition accepted; numeric envelope and regeneration remain | [EXP-0064](./EXP-0064.md) then [EXP-0065](./EXP-0065.md) | +| User hold reconciliation | ActorStateHolds and owner hold ledger | reconcile_actor_state_hold_with_authority | Refundable hold; not a fee | Included once after publication | [EXP-0044](./EXP-0044.md) | Scoped maximum-Step User Opening inclusive owner accepted; no standalone coefficient | [EXP-0064](./EXP-0064.md) then [EXP-0065](./EXP-0065.md) | +| Run transition | Run head/payload, cursor and references | Exact direct or complete consumed-source successor owner | Paid machine reuse | `write_run_state` cross-boundary profiles rejected; no subtraction or additive destination reuse | [EXP-0059](./EXP-0059.md) | Accepted finite successor envelope; low-capture Continued→Wakeup contains dense `S=3`, and Failed→Unsignaled contains completion | [EXP-0064](./EXP-0064.md) then [EXP-0065](./EXP-0065.md) | +| Task effect and settlement | Host adapter plus reservation/finalized totals | TaskEffectWeightProvider and settlement selector | Action effect fee only when attempted | NotInvoked zero effect; actual bounded by matching maximum | [EXP-0060](./EXP-0060.md) | Current provider and transactional containment accepted; exact artifact regeneration remains | [EXP-0064](./EXP-0064.md) then [EXP-0065](./EXP-0065.md) | + +## Derived Metrics + +Composition requires a common admitted domain and compatible source/Weight/artifact identities. Do not maximize independent storage capacities into an unreachable state, add inclusive owners twice, or derive a disjoint bound by subtracting aggregate coefficients. + +## Statistical / Variance Notes + +For generated FRAME evidence, pin 50 steps × 20 repeats where applicable and review model/branch fit, not merely a mean. Identity and constructive correctness checks are deterministic; no wall-clock capacity inference. Declare a different method before measurement if the bounded question requires it. + +## Result + +The finite child map has one accepted owner for every declared phase and settlement boundary. Accepted EXP-0058 resolves Ready/Queue/Waiting/Unsignaled placement and complete-owner replacement; Accepted EXP-0063 now supplies the populated Waiting envelopes without changing that ownership. Accepted EXP-0060 closes typed control/effect settlement containment; numeric admission and exact artifact closure remain downstream. + +## Interpretation + +Every required child now supplies a compatible scoped decision, and the composition meets the phase-ownership criteria without claiming downstream numeric admission or artifact identity. Conditional optimization work remains unallocated until measured evidence earns a distinct question. + +## Decision + +Accepted for the declared phase-ownership domain. EXP-0030/0031/0062 partition receipt, Pipeline collection and Action collection; EXP-0032–0036 own Current and Opening capture/traversal selectors; EXP-0037–0044 own zero-Step, rearm, cleanup, apoptosis, public-close, expiry and User-hold boundaries; EXP-0047 owns authored funding consumption; EXP-0057 owns useful Trigger occurrences; EXP-0058/0059 own source/destination and Run successors; and EXP-0060 closes typed control/effect settlement containment. Reject empty-head collection as a complete bound, endpoint-only capture claims, standalone coefficients subtracted from inclusive aggregates and categorical allocation fits as count weights. Retain raw observations, generated-model residuals and targeted controls without manual padding or outlier deletion. This acceptance decides ownership and no-double-charge composition only; EXP-0029/0063/0064 retain reachable numeric admission, and EXP-0065 retains exact production artifacts. + +## Rejected Alternatives and Tradeoffs + +| Shortcut | Disposition | Reason | +| --- | --- | --- | +| Reopen C1 because this question is expensive | Excluded | Requires the explicit invariant/impossibility test in EXP-0025 | +| Reuse direct historical C1 as production acceptance | Excluded | Source/path/artifact authority differs | +| Hide missing control under another allowance | Excluded | Any uncovered branch, double charge, mismatched actual selector or collection-funded base undercount; classification by Task name alone is insufficient. | + +## Validity + +- `Establishes`: Source-bound phase partition; accepted scoped receipt, predicate/capture/traversal, collection, funding, Manual/temporal/Crossing, cleanup, apoptosis, public-close, expiry, Trigger and Run owners; and the admitted reference Ready/Queue/Waiting/Unsignaled placement and complete-owner replacement matrix. Inherited findings retain their declared numeric and artifact limits. +- `Does not establish`: New Weight soundness, final artifact identity, throughput, release acceptance or permission to change frozen C1. +- `Known limitations`: Receipt and CompleteMax t=0/1 are measured; CompleteMax t=2/3 fails reference admission. Accepted EXP-0063 owns numeric Waiting qualification, EXP-0064 owns complete admission composition, and exact generated binding remains G5; the accepted User hold value is inclusive, not a standalone coefficient. +- `Invalidation triggers`: Relevant source, semantics, host bounds, Weight, SDK/toolchain, configuration, fixture or dependency identity changes; refresh affected descendants. +- `Residual qualitative insight`: The four-owner matrix is input to EXP-0064 admission, EXP-0063 branch composition and EXP-0065 generation; numerical results require reachable-domain certification from EXP-0029 before production acceptance. + +## Follow-Up / Next Gradient + +Stop this Synthesis. Consume its accepted phase matrix in EXP-0064 without repeating receipt, capture, Trigger, source/destination, Run, Waiting-attribution, funding, User-hold or settlement witnesses. Reachability remains EXP-0029, Waiting numeric qualification remains EXP-0063, and admission/exact binding belong to EXP-0064/0065. + +## Architecture Impact + +- `Affected physical truth`: Control phase reservation and actual settlement, within frozen C1. +- `Architecture update`: Package architecture records the staged receipt/collection distinction and its binding limit; full numeric acceptance remains open. +- `Provenance citation`: `actors/EXP-0028` with decision scope and exact identity once decided. + +## Relations + +- `Decomposes into`: [EXP-0030](./EXP-0030.md), [EXP-0031](./EXP-0031.md), [EXP-0032](./EXP-0032.md), [EXP-0033](./EXP-0033.md), [EXP-0034](./EXP-0034.md), [EXP-0035](./EXP-0035.md), [EXP-0036](./EXP-0036.md), [EXP-0037](./EXP-0037.md), [EXP-0038](./EXP-0038.md), [EXP-0039](./EXP-0039.md), [EXP-0040](./EXP-0040.md), [EXP-0041](./EXP-0041.md), [EXP-0042](./EXP-0042.md), [EXP-0043](./EXP-0043.md), [EXP-0044](./EXP-0044.md), [EXP-0047](./EXP-0047.md), [EXP-0057](./EXP-0057.md), [EXP-0058](./EXP-0058.md), [EXP-0059](./EXP-0059.md), [EXP-0060](./EXP-0060.md), [EXP-0062](./EXP-0062.md) +- `Satisfies obligation`: [EXP-0064](./EXP-0064.md) O1 +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: [EXP-0025](./EXP-0025.md), [EXP-0029](./EXP-0029.md), [EXP-0030](./EXP-0030.md), [EXP-0031](./EXP-0031.md), [EXP-0032](./EXP-0032.md), [EXP-0033](./EXP-0033.md), [EXP-0034](./EXP-0034.md), [EXP-0035](./EXP-0035.md), [EXP-0036](./EXP-0036.md), [EXP-0037](./EXP-0037.md), [EXP-0038](./EXP-0038.md), [EXP-0039](./EXP-0039.md), [EXP-0040](./EXP-0040.md), [EXP-0041](./EXP-0041.md), [EXP-0042](./EXP-0042.md), [EXP-0043](./EXP-0043.md), [EXP-0044](./EXP-0044.md), [EXP-0047](./EXP-0047.md), [EXP-0057](./EXP-0057.md), [EXP-0058](./EXP-0058.md), [EXP-0059](./EXP-0059.md), [EXP-0060](./EXP-0060.md), [EXP-0063](./EXP-0063.md), [EXP-0062](./EXP-0062.md) +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: [EXP-0030](./EXP-0030.md), [EXP-0031](./EXP-0031.md), [EXP-0032](./EXP-0032.md), [EXP-0033](./EXP-0033.md), [EXP-0034](./EXP-0034.md), [EXP-0035](./EXP-0035.md), [EXP-0036](./EXP-0036.md), [EXP-0037](./EXP-0037.md), [EXP-0038](./EXP-0038.md), [EXP-0039](./EXP-0039.md), [EXP-0040](./EXP-0040.md), [EXP-0041](./EXP-0041.md), [EXP-0042](./EXP-0042.md), [EXP-0043](./EXP-0043.md), [EXP-0044](./EXP-0044.md), [EXP-0047](./EXP-0047.md), [EXP-0057](./EXP-0057.md), [EXP-0058](./EXP-0058.md), [EXP-0059](./EXP-0059.md), [EXP-0060](./EXP-0060.md), [EXP-0062](./EXP-0062.md) +- `Transfers question to`: None; independent findings require an explicit new transfer. +- `Produces input for`: [EXP-0064](./EXP-0064.md) +- `Reopen trigger`: A falsifier of this declared owner/domain or exact artifact identity; cost alone does not reopen frozen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0029.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0029.md new file mode 100644 index 00000000..bd5bf159 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0029.md @@ -0,0 +1,155 @@ +# EXP-0029 — Reachable Actor State Geometry + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Synthesis | +| Former IDs | None | +| Parent question | [EXP-0064](./EXP-0064.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-05 | +| Affected domain | Contract-produced state, host bounds and benchmark reachability | +| Physical mechanism | Reachable Contract, Opening, funding and Run geometry | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0025](./EXP-0025.md) — Accepted, physical architecture only | +| Successor experiment | [EXP-0028](./EXP-0028.md), [EXP-0064](./EXP-0064.md), [EXP-0063](./EXP-0063.md), [EXP-0065](./EXP-0065.md) | + +## Decision Question + +What is the actual reachable domain of Actor state under authored Contract semantics and host bounds? + +## Governing Specification + +- `Owners`: `template/pallets/actors/docs/specification.en.md` (sole ownership, lifecycle/Trigger/Pipeline/Action semantics, placement and scheduler); `docs/actors-resource-policy.specification.en.md` (component-wise admission/actual settlement, equal-thirds block policy); `docs/actors-performance-assurance.specification.en.md` (evidence and target). +- `Frozen physical input`: [EXP-0025](./EXP-0025.md) C1 Architecture Freeze. No change to FIFO, ticket chronology, Q1, causal floor, primary/reference ontology, source/successor atomicity or sole execution owner. +- `Semantic oracle`: A0 SHA-256 `8746e423168309975e509be91e4eb06fdf0eb0b43369d75f1ee62202a4341678` remains frozen; use only for relevant semantic equivalence, never as a Weight oracle. +- `Scope`: + +- Own relationships among Step count, Opening amount surfaces, Opening predicate results, funding-tracked assets, current predicates, Run payload and head/tail fragment geometry. +- Prove mutually exclusive amount-source families, DNF representability and host-specific bounds. Separate reachable maxima, conservative storage-only maxima and resource-admitted state. +- Own fixture reachability and elimination of synthetic state masquerading as Contract-produced state. Reconcile final storage/cardinality assumptions with forward/reverse process locators; EXP-0063 owns Waiting-directory/heap branch geometry. +- Output a mechanically usable domain: host parameters, constraints, task-family qualifications, admissible tuples, constructive witnesses and boundary rejection assertions. Do not change authored semantics to manufacture a convenient maximum. + +## Context and Hypothesis + +This broad decision composes the finite obligations below. Detailed evidence was extracted from pre-fission records at `a60de7748132211d9e58cecd5be82b41f76ce230` without measurement or promotion of scope. C1 remains frozen. Newly independent proof questions must be transferred before further investigation. + +G3 structural reconciliation remains completed only within its audited host/schema scope. Ordinary correction chronology belongs to Git and regression tests; the manifest identifies the pre-fission paragraphs and exact source hashes. This is not production resource acceptance. + +Retained G3 regression provenance: the strict loader and TryRuntime audits reconcile head/tail ownership, exact Opening cursor, positive tracked funding, Trigger-derived subscription membership, allocation frontier/free-page partition, connected pages, and strict retained Crossing revisions. Ordinary fixes add no new consensus owner. Final source `1e4fd7d`, `lib.rs` SHA-256 `5b08ad7fd52cfefd52acf55fb6fe24ee862c71c70f4719ea7b6442432ff3aefd`, scheduler SHA-256 `8ed61e1a5c009eb7a940e4fc9f816c74d131a502ffa49d13d88db9e7099a95cd`; retained package result 1,079 passed/five ignored and native runtime TryRuntime result 203 passed/21 ignored. Earlier exact correction identities remain enumerated with their original source lines in the migration manifest; Git/tests own the repair sequence. This does not certify production Weight or historical host receipt mappings. + +## Baseline and Candidates + +Composition of the retained frozen C1 baseline and declared child decisions. This Synthesis introduces no benchmark implementation or candidate sweep. + +## Controls + +- `Controlled variables`: Freeze source/configuration, host bounds, toolchain/SDK, database/cache, legal state and workload for each comparison; name any changed input and refresh dependent evidence. +- `Changed variable`: Only the question-specific model, fixture, accounting or binding under test; no macrogeometry or authored semantics. +- `Confounders`: Historical native, direct-path, partial-heap and old generated evidence are narrower than production acceptance. [EXP-0026](./EXP-0026.md) and [EXP-0027](./EXP-0027.md) reject ceiling-only and state-hold-cache-only proof improvements; do not reintroduce them without changed evidence. + +## Benchmark Class, Workloads, and Criteria + +- `Smallest falsifier`: Attempt the old 24 Opening / 48 predicate-result / 40 funding joint maximum through actual creation, certified ingress, useful Opening and Q1. Reject the fixture if no authored allocation realizes it; retain its storage-only classification where appropriate. +- `Evidence class`: Constructive lifecycle/property/TryRuntime evidence; production-Wasm fixture verification where consumed by Weight. + +| Domain partition | Constructive witness | Boundary | +| --- | --- | --- | +| Amount sources | Pure Opening, pure LastFunding and mixed two-leg Tasks | Distinct union cardinality, deduplication and family exclusivity | +| Predicates | Opening/Current; true/false; multi-clause DNF | Effective capacity and evaluation timing, not independent impossible maxima | +| Run/current Step | Opening, middle/terminal Running, Temporary suspension | Captures, funding and current tail from actual prefix execution | +| Fragment/host | Every head/tail length, reference and embedding hosts | Full current fragment differs from cursor; no silent clamping | +| Storage/authority | Active/dormant, locator/primary and reference counts | Reachable population versus mechanically enforced storage bound | + +- `Acceptance criteria`: Provide executable-use constraints and real lifecycle constructors or validated tuple generation, with TryRuntime/property witnesses for minimal/interior/maximal points and explicit infeasible tuples. For each benchmark axis, prove reachability under its host and semantic Task family; storage-only probes cannot certify production transitions. +- `Rejection criteria`: Injected Run/Contract mutation, admission bypass presented as lifecycle reachability, DNF overflow, host-specific bound generalized universally, or bounds changed merely to make a benchmark pass. + +## Environment and Artifact Identity + +Migration provenance: Git baseline `a60de7748132211d9e58cecd5be82b41f76ce230` and the content/mode manifest archived at Git commit `6c650695568a6e5d05437351ef014e8ee950c484`, repository path `.agents/skills/architecture-experiments/migrations/actors-proof-graph-2026-09-06.json`. Each Leaf retains its measured artifact identity. No final production artifact is created or certified by this migration. + +## Proof Obligations + +- `Freeze`: Frozen by explicit migration reconstruction from the pre-fission decision and retained witnesses at a60de7748132211d9e58cecd5be82b41f76ce230. No new measurement or historical decision is implied. +- `Review triggers`: None. +- `Decomposition review`: Synthesis owns only the frozen finite composition; independent primary evidence is owned by the reciprocal child map. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | Authored amount positions bound Opening and LastFunding cardinalities jointly, with host-qualified receipt identity. | One two-leg authored Step split between Opening and LastFunding, plus an admitted Unstake receipt whose current host mapping disappears | Child decision composition | [EXP-0045](./EXP-0045.md) | Required | This synthesis decision | Satisfied with scope; original binding/host limitations retained; complete release binding remains open | +| O2 | Canonical DNF and timed predicate occurrences determine admitted result cardinality and exact committed-prefix cursor. | One canonical DNF whose remaining Opening occurrences or committed-prefix result cursor differ from the derived count | Child decision composition | [EXP-0046](./EXP-0046.md) | Required | This synthesis decision | Satisfied with scope; original binding/host limitations retained; complete release binding remains open | +| O3 | Public lifecycle constructors jointly realize the declared Run, head, tail, cursor and zero-Step encoded geometry. | One terminal one-Step head incorrectly retaining a tail, or a proposed encoded maximum that public construction cannot attain | Child decision composition | [EXP-0048](./EXP-0048.md) | Required | This synthesis decision | Satisfied by Accepted EXP-0048 for the admitted mock/reference host domain; refused dense tuples remain excluded | +| O4 | Additional System creation is bounded by the joint active, identity and retained-sovereign capacities of the named host history. | One ordinary System creation beyond the minimum remaining active/identity/sovereign capacity with genesis identities retained | Child decision composition | [EXP-0049](./EXP-0049.md) | Required | This synthesis decision | Satisfied by Accepted EXP-0049 for the named retained-genesis host histories | +| O5 | Ordinary lifecycle construction realizes the declared Waiting Primary publication and removal populations without injected scheduler state. | One public retry publishing into a full Waiting Primary page or new minimum key under the declared saturated host population | Child decision composition | [EXP-0050](./EXP-0050.md) | Required | This synthesis decision | Satisfied by Accepted EXP-0050 for existing/new-minimum/full-page publication and upward/tail removal populations | +| O6 | Ordinary lifecycle construction realizes the declared mixed and final-Tick Waiting Reference populations without injected reference state. | One ordinarily constructed cleanup removing the final Tick reference or reclaiming a mixed Reference page | Child decision composition | [EXP-0051](./EXP-0051.md) | Required | This synthesis decision | Satisfied by Accepted EXP-0051 for mixed-page and saturated final-Tick histories | +| O7 | Ordinary admission, funding and actual suspension jointly realize the declared User close capture/detector/Run geometry. | One admitted capture allocation that fails real suspension/public close, or whose retained detector/Run geometry differs from its certificate | Child decision composition | [EXP-0052](./EXP-0052.md) | Required | This synthesis decision | Satisfied by Accepted EXP-0052 for the observed admitted reference subdomain and adjacent exclusions | +| O8 | The V10 charged proof lower bound suffices to reject the five declared maximal lifecycle tuples through ordinary admission. | One of the five declared V10 maximum tuples whose charged proof lower bound does not exceed the strict service ceiling | Child decision composition | [EXP-0053](./EXP-0053.md) | Required | This synthesis decision | Satisfied with scope; original binding/host limitations retained; complete release binding remains open | +| O9 | The maximum claimed encoded ActorControlCell is jointly attainable under the host lifecycle, pointer and compact Weight constraints. | One counterexample to The maximum claimed encoded ActorControlCell is jointly attainable under the host lifecycle, pointer and compact Weight constraints. | Child decision composition | [EXP-0061](./EXP-0061.md) | Required | This synthesis decision | Scoped satisfied by Accepted child; reference Weight binding must be requalified at G5 | + +## Measurements + +No primary measurements. Consume only the scoped decisions of the children declared in Proof Obligations. Native correctness, diagnostic construction, generated component bounds and exact production acceptance remain distinct evidence classes. + +## Derived Metrics + +Composition requires a common admitted domain and compatible source/Weight/artifact identities. Do not maximize independent storage capacities into an unreachable state, add inclusive owners twice, or derive a disjoint bound by subtracting aggregate coefficients. + +## Statistical / Variance Notes + +For generated FRAME evidence, pin 50 steps × 20 repeats where applicable and review model/branch fit, not merely a mean. Identity and constructive correctness checks are deterministic; no wall-clock capacity inference. Declare a different method before measurement if the bounded question requires it. + +## Result + +All nine obligations now compose over compatible declared scopes. Authored Task algebra constrains amount/funding state; canonical DNF fixes result cardinality and committed-prefix cursors; public constructors realize admitted zero-Step, head/tail, Running/Suspended, host-population and Waiting Primary/Reference histories. The five V10 maxima and adjacent User-close points remain ordinary resource-admission refusals, while the 334-byte reference control-cell maximum is publicly attained. Current-tree focused revalidation passes nine `reachable_`, 37 `waiting_` and 14 `close_actor` all-feature tests. + +## Interpretation + +Reachability is a constrained union of host-qualified lifecycle witnesses, not a Cartesian product of `12/24/48`, storage encodings or independent maxima. Structurally legal but resource-refused tuples remain outside the admitted reference domain; accepted legal population shapes feed numeric Waiting and admission composition without converting diagnostic timings into production Weight. + +## Decision + +Accepted for the declared mock/reference phase-geometry domain. The mechanically usable model combines `O + F ≤ L ≤ 2S`, canonical timed-DNF/prefix constraints, exact public head/tail/cursor constructors, host-history capacity intersections, legal Primary/Reference Waiting populations, admitted User suspension/close boundaries and explicit V10 refusals. This satisfies EXP-0064 O2. Accepted EXP-0063 now owns the numeric Waiting envelopes, EXP-0064 owns the final admission envelope, and EXP-0065 owns exact artifacts. + +## Rejected Alternatives and Tradeoffs + +| Shortcut | Disposition | Reason | +| --- | --- | --- | +| Reopen C1 because this question is expensive | Excluded | Requires the explicit invariant/impossibility test in EXP-0025 | +| Reuse direct historical C1 as production acceptance | Excluded | Source/path/artifact authority differs | +| Change semantics to realize maxima | Excluded | Injected Run/Contract mutation, admission bypass presented as lifecycle reachability, DNF overflow, host-specific bound generalized universally, or bounds changed merely to make a benchmark pass. | + +## Validity + +- `Establishes`: The declared reachable-state model across authored amount/predicate algebra, public head/tail/Run constructors, named host capacity intersections, legal Waiting populations, admitted User-close geometry, V10 exclusions and the attained reference control-cell maximum. +- `Does not establish`: New Weight soundness, final artifact identity, throughput, release acceptance or permission to change frozen C1. +- `Known limitations`: The four maximal-tail heads and independent 12/24/48 User maximum are resource-inadmissible on the recorded binding. Current host mapping cannot reconstruct historical Unstake receipt identity. Numeric Waiting/admission composition, complete scheduler/Executive Weight and final artifacts remain downstream. +- `Invalidation triggers`: Relevant source, semantics, host bounds, Weight, SDK/toolchain, configuration, fixture or dependency identity changes; refresh affected descendants. +- `Residual qualitative insight`: Certified reachable tuples and constructors feed EXP-0028 coverage, EXP-0064 admission, EXP-0063 legal population setup and EXP-0065 bounds/fixtures. Resource admissibility remains EXP-0064's distinct decision. + +## Follow-Up / Next Gradient + +EXP-0064 now consumes this accepted reachable domain together with Accepted EXP-0063. Do not repeat satisfied constructors or rejected tuples. Any changed host bound, Task algebra, lifecycle mechanism or production Weight requalifies only the affected domain before admission/artifact closure. + +## Architecture Impact + +- `Affected physical truth`: Reachable Contract, Opening, funding and Run geometry, within frozen C1. +- `Architecture update`: Funding architecture describes the qualified audit and its inability to certify historical receipt identity from current lookup; full reachable-domain acceptance remains open. +- `Provenance citation`: `actors/EXP-0029` with decision scope and exact identity once decided. + +## Relations + +- `Decomposes into`: [EXP-0045](./EXP-0045.md), [EXP-0046](./EXP-0046.md), [EXP-0048](./EXP-0048.md), [EXP-0049](./EXP-0049.md), [EXP-0050](./EXP-0050.md), [EXP-0051](./EXP-0051.md), [EXP-0052](./EXP-0052.md), [EXP-0053](./EXP-0053.md), [EXP-0061](./EXP-0061.md) +- `Satisfies obligation`: [EXP-0064](./EXP-0064.md) O2 +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: [EXP-0025](./EXP-0025.md), [EXP-0045](./EXP-0045.md), [EXP-0046](./EXP-0046.md), [EXP-0048](./EXP-0048.md), [EXP-0049](./EXP-0049.md), [EXP-0050](./EXP-0050.md), [EXP-0051](./EXP-0051.md), [EXP-0052](./EXP-0052.md), [EXP-0053](./EXP-0053.md), [EXP-0061](./EXP-0061.md) +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: [EXP-0045](./EXP-0045.md), [EXP-0046](./EXP-0046.md), [EXP-0048](./EXP-0048.md), [EXP-0049](./EXP-0049.md), [EXP-0050](./EXP-0050.md), [EXP-0051](./EXP-0051.md), [EXP-0052](./EXP-0052.md), [EXP-0053](./EXP-0053.md), [EXP-0061](./EXP-0061.md) +- `Transfers question to`: None; independent findings require an explicit new transfer. +- `Produces input for`: [EXP-0064](./EXP-0064.md) +- `Reopen trigger`: A falsifier of this declared owner/domain or exact artifact identity; cost alone does not reopen frozen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0030.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0030.md new file mode 100644 index 00000000..975fd8f2 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0030.md @@ -0,0 +1,200 @@ +# EXP-0030 — Action Invocation Receipt Ownership + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | [EXP-0028](./EXP-0028.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-06 | +| Affected domain | Every general-path invoked Action receipt has exactly one Control owner, including inclusive CompleteMax and zero-fee System invocation. | +| Physical mechanism | Action Invocation Receipt Ownership | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0025](./EXP-0025.md) — Accepted, physical architecture only | +| Successor experiment | [EXP-0028](./EXP-0028.md) | + + +## Decision Question + +Every general-path invoked Action receipt has exactly one Control owner, including inclusive CompleteMax and zero-fee System invocation. + +## Governing Specification + +- `Owners`: `template/pallets/actors/docs/specification.en.md` (sole ownership, lifecycle/Trigger/Pipeline/Action semantics, placement and scheduler); `docs/actors-resource-policy.specification.en.md` (component-wise admission/actual settlement, equal-thirds block policy); `docs/actors-performance-assurance.specification.en.md` (evidence and target). +- `Frozen physical input`: [EXP-0025](./EXP-0025.md) C1 Architecture Freeze. No change to FIFO, ticket chronology, Q1, causal floor, primary/reference ontology, source/successor atomicity or sole execution owner. +- `Semantic oracle`: A0 SHA-256 `8746e423168309975e509be91e4eb06fdf0eb0b43369d75f1ee62202a4341678` remains frozen; use only for relevant semantic equivalence, never as a Weight oracle. + +## Context and Hypothesis + +This Leaf owns action invocation receipt ownership. Its boundary is the single claim above. Evidence was extracted from the explicitly marked pre-fission parent subsections at Git baseline `a60de7748132211d9e58cecd5be82b41f76ce230`; this migration changes navigation and evidence ownership only. Source, Wasm, commands, rejection outcomes and evidence limitations retain their original meaning. + +Extraction provenance at the recorded Git baseline: EXP-0028 / “Isolated Invocation Receipt”, “Receipt Coverage Partition”, “Reference Wasm Inclusion and Admission Boundary”, “Measurements”. Original directional prose (above/below) belongs to these pre-fission subsections; current cross-owner navigation follows the parent proof map. + +## Baseline and Candidates + +Retained measured implementations and rejected candidates are identified in the extracted evidence. No candidate is introduced by this migration. + +## Controls + +Only the declared witness input varies within each source-bound comparison. Preserve the exact host, toolchain, database, Wasm and sampling controls printed with each evidence item; do not compare unlike artifacts as a controlled performance result. + +## Benchmark Class, Workloads, and Criteria + +Acceptance requires the single claim to hold over its declared admitted domain, with each physical operation charged once and every retained limitation resolved before broader production use. One legal omitted-owner, domain or containment counterexample falsifies that claim. Existing workloads below are bounded witnesses, not permission to add independent questions. + +## Environment and Artifact Identity + +Extraction baseline: `a60de7748132211d9e58cecd5be82b41f76ce230`. The migration manifest records exact original content/mode identities and source spans. Question-specific source/Wasm/output identities remain inline. Historical fragments remain historical; no new runtime or production artifact was built. + +| Artifact | Identity / required binding | +| --- | --- | +| Baseline source | Commit `5b46b19004b62a723d6c45a610dfe1e5c79982b0`; template tree `98af72ffbb55fe8840b5bb44b5b01d65c8e775e1` | +| New experimental source/config/toolchain | Preparation source above; Rust 1.94.1, reference 12/24/48 and equal thirds. Receipt measurement identity is pinned below; later selector binding is a distinct source | +| Benchmark / production Wasm | EXP-0029 reference benchmark Wasm `c79eaec1e59bd2f69a8912ddce1b59fa65bf37f784b7d2c7dd5eaac3c129b696` supplies scoped terminal geometry only; the question-specific benchmark Wasm identities are pinned below, with no production-Wasm acceptance | +| Generated Weight / metadata / ABI | Not accepted by this experiment | +| Commands | Preparation uses owning `scripts/benchmarks.sh --help` / `--check` and `scripts/actors-assurance.sh --help`; isolated receipt command and measurement are pinned below | +| Evidence owner | Baseline source and the question-specific facts below; new measurements must carry exact source/artifact identities in this record | + +## Proof Obligations + +- `Freeze`: Frozen by explicit migration reconstruction from the pre-fission decision and retained witnesses at a60de7748132211d9e58cecd5be82b41f76ce230. No new measurement or historical decision is implied. +- `Review triggers`: None. +- `Decomposition review`: Boundary reviewed during provenance-preserving fission. Workloads within this Leaf share the declared claim, physical owner, changed variable, evidence domain and acceptance rule. Independently decidable owners were transferred; a new owner or selector requires another node. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | Every general-path invoked Action receipt has exactly one Control owner, including inclusive CompleteMax and zero-fee System invocation. | One general-path System invocation and one inclusive CompleteMax receipt; a missing or doubled event owner falsifies the selector | Reference-Wasm FRAME evidence in the declared owner/domain; native checks are construction controls | Self | Required | Named parent decision and declared downstream consumers | Satisfied for the admitted reference domain; isolated generated owner, inclusive CompleteMax and zero-fee System selection are evidenced | + +## Measurements + + +### Isolated Invocation Receipt + +The Wasm benchmark calls the same receipt helper as general Step execution, at maximum encoded field widths, with block setup outside the measured block. This is an event primitive, not a lifecycle or full-path benchmark. FRAME whitelists System event storage: estimated ProofSize zero does not mean the operation touches no storage. + +| Item | Measured evidence | +| --- | --- | +| Measurement source content/modes | `d60527d6f85d18bb09bf525a64ae4164beb35fb75d089029462de2b99a446d56` | +| Benchmark source SHA-256 | `60c7002f4c12b1db8162756096b83079b41defba283cf4ab91efae49317f91ec` | +| Benchmark Wasm SHA-256 | `ae689f0d34e29305fe856bd67d9ad8fb77c7ab8338cf6943202d3f2aa2d7f6c4` | +| Generated fragment SHA-256 | `393f7e844d475891115b45407c51840730af4b78294650db1a82c6121ad69203` | +| Later staged selector source / Weight SHA-256 | `4dc5f0ce2c32f6756c093c3a11196a4b576e3e3bf1aa4990d24beba512944f0f` / `6bad800cfbe4848b13f046b3598746e9a6163ec91a54ddbaa0f2e0b2fd5ba2ea`; primitive measurement reused only within its scope | +| Method / RefTime / estimated ProofSize | `action_invocation_receipt` / 4,330,000 ps / 0 bytes; measured minimum 2,724,000 ps | +| Runtime / environment | Reference benchmark runtime; Rust 1.94.1; FRAME CLI 58.0.0; compiled Wasm; AMD Ryzen 7 4800H; DB cache 1024; heap 4096; map bound 1,000,000; 50 steps × 20 repeats | +| Reproduction | `scripts/benchmarks.sh --extrinsic action_invocation_receipt --output /tmp/deos-exp28-action-invocation-receipt.rs pallet_deos_actors` after the owning preflight/build; fragment is temporary, normalized result retained here | + +The generated method is copied without coefficient or storage-comment repair into the staged runtime Weight implementation. Subsequent selector/type changes do not change the measured helper but do change whole-source identity; this primitive measurement cannot certify the later whole runtime, other generated methods, metadata, or production Wasm. EXP-0065 still requires complete regeneration. + +`StepControlExecution.task_effect` now carries invocation independently of `action_fee_collected`. V4 reserved base + collection + receipt and committed the coefficient in its identity. The V5 correction compares receipt-complete candidates before adding collection, and actual selection removes only collection before choosing a typed receipt-inclusive or receipt-separate envelope. An inclusive maximum fallback remains conservative even on noninvocation; the isolated constant is never subtracted from an aggregate. No Action economic fee changes. Legal package coverage spans User/System × Opening/Running/Suspended × noninvocation/success/typed failure (18 cases), asserting actual selector facts, receipt count, fee-sink delta and Task movement with TryRuntime consistency. Runtime complete-path expectations compose the generated receipt explicitly, including its absence on unfunded retry; no-effect and specialized StopCycle baselines retain their existing cost. + +Validation on the staged selector source: full `deos-runtime` default and all-features tests, full `pallet-deos-actors` all-features tests, workspace all-features Clippy, workspace check and changed-scope completion passed. The completion route separately reran default runtime tests; its prose-length failure was corrected without code changes and the remaining audits passed. Independent embedding passed the default profile (22 tests), combined `dex-fixture,try-runtime` profile (27 tests), and `cargo check --locked -p pallet-deos-actors-embedding-fixture --no-default-features`. These checks certify behavioral integration, not the unresolved numeric envelope. + +### Receipt Coverage Partition + +| Estimate family | Receipt in measured inner operation | V5 composition | +| --- | --- | --- | +| Opening CompleteMax | One, from true-predicate StopCycle on the general path | Compare inclusive maximum; actual returns inclusive owner | +| Opening CompleteMin | None; specialized unpredicated StopCycle | Add no receipt for that Step; other invoked Tasks using this base add the isolated owner | +| Running Complete | None; p=0 uses specialized StopCycle, p>0 has false Current predicates | Separate receipt for general-path invocation | +| Opening failed/retry/progress and Suspended head/tail | None; preparation failure, funding refusal, false predicate or zero resolution | Separate receipt for general-path invocation | +| Composed control primitives | None; no invocation-receipt depositor | Separate receipt for general-path invocation | +| Existing admitted-maximum fallback | Included in complete reservation | No second receipt addition; no unsupported subtraction on noninvocation | + +Native Opening CompleteMin/CompleteMax benchmark tests now assert the exact receipt event count outside the measured block, using the canonical host effect Weight. The runtime counterexample test spans all reference tail counts as selector algebra and distinguishes inclusive direct settlement from independent User collection; V4's extra receipt would violate the Invoked equality. + + + +### Reference Wasm Inclusion and Admission Boundary + +The V5 source `873161eb1838fb7e6d15b894f59c54ca41671a74514f70fff3df3901a5eb807a` produced benchmark Wasm `6f78ea5b9d9db5f7c385a86e2caeacf1e6ec9f95bf8522f835d45e24aef2276d`. Benchmark compilation passed; `scripts/benchmarks.sh --check` then rejected the known stale Actors/Router/TMC storage references. No generated comments were repaired. The full `scheduler_inner_opening_complete_max` route built successfully and failed in ordinary System creation with `ContractStepsExceedOnIdleBudget` before measured execution. + +| Tail count t | Authored Steps | Reference outcome | +| --- | --- | --- | +| 0 | 1 | Admission, execution and exactly-one-receipt postcondition pass | +| 1 | 5 | Admission, execution and exactly-one-receipt postcondition pass | +| 2 | 9 | Ordinary creation rejects `ContractStepsExceedOnIdleBudget`; no measured execution | +| 3 | 12 | Ordinary creation rejects `ContractStepsExceedOnIdleBudget`; no measured execution | + +Singleton low/high probes established those outcomes but could not generate a slope from one unique value; discard their singular regression output. A subsequent two-point range succeeded using `bin/frame-omni-bencher v1 benchmark pallet --runtime template/target/benchmarks/release/wbuild/deos-runtime/deos_runtime.compact.compressed.wasm --pallet pallet_deos_actors --extrinsic scheduler_inner_opening_complete_max --low 0 --high 1 --steps 50 --repeat 20 --heap-pages 4096 --output /tmp/deos-exp28-complete-max-admitted.rs`. Singleton probes use the same command with `--low t --high t`; full-range reproduction uses the owning `scripts/benchmarks.sh --extrinsic scheduler_inner_opening_complete_max --output /tmp/deos-exp28-opening-complete-inclusive.rs pallet_deos_actors`. + +| Measurement | Inclusive CompleteMax, t ∈ {0, 1} only | +| --- | --- | +| Generated RefTime before DB additions | 119,679,250 + 153,448,237 × t ps; slope standard error 658,394 ps | +| Estimated ProofSize | 5,755 + 22,675 × t bytes | +| Recorded proof size | 2,290 + 2,875 × t bytes | +| Reads / writes | 8 + 17 × t / 3 | +| Sample mean / sigma, t=0 | 119.6 / 5.734 µs | +| Sample mean / sigma, t=1 | 273.1 / 9.343 µs | +| Temporary generated fragment SHA-256 | `fec226433e891202c5b4291158e0358178767a1ac203ce1c316b6bae122b4106` | + +The fragment is not installed as a full-range production coefficient. The result confirms receipt inclusion on reference execution and supplies a bounded numeric input; it does not extrapolate to t=2/3, establish admission-envelope soundness, or replace G5 generation. EXP-0029 owns the qualified domain and EXP-0064 owns reconciliation of admission with the complete measured envelopes. C1 is not reopened. + +V5 retained source `0ad0b680ff6a7861249d50555cb911345a2d2a26e25d7e26bc296a6349a2e73e` additionally includes the corrected complete-path test expectations and architecture wording. Full default runtime tests pass. The all-features suite passed 467 tests and exposed two outdated receipt expectations; after correcting only those expectations, the complete-path matrix filter passed. Scoped Actors/runtime all-targets all-features Clippy, workspace check and changed-scope completion passed. Native CompleteMin/CompleteMax benchmark tests and the dedicated inclusive-owner selector test passed. This validation does not promote the stale complete generated Weight files or the limited Wasm range to release evidence. + + + +### Receipt Inclusive Boundary Audit + +The preparation gap was a missing demonstrated owner, not a proven numeric underbound: incidental margin in old coefficients proves neither attribution nor a bound. The staged candidate measures the exact receipt operation independently and exposes Invoked versus NotInvoked to the Control selector, preserving nonzero fee collection as a separate fact. Reject folding System receipts into a User collection allowance or charging the receipt to Task effects. User holds and trigger-specific rearm remain distinct unresolved rows. + +The source audit also finds that `scheduler_inner_opening_complete_max` uses true Opening predicates and StopCycle through the general path, so its measured atom already includes the receipt. The V4 blanket additive allowance therefore overlapped that direct owner. V5 excludes CompleteMax from receipt-free maxima, adds receipt to the remaining envelope, then takes the component-wise maximum with inclusive CompleteMax. Its actual selector marks CompleteMax and conservative maximum fallback as inclusive; all other audited direct and composed paths keep receipt separate. Passing accounting tests alone does not prove disjoint physical work. Running Complete with predicates instead expects NotInvoked, while its unpredicated specialization omits the receipt. Do not subtract an independently measured constant from an old aggregate coefficient to fabricate a disjoint bound. + + +## Derived Metrics + +Derived values are retained beside the evidence that defines their source and units. No subtraction of overlapping aggregate owners or cross-artifact timing comparison is introduced. + +## Statistical / Variance Notes + +Retain each item’s original sampling and uncertainty. Singleton construction probes establish only their admitted state/access witness, never statistical timing dominance or a production bound. + +## Result + +The isolated generated owner is `4,330,000 / 0`, and the admitted reference-Wasm CompleteMax points `t ∈ {0, 1}` execute with exactly one inclusive receipt. At code baseline `e92ab80`, `actor_config.rs` SHA-256 `0858133d39eaabb1d2591def2454f1b72d7ee6746b3c1f5ecd720f49a37750f9` and runtime Actors Weight SHA-256 `2d9c60ac7764d1e7dc366531dd4a409444daf66cf3e37ed7feb922bb614e8f1a` pass the package User/System × Opening/Running/Suspended receipt partition and the runtime inclusive-CompleteMax selector regression. The `t ∈ {2, 3}` constructors remain resource-inadmissible and therefore do not falsify ownership over the admitted domain. + +## Interpretation + +The evidence distinguishes three owners without subtraction: the invocation receipt is Control work, nonzero User collection is separate Control work owned by EXP-0062, and Task effects remain effect work. General-path System invocation selects the same receipt owner despite fee exemption; direct CompleteMax already includes it and must not add it twice. This decides ownership and the scoped generated primitive, not the final complete production Weight identity. + +## Decision + +Accepted for the admitted reference domain. Select the isolated generated receipt owner for receipt-free general paths, retain the inclusive CompleteMax owner without a second addition, and charge no receipt for `NotInvoked`. Exact full-file regeneration and final benchmark/production Wasm binding remain EXP-0065 rather than an open obligation of this Leaf. + +## Rejected Alternatives and Tradeoffs + +Retain all rejected candidates and causal limits in the extracted evidence. Do not infer a new selection from record relocation or from aggregate coefficient subtraction. + +## Validity + +- `Establishes`: Exactly one invocation-receipt Control owner over the admitted reference domain, including zero-fee System invocation and inclusive CompleteMax selection, plus the isolated generated primitive within its recorded artifact identity. +- `Does not establish`: Complete production Weight, exact release artifact closure, inadmissible `t ∈ {2, 3}` execution or integrated throughput. +- `Known limitations`: CompleteMax production-Wasm evidence is limited to admitted `t ∈ {0, 1}`; the isolated primitive and staged selector are not a final complete generated Weight file. +- `Invalidation triggers`: A changed measured body, host domain, source/Weight/Wasm identity or acceptance rule requires scoped requalification. +- `Residual qualitative insight`: Follow the owning physical boundary, not a benchmark name. + +## Follow-Up / Next Gradient + +Stop measuring this satisfied owner. EXP-0062 independently decides nonzero Action fee collection; EXP-0065 regenerates and binds the complete production artifacts. Reopen only on the declared falsifier or changed relevant identity, not because broader admission or release binding remains open. + +## Architecture Impact + +No runtime, semantic, storage or production binding change. C1 and A0 remain frozen; this record owns only its declared proof evidence. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: [EXP-0028](./EXP-0028.md) O1 +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: [EXP-0025](./EXP-0025.md) +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: None. +- `Transfers question to`: None; independent findings require an explicit new transfer. +- `Produces input for`: [EXP-0028](./EXP-0028.md) +- `Reopen trigger`: A falsifier of this declared owner/domain or exact artifact identity; cost alone does not reopen frozen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0031.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0031.md new file mode 100644 index 00000000..cc974fc0 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0031.md @@ -0,0 +1,179 @@ +# EXP-0031 — Pipeline Opening Collection Ownership + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | [EXP-0028](./EXP-0028.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-06 | +| Affected domain | Pipeline Opening collection has one complete inclusive Control owner for admitted User service. | +| Physical mechanism | Pipeline Opening Collection Ownership | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0025](./EXP-0025.md) — Accepted, physical architecture only | +| Successor experiment | [EXP-0028](./EXP-0028.md) | + + +## Decision Question + +Pipeline Opening collection has one complete inclusive Control owner for admitted User service. + +## Governing Specification + +- `Owners`: `template/pallets/actors/docs/specification.en.md` (sole ownership, lifecycle/Trigger/Pipeline/Action semantics, placement and scheduler); `docs/actors-resource-policy.specification.en.md` (component-wise admission/actual settlement, equal-thirds block policy); `docs/actors-performance-assurance.specification.en.md` (evidence and target). +- `Frozen physical input`: [EXP-0025](./EXP-0025.md) C1 Architecture Freeze. No change to FIFO, ticket chronology, Q1, causal floor, primary/reference ontology, source/successor atomicity or sole execution owner. +- `Semantic oracle`: A0 SHA-256 `8746e423168309975e509be91e4eb06fdf0eb0b43369d75f1ee62202a4341678` remains frozen; use only for relevant semantic equivalence, never as a Weight oracle. + +## Context and Hypothesis + +This Leaf owns pipeline opening collection ownership. Its boundary is the single claim above. Evidence was extracted from the explicitly marked pre-fission parent subsections at Git baseline `a60de7748132211d9e58cecd5be82b41f76ce230`; this migration changes navigation and evidence ownership only. Source, Wasm, commands, rejection outcomes and evidence limitations retain their original meaning. + +Extraction provenance at the recorded Git baseline: EXP-0028 / “Large-Head Collection Comparison”, “User Zero-Step Comparison”. Original directional prose (above/below) belongs to these pre-fission subsections; current cross-owner navigation follows the parent proof map. + +## Baseline and Candidates + +Retained measured implementations and rejected candidates are identified in the extracted evidence. The deciding candidate is the existing inclusive User-opening ownership: use the complete maximum-head User zero-Step owner for zero-Step service and retain the complete nonzero-Step User Opening profile, while the isolated large-head collection primitive classifies the collection-specific storage envelope. No aggregate subtraction or partial generated-file installation is admitted. + +## Controls + +Only the declared witness input varies within each source-bound comparison. Preserve the exact host, toolchain, database, Wasm and sampling controls printed with each evidence item; do not compare unlike artifacts as a controlled performance result. + +## Benchmark Class, Workloads, and Criteria + +Acceptance requires the single claim to hold over its declared admitted domain, with each physical operation charged once and every retained limitation resolved before broader production use. One legal omitted-owner, domain or containment counterexample falsifies that claim. Existing workloads below are bounded witnesses, not permission to add independent questions. + +## Environment and Artifact Identity + +Pre-fission baseline `a60de7748132211d9e58cecd5be82b41f76ce230`. Each extracted block retains its own exact source/Wasm/output identity. Shared pinned hardware/toolchain/cache controls follow [EXP-0030](./EXP-0030.md#environment-and-artifact-identity) only where the original comparison explicitly reuses them. A shared environment never substitutes for the block’s own source or Wasm identity. + +The deciding rerun uses code baseline `e92ab80` plus benchmark-only postcondition strengthening, final `benchmarking.rs` SHA-256 `56178d1e89c600bed20dc737e9fe96376929c9d41c00cd0cd9586dea66d8bdba`, unchanged scheduler SHA-256 `bdedd82571a229f3609194547c6256d8ca454055640e407c2c9af03d2245fbd1`, unchanged pallet root SHA-256 `7432b3b5df56cf7507d0991270b62a5a35e4437a2548e81bd6bd66bb57d6c788`, benchmark script SHA-256 `303d94b87dd04514118764b96c8c064da758337cc044cd57ba59098cdf292632` and benchmark Wasm SHA-256 `cb901d049e328d2d1074fabf3f3583e6800526e7ba9b4188f8a258ee04e0a1a6`. The postconditions do not alter a measured block: they check exact Pipeline payer/Sink deltas, exactly one receipt, persistent zero-Step hold identity, and the nonzero-Step funding-hold reconciliation boundary. + +## Proof Obligations + +- `Freeze`: Frozen by explicit migration reconstruction from the pre-fission decision and retained witnesses at a60de7748132211d9e58cecd5be82b41f76ce230. No new measurement or historical decision is implied. +- `Review triggers`: Existing owner evidence is reused by EXP-0067/EXP-0068 as well as EXP-0028; no new measurement or owner qualification is added here. +- `Decomposition review`: Boundary reviewed during provenance-preserving fission. Workloads within this Leaf share the declared claim, physical owner, changed variable, evidence domain and acceptance rule. Independently decidable owners were transferred; a new owner or selector requires another node. EXP-0067 independently owns live production selection; EXP-0068 qualifies the missing User completion inner owner. Both borrow this same scoped collection evidence. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | Pipeline Opening collection has one complete inclusive Control owner for admitted User service. | One admitted large User head collecting Pipeline fees compared with the empty-head primitive, including both native ledger accounts | Reference-Wasm FRAME evidence in the declared owner/domain; native checks are construction controls | Self | Required | Named parent decision and declared downstream consumers | Satisfied for the admitted reference domain; isolated empty/large-head storage, zero-Step inclusive ownership and nonzero-Step full Opening boundaries are evidenced | + +## Measurements + + +### Large-Head Collection Comparison + +Both profiles pass reference-Wasm admission, measurement and verification on the same fresh binary, with the empty-head rerun serving as the controlled comparison. Both measure only `charge_pipeline_opening`, excluding cycle execution, placement, Action effects and User hold reconciliation. + +| Profile | RefTime before DB additions / minimum (ps) | Estimated / recorded proof (bytes) | Reads / writes | Generated fragment SHA-256 | +| --- | --- | --- | --- | --- | +| `pipeline_zero_step_opening_collection` | 64,883,000 / 62,789,000 | 7,132 / 1,192 | 3 / 2 | `fb8d332e9982fc6696fa5245daf71c0aa82217f6a83388cc627c904f13064870` | +| `pipeline_large_head_opening_collection` | 74,382,000 / 71,868,000 | 8,817 / 2,877 | 3 / 2 | `6a3e396502cbc71754663b8e60b18b4e3ecd87228f7a35baa219ced8d59d47b9` | + +Measurement source content/modes `033515755de4f8367ffb2c019100bc704c86b7f6311a018029dfeef5d696ed68`; benchmark source SHA-256 `cc9962cfa4b99715cf88bc55daa2bde39b5d840112fa32290e61fcab7efa478c`; reference helper source SHA-256 `b0b9a0bc79f7f6d5044eb3e7272a69feec309bf8933874080d86936552969e2c`; benchmark Wasm SHA-256 `d38d8ebc88f1370d203cfde80806e3beb45f9117cb41d28656532cb8f4ea130a`. Same pinned reference environment and 50 steps × 20 repeats as above; no concurrent agent build/test/benchmark job during measurement. + +Reproduce with `scripts/benchmarks.sh --extrinsic pipeline_large_head_opening_collection --output /tmp/deos-exp28-large-head-collection.rs pallet_deos_actors`, then `scripts/benchmarks.sh --skip-build --extrinsic pipeline_zero_step_opening_collection --output /tmp/deos-exp28-empty-head-collection-paired.rs pallet_deos_actors`. Generation took 13 seconds per profile after the fresh runtime build. Fragments remain temporary and uninstalled; exact-selector exclusions keep both diagnostics outside default production generation. + +The large-head point exceeds the paired empty-head model by 9,499,000 ps and 1,685 bytes of estimated proof. The trace retains one unique Contract-head key and two native account keys; source collection still decodes the head twice. Ordinary setup also changes trie context, so the proof difference is a fixture comparison, not an isolated serialized-head delta or a removable proof allowance. This falsifies treating the empty-head coefficient as complete collection coverage, without proving the large profile dominates every admitted head or decode branch. EXP-0029 / EXP-0064 still own complete domain/envelope qualification; no production Weight, fee, selector or C1 behavior changes. + +Retained-source validation: Actors and runtime all-features Clippy, the focused native benchmark, both Wasm profiles, workspace check and changed-scope completion pass. Completion uses `--skip-runtime-tests`: the runtime diff is wholly inside its benchmark-only helper, and existing default-runtime behavioral evidence remains applicable. Canonical formatting and Experiment Record normalization/graph checks pass. Source content/modes remain identical to the measurement identity; later edits affect only root context and experiment records. These checks do not override the known G5 preflight rejection or certify production artifacts. + +`charge_pipeline_opening` runs for Idle User service and emits `PipelineFeeCharged`, independently of Action invocation. `collect_pipeline_fee` reads the Contract head for the certified fee and again for capacity, checks native balance, then calls the collector. Canonical inner Opening and zero-Step Weight bindings remain System-derived; the new Manual User zero-Step diagnostic includes this work but is not yet bound. UserPaged includes full FIFO discovery and cannot be treated as a disjoint collection atom. User state-hold reconciliation occurs after placement and remains a separate owner. The next falsifier must cover User zero/nonzero-Step Opening with exact ledger, receipt and hold boundaries, preserving ordinary admission and the four monetary owners. + + + +### User Zero-Step Comparison + +The Cadenced extension below compares admitted User zero-Step Opening, persistent versus authored auto-close nonce one. It reaches readiness by advancing the host clock and draining the original due temporal primary, without injecting trigger anchors, latches or Waiting entries. The measured consumed-primary inner boundary includes Tick rearm, collection, destination placement and User hold reconciliation, or complete terminal cleanup. The source rearms Cadenced before testing the terminal nonce, so the terminal path includes insertion followed by removal. Crossing's disabled-member removal/reinsertion and ObservationChange disablement deletion remain separately required branches; EXP-0063 qualifies competing Waiting/heap geometry. + +Ordinary admitted zero-Step System/User Actors now reach service through a real Manual latch; canonical primary consumption remains outside the inner measurement. This replaces the System fixture's injected hot state/ticket. Complete User postconditions bind one Pipeline receipt, exact sovereign/Fee Sink delta, completion nonce, absent Run and final state-hold consistency. The isolated primitive measures the same `charge_pipeline_opening` operation on an admitted empty User head without cycle execution, placement or hold reconciliation. + +| Profile | RefTime before DB additions (ps) | Estimated / recorded proof (bytes) | Reads / writes | Generated fragment SHA-256 | +| --- | --- | --- | --- | --- | +| `pipeline_zero_step_opening_collection` | 66,281,000 | 7,132 / 1,192 | 3 / 2 | `3090e6f67a71a6b4f3a52961641316f2c72752d38618a3907aae363930ada0d3` | + +Source content/modes `1b63a1e1399b1c7a3c37ba1a6e80d5e540ff14238cf3ce3fee1fa491378c70fe`, benchmark Wasm `93b132ad0970a0d5cbfb5c9c370b396ce26268e757ff0d2eee242a1f3bf023c6`, reference runtime, same pinned toolchain/host/cache/heap environment, 50 steps × 20 repeats, no concurrent heavy jobs. Native `ci-local.sh --only tests --package pallet-deos-actors --all-features --test-filter zero_step` passed. Benchmark compilation passed; preflight retained the existing stale-storage rejection. The System route rebuilt Wasm; User and collection reused exactly that Wasm with `--skip-build`. + +Reproduce with `scripts/benchmarks.sh --extrinsic PROFILE --output /tmp/deos-exp28-zero-step-KIND.rs pallet_deos_actors`, selecting the table's profile and KIND=`system`, `user`, or `collection`; add `--skip-build` only for the remaining profiles on the unchanged fresh benchmark runtime. All three generation routes and verification postconditions passed. Fragments remain temporary and uninstalled. The two new unbound diagnostic profiles are excluded from default production generation by the existing exact-selector mechanism; their eventual trait/production binding remains a G4/G5 decision. + +The User inner measurement already includes collection and hold reconciliation, so adding the primitive again would duplicate work. The primitive trace reads Contract head and two native accounts; complete User also probes activation authority, observation/feed/subscription and crossing metadata during hold reconciliation. Its two additional writes versus System are the fee-transfer accounts; this zero-Step case does not establish the variable Run/hold resize envelope of nonzero Pipelines. Do not derive a hold Weight or subtract ProofSize from these aggregate measurements. This comparison covers persistent Manual zero-Step only, not temporal/Crossing rearm, terminal close, arbitrary Contract-head decoding or all Trigger families. Existing zero-Step reservation/quote sites still use the older System-derived generated method and need a complete inclusive binding before acceptance. + +Retained source `bd3db85a36e11b4820bb2ea11b97418f1bf02792376d24e09b982257bbcb3f37` adds the diagnostic-selection exclusion and documentation after measurement; the measured benchmark bodies remain unchanged. Actors all-features Clippy, shell syntax, workspace check, record normalization and changed-scope completion passed. Default runtime behavioral evidence from `a5f79a0` is reused: the only production scheduler diff makes the unchanged collection method crate-visible for the benchmark; its body and production callers are unchanged. No new trait/selector/production coefficient binding is claimed. + + +### Deciding Current Reference-Wasm Witness + +All five profiles ran sequentially on one freshly rebuilt benchmark Wasm with 50 steps × 20 repeats, compiled Wasm, 4,096 heap pages, 1,024 MiB DB cache and map bound 1,000,000. The complete nonzero-Step profile rebuilt the runtime; the remaining profiles used `--skip-build`. The generated fragments remain temporary evidence and are not partial production bindings. + +| Profile | Generated RefTime (ps) | Estimated / recorded proof (bytes) | Reads / writes | Generated fragment SHA-256 | Raw JSON SHA-256 | +| --- | ---: | ---: | ---: | --- | --- | +| `pipeline_zero_step_opening_collection` | 67,258,000 | 7,132 / 1,192 | 3 / 2 | `6ce00776c78545e4094bcca9401d96a8eedee1a577f4f4382620cbc1938c9cbe` | `984943bb607ced983adb8171f7c76496144bd69d3cb28b0e2f592b253e100f9a` | +| `pipeline_large_head_opening_collection` | 77,245,000 | 8,817 / 2,877 | 3 / 2 | `c03dc41e88b9f972619c2768eff807bc091ceec1caba66a7c66d8131abc3cfac` | `408cd4bb3f8324d5cd44798cef000332bcb11bfec0ea57632f7731ceb33dc689` | +| `scheduler_inner_zero_step_user_complete` | 119,081,000 | 7,529 / 1,589 | 9 / 5 | `a990ae7c30ff02bcd6717a9988bbd512540677b93c9f3a997595185852b68b2f` | `4d4959c148cd861ef74c1b9e9c6b8a45dceb8cfd07b2473ed811302dab73467f` | +| `scheduler_inner_zero_step_user_max_head` | 167,832,000 | 9,047 / 3,107 | 11 / 5 | `7ae7922ebb1f2649b7547a884575121b6ab8de785d6fc51ac3a69297d2546630` | `f9ab67145ea405c93a704a68d03f38ae21cc1ab9b7de0a0e1f8829ae838b2c37` | +| `scheduler_paged_execute_opening_max` | 536,459,000 | 14,158 / 5,743 | 24 / 13 | `2d824d36d9f1e9821c71a0df88cd02a3e868ea491a413bc0b689cbec022c9f12` | `21f268723a55ca53211ed0a2987b28609b91c14831595b6b0ebda87e23a98ad4` | + +Reproduce each with `scripts/benchmarks.sh --extrinsic PROFILE --output /tmp/PROFILE.rs --json-file /tmp/PROFILE.json pallet_deos_actors`: rebuild once, then add `--skip-build` for every remaining profile on the unchanged Wasm. Minimum execution times in table order were 63,766,000, 71,448,000, 112,027,000, 155,748,000 and 479,678,000 ps. + +The isolated profiles each read one Contract head and both native ledger accounts, then write both accounts and emit exactly one `PipelineFeeCharged` receipt. The large admitted head retains the same physical 3-read/2-write owner but raises RefTime by 9,987,000 ps and estimated proof by 1,685 bytes over the paired empty head. The complete persistent zero-Step User atom proves the exact ledger deltas and one receipt while preserving its installed hold record; the 167,832,000 / 9,047 maximum-head zero-Step point is the scoped inclusive owner rather than either isolated primitive or the empty-head complete point. The production `scheduler_paged_execute_opening_max` point covers an ordinarily admitted 12-Step User Opening, source discovery/consumption, Pipeline collection, one resolution skip, Running publication and hold reconciliation; strengthened postconditions prove the same exact ledger/receipt owner while the funding hold alone decreases after accumulated ingress moves into the Run snapshot. That hold delta is independently reconciled work, not part of the collection primitive. + +Fresh native benchmark tests for the isolated pair, zero-Step complete, maximum-head zero-Step and nonzero-Step full Opening pass. The focused package `pipeline_fee` family and `executable_task_charges_pipeline_and_independent_action_fee` pass, confirming that zero/noninvocation still have one Pipeline receipt and that an invoked Action fee remains separately collected. Actors/runtime all-target all-feature Clippy, workspace check, canonical formatting, Experiment normalization/graph validation, diff checks and changed-scope completion pass. The initial full-Opening postcondition incorrectly required the entire state-hold record to remain byte-identical; reference Wasm rejected it because consumed funding geometry legitimately lowers only the funding component. The corrected postcondition preserves all other components, requires that decrease, and passes. This is evidence of the owner boundary, not a semantic or production change. + +## Derived Metrics + +Derived values are retained beside the evidence that defines their source and units. No subtraction of overlapping aggregate owners or cross-artifact timing comparison is introduced. + +## Statistical / Variance Notes + +Retain each item’s original sampling and uncertainty. Singleton construction probes establish only their admitted state/access witness, never statistical timing dominance or a production bound. + +## Result + +The current reference Wasm confirms one Pipeline collection call-site owner over the admitted User domain. Empty and large heads share the same three-read/two-write physical shape, with the large head selecting the larger scoped primitive. Complete zero-Step and 12-Step User Opening witnesses each produce exact payer/Sink deltas and exactly one Pipeline receipt while retaining state-hold reconciliation as a separate part of the inclusive complete owner. + +## Interpretation + +Pipeline collection is neither a second scheduler source owner nor Action collection. It occurs once only when admitted Idle User service opens a Pipeline; the complete User owners include that work once, while System and non-Opening Running/Suspended service select none. Head decoding changes the collection primitive's resource envelope, so the empty-head diagnostic is rejected as the complete collection input. The complete nonzero-Step benchmark validates composition without subtracting overlapping proof or timing. + +## Decision + +Accepted for the admitted reference domain. Use the complete maximum-head User zero-Step owner for zero-Step Opening and retain the complete User nonzero-Step Opening owner; within those inclusive owners, select exactly one large-head-qualified Pipeline collection operation and receipt for Idle User service. Select none for System or non-Opening phases. EXP-0065 owns complete generated-file regeneration and exact final binding; do not install these diagnostic fragments or manually combine aggregate measurements. + +## Rejected Alternatives and Tradeoffs + +Retain all rejected candidates and causal limits in the extracted evidence. Do not infer a new selection from record relocation or from aggregate coefficient subtraction. + +## Validity + +- `Establishes`: Exactly one large-head-qualified Pipeline collection/receipt owner for admitted reference User Opening, with exact native ledger and separate hold boundaries in complete zero-Step and nonzero-Step witnesses. +- `Does not establish`: Arbitrary host implementations, complete production Weight, exact release artifact closure or integrated throughput. +- `Known limitations`: Diagnostic collection and zero-Step fragments are uninstalled; the existing complete Weight file remains stale until EXP-0065 regeneration, and complete admitted head/reachability composition remains governed by EXP-0029/EXP-0064. +- `Invalidation triggers`: A changed measured body, host domain, source/Weight/Wasm identity or acceptance rule requires scoped requalification. +- `Residual qualitative insight`: Follow the owning physical boundary, not a benchmark name. + +## Follow-Up / Next Gradient + +Stop measuring this satisfied owner. EXP-0044 independently owns the numeric hold envelope, EXP-0064 composes admission, and EXP-0065 regenerates and binds complete artifacts. Reopen only for a changed collection body/host, a legal larger admitted collection head, or an omitted/duplicate Pipeline charge; broader release binding alone is not a reopen trigger. + +## Architecture Impact + +Benchmark-only postconditions now make ledger, receipt and hold boundaries explicit. Runtime semantics, production storage and bindings are unchanged; C1 and A0 remain frozen. The accepted measurements are inputs to later complete regeneration, not installed partial Weight. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: [EXP-0028](./EXP-0028.md) O2 +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: [EXP-0025](./EXP-0025.md) +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: None. +- `Transfers question to`: None; independent findings require an explicit new transfer. +- `Produces input for`: [EXP-0028](./EXP-0028.md), [EXP-0067](./EXP-0067.md), [EXP-0068](./EXP-0068.md) — qualify live selection and completion-inner ownership without invalidating these reference measurements. +- `Reopen trigger`: A falsifier of this declared owner/domain or exact artifact identity; cost alone does not reopen frozen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0032.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0032.md new file mode 100644 index 00000000..f27cde39 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0032.md @@ -0,0 +1,374 @@ +# EXP-0032 — Current Predicate Component Bound + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | [EXP-0028](./EXP-0028.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-06 | +| Affected domain | The Current predicate component and composed selector dominate the declared canonical predicate and host-state domain. | +| Physical mechanism | Current Predicate Component Bound | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0025](./EXP-0025.md) — Accepted, physical architecture only | +| Successor experiment | [EXP-0028](./EXP-0028.md) | + + +## Decision Question + +The Current predicate component and composed selector dominate the declared canonical predicate and host-state domain. + +## Governing Specification + +- `Owners`: `template/pallets/actors/docs/specification.en.md` (sole ownership, lifecycle/Trigger/Pipeline/Action semantics, placement and scheduler); `docs/actors-resource-policy.specification.en.md` (component-wise admission/actual settlement, equal-thirds block policy); `docs/actors-performance-assurance.specification.en.md` (evidence and target). +- `Frozen physical input`: [EXP-0025](./EXP-0025.md) C1 Architecture Freeze. No change to FIFO, ticket chronology, Q1, causal floor, primary/reference ontology, source/successor atomicity or sole execution owner. +- `Semantic oracle`: A0 SHA-256 `8746e423168309975e509be91e4eb06fdf0eb0b43369d75f1ee62202a4341678` remains frozen; use only for relevant semantic equivalence, never as a Weight oracle. + +## Context and Hypothesis + +This Leaf owns current predicate component bound. Its boundary is the single claim above. Evidence was extracted from the explicitly marked pre-fission parent subsections at Git baseline `a60de7748132211d9e58cecd5be82b41f76ce230`; this migration changes navigation and evidence ownership only. Source, Wasm, commands, rejection outcomes and evidence limitations retain their original meaning. + +Extraction provenance at the recorded Git baseline: EXP-0028 / “Predicate Visit Cardinality”, “Current and Cached Predicate Falsifier”, “Current Component Domain Correction”, “Asset-Heavy Current Count Candidate”, “Observation-Heavy Current Count Falsifier”, “Current Component Binding”, “Current Host and Composed-Selector Closure”. Original directional prose (above/below) belongs to these pre-fission subsections; current cross-owner navigation follows the parent proof map. + +## Baseline and Candidates + +Retained measured implementations and rejected candidates are identified in the extracted evidence. The deciding candidate is the staged component-wise maximum of the generated asset-heavy mixed-total, pure maximum-account and observation-heavy Oracle-count inputs. `RuntimeStepControlWeight::current_predicate_weight` maps admitted counts to those owners, clamps the doubled Opening hint to the four-Current-visit bound and is shared by maximum and actual selectors. + +## Controls + +Only the declared witness input varies within each source-bound comparison. Preserve the exact host, toolchain, database, Wasm and sampling controls printed with each evidence item; do not compare unlike artifacts as a controlled performance result. + +## Benchmark Class, Workloads, and Criteria + +Acceptance requires the single claim to hold over its declared admitted domain, with each physical operation charged once and every retained limitation resolved before broader production use. One legal omitted-owner, domain or containment counterexample falsifies that claim. Existing workloads below are bounded witnesses, not permission to add independent questions. + +## Environment and Artifact Identity + +Pre-fission baseline `a60de7748132211d9e58cecd5be82b41f76ce230`. Each extracted block retains its own exact source/Wasm/output identity. Shared pinned hardware/toolchain/cache controls follow [EXP-0030](./EXP-0030.md#environment-and-artifact-identity) only where the original comparison explicitly reuses them. A shared environment never substitutes for the block’s own source or Wasm identity. No runtime artifact was rebuilt during migration. + +## Proof Obligations + +- `Freeze`: Frozen by explicit migration reconstruction from the pre-fission decision and retained witnesses at a60de7748132211d9e58cecd5be82b41f76ce230. No new measurement or historical decision is implied. +- `Review triggers`: More than six proof subsections; multiple consumers reviewed. +- `Decomposition review`: Seven historical subsections compare candidate bounds for the same Current predicate component over the declared canonical DNF/host read domain. Pure/mixed/host-state workloads are witnesses of its component-wise dominance claim. The nonzero Opening and zero-result traversal owners were extracted into EXP-0033 / EXP-0035. This Leaf cannot absorb a different Step/receipt/hold owner; complete production identity transfers to EXP-0065. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | The Current predicate component and composed selector dominate the declared canonical predicate and host-state domain. | One canonical mixed Current predicate Step at the reference four-occurrence bound whose selected component misses either charged resource | Reference-Wasm FRAME evidence in the declared owner/domain; native checks are construction controls | Self | Required | Named parent decision and declared downstream consumers | Satisfied for the reference binding: generated component owners cover admitted typed hosts/results, and composed maximum/actual selectors preserve the bound without Opening double charge | + +## Measurements + + +### Predicate Visit Cardinality + +Source audit on `c875973` separates the hint from physical reads. `types/contract.rs::Precondition::evaluation_units` returns `p + o`; `execution.rs::evaluate_precondition_with` visits every predicate, and `evaluate_step_precondition` invokes `evaluate_atomic_predicate` only for Current timing. Opening timing reads the retained result and increments its cursor. For `0 <= o <= p <= 4`, both composed selectors in `RuntimeStepControlWeight` use `min(p + o, 4) >= p`: the clamp does not truncate visit cardinality. Four Opening predicates give hint eight, four cached visits and zero current host evaluations. Opening capture is already a separate component; charging eight host evaluations would not follow from this hint. + +This is a source/cardinality conclusion, not numeric Weight acceptance. `predicate_set_evaluation` measures Current BalanceAbove predicates and does not establish cached-Opening, Oracle or maximum asset-account dominance. The existing non-short-circuit DNF, distinct Opening/Current state and retained-result retry tests falsify the described semantics. Direct aggregate selectors still require their own reachable-domain qualification. Corrected the package architecture's unsupported claim of chunked predicate charges; no executable, generated coefficient or C1 geometry changed. + +### Current and Cached Predicate Falsifier + +The next isolated comparison reuses maximum-encoded asset accounts and published Oracle feeds in the actual Step DNF evaluator. Extra profiles `predicate_max_encoded_balance_evaluation(p)` and `predicate_observation_evaluation(p)` cover `p = 1..4`, with the same singleton-clause layout as the existing `predicate_set_evaluation` control. `predicate_cached_evaluation(o)` keeps four total predicates and replaces `o = 0..4` maximum-account Current visits with saved Opening results at the end of the bounded result vector. Setup stays outside measurement; assert true evaluation and exact cache cursor advancement. The cached allocation axis is categorical timing composition, not a predicate-count Weight model. + +Run the three profiles and the existing control on one rebuilt Wasm at 50 steps × 20 repeats, using `scripts/benchmarks.sh --extra --extrinsic PROFILE --output /tmp/deos-exp28-PROFILE.rs --json-file /tmp/deos-exp28-PROFILE.json pallet_deos_actors`, with `--skip-build` after the first profile. Compare raw samples, proof and DB models separately. Any host-family resource excess rejects the current balance-only component as a generic bound; an all-cached excess rejects assumed cached-visit dominance. Pure families and this cached balance mixture do not qualify all mixed host/refusal/DNF states or whole-Step admission. No partial coefficient is installed. + +All four profiles pass on source content/modes `2a611d777d1c5b3f5f6d78ba925dee7bcaf1becf6204f51c6558f2b957ab958c`, benchmark source `da39c1c04cdd15b6f388fe8db694cbd8884767820aa4a00ee031b6ab9336a512`, unchanged runtime-helper source `7b3ce4cbd426fe4664bcc2931eb64afd1df37b306eac7f9e7e21f659056b7db6` and benchmark Wasm `17dd0f6e8dd978b3b3e88c74405c4b8cd6bd1b59e4f9b5cf22569ba1867c7335`. Compilation passes in 55 seconds, followed by the known G5 stale-storage rejection; fresh Wasm build takes 100 seconds. Isolated measurements take 22/25/20/31 seconds in the declared order, with previous map/cache/heap/toolchain controls unchanged and no concurrent builds or tests. The old control asserts inside measurement; new profiles black-box the result and assert afterward, so CPU comparison includes that small boundary difference. Storage conclusions are independent of it. + +| Profile | RefTime before DB, ps | Estimated / recorded proof model, bytes | Generated DB reads | +| --- | --- | --- | --- | +| Maximum-account Current, `p = 1..4` | `7,690,637 + 8,812,291p`; slope error `5,589` | `1,345 + 2,838p` / `356 + 363p` | `2p` | +| Fresh Oracle Current, `p = 1..4` | `5,304,935 + 9,349,282p`; slope error `4,070` | `1,246 + 2,671p` / `255 + 196p` | `2p` | +| Cached allocation, `o = 0..4`, total four | Constant `43,695,512` | Constant `12,716` / `1,812` | Constant `8` | +| Existing Native-first Current control, `c = 1..4` | `7,543,000 + 7,073,093c`; slope error `14,906` | `3,675 + 673c` / `488c` | `1 + c` | + +All writes are zero. The new profiles use measured proof; the existing control uses MaxEncodedLen. These estimates must not be conflated with the exact recorded proof below. Minimum-duration repetition produces more samples than 50 × 20; no observations are discarded. Times below are raw nanoseconds; median is the upper middle sample and p95 is nearest-rank. Each DB sample at a given point reports the same read count and proof size. + +| Profile / parameter | Time / DB samples | Min | Median | p95 | Max | Actual reads | Recorded proof bytes | +| --- | --- | --- | --- | --- | --- | --- | --- | +| Maximum account / 1 | 8,500 / 425 | 14,737 | 16,763 | 21,442 | 79,551 | 2 | 719 | +| Maximum account / 2 | 8,000 / 400 | 22,768 | 24,654 | 29,613 | 123,551 | 4 | 1,079 | +| Maximum account / 3 | 8,000 / 400 | 32,406 | 34,363 | 44,908 | 124,878 | 6 | 1,445 | +| Maximum account / 4 | 500 / 25 | 41,626 | 43,233 | 47,842 | 66,071 | 8 | 1,812 | +| Oracle / 1 | 9,180 / 459 | 12,711 | 14,387 | 15,994 | 62,578 | 2 | 451 | +| Oracle / 2 | 8,640 / 432 | 22,769 | 24,445 | 27,238 | 81,157 | 4 | 650 | +| Oracle / 3 | 8,640 / 432 | 31,359 | 32,966 | 37,156 | 103,646 | 6 | 843 | +| Oracle / 4 | 540 / 27 | 40,439 | 42,464 | 48,959 | 99,944 | 8 | 1,042 | +| Cached / 0 | 4,940 / 247 | 40,788 | 42,813 | 53,359 | 99,665 | 8 | 1,812 | +| Cached / 1 | 4,560 / 228 | 34,293 | 36,178 | 47,353 | 106,440 | 6 | 1,520 | +| Cached / 2 | 4,560 / 228 | 25,213 | 27,169 | 36,667 | 95,405 | 4 | 1,227 | +| Cached / 3 | 4,560 / 228 | 17,041 | 18,578 | 23,118 | 191,088 | 2 | 859 | +| Cached / 4 | 380 / 19 | 908 | 1,467 | 1,956 | 16,274 | 0 | 0 | +| Existing control / 1 | 11,220 / 561 | 6,076 | 7,683 | 9,010 | 112,097 | 1 | 140 | +| Existing control / 2 | 10,560 / 528 | 20,883 | 22,489 | 29,892 | 149,322 | 3 | 808 | +| Existing control / 3 | 10,560 / 528 | 28,077 | 30,102 | 36,528 | 126,764 | 5 | 1,117 | +| Existing control / 4 | 660 / 33 | 38,064 | 40,090 | 50,915 | 262,398 | 7 | 1,432 | + +| Profile | Generated fragment SHA-256 | Raw JSON SHA-256 | +| --- | --- | --- | +| Maximum account | `a985d477b61f2b67460f512c8ed0e6e128e782cf64a03cc39728fff852284fd7` | `2b2dec70dfa629aac71047b4d2809078820e7612187f95c0f05af8cdf3465629` | +| Oracle | `3695229c044c803422f6a7eeebefc3ef76ef0cd49415d9e9d6ab2ee2544bb443` | `d0dfccb8248723820ff827935732ced3cd718d1e9e65e1e811933d5a1d3a7381` | +| Cached | `8c2c6597b712d935d3b03c8cdd5e0596d6e5c45bf01a7fcd3ddcf659c3b9db89` | `c369cf116bb739e1b91501ab11825271317f76985c029cae65f0b336225101ea` | +| Existing control | `33dd47fc47b83af59e052a5a3d1f1bfa1b8db12cd674e1ee75e6341fabbbbabf` | `0e7e38e74ad9b6ab602e27ac24befa25fbe780390fc9d6ebb0b7e64d726b4cf4` | + +Reject the existing control as the generic component domain. Its own actual read sequence is `2c - 1`, while generation emits `1 + c`, undercounting at three and four; a fresh generation alone does not correct this. At four, the maximum-account model exceeds the control by `82,104,429` ps including the configured `25,000,000` ps/read and by `6,330` estimated proof bytes. Oracle also needs eight reads versus the control model's five. These are component-level falsifiers, not proof of an exhausted complete scheduler envelope or permission to reopen C1. + +Maximum-account and Oracle pure models differ by only `237,738` ps at four before DB, so their ordering is not robust generic dominance. Mixed host namespaces, refusal states and other DNF results remain unqualified. The cached series shows `8,6,4,2,0` reads and no proof for all-Opening visits; medians fall to 1,467 ns, but the 191,088 ns tail at three prevents claiming pointwise raw-time dominance. Its constant generated fit models this fixed-total sample family, not a new count component or an upper bound on every noisy timing sample. Select a sound production benchmark/composition that covers the complete host domain and preserves exact Opening-capture ownership; do not install these fragments or manually repair regression coefficients. + +Validation: canonical formatting passes; native all-features `bench_predicate_` tests pass in 29 seconds; workspace/all-target/all-feature Clippy passes in 22 seconds; changed-scope completion passes. All 17 raw-data table rows and the DB-inclusive model delta were checked mechanically against JSON. Only benchmark construction and documentation changed; prior production runtime/embedding evidence remains applicable within its scope, while fresh benchmark Wasm qualifies the new profiles. The existing G5 storage-name failure remains open. + +### Current Component Domain Correction + +The Native-first read-model rejection admits a concrete fixture correction: `predicate_set_evaluation(c)` now uses distinct maximum-encoded non-native accounts and measured proof, preserving the real bounded Step evaluator. This is the previous maximum-account diagnostic's measured operation under the production component name; remove that duplicate diagnostic. The expected actual DB sequence is `2c`, avoiding the previous negative-intercept `2c - 1` domain. This strengthens one component input without claiming complete host dominance or repairing installed coefficients by hand. + +Before accepting its scope, add two fixed-total falsifiers using the same singleton-clause constructor: `predicate_host_allocation(b = 0..4)` uses `b` non-native balances plus `4-b` fresh Oracle predicates; `predicate_native_host_allocation(b = 0..3)` adds one native balance plus `b` non-native balances and `3-b` fresh Oracle predicates. The nine cases enumerate all distinct-key native-presence/host-family allocations at the reference Step maximum. Setup remains outside measurement and every evaluation must return true. They are allocation diagnostics, not predicate-count fits; lower counts, Oracle refusal and other DNF result states still require qualification before complete acceptance. + +Run corrected `predicate_set_evaluation`, then the two extra allocation profiles on one fresh Wasm with the previous 50 × 20/map/cache/heap controls and raw JSON retention. Use `scripts/benchmarks.sh --extrinsic predicate_set_evaluation --output /tmp/deos-exp28-current-corrected.rs --json-file /tmp/deos-exp28-current-corrected.json pallet_deos_actors`, then `--skip-build --extra --extrinsic PROFILE --output /tmp/deos-exp28-PROFILE.rs --json-file /tmp/deos-exp28-PROFILE.json`. Check actual versus modeled reads at every count and mixed proof/time against the corrected maximum. An interior excess rejects pure-family maximum coverage without requiring C1 changes or erasing the already established visit-cardinality result. + +The corrected component and both allocation profiles pass. Preparation source content/modes is `c75be6b5c63262d9189dddda43b041ac873bcc94b4ee39c87821d82e458e0e9d`; benchmark source is `6ef913c6f6f8b4691357aa92bfa6d8b829091f7667a95e917478fc72ff278046`; fresh benchmark Wasm is `2b48154bffc7d2789d3b01835e9163e311d2b4b4d47260025eda2be561389fea`. Subsequent documentation changes do not change those benchmark inputs. Compilation passes in 56 seconds before the known G5 stale-storage failure; Wasm builds in 100 seconds and isolated measurements take 22/18/19 seconds. Runtime helpers, configured limits, host, map/cache/heap controls and production semantics are unchanged. + +| Profile | RefTime before DB, ps | Estimated / recorded proof model, bytes | Generated DB reads | +| --- | --- | --- | --- | +| Corrected component, `c = 1..4` | `8,057,270 + 8,780,970c`; slope error `5,508` | `1,345 + 2,838c` / `356 + 363c` | `2c` | +| Non-native/Oracle allocation, `b = 0..4` | `43,183,873 + 1,135,174b`; slope error `12,103` | `12,011 + 2,629b` / `1,245 + 192b` | `8` | +| Native/non-native/Oracle allocation, `b = 0..3` | `40,293,860 + 1,948,758b`; slope error `21,529` | `9,406 + 2,608b` / `945 + 307b` | `7` | + +All writes are zero. The allocation fits combine changing storage namespaces while keeping the total count fixed; neither is an accepted count model. At non-native `b = 4`, recorded proof exactly equals the corrected pure component's `c = 4` point (1,812 bytes), yet the allocation fit estimates 22,527 versus 12,697 bytes. This difference is model conservatism, not an extra storage operation or measured 9,830-byte proof regression. Compare individual raw points before interpreting a categorical slope. + +| Profile / parameter | Time / DB samples | Min ns | Median ns | p95 ns | Max ns | Actual reads | Recorded proof bytes | +| --- | --- | --- | --- | --- | --- | --- | --- | +| Corrected component / 1 | 8,500 / 425 | 15,365 | 17,181 | 20,115 | 245,705 | 2 | 719 | +| Corrected component / 2 | 8,000 / 400 | 22,838 | 24,863 | 30,032 | 122,852 | 4 | 1,079 | +| Corrected component / 3 | 8,000 / 400 | 32,895 | 34,781 | 41,416 | 116,776 | 6 | 1,445 | +| Corrected component / 4 | 500 / 25 | 41,905 | 43,372 | 46,165 | 70,471 | 8 | 1,812 | +| Non-native allocation / 0 | 4,420 / 221 | 39,949 | 41,905 | 51,962 | 803,676 | 8 | 1,042 | +| Non-native allocation / 1 | 4,080 / 204 | 43,931 | 45,817 | 57,201 | 274,830 | 8 | 1,489 | +| Non-native allocation / 2 | 4,080 / 204 | 44,350 | 46,096 | 57,341 | 131,234 | 8 | 1,656 | +| Non-native allocation / 3 | 4,080 / 204 | 44,350 | 46,235 | 57,690 | 178,866 | 8 | 1,823 | +| Non-native allocation / 4 | 340 / 17 | 40,997 | 42,534 | 55,385 | 132,491 | 8 | 1,812 | +| Native allocation / 0 | 6,460 / 323 | 36,667 | 39,042 | 49,169 | 135,075 | 7 | 945 | +| Native allocation / 1 | 6,080 / 304 | 42,603 | 44,490 | 55,734 | 213,299 | 7 | 1,398 | +| Native allocation / 2 | 6,080 / 304 | 41,416 | 43,582 | 53,360 | 140,523 | 7 | 1,559 | +| Native allocation / 3 | 380 / 19 | 38,484 | 39,671 | 43,303 | 62,509 | 7 | 1,547 | + +Statistics use the preceding upper-median/nearest-rank convention with no discarded samples; the maximum 803,676 ns is retained. Actual/model reads now agree at every corrected component point, closing the identified Native-first fixture read-model defect. Every allocation has the predicted eight or seven reads. The largest recorded proof is the three-non-native/one-Oracle interior point at 1,823 bytes, 11 above the pure maximum-account point; its median is also higher. This falsifies the pure fixture's claim to the largest observed recorded proof, not the soundness of its larger estimated proof in isolation. Four-predicate mixed-state coverage therefore remains open even after fixing the read model. + +| Profile | Generated fragment SHA-256 | Raw JSON SHA-256 | +| --- | --- | --- | +| Corrected component | `5943b88fdbe9ed2dc5e33d30557ab33bbb69dd4258130ec0ef9e694de65466c3` | `1190273386c60069c546aebcb3d2a447daa1c5b183bb40d10f9f2828942b9332` | +| Non-native allocation | `91cf095a984766fe2758b42a8345cdae923eaff2bc2f478c4ac28879ddc92ddd` | `095f35833b0e415641e45183c4ab7c0c42949d8cd831dfe2ab1a8a6cdf4d3f11` | +| Native allocation | `4469f498923219cbfdb7166eb509bb9cc9d9b42a403dbd622183ce71962d5609` | `030209d04d1ba13e499a3b17f8121730a62252b3ce5369af7d325e2521800a85` | + +The next count-domain candidate is the measured asset-heavy mixture: one Oracle plus remaining distinct non-native accounts for counts of at least two, with the already qualified maximum-account case at one. Qualify its lower-count behavior and host refusal/result branches before accepting a complete scalar envelope; do not repeat the nine unchanged maximum-count allocations or infer proof dominance from fitted categorical endpoints. The strengthened pure fixture remains an intermediate benchmark input, not a release-ready coefficient. No generated fragment is installed, and C1 stays frozen. + +Validation: formatting passes; native all-features `bench_predicate_` tests pass in 28 seconds; workspace/all-target/all-feature Clippy passes in 23 seconds. All 13 new raw-data rows and the exact `2c` read model were checked against JSON. Embedding guidance now names the maximum-account helper required by the corrected component and its explicit unsupported-host failure. Production behavior and helper implementations are unchanged, so existing runtime/embedding behavioral evidence remains applicable; complete generated bindings remain open under G5. + +### Asset-Heavy Current Count Candidate + +The measured interior maximum selects the next `predicate_set_evaluation(c)` fixture: one maximum-encoded fresh Oracle predicate plus `c - 1` distinct maximum-encoded non-native balance predicates for `c >= 2`, retaining the pure maximum-account case at one. This keeps exactly `c` legal Current visits and the expected `2c` physical reads. Measure the full `1..4` count range; this is a count-domain refinement, not an extrapolation of the previous allocation slope. Previous pure-family and nine maximum-count allocation evidence remain applicable within their declared scope. + +Add `predicate_mixed_read_state(s = 0..4)` at the reference maximum: three maximum accounts and one Missing/Deactivated/Uninitialized/Stale/Fresh Oracle. The shared benchmark setup verifies the actual typed host observation before measurement; the full DNF evaluator must visit all predicates even when the three true singleton balance clauses already determine the result. Expected results account for hosts with one clause. Factor the existing Opening setup into the same helper without changing its measured capture operation. The new state axis is categorical and cannot supply a count coefficient. + +Run `scripts/benchmarks.sh --extrinsic predicate_set_evaluation --output /tmp/deos-exp28-current-mixed-count.rs --json-file /tmp/deos-exp28-current-mixed-count.json pallet_deos_actors`, then the same fresh Wasm with `--skip-build --extra --extrinsic predicate_mixed_read_state --output /tmp/deos-exp28-current-mixed-states.rs --json-file /tmp/deos-exp28-current-mixed-states.json pallet_deos_actors`. Retain 50 × 20/map/cache/heap controls, actual/model DB counts and per-point proof/timing. Compare lower-count results with prior pure models and the maximum with the nine allocation witnesses. Refusal proof exceeding Fresh rejects Fresh-only state coverage; no categorical fit, arbitrary padding or partial fragment may substitute for a qualified component. + +The mixed count and five state points pass on prepared source content/modes `ec229e628937812df4fc48360dd334f0d5ef07418d7477d6bbca8c2eb583ad3a`, benchmark source `fa3696f74ac6ab1bef83eb70a9bc978e4318af119199b5b2fcb4c10b251963a4` and benchmark Wasm `b51ade70b387f4fd29cde99bf0fe976d8304fdbfc7f22bd4ad3367e2bb77135f`. Later documentation edits do not change benchmark inputs. Compilation passes in 53 seconds before the known G5 stale-storage failure; fresh Wasm builds in 98 seconds and isolated measurements take 22/19 seconds. Existing host setup, toolchain, cache, map, heap and repeat controls are unchanged. + +| Profile | RefTime before DB, ps | Estimated / recorded proof model, bytes | Generated DB reads | +| --- | --- | --- | --- | +| Mixed count, `c = 1..4` | `6,843,523 + 10,554,141c`; slope error `9,031` | `3,840 + 1,692c` / `350 + 369c` | `2c` | +| Mixed read state, `s = 0..4` | `41,728,153 + 1,789,380s`; slope error `6,057` | `10,143 + 36s` / `1,714 + 36s` | Constant `7` | + +| Profile / parameter | Time / DB samples | Min ns | Median ns | p95 ns | Max ns | Actual reads | Recorded proof bytes | +| --- | --- | --- | --- | --- | --- | --- | --- | +| Mixed count / 1 | 7,820 / 391 | 15,225 | 16,762 | 19,556 | 70,541 | 2 | 719 | +| Mixed count / 2 | 7,360 / 368 | 27,238 | 29,055 | 35,200 | 86,815 | 4 | 1,097 | +| Mixed count / 3 | 7,360 / 368 | 36,178 | 38,134 | 48,750 | 134,796 | 6 | 1,457 | +| Mixed count / 4 | 460 / 23 | 45,398 | 46,794 | 62,998 | 86,395 | 8 | 1,823 | +| Mixed state / Missing | 4,420 / 221 | 39,949 | 41,626 | 52,591 | 132,002 | 7 | 1,714 | +| Mixed state / Deactivated | 4,080 / 204 | 41,067 | 42,953 | 54,407 | 190,880 | 7 | 1,773 | +| Mixed state / Uninitialized | 4,080 / 204 | 43,791 | 46,236 | 57,341 | 124,948 | 8 | 1,823 | +| Mixed state / Stale | 4,080 / 204 | 44,909 | 46,794 | 58,109 | 201,845 | 8 | 1,823 | +| Mixed state / Fresh | 340 / 17 | 45,607 | 46,795 | 58,667 | 76,128 | 8 | 1,823 | + +All writes are zero; statistics retain the preceding no-discard, upper-median and nearest-rank conventions. The mixed count keeps the correct `2c` read model. Missing and Deactivated save one read, while the other states use eight; none exceeds Fresh's recorded proof at the tested maximum. Reject the categorical state fit as a resource bound: it emits seven reads even for eight-read states. No state coefficient is installed and no numeric state order is inferred. + +| Profile | Generated fragment SHA-256 | Raw JSON SHA-256 | +| --- | --- | --- | +| Mixed count | `ba5796840ac6b6b77c87be5d2a955df6bcd84b6bde3d0e1872643250c89d5c6e` | `5c413d41b39cd6ad2ffbcb188961fbf050d685f01cdba1a60e31dc83bebcabe4` | +| Mixed read state | `50853a651b0e22197666b605a2a28c8838075d2f17396ac91e91953179cb0bd6` | `9ae5568506f7bf0ae9395ea973da6a51c7dfbe300d57f18eb8b68c9b33a2bdf5` | + +Recorded mixed proof exceeds the earlier pure maximum-account points by 18/12/11 bytes at counts two/three/four. However, the mixed estimated model is lower than the pure model by 943/2,089 bytes at three/four. Pinned `frame-benchmarking-cli 58.0.0`, `pallet/writer.rs::{process_storage_results,get_benchmark_data}` explains the distinction: it clones recorded whole-operation proof into per-prefix samples, adds that prefix's modeled map/trie overhead, analyzes each prefix separately and takes maxima of bases and slopes across prefixes. It does not simply sum all prefix models. Writer source SHA-256 is `eb20296cba75332f183dfa02995b3d6e3ca73a068ddffe506a47af0bdc0d5c34`. Moving a read from the largest prefix to Oracle can therefore lower the generated estimate while increasing recorded proof. + +Reject replacing every Current host-domain bound with this one mixed estimate. The evidence selects a component-wise envelope of pure maximum-account and mixed count owners, preserving the larger RefTime and ProofSize independently without adding duplicate phase fees. At the sampled counts their proposed combined values below include `2c` reads at the configured `25,000,000` ps/read. This is an input proposal for EXP-0064, not an installed or fully accepted runtime binding; qualification of unmeasured lower-count observation-heavy compositions and DNF result branches remains open. Existing maximum-count allocations and state/cache witnesses need not be blindly repeated. + +| Count | Proposed DB-inclusive RefTime, ps | Proposed estimated ProofSize, bytes | Selecting owners | +| --- | --- | --- | --- | +| 1 | 67,397,664 | 5,532 | Mixed RefTime / mixed proof | +| 2 | 127,951,805 | 7,224 | Mixed RefTime / mixed proof | +| 3 | 188,505,946 | 9,859 | Mixed RefTime / pure proof | +| 4 | 249,060,087 | 12,697 | Mixed RefTime / pure proof | + +The count-one mixed fixture is itself pure, and its model includes conservative cross-count intercepts; the table is a modeled envelope, not physical work attribution. Retain the pure input as extra `predicate_asset_evaluation(p)`: it exactly restores the previous pure component's setup/evaluator/black-box/postcondition under a separate name, so its existing measurements remain scoped evidence without repeating them solely for the rename. This callable input was restored after the two new measurements; those Wasm/source identities certify the measured owners, not final G5 bindings. No semantic predicate count, public Control context or C1 geometry changed. The next implementation decision must compose both inputs and preserve exact useful-readiness/Opening/Current fee separation; manually transplanting one proof slope into another generated function is rejected. + +Validation: final formatting, native all-features `bench_predicate_` tests (29 seconds), the shared `bench_opening_observation_read_state` test and final workspace/all-target/all-feature Clippy (25 seconds) pass. Mechanical checks verify all nine raw rows, all four proposed envelope values and textual equivalence of the restored pure measured owner after function/parameter renaming. Benchmark-only changes preserve the existing production runtime/embedding behavior; metadata and complete Weight generation remain G5 work. + +### Observation-Heavy Current Count Falsifier + +The remaining lower-count mixed family is one maximum-encoded non-native account plus `p - 1` fresh maximum-encoded Oracle predicates. Extra `predicate_observation_heavy_evaluation(p = 2..4)` reuses the same actual Step evaluator, singleton-clause constructor, setup and postcondition. Two is the existing one-plus-one mixture; four is the already measured maximum-count allocation with one asset. Three supplies the previously missing one-asset/two-Oracle point. The count range is necessary to compare a generated model with the proposed envelope, rather than treating a singleton timing sample or the fixed-four allocation slope as a count bound. + +Run `scripts/benchmarks.sh --extra --extrinsic predicate_observation_heavy_evaluation --output /tmp/deos-exp28-current-observation-heavy.rs --json-file /tmp/deos-exp28-current-observation-heavy.json pallet_deos_actors` on fresh Wasm with the same 50 × 20/map/cache/heap controls. Check `2p` actual/model reads, recorded versus estimated proof and DB-inclusive RefTime against both proposed input owners. An excess requires a third generated input or an explicitly justified broader composition; it does not permit editing an existing coefficient by hand. No unchanged pure/state/cached/max-allocation profile needs another run solely because this diagnostic was added. + +The observation-heavy model exceeds the previous two-input RefTime envelope at two/three, so a third generated input is required there. Before selecting that expanded proposal, the remaining bounded result-path falsifier is `predicate_result_branch(r = 0..2)`: maximum mixed Current true, the same host keys with all predicates false, and all-Opening cached `InvalidObservation` errors at the end of a maximum result vector. It invokes the real DNF owner, asserts the exact aggregate result and final cache cursor, and never runs Task effects. The error fixture is a generic evaluator input, not evidence that ordinary reference Oracle publication creates invalid Fresh state or that an Actor lifecycle fixture is admitted. + +Run `scripts/benchmarks.sh --extra --extrinsic predicate_result_branch --output /tmp/deos-exp28-current-result-branches.rs --json-file /tmp/deos-exp28-current-result-branches.json pallet_deos_actors` after rebuilding for this new diagnostic. Use the same controls and retain raw outcomes/I/O. A cached error or false-expression owner exceeding the proposed count-four envelope rejects coverage; source branch equivalence alone is not substituted for this bounded measurement. The category fit is diagnostic and may not become a predicate-count coefficient. + +Inspection of pinned `frame-benchmarking::analysis::linear_regression` finds that a negative intercept is replaced by the minimum observed value, after which the residuals are fitted without an intercept against the original component values. The observation-heavy total-count axis gives Oracle prefix reads `p - 1`; its generated proof model therefore needs a parameter-domain correction even though total reads remain `2p`. Reparameterize the same three physical cases by `o = 1..3` Oracle predicates, with one fixed asset and total count `o + 1`. This exposes positive fixed cost plus linear Oracle work instead of a negative-intercept prefix domain. Rebuild and repeat only this corrected model using `/tmp/deos-exp28-current-observation-axis.{rs,json}`; future composition must pass `total - 1` only for totals of at least two. The former total-count fragment remains historical diagnostic evidence, not an accepted third bound. + +Three sequential measurement identities distinguish the original axis, result branches and corrected axis. Each build was isolated from tests/other measurements and retained the same host, 50 × 20, map, cache and heap controls. Preflight compilation passes at each source (56/55/55 seconds), followed by the known G5 stale-storage failure; Wasm builds take 101/102/102 seconds and measurements 18/17/18 seconds. + +| Measurement | Prepared content/modes identity | Benchmark source SHA-256 | Benchmark Wasm SHA-256 | +| --- | --- | --- | --- | +| Original total-count axis | `3b730247166e24e86fb3df9509ef7a02dee4685396dc5f8b2ef6995510ee871b` | `7b8d601e828241061d13678605d25ebf89c4c3246aa4e57a490272d9fd305713` | `b81961d53da2230e4af8eab86ada202b9dd6f695c45a75b544a4dff4b80521a8` | +| Result branches | `3d8631e472c35f4f5f603ad7707eb2e305b1137d4c96ba64c2dd19572f280ffc` | `da901035e3610ed70e7bd9672f21d03525e7b3f69199d1fc986259c6b1883281` | `e163f01ff157db459be7db92365b1fd5826a9d27b70e8d29096890509675a253` | +| Corrected Oracle-count axis | `2a7e16075d1e974af393d5c7b267d1c6f4c9a1bc423171fec92a4579b44d6010` | `8250bf020be95310b8081927390a7e2c71a22101921ac25c39502f9474c3c49f` | `21e6382061b1d291d32fddaa5ab46eaa825b900ef279afeff381eb3497d3888c` | + +| Profile | RefTime before DB, ps | Estimated / recorded proof model, bytes | Generated DB reads | +| --- | --- | --- | --- | +| Original axis, `p = 2..4`, rejected bound | `9,959,452 + 9,674,841p`; slope error `8,270` | `4,562 + 644p` / `699 + 199p` | `2p` | +| Result branches, `r = 0..2`, diagnostic | Constant `47,919,460` | Constant `10,238` / `1,823` | Constant `8` | +| Corrected axis, `o = 1..3` | `19,604,950 + 9,589,122o`; slope error `8,524` | `4,364 + 2,673o` / `898 + 199o` | `2 + 2o` | + +| Profile / parameter | Time / DB samples | Min ns | Median ns | p95 ns | Max ns | Actual reads | Recorded proof bytes | +| --- | --- | --- | --- | --- | --- | --- | --- | +| Original axis / 2 | 10,000 / 500 | 27,517 | 29,124 | 38,972 | 112,656 | 4 | 1,097 | +| Original axis / 3 | 9,600 / 480 | 37,016 | 39,112 | 50,147 | 229,432 | 6 | 1,296 | +| Original axis / 4 | 400 / 20 | 44,838 | 46,375 | 56,782 | 68,166 | 8 | 1,489 | +| Result / Current true | 8,500 / 425 | 44,210 | 46,376 | 58,389 | 171,254 | 8 | 1,823 | +| Result / Current false | 8,160 / 408 | 44,280 | 46,306 | 58,738 | 198,492 | 8 | 1,823 | +| Result / Cached error | 340 / 17 | 1,816 | 2,864 | 3,353 | 15,924 | 0 | 0 | +| Corrected axis / 1 | 10,000 / 500 | 27,099 | 29,055 | 34,572 | 120,478 | 4 | 1,097 | +| Corrected axis / 2 | 9,600 / 480 | 36,807 | 38,903 | 49,239 | 149,603 | 6 | 1,296 | +| Corrected axis / 3 | 400 / 20 | 44,769 | 46,305 | 58,248 | 67,748 | 8 | 1,489 | + +All writes are zero; raw summaries keep all samples and the preceding upper-median/nearest-rank convention. The corrected axis reproduces every physical read count and recorded proof of the original cases exactly. Only the model coordinate and consequent regression change. Pinned `frame-benchmarking 49.0.0` analysis source SHA-256 is `7cef9829797e42d09677d191f9d5f0a0cf5baeeb2985c2d4d0daaa9441f662be`; `linear_regression` owns the inspected negative-intercept transformation. Do not treat the old 7,138-byte estimate at total four as an alternative cheaper physical design: the corrected model estimates 12,383 bytes for the same 1,489-byte recorded proof. + +| Measurement | Generated fragment SHA-256 | Raw JSON SHA-256 | +| --- | --- | --- | +| Original axis | `b508e5f5e74b99bb3e7cb96d67638623e395739638384137ef029633d7a897e8` | `71bc5a001dc8d4ef7891b26516374817269cf1b1c20d33cc25e554f49e4cc239` | +| Result branches | `794016e3683524a7894c8e7bb88f429665a736c1412a7e1defe582830e9ada77` | `9fa7c3faada4680f2b4fb9a05f0894dc1d47ac7f917f678ace62acb574887e53` | +| Corrected axis | `d5a001d77f8f679dcff535860de5f3455698de4a6f5403398be705396e72cc76` | `556744b0a7c5002d6a00e3c1274c194354c3fdffc738c3b5e9760ec2cc7d4dda` | + +This supersedes the earlier two-input numeric proposal with three inputs: pure maximum accounts, asset-heavy mixed count, and observation-heavy Oracle count. The third is called only for total count `c >= 2`, with `o = c - 1`; it is not extrapolated to zero. It raises RefTime at totals two/three by 1,242,267/277,248 ps over the previous proposal; the chosen proof bounds remain unchanged. The result-branch diagnostic is below the count-four envelope in both generated dimensions, and exact cursor checks establish traversal of all four cached errors. These qualify the sampled result classes without inventing a reference publication path for invalid Fresh state. + +| Total count | Revised DB-inclusive RefTime, ps | Revised estimated ProofSize, bytes | Selecting owners | +| --- | --- | --- | --- | +| 1 | 67,397,664 | 5,532 | Asset-heavy RefTime / asset-heavy proof | +| 2 | 129,194,072 | 7,224 | Observation-heavy RefTime / asset-heavy proof | +| 3 | 188,783,194 | 9,859 | Observation-heavy RefTime / pure proof | +| 4 | 249,060,087 | 12,697 | Asset-heavy RefTime / pure proof | + +All distinct-key non-native/Fresh-Oracle allocations at totals one through four now have sampled witnesses across the pure, mixed-count and fixed-four profiles. Native-presence and mixed/cache/refusal witnesses retain their stated maximum-count scope; full selector/admission closure is not inferred from this component table. Stop blind resampling of those unchanged cases. The next integration obligation is to bind the three generated inputs, their parameter mapping and the maximum/actual selector consistently, then consume them in EXP-0064 and G5. No coefficient is hand-repaired, no fragment is installed as complete production Weight, and C1 remains frozen. + +Validation: final formatting, all-features native `bench_predicate_` tests (30 seconds) and workspace/all-target/all-feature Clippy (25 seconds) pass. Mechanical checks verify all nine new raw rows and the revised four-point envelope. Runtime `AssetOps::balance` dispatches Native to Balances reducible balance and Local/Foreign to Assets reducible balance; the reference `AnchorLpFreezer` performs only namespace/account checks and adds no storage owner. This source check does not upgrade the stated measurement scopes to whole-Actor admission evidence. Production behavior and helper implementations are unchanged; complete generated binding remains G5 work. + +### Current Component Binding + +The runtime now stages the three generated Current methods unchanged from their normalized tool output. `RuntimeStepControlWeight::current_predicate_weight` owns zero handling, clamping to four, component-wise selection and the guarded observation parameter `n - 1`. Both composed maximum and actual call it; the V6 control identity commits all three input vectors. The two auxiliary profiles are regular production benchmarks and required `WeightInfo` methods. Generic package weights remain explicit non-production estimates. At this stage complete host-state qualification remained open and is closed by the next subsection; direct aggregate selectors and full G5 regeneration stay with their separate owners, so this is not whole-Actor Weight acceptance. + +For concrete binding, generate the pure profile with its actual method name and refresh the mixed control on the same existing benchmark Wasm `21e6382061b1d291d32fddaa5ab46eaa825b900ef279afeff381eb3497d3888c`, using `scripts/benchmarks.sh --skip-build --extra --extrinsic predicate_asset_evaluation --output /tmp/deos-exp28-current-asset-binding.rs --json-file /tmp/deos-exp28-current-asset-binding.json pallet_deos_actors` and the corresponding `--extrinsic predicate_set_evaluation` command with `/tmp/deos-exp28-current-mixed-binding.{rs,json}`. Reuse the corrected observation-axis artifact above. These runs use unchanged measured bodies and 50 × 20 controls; the Wasm predates the selector change and is measurement provenance, not final-tree production evidence. + +| Input | RefTime before DB, ps | Estimated proof bytes | Reads | Generated / raw SHA-256 | +| --- | --- | --- | --- | --- | +| Mixed total `n` | `6,797,239 + 10,691,960n` | `3,840 + 1,689n` | `2n` | `2cd5f42bafd2ae52c745307cca19921edee579949919dc80d48f0a808f352b79` / `7c8f928f1e9e94ce01a1f552f86bbf041c05988dfc7d9306e7ca01bb2d16b431` | +| Pure assets `n` | `7,859,605 + 9,430,718n` | `1,345 + 2,838n` | `2n` | `ebca2b068fc8dd6ef71b0c2de8b8cb6efdfa68a30312754bacd6eef00383acde` / `be245d1577ffacb46daeae712b10afff503f1296d4de3f4c0a3d25349b478f0a` | + +With reference read cost 25,000,000 ps, staged maxima at totals 1..4 are `(67,489,199; 5,529)`, `(129,194,072; 7,218)`, `(188,873,119; 9,859)`, `(249,565,079; 12,697)` in RefTime/ProofSize. Zero and Opening-doubled hints are tested through composed actual, with maximum containment and unchanged separate Opening capture. These replace the earlier proposal's numeric values only for this staged component. + +| Input / count | Time / DB samples | Min / median / p95 / max ns | Reads | Recorded proof bytes | +| --- | --- | --- | --- | --- | +| Mixed / 1 | 7480 / 374 | 15016 / 16832 / 22908 / 109094 | 2 | 719 | +| Mixed / 2 | 7040 / 352 | 27029 / 29124 / 40788 / 202753 | 4 | 1097 | +| Mixed / 3 | 7040 / 352 | 36178 / 38483 / 52522 / 128510 | 6 | 1457 | +| Mixed / 4 | 440 / 22 | 45118 / 47143 / 62788 / 87023 | 8 | 1823 | +| Pure / 1 | 7480 / 374 | 15435 / 17600 / 28077 / 96942 | 2 | 719 | +| Pure / 2 | 7040 / 352 | 22768 / 25702 / 41556 / 125856 | 4 | 1079 | +| Pure / 3 | 7040 / 352 | 32895 / 36318 / 58388 / 272665 | 6 | 1445 | +| Pure / 4 | 440 / 22 | 41905 / 44699 / 67258 / 88281 | 8 | 1812 | + +All recorded writes are zero; timing summaries retain the preceding upper-median and nearest-rank convention. + +Validation: the default runtime suite passed 488 tests and exposed four affected numeric/selector fixtures. After correction, the Actors suite passed the Transfer/success cases and focused reruns passed composed Current and SwapOut success/retry checks. The changed complete-path values follow the exact Current delta `(+88,563,103 ps, +6,330 bytes)` at saturated hints; direct aggregate results remain unchanged. Independent embedding tests and all-features native `bench_predicate_` tests pass. Exact text comparison confirms that all three installed methods, including annotations, match their generated artifacts. Wiki inspection found no component-level claim requiring a semantic update. + +### Current Host and Composed-Selector Closure + +The retained evaluator and reference adapters close the remaining scoped Current component review. This consumes the earlier pure/mixed count, nine Native-presence allocations, five Oracle states, cached-result and true/false/error measurements; no unchanged profile is repeated. `evaluate_precondition_with` visits every admitted atom and preserves the first error. Singleton clauses maximize outer-clause visits at the fixed four-atom reference bound. Comparison direction changes only fixed-width scalar operations, not host calls or iteration cardinality. + +| Branch family | Source owner and retained evidence | Component conclusion | +| --- | --- | --- | +| Balance comparisons | `execution.rs::evaluate_atomic_predicate` and `spendable_balance`; runtime `AssetOps::balance`; maximum-account count and Native allocation profiles | Each balance atom invokes one adapter read path. Local/Foreign share Assets; Native uses Balances and subtracts already supplied reserved fees without an additional ledger read. | +| Observation comparisons/refusals | `fresh_observation_value`, runtime `ObservationProvider::observe`, Oracle `observation_state`; five mixed read-state witnesses | At most the Feed/Observation owners per atom; Missing/Deactivated stop after Feed, other states use both. Refusal/result handling adds no scheduler, funding or capture read. Populated-trie witnesses retain their exact scope. | +| Cached Opening visits | `evaluate_step_precondition`; cached count and cached-error profiles; Opening reuse tests | Indexed bounded result access, checked cursor advancement and error mapping add no host read. Cached visits remain covered by the existing total-visit hint; no second Opening capture is charged. | +| Block predicates and DNF outcomes | `evaluate_atomic_predicate`, `evaluate_precondition_with`; all-atom/first-error test and true/false/error diagnostics | Block context and fixed-width comparison require no new host namespace; false clauses and errors do not short-circuit the visit accounting. | +| Runtime selector | `current_predicate_weight`, composed maximum and composed actual; expanded existing regression | Zero and hints 1..8 select the declared models at `min(hint, 4)` in Opening, Running and both Suspended head/tail contexts, with actual contained by maximum. | + +The expanded regression uses the composed failed-result path to avoid claiming that an inclusive direct aggregate consumes a separate component. Its contexts test provider arithmetic, not constructive Contract admission. The earlier predicate cardinality and lifecycle tests remain applicable because evaluator/adapter behavior is unchanged. This closes the reference Current component's model/source/selector input review; direct aggregate regeneration, whole-transition refusal coverage and legal admission remain with EXP-0029 / EXP-0064 / EXP-0065. Timing observations, generated fits and actual charged Weight are still distinct classes, and an unrelated embedding must qualify its own host. + +Validation: the expanded default-feature runtime regression passes through `ci-local.sh --only tests --package deos-runtime --default-features --test-filter composed_current_weight_covers_host_models_without_opening_double_charge` (9 seconds); workspace/all-target/all-feature Clippy passes (6 seconds). This test-only extension changes no evaluator, adapter, generated method or production identity vector, so the preceding V10 runtime/embedding and component measurements remain applicable within their scopes. Source content/modes for this review and EXP-0029's subsequent admission probes is `abad930ade82698f93b7352f4baf3604961efdcb5beae285cb46577f6c19d37d`. + + +## Derived Metrics + +Derived values are retained beside the evidence that defines their source and units. No subtraction of overlapping aggregate owners or cross-artifact timing comparison is introduced. + +## Statistical / Variance Notes + +Retain each item’s original sampling and uncertainty. Singleton construction probes establish only their admitted state/access witness, never statistical timing dominance or a production bound. + +## Result + +The retained V10 evidence closes the reference Current component. Across admitted counts one through four, the component-wise selector preserves the independently larger RefTime and ProofSize owners from the three generated inputs. Source review plus measured pure/mixed allocations, Native presence, Oracle states, cached Opening results and true/false/error branches cover every reference typed-host and DNF result family. The composed regression confirms identical Current selection in Opening, Running and Suspended head/tail contexts, clamps doubled Opening hints to four physical Current visits, and keeps actual contained by maximum without adding Opening capture twice. + +## Interpretation + +The sound owner is a component-wise generated envelope, not any one categorical regression or raw timing maximum. Opening cached visits remain represented by the total-visit hint but add no Current host reads; their capture owner is separate. Direct aggregate branches retain their inclusive owners rather than adding this component again. This accepts the Current component and selector for the reference binding while leaving whole-transition admission, complete generated-file identity and unrelated host implementations to their own owners. + +## Decision + +Accepted for the reference binding. Select the component-wise maximum of `predicate_set_evaluation(n)`, `predicate_asset_evaluation(n)` and, only for `n >= 2`, `predicate_observation_heavy_evaluation(n - 1)`, with `n = min(evaluation_units, 4)` and zero selecting no Current component. Use the same selector for maximum and actual control Weight in Opening, Running and Suspended contexts. Preserve separate Opening capture and inclusive direct aggregate ownership. EXP-0065 owns complete regeneration and exact final artifact identity; no coefficient may be manually combined or repaired. + +## Rejected Alternatives and Tradeoffs + +Retain all rejected candidates and causal limits in the extracted evidence. Do not infer a new selection from record relocation or from aggregate coefficient subtraction. + +## Validity + +- `Establishes`: A generated component-wise Current envelope and shared maximum/actual selector over the reference four-predicate Step domain, including admitted Balance, Observation, block, cached-Opening and DNF true/false/error families. +- `Does not establish`: Whole-transition admission, arbitrary embedding hosts, complete production Weight-file identity, exact release artifact closure or integrated throughput. +- `Known limitations`: The three methods are staged from recorded generated artifacts rather than a complete final-tree regeneration; direct aggregate and broader admission evidence remain owned by EXP-0029/EXP-0064, and EXP-0065 must refresh exact production artifacts. +- `Invalidation triggers`: A changed predicate evaluator, adapter host/read domain, configured predicate bound, selector, measured body, generated Weight, source/Wasm identity or acceptance rule requires scoped requalification. +- `Residual qualitative insight`: Component-wise maxima preserve independent resource owners without double-charging cached Opening work. + +## Follow-Up / Next Gradient + +Stop measuring this satisfied component. EXP-0033 and EXP-0034 independently own Opening capture, EXP-0064 composes admission, and EXP-0065 regenerates exact artifacts. Reopen only for the declared falsifier, a changed reference host/predicate domain or a changed relevant identity; broader release closure alone is not a reopen trigger. + +## Architecture Impact + +The existing V10 runtime selector and three staged generated methods are accepted as the scoped Current component input. No new runtime, semantic or storage change is introduced by this decision; C1 and A0 remain frozen, and complete production regeneration remains G5 work. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: [EXP-0028](./EXP-0028.md) O3 +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: [EXP-0025](./EXP-0025.md) +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: None. +- `Transfers question to`: None; independent findings require an explicit new transfer. +- `Produces input for`: [EXP-0028](./EXP-0028.md) +- `Reopen trigger`: A falsifier of this declared owner/domain or exact artifact identity; cost alone does not reopen frozen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0033.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0033.md new file mode 100644 index 00000000..d578f5a7 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0033.md @@ -0,0 +1,542 @@ +# EXP-0033 — Opening Predicate Capture Bound + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | [EXP-0028](./EXP-0028.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-06 | +| Affected domain | The nonzero Opening predicate capture bound dominates the declared full-Contract predicate allocation and host-state domain. | +| Physical mechanism | Opening Predicate Capture Bound | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0025](./EXP-0025.md) — Accepted, physical architecture only | +| Successor experiment | [EXP-0028](./EXP-0028.md) | + + +## Decision Question + +The nonzero Opening predicate capture bound dominates the declared full-Contract predicate allocation and host-state domain. + +## Governing Specification + +- `Owners`: `template/pallets/actors/docs/specification.en.md` (sole ownership, lifecycle/Trigger/Pipeline/Action semantics, placement and scheduler); `docs/actors-resource-policy.specification.en.md` (component-wise admission/actual settlement, equal-thirds block policy); `docs/actors-performance-assurance.specification.en.md` (evidence and target). +- `Frozen physical input`: [EXP-0025](./EXP-0025.md) C1 Architecture Freeze. No change to FIFO, ticket chronology, Q1, causal floor, primary/reference ontology, source/successor atomicity or sole execution owner. +- `Semantic oracle`: A0 SHA-256 `8746e423168309975e509be91e4eb06fdf0eb0b43369d75f1ee62202a4341678` remains frozen; use only for relevant semantic equivalence, never as a Weight oracle. + +## Context and Hypothesis + +This Leaf owns opening predicate capture bound. Its boundary is the single claim above. Evidence was extracted from the explicitly marked pre-fission parent subsections at Git baseline `a60de7748132211d9e58cecd5be82b41f76ce230`; this migration changes navigation and evidence ownership only. Source, Wasm, commands, rejection outcomes and evidence limitations retain their original meaning. + +Extraction provenance at the recorded Git baseline: EXP-0028 / “Opening Capture Component Ranges”, “Asset-Heavy Maximum-Account Capture”, “Maximum-Account Range and Complete Fixed-Total Allocations”, “Oracle Read-State Coverage”, “Full-Contract Opening Predicate Capture”, “Full-Contract Predicate Model Binding”. Original directional prose (above/below) belongs to these pre-fission subsections; current cross-owner navigation follows the parent proof map. + +## Baseline and Candidates + +Retained measured implementations and rejected candidates are identified in the extracted evidence. The deciding candidate is the staged component-wise maximum of the generated full-Contract mixed, pure maximum-account and one-account/Oracle-heavy inputs. `RuntimeStepControlWeight::opening_predicate_weight` selects the first two at every nonzero count, adds the Oracle-heavy input at `count - 1` only for counts of at least two, and selects only the independent full-Contract traversal at zero. + +## Controls + +Only the declared witness input varies within each source-bound comparison. Preserve the exact host, toolchain, database, Wasm and sampling controls printed with each evidence item; do not compare unlike artifacts as a controlled performance result. + +## Benchmark Class, Workloads, and Criteria + +Acceptance requires the single claim to hold over its declared admitted domain, with each physical operation charged once and every retained limitation resolved before broader production use. One legal omitted-owner, domain or containment counterexample falsifies that claim. Existing workloads below are bounded witnesses, not permission to add independent questions. + +## Environment and Artifact Identity + +Pre-fission baseline `a60de7748132211d9e58cecd5be82b41f76ce230`. Each extracted block retains its own exact source/Wasm/output identity. Shared pinned hardware/toolchain/cache controls follow [EXP-0030](./EXP-0030.md#environment-and-artifact-identity) only where the original comparison explicitly reuses them. A shared environment never substitutes for the block’s own source or Wasm identity. No runtime artifact was rebuilt during migration. + +## Proof Obligations + +- `Freeze`: Frozen by explicit migration reconstruction from the pre-fission decision and retained witnesses at a60de7748132211d9e58cecd5be82b41f76ce230. No new measurement or historical decision is implied. +- `Review triggers`: More than six proof subsections; multiple consumers reviewed. +- `Decomposition review`: Seven retained subsections qualify one nonzero Opening predicate capture owner. Packed/full-Contract, ledger/Oracle allocation and typed read-state witnesses challenge the same count-bound acceptance rule; historical rejected fits remain causal evidence for the retained model. Independently charged zero-result traversal moved to EXP-0035, amount capture to EXP-0034, and exact complete artifact closure to EXP-0065. No independent model may be investigated here merely because it shares an Opening name. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | The nonzero Opening predicate capture bound dominates the declared full-Contract predicate allocation and host-state domain. | One full-Contract nonzero capture allocation whose typed host reads or measured model exceed the selected predicate bound | Reference-Wasm FRAME evidence in the declared owner/domain; native checks are construction controls | Self | Required | Named parent decision and declared downstream consumers | Satisfied for the reference binding: generated component owners cover all admitted counts, Fresh host allocations and qualified refusal states; maximum/actual share the selector without adding zero traversal or direct inclusive capture | + +## Measurements + + +### Asset-Heavy Maximum-Account Capture + +| Paired `p = 1..48` profile | RefTime before DB, ps | Proof bytes | Database model | +| --- | --- | --- | --- | +| Fresh ObservationAbove | `4,586,232 + 9,113,090p`; slope error `13,572`; minimum `13,270,000` | Estimated `1,280 + 2,654p`; recorded `289 + 179p` | `2p` reads, zero writes | +| BalanceAbove | `2,610,353 + 8,679,894p`; slope error `7,888`; minimum `6,076,000` | Estimated `4,235 + 2,613p`; recorded `156 + 310p` | `1 + 2p` reads, zero writes | + +Observation fragment SHA-256 `6cb709faa40d4f061590126e94f112a2acdb139e55f7b407d7dc910687bbe23b`; paired balance fragment `74fb6c66cd91904fee10d3855e06693488d6e45631a4c2be59fd647db8212461`. Observation reads `p` Oracle Feeds and `p` Observations; each expected result is `Ok(true)`. The reference helper uses published EMA feeds with Local input and Native output. Maximum feed identity/value widths, mixed predicate families and unavailable/stale/error states are not measured here. + +With the reference RocksDb read charge of `25,000,000` ps, BalanceAbove minus Observation is `23,024,121 − 433,196p` ps and `2,955 − 41p` estimated proof bytes. Both differences remain positive for every integer `1..48`; their minima at 48 are `2,230,713` ps and `987` bytes. Thus the paired generated models do not falsify balance coverage for this feed shape, despite Observation's larger pre-DB CPU slope. Do not subtract the DB allowance from production ownership or promote this small margin to robust generic dominance: timing/model uncertainty and unqualified feed widths remain material. No numeric production binding or architecture change is selected. + +Validation: native all-features `bench_opening_` tests pass (31 seconds), canonical formatting and Actors all-features Clippy pass (36 seconds). Benchmark compilation passes (62 seconds), with the known G5 stale-storage rejection retained. The only code change is benchmark setup/helper and the extra diagnostic; execution, runtime policy, weights and public production contracts are unchanged. + +The next controlled refinement strengthens the same Observation diagnostic to maximum host encoding. A separate optional benchmark-helper capability prepares Local/Local EMA feeds and asserts `encoded_size == MaxEncodedLen` for every feed identifier, registered configuration and published observation in the reference runtime. Existing general observation helpers and their historical fixtures stay unchanged; unsupported hosts fail explicitly. Repeat the diagnostic and balance control on one new Wasm at 50 × 20. This tests whether the earlier small resource margin survives complete encoded feed widths, without changing admission, runtime observations or production Weight. + +The maximum-encoding refinement passes native setup and reference Wasm: each feed key is 17 bytes, configuration 53 bytes and observation 28 bytes, with equality to their host `MaxEncodedLen` asserted before measurement. Source content/modes `fb7fe7d8f82d58a3c0eff57fc506654fa6b784542a422046f492cc79cfc754f8`; benchmark source `de45c503f695bb04c9d3eaf7eb1cf04f87b0582f9bea3d64a74d33cd4cddcb66`; runtime helper source `2c305f9c1e27dacb7bde37dd2435bc1c7082b94242653659860934c1fab9ef0e`; benchmark Wasm `c46f600591afdda7aead219a09d145c13e4307012191cfb06d02d33d3225f3bb`. Same 50 × 20 controls; build 112 seconds, Observation/balance runs 14/16 seconds without competing jobs. + +| Maximum-encoding pair, `p = 1..48` | RefTime before DB, ps | Proof bytes | Database model | +| --- | --- | --- | --- | +| Fresh ObservationAbove, full host encoding | `2,051,877 + 9,261,879p`; slope error `14,587`; minimum `13,340,000` | Estimated `1,209 + 2,667p`; recorded `216 + 192p` | `2p` reads, zero writes | +| BalanceAbove control | `910,881 + 8,719,271p`; slope error `7,780`; minimum `5,727,000` | Estimated `4,235 + 2,613p`; recorded `156 + 310p` | `1 + 2p` reads, zero writes | + +Commands reuse the preceding pair, writing `/tmp/deos-exp28-opening-observation-max-encoding.rs` and `/tmp/deos-exp28-opening-balance-max-encoding-pair.rs`. Fragment SHA-256 respectively `ac8aa1bf2c67391a3ac7f812542aeb876955be55cce965cb01440263b4cdef28` and `d4a32e6f39a49016cffac6f840b33423c01790695159ca78959a567804e03a92`. Both remain uninstalled. Ordinary registration and publication construct the host values; no direct Oracle storage installation or admission bypass is used. + +After the same DB charge, Observation minus balance RefTime is `−23,859,004 + 542,608p` ps: the fit crosses at `p = 44` and reaches `2,186,180` ps at 48. Balance estimated proof remains higher by `3,026 − 54p` bytes, with `434` bytes at 48. The CPU crossing is small and model uncertainty matters; it is not a throughput regression claim. It does reject using the previous paired fit as robust generic dominance. Complete scalar coverage must also address mixed storage families; taking the maximum of two pure-family models does not automatically bound their combined namespace/read geometry. + +The source-state audit separates semantic coverage from numeric bounds. `Oracle::observation_state` reads Feed configuration and, unless missing/deactivated, the observation. Fresh and Stale therefore share those two owners; Uninitialized probes the second owner without a value; missing/deactivated feeds stop after the first. The Actor validates a Fresh timestamp/age before applying one scalar comparison. Existing `observation_conditions_compare_only_fresh_scalar_values`, `invalid_fresh_observation_fails_permanently_and_applies_step_policy`, `zero_observation_max_age_is_rejected_during_plan_validation` and Oracle lifecycle tests cover those meanings. This inspection identifies no additional storage owner in refusal states, but does not turn Fresh-only measured proof into a numeric certificate for every missing-key trie shape or generic host error. + +The mixed-family falsifier adds `opening_mixed_predicate_capture(p)`, `p = 2..48`: one non-native balance plus one maximum-encoded fresh Observation at two; at three or more, one native and one non-native balance plus `p−2` distinct maximum-encoded Observations. It retains one capture call and exactly `p` true results while introducing both Assets and Oracle namespaces, plus System at three. Compare it with both pure families on one fresh Wasm at 50 × 20. If either component exceeds the component-wise maximum of the pure models, that pure-model envelope is rejected. This is a specified mixture, not an assertion that it maximizes every possible allocation or ledger state. + +All three profiles pass on source content/modes `6619ad97bd2abffb60e08f67ab57939cb873de853760bf4ef0024a3a032fed55`, benchmark source `ea63d0bd7fc9838ebce6f9c3784363034a68c9166c13db2c9ef3fd3e1fdfe1cb`, runtime helper source `24525add5debbda929e2047e2358cf4d64a27653ab8a994d80c76e57443f603a`, and benchmark Wasm `173af97376fde5a805d56abecdb04d82b268e10c7455ee2243498cdae067003b`. Use the same public commands with `opening_mixed_predicate_capture`, `opening_observation_capture`, then `opening_predicate_capture`; `--extra` for the first two, `--skip-build` for the controls. Outputs are `/tmp/deos-exp28-opening-mixed.rs`, `/tmp/deos-exp28-opening-observation-mixed-control.rs`, and `/tmp/deos-exp28-opening-balance-mixed-control.rs`. Build 114 seconds; isolated runs 15/14/16 seconds, same 50 × 20/map/cache/heap controls and no contention. + +| Mixed comparison | RefTime before DB, ps | Proof bytes | Database model | +| --- | --- | --- | --- | +| Mixed, `p = 2..48` | `7,316,446 + 9,171,970p`; slope error `13,728`; minimum `27,518,000` | Estimated `4,511 + 2,420p`; recorded `536 + 191p` | `4 + 2p` reads, zero writes | +| Full-encoding Observation control, `p = 1..48` | `4,587,686 + 9,148,984p`; slope error `13,426`; minimum `13,270,000` | Estimated `1,209 + 2,667p`; recorded `216 + 192p` | `2p` reads, zero writes | +| Balance control, `p = 1..48` | `1,119,604 + 8,829,485p`; slope error `8,279`; minimum `6,146,000` | Estimated `4,235 + 2,613p`; recorded `156 + 310p` | `1 + 2p` reads, zero writes | + +Fragment SHA-256 respectively `d1f28774beb603821e67d331f047017331daad259550f6fbf6cb85d3516b54af`, `f43af00db184219485c21c24d9130341b07d0f8ac709984e2baa83505870a252`, and `4e74a5046c354cd03056c1bb27b89217cffc4b786f3fd587e68acd92d119f2d8`. No fragment is installed. The mixed maximum trace reads one System account, one Asset/Account pair and 46 Feed/Observation pairs: 95 physical reads, versus the generated model's 100. At `p = 2` there is no native predicate; the actual read count is four. Generated parameter/intercept conservatism must remain distinct from actual I/O. + +On this exact triple, balance is the larger pure generated model in both dimensions throughout `2..48`. Mixed exceeds its DB-inclusive RefTime by `81,196,842 + 342,485p` ps, reaching `97,636,122` ps at 48, while estimated proof is lower by `193p − 276` bytes. Of that maximum RefTime difference, `75,000,000` ps comes from three additional modeled reads, not three additional physical reads. Reject the pure-model maximum as a complete modeled envelope for this mixture; do not infer an equivalent wall-clock penalty or manually subtract DB/aggregate terms. The sign of the small pure-family CPU margin varies across the recorded fits, reinforcing that one fit is not robust dominance. + +This mixture is Observation-heavy and does not qualify asset-heavy allocation or maximum encoded asset/account state; the next subsection tests that separate falsifier. The new capture inputs preserve ordinary host registration/publication, and the helper now returns typed errors for missing published state instead of `expect`; all maximum-size and result assertions remain. Final native `bench_opening_` tests pass (36 seconds), workspace/all-target/all-feature Clippy passes (26 seconds), and formatting passes. Preflight compilation passes (64 seconds), retaining the known G5 stale-storage rejection. Production feature inputs are unchanged; runtime/embedding behavior reuses prior evidence, with fresh benchmark Wasm covering the changed host helper. + +### Asset-Heavy Maximum-Account Capture + +The next falsifier is `opening_asset_heavy_predicate_capture(p)`, `p = 2..48`: one maximum-encoded fresh Observation plus `p−1` distinct balance predicates. At two, the balance is non-native; at three or more, one is native and the others non-native. The reference host prepares asset accounts through ordinary `Assets::touch_other` from a distinct funded depositor before minting. This reaches `ExistenceReason::DepositFrom(AccountId, Balance)`, unlike the short `Sufficient` fixture. Assertions require AssetDetails and AssetAccount to attain their declared maximum encoded lengths. The mock's scalar balances have no account-reason variants; unsupported external hosts return an explicit benchmark-only error. + +Prepared source content/modes `7ca83481dc7fbb27fdf16892b820acd8b6ca83281142ffbd0a5b58268f8293c6`, benchmark source `469ed974be652768bfaf5b92ef19336b76870351df61437de30c0933539b4bca`, runtime helper `1c134bf03072ed87bb0d022dd5e3842d5105f1aeed35caaa8d0821e5e05f6045`. Compare the new profile and previous Observation-heavy profile on one fresh benchmark Wasm with unchanged 50 × 20/map/cache/heap controls. Commands use `./scripts/benchmarks.sh --extra --extrinsic opening_asset_heavy_predicate_capture --output /tmp/deos-exp28-opening-asset-heavy.rs pallet_deos_actors`, then `--skip-build --extra --extrinsic opening_mixed_predicate_capture --output /tmp/deos-exp28-opening-mixed-asset-control.rs pallet_deos_actors`. Any component above the previous mixed model rejects that model as a universal scalar bound. Setup, deposits and minting remain outside the measured capture; exactly `p` true results are required. This endpoint mixture does not by itself certify all allocations or refusal states, and no partial generated fragment is a production binding. + +Both profiles pass on benchmark Wasm `2d528c8555d0e27607554aca3eedccaf425976783e9720fc4da09a6aff089183`, with the prepared source unchanged. Build 100 seconds; isolated measurement 16 seconds each with no competing jobs. AssetDetails is 190 encoded bytes and AssetAccount is 66; `DepositFrom` adds 48 bytes over `Sufficient`, without an additional read in `reducible_balance`. Its depositor reserve is setup state, not a capture cost or Actor fee. Maximum assertions execute successfully in Wasm. + +| Maximum-account comparison | RefTime before DB, ps | Proof bytes | Database model | +| --- | --- | --- | --- | +| Asset-heavy, `p = 2..48` | `10,124,651 + 8,234,437p`; slope error `5,399`; minimum `27,588,000` | Estimated `4,562 + 2,576p`, slope error `9`; recorded `126 + 361p` | `4 + 2p` reads, zero writes | +| Observation-heavy control, `p = 2..48` | `5,826,282 + 8,909,973p`; slope error `8,510`; minimum `27,587,000` | Estimated `4,511 + 2,420p`; recorded `536 + 191p` | `4 + 2p` reads, zero writes | + +Fragment SHA-256 respectively `762858879131ea986e23aeda4722afbb61b9d32e225127499ca94fc2e5bb1713` and `d1ea7c76d5c3410f53c06d9d7d8067510a8279d467c171e247deb9446560ba59`; neither is installed. At 48, both traces have 95 physical reads: asset-heavy reads one System account, 46 Asset/Account pairs and one Feed/Observation pair; the control reverses those pair counts. Both generated models reserve 100 reads at that point. The new maximum account encoding and family allocation both differ from the control, so this pair does not isolate their individual timing effects. + +Asset-heavy minus control RefTime is `4,298,369 − 675,536p` ps; it is positive at `2..6` and negative at `7..48`, reaching `−28,127,359` ps at 48. Estimated proof is higher by `51 + 156p` bytes throughout: `128,210` versus `120,671` bytes at 48, a `7,539`-byte difference. The Observation-heavy model therefore fails as a universal scalar bound in both dimensions over different parts of this range. Preserve the component-wise tradeoff; neither smaller RefTime nor a maximum across these endpoint fits proves all allocations. This pair does not qualify pure maximum-account capture or interior family allocations; the next subsection tests those separate cases. Oracle refusal proof remains separate. The helper qualifies the reference host's account width, not generic host cost dominance. + +Native `bench_opening_` tests pass (31 seconds), workspace/all-target/all-feature Clippy passes (24 seconds), and formatting passes. Benchmark preflight compilation passes (62 seconds); its existing G5 stale-storage check still fails. Production feature behavior is unchanged; no runtime weight or semantic binding is updated by these diagnostic fixtures. + +### Maximum-Account Range and Complete Fixed-Total Allocations + +The next comparison uses three extra profiles sharing one maximum-encoding constructor: `opening_max_encoded_balance_capture(p = 1..48)` uses only distinct non-native accounts; `opening_predicate_allocation(b = 0..48)` captures exactly 48 results from `b` non-native balances and `48−b` fresh Observations; `opening_predicate_native_allocation(b = 0..47)` adds one native balance and uses `b` non-native balances plus `47−b` Observations. All accounts use the previous ordinary `DepositFrom` setup and all feeds use maximum encoded host state. This closes the omitted all-non-native endpoint and enumerates both fixed-total allocation families rather than assuming dominance from the Observation-heavy or asset-heavy corners. Exactly 48 true results and distinct family keys are asserted; Actor creation/admission, fee collection and whole-Pipeline liability remain outside this component. + +Prepared source content/modes `b95bca850fbff42009f59c6a257a45b4ada2c3b24292297d17ccf6dfcc83e261`, benchmark source `5d42b9d7a6cf8e87b665e1507b2b51627c73fd6433b7a2f65690ae30adbf14b4`, script `159075e17ca22c3833a9fee4fd2ff8a941e15d98531cac130e01ea6453fefe1c`. The public script now forwards optional `--json-file FILE` for one exact benchmark, rejects colliding output paths and refuses compile-only fallback when raw samples are requested. Raw JSON records time and DB results; SDK serialization omits tracked keys, so it is not a complete storage-prefix trace or replayable proof-estimation artifact. Default 50 steps × 20 repeats samples every integer in these ranges under CLI 58.0.0's component selection; verify that coverage from the raw output before claiming enumeration. Preserve all samples without outlier removal and report per-component timing summaries, recorded proof and DB I/O separately from fitted Weight. + +Run `./scripts/benchmarks.sh --extra --extrinsic opening_max_encoded_balance_capture --output /tmp/deos-exp28-opening-max-balance.rs --json-file /tmp/deos-exp28-opening-max-balance.json pallet_deos_actors`, then same-Wasm `--skip-build` runs for `opening_predicate_allocation` and `opening_predicate_native_allocation`, using `/tmp/deos-exp28-opening-allocation.{rs,json}` and `/tmp/deos-exp28-opening-native-allocation.{rs,json}` respectively. Keep prior map/cache/heap/toolchain controls and isolated execution. The fixed-total models are diagnostic family-allocation fits, not a scalar total-predicate Weight; a negative/clamped slope or interior excess rejects endpoint interpolation without licensing installation of that fit. Compare maxima and every sampled allocation against proposed bounds, retaining generated estimated proof, recorded proof, host timing and charged DB costs as separate evidence classes. This bounded enumeration can select or reject capture-family dominance at total 48; lower-total mixed states and Oracle refusal proof require owner-based justification or their own falsifier before complete production coverage. + +All three runs pass on benchmark Wasm `46e4101858d1bbe1352364a637fd79797e151c7622b974a9eb27e93506750982`, with prepared source unchanged. Build 101 seconds; isolated measurement 15/15/17 seconds. Exact raw coverage is `p = 1..48`, `b = 0..48` without native and `b = 0..47` with native: 48 range points and 97 fixed-total compositions. The CLI's default ten-second minimum duration expands the requested minimum repetitions: pure balance has 6,000 timing/300 DB samples; each allocation profile has 3,000 timing/150 DB samples. Pure `p = 1,24`, no-native `b = 0` and native `b = 0,23` have twice the ordinary samples due to integer component sampling. No sample was discarded. + +| Profile | Generated RefTime before DB, ps | Generated proof bytes | Generated DB | +| --- | --- | --- | --- | +| Pure maximum non-native balance, `p = 1..48` | `7,013,802 + 8,328,115p`; slope error `4,893`; minimum `15,575,000` | Estimated `1,358 + 2,836p`; recorded fit `367 + 361p` | `2p` reads | +| Fixed 48, no native, `b = 0..48` | Constant `429,976,500`; minimum `395,098,000` | Estimated `129,496 + 2,644b`; recorded fit `9,722 + 169b` | Constant 96 reads | +| Fixed 48, one native, `b = 0..47` | Constant `439,951,934`; minimum `393,422,000` | Estimated `126,929 + 2,644b`; recorded fit `9,630 + 169b` | Constant 95 reads | + +All writes are zero. Raw DB tracking verifies `2p` unique reads with zero repeated reads for pure balance, 96 unique/zero repeated for no-native allocations, and 95 unique/one repeated for native allocations at every sampled point. The native repeated read is consistent with `pallet-balances 50.0.0`'s `reducible_balance`: it reads account data and, for a nonzero Expendable balance, asks `System::can_dec_provider` about that same account. It is not another unique database owner or a write. + +| Artifact | Pure maximum balance SHA-256 | No-native allocation SHA-256 | Native allocation SHA-256 | +| --- | --- | --- | --- | +| Generated fragment, uninstalled | `d629a52e6c3318cf0e6fc5b52ab59c6441327208970be6a268fcc33ea1e5a811` | `bdafa19ae62db615193e6014d4cd5468217a4980b32eafe68167ecbb964a57f6` | `bf4aea4d3ecc6b2de9426a9c4e34c87b4d4141806e18276a2b372bfe9c4ae63b` | +| Raw JSON | `adb9dff9924714fc0b1481ec3aa326b0bca91718e869654f1d982c70e58d8559` | `a92f51639c76cc25a33b1742cdaeda55ef62dd961ef471f2b7fcb64db44385c4` | `aff419303b67fec3d2f389b8730dd4da5a3983c788661eb43fa0a774e9f7d503` | + +The table retains every normalized component row. Its first column is `p` for pure balance and `b` for the fixed-48 allocations; those are different semantic axes. Timing is the median of all raw `extrinsic_time` samples in nanoseconds, without modeled DB charge. Recorded proof is identical across DB samples at each point. Ordinary timing sample counts are 120 for pure balance and 60 for either allocation, doubled at the points listed above. + +| p or b | Pure balance median ns | Recorded B | No-native allocation median ns | Recorded B | Native allocation median ns | Recorded B | +| --- | --- | --- | --- | --- | --- | --- | +| 0 | — | — | 420974.5 | 9441 | 429705 | 9352 | +| 1 | 17461 | 719 | 429844.5 | 9896 | 430543 | 9800 | +| 2 | 25493 | 1079 | 427400 | 10058 | 431486.5 | 9967 | +| 3 | 34782 | 1445 | 426562 | 10231 | 428483 | 10135 | +| 4 | 43093 | 1812 | 425724 | 10400 | 435083 | 10312 | +| 5 | 50635.5 | 2172 | 432953 | 10570 | 431451 | 10482 | +| 6 | 57760 | 2532 | 429530.5 | 10740 | 430927.5 | 10652 | +| 7 | 65862 | 2898 | 426143 | 10916 | 436235 | 10861 | +| 8 | 73544 | 3258 | 430019 | 11119 | 434908 | 11025 | +| 9 | 81121.5 | 3619 | 431067 | 11283 | 440775.5 | 11221 | +| 10 | 88211 | 3977 | 433651 | 11477 | 436060.5 | 11389 | +| 11 | 95405 | 4337 | 430543.5 | 11647 | 432219.5 | 11558 | +| 12 | 105951 | 4695 | 435851 | 11814 | 439343.5 | 11716 | +| 13 | 114017.5 | 5061 | 421952 | 11980 | 430054.5 | 11889 | +| 14 | 122538 | 5427 | 430822.5 | 12153 | 438575.5 | 12058 | +| 15 | 130256 | 5787 | 426493 | 12316 | 431975 | 12187 | +| 16 | 138043 | 6153 | 432952.5 | 12451 | 432290 | 12360 | +| 17 | 148764 | 6513 | 427924 | 12618 | 441682.5 | 12526 | +| 18 | 156517 | 6871 | 426911.5 | 12782 | 437842 | 12653 | +| 19 | 163186.5 | 7229 | 426073 | 12909 | 447759 | 12821 | +| 20 | 171113.5 | 7589 | 429042 | 13079 | 436759 | 12986 | +| 21 | 179215 | 7955 | 424851.5 | 13250 | 434419.5 | 13157 | +| 22 | 189063 | 8319 | 429007 | 13419 | 441368.5 | 13330 | +| 23 | 196467 | 8685 | 430124 | 13594 | 456001 | 13503 | +| 24 | 205441.5 | 9051 | 430682.5 | 13767 | 460715 | 13673 | +| 25 | 215638 | 9419 | 427610 | 13939 | 457537.5 | 13844 | +| 26 | 224368.5 | 9777 | 429425.5 | 14100 | 455197.5 | 14035 | +| 27 | 228489 | 10138 | 424397 | 14294 | 450972.5 | 14194 | +| 28 | 236835 | 10496 | 424571.5 | 14450 | 447514.5 | 14392 | +| 29 | 245775 | 10862 | 425305 | 14656 | 446641.5 | 14564 | +| 30 | 253632.5 | 11220 | 423314.5 | 14820 | 451461 | 14691 | +| 31 | 259848.5 | 11580 | 423943.5 | 14949 | 457258 | 14854 | +| 32 | 274026.5 | 11938 | 420486.5 | 15110 | 441753 | 15047 | +| 33 | 285271 | 12304 | 422267 | 15311 | 444442 | 15253 | +| 34 | 292640 | 12662 | 421987.5 | 15509 | 443743.5 | 15347 | +| 35 | 299938 | 13020 | 423594 | 15603 | 438051.5 | 15512 | +| 36 | 305735 | 13378 | 420346 | 15768 | 444546 | 15713 | +| 37 | 314605 | 13736 | 421428.5 | 15969 | 432429.5 | 15906 | +| 38 | 319773 | 14094 | 418774.5 | 16162 | 433197 | 16070 | +| 39 | 328503.5 | 14452 | 415003.5 | 16326 | 435711.5 | 16230 | +| 40 | 338595.5 | 14810 | 414968.5 | 16486 | 424152.5 | 16428 | +| 41 | 349316.5 | 15176 | 424536.5 | 16692 | 423559 | 16601 | +| 42 | 356126.5 | 15534 | 420870 | 16857 | 428098.5 | 16769 | +| 43 | 361120 | 15892 | 419857 | 17025 | 419647.5 | 16963 | +| 44 | 371736 | 16250 | 430333.5 | 17219 | 418181.5 | 17122 | +| 45 | 383888.5 | 16618 | 424117.5 | 17388 | 421533.5 | 17297 | +| 46 | 394085.5 | 16976 | 414270 | 17553 | 416644.5 | 17456 | +| 47 | 402292 | 17343 | 419159 | 17721 | 407635 | 17445 | +| 48 | 408613 | 17701 | 409451 | 17701 | — | — | + +Recorded-proof maxima are 17,701 bytes for pure `p = 48`, 17,721 for no-native `b = 47` and 17,456 for native `b = 46`. The mixed maxima exceed their corresponding all-balance endpoints by 20 and 11 bytes, so exact recorded-proof endpoint dominance fails. These tiny differences do not imply a charged-Weight violation: recorded proof and the much larger modeled trie allowance remain distinct. The new pure maximum-account generated proof at 48 is 137,486 bytes; it also differs materially from the earlier short-account/native-containing pure fit, whose shape cannot stand in for this newly qualified domain. + +Timing has interior variation and visible tails. The largest per-component medians are 408,613 ns at pure `p = 48`, 435,851 ns at no-native `b = 12`, and 460,715 ns at native `b = 24`; their nearest-rank p95 values are 438,611/470,249/502,377 ns. Maximum individual samples across each entire profile are 750,247 ns at `p = 38`, 1,066,563 ns at no-native `b = 23`, and 1,235,791 ns at native `b = 8`. Preserve this variability; one median or constant fit is not a worst-case latency or block-throughput certificate. The fixed-total RefTime fits have no positive allocation slope, while their estimated-proof fits conservatively retain an Observation-dominated intercept and asset slope. At the pure-balance endpoint those diagnostic proof fits reach 256,408/251,197 bytes, despite recorded proof being 17,701/17,445. Do not install them as a predicate-count model or interpret that fitted growth as additional physical reads. + +The component decision is narrower than production selection: reject exact endpoint dominance and qualify the all-non-native range plus both complete fixed-total allocation families. The remaining capture question is a sound scalar or typed owner composition across lower totals and Oracle refusal states. Source-based bounded justification should precede another sweep; any new measurement must name the missing owner or falsify a specific composition. No production Weight, admission policy or frozen C1 geometry changes here. Native `bench_opening_` tests pass (29 seconds), workspace/all-target/all-feature Clippy passes (23 seconds), formatting and script syntax/help/invalid-argument/colliding-path checks pass. Benchmark compilation passes (54 seconds), with the known G5 stale-storage rejection retained. + +The retained fixture also rejects a host whose effective capture bound is zero because of `MaxOpeningPredicateResults` or `MaxContractSteps`, rather than checking per-Step predicate capacity alone. Retained source is `bc01098321a815e2320257a5db5c5f7c932a06d5bbbf04936bf18f2979bb6713`, benchmark source `0081efdb3783740031d800fe1cbf12d7d3c27050197d44226b12061ef1139e7a`. This guard is outside the measured block; both old and corrected conditions are false for the measured reference host (capacity four, effective maximum 48) and native test host, with identical constructor arguments, setup state and capture call. The samples retain their original source/Wasm binding above; this scoped equivalence does not claim a newly built binary identity or refresh final production artifacts. + +### Oracle Read-State Coverage + +`Oracle::observation_state` validates positive age before reads, then reads Feed configuration and stops for missing/deactivated feeds. Otherwise it reads Observation and returns Uninitialized, Stale or Fresh. The Actor maps the first three non-fresh results to `Ok(false)` and additionally validates a Fresh timestamp/age before comparison. Zero maximum age is rejected by Contract validation; invalid Fresh timestamps are a generic-host error case already covered by Actor tests, not a normal reference Oracle publication state. This source partition identifies read owners but does not prove that absence witnesses in a populated trie are smaller than present-value witnesses. + +The extra `opening_observation_read_state(p = 1..48, s = 0..4)` uses typed benchmark-only states `Missing`, `Deactivated`, `Uninitialized`, `Stale`, `Fresh` in that order. FRAME varies one component while holding the other at maximum: the intended evidence is the complete fresh `p` range and all five states at `p = 48`, not a full state/count Cartesian product or an ordinal state-cost formula. It requires the provider to return the chosen state before measurement, distinct feeds, exactly `p` captured results, true for Fresh and false otherwise. The reference helper publishes maximum-width forward feeds through the existing owner for background state in every case. Missing queries distinct unregistered maximum-width keys against those populated Feed/Observation tries; Uninitialized queries the registered reverse feeds, whose configurations are maximum width while forward Observation values remain populated; Deactivated uses ordinary `deactivate_feed`; Stale advances only the benchmark's block-number input beyond age 100. No Oracle storage value is manually replaced or removed. The scalar mock validates result semantics but has no separate Oracle trie topology. + +Prepared source content/modes `09dece91d7fe6c78a54e5e8abb806a08cc08997bd2f19b51ad102e181bd600cc`, benchmark source `00d9c895856c0dd70046e76ebf34a031ef16bebfb409d9959ebeaf801cee07f8`, runtime helper source `7b3ce4cbd426fe4664bcc2931eb64afd1df37b306eac7f9e7e21f659056b7db6`. Run `./scripts/benchmarks.sh --extra --extrinsic opening_observation_read_state --output /tmp/deos-exp28-opening-observation-states.rs --json-file /tmp/deos-exp28-opening-observation-states.json pallet_deos_actors`, rebuilding the benchmark Wasm and retaining the same 50-step/20-minimum-repeat/ten-second-minimum-duration/map/cache/heap controls. Verify actual component coverage and state-specific DB/recorded proof from raw samples. A refusal point exceeding Fresh in recorded proof or unique reads rejects corresponding Fresh-only dominance; timing differences retain sample variability and do not become wall-clock capacity claims. A passing point is a reference populated-trie witness, not a generic host certificate or exhaustive trie-shape proof. State/count fit coefficients must remain diagnostic and uninstalled. + +The categorical run completed, and its generated DB model (`2 + p + 4s`) undercounts Fresh at `(48,4)`: 66 modeled reads versus 96 tracked. This rejects that additive categorical fit, not the benchmark's state assertions. A same-Wasm `opening_observation_capture` control is therefore required for a usable Fresh-only count model; run `./scripts/benchmarks.sh --skip-build --extra --extrinsic opening_observation_capture --output /tmp/deos-exp28-opening-observation-state-control.rs --json-file /tmp/deos-exp28-opening-observation-state-control.json pallet_deos_actors` with unchanged controls. The state profile remains raw branch evidence and cannot supply production coefficients. + +Both runs pass on benchmark Wasm `272e055ddc6bcdf6574246b8ead6fed5e99faa5fb0e3086f4a326b1f1cd7bd91`, with the prepared source unchanged. Build 100 seconds; state measurement 18 seconds and Fresh-only control 16 seconds, with no competing jobs. The state run contains 4,000 timing and 200 DB samples over exactly 52 distinct `(p,s)` combinations: every Fresh count plus four refusal states at 48. The control contains 5,000 timing and 250 DB samples over all `p = 1..48`. Minimum-duration repetition and integer component selection explain unequal sample counts; no samples were removed. + +| State at p = 48 | Timing / DB samples | Unique reads | Recorded proof B | Minimum ns | Median ns | p95 ns | Maximum ns | +| --- | --- | --- | --- | --- | --- | --- | --- | +| Missing | 520 / 26 | 48 | 3342 | 154142 | 160183.5 | 184943 | 440357 | +| Deactivated | 480 / 24 | 48 | 6243 | 220911 | 229152.5 | 255274 | 560136 | +| Uninitialized | 480 / 24 | 96 | 7818 | 340062 | 353297.5 | 383574 | 924713 | +| Stale | 480 / 24 | 96 | 9441 | 409067 | 424153 | 452509 | 1457330 | +| Fresh, state profile | 80 / 4 | 96 | 9441 | 409835 | 427610 | 448946 | 464172 | +| Fresh, independent control | 100 / 5 | 96 | 9441 | 409276 | 424013 | 453487 | 789987 | + +Every DB sample has zero writes, repeated reads and repeated writes. The two one-owner refusal branches have fewer unique reads and lower recorded proof than Fresh; Uninitialized retains the same two logical owners while its absent-value witness is smaller here. Stale and Fresh have identical recorded proof. These populated-trie witnesses do not falsify Fresh dominance in those two metrics at 48. Timing medians are consistent with the source partition, but unequal sample counts, p95 crossover and large tails forbid claiming a maximum-latency bound or a universal generic-host result from this comparison. + +| Model, uninstalled | RefTime before DB, ps | Estimated proof B | DB model | +| --- | --- | --- | --- | +| Categorical state/count fit, rejected | `14,248,000 + 5,116,419p + 24,576,040s`; slope errors `52,235` and `653,049`; minimum `13,619,000` | `3,916 + 2,634p + 19s`; slope errors `5` and `63`; recorded fit `192p + 1,623s` | `2 + p + 4s` reads | +| Independent Fresh count model | `5,671,671 + 8,742,925p`; slope error `7,372`; minimum `13,340,000` | `1,209 + 2,667p`; recorded fit `216 + 192p` | `2p` reads | + +State is categorical and changes the read-count multiplier. The additive fit cannot represent `p` reads for Missing/Deactivated and `2p` for the other states: at Fresh `(48,4)` it predicts only 66 reads. Its RefTime before DB is also `358,140,272` ps at that point, below the state's minimum observed `409,835,000` ps. This is a model-domain defect; do not repair its coefficients or treat an ordinal state selector as a production parameter. The independent count model retains 96 reads and estimated proof 129,225 bytes at 48. It is a Fresh component input, not acceptance of the whole capture envelope. + +| Artifact | State profile SHA-256 | Fresh control SHA-256 | +| --- | --- | --- | +| Generated fragment | `4312ea21539435e77bb904493e057a01c3044b72467e89170aca35d64dc05918` | `0d5ba2d39b837a6c0ce4dfb3c3751b6dcba829b42d9f61c3c421f6f70b3f8c3c` | +| Raw JSON | `2f058f8f929c1ce89b09bb515e1c22271ed688478d83922c28948a8069e68906` | `a2ed7abe553b0c876d5d5c3a6cdaaed046d3a4b736361aee8303056176e7b39b` | + +The complete Fresh range below preserves both timing medians and their common recorded proof; raw validation confirms `2p` unique reads at every point in both runs. The state profile has 40 timing samples per ordinary Fresh count, 80 at `p = 1,24,48`; the independent control has 100 per ordinary count, 200 at `p = 1,24`. Recorded proof is invariant within each point and identical between the two runs. All timings are nanoseconds before modeled DB charge. + +| Fresh p | State-profile median ns | Fresh-control median ns | Recorded proof B | +| --- | --- | --- | --- | +| 1 | 14388 | 14528 | 451 | +| 2 | 24305 | 24794.5 | 650 | +| 3 | 32931 | 33140.5 | 843 | +| 4 | 42464 | 42010 | 1042 | +| 5 | 50705.5 | 50496 | 1206 | +| 6 | 58179 | 57794.5 | 1396 | +| 7 | 65302.5 | 64884 | 1589 | +| 8 | 72286.5 | 72112.5 | 1749 | +| 9 | 83252 | 83636.5 | 1947 | +| 10 | 91528.5 | 92471 | 2141 | +| 11 | 99665 | 100782.5 | 2307 | +| 12 | 106963.5 | 108535 | 2464 | +| 13 | 114681 | 114961 | 2657 | +| 14 | 122433.5 | 122539 | 2921 | +| 15 | 130186.5 | 130954.5 | 3081 | +| 16 | 139056.5 | 139405.5 | 3246 | +| 17 | 150964.5 | 151173.5 | 3443 | +| 18 | 160882 | 160009 | 3674 | +| 19 | 167377.5 | 167412 | 3868 | +| 20 | 174710.5 | 173523 | 4028 | +| 21 | 186199.5 | 184593 | 4230 | +| 22 | 196257 | 194441 | 4397 | +| 23 | 208688.5 | 212251 | 4594 | +| 24 | 219235.5 | 219340 | 4790 | +| 25 | 226638 | 229990.5 | 4983 | +| 26 | 235403.5 | 237254.5 | 5174 | +| 27 | 243400.5 | 245600.5 | 5369 | +| 28 | 251467.5 | 256671 | 5564 | +| 29 | 261454.5 | 264598 | 5754 | +| 30 | 272350 | 272315.5 | 5985 | +| 31 | 283175.5 | 280172.5 | 6179 | +| 32 | 286598 | 284852.5 | 6372 | +| 33 | 295957 | 292395 | 6603 | +| 34 | 300985.5 | 304477.5 | 6800 | +| 35 | 318411 | 311951 | 6993 | +| 36 | 318760.5 | 324278.5 | 7193 | +| 37 | 328957.5 | 330285 | 7384 | +| 38 | 335208.5 | 339678.5 | 7574 | +| 39 | 342716 | 348967.5 | 7738 | +| 40 | 355008.5 | 356894.5 | 7934 | +| 41 | 358046.5 | 362098 | 8091 | +| 42 | 367091.5 | 366742.5 | 8281 | +| 43 | 377533.5 | 374634.5 | 8471 | +| 44 | 381304 | 383399.5 | 8661 | +| 45 | 399393.5 | 399952 | 8859 | +| 46 | 416923.5 | 402222 | 9052 | +| 47 | 419648 | 412419.5 | 9250 | +| 48 | 427610 | 424013 | 9441 | + +The scoped outcome qualifies reference Oracle read-state topology and maximum-count refusal witnesses while rejecting the categorical model. Complete lower-total/state/asset composition remains open; new evidence must address a named bound, not repeat these branch points. Paused feeds share the same readable branch as Active, as the Oracle lifecycle test verifies. Native read-state benchmark tests pass (30 seconds), the full Oracle all-feature test suite passes (21 seconds), workspace/all-target/all-feature Clippy passes (26 seconds), and formatting passes. Benchmark preflight compilation passes (57 seconds), retaining the known G5 stale-storage rejection. The public enum/helper is benchmark-only with an explicit unsupported default; production contracts, weights and feature behavior are unchanged. + +### Full-Contract Opening Predicate Capture + +Source inspection exposes a distinct omitted domain in the earlier packed capture fixtures: `capture_opening_predicate_results` visits every Contract Step, clause and timed Predicate, including Current entries when there are zero Opening results. The candidate count bound must own that traversal as well as host evaluation. A nonzero-only capture selector cannot claim coverage of the zero-result traversal unless another measured owner explicitly includes it. This is phase-accounting work within frozen C1, not a new Actor execution mechanism. + +The prepared fixtures hold all reference 12 Steps, four clauses per Step and 48 timed predicates constant. Exactly `p` entries are Opening and the rest are skipped Current block predicates. The corrected `opening_predicate_capture(p = 0..48)` uses maximum non-native accounts plus one maximum fresh Observation for `p >= 2`; `opening_max_encoded_balance_capture(p = 0..48)` uses only maximum non-native accounts. `opening_observation_heavy_capture(o = 1..47)` uses one fixed maximum non-native account and `o` maximum fresh Observations. This preserves a nonnegative Oracle-count axis and measures totals 2..48 without native namespace substitutions. Constructors and host preparation remain outside the measured capture. Maximum balanced clause count is derived from each host's admitted predicate/Step limits; result count and values are verified after capture. + +Prepared source content/modes `5e071b03fed9836df458737c07454c62e866b1571fca7cd95c3a58d4f8d9600e`, benchmark source `9bb683bfe38a5d992d6c843ff2116b819b907ac54b140a541320bd886c0b2186`, runtime helper `4a776e3c347b1a2431283aaea537908840d812ef53fe310812c0edc6b5d041a3`. Run `scripts/benchmarks.sh --extrinsic opening_predicate_capture --output /tmp/deos-exp28-opening-full-mixed.rs --json-file /tmp/deos-exp28-opening-full-mixed.json pallet_deos_actors`; then reuse the same Wasm with `--skip-build --extra` for the pure and observation-heavy profiles and `/tmp/deos-exp28-opening-full-{asset,observation}.{rs,json}`. Keep 50 × 20/map/cache/heap controls, raw per-count samples and no competing build/measurement. Compare fitted reads against tracked reads, the nonzero traversal intercept against zero-result samples, and resource dimensions against the prior 97 fixed-total allocations. A model that undercounts tracked owners or omits the traversal baseline is rejected. Endpoint fits alone do not certify every lower-total allocation, predicate placement, result state or whole-Actor admission. + +The initial full-Contract triple passes on benchmark Wasm `856fff8d75f52a148eba21ab7047fa8c631edd95fb95537e6ffa585520d286d0` (101-second build; 14/15/16-second runs). Mixed/pure cover all 49 totals 0..48 with 5,000/6,000 timing and 250/300 DB samples; Observation-heavy covers all 47 Oracle counts with 5,000 timing/250 DB samples. Every raw DB point has exactly twice the total predicate count in unique reads and zero writes. At zero, mixed/pure record zero reads and proof but median traversal times 1,816/1,816 ns; generated intercepts conservatively retain proof despite that zero-read case. + +| Full-Contract input | RefTime before DB, ps | Estimated proof bytes | Reads | Generated / raw SHA-256 | +| --- | --- | --- | --- | --- | +| Mixed `p = 0..48` | `7,888,282 + 8,651,681p` | `4,184 + 2,673p` | `2p` | `786d086e5091730515cd63d1d7bf59aca6b1459635a2d83f838e207d5f505d0d` / `dee3c445b4db5a9415ce3573b25c16e9ccf53a49174ba213f2570a8f281e88cc` | +| Pure `p = 0..48` | `5,971,372 + 8,300,144p` | `1,358 + 2,836p` | `2p` | `9494a9a0031bfff71218b7758f2a5d8ca509d579aa0d353a637dee1327cc9a5c` / `396c85f8944d7f68754e654d770c8e191cb350268fdd04de87fce416c3469a63` | +| Observation-heavy `o = 1..47` | `20,088,359 + 8,793,864o` | `4,329 + 2,667o` | `2 + 2o` | `ecd8f0ad46ca018fe3c5e710fc4be4859e3faed0ddcb58a630fc9c60166b646e` / `58bbc433e8d36c870d47a6069839abbe0182e3154e3d3ebb1048fd04fbab2b87` | + +Each cell reports time/DB sample counts, followed by upper median/p95/max nanoseconds and recorded proof bytes. All samples are retained; p95 uses nearest rank. + +| Total | Mixed | Pure assets | Observation-heavy | +| --- | --- | --- | --- | +| 0 | 200/10: 1816/1956/15016; 0 | 240/12: 1816/1956/2933; 0 | — | +| 1 | 100/5: 17601/21023/56992; 719 | 120/6: 17530/21512/35759; 719 | — | +| 2 | 100/5: 30033/37505/61042; 1097 | 120/6: 25144/28566/41556; 1079 | 200/10: 29823/34292/57619; 1097 | +| 3 | 100/5: 39950/58737/153583; 1457 | 120/6: 34782/45607/57061; 1445 | 100/5: 39671/51753/56154; 1296 | +| 4 | 100/5: 48191/65652/116567; 1823 | 120/6: 43302/49029/61321; 1812 | 100/5: 46655/56642/70401; 1489 | +| 5 | 100/5: 56293/68725/90097; 2190 | 120/6: 50566/65861/139964; 2172 | 100/5: 55804/62090/74312; 1688 | +| 6 | 100/5: 63416/83112/91772; 2550 | 120/6: 57690/73963/88979; 2532 | 100/5: 63906/77734/85068; 1852 | +| 7 | 100/5: 70471/92123/167273; 2910 | 120/6: 65792/76687/188574; 2898 | 100/5: 71449/86605/169368; 2042 | +| 8 | 100/5: 79061/95894/179914; 3277 | 120/6: 73334/85767/96033; 3258 | 100/5: 82902/95335/114751; 2235 | +| 9 | 100/5: 86535/107208/120338; 3637 | 120/6: 80319/92611/97849; 3619 | 100/5: 89608/101202/112865; 2395 | +| 10 | 100/5: 98059/117335/212600; 3997 | 120/6: 87512/100223/122015; 3977 | 100/5: 97849/115869/144015; 2594 | +| 11 | 100/5: 104484/118173/256182; 4355 | 120/6: 94636/108186/217769; 4337 | 100/5: 105811/122224/384483; 2788 | +| 12 | 100/5: 110770/128859/259046; 4715 | 120/6: 104904/118732/242283; 4695 | 100/5: 114123/130955/137660; 2953 | +| 13 | 100/5: 119570/137450/263376; 5073 | 120/6: 113005/126414/140732; 5061 | 100/5: 122015/138149/173279; 3110 | +| 14 | 100/5: 127323/147088/293478; 5439 | 120/6: 121386/135425/149742; 5427 | 100/5: 130186/150580/259464; 3303 | +| 15 | 100/5: 135983/152746/200447; 5805 | 120/6: 129069/151698/302138; 5787 | 100/5: 138497/213787/236975; 3567 | +| 16 | 100/5: 143387/161266/209807; 6165 | 120/6: 137310/159380/313592; 6153 | 100/5: 144574/161266/175794; 3727 | +| 17 | 100/5: 156028/174466/354380; 6531 | 120/6: 147856/171812/323021; 6513 | 200/10: 156167/170765/185921; 3892 | +| 18 | 100/5: 163082/191369/391048; 6891 | 120/6: 155539/171253/185641; 6871 | 100/5: 166225/185571/199330; 4089 | +| 19 | 100/5: 169368/186060/213438; 7249 | 120/6: 162383/175025/193883; 7229 | 100/5: 175514/198073/241026; 4320 | +| 20 | 100/5: 177539/198283/220632; 7607 | 120/6: 169856/183476/211064; 7589 | 100/5: 180752/198981/225661; 4514 | +| 21 | 100/5: 184593/205965/228175; 7967 | 120/6: 177609/192206/216023; 7955 | 100/5: 188644/204079/219025; 4674 | +| 22 | 100/5: 192765/212181/225451; 8333 | 120/6: 185501/201495/299484; 8319 | 100/5: 216093/234740/260862; 4876 | +| 23 | 100/5: 201984/231458/242493; 8697 | 120/6: 194231/214626/226569; 8685 | 100/5: 217839/242912/250874; 5043 | +| 24 | 100/5: 210017/231597/237743; 9063 | 120/6: 203731/228594/521233; 9051 | 100/5: 225172/251363/299624; 5240 | +| 25 | 100/5: 218258/243541/253808; 9429 | 120/6: 213299/239071/244798; 9419 | 100/5: 235439/270569/729224; 5436 | +| 26 | 100/5: 229991/254296/267915; 9797 | 120/6: 221400/249058/500212; 9777 | 100/5: 245426/276785/430369; 5629 | +| 27 | 100/5: 239070/265750/463894; 10155 | 120/6: 228594/250036/272315; 10138 | 100/5: 255763/389791/464661; 5820 | +| 28 | 100/5: 247242/281604/473462; 10516 | 120/6: 235719/254715/486871; 10496 | 100/5: 261420/295643/374984; 6015 | +| 29 | 100/5: 254506/279230/291034; 10874 | 120/6: 243541/262398/323510; 10862 | 100/5: 266728/291732/308703; 6210 | +| 30 | 100/5: 273224/312963/420870; 11240 | 120/6: 249617/272036/278531; 11220 | 100/5: 276227/300252/336152; 6400 | +| 31 | 100/5: 276296/311008/341878; 11598 | 120/6: 258836/280766/300462; 11580 | 100/5: 291312/317713/342018; 6631 | +| 32 | 100/5: 286703/314151/323440; 11958 | 120/6: 272804/295782/376380; 11938 | 200/10: 295852/328608/430439; 6825 | +| 33 | 100/5: 292500/316805/365345; 12316 | 120/6: 281325/306119/326723; 12304 | 100/5: 301719/325326/371422; 7018 | +| 34 | 100/5: 303326/340482/458375; 12682 | 120/6: 288868/311916/368139; 12662 | 100/5: 314082/336919/587305; 7249 | +| 35 | 100/5: 314290/349701/857944; 13040 | 120/6: 296900/320647/340830; 13020 | 100/5: 322463/344113/1436169; 7446 | +| 36 | 100/5: 319668/349212/398730; 13398 | 120/6: 302906/329237/420381; 13378 | 100/5: 327422/349212/405505; 7639 | +| 37 | 100/5: 326793/358222/375892; 13756 | 120/6: 312125/336501/719307; 13736 | 100/5: 338596/357802/392654; 7839 | +| 38 | 100/5: 335593/367302/389232; 14114 | 120/6: 320926/355078/403061; 14094 | 100/5: 346837/371840/448528; 8030 | +| 39 | 100/5: 342507/368279/434979; 14472 | 120/6: 328468/351098/705408; 14452 | 100/5: 354171/382108/552732; 8220 | +| 40 | 100/5: 351587/378545/731180; 14830 | 120/6: 334126/366533/388883; 14810 | 100/5: 360527/377638/446013; 8384 | +| 41 | 100/5: 362342/398381/436794; 15188 | 120/6: 346698/371562/512364; 15176 | 100/5: 370165/392724/434769; 8580 | +| 42 | 100/5: 368209/396076/426737; 15554 | 120/6: 352425/384272/450623; 15534 | 100/5: 378755/439588/472623; 8737 | +| 43 | 100/5: 376799/404527/455512; 15912 | 120/6: 358851/383505/444128; 15892 | 100/5: 386438/424712/450274; 8927 | +| 44 | 100/5: 388254/449366/865906; 16270 | 120/6: 363529/392374/758279; 16250 | 100/5: 395797/442661/464941; 9117 | +| 45 | 100/5: 401804/496580/955863; 16628 | 120/6: 382876/408368/426877; 16618 | 100/5: 405505/442381/542885; 9307 | +| 46 | 100/5: 414305/462985/634169; 16996 | 120/6: 390280/412070/429252; 16976 | 100/5: 415841/446851/480656; 9505 | +| 47 | 100/5: 419893/465709/916402; 17354 | 120/6: 400825/431347/467176; 17343 | 100/5: 424153/448388/478281; 9698 | +| 48 | 100/5: 429112/493367/891329; 17721 | 120/6: 409486/431486/447900; 17701 | 100/5: 430788/456699/488268; 9896 | + +At total 48 their proposed component-wise maximum is `2,833,399,967 ps / 137,486 bytes`, including reference DB charge. It covers the earlier fixed-total generated time models after their own DB charges, but the largest prior allocation medians plus actual DB charge exceed it by 3,115,033 ps without Native and 3,258,033 ps with Native. These are different evidence classes and binaries, with within-run timing variance; neither a fit comparison nor that small median difference alone establishes complete coverage. The next falsifier enumerates the full total/allocation domain instead of installing an endpoint-only bound. + +`opening_predicate_composition(c)` enumerates `(M + 1)^2` cases for maximum total `M`: total `p` occupies indices `p²..(p+1)²-1`, first all `p+1` non-native/Oracle allocations without Native, then all `p` allocations with one Native. At reference `M = 48` this is 2,401 distinct cases, including zero. Every case uses the same full Contract/DNF traversal and maximum host encoding. Prepared source content/modes `7418e48c9e7d29bad8f9343ef61340734b529cde57d24bc80c573134abdf8481`, benchmark source `510f53268a5a1863b602d97fe302275b8b95549217f498d8ba6793c472dae379`. Run `scripts/benchmarks.sh --extra --steps 2401 --extrinsic opening_predicate_composition --output /tmp/deos-exp28-opening-composition.rs --json-file /tmp/deos-exp28-opening-composition.json pallet_deos_actors`; 2,401 steps deliberately replaces the ordinary 50-step sampling to cover every categorical index, retaining 20 repeats and other controls. Verify the exact index set, total/allocation bijection, read owners, proof and per-total timing distribution from raw output. Its categorical regression is diagnostic only and must never be installed as a count Weight. The capture implementation, three measured profile bodies and their concrete fixtures are unchanged; only setup gains the optional Native argument and the extra enumeration caller. Reuse their applicable prior evidence without claiming an isolated same-binary timing comparison. + +The enumeration passes on Wasm `e488babbca892918ec34e7402a4f6f05166daffeafd123496ed7b5c4fc86711c` (101-second build, 236-second run). Raw output contains exactly indices 0..2400, 2,401 distinct `(total, non-native balances, Observations, Native presence)` tuples, 1,176 Native cases, 48,020 timing samples and 2,401 DB samples. Every case has 20 timing samples and one DB sample. Unique reads are exactly `2p - native`, repeated reads exactly `native`, and writes zero. All captured results pass. Generated diagnostic SHA-256 `4e6d84bf98afdc85530ec887ce3328048c54fcde7cb844d89ec7ff2d0708e278`; raw JSON `16ed3cefb566f20d520ab58212884d758f5b741ddaa996c55cc0dda231f30215`. + +The following per-total reduction covers all categorical rows. Each total contains every one of its `2p + 1` allocations, each sampled 20 times; retained case IDs reproduce the decisive timing/proof witnesses through the documented bijection. DB-adjusted medians are diagnostic arithmetic, not fitted Weight or wall-clock duration. + +| Total | Cases / medians above proposed RefTime | Peak DB-adjusted median: case / raw ns | All-sample p95 ns | Largest recorded proof: case / bytes | +| --- | --- | --- | --- | --- | +| 0 | 1 / 0 | 0 / 1816 | 1956 | 0 / 0 | +| 1 | 3 / 1 | 2 / 17321 | 20953 | 2 / 719 | +| 2 | 5 / 1 | 5 / 29543 | 29892 | 5 / 1097 | +| 3 | 7 / 2 | 10 / 39042 | 40648 | 11 / 1457 | +| 4 | 9 / 2 | 18 / 47354 | 55316 | 19 / 1823 | +| 5 | 11 / 3 | 29 / 55874 | 64255 | 29 / 2190 | +| 6 | 13 / 5 | 37 / 67189 | 78642 | 41 / 2550 | +| 7 | 15 / 0 | 50 / 72496 | 86046 | 55 / 2910 | +| 8 | 17 / 4 | 67 / 84090 | 92960 | 71 / 3277 | +| 9 | 19 / 6 | 82 / 95405 | 110491 | 89 / 3637 | +| 10 | 21 / 5 | 108 / 117614 | 118802 | 109 / 3997 | +| 11 | 23 / 3 | 130 / 108395 | 123551 | 131 / 4355 | +| 12 | 25 / 3 | 151 / 117894 | 130885 | 155 / 4715 | +| 13 | 27 / 1 | 175 / 126415 | 141012 | 181 / 5073 | +| 14 | 29 / 0 | 205 / 132910 | 143456 | 209 / 5439 | +| 15 | 31 / 0 | 235 / 140942 | 151697 | 239 / 5805 | +| 16 | 33 / 0 | 260 / 147507 | 157914 | 271 / 6165 | +| 17 | 35 / 0 | 292 / 157913 | 166854 | 305 / 6531 | +| 18 | 37 / 0 | 331 / 165597 | 177679 | 341 / 6891 | +| 19 | 39 / 0 | 365 / 174117 | 187387 | 379 / 7249 | +| 20 | 41 / 1 | 403 / 189413 | 203450 | 419 / 7607 | +| 21 | 43 / 3 | 445 / 199819 | 210784 | 461 / 7967 | +| 22 | 45 / 0 | 485 / 203451 | 220423 | 505 / 8333 | +| 23 | 47 / 6 | 531 / 225172 | 237045 | 551 / 8697 | +| 24 | 49 / 3 | 578 / 229223 | 235997 | 599 / 9063 | +| 25 | 51 / 11 | 628 / 248988 | 261420 | 649 / 9429 | +| 26 | 53 / 15 | 683 / 253178 | 264214 | 701 / 9797 | +| 27 | 55 / 12 | 733 / 262468 | 268614 | 755 / 10155 | +| 28 | 57 / 7 | 788 / 267008 | 278043 | 811 / 10516 | +| 29 | 59 / 11 | 844 / 276715 | 290125 | 869 / 10874 | +| 30 | 61 / 6 | 916 / 279021 | 297389 | 929 / 11240 | +| 31 | 63 / 18 | 965 / 299973 | 323859 | 991 / 11598 | +| 32 | 65 / 5 | 1033 / 294315 | 305490 | 1055 / 11958 | +| 33 | 67 / 0 | 1093 / 300183 | 314920 | 1121 / 12316 | +| 34 | 69 / 11 | 1158 / 320088 | 343485 | 1189 / 12682 | +| 35 | 71 / 2 | 1234 / 333148 | 349002 | 1259 / 13040 | +| 36 | 73 / 9 | 1300 / 352075 | 352006 | 1331 / 13398 | +| 37 | 75 / 4 | 1380 / 342716 | 346977 | 1405 / 13756 | +| 38 | 77 / 8 | 1448 / 356894 | 370095 | 1481 / 14114 | +| 39 | 79 / 18 | 1534 / 366672 | 377009 | 1559 / 14472 | +| 40 | 81 / 19 | 1611 / 386508 | 400756 | 1639 / 14830 | +| 41 | 83 / 11 | 1706 / 380571 | 393562 | 1721 / 15188 | +| 42 | 85 / 11 | 1787 / 391745 | 420521 | 1805 / 15554 | +| 43 | 87 / 25 | 1885 / 460820 | 437562 | 1891 / 15912 | +| 44 | 89 / 40 | 1970 / 492249 | 468992 | 1979 / 16270 | +| 45 | 91 / 26 | 2059 / 448947 | 479887 | 2069 / 16628 | +| 46 | 93 / 41 | 2164 / 575502 | 486522 | 2161 / 16996 | +| 47 | 95 / 56 | 2213 / 476465 | 497697 | 2255 / 17354 | +| 48 | 97 / 76 | 2309 / 495252 | 512364 | 2351 / 17721 | + +All recorded proof lies below the proposed generated proof envelope; the largest ratio is case 305 at total 17, `6,531 / 49,625 bytes`. This comparison does not equate recorded proof with conservative proof estimation. Conversely, 491 case medians plus actual DB charge exceed the proposed RefTime fit. The largest excess is case 2164: total 46, one Native, one non-native balance and 44 Observations, median 575,502 ns and excess 134,689,761 ps (4.9595%). Its samples range 415,842..750,875 ns and are visibly split between ordinary and delayed observations. No sample is removed and no new storage owner is inferred from time variation. + +A targeted same-Wasm control tests cases 2163/2164 through `bin/frame-omni-bencher v1 benchmark pallet --runtime template/target/benchmarks/release/wbuild/deos-runtime/deos_runtime.compact.compressed.wasm --pallet pallet_deos_actors --extrinsic opening_predicate_composition --extra --low 2163 --high 2164 --steps 2 --repeat 20 --heap-pages 4096 --output /tmp/deos-exp28-opening-composition-control.rs --json-file /tmp/deos-exp28-opening-composition-control.json`. Each case has 1,160 timing/58 DB samples. Upper medians are 407,321/413,816 ns, p95 442,591/451,531 ns, minima 388,603/393,842 ns and maxima 1,190,044/929,602 ns; both have 91 unique reads, one repeated read, zero writes and recorded proof 8,961/9,409 bytes. Generated/raw SHA-256 are `8c2c8a010582a0d2ca717beceffa0a9f0cf5cf5af04cfdcf2f704ab4b8086da4` / `3dfe332cacb7e58f3568169a6c7c1037e84a8470923916db445869dcd01e2ee0`. The previously worst median is not stable at that magnitude; this control does not erase long-run variation or certify all timing bounds. Lower-total Fresh allocation/read topology is now enumerated; universal RefTime acceptance remains open. + + + +### Full-Contract Predicate Model Binding + +Consume the three full-Contract generated artifacts above without resampling unchanged profiles. The 2,401-case diagnostic established every Fresh allocation's read topology; the retained same-Wasm control reduced the largest timing anomaly without discarding it. Oracle-state witnesses and source partition retain their stated populated-trie scope. Neither literal containment of every timing sample nor categorical regression is an additional consensus requirement. These observations support staged count-model selection, while complete runtime admission remains EXP-0064's separate obligation. + +Replace the old Native-first 1..32 `opening_predicate_capture` fragment with its unchanged full-Contract 0..48 mixed model. Promote the measured pure maximum-account and one-account/Oracle-heavy profiles to regular production generation and required Weight methods, preserving their setup and measured capture bodies. Generic fallbacks delegate to the existing unmeasured estimate, with `observations + 1` total predicates for the Oracle-heavy method. No generated coefficient or annotation is repaired manually. + +`opening_predicate_models` supplies mixed and pure inputs at the nonzero result count and the Oracle-heavy input at `count - 1` only for counts of at least two. `opening_predicate_weight` takes their component-wise maximum; zero selects only the independent traversal. Composed maximum and actual share this mapping. Direct inclusive owners receive no new additive capture charge. V10 commits every applicable input and selected result, including both Opening zero alternatives and the unchanged Current and amount selectors. + +| Captured predicates | DB-inclusive RefTime, ps | Estimated proof bytes | Selecting owners: time / proof | +| --- | --- | --- | --- | +| 1 | 66,539,963 | 6,857 | Mixed / mixed | +| 2 | 128,882,223 | 9,530 | Oracle-heavy / mixed | +| 17 | 1,010,790,183 | 49,625 | Oracle-heavy / mixed | +| 18 | 1,069,584,047 | 52,406 | Oracle-heavy / pure | +| 48 | 2,833,399,967 | 137,486 | Oracle-heavy / pure | + +All selected count models account for `2p` unique reads and zero writes. Native-presence diagnostics require `2p - 1` unique reads and one repeated read; their timing and trie witnesses remain diagnostics rather than a new DB owner. The mixed proof model owns counts 1..17 and pure owns 18..48. The observation-heavy time model owns 2..48. Earlier sampled timing residuals and recorded-versus-estimated proof distinctions remain in force; the scalar maximum is a generated Weight input, not a hardware maximum-latency certificate. + +The existing replacement/containment regression now covers all 48 predicate counts as well as all 24 amount counts. Initial default runtime validation passed 490 tests and exposed three tests with stale complete-path numeric expectations. The two-predicate Opening change is exactly `-14,862,756 ps / +245 bytes`; four-predicate Temporary Opening changes by `-14,046,780 ps / +541 bytes`. Update only these affected values; Running/Retry and inclusive CompleteMax expectations remain unchanged. This checks the integration consequences without hiding the changed charged proof behind a fee or admission policy adjustment. + +After those four numeric corrections, the complete Actors runtime integration family passes. All-feature native `bench_opening_` and generic Weight-interface parity pass, as do 27 independent embedding tests with DEX/TryRuntime and workspace/all-target/all-feature Clippy. Canonical formatting, shell syntax, generated-method byte comparison, record normalization/DAG and changed-scope completion pass. These results refresh the binding and public trait closure, not production Wasm or G5's full generated file. + +Retained source content/modes `9dcb5732530cd3ac47dc09bcac23c295fd40d0c174047f0e1cc49127271dce71`, benchmark source `656def3ed440db18adcae3b63264d7458d48dec442237741be53fc5ea552c8c9`, provider `0858133d39eaabb1d2591def2454f1b72d7ee6746b3c1f5ecd720f49a37750f9`, staged Weight file `2d9c60ac7764d1e7dc366531dd4a409444daf66cf3e37ed7feb922bb614e8f1a`. The three generated artifact hashes match their recorded full-Contract measurement identities; every staged method and annotation is byte-identical. Promotion changes registration only, preserving the measured setup and capture body. No new benchmark or production Wasm identity is claimed. + + + +### Opening Capture Component Ranges + +EXP-0029's independent close boundaries motivate qualifying the existing capture components over the current reference cardinalities. The bound file still models `opening_snapshot_capture` over `1..16` and `opening_predicate_capture` over `1..32`, while the current benchmark constructors expose `1..24` and `1..48`. Measure both existing profiles at 50 steps × 20 repeats on the unchanged independent-partition benchmark Wasm. The decision is whether these component inputs and storage models cover those full current ranges; no partial fragment will be installed as a complete production envelope. + +These are isolated function measurements: funded distinct assets, authored two-leg/one-leg Opening amount positions or packed Opening BalanceAbove occurrences, setup outside the measured block, and exact nonzero snapshot/true-result postconditions. They do not create Actors or certify full lifecycle admission, User fees, dependency-fragment loading or complete Opening. The amount profile exercises System `PreservableAsset`; `TargetAsset`, `StakingShares` and other Predicate/Observation adapter branches require their own coverage or dominance proof before a generic maximum is accepted. + +Both initial profiles pass on source content/modes `1f53c465c7db0844e73f2905b607b0287859cc6c2ea692c671be078f1fd4cf15`, benchmark source `171e68cf5e186b51d79cbedf709f937d2100181c3757163d41b331d6fa93b0e2`, and benchmark Wasm `068c40b5263e66ed0669fe12fee2569d57f549d200064fbf69fc20be229f50f3`. The prior successful compilation and known stale-storage preflight apply unchanged. Commands use `scripts/benchmarks.sh --skip-build --extrinsic PROFILE --output /tmp/deos-exp28-opening-KIND-current.rs pallet_deos_actors`, with PROFILE=`opening_snapshot_capture`/`opening_predicate_capture`, KIND=`snapshot`/`predicate`. Both use 50 × 20, compiled Wasm, 4096 heap pages, one-million worst-case map size and 1024 cache; no competing measurement/build process. + +| Component / full sampled range | RefTime before DB, ps | Proof bytes | Database model | +| --- | --- | --- | --- | +| BalanceAbove capture, `p = 1..48` | `2,712,084 + 8,736,810p`; slope error `9,753`; minimum `6,216,000` | Estimated `4,235 + 2,613p`; recorded `156 + 310p` | `1 + 2p` reads, zero writes | + +Snapshot fragment SHA-256 `061c4a1c51400f4ec153dcabcaad70387143eccce45c57ee4c99ef5feb538e14`; predicate fragment `fa5b4638d560c1e0ee9cdd9d831ec3b07299da1ccc193cfa3529b247d8868664`. The storage trace contains one native System account and `n−1` Asset/Account pairs at maximum; generated read models retain their conservative intercept. These fragments remain uninstalled. Their larger estimated proof slopes (`2,437` versus `2,260`, `2,613` versus `2,525`) do not support a cheaper maximum admission claim; old ranges/configuration and binary differ, so this is range qualification, not an isolated regression or optimization comparison. + +The follow-on predicate comparison uses an extra diagnostic `opening_observation_capture`, sharing the same packed Opening constructor with BalanceAbove, but reading distinct host-published feeds with fresh positive observations. Measure the complete `1..48` range on one rebuilt Wasm alongside the balance profile, retaining 50 × 20 and measured proof. A higher Observation cost in either resource dimension rejects BalanceAbove-only coverage of the generic capture owner; even a lower cost proves only this configured feed shape, not maximum Oracle configuration or unavailable/stale/error branches. Setup and exact `Ok(true)`/count assertions stay outside measured capture. + +Both paired profiles pass on source content/modes `6847b15a6225cccc37f6e8b11c19006ce9a73f99f2dceac6efd57e486596e6f5`, benchmark source `e0ecee744fe7e32061e42b09be7d5fcc27f59f9baae50925ce0c4e68a6cafc28`, and benchmark Wasm `eaf6012430160ba32a5cd8156e26de6519ecacefc48917541da07fa0b9542971`. Commands: `scripts/benchmarks.sh --extra --extrinsic opening_observation_capture --output /tmp/deos-exp28-opening-observation.rs pallet_deos_actors`, then `scripts/benchmarks.sh --skip-build --extrinsic opening_predicate_capture --output /tmp/deos-exp28-opening-balance-paired.rs pallet_deos_actors`. Build takes 110 seconds; measurements take 17/16 seconds without competing builds/tests. Same host, 50 × 20, cache, map and heap controls as above. + + +## Derived Metrics + +Derived values are retained beside the evidence that defines their source and units. No subtraction of overlapping aggregate owners or cross-artifact timing comparison is introduced. + +## Statistical / Variance Notes + +Retain each item’s original sampling and uncertainty. Singleton construction probes establish only their admitted state/access witness, never statistical timing dominance or a production bound. + +## Result + +The retained V10 evidence closes the reference Opening predicate capture component. The 2,401-case reference-Wasm enumeration covers every total `0..48`, every non-native/Fresh-Oracle allocation and both Native-presence classes through the same full 12-Step, four-clause traversal. Tracked reads are exactly `2p - native`, with one repeated Native read and zero writes. The separate typed Oracle-state run and source partition show that Missing/Deactivated stop after Feed while Uninitialized/Stale/Fresh use at most Feed plus Observation; none adds another physical owner or exceeds Fresh recorded proof at the qualified maximum. + +For nonzero counts, the component-wise selector preserves the independently larger RefTime and ProofSize owners from the three generated inputs. At count 48 it selects `2,833,399,967` RefTime and `137,486` ProofSize. The runtime regression covers every count `1..48`, verifies maximum/actual containment, and proves that nonzero capture replaces rather than adds the independent zero-result traversal. Direct inclusive Opening owners remain unchanged. + +Decision-time identity checks reproduce the retained staged Weight/provider SHA-256 values `2d9c60ac7764d1e7dc366531dd4a409444daf66cf3e37ed7feb922bb614e8f1a` / `0858133d39eaabb1d2591def2454f1b72d7ee6746b3c1f5ecd720f49a37750f9`. The focused default-runtime `composed_opening_capture_models_replace_traversal_without_double_charge` test, 61-record normalization/DAG validation, diff checks and changed-scope completion gate pass on the retained tree. + +## Interpretation + +The sound owner is a component-wise generated envelope, not an endpoint fit, categorical allocation regression, recorded-proof maximum or hardware-latency maximum. The exhaustive Fresh allocation enumeration certifies the reference count/allocation topology; source-qualified refusal evidence certifies that reference Oracle branches add no owner beyond the selected Fresh models. This accepts the capture component and selector for the reference binding while leaving whole-transition admission and complete generated-file identity to their own owners. + +## Decision + +Accepted for the reference binding. For each nonzero captured result count `p`, select the component-wise maximum of `opening_predicate_capture(p)`, `opening_max_encoded_balance_capture(p)` and, only for `p >= 2`, `opening_observation_heavy_capture(p - 1)`. Zero selects only `opening_predicate_traversal`. Use the same mapping in maximum and actual Opening Control Weight, and do not add it to inclusive direct owners. EXP-0065 owns complete regeneration and exact final artifact identity; no coefficient may be manually combined or repaired. + +## Rejected Alternatives and Tradeoffs + +Retain all rejected candidates and causal limits in the extracted evidence. In particular, reject pure-family or endpoint dominance, the categorical state/allocation fits as count weights, literal containment of every raw timing sample, and any manual subtraction or coefficient repair. + +## Validity + +- `Establishes`: A generated component-wise nonzero Opening predicate capture envelope over the reference full-Contract `0..48` allocation domain, including Native presence and qualified Oracle Fresh/refusal states, with a shared maximum/actual selector and separate zero traversal. +- `Does not establish`: Whole-transition admission, arbitrary embedding hosts, complete production Weight-file identity, exact release artifact closure or integrated throughput. +- `Known limitations`: The three methods are staged from recorded generated artifacts rather than a complete final-tree regeneration; populated-trie refusal witnesses remain reference-host evidence, and EXP-0064/EXP-0065 retain admission and exact-artifact closure. +- `Invalidation triggers`: A changed Contract traversal, predicate/host domain, configured count bound, selector, measured body, generated Weight, source/Wasm identity or acceptance rule requires scoped requalification. +- `Residual qualitative insight`: Component-wise selection preserves independent time/proof owners while zero-result traversal and inclusive complete paths remain mutually exclusive charging boundaries. + +## Follow-Up / Next Gradient + +Stop measuring this satisfied component. EXP-0034 independently owns Opening amount capture, EXP-0064 composes admission, and EXP-0065 regenerates exact artifacts. Reopen only for the declared falsifier, a changed reference host/predicate domain or a changed relevant identity; broader release closure alone is not a reopen trigger. + +## Architecture Impact + +The existing V10 runtime selector and three staged generated methods are accepted as the scoped Opening predicate capture input. No new runtime, semantic or storage change is introduced by this decision; C1 and A0 remain frozen, and complete production regeneration remains G5 work. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: [EXP-0028](./EXP-0028.md) O4 +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: [EXP-0025](./EXP-0025.md) +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: None. +- `Transfers question to`: None; independent findings require an explicit new transfer. +- `Produces input for`: [EXP-0028](./EXP-0028.md) +- `Reopen trigger`: A falsifier of this declared owner/domain or exact artifact identity; cost alone does not reopen frozen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0034.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0034.md new file mode 100644 index 00000000..a4343b75 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0034.md @@ -0,0 +1,359 @@ +# EXP-0034 — Opening Amount Capture Bound + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | [EXP-0028](./EXP-0028.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-06 | +| Affected domain | The nonzero Opening amount capture bound dominates the declared typed amount allocation and host-ledger domain. | +| Physical mechanism | Opening Amount Capture Bound | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0025](./EXP-0025.md) — Accepted, physical architecture only | +| Successor experiment | [EXP-0028](./EXP-0028.md) | + + +## Decision Question + +The nonzero Opening amount capture bound dominates the declared typed amount allocation and host-ledger domain. + +## Governing Specification + +- `Owners`: `template/pallets/actors/docs/specification.en.md` (sole ownership, lifecycle/Trigger/Pipeline/Action semantics, placement and scheduler); `docs/actors-resource-policy.specification.en.md` (component-wise admission/actual settlement, equal-thirds block policy); `docs/actors-performance-assurance.specification.en.md` (evidence and target). +- `Frozen physical input`: [EXP-0025](./EXP-0025.md) C1 Architecture Freeze. No change to FIFO, ticket chronology, Q1, causal floor, primary/reference ontology, source/successor atomicity or sole execution owner. +- `Semantic oracle`: A0 SHA-256 `8746e423168309975e509be91e4eb06fdf0eb0b43369d75f1ee62202a4341678` remains frozen; use only for relevant semantic equivalence, never as a Weight oracle. + +## Context and Hypothesis + +This Leaf owns opening amount capture bound. Its boundary is the single claim above. Evidence was extracted from the explicitly marked pre-fission parent subsections at Git baseline `a60de7748132211d9e58cecd5be82b41f76ce230`; this migration changes navigation and evidence ownership only. Source, Wasm, commands, rejection outcomes and evidence limitations retain their original meaning. + +Extraction provenance at the recorded Git baseline: EXP-0028 / “Full-Contract Amount Capture Range”, “Native Amount Capture Reservation Qualification”, “Unfunded and Missing-Receipt Amount Capture Qualification”, “Mixed Amount Capture Allocation Qualification”, “Share-Mixed Count Model”, “Opening Capture Component Ranges”. Original directional prose (above/below) belongs to these pre-fission subsections; current cross-owner navigation follows the parent proof map. + +## Baseline and Candidates + +Retained measured implementations and rejected candidates are identified in the extracted evidence. The deciding candidate is the staged component-wise maximum of the generated full-Contract PreservableAsset, TargetAsset and one-share/remaining-PreservableAsset inputs. `RuntimeStepControlWeight::opening_snapshot_weight` caps each input at its measured structural domain, selects their component-wise maximum for every nonzero result count, and selects only the independent full-Contract traversal at zero. + +## Controls + +Only the declared witness input varies within each source-bound comparison. Preserve the exact host, toolchain, database, Wasm and sampling controls printed with each evidence item; do not compare unlike artifacts as a controlled performance result. + +## Benchmark Class, Workloads, and Criteria + +Acceptance requires the single claim to hold over its declared admitted domain, with each physical operation charged once and every retained limitation resolved before broader production use. One legal omitted-owner, domain or containment counterexample falsifies that claim. Existing workloads below are bounded witnesses, not permission to add independent questions. + +## Environment and Artifact Identity + +Pre-fission baseline `a60de7748132211d9e58cecd5be82b41f76ce230`. Each extracted block retains its own exact source/Wasm/output identity. Shared pinned hardware/toolchain/cache controls follow [EXP-0030](./EXP-0030.md#environment-and-artifact-identity) only where the original comparison explicitly reuses them. A shared environment never substitutes for the block’s own source or Wasm identity. No runtime artifact was rebuilt during migration. + +## Proof Obligations + +- `Freeze`: Frozen by explicit migration reconstruction from the pre-fission decision and retained witnesses at a60de7748132211d9e58cecd5be82b41f76ce230. No new measurement or historical decision is implied. +- `Review triggers`: More than six proof subsections; multiple consumers reviewed. +- `Decomposition review`: All retained amount profiles challenge the same typed nonzero Opening amount capture bound: native/non-native preservable amounts, target assets, staking shares, funded/unfunded states and fixed-total mixtures. The zero-result traversal owner is EXP-0036; cardinality algebra is EXP-0045; final complete identity is EXP-0065. These bounded witnesses cannot justify adding a new charged operation or a new independent acceptance domain. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | The nonzero Opening amount capture bound dominates the declared typed amount allocation and host-ledger domain. | One admitted typed amount surface or fixed-total mixture whose host-ledger capture exceeds the selected amount bound | Reference-Wasm FRAME evidence in the declared owner/domain; native checks are construction controls | Self | Required | Named parent decision and declared downstream consumers | Satisfied for the reference binding: generated component owners cover every structurally admitted typed allocation and qualified host-ledger state; maximum/actual share the selector without adding zero traversal or direct inclusive capture | + +## Measurements + + +### Full-Contract Amount Capture Range + +The remaining nonzero amount fixture on `2761bf1` builds only `ceil(e/2)` Steps and begins with Native; its installed historical model covers only `e = 1..16`, while the reference bound is 24. This cannot certify full-Contract traversal or maximum non-native account encoding. Strengthen the existing `opening_snapshot_capture(e)` to retain every configured Step, exactly `e` distinct PreservableAsset surfaces, and maximum encoded non-native accounts. Once all distinct surfaces are introduced, remaining AddLiquidity Steps repeat the final surfaces, retaining duplicate-detection work without adding snapshot entries. At one entry, a distinct second asset has a Fixed amount. Verify every captured value against its exact balance minus minimum, not only a nonzero count. + +The revised fixture is a capture component, not an admitted full Actor or an executed liquidity plan. It preserves the independent zero-entry owner. Reference staking inspection disproves an assumed extra Pool read: `RuntimeStakedAssetIdResolver` computes the receipt ID, then `effective_share_balance` reads receipt Asset existence and Account balance. This identifies two store families but does not certify StakingShares timing/encoding or generic-host equivalence; TargetAsset, Native/reservation and staking coverage remain separate qualifications. + +Prepared source content/modes `437679263d11b4525f821a27c887b51c306aec97d36a5aff4f0ca4ccfd5e1f03`; benchmark source SHA-256 `6feebf03f0753d00255cf020049ffa4e2280e3fc68434a1976b2891ee37e032c`. The native all-feature benchmark passes. Measure `scripts/benchmarks.sh --extrinsic opening_snapshot_capture --output /tmp/deos-exp28-opening-amount-full.rs --json-file /tmp/deos-exp28-opening-amount-full.json pallet_deos_actors` on fresh Wasm with ordinary 50 × 20/map/cache/heap controls and no competing build or measurement. Require all reference counts 1..24, exact distinct surfaces and tracked owners. Compare the generated count model with raw DB/proof and the old model only within its stated domain; a read mismatch or larger component rejects old complete-coverage claims. Do not infer generic-host bounds, whole-Actor admissibility or an isolated speed comparison across binaries. + +The range passes on Wasm `d1771f189d9afa8136bc9c03c85709f34b39d5b62435013afb5d0417ed0cf541` (100-second build, 15-second run). All counts 1..24 are present among 9,000 timing/450 DB samples. Each count has exactly `2e` unique reads, `e` repeated reads and zero writes. The generated model is `8,919,121 + 9,720,116e ps` before DB, estimated proof `1,366 + 2,836e bytes`, and `2e` database reads. The slope error is `3,903 ps`; recorded proof is 785 bytes at one and 9,051 at 24. At 24, timing minimum/upper-median/p95/maximum are `235,579 / 242,004 / 260,932 / 312,475 ns`. Generated/raw SHA-256: `de7d44f5349c0ed7510a4efed4f01bd6b770cf65d5fb9a3a48abb4a1531a975a` / `1a703b8049e4010bc28a01e2a8600c2fe193795b87855dfe19d4240a31812ba1`. + +Within the historical 1..16 domain, new estimated proof exceeds the old `4,373 + 2,260e` model from six entries; at 16 it is 46,742 versus 40,533 bytes. Recorded proof remains a distinct metric and does not by itself prove actual underweight. The changed state/encoding/traversal and larger estimated component reject retaining the old profile as complete maximum-state evidence. The new fragment remains uninstalled pending typed-surface qualification; its lower DB-adjusted time is not an isolated optimization result. + +The next bounded falsifier compares full-Contract `opening_target_snapshot_capture(e)` and `opening_staking_snapshot_capture(e)`, each over `1..min(MaxOpeningSnapshotEntries, MaxContractSteps)` because each such Task contributes one surface. Remaining Steps repeat the last surface. Target uses the same maximum account preparation; staking uses ordinary registration and stake into distinct local positions, with receipt Asset/Account encoding checked before and after staking. The benchmark-only host helper defaults to explicit unsupported failure. Existing maximum account preparation is shared without changing the prior pure fixture's input values or measured capture body. Compare typed host models and tracked reads against the PreservableAsset candidate; do not treat a type ordinal as a count parameter, extrapolate staking count past the Step bound, or infer mixed/Native/reservation coverage from pure families. + +Typed-profile preparation binds source content/modes `7be1c99a132f38f94f0854bf4949a98b1f5588a157003b2fd7a2f5bb7db1472d`, benchmark source `a4ef71d93807f2d445527737fde3fb45a6a90d2c041988e307037bd77d01953b`, and runtime helper `72d385a2f0bbfac7083e4130730563d28ba82bc74a6b686b1d9f0382c9440b3b`. Three native capture benchmark tests, workspace/all-target/all-feature Clippy and formatting pass. Run `scripts/benchmarks.sh --extra --extrinsic opening_target_snapshot_capture --output /tmp/deos-exp28-opening-target-full.rs --json-file /tmp/deos-exp28-opening-target-full.json pallet_deos_actors`, then reuse that Wasm with `--skip-build --extra --extrinsic opening_staking_snapshot_capture` and `/tmp/deos-exp28-opening-staking-full.{rs,json}`. Retain the preceding 50 × 20/map/cache/heap controls and raw per-count samples; no competing build or benchmark runs during measurement. + +The two typed ranges pass on the same Wasm `a887a69199df13c3802b2a85ad782a0c74e51915b7dc27db5514c14166f78a9d`: Target takes 18 seconds after a 100-second build; staking takes 15 seconds without rebuilding. Every count 1..12 has exactly `2e` unique reads, no repeated reads and no writes. Target retains 15,000 timing/750 DB samples; staking retains 7,000/350. Ordinary staking setup and both maximum-encoding assertions execute successfully in Wasm, qualifying the reference helper beyond native compilation. + +| Typed surface | Generated RefTime before DB (ps) | Estimated proof (bytes) | Slope error (ps) | Recorded proof at 1 / 12 | Timing at 12, min / upper-median / p95 / max (ns) | +| --- | --- | --- | --- | --- | --- | +| TargetAsset | `10,563,163 + 7,873,507e` | `1,376 + 2,834e` | 3,548 | 785 / 4,695 | 103,577 / 106,510 / 119,640 / 244,868 | +| StakingShares | `8,318,032 + 7,492,937e` | `1,361 + 2,716e` | 4,999 | 624 / 3,236 | 95,824 / 99,246 / 122,154 / 294,315 | + +Both generated models add `2e` database reads. Target generated/raw SHA-256: `489f641aa1ad41fe86ccbee026a8b2ed0ce4a088477941137d6fd1c9fd1c89d4` / `e7dbc534c50158ee1d6905482f7f77f7e83f1d9d7ac01d1c65c49f8a4d3ddee6`; staking: `cedc0f4fba9fa8114cad6b19fca9224eb152d7e0b4c2c995200eacfc80693f17` / `c7d8dc8dea20477bb9c930d868f84d93b7e832ecb401759eb73b919a9a31a9db`. No samples or outliers are deleted. The pure PreservableAsset measurement used the preceding binary; its unchanged capture body and equivalent inputs support coverage comparison, not an isolated cross-binary speed claim. + +Within 1..12, PreservableAsset's fitted RefTime exceeds both typed models and its estimated proof exceeds staking. Target estimated proof is larger by `10 - 2e` bytes at counts 1..4, equal at five, then smaller. This rejects selecting the PreservableAsset regression alone as a component-wise envelope even though the difference is small; it is not proof of actual undercharging. The typed profiles qualify present, funded, maximum-encoded non-native records only. Keep all three fragments uninstalled until mixed-type traversal, Native/reserved funding and unavailable host states are qualified. Native three-profile tests, all-feature workspace Clippy and these real-host Wasm executions validate the retained benchmark-only changes; no production algorithm, provider or Weight changes in this slice. + +### Native Amount Capture Reservation Qualification + +The next scoped falsifier tests whether Native amount capture adds a storage owner or exceeds the typed non-native candidates at the same captured count. `spendable_balance` reads the host balance once and subtracts Actors-local reservation only for FeeNative; `protected_minimum` selects the User floor or asset minimum without reading Actor state. Measure full-Contract Native PreservableAsset, TargetAsset and their same-asset typed combination for both classes and zero/half/full visible-balance reservation. Use ordinary native funding, a distinct fixed auxiliary asset, AddLiquidity and SwapOut shapes, and exact snapshot postconditions. This is a capture-component reservation boundary, not proof that every supplied reservation is admitted as a complete Pipeline. + +Encode the 18 finite cases categorically, retaining each case's raw time/reads/proof rather than using an ordinal regression as a production count Weight. Existing pure non-native models remain the comparison inputs; no runtime algorithm or fee policy change is proposed. Missing-account/receipt and mixed three-family counts remain independent of this Native qualification. + +Prepared source content/modes `9f48f9a5170fb9759c5d61f46c1f8b3418dcfe9bbc4d894adcf194fc67a2332c`, benchmark source SHA-256 `1e0179a675412d4926c92eac490b2c2be1ec2664b7f893dcf9d72cc3f26567d0`. Native benchmark test and canonical formatting pass. Measure `scripts/benchmarks.sh --extra --extrinsic opening_native_snapshot_capture --output /tmp/deos-exp28-opening-native.rs --json-file /tmp/deos-exp28-opening-native.json pallet_deos_actors`, retaining 50 × 20/map/cache/heap controls, checking that every ordinal 0..17 occurs, and excluding competing builds or measurement. Case `c` uses class `c / 9` (System/User), family `(c % 9) / 3` (Preservable/Target/both), and reservation `c % 3` (zero/half/full). + +All 18 cases pass on Wasm `10a407d118a1ad2e470decbb589a91f586b59ec60572bda05b1cd7dceb1709e6` (100-second build, 52-second run), retaining 41,000 timing/2,050 DB samples. Each has one unique `System::Account` read, zero writes and 103 recorded proof bytes; the generated estimated proof is 3,568 bytes. Single surfaces have one repeated read and the pair has three: Balances' expendable reducible-balance path reads Account and calls `System::can_dec_provider`, which reads the same Account. Actors-local reservation and minimum selection add no storage owner. This matches the pinned host source, rather than treating the adapter invocation as exactly one physical read. + +| c | Class | Family | Reserve | Min / upper-median / p95 / max (ns) | +| --- | --- | --- | --- | --- | +| 0 | System | Preservable | Zero | 6,286 / 7,683 / 9,079 / 24,724 | +| 1 | System | Preservable | Half | 6,216 / 7,752 / 9,010 / 29,334 | +| 2 | System | Preservable | Full | 6,216 / 7,752 / 9,080 / 22,978 | +| 3 | System | Target | Zero | 6,146 / 7,683 / 9,149 / 27,658 | +| 4 | System | Target | Half | 6,146 / 7,683 / 9,010 / 26,749 | +| 5 | System | Target | Full | 6,146 / 7,683 / 9,080 / 68,305 | +| 6 | System | Both | Zero | 9,359 / 10,965 / 12,781 / 68,725 | +| 7 | System | Both | Half | 9,150 / 10,896 / 12,502 / 47,423 | +| 8 | System | Both | Full | 9,428 / 10,965 / 12,571 / 27,657 | +| 9 | User | Preservable | Zero | 6,286 / 7,752 / 9,080 / 27,588 | +| 10 | User | Preservable | Half | 6,146 / 7,752 / 9,080 / 26,261 | +| 11 | User | Preservable | Full | 6,077 / 7,683 / 9,079 / 25,771 | +| 12 | User | Target | Zero | 6,565 / 7,683 / 9,079 / 41,905 | +| 13 | User | Target | Half | 6,216 / 7,683 / 9,009 / 30,102 | +| 14 | User | Target | Full | 6,286 / 7,683 / 9,080 / 36,738 | +| 15 | User | Both | Zero | 9,359 / 10,896 / 12,501 / 34,781 | +| 16 | User | Both | Half | 9,498 / 10,896 / 12,362 / 54,268 | +| 17 | User | Both | Full | 9,498 / 10,895 / 12,362 / 25,214 | + +Generated/raw SHA-256: `313dd3862437168e5f5a19a0ac6834a8756844c27a571a33dceab7e42a505939` / `a731cafeb1d453b77869fd4ee336927cce48a2642b579574267932db0b3f1f3b`. Cases 8/16 retain 1,640 timing/82 DB samples, case 17 retains 820/41, all others retain 2,460/123. Preserve the reported outliers; no raw maximum is replaced by a fitted expectation. The categorical RefTime regression is not a count Weight and is not installed. + +The 3,568-byte estimate is below every preceding non-native typed model at the matching one/two captured entries; Native medians and p95 are also below those models' pre-DB RefTime. This scoped comparison supports retaining the typed non-native envelope candidates, not claiming a deterministic wall-clock maximum or isolating a speed change across binaries. Native reservation arithmetic, User/System minimum selection and same-asset typed deduplication now have explicit reference-Wasm witnesses. Mixed non-native/Native/StakingShares allocations, other host states and whole-Actor reservation reachability remain separate acceptance inputs. + +### Unfunded and Missing-Receipt Amount Capture Qualification + +The next falsifier isolates zero-valued snapshots with absent host accounts, plus the early return after an absent staking receipt. An absent account does not imply an empty Opening snapshot: the typed key must still be inserted with zero. Use maximum-encoded non-native records owned by another account, leave the capture account unfunded, and retain full configured Contract traversal. Cover PreservableAsset counts through `min(MaxOpeningSnapshotEntries, 2 * MaxContractSteps)` and TargetAsset/StakingShares counts through `min(MaxOpeningSnapshotEntries, MaxContractSteps)`. Fund real reference staking positions on the other owner for present-receipt/absent-account cases. For the single missing-receipt witness, reuse ordinary empty receipt registration and destruction through the existing host helper; no fabricated storage deletion or new helper is needed. + +The categorical profile contains all declared counts plus the one missing-receipt case (49 reference cases). Preserve per-case raw measurements and compare storage owners, proof and timing with funded typed profiles; never install its ordinal fit as a count Weight. These component fixtures qualify result state, not whole-Actor admission or a general missing-asset lifecycle. Mixed typed traversal remains the next independent input. + +Prepared source content/modes `673bb79a606e21a101f914658ad0bb03983cf0728c50d297dd216e691ea3b2d6`, benchmark source SHA-256 `0c1f7c25e37c3abe9fe018b2c67c0dc71bf676eb420eacb808b0237760d68560`. Native benchmark test and canonical formatting pass. Measure `scripts/benchmarks.sh --extra --extrinsic opening_unfunded_snapshot_capture --output /tmp/deos-exp28-opening-unfunded.rs --json-file /tmp/deos-exp28-opening-unfunded.json pallet_deos_actors`, keeping 50 × 20/map/cache/heap controls and no competing builds/measurements. Reference ordinals 0..23 are Preservable counts 1..24, 24..35 are Target counts 1..12, 36..47 are present-receipt StakingShares counts 1..12, and 48 is the single removed-receipt surface. Require every ordinal in the raw results. + +All 49 reference cases pass on Wasm `c606a51fefe3b4601af5a21a7c79fab788a8fd44d5a0d66bac515a4e645e38ce` (100-second build, 17-second run), including ordinary empty-receipt destruction. Retain 11,000 timing/550 DB samples: case zero has 440/22 and every other case has 220/11. Preservable captures have `2e` unique and `e` repeated reads; Target and present-receipt StakingShares have `2e` unique and zero repeated reads. The missing receipt has one Asset read and no Account read. All writes are zero and every expected typed key remains present with value zero, matching specification §5.3. + +Upper-median time is in ns; proof is recorded bytes, not estimated ProofSize: + +| Count | Preservable median / proof | Target median / proof | StakingShares median / proof | +| --- | --- | --- | --- | +| 1 | 16,274 / 747 | 14,806 / 747 | 14,178 / 586 | +| 2 | 25,981 / 1,003 | 22,908 / 1,003 | 22,280 / 813 | +| 3 | 36,737 / 1,331 | 32,337 / 1,331 | 29,823 / 974 | +| 4 | 46,236 / 1,660 | 40,438 / 1,660 | 37,785 / 1,141 | +| 5 | 54,686 / 1,982 | 46,865 / 1,982 | 45,048 / 1,372 | +| 6 | 62,439 / 2,304 | 53,011 / 2,304 | 51,054 / 1,599 | +| 7 | 71,519 / 2,632 | 60,972 / 2,632 | 58,109 / 1,766 | +| 8 | 79,201 / 2,954 | 67,189 / 2,954 | 65,792 / 2,001 | +| 9 | 87,513 / 3,276 | 73,963 / 3,276 | 73,544 / 2,236 | +| 10 | 95,684 / 3,596 | 80,528 / 3,596 | 79,550 / 2,395 | +| 11 | 103,785 / 3,919 | 86,884 / 3,919 | 85,906 / 2,620 | +| 12 | 112,166 / 4,239 | 93,868 / 4,239 | 96,731 / 2,779 | +| 13 | 125,367 / 4,567 | — | — | +| 14 | 134,586 / 4,895 | — | — | +| 15 | 142,060 / 5,217 | — | — | +| 16 | 151,139 / 5,545 | — | — | +| 17 | 162,662 / 5,867 | — | — | +| 18 | 170,764 / 6,187 | — | — | +| 19 | 179,635 / 6,507 | — | — | +| 20 | 187,178 / 6,829 | — | — | +| 21 | 197,305 / 7,157 | — | — | +| 22 | 205,546 / 7,483 | — | — | +| 23 | 215,184 / 7,811 | — | — | +| 24 | 224,264 / 8,139 | — | — | + +At each family's maximum count, minimum/upper-median/p95/maximum time is Preservable `219,305 / 224,264 / 249,547 / 486,522 ns`, Target `91,563 / 93,868 / 111,050 / 144,993 ns`, and shares `92,960 / 96,731 / 114,193 / 127,812 ns`. The missing-receipt case is `6,286 / 7,682 / 9,778 / 19,975 ns` with 301 recorded proof bytes. The global largest time sample is 614,124 ns at 22 Preservable surfaces; it remains in the data. + +Generated/raw SHA-256: `ce0adcd57e686aeab2786b35ba762d07bf21149c76dc23c9b4d6cbdc82986e5f` / `7d41a26022ac8cb391f3d64de4ca4636bacb49c9d3ef1995f9ddf2ca86afea3c`. The ordinal regression produces a constant 20-read model despite the 48-read endpoint, further demonstrating why this diagnostic fit is unusable as a count Weight. It remains uninstalled. + +Every unfunded case median is below its corresponding funded type's pre-DB model; the smallest margins are 1,342,469 ps at three Preservable surfaces, 1,619,191 ps at four Target surfaces, and 504,780 ps at four share surfaces. Unique-read counts match those funded profiles; recorded proof is lower at every corresponding count. These are source-qualified comparisons across binaries, not isolated speed gains, p95/max containment or a complete estimated-proof envelope. The missing-account and missing-receipt result-state witnesses add no new phase or storage owner. Other host lifecycle states must be reconciled through their actual adapter paths; this run neither fabricates those states nor certifies mixed-type traversal. Native benchmark, all-feature workspace Clippy and real-host Wasm setup/capture pass without changes to production behavior or weights. + +### Mixed Amount Capture Allocation Qualification + +The deciding question is whether a component-wise maximum of the three funded type models can cover mixed capture at the same total count, without adding duplicate full-Contract traversal charges. For reference counts `(p, t, s)`, structural feasibility requires `p + t + s <= 24` and `ceil(p / 2) + t + s <= 12`; require at least two nonzero families. This yields 770 mixed allocations (440 using all three families). The 48 pure allocations already have range evidence and are excluded from this diagnostic. + +Prepare maximum-encoded asset accounts before creating funded staking positions, then choose asset surfaces disjoint from position and receipt identities. Keep all records ordinary host state and assert exact nonzero snapshot values. Build Preservable pair Steps, then Target and share Steps; fill the remaining configured Steps with the final Preservable pair when available, otherwise the last one-surface Step. Compare that order with its complete reversal, preserving allocation, state and values. The 1,540 reference cases test two explicit orders, not every permutation, generic-host admissibility or full-Actor admission. Native/reservation and unavailable states retain their separate witnesses. + +The provisional comparison uses the component-wise maximum of the preceding Preservable model at total `e` and Target/share models at `min(e, 12)`; above 12 the latter are only comparison constants, never extrapolated host ranges. Compare every case's median plus actual unique-read DB charge and recorded proof with that candidate, retain outliers and all lower counts, and inspect tracked owners before attributing a failure to timing. A categorical ordinal regression is not a count Weight. Any larger mixed owner or reproducible excess rejects the candidate; this does not reopen C1 or authorize hand-padding generated coefficients. + +Prepared source content/modes `96b5b400dd3f6a15c3b909574e74424f384de9a36289d2f674aefac2ad52ca61`, benchmark source SHA-256 `e6a3de161814128f6ed374380a6d473354ae1f8ea8aa25ce89591d81a7dbcb67`. Native benchmark and canonical formatting pass. Measure `scripts/benchmarks.sh --extra --extrinsic opening_mixed_snapshot_capture --steps 1540 --output /tmp/deos-exp28-opening-mixed.rs --json-file /tmp/deos-exp28-opening-mixed.json pallet_deos_actors`, retaining 20 repeats and the preceding map/cache/heap controls with no competing build/measurement. Allocations are ordered by total, Preservable count, then Target count; `c / 2` selects that tuple and `c % 2` selects reversal. Require all ordinals 0..1539 and exact `2e` unique reads, `p` repeated reads and zero writes before interpreting timing. The expanded step count enumerates the reference domain instead of sampling only 50 categorical points. + +Pinned host-tool inspection adds a stronger proof check before acceptance. `frame-omni-bencher 0.22.0` uses `frame-benchmarking-cli 58.0.0`; its `process_storage_results` adds map-depth overhead to recorded proof per storage prefix before fitting and selecting component-wise maxima. The backend groups unique keys by their 32-byte prefix, excluding repeated overlay reads. For these two non-native prefixes, each with `e` unique reads, default map size 1,000,000 contributes `5 * 495 = 2,475` bytes per read and two additional trie layers contribute 990 bytes. Thus reconstruct each point's pre-regression estimator as `recorded_proof + 2,475e + 990`, conditional on the expected owner trace. Compare that quantity as well as recorded proof with the candidate; raw proof alone cannot establish containment of the tool's conservative estimate. This is interpretation of the pinned writer, not a new production coefficient or a claim that the estimator equals actual PoV. + +The full run passes every setup/value/trace assertion on Wasm `c54190bb45fd148be4662a82981b2024840948ed66a866b97f804cb0b722634e` (100-second build, 89-second run). All 1,540 cases have 20 timing samples and one DB sample: 30,800/1,540 total. Every case has exactly `2e` unique reads, `p` repeated reads and zero writes. Generated/raw SHA-256: `d9b9db44de6674d0d94071ce071bfcb6006c06b23d9067c8d881526733098e08` / `5af60496afeb041d2447994c40428c0c7fac1a078f2d6062c92a77f5c7f51577`. + +The provisional candidate is not accepted: 148 medians exceed its RefTime and 22 reconstructed pre-regression proof estimates exceed its ProofSize. Recorded proof alone is below the candidate everywhere, illustrating why that weaker check is insufficient. Worst time is case 815, reverse `(8, 3, 1)`, total 12: minimum/median/p95/maximum `122,085 / 190,181 / 215,184 / 470,529 ns`, excess `64,620,487 ps` after equal DB charges. Forward case 814 has median 121,246 ns. The largest proof excess is six bytes at cases 1538/1539, `(22, 1, 0)`, total 23: raw proof 8,685, reconstructed estimate 66,600 versus candidate 66,594 bytes. Sixteen total-16 cases and four total-22 cases exceed by one byte; neither rounding-sized excess nor timing variation authorizes silently padding coefficients. + +Before interpreting the time excess, reuse this exact Wasm for `opening_mixed_snapshot_capture` controls at `--low 814 --high 815 --steps 2` and `--low 1536 --high 1539 --steps 4`, each with 20 repeats and the same heap/map/trie/cache defaults. Run through `bin/frame-omni-bencher v1 benchmark pallet --runtime template/target/benchmarks/release/wbuild/deos-runtime/deos_runtime.compact.compressed.wasm --pallet pallet_deos_actors --extrinsic opening_mixed_snapshot_capture --extra`, retaining `/tmp/deos-exp28-opening-mixed-{control,tail}.{rs,json}`. The first pair tests the largest order/time discrepancy; the second covers both full-tail mixtures and orders. Workspace/all-target/all-feature Clippy has completed successfully before these controls; no competing build or measurement runs. + +Per-total full-run comparison (time is ns, proof is bytes). Case IDs retain the exact tuple/order mapping above. The proof column is the reconstructed pre-regression estimator, not actual PoV: + +| e | Cases | Time / proof excess cases | Maximum median case / ns | Maximum proof case / estimate | Candidate proof | +| --- | --- | --- | --- | --- | --- | +| 2 | 6 | 0 / 0 | 3 / 26,680 | 4 / 7,019 | 7,044 | +| 3 | 14 | 3 / 0 | 17 / 38,692 | 14 / 9,860 | 9,878 | +| 4 | 24 | 6 / 0 | 42 / 48,750 | 32 / 12,702 | 12,712 | +| 5 | 36 | 6 / 0 | 71 / 58,248 | 60 / 15,537 | 15,546 | +| 6 | 50 | 6 / 0 | 126 / 68,725 | 100 / 18,372 | 18,382 | +| 7 | 66 | 2 / 0 | 192 / 77,665 | 154 / 21,213 | 21,218 | +| 8 | 84 | 2 / 0 | 253 / 103,506 | 224 / 24,048 | 24,054 | +| 9 | 104 | 0 / 0 | 381 / 96,313 | 312 / 26,884 | 26,890 | +| 10 | 126 | 1 / 0 | 393 / 111,189 | 420 / 29,717 | 29,726 | +| 11 | 150 | 1 / 0 | 651 / 116,637 | 550 / 32,552 | 32,562 | +| 12 | 176 | 9 / 0 | 815 / 190,181 | 704 / 35,385 | 35,398 | +| 13 | 154 | 8 / 0 | 987 / 138,427 | 858 / 38,226 | 38,234 | +| 14 | 130 | 13 / 0 | 1081 / 152,117 | 1010 / 41,067 | 41,070 | +| 15 | 108 | 9 / 0 | 1215 / 158,403 | 1138 / 43,902 | 43,906 | +| 16 | 88 | 6 / 16 | 1299 / 168,670 | 1244 / 46,743 | 46,742 | +| 17 | 70 | 21 / 0 | 1365 / 185,920 | 1330 / 49,578 | 49,578 | +| 18 | 54 | 16 / 0 | 1437 / 190,391 | 1398 / 52,411 | 52,414 | +| 19 | 40 | 10 / 0 | 1464 / 203,242 | 1450 / 55,244 | 55,250 | +| 20 | 28 | 9 / 0 | 1493 / 224,194 | 1488 / 58,079 | 58,086 | +| 21 | 18 | 10 / 0 | 1519 / 224,474 | 1514 / 60,920 | 60,922 | +| 22 | 10 | 7 / 4 | 1533 / 230,969 | 1530 / 63,759 | 63,758 | +| 23 | 4 | 3 / 2 | 1536 / 237,534 | 1538 / 66,600 | 66,594 | + +Both same-Wasm controls complete. The first has 7,240 timing/362 DB samples (3,620/181 per case); the tail has 4,880/244 (1,220/61 per case). Owner traces and proof exactly match the full run. No sample is discarded: + +| Case | Tuple / order | Min / upper-median / p95 / max (ns) | Median minus candidate pre-DB RefTime (ps) | +| --- | --- | --- | --- | +| 814 | `(8,3,1)` / Forward | 117,755 / 121,107 / 134,587 / 284,818 | −4,453,513 | +| 815 | `(8,3,1)` / Reverse | 118,243 / 121,596 / 135,145 / 468,643 | −3,964,513 | +| 1536 | `(22,0,1)` / Forward | 226,150 / 232,784 / 248,010 / 535,970 | +302,211 | +| 1537 | `(22,0,1)` / Reverse | 227,477 / 233,972 / 248,569 / 338,387 | +1,490,211 | +| 1538 | `(22,1,0)` / Forward | 224,543 / 230,480 / 245,147 / 539,742 | −2,001,789 | +| 1539 | `(22,1,0)` / Reverse | 224,962 / 231,528 / 247,591 / 554,270 | −953,789 | + +Control generated/raw SHA-256: `0aec8924676d6ceaf7c140d94c8fd69c139208541b0bcd50c291297bd66e2504` / `c6ca7192ca14e5c1e6ae795b297ee731a72e65520e690df7bfbd0a1c9d69b98d`; tail: `bd31e71a7cdba3a736daab3da8d88ccddb8bf813501885f7529762df6a82ab34` / `e384947b4b06a270150910e58cb5563b68ee432ba6dc2c633fe5a4a7518de6fa`. These direct control outputs are retained as generated, without treating their categorical slopes as production methods. + +The 68,935-ns forward/reverse median gap at 814/815 contracts to 489 ns in the control: the large excess is not stable, and the long-run samples remain valid variance evidence. The share-containing tail retains a smaller positive median excess in both orders; measure its count-shaped owner before asserting that pure Preservable timing dominates all mixtures. Do not repeat the unchanged 1,540-case topology enumeration to investigate that narrower question. + +The small proof residuals are not a new mixed storage owner. Applying the same writer reconstruction to the retained pure data already exposes positive fitted residuals: Preservable at counts 1/16/22/23/24 by 48/1/1/6/11 bytes, Target at 1/9/10/11/12 by 40/2/1/2/1 bytes, and shares at 1/2/6/8/9/11 by 12/36/10/6/38/17 bytes. A fitted FRAME model and every pre-regression observation are distinct objects. Review that approximation under the governing generated-model contract; do not promote an agent-added strict per-sample fit criterion into a new consensus invariant, hide the residuals, or manually pad output. Maximum/actual reservation dominance concerns the runtime's complete generated Weight owners and selectors; physical timing and proof observations remain separately classified evidence. + +This slice closes the declared mixed allocation/value/read-topology enumeration for two orders. It does not accept the pure-only timing maximum, certify every permutation or whole-Actor resource admission, or install a generated Weight fragment. The next bounded input is a count-shaped one-share/remaining-Preservable capture profile using this exact host preparation and the measured order, followed by model/selector reconciliation. Frozen C1, the reference bounds and runtime execution stay unchanged. + +### Share-Mixed Count Model + +The retained tail discrepancy selects one concrete count profile, not another allocation sweep: one funded StakingShares surface plus `e - 1` distinct Preservable surfaces, full configured Contract length, and the reversed Step order. Its range is `1..min(MaxOpeningSnapshotEntries, 2 * MaxContractSteps - 1)` (reference 1..23); one is a pure share boundary and 24 cannot contain a share under the reference Step bound. Extract the existing mixed fixture preparation for reuse without changing its state, values or measured capture body. Review the generated count model against its exact two-prefix reads and the retained mixed/pure observations, keeping fitted residuals distinct from runtime maximum/actual accounting. No physical optimization or new Actor semantics are proposed. + +Prepared source content/modes `73123d39a006cc4c9a6da557262a0c3b9f057ecc0e324842e1f4388baeeb3eb2`, benchmark source SHA-256 `0f3ee212078a1609461538cb11b98710a58b195778ae2ac1788619df8918bbd9`. Native tests for both the extracted mixed diagnostic and the new count profile pass; canonical formatting passes. Run `scripts/benchmarks.sh --extra --extrinsic opening_share_mixed_capture --output /tmp/deos-exp28-opening-share-count.rs --json-file /tmp/deos-exp28-opening-share-count.json pallet_deos_actors` with ordinary 50 × 20/map/cache/heap controls and no competing build/measurement. Require every reference count 1..23, `2e` unique reads, `e - 1` repeated reads and no writes. The existing 1,540-case fixture body, input values and capture owner remain equivalent after extraction; do not repeat that enumeration merely because preparation moved into a helper. + +The count profile passes on Wasm `29eb45cb47ae722dc8315c578d542095fc93584020b3082c5c317217fa604211` (100-second build, 16-second run). All 23 counts are present among 8,000 timing/400 DB samples. Counts 1/5/9/14/18 have 480/24 samples, count 23 has 160/8, and the others have 320/16. All traces match `2e` unique reads, `e - 1` repeated reads and zero writes. Generated RefTime is `7,075,624 + 9,847,218e ps` plus `2e` database reads; estimated proof is `1,308 + 2,838e bytes`, with slope error `5,020 ps`. Generated/raw SHA-256: `34f950c983a65f988da89d88356f37824341c0fc9e0deeb884562e5b8db6904d` / `bd2e2256dd687c3e23d1a48c8ce3fe0606ff32eab0f6572da85f7762f177d0b3`. + +| e | Upper-median time (ns) | Recorded proof (bytes) | +| --- | --- | --- | +| 1 | 15,086 | 690 | +| 2 | 26,680 | 984 | +| 3 | 38,343 | 1,352 | +| 4 | 48,680 | 1,786 | +| 5 | 57,760 | 2,146 | +| 6 | 68,026 | 2,513 | +| 7 | 76,826 | 2,872 | +| 8 | 85,417 | 3,232 | +| 9 | 94,426 | 3,594 | +| 10 | 103,366 | 3,951 | +| 11 | 112,028 | 4,312 | +| 12 | 125,367 | 4,676 | +| 13 | 135,285 | 5,042 | +| 14 | 143,876 | 5,401 | +| 15 | 154,002 | 5,768 | +| 16 | 163,082 | 6,127 | +| 17 | 176,003 | 6,486 | +| 18 | 184,593 | 6,844 | +| 19 | 193,254 | 7,203 | +| 20 | 203,311 | 7,570 | +| 21 | 211,972 | 7,896 | +| 22 | 224,474 | 8,301 | +| 23 | 234,949 | 8,667 | + +At one entry, minimum/upper-median/p95/maximum are `13,759 / 15,086 / 18,019 / 31,988 ns`; at 23 they are `228,455 / 234,949 / 257,160 / 263,306 ns`. The global maximum is 507,824 ns at 19 entries. Samples remain unfiltered. The new count model exceeds the Preservable RefTime model at counts 15..23, while Preservable regains the maximum at 24 because the share model is capped at its actual 23-entry domain. Target owns the slightly larger generated proof at 1..4; Preservable owns proof from five onward. The earlier pure share model remains below these generated alternatives over its measured domain. + +Stage the three generated methods unchanged: replace the obsolete Native-first `opening_snapshot_capture` model with the maximum-encoded full 1..24 profile, and add `opening_target_snapshot_capture` plus `opening_share_mixed_capture` to the required Weight interface and regular generation. The runtime's shared `opening_snapshot_models` defines the three inputs, capped at each model's domain; `opening_snapshot_weight` takes their component-wise maximum for nonzero capture and retains the independent traversal at zero. Composed maximum and actual consume this same owner; direct inclusive paths receive no additional charge. V9 binds all three raw model inputs, the selected result and zero alternatives. Generic fallback implementations remain explicit unmeasured estimates. + +This is a scoped generated-model binding, not a claim of literal per-observation timing containment. Nine new profile medians exceed the combined fitted RefTime, with largest residual 1,524,670 ps at 17 entries; none of this profile's reconstructed pre-regression proof estimates exceed the combined model. Earlier residuals remain recorded and no coefficient is padded or comment repaired. The governing runtime invariant is component-wise maximum/valid-actual ownership and settlement; full release capacity and observed proof remain subject to exact production/runtime evidence. Nonzero amount capture now has the declared model/selector inputs, while the complete phase matrix, reference-admitted domain and G5 artifacts remain open. + +Exact comparison verifies every staged method and annotation against its generated artifact. The expanded existing selector regression checks all 24 amount counts, model-domain caps, maximum containment and replacement of the zero traversal without addition. The pinned default runtime suite passes 493 tests with 22 intentionally ignored; workspace/all-target/all-feature Clippy passes, as do the updated generic Weight-interface parity test and 27 independent embedding tests with DEX and TryRuntime. Retained source content/modes `098d8d77c8242eb29034e20caa2de6aafb3f2c6a4e45fa2904a9c855887cf366`, benchmark source `01fedae0d66a422390a6b27ddb23547723d84e5872e64c19b71151dae6cd98b8`, provider `78c8ed7297dbd661ece19bed0783cce46f5eff98f7f727e5f223a4e407958144`, staged Weight file `e3debef345458fa392e906af0596eb45173ca329c16268db8d13cf007fba0db8`. Promotion and interface/binding edits preserve the measured capture body and setup values; final production-Wasm and complete file regeneration belong to G5. + + + +### Opening Capture Component Ranges + +EXP-0029's independent close boundaries motivate qualifying the existing capture components over the current reference cardinalities. The bound file still models `opening_snapshot_capture` over `1..16` and `opening_predicate_capture` over `1..32`, while the current benchmark constructors expose `1..24` and `1..48`. Measure both existing profiles at 50 steps × 20 repeats on the unchanged independent-partition benchmark Wasm. The decision is whether these component inputs and storage models cover those full current ranges; no partial fragment will be installed as a complete production envelope. + +These are isolated function measurements: funded distinct assets, authored two-leg/one-leg Opening amount positions or packed Opening BalanceAbove occurrences, setup outside the measured block, and exact nonzero snapshot/true-result postconditions. They do not create Actors or certify full lifecycle admission, User fees, dependency-fragment loading or complete Opening. The amount profile exercises System `PreservableAsset`; `TargetAsset`, `StakingShares` and other Predicate/Observation adapter branches require their own coverage or dominance proof before a generic maximum is accepted. + +Both initial profiles pass on source content/modes `1f53c465c7db0844e73f2905b607b0287859cc6c2ea692c671be078f1fd4cf15`, benchmark source `171e68cf5e186b51d79cbedf709f937d2100181c3757163d41b331d6fa93b0e2`, and benchmark Wasm `068c40b5263e66ed0669fe12fee2569d57f549d200064fbf69fc20be229f50f3`. The prior successful compilation and known stale-storage preflight apply unchanged. Commands use `scripts/benchmarks.sh --skip-build --extrinsic PROFILE --output /tmp/deos-exp28-opening-KIND-current.rs pallet_deos_actors`, with PROFILE=`opening_snapshot_capture`/`opening_predicate_capture`, KIND=`snapshot`/`predicate`. Both use 50 × 20, compiled Wasm, 4096 heap pages, one-million worst-case map size and 1024 cache; no competing measurement/build process. + +| Component / full sampled range | RefTime before DB, ps | Proof bytes | Database model | +| --- | --- | --- | --- | +| Preservable amount capture, `e = 1..24` | `3,064,882 + 10,042,874e`; slope error `5,485`; minimum `6,984,000` | Estimated `4,373 + 2,437e`; recorded `288 + 310e` | `1 + 2e` reads, zero writes | + +Snapshot fragment SHA-256 `061c4a1c51400f4ec153dcabcaad70387143eccce45c57ee4c99ef5feb538e14`; predicate fragment `fa5b4638d560c1e0ee9cdd9d831ec3b07299da1ccc193cfa3529b247d8868664`. The storage trace contains one native System account and `n−1` Asset/Account pairs at maximum; generated read models retain their conservative intercept. These fragments remain uninstalled. Their larger estimated proof slopes (`2,437` versus `2,260`, `2,613` versus `2,525`) do not support a cheaper maximum admission claim; old ranges/configuration and binary differ, so this is range qualification, not an isolated regression or optimization comparison. + +The follow-on predicate comparison uses an extra diagnostic `opening_observation_capture`, sharing the same packed Opening constructor with BalanceAbove, but reading distinct host-published feeds with fresh positive observations. Measure the complete `1..48` range on one rebuilt Wasm alongside the balance profile, retaining 50 × 20 and measured proof. A higher Observation cost in either resource dimension rejects BalanceAbove-only coverage of the generic capture owner; even a lower cost proves only this configured feed shape, not maximum Oracle configuration or unavailable/stale/error branches. Setup and exact `Ok(true)`/count assertions stay outside measured capture. + +Both paired profiles pass on source content/modes `6847b15a6225cccc37f6e8b11c19006ce9a73f99f2dceac6efd57e486596e6f5`, benchmark source `e0ecee744fe7e32061e42b09be7d5fcc27f59f9baae50925ce0c4e68a6cafc28`, and benchmark Wasm `eaf6012430160ba32a5cd8156e26de6519ecacefc48917541da07fa0b9542971`. Commands: `scripts/benchmarks.sh --extra --extrinsic opening_observation_capture --output /tmp/deos-exp28-opening-observation.rs pallet_deos_actors`, then `scripts/benchmarks.sh --skip-build --extrinsic opening_predicate_capture --output /tmp/deos-exp28-opening-balance-paired.rs pallet_deos_actors`. Build takes 110 seconds; measurements take 17/16 seconds without competing builds/tests. Same host, 50 × 20, cache, map and heap controls as above. + + +## Derived Metrics + +Derived values are retained beside the evidence that defines their source and units. No subtraction of overlapping aggregate owners or cross-artifact timing comparison is introduced. + +## Statistical / Variance Notes + +Retain each item’s original sampling and uncertainty. Singleton construction probes establish only their admitted state/access witness, never statistical timing dominance or a production bound. + +## Result + +The retained V9 evidence closes the reference Opening amount capture component. Source-derived Task algebra permits exactly three typed surfaces: PreservableAsset contributes at most two distinct entries per Step, while TargetAsset and StakingShares each contribute at most one. The 48 pure-family range points plus all 770 feasible mixed `(preservable, target, shares)` allocations, each measured in forward and reverse order, cover the complete non-native reference count/allocation domain under 12 Steps and 24 entries. Every one of the 1,540 mixed cases has exactly `2e` unique reads, `p` repeated reads and zero writes. + +The host-ledger qualifications preserve the same owner partition. The 18 Native class/family/reservation cases use one System account owner and remain below the non-native candidates at the corresponding one/two-entry counts. The 49 unfunded or missing-receipt cases add no owner: non-native surfaces retain their Asset/Account reads, present staking receipts retain their Asset/Account reads, and a missing receipt stops after Asset existence. Maximum-encoded funded Asset and staking records, exact zero/nonzero values and distinct surface identities are asserted in reference Wasm. The source path performs only these typed lookups before inserting into the bounded map; Actors-local reservation and minimum arithmetic add no storage owner. + +For each nonzero reference count `e`, the staged selector takes the component-wise maximum of generated Preservable `8,919,121 + 9,720,116e` RefTime / `1,366 + 2,836e` ProofSize, Target `10,563,163 + 7,873,507e` / `1,376 + 2,834e` capped at 12, and share-mixed `7,075,624 + 9,847,218e` / `1,308 + 2,838e` capped at 23, with `2e` reads in each measured model. At 24 it selects `242,201,905` RefTime before database charge, `69,430` ProofSize and 48 reads; under the pinned reference RocksDB charge the complete selected RefTime is `1,442,201,905`. Raw timing and fitted-proof residuals remain classified observations rather than manually padded consensus coefficients. + +Decision-time identity checks reproduce staged Weight/provider SHA-256 values `2d9c60ac7764d1e7dc366531dd4a409444daf66cf3e37ed7feb922bb614e8f1a` / `0858133d39eaabb1d2591def2454f1b72d7ee6746b3c1f5ecd720f49a37750f9`. The focused default-runtime `composed_opening_capture_models_replace_traversal_without_double_charge` regression passes on the retained tree. + +## Interpretation + +The sound owner is a component-wise generated envelope, not a pure-family endpoint, categorical allocation regression, recorded-proof maximum or literal maximum of every timing sample. The complete allocation enumeration certifies reference typed read topology and the source/branch qualifications certify that Native, unavailable and missing-receipt states add no larger owner. This accepts the capture component and selector for the reference binding while leaving whole-transition admission and complete generated-file identity to their own owners. + +## Decision + +Accepted for the reference binding. For each nonzero captured entry count `e`, select the component-wise maximum of `opening_snapshot_capture(e)`, `opening_target_snapshot_capture(min(e, MaxContractSteps))` and `opening_share_mixed_capture(min(e, 2 * MaxContractSteps - 1))`, with each method additionally bounded by `MaxOpeningSnapshotEntries`. Zero selects only `opening_snapshot_traversal`. Use the same mapping in maximum and actual Opening Control Weight, and do not add it to inclusive direct owners. EXP-0065 owns complete regeneration and exact final artifact identity; no coefficient may be manually combined or repaired. + +## Rejected Alternatives and Tradeoffs + +Retain all rejected candidates and causal limits in the extracted evidence. In particular, reject pure-family or endpoint dominance, categorical allocation fits as count weights, literal containment of every raw timing/proof observation, and any manual subtraction, padding or coefficient repair. + +## Validity + +- `Establishes`: A generated component-wise nonzero Opening amount capture envelope over the reference 12-Step/24-entry typed allocation domain, including maximum-encoded non-native records, Native reservation/class variants and qualified funded/unfunded/missing-receipt states, with a shared maximum/actual selector and separate zero traversal. +- `Does not establish`: Whole-transition admission, arbitrary embedding hosts, complete production Weight-file identity, exact release artifact closure or integrated throughput. +- `Known limitations`: The three methods are staged from recorded generated artifacts rather than a complete final-tree regeneration; raw generated-model residuals remain explicit, and EXP-0064/EXP-0065 retain admission and exact-artifact closure. +- `Invalidation triggers`: A changed Task-to-surface mapping, host ledger adapter, configured Step/entry bound, selector, measured body, generated Weight, source/Wasm identity or acceptance rule requires scoped requalification. +- `Residual qualitative insight`: Component-wise selection preserves independent time/proof owners while Native and unavailable states reduce physical ownership and zero-result traversal remains mutually exclusive. + +## Follow-Up / Next Gradient + +Stop measuring this satisfied component. EXP-0035 and EXP-0036 own the independent zero-result traversal decisions, EXP-0064 composes admission, and EXP-0065 regenerates exact artifacts. Reopen only for the declared falsifier, a changed reference host/typed amount domain or a changed relevant identity; broader release closure alone is not a reopen trigger. + +## Architecture Impact + +The existing V9 runtime selector and three staged generated methods are accepted as the scoped Opening amount capture input. No new runtime, semantic or storage change is introduced by this decision; C1 and A0 remain frozen, and complete production regeneration remains G5 work. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: [EXP-0028](./EXP-0028.md) O5 +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: [EXP-0025](./EXP-0025.md) +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: None. +- `Transfers question to`: None; independent findings require an explicit new transfer. +- `Produces input for`: [EXP-0028](./EXP-0028.md) +- `Reopen trigger`: A falsifier of this declared owner/domain or exact artifact identity; cost alone does not reopen frozen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0035.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0035.md new file mode 100644 index 00000000..d6bfb6b1 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0035.md @@ -0,0 +1,132 @@ +# EXP-0035 — Zero-Result Predicate Traversal Ownership + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | [EXP-0028](./EXP-0028.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-06 | +| Affected domain | Zero-result predicate traversal selects one generated owner without adding it to inclusive or nonzero capture paths. | +| Physical mechanism | Zero-Result Predicate Traversal Ownership | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0025](./EXP-0025.md) — Accepted, physical architecture only | +| Successor experiment | [EXP-0028](./EXP-0028.md) | + + +## Decision Question + +Zero-result predicate traversal selects one generated owner without adding it to inclusive or nonzero capture paths. + +## Governing Specification + +- `Owners`: `template/pallets/actors/docs/specification.en.md` (sole ownership, lifecycle/Trigger/Pipeline/Action semantics, placement and scheduler); `docs/actors-resource-policy.specification.en.md` (component-wise admission/actual settlement, equal-thirds block policy); `docs/actors-performance-assurance.specification.en.md` (evidence and target). +- `Frozen physical input`: [EXP-0025](./EXP-0025.md) C1 Architecture Freeze. No change to FIFO, ticket chronology, Q1, causal floor, primary/reference ontology, source/successor atomicity or sole execution owner. +- `Semantic oracle`: A0 SHA-256 `8746e423168309975e509be91e4eb06fdf0eb0b43369d75f1ee62202a4341678` remains frozen; use only for relevant semantic equivalence, never as a Weight oracle. + +## Context and Hypothesis + +This Leaf owns zero-result predicate traversal ownership. Its boundary is the single claim above. Evidence was extracted from the explicitly marked pre-fission parent subsections at Git baseline `a60de7748132211d9e58cecd5be82b41f76ce230`; this migration changes navigation and evidence ownership only. Source, Wasm, commands, rejection outcomes and evidence limitations retain their original meaning. + +Extraction provenance at the recorded Git baseline: EXP-0028 / “Full-Contract Opening Predicate Capture”. Original directional prose (above/below) belongs to these pre-fission subsections; current cross-owner navigation follows the parent proof map. + +## Baseline and Candidates + +Retained measured implementations and rejected candidates are identified in the extracted evidence. The deciding candidate is the unchanged generated fixed owner `opening_predicate_traversal`: a full bounded Contract whose predicates are all Current produces zero Opening results while traversing the complete predicate structure. `RuntimeStepControlWeight::opening_predicate_weight` selects it only at zero; nonzero counts select the independent capture envelope. + +## Controls + +Only the declared witness input varies within each source-bound comparison. Preserve the exact host, toolchain, database, Wasm and sampling controls printed with each evidence item; do not compare unlike artifacts as a controlled performance result. + +## Benchmark Class, Workloads, and Criteria + +Acceptance requires the single claim to hold over its declared admitted domain, with each physical operation charged once and every retained limitation resolved before broader production use. One legal omitted-owner, domain or containment counterexample falsifies that claim. Existing workloads below are bounded witnesses, not permission to add independent questions. + +## Environment and Artifact Identity + +Pre-fission baseline `a60de7748132211d9e58cecd5be82b41f76ce230`. Each extracted block retains its own exact source/Wasm/output identity. Shared pinned hardware/toolchain/cache controls follow [EXP-0030](./EXP-0030.md#environment-and-artifact-identity) only where the original comparison explicitly reuses them. A shared environment never substitutes for the block’s own source or Wasm identity. No runtime artifact was rebuilt during migration. + +## Proof Obligations + +- `Freeze`: Frozen by explicit migration reconstruction from the pre-fission decision and retained witnesses at a60de7748132211d9e58cecd5be82b41f76ce230. No new measurement or historical decision is implied. +- `Review triggers`: None. +- `Decomposition review`: Boundary reviewed during provenance-preserving fission. Workloads within this Leaf share the declared claim, physical owner, changed variable, evidence domain and acceptance rule. Independently decidable owners were transferred; a new owner or selector requires another node. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | Zero-result predicate traversal selects one generated owner without adding it to inclusive or nonzero capture paths. | A full Contract with all predicates Current returns zero Opening results but still performs traversal | Reference-Wasm FRAME evidence in the declared owner/domain; native checks are construction controls | Self | Required | Named parent decision and declared downstream consumers | Satisfied for the reference binding: one generated fixed owner covers the full zero-result traversal and is mutually exclusive with nonzero capture and inclusive direct owners | + +## Measurements + + +### Pre-fission EXP-0028 lines 1144–1151 + +The zero-result traversal omission can be corrected independently of that timing decision. Add the fixed production profile `opening_predicate_traversal`, measuring the same full Contract with all predicates Current and verifying zero results. This obtains a generated baseline without inheriting the mixed count regression's phantom zero-count proof intercept or manually editing coefficients. It is an alternative selector for zero captures, never an additive charge on a nonzero capture or an already complete direct Opening owner. Prepared source content/modes `3cea4c21d930b0e4ffed1af63932e81b8726b31a3570b7746d8d3f0828023f53`, benchmark source `72028636ba105e987ccaa20e54cef378d33cb26ee2933dd83b67d28a8bea105f`; measure with `scripts/benchmarks.sh --extrinsic opening_predicate_traversal --output /tmp/deos-exp28-opening-traversal.rs --json-file /tmp/deos-exp28-opening-traversal.json pallet_deos_actors`, retaining ordinary 50 × 20 controls. + +The fixed traversal passes on benchmark Wasm `7d738372e8d46ff84a66130e691ad6b1f0ebafb22a1e5b583515a4e6c04c9345` (104-second build, 13-second run). Its 61,320 timing/3,066 DB samples have minimum/upper-median/p95/maximum `488 / 1,885 / 1,956 / 27,239 ns`; all DB samples have zero reads, writes and recorded proof. The generated owner is `Weight::from_parts(1_885_000, 0)` with zero estimated proof; generated/raw SHA-256 `aad7e8d8fe3f73978111fd2f6f60ebc50367f476570f0100157ceb18b06de847` / `565f0ba1b2a57ce2270db0268170d4a07fb44ea6d4deb7a9b592a4daca88d89e`. Its unchanged generated method is staged in the runtime. The required Weight interface and full-generation contract include it; generic package implementations remain explicit non-production estimates. + +`RuntimeStepControlWeight::opening_predicate_weight` selects this owner at zero and the existing capture component otherwise. Composed maximum adds the zero-result traversal only at cursor zero; composed actual adds it only in Opening. Existing direct aggregate selection remains unchanged and does not receive an additive traversal. V7 binds the zero-result selector input alongside the nonzero captures and Current inputs. Provider regression verifies replacement rather than double charge and maximum containment. Reference default runtime tests expose only four stale numeric expectations; their corrections add exactly `1,885,000 ps` with no proof change, and the subsequent Actors integration suite passes. Direct, nonzero and complete admission coverage remain open; neither the new count models nor the categorical composition fit is installed as an accepted complete bound. + +Validation: native full-Opening/composition and fixed-traversal benchmarks pass, as do the final generic Weight-interface parity test and independent embedding fixture. The full default runtime run passed 489 tests and identified the four affected numeric expectations; all Actors integration tests pass after their exact traversal-only correction. Final workspace/all-target/all-feature Clippy passes after replacing an unnecessary `Clone` with `Copy` in fixture setup; this and the comment clarification preserve measured input values and capture execution. Exact comparison verifies the staged traversal method against generated output. Record normalization/dependency DAG, Wiki/context checks and changed-scope completion pass; G5 exact artifacts and complete G4 coverage remain open. + + +## Derived Metrics + +Derived values are retained beside the evidence that defines their source and units. No subtraction of overlapping aggregate owners or cross-artifact timing comparison is introduced. + +## Statistical / Variance Notes + +Retain each item’s original sampling and uncertainty. Singleton construction probes establish only their admitted state/access witness, never statistical timing dominance or a production bound. + +## Result + +The retained reference-Wasm evidence closes the zero-result predicate traversal owner. The fixed benchmark calls the real `capture_opening_predicate_results` path over a full bounded Contract with every authored predicate classified Current, asserts an empty result vector, and therefore isolates structural Step/clause/predicate traversal without Opening host evaluation. Across 61,320 timing samples and 3,066 database samples, the upper median is `1,885 ns`; every database sample records zero reads, writes and proof. The unchanged generated owner is `Weight::from_parts(1_885_000, 0)`. + +The installed generated method still has that exact value in current staged Weight identity `2d9c60ac7764d1e7dc366531dd4a409444daf66cf3e37ed7feb922bb614e8f1a`. Current provider identity `0858133d39eaabb1d2591def2454f1b72d7ee6746b3c1f5ecd720f49a37750f9` selects it only when the Opening predicate result count is zero. Composed maximum reserves it only for the Opening head; composed actual charges it only in Opening. The focused replacement regression confirms maximum containment and that a nonzero capture replaces, rather than adds to, traversal. + +## Interpretation + +The traversal has its own fixed physical owner because zero Opening results do not imply zero structural work. Its direct full-Contract benchmark, zero database topology and mutually exclusive selector are sufficient for this bounded question without rerunning nonzero host capture or complete Opening. Raw maximum wall time is not a consensus coefficient, and the generated median owner is not combined with the nonzero capture intercept. + +## Decision + +Accepted for the reference binding. When `opening_predicate_results == 0` in an Opening head, select exactly `opening_predicate_traversal()` at `1,885,000` RefTime / zero ProofSize; for every nonzero count, select only the accepted nonzero predicate capture envelope. Maximum and actual use the same partition, and inclusive direct Opening owners receive no additive traversal. EXP-0065 owns complete final-tree regeneration and artifact identity. + +## Rejected Alternatives and Tradeoffs + +Reject omitting traversal because the result vector is empty, reusing a nonzero capture intercept with phantom proof, adding traversal to nonzero capture, and adding it to inclusive direct Opening owners. Retain raw timing tails as observations rather than manually padding the generated fixed coefficient. + +## Validity + +- `Establishes`: One generated zero-result predicate traversal owner over the reference full-Contract structural domain, selected mutually exclusively from nonzero capture in composed maximum and actual Opening Control Weight. +- `Does not establish`: Nonzero predicate capture outside EXP-0033, a complete Opening transition, whole-Actor admission, arbitrary embedding hosts, exact final production Weight identity or integrated throughput. +- `Known limitations`: The method is an unchanged staged fragment inside a not-yet-regenerated complete Actors Weight file; EXP-0064/EXP-0065 retain admission and exact-artifact closure. +- `Invalidation triggers`: A changed predicate traversal body, Contract/predicate structural bound, zero/nonzero selector, generated method, source/Wasm identity or acceptance rule requires scoped requalification. +- `Residual qualitative insight`: Empty captured state can retain bounded structural execution and therefore needs a measured alternative owner rather than an additive surcharge. + +## Follow-Up / Next Gradient + +Stop measuring this satisfied owner. EXP-0033 owns nonzero predicate capture, EXP-0064 composes admission, and EXP-0065 regenerates exact artifacts. Reopen only for the declared falsifier or a changed relevant bound, path, selector or identity. + +## Architecture Impact + +The existing generated fixed method and zero/nonzero selector are accepted as the scoped reference predicate-traversal input. No new runtime, semantic or storage change is introduced by this decision; C1 and A0 remain frozen, and complete production regeneration remains G5 work. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: [EXP-0028](./EXP-0028.md) O6 +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: [EXP-0025](./EXP-0025.md) +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: None. +- `Transfers question to`: None; independent findings require an explicit new transfer. +- `Produces input for`: [EXP-0028](./EXP-0028.md) +- `Reopen trigger`: A falsifier of this declared owner/domain or exact artifact identity; cost alone does not reopen frozen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0036.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0036.md new file mode 100644 index 00000000..bfd494de --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0036.md @@ -0,0 +1,136 @@ +# EXP-0036 — Zero-Result Amount Traversal Ownership + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | [EXP-0028](./EXP-0028.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-06 | +| Affected domain | Zero-result amount traversal selects one generated owner without adding it to inclusive or nonzero capture paths. | +| Physical mechanism | Zero-Result Amount Traversal Ownership | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0025](./EXP-0025.md) — Accepted, physical architecture only | +| Successor experiment | [EXP-0028](./EXP-0028.md) | + + +## Decision Question + +Zero-result amount traversal selects one generated owner without adding it to inclusive or nonzero capture paths. + +## Governing Specification + +- `Owners`: `template/pallets/actors/docs/specification.en.md` (sole ownership, lifecycle/Trigger/Pipeline/Action semantics, placement and scheduler); `docs/actors-resource-policy.specification.en.md` (component-wise admission/actual settlement, equal-thirds block policy); `docs/actors-performance-assurance.specification.en.md` (evidence and target). +- `Frozen physical input`: [EXP-0025](./EXP-0025.md) C1 Architecture Freeze. No change to FIFO, ticket chronology, Q1, causal floor, primary/reference ontology, source/successor atomicity or sole execution owner. +- `Semantic oracle`: A0 SHA-256 `8746e423168309975e509be91e4eb06fdf0eb0b43369d75f1ee62202a4341678` remains frozen; use only for relevant semantic equivalence, never as a Weight oracle. + +## Context and Hypothesis + +This Leaf owns zero-result amount traversal ownership. Its boundary is the single claim above. Evidence was extracted from the explicitly marked pre-fission parent subsections at Git baseline `a60de7748132211d9e58cecd5be82b41f76ce230`; this migration changes navigation and evidence ownership only. Source, Wasm, commands, rejection outcomes and evidence limitations retain their original meaning. + +Extraction provenance at the recorded Git baseline: EXP-0028 / “Opening Amount-Surface Traversal Preparation”. Original directional prose (above/below) belongs to these pre-fission subsections; current cross-owner navigation follows the parent proof map. + +## Baseline and Candidates + +Retained measured implementations and rejected candidates are identified in the extracted evidence. The deciding candidate is the unchanged generated fixed owner `opening_snapshot_traversal`: a full bounded Contract whose amount surfaces are all Fixed produces an empty Opening snapshot while traversing both authored amount classifiers per Step. `RuntimeStepControlWeight::opening_snapshot_weight` selects it only at zero; nonzero counts select the independent capture envelope. + +## Controls + +Only the declared witness input varies within each source-bound comparison. Preserve the exact host, toolchain, database, Wasm and sampling controls printed with each evidence item; do not compare unlike artifacts as a controlled performance result. + +## Benchmark Class, Workloads, and Criteria + +Acceptance requires the single claim to hold over its declared admitted domain, with each physical operation charged once and every retained limitation resolved before broader production use. One legal omitted-owner, domain or containment counterexample falsifies that claim. Existing workloads below are bounded witnesses, not permission to add independent questions. + +## Environment and Artifact Identity + +Pre-fission baseline `a60de7748132211d9e58cecd5be82b41f76ce230`. Each extracted block retains its own exact source/Wasm/output identity. Shared pinned hardware/toolchain/cache controls follow [EXP-0030](./EXP-0030.md#environment-and-artifact-identity) only where the original comparison explicitly reuses them. A shared environment never substitutes for the block’s own source or Wasm identity. No runtime artifact was rebuilt during migration. + +## Proof Obligations + +- `Freeze`: Frozen by explicit migration reconstruction from the pre-fission decision and retained witnesses at a60de7748132211d9e58cecd5be82b41f76ce230. No new measurement or historical decision is implied. +- `Review triggers`: None. +- `Decomposition review`: Boundary reviewed during provenance-preserving fission. Workloads within this Leaf share the declared claim, physical owner, changed variable, evidence domain and acceptance rule. Independently decidable owners were transferred; a new owner or selector requires another node. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | Zero-result amount traversal selects one generated owner without adding it to inclusive or nonzero capture paths. | A full Contract with only current/fixed amounts returns zero Opening surfaces but still performs traversal | Reference-Wasm FRAME evidence in the declared owner/domain; native checks are construction controls | Self | Required | Named parent decision and declared downstream consumers | Satisfied for the reference binding: one generated fixed owner covers the full zero-result traversal and is mutually exclusive with nonzero capture and inclusive direct owners | + +## Measurements + + +### Opening Amount-Surface Traversal Preparation + +On `bfa890a`, `execute_opening_step_direct` unconditionally calls `capture_opening_snapshot`, whose `opening_surfaces` visits every Step even when no amount uses `PercentageAtOpening`. Composed maximum/actual currently omit `opening_snapshot_capture` at zero entries. `current_step_plan_opening_head` measures loading the plan, and Run persistence measures storing the result; neither owns this traversal. Direct complete Opening owners remain inclusive and must not receive a second additive charge. + +The smallest correction candidate is extra `opening_snapshot_traversal`: a full `MaxContractSteps` vector of structurally valid two-asset AddLiquidity Steps with Fixed amounts, returning an empty snapshot through the real capture owner. It exercises both amount classifiers per Step without balance reads, Task effects or claims of full-Actor resource admission. Existing nonzero capture uses a shorter Step prefix and remains an independently unqualified full-Contract amount-surface bound. PreservableAsset, TargetAsset and StakingShares host coverage is not inferred from the zero-result point. + +Prepared source content/modes `f207274a05c87ee05d0ca0a7c643b62997ebf5950bc2d51931deefedc16f03c4`; benchmark source SHA-256 `3560df6852a679b9b20da885e6a28295818bb5afe1237e3765c4ac67039ec71c`. The additional FRAME benchmark crosses the macro test expansion's recursion limit of 256; raising the Rust expansion limit to 512 restores compilation without adding a runtime bound or changing storage. Measure through `scripts/benchmarks.sh --extra --extrinsic opening_snapshot_traversal --output /tmp/deos-exp28-opening-amount-traversal.rs --json-file /tmp/deos-exp28-opening-amount-traversal.json pallet_deos_actors`, with unchanged 50 × 20/reference map/cache/heap controls and no competing build or measurement. A nonzero measured traversal selects a zero-entry alternative owner; retain generated output unchanged and do not add it to nonzero capture or inclusive direct paths. + +The run passes on benchmark Wasm `473fe0f55eb1178af0a5cfd95a69e323576356b4f85301642e43897af24a8d55` (109-second build, 13-second measurement). All 51,940 timing and 2,597 DB samples have zero reads/writes/repeated accesses/recorded proof. Minimum/upper-median/p95/maximum times are `419 / 1,467 / 1,956 / 24,725 ns`; no samples are removed. The normalized generated owner is `Weight::from_parts(1_467_000, 0)` with zero estimated proof. Generated/raw SHA-256 are `46fcabccd669be14e1895946238ecca3dbf042f3e588c00b0f69c663c98f5b0f` / `c26651d955f98c3cd2d81cf35e6e22bf1cd314e6a9679835528713f63d54f12d`. The model is FRAME execution-cost evidence, not a maximum wall-clock latency claim. + +Select this generated method unchanged as the zero-entry alternative in `RuntimeStepControlWeight::opening_snapshot_weight`. Composed maximum reserves it only for head/Opening context, and composed actual charges it only in Opening. Nonzero snapshot and direct complete owners remain unchanged. V8 binds the new zero input along with all existing amount and predicate inputs. Promote the identical measured profile from extra to regular generation and require its Weight interface; generic fallback implementations remain non-production estimates. The shared empty-capture selector regression checks replacement and maximum containment for both amount and predicate captures. Full nonzero amount-surface/Contract traversal and production artifact acceptance remain open. + +Retained binding source content/modes `9328497780948398fc7a32628878b44980796051d7a798ee05faf68f9a9d0637`; benchmark source `ac18446df80504f5073abaf02706fe810bba048b0731531b3f7b98fdab4e0dfe`; provider `7581b15a9407a17d551e679a86e5db71799c568c31e9fc5adb70d6543af34823`; staged runtime Weight file `f0cb6b0ae401a244e3007b88b1b3a4e7a13430674879488999c41f9e7a6cef37`. Exact text comparison verifies the new generated method. On pinned Rust 1.94.1 with `SKIP_WASM_BUILD=1`, the full default runtime suite passes 493 tests/22 ignored. Six numeric fixture corrections add exactly `1,467,000 ps` to affected composed Opening reservations/actuals; proof, effects, direct paths and resumed Steps retain their expectations. An earlier invocation from the repository root used unpinned Rust 1.98 and is excluded from acceptance. Final native all-feature snapshot tests pass four cases, generic Weight-interface parity passes one, and independent embedding with `dex-fixture,try-runtime` passes 27. Workspace/all-target/all-feature Clippy, canonical formatting, record normalization and changed-scope completion pass. Wiki overview claims require no change for this selector detail; the complete G4/G5 release gates remain open. + + +## Derived Metrics + +Derived values are retained beside the evidence that defines their source and units. No subtraction of overlapping aggregate owners or cross-artifact timing comparison is introduced. + +## Statistical / Variance Notes + +Retain each item’s original sampling and uncertainty. Singleton construction probes establish only their admitted state/access witness, never statistical timing dominance or a production bound. + +## Result + +The retained reference-Wasm evidence closes the zero-result amount traversal owner. The fixed benchmark calls the real `capture_opening_snapshot` path over `MaxContractSteps` structurally valid two-asset AddLiquidity Steps whose two amounts are Fixed, asserts an empty snapshot, and therefore isolates both authored amount classifiers per Step without host-balance reads. Across 51,940 timing samples and 2,597 database samples, the upper median is `1,467 ns`; every database sample records zero reads, writes, repeated accesses and proof. The unchanged generated owner is `Weight::from_parts(1_467_000, 0)`. + +The installed generated method still has that exact value in current staged Weight identity `2d9c60ac7764d1e7dc366531dd4a409444daf66cf3e37ed7feb922bb614e8f1a`. Current provider identity `0858133d39eaabb1d2591def2454f1b72d7ee6746b3c1f5ecd720f49a37750f9` selects it only when the Opening snapshot entry count is zero. Composed maximum reserves it only for the Opening head; composed actual charges it only in Opening. The focused replacement regression confirms maximum containment and that a nonzero capture replaces, rather than adds to, traversal. + +## Interpretation + +The traversal has its own fixed physical owner because an empty Opening snapshot does not imply zero structural work. Its direct full-Contract benchmark, zero database topology and mutually exclusive selector are sufficient for this bounded question without rerunning nonzero host capture or complete Opening. Raw maximum wall time is not a consensus coefficient, and the generated median owner is not combined with a nonzero capture intercept. + +## Decision + +Accepted for the reference binding. When `opening_snapshot_entries == 0` in an Opening head, select exactly `opening_snapshot_traversal()` at `1,467,000` RefTime / zero ProofSize; for every nonzero count, select only the accepted nonzero amount capture envelope. Maximum and actual use the same partition, and inclusive direct Opening owners receive no additive traversal. EXP-0065 owns complete final-tree regeneration and artifact identity. + +## Rejected Alternatives and Tradeoffs + +Reject omitting traversal because the snapshot is empty, reusing a nonzero capture intercept, adding traversal to nonzero capture, and adding it to inclusive direct Opening owners. Retain raw timing tails as observations rather than manually padding the generated fixed coefficient. + +## Validity + +- `Establishes`: One generated zero-result amount traversal owner over the reference full-Contract structural domain, selected mutually exclusively from nonzero capture in composed maximum and actual Opening Control Weight. +- `Does not establish`: Nonzero amount capture outside EXP-0034, host-ledger coverage, a complete Opening transition, whole-Actor admission, arbitrary embedding hosts, exact final production Weight identity or integrated throughput. +- `Known limitations`: The method is an unchanged staged fragment inside a not-yet-regenerated complete Actors Weight file; EXP-0064/EXP-0065 retain admission and exact-artifact closure. +- `Invalidation triggers`: A changed amount-surface traversal body, Contract/amount structural bound, zero/nonzero selector, generated method, source/Wasm identity or acceptance rule requires scoped requalification. +- `Residual qualitative insight`: Empty captured state can retain bounded structural execution and therefore needs a measured alternative owner rather than an additive surcharge. + +## Follow-Up / Next Gradient + +Stop measuring this satisfied owner. EXP-0034 owns nonzero amount capture, EXP-0064 composes admission, and EXP-0065 regenerates exact artifacts. Reopen only for the declared falsifier or a changed relevant bound, path, selector or identity. + +## Architecture Impact + +The existing generated fixed method and zero/nonzero selector are accepted as the scoped reference amount-traversal input. No new runtime, semantic or storage change is introduced by this decision; C1 and A0 remain frozen, and complete production regeneration remains G5 work. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: [EXP-0028](./EXP-0028.md) O7 +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: [EXP-0025](./EXP-0025.md) +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: None. +- `Transfers question to`: None; independent findings require an explicit new transfer. +- `Produces input for`: [EXP-0028](./EXP-0028.md) +- `Reopen trigger`: A falsifier of this declared owner/domain or exact artifact identity; cost alone does not reopen frozen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0037.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0037.md new file mode 100644 index 00000000..09ebad82 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0037.md @@ -0,0 +1,169 @@ +# EXP-0037 — User and System Zero-Step Base Ownership + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | [EXP-0028](./EXP-0028.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-06 | +| Affected domain | The User/System zero-Step base boundary includes its admitted head, collection and hold work without borrowing a System-only bound for User execution. | +| Physical mechanism | User and System Zero-Step Base Ownership | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0025](./EXP-0025.md) — Accepted, physical architecture only | +| Successor experiment | [EXP-0028](./EXP-0028.md) | + + +## Decision Question + +The User/System zero-Step base boundary includes its admitted head, collection and hold work without borrowing a System-only bound for User execution. + +## Governing Specification + +- `Owners`: `template/pallets/actors/docs/specification.en.md` (sole ownership, lifecycle/Trigger/Pipeline/Action semantics, placement and scheduler); `docs/actors-resource-policy.specification.en.md` (component-wise admission/actual settlement, equal-thirds block policy); `docs/actors-performance-assurance.specification.en.md` (evidence and target). +- `Frozen physical input`: [EXP-0025](./EXP-0025.md) C1 Architecture Freeze. No change to FIFO, ticket chronology, Q1, causal floor, primary/reference ontology, source/successor atomicity or sole execution owner. +- `Semantic oracle`: A0 SHA-256 `8746e423168309975e509be91e4eb06fdf0eb0b43369d75f1ee62202a4341678` remains frozen; use only for relevant semantic equivalence, never as a Weight oracle. + +## Context and Hypothesis + +This Leaf owns user and system zero-step base ownership. Its boundary is the single claim above. Evidence was extracted from the explicitly marked pre-fission parent subsections at Git baseline `a60de7748132211d9e58cecd5be82b41f76ce230`; this migration changes navigation and evidence ownership only. Source, Wasm, commands, rejection outcomes and evidence limitations retain their original meaning. + +Extraction provenance at the recorded Git baseline: EXP-0028 / “Maximum Zero-Step Head and Window Reference”, “User Zero-Step Comparison”. Original directional prose (above/below) belongs to these pre-fission subsections; current cross-owner navigation follows the parent proof map. + +## Baseline and Candidates + +Retained measured implementations and rejected candidates are identified in the extracted evidence. The deciding candidate partitions zero-Step base ownership by actor class: System retains its complete System owner, while User selects the complete maximum-head User owner that already includes exactly one Pipeline collection and the applicable persistent hold reconciliation. Temporal rearm, terminal cleanup, variable Run/hold resizing and outer source/Waiting work remain in their independent owners. + +## Controls + +Only the declared witness input varies within each source-bound comparison. Preserve the exact host, toolchain, database, Wasm and sampling controls printed with each evidence item; do not compare unlike artifacts as a controlled performance result. + +## Benchmark Class, Workloads, and Criteria + +Acceptance requires the single claim to hold over its declared admitted domain, with each physical operation charged once and every retained limitation resolved before broader production use. One legal omitted-owner, domain or containment counterexample falsifies that claim. Existing workloads below are bounded witnesses, not permission to add independent questions. + +## Environment and Artifact Identity + +Pre-fission baseline `a60de7748132211d9e58cecd5be82b41f76ce230`. Each extracted block retains its own exact source/Wasm/output identity. Shared pinned hardware/toolchain/cache controls follow [EXP-0030](./EXP-0030.md#environment-and-artifact-identity) only where the original comparison explicitly reuses them. A shared environment never substitutes for the block’s own source or Wasm identity. + +The deciding maximum-head rerun is shared with [EXP-0031](./EXP-0031.md#deciding-current-reference-wasm-witness): code baseline `e92ab80`, benchmark source SHA-256 `56178d1e89c600bed20dc737e9fe96376929c9d41c00cd0cd9586dea66d8bdba`, unchanged scheduler SHA-256 `bdedd82571a229f3609194547c6256d8ca454055640e407c2c9af03d2245fbd1`, unchanged pallet root SHA-256 `7432b3b5df56cf7507d0991270b62a5a35e4437a2548e81bd6bd66bb57d6c788`, benchmark script SHA-256 `303d94b87dd04514118764b96c8c064da758337cc044cd57ba59098cdf292632` and benchmark Wasm SHA-256 `cb901d049e328d2d1074fabf3f3583e6800526e7ba9b4188f8a258ee04e0a1a6`. Strengthened postconditions check exact Pipeline payer/Sink deltas, exactly one receipt and persistent zero-Step hold identity outside the measured block. + +## Proof Obligations + +- `Freeze`: Frozen by explicit migration reconstruction from the pre-fission decision and retained witnesses at a60de7748132211d9e58cecd5be82b41f76ce230. No new measurement or historical decision is implied. +- `Review triggers`: None. +- `Decomposition review`: Boundary reviewed during provenance-preserving fission. Workloads within this Leaf share the declared claim, physical owner, changed variable, evidence domain and acceptance rule. Independently decidable owners were transferred; a new owner or selector requires another node. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | The User/System zero-Step base boundary includes its admitted head, collection and hold work without borrowing a System-only bound for User execution. | An ordinary Manual User zero-Step Opening whose inclusive cost exceeds the System-only bound, then the attained maximum empty head | Reference-Wasm FRAME evidence in the declared owner/domain; native checks are construction controls | Self | Required | Named parent decision and declared downstream consumers | Satisfied for the reference Manual domain: the actor-class partition and maximum-head User owner cover collection and applicable hold work exactly once | + +## Measurements + + +### Maximum Zero-Step Head and Window Reference + +The admitted User fixture `scheduler_inner_zero_step_user_max_head` reaches the [EXP-0029 byte maximum](./EXP-0052.md#derived-metrics) with full AddressEvent filters and SignedAllowlist, a present future window and terminal nonce, and no Steps. Certified signed ingress latches readiness through the real fee path; tracked and accumulated LastFunding remain empty despite custody ingress. The measured region is the complete inner Opening after canonical primary consumption. It verifies one nonzero Pipeline receipt, exact sovereign/Fee Sink deltas, Idle nonce one, no latch/queue/Run and a coherent retained Block reference at window end plus one. Native TryRuntime checks cover readiness and final holds/topology. + +| Profile | RefTime before DB additions / minimum (ps) | Estimated / recorded proof (bytes) | Reads / writes | Generated fragment SHA-256 | +| --- | --- | --- | --- | --- | +| `scheduler_inner_zero_step_user_max_head` | 157,286,000 / 152,186,000 | 9,047 / 3,107 | 11 / 5 | `6e642560b0cf00b045719ac2644ed4d8c175cf01c3a609b6cac840f1b644be97` | + +Source content/modes `727e86d3adfc413bbfddccdcfbcba9aa85fd63115addff41f70212a8c6fda888`; benchmark source SHA-256 `f2874a97b7d37c66699e15a06b360237731aecd4f5b7b40978010d7b7b56bd9f`; benchmark Wasm SHA-256 `4e83d5ac570c63dfd64e43eb02284b95762b0957990ce2cf58d383ba42a7f437`. Reproduce with `scripts/benchmarks.sh --extrinsic scheduler_inner_zero_step_user_max_head --output /tmp/deos-exp28-zero-step-max-head.rs pallet_deos_actors`. Fresh build took 96 seconds, generation 13 seconds, with 50 steps × 20 repeats and the same pinned reference hardware/toolchain/cache/heap/map controls as above. No concurrent agent build/test/benchmark job ran during measurement. The fragment remains temporary, uninstalled and excluded from default production generation. + +The trace includes the retained Waiting frame, activation authority, hold read and funding reset; it does not resize the hold or alter the future reference. Serialized head size is now exact for a canonical zero-Step reference Actor, while this measured point does not prove worst CPU branch, maximum Waiting page/heap context, temporal expiry, insolvency, close or observation rearm. Header size and branch coverage remain independent dimensions. The inclusive atom already contains Pipeline collection and final reconciliation; no component is added again or subtracted to fabricate a disjoint coefficient. The prior large nonempty-head collection body is unchanged apart from shared setup extraction, so its historical comparison remains applicable within its recorded identity and limits. + +Validation: focused native `max_head` and refactored `pipeline_large_head_opening_collection` all-features tests pass; fresh Wasm verifies the new profile. Actors all-features Clippy, canonical formatting, workspace check and record normalization/dependency graph pass. Completion initially rejected a 517-character changelog entry; shortening that prose preserves all source-bound evidence, and the remaining completion audits pass. Default runtime and unaffected package tests reuse prior evidence because only benchmark fixtures changed. Benchmark preflight compilation passes and still rejects the known stale generated-storage references; no production artifact acceptance follows. + + + +### User Zero-Step Comparison + +The Cadenced extension below compares admitted User zero-Step Opening, persistent versus authored auto-close nonce one. It reaches readiness by advancing the host clock and draining the original due temporal primary, without injecting trigger anchors, latches or Waiting entries. The measured consumed-primary inner boundary includes Tick rearm, collection, destination placement and User hold reconciliation, or complete terminal cleanup. The source rearms Cadenced before testing the terminal nonce, so the terminal path includes insertion followed by removal. Crossing's disabled-member removal/reinsertion and ObservationChange disablement deletion remain separately required branches; EXP-0063 qualifies competing Waiting/heap geometry. + +Ordinary admitted zero-Step System/User Actors now reach service through a real Manual latch; canonical primary consumption remains outside the inner measurement. This replaces the System fixture's injected hot state/ticket. Complete User postconditions bind one Pipeline receipt, exact sovereign/Fee Sink delta, completion nonce, absent Run and final state-hold consistency. The isolated primitive measures the same `charge_pipeline_opening` operation on an admitted empty User head without cycle execution, placement or hold reconciliation. + +| Profile | RefTime before DB additions (ps) | Estimated / recorded proof (bytes) | Reads / writes | Generated fragment SHA-256 | +| --- | --- | --- | --- | --- | +| `scheduler_inner_zero_step_complete` | 42,534,000 | 4,388 / 923 | 2 / 3 | `bf57bd276b404ef0208e5c1a26e9abd6f3fe4a99e44b67f48a408214a1f13ad4` | +| `scheduler_inner_zero_step_user_complete` | 121,177,000 | 7,529 / 1,589 | 9 / 5 | `929b032e7da25375918b19a9eb3c24bafd96a7f368ac01e4e7e6adeaa6a922f1` | + +Source content/modes `1b63a1e1399b1c7a3c37ba1a6e80d5e540ff14238cf3ce3fee1fa491378c70fe`, benchmark Wasm `93b132ad0970a0d5cbfb5c9c370b396ce26268e757ff0d2eee242a1f3bf023c6`, reference runtime, same pinned toolchain/host/cache/heap environment, 50 steps × 20 repeats, no concurrent heavy jobs. Native `ci-local.sh --only tests --package pallet-deos-actors --all-features --test-filter zero_step` passed. Benchmark compilation passed; preflight retained the existing stale-storage rejection. The System route rebuilt Wasm; User and collection reused exactly that Wasm with `--skip-build`. + +Reproduce with `scripts/benchmarks.sh --extrinsic PROFILE --output /tmp/deos-exp28-zero-step-KIND.rs pallet_deos_actors`, selecting the table's profile and KIND=`system`, `user`, or `collection`; add `--skip-build` only for the remaining profiles on the unchanged fresh benchmark runtime. All three generation routes and verification postconditions passed. Fragments remain temporary and uninstalled. The two new unbound diagnostic profiles are excluded from default production generation by the existing exact-selector mechanism; their eventual trait/production binding remains a G4/G5 decision. + +The User inner measurement already includes collection and hold reconciliation, so adding the primitive again would duplicate work. The primitive trace reads Contract head and two native accounts; complete User also probes activation authority, observation/feed/subscription and crossing metadata during hold reconciliation. Its two additional writes versus System are the fee-transfer accounts; this zero-Step case does not establish the variable Run/hold resize envelope of nonzero Pipelines. Do not derive a hold Weight or subtract ProofSize from these aggregate measurements. This comparison covers persistent Manual zero-Step only, not temporal/Crossing rearm, terminal close, arbitrary Contract-head decoding or all Trigger families. Existing zero-Step reservation/quote sites still use the older System-derived generated method and need a complete inclusive binding before acceptance. + +Retained source `bd3db85a36e11b4820bb2ea11b97418f1bf02792376d24e09b982257bbcb3f37` adds the diagnostic-selection exclusion and documentation after measurement; the measured benchmark bodies remain unchanged. Actors all-features Clippy, shell syntax, workspace check, record normalization and changed-scope completion passed. Default runtime behavioral evidence from `a5f79a0` is reused: the only production scheduler diff makes the unchanged collection method crate-visible for the benchmark; its body and production callers are unchanged. No new trait/selector/production coefficient binding is claimed. + + +### Deciding Current Reference-Wasm Witness + +The retained paired reference-Wasm comparison on one unchanged artifact establishes the class boundary: ordinary persistent Manual User zero-Step Opening measures `121,177,000` RefTime before database additions, `7,529` estimated ProofSize and 9 reads / 5 writes, versus `42,534,000`, `4,388` and 2 reads / 3 writes for System. The User atom verifies one Pipeline receipt, exact payer/Fee Sink deltas and final hold consistency; therefore the System owner cannot soundly price User execution. + +The current reference-Wasm rerun then attains the exact admitted zero-Step head maximum and generates the inclusive User owner: + +| Profile | Generated RefTime (ps) | Minimum (ps) | Estimated / recorded proof (bytes) | Reads / writes | Generated fragment SHA-256 | Raw JSON SHA-256 | +| --- | ---: | ---: | ---: | ---: | --- | --- | +| `scheduler_inner_zero_step_user_max_head` | 167,832,000 | 155,748,000 | 9,047 / 3,107 | 11 / 5 | `7ae7922ebb1f2649b7547a884575121b6ab8de785d6fc51ac3a69297d2546630` | `f9ab67145ea405c93a704a68d03f38ae21cc1ab9b7de0a0e1f8829ae838b2c37` | + +This rerun used 50 steps × 20 repeats and the pinned compiled-Wasm, heap, cache and map controls. The benchmark body reaches the exact zero-Step header `MaxEncodedLen`, including full AddressEvent filters, SignedAllowlist, future window and terminal nonce; postconditions preserve one collection, no Run and coherent state-hold/reference topology. The complete owner already includes collection and applicable persistent hold reconciliation, so neither may be added again. Its fragment remains uninstalled; EXP-0065 owns complete regeneration. + +## Derived Metrics + +Derived values are retained beside the evidence that defines their source and units. No subtraction of overlapping aggregate owners or cross-artifact timing comparison is introduced. + +## Statistical / Variance Notes + +Retain each item’s original sampling and uncertainty. Singleton construction probes establish only their admitted state/access witness, never statistical timing dominance or a production bound. + +## Result + +The controlled System/User witness rejects the installed System-derived zero-Step owner for User execution. The current maximum-head witness attains the exact admitted zero-Step header bound and generates `167,832,000` RefTime / `9,047` ProofSize plus 11 reads and 5 writes as the complete persistent Manual User inner owner, including one Pipeline collection and applicable hold reconciliation. + +## Interpretation + +Actor class is a required zero-Step selector dimension: User work adds native ledger collection and hold reconciliation that the System path does not perform. The maximum-head User witness closes this Leaf because the claimed Manual domain is attained; temporal/Crossing rearm, close, source/Waiting geometry and variable nonzero-Pipeline holds are separately owned and do not require inflating or reopening this base owner. + +## Decision + +Accepted for the reference Manual binding. System selects the complete System zero-Step owner; User selects the complete maximum-head owner at `167,832,000` RefTime / `9,047` ProofSize plus 11 reads and 5 writes. Maximum and actual preserve this actor-class partition. The User owner includes exactly one Pipeline collection and applicable persistent hold reconciliation; add neither again. EXP-0065 owns complete generated-file regeneration and exact production artifact identity. + +## Rejected Alternatives and Tradeoffs + +Reject borrowing the smaller System owner for User execution, selecting the ordinary empty-head User point as the admitted maximum, adding collection or hold reconciliation again, and deriving disjoint owners by subtracting aggregate profiles. Do not fold temporal, terminal, Crossing, source/Waiting or variable Run/hold owners into this Manual base. + +## Validity + +- `Establishes`: A reference Manual zero-Step actor-class partition and exact maximum-head User owner including one Pipeline collection and applicable persistent hold reconciliation. +- `Does not establish`: Temporal/Crossing rearm, terminal cleanup, outer source/Waiting work, variable nonzero-Pipeline hold resizing, arbitrary hosts, exact final production Weight identity or integrated throughput. +- `Known limitations`: The diagnostic User and System fragments remain uninstalled, and the shared installed method is stale until complete EXP-0065 regeneration; EXP-0064 retains admission composition. +- `Invalidation triggers`: A changed zero-Step body, actor-class selector, admitted head bound, collection/hold path, source/Weight/Wasm identity or acceptance rule requires scoped requalification. +- `Residual qualitative insight`: A zero-Step Contract has no Task body but User control still has class-specific ledger and hold work. + +## Follow-Up / Next Gradient + +Stop measuring this satisfied Manual owner. EXP-0038/EXP-0039 own temporal and Crossing rearm, EXP-0044 owns variable state-hold geometry, EXP-0063 owns Waiting composition, EXP-0064 composes admission and EXP-0065 regenerates exact artifacts. Reopen only for the declared falsifier or a changed relevant bound, path, selector or identity. + +## Architecture Impact + +The reference Manual zero-Step input now requires an explicit System/User ownership partition and the maximum-head complete User owner. No runtime, semantic or storage change is introduced by this decision; C1 and A0 remain frozen, and complete production regeneration remains G5 work. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: [EXP-0028](./EXP-0028.md) O8 +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: [EXP-0025](./EXP-0025.md) +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: [EXP-0031](./EXP-0031.md) current maximum-head reference-Wasm rerun. +- `Transfers question to`: None; independent findings require an explicit new transfer. +- `Produces input for`: [EXP-0028](./EXP-0028.md) +- `Reopen trigger`: A falsifier of this declared owner/domain or exact artifact identity; cost alone does not reopen frozen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0038.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0038.md new file mode 100644 index 00000000..d3ac118d --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0038.md @@ -0,0 +1,170 @@ +# EXP-0038 — Temporal Zero-Step Opening Ownership + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | [EXP-0028](./EXP-0028.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-06 | +| Affected domain | Temporal zero-Step Opening accounts for the admitted Cadenced/AtTime persistence and authored-close branches exactly once. | +| Physical mechanism | Temporal Zero-Step Opening Ownership | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0025](./EXP-0025.md) — Accepted, physical architecture only | +| Successor experiment | [EXP-0028](./EXP-0028.md) | + + +## Decision Question + +Temporal zero-Step Opening accounts for the admitted Cadenced/AtTime persistence and authored-close branches exactly once. + +## Governing Specification + +- `Owners`: `template/pallets/actors/docs/specification.en.md` (sole ownership, lifecycle/Trigger/Pipeline/Action semantics, placement and scheduler); `docs/actors-resource-policy.specification.en.md` (component-wise admission/actual settlement, equal-thirds block policy); `docs/actors-performance-assurance.specification.en.md` (evidence and target). +- `Frozen physical input`: [EXP-0025](./EXP-0025.md) C1 Architecture Freeze. No change to FIFO, ticket chronology, Q1, causal floor, primary/reference ontology, source/successor atomicity or sole execution owner. +- `Semantic oracle`: A0 SHA-256 `8746e423168309975e509be91e4eb06fdf0eb0b43369d75f1ee62202a4341678` remains frozen; use only for relevant semantic equivalence, never as a Weight oracle. + +## Context and Hypothesis + +This Leaf owns temporal zero-step opening ownership. Its boundary is the single claim above. Evidence was extracted from the explicitly marked pre-fission parent subsections at Git baseline `a60de7748132211d9e58cecd5be82b41f76ce230`; this migration changes navigation and evidence ownership only. Source, Wasm, commands, rejection outcomes and evidence limitations retain their original meaning. + +Extraction provenance at the recorded Git baseline: EXP-0028 / “Cadenced Zero-Step and Authored Close”, “AtTime Zero-Step Consumption and Authored Immutable Close”. Original directional prose (above/below) belongs to these pre-fission subsections; current cross-owner navigation follows the parent proof map. + +## Baseline and Candidates + +Retained measured implementations and rejected candidates are identified in the extracted evidence. The deciding reference binding partitions temporal zero-Step Opening by Trigger family and terminal disposition: Mutable persistence and authored nonce closure each select one complete inclusive AtTime or Cadenced owner. The consumed due occurrence, outer Waiting/cursor/heap work and unrelated terminal families remain outside this inner selector. + +## Controls + +Only the declared witness input varies within each source-bound comparison. Preserve the exact host, toolchain, database, Wasm and sampling controls printed with each evidence item; do not compare unlike artifacts as a controlled performance result. + +## Benchmark Class, Workloads, and Criteria + +Acceptance requires the single claim to hold over its declared admitted domain, with each physical operation charged once and every retained limitation resolved before broader production use. One legal omitted-owner, domain or containment counterexample falsifies that claim. Existing workloads below are bounded witnesses, not permission to add independent questions. + +## Environment and Artifact Identity + +Pre-fission baseline `a60de7748132211d9e58cecd5be82b41f76ce230`. Each extracted block retains its own exact source/Wasm/output identity. Shared pinned hardware/toolchain/cache controls follow [EXP-0030](./EXP-0030.md#environment-and-artifact-identity) only where the original comparison explicitly reuses them. A shared environment never substitutes for the block’s own source or Wasm identity. No runtime artifact was rebuilt during migration. + +## Proof Obligations + +- `Freeze`: Frozen by explicit migration reconstruction from the pre-fission decision and retained witnesses at a60de7748132211d9e58cecd5be82b41f76ce230. No new measurement or historical decision is implied. +- `Review triggers`: None. +- `Decomposition review`: Boundary reviewed during provenance-preserving fission. Workloads within this Leaf share the declared claim, physical owner, changed variable, evidence domain and acceptance rule. Independently decidable owners were transferred; a new owner or selector requires another node. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | Temporal zero-Step Opening accounts for the admitted Cadenced/AtTime persistence and authored-close branches exactly once. | One ordinary Cadenced zero-Step nonce-one close that rearms and removes its Tick reference within Opening | Reference-Wasm FRAME evidence in the declared owner/domain; native checks are construction controls | Self | Required | Named parent decision and declared downstream consumers | Satisfied for the reference temporal domain: four mutually exclusive complete owners cover AtTime/Cadenced persistence and authored close exactly once | + +## Measurements + + +### Cadenced Zero-Step and Authored Close + +Reference clock preparation calls the real Timestamp inherent outside the measured atom; the mock advances its block-derived Time provider. Ordinary temporal draining creates readiness, and canonical primary consumption remains outside the inner measurement. Final checks require exactly one nonzero Pipeline receipt, exact sovereign/Fee Sink changes, no Run, and either a future coherent Tick pointer or `AutoCloseNonceReached` with identity/head/holds removed. Native TryRuntime validates both states. The helper leaves unrelated host genesis Actors intact and assumes no global one-entry queue. + +| User zero-Step profile | RefTime before DB additions / minimum (ps) | Estimated / recorded proof (bytes) | Reads / writes | Generated fragment SHA-256 | +| --- | --- | --- | --- | --- | +| `scheduler_inner_zero_step_user_complete` | 114,961,000 / 111,539,000 | 7,529 / 1,589 | 9 / 5 | `96b04a0b7fe7140f7db49bd367ebc0e892d7f5f33a0b4c82d81e4775d65d1891` | +| `scheduler_inner_zero_step_user_cadenced` | 238,931,000 / 231,039,000 | 10,744 / 2,329 | 21 / 16 | `c19f91d4b51c3de32d72363e43c1e49606c4277a79874d8a8e2ad7a6a5bf3e14` | +| `scheduler_inner_zero_step_user_cadenced_close` | 314,360,000 / 305,700,000 | 11,476 / 3,061 | 26 / 23 | `2d53c5deaa21fa25dcfb1288bd0dcf432ef95ceb5c705867c1d6f149d10aebcb` | + +All three profiles pass reference-Wasm admission, measurement and postconditions at 50 steps × 20 repeats on one freshly built binary, with no concurrent agent build/test/benchmark job during measurement. Source content/modes `f0c3cae7c5c5519351a747e8d1967e57c80be58935c3909c6be616e41f705837`; benchmark source SHA-256 `706ec1e39f640055714e41dab812a405eddf44987f3e463803c9359868b18f8d`; reference helper source SHA-256 `d9642f44f5caae4edf0a340c4ae273ba01296d28cc16e4de76a78c49730db4bb`; benchmark Wasm SHA-256 `8d9e4371a3685f6e234dd52b7fdf4387b7be97e80a9cbec3d6c259e7d8774ae1`. Toolchain, hardware, cache/heap/map controls match the pinned environment above. + +Reproduce with `scripts/benchmarks.sh --extrinsic PROFILE --output /tmp/deos-exp28-zero-step-KIND.rs pallet_deos_actors`, first using PROFILE=`scheduler_inner_zero_step_user_cadenced`, KIND=`cadenced`; then add `--skip-build` for PROFILE=`scheduler_inner_zero_step_user_cadenced_close`, KIND=`cadenced-close`, and PROFILE=`scheduler_inner_zero_step_user_complete`, KIND=`manual-paired`. Runtime build took 96 seconds; measurements took 13/14/13 seconds. Exact-selector exclusions keep all three unbound diagnostics out of default production generation; fragments remain temporary and uninstalled. + +Cadenced persistence adds Timestamp, Waiting chunk/directory/heap and User hold settlement to the Manual key families. Its native-account trace has three reads/writes, including the owner, plus `Balances::Holds` and `ActorStateHolds`; hold settlement is a distinct monetary owner from Pipeline collection. Authored close also touches counts, sovereign/owner-slot indexes and process cleanup. The full terminal atom already contains collection, rearm and cleanup: do not add those components again or subtract aggregates to manufacture isolated coefficients. These points reject Manual-only zero-Step coverage, but do not bound maximum heap depth, occupied-page append, Crossing rearm, maximum zero-Step head or every terminal branch. No production selector, fee or coefficient changes are accepted by this comparison. + +Validation on the retained measurement source: both native benchmark tests pass through `ci-local.sh --only tests --package pallet-deos-actors --all-features --test-filter zero_step_user_cadenced`; Actors/runtime all-features Clippy, canonical formatting, workspace check and changed-scope completion pass. Completion uses `--skip-runtime-tests` because the production runtime diff is confined to its benchmark-only clock helper; prior default-runtime behavioral evidence remains applicable. `benchmarks.sh --check` compiles and retains the known stale generated-storage rejection. Source content/modes are unchanged after context/record updates. Passing these scoped checks does not establish full Executive/inherent ordering or the G5 artifact gate. + + + +### AtTime Zero-Step Consumption and Authored Immutable Close + +Two ordinary User AtTime fixtures qualify complete inner Opening after the due occurrence and consumed primary. Mutable AtTime retains a consumed process without rearming; User Immutable AtTime authors the mandatory nonce-one closure under specification §9.1. The shared temporal constructor preserves existing Cadenced preparation, selects the family-specific Trigger fee and retains host wakeups. Both profiles verify exact Pipeline collection and no Run; persistence additionally verifies absent Ready/Block/Tick references and unchanged Actor state, nonce and payer balance after advancing and draining the next tick outside measurement. Close verifies AutoCloseNonceReached and absent identity, head and holds. No temporal-state/pointer injection or host-topology clearing is used. + +The initial Wasm persistence verification failed with `Timestamp must be updated only once in the block`: the fixture advanced System's block number without finalizing the prior timestamp update. Failed source `0f9a618902f5724bdf9f48bfdc96f2a5decfd345d160111f5a0215ea72e7348f`, Wasm `e82600c35c34cd905c872f082863ad3f7c4135c5e97a24676af7f9e47b2d17cb`, log SHA-256 `52ab141de1c34eeb9feb890a0d48e09ec3bf08fc765cf166ac5e0d10816506f2`. No measurement from that run is accepted. The benchmark-only host boundary now explicitly invokes Timestamp's ordinary `on_finalize` hook before the second clock advance; the mock has no timestamp engine. This corrects fixture clock lifecycle, not production behavior, and does not establish full Executive ordering. + +| User zero-Step profile | RefTime before DB additions / minimum (ps) | Estimated / recorded proof (bytes) | Reads / writes | Generated fragment SHA-256 | +| --- | --- | --- | --- | --- | +| `scheduler_inner_zero_step_user_at_time` | 121,875,000 / 114,681,000 | 7,624 / 1,684 | 9 / 5 | `b430300c98fcfbcd905266bdc8f0ec62910bcc32744c7b633e217cf6bc73a169` | +| `scheduler_inner_zero_step_user_at_time_close` | 214,206,000 / 194,930,000 | 11,144 / 2,729 | 16 / 15 | `76bf29eda0f0ca26825e2a30659ec38b2ec914f00dc516b62a58c1a0adebb6ba` | + +Both pass fresh reference-Wasm admission, measurement and postconditions at 50 steps × 20 repeats. Source content/modes `5cd62626a945064a9beeb419bfe2546c34e325d47e6ff4dc775b54f00078cf93`; benchmark source SHA-256 `7845d0cb24b267182c190918601b7aeb1411d0d39ed890f5d52c76e9f79b3af4`; reference helper source SHA-256 `a9f1249e393c506e48ddf3ce7744983e1712080202dc0199ae10490b8524658d`; benchmark Wasm SHA-256 `49ad885ae3aa123520e51a98b58ca827a6fe5978a8d9059eb2a6552eb3986f03`. Pinned toolchain/hardware/cache/heap/map controls match the environment above, with no concurrent agent build/test/benchmark job during measurement. Reproduce with `scripts/benchmarks.sh --extrinsic scheduler_inner_zero_step_user_at_time --output /tmp/deos-exp28-zero-step-at-time.rs pallet_deos_actors`, then `--skip-build` with `scheduler_inner_zero_step_user_at_time_close` and `/tmp/deos-exp28-zero-step-at-time-close.rs`. Fresh build took 104 seconds; each measurement took 14 seconds. Exact-selector exclusions retain both as unbound diagnostics; fragments remain temporary and uninstalled. + +Persistence's measured inner trace needs no Timestamp or new Waiting key: due occurrence has already consumed temporal readiness. Close adds lifecycle/slot/hold cleanup inside the same inclusive atom. Do not add that cleanup or Pipeline collection again, infer an isolated coefficient by subtraction, or confuse these consumed-source points with a complete due-worker/heap bound. Window expiry, insolvency, unavailable observations and joint maximum head/detector/Waiting geometry remain open. + +Retained-source validation: native all-features `zero_step_user_` tests pass (32 seconds), including existing Cadenced fixtures after constructor reuse. Actors/runtime all-features Clippy pass (22/21 seconds); canonical formatting, workspace check, changed-scope completion (47 seconds), record normalization/dependency graph and diff checks pass. Completion skips runtime tests because the runtime diff is benchmark-only clock finalization; unchanged default-runtime behavioral evidence is reused. Preflight compilation passes (60 seconds) but retains the known stale generated-storage rejection. Source content/modes are unchanged after context synchronization. Neither G5 nor full Executive/inherent ordering is accepted. + + +### Deciding Temporal Branch Partition + +The retained reference-Wasm profiles directly attain every cell in the declared family × terminal partition: + +| Trigger family | Persistent owner | Authored-close owner | +| --- | --- | --- | +| AtTime | `121,875,000 / 7,624` plus 9 reads / 5 writes | `214,206,000 / 11,144` plus 16 reads / 15 writes | +| Cadenced | `238,931,000 / 10,744` plus 21 reads / 16 writes | `314,360,000 / 11,476` plus 26 reads / 23 writes | + +Values are generated RefTime before database additions / estimated ProofSize. Each cell is one complete consumed-primary inner atom and already includes exactly one Pipeline collection and applicable User hold reconciliation. Cadenced persistence includes Tick rearm; Cadenced close inserts that successor and removes it during nonce-one cleanup. AtTime persistence retains the consumed one-shot state without a successor, while AtTime close removes the complete process. The useful temporal occurrence and its Trigger fee precede this boundary and are not charged again. + +Maximum and actual control use the same family × terminal selector in the reference binding. The complete branch values are alternatives, not additive components: do not add the Manual owner, Pipeline collection, hold reconciliation, rearm or cleanup again. Outer source consumption and maximum Waiting/page/cursor/heap composition remain EXP-0058/EXP-0063 responsibilities; non-temporal close families remain in their named Leaves. The fragments remain uninstalled, and EXP-0065 owns complete generated-file regeneration. + +## Derived Metrics + +Derived values are retained beside the evidence that defines their source and units. No subtraction of overlapping aggregate owners or cross-artifact timing comparison is introduced. + +## Statistical / Variance Notes + +Retain each item’s original sampling and uncertainty. Singleton construction probes establish only their admitted state/access witness, never statistical timing dominance or a production bound. + +## Result + +The four retained reference-Wasm witnesses cover the complete declared family × terminal partition. Their postconditions prove the exact temporal state transition, one Pipeline receipt and collection, no Run, and either coherent persistence or complete authored cleanup. Cadenced nonce-one close directly satisfies the smallest falsifier by rearming and then removing its Tick successor in the same inclusive atom. + +## Interpretation + +Trigger family and terminal disposition are required selector dimensions for temporal zero-Step Opening. Cadenced rearm, AtTime one-shot consumption and authored cleanup are mutually exclusive complete owners. Their inclusive accounting prevents a second Trigger charge and prevents duplicate collection, hold, rearm or cleanup additions. Independent outer Waiting and heap geometry does not keep this inner temporal partition open. + +## Decision + +Accepted for the reference temporal binding. Select exactly one complete owner from the family × terminal matrix: AtTime persistence `121,875,000 / 7,624` plus 9 reads / 5 writes; AtTime authored close `214,206,000 / 11,144` plus 16 reads / 15 writes; Cadenced persistence `238,931,000 / 10,744` plus 21 reads / 16 writes; or Cadenced authored close `314,360,000 / 11,476` plus 26 reads / 23 writes. Maximum and actual preserve this partition. No second Trigger fee applies. EXP-0065 owns complete generated-file regeneration and exact production artifact identity. + +## Rejected Alternatives and Tradeoffs + +Reject Manual-only zero-Step coverage, adding a standalone rearm or cleanup charge to an inclusive temporal owner, charging the already-consumed temporal occurrence again, selecting by RefTime alone, and subtracting aggregate profiles to fabricate disjoint coefficients. Do not import outer Waiting/heap or unrelated terminal work into this inner partition. + +## Validity + +- `Establishes`: The reference AtTime/Cadenced zero-Step persistence/authored-close partition and four complete inclusive owners, including exactly one Pipeline collection and applicable hold reconciliation. +- `Does not establish`: Outer temporal occurrence/source/Waiting/heap bounds, Crossing or Observation rearm, window expiry, arbitrary terminal branches, final production Weight identity or integrated throughput. +- `Known limitations`: The diagnostic branch fragments remain uninstalled; Accepted EXP-0063 supplies outer maximum Waiting/heap composition, and EXP-0065 retains exact regeneration. +- `Invalidation triggers`: A changed zero-Step body, temporal state transition, branch selector, collection/hold path, source/Weight/Wasm identity or acceptance rule requires scoped requalification. +- `Residual qualitative insight`: Due temporal detection and zero-Step Opening are consecutive but separately charged atoms; family-specific Opening already owns its own persistence or cleanup. + +## Follow-Up / Next Gradient + +Stop measuring this satisfied temporal inner owner. EXP-0058/EXP-0063 own outer source and Waiting/heap composition, EXP-0043 owns window expiry, EXP-0064 composes admission and EXP-0065 regenerates exact artifacts. Reopen only for the declared falsifier or a changed relevant path, selector or identity. + +## Architecture Impact + +The reference zero-Step input now requires a four-way AtTime/Cadenced × persistence/authored-close ownership partition, with no duplicate Trigger charge. No runtime, semantic or storage change is introduced; C1 and A0 remain frozen, and complete production regeneration remains G5 work. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: [EXP-0028](./EXP-0028.md) O9 +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: [EXP-0025](./EXP-0025.md) +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: None. +- `Transfers question to`: None; independent findings require an explicit new transfer. +- `Produces input for`: [EXP-0028](./EXP-0028.md) +- `Reopen trigger`: A falsifier of this declared owner/domain or exact artifact identity; cost alone does not reopen frozen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0039.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0039.md new file mode 100644 index 00000000..9460adf2 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0039.md @@ -0,0 +1,239 @@ +# EXP-0039 — Crossing Opening Rearm Ownership + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | [EXP-0028](./EXP-0028.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-06 | +| Affected domain | Crossing Opening rearm has a sound inclusive bound for its declared reachable membership, cursor and availability domain. | +| Physical mechanism | Crossing Opening Rearm Ownership | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0025](./EXP-0025.md) — Accepted, physical architecture only | +| Successor experiment | [EXP-0028](./EXP-0028.md) | + + +## Decision Question + +Crossing Opening rearm has a sound inclusive bound for its declared reachable membership, cursor and availability domain. + +## Governing Specification + +- `Owners`: `template/pallets/actors/docs/specification.en.md` (sole ownership, lifecycle/Trigger/Pipeline/Action semantics, placement and scheduler); `docs/actors-resource-policy.specification.en.md` (component-wise admission/actual settlement, equal-thirds block policy); `docs/actors-performance-assurance.specification.en.md` (evidence and target). +- `Frozen physical input`: [EXP-0025](./EXP-0025.md) C1 Architecture Freeze. No change to FIFO, ticket chronology, Q1, causal floor, primary/reference ontology, source/successor atomicity or sole execution owner. +- `Semantic oracle`: A0 SHA-256 `8746e423168309975e509be91e4eb06fdf0eb0b43369d75f1ee62202a4341678` remains frozen; use only for relevant semantic equivalence, never as a Weight oracle. + +## Context and Hypothesis + +This Leaf owns crossing opening rearm ownership. Its boundary is the single claim above. Evidence was extracted from the explicitly marked pre-fission parent subsections at Git baseline `a60de7748132211d9e58cecd5be82b41f76ce230`; this migration changes navigation and evidence ownership only. Source, Wasm, commands, rejection outcomes and evidence limitations retain their original meaning. + +Extraction provenance at the recorded Git baseline: EXP-0028 / “Crossing Availability and FIFO Progress Prerequisites”, “Live Crossing Range Cursor Repair”, “Armed Rearm Between Populated Leaves”, “Full Crossing Source and Tail Pages”, “Observation Zero-Step Rearm and Close”. Original directional prose (above/below) belongs to these pre-fission subsections; current cross-owner navigation follows the parent proof map. + +## Baseline and Candidates + +Retained measured implementations and rejected candidates are identified in the extracted evidence. The deciding reference binding partitions Crossing Opening by current-observation availability and authored terminal disposition. An available observation selects one complete persistent or authored-close owner; a reachable unavailable observation atomically refuses before collection or topology mutation. Post-admission `Uninitialized` is outside the reference host domain. + +## Controls + +Only the declared witness input varies within each source-bound comparison. Preserve the exact host, toolchain, database, Wasm and sampling controls printed with each evidence item; do not compare unlike artifacts as a controlled performance result. + +## Benchmark Class, Workloads, and Criteria + +Acceptance requires the single claim to hold over its declared admitted domain, with each physical operation charged once and every retained limitation resolved before broader production use. One legal omitted-owner, domain or containment counterexample falsifies that claim. Existing workloads below are bounded witnesses, not permission to add independent questions. + +## Environment and Artifact Identity + +Pre-fission baseline `a60de7748132211d9e58cecd5be82b41f76ce230`. Each extracted block retains its own exact source/Wasm/output identity. Shared pinned hardware/toolchain/cache controls follow [EXP-0030](./EXP-0030.md#environment-and-artifact-identity) only where the original comparison explicitly reuses them. A shared environment never substitutes for the block’s own source or Wasm identity. No runtime artifact was rebuilt during migration. + +## Proof Obligations + +- `Freeze`: Frozen by explicit migration reconstruction from the pre-fission decision and retained witnesses at a60de7748132211d9e58cecd5be82b41f76ce230. No new measurement or historical decision is implied. +- `Review triggers`: More than six proof subsections; multiple consumers reviewed. +- `Decomposition review`: The declared Crossing Opening rearm owner includes membership removal/reinsertion and necessary live-cursor repair at the same Opening boundary. Armed/WaitingForRearm, full pages and availability profiles qualify that one inclusive operation. Public close has its own independently charged owner in EXP-0042; Observation subscription unlink is EXP-0040; Waiting heap cost is EXP-0063. None may be investigated here as another branch of rearm. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | Crossing Opening rearm has a sound inclusive bound for its declared reachable membership, cursor and availability domain. | One admitted Crossing Opening removing a target before a live cursor and rearming into a populated leaf | Reference-Wasm FRAME evidence in the declared owner/domain; native checks are construction controls | Self | Required | Named parent decision and declared downstream consumers | Satisfied for the reference Crossing domain: available persistence/close envelopes dominate all attained membership and cursor profiles; reachable unavailability atomically selects the retained refusal owner | + +## Measurements + + +### Crossing Availability and FIFO Progress Prerequisites + +Question: qualify unavailable current observations after a paid Crossing latch. Cleanup preflights removal using a Manual target and does not query the current observation; rearm instead requires an authoritative value/revision under specification §4.5. Strict FIFO §8.3 forbids bypass, while §9.2 supplies no observation-loss terminal reason. The former §8.5 prerequisite list omitted the necessary host observation and fee-collection conditions. This is a contract omission derived from those existing normative constraints, not authority to ratify arbitrary behavior or invent a phase, close reason or bypass. Specification §5.2/§8.3/§8.5 now state atomic refusal, head preservation, existing terminal precedence and conditional liveness explicitly. + +Native `crossing_rearm_unavailability_preserves_paid_head_until_host_recovers` partitions generic host `Unavailable` and `Uninitialized` after real paid Crossing readiness. Each retains complete Actor state, head, events and payer/sink balances while an eligible System follower receives no cycle. Restoring the same authoritative value/revision permits both zero-Step cycles and exactly one User Pipeline fee. `crossing_rearm_unavailability_does_not_block_pipeline_insolvency_close` separately proves one-unit-short paid readiness closes custody-neutrally without rearm. An initial test expected `starved == false`; `execute_cycle` classifies any refused head with no execution as starved. That expectation was a test defect: sparse starvation does not prove Weight exhaustion. + +The reference provider returns retained canonical values for Paused feeds and old observations, independently of age-sensitive `observe`. Crossing admission requires an existing initialized feed; ordinary Oracle lifecycle calls never delete its identity or last value. Consequently post-admission `Uninitialized` is outside this reference domain, while Root deactivation reaches `Unavailable` and has no ordinary resume path. Generic mock recovery is not evidence that a DEOS deactivated feed can recover. EXP-0029 records that qualification. + +`scheduler_paged_zero_step_user_crossing_unavailable` constructs a funded paid zero-Step User Crossing and an eligible zero-Step System follower through ordinary admission, then calls the real host Oracle deactivation before measurement. The measured full FIFO pass consumes control but no effect and preserves Actor/queue/holds/membership/events/custody/sink. Outside measurement, authorized Mutable owner close removes the head and the follower completes; no new Immutable close authority is implied. The generic benchmark compares SCALE-encoded event vectors because FRAME's Wasm `EventRecord` lacks native `PartialEq`; the first preflight exposed and rejected the native-only assertion before measurement. + +Earlier available-observation and cleanup measurements remain applicable within their declared domains. No production execution, C1 geometry or configured bounds change; this profile is not full Executive or an all-context numeric bound. + +| Refused FIFO pass | Generated RefTime / minimum ps | Estimated / recorded proof B | Reads / writes | +| --- | --- | --- | --- | +| `scheduler_paged_zero_step_user_crossing_unavailable` | 191,857,000 / 173,908,000 | 6,186 / 2,721 | 13 / 0 | + +Source content/modes `f5a5b7023aeda57de1023e939585fc05d9fb6410fce75c4de10cde70b926aa4c`; benchmark source SHA-256 `70817400fb7c81e8f254992ef47a0c9756f629420269fc4b0c8b20c14c9328e2`; reference helper SHA-256 `3bb54c35ed7db1cdfdf4f36e3f3ec8acb34d70de04db9c975b3d16f1fdef3e25`; benchmark Wasm SHA-256 `6cde806c7f6c81f3b756d2e32fdec265f8f78fcf23ff0c1e37e6356981783cf4`. Same pinned reference toolchain/hardware/cache/heap/map controls as above, 50 steps × 20 repeats, no concurrent agent build/test/benchmark job during measurement. Fresh build took 109 seconds and generation 14 seconds. + +Reproduce with `scripts/benchmarks.sh --extrinsic scheduler_paged_zero_step_user_crossing_unavailable --output /tmp/deos-exp28-crossing-unavailable.rs pallet_deos_actors`. Temporary fragment SHA-256 `0bf3095ae741ef17a1288f75c5438fcee47be1e1578e4af0956b4ce5788ecb46`, uninstalled and excluded from default production generation. Measured proof mode covers this exact admitted head/follower geometry; the trace reads `Oracle::Feeds` but no `Oracle::Observations`, matching terminal deactivation refusal. No writes, collection or effect occur. The profile does not isolate the rearm probe from preceding FIFO, fee-liability and source reads; its inclusive figure must not be added to those owners again. + +Validation: native `crossing_rearm_unavailability` properties pass after shared follower extraction; the final native `crossing_unavailable` benchmark passes (13 seconds). Fresh reference Wasm verifies the measured profile. Workspace all-features Clippy (27 seconds), canonical formatting, workspace check and changed-scope completion pass. Completion first rejected a 514-character changelog entry; shortening it preserves source-bound evidence and the final gate passes. Runtime/default package evidence is reused because production execution is unchanged and the runtime helper addition is benchmark-only; completion uses `--skip-runtime-tests`. Wiki consolidation and Russian-style guards pass, and independent review approves the one changed EN/RU pair out of 46 page IDs. Record normalization/graph and diff checks pass. Source identity remains equal after context synchronization. Preflight compilation passes (60 seconds), while the known stale generated-storage rejection remains the open G5 gate. + + + +### Live Crossing Range Cursor Repair + +Earlier full-page rearm profiles read an absent range cursor, leaving the rewind-write branch unqualified. The new profiles start from ordinary populated Armed-source/destination preparation at value two, publish value one in the next external phase and run the bounded Crossing worker once. It skips the latched target, rearms a nonzero guard prefix and retains a live cursor ahead of the target but no later than the surviving tail. Complete User Opening moves that tail guard into the target slot and rewinds the existing cursor; authored close preserves the repair. No synthetic cursor, manual counter reset, repeated same-block worker pass or injected observation is used. + +Both native and reference-Wasm profiles pass. The initial feed has 384 Actors, source page size P=128 and a full destination page. After the worker, source membership is exactly `2P − (candidates − 1)`: one candidate was the disabled target and all remaining candidates moved to their own Armed threshold-two leaf without activation. Two source pages remain. Assertions qualify target position `< cursor ≤ surviving tail`, exact cursor revision/traversal/threshold preservation with only page/offset rewound, moved inverse ownership, guard readiness, User collection and destination allocation or terminal reclamation. A subsequent block's bounded worker pass actually rearms the moved guard from the repaired position with zero faults, activations or closes. That consequence check is outside the measured Opening; native TryRuntime also checks the resumed state. + +| Profile suffix after `scheduler_inner_zero_step_user_crossing_` | RefTime before DB additions / minimum (ps) | Estimated / recorded proof (bytes) | Reads / writes | Generated fragment SHA-256 | +| --- | --- | --- | --- | --- | +| `cursor` | 358,641,000 / 335,803,000 | 40,712 / 32,297 | 21 / 16 | `eb79352a4f198fdc44a123063f01cc4edac75569700ccbe0491387eb85371237` | +| `cursor_close` | 463,125,000 / 438,401,000 | 42,457 / 34,042 | 28 / 25 | `750919b8c358ee17f2e57983a3f7e2124c4a13c27cd5cb924eeefca2aa786f6f` | + +Source content/modes `00ee34e8309cd0050e2618436fa0276ebc23b1d85f6b0eb116cebe6f9112eabf`; benchmark source SHA-256 `aff0fb70cd8ff7c0bb6a6177d4cc408db2a0224583701b3e29312eb9752993b3`; benchmark Wasm SHA-256 `4ce6e92b4f263b6f6e8b5ffe6dca55cff6d5f745f3fc85c031ea7990ebc124e7`. Same pinned reference hardware/toolchain/cache/heap/map controls as above, 50 steps × 20 repeats, no concurrent agent build/test/benchmark job. Fresh build took 104 seconds and each generation 15 seconds. Reproduce with `scripts/benchmarks.sh --extrinsic scheduler_inner_zero_step_user_crossing_cursor --output /tmp/deos-exp28-zero-step-crossing-cursor.rs pallet_deos_actors`, then the `_cursor_close` selector and `cursor-close` output with `--skip-build` on that unchanged binary. Both fragments remain temporary, uninstalled and excluded from default production generation. + +The traces now bind `CrossingRangeCursors` as one read and one write, alongside the complete populated-leaf User atom. Earlier absent-cursor points have one fewer write, but the new worker also changes population/trie context and the binary differs; do not infer an isolated CPU/proof constant by subtracting those aggregates. The repaired cursor preserves the original transition rather than restarting or duplicating observation work. This qualifies the real rewind branch and its next-block consequence, not maximal joint cursor/page/radix/header/Waiting geometry, every detector availability branch or full Executive execution. All collection, hold and cleanup work inside the inclusive atom remains counted once. + +Retained-source validation: native `zero_step_user_crossing_cursor` all-features tests pass including next-block resumption (18 seconds), both fresh-Wasm profiles pass, and Actors all-features Clippy passes (22 seconds). Canonical formatting, workspace check, changed-scope completion (14 seconds), record normalization/dependency graph and diff checks pass. Production/runtime/mock behavior is unchanged; prior unaffected behavioral evidence is reused and completion skips runtime tests. Preflight compilation passes (57 seconds), then retains the known stale generated-storage rejection. Source content/modes remain identical after context updates. Complete Weight envelopes and G5 acceptance remain open. + +### Armed Rearm Between Populated Leaves + +Question: qualify cross-phase zero-Step rearm where both Crossing leaves survive. Specification §4.5 derives phase from current authoritative value, independently of latched history. A target with Rising threshold four/rearm one fires through real publication, remains latched while the current value returns to two, then opens as Armed. Ordinary guards at threshold two populate its old WaitingForRearm leaf; guards at threshold four populate the Armed destination. The source and its tail are full; destination occupancy is page size minus one or page size, separating full-page append from new-page allocation. No actor state, observation, membership or cursor is injected. + +The shared observation constructor now accepts the qualified fire threshold and Opening value; its previous wrapper preserves threshold two/value one for all earlier profiles. In the reference EMA, sample 1,000 publishes value nine and seven decreasing publications of sample two reach value two. Publication occurs before the next bounded deferred-worker pass, and the latched User pays no duplicate Trigger fee. Value two need not cross rearm threshold one: Opening installs phase from the current value, intentionally discarding latched history. Benchmark guard creation is shared with the prior same-phase profiles without changing their ordinary creation semantics. + +All four native and reference-Wasm profiles pass. With page size 128, their feed contains 383/384 Actors: target plus 255 source guards plus 127/128 destination guards. Complete inner Opening starts after canonical primary consumption. Assertions verify exact User fee collection, receipt and lifecycle, moved source inverse ownership, unchanged guard nonce/readiness, destination append/allocation or its terminal reclamation, and coherent source/tail cardinality. Native TryRuntime checks the complete state before and after execution; existing host System Actors remain intact. + +| Profile suffix after `scheduler_inner_zero_step_user_crossing_` | RefTime before DB additions / minimum (ps) | Estimated / recorded proof (bytes) | Reads / writes | Generated fragment SHA-256 | +| --- | --- | --- | --- | --- | +| `armed_pages` | 356,615,000 / 337,269,000 | 40,549 / 32,134 | 21 / 15 | `e4ad71637449848098906977b3c671f6699f6dc8a2f815b5ac8e89e206694a2e` | +| `armed_new_page` | 351,238,000 / 333,079,000 | 40,613 / 32,198 | 21 / 15 | `a45f056d4025f0f75d4f844aa1018027877cb10d2af232c2dba3e0d83d8ffed2` | +| `armed_pages_close` | 465,430,000 / 446,083,000 | 42,294 / 33,879 | 28 / 24 | `f0c4168196bef54eb92e3491ccf5bc6df9ff5ac0976bfb9e9592ee1cad45b00d` | +| `armed_new_page_close` | 452,649,000 / 433,092,000 | 42,358 / 33,943 | 28 / 24 | `909126b2764f4229ed260427cc3e86ef208e219e50f7507c90ed4a0d83bb4e58` | + +Source content/modes `8fdbe1dd702f18e0f65d687f8384ae257dc4bdaa676632e4d0b8885fcbccc337`; benchmark source SHA-256 `661b9db3d617cd658486173d7959e21b12df8d7edfabb577b90a3611edd92df7`; benchmark Wasm SHA-256 `1c6f361cad09847b59d0c3d1b0cc323fac2fa5c24c23d30933d6a488ecc5cea8`. Same pinned reference toolchain/hardware/cache/heap/map controls as above, 50 steps × 20 repeats, no concurrent agent build/test/benchmark job. Fresh build took 95 seconds; each generation took 15 seconds. Reproduce in table order with `scripts/benchmarks.sh --extrinsic scheduler_inner_zero_step_user_crossing_SUFFIX --output /tmp/deos-exp28-zero-step-crossing-KIND.rs pallet_deos_actors`, replacing underscores in SUFFIX with hyphens for KIND; only the last three use `--skip-build` on the unchanged fresh binary. Temporary fragments remain uninstalled and excluded from default production generation. + +Both persistent traces read/write two leaf states and report three member-page reads/writes. For existing-page append, source, source tail and destination are read and written. For new-page allocation, the full destination is read but not rewritten, while the new page is written; identical counts therefore do not imply identical keys. Terminal work removes the inserted target again inside the same transaction, preserving the destination guards and reclaiming the newly allocated page where applicable. Both complete atoms already include rearm, User collection, hold reconciliation and terminal cleanup; no aggregate subtraction or duplicate addition is justified. + +The allocation points have slightly lower fitted CPU but 64 more estimated/recorded proof bytes than their append peers. This is a measured branch tradeoff, not a selected architecture or evidence that one dominates. Both leaves survive, so these profiles omit radix deletion/recreation and live range-cursor repair. They qualify the populated-leaf Armed append/allocation and close geometry, not every combination of source reclamation, threshold-tree context, Oracle availability, maximum header or Waiting/hold state. Earlier single-leaf and same-phase profiles remain complementary source-bound evidence; full production binding and numerical acceptance remain open. + +Validation: the complete native `zero_step_user_` all-features benchmark family passes after shared constructor extraction (30 seconds), covering earlier profiles and all four additions. A later assertion-message-only edit preserves that behavioral evidence; fresh Wasm verifies the retained source. Actors all-features Clippy passes (21 seconds), as do canonical formatting, workspace check (10 seconds), record normalization/dependency graph and diff checks. The initial completion pass rejected an overlong changelog entry; after prose shortening the remaining completion audits pass (5 seconds), preserving the measured source. Runtime and unaffected package behavior reuse prior evidence because no production code or host/mock behavior changed. Preflight compilation passes (52 seconds), retaining the known stale generated-storage rejection. Source content/modes remain identical after context synchronization; G5 and complete numerical acceptance stay open. + +### Full Crossing Source and Tail Pages + +Coverage question: complete User zero-Step rearm removes and reinserts Crossing membership. The earlier single-member profiles exercise radix removal/recreation but cannot prove a full non-tail page rewrite, moved-member inverse update or singleton-tail removal/reallocation. Persistent and nonce-close diagnostics retain the real published WaitingForRearm User setup and ordinarily admit zero-Step System guards on its leaf. The target occupies source page zero/offset zero; the final guard moves from either a singleton or full tail. These endpoints distinguish page deletion/reallocation from maximum two-page decode/rewrite. No membership, cursor, Contract or observation state is injected. + +All four native and reference-Wasm profiles pass. The reference and benchmark-enabled mock page size is 128, producing 129 or 256 members on the target leaf. Complete inner Opening starts after canonical primary consumption. Assertions require exact User fee accounting, the final guard moved to page zero/offset zero with unchanged nonce/readiness, a full surviving source page and either a retained/recreated tail containing the target or the precise terminal tail reduction. Native TryRuntime verifies the full state before and after execution; the reference host's existing System Actors remain intact. + +| Profile suffix after `scheduler_inner_zero_step_user_crossing_` | RefTime before DB additions / minimum (ps) | Estimated / recorded proof (bytes) | Reads / writes | Generated fragment SHA-256 | +| --- | --- | --- | --- | --- | +| `page` | 302,977,000 / 289,566,000 | 20,044 / 14,104 | 19 / 13 | `8ea289a016c3aaefff4432c1b5c5aab3e140d0db093ec1dc2fa20a9e24ff32d1` | +| `page_close` | 406,972,000 / 391,187,000 | 23,847 / 15,432 | 26 / 22 | `cc9919cc9dc0c15772552b455d0a981c1b2c4ec84d64a9476dec21b8f1211db5` | +| `page_full` | 333,567,000 / 319,529,000 | 28,946 / 23,006 | 19 / 13 | `d2fbb8c29f37b3635b2612a4bad560b479b537f3f2043f9a2fef8acab1b5a50b` | +| `page_full_close` | 448,039,000 / 422,965,000 | 32,869 / 24,454 | 26 / 22 | `5cbfbf24f3be585ca636df7fd01bbd55856c10333cc454d3e10128bd1ca3a58e` | + +Source content/modes `63c9966831b5895886f6c6abf2cb9c8a0d621152a0e2cda6c6862004746bf57d`; benchmark source SHA-256 `69b5c73d8711b85b1ad2eb9b6bf9b7c0e6a746ff7fa6a97f6bffed87a00ee606`; benchmark Wasm SHA-256 `4af6d3490e9c4f6b587beb3fcf257482a31711b18a6591cc213355c2e4c9866c`. Same pinned toolchain/hardware/cache/heap/map controls as above, 50 steps × 20 repeats and no concurrent agent build/test/benchmark job during measurement. Fresh build took 95 seconds and generation 13/14/14/14 seconds. Reproduce in table order with `scripts/benchmarks.sh --extrinsic scheduler_inner_zero_step_user_crossing_SUFFIX --output /tmp/deos-exp28-zero-step-crossing-KIND.rs pallet_deos_actors`, mapping underscores in SUFFIX to hyphens in KIND; only the last three use `--skip-build` on that unchanged binary. All fragments remain temporary and uninstalled; exact-selector exclusions prevent accidental production generation. + +Both persistent profiles touch two member pages and two membership locators, including the moved guard's inverse locator. The surviving leaf avoids the single-member profiles' radix deletion/reinsertion. Singleton-tail deletion/recreation and full-tail rewrite have identical unique-key read/write counts but distinct serialized proof and CPU cost; those counts do not enumerate repeated logical accesses inside one transaction. Terminal atoms include their own rearm, collection, holds and cleanup. Do not add those owners again or subtract aggregate measurements. + +Within this paired binary, full-tail points increase both CPU and proof over the corresponding singleton-tail points. Relative to the earlier single-member measurements, these points have lower CPU/estimated proof but higher recorded proof; that historical comparison spans different fixture binaries and topology, so it cannot isolate a single byte-cost delta or establish a global Pareto winner. Retain both radix and page geometries in the coverage matrix. This qualifies two-page WaitingForRearm remove/reinsert and close, not Armed phase movement into populated leaves, live range-cursor repair, ObservationChange page geometry, Oracle unavailability or complete head/Waiting/hold bounds. C1 and production behavior/Weight remain unchanged. + +Validation on the retained source: `scripts/ci-local.sh --only tests --package pallet-deos-actors --all-features --test-filter zero_step_user_crossing_page` passes all four profiles (29 seconds), and Actors all-features Clippy passes (20 seconds). Canonical formatting, workspace check, changed-scope completion, record normalization/dependency graph and diff checks pass. Only benchmark fixture/setup code changed; prior package/runtime behavioral evidence remains applicable and completion skips runtime tests. Preflight compiles the final source (52 seconds) but retains the known stale generated-storage rejection; source content/modes remain identical after context synchronization. These checks do not close G5 or constitute full Executive/production-Wasm acceptance. + + + +### Observation Zero-Step Rearm and Close + +Five admitted User profiles use a real host publication, ordinary Crossing/fanout readiness and the same consumed-primary inner boundary as Manual/Cadenced. Persistent profiles verify latch consumption and retained subscription or exact Crossing phase/revision; terminal profiles require AutoCloseNonceReached and absent identity, head, subscription/membership and state holds. All check one nonzero Pipeline receipt, exact sovereign/Fee Sink deltas and no Run. The native publication adapter tracks revisions only for benchmark publications, preserving the previous default for other mock cases and clearing its state on fixture reset. + +The first reference-Wasm probe falsified sample/value equality: the reference factory creates an EMA feed, and publishing sample two after value one produced value zero at revision two under its rounded aggregation. This is a fixture defect, not permission to replace the reference feed or alter Oracle semantics. Failed source `1bdeaa67754d461f4eef864b4f0ce81219c2ca4063c83fea5f1dc846f5d6371e`, benchmark Wasm `18334f869dbb27045e8ade689f9552cc8f89114a5fcd3f169c141ec124ca5aef`, log SHA-256 `4f34931f15b0d8656cf75efcd0b19c1b0c1728c03b9f53231d80523f1829ad54`; the `scheduler_inner_zero_step_user_crossing_armed` generation route failed before measurement. The corrected setup publishes sample 1,000, verifies the actual value crosses two, and reaches Armed through bounded decreasing publications of sample one while latched. Every publication checks the authoritative value/revision relation; threshold two and rearm one remain unchanged. + +Armed preparation advances to the next block after the first latch. It publishes decreasing samples during that block's external phase, then services the deferred worker once. The reference EMA half-life 100 maps the initial sample 1,000 to value nine, and eight samples of one reach value one within MaxCrossingTransitionsPerBlock=8. The benchmark checks actual values/revisions and no second Trigger fee while latched; it does not substitute a LastValue feed or inject detector state. + +| User zero-Step profile | RefTime before DB additions / minimum (ps) | Estimated / recorded proof (bytes) | Reads / writes | Generated fragment SHA-256 | +| --- | --- | --- | --- | --- | +| `scheduler_inner_zero_step_user_crossing_armed` | 453,486,000 / 432,673,000 | 163,961 / 4,571 | 82 / 77 | `4dbf90b63a3f5d1c09e10565042add43dda4922b26a4de6cbbe80346a9cf5086` | +| `scheduler_inner_zero_step_user_crossing_waiting` | 413,677,000 / 393,912,000 | 84,642 / 4,452 | 48 / 43 | `dd576fc6f2b2b5d178eead188c2944ee095961e84c098c4ebd3255054b098f31` | +| `scheduler_inner_zero_step_user_crossing_close` | 606,092,000 / 583,393,000 | 85,360 / 5,170 | 57 / 53 | `d562caa3a3c79a4bd8b160efe43158a97f8eeb82ffcfe90b9c7a18e530ced18b` | + +All five pass reference-Wasm admission, measurement and postconditions at 50 steps × 20 repeats on one fresh binary. Source content/modes `f9190bb04b4c99e4f032b1264fc71657ae361328301ea8e758d598f8d928da4b`; benchmark source SHA-256 `85aea2cd4777265cb5b326679f021d0e1edbceb5c700fdbbb0ca3a2cb382ff42`; reference helper source SHA-256 `ab4c5eb0d496b55c8ea459b48f549fd01a69a80dfd0f24a02f5ae5dd4b28fe5d`; benchmark Wasm SHA-256 `737feb8338b38d05ebae05efda65a66c636cd7f88f19a4b8f28357116f2587e8`. Same pinned toolchain/hardware/cache/heap/map controls as above; no concurrent agent build/test/benchmark job during measurement. + +Reproduce with `scripts/benchmarks.sh --extrinsic PROFILE --output /tmp/deos-exp28-zero-step-KIND.rs pallet_deos_actors`. Use the table's profile order and KIND=`crossing-armed`, `crossing-waiting`, `crossing-close`, `observation`, `observation-close`; add `--skip-build` only for the last four on the unchanged fresh binary. The corrected runtime build took 96 seconds and profiles took 13/14/13/13/13 seconds. Exact-selector exclusions keep every new diagnostic out of default production generation. Fragments remain temporary and uninstalled. + +Crossing's trace includes Oracle feed/observation reads, leaf/member pages and radix-index work. Armed rearm touches 64 unique CrossingRadixNodes keys, while same-phase WaitingForRearm touches 32; both remove and reinsert membership. Close repeats cleanup inside the same transaction, so unique-key DB counts do not enumerate every logical access. ObservationChange persistence deletes indexed disablement; its close additionally removes subscription topology and lifecycle state. These are complete inclusive atoms: do not add collection, rearm, holds or cleanup again, or select a global bound from the pre-DB CPU coefficient alone. Compare the explicit DB terms and both proof dimensions. + +This fresh single-member feed geometry does not establish full member-page/threshold-tree coverage, non-tail removal, concurrent cohort/range-cursor cases, maximum zero-Step head, unavailable observations or every terminal branch. Complete numerical qualification and production binding remain open; C1, monetary rules, Oracle aggregation and production execution are unchanged. + +Validation: full Actors all-features tests passed after the shared mock revision change (73 seconds). After fixture-only EMA and phase-order corrections, the focused `zero_step_user_` all-features family passed (20 seconds); unaffected suite evidence is reused because its mock owner is unchanged. Actors/runtime all-features Clippy, canonical formatting, workspace check, record normalization/graph and changed-scope completion pass. Completion uses `--skip-runtime-tests` for the benchmark-only runtime helper diff, reusing unchanged default-runtime behavioral evidence. Preflight compilation passes while the known stale generated-storage audit still rejects G5; the final corrected fixture is separately compiled and verified by native and fresh-Wasm runs. Source content/modes remain identical to the measurement identity after context updates. No full Executive, production-Wasm or metadata acceptance is claimed. + + +### Deciding Crossing Branch Partition + +The retained reference-Wasm profiles attain the declared topology branches through ordinary admission and publication. The single-member profiles exercise full radix removal/recreation; the page profiles exercise full non-tail source and tail rewrites; the Armed profiles exercise populated destination append and new-page allocation; and the cursor profiles directly attain the smallest falsifier by moving a tail guard before a live cursor while rearming into a populated destination leaf. The cursor consequence check proves resumed traversal visits the moved guard rather than skipping it. + +For available observations, every retained persistent profile is contained component-wise by `scheduler_inner_zero_step_user_crossing_armed`: `453,486,000 / 163,961` plus 82 reads / 77 writes. Every retained authored-close profile is contained component-wise by `scheduler_inner_zero_step_user_crossing_close`: `606,092,000 / 85,360` plus 57 reads / 53 writes. Values are generated RefTime before database additions / estimated ProofSize. These two attained complete envelopes already include current-observation lookup, membership removal/reinsertion, necessary cursor repair, one Pipeline collection and applicable User hold reconciliation; the close envelope additionally includes authored terminal cleanup. Maximum and actual select by authored terminal disposition after availability succeeds, rather than adding topology fragments. + +A reachable reference-host `Unavailable` result selects the separately measured state-preserving full-FIFO refusal owner `191,857,000 / 6,186` plus 13 reads / zero writes. It performs no collection, membership mutation, receipt or follower service and must not be added to either successful Opening owner. Post-admission `Uninitialized` is unreachable because reference admission requires an initialized feed and ordinary lifecycle retains its identity and last value. Generic mock recovery therefore remains a construction control, not a reference-host liveness claim. EXP-0058 must reconcile the refusal profile's outer FIFO/source boundary during complete scheduler composition; EXP-0065 owns complete generated-file regeneration. + +## Derived Metrics + +The two available envelopes are attained profiles, not aggregate subtraction or cross-artifact fitted deltas. Coordinate-wise containment is used only to select a conservative branch owner over the retained legal witness set; no timing-causality or architecture ranking is inferred. + +## Statistical / Variance Notes + +Retain each item’s original sampling and uncertainty. Singleton construction probes establish only their admitted state/access witness, never statistical timing dominance or a production bound. + +## Result + +The retained available profiles cover singleton radix, full source/tail pages, populated destination append/allocation and live-cursor repair for both persistence and authored close. Their complete resource vectors are contained by the attained persistent and close envelopes respectively. The reference-Wasm unavailable profile covers the only reachable missing-current-value branch and proves exact atomic refusal. + +## Interpretation + +Availability and authored terminal disposition are the required selector dimensions. Membership page, radix and cursor shapes change physical work but do not require additive owners: each is already inside the matching complete available envelope. Unavailability is a refusal alternative, not a partially completed rearm; it preserves the paid FIFO head and cannot charge Pipeline collection or let a follower bypass it. + +## Decision + +Accepted for the reference Crossing binding. After an available current observation, select exactly one complete owner: persistence `453,486,000 / 163,961` plus 82 reads / 77 writes, or authored close `606,092,000 / 85,360` plus 57 reads / 53 writes. A reachable unavailable observation selects the state-preserving full-FIFO refusal owner `191,857,000 / 6,186` plus 13 reads / zero writes and no Pipeline collection. Post-admission `Uninitialized` is outside the reference domain. EXP-0058 owns outer source-boundary composition and EXP-0065 owns exact production regeneration. + +## Rejected Alternatives and Tradeoffs + +Reject adding page, radix, cursor, collection, hold or cleanup fragments to the complete available envelopes; subtracting aggregates to invent disjoint coefficients; treating unavailable refusal as successful Opening; allowing FIFO bypass; claiming generic mock recovery as reference-host recovery; and inflating the reference domain with unreachable post-admission `Uninitialized`. + +## Validity + +- `Establishes`: The reference available persistence/authored-close Crossing Opening envelopes over singleton radix, full-page, populated-destination and live-cursor geometry, plus the reachable unavailable refusal owner. +- `Does not establish`: Generic-host recovery, an `Uninitialized` reference state, outer source/Waiting/heap composition, public close, final production Weight identity or integrated throughput. +- `Known limitations`: Diagnostic branch fragments remain uninstalled; Root-deactivated reference feeds have no ordinary resume path, and immutable liveness implications remain outside this Weight decision. EXP-0058 and EXP-0065 retain composition and regeneration. +- `Invalidation triggers`: A changed Crossing rearm/removal/insertion/cursor path, host availability lifecycle, branch selector, collection/hold/cleanup path, source/Weight/Wasm identity or acceptance rule requires scoped requalification. +- `Residual qualitative insight`: Crossing page/radix/cursor shapes are physical alternatives inside one complete Opening owner; unavailable current state refuses before that owner mutates topology. + +## Follow-Up / Next Gradient + +Stop measuring this satisfied Crossing Opening owner. EXP-0058 owns outer FIFO source/refusal composition, EXP-0042 owns public close, EXP-0063 owns Waiting/heap work, EXP-0064 composes admission and EXP-0065 regenerates exact artifacts. Reopen only for the declared falsifier or a changed relevant path, host domain, selector or identity. + +## Architecture Impact + +The reference zero-Step input now requires available persistence/authored-close Crossing owners and a distinct unavailable refusal owner, with no duplicate Trigger or Pipeline charge. No runtime, semantic or storage change is introduced; C1 and A0 remain frozen, and complete production regeneration remains G5 work. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: [EXP-0028](./EXP-0028.md) O10 +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: [EXP-0025](./EXP-0025.md) +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: None. +- `Transfers question to`: None; independent findings require an explicit new transfer. +- `Produces input for`: [EXP-0028](./EXP-0028.md) +- `Reopen trigger`: A falsifier of this declared owner/domain or exact artifact identity; cost alone does not reopen frozen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0040.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0040.md new file mode 100644 index 00000000..64866c0b --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0040.md @@ -0,0 +1,207 @@ +# EXP-0040 — Observation Subscription Cleanup Ownership + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | [EXP-0028](./EXP-0028.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-06 | +| Affected domain | Observation subscription cleanup has a sound bound for admitted dense/sparse membership, unlink, dirty traversal and free-slot publication. | +| Physical mechanism | Observation Subscription Cleanup Ownership | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0025](./EXP-0025.md) — Accepted, physical architecture only | +| Successor experiment | [EXP-0028](./EXP-0028.md) | + + +## Decision Question + +Observation subscription cleanup has a sound bound for admitted dense/sparse membership, unlink, dirty traversal and free-slot publication. + +## Governing Specification + +- `Owners`: `template/pallets/actors/docs/specification.en.md` (sole ownership, lifecycle/Trigger/Pipeline/Action semantics, placement and scheduler); `docs/actors-resource-policy.specification.en.md` (component-wise admission/actual settlement, equal-thirds block policy); `docs/actors-performance-assurance.specification.en.md` (evidence and target). +- `Frozen physical input`: [EXP-0025](./EXP-0025.md) C1 Architecture Freeze. No change to FIFO, ticket chronology, Q1, causal floor, primary/reference ontology, source/successor atomicity or sole execution owner. +- `Semantic oracle`: A0 SHA-256 `8746e423168309975e509be91e4eb06fdf0eb0b43369d75f1ee62202a4341678` remains frozen; use only for relevant semantic equivalence, never as a Weight oracle. + +## Context and Hypothesis + +This Leaf owns observation subscription cleanup ownership. Its boundary is the single claim above. Evidence was extracted from the explicitly marked pre-fission parent subsections at Git baseline `a60de7748132211d9e58cecd5be82b41f76ce230`; this migration changes navigation and evidence ownership only. Source, Wasm, commands, rejection outcomes and evidence limitations retain their original meaning. + +Extraction provenance at the recorded Git baseline: EXP-0028 / “Last Observation Subscriber, Dirty-Feed Unlink and New Free Page”, “ObservationChange Middle-Page Unlink and Fanout Repair”, “Dense ObservationChange Close and Free-Slot Publication”, “Observation Zero-Step Rearm and Close”. Original directional prose (above/below) belongs to these pre-fission subsections; current cross-owner navigation follows the parent proof map. + +## Baseline and Candidates + +Retained measured implementations and rejected candidates are identified in the extracted evidence. The deciding reference binding uses one complete Observation authored-close owner whose component-wise resource vector conservatively envelopes the retained dense-page, middle-page unlink, dirty-feed unlink/cursor repair and existing/new free-page branches. + +## Controls + +Only the declared witness input varies within each source-bound comparison. Preserve the exact host, toolchain, database, Wasm and sampling controls printed with each evidence item; do not compare unlike artifacts as a controlled performance result. + +## Benchmark Class, Workloads, and Criteria + +Acceptance requires the single claim to hold over its declared admitted domain, with each physical operation charged once and every retained limitation resolved before broader production use. One legal omitted-owner, domain or containment counterexample falsifies that claim. Existing workloads below are bounded witnesses, not permission to add independent questions. + +## Environment and Artifact Identity + +Pre-fission baseline `a60de7748132211d9e58cecd5be82b41f76ce230`. Each extracted block retains its own exact source/Wasm/output identity. Shared pinned hardware/toolchain/cache controls follow [EXP-0030](./EXP-0030.md#environment-and-artifact-identity) only where the original comparison explicitly reuses them. A shared environment never substitutes for the block’s own source or Wasm identity. No runtime artifact was rebuilt during migration. + +## Proof Obligations + +- `Freeze`: Frozen by explicit migration reconstruction from the pre-fission decision and retained witnesses at a60de7748132211d9e58cecd5be82b41f76ce230. No new measurement or historical decision is implied. +- `Review triggers`: None. +- `Decomposition review`: Boundary reviewed during provenance-preserving fission. Workloads within this Leaf share the declared claim, physical owner, changed variable, evidence domain and acceptance rule. Independently decidable owners were transferred; a new owner or selector requires another node. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | Observation subscription cleanup has a sound bound for admitted dense/sparse membership, unlink, dirty traversal and free-slot publication. | One ordinary last-subscriber close that unlinks the middle dirty feed and allocates the next free-slot page | Reference-Wasm FRAME evidence in the declared owner/domain; native checks are construction controls | Self | Required | Named parent decision and declared downstream consumers | Satisfied for the reference Observation cleanup domain: the smallest falsifier is attained and the component-wise close envelope dominates every retained dense/sparse, unlink, dirty-cursor and free-page profile | + +## Measurements + + +### Last Observation Subscriber, Dirty-Feed Unlink and New Free Page + +The profile qualifies complete User close when the last subscriber belongs to the current middle dirty feed and slot release allocates a new free page. Ordinary paused guards leave the preceding feed with two subscriber pages and the following feed with one; an ordinary create-all/close-all donor cohort fills one free-slot page. The target occupies slot `2P−1`, the final offset of its sole sparse subscriber page. No synthetic list, cursor, slot or observation state is used. + +Block three publishes a real target observation and fanout latches User readiness. Block four publishes changed previous/target/next observations in order, then processes only the predecessor's first page. Its second page keeps that feed dirty while the list cursor advances to the target. The measured target is therefore the current middle node of a three-feed list with a full existing free page and no next free page. Native P=16 and reference-Wasm P=64 pass admission, measurement and verification. + +Complete inner Opening starts after canonical primary consumption and verifies exact User receipt/collection and authored close. The target's subscriber page/list/count, ingress revision and dirty node disappear; the dirty list retains its head/tail, has count two and moves its cursor to the successor. Whole predecessor/successor states are preserved except their mutual neighbour links. The old free page remains byte-for-byte unchanged, while a newly allocated page contains only the released target slot and total free length becomes P+1. Block-five fanout completes both surviving feeds; guards keep zero nonce, paused lifecycle and no Ready ticket. Native TryRuntime checks the prepared and resumed states, and resumption stays outside the measured Opening. + +| Profile | RefTime before DB additions / minimum (ps) | Estimated / recorded proof (bytes) | Reads / writes | Generated fragment SHA-256 | +| --- | --- | --- | --- | --- | +| `scheduler_inner_zero_step_user_observation_feed_close` | 442,102,000 / 416,819,000 | 17,572 / 9,157 | 28 / 29 | `f124c7e6fa107da9d7ea41aee964aa3e33b82bf1a8d40f1d935f7ab5b3c6f313` | + +Source content/modes `78e324b43f47731a6f1622dbca84f33d9f4637c000490ffaea83568a93490937`; benchmark source SHA-256 `fe931eb605741e726ae8b3de8b0bae6fbe5ccfbf05ff2d22797c5bababfaffe3`; benchmark Wasm SHA-256 `e63ba291ece2b472108d7d7ec1ffcdba2fc84ca6834f0bf2911694ee8cf80b45`. Same pinned reference hardware/toolchain/cache/heap/map controls as above, 50 steps × 20 repeats and no concurrent agent build/test/benchmark job during measurement. Fresh build took 102 seconds, generation 14 seconds. Reproduce with `scripts/benchmarks.sh --extrinsic scheduler_inner_zero_step_user_observation_feed_close --output /tmp/deos-exp28-zero-step-observation-feed-close.rs pallet_deos_actors`. The fragment remains temporary, uninstalled and excluded from default production generation. + +The trace binds three DirtyObservationFeeds reads/writes, dirty-list read/write and ingress-revision removal, but only one subscriber-page read/write. It also reads the absent next free page before writing the released slot; it does not rewrite the preceding full free page. Existing-page fill and new-page allocation are distinct domains of `free_len % P`, so a full existing free-slot page cannot be silently combined with the allocation branch. Relative to the preceding middle-subscriber-page unlink point, this profile has the same reads, more writes and lower proof; differing binary/population/topology prevent isolated CPU/proof subtraction or a universal winner. All User fee, hold and cleanup owners are already inside the inclusive atom. Joint maximum header/Waiting/detector geometry and other terminal/availability coverage remain open; these points do not constitute full Executive or production-envelope acceptance. + +Retained-source validation: the focused native `zero_step_user_observation_feed_close` all-features benchmark passes, including surviving-feed fanout (30 seconds); fresh Wasm passes; Actors all-features Clippy passes (22 seconds). Canonical formatting, workspace check, changed-scope completion (13 seconds), record normalization/dependency graph and diff checks pass. No existing helper, mock, runtime or production behavior changed, so prior unaffected behavioral evidence is reused and completion skips runtime tests. Preflight compilation passes (56 seconds), retaining the known stale generated-storage rejection. Source content/modes remain identical after context synchronization; G5 and complete numerical acceptance stay open. + +### ObservationChange Middle-Page Unlink and Fanout Repair + +The profile qualifies complete zero-Step User close when the target is the only subscriber on a middle page, at its final offset, both neighbouring pages are full and a real dirty fanout cursor points at that page. Ordinary second-feed fillers establish global slot spacing, then close into P−1 free slots. Ordinary paused System guards retain neighbouring subscriptions without taking Ready service. No page/slot/cursor injection or FIFO bypass is used. User admission/prefund setup is extracted into a shared helper accepting an already-created feed; earlier constructors keep their existing policy and publication behavior. + +With page size P, target-feed guards occupy global slots `0..P−1` and `2P..3P−1`, fillers use `P..2P−2` on a second feed and the User occupies `2P−1`. After filler close, page one contains P−1 None entries followed by the target, with full pages zero/two as neighbours. Guards are paused through ordinary Root control in block two. Block three publishes a changed observation and processes the three pages within the declared fanout-page bound, latching target readiness and clearing the dirty feed. Block four publishes another real change and processes page zero, leaving the authoritative dirty pointer on page one at position zero. Paused guards retain zero cycle nonce and no Ready ticket; their pending latch is not artificially cleared. + +Native P=16 and reference-Wasm P=64 admission, measurement and verification pass. Complete inner Opening consumes the canonical primary, collects the exact User fee/receipt and performs authored close. Assertions require page-one deletion, both neighbour-link rewrites, a surviving two-page list, exact subscriber/free-slot counts and unchanged dirty-state fields except next page one → two. Block-five fanout then processes the successor and clears the dirty feed without losing guards. That resumption check is outside the measured region; native TryRuntime checks both the prepared and resumed states. + +| Profile | RefTime before DB additions / minimum (ps) | Estimated / recorded proof (bytes) | Reads / writes | Generated fragment SHA-256 | +| --- | --- | --- | --- | --- | +| `scheduler_inner_zero_step_user_observation_unlink` | 429,391,000 / 402,711,000 | 18,474 / 10,059 | 28 / 27 | `406b0a7b828c1789cea5b8db3542853b0acc25e8e8dc02f8e298185619807f71` | + +Source content/modes `42e136f116b0548764a459893448b29bfe00f1fd22c722954616a42b6f25efaf`; benchmark source SHA-256 `020c6018da2ebd4c2d6220518899a7f9c34fcb5b365c2b84652110453e0f5430`; benchmark Wasm SHA-256 `ba30994bc84f6c93499b3ebfe1d05c522f6444d76e55e42e170ebc6502ddcd41`. Same pinned reference hardware/toolchain/cache/heap/map controls as above, 50 steps × 20 repeats, no concurrent agent build/test/benchmark job during measurement. Fresh build took 102 seconds and generation 15 seconds. Reproduce with `scripts/benchmarks.sh --extrinsic scheduler_inner_zero_step_user_observation_unlink --output /tmp/deos-exp28-zero-step-observation-unlink.rs pallet_deos_actors`. The temporary fragment is uninstalled and excluded from default production generation. + +The trace binds three subscriber pages, the subscriber-page list and DirtyObservationFeeds as read/write, plus the complete existing free-slot publication and User terminal owners. The remaining feed has 2P guards, so it is not removed from the dirty-feed list. Compared with the preceding dense-page point, this branch has more reads/writes and higher CPU/proof, but changed binary/population/topology preclude an isolated unlink-cost subtraction. Retain the complete inclusive atom; do not add collection, holds or cleanup again. Dirty-feed unlink, new free-page allocation and maximum joint header/Waiting/detector geometry remain separate coverage requirements, and no full Executive acceptance is claimed. + +Retained-source validation: the complete native `zero_step_user_` all-features benchmark family passes after shared User-constructor extraction (36 seconds), including the new unlink and next-block fanout checks. Fresh Wasm passes; Actors all-features Clippy passes (22 seconds). Canonical formatting, workspace check, changed-scope completion (14 seconds), record normalization/dependency graph and diff checks pass. Runtime/mock/production behavior is unchanged, so prior unaffected behavioral evidence is reused and completion skips runtime tests. Preflight compilation passes (61 seconds), retaining the known stale generated-storage rejection. Source content/modes remain identical after context updates; G5 and full numerical acceptance remain open. + +### Dense ObservationChange Close and Free-Slot Publication + +Source ownership: `scheduler.rs::prepare_opening_rearm_hot` handles ObservationChange by removing indexed disablement; it does not read subscriber pages. The earlier persistent observation trace likewise reads Actor-owned feed/slot metadata but no subscriber/free-slot page. Page density therefore does not justify a new persistent rearm coefficient. Close instead preflights/removes its subscriber through `subscriptions.rs` and publishes the released global slot; that is the measured owner here. + +The new profile uses the existing real-publication User readiness constructor. Ordinary zero-Step System guards fill its subscriber page, then a separately created cohort of P−1 subscribed donors is closed through Root to populate the reusable free-slot page. Donors are all created before any closes, so reuse cannot collapse the intended free cardinality to one. The target is at offset zero, the subscriber vector is dense and full, the free page has P−1 entries, and the target feed is no longer dirty. No page, slot, Contract, readiness or observation state is injected. + +Native and reference-Wasm admission, measurement and verification pass with P=16 and P=64 respectively. Complete authored zero-Step close leaves a full-length subscriber vector with only the target position cleared, preserves the last guard's nonce/readiness, reduces subscriber count to P−1 and appends the released slot to make the existing free page full. Exact User receipt, sovereign/Fee Sink deltas and terminal cleanup are checked, with native TryRuntime before and after the measured inner Opening. This measures two large stored values inside the inclusive atom without claiming the maximum joint header/page/link geometry. + +| Profile | RefTime before DB additions / minimum (ps) | Estimated / recorded proof (bytes) | Reads / writes | Generated fragment SHA-256 | +| --- | --- | --- | --- | --- | +| `scheduler_inner_zero_step_user_observation_page_close` | 370,793,000 / 335,733,000 | 16,333 / 7,918 | 24 / 23 | `3b18545826fc90110e25385051a80d39576fdcfe51d53a9caebd21c4eef625cc` | + +Source content/modes `b6f4c6231cf71d8a809964e6bbbe7e160258c4e30e0037b629af9abd077f5e7b`; benchmark source SHA-256 `2b259ef01a4622c392cec0d5a67596f8fabe91fab678d0acc69fdd7d169b336b`; benchmark Wasm SHA-256 `d48188b0d6a18a88e12dbf9981cc7f02a070b70cacbbf030eb96c990706d342f`. Same pinned reference hardware/toolchain/cache/heap/map controls as above, 50 steps × 20 repeats and no concurrent agent build/test/benchmark job during measurement. Fresh build took 102 seconds, generation 15 seconds. Reproduce with `scripts/benchmarks.sh --extrinsic scheduler_inner_zero_step_user_observation_page_close --output /tmp/deos-exp28-zero-step-observation-page-close.rs pallet_deos_actors`. The fragment remains temporary, uninstalled and excluded from default production generation. + +The trace includes one subscriber-page and one free-slot-page read/write, while the surviving feed/page avoids last-subscriber dirty cleanup and page-list unlink. Compared with the historical single-subscriber close, this point has fewer unique reads/writes but higher measured CPU and proof; binaries, state population and branch topology differ, so no isolated page-cost subtraction or global dominance follows. Collection, holds and cleanup are already inside this inclusive atom. Empty/middle-page unlink, dirty-list/cursor context, new free-page allocation and maximal joint geometry remain separate coverage requirements. + +Retained-source validation: the focused native `zero_step_user_observation_page_close` all-features benchmark passes (28 seconds), fresh Wasm passes, and Actors all-features Clippy passes (23 seconds). Canonical formatting, workspace check, changed-scope completion (14 seconds), record normalization/dependency graph and diff checks pass. Production/runtime/mock behavior is unchanged, so prior unaffected behavioral evidence is reused and completion skips runtime tests. Preflight compilation passes (55 seconds), retaining the known stale generated-storage rejection. Source content/modes remain identical after context synchronization; no G5 or full numerical acceptance follows. + + + +### Observation Zero-Step Rearm and Close + +Five admitted User profiles use a real host publication, ordinary Crossing/fanout readiness and the same consumed-primary inner boundary as Manual/Cadenced. Persistent profiles verify latch consumption and retained subscription or exact Crossing phase/revision; terminal profiles require AutoCloseNonceReached and absent identity, head, subscription/membership and state holds. All check one nonzero Pipeline receipt, exact sovereign/Fee Sink deltas and no Run. The native publication adapter tracks revisions only for benchmark publications, preserving the previous default for other mock cases and clearing its state on fixture reset. + +The first reference-Wasm probe falsified sample/value equality: the reference factory creates an EMA feed, and publishing sample two after value one produced value zero at revision two under its rounded aggregation. This is a fixture defect, not permission to replace the reference feed or alter Oracle semantics. Failed source `1bdeaa67754d461f4eef864b4f0ce81219c2ca4063c83fea5f1dc846f5d6371e`, benchmark Wasm `18334f869dbb27045e8ade689f9552cc8f89114a5fcd3f169c141ec124ca5aef`, log SHA-256 `4f34931f15b0d8656cf75efcd0b19c1b0c1728c03b9f53231d80523f1829ad54`; the `scheduler_inner_zero_step_user_crossing_armed` generation route failed before measurement. The corrected setup publishes sample 1,000, verifies the actual value crosses two, and reaches Armed through bounded decreasing publications of sample one while latched. Every publication checks the authoritative value/revision relation; threshold two and rearm one remain unchanged. + +Armed preparation advances to the next block after the first latch. It publishes decreasing samples during that block's external phase, then services the deferred worker once. The reference EMA half-life 100 maps the initial sample 1,000 to value nine, and eight samples of one reach value one within MaxCrossingTransitionsPerBlock=8. The benchmark checks actual values/revisions and no second Trigger fee while latched; it does not substitute a LastValue feed or inject detector state. + +| User zero-Step profile | RefTime before DB additions / minimum (ps) | Estimated / recorded proof (bytes) | Reads / writes | Generated fragment SHA-256 | +| --- | --- | --- | --- | --- | +| `scheduler_inner_zero_step_user_observation` | 121,665,000 / 116,986,000 | 7,857 / 1,917 | 9 / 6 | `03ea4e95e45e75bf05959bfec79ab2c3b32b64022eb77a1500353f021cca34d1` | +| `scheduler_inner_zero_step_user_observation_close` | 261,001,000 / 252,829,000 | 11,333 / 2,918 | 26 / 26 | `c57710448b6b8e56f522c1ff836e35fbbea45c299bc7724b5418d09e8cfbbf74` | + +All five pass reference-Wasm admission, measurement and postconditions at 50 steps × 20 repeats on one fresh binary. Source content/modes `f9190bb04b4c99e4f032b1264fc71657ae361328301ea8e758d598f8d928da4b`; benchmark source SHA-256 `85aea2cd4777265cb5b326679f021d0e1edbceb5c700fdbbb0ca3a2cb382ff42`; reference helper source SHA-256 `ab4c5eb0d496b55c8ea459b48f549fd01a69a80dfd0f24a02f5ae5dd4b28fe5d`; benchmark Wasm SHA-256 `737feb8338b38d05ebae05efda65a66c636cd7f88f19a4b8f28357116f2587e8`. Same pinned toolchain/hardware/cache/heap/map controls as above; no concurrent agent build/test/benchmark job during measurement. + +Reproduce with `scripts/benchmarks.sh --extrinsic PROFILE --output /tmp/deos-exp28-zero-step-KIND.rs pallet_deos_actors`. Use the table's profile order and KIND=`crossing-armed`, `crossing-waiting`, `crossing-close`, `observation`, `observation-close`; add `--skip-build` only for the last four on the unchanged fresh binary. The corrected runtime build took 96 seconds and profiles took 13/14/13/13/13 seconds. Exact-selector exclusions keep every new diagnostic out of default production generation. Fragments remain temporary and uninstalled. + +Crossing's trace includes Oracle feed/observation reads, leaf/member pages and radix-index work. Armed rearm touches 64 unique CrossingRadixNodes keys, while same-phase WaitingForRearm touches 32; both remove and reinsert membership. Close repeats cleanup inside the same transaction, so unique-key DB counts do not enumerate every logical access. ObservationChange persistence deletes indexed disablement; its close additionally removes subscription topology and lifecycle state. These are complete inclusive atoms: do not add collection, rearm, holds or cleanup again, or select a global bound from the pre-DB CPU coefficient alone. Compare the explicit DB terms and both proof dimensions. + +This fresh single-member feed geometry does not establish full member-page/threshold-tree coverage, non-tail removal, concurrent cohort/range-cursor cases, maximum zero-Step head, unavailable observations or every terminal branch. Complete numerical qualification and production binding remain open; C1, monetary rules, Oracle aggregation and production execution are unchanged. + +Validation: full Actors all-features tests passed after the shared mock revision change (73 seconds). After fixture-only EMA and phase-order corrections, the focused `zero_step_user_` all-features family passed (20 seconds); unaffected suite evidence is reused because its mock owner is unchanged. Actors/runtime all-features Clippy, canonical formatting, workspace check, record normalization/graph and changed-scope completion pass. Completion uses `--skip-runtime-tests` for the benchmark-only runtime helper diff, reusing unchanged default-runtime behavioral evidence. Preflight compilation passes while the known stale generated-storage audit still rejects G5; the final corrected fixture is separately compiled and verified by native and fresh-Wasm runs. Source content/modes remain identical to the measurement identity after context updates. No full Executive, production-Wasm or metadata acceptance is claimed. + + +### Deciding Observation Cleanup Envelope + +The retained reference-Wasm profiles attain the declared physical alternatives through ordinary admission, publication, fanout and close. The dense-page profile clears one entry from a surviving full subscriber page and fills an existing free page. The middle-page profile deletes the target subscriber page between two full neighbours and repairs the live dirty-fanout page cursor. The last-subscriber profile directly attains the smallest falsifier: it removes the current middle dirty feed from a three-feed list, repairs the list cursor to the successor and allocates a new free page while preserving the preceding full page. + +No one retained profile dominates every resource coordinate. The complete reference Observation authored-close owner therefore selects the component-wise envelope `442,102,000` pre-database RefTime, `18,474` estimated ProofSize, 28 reads and 29 writes. RefTime and writes come from `scheduler_inner_zero_step_user_observation_feed_close`; estimated ProofSize comes from `scheduler_inner_zero_step_user_observation_unlink`; both require 28 reads. Under the pinned reference RocksDB charge, the resulting RefTime is `4,042,102,000`. This is a conservative branch envelope over complete inclusive atoms, not an additive sum or a claim that the coordinates were jointly attained by one profile. + +The envelope already includes canonical primary consumption, one Pipeline collection, applicable User hold reconciliation, authored terminal cleanup, subscription/feed unlink and free-slot publication. Maximum and actual select this one Observation authored-close owner; none of those operations may be charged again. The retained nonterminal Observation profile stays a separate persistence owner. Outer Ready/Waiting source and heap work, unrelated terminal paths, complete admission composition and generated-file regeneration remain with EXP-0058, EXP-0063, EXP-0064 and EXP-0065 respectively. + +## Derived Metrics + +The selected vector is the coordinate-wise maximum of complete legal branch atoms. It contains the retained dense-page point `370,793,000 / 16,333` plus 24 reads / 23 writes, middle-page unlink point `429,391,000 / 18,474` plus 28 / 27 and last-subscriber dirty-feed/new-page point `442,102,000 / 17,572` plus 28 / 29. No aggregate subtraction, fitted causal attribution or cross-artifact performance ranking is inferred. + +## Statistical / Variance Notes + +Retain each item’s original sampling and uncertainty. Singleton construction probes establish only their admitted state/access witness, never statistical timing dominance or a production bound. + +## Result + +The retained complete authored-close profiles cover surviving dense pages, middle subscriber-page unlink with dirty cursor repair, last-subscriber middle dirty-feed unlink and both existing-page fill and new free-page allocation. Their resource vectors are contained by the selected component-wise envelope, and the smallest falsifier is directly attained. + +## Interpretation + +Dense/sparse subscriber shape, subscriber-page unlink, dirty-feed unlink/cursor repair and free-page fill/allocation are physical alternatives inside one inclusive Observation authored-close owner. Different profiles select different maximum resource coordinates, so choosing one attained profile would understate another coordinate; summing them would duplicate the common close atom. + +## Decision + +Accepted for the reference Observation cleanup binding. Select one component-wise authored-close owner: `442,102,000` pre-database RefTime, `18,474` estimated ProofSize, 28 reads and 29 writes (`4,042,102,000` RefTime under the pinned RocksDB charge). Maximum and actual use the same owner. EXP-0065 owns exact production regeneration; no diagnostic fragment or coordinate may be manually installed, added or repaired. + +## Rejected Alternatives and Tradeoffs + +Reject choosing only the last-subscriber point because it has the largest RefTime/writes but lower estimated proof than middle-page unlink; choosing only middle-page unlink; adding dense, unlink and feed-close atoms; subtracting aggregates to invent cleanup fragments; charging collection, holds, terminal cleanup or free publication twice; and manually patching the stale generated owner before EXP-0065. + +## Validity + +- `Establishes`: One reference Observation authored-close envelope over retained dense/sparse subscriber membership, middle-page unlink, dirty-feed unlink/cursor repair and existing/new free-page publication. +- `Does not establish`: Nonterminal persistence binding, outer Ready/Waiting/heap composition, unrelated terminal paths, exact final production Weight identity or integrated throughput. +- `Known limitations`: The selected envelope is component-wise and its diagnostic fragments remain uninstalled; complete admission and regeneration remain EXP-0064/EXP-0065 work. +- `Invalidation triggers`: A changed Observation subscription/feed removal, cursor repair, free-slot publication, collection/hold/cleanup path, source/Weight/Wasm identity or acceptance rule requires scoped requalification. +- `Residual qualitative insight`: One terminal semantic owner may require coordinate-wise selection across mutually exclusive physical branch shapes. + +## Follow-Up / Next Gradient + +Stop measuring this satisfied Observation cleanup owner. EXP-0058 owns outer source composition, EXP-0063 owns Waiting/heap work, EXP-0064 composes admission and EXP-0065 regenerates exact artifacts. Reopen only for the declared falsifier or a changed relevant cleanup path, domain, selector or identity. + +## Architecture Impact + +The reference zero-Step input now requires one component-wise Observation authored-close owner covering subscription/feed cleanup and free-slot publication without duplicate Pipeline, hold or terminal charges. No runtime, semantic or storage change is introduced; C1 and A0 remain frozen, and complete production regeneration remains G5 work. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: [EXP-0028](./EXP-0028.md) O11 +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: [EXP-0025](./EXP-0025.md) +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: None. +- `Transfers question to`: None; independent findings require an explicit new transfer. +- `Produces input for`: [EXP-0028](./EXP-0028.md) +- `Reopen trigger`: A falsifier of this declared owner/domain or exact artifact identity; cost alone does not reopen frozen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0041.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0041.md new file mode 100644 index 00000000..320d0ddc --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0041.md @@ -0,0 +1,177 @@ +# EXP-0041 — Pipeline Admission Apoptosis Ownership + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | [EXP-0028](./EXP-0028.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-06 | +| Affected domain | Pipeline admission insolvency has one custody-neutral minimal-apoptosis owner covering its admitted detector, cold-tail and window domain. | +| Physical mechanism | Pipeline Admission Apoptosis Ownership | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0025](./EXP-0025.md) — Accepted, physical architecture only | +| Successor experiment | [EXP-0028](./EXP-0028.md) | + + +## Decision Question + +Pipeline admission insolvency has one custody-neutral minimal-apoptosis owner covering its admitted detector, cold-tail and window domain. + +## Governing Specification + +- `Owners`: `template/pallets/actors/docs/specification.en.md` (sole ownership, lifecycle/Trigger/Pipeline/Action semantics, placement and scheduler); `docs/actors-resource-policy.specification.en.md` (component-wise admission/actual settlement, equal-thirds block policy); `docs/actors-performance-assurance.specification.en.md` (evidence and target). +- `Frozen physical input`: [EXP-0025](./EXP-0025.md) C1 Architecture Freeze. No change to FIFO, ticket chronology, Q1, causal floor, primary/reference ontology, source/successor atomicity or sole execution owner. +- `Semantic oracle`: A0 SHA-256 `8746e423168309975e509be91e4eb06fdf0eb0b43369d75f1ee62202a4341678` remains frozen; use only for relevant semantic equivalence, never as a Weight oracle. + +## Context and Hypothesis + +This Leaf owns pipeline admission apoptosis ownership. Its boundary is the single claim above. Evidence was extracted from the explicitly marked pre-fission parent subsections at Git baseline `a60de7748132211d9e58cecd5be82b41f76ce230`; this migration changes navigation and evidence ownership only. Source, Wasm, commands, rejection outcomes and evidence limitations retain their original meaning. + +Extraction provenance at the recorded Git baseline: EXP-0028 / “Insolvency with Cold Contract Tails and Future Window References”, “Paid Trigger and Pipeline-Admission Insolvency”. Original directional prose (above/below) belongs to these pre-fission subsections; current cross-owner navigation follows the parent proof map. + +## Baseline and Candidates + +Retained measured implementations and rejected candidates are identified in the extracted evidence. The deciding reference binding selects the attained maximum-Contract Crossing plus future-window full-FIFO atom, which dominates every retained insolvency profile component-wise. + +## Controls + +Only the declared witness input varies within each source-bound comparison. Preserve the exact host, toolchain, database, Wasm and sampling controls printed with each evidence item; do not compare unlike artifacts as a controlled performance result. + +## Benchmark Class, Workloads, and Criteria + +Acceptance requires the single claim to hold over its declared admitted domain, with each physical operation charged once and every retained limitation resolved before broader production use. One legal omitted-owner, domain or containment counterexample falsifies that claim. Existing workloads below are bounded witnesses, not permission to add independent questions. + +## Environment and Artifact Identity + +Pre-fission baseline `a60de7748132211d9e58cecd5be82b41f76ce230`. Each extracted block retains its own exact source/Wasm/output identity. Shared pinned hardware/toolchain/cache controls follow [EXP-0030](./EXP-0030.md#environment-and-artifact-identity) only where the original comparison explicitly reuses them. A shared environment never substitutes for the block’s own source or Wasm identity. No runtime artifact was rebuilt during migration. + +## Proof Obligations + +- `Freeze`: Frozen by explicit migration reconstruction from the pre-fission decision and retained witnesses at a60de7748132211d9e58cecd5be82b41f76ce230. No new measurement or historical decision is implied. +- `Review triggers`: None. +- `Decomposition review`: Boundary reviewed during provenance-preserving fission. Workloads within this Leaf share the declared claim, physical owner, changed variable, evidence domain and acceptance rule. Independently decidable owners were transferred; a new owner or selector requires another node. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | Pipeline admission insolvency has one custody-neutral minimal-apoptosis owner covering its admitted detector, cold-tail and window domain. | One paid useful Trigger leaving the Actor one balance unit short of Pipeline admission with retained detector/window state | Reference-Wasm FRAME evidence in the declared owner/domain; native checks are construction controls | Self | Required | Named parent decision and declared downstream consumers | Satisfied for the admitted reference domain: the maximum-Contract Crossing plus future-window full-FIFO witness attains the falsifier and dominates every retained insolvency profile | + +## Measurements + + +### Insolvency with Cold Contract Tails and Future Window References + +Question: qualify jointly reachable retained cold Contract chunks, Crossing topology, User holds and a future Block window reference at Pipeline-admission insolvency. `remove_admitted_contract_geometry_with_admission` loads and deletes the complete admitted geometry; `finalize_actor_loaded_inner` invalidates future references before process removal. The earlier zero-Step full-FIFO point contains neither cold tail chunks nor a future window reference. Those actual owners, not a hypothetical optimization, require this domain witness. + +The shared ordinary paid-insolvency constructor now accepts authored Steps and an optional minimum-length window while preserving the former zero-Step inputs. Paired `scheduler_paged_user_crossing_insolvency_max_contract` and `_window` profiles hold a maximum-Step Transfer Contract constant and vary only the retained future window reference. Native `pipeline_insolvency_cleans_each_admitted_tail_and_future_window` checks every Step count through the host maximum with and without that reference, exact tail chunk counts and custody-neutral cleanup. New profiles remain excluded from default production generation. Step-count maximum is not maximum Contract bytes, maximum detector population or a complete Waiting/heap envelope; admission must succeed normally before any measured claim. + +Native qualification passes all 26 combinations (`S = 0..12`, window absent/present). Fresh reference Wasm admits both 12-Step points, records real Trigger payment and a one-unit Pipeline shortfall, and verifies complete process/hold/tail/reference removal with unchanged custody/Fee Sink and no new Pipeline receipt. Twelve Steps are therefore not globally resource-inadmissible: the earlier 9/12-Step CompleteMax rejection is specific to that expensive authored geometry, not permission to reduce the semantic ceiling. + +| Full FIFO insolvency profile | Generated RefTime / minimum ps | Estimated / recorded proof B | Reads / writes | Temporary fragment SHA-256 | +| --- | --- | --- | --- | --- | +| `scheduler_paged_user_crossing_insolvency_max_contract` | 577,317,000 / 525,703,000 | 86,387 / 6,197 | 63 / 58 | `50f5228b1be68e37b28dc540638415b989fc8f5fb8900dc0498c28b7882bf0f6` | +| `scheduler_paged_user_crossing_insolvency_max_contract_window` | 709,110,000 / 603,717,000 | 86,870 / 6,680 | 70 / 65 | `315aef46e499b267d838151de333f123334eb1f2a5668c947d6aef05352b691f` | + +Source content/modes `692d88aa58c342f5fa0bf3cab929d822cb08871cbeac073fcc2fb0539a1d6178`; benchmark source SHA-256 `536d3908b423e3ca1abcb61b8d78116478e0397f7858b4b45f82340ff1abc2df`; benchmark Wasm SHA-256 `ef01d799221a721ceaaff52a2b8e74786d44b0f2b91f1cc4d0948994d02dbba4`. Same pinned reference toolchain/hardware/cache/heap/map controls as above, 50 steps × 20 repeats, no concurrent agent build/test/benchmark job during measurement. Fresh build took 110 seconds and each generation 14 seconds. Both profiles use Measured proof mode. + +Reproduce with `scripts/benchmarks.sh --extrinsic scheduler_paged_user_crossing_insolvency_max_contract --output /tmp/deos-exp28-insolvency-max-contract.rs pallet_deos_actors`, then `scripts/benchmarks.sh --skip-build --extrinsic scheduler_paged_user_crossing_insolvency_max_contract_window --output /tmp/deos-exp28-insolvency-max-contract-window.rs pallet_deos_actors` on the same binary. Both fragments remain temporary and uninstalled. + +Both traces include exactly three cold tail reads/writes. The window variant additionally reads/writes the exact Waiting chunk, occupancy, head, tail, cursor index, cursor page and cursor length. It removes the last entry of a singleton Block key; it does not qualify a populated heap's competing removal branches. The inclusive values already cover FIFO liability, source handling, Crossing and lifecycle/hold cleanup. Do not add those owners again or subtract the pair to fabricate a standalone general reference-removal coefficient. EXP-0029 owns joint reachability and EXP-0063 retains the complete heap envelope; G5 production binding remains open. + +Validation: the focused all-length native property passes (33 seconds), followed by the complete native `insolvency` family (16 seconds) and the reused direct `pipeline_admission_apoptosis` family (2 seconds). Actors all-features Clippy (25 seconds), canonical formatting, workspace check, changed-scope completion (13 seconds), record normalization/dependency graph and diff checks pass. Only benchmark setup/assertions and private accounting names changed; former zero-Step constructors retain identical authored inputs and balances, and prior production/runtime evidence remains applicable. Completion uses `--skip-runtime-tests`. Preflight compilation passes (63 seconds), retaining the known G5 stale generated-storage rejection. Source content/modes remain equal after context synchronization; no production numeric envelope is accepted. + + + +### Paid Trigger and Pipeline-Admission Insolvency + +Two complete FIFO profiles ordinarily create a zero-Step User whose paid Manual or Crossing occurrence leaves exactly one balance unit below Pipeline capacity. Setup prefunds ordinary creation and adds Trigger fee minus one before the real source event; it never burns balances, mutates Contracts or injects latches. Crossing retains indexed membership after readiness. Measurement includes canonical FIFO discovery, liability check, source retirement and cleanup. Postconditions require no Run/Action/Pipeline collection, unchanged post-Trigger custody/Fee Sink balances and complete process removal. Singleton Ready and fresh Crossing geometry remain narrower than complete production envelopes. + +| Complete FIFO profile (`Measured`) | RefTime before DB additions / minimum (ps) | Estimated / recorded proof (bytes) | Reads / writes | Generated fragment SHA-256 | +| --- | --- | --- | --- | --- | +| `scheduler_paged_zero_step_user_insolvency` | 261,490,000 / 242,423,000 | 8,693 / 2,753 | 22 / 17 | `8f6d5c667b2f85d1717c7dc793eee09b4efd078e71efad413ac42edd4e9b92a5` | +| `scheduler_paged_zero_step_user_crossing_insolvency` | 491,830,000 / 461,518,000 | 85,256 / 5,066 | 60 / 55 | `57d1160a04e731cdb45da888b2462223d242273afdc54c017f9b0aab09f0451a` | + +Both pass reference-Wasm admission, measurement and postconditions on source `cad7d896f10545e5a7a85231e2b2d30ddb57eea0d7bbef2b3a271e2463755210`, benchmark source SHA-256 `a2c6a3d31b55bbd7ddc5c000f33fd48675127b63ab2eb94b81974550719f0ffe`, Wasm `28f1b05bb40519d62bbd35dbc910197fde268ca2d73885e481ee94a3ec0e09f4`. Reproduce with `scripts/benchmarks.sh --extrinsic PROFILE --output /tmp/deos-exp28-zero-step-KIND.rs pallet_deos_actors`, using the table order and KIND=`insolvency`, `crossing-insolvency`; add `--skip-build` only for the second. Fresh build took 105 seconds; each measurement took 14 seconds. + +That full-FIFO comparison exposes retained Crossing cleanup, but cannot isolate a cleanup coefficient by subtraction. The next pair uses the same `finalize_actor` boundary as the existing `pipeline_admission_apoptosis` fixture and `MaxEncodedLen` for both profiles. Manual's owner becomes an ordinary benchmark account because scheduler-owned hold release must include its ledger access; the old `whitelisted_caller` fixture and generated trace omitted that System account. Source inspection also rejects the old maximum-Contract comment: `make_contract_steps` supplies one Transfer Step. The comparison therefore qualifies Manual one-Step versus Crossing zero-Step cleanup, not a maximum Contract. + +| Direct cleanup profile (`MaxEncodedLen`) | RefTime before DB additions / minimum (ps) | Estimated / recorded proof (bytes) | Reads / writes | Generated fragment SHA-256 | +| --- | --- | --- | --- | --- | +| `pipeline_admission_apoptosis` | 206,174,000 / 191,787,000 | 15,106 / 2,677 | 19 / 16 | `64714694ebb0787d2e956e2ff7ebe21d0a5378f0fbde5a6b5ccfd6da6489ac58` | +| `pipeline_admission_apoptosis_crossing` | 440,426,000 / 400,615,000 | 81,886 / 4,850 | 57 / 54 | `1824d07afc73ef65b63fad9e0854767b4936e1519eff3d03b2c694dbbac87238` | + +Both direct profiles pass on source `75509f5108862238d3e0e0566725d755ce6c4cfedbb4ad8d22fab649ed8db568`, benchmark source SHA-256 `98cce743e42983b0ba6279f591dca0094a2f746432857da57bebed2242cc94ea`, Wasm `3a4fe1243913423b6536a0d95dc3f4edb2ed90c063f7ee91e56a06603cd375df`. Reproduce with `scripts/benchmarks.sh --extrinsic PROFILE --output /tmp/deos-exp28-apoptosis-KIND.rs pallet_deos_actors`, table order and KIND=`manual`, `crossing`, adding `--skip-build` for the second. Fresh build took 103 seconds; measurements took 13/14 seconds. All four measurements use 50 steps × 20 repeats, the pinned environment above and no concurrent agent build/test/benchmark job during measurement. Fragments remain uninstalled; all three new diagnostic profiles are excluded from default production generation. + +The two direct traces now include one System account read/write for owner hold release. Crossing additionally touches 32 radix nodes and its leaf/member/feed topology, despite having zero Steps. This rejects Manual-only qualification of `close_admission_decision`'s common `pipeline_admission_apoptosis` owner; merely regenerating the Manual fixture cannot establish the missing domain. The direct Crossing fixture retires the resulting FIFO tombstone only after measurement. Both inclusive boundaries already contain their declared source/cleanup work, so add neither twice. Neither pair bounds maximum Contract/header, populated detector/Waiting geometry or every insolvency state. EXP-0064 must reconcile the admitted cleanup domain before G5 binding; C1 and monetary rules are unchanged. + +After measurement, only two benchmark comment lines changed to correct the one-Step domain; executable setup and measured bodies are unchanged. Retained source is `8ba757ba38b54c05a1092862e409c8f44240d5df546cb0641c2553b16ba12d61`, benchmark SHA-256 `dbece9d420c67edc9cb626c75f03fa3e1659731443f132638da1a66b3e3d3749`. Numerical evidence keeps its measured identities rather than claiming a new exact-tree Wasm binding. + +Validation: focused native all-features Manual/Crossing FIFO tests pass (29/1 seconds); the final direct-apoptosis family passes after the owner/PoV correction (13 seconds). Workspace all-features Clippy passes (23 seconds), as do canonical formatting, workspace check, changed-scope completion (14 seconds), record normalization/dependency graph and diff checks. No production/runtime helper behavior changed, so unaffected behavioral evidence is reused and completion skips runtime tests. The final preflight compiles (59 seconds) and retains the known stale generated-storage rejection; G5 remains open. Context updates preserve the retained source identity above. + + +### Deciding Pipeline-Apoptosis Owner + +The retained reference-Wasm profiles partition ordinary Manual and Crossing cleanup, complete FIFO service, maximum-Step cold Contract tails and an optional future Block reference. `scheduler_paged_user_crossing_insolvency_max_contract_window` directly attains the smallest falsifier after a real paid Trigger: a 12-Step User Crossing Actor remains one balance unit short of Pipeline admission while retaining three cold tail chunks and one future window reference, then closes without Pipeline collection, Action invocation, custody movement or residue. + +Its attained vector, `709,110,000` pre-database RefTime, `86,870` estimated ProofSize, 70 reads and 65 writes, dominates every retained direct and full-FIFO insolvency profile in all four charged coordinates. Under the pinned reference RocksDB charge, the resulting RefTime is `8,959,110,000`. No cross-artifact subtraction or synthetic coordinate envelope is needed. + +This is one inclusive reference branch owner: canonical Ready discovery/consumption, Crossing cleanup, complete admitted Contract-tail deletion, singleton Block-reference invalidation, lifecycle removal and applicable owner-hold release occur once. EXP-0058 still owns source/destination composition for other transitions, but its generic source owner must not be added again when this complete apoptosis atom is selected. EXP-0063 retains populated Waiting-page and competing heap-repair coverage; the attained singleton reference does not decide those envelopes. EXP-0065 owns the exact generated selector and must regenerate it from the complete accepted G4 composition rather than subtracting or manually patching fragments. + +## Derived Metrics + +The selected attained vector contains the maximum-Contract no-window point `577,317,000 / 86,387` plus 63 reads / 58 writes, zero-Step Crossing FIFO `491,830,000 / 85,256` plus 60 / 55, direct Crossing cleanup `440,426,000 / 81,886` plus 57 / 54, and both narrower Manual points component-wise. Its RocksDB total is `709,110,000 + 70 × 25,000,000 + 65 × 100,000,000 = 8,959,110,000` RefTime. + +## Statistical / Variance Notes + +Retain each item’s original sampling and uncertainty. Singleton construction probes establish only their admitted state/access witness, never statistical timing dominance or a production bound. + +## Result + +The maximum-Contract Crossing plus future-window witness attains the declared detector/cold-tail/reference falsifier and contains every retained insolvency vector component-wise. Its postconditions establish custody-neutral complete process, hold, tail and reference removal with no Pipeline receipt. + +## Interpretation + +For the admitted reference domain, Pipeline-admission insolvency is one terminal FIFO atom rather than a direct cleanup fragment plus independently repeated source, detector, tail or reference charges. Populated Waiting/heap alternatives remain separate composition inputs, not reasons to keep this attained owner open. + +## Decision + +Accepted for the admitted reference Pipeline-apoptosis domain. Select the attained complete FIFO owner at `709,110,000` pre-database RefTime, `86,870` estimated ProofSize, 70 reads and 65 writes (`8,959,110,000` RefTime under the pinned RocksDB charge). Maximum and actual select the same custody-neutral atom. EXP-0065 owns exact regeneration; no diagnostic fragment, subtraction or manual coefficient repair is accepted. + +## Rejected Alternatives and Tradeoffs + +Reject the stale Manual-only generated owner; direct Crossing cleanup as a complete bound; the no-window maximum-Contract point; adding generic FIFO/source, Crossing, tail, hold or reference work to the selected inclusive atom; subtracting aggregate profiles to fabricate an inner coefficient; treating the singleton reference as a populated-heap envelope; and manually patching production Weight before EXP-0065. + +## Validity + +- `Establishes`: One attained reference full-FIFO apoptosis owner over admitted maximum-Step cold tails, Crossing topology, one future Block reference and applicable User hold cleanup. +- `Does not establish`: Populated Waiting-page/heap removal, every Contract byte/header or detector population combination, exact final production Weight identity or integrated throughput. +- `Known limitations`: The selected diagnostic profile remains uninstalled; EXP-0058/EXP-0063/EXP-0064 must preserve no-double-charge composition before EXP-0065 regeneration. +- `Invalidation triggers`: A changed insolvency detector, Contract-tail/reference/hold cleanup, source boundary, admitted host domain, source/Weight/Wasm identity or selector rule requires scoped requalification. +- `Residual qualitative insight`: An attained terminal atom can close its scoped owner while larger source/heap populations remain independently composed. + +## Follow-Up / Next Gradient + +Stop measuring this satisfied apoptosis owner. EXP-0058 owns other source/destination branches, EXP-0063 owns populated Waiting/heap envelopes, EXP-0064 composes admission and EXP-0065 regenerates exact artifacts. Reopen only for the declared falsifier or a changed relevant path, domain, selector or identity. + +## Architecture Impact + +The reference Pipeline-insolvency input now requires one inclusive full-FIFO owner covering source consumption, Crossing cleanup, maximum-Step cold tails, singleton future-reference invalidation and User hold release without duplicate charges. No runtime, semantic or storage change is introduced; C1 and A0 remain frozen, and complete production regeneration remains G5 work. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: [EXP-0028](./EXP-0028.md) O12 +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: [EXP-0025](./EXP-0025.md) +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: None. +- `Transfers question to`: None; independent findings require an explicit new transfer. +- `Produces input for`: [EXP-0028](./EXP-0028.md) +- `Reopen trigger`: A falsifier of this declared owner/domain or exact artifact identity; cost alone does not reopen frozen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0042.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0042.md new file mode 100644 index 00000000..6e9f5487 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0042.md @@ -0,0 +1,172 @@ +# EXP-0042 — Public Close Cleanup Ownership + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | [EXP-0028](./EXP-0028.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-06 | +| Affected domain | Public close cleanup has one sound bound covering admitted Contract/Run, live detector cursor and owner-ledger release. | +| Physical mechanism | Public Close Cleanup Ownership | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0025](./EXP-0025.md) — Accepted, physical architecture only | +| Successor experiment | [EXP-0028](./EXP-0028.md) | + + +## Decision Question + +Public close cleanup has one sound bound covering admitted Contract/Run, live detector cursor and owner-ledger release. + +## Governing Specification + +- `Owners`: `template/pallets/actors/docs/specification.en.md` (sole ownership, lifecycle/Trigger/Pipeline/Action semantics, placement and scheduler); `docs/actors-resource-policy.specification.en.md` (component-wise admission/actual settlement, equal-thirds block policy); `docs/actors-performance-assurance.specification.en.md` (evidence and target). +- `Frozen physical input`: [EXP-0025](./EXP-0025.md) C1 Architecture Freeze. No change to FIFO, ticket chronology, Q1, causal floor, primary/reference ontology, source/successor atomicity or sole execution owner. +- `Semantic oracle`: A0 SHA-256 `8746e423168309975e509be91e4eb06fdf0eb0b43369d75f1ee62202a4341678` remains frozen; use only for relevant semantic equivalence, never as a Weight oracle. + +## Context and Hypothesis + +This Leaf owns public close cleanup ownership. Its boundary is the single claim above. Evidence was extracted from the explicitly marked pre-fission parent subsections at Git baseline `a60de7748132211d9e58cecd5be82b41f76ce230`; this migration changes navigation and evidence ownership only. Source, Wasm, commands, rejection outcomes and evidence limitations retain their original meaning. + +Extraction provenance at the recorded Git baseline: EXP-0028 / “Automatic Close and Owner Ledger Coverage”, “Public Close With a Live Crossing Cursor”. Original directional prose (above/below) belongs to these pre-fission subsections; current cross-owner navigation follows the parent proof map. + +## Baseline and Candidates + +Retained measured implementations and rejected candidates are identified in the extracted evidence. The deciding reference binding selects the attained joint retry/Crossing public-close point at admitted `(O=4, P=12)`, which contains the declared Contract/Run, live-cursor and owner-ledger owners and dominates the retained zero-Step and admitted Manual close profiles component-wise. + +## Controls + +Only the declared witness input varies within each source-bound comparison. Preserve the exact host, toolchain, database, Wasm and sampling controls printed with each evidence item; do not compare unlike artifacts as a controlled performance result. + +## Benchmark Class, Workloads, and Criteria + +Acceptance requires the single claim to hold over its declared admitted domain, with each physical operation charged once and every retained limitation resolved before broader production use. One legal omitted-owner, domain or containment counterexample falsifies that claim. Existing workloads below are bounded witnesses, not permission to add independent questions. + +## Environment and Artifact Identity + +Pre-fission baseline `a60de7748132211d9e58cecd5be82b41f76ce230`. Each extracted block retains its own exact source/Wasm/output identity. Shared pinned hardware/toolchain/cache controls follow [EXP-0030](./EXP-0030.md#environment-and-artifact-identity) only where the original comparison explicitly reuses them. A shared environment never substitutes for the block’s own source or Wasm identity. No runtime artifact was rebuilt during migration. + +## Proof Obligations + +- `Freeze`: Frozen by explicit migration reconstruction from the pre-fission decision and retained witnesses at a60de7748132211d9e58cecd5be82b41f76ce230. No new measurement or historical decision is implied. +- `Review triggers`: None. +- `Decomposition review`: Boundary reviewed during provenance-preserving fission. Workloads within this Leaf share the declared claim, physical owner, changed variable, evidence domain and acceptance rule. Independently decidable owners were transferred; a new owner or selector requires another node. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | Public close cleanup has one sound bound covering admitted Contract/Run, live detector cursor and owner-ledger release. | One public owner close after real Temporary suspension with retained Run captures and owner hold | Reference-Wasm FRAME evidence in the declared owner/domain; native checks are construction controls | Self | Required | Named parent decision and declared downstream consumers | Satisfied for the admitted reference domain by the joint maximum-Step retry/Crossing witness at `(O=4, P=12)` | + +## Measurements + + +### Automatic Close and Owner Ledger Coverage + +Prepared question: qualify the owner ledger inside the common automatic cleanup bound. `close_cleanup_weight_upper` returns generated `close_actor`, and User Pipeline pricing also includes that owner. `reconcile_actor_state_hold_with_authority` releases the Actor's exact held amount from its owner after process removal. A scheduler close has no same-owner signed transaction establishing prior Account access. The previous `close_actor` and five User close diagnostics used FRAME's `whitelisted_caller`, suppressing that owner's `System::Account` DB accounting; the retained generated close trace includes `Balances::Holds` but no `System::Account`. + +All six User close fixtures now use one ordinary deterministic owner while retaining their authored Contract, real retry, owner-slot guards and detector branches. System-only close remains a separate hold-exempt domain. This corrects the fixture's accounting boundary without changing production execution or manually repairing coefficients. Native and reference-Wasm qualification must still distinguish owner ledger coverage from the maximum retry Contract's admission and detector-state reachability; the corrected owner alone does not certify a complete close envelope. + +Native close-family tests pass, but the fresh reference `close_actor` probe rejects creation with `ContractStepsExceedOnIdleBudget`, before retry or close measurement. Source content/modes `c554d5dcb858deb7776f79c2b69a0b0eefee8aa7b8cc59c19c0ffd92b6bdd838`, benchmark source SHA-256 `9038b724077410f7c453c961b7374a5fb70037d4b0bb0ce2aea7c9cd78fbc03c`, benchmark Wasm SHA-256 `bbf05d8035f080f49b96f7fc8271239e80f4847e1b2e552bc6dc5dedb3406e83`. Reproduce with `scripts/benchmarks.sh --extrinsic close_actor --output /tmp/deos-exp28-close-owner.rs pallet_deos_actors`; fresh build took 113 seconds, failed setup 3 seconds, with the same pinned 50×20 reference controls. No generated fragment or numeric close result exists. + +`ensure_contract_steps_fits_idle_budget` depends on Actor type and Steps, independently of Trigger/owner/guard topology. The other five User close fixtures share this same maximal retry Step body, so repeating each at the identical admission failure would not qualify later cleanup. Their reference domain remains unresolved. Existing injected range cursors and producer transition contexts also remain separate reachability obligations; native consistency is not proof of an Oracle-produced reference history. EXP-0029 now probes coupled Opening density while preserving the full Step count rather than reducing semantic limits or changing these failed fixtures silently. + +EXP-0029's [Opening partition](./EXP-0052.md#derived-metrics) now supplies an admitted Manual User close domain with all twelve Steps, actual Suspended Run state, ordinary owner holds and up to three coupled Opening Steps. Its fresh Wasm trace includes the previously excluded owner's `System::Account` and `Balances::Holds`, each at one read/write. That establishes the missing physical owner in a real close, without promoting this smaller capture domain to the general cleanup maximum. Original close fixtures remain numerically unqualified until reference admission and their detector histories are reconciled. + + + +### Public Close With a Live Crossing Cursor + +The existing `close_actor_crossing_cursor_repair` diagnostic injects a range cursor after an ordinarily constructed retry, and its all-Opening Contract cannot pass V10 reference admission. Its claimed cursor history is therefore not qualified. Correct this diagnostic by reusing the already admitted zero-Step User/full-page constructor and actual published-transition worker from the live-cursor profiles. The measured operation is the public owner close while readiness remains latched, before Opening rearm. This differs from the existing authored close, which first executes Opening and rearm. + +The smallest falsifier is public close at that exact live cursor: delete the latched User and its owner hold, preserve sovereign custody and the collected Trigger fee, charge no Pipeline or Action fee, rewind the cursor after dense-tail movement, and prove the next worker visits the moved guard. Extract only the shared survivor/cursor assertions, preserving the earlier inner profiles' setup and measured bodies. This corrects an existing branch diagnostic rather than introducing a new general close bound. Nonzero Run/capture, other primary placements and full cleanup composition remain separate obligations. + +Measure the corrected profile with `scripts/benchmarks.sh --extrinsic close_actor_crossing_cursor_repair --output /tmp/deos-exp28-public-cursor-close.rs --json-file /tmp/deos-exp28-public-cursor-close.json pallet_deos_actors`, using measured proof and the ordinary 50 × 20 controls. Preserve the original full-Contract setup's rejection as a domain limitation; do not install this zero-Step result as a replacement for `close_actor` or claim it bounds the earlier authored-close path. + +Native `crossing_cursor` tests pass with all features (21 seconds), covering the corrected public close and both earlier inner profiles. Preflight compilation passes (54 seconds), followed by the existing G5 stale-storage-name rejection. The fresh Wasm builds in 101 seconds and completes measurement in 15 seconds. Setup uses the existing reference P=128 page constructor with 384 ordinary Actors and the real bounded worker; no cursor or membership is injected. The shared assertion extraction preserves the earlier setup, measured operation and postconditions. + +| Artifact | SHA-256 | +| --- | --- | +| Prepared source content/modes | `c7eed8ee0b49086cc5b11284cd62476fb91cca33c745b7ea74f8e32b76070d0d` | +| Benchmark source | `3f6d913fddc9e939ca08dc18b42c6cd983a0f14d8913c5784cb7257321c55700` | +| Benchmark Wasm | `cbc2ca251957ef4c71b988f8f39c80488a69b7e938eae0f26fbdaeddd11becbf` | +| Generated fragment | `e62ede9ea3e3bac10708a08f725569462089d8f395e932553a9618ceeb083050` | +| Raw JSON | `a2abdfa90432c4a02274f46f56d609d36d43b4df3d73ab84341d48176e31fa8e` | + +| Timing / DB samples | Min / upper median / p95 / max ns | Unique / repeated reads | Writes / repeated writes | Recorded / estimated proof B | +| --- | --- | --- | --- | --- | +| 100 / 5 | 413,118 / 429,391 / 452,230 / 530,802 | 26 / 37 | 24 / 0 | 25,601 / 31,541 | + +The fixed generated model is `429,391,000 ps + 26 reads + 24 writes`, estimated proof 31,541 bytes; reference DB-inclusive RefTime is 3,479,391,000 ps. All samples are retained with nearest-rank p95. The trace includes ordinary owner `System::Account` and `Balances::Holds`, each one read/write, two Crossing member pages, the target/moved membership locators and one surviving leaf/range cursor. Run-head/payload absence is read and deletion tracked even for this zero-Step Actor; absence does not imply no storage work. No host Oracle observation is read by the measured public close. + +The result verifies public-close cleanup of paid pending readiness, owner hold release, unchanged sovereign/sink balances, `OwnerInitiated` closure and real cursor rewind/resumption. It does not include Opening rearm, Pipeline collection, nonzero Contract/Run decoding, a full owner-slot bitmap, Waiting-primary removal or a complete heap. Preserve the existing inclusive authored-close evidence separately; do not subtract the two aggregates or claim a same-state performance improvement. The corrected existing diagnostic remains excluded from default production generation and its fragment stays uninstalled. + +EXP-0029's [joint retry/Crossing cleanup qualification](./EXP-0052.md#joint-retry-and-crossing-cleanup-qualification) now supplies a separate public-close phase point with maximum Step count, present Run/captures, future Waiting authority, full User owner slots and real deferred-latch/cursor history. It records the emitted model and residuals over sampled axis faces, plus adjacent admission refusal; the model remains uninstalled. This closes the missing coexistence witness for that state family, not full cleanup coverage. Complete Waiting/heap, funding and alternative Contract/predicate families, source-consumed apoptosis and runtime reservation/actual dominance remain separate inputs; no aggregate subtraction or overlapping addition is justified. + +Final workspace/all-target/all-feature Clippy passes (17 seconds). Runtime behavior, host helpers and public Weight interfaces are unchanged; prior runtime and embedding evidence remains applicable. The retained constructor and measurement keep their prepared source identity; full production artifacts remain G5 work. + + +### Deciding Public-Close Owner + +The retained reference-Wasm evidence partitions the zero-Step live-cursor diagnostic, admitted Manual retry/capture profiles and the joint retry/Crossing profile. The joint profile directly attains O1's smallest falsifier: ordinary admission and a real failed liquidity attempt produce a maximum-Step Suspended User Run; host publications create the deferred Crossing latch and live cursor; public owner close then removes the Contract, three cold tails, Run head/payload, one future Waiting frame, Crossing pages/locators and the ordinary owner's Account/Hold state while preserving custody and resuming the moved guard. + +At admitted `(O=4, P=12)`, its generated vector is `742,998,816` pre-database RefTime, `38,096` estimated ProofSize, 34 reads and 32 writes. Under the pinned reference RocksDB charge, the complete RefTime is `4,792,998,816`. This vector dominates the retained zero-Step cursor owner (`429,391,000 / 31,541`, 26 reads / 24 writes) and admitted Manual capture profiles component-wise, so no subtraction or synthetic coordinate envelope is required. + +This accepts one inclusive public-close atom over the declared admitted reference state family. Contract/Run deletion, singleton Waiting removal, Crossing cursor repair, lifecycle cleanup and applicable owner-hold release occur once. Observation terminal cleanup remains the mutually exclusive EXP-0040 branch. EXP-0063 owns populated Waiting pages and competing heap-repair alternatives and must reconcile their already-inclusive close traces without charging Contract/Run/lifecycle work twice. EXP-0065 owns exact generation; the diagnostic fragment remains uninstalled. + +## Derived Metrics + +The selected RocksDB total is `742,998,816 + 34 × 25,000,000 + 32 × 100,000,000 = 4,792,998,816` RefTime. Its `38,096`-byte estimated proof exceeds the zero-Step live-cursor point by 6,555 bytes, while its eight additional reads and eight additional writes cover the retained Run/cold-tail/Waiting and full-owner state of this admitted joint witness. These are aggregate containment facts, not isolated marginal coefficients. + +## Statistical / Variance Notes + +Retain each item’s original sampling and uncertainty. Singleton construction probes establish only their admitted state/access witness, never statistical timing dominance or a production bound. + +## Result + +The attained joint retry/Crossing phase point satisfies the declared public-close falsifier and contains every retained non-heap public-close profile component-wise. Its postconditions establish complete admitted Contract/Run/detector/owner-ledger removal, custody neutrality and live-cursor resumption. + +## Interpretation + +For the admitted reference domain, public close is one inclusive terminal atom rather than independently additive Contract, Run, detector, Waiting, lifecycle and owner-hold fragments. Populated Waiting/heap alternatives remain a transferred composition question, not a reason to keep this owner open. + +## Decision + +Accepted for the admitted reference public-close domain. Select `742,998,816` pre-database RefTime, `38,096` estimated ProofSize, 34 reads and 32 writes (`4,792,998,816` RefTime under the pinned RocksDB charge) for the joint maximum-Step Suspended Run/Crossing owner at `(O=4, P=12)`. Maximum and actual use the same inclusive public-close atom. EXP-0065 owns exact regeneration; no diagnostic fragment, subtraction or manual coefficient repair is accepted. + +## Rejected Alternatives and Tradeoffs + +Reject the stale generated `close_actor` owner with omitted ordinary-owner Account access; the zero-Step live-cursor diagnostic as a complete bound; admitted Manual retry/capture profiles without joint detector state; the inadmissible all-Opening maximum; adding separate Contract/Run/Crossing/Waiting/hold cleanup to this inclusive atom; subtracting aggregates to fabricate inner coefficients; treating this singleton Waiting frame as a populated-heap envelope; and manually patching production Weight before EXP-0065. + +## Validity + +- `Establishes`: One attained public-close owner over the admitted reference maximum-Step Suspended Run, bounded Opening captures at `(O=4, P=12)`, real deferred Crossing latch/live cursor, one future Waiting frame, full User owner slots and ordinary owner-ledger release. +- `Does not establish`: Populated Waiting-page/heap alternatives, every rejected capture allocation, Observation terminal cleanup, exact final production Weight identity or integrated throughput. +- `Known limitations`: The selected model remains diagnostic and uninstalled; EXP-0063/EXP-0064 must preserve branch exclusivity and overlap attribution before EXP-0065 regeneration. +- `Invalidation triggers`: A changed public-close body, Contract/Run/detector/hold cleanup, admitted state domain, source/Weight/Wasm identity or selector rule requires scoped requalification. +- `Residual qualitative insight`: A joint attained close state can settle the generic public terminal owner while independently larger Waiting/heap alternatives remain separately composed. + +## Follow-Up / Next Gradient + +Stop measuring this satisfied public-close owner. EXP-0063 owns populated Waiting/heap alternatives, EXP-0064 composes admission and EXP-0065 regenerates exact artifacts. Reopen only for the declared falsifier or a changed relevant path, domain, selector or identity. + +## Architecture Impact + +The reference public-close input now requires one inclusive owner covering admitted maximum-Step Contract/Run deletion, live Crossing cursor repair, singleton future-Waiting removal and ordinary User hold release without duplicate charges. No runtime, semantic or storage change is introduced; C1 and A0 remain frozen, and complete production regeneration remains G5 work. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: [EXP-0028](./EXP-0028.md) O13 +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: [EXP-0025](./EXP-0025.md) +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: [EXP-0052](./EXP-0052.md), [EXP-0063](./EXP-0063.md) +- `Transfers question to`: None; independent findings require an explicit new transfer. +- `Produces input for`: [EXP-0028](./EXP-0028.md) +- `Reopen trigger`: A falsifier of this declared owner/domain or exact artifact identity; cost alone does not reopen frozen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0043.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0043.md new file mode 100644 index 00000000..ccf31477 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0043.md @@ -0,0 +1,144 @@ +# EXP-0043 — Window Expiry Close Ownership + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | [EXP-0028](./EXP-0028.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-06 | +| Affected domain | Window expiry source consumption and custody-neutral close have one complete terminal Control owner. | +| Physical mechanism | Window Expiry Close Ownership | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0025](./EXP-0025.md) — Accepted, physical architecture only | +| Successor experiment | [EXP-0028](./EXP-0028.md) | + + +## Decision Question + +Window expiry source consumption and custody-neutral close have one complete terminal Control owner. + +## Governing Specification + +- `Owners`: `template/pallets/actors/docs/specification.en.md` (sole ownership, lifecycle/Trigger/Pipeline/Action semantics, placement and scheduler); `docs/actors-resource-policy.specification.en.md` (component-wise admission/actual settlement, equal-thirds block policy); `docs/actors-performance-assurance.specification.en.md` (evidence and target). +- `Frozen physical input`: [EXP-0025](./EXP-0025.md) C1 Architecture Freeze. No change to FIFO, ticket chronology, Q1, causal floor, primary/reference ontology, source/successor atomicity or sole execution owner. +- `Semantic oracle`: A0 SHA-256 `8746e423168309975e509be91e4eb06fdf0eb0b43369d75f1ee62202a4341678` remains frozen; use only for relevant semantic equivalence, never as a Weight oracle. + +## Context and Hypothesis + +This Leaf owns window expiry close ownership. Its boundary is the single claim above. Evidence was extracted from the explicitly marked pre-fission parent subsections at Git baseline `a60de7748132211d9e58cecd5be82b41f76ce230`; this migration changes navigation and evidence ownership only. Source, Wasm, commands, rejection outcomes and evidence limitations retain their original meaning. + +Extraction provenance at the recorded Git baseline: EXP-0028 / “Window Expiry Source and Custody-Neutral Close”. Original directional prose (above/below) belongs to these pre-fission subsections; current cross-owner navigation follows the parent proof map. + +## Baseline and Candidates + +The retained reference-Wasm `scheduler_wakeup_zero_step_user_expiry` profile directly attains the declared unsignaled Immutable zero-Step User expiry through its singleton Block primary. It is the candidate complete source-consumption and custody-neutral terminal owner for this admitted domain; outer clock selection and competing populated Waiting/heap envelopes remain independently owned. + +## Controls + +Only the declared witness input varies within each source-bound comparison. Preserve the exact host, toolchain, database, Wasm and sampling controls printed with each evidence item; do not compare unlike artifacts as a controlled performance result. + +## Benchmark Class, Workloads, and Criteria + +Acceptance requires the single claim to hold over its declared admitted domain, with each physical operation charged once and every retained limitation resolved before broader production use. One legal omitted-owner, domain or containment counterexample falsifies that claim. Existing workloads below are bounded witnesses, not permission to add independent questions. + +## Environment and Artifact Identity + +Pre-fission baseline `a60de7748132211d9e58cecd5be82b41f76ce230`. Each extracted block retains its own exact source/Wasm/output identity. Shared pinned hardware/toolchain/cache controls follow [EXP-0030](./EXP-0030.md#environment-and-artifact-identity) only where the original comparison explicitly reuses them. A shared environment never substitutes for the block’s own source or Wasm identity. No runtime artifact was rebuilt during migration. + +## Proof Obligations + +- `Freeze`: Frozen by explicit migration reconstruction from the pre-fission decision and retained witnesses at a60de7748132211d9e58cecd5be82b41f76ce230. No new measurement or historical decision is implied. +- `Review triggers`: None. +- `Decomposition review`: Boundary reviewed during provenance-preserving fission. Workloads within this Leaf share the declared claim, physical owner, changed variable, evidence domain and acceptance rule. Independently decidable owners were transferred; a new owner or selector requires another node. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | Window expiry source consumption and custody-neutral close have one complete terminal Control owner. | One unsignaled Immutable zero-Step User expiring through its Block primary before Pipeline admission | Reference-Wasm FRAME evidence in the declared owner/domain; native checks are construction controls | Self | Required | Named parent decision and declared downstream consumers | Satisfied for the admitted reference domain by the attained singleton-Block expiry witness | + +## Measurements + + +### Window Expiry Source and Custody-Neutral Close + +The profile qualifies the complete single-key Block source-consumption/WindowExpired atom used by the due worker, excluding its outer clock selection and Weight meter. It ordinarily creates an unsignaled Immutable zero-Step Manual User with a finite window, advances the real host clock to expiry, preserves other host deadlines, and transacts canonical key drain, loaded expiry classification and consumed-state close. Postconditions require released process/hold/deadline state, no Run or Pipeline receipt and unchanged sovereign/Fee Sink balances. The singleton Block key is a declared geometry, not a maximum heap/Waiting claim; EXP-0063 retains selection and competing physical envelopes. + +The first Wasm setup failed before measurement: advancing Timestamp across Aura slots without a matching header left CurrentSlot at zero while the timestamp required slot eight. Failed source `ed755398490b4dabe13c4dc01f8791b590f4c42aeaa6db2ef3e96849f68b103e`, Wasm `49e888bc4d779f9191f3f1c7eaffebef1fd7dd90153fbddf480dfb9b9f6b4b25`, log SHA-256 `ffc90b7c6031a640b7320173b0d870bafe2290889963878e89eceb19bbe4be87`. No numeric result from that run is accepted. The reference benchmark clock helper now begins the next fixture block through public System initialization with an Aura pre-runtime digest, then invokes Aura's ordinary initialization hook before Timestamp; it never writes CurrentSlot directly. Callers leave block advancement to that helper, and the mock requires strictly increasing clock/block time. + +| User expiry profile | RefTime before DB additions / minimum (ps) | Estimated / recorded proof (bytes) | Reads / writes | Generated fragment SHA-256 | +| --- | --- | --- | --- | --- | +| `scheduler_wakeup_zero_step_user_expiry` | 261,001,000 / 239,140,000 | 6,367 / 2,902 | 24 / 21 | `f04d2a4a1b4b84e5062ce5579e9fe9e491438b214bf22e7ad1f9564b138460d9` | + +Fresh reference-Wasm admission, measurement and postconditions pass at 50 steps × 20 repeats. Source content/modes `1ee040699041b0cb305f7892359de7ec0ebf48a29312681b3eb310f6e7899765`; benchmark source SHA-256 `4bf787dcca8a8ac8bf7e17e3a2e88eb379de68bda991b8ab23e18ae922019fca`; reference helper source SHA-256 `c75d1ac5e2ae0a842518d2b5d076d9749d9e1cb33e1e927b46ab98d07bcc7af9`; benchmark Wasm SHA-256 `bff209117e546d373142e7a86204903a8bcf58d482f82125bea416c76e4e1847`. Same pinned toolchain/hardware/cache/heap/map controls as above and no concurrent agent build/test/benchmark job during measurement. Reproduce with `scripts/benchmarks.sh --extrinsic scheduler_wakeup_zero_step_user_expiry --output /tmp/deos-exp28-zero-step-window-expiry.rs pallet_deos_actors`; build took 107 seconds and measurement 14 seconds. The fragment is uninstalled and excluded from default production generation. + +The trace includes Waiting chunk/head/tail/occupancy and cursor removal, full lifecycle/index cleanup and User hold release. Its sole System account read/write belongs to hold release, with no sovereign-to-Fee-Sink collection. Source removal and cleanup are already inside this inclusive atom and must not be added again. An unsignaled Idle expiry does not establish Ready/Running/Suspended expiry bounds, maximum header or detector cleanup, nor competing populated-heap branches; complete envelopes remain open. + +Shared clock validation on this same fresh Wasm separately passes `scheduler_inner_zero_step_user_at_time` and `scheduler_inner_zero_step_user_cadenced` with `--skip-build --steps 2 --repeat 1`, using `/tmp/deos-exp28-clock-at-time-check.rs` and `/tmp/deos-exp28-clock-cadenced-check.rs`. Each took 14 seconds. These are verification-only runs for repeated-clock/no-recurrence and same-slot/rearm behavior; no new performance coefficients are inferred, and earlier measurements retain their original source identity. Clock setup stays outside all measured atoms, and none of these fixtures claims full Executive execution. + +Retained-source validation: native all-features `zero_step_user_` tests pass (32 seconds), workspace all-features Clippy passes (25 seconds), and canonical formatting, workspace check, changed-scope completion (47 seconds), record normalization/dependency graph and diff checks pass. Completion skips runtime tests for the benchmark-only runtime diff, reusing unchanged production behavior. Preflight compilation passes (61 seconds), retaining the known stale generated-storage rejection. Source content/modes remain identical after context synchronization; G5 and complete numerical acceptance remain open. + + +### Deciding Window-Expiry Owner + +The attained reference-Wasm profile directly exercises O1's smallest falsifier. An ordinarily created unsignaled Immutable zero-Step Manual User reaches expiry through its singleton Block primary before Pipeline admission. The measured transaction consumes the Block source and its Waiting chunk/head/tail/occupancy/cursor authority, classifies the loaded state as expired, removes lifecycle/process/deadline state and releases the applicable User hold. It produces no Run or Pipeline receipt, preserves sovereign and Fee Sink balances and performs no custody movement. + +The generated fixed vector is `261,001,000` pre-database RefTime, `6,367` estimated ProofSize, 24 reads and 21 writes. Under the pinned reference RocksDB charge, complete RefTime is `2,961,001,000`. Source consumption and close cleanup are already inside this inclusive atom and must not be added again. The clock helper, outer due-selection work and competing populated Waiting/heap branches are excluded; EXP-0063 owns those envelopes and their overlap attribution. Ready/Running/Suspended lifecycle branches remain owned by their respective phase experiments rather than broadening this expiry owner. + +## Derived Metrics + +The selected RocksDB total is `261,001,000 + 24 × 25,000,000 + 21 × 100,000,000 = 2,961,001,000` RefTime. This is an inclusive owner, not a source-removal or hold-release component inferred by subtraction. + +## Statistical / Variance Notes + +The fixed model comes from the retained fresh reference-Wasm run at 50 steps × 20 repeats. Its `239,140,000` minimum and `2,902` recorded proof remain diagnostic observations; the generated upper model and estimated proof define the selected reference owner. No samples are removed. + +## Result + +The attained singleton-Block witness satisfies the declared window-expiry falsifier and establishes custody-neutral source consumption plus terminal cleanup over the admitted unsignaled Immutable zero-Step User domain. + +## Interpretation + +Window expiry in this domain is one inclusive terminal Control atom. Outer clock selection and populated Waiting/heap alternatives are separate composition inputs, not missing parts of this inner owner and not permission to duplicate its source or cleanup work. + +## Decision + +Accepted for the admitted reference window-expiry domain. Select `261,001,000` pre-database RefTime, `6,367` estimated ProofSize, 24 reads and 21 writes (`2,961,001,000` RefTime under the pinned RocksDB charge) for the unsignaled Immutable zero-Step User singleton-Block expiry owner. Maximum and actual use the same inclusive atom. EXP-0065 owns exact regeneration; the diagnostic fragment remains uninstalled. + +## Rejected Alternatives and Tradeoffs + +Reject the failed Aura-clock setup and its absent numeric result; treating the singleton source as a maximum populated-heap claim; broadening this owner to Ready/Running/Suspended expiry; adding separate generic source removal, lifecycle cleanup or hold release to the inclusive atom; inferring components by aggregate subtraction; and manually patching production Weight before EXP-0065. + +## Validity + +- `Establishes`: One attained inclusive expiry owner over the admitted unsignaled Immutable zero-Step Manual User, singleton Block primary, canonical source removal, lifecycle/process/deadline cleanup and applicable User hold release. +- `Does not establish`: Outer clock selection, populated Waiting-page/heap alternatives, Ready/Running/Suspended expiry, maximum headers or detector cleanup, exact final production Weight identity or integrated throughput. +- `Known limitations`: The selected model remains diagnostic and uninstalled; EXP-0063/EXP-0064 must preserve outer-envelope attribution before EXP-0065 regeneration. +- `Invalidation triggers`: A changed expiry body, source/cleanup/hold path, admitted state domain, source/Weight/Wasm identity or selector rule requires scoped requalification. +- `Residual qualitative insight`: A narrow attained terminal atom can settle its owner while independently larger outer scheduler envelopes remain separate. + +## Follow-Up / Next Gradient + +Stop measuring this satisfied window-expiry owner. EXP-0063 owns outer clock and populated Waiting/heap alternatives, EXP-0064 composes admission and EXP-0065 regenerates exact artifacts. Reopen only for the declared falsifier or a changed relevant path, domain, selector or identity. + +## Architecture Impact + +The reference window-expiry input now requires one inclusive owner covering singleton Block-source consumption, expiry classification, terminal cleanup and applicable User hold release without duplicate charges. No runtime, semantic or storage change is introduced; C1 and A0 remain frozen, and complete production regeneration remains G5 work. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: [EXP-0028](./EXP-0028.md) O14 +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: [EXP-0025](./EXP-0025.md) +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: None. +- `Transfers question to`: None; independent findings require an explicit new transfer. +- `Produces input for`: [EXP-0028](./EXP-0028.md) +- `Reopen trigger`: A falsifier of this declared owner/domain or exact artifact identity; cost alone does not reopen frozen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0044.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0044.md new file mode 100644 index 00000000..9ba8dafa --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0044.md @@ -0,0 +1,142 @@ +# EXP-0044 — User State-Hold Reconciliation Bound + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | [EXP-0028](./EXP-0028.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-06 | +| Affected domain | User state-hold reconciliation is included exactly once and bounded over admitted live geometry and the host hold ledger. | +| Physical mechanism | User State-Hold Reconciliation Bound | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0025](./EXP-0025.md) — Accepted, physical architecture only | +| Successor experiment | [EXP-0028](./EXP-0028.md) | + + +## Decision Question + +User state-hold reconciliation is included exactly once and bounded over admitted live geometry and the host hold ledger. + +## Governing Specification + +- `Owners`: `template/pallets/actors/docs/specification.en.md` (sole ownership, lifecycle/Trigger/Pipeline/Action semantics, placement and scheduler); `docs/actors-resource-policy.specification.en.md` (component-wise admission/actual settlement, equal-thirds block policy); `docs/actors-performance-assurance.specification.en.md` (evidence and target). +- `Frozen physical input`: [EXP-0025](./EXP-0025.md) C1 Architecture Freeze. No change to FIFO, ticket chronology, Q1, causal floor, primary/reference ontology, source/successor atomicity or sole execution owner. +- `Semantic oracle`: A0 SHA-256 `8746e423168309975e509be91e4eb06fdf0eb0b43369d75f1ee62202a4341678` remains frozen; use only for relevant semantic equivalence, never as a Weight oracle. + +## Context and Hypothesis + +This Leaf owns user state-hold reconciliation bound. Its boundary is the single claim above. Evidence was extracted from the explicitly marked pre-fission parent subsections at Git baseline `a60de7748132211d9e58cecd5be82b41f76ce230`; this migration changes navigation and evidence ownership only. Source, Wasm, commands, rejection outcomes and evidence limitations retain their original meaning. + +Extraction provenance at the recorded Git baseline: EXP-0028 / “Remaining User Opening Owner”. Original directional prose (above/below) belongs to these pre-fission subsections; current cross-owner navigation follows the parent proof map. + +## Baseline and Candidates + +Retained measured implementations and rejected candidates are identified in the extracted evidence. The deciding candidate reuses the attained complete `scheduler_paged_execute_opening_max` User profile: bind hold work only through the inclusive post-publication Opening owner rather than fabricating a disjoint coefficient from aggregate subtraction. + +## Controls + +Only the declared witness input varies within each source-bound comparison. Preserve the exact host, toolchain, database, Wasm and sampling controls printed with each evidence item; do not compare unlike artifacts as a controlled performance result. + +## Benchmark Class, Workloads, and Criteria + +Acceptance requires the single claim to hold over its declared admitted domain, with each physical operation charged once and every retained limitation resolved before broader production use. One legal omitted-owner, domain or containment counterexample falsifies that claim. Existing workloads below are bounded witnesses, not permission to add independent questions. + +## Environment and Artifact Identity + +Pre-fission baseline `a60de7748132211d9e58cecd5be82b41f76ce230`. Each extracted block retains its own exact source/Wasm/output identity. Shared pinned hardware/toolchain/cache controls follow [EXP-0030](./EXP-0030.md#environment-and-artifact-identity) only where the original comparison explicitly reuses them. A shared environment never substitutes for the block’s own source or Wasm identity. + +The deciding evidence reuses [EXP-0031](./EXP-0031.md)'s exact current reference-Wasm profile at code baseline `e92ab80` plus benchmark-only postcondition strengthening: `benchmarking.rs` SHA-256 `56178d1e89c600bed20dc737e9fe96376929c9d41c00cd0cd9586dea66d8bdba`, unchanged scheduler SHA-256 `bdedd82571a229f3609194547c6256d8ca454055640e407c2c9af03d2245fbd1`, benchmark script SHA-256 `303d94b87dd04514118764b96c8c064da758337cc044cd57ba59098cdf292632` and benchmark Wasm SHA-256 `cb901d049e328d2d1074fabf3f3583e6800526e7ba9b4188f8a258ee04e0a1a6`. + +## Proof Obligations + +- `Freeze`: Frozen by explicit migration reconstruction from the pre-fission decision and retained witnesses at a60de7748132211d9e58cecd5be82b41f76ce230. No new measurement or historical decision is implied. +- `Review triggers`: EXP-0068 reuses the scoped hold-boundary evidence alongside EXP-0028; no new measurement is added here. +- `Decomposition review`: Boundary reviewed during provenance-preserving fission. Workloads within this Leaf share the declared claim, physical owner, changed variable, evidence domain and acceptance rule. Independently decidable owners were transferred; a new owner or selector requires another node. EXP-0068 owns completion-inner measurement separately from this attained full User progress owner. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | User state-hold reconciliation is included exactly once and bounded over admitted live geometry and the host hold ledger. | One admitted User Run transition changing live geometry and requiring host hold reconciliation after publication | Reference-Wasm FRAME evidence in the declared owner/domain; native checks are construction controls | Self | Required | Named parent decision and declared downstream consumers | Satisfied for the admitted reference Opening domain by the attained maximum-Step User Run publication and funding-hold decrease | + +## Measurements + + +### Remaining User Opening Owner + +The large-head comparison uses an ordinary admitted one-Step User Contract with full AddressEvent source/asset whitelists, SignedAllowlist funding, optional window/terminal nonce, full SplitTransfer legs and the legal four-predicate DNF spread across four clauses. Reference observation identifiers use the widest encoded variant; Current predicates need no published observation for admission or this collection-only operation. Readiness comes from certified signed ingress, with no Contract/head injection. Assertions qualify Trigger, funding, feed, Predicate and Task widths against host type maxima. This is a large-head diagnostic, not an accepted universal maximum: structural DNF capacity is sixteen but admission permits four, and compact resource Weight fields retain admission-derived widths. A byte-size witness alone cannot prove decode-branch CPU dominance. + +Constructive native qualification passes `ci-local.sh --only tests --package pallet-deos-actors --all-features --test-filter pipeline_large_head_opening_collection`. Admission requires distinct clauses, SCALE-byte ordering of Trigger whitelists, at least two RetryLater attempts and an auto-close nonce within the host horizon. The retained fixture uses four distinct scalar thresholds, canonical filters and the host horizon without changing field widths. It verifies stored predicate count, Task width, certified readiness, TryRuntime consistency, exact collection/receipt and unchanged cycle nonce. Benchmark preflight compiles but still rejects pre-existing stale generated storage names; this remains the G5 gate, not a new profile failure. + + +### Deciding Current Reference-Wasm Witness + +The attained `scheduler_paged_execute_opening_max` profile is an ordinarily admitted 12-Step User Opening. It consumes the real Ready source, collects the Pipeline fee, publishes a Running successor and then executes `reconcile_actor_state_hold_with_authority` in the same complete owner. Exact postconditions preserve the hold owner plus identity, Contract-head, Contract-body, detector and active-lifetime maximum Run components, while requiring the funding component to decrease after accumulated ingress moves into the Run snapshot. The profile therefore exercises a real variable live-geometry delta and the owner-funded host hold ledger after publication rather than merely constructing a large encoded head. + +| Profile | RefTime before DB additions / minimum (ps) | Estimated / recorded proof (bytes) | Reads / writes | Generated fragment SHA-256 | Raw JSON SHA-256 | +| --- | ---: | ---: | ---: | --- | --- | +| `scheduler_paged_execute_opening_max` | 536,459,000 / 479,678,000 | 14,158 / 5,743 | 24 / 13 | `2d824d36d9f1e9821c71a0df88cd02a3e868ea491a413bc0b689cbec022c9f12` | `21f268723a55ca53211ed0a2987b28609b91c14831595b6b0ebda87e23a98ad4` | + +The generated storage trace includes one read/write each for `ActorStateHolds` and the host `Balances::Holds` ledger. It also includes the three maximum reference Contract-tail reads and every optional detector-key probe performed by generic reconciliation. Under the pinned RocksDB charge of 25,000,000 RefTime/read and 100,000,000 RefTime/write, the complete inclusive owner totals `2,436,459,000` RefTime. Those aggregate terms cannot be subtracted to invent a standalone hold coefficient: source consumption, Pipeline collection, first-Step evaluation, Running publication and hold reconciliation overlap in one attained atom. + +Reproduce with `scripts/benchmarks.sh --extrinsic scheduler_paged_execute_opening_max --output /tmp/deos-exp31-user-paged-opening-complete.rs --json-file /tmp/deos-exp31-user-paged-opening-complete.json pallet_deos_actors`. The run used compiled Wasm, 4,096 heap pages, 1,024 MiB DB cache, map bound 1,000,000 and 50 steps × 20 repeats on the pinned AMD Ryzen 7 4800H reference host, with no concurrent heavy job. The artifact and raw JSON remain verified at the hashes above. Native benchmark construction, fresh reference-Wasm execution, Actors/runtime Clippy, workspace checks, canonical formatting, graph normalization and changed-scope completion passed in the source cohort recorded by EXP-0031. + +The reference binding therefore selects exactly one post-publication hold reconciliation inside each applicable complete User transition owner. For this maximum-Step Opening branch, the attained inclusive value above is the bound; no additional hold allowance is added. Zero-Step, temporal, Crossing and terminal complete owners retain their separately accepted inclusive hold behavior. Arbitrary host implementations, populated outer queue/heap composition and exact generated production identity remain outside this decision and belong to EXP-0063 through EXP-0065. + +## Derived Metrics + +Derived values are retained beside the evidence that defines their source and units. No subtraction of overlapping aggregate owners or cross-artifact timing comparison is introduced. + +## Statistical / Variance Notes + +Retain each item’s original sampling and uncertainty. Singleton construction probes establish only their admitted state/access witness, never statistical timing dominance or a production bound. + +## Result + +The current reference Wasm attains an admitted maximum-Step User Opening whose real Running publication changes funding geometry and reconciles the owner-funded hold afterward. The complete owner is `536,459,000 / 14,158` before database additions with 24 reads and 13 writes; it contains the hold record and host ledger read/write exactly once. + +## Interpretation + +State-hold work is a post-publication part of the complete User transition, not an additive component inferred from a historical aggregate. The active-lifetime Run reservation remains constant by accepted EXP-0024 policy while the exact funding component decreases; this is still a real live-geometry reconciliation and ledger release. Other complete branches own their applicable reconciliation or terminal release once. + +## Decision + +Accepted for the admitted reference domain. Select one `reconcile_actor_state_hold_with_authority` operation after applicable User publication and include it only in the matching complete transition owner. For maximum-Step User Opening, use the attained inclusive `536,459,000` RefTime / `14,158` ProofSize plus 24 reads and 13 writes; do not add or subtract a standalone hold coefficient. EXP-0065 owns complete regeneration and exact final binding. + +## Rejected Alternatives and Tradeoffs + +Retain all rejected candidates and causal limits in the extracted evidence. Do not infer a new selection from record relocation or from aggregate coefficient subtraction. + +## Validity + +- `Establishes`: Exactly one post-publication state-hold reconciliation inside the attained maximum-Step User Opening owner, including one hold-record and one host-ledger read/write and an exact funding-component decrease. +- `Does not establish`: Arbitrary host implementations, complete production Weight, exact release artifact closure or integrated throughput. +- `Known limitations`: The accepted value is an inclusive complete-Opening owner, not an isolated hold coefficient; outer queue/heap composition and exact production binding remain open. +- `Invalidation triggers`: A changed measured body, host domain, source/Weight/Wasm identity or acceptance rule requires scoped requalification. +- `Residual qualitative insight`: Follow the owning physical boundary, not a benchmark name. + +## Follow-Up / Next Gradient + +Stop measuring this satisfied owner. EXP-0058/EXP-0059 own non-hold source and Run composition, EXP-0063 owns outer Waiting/heap alternatives, and EXP-0065 owns complete generated-file regeneration. Reopen only for a legal admitted User geometry outside the declared inclusive owners, a changed hold algorithm/host ledger, or omitted/duplicate reconciliation. + +## Architecture Impact + +Benchmark-only postconditions expose the exact hold-component transition. Runtime semantics, production storage and bindings are unchanged; C1 and A0 remain frozen. The accepted inclusive owner is an input to later composition and regeneration. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: [EXP-0028](./EXP-0028.md) O15 +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: [EXP-0025](./EXP-0025.md) +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: [EXP-0031](./EXP-0031.md) exact complete User Opening profile. +- `Transfers question to`: None; independent findings require an explicit new transfer. +- `Produces input for`: [EXP-0028](./EXP-0028.md), [EXP-0068](./EXP-0068.md) +- `Reopen trigger`: A falsifier of this declared owner/domain or exact artifact identity; cost alone does not reopen frozen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0045.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0045.md new file mode 100644 index 00000000..62d77cee --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0045.md @@ -0,0 +1,160 @@ +# EXP-0045 — Authored Amount and LastFunding Algebra + +| Field | Value | +| --- | --- | +| Status | Measured | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | [EXP-0029](./EXP-0029.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-06 | +| Affected domain | Authored amount positions bound Opening and LastFunding cardinalities jointly, with host-qualified receipt identity. | +| Physical mechanism | Authored Amount and LastFunding Algebra | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0025](./EXP-0025.md) — Accepted, physical architecture only | +| Successor experiment | [EXP-0029](./EXP-0029.md) | + + +## Decision Question + +Authored amount positions bound Opening and LastFunding cardinalities jointly, with host-qualified receipt identity. + +## Governing Specification + +- `Owners`: `template/pallets/actors/docs/specification.en.md` (sole ownership, lifecycle/Trigger/Pipeline/Action semantics, placement and scheduler); `docs/actors-resource-policy.specification.en.md` (component-wise admission/actual settlement, equal-thirds block policy); `docs/actors-performance-assurance.specification.en.md` (evidence and target). +- `Frozen physical input`: [EXP-0025](./EXP-0025.md) C1 Architecture Freeze. No change to FIFO, ticket chronology, Q1, causal floor, primary/reference ontology, source/successor atomicity or sole execution owner. +- `Semantic oracle`: A0 SHA-256 `8746e423168309975e509be91e4eb06fdf0eb0b43369d75f1ee62202a4341678` remains frozen; use only for relevant semantic equivalence, never as a Weight oracle. + +## Context and Hypothesis + +This Leaf owns authored amount and lastfunding algebra. Its boundary is the single claim above. Evidence was extracted from the explicitly marked pre-fission parent subsections at Git baseline `a60de7748132211d9e58cecd5be82b41f76ce230`; this migration changes navigation and evidence ownership only. Source, Wasm, commands, rejection outcomes and evidence limitations retain their original meaning. + +Extraction provenance at the recorded Git baseline: EXP-0029 / “Retained Correction Evidence”, “Derived Metrics”. Original directional prose (above/below) belongs to these pre-fission subsections; current cross-owner navigation follows the parent proof map. + +## Baseline and Candidates + +Retained measured implementations and rejected candidates are identified in the extracted evidence. No candidate is introduced by this migration. + +## Controls + +Only the declared witness input varies within each source-bound comparison. Preserve the exact host, toolchain, database, Wasm and sampling controls printed with each evidence item; do not compare unlike artifacts as a controlled performance result. + +## Benchmark Class, Workloads, and Criteria + +Acceptance requires the single claim to hold over its declared admitted domain, with each physical operation charged once and every retained limitation resolved before broader production use. One legal omitted-owner, domain or containment counterexample falsifies that claim. Existing workloads below are bounded witnesses, not permission to add independent questions. + +## Environment and Artifact Identity + +Extraction baseline: `a60de7748132211d9e58cecd5be82b41f76ce230`. The migration manifest records exact original content/mode identities and source spans. Question-specific source/Wasm/output identities remain inline. Historical fragments remain historical; no new runtime or production artifact was built. + +Use the identified environment attached to each extracted source. Where a shared control is needed, follow its explicit evidence source; absent identity requires refresh before acceptance. + +## Proof Obligations + +- `Freeze`: Frozen by explicit migration reconstruction from the pre-fission decision and retained witnesses at a60de7748132211d9e58cecd5be82b41f76ce230. No new measurement or historical decision is implied. +- `Review triggers`: None. +- `Decomposition review`: Boundary reviewed during provenance-preserving fission. Workloads within this Leaf share the declared claim, physical owner, changed variable, evidence domain and acceptance rule. Independently decidable owners were transferred; a new owner or selector requires another node. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | Authored amount positions bound Opening and LastFunding cardinalities jointly, with host-qualified receipt identity. | One two-leg authored Step split between Opening and LastFunding, plus an admitted Unstake receipt whose current host mapping disappears | Source-derived authored-Task algebra; native constructors qualify host assumptions | Self | Required | Named parent decision and declared downstream consumers | Satisfied with scope; original binding/host limitations retained; complete release binding remains open | + +## Measurements + + +### Pre-fission EXP-0029 lines 319–344 + +Let `S` be the admitted Step count, `L` the number of authored amount positions, `O` distinct typed Opening surfaces, `F` admission-time tracked funding assets, `A` current accumulator entries and `R` Run funding-snapshot entries. Let `p_i` count canonical timed-predicate occurrences in Step `i`, with Opening subset `o_i`. Counts describe one current Contract, not an arbitrary product of storage capacities. + +| Current Task family | Amount positions per Step | Opening identity | LastFunding identity | +| --- | --- | --- | --- | +| AddLiquidity | 2 | PreservableAsset for each leg | Each leg asset | +| Transfer, SplitTransfer, Burn, SwapIn, RemoveLiquidity, Stake, DonateLiquidity | 1 | PreservableAsset of the resolved amount | Same asset; LP asset for RemoveLiquidity | +| Mint, SwapOut | 1 | TargetAsset | Target asset | +| Unstake | 1 | StakingShares of the position asset | Host receipt asset at admission | +| StopCycle | 0 | None | None | + +`InputLimit`, SwapOut absolute input ceiling, minimum output values and SplitTransfer leg count add no amount positions. `Fixed`, `PercentageOfCurrent` and `AllAvailable` allocate neither Opening nor LastFunding entries. One amount position cannot select both frozen-source families. Same asset under different Opening variants remains a different surface; same typed surface deduplicates. Funding deduplicates by receipt/direct asset identity independently, so aliasing can lower a count but cannot increase it beyond authored positions. + +| Domain | Source-derived necessary constraint | Qualification / executable owner | +| --- | --- | --- | +| Amount state | `O + F ≤ L ≤ 2S`; `A ≤ F`, `R ≤ F` | `execution.rs::opening_surfaces`, `lib.rs::derive_funding_tracked_assets`; certified ingress changes values and occupied subsets, never the tracked set | +| Concurrent funding | `A + R ≤ 2F`, with overlapping asset identities permitted | Opening snapshots and clears accumulation; later ingress accumulates for the next cycle; `suspended_cycle_freezes_its_funding_snapshot_and_preserves_new_accumulation` | +| Host storage | `S ≤ MaxContractSteps`, `F ≤ MaxFundingTrackedAssets` | Mock caps 12/10, reference caps 12/40; neither the 40-entry storage encoding nor a Weight parameter proves a reachable 40-source Contract | +| Lifecycle | Every non-no-op replacement rederives tracking, prunes accumulation and cancels the old Run | `update_contract_prunes_stale_funding_accumulators`; `prepare_reachable_update` first creates a real Run, refills nonempty accumulation, then replaces through the public call | +| Snapshot values | Accumulation and Run funding values are positive; Opening balances may be zero | Funding is certified ingress history; Opening is a balance observation. Payload commitment proves integrity of the encoded value, not its admission reachability | + +For the reference 12/24/48 host, `O = 24` forces `F = A = R = 0`, while 48 Opening predicate results can coexist with those 24 amount surfaces. Thus 24/48/40 is storage-only and unreachable under the current Task algebra. The mock independent-asset two-leg constructor has `O + F = 24`, so its legal funding-boundary allocations are `O ≥ 14`; `reachable_update_allocation_boundaries_match_authored_contract` covers 14/10, 19/5 and 24/0 through admission, certified funding, Opening, refill and replacement, plus rejection of 13/11 before Actor publication. These are family/host witnesses, not a proof that every tuple satisfying the necessary inequalities is economically or resource-admissible. + + + +### Pre-fission EXP-0029 lines 492–493 + +Zero-Step domain: `derive_funding_tracked_assets` visits only authored Steps, ingress accumulates only tracked assets, and Contract replacement prunes the accumulator to the newly derived set. Thus a canonical empty Contract has zero tracked/accumulated LastFunding entries, including after replacement; arbitrary custody balances do not enlarge that state. EXP-0028's zero-Step profiles need Trigger/head/rearm/terminal/hold qualification, but must not inherit an unreachable nonempty-Contract funding-vector maximum. This source-derived restriction does not bound User hold reconciliation or temporal/Crossing topology. + + +## Derived Metrics + +Derived values are retained beside the evidence that defines their source and units. No subtraction of overlapping aggregate owners or cross-artifact timing comparison is introduced. + +## Statistical / Variance Notes + +Retain each item’s original sampling and uncertainty. Singleton construction probes establish only their admitted state/access witness, never statistical timing dominance or a production bound. + +## Result + +The extracted observations and counterexamples above remain valid only within their original source-bound scope. No new observation was made during migration. + +## Interpretation + +A scoped witness may satisfy its declared obligation without accepting every production consumer. Existing rejected fits, admission refusals, missing host domains and uninstalled fragments remain explicit evidence boundaries. + +## Decision + +No new Accepted/Rejected decision during migration. The declared scoped proof is measured; stop repeating its satisfied witnesses and let the parent consume their stated limits. + +## Rejected Alternatives and Tradeoffs + +Retain all rejected candidates and causal limits in the extracted evidence. Do not infer a new selection from record relocation or from aggregate coefficient subtraction. + +## Validity + +- `Establishes`: Only the source-qualified observations and constraints above. +- `Does not establish`: Complete production Weight, exact release artifact closure or integrated throughput. +- `Known limitations`: Existing missing domains, diagnostic sample counts, stale bindings and host restrictions remain effective. +- `Invalidation triggers`: A changed measured body, host domain, source/Weight/Wasm identity or acceptance rule requires scoped requalification. +- `Residual qualitative insight`: Follow the owning physical boundary, not a benchmark name. + +Pre-fission EXP-0029 / Retained Correction Evidence retains these causal qualifications: + +| Witness | Observation | Classification | +| --- | --- | --- | +| Rederive every tracked asset using current host mapping | Three all-feature fixtures failed: minimum/maximum failed Opening and reachable Opening-tail boundaries, after legal removal of an empty staking receipt | Rejected audit candidate: current host availability cannot recover admission-time identity | +| Qualified audit | Direct sources must be present; total tracked count is bounded by direct sources plus distinct LastFunding staking positions; any such position requires nonempty tracking | Preserves the admission-time receipt uncertainty without new consensus state; exact set equality for Contracts without LastFunding Unstake | +| Independent embedding host | Every `S = 0..8`, pure Opening and maximum tracked-funding allocation, plus one-over-cap refusal; service every nonterminal cursor with immutable captures and TryRuntime | Host limits 8 Steps / 16 Opening / 16 predicate results / 4 funding; default unsupported economic adapters do not prevent honest control-state construction | + +## Follow-Up / Next Gradient + +Stop when the declared proof is satisfied. Transfer any independent owner, selector, parameter domain or artifact-identity question before investigating it beyond the smallest classifier. A hypothetical corner case alone is not a measurement mandate. + +## Architecture Impact + +No runtime, semantic, storage or production binding change. C1 and A0 remain frozen; this record owns only its declared proof evidence. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: [EXP-0029](./EXP-0029.md) O1 +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: [EXP-0025](./EXP-0025.md) +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: None. +- `Transfers question to`: None; independent findings require an explicit new transfer. +- `Produces input for`: [EXP-0029](./EXP-0029.md) +- `Reopen trigger`: A falsifier of this declared owner/domain or exact artifact identity; cost alone does not reopen frozen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0046.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0046.md new file mode 100644 index 00000000..012a7365 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0046.md @@ -0,0 +1,132 @@ +# EXP-0046 — Predicate and DNF Reachable Geometry + +| Field | Value | +| --- | --- | +| Status | Measured | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | [EXP-0029](./EXP-0029.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-06 | +| Affected domain | Canonical DNF and timed predicate occurrences determine admitted result cardinality and exact committed-prefix cursor. | +| Physical mechanism | Predicate and DNF Reachable Geometry | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0025](./EXP-0025.md) — Accepted, physical architecture only | +| Successor experiment | [EXP-0029](./EXP-0029.md) | + + +## Decision Question + +Canonical DNF and timed predicate occurrences determine admitted result cardinality and exact committed-prefix cursor. + +## Governing Specification + +- `Owners`: `template/pallets/actors/docs/specification.en.md` (sole ownership, lifecycle/Trigger/Pipeline/Action semantics, placement and scheduler); `docs/actors-resource-policy.specification.en.md` (component-wise admission/actual settlement, equal-thirds block policy); `docs/actors-performance-assurance.specification.en.md` (evidence and target). +- `Frozen physical input`: [EXP-0025](./EXP-0025.md) C1 Architecture Freeze. No change to FIFO, ticket chronology, Q1, causal floor, primary/reference ontology, source/successor atomicity or sole execution owner. +- `Semantic oracle`: A0 SHA-256 `8746e423168309975e509be91e4eb06fdf0eb0b43369d75f1ee62202a4341678` remains frozen; use only for relevant semantic equivalence, never as a Weight oracle. + +## Context and Hypothesis + +This Leaf owns predicate and dnf reachable geometry. Its boundary is the single claim above. Evidence was extracted from the explicitly marked pre-fission parent subsections at Git baseline `a60de7748132211d9e58cecd5be82b41f76ce230`; this migration changes navigation and evidence ownership only. Source, Wasm, commands, rejection outcomes and evidence limitations retain their original meaning. + +Extraction provenance at the recorded Git baseline: EXP-0029 / “Retained Correction Evidence”. Original directional prose (above/below) belongs to these pre-fission subsections; current cross-owner navigation follows the parent proof map. + +## Baseline and Candidates + +Retained measured implementations and rejected candidates are identified in the extracted evidence. No candidate is introduced by this migration. + +## Controls + +Only the declared witness input varies within each source-bound comparison. Preserve the exact host, toolchain, database, Wasm and sampling controls printed with each evidence item; do not compare unlike artifacts as a controlled performance result. + +## Benchmark Class, Workloads, and Criteria + +Acceptance requires the single claim to hold over its declared admitted domain, with each physical operation charged once and every retained limitation resolved before broader production use. One legal omitted-owner, domain or containment counterexample falsifies that claim. Existing workloads below are bounded witnesses, not permission to add independent questions. + +## Environment and Artifact Identity + +Extraction baseline: `a60de7748132211d9e58cecd5be82b41f76ce230`. The migration manifest records exact original content/mode identities and source spans. Question-specific source/Wasm/output identities remain inline. Historical fragments remain historical; no new runtime or production artifact was built. + +Use the identified environment attached to each extracted source. Where a shared control is needed, follow its explicit evidence source; absent identity requires refresh before acceptance. + +## Proof Obligations + +- `Freeze`: Frozen by explicit migration reconstruction from the pre-fission decision and retained witnesses at a60de7748132211d9e58cecd5be82b41f76ce230. No new measurement or historical decision is implied. +- `Review triggers`: None. +- `Decomposition review`: Boundary reviewed during provenance-preserving fission. Workloads within this Leaf share the declared claim, physical owner, changed variable, evidence domain and acceptance rule. Independently decidable owners were transferred; a new owner or selector requires another node. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | Canonical DNF and timed predicate occurrences determine admitted result cardinality and exact committed-prefix cursor. | One canonical DNF whose remaining Opening occurrences or committed-prefix result cursor differ from the derived count | Source-derived canonical DNF/prefix cardinality; native corruption controls | Self | Required | Named parent decision and declared downstream consumers | Satisfied with scope; original binding/host limitations retained; complete release binding remains open | + +## Measurements + +### Canonical Predicate and Cursor Constraints + +Let `p_i` count canonical timed-predicate occurrences in Step `i`, with Opening subset `o_i`; `S` is admitted Step count. + +| Domain | Source-derived necessary constraint | Qualification / executable owner | +| --- | --- | --- | + +| Predicates | `0 ≤ o_i ≤ p_i ≤ min(MaxPredicatesPerStep, MaxPreconditionClauses × MaxPredicatesPerClause)`; total Opening results `Σo_i` | Count after sort/dedup, duplicate-clause rejection and strict-superset absorption; capture records every remaining Opening occurrence, including repeated occurrences across distinct clauses/Steps | +| Opening-result cursor | At Step cursor `c`, `opening_predicate_cursor = Σ(i < c) o_i` | Specification §5.3; advance consumes the complete current-Step range, retry retains its start. TryRuntime checks exact prefix cardinality as well as total result length | +| Step predicate Weight axis | `evaluation_units = p_i + o_i ≤ 2 × effective predicate capacity` | `Precondition::evaluation_units` includes an additional unit for each Opening occurrence; the reference ceiling is 8 units for 4 all-Opening predicates, not 4. Results select Step actual paths without truncating predicate visits. | + + +## Derived Metrics + +Derived values are retained beside the evidence that defines their source and units. No subtraction of overlapping aggregate owners or cross-artifact timing comparison is introduced. + +## Statistical / Variance Notes + +Retain each item’s original sampling and uncertainty. Singleton construction probes establish only their admitted state/access witness, never statistical timing dominance or a production bound. + +## Result + +The extracted observations and counterexamples above remain valid only within their original source-bound scope. No new observation was made during migration. + +## Interpretation + +A scoped witness may satisfy its declared obligation without accepting every production consumer. Existing rejected fits, admission refusals, missing host domains and uninstalled fragments remain explicit evidence boundaries. + +## Decision + +No new Accepted/Rejected decision during migration. The declared scoped proof is measured; stop repeating its satisfied witnesses and let the parent consume their stated limits. + +## Rejected Alternatives and Tradeoffs + +Retain all rejected candidates and causal limits in the extracted evidence. Do not infer a new selection from record relocation or from aggregate coefficient subtraction. + +## Validity + +- `Establishes`: Only the source-qualified observations and constraints above. +- `Does not establish`: Complete production Weight, exact release artifact closure or integrated throughput. +- `Known limitations`: Existing missing domains, diagnostic sample counts, stale bindings and host restrictions remain effective. +- `Invalidation triggers`: A changed measured body, host domain, source/Weight/Wasm identity or acceptance rule requires scoped requalification. +- `Residual qualitative insight`: Follow the owning physical boundary, not a benchmark name. + +## Follow-Up / Next Gradient + +Stop when the declared proof is satisfied. Transfer any independent owner, selector, parameter domain or artifact-identity question before investigating it beyond the smallest classifier. A hypothetical corner case alone is not a measurement mandate. + +## Architecture Impact + +No runtime, semantic, storage or production binding change. C1 and A0 remain frozen; this record owns only its declared proof evidence. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: [EXP-0029](./EXP-0029.md) O2 +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: [EXP-0025](./EXP-0025.md) +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: None. +- `Transfers question to`: None; independent findings require an explicit new transfer. +- `Produces input for`: [EXP-0029](./EXP-0029.md) +- `Reopen trigger`: A falsifier of this declared owner/domain or exact artifact identity; cost alone does not reopen frozen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0047.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0047.md new file mode 100644 index 00000000..4320bb7d --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0047.md @@ -0,0 +1,172 @@ +# EXP-0047 — Funding Snapshot Consumption Bound + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | [EXP-0028](./EXP-0028.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-06 | +| Affected domain | The funding snapshot consumption component covers the legal authored tracked-asset domain with certified ingress. | +| Physical mechanism | Funding Snapshot Consumption Bound | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0025](./EXP-0025.md) — Accepted, physical architecture only | +| Successor experiment | [EXP-0028](./EXP-0028.md) | + + +## Decision Question + +The funding snapshot consumption component covers the legal authored tracked-asset domain with certified ingress. + +## Governing Specification + +- `Owners`: `template/pallets/actors/docs/specification.en.md` (sole ownership, lifecycle/Trigger/Pipeline/Action semantics, placement and scheduler); `docs/actors-resource-policy.specification.en.md` (component-wise admission/actual settlement, equal-thirds block policy); `docs/actors-performance-assurance.specification.en.md` (evidence and target). +- `Frozen physical input`: [EXP-0025](./EXP-0025.md) C1 Architecture Freeze. No change to FIFO, ticket chronology, Q1, causal floor, primary/reference ontology, source/successor atomicity or sole execution owner. +- `Semantic oracle`: A0 SHA-256 `8746e423168309975e509be91e4eb06fdf0eb0b43369d75f1ee62202a4341678` remains frozen; use only for relevant semantic equivalence, never as a Weight oracle. + +## Context and Hypothesis + +This Leaf owns funding snapshot consumption bound. Its boundary is the single claim above. Evidence was extracted from the explicitly marked pre-fission parent subsections at Git baseline `a60de7748132211d9e58cecd5be82b41f76ce230`; this migration changes navigation and evidence ownership only. Source, Wasm, commands, rejection outcomes and evidence limitations retain their original meaning. + +Extraction provenance at the recorded Git baseline: EXP-0029 / “Retained Correction Evidence”, “Environment and Artifact Identity”. Original directional prose (above/below) belongs to these pre-fission subsections; current cross-owner navigation follows the parent proof map. + +## Baseline and Candidates + +Retained measured implementations and rejected candidates are identified in the extracted evidence. No candidate is introduced by this migration. + +## Controls + +Only the declared witness input varies within each source-bound comparison. Preserve the exact host, toolchain, database, Wasm and sampling controls printed with each evidence item; do not compare unlike artifacts as a controlled performance result. + +## Benchmark Class, Workloads, and Criteria + +Acceptance requires the single claim to hold over its declared admitted domain, with each physical operation charged once and every retained limitation resolved before broader production use. One legal omitted-owner, domain or containment counterexample falsifies that claim. Existing workloads below are bounded witnesses, not permission to add independent questions. + +## Environment and Artifact Identity + +Extraction baseline: `a60de7748132211d9e58cecd5be82b41f76ce230`. The migration manifest records exact original content/mode identities and source spans. Question-specific source/Wasm/output identities remain inline. Historical fragments remain historical; no new runtime or production artifact was built. + +Use the identified environment attached to each extracted source. Where a shared control is needed, follow its explicit evidence source; absent identity requires refresh before acceptance. + +## Proof Obligations + +- `Freeze`: Frozen by explicit migration reconstruction from the pre-fission decision and retained witnesses at a60de7748132211d9e58cecd5be82b41f76ce230. No new measurement or historical decision is implied. +- `Review triggers`: None. +- `Decomposition review`: Boundary reviewed during provenance-preserving fission. Workloads within this Leaf share the declared claim, physical owner, changed variable, evidence domain and acceptance rule. Independently decidable owners were transferred; a new owner or selector requires another node. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | The funding snapshot consumption component covers the legal authored tracked-asset domain with certified ingress. | One admitted tracked asset funded by certified Signed ingress, then consumed by funding_snapshot_open | Reference-Wasm FRAME evidence in the declared owner/domain; native checks are construction controls | Self | Required | Named parent decision and declared downstream consumers | Satisfied for the reference 1..24 authored-source domain by certified Signed ingress and the complete generated component model | + +## Measurements + + +### Pre-fission EXP-0029 lines 301–318 + +Reference probe command: `./scripts/benchmarks.sh --extrinsic funding_snapshot_open --output /tmp/deos-exp29-funding-snapshot.rs pallet_deos_actors`, 50 steps × 20 repeats. The first run built successfully and failed before a Weight model was generated; failure-log SHA-256 `4c715e70f2220fd4f025c608cee9771b687f66617ae56959874b0b6379b3a5c7`. Signed-origin setup models the certified producer, not full Executive/transaction-fee composition. Unsupported embedding hosts fail closed through the benchmark-helper default instead of assigning Signed provenance to arbitrary internal transfers. + +The corrected reference-Wasm run passed the complete declared `a = 1..24` domain, including the maximum 12-Step, 24-source Contract. It used compiled Wasm on AMD Ryzen 7 4800H, DB cache 1024, 4096 heap pages and worst-case map size 1,000,000. Build and measurement followed completed native checks without competing test/Clippy jobs. Native `deos-runtime --all-features --test-filter signed_` and workspace Clippy also passed after the host-helper correction. + +| Component / legal domain | Generated RefTime, ps | ProofSize, bytes | Database work | +| --- | --- | --- | --- | +| `funding_snapshot_open(a)`, `1 ≤ a ≤ 24` | `14,154,244 + 288,800a`; slope standard error 4,245; minimum observed execution 12,432,000 | Measured `631 + 26a` (657..1,255); estimated 4,531; ActorFunding MaxEncodedLen 1,066 | ActorFunding: one read, one write; configured DB Weight added separately | + +This proves the component's input construction on the reference benchmark runtime. It does not certify the complete Opening envelope, replace User hold/fee work, prove values `25..40` reachable, or demonstrate a performance gain from smaller logical cardinality: the estimated proof still follows the storage encoding bound. EXP-0028 / EXP-0064 must reconcile this component with complete phase ownership and admission before EXP-0065 installs regenerated complete files. + +| Additional verification | Outcome / scope | +| --- | --- | +| Same all-feature test route with `--test-filter reachable_update_allocation_boundaries_match_authored_contract` | Passed after adding funding-boundary rejection; accepted allocations reuse existing lifecycle construction | +| From `template`: `SKIP_WASM_BUILD=1 cargo test --locked -p pallet-deos-actors --lib --features try-runtime try_state_ --message-format=short` | 22 passed, one ignored; audit tests compile and execute without runtime-benchmarks | +| `./scripts/ci-local.sh --only clippy` | Workspace/all-target/all-feature `-D warnings` passed after the last Rust edit | +| `./scripts/benchmarks.sh --check` | Compilation passed; final storage-name audit failed on unchanged generated Actors/Router/TMC Weight referencing retired storage. This remains EXP-0065/G5, not candidate compilation failure or permission to repair generated comments manually | +| Changed-scope completion | Passed, including the complete default-feature runtime test suite; the retained audit-loop refinement also passed the focused all-feature `try_state_` tests | + + + +### Funding Measurement Environment + +| Artifact | Identity / required binding | +| --- | --- | +| Baseline source | Commit `5b46b19004b62a723d6c45a610dfe1e5c79982b0`; template tree `98af72ffbb55fe8840b5bb44b5b01d65c8e775e1` | +| Measurement source/config/toolchain | Rust 1.94.1, frame-omni-bencher 0.22.0; worktree source identity `33ca329a7e40d826f7ec38afa72fbb51ab92dca7a19e693ef4169d6b4769737d` from `scripts/actors-assurance.sh::worktree_evidence_identity`; content and modes, independent of commit packaging | +| Measured fixture source | SHA-256: pallet `src/lib.rs` `78d113fb1592c516fc29a713601af8edc0780c014f8653e26abeaabff0a3b44f`, `src/benchmarking.rs` `4adba7cc4918f8c22d0209592caf7f2db855099e818032d5f11e8d844e626e52`, runtime `configs/actor_config.rs` `7216c72353c9ce45abaf4392e26019e76fa3fdbe88b7f1dd271884191237e4d5` | +| Funding audit-only refinement | Source `009d6cd04232b3c712ae9e7cd01713e855dfc1f5fc78a39524e1e5946f27f059`; pallet lib `4d081f6cb7cf39f6b30c95f023d826acbe9c17e977e977081c865bc859e7d56b`. Re-expressed the same zero/untracked check as bounded borrowed iteration inside the final `cfg(try-runtime)` function. Reversing only this change reconstructs the measured lib hash exactly; the benchmark profile excludes that function, so the original funding evidence remains applicable to that refinement | +| Benchmark Wasm | Successful signed-producer candidate: `2346a0865075cdefc710b1e056739dfde5b57282a30c362535af58e5ef43f5c4`; first failed probe: `8fa6ab9b0887e3edeb5902205f1176cc7b66b4c6eea44b6bbe822c590f272248` on source `6edb38cffbc9b62831cc7685765da2e851d79e76368cead2b1679f8abb6516ec` | +| Production Wasm | Not produced; absent at the canonical `template/target/release/wbuild/deos-runtime/deos_runtime.compact.compressed.wasm` path during this measurement | +| Generated component fragment | `/tmp/deos-exp29-funding-snapshot.rs`, SHA-256 `b6cbfef3bbb784be419e5f5d8f69823b03c21679d84f335d0e816ce20c3336e7`; normalized model below, not installed as production Weight | +| Commands | Native commands and outcomes below; `scripts/benchmarks.sh --check` checks compilation/storage names only, not measured Weight or production capacity | +| Evidence owner | Baseline source and the question-specific facts below; new measurements must carry exact source/artifact identities in this record | + + +### Decision-Time Qualification + +The corrected generated fragment remains available at its recorded SHA-256 `b6cbfef3bbb784be419e5f5d8f69823b03c21679d84f335d0e816ce20c3336e7`. The `funding_snapshot_open` benchmark body is byte-identical between the retained post-measurement source anchor `c2bb0c34016645a5d859684e86cb38b938ec17a4` and the decision tree (body SHA-256 `08254b3f2d57e71b5eba8f3776d8d2cc31dd8bdf5ae5468d03b9ec59e19671dd`). The production fresh-Opening execution owner and `derive_funding_tracked_assets` algebra are likewise unchanged across that interval, with scoped SHA-256 values `57b8fe64f21a85e39cd3b9e47a2791ccedb17e200982697e5f47f672c4c59af4` and `2322c89f7dc9f30934acefd34f7a7273454c16cf156c7f6d03c196cc25f7024f`. + +Accepted EXP-0045 proves `F ≤ 2S` and the reference `S = 12` authored-source ceiling, so `1..24` is the complete nonzero actual funding-snapshot domain even though `MaxFundingTrackedAssets = 40` remains a conservative storage/admission capacity. The benchmark constructs each point through an admitted Contract and certified Signed producer ingress, then consumes exactly the populated `ActorFunding` map. At the attained endpoint `a = 24`, the selected component is `21,085,444` pre-DB RefTime / `4,531` estimated ProofSize plus one read and one write, totaling `146,085,444` RefTime under the pinned RocksDB charge. + +This accepts only the consumption component and its actual authored domain. The configured-capacity maximum selector, complete Opening owner, installed stale `1..10` generated coefficient, and exact production-file identity remain EXP-0064/EXP-0065 work; values `25..40` must not be presented as reachable actual snapshots or used to widen this benchmark. + +## Derived Metrics + +Derived values are retained beside the evidence that defines their source and units. No subtraction of overlapping aggregate owners or cross-artifact timing comparison is introduced. + +## Statistical / Variance Notes + +Retain each item’s original sampling and uncertainty. Singleton construction probes establish only their admitted state/access witness, never statistical timing dominance or a production bound. + +## Result + +The corrected reference-Wasm model covers every nonzero actual snapshot cardinality in the admitted reference authored domain: `funding_snapshot_open(a) = 14,154,244 + 288,800a` pre-DB RefTime with fixed `4,531` estimated ProofSize and one `ActorFunding` read/write for `1 ≤ a ≤ 24`. Certified Signed ingress and public Contract admission construct the measured state. + +## Interpretation + +The legal actual domain comes from authored source algebra, not the wider 40-entry storage capacity. This component consumes and clears accumulated funding only; it excludes Opening snapshots, predicates, task execution, Pipeline collection, User holds, Run publication and outer scheduler work. + +## Decision + +Accepted for the reference binding's authored funding domain. Select the generated `funding_snapshot_open(a)` component only for a nonempty fresh-Opening funding snapshot with actual `1 ≤ a ≤ 24`; zero selects no component. Use `14,154,244 + 288,800a` pre-DB RefTime, fixed `4,531` estimated ProofSize and one read/write. EXP-0065 must regenerate the complete production Weight file; do not install this fragment manually or generalize it to unreachable `25..40` actual snapshots. + +## Rejected Alternatives and Tradeoffs + +Retain all rejected candidates and causal limits in the extracted evidence. Do not infer a new selection from record relocation or from aggregate coefficient subtraction. + +## Validity + +- `Establishes`: The reference 1..24 authored-source funding-snapshot consumption component, certified Signed ingress construction, generated RefTime/proof model and one-read/one-write owner. +- `Does not establish`: Reachability of storage-capacity values 25..40, the complete Opening envelope, configured-capacity maximum composition, exact production Weight, release artifact closure or integrated throughput. +- `Known limitations`: The generated fragment is intentionally uninstalled; the production file still carries a stale 1..10 coefficient and complete binding remains G5 work. +- `Invalidation triggers`: A changed measured body, authored funding algebra, host domain, source/Weight/Wasm identity or acceptance rule requires scoped requalification. +- `Residual qualitative insight`: Actual funding cardinality follows authored LastFunding sources; storage capacity remains a separate conservative input. + +Pre-fission EXP-0029 / Retained Correction Evidence retains these causal qualifications: + +| Witness | Observation | Classification | +| --- | --- | --- | +| First reference-Wasm funding probe | At `a = 1`, `AssetOps::transfer` moved custody but accumulation remained empty: the runtime supplies `InternalProtocol`, while the fixture's `OwnerOnly` requires `Signed`; the mock had reported Signed | Rejected fixture assumption, not a reason to weaken `OwnerOnly`. Shared funding setup now uses a host signed-transfer helper; the runtime composes the existing ingress extension's prepare, actual dispatch and consequence under one storage transaction | + +## Follow-Up / Next Gradient + +Stop measuring this satisfied component. EXP-0064 owns configured-capacity admission composition and EXP-0065 owns complete generated-file regeneration. Reopen only if authored funding algebra admits more than 24 actual sources, the consumption body or host encoding changes, or a legal point exceeds the selected component. + +## Architecture Impact + +No runtime, semantic, storage or production binding change. C1 and A0 remain frozen. The accepted component and actual-domain partition become inputs to phase composition and later complete regeneration. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: [EXP-0028](./EXP-0028.md) O16 +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: [EXP-0025](./EXP-0025.md), [EXP-0045](./EXP-0045.md) +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: [EXP-0045](./EXP-0045.md) authored source algebra and reference-domain bound. +- `Transfers question to`: None; independent findings require an explicit new transfer. +- `Produces input for`: [EXP-0028](./EXP-0028.md) +- `Reopen trigger`: A falsifier of this declared owner/domain or exact artifact identity; cost alone does not reopen frozen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0048.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0048.md new file mode 100644 index 00000000..5c5ba0ff --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0048.md @@ -0,0 +1,184 @@ +# EXP-0048 — Run and Contract Head-Tail Reachability + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | [EXP-0029](./EXP-0029.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-06 | +| Affected domain | Public lifecycle constructors jointly realize the declared Run, head, tail, cursor and zero-Step encoded geometry. | +| Physical mechanism | Run and Contract Head-Tail Reachability | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0025](./EXP-0025.md) — Accepted, physical architecture only | +| Successor experiment | [EXP-0029](./EXP-0029.md) | + + +## Decision Question + +Public lifecycle constructors jointly realize the declared Run, head, tail, cursor and zero-Step encoded geometry. + +## Governing Specification + +- `Owners`: `template/pallets/actors/docs/specification.en.md` (sole ownership, lifecycle/Trigger/Pipeline/Action semantics, placement and scheduler); `docs/actors-resource-policy.specification.en.md` (component-wise admission/actual settlement, equal-thirds block policy); `docs/actors-performance-assurance.specification.en.md` (evidence and target). +- `Frozen physical input`: [EXP-0025](./EXP-0025.md) C1 Architecture Freeze. No change to FIFO, ticket chronology, Q1, causal floor, primary/reference ontology, source/successor atomicity or sole execution owner. +- `Semantic oracle`: A0 SHA-256 `8746e423168309975e509be91e4eb06fdf0eb0b43369d75f1ee62202a4341678` remains frozen; use only for relevant semantic equivalence, never as a Weight oracle. + +## Context and Hypothesis + +This Leaf owns run and contract head-tail reachability. Its boundary is the single claim above. Evidence was extracted from the explicitly marked pre-fission parent subsections at Git baseline `a60de7748132211d9e58cecd5be82b41f76ce230`; this migration changes navigation and evidence ownership only. Source, Wasm, commands, rejection outcomes and evidence limitations retain their original meaning. + +Extraction provenance at the recorded Git baseline: EXP-0029 / “Retained Correction Evidence”, “Derived Metrics”, “Environment and Artifact Identity”. Original directional prose (above/below) belongs to these pre-fission subsections; current cross-owner navigation follows the parent proof map. + +## Baseline and Candidates + +Retained measured implementations and rejected candidates are identified in the extracted evidence. No candidate is introduced by this migration. + +## Controls + +Only the declared witness input varies within each source-bound comparison. Preserve the exact host, toolchain, database, Wasm and sampling controls printed with each evidence item; do not compare unlike artifacts as a controlled performance result. + +## Benchmark Class, Workloads, and Criteria + +Acceptance requires the single claim to hold over its declared admitted domain, with each physical operation charged once and every retained limitation resolved before broader production use. One legal omitted-owner, domain or containment counterexample falsifies that claim. Existing workloads below are bounded witnesses, not permission to add independent questions. + +## Environment and Artifact Identity + +Extraction baseline: `a60de7748132211d9e58cecd5be82b41f76ce230`. The migration manifest records exact original content/mode identities and source spans. Question-specific source/Wasm/output identities remain inline. Historical fragments remain historical; no new runtime or production artifact was built. + +Use the identified environment attached to each extracted source. Where a shared control is needed, follow its explicit evidence source; absent identity requires refresh before acceptance. + +## Proof Obligations + +- `Freeze`: Frozen by explicit migration reconstruction from the pre-fission decision and retained witnesses at a60de7748132211d9e58cecd5be82b41f76ce230. No new measurement or historical decision is implied. +- `Review triggers`: None. +- `Decomposition review`: Boundary reviewed during provenance-preserving fission. Workloads within this Leaf share the declared claim, physical owner, changed variable, evidence domain and acceptance rule. Independently decidable owners were transferred; a new owner or selector requires another node. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | Public lifecycle constructors jointly realize the declared Run, head, tail, cursor and zero-Step encoded geometry. | One terminal one-Step head incorrectly retaining a tail, or a proposed encoded maximum that public construction cannot attain | Reference-Wasm FRAME evidence in the declared owner/domain; native checks are construction controls | Self | Required | Named parent decision and declared downstream consumers | Accepted for the admitted mock/reference host domain; rejected dense maxima remain explicit resource-admission boundaries | + +## Measurements + + +### Pre-fission EXP-0029 lines 345–372 + +The six `scheduler_inner_suspended_head_{retry,complete,progress,opening_retry,opening_complete,opening_progress}` profiles now use `prepare_reachable_suspended_head_inner`: author before admission, certified OwnerOnly funding, real Manual Opening, actual failure, and due wakeup. Removed the unused synthetic Run installer and scalar-source consumer. The measured block still isolates the inner transition after source consumption; this is not a full scheduler or Executive measurement. `opening_snapshot_capture` remains an isolated capture benchmark, with positive `min_lp_out` so its two-leg Steps satisfy the Task shape contract. + +| Head profile partition | Authored and retained domain | Real transition | +| --- | --- | --- | +| Retry / progress | `S = MaxContractSteps`, `L_tail = 2(S−1)`, `max(0, L_tail−MaxFundingTrackedAssets) ≤ n ≤ L_tail`; `O = n`, `F = R = L_tail−n` | Retry head is unfunded Fixed Transfer; progress head is current-balance AddLiquidity whose impossible output bound first fails transactionally | +| Completion | `S = 1`, `O = F = R = 0` for this current-balance Task family | A head cannot complete by reaching the end while retaining an authored tail; host account freeze changes spendability after real Temporary failure, so current resolution skips and completes without effects | +| Predicates | Tail `o_i = p_i = capacity`; head Current-only uses `0..capacity` Current occurrences, Opening variant uses one Opening plus `0..capacity−1` Current occurrences | Retained results total `(S−1) × capacity + o_head`; head predicates are true, tail predicates false; frozen head truth survives retry | +| Native domain controls | Terminal, minimum/interior/maximum coupled allocations and every `0 ≤ o_head ≤ p_head ≤ capacity` | `reachable_suspended_head_inner_allocation_boundaries` checks admission, immutable captures, actual retry/progress/completion, custody-preserving thaw and TryRuntime; rejects terminal tails, excessive amount allocation and excessive predicate count before Actor publication | + +The changed benchmark signatures deliberately remove independent `r` and `f` axes. Existing generated Weight signatures and runtime selectors are still stale and must be reconciled by EXP-0028 / EXP-0064 before EXP-0065 regeneration; a slope along `O + F = L_tail` is a tradeoff, not an independent marginal coefficient for either field. These profiles do not prove an envelope for frozen-source heads with their additional one/two amount positions, all Opening/Current compositions, false-head branches, User holds, invoked effects or every storage layout. + +The first reference-Wasm retry probe rejected the maximum authored Contract with `ContractStepsExceedOnIdleBudget` before measurement. Source `196e9fd7d33bfe7f98584f41eb18c33b3880ce6879362496e21acfdddb656bf9`, benchmark source SHA-256 `6dab431697abb79241ae819e78fd012934902967982a1cfb1a16849f2c33aa2f`, Wasm `e66b2aef40ed62222eaf0b157c725f77d81791e4b585315cd562339358500f72`; failed-log SHA-256 `299cf5524b8864d5c5709fab42816539344558ae2c0fdceb9822f41982e9d558`. Command: `./scripts/benchmarks.sh --extrinsic scheduler_inner_suspended_head_retry --output /tmp/deos-exp29-head-retry.rs pallet_deos_actors`, 50 × 20. This is evidence of the current resource-admission boundary, not structural Contract rejection or permission to reduce semantic bounds. + +The same-Wasm terminal probe passed admission and real failure, then falsified the fixture assumption that exactly one Actor becomes Ready: host asset bootstrap also owns a due Actor. The corrected fixture preserves that topology, verifies its own Ready locator and asserts successor occupancy relative to the other live entries rather than deleting unrelated scheduler state. Native filtered `suspended_head` tests pass all nine tests, including the six generated benchmark tests and the allocation/timing control. + +After that correction, both terminal profiles pass on the bound head Wasm at 50 steps × 20 repeats. Command family: `./scripts/benchmarks.sh --extrinsic scheduler_inner_suspended_head_complete --output /tmp/deos-exp29-head-complete.rs pallet_deos_actors`; subsequent profiles use the same command with `--skip-build`, matching exact profile/output names. No concurrent heavy work ran during these measurements. Hardware/cache/heap/map controls match the funding run above. + +| Terminal profile | Domain | RefTime model, picoseconds before configured DB Weight | Estimated / measured proof bytes | DB operations | +| --- | --- | --- | --- | --- | +| Current-only completion | `p = 0..4`, one Step, no immutable amount/funding entries | `118,862,218 + 7,724,877p`; slope standard error `185,422`; minimum execution `98,338,000` | Estimated `6,007 + 2,698p`; measured `2,548 + 220p` | `6 + 2p` reads, `4` writes | +| One-Opening completion | `p = 0..3` Current plus one Opening occurrence, one Step | `119,176,095 + 8,972,528p`; slope standard error `226,071`; minimum execution `101,551,000` | Estimated `6,021 + 2,689p`; measured `2,560 + 207p` | `6 + 2p` reads, `4` writes | + +The three remaining maximal-tail probes on that same Wasm (`progress`, `opening_retry`, `opening_progress`) all fail Contract admission before a model is produced, with the same `ContractStepsExceedOnIdleBudget` class as the first retry probe. Failed-log SHA-256 respectively: `4eb2168631637f008b81a55ab8d26d2aca596f14f6e9d145e8ac5ad92a87366c`, `845e02f444b6b9d6f6dab3ccd3af2ddc24ff2104c1e9d04c2ce64a7ff35ee321`, `716de0472518ff9d68efaa66b1b35048c4e20bf83734262dca051f2519e736ca`. These are four failed parameter-domain probes, not proof that every point or every twelve-Step Contract is inadmissible. The earlier unrelated-Ready counterexample log hash is `f665660b90a0424be1cde03b2bec55610fbf0ed413728d50c730595b257977ee`. + +Retained head source validation: `./scripts/ci-local.sh --only tests --package pallet-deos-actors --all-features` passed, as did workspace/all-target/all-feature Clippy with `-D warnings`. Changed-scope completion passed with `--skip-cargo-check --skip-runtime-tests`: fresh Clippy supplies compilation, and the head changes are confined to the benchmark module, leaving the previously passed default-feature runtime inputs unchanged. This reuse does not turn the four benchmark admission failures into passing production evidence. + +The [EXP-0028 reference-Wasm receipt audit](./EXP-0030.md#reference-wasm-inclusion-and-admission-boundary) additionally qualifies CompleteMax at 1 and 5 authored Steps and records ordinary admission refusal at 9 and 12 Steps. That record owns exact source/Wasm identities, commands and scoped measurements. This is another host resource boundary, not a reason to bypass admission, lower authored bounds for the fixture, or reopen C1; EXP-0064 must reconcile the complete envelope. + + + +### Pre-fission EXP-0029 lines 466–469 + +Qualified zero-Step head certificate: `scheduler_inner_zero_step_user_max_head` ordinarily admits full AddressEvent source/asset whitelists and SignedAllowlist with a present window and terminal nonce. Native and fresh reference-Wasm assertions require the stored header's encoded length to equal its host MaxEncodedLen; first Step/resources are both absent. Thus the exact zero-Step head bound is the attained header maximum plus two Option discriminants, without compact resource widths or semantic predicate capacity. [EXP-0028](./EXP-0037.md#maximum-zero-step-head-and-window-reference) owns the complete User Opening measurement and exact source/artifact identities. + +Under the reference AccountId32, five-byte Local AssetId and sixteen-entry lists, the derived SCALE field sizes are Trigger `597`, cooldown `4`, present window `9`, funding `514`, completion `1`, present nonce `9`, Step count `4`, three hashes `96` and machine envelope `32`: header `1,266` bytes and zero-Step head `1,268`. The mock's eight-byte AccountId reduces each of the two sixteen-account lists by 384 bytes, yielding `498/500`. These sizes are derived from the qualified host types; the executable equality certifies joint reachability rather than independently maximizing unreachable fields. This closes serialized zero-Step head size for these hosts, not CPU/decode-branch, complete Waiting/hold or nonempty-head dominance. + + +### Head and Tail Constraint + +Let `p_i` count canonical timed-predicate occurrences in Step `i`, with Opening subset `o_i`; `S` is admitted Step count. + +| Domain | Source-derived necessary constraint | Qualification / executable owner | +| --- | --- | --- | + +| Head/tail | One head; `ceil(max(S−1, 0)/4)` tail chunks | For cursor `c > 0`, chunk `k = floor((c−1)/4)` has `min(4, S−1−4k)` Steps; fragment length is not cursor or total remaining Steps | + + + +### Head Measurement Environment + +| Artifact | Identity / required binding | +| --- | --- | +| Baseline source | Commit `5b46b19004b62a723d6c45a610dfe1e5c79982b0`; template tree `98af72ffbb55fe8840b5bb44b5b01d65c8e775e1` | +| Measured fixture source | SHA-256: pallet `src/lib.rs` `78d113fb1592c516fc29a713601af8edc0780c014f8653e26abeaabff0a3b44f`, `src/benchmarking.rs` `4adba7cc4918f8c22d0209592caf7f2db855099e818032d5f11e8d844e626e52`, runtime `configs/actor_config.rs` `7216c72353c9ce45abaf4392e26019e76fa3fdbe88b7f1dd271884191237e4d5` | +| Suspended-head source / Wasm | Source `83f327f5536e9401ddc50babb1e1a5cb0410c3dbd0edcdf0c64f8c889f69aecc`; benchmark source SHA-256 `1edf9c0b46e85c9a35bf8341e7c2343ff3660ba2aaa6aa8f063f5ca4116c8817`; benchmark Wasm `c79eaec1e59bd2f69a8912ddce1b59fa65bf37f784b7d2c7dd5eaac3c129b696`. Same reference host/toolchain, with unrelated host-created Ready Actors preserved | +| Terminal head fragments | Current-only `/tmp/deos-exp29-head-complete.rs` SHA-256 `f16fe85c58e6561031bec9a4f8b937f3f51affaf8ec218575ee7aca0c515d971`; one-Opening `/tmp/deos-exp29-head-opening-complete.rs` SHA-256 `44a31ca0bbba96fa0acec1b1db1580ca8f3edf1fa5225459a2aa5d6114dd8149`. Normalized models below; neither is installed as production Weight | +| Generated Weight / metadata / ABI | Not accepted by this experiment | +| Commands | Native commands and outcomes below; `scripts/benchmarks.sh --check` checks compilation/storage names only, not measured Weight or production capacity | +| Evidence owner | Baseline source and the question-specific facts below; new measurements must carry exact source/artifact identities in this record | + + +## Derived Metrics + +Derived values are retained beside the evidence that defines their source and units. No subtraction of overlapping aggregate owners or cross-artifact timing comparison is introduced. + +## Statistical / Variance Notes + +Retain each item’s original sampling and uncertainty. Singleton construction probes establish only their admitted state/access witness, never statistical timing dominance or a production bound. + +## Result + +Public constructors attain the exact zero-Step maximum head, terminal one-Step/no-tail state, every host-feasible Opening tail count, all partial/full current tail fragments, committed-prefix cursors and the declared Running/Suspended head allocations. The four maximal-tail reference tuples remain ordinary `ContractStepsExceedOnIdleBudget` refusals rather than fabricated reachable maxima. Current-tree `cargo test -p pallet-deos-actors --all-features reachable_ -- --nocapture` passes all nine selected reachability controls. + +## Interpretation + +The reachable domain is the union of publicly constructed admitted profiles, not the Cartesian product of storage capacities. Tail chunk count follows authored Steps, a terminal one-Step head has no tail, and rejected dense reference tuples remain structurally legal but resource-inadmissible. Numeric transition ownership and exact artifact generation stay with EXP-0059, EXP-0064 and EXP-0065. + +## Decision + +Accepted for the admitted mock/reference host domain. Public lifecycle construction realizes zero-Step, Opening, Running and Suspended head/tail/cursor geometry at the declared boundaries; ordinary admission refusal excludes the four stale-binding maximal-tail points. This satisfies EXP-0029 O3 without reducing semantic bounds or accepting a production Weight envelope. + +## Rejected Alternatives and Tradeoffs + +Retain all rejected candidates and causal limits in the extracted evidence. Do not infer a new selection from record relocation or from aggregate coefficient subtraction. + +## Validity + +- `Establishes`: Publicly reachable zero-Step, head/tail chunk, committed-cursor and Run geometry for the declared admitted mock/reference domains, including explicit ordinary-admission exclusions. +- `Does not establish`: Complete production Weight, exact release artifact closure or integrated throughput. +- `Known limitations`: The four maximal-tail reference points are resource-inadmissible on the recorded binding; generated fragments are uninstalled and exact production Weight/artifact identity remains downstream. +- `Invalidation triggers`: A changed measured body, host domain, source/Weight/Wasm identity or acceptance rule requires scoped requalification. +- `Residual qualitative insight`: Follow the owning physical boundary, not a benchmark name. + +## Follow-Up / Next Gradient + +Stop when the declared proof is satisfied. Transfer any independent owner, selector, parameter domain or artifact-identity question before investigating it beyond the smallest classifier. A hypothetical corner case alone is not a measurement mandate. + +## Architecture Impact + +No runtime, semantic, storage or production binding change. C1 and A0 remain frozen; this record owns only its declared proof evidence. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: [EXP-0029](./EXP-0029.md) O3 +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: [EXP-0025](./EXP-0025.md) +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: None. +- `Transfers question to`: None; independent findings require an explicit new transfer. +- `Produces input for`: [EXP-0029](./EXP-0029.md) +- `Reopen trigger`: A falsifier of this declared owner/domain or exact artifact identity; cost alone does not reopen frozen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0049.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0049.md new file mode 100644 index 00000000..2877e10e --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0049.md @@ -0,0 +1,138 @@ +# EXP-0049 — Host Active Identity and Sovereign Population + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | [EXP-0029](./EXP-0029.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-06 | +| Affected domain | Additional System creation is bounded by the joint active, identity and retained-sovereign capacities of the named host history. | +| Physical mechanism | Host Active Identity and Sovereign Population | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0025](./EXP-0025.md) — Accepted, physical architecture only | +| Successor experiment | [EXP-0029](./EXP-0029.md) | + + +## Decision Question + +Additional System creation is bounded by the joint active, identity and retained-sovereign capacities of the named host history. + +## Governing Specification + +- `Owners`: `template/pallets/actors/docs/specification.en.md` (sole ownership, lifecycle/Trigger/Pipeline/Action semantics, placement and scheduler); `docs/actors-resource-policy.specification.en.md` (component-wise admission/actual settlement, equal-thirds block policy); `docs/actors-performance-assurance.specification.en.md` (evidence and target). +- `Frozen physical input`: [EXP-0025](./EXP-0025.md) C1 Architecture Freeze. No change to FIFO, ticket chronology, Q1, causal floor, primary/reference ontology, source/successor atomicity or sole execution owner. +- `Semantic oracle`: A0 SHA-256 `8746e423168309975e509be91e4eb06fdf0eb0b43369d75f1ee62202a4341678` remains frozen; use only for relevant semantic equivalence, never as a Weight oracle. + +## Context and Hypothesis + +This Leaf owns host active identity and sovereign population. Its boundary is the single claim above. Evidence was extracted from the explicitly marked pre-fission parent subsections at Git baseline `a60de7748132211d9e58cecd5be82b41f76ce230`; this migration changes navigation and evidence ownership only. Source, Wasm, commands, rejection outcomes and evidence limitations retain their original meaning. + +Extraction provenance at the recorded Git baseline: EXP-0029 / “Complete Waiting Population Qualification”. Original directional prose (above/below) belongs to these pre-fission subsections; current cross-owner navigation follows the parent proof map. + +## Baseline and Candidates + +Retained measured implementations and rejected candidates are identified in the extracted evidence. No candidate is introduced by this migration. + +## Controls + +Only the declared witness input varies within each source-bound comparison. Preserve the exact host, toolchain, database, Wasm and sampling controls printed with each evidence item; do not compare unlike artifacts as a controlled performance result. + +## Benchmark Class, Workloads, and Criteria + +Acceptance requires the single claim to hold over its declared admitted domain, with each physical operation charged once and every retained limitation resolved before broader production use. One legal omitted-owner, domain or containment counterexample falsifies that claim. Existing workloads below are bounded witnesses, not permission to add independent questions. + +## Environment and Artifact Identity + +Extraction baseline: `a60de7748132211d9e58cecd5be82b41f76ce230`. The migration manifest records exact original content/mode identities and source spans. Question-specific source/Wasm/output identities remain inline. Historical fragments remain historical; no new runtime or production artifact was built. + +Use the identified environment attached to each extracted source. Where a shared control is needed, follow its explicit evidence source; absent identity requires refresh before acceptance. + +## Proof Obligations + +- `Freeze`: Frozen by explicit migration reconstruction from the pre-fission decision and retained witnesses at a60de7748132211d9e58cecd5be82b41f76ce230. No new measurement or historical decision is implied. +- `Review triggers`: None. +- `Decomposition review`: Boundary reviewed during provenance-preserving fission. Workloads within this Leaf share the declared claim, physical owner, changed variable, evidence domain and acceptance rule. Independently decidable owners were transferred; a new owner or selector requires another node. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | Additional System creation is bounded by the joint active, identity and retained-sovereign capacities of the named host history. | One ordinary System creation beyond the minimum remaining active/identity/sovereign capacity with genesis identities retained | Reference-Wasm FRAME evidence in the declared owner/domain; native checks are construction controls | Self | Required | Named parent decision and declared downstream consumers | Accepted for the named retained-genesis mock/reference histories | + +## Measurements + + +### Complete Waiting Population Qualification + +The next prerequisite witness holds the existing one-Step `RetryMin` Opening constant and populates every remaining active slot using public System creation and Manual readiness into future authored windows. Compare an existing destination deadline with publication of a new minimum; every heap key must retain complete Actor, primary, pointer and directory ownership. The diagnostic executes one full scheduler cycle, so its samples cannot be interpreted as an isolated heap cost. EXP-0063 still owns competing branch coverage and numeric composition after the legal-domain prerequisite is qualified. + +The initial native attempt used `MaxActiveActors = 10,000` and failed `ActiveActorCapacityExceeded` while creating guards. Inspection identifies the missing host constraint: the mock's `MaxQueueLength = 1,024` limits `effective_active_actor_limit`, whereas the reference runtime configures both limits to 10,000. No limit or admission rule is changed. The corrected native pair reaches 1,024 active Actors through ordinary creation, with 1,023 existing keys versus a new 1,024th key, full pre/post TryRuntime audits and identical source/Run/control results. Six heap pages change for the new minimum; existing-key publication changes none. The direct corrected test passes in 36 seconds including compilation. This is native host reachability, not a 10,000-key or production Weight claim. + +The shared constructor now feeds the native pair and two extra diagnostics, `scheduler_opening_retry_full_waiting_existing` and `scheduler_opening_retry_full_waiting_new`. It derives remaining capacity from the actual active limit, checks complete ownership before execution, and retains host genesis state. Other identity/sovereign caps or pre-existing Block heap keys must fail explicitly rather than silently shrink the population. Qualify each reference-Wasm constructor first through `./scripts/benchmarks.sh --extra --steps 2 --repeat 1 --min-duration 0 --extrinsic NAME --output /tmp/deos-exp29-waiting-BRANCH.rs --json-file /tmp/deos-exp29-waiting-BRANCH.json pallet_deos_actors`, substituting each exact name and `existing`/`new`. Reuse one built Wasm with `--skip-build` for the second branch. These initial runs test constructive reachability; any later numeric comparison requires declared sampling, artifact identities and phase attribution. Benchmark source SHA-256 before host qualification: `9568397ec403be61193ef904f8cfce08e185df4ce1fb6ebad8ba57933c3c2012`. + +The initial reference-Wasm build passes in 106 seconds, source content/modes `64e750bf42942cd3e46e59662cdf47c54585dfa990089046ae2c351eeb827149`, Wasm SHA-256 `9fedc32a560df3c6ff9257ceb715a98f9d819c97965d427037fb5c3773e66fff`. Existing-key preparation then fails `ActorIdentityCapacityExceeded` in nine seconds, before measurement; log `/tmp/deos-generate-pallet-deos-actors-weights.zG1Est.log`, SHA-256 `dee0d68bf724be6fb4ca6191aa5c938ba9dc6c6151a1b8ce712c369f4968bc3e`. The new-key branch has the same creation prefix and cannot adjudicate this refusal differently, so it is not redundantly run on that source. The host's three active and twelve dormant genesis Actors explain why an active-only 10,000 target is not jointly available. Generic public creation independently enforces active, identity and System-sovereign capacity in `src/lib.rs::do_create_actor`; dormant identities and retained sovereign locators cannot be ignored. + +Correct the constructor to expose all three remaining capacities, add exactly their minimum number of ordinary System guards and assert that at least one named host limit is saturated before and after the cycle. It logs the actual rooms and guard count for artifact binding. This qualifies maximal additional System creation with the retained genesis and custody state; it does not prove a global active/key maximum after other lifecycle histories. No genesis Actor is removed, activated or rewritten, and no limit changes. Source SHA-256 `ea6f6400fe5669636893d6fe95e9aae63ce8319fb130fbec32306fbc3a084b63`; repeat the two declared host probes on its rebuilt Wasm. The canonical benchmark build enables `runtime-benchmarks`, not `try-runtime`: full TryRuntime audits are native evidence, while Wasm qualification uses public construction plus the explicit state/count/heap/post-Run assertions. + +Both corrected reference-Wasm probes pass: build 107 seconds, existing-key execution 24 seconds, new-key execution 23 seconds on reused Wasm. Source content/modes `b2fb95cb75bbf894b4f316748abea1a0891d4bfb16df0658f8b76679d50bf2be`, benchmark source as above, Wasm SHA-256 `2a3d74f0220bb6aff58a4e3652434f3cbbcc8bfb013a61329c3080b0d10693db`. The runtime diagnostic reports `active_room=9996, identity_room=9984, sovereign_room=9984, guards=9984`. Ordinary creation saturates identities and sovereigns at 10,000, retaining 9,988 active and twelve dormant Actors. The two post-Run Block heaps contain 9,984 and 9,985 distinct keys respectively. Existing-key publication leaves 312 heap pages; the new minimum creates page 313 and repairs ten pages. This closes this retained-genesis population witness, not every possible lifecycle history or full Waiting-page occupancy. + +Each declared probe retains exactly one timing sample and one separate database sample; no statistical RefTime ordering or numeric production bound follows. Complete normalized samples and temporary artifact identities are: + + +## Derived Metrics + +Derived values are retained beside the evidence that defines their source and units. No subtraction of overlapping aggregate owners or cross-artifact timing comparison is introduced. + +## Statistical / Variance Notes + +Retain each item’s original sampling and uncertainty. Singleton construction probes establish only their admitted state/access witness, never statistical timing dominance or a production bound. + +## Result + +Ordinary System creation saturates the minimum remaining active, identity and retained-sovereign room without deleting or rewriting genesis authority: 1,024 active Actors in the mock and 9,984 additional reference-runtime guards, leaving 9,988 active plus twelve dormant Actors. The initial active-only constructor is retained as a rejected fixture because it crossed identity/sovereign capacity first. Current-tree all-feature `waiting_` validation passes 37 tests, including `consumed_waiting_publication_extrema_use_complete_host_populations`. + +## Interpretation + +The constructive population bound is host-history-specific `min(active_room, identity_room, sovereign_room)`. Neither `MaxActiveActors` alone nor Waiting key count is a universal Actor-capacity claim, and retained dormant identities/sovereigns remain part of the bound. + +## Decision + +Accepted for the named retained-genesis mock/reference histories. Additional ordinary System creation is exactly bounded by the minimum remaining active, identity and retained-sovereign capacity; this satisfies EXP-0029 O4 without asserting a global maximum over different lifecycle histories. + +## Rejected Alternatives and Tradeoffs + +Retain all rejected candidates and causal limits in the extracted evidence. Do not infer a new selection from record relocation or from aggregate coefficient subtraction. + +## Validity + +- `Establishes`: The joint additional-System population bound for the named retained-genesis mock/reference histories and the ordinary-construction witnesses that saturate it. +- `Does not establish`: Complete production Weight, exact release artifact closure or integrated throughput. +- `Known limitations`: Different prior lifecycle histories can leave different room; this decision does not turn active capacity or Waiting key count into a universal population maximum. +- `Invalidation triggers`: A changed measured body, host domain, source/Weight/Wasm identity or acceptance rule requires scoped requalification. +- `Residual qualitative insight`: Follow the owning physical boundary, not a benchmark name. + +## Follow-Up / Next Gradient + +Stop when the declared proof is satisfied. Transfer any independent owner, selector, parameter domain or artifact-identity question before investigating it beyond the smallest classifier. A hypothetical corner case alone is not a measurement mandate. + +## Architecture Impact + +No runtime, semantic, storage or production binding change. C1 and A0 remain frozen; this record owns only its declared proof evidence. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: [EXP-0029](./EXP-0029.md) O4 +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: [EXP-0025](./EXP-0025.md) +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: None. +- `Transfers question to`: None; independent findings require an explicit new transfer. +- `Produces input for`: [EXP-0029](./EXP-0029.md), [EXP-0050](./EXP-0050.md) +- `Reopen trigger`: A falsifier of this declared owner/domain or exact artifact identity; cost alone does not reopen frozen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0050.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0050.md new file mode 100644 index 00000000..3d4d027e --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0050.md @@ -0,0 +1,168 @@ +# EXP-0050 — Legal Waiting Primary Population + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | [EXP-0029](./EXP-0029.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-06 | +| Affected domain | Ordinary lifecycle construction realizes the declared Waiting Primary publication and removal populations without injected scheduler state. | +| Physical mechanism | Legal Waiting Primary Population | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0025](./EXP-0025.md) — Accepted, physical architecture only | +| Successor experiment | [EXP-0029](./EXP-0029.md) | + + +## Decision Question + +Ordinary lifecycle construction realizes the declared Waiting Primary publication and removal populations without injected scheduler state. + +## Governing Specification + +- `Owners`: `template/pallets/actors/docs/specification.en.md` (sole ownership, lifecycle/Trigger/Pipeline/Action semantics, placement and scheduler); `docs/actors-resource-policy.specification.en.md` (component-wise admission/actual settlement, equal-thirds block policy); `docs/actors-performance-assurance.specification.en.md` (evidence and target). +- `Frozen physical input`: [EXP-0025](./EXP-0025.md) C1 Architecture Freeze. No change to FIFO, ticket chronology, Q1, causal floor, primary/reference ontology, source/successor atomicity or sole execution owner. +- `Semantic oracle`: A0 SHA-256 `8746e423168309975e509be91e4eb06fdf0eb0b43369d75f1ee62202a4341678` remains frozen; use only for relevant semantic equivalence, never as a Weight oracle. + +## Context and Hypothesis + +This Leaf owns legal waiting primary population. Its boundary is the single claim above. Evidence was extracted from the explicitly marked pre-fission parent subsections at Git baseline `a60de7748132211d9e58cecd5be82b41f76ce230`; this migration changes navigation and evidence ownership only. Source, Wasm, commands, rejection outcomes and evidence limitations retain their original meaning. + +Extraction provenance at the recorded Git baseline: EXP-0029 / “Complete Waiting Population Qualification”, “Saturated Upward and Tail Removal Qualification”. Original directional prose (above/below) belongs to these pre-fission subsections; current cross-owner navigation follows the parent proof map. + +## Baseline and Candidates + +Retained measured implementations and rejected candidates are identified in the extracted evidence. No candidate is introduced by this migration. + +## Controls + +Only the declared witness input varies within each source-bound comparison. Preserve the exact host, toolchain, database, Wasm and sampling controls printed with each evidence item; do not compare unlike artifacts as a controlled performance result. + +## Benchmark Class, Workloads, and Criteria + +Acceptance requires the single claim to hold over its declared admitted domain, with each physical operation charged once and every retained limitation resolved before broader production use. One legal omitted-owner, domain or containment counterexample falsifies that claim. Existing workloads below are bounded witnesses, not permission to add independent questions. + +## Environment and Artifact Identity + +Extraction baseline: `a60de7748132211d9e58cecd5be82b41f76ce230`. The migration manifest records exact original content/mode identities and source spans. Question-specific source/Wasm/output identities remain inline. Historical fragments remain historical; no new runtime or production artifact was built. + +Use the identified environment attached to each extracted source. Where a shared control is needed, follow its explicit evidence source; absent identity requires refresh before acceptance. + +## Proof Obligations + +- `Freeze`: Frozen by explicit migration reconstruction from the pre-fission decision and retained witnesses at a60de7748132211d9e58cecd5be82b41f76ce230. No new measurement or historical decision is implied. +- `Review triggers`: None. +- `Decomposition review`: Boundary reviewed during provenance-preserving fission. Workloads within this Leaf share the declared claim, physical owner, changed variable, evidence domain and acceptance rule. Independently decidable owners were transferred; a new owner or selector requires another node. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | Ordinary lifecycle construction realizes the declared Waiting Primary publication and removal populations without injected scheduler state. | One public retry publishing into a full Waiting Primary page or new minimum key under the declared saturated host population | Reference-Wasm FRAME evidence in the declared owner/domain; native checks are construction controls | Self | Required | Named parent decision and declared downstream consumers | Accepted for the declared ordinary-construction mock/reference populations | + +## Measurements + + +### Waiting Primary Publication Qualification + +| Branch | Time sample / storage-root ns | DB sample / storage-root ns | Unique / repeat reads | Unique / repeat writes | Recorded / estimated proof bytes | Generated model SHA-256 | Raw JSON SHA-256 | +| --- | --- | --- | --- | --- | --- | --- | --- | +| Existing key | 2,350,197 / 7,613 | 1,070,335 / 6,705 | 19 / 43 | 10 / 0 | 16,846 / 20,311 | `07bb36060fbd93ee7bfecb9793412165dcc0d1f1edeadf64770c05f2002868ea` | `b12a53c7aaa98320c7f9a59369445de9f6cfd1c687258fbad8e7b6a189d0013b` | +| New minimum | 1,534,298 / 8,521 | 1,813,458 / 8,102 | 56 / 59 | 36 / 0 | 46,277 / 81,917 | `2f91f3b8d424b6b260cfd62a4cfa013bb0cf6c43df24fe545e037584e442b35c` | `2429fb8730c1a8d3ac1385cfcd298706be2c98ebf62f61b6fb0ccbfaccde6944` | + +The new-key trace reads/writes ten `WakeupCursorPages`, fourteen reverse indices, and the heap length, while reading fourteen per-key occupancies to validate repair. Existing-key publication reads one reverse index and one occupancy, with no heap page/length access. The recorded-proof difference is 29,431 bytes and database work differs by 37 unique reads and 26 writes on the same identified Wasm; those are this complete cycle's observed branch differences, not an independently chargeable heap allowance. One existing-key timing sample exceeding the new-key sample underscores why the probes decide reachability and access topology rather than RefTime dominance. Native all-feature regression with the final shared constructor passes in 42 seconds; no production Weight is installed. + +The next occupancy falsifier, `scheduler_opening_retry_full_waiting_page`, shares the same saturated population but assigns 32 ordinary guards to the target deadline. Public readiness must fill its first Waiting page; the measured full cycle must allocate the second page, preserve reciprocal links and place the retry target at page one/slot zero. All other guards retain distinct future deadlines. The constructor now takes an explicit destination-entry count (`0`, `1`, `32`) and returns distinct key count; the earlier `0`/`1` construction and measured operations are semantically unchanged, so their qualified reachability evidence is reused without claiming a new numeric artifact binding. The native comparison covers all three cases and requires identical source, Run and control results. + +This adds full Primary-page occupancy, not maximum encoded control-cell content, mixed Primary/Reference pages, reference reclamation or a complete Waiting envelope. The native three-case comparison passes in 71 seconds including compilation. Bind benchmark SHA-256 `2231ee1c50d05ad4aca71866ad404262f88d469fdbecee1849b8e81b49a81d1a`, including explicit post-measurement byte equality of the original page entries and unchanged scan cursor. Qualify the new point using `./scripts/benchmarks.sh --extra --steps 2 --repeat 1 --min-duration 0 --extrinsic scheduler_opening_retry_full_waiting_page --output /tmp/deos-exp29-waiting-page.rs --json-file /tmp/deos-exp29-waiting-page.json pallet_deos_actors`. Keep the earlier two artifacts at their own Wasm identity, and do not infer RefTime ordering or isolated costs across these probes. + +The full-page Wasm probe passes: build 111 seconds, execution 23 seconds. Source content/modes `152a67f24b282b93e0cbf710c7a2fa5f483ef632f0e246f87fd81df093b9168a`, benchmark hash above, Wasm SHA-256 `281afd5fd952a1cdadbf37a88f292fafad83fa82632b23da43a385b117fb18cb`. The same 9,984 guards now own 9,953 keys because 32 share the target deadline. The target raises that deadline's occupancy to 33 on two linked pages without inserting a heap key. Verification preserves every encoded original page entry and its scan cursor, checks both end links and the target's new locator. The trace reads/writes two `ActorWaitingFrameChunks` and one occupancy/tail, reads one head/reverse index, and performs no heap-page/length access. This is the full-primary-page allocation witness under the declared ordinary guard Contract. + +| Full-page probe | Time sample / storage-root ns | DB sample / storage-root ns | Unique / repeat reads | Unique / repeat writes | Recorded / estimated proof bytes | Generated model SHA-256 | Raw JSON SHA-256 | +| --- | --- | --- | --- | --- | --- | --- | --- | +| Existing deadline, full first page | 910,466 / 7,683 | 1,134,240 / 7,193 | 20 / 43 | 11 / 0 | 27,125 / 33,065 | `c94c7cb3cda807d0e0f805db393f0b6570365d651e5cbdea0d14cbee86a30ec5` | `080ae96ffc9db413e23d9a9c74cf31b114cf6d0ac94f1c963cce711f78f31b63` | + +One timing and one database sample are retained; the generated fragment stays uninstalled. Its different Wasm and key population prohibit an isolated timing/proof subtraction against the earlier pair. Production coverage still requires maximal admitted control-cell content, mixed Primary/Reference pages, competing removal/reclamation and Tick branches, and EXP-0028 / EXP-0063 ownership reconciliation. + + + +### Saturated Upward and Tail Removal Qualification + +The existing native upward-removal witness supplies the next legal-state construction: create every remaining System Actor through ordinary admission and Manual readiness into future windows. Assign unique Block deadlines in heap-index order, with one low-valued root subtree and one high-valued opposite subtree; no heap insertion needs a swap. Select the subtree containing the last key as low, then remove a deep opposite-subtree key. The low tail must move to the opposite subtree root, shifting only its ancestor path down. A second public-close probe removes the heap tail itself, requiring no repair. Both preparations retain genesis and fill the same active/identity/sovereign capacity intersection. + +Share only enough Actors at the first deadline to leave exactly one key on the final heap page. Both closes must remove that page, the target's sole Primary Waiting page and its directory/key, preserve every expected surviving heap entry, and pass native full-state plus Wasm explicit index/order checks. The target is an Idle System Actor with a pending Manual latch and an authored window; this differs from the Running Tick-reference cleanup above and does not supply User hold, maximal-cell or isolated heap-cost evidence. + +Ordinary admission rejects the old witness's large numerical deadlines with `ExecutionDelayTooLong`: the mock horizon is 5,000 blocks, whereas its direct wakeup placement did not install those deadlines as authored windows. Preserve the same heap order using root 10, low-subtree `K + 10 + index` and high-subtree `2K + 10 + index`, where `K` is the distinct key count; require `3K + 10` to fit the host horizon. This corrects fixture reachability without changing temporal bounds, population, heap branches or protocol semantics. + +Qualification uses one freshly built reference Wasm for `close_actor_full_waiting_upward` and `close_actor_full_waiting_tail`: `DEOS_VERBOSE=1 ./scripts/benchmarks.sh --extra --steps 2 --repeat 1 --min-duration 0 --extrinsic NAME --output /tmp/deos-exp29-fullheap-BRANCH.rs --json-file /tmp/deos-exp29-fullheap-BRANCH.json pallet_deos_actors`, substituting the exact name and `upward`/`tail`, with `--skip-build` for the second. Retain verbose logs for population assertions. One timing and one database sample per branch decide constructive reachability and access coverage, not statistical RefTime ordering or a production envelope; desktop background activity remains uncontrolled and no own heavy work overlaps sampling. + +The corrected all-feature native `full_waiting_heap_close_covers_upward_and_tail_removal` passes both branches in 43 seconds including compilation. It admits 1,024 ordinary windowed Actors, with 993 distinct keys and 32 Actors sharing the root deadline; either close removes the one-entry 32nd heap page, leaving 992 keys. Prepared source content/modes `c4d04a84da191699ec761aa7765955581e2ccc9be4a032cafad44dcbfbc67efb`, benchmark SHA-256 `1eaf096801b18de020a3e3e8422058f8cd251e2f7d2969ce31d73f50e2d76190`. + +Both reference probes pass on Wasm `0d907492eb8c9c924269cb52e73eed4ae80a22a6e04b7c205981a5a099286b98`: build 104 seconds, each execution 24 seconds. Each of the three preparation passes logs 9,985 guards and 9,985 keys; upward removal starts at index 8,190, tail-only at 9,984. Both leave 9,984 keys and reclaim heap page 313. The upward tail moves to index two through eleven swaps; the tail-only profile preserves every other key's position. Exact surviving pages, directories and inverse indices satisfy the postconditions. Verbose log SHA-256: upward `3d8bf1bd4c183bf8975a334169481b70c6d2cb1adf7a575d9c0897fab12a13ed`, tail `da7ba525446c915fecf118bf00d1b9f22081e3d87acffff25901961023e04160`. + +| Branch | Time sample / storage-root ns | DB sample / storage-root ns | Unique / repeat reads | Unique / repeat writes | Recorded / estimated proof bytes | Generated model SHA-256 | Raw JSON SHA-256 | +| --- | --- | --- | --- | --- | --- | --- | --- | +| Upward repair and tail-page reclamation | 1,457,471 / 7,613 | 1,730,625 / 8,101 | 55 / 69 | 39 / 0 | 42,947 / 76,112 | `90c79138e75e3e547c3e06400ab9208eb460ca404f970d43024c3d8f638cfccf` | `7bf8844b4ada69b196d4205a13097b767757e139ccb92da08372fdd8dd22ffdc` | +| Tail-only reclamation | 673,909 / 8,032 | 888,744 / 7,194 | 22 / 43 | 18 / 0 | 17,565 / 21,030 | `f5515d4532f461d4b17bed01ff1f481efaa74f614793bdda0bd2e1eeb4f37718` | `872cf5750cc84754841185d305a8cfad9e0363ab4df542c0c7c3ec68e6ec4874` | + +The upward trace reads/writes ten heap pages and 13 reverse indices, with 13 occupancy reads; tail-only reads/writes one of each. Both delete one Primary Waiting page and its occupancy/head/tail/key and include public-close Contract/lifecycle work. These same-Wasm branch observations qualify complete legal population and the chosen repair histories. One sample cannot establish RefTime dominance or a maximum over every index/page layout; no fragment is installed or combined additively with other close work. Together with the prior Tick-root witness they provide complete-state upward/downward/tail prerequisites later consumed by Accepted EXP-0063's numeric envelope and page/clock/phase reconciliation. + +Canonical formatting, workspace/all-target/all-feature Clippy (27 seconds), 28-record normalization/dependency validation and diff checks pass on the prepared benchmark digest. Existing unrelated benchmark preflight binding failures remain G5 work; the fresh full Wasm build verifies this changed diagnostic compilation without repairing generated storage annotations manually. + + +## Derived Metrics + +Derived values are retained beside the evidence that defines their source and units. No subtraction of overlapping aggregate owners or cross-artifact timing comparison is introduced. + +## Statistical / Variance Notes + +Retain each item’s original sampling and uncertainty. Singleton construction probes establish only their admitted state/access witness, never statistical timing dominance or a production bound. + +## Result + +Ordinary creation, readiness, retry and public close construct existing-key, new-minimum, full-first-page/second-page, upward-repair and tail-only Primary histories at the declared saturated host populations. Every witness preserves reciprocal pages, locators, directories, heap order and surviving entries; no scheduler state is injected. Current-tree all-feature `waiting_` validation passes all 37 selected tests. + +## Interpretation + +These witnesses close legal Primary population reachability and removal topology. Their complete-cycle timings are not isolated heap coefficients, and maximal encoded-cell publication plus numeric branch composition remain EXP-0055/EXP-0056/EXP-0063 concerns. + +## Decision + +Accepted for the declared ordinary-construction mock/reference populations. Waiting Primary publication reaches existing, new-minimum and full-page destinations, while public close reaches upward-repair and tail-only reclamation histories. This satisfies EXP-0029 O5 without installing diagnostic fragments or claiming a production envelope. + +## Rejected Alternatives and Tradeoffs + +Retain all rejected candidates and causal limits in the extracted evidence. Do not infer a new selection from record relocation or from aggregate coefficient subtraction. + +## Validity + +- `Establishes`: Legal Waiting Primary publication and removal populations for the declared saturated mock/reference histories, constructed through ordinary lifecycle operations. +- `Does not establish`: Complete production Weight, exact release artifact closure or integrated throughput. +- `Known limitations`: Maximal encoded control-cell content, mixed Reference pages and numeric production composition remain owned by EXP-0055, EXP-0056 and EXP-0063. +- `Invalidation triggers`: A changed measured body, host domain, source/Weight/Wasm identity or acceptance rule requires scoped requalification. +- `Residual qualitative insight`: Follow the owning physical boundary, not a benchmark name. + +## Follow-Up / Next Gradient + +Stop when the declared proof is satisfied. Transfer any independent owner, selector, parameter domain or artifact-identity question before investigating it beyond the smallest classifier. A hypothetical corner case alone is not a measurement mandate. + +## Architecture Impact + +No runtime, semantic, storage or production binding change. C1 and A0 remain frozen; this record owns only its declared proof evidence. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: [EXP-0029](./EXP-0029.md) O5 +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: [EXP-0025](./EXP-0025.md), [EXP-0049](./EXP-0049.md) +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: [EXP-0049](./EXP-0049.md) +- `Transfers question to`: None; independent findings require an explicit new transfer. +- `Produces input for`: [EXP-0029](./EXP-0029.md), [EXP-0055](./EXP-0055.md), [EXP-0056](./EXP-0056.md) +- `Reopen trigger`: A falsifier of this declared owner/domain or exact artifact identity; cost alone does not reopen frozen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0051.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0051.md new file mode 100644 index 00000000..7a5fa1ca --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0051.md @@ -0,0 +1,159 @@ +# EXP-0051 — Legal Waiting Reference Population + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | [EXP-0029](./EXP-0029.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-06 | +| Affected domain | Ordinary lifecycle construction realizes the declared mixed and final-Tick Waiting Reference populations without injected reference state. | +| Physical mechanism | Legal Waiting Reference Population | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0025](./EXP-0025.md) — Accepted, physical architecture only | +| Successor experiment | [EXP-0029](./EXP-0029.md) | + + +## Decision Question + +Ordinary lifecycle construction realizes the declared mixed and final-Tick Waiting Reference populations without injected reference state. + +## Governing Specification + +- `Owners`: `template/pallets/actors/docs/specification.en.md` (sole ownership, lifecycle/Trigger/Pipeline/Action semantics, placement and scheduler); `docs/actors-resource-policy.specification.en.md` (component-wise admission/actual settlement, equal-thirds block policy); `docs/actors-performance-assurance.specification.en.md` (evidence and target). +- `Frozen physical input`: [EXP-0025](./EXP-0025.md) C1 Architecture Freeze. No change to FIFO, ticket chronology, Q1, causal floor, primary/reference ontology, source/successor atomicity or sole execution owner. +- `Semantic oracle`: A0 SHA-256 `8746e423168309975e509be91e4eb06fdf0eb0b43369d75f1ee62202a4341678` remains frozen; use only for relevant semantic equivalence, never as a Weight oracle. + +## Context and Hypothesis + +This Leaf owns legal waiting reference population. Its boundary is the single claim above. Evidence was extracted from the explicitly marked pre-fission parent subsections at Git baseline `a60de7748132211d9e58cecd5be82b41f76ce230`; this migration changes navigation and evidence ownership only. Source, Wasm, commands, rejection outcomes and evidence limitations retain their original meaning. + +Extraction provenance at the recorded Git baseline: EXP-0029 / “Mixed Waiting Reference Cleanup Qualification”, “Final Tick Reference and Saturated Heap Qualification”. Original directional prose (above/below) belongs to these pre-fission subsections; current cross-owner navigation follows the parent proof map. + +## Baseline and Candidates + +Retained measured implementations and rejected candidates are identified in the extracted evidence. No candidate is introduced by this migration. + +## Controls + +Only the declared witness input varies within each source-bound comparison. Preserve the exact host, toolchain, database, Wasm and sampling controls printed with each evidence item; do not compare unlike artifacts as a controlled performance result. + +## Benchmark Class, Workloads, and Criteria + +Acceptance requires the single claim to hold over its declared admitted domain, with each physical operation charged once and every retained limitation resolved before broader production use. One legal omitted-owner, domain or containment counterexample falsifies that claim. Existing workloads below are bounded witnesses, not permission to add independent questions. + +## Environment and Artifact Identity + +Extraction baseline: `a60de7748132211d9e58cecd5be82b41f76ce230`. The migration manifest records exact original content/mode identities and source spans. Question-specific source/Wasm/output identities remain inline. Historical fragments remain historical; no new runtime or production artifact was built. + +Use the identified environment attached to each extracted source. Where a shared control is needed, follow its explicit evidence source; absent identity requires refresh before acceptance. + +## Proof Obligations + +- `Freeze`: Frozen by explicit migration reconstruction from the pre-fission decision and retained witnesses at a60de7748132211d9e58cecd5be82b41f76ce230. No new measurement or historical decision is implied. +- `Review triggers`: None. +- `Decomposition review`: Boundary reviewed during provenance-preserving fission. Workloads within this Leaf share the declared claim, physical owner, changed variable, evidence domain and acceptance rule. Independently decidable owners were transferred; a new owner or selector requires another node. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | Ordinary lifecycle construction realizes the declared mixed and final-Tick Waiting Reference populations without injected reference state. | One ordinarily constructed cleanup removing the final Tick reference or reclaiming a mixed Reference page | Reference-Wasm FRAME evidence in the declared owner/domain; native checks are construction controls | Self | Required | Named parent decision and declared downstream consumers | Accepted for the declared mixed-page and saturated final-Tick mock/reference histories | + +## Measurements + + +### Mixed Waiting Reference Cleanup Qualification + +The next legal-state prerequisite carries the existing native `mixed_waiting_pages_reclaim_both_entry_kinds_through_real_close` construction across the host clock boundary. Create 32 ordinary Cadenced primary guards, then one two-Step System Actor whose Current `BlockNumberBelow(0)` predicate skips Step zero. Advance through the host's clock helper, bounded due worker and ordinary cycle to retain a real Running suffix and rearmed Tick reference. Append 63 further Cadenced primary guards at that same deadline. The three linked pages are full; the middle begins with the target reference and continues with 31 primary guards. No Contract, Run, pointer or page is injected or rewritten after admission. + +Compare public Root `close_actor` while the mixed page is full against closing its 31 primary guards first, then removing the target as its last remaining entry. The latter must unlink the middle page between two full neighbors. Both cases retain the same target Run across preparation, preserve the neighbors' encoded entries, remove the target's complete process state and keep positive native sovereign custody unchanged. Full native TryRuntime audits own pre/post bijection; Wasm checks use the constructive transitions and explicit state assertions. The fixture has 96 Actors before optional cleanup, plus retained host genesis; it does not claim saturated global population, maximal encoded cells, User hold/fee coverage, final-key removal or a complete close/Waiting envelope. + +Native all-feature `mixed_waiting_reference_close_uses_real_opening` passes both branches in 36 seconds including compilation. Prepared source content/modes `856d8ac72b307ff24c8b3d713ec010a7fbc2440d1a8ab4a51e2a3588779ea1c9`, benchmark SHA-256 `8c45cf9cbe2882c5cdc95ce2ec9eb495d79ae050fa713873527b74d5d2e6deed`. The extra diagnostics are `close_actor_mixed_waiting_reference` and `close_actor_mixed_waiting_unlink`; they measure only the final public close, including its already-owned Run, Ready-source, contract and lifecycle work. Qualify each with `./scripts/benchmarks.sh --extra --steps 2 --repeat 1 --min-duration 0 --extrinsic NAME --output /tmp/deos-exp29-mixed-BRANCH.rs --json-file /tmp/deos-exp29-mixed-BRANCH.json pallet_deos_actors`, substituting the exact name and `reference`/`unlink`, reusing one built Wasm with `--skip-build` for the second. One time and one database sample decide constructive shape/access coverage; statistical RefTime and production-envelope decisions require later declared sampling and owner reconciliation. + +Both reference-Wasm probes pass on SHA-256 `081df3d8fae3f8ba8351a9058976e0c47df70b45cfa30edc5294d72c155a0da9`: build 107 seconds, each execution four seconds. The current host clock admits the same real two-Step Running suffix, rearmed Tick reference and three-page geometry. The full-page branch changes one Waiting page and leaves 95 primary entries at the deadline; the last-reference branch deletes the middle page, links its two full neighbors and leaves 64 primary entries. Native custody remains positive and unchanged, and the target's Run, Ready locator, Contract and funding state are absent after close. + +| Branch | Time sample / storage-root ns | DB sample / storage-root ns | Unique / repeat reads | Unique / repeat writes | Recorded / estimated proof bytes | Generated model SHA-256 | Raw JSON SHA-256 | +| --- | --- | --- | --- | --- | --- | --- | --- | +| Reference in full mixed page | 493,227 / 4,400 | 450,414 / 4,749 | 23 / 36 | 16 / 0 | 15,200 / 18,665 | `433fcbabf747fd9b9444e54c77c3182c6930e5ef6adf0b8a36393a161f2e1256` | `d26e38bb23b93786a646eb28663cc1bbdb186ad3136ff8f879f7a25643252203` | +| Last reference, middle unlink | 437,563 / 6,216 | 457,608 / 4,749 | 25 / 36 | 18 / 0 | 25,149 / 33,564 | `3b21ceb0f6eb0c686fe5d17439b8f213ce6a0b2581441df252c251bf62cfb019` | `328b3e8b42773a9d98d79c0240f517a05d5176e73fd2ec981ac6de2a7c22060d` | + +Each probe retains one timing and one database sample. Both traces include Ready-frame/occupancy removal, Run head/payload removal and the Waiting directory; the unlink branch reads/writes three Waiting frames instead of one. Neither touches heap pages/length because the deadline survives. The different preparation histories also change retained population, so these complete public-close observations are not isolated unlink coefficients. Their Run, source, Contract and lifecycle work must not be charged again through a second Waiting allowance. Generated fragments remain uninstalled; statistical RefTime, maximal cells, User accounting, final-key and other heap branches remain outside this witness. A subsequent help-text correction describes `--extra` as all diagnostic benchmarks; execution and benchmark source are unchanged. + +### Final Tick Reference and Saturated Heap Qualification + +Extend the mixed-reference constructor through ordinary lifecycle history: close its remaining 64 primary guards, preserving the target's real Running suffix and sole Tick reference. Reuse all 95 known retired guard sovereigns through `create_system_actor_at_sovereign_id`, then create fresh Cadenced guards until the active/identity/sovereign capacity intersection is exhausted. Retain host genesis and custody. Group the minimum number of future deadlines needed to place exactly one key on the final heap page; the target remains the minimum. Public target close must delete its final Waiting page/directories/key, repair the heap downward, reclaim the heap tail page and preserve positive sovereign custody. + +This prerequisite tests a complete legally constructed Tick heap with slot reuse, not the global maximum over all lifecycle histories or maximal encoded control cells. Native TryRuntime owns full pre/post state validation; explicit Tick forward/reverse, parent-order, cardinality, Run and cleanup checks also execute in Wasm. The prior index helper checked Block only; its clock-specific implementation now allows the mixed-reference diagnostics to check Tick without changing their measured close bodies or original artifact bindings. + +Qualification command: `./scripts/benchmarks.sh --extra --steps 2 --repeat 1 --min-duration 0 --extrinsic close_actor_mixed_waiting_deadline --output /tmp/deos-exp29-mixed-deadline.rs --json-file /tmp/deos-exp29-mixed-deadline.json pallet_deos_actors`. One time and one database sample are sufficient for constructive/access qualification only. Keep the existing host/toolchain, compiled Wasm and heap/cache controls; do not overlap heavy work. The measured public close includes Run, Ready-source, Contract and lifecycle cleanup, so no independent additive heap coefficient or statistical RefTime dominance follows. No generated fragment is installed. + +Native all-feature `mixed_waiting_` tests pass in 75 seconds, including the new saturated heap and existing mixed-reference branches. The mock reuses 95 sovereigns and fills 1,023 background Actors; 993 Tick keys become 992 and the 32nd heap page is reclaimed. Prepared source content/modes `4e84cad96fce654264d38e9483479dfa6feb224feaa8c19353bc02f05b168555`, benchmark SHA-256 `9629cd9dd7cab7ef87cdf691bb38b2ba0e8c267deba1794e5bef85a525bf7c1d`. Background desktop/user processes were present; this diagnostic does not control host timing sufficiently for a RefTime comparison or envelope. + +Reference Wasm `bd80b7eb860766630fa46e269b7e95f8a4d8813f0e80f5880c26c5c4f10b6e58` passes: build 110 seconds, execution 16 seconds. Public creation, sovereign reuse, saturated capacity, retained Run, root-key removal, final-page reclamation, Tick index/order and custody postconditions all hold. Retained reference genesis and the constructor's capacity intersection imply 9,984 background guards, including 95 reused sovereigns; this is not a new global bound over all possible lifecycle histories. The successful compact log is ephemeral; exact source, command, Wasm and normalized sample/artifact bindings below preserve the evidence scope. + +| Branch | Time sample / storage-root ns | DB sample / storage-root ns | Unique / repeat reads | Unique / repeat writes | Recorded / estimated proof bytes | Generated model SHA-256 | Raw JSON SHA-256 | +| --- | --- | --- | --- | --- | --- | --- | --- | +| Last Tick reference, root repair and heap-tail reclamation | 1,833,363 / 27,588 | 3,494,006 / 7,473 | 70 / 80 | 44 / 0 | 51,201 / 96,741 | `0d409cd13ea33e1f363352f42ed36a132589917c49d78826274d754046130457` | `4d01cac25a7f4ea5607a8ce58589a3ab05c11d0155d65001aa184b79fba38f53` | + +The single database trace reads 18 heap pages and writes ten, reads/writes 15 reverse indices, reads 15 Waiting occupancies and deletes the target occupancy/head/tail and sole Waiting page. It includes heap length, Ready-source, Run head/payload, Contract and lifecycle work. `scheduler.rs::wakeup_cursor_remove_inner` starts at index zero, so its upward loop exits immediately and the populated repair follows the downward branch. This qualifies a complete legal final-key/reclamation path; it neither isolates its heap cost by subtraction from older Wasm probes nor decides competing upward/tail-only, User/maximal-cell or complete production Weight envelopes. + +Canonical formatting, workspace/all-target/all-feature Clippy (27 seconds), 28-record normalization/dependency validation, diff checks and changed-scope completion (three seconds) pass. The benchmark source matches the prepared digest. Production code, Weight and metadata are unchanged; existing unrelated release gates remain in BACKLOG. + + +## Derived Metrics + +Derived values are retained beside the evidence that defines their source and units. No subtraction of overlapping aggregate owners or cross-artifact timing comparison is introduced. + +## Statistical / Variance Notes + +Retain each item’s original sampling and uncertainty. Singleton construction probes establish only their admitted state/access witness, never statistical timing dominance or a production bound. + +## Result + +Ordinary Cadenced creation, Opening, bounded due work, sovereign reuse and public close construct a full mixed Primary/Reference page, a last-entry middle-page unlink, and a sole final Tick reference at the root of a saturated heap. The final close repairs downward and reclaims the heap-tail page while preserving custody and every surviving owner. Current-tree all-feature `waiting_` validation passes all 37 selected tests, including both mixed-reference and saturated-deadline controls. + +## Interpretation + +The declared mixed and final-Tick Reference populations are lifecycle-reachable without direct Run, pointer, cursor or page injection. Complete close observations overlap Run, Ready, Contract and heap owners and therefore remain indivisible reachability evidence rather than additive cost components. + +## Decision + +Accepted for the declared mixed-page and saturated final-Tick mock/reference histories. Ordinary lifecycle construction and public close realize mixed Reference removal, middle unlink and final-Tick root/downward-repair reclamation. This satisfies EXP-0029 O6; EXP-0056/EXP-0063 retain numeric envelope ownership. + +## Rejected Alternatives and Tradeoffs + +Retain all rejected candidates and causal limits in the extracted evidence. Do not infer a new selection from record relocation or from aggregate coefficient subtraction. + +## Validity + +- `Establishes`: Lifecycle reachability of the declared mixed Waiting Reference and sole final-Tick saturated-heap cleanup histories. +- `Does not establish`: Complete production Weight, exact release artifact closure or integrated throughput. +- `Known limitations`: These complete close traces do not isolate additive page/heap costs or establish maximal encoded-cell and User-hold combinations. +- `Invalidation triggers`: A changed measured body, host domain, source/Weight/Wasm identity or acceptance rule requires scoped requalification. +- `Residual qualitative insight`: Follow the owning physical boundary, not a benchmark name. + +## Follow-Up / Next Gradient + +Stop when the declared proof is satisfied. Transfer any independent owner, selector, parameter domain or artifact-identity question before investigating it beyond the smallest classifier. A hypothetical corner case alone is not a measurement mandate. + +## Architecture Impact + +No runtime, semantic, storage or production binding change. C1 and A0 remain frozen; this record owns only its declared proof evidence. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: [EXP-0029](./EXP-0029.md) O6 +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: [EXP-0025](./EXP-0025.md) +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: None. +- `Transfers question to`: None; independent findings require an explicit new transfer. +- `Produces input for`: [EXP-0029](./EXP-0029.md), [EXP-0055](./EXP-0055.md), [EXP-0056](./EXP-0056.md) +- `Reopen trigger`: A falsifier of this declared owner/domain or exact artifact identity; cost alone does not reopen frozen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0052.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0052.md new file mode 100644 index 00000000..6da8a186 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0052.md @@ -0,0 +1,238 @@ +# EXP-0052 — Joint Suspended and User Close Geometry + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | [EXP-0029](./EXP-0029.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-06 | +| Affected domain | Ordinary admission, funding and actual suspension jointly realize the declared User close capture/detector/Run geometry. | +| Physical mechanism | Joint Suspended and User Close Geometry | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0025](./EXP-0025.md) — Accepted, physical architecture only | +| Successor experiment | [EXP-0029](./EXP-0029.md) | + + +## Decision Question + +Ordinary admission, funding and actual suspension jointly realize the declared User close capture/detector/Run geometry. + +## Governing Specification + +- `Owners`: `template/pallets/actors/docs/specification.en.md` (sole ownership, lifecycle/Trigger/Pipeline/Action semantics, placement and scheduler); `docs/actors-resource-policy.specification.en.md` (component-wise admission/actual settlement, equal-thirds block policy); `docs/actors-performance-assurance.specification.en.md` (evidence and target). +- `Frozen physical input`: [EXP-0025](./EXP-0025.md) C1 Architecture Freeze. No change to FIFO, ticket chronology, Q1, causal floor, primary/reference ontology, source/successor atomicity or sole execution owner. +- `Semantic oracle`: A0 SHA-256 `8746e423168309975e509be91e4eb06fdf0eb0b43369d75f1ee62202a4341678` remains frozen; use only for relevant semantic equivalence, never as a Weight oracle. + +## Context and Hypothesis + +This Leaf owns joint suspended and user close geometry. Its boundary is the single claim above. Evidence was extracted from the explicitly marked pre-fission parent subsections at Git baseline `a60de7748132211d9e58cecd5be82b41f76ce230`; this migration changes navigation and evidence ownership only. Source, Wasm, commands, rejection outcomes and evidence limitations retain their original meaning. + +Extraction provenance at the recorded Git baseline: EXP-0029 / “Joint Retry and Crossing Cleanup Qualification”, “Derived Metrics”. Original directional prose (above/below) belongs to these pre-fission subsections; current cross-owner navigation follows the parent proof map. + +## Baseline and Candidates + +Retained measured implementations and rejected candidates are identified in the extracted evidence. No candidate is introduced by this migration. + +## Controls + +Only the declared witness input varies within each source-bound comparison. Preserve the exact host, toolchain, database, Wasm and sampling controls printed with each evidence item; do not compare unlike artifacts as a controlled performance result. + +## Benchmark Class, Workloads, and Criteria + +Acceptance requires the single claim to hold over its declared admitted domain, with each physical operation charged once and every retained limitation resolved before broader production use. One legal omitted-owner, domain or containment counterexample falsifies that claim. Existing workloads below are bounded witnesses, not permission to add independent questions. + +## Environment and Artifact Identity + +Extraction baseline: `a60de7748132211d9e58cecd5be82b41f76ce230`. The migration manifest records exact original content/mode identities and source spans. Question-specific source/Wasm/output identities remain inline. Historical fragments remain historical; no new runtime or production artifact was built. + +Use the identified environment attached to each extracted source. Where a shared control is needed, follow its explicit evidence source; absent identity requires refresh before acceptance. + +## Proof Obligations + +- `Freeze`: Frozen by explicit migration reconstruction from the pre-fission decision and retained witnesses at a60de7748132211d9e58cecd5be82b41f76ce230. No new measurement or historical decision is implied. +- `Review triggers`: None. +- `Decomposition review`: Boundary reviewed during provenance-preserving fission. Workloads within this Leaf share the declared claim, physical owner, changed variable, evidence domain and acceptance rule. Independently decidable owners were transferred; a new owner or selector requires another node. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | Ordinary admission, funding and actual suspension jointly realize the declared User close capture/detector/Run geometry. | One admitted capture allocation that fails real suspension/public close, or whose retained detector/Run geometry differs from its certificate | Reference-Wasm FRAME evidence in the declared owner/domain; native checks are construction controls | Self | Required | Named parent decision and declared downstream consumers | Accepted for the observed admitted reference subdomain; adjacent refused tuples remain excluded | + +## Measurements + + +### Joint Retry and Crossing Cleanup Qualification + +The next falsifier extends the existing `close_actor_opening_partition(o, p)` diagnostic from Manual suspension to a Crossing-triggered suspension with a retained deferred latch and a live detector cursor. Keep the same maximum-length authored Contract and independent capture axes; produce observations through the host publisher and the existing bounded worker. Append ordinary guards only after the Run exists, then test cursor repair, Waiting/Run removal, owner holds and custody neutrality through public close. The existing Manual measurements remain historical inputs, not a bound for this joint state. Start with the previously admitted small capture rectangle; a passing point must not replace the general close Weight or qualify the full 12/24/48 envelope. No admission bypass, Contract mutation or direct cursor insertion is allowed. + +Preparation corrects the shared Crossing retry constructor to publish through the host and retain monotonic block time. The general `close_actor` fixture also replaces its inserted cursor with a real, not-yet-serviced queued transition; cursor repair belongs to the explicit diagnostics. Native close-family checks cover those changed siblings. The joint profile uses Rising threshold four/rearm one, a real initial failed liquidity attempt, then publications one/four to latch a second occurrence without changing Run bytes. Another publication of one lets the bounded worker skip the disabled target and rearm ordinary guards from two full source pages. Public close must rewind the live cursor and the next worker must visit the moved guard. + +Require `run.eligible_at > current block` immediately before measurement. A preparation that advanced time past the retry deadline was rejected before sampling; its build was deliberately terminated. The final constructor tests future Waiting, avoiding an overdue primary that a normal wakeup pass would already consume. Source content/modes `f0bcb81f8aa6503afd0d5ac875a21cfdd5b2dc23a17b638f2adc2818ed5102e0`, benchmark SHA-256 `1fc65b1346f4a531e7f47eeb67a80ca2867fd05b6ff52e40ee80157050c3fc2e`, script SHA-256 `b7c4392bb6e4a41e5714bfa7bf22f386756e06e4f59e301c8168cc2bdf116e22`; production Weight remains `2d9c60ac7764d1e7dc366531dd4a409444daf66cf3e37ed7feb922bb614e8f1a`. + +Command: `./scripts/benchmarks.sh --low 0,0 --high 6,12 --extrinsic close_actor_opening_partition --output /tmp/deos-exp29-joint-crossing-close.rs --json-file /tmp/deos-exp29-joint-crossing-close.json pallet_deos_actors`. The new paired component-bound options forward an explicitly selected rectangle to the bencher through the ordinary build/output route, requiring exact selection and explicit output. Keep the standard 50 × 20, compiled Wasm, heap 4096, database/cache and hardware controls; run no concurrent heavy work during sampling. + +The first Wasm falsifier rejects preparation before measurement: publication of sample three produces current value zero, not three. Source above, Wasm `d04c8a9c961e611a9f44c9470f39692921a335bdbb6d8e5845d60743bda6d607`, log `/tmp/deos-generate-pallet-deos-actors-weights.YKf12N.log` SHA-256 `d61bfa4af621d8b671b64a5eeba4bdc0a09999e5406e6a5bc37be2c51e86c329`; build 101 seconds, refusal three seconds, no model or samples. This is a fixture defect: the reference Oracle uses EMA with a 100-block half-life and floors both weighted terms separately. The mock's direct-value publication cannot qualify that host behavior. + +Correction reuses the existing zero-Step publication strategy: sample 1,000 crosses threshold four, and at most the host's per-block transition bound of monotonically decreasing publications reaches the rearm boundary. Bounded worker passes then drain earlier transitions until they reach the repairable source cursor; no pending queue or cursor is injected, and the future retry deadline remains an asserted prerequisite. Native close-family checks pass after this correction. The shared retry constructor now checks that the actual published value crosses its authored threshold; older near-`u128::MAX` Crossing fixtures retain their separate host-domain qualification obligation instead of gaining an implicit direct-value Oracle assumption. + +Corrected source content/modes `3422b307de3279d68783ad0a4e769f05f280ec4903a8740a81261ee0982c6439`, benchmark SHA-256 `89bd0c18938a7a51cbfc2e4e36638cf9015f99cb8bb1ad314d525221c2dfb304`; script, production Weight and the selected rectangle are unchanged. + +The corrected Wasm is `227d4e1f00a8b94c1d27d95e1e46329c1b685bcf4395fef524bbd8b01358a914`. Its build takes 100 seconds. The initial 0..6 / 0..12 run reaches real cleanup, then fails later creation with `ContractStepsExceedOnIdleBudget` after 113 seconds; no partial model or raw artifact is exported. Retain log `/tmp/deos-generate-pallet-deos-actors-weights.FCWLAu.log`, SHA-256 `2d2e511e4edaf0e603e7c93d31eeccf2e693f007d870867a60ad0b6acded50ff`. + +A four-point `--steps 2 --repeat 1 --min-duration 0 --low 0,0 --high 4,12` probe passes through creation, real suspension, detector history and verified close at `(0,12)`, `(4,12)`, `(4,0)` and repeated `(4,12)`. A separate exact `(5,12)` probe rejects creation; this is an adjacent boundary at fixed P=12, not a global Opening limit. Exact `(0,0)` reaches verified cleanup and exports four timing/DB samples; the CLI then refuses slope generation because both components are constant. All use the same Wasm with `--skip-build`; the newly exposed duration flag changes sampling controls only, and omitted duration retains the bencher default. + +| Probe artifact | SHA-256 | +| --- | --- | +| Four-point model `/tmp/deos-exp29-joint-crossing-probe.rs` | `7f149939d55e484c01a15008dc5edf9b0397a1aba477dc8bb7f75728da031a1a` | +| Four-point raw JSON, same stem | `b8d94a96e4a33a5e83bc102325f070a9645bf8650dd3bbe64a224160cf6339df` | +| `(5,12)` log `/tmp/deos-generate-pallet-deos-actors-weights.Zms0ae.log` | `bb86b5513c633292eecc5365c31d06afb2eea49ce55db97fc008e0705e3f03e0` | +| `(0,0)` raw `/tmp/deos-exp29-joint-crossing-zero.json` | `0c1a1797515175964315eb167b368f143b62b2c233df6c809873d5ce1d2ad845` | +| `(0,0)` log `/tmp/deos-generate-pallet-deos-actors-weights.NKZnrO.log` | `01ba8e86e4e7aa1268b0d30a6958ca886f9799eede3993e67eee177d673f1a1e` | + +The final command repeats the ordinary 50 × 20 run with `--skip-build --low 0,0 --high 4,12`, keeping the original `joint-crossing-close` output paths. It passes in 271 seconds. Runner source content/modes is `14062564e34466603c88aca10dca74dd33aebefff299ce4c175ddcaf53d7269c`, script SHA-256 `8099f3a7e66400c08ac5d57f78918de7a1c7911cbfe98b0bd1b696282f70e3de`; benchmark/runtime build inputs are unchanged from the corrected Wasm. Model SHA-256 `8c3445edd428e0abbf7c46ee3c74ac0cead7c55211f12db31d8633c0654f1b40`, raw JSON `c008c4272a77ff0dc5adcb22b6f594f1d7f91cd78733883100028de5dbe2f7ec`. + +FRAME CLI 58.0.0 samples each component while holding the others at their selected maxima (`pallet/command.rs`); these are axis faces, not every point of a Cartesian rectangle. The full run retains all 2,000 timing samples and 100 DB samples over 17 distinct tuples. Every tuple has 34 unique reads, 56 repeated reads, 32 writes and zero repeated writes. Reference state has a 12-Step Contract, three tail chunks, F=0, cursor-zero suspended Run, all captured predicates true, full User owner-slot occupancy before close, and a 256-member Crossing source cohort before the bounded worker. The target has one future retry service key; no full Waiting page or heap is constructed. + +| Sampled tuple family | Recorded proof B | +| --- | --- | +| `(0,12)` | 29,594 | +| `(1,12)` | 29,613 | +| `(2,12)` | 29,636 | +| `(3,12)` | 29,658 | +| `(4,P)`, every P=0..12 | `29,656 + 2P` | +| Separate `(0,0)` witness | 29,569 | + +Full-run timing is min 704,920, mean 744,983.9065, upper median 739,701, nearest-rank p95 782,584, p99 842,509 and max 1,731,602 ns. No sample is discarded. The generated model is `733,171,212 + 1,990,914 O + 155,329 P` ps plus 34 reads and 32 writes, estimated proof `37,984 + 22 O + 2 P` bytes. At `(4,12)` the reference DB-inclusive value is 4,792,998,816 ps and 38,096 bytes. This is a diagnostic model over those samples, not a replacement production allowance. + +Residuals remain separate from admission: measured-proof regression `29,568 + 22 O + 2 P` differs from observed proof by −1..+2 bytes; emitted estimated proof exceeds every observed DB sample by at least 8,414 bytes. Raw timing minus the non-DB affine term ranges from −32,554,171 to +990,594,098 ps, with 963/2,000 positive residuals. Thus the raw timing fit is not a sample-wise upper bound. Neither DB additions nor estimated-proof headroom establish the unmeasured complete-state envelope or actual/reserved runtime dominance. + +The trace includes three Contract tails, present Run head/payload, one Waiting frame, service-directory/heap removal, two Crossing pages, target/moved locators, cursor rewind, and ordinary owner `System::Account`/`Balances::Holds` each read and written once. No Oracle feed or observation is read during measured close; publication and aggregation belong to preparation. Postconditions prove custody/sink balance neutrality, removal of the target's process/hold state and resumed visitation of the moved guard. EXP-0028 may consume this joint terminal phase point and EXP-0064 its admission witnesses, but complete heap/page, funding, other Contract/predicate families and consumed-source apoptosis remain unqualified. Do not subtract this aggregate from the earlier zero-Step result or sum overlapping close owners. + +Validation on the corrected benchmark source: all-feature native `close_actor` family passes (62 seconds), independent Opening-partition boundary test passes (12 seconds), and workspace/all-target/all-feature Clippy passes (17 seconds). The earlier preflight compiles all benchmark surfaces (54 seconds) then reports the existing G5 stale-storage-name failure; the corrected full Wasm build and execution refresh the changed benchmark compilation evidence. Script syntax, help, malformed/partial-bound refusals and actual forwarding are checked through the public entrypoint. Runtime semantics, host policy, production Weight and generated metadata are unchanged. + +Dependent-caller review finds that `prepare_reachable_update` still fabricates a second Rising occurrence without rearming after the now-real first publication; native `bench_update_contract` rejects its missing deferred latch. Correct only that constructor: use an actually crossable threshold four/rearm one, publish a real rearm then fire, and retain the replacement threshold at `u128::MAX` so replacement moves to a distinct Armed leaf. Native update-contract checks pass. Source content/modes `94f6904be510de7eafea8c380afae26527e97b2c8d84053474f0fac91fabdd88`, benchmark `9db8a3cf99bd19726600f3a6861f9ccf3d5abefbbbd7cc32ddaf8c23bf04b9b1`. Comparison against archived measured source `/tmp/deos-exp29-joint-measured-benchmarking.rs` (hash `89bd0c18938a7a51cbfc2e4e36638cf9015f99cb8bb1ad314d525221c2dfb304`) changes only this update constructor; joint-close construction and measured operation remain identical. The numeric close model retains its recorded Wasm identity, with no new numeric binding claimed. + +The dependent host falsifier selects `update_contract_reachable_allocation` at exact `(o=0, t=0)` through the same public script with `--extra --steps 2 --repeat 1 --min-duration 0 --low 0,0 --high 0,0`, model path `/tmp/deos-exp29-real-update.rs` and JSON path `/tmp/deos-exp29-real-update.json`. The corrected-source Wasm build passes in 100 seconds, SHA-256 `0dc8bf1f3a959a614bd5dacda338ae2784921e6cace6cdd6576c4559f3124779`. The first invocation omitted `--extra` and therefore selected no benchmark (log `/tmp/deos-generate-pallet-deos-actors-weights.AxBGRk.log`, SHA-256 `6b2cf0bedc8cb101eabed7a810a4740b20622693d7498541c6d6cff3ed956695`). Reusing that Wasm with `--skip-build --extra` reaches the constructor but fails `ContractStepsExceedOnIdleBudget` at public creation in three seconds, before Run construction, rearm/fire or replacement (log `/tmp/deos-generate-pallet-deos-actors-weights.XM6CPW.log`, SHA-256 `83df4bd095c4f9cdedc374e9ec58d2b61e081c585711730444c16bc800f4c302`). No samples or model qualify this update state; production-host qualification remains open. Native update-contract checks and the independent allocation-boundary witness pass. This refusal supplies no reason to bypass admission, shrink the semantic limits or repeat the unchanged probe. + + + +### Derived Metrics + +Independent capture qualification extends the existing close diagnostic to `close_actor_opening_partition(o, p)`: `o` counts amount positions and `p` counts predicate occurrences, each assigned in authored order across the same maximum-length Contract. Current timing/resolution fills the remaining positions. The old coupled workload is the exact specialization `(2n, 4n)` on the reference host; the all-Opening retry constructor remains the maximum endpoint. First falsifiers are `(24, 0)` and `(0, 48)`, then accepted/rejected boundaries as needed. These probes decide which lifecycle fixtures can be measured; they cannot change production admission or install partial Weight. + +The source admission owner reserves `MaxFundingTrackedAssets` at Opening even when this Contract has no LastFunding positions (`derive_step_resource_envelopes` and Idle `execution_step_control_weight_context`). The runtime also switches Suspended-head Weight families above four first-Step predicate evaluation units. Both inputs belong in interpretation: structural `F = 0` does not imply zero funding allowance, and independent capture counts do not establish selector continuity. EXP-0064 owns that envelope decision. + +Source arithmetic rejects funding-reserve reduction as a sufficient repair for the all-Opening AddLiquidity maximum. The current generated Weight file (SHA-256 `6bad800cfbe4848b13f046b3598746e9a6163ec91a54ddbaa0f2e0b2fd5ba2ea`) contributes `58,613` proof bytes from `opening_snapshot_capture(24)`, `125,435` from `opening_predicate_capture(48)`, `81,886` from close and `34,255` from the AddLiquidity effect: `300,189` bytes before every other nonnegative owner. Guaranteed service is strictly below `(50 − 20 − 10 − 14)% × 5,000,000 = 300,000` bytes after scheduler/coordinator/drain deductions. Even zero funding cannot make this bound fit. + +This is a lower-bound calculation on the currently bound, stale model, not a new empirical proof bound or a protocol impossibility. The runtime's component-wise maximum includes both capture components; removing a different positive allowance cannot remove their sum. [EXP-0028](./EXP-0033.md#opening-capture-component-ranges) qualifies the current component ranges next; full selector/funding derivation remains EXP-0064 work. No production selector, reserve, semantic limit or fixture was changed to test this hypothesis. + +Independent-axis probes bind source content/modes `1f53c465c7db0844e73f2905b607b0287859cc6c2ea692c671be078f1fd4cf15`, benchmark source SHA-256 `171e68cf5e186b51d79cbedf709f937d2100181c3757163d41b331d6fa93b0e2`, and benchmark Wasm `068c40b5263e66ed0669fe12fee2569d57f549d200064fbf69fc20be229f50f3`. Reference host, compiler, bindings and measurement controls are unchanged; no concurrent build/test/benchmark runs during sampling. The public-script full-range probe (`--steps 2 --repeat 1`) builds in 113 seconds, then rejects creation before producing a model. + +| Independent `(O, P)` probes | Observed result | Qualification | +| --- | --- | --- | +| `(12,0)`, `(18,0)`, `(21,0)`, `(22,0)` | Admission, actual suspension and verified public close reach analysis | Singleton execution witnesses; CLI subsequently rejects slope generation because a component has only one distinct value | +| `(23,0)`, `(24,0)` | `ContractStepsExceedOnIdleBudget` during creation | No measurement or close; `(22,0)/(23,0)` is an observed adjacent boundary | +| `(0,12)`, `(0,18)`, `(0,19)`, `(0,20)` | Admission, actual suspension and verified public close reach analysis | The same singleton analysis limitation | +| `(0,21)`, `(0,24)`, `(0,48)` | `ContractStepsExceedOnIdleBudget` during creation | No measurement or close; `(0,20)/(0,21)` is an observed adjacent boundary | + +Reproduce each singleton with the earlier raw bencher command, changing both `--low` and `--high` to the comma-separated pair, retaining `--steps 2 --repeat 1 --min-duration 0`, and writing `/tmp/deos-exp29-independent-O-P.rs` with its log beside it. Boundary log SHA-256: `(22,0)` `26d7807b76fdb5194b62b49251d08846c1d5bdef7a3826a3c18bb5da581da2cf`; `(23,0)` `e4813d02a74bc3d8fa621e7ecaa16a6b6811e37b3cbf752e17f48dcce47e96e7`; `(0,20)` `87d1f96f64a7a21d4c45a02a326c41f4810b5d3690e8412dede47cce963a88f5`; `(0,21)` `12f97a99379792d72b13a85a2b008104fb2b2b5adb2be0c0d70c4fe98ffedc9e`. + +These results falsify the hypothesis that only simultaneous saturation causes the maximum fixture's refusal. They do not prove all intermediate points, all predicate placements or a global maximum. In particular, `P > 0` puts Opening timing in Step 0 and selects a different Suspended-head allowance from `P = 0`; independent authored counts are not a proof of independent resource costs. Singleton regression output is discarded as numerical evidence. + +The successful independent fit uses `--low 0,0 --high 6,12 --steps 50 --repeat 20 --heap-pages 4096`, with default minimum duration, output `/tmp/deos-exp29-independent-close-admitted.rs` and its `.log`. FRAME samples the two upper edges: every integer `(O,12)` for `O = 0..6` and `(6,P)` for `P = 0..12`, not the complete Cartesian interior. Every sampled fixture passes admission and postconditions. Fragment SHA-256 `691852c7532811d9ff7a1e9ba89a5acd46e30443b04ea7a1c56ca81410adf894`; raw-log SHA-256 `24403373a22db5049dd5d8183279b4f6712fa912bd482729da342122a10c65c8`. The fragment is not installed. + +| Independent sampled-edge close model | Result | +| --- | --- | +| Generated RefTime before database charge | `586,611,692 + 132,411P` ps; `P` slope standard error `124,787` ps; no nonnegative fitted `O` slope | +| Minimum execution | `541,488,000` ps | +| Estimated ProofSize | `16,318 + 22O + 2P` bytes | +| Recorded proof | Least-squares `7,903 + 22O + 2P` bytes; median regression intercept `7,902` | +| Database | `27` reads, `24` writes, constant across both sampled edges | +| Timing dispersion | Per-point means `573.6..596.3` µs, sigma `5.869..17.31` µs (`1.0..2.9%`); no outlier removal | + +The independent proof slopes reproduce the earlier coupled `52`-byte slope (`2 × 22 + 4 × 2`). This separates the two capture payload contributions for this close workload. The small CPU slope is comparable to its standard error and is not evidence of a performance improvement or cost-free captures. All three cold chunks and ordinary owner Account/Holds accesses remain included; detector/heap maxima, other predicate placements, the full interior, complete Opening/Step admission and Executive throughput remain outside the measurement. + +Independent refinement validation: all eight native `bench_close_actor` tests pass (34 seconds), and `reachable_retry_opening_partition_boundaries` passes all nine zero/single/maximum amount/predicate combinations through real suspension, close and TryRuntime (25 seconds). Canonical formatting and Actors all-features Clippy pass. Preflight compilation passes (62 seconds), then correctly retains the known G5 stale-storage rejection. Production code and bindings are unchanged. + +The earlier coupled `close_actor_opening_partition(o)` revision follows EXP-0028's failed User close setup. It preserves `S = MaxContractSteps`, all three reference cold chunks, four predicates and two AddLiquidity amount positions per Step. Only the first `o` Steps use Opening timing and amounts; the suffix uses Current, giving `O = 2o`, Opening predicate results `4o`, and no LastFunding entries. Ordinary User admission, paid Manual readiness and an actual output-bound failure construct Suspended Run state before measured public close. Its all-Opening constructor is the exact `o = S` specialization, preserved by the current independent maximum. No Actor Contract is shortened, mutated after admission or admitted by a bypass. + +| Reference Opening partition | Result | Evidence class | +| --- | --- | --- | +| `o = 0, 1, 2, 3`; `O = 0, 2, 4, 6`; Opening results `0, 4, 8, 12` | Admission, paid Manual Opening, actual Temporary suspension and custody-neutral public close pass | Full reference-Wasm benchmark verification on the accepted subrange | +| `o = 4` | `ContractStepsExceedOnIdleBudget` before creation | Explicit singleton boundary probe on the same Wasm | +| Every `o = 5..11` | The same creation refusal | Separate singleton probes; no monotonic-selector assumption | +| `o = 12` | The same creation refusal | EXP-0028's original maximum User close setup; identical User Step body and budget inputs, independent of its Crossing Trigger | + +The native benchmark passes its component tests including `o = 0` and the mock's full maximum. Reference qualification binds source content/modes `e40dbca3a854acaf86fe0afb8301cc9a793d834616a838842dc7c9d6f5ffac89`, benchmark source SHA-256 `cc56de02bc64c5c32b6baabaac16e95631b217114eb46fc5753d44b5b879c681`, and benchmark Wasm SHA-256 `eab00ae6738fb6950751fd9f8e767732055e24b5b9c293e6e62c19c087d50498`. Same pinned reference hardware/toolchain/cache/heap/map controls; no concurrent agent build/test/benchmark job during measurement. The full-range public-script probe builds in 110 seconds and rejects creation during sampling; it produces no accepted full-range coefficient. + +Singleton rejection probes use `bin/frame-omni-bencher v1 benchmark pallet --runtime template/target/benchmarks/release/wbuild/deos-runtime/deos_runtime.compact.compressed.wasm --pallet pallet_deos_actors --extrinsic close_actor_opening_partition --low o --high o --steps 2 --repeat 1 --min-duration 0 --heap-pages 4096 --output /tmp/deos-exp29-close-opening-rejected-o.rs`, substituting each `o = 4..11`. These are constructive admission checks, not timing evidence; none reaches measurement or creates a fragment. The prior `o = 12` failure is reused by equality of the budget's User/Step inputs rather than repeated. + +| Accepted `o = 0..3` close model | RefTime ps before DB additions | Estimated / recorded proof B | Reads / writes | +| --- | --- | --- | --- | +| `close_actor_opening_partition(o)` | 593,729,432; minimum observed 549,589,000 | `16,317 + 52o` / `7,903 + 51o` | 27 / 24 | + +Measure that subrange with the same raw bencher command using `--low 0 --high 3 --steps 50 --repeat 20 --heap-pages 4096 --output /tmp/deos-exp29-close-opening-admitted.rs` and the default minimum-duration control. The temporary uninstalled fragment has SHA-256 `9e438454ecfbab1e445b05f407284fae076d913d54eea5f8e144abaa272fa23a`. Per-point mean/σ microseconds are `593.9/12.29`, `592.8/11.46`, `591.8/10.30`, `595.7/12.85`; the fitted nonnegative RefTime slope is zero, which does not prove captures cost nothing. Measured proof mode covers only this geometry. + +The trace includes three cold chunks, Run head/payload, Waiting primary/key removal and ordinary owner `System::Account` plus `Balances::Holds`, each owner-ledger store at one read/write. Native TryRuntime and Wasm postconditions require no Actor/Run/hold/locator/tail residue and unchanged sovereign/Fee Sink balances. This direct public close is not full FIFO or Executive, nor a general Crossing/ObservationChange, maximum encoded-byte, independent Opening-amount/predicate or populated-heap bound. The measured coupled prefix domain cannot replace the original maximum fixture or become an unconditional 3-Step Opening limit; other allocations and final Weight identities remain EXP-0029 / EXP-0064 work. + +Retained-state validation: the all-features native `bench_close_actor` family passes after constructor/assertion generalization (41 seconds); prior maximum assertions remain explicit in `assert_reachable_retry`, while the generic assertion derives the exact authored Opening count. Actors all-features Clippy (25 seconds), canonical formatting, workspace check, changed-scope completion (13 seconds), record normalization/dependency graph and diff checks pass. Production execution and host bindings are unchanged; completion uses `--skip-runtime-tests` and reuses prior runtime/embedding evidence. Final preflight compiles (58 seconds) but retains the known stale generated-storage rejection. Source content/modes remain identical after context synchronization. Successful subrange evidence does not erase either full-range admission failure or close the G5 artifact gate. + + +## Derived Metrics + +Derived values are retained beside the evidence that defines their source and units. No subtraction of overlapping aggregate owners or cross-artifact timing comparison is introduced. + +## Statistical / Variance Notes + +Retain each item’s original sampling and uncertainty. Singleton construction probes establish only their admitted state/access witness, never statistical timing dominance or a production bound. + +## Result + +Ordinary User creation, paid Manual/Crossing readiness, host publication, actual Temporary suspension and public close realize the declared retained Contract/Run/capture/detector/Waiting/hold geometry throughout the measured admitted subdomain. The joint Crossing point `(O=4,P=12)` passes at maximum Contract length; `(5,12)` rejects ordinarily. Independent boundaries reach `(22,0)` and `(0,20)` while adjacent `(23,0)` and `(0,21)` reject. Current-tree all-feature `close_actor` validation passes all 14 selected tests. + +## Interpretation + +The reachable User-close domain is admission-constrained rather than the independent `12/24/48` storage product. Passing points preserve real suspension, deferred Crossing latch, live cursor repair, hold cleanup and custody neutrality; rejected adjacent points remain explicit exclusions rather than reasons to weaken bounds. + +## Decision + +Accepted for the observed admitted reference subdomain. Ordinary admission/funding and actual suspension jointly realize the certified User-close capture/detector/Run geometry, including the joint `(O=4,P=12)` Crossing point and independent admitted boundaries. This satisfies EXP-0029 O7 without accepting refused maximal tuples or a replacement production Weight. + +## Rejected Alternatives and Tradeoffs + +Retain all rejected candidates and causal limits in the extracted evidence. Do not infer a new selection from record relocation or from aggregate coefficient subtraction. + +## Validity + +- `Establishes`: The admitted reference User-close geometry, its real suspension/detector/hold/custody postconditions and the observed adjacent admission boundaries. +- `Does not establish`: Complete production Weight, exact release artifact closure or integrated throughput. +- `Known limitations`: The independent 12/24/48 maximum is not jointly admitted; sampled axis faces are not an interior Cartesian proof, and exact production Weight remains downstream. +- `Invalidation triggers`: A changed measured body, host domain, source/Weight/Wasm identity or acceptance rule requires scoped requalification. +- `Residual qualitative insight`: Follow the owning physical boundary, not a benchmark name. + +## Follow-Up / Next Gradient + +Stop when the declared proof is satisfied. Transfer any independent owner, selector, parameter domain or artifact-identity question before investigating it beyond the smallest classifier. A hypothetical corner case alone is not a measurement mandate. + +## Architecture Impact + +No runtime, semantic, storage or production binding change. C1 and A0 remain frozen; this record owns only its declared proof evidence. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: [EXP-0029](./EXP-0029.md) O7 +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: [EXP-0025](./EXP-0025.md) +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: None. +- `Transfers question to`: None; independent findings require an explicit new transfer. +- `Produces input for`: [EXP-0029](./EXP-0029.md) +- `Reopen trigger`: A falsifier of this declared owner/domain or exact artifact identity; cost alone does not reopen frozen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0053.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0053.md new file mode 100644 index 00000000..ffc53e79 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0053.md @@ -0,0 +1,154 @@ +# EXP-0053 — V10 Admission Refusal Lower Bound + +| Field | Value | +| --- | --- | +| Status | Measured | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | [EXP-0029](./EXP-0029.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-06 | +| Affected domain | The V10 charged proof lower bound suffices to reject the five declared maximal lifecycle tuples through ordinary admission. | +| Physical mechanism | V10 Admission Refusal Lower Bound | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0025](./EXP-0025.md) — Accepted, physical architecture only | +| Successor experiment | [EXP-0029](./EXP-0029.md) | + + +## Decision Question + +The V10 charged proof lower bound suffices to reject the five declared maximal lifecycle tuples through ordinary admission. + +## Governing Specification + +- `Owners`: `template/pallets/actors/docs/specification.en.md` (sole ownership, lifecycle/Trigger/Pipeline/Action semantics, placement and scheduler); `docs/actors-resource-policy.specification.en.md` (component-wise admission/actual settlement, equal-thirds block policy); `docs/actors-performance-assurance.specification.en.md` (evidence and target). +- `Frozen physical input`: [EXP-0025](./EXP-0025.md) C1 Architecture Freeze. No change to FIFO, ticket chronology, Q1, causal floor, primary/reference ontology, source/successor atomicity or sole execution owner. +- `Semantic oracle`: A0 SHA-256 `8746e423168309975e509be91e4eb06fdf0eb0b43369d75f1ee62202a4341678` remains frozen; use only for relevant semantic equivalence, never as a Weight oracle. + +## Context and Hypothesis + +This Leaf owns v10 admission refusal lower bound. Its boundary is the single claim above. Evidence was extracted from the explicitly marked pre-fission parent subsections at Git baseline `a60de7748132211d9e58cecd5be82b41f76ce230`; this migration changes navigation and evidence ownership only. Source, Wasm, commands, rejection outcomes and evidence limitations retain their original meaning. + +Extraction provenance at the recorded Git baseline: EXP-0029 / “Admission Requalification After Opening Model Binding”. Original directional prose (above/below) belongs to these pre-fission subsections; current cross-owner navigation follows the parent proof map. + +## Baseline and Candidates + +Retained measured implementations and rejected candidates are identified in the extracted evidence. No candidate is introduced by this migration. + +## Controls + +Only the declared witness input varies within each source-bound comparison. Preserve the exact host, toolchain, database, Wasm and sampling controls printed with each evidence item; do not compare unlike artifacts as a controlled performance result. + +## Benchmark Class, Workloads, and Criteria + +Acceptance requires the single claim to hold over its declared admitted domain, with each physical operation charged once and every retained limitation resolved before broader production use. One legal omitted-owner, domain or containment counterexample falsifies that claim. Existing workloads below are bounded witnesses, not permission to add independent questions. + +## Environment and Artifact Identity + +Extraction baseline: `a60de7748132211d9e58cecd5be82b41f76ce230`. The migration manifest records exact original content/mode identities and source spans. Question-specific source/Wasm/output identities remain inline. Historical fragments remain historical; no new runtime or production artifact was built. + +Use the identified environment attached to each extracted source. Where a shared control is needed, follow its explicit evidence source; absent identity requires refresh before acceptance. + +## Proof Obligations + +- `Freeze`: Frozen by explicit migration reconstruction from the pre-fission decision and retained witnesses at a60de7748132211d9e58cecd5be82b41f76ce230. No new measurement or historical decision is implied. +- `Review triggers`: None. +- `Decomposition review`: Boundary reviewed during provenance-preserving fission. Workloads within this Leaf share the declared claim, physical owner, changed variable, evidence domain and acceptance rule. Independently decidable owners were transferred; a new owner or selector requires another node. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | The V10 charged proof lower bound suffices to reject the five declared maximal lifecycle tuples through ordinary admission. | One of the five declared V10 maximum tuples whose charged proof lower bound does not exceed the strict service ceiling | Exact V10 charged-model arithmetic and reference-Wasm admission refusal | Self | Required | Named parent decision and declared downstream consumers | Satisfied with scope; original binding/host limitations retained; complete release binding remains open | + +## Measurements + + +### Admission Requalification After Opening Model Binding + +V10 changes admission's generated Opening inputs, so the earlier User-close and four maximal-tail Suspended refusals cannot certify the new numeric boundary. Reuse the unchanged legal constructors, actual OwnerOnly funding, Opening/failure/wakeup transitions and captured-state assertions. The smallest falsifier first retries maximal `close_actor` and the four nonterminal Suspended-head profiles under current reference admission. A creation refusal classifies that concrete tuple as unadmitted; it neither rejects authored semantics nor supplies a measured transition Weight. + +Build one fresh benchmark Wasm from `e77b794` plus the Current provider-regression extension. Run the existing maximal User-close profile through `scripts/benchmarks.sh --steps 2 --repeat 1 --extrinsic close_actor --output /tmp/deos-exp29-v10-user-close.rs pallet_deos_actors`. Then use that exact Wasm for singleton CLI probes of `scheduler_inner_suspended_head_{retry,progress,opening_retry,opening_progress}` at `n = 22`, `p = 4` for Current-only and `p = 3` for one-Opening heads, with `--steps 2 --repeat 1 --min-duration 0`. These are setup/admission probes, not accepted timing fits. If any passes, retain its constructive result and measure only the accepted domain needed by the owning transition. If all fail, inspect the composed reservation's binding resource and transfer that numeric obstruction to EXP-0064 rather than shrinking the authored Contract or repeating the same rejection. + +Prepared source content/modes `abad930ade82698f93b7352f4baf3604961efdcb5beae285cb46577f6c19d37d`; benchmark source `656def3ed440db18adcae3b63264d7458d48dec442237741be53fc5ea552c8c9`; provider `0858133d39eaabb1d2591def2454f1b72d7ee6746b3c1f5ecd720f49a37750f9`; staged Weight file `2d9c60ac7764d1e7dc366531dd4a409444daf66cf3e37ed7feb922bb614e8f1a`. Preflight compilation passes in 56 seconds, followed by the known G5 stale-storage-name rejection. Fresh benchmark Wasm `d8b5692148edf84b91ed4fd7d70180cae648a1083c86ba472006e19643e8123d` builds in 101 seconds. The User-close probe and all four singleton probes fail creation with `ContractStepsExceedOnIdleBudget`; no measurement model is produced. + +| Profile | Authored `(S,O,P,F)` at probe | Failure-log SHA-256 | +| --- | --- | --- | +| User `close_actor` | `(12,24,48,0)` | `4566c70b04bcdc6711531844954523fbae945f2ee7b4583e20d2ab0395e48a5d` | +| Suspended Current head retry | `(12,22,44,0)` | `9a47462ebd9da54924bfa821c6ae658d539312e04a5098e1eaee71feeaff4cc0` | +| Suspended Current head progress | `(12,22,44,0)` | `49aa1cf56d4c101fcb5a6555834214f4ae3c26fa892552f63110c8854154da79` | +| Suspended one-Opening head retry | `(12,22,45,0)` | `a44a541ecaba44e754f52034f4eb7ac8b59b7b51aedff1f7a548cdd8ebb569b9` | +| Suspended one-Opening head progress | `(12,22,45,0)` | `a5479236ddc1370a7c7681e128cb1008ba95438e87ac9b5a84c6eccf80b6d84e` | + +Singleton command: `bin/frame-omni-bencher v1 benchmark pallet --runtime template/target/benchmarks/release/wbuild/deos-runtime/deos_runtime.compact.compressed.wasm --pallet pallet_deos_actors --extrinsic scheduler_inner_suspended_head_PROFILE --low 22,P --high 22,P --steps 2 --repeat 1 --min-duration 0 --heap-pages 4096 --output /tmp/deos-exp29-v10-PROFILE.rs`, with `PROFILE` and parameter `P` defined above. Logs use the corresponding `.log` path; User close uses `/tmp/deos-generate-pallet-deos-actors-weights.8Z1wQu.log`. No concurrent heavy work ran during probes. These source-bound rejections supersede the old-binding rejections only for these exact points; they do not invalidate the already admitted smaller or terminal domains. + +Source arithmetic independently identifies ProofSize as sufficient to reject every point. `guaranteed_actor_service_weight` is strictly below `(50−20−10−14)% × 5,000,000 = 300,000` bytes after fixed scheduler/coordinator/drain deductions. The first-Step composed reservation includes both captures, the maximum head plan (at least 8,058 bytes), Current component (12,697 bytes at these hints), and its own Task effect; `contract_steps_admission_weight_upper` additionally includes close (81,886 bytes). This is a lower bound on currently charged Weight, not recorded proof or a fundamental C1 limit. + +| Profile | Amount / predicate capture B | Head plan / Current B | Head effect / close B | Charged proof lower bound B | +| --- | --- | --- | --- | --- | +| User close | 69,430 / 137,486 | 8,058 / 12,697 | 34,255 / 81,886 | 343,812 | +| Current retry | 63,758 / 126,142 | 8,058 / 12,697 | 18,280 / 81,886 | 310,821 | +| Current progress | 63,758 / 126,142 | 8,058 / 12,697 | 34,255 / 81,886 | 326,796 | +| One-Opening retry | 63,758 / 128,978 | 8,058 / 12,697 | 18,280 / 81,886 | 313,657 | +| One-Opening progress | 63,758 / 128,978 | 8,058 / 12,697 | 34,255 / 81,886 | 329,632 | + +All omitted owners are nonnegative, including funding reservation, reconstruction, Run commit and placement. The effect is the first Step's Transfer or AddLiquidity, not a maximum taken from a different Step. Thus eliminating the current 40-entry funding allowance alone cannot admit these five Contracts. Repeating these full-range benchmark setups would add no decision evidence. EXP-0064 must reconcile complete charged owners and supported constraints; EXP-0028 / EXP-0063 must qualify cleanup and placement with admitted geometry before a smaller bound can replace the current one. + +Final caller-corrected validation: `cargo clippy --workspace --all-targets --all-features -- -D warnings` passes in 2 minutes 11 seconds, including its nested debug Wasm build. Experiment normalization validates all 28 records and their dependency DAG; `git diff --check` and `completion-gate.sh --skip-cargo-check --skip-runtime-tests` pass. The earlier canonical formatting check remains applicable: no Rust or script edits followed it. These checks close this fixture/script correction, not G4–G8 or production Weight acceptance. + + +## Derived Metrics + +Derived values are retained beside the evidence that defines their source and units. No subtraction of overlapping aggregate owners or cross-artifact timing comparison is introduced. + +## Statistical / Variance Notes + +Retain each item’s original sampling and uncertainty. Singleton construction probes establish only their admitted state/access witness, never statistical timing dominance or a production bound. + +## Result + +The extracted observations and counterexamples above remain valid only within their original source-bound scope. No new observation was made during migration. + +## Interpretation + +A scoped witness may satisfy its declared obligation without accepting every production consumer. Existing rejected fits, admission refusals, missing host domains and uninstalled fragments remain explicit evidence boundaries. + +## Decision + +No new Accepted/Rejected decision during migration. The declared scoped proof is measured; stop repeating its satisfied witnesses and let the parent consume their stated limits. + +## Rejected Alternatives and Tradeoffs + +Retain all rejected candidates and causal limits in the extracted evidence. Do not infer a new selection from record relocation or from aggregate coefficient subtraction. + +## Validity + +- `Establishes`: Only the source-qualified observations and constraints above. +- `Does not establish`: Complete production Weight, exact release artifact closure or integrated throughput. +- `Known limitations`: Existing missing domains, diagnostic sample counts, stale bindings and host restrictions remain effective. +- `Invalidation triggers`: A changed measured body, host domain, source/Weight/Wasm identity or acceptance rule requires scoped requalification. +- `Residual qualitative insight`: Follow the owning physical boundary, not a benchmark name. + +## Follow-Up / Next Gradient + +Stop when the declared proof is satisfied. Transfer any independent owner, selector, parameter domain or artifact-identity question before investigating it beyond the smallest classifier. A hypothetical corner case alone is not a measurement mandate. + +## Architecture Impact + +No runtime, semantic, storage or production binding change. C1 and A0 remain frozen; this record owns only its declared proof evidence. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: [EXP-0029](./EXP-0029.md) O8 +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: [EXP-0032](./EXP-0032.md), [EXP-0033](./EXP-0033.md), [EXP-0034](./EXP-0034.md), [EXP-0035](./EXP-0035.md), [EXP-0036](./EXP-0036.md) +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: None. +- `Transfers question to`: None; independent findings require an explicit new transfer. +- `Produces input for`: [EXP-0029](./EXP-0029.md) +- `Reopen trigger`: A falsifier of this declared owner/domain or exact artifact identity; cost alone does not reopen frozen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0054.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0054.md new file mode 100644 index 00000000..22736b1c --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0054.md @@ -0,0 +1,145 @@ +# EXP-0054 — Native Admission Refusal and Precedence + +| Field | Value | +| --- | --- | +| Status | Measured | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | [EXP-0064](./EXP-0064.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-06 | +| Affected domain | Native resource refusal and terminal precedence preserve the admitted authority, custody and FIFO state in the declared bounded partition. | +| Physical mechanism | Native Admission Refusal and Precedence | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0025](./EXP-0025.md) — Accepted, physical architecture only | +| Successor experiment | [EXP-0064](./EXP-0064.md) | + + +## Decision Question + +Native resource refusal and terminal precedence preserve the admitted authority, custody and FIFO state in the declared bounded partition. + +## Governing Specification + +- `Owners`: `template/pallets/actors/docs/specification.en.md` (sole ownership, lifecycle/Trigger/Pipeline/Action semantics, placement and scheduler); `docs/actors-resource-policy.specification.en.md` (component-wise admission/actual settlement, equal-thirds block policy); `docs/actors-performance-assurance.specification.en.md` (evidence and target). +- `Frozen physical input`: [EXP-0025](./EXP-0025.md) C1 Architecture Freeze. No change to FIFO, ticket chronology, Q1, causal floor, primary/reference ontology, source/successor atomicity or sole execution owner. +- `Semantic oracle`: A0 SHA-256 `8746e423168309975e509be91e4eb06fdf0eb0b43369d75f1ee62202a4341678` remains frozen; use only for relevant semantic equivalence, never as a Weight oracle. + +## Context and Hypothesis + +This Leaf owns native admission refusal and precedence. Its boundary is the single claim above. Evidence was extracted from the explicitly marked pre-fission former actors/EXP-0030@a60de7748132211d9e58cecd5be82b41f76ce230 / Measurements subsection at Git baseline `a60de7748132211d9e58cecd5be82b41f76ce230`; this migration changes navigation and evidence ownership only. Source, Wasm, commands, rejection outcomes and evidence limitations retain their original meaning. + +Extraction provenance at the recorded Git baseline: former actors/EXP-0030@a60de7748132211d9e58cecd5be82b41f76ce230 / “Measurements”. Original directional prose (above/below) belongs to these pre-fission subsections; current cross-owner navigation follows the parent proof map. + +## Baseline and Candidates + +Retained measured implementations and rejected candidates are identified in the extracted evidence. No candidate is introduced by this migration. + +## Controls + +Only the declared witness input varies within each source-bound comparison. Preserve the exact host, toolchain, database, Wasm and sampling controls printed with each evidence item; do not compare unlike artifacts as a controlled performance result. + +## Benchmark Class, Workloads, and Criteria + +Acceptance requires the single claim to hold over its declared admitted domain, with each physical operation charged once and every retained limitation resolved before broader production use. One legal omitted-owner, domain or containment counterexample falsifies that claim. Existing workloads below are bounded witnesses, not permission to add independent questions. + +## Environment and Artifact Identity + +Extraction baseline: `a60de7748132211d9e58cecd5be82b41f76ce230`. The migration manifest records exact original content/mode identities and source spans. Question-specific source/Wasm/output identities remain inline. Historical fragments remain historical; no new runtime or production artifact was built. + +Use the identified environment attached to each extracted source. Where a shared control is needed, follow its explicit evidence source; absent identity requires refresh before acceptance. + +## Proof Obligations + +- `Freeze`: Frozen by explicit migration reconstruction from the pre-fission decision and retained witnesses at a60de7748132211d9e58cecd5be82b41f76ce230. No new measurement or historical decision is implied. +- `Review triggers`: None. +- `Decomposition review`: Boundary reviewed during provenance-preserving fission. Workloads within this Leaf share the declared claim, physical owner, changed variable, evidence domain and acceptance rule. Independently decidable owners were transferred; a new owner or selector requires another node. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | Native resource refusal and terminal precedence preserve the admitted authority, custody and FIFO state in the declared bounded partition. | One resource refusal at the last allowed retry that reads cold payload, mutates FIFO/Run, or reverses terminal precedence | Native trie/storage-root correctness under the declared refusal partition | Self | Required | Named parent decision and declared downstream consumers | Satisfied with scope; original binding/host limitations retained; complete release binding remains open | + +## Measurements + + +### Native Refusal and Terminal Precedence + +The retained Idle User trie witness on source identity `97e4e1a3aa0593d2801945121e458bd612594dedbee868e25685e9326f0fea42` checks native liability before Funding/Run and leaves Suspended cold-read proof open. `idle_weight_refusal_reads_only_header_and_user_fee_prerequisite` owns that witness. The baseline actual selector was `DEOS_ACTOR_CONTROL_ACTUAL_V3`; EXP-0028 owns its later receipt-composition correction. The withdrawn maximum-Opening shortcut must not return, and an Action-collection allowance cannot cover missing System base control. Full 12-Step/24/48 admissibility remains a question for the complete production envelope, not a reason to raise budgets. + +The native Suspended falsifier against `17de6e546a7115321b2e5eca83a6442035d3b38d` observed an avoidable Funding read after an admitted SwapIn temporarily failed and the retry returned to Ready. The repaired negative preflight reads coherent Run-head/current-Step authority and current User Action capacity before resource refusal; exhausted retry policy and exceptional paths retain full classification. No storage or scheduler authority is added. + +`suspended_weight_refusal_avoids_run_payload_and_funding_reads` covers Temporary/FundingUnavailable, User/System, cursor 0/1, aggregate/control/effect lanes and independently scarce RefTime/ProofSize (48 refusal cases). Each fixture is immediately before the last allowed attempt (`max_attempts = 2`). Trie evidence excludes Funding/RunPayload, requires native balance only for User, preserves the storage root and FIFO head across repeated calls, and resumes successfully with ample capacity and restored Task funding. Scheduler SHA-256 remains `8ed61e1a5c009eb7a940e4fc9f816c74d131a502ffa49d13d88db9e7099a95cd`; scheduling-test SHA-256 is `5a539a8d223d2007071a3b683423ab7ca581ae59717ad749ddf6adc72d0028b8`. The quick assurance composition still rejects stale fee-envelope vectors at G5; this evidence is not production Weight or release acceptance. + +`suspended_expiry_and_breaker_precede_liability_and_effect_deferral` constructs a legal window and one committed suspension, then services its retained Ready entry after expiry. Its 16 User/System × paused/unpaused × breaker on/off × scarce effect RefTime/ProofSize cases prove WindowExpired cleanup before insolvent User Action liability and effect deferral, while the breaker preserves Run. Custody, fee collection and effect usage remain unchanged; TryRuntime checks topology after each outcome. The native Suspended correctness boundary is reconciled with these existing owners: + +| Boundary | Authority and evidence | +| --- | --- | +| Solvent/insolvent Action capacity | Shared `action_capacity_sufficient`; `user_retry_insolvency_closes_before_effect_capacity_deferral` covers both effect dimensions, exact Run retention and custody-neutral insolvency | +| Paused nonterminal retry | Preflight excludes pause; `pause_and_breaker_gate_scheduler_owned_retry` preserves attempt count through owner pause/resume and breaker | +| Last allowed retry and repeated resource refusal | The 48-case trie property preserves the exact Run and current FIFO head, then completes the last allowed attempt | +| Local/global exhaustion | `prepare_loaded_resuspension` checks both bounds before choosing suspension; terminal failure removes Run instead of persisting an exhausted Suspended state. `frame_only_retry_attempt_exhaustion_closes_from_consumed_authority`, `global_failure_limit_can_close_before_retry_later_local_limit` and `retry_local_limit_precedes_simultaneous_global_failure_limit` cover the terminal owners and precedence | +| Same-block/future eligibility | Valid suspension schedules after its last attempt; inline eligibility rejects early service. Same-block attempt/commit and malformed nonce/authority guards fall through to the existing full classifier and cannot admit an effect | +| Other lifecycle reasons | Nonce exhaustion and auto-close nonce are Idle-only classifier reasons; failure-streak and window termination bypass the negative preflight. Cold binding validation, cleanup admission and effect reservation retain their ordinary owners | + +Validation: package default suite 808 passed/5 ignored; TryRuntime suite 826 passed/5 ignored before the property expanded from four to 24 cases, then the expanded TryRuntime property passed. Independent embedding with `dex-fixture,try-runtime` passed 27 tests. Clippy for Actors/runtime/embedding all targets passed with `-D warnings`; the changed-scope completion gate including workspace check passed. No production Wasm or Weight regeneration was performed for this native correction. + +The subsequent test-only expansion to 48 refusals and 16 expiry cases passed the seven `suspended_` TryRuntime tests. The reference runtime's non-ignored `actors_integration_tests` passed through `scripts/ci-local.sh --only tests --package deos-runtime --test-filter actors_integration_tests --default-features`; the listed total of 224 includes ignored profiles and is not a passed-test count. The CI route skips Wasm building, so this is native integration evidence. Execution source and the earlier unaffected package/embedding evidence remain unchanged. G3's native Suspended correctness reconciliation is complete; production reachability, measured envelopes and exact bindings stay with G4/G5. + + +## Derived Metrics + +Derived values are retained beside the evidence that defines their source and units. No subtraction of overlapping aggregate owners or cross-artifact timing comparison is introduced. + +## Statistical / Variance Notes + +Retain each item’s original sampling and uncertainty. Singleton construction probes establish only their admitted state/access witness, never statistical timing dominance or a production bound. + +## Result + +The extracted observations and counterexamples above remain valid only within their original source-bound scope. No new observation was made during migration. + +## Interpretation + +A scoped witness may satisfy its declared obligation without accepting every production consumer. Existing rejected fits, admission refusals, missing host domains and uninstalled fragments remain explicit evidence boundaries. + +## Decision + +No new Accepted/Rejected decision during migration. The declared scoped proof is measured; stop repeating its satisfied witnesses and let the parent consume their stated limits. + +## Rejected Alternatives and Tradeoffs + +Retain all rejected candidates and causal limits in the extracted evidence. Do not infer a new selection from record relocation or from aggregate coefficient subtraction. + +## Validity + +- `Establishes`: Only the source-qualified observations and constraints above. +- `Does not establish`: Complete production Weight, exact release artifact closure or integrated throughput. +- `Known limitations`: Existing missing domains, diagnostic sample counts, stale bindings and host restrictions remain effective. +- `Invalidation triggers`: A changed measured body, host domain, source/Weight/Wasm identity or acceptance rule requires scoped requalification. +- `Residual qualitative insight`: Follow the owning physical boundary, not a benchmark name. + +## Follow-Up / Next Gradient + +Stop when the declared proof is satisfied. Transfer any independent owner, selector, parameter domain or artifact-identity question before investigating it beyond the smallest classifier. A hypothetical corner case alone is not a measurement mandate. + +## Architecture Impact + +No runtime, semantic, storage or production binding change. C1 and A0 remain frozen; this record owns only its declared proof evidence. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: [EXP-0064](./EXP-0064.md) O4 +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: [EXP-0025](./EXP-0025.md) +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: None. +- `Transfers question to`: None; independent findings require an explicit new transfer. +- `Produces input for`: [EXP-0064](./EXP-0064.md) +- `Reopen trigger`: A falsifier of this declared owner/domain or exact artifact identity; cost alone does not reopen frozen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0055.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0055.md new file mode 100644 index 00000000..789c35c6 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0055.md @@ -0,0 +1,161 @@ +# EXP-0055 — Waiting Publication Production Bound + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | [EXP-0063](./EXP-0063.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-06 | +| Affected domain | Waiting publication has a production bound over accepted joint population, page, key and encoded-cell geometry. | +| Physical mechanism | Waiting Publication Production Bound | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0025](./EXP-0025.md) — Accepted, physical architecture only | +| Successor experiment | [EXP-0063](./EXP-0063.md) | + + +## Decision Question + +Waiting publication has a production bound over accepted joint population, page, key and encoded-cell geometry. + +## Governing Specification + +- `Owners`: `template/pallets/actors/docs/specification.en.md` (sole ownership, lifecycle/Trigger/Pipeline/Action semantics, placement and scheduler); `docs/actors-resource-policy.specification.en.md` (component-wise admission/actual settlement, equal-thirds block policy); `docs/actors-performance-assurance.specification.en.md` (evidence and target). +- `Frozen physical input`: [EXP-0025](./EXP-0025.md) C1 Architecture Freeze. No change to FIFO, ticket chronology, Q1, causal floor, primary/reference ontology, source/successor atomicity or sole execution owner. +- `Semantic oracle`: A0 SHA-256 `8746e423168309975e509be91e4eb06fdf0eb0b43369d75f1ee62202a4341678` remains frozen; use only for relevant semantic equivalence, never as a Weight oracle. + +## Context and Hypothesis + +This Leaf owns the Waiting publication production bound. Retained pre-fission evidence supplies legal host-population witnesses but no production bound. The candidate isolates consumed-authority Primary publication and retained-primary Reference publication while holding the ordinary reference-runtime population at its active/identity/System-sovereign intersection. Existing-key, full-page allocation, new-minimum heap insertion and Cadenced Block-primary/Tick-reference cross-clock publication are measured. Accepted EXP-0061 supplies the 334-byte jointly reachable reference control-cell maximum; the final common-Wasm cohort publishes that exact cell after two real Cadenced retries and due-source consumption. + +## Baseline and Candidates + +| ID | Candidate | Disposition | +| --- | --- | --- | +| B0 | Existing shallow publication fixtures and aggregate full-cycle diagnostics | Retained only as narrower topology/reachability evidence | +| A1 | Complete-population consumed-authority RetryMin Primary publication across existing-key, full-page and new-minimum branches | Measured; new-minimum contains the other two Primary branches component-wise | +| A2 | Complete-population retained-primary new-minimum Reference and Cadenced Block-primary/Tick-reference cross-clock publication | Measured; refreshed Primary contains cross-clock, while Primary and Block Reference require a component-wise maximum | +| A3 | Maximum-contract `RetryMax` target on the stale production binding | Rejected for this artifact: reference-Wasm admission fails `ContractStepsExceedOnIdleBudget`; it is not the maximum admitted cell | +| A4 | EXP-0061's 334-byte Cadenced suspended cell, republished as a new-minimum Block Primary under complete host population | Measured and accepted; supplies the deciding RefTime point while A2's Block Reference supplies estimated ProofSize | + +## Controls + +Only the declared witness input varies within each source-bound comparison. Preserve the exact host, toolchain, database, Wasm and sampling controls printed with each evidence item; do not compare unlike artifacts as a controlled performance result. + +## Benchmark Class, Workloads, and Criteria + +Acceptance requires the single claim to hold over its declared admitted domain, with each physical operation charged once and every retained limitation resolved before broader production use. One legal omitted-owner, domain or containment counterexample falsifies that claim. Existing workloads below are bounded witnesses, not permission to add independent questions. + +## Environment and Artifact Identity + +| Artifact | Identity | +| --- | --- | +| Baseline source | Commit `e92ab804417cdaebbfa44732c1b30a39758894ea`; declared worktree | +| Measurement source/config | Primary trio source identity `56ccff10cde1d56f2e4e169ac899de5abb19c8152d58d80f3aa9137b1abedcf9`, `benchmarking.rs` `9f1f1e413c9aedb7570862ebdc5d093da0acfc394d66cbd7951aa09062a18da2`; Reference/cross-clock and refreshed Primary source identity `faa962bdbd1a2e90e28b8fe1f41310c65a9ee87cb3c4715857e0a0a1c896426d` before record updates, `benchmarking.rs` `317871bf48d9880781457bc9bb2675fda9b559ce8816133f787c46a718fb3fc2`; final common-Wasm cohort `benchmarking.rs` `1a4349ea90390d83a48d249e86740ff73a40f61e5a9b0df65269717b6c80b1bb`; runtime Actor config `0858133d39eaabb1d2591def2454f1b72d7ee6746b3c1f5ecd720f49a37750f9` | +| Toolchain / bencher | Rust `1.98.0 (88d9e12ae 2026-08-18)`; `frame-omni-bencher 0.22.0`, benchmark CLI 58.0.0 | +| Benchmark Wasm | Primary trio `fa667c80d9eca239e07516fe9122b763289b290725a64b38801f0ceffbdd2924`; Reference/cross-clock/refreshed Primary `e9e3d327d0bad7b1db803fbce091507a2bdea32c8de19f981f54c808a644ae57`; deciding common-Wasm cohort `1bfe152704f966b80beaaf468941483fb35942372c3da15d634f7f67008d24ef` | +| Controls | Compiled Wasm; AMD Ryzen 7 4800H; DB cache 1024; heap pages 4096; worst-case map size 1,000,000; 50 steps × 20 repeats; no concurrent project benchmark/build/test job | +| Command | `./scripts/benchmarks.sh --extra --skip-build --steps 50 --repeat 20 --min-duration 0 --extrinsic NAME --output /tmp/deos-exp63-publish-BRANCH.rs --json-file /tmp/deos-exp63-publish-BRANCH.json pallet_deos_actors` after one fresh build/qualification | +| Production artifacts | No generated Weight installed; EXP-0065 owns complete regeneration | + +## Proof Obligations + +- `Freeze`: Frozen finite input. Primary existing-key, full-page and new-minimum alternatives plus retained-primary Reference, Cadenced cross-clock and the accepted 334-byte maximal-cell publication exhaust this Leaf's branch and encoded-cell families. +- `Review triggers`: Waiting topology, primary/reference authority, clock domain, host capacity intersection, encoded control-cell geometry, source/Weight/Wasm identity or publication ownership changes. +- `Decomposition review`: Boundary reviewed during provenance-preserving fission. Workloads within this Leaf share the declared claim, physical owner, changed variable, evidence domain and acceptance rule. Independently decidable owners were transferred; a new owner or selector requires another node. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | Waiting publication has a production bound over accepted joint population, page, key and encoded-cell geometry. | One admitted full-page or new-minimum Waiting publication whose complete charged cost exceeds the proposed production bound | Reference-Wasm FRAME evidence in the declared owner/domain; native checks are construction controls | Self | Required | Named parent decision and declared downstream consumers | Satisfied on the declared reference binding by the complete branch map and publicly attained 334-byte maximal-cell publication | + +## Measurements + +Generated figures are pre-database RefTime and estimated ProofSize. The pinned RocksDB schedule charges 25,000,000 RefTime/read and 100,000,000/write. + +| Complete Primary publication branch | RefTime, ps | Estimated ProofSize | Reads | Writes | Pinned-DB total RefTime | Fragment SHA-256 | JSON SHA-256 | +| --- | ---: | ---: | ---: | ---: | ---: | --- | --- | +| Existing populated key | 245,496,000 | 14,123 | 7 | 4 | 820,496,000 | `7d3a818ef55f590405ba8df0713f866ac60148e7ec17743233c991f806e32c9c` | `71cd66cce2eb3ffebfbf17b8490caac597f52eec2b398b30974ba2f7454f22ac` | +| Existing key, full first page | 328,329,000 | 26,877 | 8 | 5 | 1,028,329,000 | `f211d3981c583a3234eeeb0ba137cc3e0400ba7a1cc38b6613120aaf25cfcc20` | `306fded4e6079c153413a3bdec2ea2dc45be260b2d9ff2e5a546d8b6f81d485d` | +| New minimum in populated heap | 1,018,930,000 | 76,246 | 44 | 30 | 5,118,930,000 | `128bdca8bcad18496b19d464492c43c60e557edea4bc38f8ded2ce7aa37310fa` | `d0c869dbb4e569cee42f610cdfaab9206d7fd4c21d171ea1a7ab3e9230d84211` | + +Each setup creates one RetryMin target, then 9,984 ordinary future-window System guards while retaining reference-runtime genesis. Identity and System-sovereign capacity saturate at 10,000. Existing-key publication retains 9,984 distinct keys; the full-page branch retains 9,953; new-minimum publication grows 9,984 to 9,985 keys and repairs ten heap pages. The measured block starts after Ready source consumption and performs exactly one Primary Waiting publication. Native three-branch regression passes complete mock host population and TryRuntime state checks. + +| Current-Wasm new-minimum branch | RefTime, ps | Estimated ProofSize | Reads | Writes | Pinned-DB total RefTime | Fragment SHA-256 | JSON SHA-256 | +| --- | ---: | ---: | ---: | ---: | ---: | --- | --- | +| Refreshed consumed-authority Primary | 1,052,245,000 | 76,246 | 44 | 30 | 5,152,245,000 | `b75a40d1fd005eed49fafd57d0d344bc64228429cfe09818f06baa7da3e41115` | `a4f6a6ace7c450a3b4f2455b5174363dff4da1fc657974a89901a74f36667540` | +| Retained Ready primary, Block Reference | 1,036,880,000 | 76,618 | 44 | 30 | 5,136,880,000 | `c45ffff087cc6cb0915418f1f57501b26f7a59c11a3d42af9d23c3b849b7e09b` | `f5bff4131ef81e9857e361cc24ad8909f87916ada2fb894613f6d86b0379cf01` | +| Cadenced Block primary, new Tick Reference | 1,034,155,000 | 74,057 | 44 | 30 | 5,134,155,000 | `ec87783d36b6c4dfa068cc7f59cf5b4523b95e7a479b2f9f56aaa316cf6b34c2` | `00ff5862a3c9aab1da345427fdf63efa63ac9245a0c76182085b4a25c28b26fd` | + +The retained-primary branch keeps the exact Ready locator, appends one Block Reference and rewrites that primary with its pointer. The Cadenced fixture reaches its paid latch through ordinary Tick scheduling, republishes the consumed authority as a Block primary, fills the independent Tick heap with ordinary Cadenced guards, then measures one new-minimum Tick Reference plus the retained Block-primary rewrite. Both saturate the same active/identity/System-sovereign intersection and pass full TryRuntime reconciliation. The refreshed Primary point puts all deciding new-minimum alternatives on one source and Wasm. + +The initial maximum-contract `RetryMax` candidate failed reference-Wasm construction before measurement with `ContractStepsExceedOnIdleBudget`. This is an admission-domain result, not evidence that the RetryMin cell is maximal. Accepted EXP-0061 instead proves and publicly attains the 334-byte maximum on this binding. + +The final requalification puts all deciding new-minimum alternatives on Wasm `1bfe152704f966b80beaaf468941483fb35942372c3da15d634f7f67008d24ef`. The maximal-cell constructor uses ordinary Cadenced creation and two real unfunded SplitTransfer retries, consumes the due Block source into Ready, saturates the remaining active/identity/System-sovereign intersection with ordinary future-window guards, then republishes one new-minimum Block Primary. The stored result is exactly 334 bytes with both Block and Tick pointers, Suspended state, last-cycle state, eligibility and the 19-byte resource envelope. + +| Deciding common-Wasm new-minimum branch | RefTime, ps | Estimated ProofSize | Reads | Writes | Pinned-DB total RefTime | Fragment SHA-256 | JSON SHA-256 | +| --- | ---: | ---: | ---: | ---: | ---: | --- | --- | +| RetryMin consumed-authority Primary | 983,590,000 | 76,246 | 44 | 30 | 5,083,590,000 | `b7e78db9df02269ab46086211b152ff8b14ea917d34691438860cd2a671c0772` | `6c54a8ebd2629f2bc33f13075d19c5b01d4b4409e9b9c582245ca44f00f01355` | +| Retained Ready primary, Block Reference | 1,019,839,000 | 76,618 | 44 | 30 | 5,119,839,000 | `75a6417044a1cc2cd860073687293de1c3efd17f1df13546d8c170179bd6dca8` | `e37ce75df493f2e76ea52d901c56cfc0b3a2848c65c4956552303b7f8d9214a1` | +| Cadenced Block primary, new Tick Reference | 1,023,540,000 | 74,057 | 44 | 30 | 5,123,540,000 | `080d517ade29d7a9840d7ef35503ccb7c2b0f5f0a540f83192014ae63a70ce6a` | `4f308f2e041b8faf929c964e28a649ed7f326394e56b6bb6790466793ed7f924` | +| 334-byte Cadenced suspended Block Primary | 1,031,223,000 | 74,993 | 44 | 30 | 5,131,223,000 | `94ef02a74d57b0b8ca8f1e43fa1fd9ec9ab944fa44f05519ac133bb91c9eeab6` | `478fc5cc5f857aafef504a0448983188c86ef22559766573d6df2627f4936270` | + +Each deciding point uses 50 steps × 20 repeats. The maximal-cell verbose log SHA-256 is `7bd25e68c9a14d1f11427fcd31ae40130fa28c195df6355e6dc5993a1dd1b7e7`; it records `total=334`, nominal `362`, and full TryRuntime reconciliation. The focused generated benchmark test passes; the complete all-feature Actors package reports 1,125 passed / 5 ignored; package all-target/all-feature Clippy is warning-free. Canonical formatting, experiment normalization with all three regenerated graph projections, diff checks and the changed-scope completion gate pass. No generated fragment is installed. + +## Derived Metrics + +The new-minimum branch contains the earlier existing-key and full-page Primary alternatives by the retained branch audit. On the deciding common Wasm, the 334-byte Primary supplies maximum RefTime, while retained-primary Block Reference supplies maximum estimated ProofSize. The accepted component-wise publication envelope is therefore `1,031,223,000 / 76,618 +44R/30W`, totaling `5,131,223,000` pinned-DB RefTime. It contains RetryMin Primary and Cadenced cross-clock publication component-wise; no cross-artifact timing comparison or aggregate subtraction selects the result. + +## Statistical / Variance Notes + +Each deciding branch uses 50 steps × 20 repeats on one source/Wasm. The fixed benchmark has no model component; generated output selects a conservative fixed RefTime. Construction/qualification probes remain deterministic state evidence only. + +## Result + +The complete legal publication branch map and 334-byte encoded-cell maximum select `1,031,223,000 / 76,618 +44R/30W`, totaling `5,131,223,000` pinned-DB RefTime. The maximal Cadenced suspended Primary supplies RefTime; retained-primary Block Reference supplies estimated ProofSize. This is an accepted scoped envelope, not an installed coefficient or final release artifact. + +## Interpretation + +Population and page/key geometry materially dominate the shallow append fixtures. Consumed Primary, retained-primary Reference, Cadenced cross-clock and maximal-cell work have explicit non-duplicating boundaries. The rejected RetryMax Contract is irrelevant to encoded-cell closure because EXP-0061 bounds and publicly attains the actual admitted 334-byte maximum. + +## Decision + +Accepted for the declared current reference binding. Select one component-wise Waiting publication envelope of `1,031,223,000 / 76,618 +44R/30W`; do not add Cadenced publication again, reduce the legal contract domain or install temporary fragments. A changed production Weight binding requalifies compact resource widths and this scoped result through EXP-0065 rather than keeping G4 circularly open. + +## Rejected Alternatives and Tradeoffs + +Retain all rejected candidates and causal limits in the extracted evidence. Do not infer a new selection from record relocation or from aggregate coefficient subtraction. + +## Validity + +- `Establishes`: Complete-population consumed-authority Primary, retained-primary Reference, Cadenced Block/Tick cross-clock and 334-byte maximal-cell publication branches, with one component-wise envelope on the named common Wasm. +- `Does not establish`: Installed production Weight, replacement/removal, exact release artifact closure or integrated throughput. +- `Known limitations`: The accepted bound is specific to the current reference admission/Weight binding. EXP-0065 must requalify it if regenerated compact resource widths or measured code identity changes. +- `Invalidation triggers`: A changed measured body, host domain, source/Weight/Wasm identity or acceptance rule requires scoped requalification. +- `Residual qualitative insight`: Follow the owning physical boundary, not a benchmark name. + +## Follow-Up / Next Gradient + +Accepted EXP-0063 composes this publication envelope with Accepted EXP-0056; EXP-0064 consumes the result. EXP-0065 owns exact regeneration and scoped requalification on any changed binding. + +## Architecture Impact + +No runtime, semantic, storage or production binding change. C1 and A0 remain frozen; this record owns only its declared proof evidence. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: [EXP-0063](./EXP-0063.md) O1 +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: [EXP-0049](./EXP-0049.md), [EXP-0050](./EXP-0050.md), [EXP-0051](./EXP-0051.md) +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: None. +- `Transfers question to`: None; independent findings require an explicit new transfer. +- `Produces input for`: [EXP-0063](./EXP-0063.md) +- `Reopen trigger`: A falsifier of this declared owner/domain or exact artifact identity; cost alone does not reopen frozen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0056.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0056.md new file mode 100644 index 00000000..cecdaf70 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0056.md @@ -0,0 +1,144 @@ +# EXP-0056 — Waiting Removal and Heap Repair Production Bound + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | [EXP-0063](./EXP-0063.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-07 | +| Affected domain | Waiting removal and heap repair have a production bound over accepted joint population and repair geometry. | +| Physical mechanism | Waiting Removal and Heap Repair Production Bound | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0025](./EXP-0025.md) — Accepted, physical architecture only | +| Successor experiment | [EXP-0063](./EXP-0063.md) | + +## Decision Question + +Waiting removal and heap repair have a production bound over accepted joint population and repair geometry. + +## Governing Specification + +- `Owners`: `template/pallets/actors/docs/specification.en.md` (sole ownership, lifecycle/Trigger/Pipeline/Action semantics, placement and scheduler); `docs/actors-resource-policy.specification.en.md` (component-wise admission/actual settlement, equal-thirds block policy); `docs/actors-performance-assurance.specification.en.md` (evidence and target). +- `Frozen physical input`: [EXP-0025](./EXP-0025.md) C1 Architecture Freeze. No change to FIFO, ticket chronology, Q1, causal floor, primary/reference ontology, source/successor atomicity or sole execution owner. +- `Semantic oracle`: A0 SHA-256 `8746e423168309975e509be91e4eb06fdf0eb0b43369d75f1ee62202a4341678` remains frozen; use only for relevant semantic equivalence, never as a Weight oracle. + +## Context and Hypothesis + +This Leaf owns the complete Waiting-removal and heap-repair branch family. Retained EXP-0050/0051 construction evidence supplies ordinary lifecycle populations for mixed Reference removal, middle-page unlink, final Tick-key downward repair, saturated Block-key upward repair and tail-only removal. The production-quality candidate measures those five already-inclusive public-close atoms on one source and Wasm, then selects a conservative component-wise branch bound without subtracting or adding generic close work. + +## Baseline and Candidates + +| ID | Candidate | Disposition | +| --- | --- | --- | +| B0 | Historical one-sample constructive/access probes | Retained as legal-state and branch-topology evidence only | +| A1 | Full mixed page Reference removal and last-Reference middle-page unlink | Measured; both contained by A3 | +| A2 | Saturated Block heap upward repair and tail-only reclamation | Measured; both contained by A3 | +| A3 | Last Tick Reference, root/downward repair and final heap-page reclamation | Accepted as the component-wise removal/repair bound over the admitted reference branch family | + +## Controls + +Only the declared removal branch varies. All five points use the same source/configuration, reference host, toolchain, database/cache, Wasm, sampling controls and public `close_actor` measured boundary. Setup remains outside the measured block. No project benchmark, build or test process overlapped sampling. + +## Benchmark Class, Workloads, and Criteria + +Acceptance requires upward, downward and tail-only heap removal, ordinary Reference deletion, middle-page unlink and final-key/page reclamation under their complete legal population witnesses. Each measured atom includes Contract/Run/lifecycle and source cleanup already performed by public close. A branch exceeding the selected bound in RefTime, estimated ProofSize, reads or writes falsifies it; an additive heap coefficient or subtraction from another close atom is inadmissible. + +## Environment and Artifact Identity + +| Artifact | Identity | +| --- | --- | +| Baseline source | Commit `e92ab804417cdaebbfa44732c1b30a39758894ea`; declared worktree | +| Measurement source/config | `scripts/actors-assurance.sh::worktree_evidence_identity` = `faa962bdbd1a2e90e28b8fe1f41310c65a9ee87cb3c4715857e0a0a1c896426d` before this record update; `benchmarking.rs` `317871bf48d9880781457bc9bb2675fda9b559ce8816133f787c46a718fb3fc2`; `scheduler.rs` `20a0188594958caf42b46ce5cb4177bb64193ca37575c979432f9c6455395f9b`; runtime Actor config `0858133d39eaabb1d2591def2454f1b72d7ee6746b3c1f5ecd720f49a37750f9`; benchmark script `0b132dd78725394a3f3a5fb605b9429c991729453e7b4816bd340649d329a7d5` | +| Toolchain / bencher | Rust `1.98.0 (88d9e12ae 2026-08-18)`; `frame-omni-bencher 0.22.0`, benchmark CLI 58.0.0 | +| Benchmark Wasm | Uncompressed SHA-256 `e9e3d327d0bad7b1db803fbce091507a2bdea32c8de19f981f54c808a644ae57` | +| Controls | Compiled Wasm; AMD Ryzen 7 4800H; DB cache 1024; heap pages 4096; worst-case map size 1,000,000; 50 steps × 20 repeats; no concurrent project benchmark/build/test job | +| Command | `./scripts/benchmarks.sh --extra --skip-build --steps 50 --repeat 20 --min-duration 0 --extrinsic NAME --output /tmp/deos-exp56-BRANCH.rs --json-file /tmp/deos-exp56-BRANCH.json pallet_deos_actors` for each exact branch | +| Evidence manifest | `/tmp/deos-exp56-removal.sha256`, SHA-256 `2b90c378c8f6eeadcc601d9a205fbcaec2a9c1088ad4415c430a3fe8c962c26b` | +| Production artifacts | No generated Weight installed; EXP-0065 owns complete regeneration | + +## Proof Obligations + +- `Freeze`: Frozen finite branch map: mixed Reference retention/removal, bounded page unlink, final Tick-key downward repair, Block-key upward repair and tail-only removal. +- `Review triggers`: Waiting topology, close/removal ownership, heap comparison/order, host population, source/Weight/Wasm identity or admission domain changes. +- `Decomposition review`: All workloads vary only the removal/repair branch of the same inclusive public-close owner. Independent publication and encoded-admission questions remain with EXP-0055/EXP-0063/EXP-0065. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | Waiting removal and heap repair have a production bound over accepted joint population and repair geometry. | One admitted upward/downward/tail or final-key removal whose repair cost exceeds the proposed production bound | Reference-Wasm FRAME evidence in the declared owner/domain; native checks are construction controls | Self | Required | Named parent decision and declared downstream consumers | Satisfied for the admitted reference branch family by A3 | + +## Measurements + +Generated figures are pre-database RefTime and estimated ProofSize. The pinned RocksDB schedule charges 25,000,000 RefTime/read and 100,000,000/write. + +| Complete public-close removal branch | RefTime, ps | Estimated ProofSize | Reads | Writes | Pinned-DB total RefTime | Fragment SHA-256 | JSON SHA-256 | +| --- | ---: | ---: | ---: | ---: | ---: | --- | --- | +| Reference retained in full mixed page | 445,804,000 | 18,665 | 23 | 16 | 2,620,804,000 | `4c3be6ee93ac1fad480ce9753fd29f3a62a616fa00fa1013d5d43e1457519f16` | `9f18decbe3e0e40a927d487fcfffe184fa5d7ecc386a37e78013007e7084555d` | +| Last Reference, middle-page unlink | 466,826,000 | 33,564 | 25 | 18 | 2,891,826,000 | `0c6514d653548e182d2a91f2c28bdbfedd337dc9df8d73d1014c7324730f4af5` | `f7637c22d1e11863dbd7d8c830aed862bc3c125a2a1b96d24e7f83747216a533` | +| Last Tick Reference, root/downward repair and heap-tail reclamation | 1,928,069,000 | 96,741 | 70 | 44 | 8,078,069,000 | `08f4aa523df07c156c0d500e45bc73bbddbc620131c48ddf7ec844c8c255c469` | `3838b0fb339b348aab9b6625e9a34104c909486ff819ee0541af8cafc313111f` | +| Saturated Block heap upward repair and heap-tail reclamation | 1,571,035,000 | 76,112 | 55 | 39 | 6,846,035,000 | `7d35b7bf38fa56db7b374458d677c7be4af22984298e87164c1c470ee2c04250` | `5441cde5529086b5e061e0f6b17d8b8d3ac6bbee92a33e9d24da77e23bbe2cc1` | +| Saturated Block heap tail-only reclamation | 961,590,000 | 21,030 | 22 | 18 | 3,311,590,000 | `5ee39d78b43bd5e65d98f9dd7071c6e2c4cc01606016ba5649e40edf59633043` | `5e3abca9214091cca81d1797435a7525deec5840eb998a9eb8878b9f211f275f` | + +The full Waiting native family passes 37 tests. The mixed final-deadline trace deletes the sole Tick Reference and Waiting directory, reads 18 heap pages, writes ten, reads/writes 15 reverse indices and follows the downward repair from root. The saturated Block upward trace rewrites ten heap pages; the tail-only trace touches one. All three final-key branches reclaim the one-entry heap tail page and validate complete forward/reverse order and TryRuntime state. Mixed non-final branches either rewrite one frame or unlink the empty middle frame between two full neighbors. + +## Derived Metrics + +A3 component-wise contains every other removal branch and the accepted EXP-0042 maximum-Step Suspended/Crossing public-close point. The selected complete removal/repair candidate is therefore `1,928,069,000 / 96,741 +70 reads / 44 writes`, totaling `8,078,069,000` RefTime under pinned RocksDB. This is selection among mutually exclusive complete close branches, not an additive heap allowance, aggregate subtraction or isolated repair coefficient. + +## Statistical / Variance Notes + +Each deciding branch uses 50 steps × 20 repeats on one source/Wasm. Fixed benchmarks have no model component; generated output selects conservative fixed RefTime. Earlier singleton samples remain construction/access evidence only and are not used for timing dominance. + +## Result + +The complete admitted reference removal branch map selects the final Tick Reference/root-downward repair atom at `1,928,069,000 / 96,741 +70R/44W`. It contains mixed Reference retention/unlink, saturated upward repair, tail-only removal and the retained admitted public-close owner component-wise. No generated fragment is installed. + +## Interpretation + +Heap population dominates shallow mixed-page cleanup, and downward root repair dominates upward and tail-only alternatives in every Weight component on the common Wasm. Because every profile measures public close through completion, Contract/Run/lifecycle/source work is already included exactly once. EXP-0063 must select this complete atom for populated removal rather than add it to EXP-0042 or a generic close coefficient. + +## Decision + +Accepted for the admitted reference removal/repair domain. Select exactly one inclusive complete close branch; the conservative bound is `1,928,069,000` pre-DB RefTime / `96,741` estimated ProofSize plus 70 reads and 44 writes (`8,078,069,000` pinned-DB RefTime). Do not install the diagnostic fragment or add separate close, Run, Waiting, heap, page-unlink or lifecycle work. EXP-0065 owns exact regeneration and any requalification required by a changed admitted domain. + +## Rejected Alternatives and Tradeoffs + +- Adding heap repair to EXP-0042's inclusive public-close atom is rejected as duplicate charging. +- Subtracting mixed or tail close aggregates to synthesize an isolated heap coefficient is rejected because setup and retained state differ. +- Selecting upward repair from changed-page intuition is rejected: the measured root/downward branch is larger in all Weight components. +- Reopening frozen C1 topology or beginning residual optimization is rejected; this owner now has a conservative measured bound. + +## Validity + +- `Establishes`: Common-Wasm 50 × 20 costs and component-wise containment for ordinary mixed Reference retention/unlink and complete legal downward/upward/tail final-key repair branches at the accepted reference host population. +- `Does not establish`: A new admission domain, publication encoded-cell closure, exact production Weight/artifact identity, integrated throughput or isolated heap costs. +- `Known limitations`: The selected bound is a complete public-close branch, not a reusable standalone repair allowance. A later changed admission/Weight binding may require scoped requalification before G5 acceptance. +- `Invalidation triggers`: A changed measured body, Waiting topology, close/removal selector, host domain, source/Weight/Wasm identity or acceptance rule. +- `Residual qualitative insight`: Select complete branch owners; population and repair direction matter more than the benchmark's nominal close label. + +## Follow-Up / Next Gradient + +Accepted EXP-0063 composes this removal atom with Accepted EXP-0055 and reconciles overlap with EXP-0028/EXP-0042/EXP-0058. EXP-0065 retains exact regeneration and changed-binding requalification. Stop removal measurement unless an invalidation trigger or an admitted counterexample appears. + +## Architecture Impact + +No runtime, semantic, storage or production binding change. C1 and A0 remain frozen. The experiment records one complete removal selector and forbids additive duplicate close/heap ownership. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: [EXP-0063](./EXP-0063.md) O2 +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: [EXP-0049](./EXP-0049.md), [EXP-0050](./EXP-0050.md), [EXP-0051](./EXP-0051.md) +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: [EXP-0042](./EXP-0042.md), [EXP-0050](./EXP-0050.md), [EXP-0051](./EXP-0051.md) +- `Transfers question to`: None; independent findings require an explicit new transfer. +- `Produces input for`: [EXP-0063](./EXP-0063.md) +- `Reopen trigger`: A falsifier of this declared owner/domain or exact artifact identity; cost alone does not reopen frozen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0057.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0057.md new file mode 100644 index 00000000..fb36a2c7 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0057.md @@ -0,0 +1,172 @@ +# EXP-0057 — Useful Trigger Detection Weight Coverage + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | [EXP-0028](./EXP-0028.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-06 | +| Affected domain | Each useful Trigger transition and deferred family detector has one sound charged owner, with redundant latched work excluded. | +| Physical mechanism | Useful Trigger Detection Weight Coverage | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0020](./EXP-0020.md) — Accepted useful-latch and detector-disable topology | +| Successor experiment | [EXP-0028](./EXP-0028.md) | + +## Decision Question + +Does each of the six useful Trigger families have one complete reference-Wasm occurrence owner, while direct, indexed and temporal source paths reserve their physical work once and exclude already-latched Actor-specific evaluation and Trigger collection? + +## Governing Specification + +- `Owners`: `template/pallets/actors/docs/specification.en.md` (sole ownership, useful Trigger occurrence, lifecycle, placement and scheduler); `docs/actors-resource-policy.specification.en.md` (component-wise admission/actual settlement and equal-thirds policy); `docs/actors-performance-assurance.specification.en.md` (evidence contract and target). +- `Frozen physical input`: [EXP-0025](./EXP-0025.md) C1 Architecture Freeze. No change to FIFO, ticket chronology, Q1, causal floor, primary/reference ontology, source/successor atomicity or sole execution owner. +- `Accepted semantic input`: [EXP-0020](./EXP-0020.md) selects early direct rejection, eager Cadenced detach, consumed AtTime and detector-local indexed disablement. This experiment requalifies Weight ownership; it does not reopen that topology. +- `Semantic oracle`: A0 SHA-256 `8746e423168309975e509be91e4eb06fdf0eb0b43369d75f1ee62202a4341678` remains frozen; use only for relevant semantic equivalence, never as a Weight oracle. + +## Context and Hypothesis + +The six family-specific `WeightInfo` methods are both the User Trigger-fee basis and the bounded useful-occurrence witnesses. Physical source publication and deferred traversal remain caller/worker-owned: an occurrence coefficient is not added a second time to an inclusive host, fanout or Crossing branch. Already-latched Manual and AddressEvent calls retain ordinary source/transaction ownership but no second Actor Trigger fee; ObservationChange and Crossing skip through `IndexedTriggerDetectionDisabled` before loading Actor state; Cadenced has no live Trigger deadline while latched; AtTime is consumed. + +The retained 0.7.24 evidence established these semantics, but the 0.7.25 control-cell source and installed generated file no longer share exact storage identity. The smallest deciding refresh is therefore the six existing useful-occurrence profiles on one current reference benchmark Wasm, combined with source inspection and focused redundancy regressions. Complete file regeneration remains G5 rather than a reason to patch coefficients manually. + +## Baseline and Candidates + +| ID | Candidate | Disposition | +| --- | --- | --- | +| B0 | Treat the installed 2026-09-01 generated occurrence coefficients as exact current evidence | Rejected: they predate the current control-cell storage paths and are stale G5 artifacts | +| A1 | Reuse one generic Trigger coefficient for all families | Rejected: source, topology, proof and database ownership differ materially by family | +| A2 | Six generated useful-occurrence owners plus inclusive source/worker ownership and early redundant exclusion | Measured and accepted for the reference binding | + +## Controls + +All six profiles use the same worktree source identity, Rust toolchain, benchmark runtime Wasm, compiled Wasm execution, map bound, heap pages, database cache, 50 steps and 20 repeats. Setup is outside each measured block. Each useful profile begins from an unlatched admitted User Actor and asserts canonical readiness after the measured transition. No competing project benchmark/build/test process ran during measurement. + +## Benchmark Class, Workloads, and Criteria + +| Workload | Measured boundary | Required postcondition | +| --- | --- | --- | +| W1 Manual | Signed useful Manual dispatch | One pending readiness; family fee owner only for useful transition | +| W2 AddressEvent | Matched certified address consequence, excluding source publication and funding-state work | One pending readiness and canonical placement | +| W3 ObservationChange | One affected subscriber, excluding publisher ingress and fanout traversal | Disabled indexed detector plus one pending readiness | +| W4 ObservationCrossing | One retained threshold fire from an already-published transition | Phase/index movement, detector disablement, collection and one placement | +| W5 AtTime | One due one-shot Tick source after physical source extraction | Consumed one-shot state, collection and one placement | +| W6 Cadenced | One due cadence Tick source after physical source extraction | Deadline advancement/detach, collection and one placement | +| W7 Redundancy controls | Repeated direct/indexed activity and latched temporal state | No second Trigger fee/event/activation or Actor-specific indexed evaluation | + +Acceptance requires all six useful profiles to pass reference-Wasm measurement and postconditions, each family to map to one runtime Weight/fee selector, and each deferred worker to reserve an inclusive physical branch that does not add the economic occurrence owner again. One omitted family, unmetered deferred branch, or repeated latched Actor-specific fee/event/activation falsifies the decision. + +## Environment and Artifact Identity + +| Artifact | Identity | +| --- | --- | +| Baseline source | Commit `e92ab804417cdaebbfa44732c1b30a39758894ea`; tree `511ee1af664e4561c5a902d8aa6c01d5930e32e3` plus the declared worktree | +| Measurement source/config | `scripts/actors-assurance.sh::worktree_evidence_identity` = `49d5faab9b68b676d63f58d8607c6f8ee4e4ffc58766ac898cdac9fb06334dc2`; content and executable modes, independent of commit packaging | +| Measured fixture sources | `lib.rs` `7432b3b5df56cf7507d0991270b62a5a35e4437a2548e81bd6bd66bb57d6c788`; `scheduler.rs` `bdedd82571a229f3609194547c6256d8ca454055640e407c2c9af03d2245fbd1`; `reactions.rs` `d30cec454c0d8d35042f062e2e3f60eac85f335b07489f4d368afc8e1ca26baa`; `crossing.rs` `791588a1d60b5de0f7952006618eee798a8e9cb2c6052086dece5bd06971cc37`; `benchmarking.rs` `56178d1e89c600bed20dc737e9fe96376929c9d41c00cd0cd9586dea66d8bdba` | +| Runtime configuration | `template/runtime/src/configs/actor_config.rs` SHA-256 `0858133d39eaabb1d2591def2454f1b72d7ee6746b3c1f5ecd720f49a37750f9` | +| Toolchain / bencher | Rust `1.98.0 (88d9e12ae 2026-08-18)`; `frame-omni-bencher 0.22.0`, benchmark CLI 58.0.0 | +| Benchmark Wasm | `cb901d049e328d2d1074fabf3f3583e6800526e7ba9b4188f8a258ee04e0a1a6` | +| Backend controls | Compiled Wasm; AMD Ryzen 7 4800H reference host; DB cache 1024; heap pages 4096; worst-case map size 1,000,000; 50 steps × 20 repeats | +| Commands | `./scripts/benchmarks.sh --steps 50 --repeat 20 --extrinsic NAME --output /tmp/deos-exp57-NAME.rs --json-file /tmp/deos-exp57-NAME.json pallet_deos_actors`; one exact build, then `--skip-build` for the remaining five names | +| Production artifacts | No production Wasm or Weight generated; six temporary fragments remain uninstalled and EXP-0065 owns complete regeneration | + +## Proof Obligations + +- `Freeze`: Frozen after the six-family matrix and exact useful/redundant ownership partition below. New Trigger families or a changed physical source boundary require scoped review. +- `Review triggers`: Trigger semantics, detector topology, occurrence benchmark body, source/worker composition, fee selector, or reference runtime binding changes. +- `Decomposition review`: All six profiles decide the same finite claim: one useful family occurrence owner and no duplicate latched Actor-specific work. Generic source/Waiting/cleanup questions remain with their existing experiments. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | Each useful Trigger transition and deferred family detector has one sound charged owner, with redundant latched work excluded. | One useful versus already-latched Trigger occurrence with an omitted detector owner or duplicate Actor-specific evaluation/charge | Reference-Wasm FRAME evidence in the declared owner/domain; native checks are construction controls | Self | Required | [EXP-0028](./EXP-0028.md) O17 and G4 admission composition | Satisfied for all six reference families; exact complete production artifact remains G5 | + +## Measurements + +Generated figures below are pre-database RefTime. Estimated ProofSize is the generated conservative value. Under the pinned RocksDB schedule, one read is 25,000,000 RefTime and one write is 100,000,000 RefTime. + +| Useful family owner | RefTime, ps | Estimated ProofSize, bytes | Reads | Writes | Total RefTime with DB, ps | Fragment SHA-256 | +| --- | ---: | ---: | ---: | ---: | ---: | --- | +| `manual_trigger` | 207,502,000 | 15,106 | 14 | 7 | 1,257,502,000 | `9f907372d1872ad4a940a35c3b4eefb23f9f759cca48144a674997b7725f26e8` | +| `address_event_trigger_occurrence` | 230,410,000 | 8,394 | 14 | 7 | 1,280,410,000 | `cb1ff2f5c2519416e294b73bf59f780dd067ab3d375492c5a06768e4836b14c1` | +| `observation_change_trigger_occurrence` | 153,933,000 | 8,299 | 13 | 8 | 1,278,933,000 | `86e6d8814b4097348e56f7ef5bdf0db0d90cc22899997ce56d08a5a4b75971f9` | +| `observation_crossing_trigger_occurrence` | 618,734,000 | 164,188 | 90 | 81 | 10,968,734,000 | `c7256a5f33988a79f11e4e8954914df0655ccb99d06dea055537de426e96555a` | +| `at_time_trigger_occurrence` | 301,091,000 | 8,451 | 20 | 9 | 1,701,091,000 | `7d6dab9da3d48bbfd06399e81ae85a55c293e775e90543d2b3b58c280e8eddc7` | +| `cadenced_trigger_occurrence` | 469,271,000 | 8,450 | 27 | 16 | 2,744,271,000 | `5539c863a837635f076cce0ee2d5b4f7152984878efe5a946fa5331622729939` | + +All six reference-Wasm runs and postconditions passed. The six matching native benchmark tests passed. Seven focused redundancy controls also passed: repeated pending Manual, AddressEvent and ObservationChange; repeated and detector-disabled Crossing; Cadenced latch disablement until Opening; and retained-latch AtTime replacement without retrigger. + +### Source and Worker Ownership Matrix + +| Family | Useful economic/quote owner | Physical source or deferred owner | Redundant-latched disposition | +| --- | --- | --- | --- | +| Manual | `manual_trigger` | Public dispatch owns transaction/lifecycle boundary | Ordinary transaction payment remains; no second Trigger fee/event/activation | +| AddressEvent | `address_event_trigger_occurrence` | Certified producer/host owns source and synchronous consequence; funding accounting remains independent | Filter/Trigger path is skipped when pending; any authorized funding update retains its own source ownership | +| ObservationChange | `observation_change_trigger_occurrence` | Ingress plus fanout base/branch and inclusive page alternative | `IndexedTriggerDetectionDisabled` returns before Actor-state classification; coalesced/page traversal remains worker-owned | +| ObservationCrossing | `observation_crossing_trigger_occurrence` | Crossing base/probes and exact plan branch own the complete materialization unit | Disabled candidate selects generated skip work before Actor-state load; no useful owner is added to the branch again | +| AtTime | `at_time_trigger_occurrence` | Tick cursor branch adds the component-wise maximum temporal occurrence reservation | One-shot state is consumed; an already-pending loaded state exits before family occurrence work | +| Cadenced | `cadenced_trigger_occurrence` | Tick cursor branch adds the component-wise maximum temporal occurrence reservation | Useful latch removes the Trigger deadline; Opening re-arms from current time, so no latched-period detector work accumulates | + +## Derived Metrics + +The component-wise maximum useful Trigger quote across the six families is ObservationCrossing: `618,734,000 / 164,188` pre-DB with 90 reads and 81 writes. This maximum is for bounded quoting/admission identity, not permission to replace family-specific selectors or to add it to an inclusive Crossing branch. Database totals above are comparisons under one pinned schedule, not generated coefficients. + +## Statistical / Variance Notes + +Each fixed profile has no independent count component, so generated output selects the fixed conservative point from 50 steps × 20 repeats. The benchmark postconditions and storage traces decide branch ownership; timing does not prove semantics. No cross-Wasm subtraction, additive reconstruction or outlier deletion is used. + +## Result + +Every admitted Trigger family reaches a measured current-source useful occurrence owner. Runtime quote/fee selection is exhaustive over `TriggerFamily`; physical Observation, Crossing and temporal workers reserve their inclusive branch separately; direct and indexed regressions establish that an existing latch cannot produce a second Actor-specific fee, event or activation. + +## Interpretation + +Useful economic ownership and physical block ownership are related but not additive. Observation/Crossing host or worker methods own traversal and publication; temporal cursor admission explicitly reserves the maximum family occurrence; Manual retains ordinary dispatch Weight. The family coefficient prices one successful User occurrence and public cost quote. Redundant source work that remains necessary for source truth or cursor progress is not mislabeled as a useful Actor occurrence. + +## Decision + +Accepted for the six-family reference binding. Select exactly one family-specific occurrence Weight and Trigger fee only when that family commits `pending_signal: false -> true`. Do not select it for an already-latched occurrence, and do not add it to an inclusive host/fanout/Crossing physical owner. Stage the six generated values above for EXP-0064 composition and EXP-0065 complete regeneration; the installed coefficients remain stale and must not be patched from temporary fragments. + +## Rejected Alternatives and Tradeoffs + +| Candidate | Decision | Reason | +| --- | --- | --- | +| Installed coefficients as current proof | Rejected | Storage and source identity are stale relative to the control-cell tree | +| One family-independent Trigger coefficient | Rejected | ObservationCrossing proof/database work and Cadenced topology differ materially from direct families | +| Charge every accepted source occurrence while pending | Rejected semantically | Reintroduces paid coalescence and duplicate Actor-specific work without new readiness | +| Add occurrence Weight to every deferred worker branch | Rejected | Useful page/Crossing branches already include physical occurrence work; addition double counts | +| Remove indexed membership on latch | Retains EXP-0020 rejection | Mutates authoritative fanout/range traversal; detector-local disablement preserves source topology | + +## Validity + +- `Establishes`: Current reference-Wasm generated useful-occurrence values for all six Trigger families; exhaustive fee selector; physical source/worker ownership partition; focused useful and redundant-latch behavior. +- `Does not establish`: Exact installed production Weight, final production Wasm, generic host implementation bounds, complete Waiting/source/cleanup composition, admission closure or integrated throughput. +- `Known limitations`: Fragments are intentionally uninstalled. Inclusive fanout/Crossing/source files remain stale until complete regeneration; external hosts must preserve their declared synchronous consequence owner. +- `Invalidation triggers`: A changed measured body, Trigger family, detector topology, physical worker composition, fee selector, source/runtime/Wasm identity or acceptance rule requires scoped requalification. +- `Residual qualitative insight`: Price the useful readiness mutation by family, but meter unavoidable source traversal at its physical owner even when the candidate is already latched. + +## Follow-Up / Next Gradient + +Stop measuring useful Trigger families. EXP-0058 owns non-apoptosis scheduler source/destination composition, EXP-0064 owns admission composition, and EXP-0065 owns complete production regeneration. Reopen only for a concrete omitted family/path, a duplicate charge, or a changed measured/worker boundary. + +## Architecture Impact + +No runtime, semantic or storage change. C1 and A0 remain frozen. The accepted six-value occurrence matrix and no-double-count partition become G4/G5 inputs. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: [EXP-0028](./EXP-0028.md) O17 +- `Replaces`: None. +- `Supersedes`: Installed stale occurrence coefficients as current evidence only; not their historical artifact identity. +- `Refines`: [EXP-0020](./EXP-0020.md) Weight validity on the current reference source. +- `Contradicts`: Paid-coalescence occurrence ownership. +- `Depends on`: [EXP-0020](./EXP-0020.md), [EXP-0025](./EXP-0025.md) +- `Baseline of`: None. +- `Confirms`: Six-family useful-only charging and detector-local redundant exclusion. +- `Invalidates`: Treating the installed 2026-09-01 occurrence values as exact current-source bounds. +- `Uses evidence from`: EXP-0020 semantic topology plus the current source-bound measurements above. +- `Transfers question to`: [EXP-0058](./EXP-0058.md) for generic source/destination composition; [EXP-0065](./EXP-0065.md) for exact artifacts. +- `Produces input for`: [EXP-0028](./EXP-0028.md), [EXP-0064](./EXP-0064.md), [EXP-0065](./EXP-0065.md) +- `Reopen trigger`: A falsifier of the declared family/owner/domain or exact artifact identity; cost alone does not reopen frozen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0058.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0058.md new file mode 100644 index 00000000..a3ebe436 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0058.md @@ -0,0 +1,213 @@ +# EXP-0058 — Scheduler Source and Destination Weight Coverage + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | [EXP-0028](./EXP-0028.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-06 | +| Affected domain | Scheduler discovery, Ready source consumption and destination publication compose with exactly one charged physical transition owner. | +| Physical mechanism | Scheduler Source and Destination Weight Coverage | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0025](./EXP-0025.md) — Accepted, physical architecture only | +| Successor experiment | [EXP-0028](./EXP-0028.md) | + +## Decision Question + +Do scheduler discovery, Ready source consumption and destination publication compose with exactly one charged physical transition owner for every non-apoptosis service path, without adding generic work to a complete inclusive owner? + +## Governing Specification + +- `Owners`: `template/pallets/actors/docs/specification.en.md` (sole ownership, lifecycle/Trigger/Pipeline/Action semantics, placement and scheduler); `docs/actors-resource-policy.specification.en.md` (component-wise admission/actual settlement, equal-thirds block policy); `docs/actors-performance-assurance.specification.en.md` (evidence and target). +- `Frozen physical input`: [EXP-0025](./EXP-0025.md) C1 Architecture Freeze. No change to FIFO, ticket chronology, Q1, causal floor, primary/reference ontology, source/successor atomicity or sole execution owner. +- `Accepted inclusive inputs`: [EXP-0039](./EXP-0039.md) owns the reachable Crossing-unavailable full-FIFO refusal; [EXP-0041](./EXP-0041.md) owns complete Pipeline apoptosis; [EXP-0042](./EXP-0042.md) owns complete public close; [EXP-0043](./EXP-0043.md) owns singleton-Block expiry. Generic source work must not be added to those complete owners. +- `Waiting boundary`: [EXP-0063](./EXP-0063.md), through EXP-0055/EXP-0056, owns numeric Waiting publication/removal and heap envelopes. This Leaf owns their placement in the phase matrix, not their independent physical maxima. +- `Semantic oracle`: A0 SHA-256 `8746e423168309975e509be91e4eb06fdf0eb0b43369d75f1ee62202a4341678` remains frozen; use only for relevant semantic equivalence, never as a Weight oracle. + +## Context and Hypothesis + +The canonical service path has four accounting boundaries: one bounded FIFO discovery scan, one loaded Actor-state probe, one Ready source consumption, and one inner transition that publishes exactly one Queue, Waiting or Unsignaled destination or consumes authority in a terminal atom. Direct inner benchmarks intentionally begin after Ready source consumption. The generic Queue destination coefficient must therefore begin from consumed authority; the generic Ready source coefficient must reuse the state already loaded by the actor probe. + +Inspection falsified the old primitive fixtures. `scheduler_paged_append_*` started from a retained Unsignaled primary and therefore included source detachment plus Contract/Funding/Run reads. `scheduler_paged_consume_*` called the discover-canonical helper and reloaded the same service state already owned by `scheduler_actor_state_probe`. The benchmark-only fixtures now call the production consumed-authority seams directly; production execution and storage semantics are unchanged. + +The first complete-owner falsifier was the admission-close path: `live_queue_head` consumed generic discovery before classification, `service_live_queue_entry` consumed the state probe, and `service_admitted_close` added generic Ready consumption to its close coefficient. `ProvisionalEntryAccounting` now snapshots both meters before one live entry. Admission and settlement for `AdmissionDecision::Close` evaluate from that baseline and replace the entry's discovery/probe/source prefix with exactly one complete apoptosis or terminal-close owner. Earlier tombstone work and a later next-head/empty probe remain distinct operations. + +The second falsifier was Crossing unavailability. Zero-Step admission now checks current Crossing availability only after terminal and User Pipeline-capacity precedence. `Unavailable` selects typed `CompleteEntryOwner::CrossingUnavailable`; settlement restores the same per-entry baseline and charges only the complete full-FIFO refusal owner. `Available` carries the observed value/revision into transactional rearm, avoiding a second provider read; `Uninitialized` remains an invariant outside the reference domain. The benchmark is now a production `WeightInfo` surface for EXP-0065 regeneration, while the retained generated file uses the existing conservative Opening fallback rather than a manually copied coefficient. + +The placement audit rejected two apparent service transitions. A successful pause atom already clears `queue_ticket` and demotes the Ready primary to Unsignaled transactionally, leaving only an ordinary stale FIFO tombstone; a simultaneously live paused Ready cell is malformed state, not a production Weight branch. Global-breaker classification is now a state-preserving refusal: the exact Ready primary, ticket and queue counters remain unchanged after the metered discovery/state probe, rather than consuming and republishing the same authority under an unmeasured destination owner. + +Final Waiting overlap reconciliation separates owner selection from numeric-envelope acceptance. A consumed Ready source followed by an unmatched Wakeup placement selects one complete EXP-0055 publication owner; exact Running/Suspended successor and temporal profiles that already publish Waiting remain inclusive and receive no second publication charge. A due Waiting worker owns its own source removal and Ready publication. Terminal/public close selects one complete EXP-0056 populated-removal atom instead of EXP-0042 plus generic source, close or heap work. EXP-0055's pending maximal-cell requalification can change the selected value, but not this sole-owner partition. + +## Baseline and Candidates + +| ID | Candidate | Disposition | +| --- | --- | --- | +| B0 | Retained-primary append and discover-canonical consume fixtures | Rejected as production primitives: each includes already-owned service/source reads | +| A1 | Loaded Ready consume plus consumed-authority Queue publication; choose each branch component-wise under the pinned DB schedule | Measured and retained for nonterminal composition | +| A2 | Add generic discovery/probe/consume to every complete full-FIFO owner | Rejected by accepted EXP-0039/0041/0042/0043 ownership; current accounting still exhibits this open falsifier | +| A3 | Complete-owner settlement that replaces, rather than adds to, provisional generic accounting for terminal/refusal paths | Implemented and retained for admission-time apoptosis/terminal close and Crossing-unavailable refusal | +| A4 | Treat paused and breaker service as consume-and-republish transitions | Rejected: pause already demotes before service, while breaker refusal preserves the live Ready owner in place | + +## Controls + +All deciding primitive profiles use one freshly built reference benchmark Wasm, compiled Wasm execution, the pinned Rust/bencher versions, DB cache 1024, heap pages 4096, worst-case map size 1,000,000, 50 steps and 20 repeats. Setup creates legal System Actors and queue pages outside the measured block. The changed variable is existing versus new Queue page, or preserved versus deleted source page. No concurrent project benchmark/build/test job ran during measurement. + +The destination fixtures remove the target's Unsignaled primary before measurement, retain its exact loaded identity/admission/resource authority, then invoke `preflight_paged_enqueue_authority` and `commit_paged_enqueue`. The source fixtures load the canonical Ready owner before measurement, then invoke `paged_consume_loaded_head_at`, matching `consume_actor_service_source(FifoHead)` after the actor probe. + +## Benchmark Class, Workloads, and Criteria + +| Path | Source owner | Transition/destination owner | Inclusion rule | +| --- | --- | --- | --- | +| Live nonterminal Opening/Running/Suspended | `scheduler_paged_consume_{preserve,delete}_page`, selected component-wise | Matching inner phase/outcome owner; Queue/Waiting publication is already inside direct profiles or selected by `StepControlPlacement` in composed fallback | One source plus one inner owner | +| Paused source | No live service source: the pause atom has already moved the primary to Unsignaled; any old Ready slot is a tombstone | None in actor service | Discovery drains the stale slot; a live paused Ready cell is malformed and must not gain a benchmark owner | +| Global breaker | Discovery and loaded-state probe only; Ready source remains unconsumed | Existing Ready primary, exact ticket and queue counters remain unchanged | State-preserving refusal; no Queue/Waiting/Unsignaled destination publication | +| Crossing current-value refusal | No committed source mutation | Accepted complete full-FIFO refusal from EXP-0039 | Implemented: typed zero-Step admission result settles the complete refusal from the per-entry baseline, with no generic source/inner addition | +| Pipeline apoptosis / admission-time terminal close | Source is physically retired inside complete terminal atom | Accepted complete owner from EXP-0041/0043 and applicable close inputs | Implemented: per-entry settlement replaces provisional discovery/probe/source; no generic source, cleanup or detector addition | +| Due Block/Tick promotion | Waiting worker owns source removal and Ready publication | Inclusive worker branch, then later Ready service under cutoff rules | Never add Queue append separately to the worker | +| Retry/continuation Waiting publication | Ready source is generic for nonterminal service | Matching inner `Wakeup` owner; numeric page/heap envelope comes from EXP-0063 | Never add a second generic Waiting owner to a direct inclusive inner profile | + +Acceptance requires a finite path matrix with one owner per physical operation, component-wise containment for each generic alternative, reference-Wasm evidence for the corrected primitives, and a meter/selector implementation that can select accepted complete owners without additive discovery/probe/source duplication. One duplicated or omitted source/destination operation falsifies the decision. + +## Environment and Artifact Identity + +| Artifact | Identity | +| --- | --- | +| Baseline source | Commit `e92ab804417cdaebbfa44732c1b30a39758894ea`; tree `511ee1af664e4561c5a902d8aa6c01d5930e32e3` plus the declared worktree | +| Measurement source/config identity | `scripts/actors-assurance.sh::worktree_evidence_identity` = `f64048e1f0be558ad06644a7fdfff6b63575768090fb5775d75e53c81f41e240` before this experiment-record update | +| Measured fixture sources | `benchmarking.rs` `767d5bc11d3ee6e2306cc2a72636dc94aee33a6578cb7afd330b96018d2b8156`; `scheduler.rs` `6b41bee6f7fb3f6cadec2d12d32439815eb27b3ce96f50512af07d0ce9685859`; `lib.rs` `7432b3b5df56cf7507d0991270b62a5a35e4437a2548e81bd6bd66bb57d6c788`; `execution.rs` `3f2366e38a9d82e21d23ecac3756fdaf7000033f2c3b02ece05eaf08e93184b9` | +| Runtime configuration | `template/runtime/src/configs/actor_config.rs` SHA-256 `0858133d39eaabb1d2591def2454f1b72d7ee6746b3c1f5ecd720f49a37750f9` | +| Toolchain / bencher | Rust `1.98.0 (88d9e12ae 2026-08-18)`; `frame-omni-bencher 0.22.0`, benchmark CLI 58.0.0 | +| Benchmark Wasm | `4ea53509ff6916f8f07d031f90c4f7847648b0c1357d4fe23f292064166f2724` | +| Backend controls | Compiled Wasm; AMD Ryzen 7 4800H reference host; DB cache 1024; heap pages 4096; worst-case map size 1,000,000; 50 steps × 20 repeats | +| Commands | `./scripts/benchmarks.sh --steps 50 --repeat 20 --extrinsic NAME --output /tmp/deos-exp58-current-NAME.rs --json-file /tmp/deos-exp58-current-NAME.json pallet_deos_actors`; fresh build for the first name, then `--skip-build` | +| Production artifacts | No production Weight or production Wasm generated; temporary fragments remain uninstalled and EXP-0065 owns complete regeneration | + +## Proof Obligations + +- `Freeze`: Frozen finite input. The four accounting boundaries, three destination classes, accepted complete-owner exclusions and named Waiting handoff exhaust the scoped scheduler source/destination question; settlement remains undecided, not unbounded. +- `Review triggers`: Ready/Waiting topology, service-state probe, source-consume seam, placement selector, complete terminal/refusal owner, or meter settlement changes. +- `Decomposition review`: Primitive alternatives and phase composition are evidence for one source/destination ownership claim. Independent Waiting/heap maxima remain in EXP-0063; Run-path inner maxima remain in EXP-0059; settlement containment remains in EXP-0060. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O18 | Scheduler discovery, loaded Ready source consumption and Queue/Waiting/Unsignaled destination publication compose with exactly one charged physical transition owner, while accepted complete full-FIFO branches replace generic accounting. | One production path duplicates or omits a source/destination operation, or one legal alternative exceeds its component-wise selected owner | Reference-Wasm primitive pairs, finite call-path/placement audit, focused meter tests and accepted complete-owner evidence | Self | Required | Parent EXP-0028 O18, EXP-0064 composition and EXP-0065 regeneration | Satisfied for the admitted reference ownership domain; Accepted EXP-0063 supplies numeric maximal-cell qualification without reopening placement | + +## Measurements + +Generated figures are pre-database RefTime and estimated ProofSize. The pinned RocksDB schedule charges 25,000,000 RefTime per read and 100,000,000 per write. + +| Corrected production primitive | RefTime, ps | Estimated ProofSize, bytes | Reads | Writes | Total RefTime with DB, ps | Fragment SHA-256 | Raw JSON SHA-256 | +| --- | ---: | ---: | ---: | ---: | ---: | --- | --- | +| `scheduler_paged_append_existing_page` | 92,471,000 | 13,660 | 5 | 4 | 617,471,000 | `f01bab4eda5ac7f12841d778e7fefaf262cb504869264af62196e34963262ee9` | `1e529c1d5341048c4dcc495e7a3d43c3a718edaa34924eedf42d6099192d79ae` | +| `scheduler_paged_append_new_page` | 72,636,000 | 16,446 | 6 | 4 | 622,636,000 | `1b4561d86d495801a5869a90009191902131863d5d803bbc2be32645d43dc865` | `45127de242c1974aaeeb482a082a758cb05066e6793da14c5f9ad02d2a8ac8cd` | +| `scheduler_paged_consume_preserve_page` | 62,509,000 | 5,118 | 5 | 4 | 587,509,000 | `beb8af0ce6075c15b512c21763ee104c9123c78c9d9a7ff9656f79fa59d6daad` | `d095776d291f0aa35b359a3dc72f6bcbc7b0072d85ee5d3cb95c38e65aaf4910` | +| `scheduler_paged_consume_delete_page` | 60,623,000 | 4,809 | 5 | 4 | 585,623,000 | `3f1aa289d548932ed43b34d2f166381022010d1aa8f0dd845b1b2573e112010e` | `57318163ce39ab03bc51dff3281173cabdbe7df3c7837618128347402ec191a6` | + +The new-page Queue branch has lower pre-DB CPU than existing-page append, but one additional read makes its total RefTime larger under the pinned runtime database schedule; its estimated ProofSize is also larger. It therefore dominates the existing-page branch in both final Weight dimensions for this binding. Preserve-page Ready consumption directly dominates delete-page consumption because both have identical database counts and preserve-page has larger RefTime and ProofSize. + +### Complete-Entry Replacement Settlement + +`ProvisionalEntryAccounting` records the cycle and Actor Control meter positions before one live-entry discovery. The generic path continues to consume discovery, the loaded-state probe, Ready source and its selected inner/destination owner. If loaded classification instead selects `AdmissionDecision::Close`, admission uses a meter projected from the saved baseline; successful or rolled-back close settlement restores that baseline and accrues only the complete close Weight. No coefficient is subtracted, and already-settled work belonging to earlier FIFO entries is preserved. + +The package regression covers `WindowExpired`, `CycleAdmissionInsufficient`, `CycleNonceExhausted`, `ConsecutiveFailures` and `AutoCloseNonceReached` in both Weight dimensions. A one-unit shortfall preserves the source and emits no close; a funded pass consumes the selected complete entry owner plus only the later empty-queue discovery. The runtime minimal-apoptosis fixture now records `4,971,257,226 / 45,716` Actor Control and zero effect for the whole on-idle fixture under the still-stale production file, versus its former additive `6,637,960,839 / 63,707`. The removed `1,666,703,613 / 17,991` is exactly the then-generated discovery + state-probe + component-wise Ready-consume prefix; this comparison diagnoses duplicate accounting and is not a coefficient derivation or accepted production envelope. + +Crossing unavailability uses the same baseline with a typed refusal owner. One-unit RefTime and ProofSize shortfalls preserve the paid head, follower, events, custody and detector state; an ample pass settles exactly the selected complete owner and remains starved without consuming the head. Pipeline insolvency and existing terminal reasons still close before the provider availability selector. Available current state is carried into rearm so selector integration adds no duplicate observation read. + +Validation on the retained complete-entry implementation before the placement-audit slice: all 1,101 non-ignored all-feature Actors tests passed, including corruption, simulation, in-loop close, resource reconciliation, Crossing one-unit refusal boundaries and recovery; Actors and runtime all-target/all-feature Clippy passed. The full default-feature runtime Actors integration target passed 203 tests, including the corrected apoptosis telemetry. Production generated Weight remains untouched. + +The next finite matrix slice proves by canonical-state regression that pause has already produced Unsignaled authority before actor service. Breaker service now consumes exactly one discovery plus loaded-state probe in the mock binding and preserves the byte-identical control cell and exact Ready head/tail/occupancy tuple. This removes the former unmeasured consume/requeue/hold path; focused default-feature regressions and runtime-benchmark compilation pass. Final context-only overlap reconciliation passes canonical formatting, 61-record normalization with all three graph projections, diff checks and the changed-scope completion gate with unchanged runtime tests reused. + +The EXP-0059 trace reconciliation rejects its first categorical fixture family as a composable Run input. Progress/suspension start from and rewrite a retained Ready primary, then touch Waiting occupancy/tail/head/frame, cursor index, cursor length and cursor pages; completion consumes Ready and publishes Unsignaled. Those operations overlap this Leaf's generic Ready source/Queue destination and EXP-0063's Waiting domain. Production Step execution instead consumes Ready before `execute_running_current_step_core` / `commit_loaded_single_step_suspension`, then calls `schedule_next_work_with_authority`. Therefore no `run_* + destination` arithmetic using the measured 14/9 or 8/6 traces is valid. + +The first corrected exact successor follows that production seam. `run_successor_continued_wakeup_low_capture` measures an admitted twelve-Step low-capture Running continuation after Ready consumption. A canonical saturated tombstone span forces Queue fallback into a new Block Waiting key. The complete measured atom is `247,591,000` pre-DB RefTime / `10,074` estimated ProofSize plus 19 reads and 9 writes (`1,622,591,000` total RefTime under pinned RocksDB); fragment/JSON hashes are `b8fbab84bd840fb67d975b99ece52b7a7c519b31e9d140420fd22d105007f220` / `9da72c083ac1e349f81a84e1c705967f50dd81c74096733ae2d50b6bebced243`. It contains Queue-capacity preflight and one Waiting publication but no Ready source write, confirming the intended boundary. + +The next same-Wasm pair measures low-capture `S=12` Running suspension after source consumption. An unfunded fixed Transfer yields `FundingUnavailable` without Task invocation. Exact-next-block retry eligibility selects Queue at `147,927,000 / 6,258 + 11 reads / 5 writes` (`922,927,000` total RefTime); a two-block cooldown selects a new Block Waiting key at `178,238,000 / 6,500 + 15 / 9` (`1,453,238,000` total). The traces differ at their physical Ready versus Waiting publication and neither rewrites the consumed source. EXP-0059 owns payload/extrema interpretation; EXP-0063 still owns populated Waiting alternatives. + +A further fresh same-Wasm pair measures maximum-length low-capture User terminal successors after source consumption. `Completed→Unsignaled` is `104,134,000 / 10,918 + 11 reads / 4 writes` (`779,134,000` total RefTime); permanent pre-effect `Failed→Unsignaled` is `119,151,000 / 11,239 + 12 / 4` (`819,151,000` total) and dominates completion component-wise. Both write exactly one Unsignaled primary and locator, remove Run head/payload and include one post-publication hold reconciliation without a Ready-source write. Source/config identity is `d80ee6269373a4a517bfbc59fcb2f7c34cbca091f3dd1ea932a2384af07810e5`, compressed benchmark Wasm `f1fa52b46e449464271a958a044c4790495e3f5a3063fc6ee6e1bafee2d89672`, and EXP-0059 owns the exact artifact manifest and terminal envelope decision. + +The final dense `S=3` Continued→Wakeup alternative is `250,524,000 / 9,968 + 18 reads / 9 writes`, totaling `1,600,524,000` RefTime under the pinned DB schedule. The existing low-capture `S=12` point totals `1,622,591,000 / 10,074` and therefore contains dense in both final Weight dimensions despite dense's `2,933,000` ps larger pre-DB CPU. EXP-0059 owns source/config identity `3a0db284d5fb1415c365895454d382df9f68cfe71ab3260ee213b345a6d150a3`, compressed benchmark Wasm `a846d4f1fe14cf5c2185069ba7746cfe5ec7ab0d11efea5fdbf3bc9b61725cc5`, exact artifact hashes and the accepted finite successor decision. + +### Waiting Overlap Reconciliation + +The final phase matrix uses mutually exclusive cells rather than arithmetic over aggregate profiles: + +| Production cell | Selected physical owner | Forbidden overlap | +| --- | --- | --- | +| Ready source, exact Running/Suspended successor to Waiting | Accepted complete consumed-source successor from EXP-0059 | No generic Queue destination or second EXP-0055 publication owner | +| Ready source, otherwise-unmatched Wakeup placement | Generic loaded Ready consume plus exactly one EXP-0055 complete publication branch | No separate page, locator, Reference, heap or hold component | +| Temporal/Crossing direct profile that already rearms Waiting | Its matching inclusive direct owner | No generic Waiting publication added again | +| Due Block/Tick worker | Matching inclusive worker branch owns Waiting source drain/repair and Ready or Unsignaled publication | No generic Ready-source or Queue-destination owner | +| Public/terminal close with populated Waiting removal | Exactly one EXP-0056 complete close branch, replacing the narrower EXP-0042/singleton alternatives | No separate close, Run, lifecycle, Waiting, unlink or heap component | + +EXP-0055's current component-wise candidate is `1,052,245,000 / 76,618 +44R/30W`; EXP-0056's accepted complete removal selector is `1,928,069,000 / 96,741 +70R/44W`. These values establish the populated physical alternatives needed to decide placement. EXP-0063 still owns whether the publication value bounds the final admitted encoded-cell domain, and EXP-0065 owns exact generated selection. A later larger publication value requalifies the numeric owner but does not license a second physical owner. + +### Rejected Fixture Measurements + +Before correction, the same prior Wasm measured retained-primary append at `164,689,000 / 15,309` plus 9 reads / 5 writes and `136,333,000 / 18,157` plus 10 / 5. Discover-canonical consumption measured `104,065,000 / 5,599` plus 8 / 4 and `100,992,000 / 5,286` plus 8 / 4. Those generated points are not retained as production primitives: their traces include Contract/Funding/Run or Unsignaled-source work already owned outside the intended boundary. No numeric subtraction is used to transform them. + +## Derived Metrics + +The staged generic nonterminal source owner is the complete `scheduler_paged_consume_preserve_page` Weight. The staged generic Queue destination owner is the complete `scheduler_paged_append_new_page` Weight under the pinned RocksDB schedule. These are independent measured primitives, not differences between full-path aggregates. Their temporary coefficients must not be patched into generated files; EXP-0065 regenerates the complete binding after all G4 decisions. + +## Statistical / Variance Notes + +Each fixed benchmark has no count component, so generated output selects a conservative fixed point from 50 steps × 20 repeats. Existing/new and preserve/delete pairs share one source, Wasm and environment. Database and proof traces decide ownership boundaries; timing does not redefine semantics. The pre-DB CPU/new-page tradeoff is resolved only after the exact reference DB schedule is included. + +## Result + +Corrected reference-Wasm profiles establish a loaded-source Ready consume pair and a consumed-authority Queue destination pair without repeated Actor-state/source reads. Admission-close and Crossing-unavailable duplicate accounting is resolved by per-entry replacement. Paused authority is not a live service branch, breaker refusal preserves Ready, and the overlapping `write_run_state` family is excluded. Accepted EXP-0059 exact successors cover Continued→Wakeup, Suspended→Queue/Wakeup and terminal Completed/Failed→Unsignaled; low-capture Continued→Wakeup contains the measured dense alternative. EXP-0055/0056 populated Waiting evidence closes the remaining placement cells by whole-owner selection without additive publication, removal, close or heap work. + +## Interpretation + +Nonterminal service composes one discovery, one actor probe, one generic source owner and one exact destination-bearing successor owner. Admission-time apoptosis, terminal close and Crossing-unavailable refusal use the distinct replacement path: the complete entry owner settles from the saved per-entry baseline while unrelated prior/later FIFO work remains charged. The typed refusal selector preserves terminal and insolvency precedence and avoids aggregate subtraction. Paused entries reduce to stale-slot discovery because pause already owns Unsignaled publication; breaker refusal preserves the live Ready source and therefore selects no source or destination transition owner. Waiting publication is either already inside that exact successor/direct profile or selected once as the unmatched destination; populated removal similarly replaces narrower complete-close alternatives. Numeric requalification changes a selected Weight, not the physical partition. + +## Decision + +Accepted for the admitted reference ownership domain. Select one loaded Ready source owner and one exact successor/placement owner for generic nonterminal service; use per-entry replacement for complete refusal/terminal owners. Waiting publication is inclusive when already measured by an exact successor or direct profile and otherwise selects exactly one EXP-0055 owner. Populated terminal removal selects exactly one EXP-0056 complete close owner. Reject retained-primary/discover-canonical fixtures, live paused service, cross-boundary `write_run_state` profiles and every additive close/Waiting/heap reconstruction. Accepted EXP-0063 supplies numeric envelopes and EXP-0065 owns regeneration. + +## Rejected Alternatives and Tradeoffs + +| Alternative | Disposition | Reason | +| --- | --- | --- | +| Keep old append fixtures because their larger values are conservative | Rejected | They hide duplicate ownership and bind unrelated source reads to destination publication | +| Keep discover-canonical consume fixtures | Rejected | Production service already loaded and validated the same Actor authority through the state probe | +| Select existing/new by pre-DB CPU alone | Rejected | Weight is two-dimensional and the pinned database read reverses final RefTime ordering | +| Add generic boundaries to accepted complete owners | Rejected | Duplicates physical source work and contradicts accepted child decisions | +| Subtract generic coefficients from complete aggregates | Rejected | Produces an unmeasured synthetic inner owner | +| Patch generated production Weight now | Rejected | EXP-0065 owns complete regeneration after G4 closure | + +## Validity + +- `Establishes`: Corrected generic Ready consume and Queue publication primitives on the named reference source/Wasm; their component-wise branch selectors under the pinned DB schedule; per-entry replacement settlement for admission-time apoptosis, terminal close and Crossing-unavailable refusal; the mutually exclusive Waiting publication/removal placement matrix over the admitted reference ownership domain. +- `Does not establish`: Final Waiting numeric acceptance, complete settlement containment, exact production Weight identity or integrated throughput. +- `Known limitations`: Accepted EXP-0063 closes current-binding maximal-cell Waiting qualification. Accepted complete-owner and successor values remain uninstalled. The production generated file remains stale and conservatively routes the new refusal method through its prior Opening maximum until EXP-0065 regeneration. +- `Invalidation triggers`: Changed queue/control-cell storage, actor-probe boundary, source-consume or publication implementation, DB schedule, complete-owner selector, source/Weight/Wasm identity or acceptance rule. +- `Residual qualitative insight`: Measurement boundaries should follow consumed authority. Conservative overcharge does not prove sole physical ownership when the same operation is named by two owners. + +## Follow-Up / Next Gradient + +Stop this Leaf. Keep matched direct profiles inclusive and preserve the whole-owner Waiting selectors above. EXP-0060 owns settlement containment, Accepted EXP-0063 owns publication numeric qualification, and EXP-0065 owns regeneration. Reopen only for a duplicated/omitted production placement, changed topology/selector, or exact identity invalidation. + +## Architecture Impact + +Benchmark fixtures mirror the production consumed-authority seams, and scheduler meters retain an explicit per-entry replacement boundary for complete admission close and Crossing refusal. The availability selector carries one canonical observation into successful rearm, and the refusal benchmark now participates in future production Weight generation. Pause remains the sole Unsignaled demotion owner; breaker service preserves the Ready cell and FIFO position without republishing it. Exact successors/direct profiles keep inclusive Waiting work; unmatched publication and populated terminal removal select one whole owner each. Generated Weight remains untouched and stale. C1 and A0 remain frozen. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: [EXP-0028](./EXP-0028.md) O18 +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: [EXP-0025](./EXP-0025.md), [EXP-0039](./EXP-0039.md), [EXP-0041](./EXP-0041.md), [EXP-0042](./EXP-0042.md), [EXP-0043](./EXP-0043.md) +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: The old retained-primary/discover-canonical primitive fixture boundary, not the frozen scheduler topology. +- `Uses evidence from`: [EXP-0039](./EXP-0039.md), [EXP-0041](./EXP-0041.md), [EXP-0042](./EXP-0042.md), [EXP-0043](./EXP-0043.md), [EXP-0055](./EXP-0055.md), and [EXP-0056](./EXP-0056.md). +- `Transfers question to`: None; independent findings remain in their named experiments. +- `Produces input for`: [EXP-0028](./EXP-0028.md), [EXP-0064](./EXP-0064.md), [EXP-0065](./EXP-0065.md) +- `Reopen trigger`: A falsifier of this declared owner/domain or exact artifact identity; cost alone does not reopen frozen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0059.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0059.md new file mode 100644 index 00000000..d17ddcc6 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0059.md @@ -0,0 +1,209 @@ +# EXP-0059 — Running and Suspended Control Weight Coverage + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | [EXP-0028](./EXP-0028.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-06 | +| Affected domain | Running and Suspended control includes reconstruction, retry, persistence and completion exactly once over admitted geometry. | +| Physical mechanism | Running and Suspended Control Weight Coverage | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0025](./EXP-0025.md) — Accepted, physical architecture only | +| Successor experiment | [EXP-0028](./EXP-0028.md) | + +## Decision Question + +Running and Suspended control includes reconstruction, retry, persistence and completion exactly once over admitted geometry. + +## Governing Specification + +- `Owners`: `template/pallets/actors/docs/specification.en.md` (sole ownership, lifecycle/Trigger/Pipeline/Action semantics, placement and scheduler); `docs/actors-resource-policy.specification.en.md` (component-wise admission/actual settlement, equal-thirds block policy); `docs/actors-performance-assurance.specification.en.md` (evidence and target). +- `Frozen physical input`: [EXP-0025](./EXP-0025.md) C1 Architecture Freeze. No change to FIFO, ticket chronology, Q1, causal floor, primary/reference ontology, source/successor atomicity or sole execution owner. +- `Semantic oracle`: A0 SHA-256 `8746e423168309975e509be91e4eb06fdf0eb0b43369d75f1ee62202a4341678` remains frozen; use only for relevant semantic equivalence, never as a Weight oracle. + +## Context and Hypothesis + +This Leaf owns the complete Run-state mutation primitive used by composed Running and Suspended fallbacks. The production Step core writes exactly one successor: Running progress, Suspended retry, or Idle completion/failure. The later placement selector separately publishes Queue, Waiting, or Unsignaled authority. Direct `scheduler_inner_*` profiles may already include both operations; only unmatched phase/outcome/placement cells may use a separately measured composition. + +The initial hypothesis was that the existing `run_progress`, `run_suspend`, and `run_complete` fixture family represented that composable mutation boundary. Exact generated storage traces falsified it. The fixtures call benchmark-only `write_run_state` from a retained Ready primary: progress and suspension also maintain a Block wakeup reference, while completion consumes Ready and publishes Unsignaled. Production committed-Step execution instead mutates `ActorRunStateStore` in the core and publishes the successor only afterward through `schedule_next_work_with_authority`. The measured values remain useful admitted-payload and overlap classifiers, but are not production Run coefficients and cannot be repaired by subtraction. + +## Baseline and Candidates + +| Candidate | Scope | Disposition before measurement | +| --- | --- | --- | +| Three `write_run_state` owners | Select one of `run_progress`, `run_suspend`, or `run_complete` by committed successor class | Rejected as composable production owners; their retained-Ready/reference or Unsignaled work crosses the destination boundary | +| Exact consumed-source successor profiles | Measure the complete unmatched committed-Step successor path for each reachable placement class | Primary candidate; follows the production seam without synthetic subtraction | +| Direct `scheduler_inner_*` owner | Complete matched phase/outcome/placement profile | Keep inclusive; never add a Run or destination primitive again | +| Aggregate subtraction | Infer a Run or destination remainder from overlapping profiles | Rejected; overlap cannot produce measured ownership | + +## Controls + +Freeze the retained source/configuration, reference host, Rust/bencher versions, RocksDB schedule, heap pages, map size, 50 steps × 20 repeats and one fresh benchmark Wasm per deciding cohort. Native benchmark tests are construction controls only. Generated fragments remain temporary; EXP-0065 owns installation. + +## Benchmark Class, Workloads, and Criteria + +Acceptance requires a call-path matrix and same-Wasm measurements showing that exactly one owner covers each committed successor and destination handoff without reusing any `write_run_state` cross-boundary work. + +| Successor class | Reachable destination classes after consumed Ready source | Required ownership | +| --- | --- | --- | +| Continued | Queue normally; Waiting on Ready-capacity fallback | Direct inclusive profile where matched; otherwise one exact complete consumed-source successor profile | +| Suspended | Waiting normally; Queue for an exact next-block retry inside the snapshotted cutoff | Direct inclusive profile where matched; otherwise one exact complete consumed-source successor profile | +| Completed or Failed | Unsignaled with no deferred authority; Queue/Waiting for a retained signal or terminal deadline | Direct inclusive/terminal owner where matched; otherwise one exact complete consumed-source successor profile | + +The smallest falsifier is one legal branch where selected accounting omits or duplicates Run head/payload, primary publication, Waiting reference/heap work or applicable hold reconciliation. A fixture that begins from a retained primary after production already consumed it is itself a boundary falsifier. + +## Environment and Artifact Identity + +Preparation baseline: commit `e92ab804417cdaebbfa44732c1b30a39758894ea`, tree `511ee1af664e4561c5a902d8aa6c01d5930e32e3`, plus the declared worktree. The first failed maximum-dense attempt used source/config identity `be4336ff138cfe18507678d0c38f2e560fb31e739e53df38081728e1d19bf608` and benchmark Wasm `c2c4c0e79b80f4d20470768555f9290b137e3ee5404184aa5b849b6b12a5a73e`. The classifier source/config identity is `82ec07d4b0a3e1844e74d1386824ee645c1d5072a2d25f2dbfe6417dc29cc582`; its fresh benchmark Wasm is `94223570344b2526e71e9642bc3a24c90901d57084d41366750fef1de1e47cd8`. + +The categorical owner measurement source/config identity is `a503e5ae751693e1da5876e0ad5ded8ebe753d061d1fb8acfecac6194e2891b9`; fixture files are `benchmarking.rs` SHA-256 `843916cd065c264b2b4a0f96a4b16fcb01928915b68252d23bd39883bb2f736c`, `execution.rs` `3f2366e38a9d82e21d23ecac3756fdaf7000033f2c3b02ece05eaf08e93184b9`, `scheduler.rs` `20a0188594958caf42b46ce5cb4177bb64193ca37575c979432f9c6455395f9b`, runtime config `0858133d39eaabb1d2591def2454f1b72d7ee6746b3c1f5ecd720f49a37750f9`, and benchmark script `0b132dd78725394a3f3a5fb605b9429c991729453e7b4816bd340649d329a7d5`. Toolchain: Rust `1.98.0 (88d9e12ae 2026-08-18)`, `frame-omni-bencher 0.22.0`, benchmark CLI 58.0.0. Fresh categorical benchmark Wasm SHA-256: `c29203b9760f3f704ca1bfd12b56c0c22139d0fb51ba2fa736497b17de4697ca`. + +The first complete-successor candidate uses source/config identity `cf4ed152113a0ede4916c68051530b0c421d00cba27c2beaeba3a6cd52d388be`; `benchmarking.rs` SHA-256 `3fce289b3a38e5919520cd377cde63b36cafe58a4a7c9f548ac6986a537b7896`; unchanged `execution.rs`, `scheduler.rs`, runtime config and benchmark-script identities named above; and fresh compressed benchmark Wasm SHA-256 `b11f3e81c87a821d14ee7ec537cb628cf7fe510745c632f44329484fecfb68cb` (uncompressed `3cb7c712a9b490dec7a9ec9307922f04ef2636c80e3ca1e475d4b0a986228375`). + +The paired low-capture suspension successors use source/config identity `b19422e7235b0fdfa74516d8043a3e92351ef59918e05936f07a71dd6430c3e6`; `benchmarking.rs` SHA-256 `5944f0830b4e714f8fcbfc0d4e4ace25cf6f417fa5eaa913e29d6c286da1051b`; unchanged `execution.rs`, `scheduler.rs`, runtime config and benchmark script identities named above; fresh compressed benchmark Wasm SHA-256 `b86f2ff4cfbf6ec31737dd69890b43e62e3e7aac30ad25f12df110c8c81fbf2e` (uncompressed compact `b23257eeb337c24620ff836e830683c030b8c12bf0ff6afd531edf8cec644926`). The evidence manifest `/tmp/deos-exp59-suspended-successors.sha256` is SHA-256 `45721b95947c5fa7056a9a2195caac4101d19ddbb69df9296f57e281e3ac0108`. + +The paired low-capture terminal successors use source/config identity `d80ee6269373a4a517bfbc59fcb2f7c34cbca091f3dd1ea932a2384af07810e5`; `benchmarking.rs` SHA-256 `ed3232e738c226dc12865164484e03ed9b35e25bf61b9b7eb544638eb4339bd6`; unchanged `execution.rs`, `scheduler.rs`, runtime config and benchmark-script identities named above; fresh compressed benchmark Wasm SHA-256 `f1fa52b46e449464271a958a044c4790495e3f5a3063fc6ee6e1bafee2d89672` (uncompressed compact `36db09d7cfb2a51dd2a428f624b109ee5bdf9552ed23742be07d1f43993ebbc1`). The evidence manifest `/tmp/deos-exp59-terminal-successors.sha256` is SHA-256 `9521620b6953c68cbe6836be9b42d471e25340e033600d28d52ef73db86355cc`. + +The dense continuation containment profile uses source/config identity `3a0db284d5fb1415c365895454d382df9f68cfe71ab3260ee213b345a6d150a3`; `benchmarking.rs` SHA-256 `68276d0698ed810dfe9695a840ee8b5c0cf4de1f903b6efd0d068a252295ec4b`; unchanged `execution.rs` SHA-256 `3f2366e38a9d82e21d23ecac3756fdaf7000033f2c3b02ece05eaf08e93184b9`, `scheduler.rs` `20a0188594958caf42b46ce5cb4177bb64193ca37575c979432f9c6455395f9b`, runtime config `0858133d39eaabb1d2591def2454f1b72d7ee6746b3c1f5ecd720f49a37750f9`, and benchmark script `0b132dd78725394a3f3a5fb605b9429c991729453e7b4816bd340649d329a7d5`. Its fresh compressed benchmark Wasm SHA-256 is `a846d4f1fe14cf5c2185069ba7746cfe5ec7ab0d11efea5fdbf3bc9b61725cc5` (uncompressed compact `b0a929065065d765e70fce575125c81ebb24ce2a81f0b1f49c7c99bb7ce633a2`). The evidence manifest `/tmp/deos-exp59-dense-successor.sha256` is SHA-256 `68ec06fac35536701935eaa52c9638478936318dda5e2c37126d57e87cc8a69a`. + +## Proof Obligations + +- `Freeze`: Frozen finite input: three Run successor classes, three existing mutation profiles, and the finite direct-versus-composed selector boundary. +- `Review triggers`: Run encoding/storage, primary/reference topology, hold reconciliation, phase/outcome/placement selector, or source/Weight/Wasm identity changes. +- `Decomposition review`: EXP-0059 owns Run mutation maxima. EXP-0058 owns source/destination placement; EXP-0063 owns independent Waiting numeric maxima; EXP-0060 owns reservation/actual containment. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O19 | Running and Suspended control includes reconstruction, retry, persistence and completion exactly once over admitted geometry. | One admitted Running/Suspended transition whose reconstruction, retry, persistence, reference, primary publication or applicable hold work lacks one charged owner or receives two | Same-Wasm FRAME evidence, finite call-path matrix and native construction controls | Self | Required | Parent EXP-0028 O19 and EXP-0058 O18 | Satisfied: the finite admitted extrema and exact successor partition are measured; low-capture Continued→Wakeup component-wise contains the dense alternative under the pinned DB schedule | + +## Measurements + +Prepared owner command family: `./scripts/benchmarks.sh --steps 50 --repeat 20 --extrinsic NAME --output /tmp/deos-exp59-NAME.rs --json-file /tmp/deos-exp59-NAME.json pallet_deos_actors`; build fresh for the first accepted owner profile, then reuse that exact Wasm with `--skip-build`. No production file is an output of this experiment. + +The first fresh `run_progress` attempt failed during fixture construction before measurement. `prepare_reachable_running(MaxContractSteps=12)` authored the dense two-Opening-surface/four-predicate-per-Step System Contract, and ordinary admission rejected it with `ContractStepsExceedOnIdleBudget`. No fragment or JSON was produced. Failed log: `/tmp/deos-generate-pallet-deos-actors-weights.5aQnph.log`, SHA-256 `a5380a6697225c717c678d64cd6271ce828a8611b33a59915aa7e6f010389000`. + +Two benchmark-only diagnostics then classified the finite domain without bypassing admission. `run_dense_admission_classifier(s)` invokes the public System constructor for the exact dense shape. Singleton CLI probes use `--extra --steps 2 --repeat 1 --min-duration 0 --low s --high s`; accepted singleton points intentionally reach regression fitting with one unique component after successful setup, while rejected points stop in ordinary creation. The adjacent `s=2..3` probe also produced a complete model, confirming both accepted setup points. `run_low_capture_max_probe` uses public maximum-length creation, Manual readiness and normal Q1 execution. + +| Shape | Ordinary reference-Wasm result | Evidence | +| --- | --- | --- | +| Dense `S=1` | Admitted Contract; not Running-capable after its sole Step | JSON `98f1634ff2febccaa94f252b4b59b9fcc49904275cb4900cc1fd147df48ad068`; log `2f263ff3f0084b272720ef1bf03bb9b448bb6ac638dc7104d6fb38775c2ad117` | +| Dense `S=2` | Admitted | JSON `84caad86e74e88422ec666e6b6631b127136df54c25c36d1446871eb673d8b81`; log `94f17dd19fbac86c98773bb3c0ef6e26202df156ae99397325590d3bfcbac74a` | +| Dense `S=3` | Admitted; largest dense Running-capable point | JSON `ab7e6ea48f5f46729f0e0c61e5f8507ecb762f1ed2283c5f1353f8b49a33117f`; log `136e5a942b920a2544b58d29d52dddfb20fb1503742ba6a91fd6b80c0b077c95` | +| Dense `S=4..12` | Each exact singleton rejected `ContractStepsExceedOnIdleBudget` | Logs in ascending `S`: `dd33a1aa120e091ed2a6d3143fc8f2e7f2139047d6360b5d6bf4858f5038d997`, `3add2bc9c4d893ae653555dcce48ce0d8e7cafd580a2f56e0f7e16492b4e01bd`, `14a6227ad5345567494a4ff976918cc10e5219cbc5ab19404c6fde9d67cc856a`, `38a6304990c033236397fe2b454031bba47e76e587d73cbbecc118dd20d04f36`, `97e18a7910bef4dade5eb69ffe176d2eb2326bfe0d2789fa9d08c855ae4ac8f6`, `25c257f8ead2b800717db22a4251f191f8e5f96c1fbd12a08213ef4351281b21`, `ce578a4a5ba3199f5f30121b79941216130cf1e7ba4f396d15b11d982adcd70d`, `56849ff24e742c625d42ff183c1abc898b97d5fe154839fca069f76fb7773733`, `c26bfb32f5a235584db1f9b8255847c2bb2ec4dffc41c0a30b07625e0e637aff` | +| Low-capture `S=12` | Admitted and reached real Running cursor `1` with empty Opening snapshot, Opening predicate results and funding snapshot | JSON `5ca422395a2010016f7d5c249b6551065c61a1a5e46005beb33d8241801964e7`; log `4aaac1b210edd4f72c2d70850b58df37f3b800fce317f50bf562f72584753584` | + +The evidence manifest `/tmp/deos-exp59-classifier.sha256` has SHA-256 `05582a2ba2fcd2bbf86d6f5bccc11cb0eaac4cc75b8275b3925bbc8cf69acd8e`. Native benchmark controls for both diagnostics pass. Diagnostic load timings are not Run-owner measurements and are not candidates for production installation. + +Six benchmark-only categorical profiles then measure `write_run_state` at the two admitted extrema. Dense progress/completion use the largest admitted fully retained `S=3` Run (six Opening amount entries and twelve Opening predicate results). Dense suspension uses an organically reached `S=3` FundingUnavailable retry with all twelve Opening predicate results and the four Opening amount entries not owned by its fixed retry Step. Low-capture profiles use the ordinary maximum-length `S=12` Contract with empty Opening/funding payload; suspension is likewise reached through normal Q1 execution and due-wakeup collection. All six focused native controls pass. Their timings classify payload extrema only; the later trace audit rejects the measured body as a composable production mutation atom. + +| Category / admitted profile | Generated pre-DB RefTime | Estimated ProofSize | Reads / writes | Diagnostic result | +| --- | ---: | ---: | ---: | --- | +| Progress, dense `S=3` | 197,794,000 | 6,273 | 14 / 9 | Larger ProofSize within rejected fixture family | +| Progress, low-capture `S=12` | 203,870,000 | 6,016 | 14 / 9 | Larger RefTime within rejected fixture family | +| Suspension, dense `S=3` | 208,479,000 | 6,299 | 14 / 9 | Dominates low-capture within rejected fixture family | +| Suspension, low-capture `S=12` | 196,118,000 | 6,088 | 14 / 9 | Contained within rejected fixture family | +| Completion, dense `S=3` | 176,003,000 | 6,054 | 8 / 6 | Dominates low-capture within rejected fixture family | +| Completion, low-capture `S=12` | 135,284,000 | 5,797 | 8 / 6 | Contained within rejected fixture family | + +The categorical evidence manifest `/tmp/deos-exp59-run-owner.sha256` has SHA-256 `760d6f0bfefed2f3d3108ccd246d1f12a6650f748a9cf736b2948a21b41c722c`; the record retains each generated fragment/JSON/log hash through that manifest. Two discarded pre-final Wasm attempts exposed fixture-only assumptions: Opening results were incorrectly required to be uniformly false, and due-wakeup drain was incorrectly required to discover no host-bootstrap actor besides the target. The final fixture instead checks exact target payload/authority while admitting unrelated real host bootstrap topology; failed full logs are SHA-256 `089eef2fad4d11469ec42e05d338775fc6440b40a828bd7cd71a69b73a4b769b` and `3eb4906768a769548947f2c1c2c2d347e9328d856c69b6b500e9b1d4d99be11d`. Neither failed artifact contributes numeric evidence. + +The exact generated storage comments decide the ownership question without a timing rerun: + +| Fixture category | Generated storage shape | Production-boundary conflict | +| --- | --- | --- | +| Progress / suspension, both extrema | `ActorControlLocators` read; `ActorReadyFrameChunks` read/write; Contract head, Funding, Run head/payload and tail reads; Run-head write; Waiting occupancy/tail/head/frame, cursor index, cursor length and cursor page each read/write | Starts from and rewrites a retained Ready primary, then appends/repairs a Block wakeup reference and heap. Production Step service has already consumed Ready; the core writes the Run successor before the separate Queue/Waiting publication. | +| Completion, both extrema | Locator read/write; Ready frame read/write; Contract head, Funding, Run head/payload and tail reads; Run head/payload removal; Ready occupancy read/write; Unsignaled write | Consumes Ready and publishes Unsignaled inside `write_run_state`, duplicating the generic source and destination boundaries when composed. | + +This directly overlaps EXP-0058's measured Queue/Ready primitives: Queue publication owns Ready frame/locator/occupancy work, while generic Ready consumption owns Ready frame/locator/occupancy removal. It also enters EXP-0063's Waiting page/index/heap domain. The production source locators are `execution.rs::execute_running_current_step_core`, `execution.rs::commit_loaded_single_step_suspension`, and `scheduler.rs::schedule_next_work_with_authority`; the rejected fixture body is `execution.rs::write_run_state`. No coefficient subtraction can recover the core-only cost. + +The first replacement candidate, `run_successor_continued_wakeup_low_capture`, starts from an ordinarily admitted twelve-Step low-capture Running source, loads and consumes its Ready authority before the measured block, and executes the production Step-and-placement atom. A canonical full-span tombstone FIFO forces Queue-capacity fallback; the continued Run is published once into a new Block Waiting key. The generated point is `247,591,000` pre-DB RefTime / `10,074` estimated ProofSize plus 19 reads and 9 writes, totaling `1,622,591,000` RefTime under the pinned RocksDB schedule. Fragment SHA-256 is `b8fbab84bd840fb67d975b99ece52b7a7c519b31e9d140420fd22d105007f220`; raw JSON SHA-256 is `9da72c083ac1e349f81a84e1c705967f50dd81c74096733ae2d50b6bebced243`. + +Its trace covers Contract head/current tail, Run-head update, Queue-capacity preflight, new Waiting primary/page/index/heap publication, Unsignaled absence check and state-hold read. It performs no Ready source write because that source was consumed before measurement. The all-feature native regression also validates the resulting canonical state under try-runtime. This is an exact complete successor for one admitted low-capture placement cell, not a complete Waiting population envelope and not evidence that the remaining dense cell is contained. + +Two same-Wasm low-capture `S=12` Running suspension profiles use an ordinarily admitted fixed Transfer over a certified non-native but unfunded asset. Both start after Ready source consumption and reach `FundingUnavailable` without invoking the Task effect. Cooldown zero makes first-retry eligibility exactly `now+1`, which the snapshotted cutoff publishes directly to Queue; cooldown two publishes a new Block Waiting primary. The generated complete points are: + +| Exact suspension successor | Pre-DB RefTime | Estimated ProofSize | Reads / writes | Total RefTime with DB | Fragment / JSON SHA-256 | +| --- | ---: | ---: | ---: | ---: | --- | +| `Suspended→Queue`, low-capture `S=12` | 147,927,000 | 6,258 | 11 / 5 | 922,927,000 | `84bb851360a4c8d2cd049e4a7ba0b86fb57da497b918e66dfe7fdd5621d337d8` / `1daf4a07f6dec123d01944dcc9accdf64b3fe674d74ace81603d549632915a85` | +| `Suspended→Wakeup`, low-capture `S=12` | 178,238,000 | 6,500 | 15 / 9 | 1,453,238,000 | `5be68268c6d66309ebf60b503e0ac7563ce5ec8f808e041ddb1db9ced1500036` / `cbe682434686cc0ecc5ce6a040bb711ff2f904c6011cff702261c9d84d73868b` | + +The Queue trace writes one Ready frame/locator/occupancy successor; the Wakeup trace instead writes one new Waiting primary/page/index/heap successor. Neither rewrites the consumed Ready source. Their exact branch difference is semantic eligibility under one fixture and Wasm, not an inferred destination subtraction. + +Two more same-Wasm `S=12` User Running profiles begin after Ready source consumption and terminate at cursor one. Successful `StopCycle` and a permanently failed admitted `Unstake` whose empty receipt mapping disappears after Opening both clear Run authority, publish exactly one Unsignaled primary and execute one post-publication state-hold reconciliation. The generated complete points are: + +| Exact terminal successor | Pre-DB RefTime | Estimated ProofSize | Reads / writes | Total RefTime with DB | Fragment / JSON SHA-256 | +| --- | ---: | ---: | ---: | ---: | --- | +| `Completed→Unsignaled`, low-capture `S=12` User | 104,134,000 | 10,918 | 11 / 4 | 779,134,000 | `b3f614efe32e07944432515b8148c426f21567e26fd1ff7e12c4deb62bad1f9c` / `810cdafc3007c4bf72b70d9397aec7b8025fcfaed6e21b1369a3bec6fbd8ceba` | +| `Failed→Unsignaled`, low-capture `S=12` User | 119,151,000 | 11,239 | 12 / 4 | 819,151,000 | `db3492e0727b329210f20c7048e9672bfc4009cc3a774ec3b258c283796fa693` / `b4c7340a7d8e91dcd6d87df176bc065e25739584f6e71997c75955212129bc51` | + +The failed terminal point component-wise dominates successful completion and adds only the permanent-failure host asset read. Both traces write the locator, Unsignaled cell and Run head/payload exactly once, read the three maximum-length Contract tail chunks during hold reconciliation, and contain no Ready-source write. This pair decides the low-capture terminal partition without subtracting the Unsignaled destination or adding a standalone hold coefficient. The first CLI attempt omitted `--extra` and found no matching benchmark; it produced no numeric evidence, and its retained full log hash is `aa6e0e153be562008cbe9a5fd961622f9f81eaf0b8a25aa941a30d01adaa15c8`. + +The final dense `S=3` Continued→Wakeup profile follows the same complete consumed-source seam and saturated-tombstone Queue fallback as the low-capture candidate. It measures `250,524,000` pre-DB RefTime / `9,968` estimated ProofSize plus 18 reads and 9 writes, totaling `1,600,524,000` RefTime under the pinned RocksDB schedule. Fragment/JSON hashes are `72ae3f74973192ce4683ac769fb8e3d201a2c2a8ecfbc122bc8880db150fcdf2` / `f4f1cd92268a4a586c96619e671a5874769cdcef223d7879bc5450370fd156c8`. Dense CPU is `2,933,000` ps higher before DB, but one fewer read makes its final RefTime `22,067,000` ps lower and its ProofSize is 106 bytes lower. Therefore the already-measured low-capture Continued→Wakeup owner (`1,622,591,000 / 10,074`) component-wise contains dense geometry; no synthetic maximum or second coefficient is needed. + +Retained-source validation passes the dense benchmark control and the extrema complete-successor regression. The full Actors all-feature suite passes 1,118 tests with five declared ignores; Actors all-target/all-feature Clippy passes with warnings denied. Canonical formatting, experiment normalization/dependency graphs, diff checks and the changed-scope completion gate pass. All successful 50×20 profiles execute on their named fresh compiled Wasm. Runtime execution is unchanged and the source diff is benchmark-only. No production Weight or artifact is installed. + +## Derived Metrics + +Within the rejected `write_run_state` fixture family, the diagnostic extrema are progress `203,870,000 RefTime / 6,273 ProofSize + 14 reads / 9 writes`, suspension `208,479,000 / 6,299 + 14 / 9`, and completion `176,003,000 / 6,054 + 8 / 6`, all pre-DB. They establish that dense retained payload and low-capture maximum tail are competing geometry inputs, but are not staged production coefficients. The accepted Continued→Wakeup selector is the complete low-capture `247,591,000 / 10,074 + 19 / 9` profile because its final pinned-DB Weight component-wise contains dense `250,524,000 / 9,968 + 18 / 9`; no subtraction or additive composition is permitted. + +## Statistical / Variance Notes + +Retain each item’s original sampling and uncertainty. Singleton construction probes establish only their admitted state/access witness, never statistical timing dominance or a production bound. + +## Result + +The successor partition and admitted classifier are frozen. Dense Running-capable geometry is admitted through `S=3` and rejected independently at every `S=4..12`, while an ordinary low-capture `S=12` Contract reaches real Running authority. Exact traces falsify the categorical body's claimed composability. Corrected complete-path measurements establish Continued→Wakeup, both first-retry Suspended→Queue/Wakeup and terminal Completed/Failed→Unsignaled successors from consumed Ready authority. The low-capture Continued→Wakeup profile component-wise contains the measured dense alternative under the pinned DB schedule, closing the finite Run successor envelope. + +## Interpretation + +A matched direct `scheduler_inner_*` profile remains inclusive and replaces composed Run/destination accounting. For unmatched cells, the safe unit is an exact complete consumed-source successor profile. A synthetic `core-only + destination` split is admissible only if both sides are independently measured at their real seams; the rejected `write_run_state` values cannot supply the core side. + +## Decision + +Accepted for the admitted reference domain. Retain the finite admission classification and the six `write_run_state` results only as diagnostic extrema. Select the exact low-capture Running Continued→Wakeup owner at `247,591,000 / 10,074 + 19 reads / 9 writes` pre-DB because its `1,622,591,000 / 10,074` pinned-DB Weight component-wise contains dense `S=3` at `1,600,524,000 / 9,968`. Retain the measured low-capture Suspended→Queue/Wakeup successors; select Failed→Unsignaled over Completed→Unsignaled because it dominates component-wise. Reject categorical cross-boundary owners and all aggregate subtraction. This does not reduce the semantic twelve-Step limit. EXP-0065 owns complete regeneration. + +## Rejected Alternatives and Tradeoffs + +| Alternative | Disposition | Reason | +| --- | --- | --- | +| Install the 14/9 and 8/6 category extrema as `run_*` | Rejected | The measured body rewrites a retained Ready primary and performs destination/reference work absent from the core-only production seam | +| Add EXP-0058 Queue/Waiting/Unsignaled owners to those extrema | Rejected | Duplicates locator, primary-cell, occupancy or Waiting heap operations | +| Subtract overlapping destination traces | Rejected | Produces an unmeasured synthetic remainder and loses proof/branch covariance | +| Repeat the same fixture with more samples | Stopped | Sampling cannot correct a physical ownership mismatch | + +## Validity + +- `Establishes`: The admitted dense/low-capture extrema, exact cross-boundary storage traces, rejection of `write_run_state` as a composable owner, the complete finite Running/Suspended successor envelope, and component-wise containment of dense Continued→Wakeup by the selected low-capture owner under the pinned DB schedule. +- `Does not establish`: Complete Waiting population envelopes, final cross-phase placement composition, complete production Weight, artifact closure or integrated throughput. +- `Known limitations`: Accepted EXP-0063 now owns complete Waiting population envelopes; accepted successor values remain uninstalled until EXP-0065 regeneration. +- `Invalidation triggers`: A changed production core, placement seam, Run encoding, host domain, source/Weight/Wasm identity or acceptance rule requires scoped requalification. +- `Residual qualitative insight`: A larger conservative fixture is not a valid owner when it crosses already-owned authority boundaries. + +## Follow-Up / Next Gradient + +Stop this Leaf. Feed the accepted successor selector into EXP-0028/EXP-0058 and defer complete Waiting population extrema to EXP-0063. Do not rerun classifiers, categorical profiles or accepted successors unless an invalidation trigger fires. EXP-0065 owns generated installation. + +## Architecture Impact + +No runtime, semantic, storage or production binding change. The benchmark source now labels the six extra `write_run_state` profiles as diagnostic overlap classifiers so future regeneration cannot mistake them for production primitives. C1 and A0 remain frozen. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: [EXP-0028](./EXP-0028.md) O19 +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: [EXP-0025](./EXP-0025.md) +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: The `write_run_state` categorical fixture boundary as a composable production Run owner; the measured diagnostic observations remain retained. +- `Uses evidence from`: [EXP-0058](./EXP-0058.md) for exact generic Ready/Queue trace ownership and [EXP-0063](./EXP-0063.md) for the declared Waiting boundary only. +- `Transfers question to`: None; independent findings require an explicit new transfer. +- `Produces input for`: [EXP-0028](./EXP-0028.md), [EXP-0058](./EXP-0058.md), [EXP-0065](./EXP-0065.md) +- `Reopen trigger`: A falsifier of this declared owner/domain or exact artifact identity; cost alone does not reopen frozen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0060.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0060.md new file mode 100644 index 00000000..6fad45ba --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0060.md @@ -0,0 +1,147 @@ +# EXP-0060 — Control and Effect Settlement Containment + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | [EXP-0028](./EXP-0028.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-06 | +| Affected domain | The control/effect settlement selector remains component-wise contained by its matching reservation and charges no effect for NotInvoked. | +| Physical mechanism | Control and Effect Settlement Containment | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0025](./EXP-0025.md) — Accepted, physical architecture only | +| Successor experiment | [EXP-0028](./EXP-0028.md) | + +## Decision Question + +Does the current control/effect settlement boundary return only actual Weight component-wise contained by its matching reservation, with zero Task-effect Weight for `NotInvoked`? + +## Governing Specification + +- `Owners`: `template/pallets/actors/docs/specification.en.md` (sole ownership, lifecycle/Trigger/Pipeline/Action semantics, placement and scheduler); `docs/actors-resource-policy.specification.en.md` (component-wise admission/actual settlement, equal-thirds block policy); `docs/actors-performance-assurance.specification.en.md` (evidence and target). +- `Frozen physical input`: [EXP-0025](./EXP-0025.md) C1 Architecture Freeze. No change to FIFO, ticket chronology, Q1, causal floor, primary/reference ontology, source/successor atomicity or sole execution owner. +- `Semantic oracle`: A0 SHA-256 `8746e423168309975e509be91e4eb06fdf0eb0b43369d75f1ee62202a4341678` remains frozen; use only for relevant semantic equivalence, never as a Weight oracle. + +## Context and Hypothesis + +The phase-specific numeric owners are decided by the other EXP-0028 children. This Leaf decides only their common post-dispatch trust boundary: the runtime effect provider must be total over the configured `Task` shapes, `NotInvoked` must select zero, every returned Control actual must fit the supplied matching maximum, and the resource ledger must reject an overrun atomically. A numeric underbound in an owning child remains that child’s or EXP-0065’s artifact problem; it is not hidden by this settlement proof. + +## Baseline and Candidates + +The retained candidate is the existing typed selector chain: `TaskEffectWeightProvider` maps `NotInvoked` to zero and `Invoked` to the same family method as maximum admission; `RuntimeStepControlWeight` returns a Control actual only after a component-wise comparison with the supplied maximum; scheduler call sites independently reject missing or excessive evidence; and `BlockResourceUsage::settle` performs the final reservation check. The rejected alternative is accepting absent or over-reservation evidence and relying on incidental budget margin. + +## Controls + +Source semantics, configured bounds, generated Weight inputs, host adapters and resource partition remain unchanged. The audit varies only the typed execution branch or one resource component. Native rollback witnesses use the same admitted one-Step Transfer and add exactly one unit to either RefTime or ProofSize. No benchmark body, production Weight or runtime behavior changes. + +## Benchmark Class, Workloads, and Criteria + +This is deterministic selector and transaction-boundary evidence rather than timing measurement. Acceptance requires all configured Task-family shapes to map `NotInvoked → 0` and `Invoked → matching maximum`; every successful Control actual and resource settlement to satisfy `actual.all_lte(maximum)`; and independent one-unit RefTime/ProofSize overruns in either Control or effect to roll back the complete attempt without authority, event, custody or recipient mutation. + +## Environment and Artifact Identity + +Pinned repository toolchain and locked workspace dependencies were used with `SKIP_WASM_BUILD=1`; no Wasm or generated Weight artifact was produced. Deciding source SHA-256 values are: runtime provider `0858133d39eaabb1d2591def2454f1b72d7ee6746b3c1f5ecd720f49a37750f9`, runtime matrix tests `31bee9a20b7f6f86d8bcf6267cb1b592b04ab2c9490d991f444e52027c5b8cf3`, scheduler `20a0188594958caf42b46ce5cb4177bb64193ca37575c979432f9c6455395f9b`, resource ledger `61c71fb7169c3c3feb25c0a75f5d906170d25ca41129df2ccc1d729a33a195a4`, and package rollback tests `fe1e73ed24484922bd0247f769b5851432a1458eb58417313cf3a47319b97ae5`. + +## Proof Obligations + +- `Freeze`: Frozen to the typed production selector and settlement boundary identified above. +- `Review triggers`: Any `Task` variant, provider mapping, Control actual selector, scheduler settlement call site, reservation semantics or relevant configured bound changes. +- `Decomposition review`: One Leaf remains sufficient because effect selection, Control selection and final settlement are consecutive defenses for the same post-dispatch containment claim; their numeric phase owners remain independently decided children of EXP-0028. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | The control/effect settlement selector remains component-wise contained by its matching reservation and charges no effect for NotInvoked. | One typed NotInvoked or Invoked actual exceeding its matching control/effect reservation in either resource dimension | Production-provider source audit, finite native selector matrix and transactional one-unit rollback witnesses | Self | Required | [EXP-0028](./EXP-0028.md) O20 | Satisfied for the declared production provider and settlement boundary; exact generated artifact identity remains EXP-0065 | + +## Measurements + +No timing measurement is required: the deciding property is exact typed equality, component-wise ordering and rollback. + +| Witness | Finite domain | Result | +| --- | --- | --- | +| Production Task effect selector | All 12 `Task` families; every configured SplitTransfer cardinality `0..=8`; both execution variants | `NotInvoked` returns zero; `Invoked` equals the same family/cardinality maximum; every maximum fits the Actor effect turn | +| Production Control selector | Opening, Running and Suspended exact branch regressions, including receipt and collection partitions | Every returned actual is component-wise at most the supplied matching maximum; mismatched Opening maximum or invalid collection evidence returns `None` | +| Resource settlement grid | Maximum RefTime/ProofSize `0..=4` × actual RefTime/ProofSize `0..=5`, 900 combinations | Settlement succeeds exactly when `actual.all_lte(maximum)` | +| Complete-attempt rollback | Control/effect × one RefTime/one ProofSize unit above reservation, four cases | Attempt starves and rolls back; storage root, recipient balance, events and live FIFO authority are unchanged | +| Atomic pair settlement | Valid pair, effect overrun after Control settlement, then valid retry | Failed second settlement restores both ledger state and one-shot reservation authorities | + +Commands passed: + +- `scripts/ci-local.sh --only tests --package deos-runtime --test-filter task_effect_weight_provider_owns_all_task_families` +- `scripts/ci-local.sh --only tests --package deos-runtime --test-filter runtime_step_control_weight_identity_commits_every_staged_production_branch` +- `scripts/ci-local.sh --only tests --package deos-runtime --test-filter action_collection_control_is_reserved_and_settled_once_in_each_phase` +- `scripts/ci-local.sh --only tests --package pallet-deos-actors --all-features --test-filter greater_than_reserved_actual` +- `scripts/ci-local.sh --only tests --package pallet-deos-actors --all-features --test-filter settlement_grid_accepts_exactly_component_wise_actuals` +- `scripts/ci-local.sh --only tests --package pallet-deos-actors --all-features --test-filter actor_step_pair` +- `scripts/ci-local.sh --only clippy --package pallet-deos-actors --all-features` +- `scripts/ci-local.sh --only clippy --package deos-runtime --all-features` +- `scripts/ci-local.sh --only format --fix` +- `./.agents/skills/alignment/scripts/completion-gate.sh` + +The full `scripts/actors-assurance.sh --quick` profile was unavailable because no production runtime Wasm exists in the worktree. This does not weaken the selector decision: no Wasm or generated binding is claimed here, and production-artifact assurance remains explicitly owned by EXP-0065. + +## Derived Metrics + +The effect selector has two closed post-dispatch states. Across 20 configured Task-family/cardinality shapes, the equality matrix contains 40 exact outcomes: 20 zero `NotInvoked` values and 20 `Invoked == maximum` values. This count is a coverage statement, not performance evidence. + +## Statistical / Variance Notes + +Not applicable. All deciding assertions are deterministic typed values, exact storage roots or exhaustive small integer grids; no timing sample or fitted coefficient is interpreted. + +## Result + +The production effect provider is exhaustive over the current `Task` enum and SplitTransfer bound. Its two branches are definitionally contained: zero or the matching maximum. `RuntimeStepControlWeight::actual_control_weight` validates the supplied Opening maximum where required, rejects inconsistent collection evidence, and exposes an actual only through `actual.all_lte(maximum).then_some(actual)`. All scheduler execution paths independently compare provider output with the reserved value before fee collection and commit evidence. The resource ledger repeats the same component-wise check, and pair settlement restores both lanes if either lane fails. + +The new finite provider assertions and two-dimensional one-unit transaction witnesses pass. Existing exhaustive settlement-grid and phase-selector regressions also pass. No numeric owner, generated coefficient, fee rule or runtime transition changed. + +## Interpretation + +Containment does not depend on spare RefTime, spare ProofSize or an aggregate allowance. `NotInvoked` releases the entire effect reservation, while `Invoked` cannot select a different or larger family method than admission. A missing, malformed or over-reservation Control/effect actual fails closed before the attempt can retain semantic, fee or FIFO mutation. Therefore the common settlement boundary is sufficient for the admitted current provider domain; numeric soundness and exact artifact identity remain with their declared downstream owners. + +## Decision + +Accepted for the declared production provider and settlement boundary. Select zero effect for `NotInvoked`, the matching admitted Task-family maximum for `Invoked`, and only component-wise-contained Control actuals. Preserve scheduler and ledger rejection as independent defenses. This satisfies EXP-0028 O20 without installing or endorsing any partial Weight fragment; EXP-0065 still owns exact regeneration and production artifact identity. + +## Rejected Alternatives and Tradeoffs + +| Shortcut | Disposition | Reason | +| --- | --- | --- | +| Treat missing actual evidence as the reserved maximum | Rejected | It conceals a broken provider contract; scheduler service must fail closed and preserve authority | +| Accept scalar total containment | Rejected | RefTime and ProofSize are independently binding consensus resources | +| Rely only on the provider’s comparison | Rejected | Scheduler and resource-ledger checks preserve defense in depth across host implementations | +| Re-measure every phase owner here | Rejected | This Leaf decides settlement algebra; timing and physical ownership remain with the other EXP-0028 children and EXP-0065 | + +## Validity + +- `Establishes`: Exact `NotInvoked`/`Invoked` Task-effect selection, successful Control-actual containment, final ledger containment and atomic overrun rollback for the current declared provider/domain. +- `Does not establish`: Numeric soundness of an independently measured phase owner, final generated Weight or Wasm identity, throughput, release acceptance, or a broader future Task/provider domain. +- `Known limitations`: Current generated Weight inputs remain staged and exact full-file regeneration remains G5. Split cardinalities below semantic admission minima are tested only because the provider and production identity deliberately cover the full configured bounded representation. +- `Invalidation triggers`: A changed Task enum, SplitTransfer bound, provider selector, Control branch/receipt/collection selector, scheduler settlement path, resource-ledger comparison, Weight identity or relevant host configuration. +- `Residual qualitative insight`: Keep validity as typed component-wise evidence rather than converting post-dispatch observations into a scalar fee or a new phase owner. + +## Follow-Up / Next Gradient + +Stop this Leaf. Accepted EXP-0028 composes O20, Accepted EXP-0029 supplies reachability and Accepted EXP-0063 supplies Waiting numeric qualification. Remaining G4 work is EXP-0064 admission composition; exact regeneration remains EXP-0065. + +## Architecture Impact + +No runtime, semantic, storage, fee or production-Weight change. Test coverage now exhausts the production Task-effect branch/cardinality surface and independently proves one-unit RefTime and ProofSize overrun rollback. Package architecture already describes the accepted zero/nonzero effect and fail-closed settlement boundary. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: [EXP-0028](./EXP-0028.md) O20 +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: [EXP-0025](./EXP-0025.md) +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: None. +- `Transfers question to`: None. +- `Produces input for`: [EXP-0028](./EXP-0028.md) +- `Reopen trigger`: A falsifier of the declared provider/domain or settlement boundary; cost or stale final artifact identity transfers to the owning numeric/binding experiment and does not reopen frozen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0061.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0061.md new file mode 100644 index 00000000..86b6b71e --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0061.md @@ -0,0 +1,181 @@ +# EXP-0061 — Joint Encoded Actor Control Cell Reachability + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | [EXP-0029](./EXP-0029.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-06 | +| Affected domain | Joint Encoded Actor Control Cell Reachability | +| Physical mechanism | Joint Encoded Actor Control Cell Reachability | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0025](./EXP-0025.md) — Accepted, physical architecture only | +| Successor experiment | [EXP-0029](./EXP-0029.md) | + + +## Decision Question + +The maximum claimed encoded ActorControlCell is jointly attainable under the host lifecycle, pointer and compact Weight constraints. + +## Governing Specification + +- `Owners`: `template/pallets/actors/docs/specification.en.md` (sole ownership, lifecycle/Trigger/Pipeline/Action semantics, placement and scheduler); `docs/actors-resource-policy.specification.en.md` (component-wise admission/actual settlement, equal-thirds block policy); `docs/actors-performance-assurance.specification.en.md` (evidence and target). +- `Frozen physical input`: [EXP-0025](./EXP-0025.md) C1 Architecture Freeze. No change to FIFO, ticket chronology, Q1, causal floor, primary/reference ontology, source/successor atomicity or sole execution owner. +- `Semantic oracle`: A0 SHA-256 `8746e423168309975e509be91e4eb06fdf0eb0b43369d75f1ee62202a4341678` remains frozen; use only for relevant semantic equivalence, never as a Weight oracle. + +## Context and Hypothesis + +The source qualification rejects treating independent SCALE maxima as an admitted control cell. ActorControlCell contains fixed identity/hot fields, one admission certificate and one Step resource envelope; Contract tails are external. The certificate carries five hashes, two versions, one host-authority lifecycle Weight and PhantomData, not a resource vector. Its lifecycle Weight is fixed by RuntimeAdmissionCertificateAuthority for this binding, independent of authored Contract length. The remaining hypothesis is that a public lifecycle history attains the largest jointly legal temporal/options shape at the required compact resource widths. + +## Baseline and Candidates + +Retain the current frozen C1 implementation. Compare the nominal type encoding domain with ordinary admitted temporal lifecycle states on the same reference host. This is a reachability certificate, not a candidate storage change or a CPU benchmark. + +## Controls + +Reference AccountId32 and u32 BlockNumber; existing runtime limits, generated Weight and admission authority; unchanged genesis Actors. Use public creation/replacement, certified funding, real Trigger service and actual Temporary failure. No injected cell, pointer, latch, Run, resource value or admission bypass may support reachability. Mock results qualify only their own host. Read SCALE lengths directly from stored cells and their fields; source constants alone are not an attained-state witness. + +## Benchmark Class, Workloads, and Criteria + +Primary evidence is native and reference-Wasm public lifecycle construction, with source-derived encoding constraints. No timing fit is needed to decide this Leaf. + +| Bounded witness | Role in the same joint-encoding claim | +| --- | --- | +| Cadenced Mutable System Actor with two real suspended attempts | Test coexistence of process and Trigger temporal pointers, eligibility, last-cycle state and nonzero Step resources; temporal windows and nonzero cooldown are forbidden | +| AtTime before consumption, after Temporary retries and after Contract replacement | Determine whether its extra state byte can coexist with the other longest fields; do not independently maximize incompatible histories | +| Exact lifecycle-authority Weight and admitted Step control/effect values | Qualify compact encoding widths under the same binding rather than arbitrary u64 maxima | + +Acceptance requires a source-derived upper bound and an ordinary stored-state witness attaining it, or an explicit scoped bound with a named unclosed gap. A proposed maximum rejected by ordinary admission or incompatible field coexistence is not repaired by shortening unrelated Contracts or injecting storage. Stop sampling any witnessed option combination; investigate only a named unresolved width/coexistence claim. + +## Environment and Artifact Identity + +Preparation source commit `6c650695568a6e5d05437351ef014e8ee950c484`. The table below records preparation inputs; subsequent public-constructor measurements used benchmark-source SHA-256 `24df45ef2a63d7af1f9f3dbc7d27c8b224fc49d78bed6e6e16e5ea572d700872` and compressed benchmark Wasm `14f2f80f7e3ef7d6d161ab3d1b71933fbb25b4524811e6882fd94eeccc4d9a06`. Cargo.lock pins sp-weights 36.0.0; its Weight encodes both u64 fields compactly. These are diagnostic benchmark artifacts, not production release bindings. + +| Source | SHA-256 | +| --- | --- | +| `template/pallets/actors/src/lib.rs` | `7432b3b5df56cf7507d0991270b62a5a35e4437a2548e81bd6bd66bb57d6c788` | +| `template/pallets/actors/src/scheduler.rs` | `8ed61e1a5c009eb7a940e4fc9f816c74d131a502ffa49d13d88db9e7099a95cd` | +| `template/pallets/actors/src/types/contract.rs` | `3827412b44320a393ac5d1f2c5b90fb9afd2312fc8888bea66dbf88fe376d6b7` | +| `template/pallets/actors/src/types/lifecycle.rs` | `311a26df456b206910f4fce70575c76aa18ef6bae11c07ae6b860f213c51c6a9` | +| `template/pallets/actors/src/types/scheduler.rs` | `daae00ce2077623ac4fe12ebb2646e4eaf09fc201c7eb116403a5b1eb7a6172d` | +| `template/runtime/src/configs/actor_config.rs` | `0858133d39eaabb1d2591def2454f1b72d7ee6746b3c1f5ecd720f49a37750f9` | +| `template/runtime/src/lib.rs` | `528d5ab5b6e1174f662e147b6743eba74f6a0040a8db2412d6491cd15e225f6d` | +| `template/runtime/src/weights/pallet_deos_actors.rs` | `2d9c60ac7764d1e7dc366531dd4a409444daf66cf3e37ed7feb922bb614e8f1a` | +| `template/Cargo.lock` | `b8f1a3665f555071c1fc339a73999f8af2361d7586fa737e409f89939c53c8e9` | + +## Proof Obligations + +- `Freeze`: Frozen at Measuring on `a4c71bf`; O1 remains unchanged. Source correction rejects the prepared temporal/window combination: specification §7.3.2 and admission forbid both temporal windows and nonzero cooldown. The corrected witness uses two real retries to reach backoff beyond the next block; Opening already records last_cycle_block. This corrects experiment setup without changing the governing contract. +- `Review triggers`: None. +- `Decomposition review`: Boundary reviewed during provenance-preserving fission. Workloads within this Leaf share the declared claim, physical owner, changed variable, evidence domain and acceptance rule. Independently decidable owners were transferred; a new owner or selector requires another node. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | The maximum claimed encoded ActorControlCell is jointly attainable under the host lifecycle, pointer and compact Weight constraints. | Construct one ordinary legal state that attains the proposed maximum, or prove a proposed option combination unreachable | Source-derived SCALE geometry plus native and reference-Wasm construction | Self | Required | [EXP-0029](./EXP-0029.md) O9 | Satisfied on the declared reference Weight binding: 334-byte upper bound and public Wasm attainment | + +## Measurements + +Public `create_system_actor` admission, temporal service, two real unfunded SplitTransfer retries and ordinary replacement produce the following stored SCALE lengths. The diagnostic constructors span two logical blocks and make no RefTime fit claim. + +| Trigger/history | Cell bytes | Hot bytes | Eligibility bytes | Resource bytes | +| --- | --- | --- | --- | --- | +| Cadenced, created | 309 | 45 | 1 | 19 | +| Cadenced, second suspended attempt | 334 | 66 | 5 | 19 | +| Cadenced replaced with AtTime | 314 | 50 | 1 | 19 | +| AtTime, created | 310 | 46 | 1 | 19 | +| AtTime, second suspended attempt | 315 | 47 | 5 | 19 | + +Identity is 54 bytes, cursor 4 bytes and admission certificate 178 bytes throughout. Lifecycle Weight is `(8980381000, 81886)` encoding 10 bytes; Step control `(2288342349, 43787)` encodes 9 bytes and SplitTransfer(8) effect `(6628234184, 56248)` encodes 10 bytes. Nominal `MaxEncodedLen` reports 362 bytes; independent nominal fields do not establish public-state attainability. + +Reference commands: `./scripts/benchmarks.sh --extra --steps 2 --repeat 1 --min-duration 0 --extrinsic control_cell_cadenced_reachability --output /tmp/deos-exp61-cadenced.rs --json-file /tmp/deos-exp61-cadenced.json pallet_deos_actors`, followed by the corresponding `control_cell_at_time_reachability` selector with `--skip-build` and `at-time` output stem. Result JSON SHA-256: Cadenced `4cb5a7b428a7a9189721d4111d447dae17be21e06a14ea42479ad394dfed934f`; AtTime `0b3321b4517036602e72ab3306c20dfefac6a60f2bbf040703e845784573d26f`. + +The native runtime `encoded_control_resource_boundary` diagnostic enumerates 44,136 source contexts covering admitted head geometry and tail fragments against every non-StopCycle effect family/leg count. Per-Step capacity after scheduler overhead and cleanup is `(105957751161, 148440)`; the largest retained control/effect encoding is 19 bytes. This is an overapproximation of authorable contexts, not another public-state witness. Stored admission resources use maximum funding capacity at cursor zero, and zero elsewhere. Its binding remains the generated Weight hash below; log SHA-256 `03dcca4499dddc2404180388514e3a4f0909ab4e455da7c5d0f08c913d791897`. + +Source inspection establishes these constraints: + +| Owner | Source-qualified constraint | Consequence | +| --- | --- | --- | +| `ActorAdmissionCertificate` / `build_admission_certificate` | Five fixed hashes and two versions; lifecycle Weight comes from the runtime authority; generic resource parameter is PhantomData | Increasing Contract length does not enlarge a certificate vector | +| Specification Time bounds / `validate_trigger` / `validate_future_schedule_targets` | AtTime and Cadenced require zero cooldown and no ScheduleWindow | The prepared Cadenced/window tuple was invalid; no temporal cell may independently maximize terminal_at | +| `RuntimeAdmissionCertificateAuthority::current` | Lifecycle Weight is the component-wise maximum of the listed generated lifecycle dispatch methods | Capture this exact host value; do not substitute a per-Contract sum or assume the block limit bounds it | +| `control_append_waiting` | Service authority accepts only Block keys; Trigger authority accepts Tick with Idle, no pending signal, no eligibility and no process wakeup | The largest union alternative is not automatically a legal service-pointer shape | +| `WakeupPointer` / `TriggerWakeupPointer` | Both use WakeupSlot = u32, even though ActorControlLocation stores a u8 slot | Count the actual pointer fields; a location-slot width cannot substitute for them | +| `prime_initial_actor_schedule` versus `prime_frame_actor_schedule` | Initial scheduling installs temporal readiness before next-work placement; replacement invalidates an existing Trigger pointer and re-primes through a different path | Creation and replacement histories are not interchangeable witnesses of option coexistence | +| `TriggerRuntimeState` and compact Weight | AtTime has one additional consumed byte relative to Cadenced; each of three stored Weights has value-dependent encoded length | A maximum requires joint lifecycle and width qualification, not a Cartesian product | + +## Derived Metrics + +The candidate 334-byte upper bound is `8 ActorId + 54 identity + 66 hot + 4 cursor + 5 eligibility + 178 certificate + 19 resources`. System identity is the longest class; the User owner-slot alternative is shorter. The certificate is fixed by the reference admission authority, not authored length. + +| Joint-state partition | Bound argument | +| --- | --- | +| Cadenced | Initialized Trigger state is 10 bytes; Block process pointer is 18 bytes including Option, Tick reference is 21; temporal terminal marker is None; last-cycle and eligibility may both be Some. Hot reaches 66 bytes. | +| Non-temporal | Crossing state is at most 10 bytes, no Tick reference is legal, and a window adds only 4 terminal bytes. Even allowing every remaining option gives hot at most 50 bytes. | +| AtTime before Opening | Creation is Idle; replacement cancels the old Run. An unlatched Tick owner has no process placement or eligibility. A latched occurrence disables its Tick reference. The extra consumed byte therefore cannot coexist with both longest pointers. | +| AtTime after Opening | The shared Opening transition sets consumed, including retained-latch replacement; consumed AtTime has no Tick reference. Running/Suspended cannot recover the extra 20 bytes. | +| Non-StopCycle resources | `derive_step_resource_envelopes` fixes head funding count to MaxFundingTrackedAssets and tail counts to zero. `step_control_weight_context` reduces nonzero cursor identity to fragment size; the reference maximum provider depends only on that distinction. The finite overapproximation covers those contexts and all effect choices, retaining only component-wise admissible sums; maximum width is 19. | +| StopCycle/zero-Step resources | Effect is zero (2 bytes); admitted control is component-wise below service, whose encoding is 10 bytes. This partition is at most 12 bytes and cannot enlarge the 19-byte resource ceiling. | + +Pointer slot types, window prohibition, current compact widths and complete public lifecycle transitions are load-bearing premises. Neither nominal type maxima nor a rejected wide constructor substitutes for these premises. + +## Statistical / Variance Notes + +This is deterministic encoding/reachability evidence. Timing, throughput and confidence intervals are outside the claim. A single fully asserted legal construction can attain a bound; a single refused tuple cannot bound every legal history. + +## Result + +An ordinary reference-Wasm Cadenced retry state attains 334 bytes. A candidate wider Contract with one SplitTransfer(8)/four Current predicates plus eleven StopCycle/four Opening predicate tails passes the mock host but fails ordinary reference-Wasm creation with `ContractStepsExceedOnIdleBudget`. Its benchmark source hash is `c1d5edaf617b2d964785b188d3aba84dd0c326db650b8bae1663502b01e1fdaa`, Wasm hash `40ca8626245cb74406835f6fed6d129cd054289b35ce23b19222f5b1efc29282`, refusal-log hash `e4121f4732fe09812ccb21cd19c146dccd6d09df3dad01a12c85e45eb10714cb`. The refused constructor is removed; no admission bypass or shortened substitute is accepted. + +A public Manual latch followed by AtTime replacement and Opening exposed an implementation defect: Opening left `consumed = false` with no latch or deadline. Specification §4.3 preserves the latch and §4.5 requires consumed AtTime after Opening. The shared Opening owner now enforces that flag. Public zero-Step, one-Step and Running User regressions pass; the former busy-AtTime test was a test defect because it fabricated pre-deadline activation through a private helper. Its replacement proves no second Trigger fee or deferred latch after public replacement and Opening. + +Validation on the corrected source: complete all-feature Actors package tests pass (1,101 passed, five ignored), the exact runtime resource-domain assertion passes, and workspace/all-feature Clippy passes with warnings denied. Commands use `./scripts/ci-local.sh --only tests --all-features --package pallet-deos-actors`, `--only tests --default-features --package deos-runtime --test-filter encoded_control_resource_boundary`, and `--only clippy`. Scheduler source SHA-256 is `bdedd82571a229f3609194547c6256d8ca454055640e407c2c9af03d2245fbd1`; runtime diagnostic source is `85efb7ede02aec1112ca4e1bb6a6be1e1e5ba1897a340272b760431c6f5ea3d3`, final diagnostic log `0537ec11a4fbf0fbe6094ff8ee8fc20b6a1b2bcb02501c3f8747ec288410cfcd`. + +The original Wasm witnesses are reused by scoped semantic equivalence: Cadenced Opening is unchanged, naturally fired AtTime already had `consumed = true` before Opening, and the measured replacement had no retained latch. The correction changes only the previously unqualified retained-latch case; it changes no encoded field, runtime limit, admission envelope or generated Weight. This reuse does not claim a newly built production Wasm. The aggregate quick assurance route still stops at stale fee-vector binding, and benchmark annotation freshness remains an existing G5 obligation. + +## Interpretation + +334 bytes is both attained and bounded on the declared reference binding. The 19-byte resource ceiling and public lifecycle partition exclude the independent-field Cartesian maximum. The corrected Opening transition restores the specification without changing C1 geometry. + +## Decision + +Accepted with scope reference-host joint control-cell encoding under the declared generated Weight: maximum 334 bytes. Measurement, interpretation and acceptance close O1. This decision supplies EXP-0029 O9 and later Waiting calculations; it does not close their complete domains, numeric operation Weight or G5 production artifact identity. + +## Rejected Alternatives and Tradeoffs + +Retain all rejected candidates and causal limits in the extracted evidence. Do not infer a new selection from record relocation or from aggregate coefficient subtraction. + +## Validity + +- `Establishes`: A 334-byte jointly reachable control-cell maximum on the declared reference binding, with ordinary Wasm attainment and source-qualified lifecycle/resource upper bounds. +- `Does not establish`: Complete production Weight, exact release artifact closure or integrated throughput. +- `Known limitations`: The bound is host- and Weight-specific. Diagnostic Wasm is not production Wasm; complete production bindings and operation envelopes remain open. +- `Invalidation triggers`: A changed measured body, host domain, source/Weight/Wasm identity or acceptance rule requires scoped requalification. +- `Residual qualitative insight`: Follow the owning physical boundary, not a benchmark name. + +## Follow-Up / Next Gradient + +Consume the 334-byte cell bound in EXP-0055/0056 with their own page/entry/cardinality bounds; do not sample more control-cell histories without a concrete counterexample. Regenerated G5 Weight must requalify compact resource widths before final production Waiting evidence consumes them. + +## Architecture Impact + +The discovered Opening correction restores existing Trigger semantics; it does not change storage geometry or production Weight. C1 and A0 remain frozen; this record owns only its declared proof evidence. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: [EXP-0029](./EXP-0029.md) O9 +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: [EXP-0025](./EXP-0025.md) +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: [EXP-0048](./EXP-0048.md) +- `Transfers question to`: None; independent findings require an explicit new transfer. +- `Produces input for`: [EXP-0029](./EXP-0029.md) +- `Reopen trigger`: A falsifier of this declared owner/domain or exact artifact identity; cost alone does not reopen frozen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0062.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0062.md new file mode 100644 index 00000000..2d5699a5 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0062.md @@ -0,0 +1,134 @@ +# EXP-0062 — Nonzero Action Fee Collection Ownership + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | [EXP-0028](./EXP-0028.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-06 | +| Affected domain | Nonzero Action Fee Collection Ownership | +| Physical mechanism | Nonzero Action Fee Collection Ownership | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0025](./EXP-0025.md) — Accepted, physical architecture only | +| Successor experiment | [EXP-0028](./EXP-0028.md) | + + +## Decision Question + +Nonzero Action fee collection has one complete Control owner, selected independently of invocation receipts and Pipeline collection. + +## Governing Specification + +- `Owners`: `template/pallets/actors/docs/specification.en.md` (sole ownership, lifecycle/Trigger/Pipeline/Action semantics, placement and scheduler); `docs/actors-resource-policy.specification.en.md` (component-wise admission/actual settlement, equal-thirds block policy); `docs/actors-performance-assurance.specification.en.md` (evidence and target). +- `Frozen physical input`: [EXP-0025](./EXP-0025.md) C1 Architecture Freeze. No change to FIFO, ticket chronology, Q1, causal floor, primary/reference ontology, source/successor atomicity or sole execution owner. +- `Semantic oracle`: A0 SHA-256 `8746e423168309975e509be91e4eb06fdf0eb0b43369d75f1ee62202a4341678` remains frozen; use only for relevant semantic equivalence, never as a Weight oracle. + +## Context and Hypothesis + +This required question was already uncovered by the pre-fission phase/domain map. It is allocated because its physical owner can be decided independently; no speculative optimization or new semantic requirement is introduced. + +The existing source already exposes collection as a distinct ledger primitive and a distinct selector input. This experiment measures that primitive on the current reference benchmark Wasm and tests the selector and complete ledger trace without changing runtime semantics or installing a partial generated Weight file. + +## Baseline and Candidates + +The candidate is the existing isolated `fee_collection` benchmark and the existing `RuntimeStepControlWeight::action_collection_allowance`: reserve one generated collection owner for every non-`StopCycle` Step, settle it exactly when an invoked User Action has a nonzero total fee, and keep it independent of Pipeline collection and the invocation receipt. The zero owner remains selected for `StopCycle`, System execution and noninvocation. + +## Controls + +Only the declared witness input varies within each source-bound comparison. Preserve the exact host, toolchain, database, Wasm and sampling controls printed with each evidence item; do not compare unlike artifacts as a controlled performance result. + +## Benchmark Class, Workloads, and Criteria + +Acceptance requires the single claim to hold over its declared admitted domain, with each physical operation charged once and every retained limitation resolved before broader production use. One legal omitted-owner, domain or containment counterexample falsifies that claim. Existing workloads below are bounded witnesses, not permission to add independent questions. + +## Environment and Artifact Identity + +Allocation derives from the pre-fission EXP-0028/EXP-0029 at Git baseline `a60de7748132211d9e58cecd5be82b41f76ce230`. The deciding measurement uses current code baseline `e92ab80`, Actors evidence-source identity `56dd8b09d970c68c3aaed37a2a9e6e70c964fda4a175b82270142d21fa1f32d4`, benchmark source SHA-256 `675745ac67f01c82a618c49184d4e25b66354e76bd960169bad3ff2b7034ad31`, selector source SHA-256 `0858133d39eaabb1d2591def2454f1b72d7ee6746b3c1f5ecd720f49a37750f9`, scheduler source SHA-256 `bdedd82571a229f3609194547c6256d8ca454055640e407c2c9af03d2245fbd1` and benchmark Wasm SHA-256 `ca82f02f4b9d3b4816063f0a0b21393dd762fe9cfbb6761604569bf671e36541`. + +## Proof Obligations + +- `Freeze`: Frozen before measurement to the existing isolated collection primitive, non-`StopCycle` maximum allowance, and actual selector described above. +- `Review triggers`: Existing Action-collection evidence is reused by EXP-0067 as well as EXP-0028; no new measurement is added here. +- `Decomposition review`: Boundary reviewed during provenance-preserving fission. Workloads within this Leaf share the declared claim, physical owner, changed variable, evidence domain and acceptance rule. Independently decidable owners were transferred; a new owner or selector requires another node. EXP-0067 borrows this unchanged disjoint Action owner while qualifying Pipeline-inclusive settlement separately. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | Nonzero Action fee collection has one complete Control owner, selected independently of invocation receipts and Pipeline collection. | One legal nonzero User Action collection with its complete ledger trace and actual selector | Source ownership audit and production-Wasm ledger component | Self | Required | [EXP-0028](./EXP-0028.md) O21 | Satisfied for the reference native-ledger domain; isolated generated owner, complete ledger trace and phase selector are evidenced | + +## Measurements + +The reference benchmark funds distinct payer and Fee Sink native accounts outside the measured block, measures only `collect_user_step_fee`, and verifies exact payer decrease and Fee Sink increase by the same nonzero amount. It therefore owns the complete two-account transfer rather than an event, invocation receipt, Pipeline charge or Task effect. + +| Item | Measured evidence | +| --- | --- | +| Method / generated RefTime / estimated ProofSize | `fee_collection` / 47,703,000 ps / 6,196 bytes | +| Measured proof / DB | 381 bytes / 2 reads / 2 writes on `System::Account` | +| Minimum execution time | 45,188,000 ps | +| Runtime / environment | Reference benchmark runtime; Rust 1.98.0; FRAME CLI 58.0.0; `frame-omni-bencher` 0.22.0; compiled Wasm; AMD Ryzen 7 4800H; DB cache 1024; heap 4096; map bound 1,000,000; 50 steps × 20 repeats | +| Reproduction | `scripts/benchmarks.sh --extrinsic fee_collection --output /tmp/deos-exp62-fee-collection.rs --json-file /tmp/deos-exp62-fee-collection.json pallet_deos_actors` | +| Generated fragment SHA-256 | `987a29bba0fcfca521a897c7aa764470e3eaf2f26f84ca8907d5d4c27c885da5` | +| Raw JSON SHA-256 | `ba530c6d1573f55863abd8528e217ee78de92d56730a67830f946d32ac1d8341` | + +The generated fragment is retained as experiment evidence, not installed independently. Its 47,703,000 ps RefTime supersedes the stale installed method's 47,004,000 ps only as the selected primitive input; EXP-0065 must regenerate and bind the complete Actors Weight file without manual coefficient repair. + +Source ownership is disjoint. `execute_current_step_and_place` computes `action_fee_collected` only for User execution with a nonzero actual fee, supplies that fact to the Control selector, charges the selected Control Weight, then performs exactly one `collect_user_step_fee`. Invocation receipt deposition is a later operation selected from `task_effect`; Pipeline collection occurs at Opening before this per-attempt path. The package matrix executes User/System × Opening/Running/Suspended × noninvocation/success/typed failure and checks the exact Fee Sink delta, receipt count and effect delta. The runtime selector regression checks all three phases, rejects collection without invocation, rejects collection for `StopCycle`, and proves collection adds exactly one `fee_collection` component beside the independent receipt. The direct Opening-completion regression confirms inclusive receipt handling still adds collection exactly once for a User transfer. + +## Derived Metrics + +Derived values are retained beside the evidence that defines their source and units. No subtraction of overlapping aggregate owners or cross-artifact timing comparison is introduced. + +## Statistical / Variance Notes + +The generated no-component model uses the FRAME 50 × 20 run and its conservative generated coefficient. Raw output contains 17,820 timing observations (median 47,703,000 ps, minimum 45,188,000 ps) and constant 381-byte measured proof. Timing dispersion is not used to compare architectures; generated output is preserved unchanged and final full-file regeneration remains required. + +## Result + +The reference-Wasm primitive produces a generated `47,703,000 / 6,196` owner plus two native-account reads and writes. Fresh package ledger/receipt coverage and runtime selector regressions pass over Opening, Running and Suspended. The selected allowance is distinct from Pipeline collection and the invocation receipt, and the actual path can collect it only once for an invoked, nonzero-fee User Action. + +## Interpretation + +The isolated native-ledger transfer is the complete physical Action collection owner for the reference runtime. Phase does not alter that primitive; phase-specific base Control work remains outside it. `action_fee_collected` and `task_effect` are separate selector facts, while Pipeline collection has a separate call site and receipt. This accepts ownership and the measured primitive without claiming that the stale complete Weight file is a release artifact. + +## Decision + +Accepted for the reference native-ledger domain. Select exactly one generated `fee_collection` owner for invoked User Actions with nonzero total fee; select zero for System, noninvocation and `StopCycle`. Preserve Pipeline collection and invocation receipt as independent owners. EXP-0065 must regenerate the complete production Weight identity. + +## Rejected Alternatives and Tradeoffs + +Reject folding Action collection into the invocation receipt: System can emit a zero-fee invocation receipt without a ledger transfer. Reject folding it into Pipeline collection: Running and Suspended Action attempts collect without Opening, while Opening can charge both independently. Reject treating the old installed 47,004,000 ps coefficient as current evidence: the fresh generated primitive is 47,703,000 ps. Do not patch that coefficient manually; complete regeneration belongs to EXP-0065. + +## Validity + +- `Establishes`: One complete Action collection Control owner for the reference runtime's native-ledger transfer, exact two-account storage ownership, and independent maximum/actual selection over Opening, Running and Suspended. +- `Does not establish`: Generic host implementations, complete production Weight, exact release artifact closure or integrated throughput. +- `Known limitations`: The accepted generated fragment is not installed independently; the existing complete Actors Weight file retains a stale smaller coefficient until EXP-0065 regeneration. +- `Invalidation triggers`: A changed measured body, host domain, source/Weight/Wasm identity or acceptance rule requires scoped requalification. +- `Residual qualitative insight`: Follow the owning physical boundary, not a benchmark name. + +## Follow-Up / Next Gradient + +Stop measuring this satisfied owner. EXP-0064 composes it into maximum/admission envelopes and EXP-0065 regenerates and binds the complete production artifacts. Reopen only for a changed collection body/host or a legal duplicate/omitted collection counterexample. + +## Architecture Impact + +No runtime, semantic, storage or production binding change. C1 and A0 remain frozen. The experiment accepts the isolated generated primitive as an input to later complete regeneration; it does not install a partial Weight file. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: [EXP-0028](./EXP-0028.md) O21 +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: [EXP-0025](./EXP-0025.md) +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: [EXP-0030](./EXP-0030.md) +- `Transfers question to`: None; independent findings require an explicit new transfer. +- `Produces input for`: [EXP-0028](./EXP-0028.md), [EXP-0067](./EXP-0067.md) — preserve disjoint Action collection while qualifying User Opening. +- `Reopen trigger`: A falsifier of this declared owner/domain or exact artifact identity; cost alone does not reopen frozen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0063.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0063.md new file mode 100644 index 00000000..da50c784 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0063.md @@ -0,0 +1,149 @@ +# EXP-0063 — Waiting / Deadline Heap Production Envelope + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Synthesis | +| Former IDs | actors/EXP-0031@a60de7748132211d9e58cecd5be82b41f76ce230 | +| Parent question | [EXP-0064](./EXP-0064.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-05 | +| Affected domain | Waiting and deadline physical branch coverage | +| Physical mechanism | Canonical linked Waiting pages and shared deadline heap | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0025](./EXP-0025.md) — Accepted, physical architecture only | +| Successor experiment | [EXP-0065](./EXP-0065.md) | + +## Decision Question + +What sound Weight envelope covers canonical Waiting publication, reference management, heap insertion/removal and bounded unlink? + +## Governing Specification + +- `Owners`: `template/pallets/actors/docs/specification.en.md` (sole ownership, lifecycle/Trigger/Pipeline/Action semantics, placement and scheduler); `docs/actors-resource-policy.specification.en.md` (component-wise admission/actual settlement, equal-thirds block policy); `docs/actors-performance-assurance.specification.en.md` (evidence and target). +- `Frozen physical input`: [EXP-0025](./EXP-0025.md) C1 Architecture Freeze. No change to FIFO, ticket chronology, Q1, causal floor, primary/reference ontology, source/successor atomicity or sole execution owner. +- `Semantic oracle`: A0 SHA-256 `8746e423168309975e509be91e4eb06fdf0eb0b43369d75f1ee62202a4341678` remains frozen; use only for relevant semantic equivalence, never as a Weight oracle. +- `Scope`: + +- Separately own existing/new deadline insertion, Primary/Reference append, last-entry removal, directory unlink, heap key removal, pop minimum and exact removal. +- Cover upward/downward repair, tail-only removal, page allocation/reclamation, Cadenced Tick rearm and terminal reference cleanup. +- Use complete legal state where the claim concerns full Waiting/Actor publication/removal. Partial accessed-page leaf fixtures remain explicitly narrower evidence. +- Do not infer worst-case Weight from changed-page count alone; compare competing branches across RefTime, ProofSize, database I/O and full owner composition. Do not alter C1 Waiting topology. + +## Context and Hypothesis + +This broad decision composes the finite obligations below. Accepted EXP-0055 covers complete-population Primary, retained-primary Reference, Cadenced Block/Tick cross-clock and the publicly attained 334-byte maximal-cell publication alternatives with one component-wise envelope. Accepted EXP-0056 covers the complete admitted reference removal/repair branch family with an inclusive final-Tick/downward-repair close bound. C1 remains frozen, and newly independent proof questions must be transferred before investigation. + +## Baseline and Candidates + +Composition of the retained frozen C1 baseline and declared child decisions. This Synthesis introduces no benchmark implementation or candidate sweep. + +## Controls + +- `Controlled variables`: Freeze source/configuration, host bounds, toolchain/SDK, database/cache, legal state and workload for each comparison; name any changed input and refresh dependent evidence. +- `Changed variable`: Only the question-specific model, fixture, accounting or binding under test; no macrogeometry or authored semantics. +- `Confounders`: Historical native, direct-path, partial-heap and old generated evidence are narrower than production acceptance. [EXP-0026](./EXP-0026.md) and [EXP-0027](./EXP-0027.md) reject ceiling-only and state-hold-cache-only proof improvements; do not reintroduce them without changed evidence. + +## Benchmark Class, Workloads, and Criteria + +- `Smallest falsifier`: Hold one admitted retry/context constant while comparing existing versus absent deadline in a legally populated heap, then independently exercise Cadenced Tick rearm even when the Step successor is Queue. Any uncovered branch or larger competing removal falsifies the proposed envelope. +- `Evidence class`: Call-path ownership audit, legal native witnesses and production-Wasm FRAME branch evidence. + +| Physical family | Competing branches | Required state proof | +| --- | --- | --- | +| Publish | Existing/new key; Primary/Reference; spare/full page | Exact primary/locator/pointer, key index and complete legal occupants | +| Remove | Interior/last entry; head/middle/tail page | Live count, bounded unlink, neighbors, directory/key reclamation | +| Heap | Insert/pop/exact remove; upward/downward/tail-only | Full forward/reverse bijection, parent order, depth and page boundaries | +| Cross-phase | Cadenced rearm, terminal references, Ready-capacity fallback | Exact source/successor, clock arbitration and charged phase | +| Host/population | Minimum and reference maximum, sparse churn, both clocks | Enforced cardinality and independent legal reference limits | + +- `Acceptance criteria`: Prove a sound envelope over legal branch/population geometry, or publish explicit complete branch owners with component-wise selectors. Measure competing branches on one identified Wasm, include allocation/reclamation and Cadenced/terminal work, and reconcile incidental coverage against EXP-0028's matrix. +- `Rejection criteria`: Changed-page count used as Weight proxy; sampled upward branches generalized to all layouts; shallow existing-key fixture standing for new key; illegal synthetic population or double-added reference cost. + +## Environment and Artifact Identity + +Migration provenance: Git baseline `a60de7748132211d9e58cecd5be82b41f76ce230` and the content/mode manifest archived at Git commit `6c650695568a6e5d05437351ef014e8ee950c484`, repository path `.agents/skills/architecture-experiments/migrations/actors-proof-graph-2026-09-06.json`. Each Leaf retains its measured artifact identity. No final production artifact is created or certified by this migration. + +## Proof Obligations + +- `Freeze`: Frozen finite composition. EXP-0055 publication and EXP-0056 removal/repair are the only required children; their declared branch maps are finite. +- `Review triggers`: Waiting/heap topology, primary/reference authority, host capacity domain, phase attribution, source/Weight/Wasm identity or either child decision changes. +- `Decomposition review`: Synthesis owns only the frozen finite composition; independent primary evidence is owned by the reciprocal child map. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | Waiting publication has a production bound over accepted joint population, page, key and encoded-cell geometry. | One admitted full-page or new-minimum Waiting publication whose complete charged cost exceeds the proposed production bound | Child decision composition | [EXP-0055](./EXP-0055.md) | Required | This synthesis decision | Satisfied in the current admitted reference domain by Accepted EXP-0055 | +| O2 | Waiting removal and heap repair have a production bound over accepted joint population and repair geometry. | One admitted upward/downward/tail or final-key removal whose repair cost exceeds the proposed production bound | Child decision composition | [EXP-0056](./EXP-0056.md) | Required | This synthesis decision | Satisfied in the admitted reference domain by Accepted EXP-0056 | + +## Measurements + +No synthesis-primary measurements. Accepted EXP-0055 selects the current-binding publication envelope `1,031,223,000` pre-DB RefTime / `76,618` estimated ProofSize plus 44 reads and 30 writes (`5,131,223,000` pinned-DB RefTime). The publicly attained 334-byte Cadenced suspended Primary supplies RefTime, retained-primary Block Reference supplies ProofSize, and the common-Wasm envelope contains RetryMin Primary and Cadenced cross-clock publication. Accepted EXP-0056 selects the complete admitted reference removal/repair envelope `1,928,069,000 / 96,741 +70R/44W` (`8,078,069,000` pinned-DB RefTime), as one inclusive public-close atom rather than an addition to EXP-0042. + +## Derived Metrics + +Composition requires a common admitted domain and compatible source/Weight/artifact identities. Do not maximize independent storage capacities into an unreachable state, add inclusive owners twice, or derive a disjoint bound by subtracting aggregate coefficients. + +Phase overlap is reconciled through Accepted EXP-0058. Exact Running/Suspended and temporal profiles that already publish Waiting remain inclusive; an otherwise-unmatched Wakeup destination selects one whole EXP-0055 publication owner after its loaded Ready-source owner. Due workers own their complete Waiting-source removal and successor publication. A populated terminal/public close selects one whole EXP-0056 branch, replacing rather than supplementing EXP-0042, EXP-0043 or generic source/cleanup work. Publication and removal are mutually exclusive whole-owner alternatives, never additive heap allowances. + +## Statistical / Variance Notes + +For generated FRAME evidence, pin 50 steps × 20 repeats where applicable and review model/branch fit, not merely a mean. Identity and constructive correctness checks are deterministic; no wall-clock capacity inference. Declare a different method before measurement if the bounded question requires it. + +## Result + +Both required children are Accepted over the compatible current admitted reference domain. EXP-0055 covers publication through the 334-byte maximum; EXP-0056 covers complete removal/repair. Accepted EXP-0058 resolves cross-phase overlap by mutually exclusive whole-owner selection. No production coefficient is installed; EXP-0064 still owns admission composition and EXP-0065 owns exact artifacts. + +## Interpretation + +The maximal-cell Primary supplies publication RefTime while retained-primary Block Reference supplies publication ProofSize, requiring a component-wise maximum rather than one observed branch. For removal, the final Tick Reference/root-downward repair close contains mixed-page, upward, tail-only and EXP-0042 public-close alternatives component-wise and is selected whole. Accepted EXP-0029 supplies the compatible legal population and encoded-state domain; EXP-0064 consumes this result rather than acting as a circular prerequisite. + +## Decision + +Accepted for the current admitted reference domain. Select exactly one complete publication envelope `1,031,223,000 / 76,618 +44R/30W` or one complete removal/repair envelope `1,928,069,000 / 96,741 +70R/44W` according to the physical transition. Cross-phase overlap follows Accepted EXP-0058: exact direct/successor profiles remain inclusive, unmatched publication selects EXP-0055 once, due-worker removal remains inclusive, and populated terminal close selects EXP-0056 instead of narrower close/source/heap owners. EXP-0064 owns admission composition; EXP-0065 owns exact regeneration and changed-binding requalification. + +## Rejected Alternatives and Tradeoffs + +| Shortcut | Disposition | Reason | +| --- | --- | --- | +| Reopen C1 because this question is expensive | Excluded | Requires the explicit invariant/impossibility test in EXP-0025 | +| Reuse direct historical C1 as production acceptance | Excluded | Source/path/artifact authority differs | +| Hide missing control under another allowance | Excluded | Changed-page count used as Weight proxy; sampled upward branches generalized to all layouts; shallow existing-key fixture standing for new key; illegal synthetic population or double-added reference cost. | + +## Validity + +- `Establishes`: Complete publication and removal/repair Waiting envelopes with mutually exclusive selectors over the current admitted reference domain. +- `Does not establish`: Installed production Weight, final artifact identity, complete admission, throughput, release acceptance or permission to change frozen C1. +- `Known limitations`: The envelopes are binding-specific diagnostic fragments. EXP-0065 must regenerate and requalify affected compact widths or operation costs; removal/repair remains one inclusive complete-close selector rather than a generic additive allowance. +- `Invalidation triggers`: Relevant source, semantics, host bounds, Weight, SDK/toolchain, configuration, fixture or dependency identity changes; refresh affected descendants. +- `Residual qualitative insight`: Conservative maximum or explicit branch owners plus legal-state and no-double-charge evidence feed EXP-0065; admission composition must incorporate the result before exact production bindings are accepted. + +## Follow-Up / Next Gradient + +- `Produces`: Conservative maximum or explicit branch owners plus legal-state and no-double-charge evidence feed EXP-0065; admission composition must incorporate the result before exact production bindings are accepted. +- `Blocking evidence required`: None within this synthesis. EXP-0064 must compose these envelopes with accepted ownership, reachability and native precedence; EXP-0065 must regenerate exact artifacts and requalify any changed binding. +- `Stop condition`: Decide only this question against the stated criteria. Transfer materially distinct hypotheses to dedicated records; BACKLOG alone owns release-blocking work. +- `Residual trigger`: A production experiment must name a measured owner and changed assumption before any bitmap/page/shared-body/key-locality/compression/PoV candidate begins. + +## Architecture Impact + +- `Affected physical truth`: Canonical linked Waiting pages and shared deadline heap, within frozen C1. +- `Architecture update`: Physical overlap is resolved by whole-owner selection; no runtime, storage or generated Weight change follows from this synthesis update. +- `Provenance citation`: `actors/EXP-0063` with decision scope and exact identity once decided. + +## Relations + +- `Decomposes into`: [EXP-0055](./EXP-0055.md), [EXP-0056](./EXP-0056.md) +- `Satisfies obligation`: [EXP-0064](./EXP-0064.md) O3 +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: [EXP-0025](./EXP-0025.md), [EXP-0029](./EXP-0029.md), [EXP-0055](./EXP-0055.md), [EXP-0056](./EXP-0056.md) +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: [EXP-0055](./EXP-0055.md), [EXP-0056](./EXP-0056.md), [EXP-0058](./EXP-0058.md) scoped phase attribution only +- `Transfers question to`: None; independent findings require an explicit new transfer. +- `Produces input for`: [EXP-0064](./EXP-0064.md) +- `Reopen trigger`: A falsifier of this declared owner/domain or exact artifact identity; cost alone does not reopen frozen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0064.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0064.md new file mode 100644 index 00000000..d1db7f5a --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0064.md @@ -0,0 +1,183 @@ +# EXP-0064 — Actor Admission Envelope + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Synthesis | +| Former IDs | actors/EXP-0030@a60de7748132211d9e58cecd5be82b41f76ce230 | +| Parent question | None | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-05 | +| Affected domain | Component-wise maximum/actual admission and fee prerequisites | +| Physical mechanism | Phase-local resource envelope selection | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0025](./EXP-0025.md) — Accepted, physical architecture only | +| Successor experiment | [EXP-0065](./EXP-0065.md) | + +## Decision Question + +Can production admission soundly cover every reachable Actor transition inside the configured DEOS resource policy? + +## Governing Specification + +- `Owners`: `template/pallets/actors/docs/specification.en.md` (sole ownership, lifecycle/Trigger/Pipeline/Action semantics, placement and scheduler); `docs/actors-resource-policy.specification.en.md` (component-wise admission/actual settlement, equal-thirds block policy); `docs/actors-performance-assurance.specification.en.md` (evidence and target). +- `Frozen physical input`: [EXP-0025](./EXP-0025.md) C1 Architecture Freeze. No change to FIFO, ticket chronology, Q1, causal floor, primary/reference ontology, source/successor atomicity or sole execution owner. +- `Semantic oracle`: A0 SHA-256 `8746e423168309975e509be91e4eb06fdf0eb0b43369d75f1ee62202a4341678` remains frozen; use only for relevant semantic equivalence, never as a Weight oracle. +- `Scope`: + +- Own maximum/actual control envelopes, component-wise RefTime/ProofSize, cleanup inclusion, effect-domain reservation and Action collection allowance. +- Own User fee-liability prerequisites, Running/Idle/Suspended admission, terminal/paused/breaker precedence, pre-cold-read negative decisions and selector continuity. +- Decide maximum Contract admissibility and explicitly answer: are 12 / 24 / 48 independent limits, or only a constrained subset jointly admissible? +- Distinguish structurally legal Contracts from resource-admissible Contracts under current runtime policy. Do not silently raise budgets, weaken Weight or generalize a one-point model to admit maxima. + +## Context and Hypothesis + +This broad decision composes the finite obligations below. Detailed evidence was extracted from pre-fission records at `a60de7748132211d9e58cecd5be82b41f76ce230` without measurement or promotion of scope. C1 remains frozen. Newly independent proof questions must be transferred before further investigation. + +## Baseline and Candidates + +Composition of the retained frozen C1 baseline and declared child decisions. This Synthesis introduces no benchmark implementation or candidate sweep. + +## Controls + +- `Controlled variables`: Freeze source/configuration, host bounds, toolchain/SDK, database/cache, legal state and workload for each comparison; name any changed input and refresh dependent evidence. +- `Changed variable`: Only the question-specific model, fixture, accounting or binding under test; no macrogeometry or authored semantics. +- `Confounders`: Historical native, direct-path, partial-heap and old generated evidence are narrower than production acceptance. [EXP-0026](./EXP-0026.md) and [EXP-0027](./EXP-0027.md) reject ceiling-only and state-hold-cache-only proof improvements; do not reintroduce them without changed evidence. + +## Benchmark Class, Workloads, and Criteria + +- `Smallest falsifier`: Replay one legal maximum independent-source Opening and adjacent parameter points against selected maximum/actual envelopes, independently exhausting RefTime and ProofSize. Then cross solvent/insolvent User Suspended retry with scarce effect capacity; a wrong terminal outcome or avoidable cold payload read falsifies the corresponding admission claim. +- `Evidence class`: Call-path ownership audit, legal native witnesses and production-Wasm FRAME branch evidence. + +| Admission family | Required partitions | Observable | +| --- | --- | --- | +| Idle Opening | Zero/nonzero, User/System, solvent/insolvent, due terminal/paused/breaker | Liability before deferral; correct cold-read frontier | +| Running | Middle/terminal, every Task and current fragment | Paid machine reuse, effect reservation and actual≤maximum | +| Suspended | Temporary/Funding retry, cursor/error-policy/attempt boundaries | Action liability and terminal precedence before resource refusal | +| Maximum domain | Legal 12/24/48 and adjacent/mixed tuples from EXP-0029 | Selector continuity, maximum-tail and genesis fit | +| Independent meters | Aggregate/Control/shared-effect; RefTime and ProofSize | Exact refusal, no orphan authority, committed-prefix durability | + +- `Acceptance criteria`: A complete component-wise envelope over EXP-0029's reachable domain with EXP-0028 ownership, cleanup and effect reservations included exactly once; every chosen actual is bounded by admitted maximum. Publish the exact 12/24/48 product contract and sound rejection behavior, including any structurally legal but resource-inadmissible subset. Bind every numeric statement to source/Weight/config and production-Wasm evidence. +- `Rejection criteria`: Underweight, discontinuous cheap maximum, omitted liability/native read, changed terminal precedence, reserved Action allowance masking System base undercount, skipped fitting FIFO head or budget increase concealed as optimization. + +## Environment and Artifact Identity + +Migration provenance: Git baseline `a60de7748132211d9e58cecd5be82b41f76ce230` and the content/mode manifest archived at Git commit `6c650695568a6e5d05437351ef014e8ee950c484`, repository path `.agents/skills/architecture-experiments/migrations/actors-proof-graph-2026-09-06.json`. Each Leaf retains its measured artifact identity. No final production artifact is created or certified by this migration. + +Current composition baseline is Git `4b3b18d6fd440286338390e9102ce3770ddd0080`, tree `c629faac5992d06c854610c96685a7c67d41810a`; provider SHA-256 `0858133d39eaabb1d2591def2454f1b72d7ee6746b3c1f5ecd720f49a37750f9`, generated Actors Weight `2d9c60ac7764d1e7dc366531dd4a409444daf66cf3e37ed7feb922bb614e8f1a`, pallet source `7432b3b5df56cf7507d0991270b62a5a35e4437a2548e81bd6bd66bb57d6c788`, and retained benchmark Wasm `1bfe152704f966b80beaaf468941483fb35942372c3da15d634f7f67008d24ef`. The current test/comment clarification changes no executable selector; EXP-0065 must bind the eventual post-generation source and production Wasm separately. + +## Proof Obligations + +- `Freeze`: Frozen finite composition. EXP-0028, EXP-0029, EXP-0063 and EXP-0054 are the only required children; admission composes their declared scopes without adding another primary evidence owner. +- `Review triggers`: None. +- `Decomposition review`: Synthesis owns only the frozen finite composition; independent primary evidence is owned by the reciprocal child map. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | Complete phase ownership synthesis | One counterexample to Complete phase ownership synthesis | Child decision composition | [EXP-0028](./EXP-0028.md) | Required | This synthesis decision | Satisfied by Accepted EXP-0028 for phase ownership and settlement containment; exact artifacts remain EXP-0065 | +| O2 | Complete reachable-domain synthesis | One counterexample to Complete reachable-domain synthesis | Child decision composition | [EXP-0029](./EXP-0029.md) | Required | This synthesis decision | Satisfied by Accepted EXP-0029 for the declared mock/reference reachability domain and by this composition for numeric admission | +| O3 | Waiting and Deadline Heap Production Envelope | One counterexample to Waiting and Deadline Heap Production Envelope | Child decision composition | [EXP-0063](./EXP-0063.md) | Required | This synthesis decision | Satisfied for the current admitted reference domain by Accepted EXP-0063 and this finite selector composition; exact binding remains EXP-0065 | +| O4 | Native resource refusal and terminal precedence preserve the admitted authority, custody and FIFO state in the declared bounded partition. | One resource refusal at the last allowed retry that reads cold payload, mutates FIFO/Run, or reverses terminal precedence | Child decision composition | [EXP-0054](./EXP-0054.md) | Required | This synthesis decision | Satisfied with scope; original binding/host limitations retained; complete release binding remains open | + +## Measurements + +No synthesis-primary benchmark measurement. Consume only the scoped decisions of the children declared in Proof Obligations. Native correctness, diagnostic construction, generated component bounds and exact production acceptance remain distinct evidence classes. + +`Product admission composition`: The reference `12 / 24 / 48` values are independent structural ceilings, not a Cartesian resource promise. For one authored Contract let `S` be Step count, `O` distinct Opening amount surfaces, `P` canonical Opening predicate occurrences and `F` tracked funding assets. The reachable pre-admission domain is constrained by `0 ≤ S ≤ 12`, `O ≤ min(2S, 24)`, `P ≤ min(4S, 48)` and `O + F ≤ L ≤ 2S`, plus the typed Task/host qualifications accepted by EXP-0029. The admitted subset is exactly the Contracts in that domain satisfying: + +```text +scheduler_admission_overhead ++ component_max over Steps(control_maximum(context, Step) + effect_maximum(Task)) ++ complete lifecycle_cleanup_maximum +<= guaranteed_actor_service_weight +``` + +Every addition and comparison is component-wise. `context` is derived from the authored Contract, including head/tail fragment, `O`, `P`, `F` and current predicate units. The tuple alone cannot bypass Task-specific effect Weight or branch-specific control Weight. User/System use the same resource predicate; User admission additionally validates fee liability without changing scheduler order or resource limits. + +Current-tree executable evidence fixes the pre-G5 source calculation at: + +| Witness | Complete admission Weight | Guaranteed Actor service | Decision | +| --- | --- | --- | --- | +| Sparse 12-Step fixed Mint, `O=0`, `P=0` | `16,387,281,717 / 207,630` | `118,617,525,387 / 285,993` | Admitted in both dimensions | +| Dense authored `S/O/P = 12/24/48` AddLiquidity | `21,754,626,668 / 443,218` | `118,617,525,387 / 285,993` | RefTime fits; ProofSize refuses | + +`twelve_twenty_four_forty_eight_product_is_selected_by_complete_contract_weight` constructs both legal Contracts and proves the component-wise result. `deos_reference_actor_contract_bounds_are_structural_and_genesis_is_resource_admitted` proves every active genesis System Contract against the guaranteed service floor rather than the broader raw on-idle reserve. Current retained genesis maximum before EXP-0063 selector regeneration is `17,300,716,984 / 214,641`, leaving both-dimensional service margin. + +`Final Waiting selector composition`: Accepted EXP-0063 contributes mutually exclusive whole owners: publication `1,031,223,000 / 76,618 +44R/30W` (`5,131,223,000 / 76,618` with the pinned database schedule) and removal `1,928,069,000 / 96,741 +70R/44W` (`8,078,069,000 / 96,741`). The finite selector is: + +| Transition cell | Selected owner | Matching containment | +| --- | --- | --- | +| Opening, Running, Suspended or temporal direct profile already publishing Waiting | Inclusive direct owner | Existing typed `actual_control_weight` regressions require actual ≤ its Contract maximum; no second publication | +| Otherwise-unmatched Wakeup destination after loaded Ready source | One complete EXP-0055 publication owner | All four accepted publication extrema are component-wise ≤ `5,131,223,000 / 76,618` | +| Due Block/Tick partial or removal worker | Matching inclusive due-worker owner | Physical worker plus maximum temporal occurrence is component-wise ≤ its drain-unit maximum and configured worker limit | +| Populated terminal/public close | One complete EXP-0056 removal owner | All five accepted removal extrema are component-wise ≤ `8,078,069,000 / 96,741`; no generic source/close/heap addition | +| Queue or Unsignaled destination | Existing matching owner; no Waiting owner | No publication or removal selected | + +`pre_g5_waiting_selector_is_finite_contained_and_genesis_fit` makes this partition exhaustive as a closed enum, proves the publication/removal and due-worker inequalities, and reconstructs the current one-Step genesis admission alternatives from independently named scheduler prefix, destination, Step and cleanup owners. The largest retained direct genesis envelope is `17,300,716,984 / 214,641`; replacing only the unmatched outer destination with the whole publication owner yields the selected pre-G5 maximum `21,515,651,984 / 274,824`. The whole populated-removal alternative does not dominate it. Against guaranteed service `118,617,525,387 / 285,993`, the final margin is `97,101,873,403 / 11,169`. Publication and removal are never combined, and no aggregate subtraction or generated coefficient edit is used. + +## Derived Metrics + +Composition requires a common admitted domain and compatible source/Weight/artifact identities. Do not maximize independent storage capacities into an unreachable state, add inclusive owners twice, or derive a disjoint bound by subtracting aggregate coefficients. + +## Statistical / Variance Notes + +For generated FRAME evidence, pin 50 steps × 20 repeats where applicable and review model/branch fit, not merely a mean. Identity and constructive correctness checks are deterministic; no wall-clock capacity inference. Declare a different method before measurement if the bounded question requires it. + +## Result + +All four required children supply compatible scoped decisions and the composition meets the governing criteria. The exact product contract is decided: `12 / 24 / 48` are structural ceilings, while the admitted subset is the authored reachable domain filtered by complete component-wise Contract Weight. A sparse 12-Step Contract fits and the dense authored `12/24/48` witness is refused by ProofSize. The finite Waiting selector preserves direct and due-worker whole owners, selects publication once only for an unmatched Wakeup destination, and substitutes removal for populated terminal close. Every retained actual is contained by its matching maximum. Current genesis Contracts fit the selected pre-G5 matrix with `11,169` bytes of ProofSize margin. Exact generated artifacts remain EXP-0065. + +## Interpretation + +The product cannot be honestly represented by three independently admissible maxima or by one static tuple frontier: authored Task algebra restricts reachability, and Task/control owners decide resource admission. The complete-Contract predicate preserves structural expressiveness and exact refusal without raising budgets or weakening Weight. Whole-owner selection also matters numerically: combining publication and removal would invent an unreachable additive path, while the finite transition partition retains both sound containment and genesis fit. Conditional optimization remains unallocated. + +## Decision + +Accepted for the current reference composition. Structural reachability is filtered by complete component-wise Contract Weight. Direct Waiting profiles remain inclusive; unmatched Wakeup selects publication once; due workers remain inclusive; populated terminal close selects removal whole. The selected pre-G5 genesis maximum fits the guaranteed service floor. Do not hand-edit generated coefficients, widen budgets, combine mutually exclusive Waiting owners or treat `12/24/48` as a Cartesian admitted product. EXP-0065 owns exact regeneration and changed-binding requalification. + +## Rejected Alternatives and Tradeoffs + +| Shortcut | Disposition | Reason | +| --- | --- | --- | +| Reopen C1 because this question is expensive | Excluded | Requires the explicit invariant/impossibility test in EXP-0025 | +| Reuse direct historical C1 as production acceptance | Excluded | Source/path/artifact authority differs | +| Hide missing control under another allowance | Excluded | Underweight, discontinuous cheap maximum, omitted liability/native read, changed terminal precedence, reserved Action allowance masking System base undercount, skipped fitting FIFO head or budget increase concealed as optimization. | + +## Validity + +- `Establishes`: One bounded question owner and its dependency/evidence contract; inherited findings retain original limits. +- `Does not establish`: New Weight soundness, final artifact identity, throughput, release acceptance or permission to change frozen C1. +- `Known limitations`: Accepted EXP-0028 supplies phase ownership, Accepted EXP-0029 supplies reachable geometry, Accepted EXP-0063 supplies Waiting envelopes and EXP-0054 supplies scoped native precedence. This decision is a pre-G5 composition over their current identities; it does not install or certify regenerated production artifacts. Existing native counterexamples are inherited evidence only. +- `Invalidation triggers`: Relevant source, semantics, host bounds, Weight, SDK/toolchain, configuration, fixture or dependency identity changes; refresh affected descendants. +- `Residual qualitative insight`: The exact Contract-level admission predicate, qualified structural product, finite Waiting selector and accepted envelope identities feed EXP-0065. EXP-0063's resolved Waiting/heap cost does not reopen C1. + +## Follow-Up / Next Gradient + +- `Produces`: Accepted admission selector, qualified product constraints and exact envelope identities feed EXP-0065. Any missing Waiting/heap cost is resolved by EXP-0063 and reconciled before binding closure; it does not reopen C1. +- `Blocking evidence required`: None within this synthesis. EXP-0065 must regenerate exact production Weight and requalify source, compact resource widths, provider identity, production Wasm and the selected envelopes on one final binding. An explicit changed-binding constraint result is valid evidence, not permission to weaken resource policy. +- `Stop condition`: Decide only this question against the stated criteria. Transfer materially distinct hypotheses to dedicated records; BACKLOG alone owns release-blocking work. +- `Residual trigger`: A production experiment must name a measured owner and changed assumption before any bitmap/page/shared-body/key-locality/compression/PoV candidate begins. + +## Architecture Impact + +- `Affected physical truth`: Phase-local resource envelope selection, within frozen C1. +- `Architecture update`: Package architecture records the native-qualified Suspended pre-cold refusal boundary, Contract-level admission predicate and mutually exclusive Waiting selector; exact generated production binding remains open in EXP-0065. +- `Provenance citation`: `actors/EXP-0064` with decision scope and exact identity once decided. + +## Relations + +- `Decomposes into`: [EXP-0028](./EXP-0028.md), [EXP-0029](./EXP-0029.md), [EXP-0063](./EXP-0063.md), [EXP-0054](./EXP-0054.md) +- `Satisfies obligation`: None. +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: [EXP-0028](./EXP-0028.md), [EXP-0029](./EXP-0029.md), [EXP-0053](./EXP-0053.md), [EXP-0054](./EXP-0054.md), [EXP-0063](./EXP-0063.md) +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: [EXP-0028](./EXP-0028.md), [EXP-0029](./EXP-0029.md), [EXP-0063](./EXP-0063.md), [EXP-0054](./EXP-0054.md) +- `Transfers question to`: None; independent findings require an explicit new transfer. +- `Produces input for`: [EXP-0065](./EXP-0065.md) +- `Reopen trigger`: A falsifier of this declared owner/domain or exact artifact identity; cost alone does not reopen frozen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0065.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0065.md new file mode 100644 index 00000000..eb97103e --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0065.md @@ -0,0 +1,189 @@ +# EXP-0065 — Production Binding Closure + +| Field | Value | +| --- | --- | +| Status | Invalidated | +| Record kind | Leaf | +| Former IDs | actors/EXP-0032@a60de7748132211d9e58cecd5be82b41f76ce230 | +| Parent question | None | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-07 | +| Affected domain | Exact production artifact and consumer identity | +| Physical mechanism | Source-to-generated production binding | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0025](./EXP-0025.md) — Accepted, physical architecture only | +| Successor experiment | [EXP-0066](./EXP-0066.md) | + +## Decision Question + +Can one exact final source tree generate self-consistent Weight, Wasm, metadata, ABI, client bounds and storage identities? + +## Governing Specification + +- `Owners`: `template/pallets/actors/docs/specification.en.md` (sole ownership, lifecycle/Trigger/Pipeline/Action semantics, placement and scheduler); `docs/actors-resource-policy.specification.en.md` (component-wise admission/actual settlement, equal-thirds block policy); `docs/actors-performance-assurance.specification.en.md` (evidence and target). +- `Frozen physical input`: [EXP-0025](./EXP-0025.md) C1 Architecture Freeze. No change to FIFO, ticket chronology, Q1, causal floor, primary/reference ontology, source/successor atomicity or sole execution owner. +- `Semantic oracle`: A0 SHA-256 `8746e423168309975e509be91e4eb06fdf0eb0b43369d75f1ee62202a4341678` remains frozen; use only for relevant semantic equivalence, never as a Weight oracle. +- `Scope`: + +- Own complete Actors Weight regeneration and Router/TMC cross-pallet regeneration where Actors storage names changed. +- Bind benchmark source, benchmark Wasm, production source/Wasm, metadata, ABI/PAPI, client descriptors, cost vectors, runtime bounds and storage-name audit. +- Reconcile 12/24/48, maximum-tail/genesis and host-configurable package bounds, ActorSimulationApi v2 with explicit synthetic budgets and one-record result bound. +- Verify canonical browser and staking-bootstrap consumers against regenerated metadata and live reference composition, including finalized identity and corruption-versus-absence. +- Never manually edit generated coefficients/comments to evade stale-storage checks. + +## Context and Hypothesis + +- `Entry condition`: Satisfied: EXP-0028, EXP-0029, EXP-0063 and EXP-0064 are Accepted. Complete generation began only after those decisions, and the final bundle is accepted below. +- `Hypothesis`: One closed source/configuration/toolchain identity set can reproduce all production and client artifacts without stale scalar storage annotations or mismatched resource bounds. +- `Materiality`: Publish one exact source ↔ benchmark source ↔ generated Weight ↔ benchmark Wasm ↔ production Wasm ↔ metadata ↔ ABI/client-bounds identity set, with justified feature/build differences and no stale generated owners. All accepted upstream envelopes and canonical consumers must agree on that set. +- `Dependency boundary`: Satisfied by accepted complete Weight/admission/domain/Waiting outputs. This is the explicitly requested binding-consistency question, not a reason to open experiments for routine generation chores. + +## Baseline and Candidates + +| ID | Comparison | Source tree | Runtime/config identity | +| --- | --- | --- | --- | +| B0 | Retained frozen C1 with incomplete production evidence | Commit `5b46b19004b62a723d6c45a610dfe1e5c79982b0`; template `98af72ffbb55fe8840b5bb44b5b01d65c8e775e1` | Reference 12/24/48, equal thirds; current artifacts not accepted as a complete set | +| A | One closed source/configuration/toolchain identity set can reproduce all production and client artifacts without stale scalar storage annotations or mismatched resource bounds. | Accepted post-binding source/configuration identity `c5345d6ba9574b9f16d0c62541400df027edd954039167f413f9b0aa9297c26d` | Same frozen C1 and semantic policy; accepted dependency outputs identified | + +## Controls + +- `Controlled variables`: Freeze source/configuration, host bounds, toolchain/SDK, database/cache, legal state and workload for each comparison; name any changed input and refresh dependent evidence. +- `Changed variable`: Only the question-specific model, fixture, accounting or binding under test; no macrogeometry or authored semantics. +- `Confounders`: Historical native, direct-path, partial-heap and old generated evidence are narrower than production acceptance. [EXP-0026](./EXP-0026.md) and [EXP-0027](./EXP-0027.md) reject ceiling-only and state-hold-cache-only proof improvements; do not reintroduce them without changed evidence. + +## Benchmark Class, Workloads, and Criteria + +- `Smallest falsifier`: Run the storage-name/binding audit over a complete generated Actors/Router/TMC set, then compare metadata/ABI/client bounds and production Wasm against the same source identity. One retired storage owner or divergent 12/24/48/simulation bound rejects the set. +- `Evidence class`: Exact-tree generated identity and consumer integration validation. + +| Identity edge | Required artifact / check | +| --- | --- | +| Source ↔ benchmark source ↔ Weight | Exact content/mode/config/toolchain and complete generated files | +| Benchmark Wasm ↔ production Wasm | Separate hashes and declared feature/build difference, no accidental reuse | +| Production Wasm ↔ metadata ↔ ABI/PAPI | Regenerated runtime APIs, simulation bounds and storage identities | +| Runtime bounds ↔ client/cost vectors | 12/24/48, maximum-tail/genesis fit and canonical consumer paths | +| Actors ↔ Router/TMC | Changed Actors storage names and complete cross-pallet generated provenance | + +- `Acceptance criteria`: Publish one exact source ↔ benchmark source ↔ generated Weight ↔ benchmark Wasm ↔ production Wasm ↔ metadata ↔ ABI/client-bounds identity set, with justified feature/build differences and no stale generated owners. All accepted upstream envelopes and canonical consumers must agree on that set. +- `Rejection criteria`: Partial Weight fragment presented as full regeneration, handwritten annotation/coefficient cleanup, stale scalar/32-Step artifacts, mock-only consumer closure or unidentified Wasm reuse. + +## Environment and Artifact Identity + +| Artifact | Identity / required binding | +| --- | --- | +| Baseline source | Commit `5b46b19004b62a723d6c45a610dfe1e5c79982b0`; template tree `98af72ffbb55fe8840b5bb44b5b01d65c8e775e1` | +| New experimental source/config/toolchain | Accepted post-binding content-and-mode identity `c5345d6ba9574b9f16d0c62541400df027edd954039167f413f9b0aa9297c26d`; pre-generation identity `b9688a4f7259cda08f3b6360ba735e848df9d23999e72e6bf2c3d696eeb6aa06`; Rust `1.98.0 (88d9e12ae 2026-08-18)`; `frame-omni-bencher 0.22.0`, benchmark CLI 58.0.0; reference 12/24/48 and equal-thirds policy retained | +| Benchmark / production Wasm | Corrected Actors measurement Wasm `584565e3a340890333746882bb599b0b106caa761cb21466295b50740a2a4b40`; final converged Router/TMC benchmark compressed Wasm `3004ac8553962cb73c34c6adc0a063f3ae8fa10d56ae621a1ad855a647c72be5`; separate production compressed Wasm `155749e22a6235570d2bb97d4a42b1327b86f4f5d82742df36494fca0fd88c1d` | +| Generated Weight / metadata / ABI | Actors `20d56df225eca3ff52857f2170eccf50587c5ca17f997d18d083e61e8afbeed9`; Router `4f663e5925ebd187d15414f61439e6fb97ba61380aa1d29e946afdba0f5c34f6`; TMC `a1e792e801bc5f64be3024b7e29ba3a0acb70303addeabb6d787eea1d6eda6fa`; live-matching metadata `75868ea75fd85e7b79db12650a7407e89e249f5564b2157d55ac46cd26cb5d12`; descriptor package `2f9c6de877f2f4d575c49992a262109d1635f68e6b586cb9b34844cbf3849da2`; Actors ABI `7c645cde7c5405ee805281dace5cddd0e80fa60be6fdf45ac06fbeabe3403e71`; bounds `0cd7716d1e694b516a33a47c40f16cf97ba90510e8b4792feddac89826e29d15`; cost vectors `df65f158ba755e42f6487cac46525ce5f3a910b643172169054b9cb755030e33`; fee-envelope vectors `e7457d02450ba257253c325805c11b636650d1a8262796f622157c5fc64671e9` | +| Commands | `./scripts/benchmarks.sh --check`; complete Actors/Router/TMC generation at 50 × 20 in `/tmp/deos-exp-0065-actors-50x20-r3`, `/tmp/deos-exp-0065-router-50x20-final` and `/tmp/deos-exp-0065-tmc-50x20-final`; `./scripts/03-build-runtime.sh`; canonical-ParaId `./scripts/export-papi-metadata.sh`; runtime/client artifact generators; pinned-Node `./scripts/actors-assurance.sh --quick` and `npm run validate`; live reference Zombienet browser adapter/runtime identity probe and `bootstrap-native-staking-local.sh check --json`; dependent runtime compilation and changed-scope completion gate | +| Evidence owner | Baseline source and the question-specific facts below; new measurements must carry exact source/artifact identities in this record | + +## Proof Obligations + +- `Freeze`: Frozen at Measuring to O1 and the finite identity edges above; the accepted decision remains frozen. Newly independent selectors, owners or evidence questions transfer rather than expanding this Leaf. +- `Review triggers`: None. +- `Decomposition review`: One indivisible artifact-consistency decision: any stale member rejects the complete bundle. Regeneration and consumer checks verify that same identity contract; they are not new component-bound research. Any changed physical selector or independent benchmark question transfers to its own Leaf before generation continues. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | One exact complete source, generated Weight, benchmark/production Wasm, metadata and canonical-consumer identity set realizes all accepted upstream contracts. | One mismatch in the regenerated complete source/Weight/Wasm/metadata/consumer bundle prevents exact binding closure | Exact-tree production artifact and canonical-consumer integration closure | Self | Required | Named parent decision and declared downstream consumers | Satisfied; exact artifacts and both live canonical consumers converge on the recorded reference identity | + +## Measurements + +The initial preflight compiled the benchmark runtime in 73 seconds, then rejected the installed complete set for retired Actors storage annotations in Actors, Router and TMC. On one fresh compiled-Wasm cohort with 50 steps × 20 repeats, Router regenerated in 75 seconds after a 109-second build and TMC regenerated in 24 seconds by reusing exactly that benchmark Wasm. Generated files were installed only through the script's staged normalization and contract checks; no coefficient or storage comment was edited manually. + +| Pallet | Previous Weight SHA-256 | Generated Weight SHA-256 | Cross-pallet storage result | +| --- | --- | --- | --- | +| Router | `ca94deb1801f9ef2af45bb3914805a8d75e228c9836e0a7981fbd32de1ccf379` | `2d3a941cabf5ffe24cd18eed5c9909a114813ed1eabec1ce9823822332c1b7bb` | Retired ActorHot/Admission/Queue names removed; current split control, Run payload and Ready owners measured | +| TMC | `851d61b5f1e421630701b3c5cea63886b23d548dd1ad7d880eac5a077d08ab0b` | `97a5d76a2272514fd926e4bc80847c6f61f7f4bc36ce3bdaee57df284151baa1` | Retired ActorHot removed; current locator/control/activation/Run/tail owners measured | + +The repeated preflight compiled in 47 seconds and now reports stale names only in `pallet_deos_actors.rs`: ActorAdmissionCertificate, ActorHot, NextQueueTicket, QueueHead, QueueOccupancy, QueuePages, QueueTail, WakeupBuckets and WakeupPages. This is an expected incomplete-bundle falsifier, not acceptance. Cargo formatting, `cargo check --locked -p deos-runtime`, and diff checks pass for the retained cross-pallet generation. + +The first complete Actors attempt rebuilt the benchmark runtime in 121 seconds from the post-Router/TMC tree, producing compressed benchmark Wasm `c43d01fd41f69912be3459ff43f6cc8d906765bd4315fb326af6a83a36286d5a`. Before the 3,600-second execution bound terminated the run at 77%, 23 production benchmark paths failed at five shared setup constructors with `ContractStepsExceedOnIdleBudget`: deactivate/close/update, current-Step plan, scheduler inner Opening/Running/Suspended, and Run progress/suspend/complete/cancel paths. Each failed setup still requested a dense maximum Contract that the accepted component-wise G4 admission frontier intentionally rejects. No generated Actors file was installed; its SHA-256 remains `2d9c60ac7764d1e7dc366531dd4a409444daf66cf3e37ed7feb922bb614e8f1a`. + +Fixture correction now retains the requested structural Step/tail geometry and each measured Step, then reduces only unrelated setup payload until the complete Contract passes the same ordinary component-wise admission used in production. Crossing retry setup uses real bounded rearm plus admitted transition notification; successful Suspended-tail branches arise from a real unfunded fixed Transfer retry followed by spendable custody, not direct state installation. The focused regression preserves length/protected-Step identity and proves the resulting maximum against the service floor. A fresh benchmark build produced compressed Wasm `c9f31b7da31f9eb4415c16df2c47351c0ac30a4e8e53946139aafea6537f03e7`; every one of the 23 formerly failing production benchmarks executed successfully from it at 3 steps × 1 repeat. This is reachability smoke evidence, not generated production Weight. + +The next complete run reached 96% before `control_frame_locator_transition_batch32` exposed a benchmark-verification defect: its measured map writes were valid, but its postcondition assumed an empty `ActorControlLocators` map while the reference genesis already activates two System Actors. No output was installed. The fixture now selects fresh IDs from `NextActorId` and verifies each requested location exactly without treating unrelated genesis entries as benchmark output. Fresh compressed benchmark Wasm `cf19dd830d7297821fb29bfa1164fd1f43e74bf47c9c8121cd12b559994eb129` passed the focused path at 3 steps × 1 repeat with the expected `0 + n` writes. + +The corrected complete run then succeeded in 4,337 seconds and installed Actors output `9926c2320691ec44444ef5d61599f5fbdf08467e91637aab1aacdcdf34eaa880`, but the first dependent runtime build correctly rejected it with 19 interface errors. Thirteen EXP-0064 candidate-only control probes had not been marked `extra`, so they leaked into a production trait implementation; the six Suspended-head benchmarks generated their deliberately coupled `n,p` or `p` signatures while `WeightInfo` and runtime selectors still claimed the retired independent Opening/result/funding axes explicitly rejected by EXP-0048. The source now marks all candidate probes extra, binds `WeightInfo` to the measured coupled signatures, commits their bounded endpoints in the production identity and retains the complete admitted maximum rather than issuing an unsupported independent-axis Suspended-head refund. A bootstrap-only removal of the leaked methods from the rejected output restored compilation; no generated coefficient or proof annotation is accepted from that cleanup, and the corrected complete run must supersede it. + +`./scripts/benchmarks.sh --check`, runtime compilation, the retained allocation-boundary test, production-identity regression and exact funding-retry regression pass. All six corrected Suspended-head profiles execute at 3 × 1 from benchmark Wasm `584565e3a340890333746882bb599b0b106caa761cb21466295b50740a2a4b40`. Corrected benchmark source is `6afc5421deb8a37011e4a12499427ee7d2944025513fba6bbd41b0c4c9c52b76`. + +The corrected complete Actors run succeeded in 3,952 seconds at 50 × 20 and installed Weight `20d56df225eca3ff52857f2170eccf50587c5ca17f997d18d083e61e8afbeed9`. Dependent runtime compilation and the storage-name audit passed without candidate-only methods or stale Suspended-head signatures. Rebuilding the benchmark runtime from that installed Weight produced compressed Wasm `3004ac8553962cb73c34c6adc0a063f3ae8fa10d56ae621a1ad855a647c72be5`; Router then completed in 74 seconds and TMC in 24 seconds at 50 × 20, installing `4f663e5925ebd187d15414f61439e6fb97ba61380aa1d29e946afdba0f5c34f6` and `a1e792e801bc5f64be3024b7e29ba3a0acb70303addeabb6d787eea1d6eda6fa`. + +The first full behavioral gate against this final cohort exposed 15 stale pre-final Weight snapshots, including an assertion that still expected an independent Suspended-head receipt refund. The retained tests now bind the final generated owners and explicitly preserve the conservative coupled-head no-refund contract. All 206 focused Actors integration tests pass, followed by the complete changed-scope alignment gate; the converged three-Weight set therefore passes behavioral validation, dependent compilation and the generated storage-name audit. + +The deterministic production build completed in 404 seconds and published compressed Wasm `155749e22a6235570d2bb97d4a42b1327b86f4f5d82742df36494fca0fd88c1d`, distinct from the benchmark Wasm. Metadata export completed in 306 seconds, descriptor generation in 17 seconds and runtime-evidence generation in one second. The retained metadata, descriptor, ABI, 12-Step bound, fee-envelope and cost-vector hashes are listed above. + +The regenerated metadata falsified stale browser assumptions: direct `ActorHot` active reads no longer compile, cost and authoring fixtures still claimed 32 Steps, and the matching-Wasm codec still required five-input `ActorSimulationApi` v1. The retained client now resolves one bounded canonical control cell through `ActorControlLocators`, distinguishes NotRegistered, Dormant and corrupt split/mismatched authority, binds `ActorSimulationApi` v2's explicit synthetic Actor Control/Shared Economic budget into request/response provenance, accepts `Continued`, and rejects more than one result Step. Pinned Node 22.22.0 Actors quick assurance passed in 75 seconds; all 151 browser automation tests, Svelte checking and formatting pass. Operator evidence scripts use the same canonical control projection; staking bootstrap already consumes bounded `ActorEligibilityApi` state. + +The first live reference-composition pass correctly rejected the candidate metadata identity. Production `:code` matched `155749e22a6235570d2bb97d4a42b1327b86f4f5d82742df36494fca0fd88c1d`, but Metadata API bytes were `75868ea75fd85e7b79db12650a7407e89e249f5564b2157d55ac46cd26cb5d12` while the native exporter had produced `4b566e3df9e0f5633e89862c60bc8c0c311566c605193ec78ee603f051cfaf90`. Four bytes differed: the exporter ran under empty externalities and encoded storage-backed `ParachainSystem::SelfParaId` as 100 rather than the reference 2000, with the corresponding `PolkadotXcm::UniversalLocation` difference. The exporter now builds canonical ParaId externalities, its regression passes, and regenerated metadata exactly matches all 288,278 live bytes. + +The corrected live pass used the generated chain spec and owned Zombienet topology. The Vite-loaded production `BlockchainAdapter` resolved all 14 known System Actors through canonical control (3 Active, 11 Dormant), read Actor resource APIs and the finalized authoring context, retained the 12-Step bound, and matched both finalized runtime code and metadata hashes exactly. `bootstrap-native-staking-local.sh check --json` exposed a second stale assumption: `NativeSecurityView` is mode-shaped, not a struct containing `mode`. After correction it decoded finalized TrustedSet epoch 0, both genesis staking assets, bounded Dormant Actor 14, and honestly reported `missing-staking-assets` because the ephemeral reference state had no exchange rate or AMM pool. Every owned process was terminated and ports 9944/9988/9999 were clear. + +## Derived Metrics + +No new derived metric. Inherited numbers cannot be promoted beyond their original source and evidence class. + +## Statistical / Variance Notes + +For generated FRAME evidence, pin 50 steps × 20 repeats where applicable and review model/branch fit, not merely a mean. Identity and constructive correctness checks are deterministic; no wall-clock capacity inference. Declare a different method before measurement if the bounded question requires it. + +## Result + +Accepted. Corrected Actors and converged Router/TMC generation completed at 50 × 20. Production Wasm, live-matching metadata, ABI/PAPI, descriptors, 12-Step bounds, fee-envelope and cost vectors form the recorded exact bundle. Pinned source/type/behavioral assurance and live finalized browser/staking consumers converge on it. + +## Interpretation + +The converged Weight and downstream artifact cohort removes stale generated bindings without changing C1. Fixture and locator corrections remove false blockers without weakening production admission. Live execution rejected storage-default metadata and mode-shape client drift before acceptance, and the retained fixes bind generated metadata constants and both consumers to the canonical reference composition. O1 is satisfied; this establishes exact binding closure only, not W0–W9 throughput. + +## Decision + +Accepted. Permanently reject the first complete Actors output and the empty-externalities metadata candidate. Retain the corrected Weight, production Wasm, canonical-ParaId metadata, ABI/PAPI, descriptor, bounds and vector cohort as one exact bundle. EXP-0066 is now unblocked on this identity; no throughput or release-Geometry claim is imported. + +## Rejected Alternatives and Tradeoffs + +| Shortcut | Disposition | Reason | +| --- | --- | --- | +| Reopen C1 because this question is expensive | Excluded | Requires the explicit invariant/impossibility test in EXP-0025 | +| Reuse direct historical C1 as production acceptance | Excluded | Source/path/artifact authority differs | +| Manually repair generated annotations | Excluded | Partial Weight fragment presented as full regeneration, handwritten annotation/coefficient cleanup, stale scalar/32-Step artifacts, mock-only consumer closure or unidentified Wasm reuse. | + +## Validity + +- `Establishes`: Historically, Accepted G4 inputs were ready; all 23 admission-blocked fixture paths execute through ordinary admission; the locator-write fixture passes against non-empty reference genesis; candidate probes are extra-only; all six coupled Suspended-head signatures compile and execute; complete Actors/Router/TMC 50 × 20 Weight generation passes; and the exact production Wasm, live-matching metadata, ABI/PAPI, descriptors, 12-Step bounds, vectors and finalized browser/staking consumers converge. +- `Does not establish`: Integrated W0–W9 throughput, release acceptance or permission to change frozen C1. +- `Known limitations`: Earlier complete attempts stopped at 77% and 96%, the first successful Actors output failed dependent compilation, and the first metadata candidate encoded empty-externalities ParaId 100. They remain rejected evidence. +- `Invalidation triggers`: Triggered on 2026-09-08 by EXP-0067's User completion selector and regenerated Actors Weight/Wasm. EXP-0095 owns replacement binding closure. +- `Residual qualitative insight`: This accepted exact artifact set unlocks EXP-0066. Any upstream correction invalidates affected generated descendants; refresh those dependencies without reopening physical C1. + +## Follow-Up / Next Gradient + +- `Produces`: Historical exact artifact set `c5345d6ba9574b9f16d0c62541400df027edd954039167f413f9b0aa9297c26d` with production Wasm `155749e22a6235570d2bb97d4a42b1327b86f4f5d82742df36494fca0fd88c1d` and metadata `75868ea75fd85e7b79db12650a7407e89e249f5564b2157d55ac46cd26cb5d12` unlocks EXP-0066. Any upstream correction invalidates affected generated descendants; refresh those dependencies without reopening physical C1. +- `Blocking evidence required`: Satisfied by exact live canonical-browser and staking-bootstrap identity checks. +- `Stop condition`: Decide only this question against the stated criteria. Transfer materially distinct hypotheses to dedicated records; BACKLOG alone owns release-blocking work. +- `Residual trigger`: A production experiment must name a measured owner and changed assumption before any bitmap/page/shared-body/key-locality/compression/PoV candidate begins. + +## Architecture Impact + +- `Affected physical truth`: Source-to-generated production binding, within frozen C1. +- `Architecture update`: Historical exact binding invalidated as current by changed generated descendants; no change to frozen C1 physical geometry. +- `Provenance citation`: `actors/EXP-0065`, Accepted for exact production binding only, with the identities recorded above. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: None. +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: [EXP-0028](./EXP-0028.md), [EXP-0029](./EXP-0029.md), [EXP-0063](./EXP-0063.md), [EXP-0064](./EXP-0064.md) +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None; current authority is explicitly invalidated by [EXP-0095](./EXP-0095.md). +- `Uses evidence from`: [EXP-0064](./EXP-0064.md), [EXP-0028](./EXP-0028.md), [EXP-0029](./EXP-0029.md), [EXP-0063](./EXP-0063.md) +- `Transfers question to`: [EXP-0095](./EXP-0095.md) — refreshed exact production binding. +- `Produces input for`: [EXP-0095](./EXP-0095.md); prior EXP-0066 evidence retains historical identity only. +- `Reopen trigger`: A falsifier of this declared owner/domain or exact artifact identity; cost alone does not reopen frozen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0066.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0066.md new file mode 100644 index 00000000..61e71aa6 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0066.md @@ -0,0 +1,168 @@ +# EXP-0066 — Integrated Actor Throughput + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Synthesis | +| Former IDs | actors/EXP-0033@a60de7748132211d9e58cecd5be82b41f76ce230 | +| Parent question | None | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-08 | +| Affected domain | Complete production block throughput and evidence sufficiency | +| Physical mechanism | Composition of independently owned Executive, service, resource and proof evidence | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0025](./EXP-0025.md) — Accepted, physical architecture only | +| Successor experiment | Conditional residual experiments; no optimization allocated by fission | + +## Decision Question + +Does frozen C1 on qualified exact production bindings satisfy the declared throughput and service contract, or support an explicitly scoped unmet-target constraint decision under governing release policy? + +This record owns composition and the final G6 conclusion, not primary measurements. The original integrated question and identity survive; independent outcomes, costs, selectors, proof anatomy and evidence-linkage questions have separate owners. + +## Governing Specification + +- `Semantics`: `template/pallets/actors/docs/specification.en.md` owns lifecycle, Trigger/Pipeline/Action fees, placement, custody and scheduler behavior. +- `Resource policy`: `docs/actors-resource-policy.specification.en.md` owns component-wise admission/settlement and equal-thirds resource policy. +- `Performance`: `docs/actors-performance-assurance.specification.en.md`, particularly §1.2, owns targets, metric definitions, service gaps and evidence sufficiency. +- `Frozen input`: [EXP-0025](./EXP-0025.md) owns C1 Architecture Freeze and the exact invariant/impossibility reopen test. FIFO, immutable cutoff, Q1, causal floor, committed-prefix durability and sole execution ownership remain unchanged. +- `Semantic oracle`: A0 SHA-256 `8746e423168309975e509be91e4eb06fdf0eb0b43369d75f1ee62202a4341678` is semantic evidence only, never a Weight oracle. + +## Context and Hypothesis + +- `Hypothesis`: Qualified complete production evidence either satisfies the performance contract or identifies a bounded, reproducible constraint without misrepresenting weaker evidence as acceptance. +- `Materiality`: The governing target remains 10,000 cheap Cycles within 100 eligible blocks and at least 100 Steps/distinct progressed Actors per saturated block, under both demand modes. +- `Entry qualification`: [EXP-0065](./EXP-0065.md) retains its historical artifact decision; [EXP-0067](./EXP-0067.md) reopens funded User Opening binding coverage. Earlier artifact identity does not certify that newly exposed domain. +- `Fission rationale`: Completion of a raw-node partition, a bounded lifecycle trace, a generated-charge ledger or a signed-state linkage question can be decided independently of integrated throughput. Treating them as one Leaf concealed those proof boundaries. + +## Baseline and Candidates + +- `Physical baseline`: Accepted C1 from EXP-0025; no candidate implementation or geometry sweep is introduced here. +- `Production baseline`: EXP-0065's exact binding remains the identity of extracted evidence. Changed binding inputs require affected-child review, not indiscriminate reruns. +- `Comparison ownership`: Leaves own the controlled workload comparisons. This Synthesis composes their scoped conclusions and unresolved conditions only. + +## Controls + +- `Composition control`: Never combine different source, runtime, Weight, fixture or evidence identities as one exact-tree result without an explicit equivalence proof. +- `Evidence control`: Native reconstruction, retained exact-Wasm state linkage, summary parity, generated I/O and raw proof nodes remain distinct classes. +- `Semantic control`: No Task/Control double-counting, cross-component credit, raw-proof discount, class priority or custody change. +- `Confounders`: System workload Actors plus signed user calls are not User Actor fee-path evidence; bounded horizons and missing repeat samples are not sustained fairness. + +## Benchmark Class, Workloads, and Criteria + +- `Class`: Synthesis of child decisions; no measurement is executed in this owner. +- `Coverage`: W0–W9 are navigation labels retained in their original evidence, not proof-node identities. Outcome, accounting and evidence-linkage obligations remain independently decidable. +- `Acceptance`: Every required obligation must be decided with applicable scope and exact identity; satisfy the governing performance contract or obtain the governing-policy permission for a precise constraint result. A Measured child is not an accepted input merely because a test passed. +- `Rejection`: Invalid fixtures, unfinished Executive/finalization, unsupported demand saturation, additive throughput estimates, incomplete target populations or semantic drift cannot support acceptance. + +## Environment and Artifact Identity + +- `Production binding`: [EXP-0095](./EXP-0095.md) owns production Wasm `25b9695fd9e900f17ae1f3fb0b815ac1403830264d9c31f7cee54e29f434b700`, generated Actors Weight `9ce37ade391a29d4f60779f1887ad242dfeb4919d25cf0047ffba85d18f33901` and metadata `75868ea75fd85e7b79db12650a7407e89e249f5564b2157d55ac46cd26cb5d12`. EXP-0065 is historical and cannot bind current acceptance. +- `Pre-fission source`: Dirty EXP-0066, SHA-256 `973bb0dd5f63430da3bff5aa1cc74a6df35b22b48510f47c44abf7dd8519f923`, 170,875 bytes, mode `100644`, status Measuring/Leaf. Git baseline commit `a8d9ec1a07fb7fdc383148d1dbd37c83874d62be`, tree `a1cb8c8fa19dd790af5032038a352bdfd2dc673d`; that commit alone does not contain the later uncommitted evidence. +- `Migration`: [Archived fission provenance](./experiments.md#current-decision-critical-path) binds source ranges, destination payload digests, Git baseline and pre-edit shared-file identities. The validated working manifest is retired into Git; the index records its exact commit/path. Literal extraction markers preserve historical text; navigation notes outside them resolve former local references. +- `Continuity`: No existing ID is renumbered, EXP-0069 remains reserved, and no benchmark, production artifact or source implementation changes through this migration. Temporary logs retain their original availability limitations. + +## Proof Obligations + +- `Freeze`: Frozen by explicit 2026-09-08 pre-fission reconstruction from the captured dirty source and canonical BACKLOG. This finite map replaces the former implicit broad O1; it does not claim that these independent obligations were separately frozen before measurement. +- `Review triggers`: More than six independent measurement sections; independently reusable raw-proof, generated-cost, service-stop and signed-state evidence; User Opening selector/domain discovery. All require fission, not a size-only split. +- `Decomposition review`: EXP-0070–EXP-0086 receive existing primary evidence. EXP-0067 remains a prerequisite, not a child or duplicated fee witness. Proposed EXP-0087–EXP-0093 name already-open coverage obligations, with no invented measurement or acceptance; real records provide reciprocal ownership required by the graph validator. New independent questions must transfer before investigation; no raw evidence may accumulate here. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | System schedule outcomes and censored waits are truthful within retained campaigns | Failed Transfer counted as a Cycle or unserved Actor omitted | Exact-Wasm full-block profiles | [EXP-0070](./EXP-0070.md) | Required | Scoped System target conclusion | Accepted; current W0–W2 target misses with exact state/proof linkage | +| O2 | Declared Contract geometry preserves ordered service and Running timelines | Cursor/completion mismatch | Exact-Wasm bounded matrix | [EXP-0071](./EXP-0071.md) | Required | Opening/middle/final interpretation | Accepted; current exact-Wasm bounded geometry | +| O3 | Heterogeneous Task outcomes and class waits are correctly distinguished | Typed outcome or population reconciliation failure | Exact-Wasm bounded campaigns | [EXP-0072](./EXP-0072.md) | Required | Task-mix interpretation | Accepted; current bounded W4, no Task-only causal isolation | +| O4 | Lifecycle outcomes preserve the committed prefix | Duplicate or compensated Step, incorrect terminal state | Exact-Wasm lifecycle timeline | [EXP-0073](./EXP-0073.md) | Required | Lifecycle interpretation | Accepted; current bounded short-retry fixture only | +| O5 | Actual readiness clocks and generation govern service | Stale generation executes or clock domains conflated | Exact-Wasm User arrival timeline | [EXP-0074](./EXP-0074.md) | Required | Arrival/eligibility interpretation | Accepted; current bounded W6, not temporal-rate or scale acceptance | +| O6 | Non-due population stays outside due service work | Changed due prefix or charged Control on matched frontier | Exact-Wasm matched populations | [EXP-0075](./EXP-0075.md) | Required | Active-frontier isolation | Accepted; current paired bounded populations | +| O7 | Legal Ready tombstone prefixes have bounded reclaim/service pressure | Wrong reclaim or live FIFO prefix | Exact-Wasm bounded prefixes | [EXP-0076](./EXP-0076.md) | Required | Ready pressure interpretation | Accepted; current bounded C32 prefixes, no layout selection | +| O8 | User demand preserves separate Actor resource accounting | Changed Actor prefix/accounting on identical frontier | Exact-Wasm demand comparison | [EXP-0077](./EXP-0077.md) | Required | Demand independence | Accepted; current bounded profiles, RefTime-heavy fallback only | +| O9 | Temporal materialization and service share the recorded phase with cutoff deferral | Phase or newly materialized/progressed overlap mismatch | Native phase checkpoints with exact-Wasm block replay | [EXP-0078](./EXP-0078.md) | Required | Schedule phase attribution | Accepted; current bounded matched schedules | +| O10 | Temporal generated charges and admission follow their component-wise owners | Ledger differs from live production selectors | Generated-Weight/native selector checks | [EXP-0079](./EXP-0079.md) | Required | Non-overlapping resource composition | Accepted; current generated selectors, not universal frequencies | +| O11 | Key paths plus committed-root witness cover matched execution proof nodes | Any unexplained node or invalid reconstructed root | Repeated exact-Wasm proof reconstruction | [EXP-0080](./EXP-0080.md) | Required | Scoped raw-node coverage | Satisfied within 18 retained blocks; Accepted | +| O12 | Bounded Manual stops use live eligible heads and phase-local budgets | Head/cutoff/path/budget mismatch | Native prefix and source-bound reconstruction | [EXP-0081](./EXP-0081.md) | Required | Manual stop interpretation | Accepted; current bounded Manual path, wider paths excluded | +| O13 | Successful System Transfer has a complete separated charge delta | Block/phase/effect equation mismatch | Native ledger and exact-Wasm preparation | [EXP-0082](./EXP-0082.md) | Required | Marginal charge owner | Accepted; current bounded System Transfer, not User fees | +| O14 | No-due and genesis work have distinct baseline owners | Startup counted as useful occurrence or no-due mismatch | Native accounting with retained Wasm totals | [EXP-0083](./EXP-0083.md) | Required | Fixed/no-progress accounting | Accepted; current generated baseline owners | +| O15 | System schedule charged-owner frequencies close the retained cohorts | Owner equation, occurrence order or retained header mismatch | Native workers with scoped retained-Wasm linkage | [EXP-0084](./EXP-0084.md) | Required | System schedule resource ledger | Accepted; current bounded System schedule cohorts | +| O16 | Signed-demand accounting has sufficient production evidence linkage | Native parity promoted despite unmatched signed states | Native accounting versus retained signed-Wasm evidence | [EXP-0085](./EXP-0085.md) | Required | Signed-demand evidence sufficiency | Accepted; 100 current signed blocks linked to exact Wasm state/proof replay | +| O17 | Bounded lifecycle settlement has complete nonduplicated owners | Provisional/terminal/effect double-count or header mismatch | Source-derived accounting with retained Wasm headers | [EXP-0086](./EXP-0086.md) | Required | Lifecycle resource ledger | Accepted; current bounded W5, broader paths excluded | +| O18 | Required User/reactive target populations are covered on qualified bindings | System-only witnesses substituted for required population | Complete production profiles or evidenced reachable-population constraint | EXP-0087 | Required | Governing target decision | Accepted negative: 10,000/100 target unreachable under current cap/admission | +| O19 | Funded User Pipeline collection is separate from Trigger and Action | Pipeline charge missing, duplicated or borrowed | Native full-Executive monetary/resource evidence | [EXP-0088](./EXP-0088.md) | Required | User Pipeline resource coverage | Accepted; bounded StopCycle Pipeline witness | +| O20 | Reactive execution has complete detector/fanout/service accounting | Missing or duplicated executed owner | Qualified reactive production-path evidence | [EXP-0089](./EXP-0089.md) | Required | Reactive resource coverage | Accepted for selected funded ObservationChange-to-Transfer path | +| O21 | Heavy economic Task coverage satisfies the resource contract | Required liquidity-heavy effect or Shared Economic limit left uncovered | Qualified complete-path evidence | EXP-0090 | Required | Remaining economic Task coverage | Accepted for selected donation cohort; Actor Control ProofSize binds before Shared Economic | +| O22 | Service evidence meets required fairness scope beyond bounded censoring | Censored or inter-cycle waits mislabeled as Running bounds | Eligibility-aware production service evidence | EXP-0091 | Required | Fairness evidence sufficiency | Accepted negative: current evidence cannot support required User fairness | +| O23 | Remaining service-stop claims use their actual phase-local path | Finalized maximum comparison substituted for a stop | Head/cutoff/eligibility and local-budget evidence | EXP-0092 | Required | Complete stop attribution | Accepted for selected non-Manual production prepass | +| O24 | Long retry placement has complete resource ownership | Short Ready retry charges substituted for a Wakeup path | Qualified long-retry complete-path evidence | EXP-0093 | Required | Remaining retry coverage | Accepted for selected System `SwapIn` path | +| O25 | Funded User Action collection is separate from Trigger and Pipeline | Action fee missing, duplicated or borrowed | Qualified successful-Action monetary/resource evidence | [EXP-0097](./EXP-0097.md) | Required | Complete funded User fee coverage | Accepted; selected funded User Transfer path | +| O26 | ObservationCrossing detector/latch and service owners compose separately from broad fanout | Broad-fanout owner borrowed or Crossing owner omitted | Source-bound W6 ledger with exact-state linkage | [EXP-0098](./EXP-0098.md) | Required | Complete reactive-family coverage | Accepted for selected funded Crossing-to-Transfer path | + +## Measurements + +Child-decision pointers only: [System service](./EXP-0070.md), [Contract geometry](./EXP-0071.md), [Task mix](./EXP-0072.md), [lifecycle](./EXP-0073.md), [arrival](./EXP-0074.md), [due frontier](./EXP-0075.md), [tombstones](./EXP-0076.md), [demand independence](./EXP-0077.md), [phase ownership](./EXP-0078.md), [temporal selectors](./EXP-0079.md), [raw proof](./EXP-0080.md), [Manual stops](./EXP-0081.md), [Transfer delta](./EXP-0082.md), [baseline](./EXP-0083.md), [schedule accounting](./EXP-0084.md), [signed linkage](./EXP-0085.md), [lifecycle accounting](./EXP-0086.md). + +All required child obligations are decided in the [experiment index](./experiments.md#experiment-index). Numerical and causal claims retain their narrow Leaf owners. + +## Derived Metrics + +The decisive integrated derivation is bounded: configured User Crossing admission `9,000 < 10,000`, while generated detection capacity `4 * 100 = 400 < 10,000`. Thus current production can cover at most 4% of the governing target window before scheduler/effect service. Leaves retain all resource, stop and proof calculations; composition never converts generated I/O to physical accesses or adds overlapping owners. + +## Statistical / Variance Notes + +Use each Leaf's exact evidence class, population, horizon, repeat method and censoring limits. Deterministic summary equality is not signed-state identity, noisy timing variance or sustained fairness. No missing repeat sample is zero latency. + +## Result + +All 26 required obligations are decided on current EXP-0095 binding. Bounded System, User, reactive, lifecycle, resource, stop and proof evidence is accepted within each declared scope. The governing 10,000-User ObservationCrossing target over 100 eligible blocks is not constructible and cannot be materialized: current production admits 9,000 User members/feed and four Crossing candidates/block. Sustained required-population fairness is therefore unsupported. + +## Interpretation + +Evidence closure and target satisfaction are different decisions. C1 preserves its frozen semantics and has complete scoped production evidence, but it does not satisfy the governing target. The exact negative constraint—not System proxy success, censored fairness or speculative optimization—must enter G8. Cost alone does not reopen C1, and known shared proof authorizes no Weight reduction. + +## Decision + +Accepted as a negative constraint Synthesis. G6 evidence composition is complete on EXP-0095, while performance-assurance §§2.1–2.3 remain unsatisfied under current User/feed and Crossing admission bounds. This does not authorize target rewriting, System substitution, C1 reopening or Geometry Freeze; G7/G8 must carry the explicit constraint to the release decision. + +## Rejected Alternatives and Tradeoffs + +- `One expanding Leaf`: Rejected as an ownership model; independently decidable evidence cannot be hidden behind one broad O1. +- `Green-leaf target acceptance`: Rejected because complete evidence exposes rather than removes the governing target failure. +- `Historical throughput substitution`: Direct C1/A0, failed-Step counters and sorted prefix membership retain only their historical scope; their negative controls remain in [EXP-0022](./EXP-0022.md), [EXP-0026](./EXP-0026.md), [EXP-0027](./EXP-0027.md) and the extracted Leaves. +- `Premature optimization`: No coefficient patch, aggregate subtraction, raw-proof discount or new topology follows from fission. + +## Validity + +- `Establishes`: Complete G6 proof-map decisions, current binding, scoped resource soundness and explicit target/fairness constraints. +- `Does not establish`: Required User/reactive target success, sustained User fairness, universal physical I/O minima or release acceptance. +- `Known limitations`: Historical extraction text remains historical; narrow Leaves own current numerical evidence. Temporary logs remain local evidence rather than durable release artifacts. +- `Invalidation triggers`: Relevant semantics, source, host bounds, SDK/toolchain, generated Weight, workload, artifact or evidence-method changes require affected-child review. +- `Residual insight`: A ranked resource owner may feed G7/G8 only after its complete-path attribution and governing gates are sufficient. + +## Follow-Up / Next Gradient + +- `Next`: [BACKLOG](../../../../../BACKLOG.md) owns G7 continuation/backpressure audits and G8 production acceptance under the explicit target constraint. +- `Stop condition`: Reopen only a falsified child or changed binding; do not rerun closed evidence solely to seek a preferred conclusion. + +## Architecture Impact + +- `Physical truth`: Frozen C1 and current production runtime remain unchanged. +- `Architecture update`: None; G6 closes evidence, not target compliance or release geometry. +- `Provenance citation`: `actors/EXP-0066` identifies integrated synthesis; cite the narrow child for a numerical or causal claim. + +## Relations + +- `Decomposes into`: [EXP-0070](./EXP-0070.md), [EXP-0071](./EXP-0071.md), [EXP-0072](./EXP-0072.md), [EXP-0073](./EXP-0073.md), [EXP-0074](./EXP-0074.md), [EXP-0075](./EXP-0075.md), [EXP-0076](./EXP-0076.md), [EXP-0077](./EXP-0077.md), [EXP-0078](./EXP-0078.md), [EXP-0079](./EXP-0079.md), [EXP-0080](./EXP-0080.md), [EXP-0081](./EXP-0081.md), [EXP-0082](./EXP-0082.md), [EXP-0083](./EXP-0083.md), [EXP-0084](./EXP-0084.md), [EXP-0085](./EXP-0085.md), [EXP-0086](./EXP-0086.md), [EXP-0087](./EXP-0087.md), [EXP-0088](./EXP-0088.md), [EXP-0089](./EXP-0089.md), [EXP-0090](./EXP-0090.md), [EXP-0091](./EXP-0091.md), [EXP-0092](./EXP-0092.md), [EXP-0093](./EXP-0093.md), [EXP-0097](./EXP-0097.md), [EXP-0098](./EXP-0098.md). +- `Satisfies obligation`: None. +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: [EXP-0064](./EXP-0064.md), [EXP-0067](./EXP-0067.md), [EXP-0095](./EXP-0095.md), [EXP-0070](./EXP-0070.md), [EXP-0071](./EXP-0071.md), [EXP-0072](./EXP-0072.md), [EXP-0073](./EXP-0073.md), [EXP-0074](./EXP-0074.md), [EXP-0075](./EXP-0075.md), [EXP-0076](./EXP-0076.md), [EXP-0077](./EXP-0077.md), [EXP-0078](./EXP-0078.md), [EXP-0079](./EXP-0079.md), [EXP-0080](./EXP-0080.md), [EXP-0081](./EXP-0081.md), [EXP-0082](./EXP-0082.md), [EXP-0083](./EXP-0083.md), [EXP-0084](./EXP-0084.md), [EXP-0085](./EXP-0085.md), [EXP-0086](./EXP-0086.md), [EXP-0087](./EXP-0087.md), [EXP-0088](./EXP-0088.md), [EXP-0089](./EXP-0089.md), [EXP-0090](./EXP-0090.md), [EXP-0091](./EXP-0091.md), [EXP-0092](./EXP-0092.md), [EXP-0093](./EXP-0093.md), [EXP-0097](./EXP-0097.md), [EXP-0098](./EXP-0098.md). +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: [EXP-0065](./EXP-0065.md), [EXP-0070](./EXP-0070.md), [EXP-0071](./EXP-0071.md), [EXP-0072](./EXP-0072.md), [EXP-0073](./EXP-0073.md), [EXP-0074](./EXP-0074.md), [EXP-0075](./EXP-0075.md), [EXP-0076](./EXP-0076.md), [EXP-0077](./EXP-0077.md), [EXP-0078](./EXP-0078.md), [EXP-0079](./EXP-0079.md), [EXP-0080](./EXP-0080.md), [EXP-0081](./EXP-0081.md), [EXP-0082](./EXP-0082.md), [EXP-0083](./EXP-0083.md), [EXP-0084](./EXP-0084.md), [EXP-0085](./EXP-0085.md), [EXP-0086](./EXP-0086.md). +- `Transfers question to`: G7/G8 release decision under EXP-0087 and EXP-0091's explicit negative constraints. +- `Produces input for`: G7/G8 and final release decision, owned by BACKLOG. +- `Reopen trigger`: A falsifier of a named obligation or changed relevant artifact identity; cost alone never reopens C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0067.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0067.md new file mode 100644 index 00000000..ab8a3b2c --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0067.md @@ -0,0 +1,202 @@ +# EXP-0067 — User Opening Settlement Binding + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | None | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-08 | +| Affected domain | Funded nonzero-Step User Opening Control settlement | +| Physical mechanism | Actor-class-qualified inclusive Opening ownership | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0025](./EXP-0025.md) — physical architecture only | +| Successor experiment | None | + +## Decision Question + +Does the production nonzero-Step User Opening path select a qualified inclusive Control owner for its Pipeline collection and settlement work, rather than relying on a System-derived minimal owner without demonstrated coverage? + +## Governing Specification + +- `Owners`: `template/pallets/actors/docs/specification.en.md` and `docs/actors-resource-policy.specification.en.md` retain independent fee owners, complete Weight accounting, admission/actual containment and transactional settlement. +- `Accepted input`: [EXP-0031](./EXP-0031.md) qualifies inclusive User Opening collection ownership; [EXP-0062](./EXP-0062.md) independently qualifies Action collection. Their scoped measurements are not invalidated by this binding question. +- `Frozen invariants`: Preserve C1, class-neutral FIFO, immutable cutoff, Q1, causality, fee semantics, custody neutrality and independent committed-Step durability. +- `Boundary`: Correcting a coverage/selector defect is not a G8 optimization or permission to change economic semantics. A semantic conflict must return to its specification owner. + +## Context and Hypothesis + +G6 funded-User inspection found that a real one-Step Manual `StopCycle` User pays a nonzero Pipeline fee, while its full-block and Prepass Control equal the System counterpart. The baseline classification witness was produced during EXP-0066's permitted smallest independence check; detailed evidence is transferred here, not duplicated in the integration Leaf. + +`RuntimeStepControlWeight::actual_control_weight` selects `scheduler_inner_opening_complete_min(0)` for this context. That minimal benchmark is System-derived. Its arguments do not carry Actor type, and `StopCycle` has neither an Action collection allowance nor an invocation receipt, so those independent owners cannot distinguish this User execution. `scheduler_paged_execute_opening_max` appears in the control-model metadata and a maximal admission branch, but not in this minimal actual-settlement branch. Its presence in the model is not proof that this User execution settles through it. + +The hypothesis is that a class-qualified inclusive settlement can restore the accepted ownership contract without counting overlapping source/collection work twice. Identical charged Weight is a coverage warning, not by itself a numerical underweight proof: existing envelopes may overprice other work. The release-relevant issue is the missing demonstrated User-domain binding, not a claimed measured RefTime or ProofSize shortfall. + +## Baseline and Candidates + +| ID | Binding | State | Purpose | +| --- | --- | --- | --- | +| Baseline | Current System-derived minimal Opening settlement also selected for the User witness | Inspected and observed natively | Classify the coverage gap | +| A | Explicit qualified inclusive User Opening settlement, preserving separate Action collection/effects | Implemented and bound by EXP-0095 | Prove complete coverage and single ownership before refreshed production acceptance | + +Do not mechanically add an isolated Pipeline collection primitive to an inclusive owner, subtract overlapping aggregate proofs, or assume the existing maximum-head User benchmark covers every reachable outcome without checking its domain. + +## Controls + +- `Baseline pair`: Separate fresh reference-genesis externalities; one Manual Persistent Actor with one unpredicated `StopCycle`, either System or User; same owner, funding, block clock and runtime binding. +- `Funding`: Actor sovereign balance is 1,000 ED before the useful Trigger; User Trigger and Pipeline payments are verified separately against payer/Fee Sink deltas. +- `Excluded`: Creation/setup costs in measured block Control, Action invocation, successful transfers, User zero-Step, Running/Suspended service and general host implementations. +- `Candidate controls`: Freeze the exact comparison and inclusive owner domain before candidate measurement; preserve all governing semantic invariants. + +## Benchmark Class, Workloads, and Criteria + +The initial evidence is a native full-Executive monetary/accounting witness plus a production-selector/source audit, not a Wasm timing measurement or a universal lower bound. + +| Witness | Geometry | Deciding observation | +| --- | --- | --- | +| Manual StopCycle pair | One Step, no predicates/capture/Run, funded System versus User | Exact fee deltas and receipts alongside selected Control | + +Acceptance requires a demonstrated complete User Opening owner, no duplicated collection/source charge, and maximum/actual component-wise containment over the qualified domain. Preserve exact monetary deltas and receipts. Refresh production artifacts and affected integrated evidence only after a correction or changed binding earns that refresh. Reject unqualified System-only refunds and overlapping aggregate arithmetic as coverage proofs. + +## Environment and Artifact Identity + +| Artifact | Identity | +| --- | --- | +| Git baseline commit / tree | `a8d9ec1a07fb7fdc383148d1dbd37c83874d62be` / `a1cb8c8fa19dd790af5032038a352bdfd2dc673d` | +| Worktree qualification | Existing uncommitted diagnostics retained; file hashes identify the probe, not a clean tree attestation | +| Native Rust toolchain | `rustc 1.94.1 (e408947bf 2026-03-25)` under `template` | +| Producer file | `template/runtime/src/tests/production_block_replay.rs`, SHA-256 `c3c14d2546f34323328b6bb40f5a662398523ea478e633f2f6342c4c3b9188e3` | +| Runtime selector | `template/runtime/src/configs/actor_config.rs`, SHA-256 `e2d1efb3276363f1e7e8352c55be2214880df12c41cc70f2751fc26d4fcfda82` | +| Scheduler | `template/pallets/actors/src/scheduler.rs`, SHA-256 `20a0188594958caf42b46ce5cb4177bb64193ca37575c979432f9c6455395f9b` | +| Benchmark source | `template/pallets/actors/src/benchmarking.rs`, SHA-256 `6afc5421deb8a37011e4a12499427ee7d2944025513fba6bbd41b0c4c9c52b76` | +| Generated Actors Weight | `20d56df225eca3ff52857f2170eccf50587c5ca17f997d18d083e61e8afbeed9`, unchanged | +| Execution/backend | Native `TestExternalities`, RocksDbWeight; no Wasm executed or generated | +| Reproduction | `RUST_TEST_NOCAPTURE=1 DEOS_VERBOSE=1 SKIP_WASM_BUILD=1 scripts/ci-local.sh --only tests --package deos-runtime --test-filter full_executive_funded_pipeline_collection_witness` | +| Raw log | `/tmp/deos-0725-funded-pipeline-owner.log`, SHA-256 `6458f94cc87126ba4933174ac25f4543b80c5a61ad63473bcf3d5061e529711e`; normalized deciding data retained below | + +## Proof Obligations + +- `Freeze`: Frozen on 2026-09-08 before production selection: zero tails use EXP-0069 H16; positive tails use EXP-0094 T16; the existing scalar context, complete inner boundary, separate receipt/Action owners and component-wise containment remain fixed. +- `Review triggers`: New production-selector qualification question discovered in EXP-0066; its one binding decision has two declared downstream consumers. Source qualification exposes a separately measurable inner-completion owner, transferred before measurement to EXP-0068. A public-admission header-width counterexample now identifies an independent completion-header envelope question, transferred to EXP-0069 before numeric investigation. +- `Decomposition review`: Transferred from integration to this independent nonzero-Step User Opening binding owner. Zero-Step, Action collection and other phases are not silently added to its claim. The two downstream consumers reuse that same binding decision rather than introducing independently measured owners. Source qualification transfers the inner-completion numeric owner to EXP-0068 and the admitted funding-policy header-width envelope to EXP-0069. The native alias witness is the smallest classification proof, not permission to grow numeric owner studies in this Leaf. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | Production nonzero-Step User Opening settlement has complete, non-duplicated qualified ownership | Funded User StopCycle with Pipeline collection but System-only settlement coverage | Source binding and native classification; qualified reference-Wasm evidence for any selected correction | Self | Required | G4 coverage qualification and G6 acceptance input | Satisfied with scope | + +## Measurements + +`full_executive_funded_pipeline_collection_witness` verifies exactly one useful Trigger receipt per Actor, a positive Trigger fee only for User, exactly one positive Pipeline receipt only for User, and exact payer/Fee Sink deltas for each stage. Both Actors complete one StopCycle with zero Action receipts and zero effect Weight. The test does not assert that equal System/User Control is correct or lock that equality as a desired contract. + +| Actor type | Trigger fee, native base units | Pipeline fee, native base units | Full-block Control RefTime / ProofSize | Prepass Control RefTime / ProofSize | Action receipts / effect Weight | +| --- | --- | --- | --- | --- | --- | +| System | 0 | 0 | `12,879,197,275 / 138,361` | `11,942,596,248 / 132,130` | `0 / 0` | +| User | 10,086,280 | 94,259,676 | `12,879,197,275 / 138,361` | `11,942,596,248 / 132,130` | `0 / 0` | + +The scheduler's `charge_pipeline_opening` calls `collect_pipeline_fee` and emits `PipelineFeeCharged` for Idle User service. Collection reads the Contract head/capacity and transfers native funds. The actual Control selector takes the same minimal completion branch for both witnesses. In `prepare_reachable_opening`, only `ReachableOpeningProfile::UserPaged` constructs a User Actor; the `CompleteMin` profile used by `scheduler_inner_opening_complete_min` constructs System. `fee_collection` is the separate Action collector and is zero for StopCycle; its coverage cannot be borrowed for Pipeline collection. + +### Source Boundary Qualification + +The existing complete User profile is not a drop-in completion refund. The following source-bound distinctions decide which numeric input is missing; no prior Wasm run is repeated to rediscover them. + +| Existing owner | Measured boundary | Attained outcome | Binding limitation | +| --- | --- | --- | --- | +| `scheduler_paged_execute_opening_max` | Full `execute_cycle`, including outer FIFO discovery/consumption | User maximum-length Opening, one zero-balance Opening capture, resolution skip, Running/Queue publication and funding-hold decrease | Neither a completion witness nor a disjoint inner atom | +| `scheduler_inner_opening_complete_min(t)` | `execute_reachable_step_inner` after carried state/current-Step load and primary consumption | System StopCycle, Idle/Unsignaled, no Run or Action receipt | Correct inner boundary but no User collection/hold domain | +| `control_user_opening_complete_batch32(n)` | Full scheduler loop plus explicit post-execution projection/persistence inside the measured block | User completion cohort with additional fixture writes | Not the production carried-source inner boundary | + +`StepControlPlacement::None` after completion does not imply absence of a physical owner: the persistent Actor retains an Unsignaled primary, so `ActorControlLocators::contains_key` still permits post-publication hold reconciliation. The source helper and terminal postconditions preserve that distinction. + +The missing complete User inner-completion coefficient is independently reusable and therefore belongs to [EXP-0068](./EXP-0068.md). Stop numeric owner investigation here. That Leaf now accepts partitioned single/positive-tail models for its attained reference fixture domain, with exact I/O agreement across 447 DB samples; the unsplit fit remains rejected. EXP-0067 must qualify live Contract-head/tail/funding geometry before selection: t and minimal current-Step captures alone do not prove membership. Neither diagnostic output is installed. + +### Admitted Funding-Policy Header Counterexample + +`full_executive_user_completion_header_domain_witness` constructs two independent reference-genesis native states. Both use public Mutable User creation at block 0, a prefunded prospective slot-0 sovereign, ordinary signed Manual readiness at block 1 and a full Executive block 2. The fixture never rewrites Actor authority or ages a control cell. The only Contract difference is `OwnerOnly` versus a full 16-address `SignedAllowlist` containing the owner; both retain one unpredicated StopCycle, no window/auto-close, no tail, no tracked funding and Persistent completion. + +The reconstructed context follows `execution_step_control_weight_context` and is independently checked against the stored first-Step maximum through the production provider. It is source-derived evidence, not instrumentation of live call arguments. Context order is cursor, steps-in-fragment, tail chunks, predicate evaluation units, Opening snapshots, Opening predicate results and funding snapshot upper bound. + +| Funding policy | Funding encoding bytes | Complete Contract-head bytes | Context | Stored Control RefTime / ProofSize | Final block Control RefTime / ProofSize | +| --- | --- | --- | --- | --- | --- | +| OwnerOnly | 1 | 157 | `[0,1,0,0,0,0,40]` | `2,810,763,536 / 33,863` | `12,879,197,275 / 138,361` | +| SignedAllowlist, 16 sources | 514 | 670 | `[0,1,0,0,0,0,40]` | `2,810,763,536 / 33,863` | `12,879,197,275 / 138,361` | + +Both commit one successful StopCycle and one completed Cycle with zero effect Weight. The test asserts equal reconstructed contexts and stored Step resources, greater head size and exact equality between head-size growth and funding-policy-size growth. It reports block Control without asserting that equal actual charges are a desired contract. The 513-byte growth comes entirely from the policy encoding; scalar context does not identify the attained small-head profile. + +This establishes a legal domain alias, not a proof that the existing conservative coefficient numerically underweights the larger head. Pipeline collection and User hold reconciliation read the head, but only a qualified complete owner can establish the wider bound. A sufficiently qualified envelope may still use scalar selection; the witness does not mandate extra public context fields, storage, a profile hash or a policy-based fee surcharge. + +| Artifact / validation | Identity or result | +| --- | --- | +| Producer | `template/runtime/src/tests/production_block_replay.rs`, SHA-256 `d3dbfbacc82370917f44f859457e0886c39fcabc024089a41e4ff0ad9613bf0b` | +| Context constructor | `template/pallets/actors/src/lib.rs`, SHA-256 `7432b3b5df56cf7507d0991270b62a5a35e4437a2548e81bd6bd66bb57d6c788` | +| Contract/head types | `template/pallets/actors/src/types/contract.rs`, SHA-256 `3827412b44320a393ac5d1f2c5b90fb9afd2312fc8888bea66dbf88fe376d6b7` | +| Output | `/tmp/deos-0725-user-header-domain.log`, SHA-256 `fe4edad2f39d7795181469dbb672f59b3c34353dff61afac22873863925d7697` | +| Reproduction | `RUST_TEST_NOCAPTURE=1 DEOS_VERBOSE=1 SKIP_WASM_BUILD=1 scripts/ci-local.sh --only tests --package deos-runtime --test-filter full_executive_user_completion_header_domain_witness` | +| Focused result | One native test passes; workspace Clippy and format pass; no new Wasm or production Weight generated | + +Accepted [EXP-0069](./EXP-0069.md) supplies the independent zero-tail maximum FundingSourcePolicy header envelope; EXP-0068 remains accepted for minimal-header positive tails. Live review found both extrema can occur together, so their maximum could not prove the combined branch. Accepted [EXP-0094](./EXP-0094.md) now supplies that cross-product: T16 preserves `11+t / 8` I/O, adds exact 513-byte proof over T across `t=1..=3`, and component-wise dominates it. No numeric header/tail measurement belongs here. + +## Derived Metrics + +Observed User-minus-System full-block and Prepass Control deltas are `0 / 0`, despite the additional positive Pipeline transfer and receipt. This is not subtraction of overlapping benchmark envelopes and does not establish a numeric shortage or a safe fee/Weight coefficient. + +## Statistical / Variance Notes + +Deterministic generated Weight, exact monetary assertions and component-wise selector tests decide this binding. Host wall-clock is not a decision metric. + +## Result + +The selector maps zero-tail completion to EXP-0069 H16 and positive tails to EXP-0094 T16; maximum admission selects the same inclusive owner. Focused containment and full-Executive monetary/header witnesses pass without adding a receipt, Action owner or public context dimension. EXP-0095 accepts generated Weight `9ce37ade391a29d4f60779f1887ad242dfeb4919d25cf0047ffba85d18f33901`, post-EXP-0096 Wasm `25b9695fd9e900f17ae1f3fb0b815ac1403830264d9c31f7cee54e29f434b700`, unchanged metadata/ABI identities, current-Wasm block/proof replay and the uninterrupted full Actors gate. + +## Interpretation + +Do not confuse fee collection correctness, actual Weight remaining below its reservation, and coverage of the work performed. They are separate claims. Earlier primitive/inclusive reference measurements remain valid within their original scopes; an internally consistent generated artifact bundle does not by itself prove that a live selector uses the qualified User owner. + +## Decision + +Accepted. Select the inclusive scalar binding: EXP-0069 H16 at zero tails and EXP-0094 T16 at positive tails, conservatively shared by System/User because Actor class is absent from the stable context. Admission and actual settlement use the same generated owners; no overlapping charge or new context field is added. EXP-0095 supplies exact binding convergence. C1 remains frozen. + +## Rejected Alternatives and Tradeoffs + +| Alternative | Decision | Reason | +| --- | --- | --- | +| Infer sound coverage from equal Control totals | Rejected | Equality does not show ownership of User-only work | +| Declare a numeric lower bound from this pair | Rejected | No complete same-domain timing/proof comparison was performed | +| Add Action collection for Pipeline work | Rejected | Separate call sites and receipts; StopCycle has no Action collection | +| Patch generated coefficients or reopen C1 | Rejected | Qualification belongs to the binding/owner boundary, not geometry or manual artifact edits | + +## Validity + +- `Establishes`: Complete non-duplicated production ownership for funded nonzero-Step User Opening completion across zero-tail and admitted positive-tail/header extrema in the reference binding. +- `Does not establish`: Generic-host coefficients, integrated G6 throughput, a release Geometry Freeze or a numerical shortage in the historical selector. +- `Known limitations`: Reference `MaxWhitelistSize = 16`, `t=1..=3`, nonzero-Step completion and the stable scalar context; other phases retain their own owners. +- `Invalidation triggers`: Collection body, selector, host, generated Weight or admission changes require scoped reevaluation. +- `Residual qualitative insight`: An accepted owner measurement is not proof that live settlement selects it. + +## Follow-Up / Next Gradient + +G4 binding accepted → retain EXP-0068/0069/0094 scoped inputs and EXP-0095 exact artifact cohort → refresh only invalidated or still-Measuring G6 owners. + +Stop this Leaf. Reopen only for a legal omitted/duplicate owner or unsupported selector domain. + +## Architecture Impact + +Runtime Weight selection and generated artifacts now implement the qualified inclusive owner; C1 and authored semantics did not change. Tests retain monetary/header witnesses and exact admission/actual containment. EXP-0095 owns the accepted artifact cohort. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: None. +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None; prior reference measurements retain their scoped validity. +- `Depends on`: [EXP-0025](./EXP-0025.md), [EXP-0031](./EXP-0031.md), [EXP-0068](./EXP-0068.md), [EXP-0069](./EXP-0069.md), [EXP-0094](./EXP-0094.md) +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None; current production acceptance is gated without rewriting historical artifact identity. +- `Uses evidence from`: [EXP-0031](./EXP-0031.md), [EXP-0062](./EXP-0062.md), [EXP-0066](./EXP-0066.md), [EXP-0095](./EXP-0095.md) exact binding convergence. +- `Transfers question to`: [EXP-0068](./EXP-0068.md) — complete consumed-source User minimal-capture completion owner; [EXP-0069](./EXP-0069.md) — zero-tail funding-policy width; [EXP-0094](./EXP-0094.md) — combined maximum header and positive tails. +- `Produces input for`: [EXP-0066](./EXP-0066.md) G6 refresh. +- `Reopen trigger`: A legal omitted/duplicate User Opening owner or unsupported selector domain; cost alone does not reopen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0068.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0068.md new file mode 100644 index 00000000..efe3280a --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0068.md @@ -0,0 +1,245 @@ +# EXP-0068 — User Opening Completion Inner Owner + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | None | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-08 | +| Affected domain | Minimal-capture nonzero-Step User Opening completion | +| Physical mechanism | Complete consumed-source User Opening inner atom | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0067](./EXP-0067.md) — native classification, not an accepted Weight input | +| Successor experiment | [EXP-0067](./EXP-0067.md) | + +## Decision Question + +Can one complete consumed-source User Opening completion atom qualify Pipeline collection, StopCycle completion and applicable hold reconciliation without recharging outer FIFO discovery/consumption? + +## Governing Specification + +- `Owners`: `template/pallets/actors/docs/specification.en.md` defines independent Pipeline/Action fees and exact state holds; `docs/actors-resource-policy.specification.en.md` defines sole Control ownership and component-wise settlement containment. +- `Invariants`: Preserve C1, public admission, Manual useful readiness, class-neutral FIFO, cutoff, Q1, custody and fee semantics. No production binding changes in this preparation. +- `Reopen trigger`: A semantic or custody failure must return to its owning implementation/specification; measurements cannot ratify it. + +## Context and Hypothesis + +EXP-0067's smallest source qualification found two incompatible measurement boundaries. `scheduler_paged_execute_opening_max` measures full FIFO service of a maximum-length User Contract, a resolution skip, Running publication and hold release. `scheduler_inner_opening_complete_min` measures consumed-source System StopCycle completion. The first neither attains completion nor excludes the already charged outer source prefix. Reusing its aggregate as a disjoint inner completion atom is unqualified. + +This Leaf owns the missing numeric inner-completion input, independently of EXP-0067's production selector decision. The existing User full-Opening and hold measurements remain scoped inputs, not a universal completion envelope. The hypothesis is a class-qualified version of the existing minimal completion fixture at the same consumed-source boundary. + +## Baseline and Candidates + +| ID | Owner | Boundary | +| --- | --- | --- | +| Baseline | `scheduler_inner_opening_complete_min(t)` | System, carried state/admission/current Step after source consumption | +| A | `scheduler_inner_opening_user_complete_min(t)` diagnostic, one fit over `0..=3` | User, identical inner execution boundary; fitted I/O fails at `t=1` | +| A-partitioned | `scheduler_inner_opening_user_complete_single()` plus `scheduler_inner_opening_user_complete_tail(t)` | Accepted for zero tails versus positive tails in the attained fixture domain; same inner body | + +No candidate Weight is installed. Production selection and complete admitted-domain composition remain in EXP-0067. + +## Controls + +- `Geometry`: Manual Persistent Contract, StopCycle at Step 0, no predicates or Opening captures; reachable tail LastFunding ingress; `t = 0..=ceil((MaxContractSteps-1)/4)`. +- `Changed variable`: Actor class and its required collection/hold work; public User admission and ledger state intentionally differ from System setup. +- `Boundary`: Real state/admission/current Step loading and source consumption happen before measurement. `execute_reachable_step_inner` owns plan construction, execution, publication and applicable hold reconciliation. +- `Exclusions`: Outer FIFO, temporal/Crossing rearm, authored close, predicated completion, failure/retry/progress, invoked Task effects and generic host dominance. + +## Benchmark Class, Workloads, and Criteria + +| Witness | Domain | Purpose | +| --- | --- | --- | +| Native benchmark construction | FRAME benchmark test plus every mock tail component | Check legal User admission and exact postconditions | +| Paired reference-Wasm profiles | Identical runtime, System/User minimal completion across `t=0..=3` | Exact postconditions pass; unsplit User I/O regression is rejected | + +Acceptance requires the measured User domain, positive Pipeline collection with exact payer/Sink deltas and one receipt, zero Action receipt/effect, one completed Cycle, Unsignaled successor with no Run, and exact retained hold components with the appropriate funding decrease. Reject a contaminated outer boundary, unadmitted profile or unjustified extrapolation. No host-latency statistics or production acceptance may be inferred from native construction. + +## Environment and Artifact Identity + +- `Baseline`: Git commit `a8d9ec1a07fb7fdc383148d1dbd37c83874d62be`, tree `a1cb8c8fa19dd790af5032038a352bdfd2dc673d`, with retained dirty diagnostics. EXP-0067 binds the inspected pre-change source hashes. +- `Toolchain`: Workspace Rust `1.94.1`; production Actors Weight remains `20d56df225eca3ff52857f2170eccf50587c5ca17f997d18d083e61e8afbeed9`. +- `Construction route`: `SKIP_WASM_BUILD=1 scripts/ci-local.sh --only tests --package pallet-deos-actors --all-features --test-filter scheduler_inner_opening_user_complete_` selects both current profiles; the historical unsplit test used the exact `_min` suffix. +- `Measurement route`: Run `scripts/benchmarks.sh --check`, then `scripts/benchmarks.sh --extra --extrinsic scheduler_inner_opening_user_complete_single --output /tmp/deos-0725-user-complete-single.rs --json-file /tmp/deos-0725-user-complete-single.json pallet_deos_actors`. Build fresh benchmark Wasm with `SKIP_WASM_BUILD` unset; run `_tail` next with `--skip-build` and corresponding `tail` artifact paths. Preserve default 50 steps / 20 repeats. The historical unsplit/System pair below retains its exact original commands. +- `Initial construction producer`: `benchmarking.rs` SHA-256 `6066a3fcdc87d439bfa60bef3332d1f88d8f09e6f70915e6c367b9c081be8338`; `/tmp/deos-0725-user-complete-native.log` SHA-256 `ac49a47cc6784fbaf3d247eb6f6d46b64ec96887f3ea45dd988c7fd42f04cf31`; compilation/storage-name preflight `/tmp/deos-0725-user-complete-benchmark-check.log` SHA-256 `b7b3036954d7172b75d0178d5bec711472fed79065d61c04e4f8d500a8d87225`. +- `Boundary regression producer`: `benchmarking.rs` SHA-256 `33f06e6edac330c0bf1beb61963bedab3c61186c52d05d054bc96826e41a054f`; only the native boundary-test matrix changed after the initial construction. `/tmp/deos-0725-user-complete-boundaries.log` SHA-256 `986f4a10185c54d90e6d2ec7e428011d9bc70ee24c5f541aeeec7652508f1478`, produced with the construction route's filter replaced by `reachable_opening_tail_profile_boundaries`. +- `Reference-Wasm cohort`: Fresh benchmark Wasm `a28bfabc8399e9a9c32edb0db5258cc9fc13f19c33158a0cfe10051334d6e02b`, at `template/target/benchmarks/release/wbuild/deos-runtime/deos_runtime.compact.compressed.wasm`. Producer remains `33f06e6edac330c0bf1beb61963bedab3c61186c52d05d054bc96826e41a054f`; scheduler remains `20a0188594958caf42b46ce5cb4177bb64193ca37575c979432f9c6455395f9b`; runtime selector remains `e2d1efb3276363f1e7e8352c55be2214880df12c41cc70f2751fc26d4fcfda82`; `scripts/benchmarks.sh` is `0b132dd78725394a3f3a5fb605b9429c991729453e7b4816bd340649d329a7d5`. +- `Commands`: User used `env -u SKIP_WASM_BUILD DEOS_VERBOSE=1 scripts/benchmarks.sh --extra --extrinsic scheduler_inner_opening_user_complete_min --output /tmp/deos-0725-user-opening-complete.rs --json-file /tmp/deos-0725-user-opening-complete.json pallet_deos_actors`; System then reused exactly that Wasm with `DEOS_VERBOSE=1 scripts/benchmarks.sh --skip-build --extrinsic scheduler_inner_opening_complete_min --output /tmp/deos-0725-system-opening-complete.rs --json-file /tmp/deos-0725-system-opening-complete.json pallet_deos_actors`. +- `Environment`: `frame-omni-bencher 0.22.0`, benchmark CLI 58.0.0, Ryzen 7 4800H, compiled Wasm, 4,096 heap pages, 1,024 MiB DB cache, map bound 1,000,000; configured 50 steps / 20 repeats with default minimum-duration behavior. User ran first after the 107-second build; System followed without another build. No overlapping build/test job was launched by this worker. +- `Production preservation`: Actors Weight remains `20d56df225eca3ff52857f2170eccf50587c5ca17f997d18d083e61e8afbeed9`; production Wasm remains `155749e22a6235570d2bb97d4a42b1327b86f4f5d82742df36494fca0fd88c1d`. Neither temporary fragment is installed; metadata and runtime source are unchanged. + +| Artifact | SHA-256 | +| --- | --- | +| `/tmp/deos-0725-user-opening-complete.rs` | `f22fdce822daf43a21f26f299ee3c0f137ea277b9956647c1f08eedfd7643d3e` | +| `/tmp/deos-0725-user-opening-complete.json` | `ffb78a637226d304ff5c3f5f07ec44fe20d020b72474574ca44288543f6dd4dd` | +| `/tmp/deos-0725-user-opening-wasm.log` | `5686767c154e15cdd53ea2545c4765c3757a1072495adf71757da02524b7d3dc` | +| `/tmp/deos-0725-system-opening-complete.rs` | `3077784930a522da1526e2be45a0134e61e2c67741419ac9ea6337045cdcc8a4` | +| `/tmp/deos-0725-system-opening-complete.json` | `373807666473264c53baf0293cf4bc1453feaf86b97280537d38182dee1ef041` | +| `/tmp/deos-0725-system-opening-wasm.log` | `fda4de975a4fb4f5f5e6e5b58fec4ec017c3533e08c7b331e1b296392695f321` | + +### Partitioned Cohort Identity + +The split definitions and their postconditions are bound to `benchmarking.rs` SHA-256 `ab074b3e0175a44f90c766ee555828a44a1afa519c5e39b99164fbe14889343b`. Fresh benchmark Wasm is `1a55dd53472cd1669cf7e1050ceebfcca0a735f3f2e75cdb4565d75dfce6f087`; single ran after its fresh build and tail reused that exact binary. Scheduler, runtime selector, benchmark script and production Weight/Wasm identities remain unchanged. The measured `execute_reachable_step_inner` call is unchanged; setup ranges and postconditions select the existing zero/nonzero hold branches. + +Both commands used `DEOS_VERBOSE=1` and the current measurement route above; single additionally used `env -u SKIP_WASM_BUILD`. Native preflight used `SKIP_WASM_BUILD=1 scripts/benchmarks.sh --check`, and both generated native tests ran with the current construction filter. The previous System pair is retained only as source-qualified historical evidence, not rerun or presented as a same-binary timing control for this cohort. + +| Artifact | SHA-256 | +| --- | --- | +| `/tmp/deos-0725-user-complete-single.rs` | `623ad4f42188f9ae79b23b8ad079d279816f3e218e652452e46f1ef2158c5513` | +| `/tmp/deos-0725-user-complete-single.json` | `a14306add7553480b548af2898e55bb0382194bdb50073f6ee87c838deb1b2ff` | +| `/tmp/deos-0725-user-single-wasm.log` | `b70648e10c32364214b78df346644a4dc9bd277f22d3128bf3600b0718be2da3` | +| `/tmp/deos-0725-user-complete-tail.rs` | `281f9a534931b1e4931040d6123e44ded792d99be9c5d6214bbec38cc74f409a` | +| `/tmp/deos-0725-user-complete-tail.json` | `2ee19d8cc5325706590653cf04ccf7a67e501824928036b06bf086f0a70acc82` | +| `/tmp/deos-0725-user-tail-wasm.log` | `777ff7e2ec94608c3632fd1f1787cb2a002963aea7db22aeb9e1fddeafea503e` | +| `/tmp/deos-0725-user-partition-check.log` | `b7b3036954d7172b75d0178d5bec711472fed79065d61c04e4f8d500a8d87225` | +| `/tmp/deos-0725-user-partition-native.log` | `0b518caf76054076abbd1692fd45db012903b7d6cfc6d8245b1bbf9dc5411232` | +| `/tmp/deos-0725-user-partition-model-check.log` | `d3bf97810b693c1c1aeffc0317a534cc4f2c3bb4206f4be5cff5a5a7f3305970` | + +## Proof Obligations + +- `Freeze`: Frozen O1 on 2026-09-08 after benchmark preparation and before the first native construction execution. Initial evidence is construction only, with reference-Wasm measurement still required. +- `Review triggers`: New independent complete User completion owner discovered during EXP-0067's selector qualification. The first reference fit fails inside the frozen tail domain; its zero/nonzero partition remains the same owner, domain and coverage criterion, not a new obligation. +- `Decomposition review`: This numeric owner can be qualified independently of the production selector and therefore receives its own Leaf. The bounded tail witnesses share one inner completion mechanism and criterion. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | One complete User minimal-capture Opening completion atom has exact non-duplicated inner ownership over the declared domain | Admitted one-Step User StopCycle after real source consumption | Native construction then reference-Wasm FRAME measurement | Self | Required | Qualified numeric input to EXP-0067 | Satisfied for the attained reference fixture domain: native/Wasm postconditions and all 447 partitioned DB samples agree | + +## Measurements + +The initial checks prove native construction. The later paired reference-Wasm cohort below measures the same body and exposes an unfit generated regression, not an accepted production bound. + +| Check | Outcome | Scope | +| --- | --- | --- | +| New FRAME benchmark test | 1 passed | Positive Pipeline fee, exact payer/Sink deltas, one receipt, zero StopCycle effect/invocation receipt and retained hold assertions | +| `reachable_opening_tail_profile_boundaries` | 1 passed | Every mock tail component across existing profiles plus UserCompleteMin; public admission, completion/Unsignaled state, native Pipeline accounting and TryRuntime consistency | +| Benchmark preflight | Passed | Native runtime-benchmarks compilation and generated storage names; no Wasm build | + +The new benchmark is `extra`, not a production Weight method. Setup consumes the actual frame primary; only `execute_reachable_step_inner` lies inside the measured block. The User hold stays identical at zero tails; with tail ingress, only its funding component decreases. The added native boundary matrix preserves the existing System and UserPaged profiles rather than replacing their evidence. + +### Paired Reference-Wasm Results + +All four component values pass the User ledger, receipt, completion and hold postconditions. Values below are normalized directly from raw JSON. Time is nanoseconds; P50/P95 use nearest-rank quantiles without filtering. Reads, repeat reads and writes are benchmark-tracker observations, not physical database operations. Repeat writes are zero throughout. + +| Class | t | Time / DB sample counts | Raw time min / P50 / P95 / max (ns) | Tracked reads / repeat reads / writes | Recorded proof bytes | +| --- | --- | --- | --- | --- | --- | +| User | 0 | 1,020 / 51 | 122,433 / 126,904 / 152,116 / 313,313 | 9 / 23 / 5 | 1,537 | +| User | 1 | 960 / 48 | 169,857 / 182,987 / 222,378 / 479,189 | 12 / 30 / 8 | 2,105 | +| User | 2 | 960 / 48 | 182,638 / 197,794 / 238,443 / 620,829 | 13 / 30 / 8 | 2,475 | +| User | 3 | 60 / 3 | 193,254 / 205,546 / 231,038 / 316,037 | 14 / 30 / 8 | 2,783 | +| System | 0 | 1,360 / 68 | 48,192 / 51,753 / 65,303 / 233,204 | 2 / 1 / 3 | 933 | +| System | 1 | 1,280 / 64 | 53,010 / 59,157 / 79,131 / 206,035 | 2 / 1 / 3 | 933 | +| System | 2 | 1,280 / 64 | 57,270 / 66,280 / 94,916 / 251,782 | 2 / 1 / 3 | 933 | +| System | 3 | 80 / 4 | 59,855 / 67,747 / 108,465 / 141,361 | 2 / 1 / 3 | 933 | + +The User raw storage trace contains Contract-head and native payer/Sink access absent from System completion, plus optional detector probes during hold reconciliation. Positive tails additionally reach tail reads and the owner-funded hold-release ledger. `ActorStateHolds`, `Balances::Holds` and the owner's native account participate in that release. The existing assertions establish an unchanged funding hold at `t=0` and a strict funding-only decrease at positive tails. + +| Generated unsplit model | RefTime before DB additions (ps) | Estimated ProofSize (bytes) | Generated reads / writes | Minimum execution time (ps) | +| --- | --- | --- | --- | --- | +| User | `135,438,443 + 33,835,223*t` | `7,991 + 2,831*t` | `9+2*t / 6+t` | 122,433,000 | +| System | `52,292,644 + 7,046,594*t` | `4,398` | `2 / 3` | 48,192,000 | + +**Reject the User unsplit model.** At `t=1`, the fitted terms price 11 reads and 7 writes against 12 reads and 8 writes in every DB sample. Passing benchmark postconditions and successful generation did not prove that the generated model covered the measured domain. System's tracked I/O is constant and its fitted I/O matches every point; this does not certify all possible System geometries. + +### Partitioned Reference-Wasm Results + +The two new native benchmark tests and both reference-Wasm profiles pass. Single retains exactly the original hold; tail requires a funding-only decrease. Both preserve Pipeline payer/Sink accounting and one receipt, zero Action effect/receipt, one completed Cycle, and the Unsignaled/no-Run successor. No prefix is moved inside the measured block. + +| Profile | t | Time / DB sample counts | Raw time min / P50 / P95 / max (ns) | Tracked reads / repeat reads / writes | Recorded proof bytes | +| --- | --- | --- | --- | --- | --- | +| Single | 0 | 6,940 / 347 | 120,199 / 125,786 / 146,809 / 523,678 | 9 / 23 / 5 | 1,537 | +| Tail | 1 | 1,000 / 50 | 169,927 / 179,984 / 211,482 / 739,701 | 12 / 30 / 8 | 2,105 | +| Tail | 2 | 960 / 48 | 181,032 / 196,676 / 237,324 / 491,551 | 13 / 30 / 8 | 2,475 | +| Tail | 3 | 40 / 2 | 191,298 / 200,797 / 246,823 / 257,160 | 14 / 30 / 8 | 2,783 | + +| Partitioned generated model | RefTime before DB additions (ps) | Estimated ProofSize (bytes) | Generated reads / writes | +| --- | --- | --- | --- | +| Single | `125,786,000` | `7,477` | `9 / 5` | +| Tail, `1..=3` | `166,032,549 + 15,467,609*t` | `10,164 + 2,834*t` | `11+t / 8` | + +Minimum execution times are `120,199,000` ps for single and `169,927,000` ps for tail. An independent raw-JSON check extracts the formulas from both generated fragments and proves exact read/write equality for all 447 DB samples. Formula equality means tracker coverage, not physical-database equivalence. The tail model has no zero-tail authority. + +## Derived Metrics + +With RocksDbWeight `25,000,000` RefTime/read and `100,000,000` RefTime/write, the unsplit User fit omits `125,000,000` RefTime of reported DB charges at `t=1`. This is a generated-model coverage failure, not an observed physical-database latency claim or a removable cost. + +| t | User tracked reads / writes | Unsplit fitted reads / writes | Unsplit generated total RefTime / ProofSize | DB coverage | +| --- | --- | --- | --- | --- | +| 0 | 9 / 5 | 9 / 6 | `960,438,443 / 7,991` | One excess fitted write | +| 1 | 12 / 8 | 11 / 7 | `1,144,273,666 / 10,822` | Missing one read and one write | +| 2 | 13 / 8 | 13 / 8 | `1,328,108,889 / 13,653` | Matches tracked counts | +| 3 | 14 / 8 | 15 / 9 | `1,511,944,112 / 16,484` | One excess fitted read and write | + +The observed User branch shape is `9/5` at zero tails and `(11+t)/8` at positive tails. It motivates separate generated profiles rather than a manually repaired coefficient. Before partitioned measurement, the candidate splits the benchmark definitions into a fixed single-Step case and `t=1..=3`, retaining the measured inner call and exact fee/hold postconditions. Acceptance requires the emitted I/O formulas to cover every raw DB sample at its component value; the prior paired System measurement remains a historical control, not a same-binary timing comparison with this new cohort. Estimated model proof is not recorded proof; neither raw proof bytes nor aggregate subtraction may discount production Weight. + +For the partitioned models, RocksDbWeight yields the following complete inner coefficients. These values do not include the already consumed outer FIFO source, other Trigger work, invoked Task effects or Action collection. + +| t | Selected model | Generated reads / writes | Total RefTime / estimated ProofSize | +| --- | --- | --- | --- | +| 0 | Single | 9 / 5 | `850,786,000 / 7,477` | +| 1 | Tail | 12 / 8 | `1,281,500,158 / 12,998` | +| 2 | Tail | 13 / 8 | `1,321,967,767 / 15,832` | +| 3 | Tail | 14 / 8 | `1,362,435,376 / 18,666` | + +## Statistical / Variance Notes + +Native construction is deterministic. Both reference runs use one unchanged compiled Wasm and identical sampling configuration; `--extra` only admits the User diagnostic. Minimum-duration repetition produced 3,000 time/150 DB samples for User and 4,000/200 for System, not identical sample counts. Integer component rounding heavily weights t=0..2; the tables expose the smaller t=3 sample set. Raw timing tails are retained rather than discarded. No order-reversed timing study, whole-block throughput inference or host-independent latency claim is made. The deciding I/O mismatch occurs identically in all 48 User DB samples at t=1 and does not depend on timing statistics. Benchmark genesis logs report unavailable LP-backed collator security; these profiles do not exercise that subsystem and no production-genesis liveness is inferred. + +The partitioned cohort retains the same sampling configuration and nearest-rank, unfiltered raw quantiles. It contains 6,940 time/347 DB single samples and 2,000 time/100 DB tail samples; only two DB observations land at t=3. This is standard FRAME model evidence for the attained branch, not a latency stress test or universal worst-case timing guarantee. + +## Result + +A legal User completion inner benchmark and the complete mock tail boundary regression pass. Package all-feature Clippy, workspace Clippy, canonical format, completion gate with `--skip-runtime-tests`, Experiment normalization (63 records) and `git diff --check` pass. The full runtime suite was not rerun. Fresh paired reference-Wasm execution now validates the stated postconditions. The unsplit generated model fails at t=1. Partitioning restores exact fitted/tracked I/O agreement across all four component values while retaining the same completion, fee and hold postconditions. The new coefficients remain temporary and uninstalled; no production correction exists. For the retained split source, both native benchmark tests, native benchmark preflight, package all-feature Clippy, workspace Clippy, format, model/table/hash checks, completion gate with `--skip-runtime-tests`, normalization and `git diff --check` pass. + +## Interpretation + +The inner boundary is attainable and includes the intended User collection/hold work without outer FIFO consumption. The initial defect was calibration: a single linear regression smeared a real zero-versus-nonzero funding-hold branch. Rejecting the fit does not reject the User inner owner or its legal fixture. The source-defined partition preserves the postconditions and removes the deterministic I/O underfit without manual coefficient changes or borrowed accounting. The original System comparison is a same-binary control for the rejected unsplit cohort, not authority to use System accounting for User. The partitioned candidate qualifies the attained User branch witnesses only. Minimal current-Step captures and t alone do not identify arbitrary Contract-head/tail geometry, so these coefficients are not a universal production envelope and cannot be selected merely from those scalar fields. EXP-0067 must prove its live selection domain or obtain a stronger owner; no relaxed geometry rule is accepted here. + +## Decision + +Accepted for the attained reference fixture domain only. Select A-partitioned as the complete inner branch evidence: single-Step `850,786,000 / 7,477`, and positive-tail `1,241,032,549 + 40,467,609*t` RefTime with `10,164 + 2,834*t` estimated ProofSize, including their generated DB charges. The unsplit fit remains rejected. This accepts neither arbitrary User geometry nor a production binding; EXP-0067 owns live-domain qualification, exact selection and containment before complete regeneration. Stop measuring the satisfied branch witnesses. + +## Rejected Alternatives and Tradeoffs + +| Alternative | Decision | Reason | +| --- | --- | --- | +| Use full UserPaged as a disjoint completion atom | Rejected | Different outcome and overlapping FIFO source boundary | +| Use `control_user_opening_complete_batch32` as inner Weight | Rejected | Full scheduler loop and explicit projection/persistence inside the measurement; not a carried-source inner atom | +| Add the isolated fee primitive to either aggregate | Rejected | Does not qualify hold work or eliminate overlap | +| Install the successful unsplit generation | Rejected | At t=1, fitted 11/7 underprices tracked 12/8 despite passing runtime postconditions | +| Patch coefficients manually or credit overpricing at other t | Rejected | Does not provide a source-owned generated bound for each admitted execution | + +## Validity + +- `Establishes`: Legal native/reference-Wasm construction, exact fee/hold postconditions, generated partitioned inner coefficients matching every tracked I/O point, and a concrete rejection of the unsplit User regression. +- `Does not establish`: All User Opening branches, generic host maxima, production selector correctness or release acceptance. +- `Known limitations`: The attained Mutable Manual/Persistent StopCycle fixtures have minimal capture and specific Contract-head/tail/funding geometry. Neither t alone nor a minimal current-Step context proves that arbitrary optional head fields, tail contents, triggers or other Tasks belong to this domain. Complete admitted-domain composition and production selection remain unproved. +- `Invalidation triggers`: Measured body, host, geometry, accounting or outcome changes require scoped refresh. +- `Residual qualitative insight`: Charge ownership follows the actual measurement boundary, not the benchmark name. + +## Follow-Up / Next Gradient + +User coverage → legal complete inner boundary → rejected unsplit fit → source-defined zero/positive-tail models → exact tracker/model comparison → scoped interpretation → EXP-0067 selection/containment. Preserve the rejected fit and both artifact cohorts; no further measurement is warranted once the declared fixture domain is qualified. + +## Architecture Impact + +Benchmark-only branch partition and scoped model acceptance. No runtime behavior, public API, C1 topology or production coefficient changes. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: None. +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: [EXP-0025](./EXP-0025.md), [EXP-0031](./EXP-0031.md), [EXP-0044](./EXP-0044.md) +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: [EXP-0067](./EXP-0067.md) +- `Transfers question to`: None. +- `Produces input for`: [EXP-0067](./EXP-0067.md) +- `Reopen trigger`: An omitted/duplicated operation or legal completion geometry outside this qualified inner owner; cost alone does not reopen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0069.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0069.md new file mode 100644 index 00000000..0afbc898 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0069.md @@ -0,0 +1,167 @@ +# EXP-0069 — User Completion Header Envelope + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | None | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-08 | +| Affected domain | Funded nonzero-Step User Opening completion header width | +| Physical mechanism | Consumed-source User completion inner owner over FundingSourcePolicy encoding | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0068](./EXP-0068.md) — accepted attained OwnerOnly fixture domain only | +| Successor experiment | [EXP-0067](./EXP-0067.md) — live selector qualification | + +## Decision Question + +Does one complete consumed-source User Opening completion owner cover the admitted FundingSourcePolicy header-width envelope, from `OwnerOnly` through maximum `SignedAllowlist`, without changing public context or duplicating outer FIFO, Pipeline, Action, or hold ownership? + +## Governing Specification + +- `Header and funding semantics`: `template/pallets/actors/docs/specification.en.md` §§3.1, 3.4, 3.5 and 7.3–7.7. +- `Resource ownership`: `docs/actors-resource-policy.specification.en.md` §§1, 4–6 requires complete component-wise admission and actual settlement with one owner per cost. +- `Invariant`: Preserve C1, class-neutral FIFO, immutable cutoff, Q1, causal floor, custody, independent Pipeline/Action fees and committed-Step durability. +- `Reopen trigger`: A semantic need to distinguish funding policies belongs to the specification; measured byte or Weight growth alone does not authorize a context/API/storage change. + +## Context and Hypothesis + +- `Current evidence`: EXP-0067's legal native witness gives `OwnerOnly` versus maximum 16-source `SignedAllowlist` header sizes 157/670 bytes while both produce context `[0,1,0,0,0,0,40]` and stored Control `2,810,763,536 / 33,863`. +- `Accepted input`: EXP-0068 accepts separate zero-tail and positive-tail User completion inner models only for its attained minimal-head fixtures; it does not qualify arbitrary Contract heads. +- `Hypothesis`: A maximum admitted FundingSourcePolicy benchmark at the same consumed-source completion boundary can provide a conservative inclusive header envelope while retaining the existing scalar selector. If it cannot, EXP-0067 must choose the smallest source-owned distinction proven necessary. +- `Materiality`: Coverage and containment, not a percentage speedup. One reproducible wider-header path outside the selected envelope falsifies qualification. + +## Baseline and Candidates + +| ID | Architecture | Source tree | Runtime/config identity | +| --- | --- | --- | --- | +| B0 | EXP-0068 `scheduler_inner_opening_user_complete_single`: OwnerOnly, zero tails, unchanged User hold | Checkpoint `a6d4bc926dae7f657902de5d0877a693aa05e891` | Reference `MaxWhitelistSize = 16`; production binding unchanged | +| A | Same inner call and postconditions with maximum admitted `SignedAllowlist` containing the owner | Benchmark-only profile implemented; reference-Wasm measurement pending | Same runtime and one freshly built benchmark Wasm for both profiles | + +No production Weight or selector change is part of candidate construction. Positive tails stay outside this Leaf: EXP-0068 already owns their independent hold-release branch, while this question changes funding-policy head width only. + +## Controls + +- `Controlled variables`: Same Mutable Manual Persistent one-Step `StopCycle` User Actor, zero predicates/captures/tails/tracked funding, public creation/admission, real Pipeline accounting, consumed FIFO source boundary, unchanged hold, runtime, toolchain, benchmark Wasm and repeats. +- `Changed variables`: Funding policy only: `OwnerOnly` versus maximum `SignedAllowlist` containing the owner. +- `Boundary`: State/admission/current-Step loading and source consumption remain outside `#[block]`; plan construction, Pipeline collection, completion publication and hold reconciliation remain inside the existing `execute_reachable_step_inner` boundary. +- `Confounders`: A maximum allowlist changes encoded Contract head and collection authorization data, but not tail count or scalar context. Generated I/O is tracker accounting, not physical database operations; estimated ProofSize is not recorded proof. + +## Benchmark Class, Workloads, and Criteria + +- `Benchmark class`: FRAME pallet benchmark on one fresh reference benchmark Wasm, plus native benchmark construction tests. + +| Workload | State/population geometry | Hot/cold assumptions | Purpose | +| --- | --- | --- | --- | +| H0 | Existing zero-tail OwnerOnly User completion | Fresh admitted Actor; setup outside measured block | Same-Wasm control | +| H16 | Same completion with maximum 16-source SignedAllowlist containing owner | Fresh admitted Actor; maximum policy encoding | Smallest wider-header falsifier | + +- `Metrics`: Raw time distribution, recorded proof bytes, generated RefTime/ProofSize, tracked reads/repeat reads/writes, exact header/funding bytes, Pipeline payer/Sink deltas, receipts, completion and hold state. +- `Statistical method`: Use canonical benchmark defaults (50 steps / 20 repeats and minimum-duration behavior), retain all raw samples, nearest-rank P50/P95 and no outlier filtering. Review generated formulas against every DB sample. +- `Acceptance criteria`: Both profiles pass identical semantic/accounting postconditions; one generated maximum-header owner covers every tracked DB sample and component-wise dominates the OwnerOnly profile where necessary, without outer-prefix overlap. EXP-0067 can map the live scalar context to that inclusive owner and prove maximum/actual containment. +- `Rejection criteria`: Invalid admission, absent owner in allowlist, different Task/tail/hold branch, fitted I/O below any sample, aggregate subtraction, cross-profile credit, or need for an unmeasured public selector distinction. + +## Environment and Artifact Identity + +| Artifact | Identity | +| --- | --- | +| Source checkpoint | `a6d4bc926dae7f657902de5d0877a693aa05e891` | +| Rust toolchain | Workspace `rustc 1.94.1 (e408947bf 2026-03-25)` | +| Existing benchmark source | Checkpoint SHA-256 `ab074b3e0175a44f90c766ee555828a44a1afa519c5e39b99164fbe14889343b`; prepared candidate `template/pallets/actors/src/benchmarking.rs` SHA-256 `20a95e0199bb20ceec3c518a0077dfc08e9d9536e82ca6716744bad8bb7f9c70` | +| Runtime selector | `template/runtime/src/configs/actor_config.rs`, SHA-256 `e2d1efb3276363f1e7e8352c55be2214880df12c41cc70f2751fc26d4fcfda82` | +| Scheduler | `template/pallets/actors/src/scheduler.rs`, SHA-256 `20a0188594958caf42b46ce5cb4177bb64193ca37575c979432f9c6455395f9b` | +| Production preservation | Actors Weight `20d56df225eca3ff52857f2170eccf50587c5ca17f997d18d083e61e8afbeed9`; production Wasm `155749e22a6235570d2bb97d4a42b1327b86f4f5d82742df36494fca0fd88c1d`; neither may change during diagnostic measurement | +| Benchmark runtime Wasm | Fresh shared H0/H16 Wasm SHA-256 `36a5cc44b0a3a323cba6135ac03e79e449236267bd675ccb2e01e18c2cc2e56e` | +| Database/backend | Compiled Wasm, 4,096 heap pages, 1,024 MiB DB cache, map bound 1,000,000; `frame-omni-bencher 0.22.0`, benchmark CLI 58.0.0 | +| Preparation checks | Passed `reachable_opening_tail_profile_boundaries`, generated `scheduler_inner_opening_user_complete_header_max`, `./scripts/benchmarks.sh --check`, format and package all-feature Clippy through canonical scripts | +| Measurement command | Fresh H0 then H16 with `./scripts/benchmarks.sh --extra --extrinsic --output /tmp/...rs --json-file /tmp/...json pallet_deos_actors`; H16 reuses the same Wasm with `--skip-build` | +| Raw evidence | H0 `.rs/.json/.log`: `9c82193fb04fa45410091e3a7abe3dab5513767f6050af51107c16fbfa28325a` / `7b835f600e66fcd353ec4d74f515638bb43fbc0b4e915116ca25c3805cd2c82e` / `4c7eb85d70bdb745967b60b9e3a9895c2b1af71000ae3ce9a4177fff4aaac0eb`; H16: `4a39a3c1b7802e515e503a856d5bff9bc828a4646dddc384e53b822591d351ff` / `a49c4d36e70d43f914bcd760d37b93c461714e2e81277bba97b0bcf3d28e960a` / `0c521f8ae5794be178f8d3126999f99eb5f67103e83347c7d80d02c507831a94` | + +## Proof Obligations + +- `Freeze`: Frozen on 2026-09-08 at Prepared against checkpoint `a6d4bc926dae7f657902de5d0877a693aa05e891`: one zero-tail User completion mechanism, one funding-policy changed variable, H0/H16, one same-Wasm comparison and the criteria above. +- `Review triggers`: New independently reusable header-width question transferred from EXP-0067 before numeric investigation. A different optional Contract-head field, positive-tail hold branch, Task, outcome or public selector is independent and must transfer. +- `Decomposition review`: H0/H16 share one inner owner, changed variable, evidence class and coverage criterion. EXP-0068 owns tail branching; EXP-0067 owns production selection. Do not add either question here. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | Complete User completion inner ownership covers admitted FundingSourcePolicy head width | Maximum 16-source SignedAllowlist completion outside the generated/tracked component-wise envelope | Native construction plus same-Wasm FRAME H0/H16 measurement | Self | Required | Qualified numeric/header input to EXP-0067 | Satisfied for reference `MaxWhitelistSize = 16`: H16 component-wise covers H0 and every tracked DB sample | + +## Measurements + +H0 and H16 ran sequentially on fresh benchmark Wasm `36a5cc44b0a3a323cba6135ac03e79e449236267bd675ccb2e01e18c2cc2e56e`; H16 reused it with `--skip-build`. Both generated benchmark tests pass the same Pipeline payer/Sink, receipt, completion, zero-effect and unchanged-hold postconditions. H16 uses public User admission, fills `MaxWhitelistSize = 16` and requires owner membership. + +| Profile | Time / DB samples | Raw time min / P50 / P95 / max (ns) | Tracked reads / repeat reads / writes | Recorded proof bytes | Generated pre-DB RefTime / estimated ProofSize | +| --- | --- | --- | --- | ---: | --- | +| H0 OwnerOnly | `5,620 / 281` | `123,062 / 153,933 / 270,430 / 621,668` | `9 / 23 / 5`; repeat writes 0 | 1,537 | `154,002,000 / 7,477` | +| H16 maximum SignedAllowlist | `5,040 / 252` | `128,510 / 168,949 / 327,910 / 649,674` | `9 / 23 / 5`; repeat writes 0 | 2,050 | `168,949,000 / 7,990` | + +With reference RocksDbWeight, complete generated totals are H0 `879,002,000 / 7,477` and H16 `893,949,000 / 7,990`. Every H0/H16 DB sample reports the generated `9 / 5`; no fitted component exists to underprice one endpoint. Recorded and estimated ProofSize each increase by exactly 513 bytes, matching EXP-0067's independently observed Contract-head/funding-policy growth. This equality is evidence for this encoding axis, not a general theorem that encoded bytes always equal trie-proof growth. + +## Derived Metrics + +Same-Wasm H16 minus H0 is `14,947,000` pre-DB RefTime and 513 bytes in both recorded and estimated ProofSize; generated DB charges are identical. H16 component-wise dominates H0, so selecting H16 as the inclusive zero-tail funding-policy owner requires no aggregate subtraction or cross-component credit. EXP-0068's historical OwnerOnly coefficient remains a different-Wasm comparison input, not the deciding delta. + +## Statistical / Variance Notes + +Both profiles use one Wasm and canonical 50-step/20-repeat configuration with default minimum-duration repetition. Nearest-rank P50/P95 values are unfiltered; no outlier removal. Different minimum-duration sample counts are explicit. Timing is host-scoped and secondary; exact tracked I/O agreement and component-wise H16 dominance decide coverage. + +## Result + +Measured and interpreted. H0/H16 execute successfully on one fresh benchmark Wasm. H16 retains identical generated I/O, adds exactly the independently observed 513 proof bytes and component-wise dominates H0. Production Weight, production Wasm and runtime binding remain unchanged. + +## Interpretation + +The wider legal header is not an unmodelled branch after measuring H16: its tracked I/O is identical and its generated ProofSize grows by the exact 513-byte policy width. H16 is therefore the sound reference-binding maximum for this isolated zero-tail FundingSourcePolicy axis. This does not qualify other optional Contract-head fields or positive-tail hold release. The existing scalar context may select the inclusive H16 owner; richer public context is not earned by this evidence. + +## Decision + +Accepted for the reference `MaxWhitelistSize = 16` FundingSourcePolicy axis. Select H16 `893,949,000 / 7,990` as the complete inclusive zero-tail User completion inner input for EXP-0067. Retain EXP-0068's accepted positive-tail owner independently. Do not install either coefficient until EXP-0067 proves non-overlapping live selection and component-wise maximum/actual containment. + +## Rejected Alternatives and Tradeoffs + +| Candidate | Decision | Evidence-backed reason | +| --- | --- | --- | +| Infer H16 from H0 plus 513 raw bytes | Rejected | Encoded growth does not determine trie proof, reads/writes or execution timing | +| Add funding-policy context now | Rejected | The alias establishes qualification pressure, not necessity of a public distinction | +| Reuse EXP-0068 H0 from another Wasm as direct timing control | Rejected | Artifact identity differs; retain it as scoped historical evidence only | +| Measure positive tails again | Excluded | Tail hold-release branching is already independently accepted by EXP-0068 | + +## Validity + +- `Establishes`: Same-Wasm H0/H16 reference evidence, exact fee/hold postconditions, exact tracked-I/O coverage and an inclusive zero-tail FundingSourcePolicy maximum for `MaxWhitelistSize = 16`. +- `Does not establish`: Production selector correctness, arbitrary optional Contract-head coverage, positive-tail or combined maximum-header/positive-tail coverage, generic-host maxima or release acceptance. +- `Known limitations`: Reference maximum allowlist only; generic hosts with different bounds require their own generated binding. Timing is host-scoped and raw logs remain temporary artifacts identified by digest. +- `Invalidation triggers`: Funding policy encoding/admission, User completion body, Pipeline collection, hold reconciliation, benchmark method, `MaxWhitelistSize` or toolchain change. +- `Residual qualitative insight`: Scalar selection can remain sound when its inclusive owner is proven; equal contexts alone do not prove that owner. + +## Follow-Up / Next Gradient + +G4 gap → legal 513-byte funding-policy alias → H0/H16 same-Wasm benchmark → generated/tracked containment → EXP-0067 live selector qualification → affected artifacts only if binding changes. + +O1 is decided. The simultaneously reachable maximum-header/positive-tail product transfers to [EXP-0094](./EXP-0094.md); selector/API composition remains with EXP-0067. + +## Architecture Impact + +- `Affected physical truth`: None while Prepared; benchmark-only candidate planned. +- `Architecture update`: Only after EXP-0067 selects and validates a production correction, if one is necessary. +- `Provenance citation`: `actors/EXP-0069` for FundingSourcePolicy header-envelope evidence. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: None. +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: [EXP-0068](./EXP-0068.md) only along the independent FundingSourcePolicy head-width axis; tail acceptance remains unchanged. +- `Contradicts`: None. +- `Depends on`: [EXP-0068](./EXP-0068.md) +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: [EXP-0067](./EXP-0067.md) legal header alias; [EXP-0068](./EXP-0068.md) attained inner boundary and branch models. +- `Transfers question to`: [EXP-0067](./EXP-0067.md) — inclusive live selection after O1 decision. +- `Produces input for`: [EXP-0067](./EXP-0067.md), [EXP-0094](./EXP-0094.md), [EXP-0066](./EXP-0066.md) +- `Reopen trigger`: A legal FundingSourcePolicy path outside H0/H16 or changed policy encoding/bounds; cost alone does not reopen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0070.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0070.md new file mode 100644 index 00000000..b2f9b5f8 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0070.md @@ -0,0 +1,318 @@ +# EXP-0070 — System Schedule Throughput and Censored Service + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | [EXP-0066](./EXP-0066.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-08 pre-fission reconstruction | +| Affected domain | Bounded one-Step System schedule service | +| Physical mechanism | Full-Executive Manual/Cadenced System service and readiness observation | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0025](./EXP-0025.md) — physical architecture only | +| Successor experiment | None | + +## Decision Question + +What successful Transfer service and right-censored waiting do the recorded W0–W2 System schedules exhibit over the declared finite horizon on the retained production binding? + +## Governing Specification + +- `Owners`: `template/pallets/actors/docs/specification.en.md` (readiness, scheduler and completion); `docs/actors-resource-policy.specification.en.md` (component-wise admission/equal thirds); `docs/actors-performance-assurance.specification.en.md` (targets and evidence classification). +- `Invariant`: FIFO, ticket chronology, Q1, causal floor and committed-Step durability remain unchanged. +- `Frozen input`: EXP-0025 physical architecture only; semantic A0 SHA-256 `8746e423168309975e509be91e4eb06fdf0eb0b43369d75f1ee62202a4341678` is not a Weight oracle. +- `Reopen trigger`: A demonstrated semantic conflict requires defect classification, not target-driven weakening of C1. + +## Context and Hypothesis + +- `Current architecture`: Frozen C1 on EXP-0065 production identities. +- `Hypothesis`: Corrected one-Step schedules provide valid successful-Cycle counts and separate completed first service from pending and never-materialized populations. +- `Materiality`: A failed/Step-proxy Cycle count or unreconciled readiness sample invalidates this service witness. The parent 10,000-Cycle/100-block and 100 Steps/distinct Actors target remains a separate acceptance decision. + +## Baseline and Candidates + +| ID | Architecture | Source tree | Runtime/config identity | +| --- | --- | --- | --- | +| B0 | Frozen C1 provenance | Commit `5b46b19004b62a723d6c45a610dfe1e5c79982b0`; template tree `98af72ffbb55fe8840b5bb44b5b01d65c8e775e1` | Reference 12/24/48, equal thirds | +| Witness | Same architecture, corrected System schedules | Initial, correction and observer source digests below | Unchanged EXP-0065 artifacts | + +## Controls + +- `Controlled variables`: Production topology/artifacts, deterministic fixture, full Executive/inherent/finalization and exact production replay. +- `Changed variables`: Manual/Cadenced schedule composition and ordinary signed-user demand; fixture correction and observer refresh are explicitly source-bound below. +- `Confounders`: Historical nonempty W0–W2 transferred one unit into an unfunded recipient and counted Step outcomes as Cycles; those rows remain invalid-fixture diagnostics only. + +## Benchmark Class, Workloads, and Criteria + +- `Benchmark class`: Full-runtime block profile on exact production Wasm; preparation is a bounded harness witness, not a 10,000-Actor sample. +- `Workload`: W0 empty/one-Step and 128-Actor preparation; W1 mixed schedules in both demand modes; W2 Manual-only/Cadenced-only, each retaining 15 System identities and adding 9,985 workload System Actors over blocks `2..=101`. +- `Controls`: Signer seed `[66; 32]`, Aura slots matching block, relay `max_pov_size` 5,000,000 bytes; monotonic nonce and valid remarks to User frontier under continuous demand. +- `Metrics`: Successful Transfers/Completed Cycles, distinct Actors, readiness/censored waits, Control/effect Weight, headroom, execution/verification proof sizes and valid demand. +- `Acceptance criteria`: Exact finite-horizon outcome/readiness reconciliation; parent target succeeds only on its required populations and both demand modes, not by this relocation. +- `Rejection criteria`: Invalid/unfinalized replay, failed Step counted as success, censoring called completed latency or finalized headroom called phase-local stop proof. + +## Environment and Artifact Identity + +| Artifact | Identity | +| --- | --- | +| Migration source | `/tmp/deos-exp0066-prefission.md`, SHA-256 `973bb0dd5f63430da3bff5aa1cc74a6df35b22b48510f47c44abf7dd8519f923`, 170875 bytes, mode `100644`; inclusive lines 102–254 | +| Migration baseline | Dirty worktree atop commit `a8d9ec1a07fb7fdc383148d1dbd37c83874d62be`, tree `a1cb8c8fa19dd790af5032038a352bdfd2dc673d`; not a clean experimental source identity | +| Migration manifest | [Extraction provenance](./experiments.md#current-decision-critical-path) | +| Accepted input source | EXP-0065 `c5345d6ba9574b9f16d0c62541400df027edd954039167f413f9b0aa9297c26d` | +| Initial Actor-only W1 source | `5045db07983dc501641816c5fa6221ff081b4d98d00d6d76261da74a37003950` | +| Initial continuous-user W1 source | `1cb1efc86d22dd869677fffeb63ccfeff75d5ae3a18a0582b0f57dbb34afd19b` | +| Initial W2 source | `a1390ec4dfd7406674c6bf98a2c52c81d0e1a90f14c12654bc973a7ea992c044` | +| Rust toolchain | `1.94.1` | +| Benchmark runtime Wasm | No new benchmark build; preserved production replay | +| Post-generation production runtime Wasm | `155749e22a6235570d2bb97d4a42b1327b86f4f5d82742df36494fca0fd88c1d` | +| Final release production runtime Wasm | Not established by relocation | +| Generated Actors Weight | `20d56df225eca3ff52857f2170eccf50587c5ca17f997d18d083e61e8afbeed9` | +| Metadata | `75868ea75fd85e7b79db12650a7407e89e249f5564b2157d55ac46cd26cb5d12` | +| Database/backend | Not separately recorded in source preamble; full-state and proof replay scope only | +| Benchmark command/config | `scripts/actors-assurance.sh --integrated-w0-w1`, `--integrated-w1-actor-only`, `--integrated-w1-continuous-user`, `--integrated-w2-schedules`; complete corrected commands/output digests below | +| Raw evidence | Exact pre-fission W0–W2/service-wait subsections below; temporary-log availability not newly certified | + +## Proof Obligations + +- `Freeze`: Frozen via explicit 2026-09-08 pre-fission reconstruction, not historical freeze. +- `Review triggers`: Compound parent fission; more than six extracted Measurement subsections reviewed as bounded schedule-service witnesses and their correction history. New owner, domain, selector or acceptance criterion requires another Leaf. +- `Decomposition review`: W0 preparation, corrected W1/W2 counts and censored waits share this single one-Step System schedule-service claim. The observer chronology correction is retained historical limitation, not a new FIFO proof owner; W7–W9 sequence evidence remains with its owning children of EXP-0066. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | Corrected W0–W2 supply valid bounded System schedule service and censored-wait observations | One failed Step counted as Cycle or terminal-readiness mismatch | Full-runtime production-Wasm profile | Self | Required | EXP-0066 O1 bounded schedule input | Open; measured target misses retained, no acceptance decision at relocation | + +## Measurements + +Exact extraction from pre-fission EXP-0066 W0/W1 preparation through W1/W2 censored service. “Above/below” refers to the captured parent ordering; references to phase cohorts and W7–W9 now resolve through the parent's child map. Historical negative/proxy measurements are preserved, not revived as successful evidence. + + +### W0/W1 Preparation Evidence + +Historical nonempty fixture observations in this subsection do not prove successful Transfers; the corrected preparation is recorded in the fixture-correction subsection below. Empty-W0 and complete-block replay/path validity retain their narrower meanings. + +The project-owned `--integrated-w0-w1` gate exact-resolves its native and ignored Wasm profiles, creates parachain/timestamp/Actor Prepass inherents through `InherentDataExt`, applies them in runtime order, finalizes through `Executive`, and independently replays each complete block under the accepted production Wasm with full-state and compact proof verification. W0 covers an empty finalized block and one one-Step Transfer. The W1 falsifier adds 128 deterministic Manual one-Step Transfer Actors and authors both Actor-only and continuously saturated valid signed-remark modes. Both W1 modes committed 15 Steps from 15 distinct Actors with Q1 intact; the demand mode admitted 77 valid calls and rejected the next only at the User ProofSize frontier. These are harness assertions and bounded preparation observations, not 10,000-Actor throughput samples. + +The first preparation run invalidly passed the canonical Wasm path directly to release Cargo, whose build replaced that path before the test. The corrected gate snapshots the accepted artifact and passes the immutable snapshot to replay while restoring the canonical path afterward. Exact runtime-version comparison also normalizes API declaration order only; API IDs and versions remain identical. An initial continuous-demand implementation read the signer nonce inside the recorded block path, adding fixture-only proof observation and causing native replay root disagreement; the corrected campaign carries the monotonic nonce explicitly outside that path. No decision measurement was retained from these invalid probes. + +### W1 Actor-Only 100-Block Measurement + +Historical invalid-fixture diagnostic: every Cycle quantity here was a Step-outcome proxy, not a verified successful Cycle. Use the successful-Transfer correction below for current Actor-only W1 throughput and resource evidence. + +The reference preset consumes 15 of 10,000 identity slots: three active and twelve dormant System Actors. W1 preserved all 15 rather than mutating production topology, added the remaining 9,985 mixed Manual/Cadenced workload Actors, and measured 100 linked finalized Actor-only blocks. This is a 10,000-Cycle campaign over 9,985 workload identities; the cadence permits repeated Cycles, while Q1 still limits each Actor to one committed Step per block. Every block retained pending FIFO work and independently replayed under the accepted production Wasm. + +| Metric | Result | +| --- | --- | +| Target / result | 10,000 Cycles and at least 100 Steps/distinct Actors in each of 100 eligible blocks / 132 Cycles from 132 distinct workload Actors; target false | +| Steps/block | Mean 1.32; median 0; p95 4; p99 4; min 0; max 4; histogram `0:67, 4:33` | +| Actor Control actual | RefTime min 82,152,332,222; median/p95/p99/max 105,360,374,054. ProofSize min 578,144; median/p95/p99 641,108; max 659,464 | +| Actor effect actual | RefTime min/median 0; p95/p99/max 8,967,000,000. ProofSize min/median 0; p95/p99/max 116,888 | +| Next complete attempt | Actor Control 12,677,308,536 RefTime / 127,063 ProofSize | +| Actor Control remainder | RefTime min/median 199,766,082,522; p95/p99/max 222,974,124,354. ProofSize min 16,686; median 35,042; p95/p99/max 98,006 | +| Stop attribution | The fixture Control maximum exceeded finalized ProofSize headroom in all 100 blocks, but fit RefTime; this is not phase-local stop attribution. No workload Actor repeated, so the observed maximum repeated-service gap is 0 | +| Execution StorageProof / CompactProof bytes | Storage min 78,591; median 92,634; p95 101,679; p99 103,635; max 104,849. Compact min 72,198; median 85,781; p95 94,567; p99 96,545; max 97,670 | +| Verification StorageProof / CompactProof bytes | Storage min 1,561,763; median 1,575,806; p95 1,584,851; p99 1,586,807; max 1,588,021. Compact min 1,555,308; median 1,568,891; p95 1,577,677; p99 1,579,655; max 1,580,780 | + +### W1 Continuous Valid-User 100-Block Measurement + +Historical invalid-fixture diagnostic: Cycle quantities here were Step-outcome proxies. Valid signed-user demand and block replay were observed; use the corrected successful-Transfer campaign below for current W1 throughput and schedule comparisons. + +The same retained topology and linked block sequence admitted 77 valid signed System remarks in every block before the next call exceeded the User base-turn ProofSize remainder. Nonces advanced monotonically across all 7,700 accepted calls, every block retained pending Actor work, and every finalized block replayed under the accepted production Wasm. + +| Metric | Result | +| --- | --- | +| Target / result | 10,000 Cycles and at least 100 Steps/distinct Actors in each of 100 eligible blocks / 132 Cycles from 132 distinct workload Actors; target false | +| Steps/block | Mean 1.32; median 0; p95 4; p99 4; min 0; max 4; histogram `0:67, 4:33` | +| Valid user demand | 7,700 accepted calls; exactly 77/block; next valid call blocked at the User ProofSize frontier in all 100 blocks | +| User dispatch actual | RefTime 38,334,246,720 and ProofSize 673,442 in every block | +| Actor Control actual | RefTime min 82,152,332,222; median/p95/p99/max 105,360,374,054. ProofSize min 578,144; median/p95/p99 641,108; max 659,464 | +| Actor effect actual | RefTime min/median 0; p95/p99/max 8,967,000,000. ProofSize min/median 0; p95/p99/max 116,888 | +| Next complete Actor attempt | Actor Control 12,677,308,536 RefTime / 127,063 ProofSize | +| Actor Control remainder | RefTime min/median 199,766,082,522; p95/p99/max 222,974,124,354. ProofSize min 16,686; median 35,042; p95/p99/max 98,006 | +| Stop attribution | The fixture Control maximum exceeded finalized ProofSize headroom in all 100 blocks, but fit RefTime; this is not phase-local stop attribution; no workload Actor repeated, so the observed maximum repeated-service gap is 0 | +| Execution StorageProof / CompactProof bytes | Storage min 81,128; median 94,909; p95 104,251; p99 106,205; max 107,312. Compact min 74,287; median 87,811; p95 96,614; p99 98,667; max 99,750 | +| Verification StorageProof / CompactProof bytes | Storage min 1,564,300; median 1,578,081; p95 1,587,423; p99 1,589,377; max 1,590,484. Compact min 1,557,397; median 1,570,921; p95 1,579,724; p99 1,581,777; max 1,582,860 | + +### W2 Manual-Only and Cadenced-Only 100-Block Measurements + +Historical invalid-fixture diagnostics: these outcome counts and their 13.59-times ratio are not verified successful-Transfer Cycle counts or a valid successful-Transfer schedule comparison. Use the corrected successful-Transfer schedules below; their counts and ratio differ. + +Both Actor-only cohorts preserved the same 15 reference System identities, added 9,985 one-Step Transfer Actors, retained pending FIFO work for 100 linked finalized blocks, and exact-Wasm replayed every block. Manual-only actors were latched at fixture creation; Cadenced-only actors used cadence one without manual latches. + +| Metric | Manual-only | Cadenced-only | +| --- | --- | --- | +| Target / result | 1,794 Cycles from 1,794 distinct workload Actors; target false | 132 Cycles from 132 distinct workload Actors; target false | +| Steps/block | Mean 17.94; median/p95/p99/max 18; min 15; histogram `15:2, 18:98` | Mean 1.32; median 0; p95/p99/max 4; min 0; histogram `0:67, 4:33` | +| Actor Control actual | RefTime min/median/p95 37,272,226,810; p99/max 40,512,139,684. ProofSize min 569,420; median/p95/p99/max 577,696 | RefTime min 82,152,332,222; median/p95/p99/max 105,360,374,054. ProofSize min 578,144; median/p95/p99/max 641,108 | +| Actor effect actual | RefTime min 33,626,250,000; median/p95/p99/max 40,351,500,000. ProofSize min 438,330; median/p95/p99/max 525,996 | RefTime min/median 0; p95/p99/max 8,967,000,000. ProofSize min/median 0; p95/p99/max 116,888 | +| Next complete attempt | Actor Control 12,677,308,536 RefTime / 127,063 ProofSize | Same complete one-Step maximum | +| Actor Control remainder | RefTime min 264,614,316,892; median/p95/p99/max 267,854,229,766. ProofSize min/median/p95 98,454; p99/max 106,730 | RefTime min/median 199,766,082,522; p95/p99/max 222,974,124,354. ProofSize min/median 35,042; p95/p99/max 98,006 | +| Stop attribution | The fixture Control maximum exceeded finalized ProofSize headroom in all 100 blocks, but fit RefTime | Same; 67 blocks committed no workload Step despite pending FIFO work; pending does not imply pre-cutoff eligibility | +| Execution StorageProof / CompactProof bytes | Storage min 86,730; median 102,428; p95 109,615; p99 111,931; max 115,726. Compact min 80,577; median 94,337; p95 101,487; p99 104,019; max 107,269 | Storage min 72,715; median 93,702; p95 103,561; p99 107,968; max 108,821. Compact min 67,554; median 87,232; p95 96,965; p99 100,638; max 101,263 | +| Verification StorageProof / CompactProof bytes | Storage min 1,569,902; median 1,585,600; p95 1,592,787; p99 1,595,103; max 1,598,898. Compact min 1,563,687; median 1,577,447; p95 1,584,597; p99 1,587,129; max 1,590,379 | Storage min 1,555,887; median 1,576,874; p95 1,586,733; p99 1,591,140; max 1,591,993. Compact min 1,550,664; median 1,570,342; p95 1,580,075; p99 1,583,748; max 1,584,373 | + +### W0–W2 Successful-Transfer Fixture Correction + +The shared fixture now transfers one existential deposit, reusing the same valid amount already used by the independent phase cohort rather than prefunding the recipient through another setup policy. W0/W1 preparation and every W1/W2 campaign block require zero failed/skipped Steps, one Transfer event per workload Step and one Completed Cycle event per Transfer. The campaign total now accumulates `completed_cycles` rather than `actor_steps`; `EXP_0066_{workload}_V4` reports successful Transfer and failed/skipped counts explicitly. The redundant amount-override helper was removed; phase-cohort inputs remain equivalent and its closed raw-node witness is not rerun for this representation change. + +Corrected evidence uses source `f1c5eb5332520a4bf891a06fc97099a2e679fc43d030528b6dd133a3e1d74ca9`, the unchanged accepted production Wasm/Weight/metadata identities, `DEOS_VERBOSE=1 scripts/actors-assurance.sh --integrated-w0-w1` and `DEOS_VERBOSE=1 scripts/actors-assurance.sh --integrated-w1-actor-only`. Raw output SHA-256 values are `b70d9eb6216caf56d358f01b2514276a08bd80b8b132ff113c6c18b55b25bc0d` and `88a2559ccf7251095bdbb8c1cd830f979b4452db8f74627c96b0390477787f79`. Preparation replayed four full blocks; Actor-only W1 preserved all 15 reference identities, added 9,985 mixed-schedule workload Actors and exact-Wasm replayed blocks `2..=101`. The W0–W2 workload identities are System Actors created by `prepare_actor_fixture`; signed user-demand calls are a separate population. These schedule witnesses do not directly exercise User Actor fee admission or discharge the specification's User/ObservationCrossing target populations. + +| Corrected preparation profile | Successful Transfers / Completed Cycles | Valid user calls | Actor Control RefTime / ProofSize | Actor effect RefTime / ProofSize | +| --- | ---: | ---: | ---: | ---: | +| W0 empty | 0 / 0 | 0 | 11,320,185,054 / 110,628 | 0 / 0 | +| W0 one-Step | 1 / 1 | 0 | 12,883,527,275 / 138,361 | 2,241,750,000 / 29,222 | +| 128-Actor W1 Actor-only | 16 / 16 | 0 | 36,837,034,590 / 584,568 | 35,868,000,000 / 467,552 | +| 128-Actor W1 continuous-user | 16 / 16 | 77 | 36,837,034,590 / 584,568 | 35,868,000,000 / 467,552 | + +| Corrected Actor-only W1 metric | Result | +| --- | --- | +| Success / target | 132 successful Transfers and 132 Completed Cycles from 132 distinct workload Actors; zero failed/skipped Steps. Target false | +| Steps/block | Mean 1.32; median/min 0; p95/p99/max 4; histogram `0:67, 4:33` | +| Actor Control actual | RefTime min 80,755,412,938; median/p95/p99/max 105,360,374,054. ProofSize min 574,788; median/p95/p99 641,108; max 659,464 | +| Actor effect actual | RefTime min/median 0; p95/p99/max 8,967,000,000. ProofSize min/median 0; p95/p99/max 116,888 | +| Next complete attempt / Control remainder | Next Control maximum `12,677,308,536 / 127,063`. Remainder RefTime min/median 199,766,082,522 and p95/p99/max 224,371,043,638; ProofSize min 16,686, median 35,042 and p95/p99/max 101,362 | +| Finalized headroom / service limitation | The fixture Control maximum exceeded ProofSize remainder in all 100 blocks, but fit RefTime; exact phase-local stop attribution is not established by this comparison. No workload Actor repeated, so the historical zero repeated-service-gap statistic is not a proof about unserved Actors or censored waits | +| Execution StorageProof / CompactProof bytes | Storage min 78,591; median 92,659; p95 101,679; p99 103,744; max 105,362. Compact min 72,198; median 85,965; p95 94,624; p99 96,545; max 98,023 | +| Verification StorageProof / CompactProof bytes | Storage min 1,561,763; median 1,575,831; p95 1,584,851; p99 1,586,916; max 1,588,534. Compact min 1,555,308; median 1,569,075; p95 1,577,734; p99 1,579,655; max 1,581,133 | + +The corrected campaign independently confirms the Actor-only target miss, but equal aggregate counts do not validate the old failed-Step fixture: Control minima, headroom and proof bytes changed. The corrected continuous-user and W2 measurements below replace their former Cycle-proxy interpretation rather than carrying ratios forward by assumption. + +The remaining corrected profiles use the same source `f1c5eb5332520a4bf891a06fc97099a2e679fc43d030528b6dd133a3e1d74ca9`, production identities and 100-block horizon. Commands were `DEOS_VERBOSE=1 scripts/actors-assurance.sh --integrated-w1-continuous-user` and `DEOS_VERBOSE=1 scripts/actors-assurance.sh --integrated-w2-schedules`; raw output SHA-256 values are `3ad2ffa9a0381275fbecb9a0af12be287f0b444b122107a7898f91d35f51c09e` and `511f24bd9f032ebef21261c532d9f2c23eb2e1d4e50390cc50a8d8c1a618de86`. Every block passed the successful-Transfer/Completed-Cycle guards and exact production-Wasm replay. + +| Corrected continuous-user W1 metric | Result | +| --- | --- | +| Success / target | 132 successful Transfers and Completed Cycles from 132 distinct workload Actors; zero failed/skipped Steps; target false | +| Actor service/accounting | Same Step histogram, Control/effect distributions and Control headroom as corrected Actor-only W1; Control ProofSize bound all 100 blocks | +| User demand | 77 valid signed calls per block, 7,700 total; next call inadmissible at User ProofSize in every block; User actual `38,334,246,720 / 673,442` per block | +| Execution StorageProof / CompactProof bytes | Storage min 81,128; median 95,110; p95 104,251; p99 106,205; max 107,825. Compact min 74,287; median 87,829; p95 96,748; p99 98,667; max 100,103 | +| Verification StorageProof / CompactProof bytes | Storage min 1,564,300; median 1,578,282; p95 1,587,423; p99 1,589,377; max 1,590,997. Compact min 1,557,397; median 1,570,939; p95 1,579,858; p99 1,581,777; max 1,583,213 | + +The W2 table uses ordered tuples `(min, median, p95, p99, max)` where five values are shown. Both cohorts preserve 15 reference identities, add 9,985 System workload Actors, have no ordinary user calls, and retain less finalized Control ProofSize than the fixture maximum in every block, while its RefTime fits. This is a capacity comparison, not an observation of each stopped live-head attempt. The next complete Control maximum is unchanged at `12,677,308,536 / 127,063`. + +| Corrected W2 metric | Manual-only | Cadenced-only | +| --- | --- | --- | +| Successful Transfers / Completed Cycles / distinct Actors | 1,894 / 1,894 / 1,894; zero failed/skipped Steps | 132 / 132 / 132; zero failed/skipped Steps | +| Steps/block and target | Mean 18.94; min 16; median/p95/p99/max 19; histogram `16:2, 19:98`; target false | Mean 1.32; min/median 0; p95/p99/max 4; histogram `0:67, 4:33`; target false | +| Control RefTime | `(32,549,432,253; 32,549,432,253; 32,549,432,253; 36,837,034,590; 36,837,034,590)` | Same as corrected Actor-only W1 | +| Control ProofSize | `(584,568; 590,327; 590,327; 590,327; 590,327)` | `(574,788; 641,108; 641,108; 641,108; 641,108)` | +| Effect RefTime | `(35,868,000,000; 42,593,250,000; 42,593,250,000; 42,593,250,000; 42,593,250,000)` | Same as corrected Actor-only W1 | +| Effect ProofSize | `(467,552; 555,218; 555,218; 555,218; 555,218)` | Same as corrected Actor-only W1 | +| Control remainder RefTime | `(268,289,421,986; 272,577,024,323; 272,577,024,323; 272,577,024,323; 272,577,024,323)` | Same as corrected Actor-only W1 | +| Control remainder ProofSize | `(85,823; 85,823; 85,823; 91,582; 91,582)` | `(35,042; 35,042; 101,362; 101,362; 101,362)` | +| Execution StorageProof bytes | `(89,829; 105,811; 113,882; 115,803; 117,373)` | `(72,715; 94,057; 104,074; 108,481; 109,334)` | +| Execution CompactProof bytes | `(83,461; 97,654; 105,265; 106,759; 108,696)` | `(67,554; 87,232; 97,318; 100,991; 101,616)` | +| Verification StorageProof bytes | `(1,573,001; 1,588,983; 1,597,054; 1,598,975; 1,600,545)` | `(1,555,887; 1,577,229; 1,587,246; 1,591,653; 1,592,506)` | +| Verification CompactProof bytes | `(1,566,571; 1,580,764; 1,588,375; 1,589,869; 1,591,806)` | `(1,550,664; 1,570,342; 1,580,428; 1,584,101; 1,584,726)` | + +The valid successful-Transfer schedule ratio is now `1,894 / 132 = 14.35` (rounded), replacing the invalid 13.59-times Cycle-proxy interpretation. Corrected Manual completes 100 more Cycles than its historical failed-Step counter. Both corrected W1 demand modes and corrected Cadenced-only W2 retain the same `0:67, 4:33` service histogram, while their proof geometry remains separately reported. No workload Actor repeats in any corrected W1/W2 campaign; zero observed repeated gaps therefore does not establish bounded first-service waiting for the thousands of unserved identities. All four W1/W2 repeated summaries and censored waits are quantified below; wider reproducibility, fairness coverage and User/reactive target scope remain open. + +### W1/W2 Service Waits and Right-Censored Horizons + +`OneStepServiceTrace` records each workload Actor's known materialized signal and first/last Step block. Initial Manual signals come from the shared fixture's successful block-1 setup; subsequent signals come from workload-filtered `TriggerOccurrenceProcessed` events. Every observed Step consumes known readiness after the next-block causal floor. Successful one-Step completion and absence of lifecycle interruptions constrain this model; it is not a generic Running/retry eligibility model. Terminal pending latches are checked against all 9,985 workload hot records outside measured execution. No consensus state, Weight or production artifact changes. + +The observer distinguishes creation-to-first-Step latency, materialized-signal-to-Step latency, unfinished signal waits and raw inter-Step gaps. A Cadenced signal timestamp is its actual materialization block, not its nominal due tick. Completed-sample percentiles exclude censored Actors and cannot describe the whole population. A pending signal first observed in the final block has zero subsequent eligible blocks, not a violated service opportunity. Inter-Step gaps would include idle time awaiting another signal; empty samples produce `null` statistics rather than zero. None of these observations alone proves starvation freedom or a steady-state service bound. + +Four `one_step_service_trace` tests cover completed/repeated service, never-signaled and pending Actors, missing-sample nulls, same-block rejection, and native Manual/Cadenced/mixed sequences with terminal-latch reconciliation. Production evidence is `DEOS_VERBOSE=1 scripts/actors-assurance.sh --integrated-w2-schedules` at source `5b63516e96a3959ae326ba5c0f06b18ebc6ed13787725080d7fde9826e9d7677`, using the unchanged production Weight/Wasm/metadata identities above. Raw output `/tmp/deos-0725-service-waits-w2.log` has SHA-256 `06cc548a2ede1ca4c744597c4eaae730ec9f047b393efef6bcbe58d0e462cff7`; both 100-block exact-Wasm campaigns and terminal checks passed. `EXP_0066_SERVICE_WAITS_V1` owns the wait summaries; W1/W2 `V5` removes the ambiguous scalar gap and explicitly labels the System workload class. + +Both W1 demand modes were then measured at source `64ca34a45568e3e3ed9b05c4939bbba7ea3fc038cc5e596e89945e688d74a01c`, with unchanged production Weight/Wasm/metadata. Commands were `DEOS_VERBOSE=1 scripts/actors-assurance.sh --integrated-w1-actor-only` and `DEOS_VERBOSE=1 scripts/actors-assurance.sh --integrated-w1-continuous-user`. Logs `/tmp/deos-0725-service-waits-w1-actor-only.log` and `/tmp/deos-0725-service-waits-w1-continuous-user.log` have SHA-256 `274b4409030b85e5b26aae23497404ab811c4e19c788896a4fce881f0605f7c6` and `4258173c235f673a2c7629ce8d0f3865e404c76344e94f828daba6d7387b71ee`. All 200 blocks replayed in exact production Wasm and terminal latch checks passed. Their wait summaries are identical; 7,700 successful ordinary user calls do not alter measured Actor waiting. + +| Population at end of block 101 | W2 Manual-only | W2 Cadenced-only | W1, either demand mode | +| --- | ---: | ---: | ---: | +| Workload Actors, created in fixture block 1 | 9,985 | 9,985 | 9,985 | +| First Step observed | 1,894 | 132 | 132 | +| First Step unobserved, right-censored at 100 blocks since creation | 8,091 | 9,853 | 9,853 | +| First signal materialized | 9,985 | 1,666 | 6,659 | +| First signal never materialized in the observed horizon | 0 | 8,319 | 3,326 | +| Pending materialized signals | 8,091 | 1,534 | 6,527 | +| Pending signals past their next-block causal floor | 8,091 | 1,516 | 6,509 | +| Pending signals materialized only in final block | 0 | 18 | 18 | +| Repeated-Step gap samples | 0; statistics `null` | 0; statistics `null` | 0; statistics `null` | + +The following block-count tuples are `(min, mean, median, p95, p99, max)`; means are rounded to six decimal places. The last row describes censoring ages, not completed latencies. + +| Wait measure | W2 Manual-only | W2 Cadenced-only | W1, either demand mode | +| --- | --- | --- | --- | +| Creation to first Step, observed Actors only | 1,894 samples; `(1; 50.640971; 51; 96; 100; 100)` | 132 samples; `(3; 51; 51; 96; 99; 99)` | 132 samples; `(3; 51; 51; 96; 99; 99)` | +| Materialized signal to successful Step | 1,894 samples; `(1; 50.640971; 51; 96; 100; 100)` | 132 samples; `(2; 46.515152; 46; 88; 91; 91)` | 132 samples; `(3; 51; 51; 96; 99; 99)` | +| Pending-signal censoring age at horizon | 8,091 samples; `(100; 100; 100; 100; 100; 100)` | 1,534 samples; `(0; 45.540417; 45; 87; 91; 92)` | 6,527 samples; `(0; 87.110005; 100; 100; 100; 100)` | + +All four W1/W2 runs exactly reproduce every retained corrected `V4` throughput, demand, Control/effect Weight, headroom and proof-summary field after removing the obsolete gap scalar and adding the explicit class label. This is deterministic summary reproducibility, not a claim of byte-identical logs or noisy wall-clock variance. Manual's 8,091 unserved Actors have already materialized signals throughout the 100-block horizon. Cadenced separates 8,319 not-yet-materialized Actors from 1,534 pending Actors instead of treating all 9,853 unserved identities as continuously runnable. Finite right-censoring establishes neither eventual starvation nor a finite maximum service wait. W1 likewise separates 3,326 never-materialized Actors from 6,527 pending Actors; its 100-block censoring is not a complete first traversal. Wider fairness coverage remains a separate obligation. + +The `authored_metrics` collector now retains Step-event chronology instead of sorting by Actor identity. `full_executive_step_observations_preserve_ticket_order_not_actor_id_order` creates two funded Manual Actors in ascending ID order, signals them in descending order and requires successful Steps `[16,15]`. The old collector failed with `[15,16]`; removing its sort makes this counterexample and the native `full_executive_` test family pass, including W7–W9 prefix checks. This is an observer defect, not a runtime FIFO defect. Historical sorted-prefix evidence establishes membership only. Fresh native chronology checks were followed by 22 exact-production-Wasm blocks across W7–W9 with the corrected collector; their owning subsections bind the refreshed sequence evidence. Block-level waits, counts, Weight and proof summaries are unaffected. + + + +### Current EXP-0095 W0–W2 Refresh + +The dedicated Actor-only W1 campaign now authors and proof-replays the same 100 linked blocks through production Wasm `25b9695fd9e900f17ae1f3fb0b815ac1403830264d9c31f7cee54e29f434b700` with Actors Weight `9ce37ade391a29d4f60779f1887ad242dfeb4919d25cf0047ffba85d18f33901`. Exact source identity is `f4007411eea86c1fbc36b670dd36f994b4eb0d6debcbd7ab1459abd63717c0e9`; log `/tmp/deos-0725-exp0070-current-w1-actor-only.log` has SHA-256 `a03b86f24a3e98aa26522a3ba10cee1855da8e48fb59cbc892c0636cd1032777`. + +Current W1 reproduces 132 successful Transfer Cycles, zero failed/skipped Steps, 132 distinct progressed Actors and the prior right-censored wait geometry; the 10,000-Cycle target remains unmet. Together with accepted EXP-0085, both Actor-only and continuous-signed W1 have exact current-Wasm state/proof linkage and equal 132-Step Actor outcomes. + +Current W0 preparation records zero Actor Steps at `11,491,516,748 / 110,628` Control and one successful Step at `13,619,062,122 / 141,953` Control plus `2,293,433,000 / 29,222` effect. Its exact log `/tmp/deos-0725-exp0070-current-w0.log` has SHA-256 `a17bf9c1752401b57aa4acfc7f8660f32687736328345901cab5e893d6f3b932`. The refreshed component ledger selects the inclusive zero-tail completion owner for the live conservative System/User context rather than retaining the obsolete minimal completion equation. + +Current W2 Manual commits 1,694 successful Transfers across 1,694 distinct Actors, with 14–17 Steps/block and no failures. Cadenced commits 132 across 132 Actors with the same zero-failure result. Both miss 10,000. Their exact linked native/Wasm log `/tmp/deos-0725-exp0070-current-w2.log` has SHA-256 `3a7ca65c9699a3aa97614caa20e990e12989e67b5f72e3afbd989754ad433c85`. Current Control/proof distributions belong to these logs and do not inherit invalidated EXP-0065 totals. + +## Derived Metrics + +The current schedule ratio is `1,694 / 132 = 12.83`. This compares successful System Transfer service under Manual versus Cadenced materialization only; it is not a generic scheduler speedup, User/reactive coverage or Weight-owner subtraction. + +## Statistical / Variance Notes + +Deterministic summary reproducibility is distinct from byte-identical logs and wall-clock variance. No repeated-Step samples exist; right-censored observations cannot establish starvation freedom or a finite maximum wait. + +## Result + +Current EXP-0095 W1 Actor-only and continuous-signed modes each report 132 successful Cycles. Current W2 reports 1,694 Manual and 132 Cadenced successful Cycles. Every campaign has zero failed/skipped Steps, exact Wasm linkage and misses 10,000. + +## Interpretation + +These are System schedule witnesses, not User Actor fee-admission or User/ObservationCrossing target coverage. Finalized ProofSize headroom comparisons do not establish phase-local stops. Historical “closed” statements refer to scoped evidence reuse/reporting, not acceptance. The sorted-chronology correction limits older sequence claims to membership; its wider sequence evidence remains outside this Leaf. + +## Decision + +Accepted for EXP-0066 O1. Select the current EXP-0095 W0–W2 cohort as truthful finite System schedule and censoring evidence. The target miss is an accepted result, not acceptance of release performance or permission to extrapolate finite waits into fairness. + +## Rejected Alternatives and Tradeoffs + +- `Invalid historical fixture`: Failed one-unit Transfers and Step-proxy counts are not successful-Cycle throughput; the 13.59 ratio is not retained as current success evidence. +- `Invalid preparation`: Cargo replacement of the artifact and recorded-path nonce reads invalidated probes; no decision measurements were retained from them. +- `Unsupported inference`: Zero repeated samples are not zero waiting; finalized capacity is not phase-local stop attribution; System schedules are not User/reactive acceptance. + +## Validity + +- `Establishes`: Current EXP-0095 W0–W2 successful System service, exact linked state/proof evidence, bounded censoring and target misses. +- `Does not establish`: General fairness, eventual starvation, finite maximum waiting, complete first traversal, phase-local stop attribution or release target acceptance. +- `Known limitations`: System Transfer workloads only; right censoring remains non-fairness evidence and Manual/Cadenced ratio is schedule-specific. +- `Invalidation triggers`: Changed relevant production/fixture/observer identities, scheduler policy, workload population or semantics. +- `Residual qualitative insight`: Readiness materialization, eligible waiting and completed service are distinct; invalid outcomes cannot be rescued by coincident aggregate counts. + +## Follow-Up / Next Gradient + +- `Next owner`: EXP-0066 consumes the bounded schedule result; independent phase-resource, User/reactive domain or optimization claims need their own Leaves. +- `Stop condition`: Stop at lossless relocation; do not extend this owner/domain or rerun unchanged evidence for migration alone. + +## Architecture Impact + +- `Affected physical truth`: No implementation or frozen-C1 change. +- `Architecture update`: None at relocation. +- `Provenance citation`: EXP-0070 for bounded W0–W2 System schedule evidence. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: [EXP-0066](./EXP-0066.md) O1. +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: None; parent decomposition is not a prerequisite. +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: [EXP-0065](./EXP-0065.md) production binding; [EXP-0025](./EXP-0025.md) physical baseline. +- `Transfers question to`: None. +- `Produces input for`: [EXP-0066](./EXP-0066.md) O1 System schedule/censored-service synthesis. +- `Reopen trigger`: Changed identity or concrete successful-outcome/readiness counterexample; a cost miss alone cannot reopen frozen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0071.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0071.md new file mode 100644 index 00000000..ccbdda95 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0071.md @@ -0,0 +1,226 @@ +# EXP-0071 — Contract Geometry Service + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | [EXP-0066](./EXP-0066.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-08 pre-fission reconstruction | +| Affected domain | Bounded mixed-Contract service | +| Physical mechanism | Full-Executive FIFO/Q1 service across Opening and Running geometry | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0025](./EXP-0025.md) — physical architecture only | +| Successor experiment | None | + +## Decision Question + +Does the recorded W3 mixed-length/Opening-predicate cohort complete with ordered Q1 service and explicitly separated first-service, Running-gap and completion observations on the retained production binding? + +## Governing Specification + +- `Owners`: `template/pallets/actors/docs/specification.en.md` (Opening, Running and scheduler); `docs/actors-resource-policy.specification.en.md` (component-wise admission); `docs/actors-performance-assurance.specification.en.md` (service evidence). +- `Invariant`: FIFO, ticket chronology, Q1, causal floor, committed-Step durability and equal-thirds policy remain unchanged. +- `Reopen trigger`: A demonstrated semantic violation requires specification/implementation classification, not a throughput-driven rewrite. + +## Context and Hypothesis + +- `Current architecture`: Frozen C1 from EXP-0025, using EXP-0065 production identities. +- `Hypothesis`: W3 supplies complete bounded service traces for its mixed Contract geometry without interpreting between-Cycle waiting as Running service. +- `Materiality`: One missing/duplicate/out-of-order cursor or unmatched completion invalidates this bounded witness; it is not the 10,000-Actor acceptance target. + +## Baseline and Candidates + +| ID | Architecture | Source tree | Runtime/config identity | +| --- | --- | --- | --- | +| B0 | Frozen C1 provenance | Commit `5b46b19004b62a723d6c45a610dfe1e5c79982b0`; template tree `98af72ffbb55fe8840b5bb44b5b01d65c8e775e1` | Reference 12/24/48, equal thirds | +| Witness | Same architecture, W3 matrix | Initial and timeline source digests below | Unchanged EXP-0065 binding | + +## Controls + +- `Controlled variables`: Production Wasm/Weight/metadata, deterministic reference preset, full Executive/inherent/finalization path and proof replay. +- `Changed variables`: Bounded witness cells vary 0/2/4 Opening predicates and 1/2/3 Steps; timeline observation adds no consensus state. +- `Confounders`: Initial ticket order follows complexity and population shrinks; no isolated predicate-cost comparison. + +## Benchmark Class, Workloads, and Criteria + +- `Benchmark class`: Full-runtime block profile on exact production Wasm. +- `Workload`: 126 Manual System Actors, 14 repetitions per cell, 252 successful existential-deposit Transfers in blocks `2..=28`; retain 15 reference System identities. +- `Controls`: Signer seed `[66; 32]`, Aura slots matching block, relay `max_pov_size` 5,000,000 bytes. +- `Metrics`: Phase counts, Step sequences, first/completion latency, Running gaps, Control/effect Weight and proof bytes. +- `Acceptance criteria`: Complete ordered cursor/completion traces for this finite cohort only. +- `Rejection criteria`: Failed replay, invalid Transfer, Q1/ordering violation or unsupported homogeneous-service extrapolation. + +## Environment and Artifact Identity + +| Artifact | Identity | +| --- | --- | +| Migration source | `/tmp/deos-exp0066-prefission.md`, SHA-256 `973bb0dd5f63430da3bff5aa1cc74a6df35b22b48510f47c44abf7dd8519f923`, 170875 bytes, mode `100644`; inclusive lines 255–324 | +| Migration baseline | Dirty worktree atop commit `a8d9ec1a07fb7fdc383148d1dbd37c83874d62be`, tree `a1cb8c8fa19dd790af5032038a352bdfd2dc673d`; not a clean experimental source identity | +| Migration manifest | [Extraction provenance](./experiments.md#current-decision-critical-path) | +| Accepted input source | EXP-0065 `c5345d6ba9574b9f16d0c62541400df027edd954039167f413f9b0aa9297c26d` | +| Initial W3 source | `7c3cea07ae4b516a26e58a920e864cf383b1c9302bbb873422a7e5e4eedead3b` | +| Rust toolchain | `1.94.1` | +| Benchmark runtime Wasm | Not a new benchmark build; preserved production artifact replay | +| Post-generation production runtime Wasm | `155749e22a6235570d2bb97d4a42b1327b86f4f5d82742df36494fca0fd88c1d` | +| Final release production runtime Wasm | Not established by relocation | +| Generated Actors Weight | `20d56df225eca3ff52857f2170eccf50587c5ca17f997d18d083e61e8afbeed9` | +| Metadata | `75868ea75fd85e7b79db12650a7407e89e249f5564b2157d55ac46cd26cb5d12` | +| Database/backend | Not separately recorded in source preamble; full-state and proof replay scope only | +| Benchmark command/config | `scripts/actors-assurance.sh --integrated-w3-opening-matrix`; verbose refresh command and output digest retained below | +| Raw evidence | Exact pre-fission W3 subsection below; temporary output availability is not newly certified | + +## Proof Obligations + +- `Freeze`: Frozen via explicit 2026-09-08 pre-fission reconstruction from the captured EXP-0066; not a claim of historical freeze. +- `Review triggers`: Compound parent fission; any new owner, domain, selector or acceptance criterion requires a different Leaf. +- `Decomposition review`: The nine cells are bounded witnesses of this one Contract-geometry service claim. No extension to sustained ingress or isolated predicate costing is admitted here. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | W3 yields complete Q1-ordered mixed-Contract service traces | One missing/duplicate cursor or mismatched completion in the 126-Actor trace | Full-runtime production-Wasm profile | Self | Required | EXP-0066 O2 bounded service input | Satisfied with bounded current-production scope | + +## Measurements + +Exact extraction from pre-fission EXP-0066, W3 Opening-Predicate and Mixed-Length Measurement. Historical local references inside the markers retain their original scope. + + +### W3 Opening-Predicate and Mixed-Length Measurement + +The W3 Actor-only witness used 126 Manual-only Actors: 14 repetitions of every Cartesian cell formed by 0/2/4 Opening predicates per Step and 1/2/3-Step Transfer Contracts. Predicates were distinct true `BalanceNotEquals` checks observed at cycle Opening; transfers used one existential deposit and all 252 attempts succeeded. One-Step executions are classified as Opening; for longer Contracts, index zero is Opening, interior indices are strict-middle, and the last nonzero index is final. Every linked block used full Executive/finalization and independently replayed under the accepted production Wasm. This bounded phase/geometry witness is not represented as a 10,000-Actor saturation sample. + +| Metric | Result | +| --- | --- | +| Completion | All 126 Cycles and 252 Steps completed in 27 linked blocks; 126 Opening, 42 strict-middle and 84 final Steps | +| Per predicate cohort | Each of 0/2/4 predicates completed 84 Steps: 42 Opening, 14 strict-middle and 28 final | +| Steps/block | Mean 9.3333; median 8; p95/p99/max 16; min 4; histogram `4:1, 5:4, 6:4, 7:3, 8:2, 10:2, 11:2, 12:2, 13:2, 14:2, 15:1, 16:2` | +| Service | All 126 distinct Actors progressed; maximum repeated-service gap was 15 blocks; Q1 held in every block | +| Actor Control actual | RefTime min 20,966,273,432; median 29,100,223,110; p95 38,376,378,158; max 40,174,154,134. ProofSize min 370,665; median 584,737; p95 611,350; max 611,732 | +| Actor effect actual | RefTime min 8,967,000,000; median 17,934,000,000; p95/max 35,868,000,000. ProofSize min 116,888; median 233,776; p95/max 467,552 | +| Execution StorageProof / CompactProof bytes | Storage min 29,164; median 43,071; p95 50,284; max 51,850. Compact min 25,183; median 38,008; p95 45,452; max 47,730 | +| Verification StorageProof / CompactProof bytes | Storage min 1,512,336; median 1,526,243; p95 1,533,456; max 1,535,022. Compact min 1,508,293; median 1,521,118; p95 1,528,562; max 1,530,840 | + +The service-timeline refresh used source `0bd6366a5fdd9f864675785fe6c627e3a04ac83b813c53a2fcef631886407a8e`, unchanged production Weight/Wasm/metadata and `DEOS_VERBOSE=1 scripts/actors-assurance.sh --integrated-w3-opening-matrix`. All 27 blocks replayed in exact production Wasm; the tabulated aggregate service, Control/effect and proof values above reproduce. Output `/tmp/deos-0725-w3-service-timelines.log` has SHA-256 `7319ee1f1f819e871c2fe5a3ed00c14057cf3bdea785228016ad19c789de7e12`. + +`EXP_0066_W3_SERVICE_V1` retains each System Actor's Step-block sequence and its independently observed Completed Cycle block. Guards require each cursor exactly once in order, one completion per Actor in its final Step block, successful Transfers and no pause, resume, close, suspension, continuation or failed Cycle. The three `completed_manual_service_timeline` regressions distinguish initial latency from Running gaps, reject duplicate-block progress and reject unmatched completion. All Manual signals were seeded at block 1; Contracts have no windows and their predicates are Opening-only. Thus intervening gaps belong to continuously Running service in this fixture, not waiting for a new Trigger or retry. + +Each cell contains 14 Actors. Tuples below are `(min, mean, median, p95, p99, max)` in blocks, with means rounded to six decimals. First-Step and completion distributions each have 14 samples. One-Step Actors never enter a between-Step Running interval, so they supply no gap samples rather than zero-valued gaps. + +| Predicates / Steps | First-Step latency | Completion latency | Running gaps: samples / Actors; distribution | +| --- | --- | --- | --- | +| 0/1 | `(1; 1; 1; 1; 1; 1)` | `(1; 1; 1; 1; 1; 1)` | 0 / 0; no samples | +| 0/2 | `(1; 1.857143; 2; 2; 2; 2)` | `(16; 16.785714; 17; 17; 17; 17)` | 14 / 14; `(14; 14.928571; 15; 15; 15; 15)` | +| 0/3 | `(2; 2.714286; 3; 3; 3; 3)` | `(24; 24.142857; 24; 25; 25; 25)` | 28 / 14; `(6; 10.714286; 7; 15; 15; 15)` | +| 2/1 | `(3; 4; 4; 5; 5; 5)` | `(3; 4; 4; 5; 5; 5)` | 0 / 0; no samples | +| 2/2 | `(5; 6; 6; 7; 7; 7)` | `(18; 18.785714; 19; 19; 19; 19)` | 14 / 14; `(12; 12.785714; 13; 14; 14; 14)` | +| 2/3 | `(7; 8.285714; 8; 9; 9; 9)` | `(25; 25.285714; 25; 26; 26; 26)` | 28 / 14; `(5; 8.5; 6; 13; 13; 13)` | +| 4/1 | `(10; 10; 10; 10; 10; 10)` | `(10; 10; 10; 10; 10; 10)` | 0 / 0; no samples | +| 4/2 | `(11; 12.071429; 12; 13; 13; 13)` | `(21; 21.428571; 21; 22; 22; 22)` | 14 / 14; `(9; 9.357143; 9; 10; 10; 10)` | +| 4/3 | `(14; 14.928571; 15; 16; 16; 16)` | `(26; 26.5; 26; 27; 27; 27)` | 28 / 14; `(3; 5.785714; 4; 8; 9; 9)` | + +The complete deterministic sequences are losslessly grouped below by contiguous Actor IDs with identical Step blocks. The last block is also that Actor's observed completion block; subtract block 1 to recover trigger-to-first-Step and trigger-to-completion latency. Consecutive differences recover every Running gap. All 126 Actors complete, so no first-service or completion observation is censored. + +| Predicates / Steps | Actor IDs | Step blocks | +| --- | --- | --- | +| 0/1 | 15–28 | `[2]` | +| 0/2 | 29–30 | `[2,17]` | +| 0/2 | 31 | `[3,17]` | +| 0/2 | 32–42 | `[3,18]` | +| 0/3 | 43–46 | `[3,18,25]` | +| 0/3 | 47–54 | `[4,19,25]` | +| 0/3 | 55–56 | `[4,19,26]` | +| 2/1 | 57–59 | `[4]` | +| 2/1 | 60–67 | `[5]` | +| 2/1 | 68–70 | `[6]` | +| 2/2 | 71–73 | `[6,19]` | +| 2/2 | 74 | `[6,20]` | +| 2/2 | 75–80 | `[7,20]` | +| 2/2 | 81–84 | `[8,20]` | +| 2/3 | 85–86 | `[8,21,26]` | +| 2/3 | 87–92 | `[9,21,26]` | +| 2/3 | 93–94 | `[10,21,26]` | +| 2/3 | 95–96 | `[10,21,27]` | +| 2/3 | 97–98 | `[10,22,27]` | +| 4/1 | 99–112 | `[11]` | +| 4/2 | 113–116 | `[12,22]` | +| 4/2 | 117–120 | `[13,22]` | +| 4/2 | 121 | `[13,23]` | +| 4/2 | 122–126 | `[14,23]` | +| 4/3 | 127–130 | `[15,23,27]` | +| 4/3 | 131 | `[15,24,27]` | +| 4/3 | 132–133 | `[16,24,27]` | +| 4/3 | 134–136 | `[16,24,28]` | +| 4/3 | 137–138 | `[17,24,28]` | +| 4/3 | 139–140 | `[17,25,28]` | + +The maximum Running gap remains 15 blocks across 126 intervals belonging to 84 multi-Step Actors. This matrix is not homogeneous: lengths and predicate counts differ, the active population shrinks as Actors finish, and committed capacity varies from 4 to 16 Steps/block. Therefore `ceil(126 / 9.3333)` is not a sustainable homogeneous service bound. Creation and initial ticket order are grouped by complexity; later first service in a higher-predicate cell does not isolate predicate cost or prove class priority. These complete traces close W3's bounded service-reporting gap, not sustained-ingress fairness or the 10,000-Actor target. + + + +### Current EXP-0095 W3 Refresh + +The exact current-production campaign repeats all nine `1/2/3`-Step by `0/2/4`-Opening-predicate cells with 14 Actors each. All 126 Cycles and 252 Steps complete over 27 linked blocks with exact Wasm state/proof replay, zero missing or duplicate cursor progress, 126 Opening Steps, 42 strict-middle Steps and 84 final Steps. Maximum observed Running service gap remains 15 blocks; p50/p95/p99 Steps per block are `8/15/16`. + +The campaign binds source identity `84d5fcdea43139cf64d647f395de3d207c11711968f206faf15361692c12f91f`, Weight `9ce37ade391a29d4f60779f1887ad242dfeb4919d25cf0047ffba85d18f33901`, Wasm `25b9695fd9e900f17ae1f3fb0b815ac1403830264d9c31f7cee54e29f434b700` and metadata `75868ea75fd85e7b79db12650a7407e89e249f5564b2157d55ac46cd26cb5d12`. Raw log `/tmp/deos-0725-exp0071-current-w3.log` has SHA-256 `50681e11a68e09744600590b296e0d4fb30e1b5034bd78014377bdd42a06d2a4`. + +## Derived Metrics + +The extracted distributions and grouped sequences retain the original derivations; no new delta or Pareto ranking is computed during relocation. + +## Statistical / Variance Notes + +Deterministic finite cohort with complete traces, not wall-clock sampling. Complexity-correlated ordering and shrinking population prevent a homogeneous service-bound inference. + +## Result + +The current EXP-0095 record reports 126 completed Cycles, 252 successful ordered Steps and a maximum 15-block Running gap across 27 exact-Wasm replayed blocks. + +## Interpretation + +The traces describe this bounded mixed-geometry cohort only. Historical “closes” language concerns service reporting, not experiment acceptance or the parent release gate. No sustainable homogeneous bound or isolated predicate-cost claim follows. + +## Decision + +Accepted for EXP-0066 O2 within the bounded W3 geometry. The current campaign preserves Q1 cursor order and completion for every cell. It does not convert the finite maximum gap into sustained fairness or isolate predicate cost. + +## Rejected Alternatives and Tradeoffs + +- `Unsupported projection`: `ceil(126 / 9.3333)` is not a sustainable homogeneous bound; geometry and initial ordering are confounded. + +## Validity + +- `Establishes`: Current-production bounded Contract-service and ordered cursor/completion observations for all declared W3 cells. +- `Does not establish`: Sustained fairness, User/reactive scale acceptance or isolated predicate pricing. +- `Known limitations`: Finite System Manual cohort; geometry ordering and shrinking population prevent a homogeneous service bound. +- `Invalidation triggers`: Relevant runtime, Weight, fixture, observer, geometry or semantic changes require scoped review. +- `Residual qualitative insight`: First service, Running gaps and completion have distinct meanings. + +## Follow-Up / Next Gradient + +- `Next owner`: EXP-0066 consumes this bounded evidence; independent costs or new reachable domains require their own Leaf. +- `Stop condition`: Stop at relocation; do not expand this claim or rerun closed evidence without changed inputs. + +## Architecture Impact + +- `Affected physical truth`: No implementation change. +- `Architecture update`: None from relocation. +- `Provenance citation`: EXP-0071 for bounded W3 Contract-service evidence. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: [EXP-0066](./EXP-0066.md) O2. +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: None; parent decomposition is not a prerequisite. +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: [EXP-0065](./EXP-0065.md) production binding; [EXP-0025](./EXP-0025.md) frozen physical baseline. +- `Transfers question to`: None. +- `Produces input for`: [EXP-0066](./EXP-0066.md) bounded Contract-geometry service synthesis. +- `Reopen trigger`: Changed measured identities or a concrete counterexample to this bounded trace claim; cost alone does not reopen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0072.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0072.md new file mode 100644 index 00000000..7f8af1d3 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0072.md @@ -0,0 +1,203 @@ +# EXP-0072 — Heterogeneous Task Service + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | [EXP-0066](./EXP-0066.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-08 pre-fission reconstruction | +| Affected domain | Bounded heterogeneous one-Step System service | +| Physical mechanism | Full-Executive service of interleaved Transfer, SwapOut and StopCycle | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0025](./EXP-0025.md) — physical architecture only | +| Successor experiment | None | + +## Decision Question + +What bounded class-specific successful service and censored waiting does the recorded W4 Task mix exhibit under its two demand modes on the retained production binding? + +## Governing Specification + +- `Owners`: `template/pallets/actors/docs/specification.en.md` (Task outcomes and scheduler); `docs/actors-resource-policy.specification.en.md` (separate Control/effect admission); `docs/actors-performance-assurance.specification.en.md` (service evidence). +- `Invariant`: FIFO, Q1, causal floor, typed outcomes, exact fees and equal-thirds policy remain unchanged. +- `Reopen trigger`: Demonstrated semantic conflict requires explicit classification, not benchmark-driven semantic changes. + +## Context and Hypothesis + +- `Current architecture`: Frozen C1 with EXP-0065 production bindings. +- `Hypothesis`: The W4 observer accounts for every Task-class outcome and separates unfinished readiness from completed latency in the finite horizon. +- `Materiality`: Any class/aggregate mismatch, invalid typed outcome or conflation of censoring with completion invalidates this witness. + +## Baseline and Candidates + +| ID | Architecture | Source tree | Runtime/config identity | +| --- | --- | --- | --- | +| B0 | Frozen C1 provenance | Commit `5b46b19004b62a723d6c45a610dfe1e5c79982b0`; template tree `98af72ffbb55fe8840b5bb44b5b01d65c8e775e1` | Reference 12/24/48, equal thirds | +| Witness | Same architecture, W4 heterogeneous fixture | Initial/refresh digests below | Same production binding | + +## Controls + +- `Controlled variables`: Reference topology, production Wasm/Weight/metadata, full Executive and finalized proof replay. +- `Changed variables`: Actor-only versus valid signed-user demand; Task classes are bounded projections of one interleaved fixture, not isolated candidate implementations. +- `Confounders`: Task and schedule assignments correlate; class means cannot isolate Task cost or priority. + +## Benchmark Class, Workloads, and Criteria + +- `Benchmark class`: Full-runtime block profile on exact production Wasm. +- `Workload`: 15 retained reference identities plus 9,985 System workload Actors (9,485 Transfer, 400 SwapOut, 100 StopCycle), blocks `2..=101` in each demand mode. +- `Controls`: Signer seed `[66; 32]`, monotonic nonce, Aura slots matching blocks, relay `max_pov_size` 5,000,000 bytes; signed remarks admitted to User frontier. +- `Metrics`: Typed successful outcomes, per-class waiting/censoring, Control/effect Weight, proof sizes and valid demand. +- `Acceptance criteria`: Reconciled class/aggregate service summaries with causal/Q1 and terminal-latch guards for this horizon. +- `Rejection criteria`: Invalid effects/replay, class-count mismatch, or unsaturated demand represented as saturated. + +## Environment and Artifact Identity + +| Artifact | Identity | +| --- | --- | +| Migration source | `/tmp/deos-exp0066-prefission.md`, SHA-256 `973bb0dd5f63430da3bff5aa1cc74a6df35b22b48510f47c44abf7dd8519f923`, 170875 bytes, mode `100644`; inclusive lines 325–370 | +| Migration baseline | Dirty worktree atop commit `a8d9ec1a07fb7fdc383148d1dbd37c83874d62be`, tree `a1cb8c8fa19dd790af5032038a352bdfd2dc673d`; not a clean experimental source identity | +| Migration manifest | [Extraction provenance](./experiments.md#current-decision-critical-path) | +| Accepted input source | EXP-0065 `c5345d6ba9574b9f16d0c62541400df027edd954039167f413f9b0aa9297c26d` | +| Initial W4 source | `fed9221045bc268bb44f2dccb9efdbd5692176862aa7db050bddcd91896a270b` | +| Rust toolchain | `1.94.1` | +| Benchmark runtime Wasm | No new benchmark build; preserved production replay | +| Post-generation production runtime Wasm | `155749e22a6235570d2bb97d4a42b1327b86f4f5d82742df36494fca0fd88c1d` | +| Final release production runtime Wasm | Not established by relocation | +| Generated Actors Weight | `20d56df225eca3ff52857f2170eccf50587c5ca17f997d18d083e61e8afbeed9` | +| Metadata | `75868ea75fd85e7b79db12650a7407e89e249f5564b2157d55ac46cd26cb5d12` | +| Database/backend | Not separately recorded in source preamble; full-state and proof replay scope only | +| Benchmark command/config | `scripts/actors-assurance.sh --integrated-w4-heterogeneous-effects`; verbose refresh/output identity below | +| Raw evidence | Exact pre-fission W4 subsection below; no new certification of temporary-log availability | + +## Proof Obligations + +- `Freeze`: Frozen via explicit 2026-09-08 pre-fission reconstruction, not historical freeze. +- `Review triggers`: Compound parent fission; new Task owner, domain, selector or acceptance claim requires another Leaf. +- `Decomposition review`: Task-class projections and two demand modes remain bounded witnesses of this one heterogeneous service-accounting claim, not Task-cost optimization claims. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | W4 provides reconciled bounded class-specific successful service and censored waits | One typed-outcome or class/aggregate mismatch | Full-runtime production-Wasm profile | Self | Required | EXP-0066 O3 heterogeneous service input | Satisfied with bounded current-production scope | + +## Measurements + +Exact extraction from pre-fission EXP-0066, W4 Heterogeneous-Effect Measurements. References to W1/W2 concern the original sibling evidence, now routed through the parent and EXP-0070. + + +### W4 Heterogeneous-Effect Measurements + +W4 preserved all 15 reference System identities and filled the remaining production identity capacity with 9,985 one-Step mixed Manual/Cadenced Actors. Because replacing production topology would invalidate the production-composition control, the nominal 9,500 Transfer slots became 9,485 workload Transfer Actors plus the 15 retained production identities; the declared 400 Router SwapOut and 100 StopCycle populations remained exact. Tasks were interleaved deterministically at 95/4/1 positions per hundred, with the final 85 positions contributing 80/4/1. The fixture created one canonical Native/test-asset Router pool before block authoring. Both demand modes then ran 100 linked finalized blocks and independently replayed every block under the accepted production Wasm. + +| Metric | Actor-only | Continuous valid-user demand | +| --- | --- | --- | +| Population / committed result | 9,485 Transfer, 400 SwapOut, 100 StopCycle / 123 Transfer, 6 SwapOut, 3 StopCycle; 132 successful Steps and Cycles from 132 distinct Actors | Same | +| Steps/block | Mean 1.32; median 0; p95/p99/max 4; min 0; target false | Same | +| Valid user demand | None | 7,700 accepted calls; exactly 77/block; next valid call blocked at the User ProofSize frontier in all 100 blocks | +| Actor Control actual | RefTime min 80,751,082,938; median/p95/max 105,360,374,054. ProofSize min 574,788; median/p95 641,108; max 659,464 | Same | +| Actor effect actual | RefTime min/median 0; p95 8,967,000,000; max 11,151,560,000. ProofSize min/median 0; p95/max 116,888 | Same | +| Maximum heterogeneous candidate | Control 12,677,308,536 RefTime / 127,063 ProofSize; effect 3,334,030,000 RefTime / 29,222 ProofSize | Same | +| Service | No workload Actor repeated; gap statistics have zero samples and remain unknown. Q1 held; class-specific pending and censored waits are retained below | Same | +| Execution StorageProof / CompactProof bytes | Storage min 78,234; median 92,935; p95 103,584; max 119,676. Compact min 72,138; median 86,012; p95 96,147; max 110,200 | Storage min 80,773; median 95,436; p95 105,481; max 122,106. Compact min 74,229; median 88,035; p95 97,693; max 112,247 | +| Verification StorageProof / CompactProof bytes | Storage min 1,561,406; median 1,576,107; p95 1,586,756; max 1,602,848. Compact min 1,555,248; median 1,569,122; p95 1,579,257; max 1,593,310 | Storage min 1,563,945; median 1,578,608; p95 1,588,653; max 1,605,278. Compact min 1,557,339; median 1,571,145; p95 1,580,803; max 1,595,357 | + +The first fixture attempt used a one-unit native Transfer into an unfunded test recipient. That probe produced a `Token(BelowMinimum)` fixture failure and was discarded. The retained fixture transfers one existential deposit, so all 264 W4 Steps across both campaigns succeeded. This correction changes only fixture validity, not runtime semantics or the accepted production artifact. + +The class-specific wait refresh used source `6b33b5f9ddb7a4b6048607a4d4496eb19b1ab1ac9c08a749e4aaf6d6021ec34e`, unchanged production Weight/Wasm/metadata and `DEOS_VERBOSE=1 scripts/actors-assurance.sh --integrated-w4-heterogeneous-effects`. Output `/tmp/deos-0725-w4-service-waits.log` has SHA-256 `5c41f86df1d8952e984b102a4dbe160bcdd45d22fa03a194c8375c9a18f0b7ec`. Both 100-block campaigns replayed in exact production Wasm, retaining 123 Transfer, six SwapOut and three StopCycle outcomes each and the tabled service, Control/effect and proof values above. Continuous user demand again admitted 7,700 valid calls. + +The one-Step observer now tags completed-signal waits and repeated gaps with their Actor identity, allowing exact Task-class projections without reindexing the original Manual/Cadenced setup. It requires successful typed outcomes, one Completed Cycle per Step from the same Actor identities, no lifecycle interruption, causal/Q1 ordering and terminal-latch agreement for all workload Actors. Transfer-only W1/W2 retain their additional Transfer-specific guard. Five `one_step_service_trace` tests cover whole/subset statistics, missing repeats and native single/heterogeneous fixtures; the heterogeneous fixture actually services all three Task families. W4 `V2` replaces the ambiguous scalar gap with explicit System class, while `EXP_0066_W4_SERVICE_V1` owns class-specific waits. W1/W2 observer inputs and summary semantics remain unchanged and their closed evidence is reused. + +Class-specific wait summaries are identical between both demand modes. All Actors were created in setup block 1 and observed through block 101. Every unserved first Step is right-censored at 100 blocks since creation; this is not a completed latency or a proof of indefinite starvation. + +| End-of-horizon population | Transfer | SwapOut | StopCycle | +| --- | ---: | ---: | ---: | +| Workload Actors | 9,485 | 400 | 100 | +| Successful Steps / Completed Cycles / distinct progressed Actors | 123 / 123 / 123 | 6 / 6 / 6 | 3 / 3 / 3 | +| First Step unobserved at horizon | 9,362 | 394 | 97 | +| First signal materialized | 6,291 | 268 | 100 | +| First signal never materialized | 3,194 | 132 | 0 | +| Pending materialized signals | 6,168 | 262 | 97 | +| Pending past next-block causal floor | 6,152 | 260 | 97 | +| Pending first materialized in final block | 16 | 2 | 0 | +| Repeated-Step gap samples | 0; statistics `null` | 0; statistics `null` | 0; statistics `null` | + +Tuples below are `(min, mean, median, p95, p99, max)` in blocks; means are rounded to six decimals. Observed first-Step and materialized-signal-to-Step distributions coincide here; each successful one-Step outcome also completes its Cycle in the same block. Pending rows describe censoring ages, not successful latencies. + +| Class | Observed first-Step / signal-to-Step latency | Pending-signal censoring age | +| --- | --- | --- | +| Transfer | 123 samples; `(3; 51.780488; 51; 96; 99; 99)` | 6,168 samples; `(0; 86.916505; 100; 100; 100; 100)` | +| SwapOut | 6 samples; `(3; 40; 39; 78; 78; 78)` | 262 samples; `(0; 86.893130; 100; 100; 100; 100)` | +| StopCycle | 3 samples; `(3; 41; 42; 78; 78; 78)` | 97 samples; `(100; 100; 100; 100; 100; 100)` | + +All 9,985 workload Actors belong to exactly one class; class populations, progressed counts and successful sample counts sum to the aggregate witness. The Task/schedule assignment is correlated: every StopCycle occupies even position four in its hundred and is Manual, while Transfer and SwapOut span both schedules. Their different observed means therefore do not isolate Task cost or prove class priority. StopCycle is a successful control outcome, not a Transfer; its Actor remains active after Cycle completion. This closes W4's bounded class-specific wait reporting, not complete heterogeneous traversal, sustained fairness or User/reactive target acceptance. + + + +### Current EXP-0095 W4 Refresh + +Both 100-block demand modes replay the same declared heterogeneous population through exact production Wasm. Each mode commits 132 successful System Steps/Cycles from 132 distinct Actors: 123 Transfer, six SwapOut and three StopCycle, with zero failed/skipped outcomes. Continuous demand additionally executes 7,700 valid signed calls. Both modes miss the target and preserve class-specific right censoring; no workload Actor repeats. + +The campaign binds source identity `ead5912e16e3dfda5d53582b98ed365bdcc00780c0d074c93b023cb8f4b4b55b`, Weight `9ce37ade391a29d4f60779f1887ad242dfeb4919d25cf0047ffba85d18f33901`, Wasm `25b9695fd9e900f17ae1f3fb0b815ac1403830264d9c31f7cee54e29f434b700` and metadata `75868ea75fd85e7b79db12650a7407e89e249f5564b2157d55ac46cd26cb5d12`. Raw log `/tmp/deos-0725-exp0072-current-w4.log` has SHA-256 `b9fdbe6821daa70278887df8d537749db22743f9c4e9631fdb9ebbe528beb452`. + +## Derived Metrics + +Original class distributions are retained without new derivation or Pareto ranking. No independent Task-cost delta is inferred. + +## Statistical / Variance Notes + +Deterministic 100-block horizons; completed samples exclude censored populations. No repeat-gap samples exist. Task/schedule correlation prevents causal class-cost attribution. + +## Result + +Each current mode reports 123 Transfer, six SwapOut and three StopCycle successful outcomes with exact state/proof replay; class-specific censored waits remain explicit. + +## Interpretation + +This is bounded heterogeneous System-service evidence, not complete traversal or a Task-cost comparison. Historical “closes” language means reporting closure only, not Accepted status or parent acceptance. + +## Decision + +Accepted for EXP-0066 O3 within bounded W4. The current class and aggregate outcomes reconcile under both demand modes. This does not establish complete traversal, isolated Task cost, class priority, User Actor coverage or liquidity-heavy coverage. + +## Rejected Alternatives and Tradeoffs + +- `Invalid fixture`: One-unit Transfer into an unfunded recipient was discarded after `Token(BelowMinimum)`. +- `Unsupported inference`: Different class means do not establish Task priority or cost because schedule assignment correlates. + +## Validity + +- `Establishes`: Current-production bounded class-specific service/censoring and typed outcomes for the declared W4 mix. +- `Does not establish`: Complete heterogeneous traversal, sustained fairness, User/reactive target acceptance or liquidity-heavy coverage beyond the declared mix. +- `Known limitations`: No repeated workload Actor; Task and schedule assignments remain correlated, and most Actors are right-censored. +- `Invalidation triggers`: Relevant artifact, observer, Task, workload or policy changes. +- `Residual qualitative insight`: Successful StopCycle is not a Transfer; missing repeated samples are unknown, not zero gaps. + +## Follow-Up / Next Gradient + +- `Next owner`: EXP-0066 synthesis; independent Task resource or new-domain questions require separate Leaves. +- `Stop condition`: Retain this finite witness without expansion or benchmark reruns from relocation alone. + +## Architecture Impact + +- `Affected physical truth`: None changed. +- `Architecture update`: None at relocation. +- `Provenance citation`: EXP-0072 for W4 heterogeneous service. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: [EXP-0066](./EXP-0066.md) O3. +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: None; parent decomposition is not a prerequisite. +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: [EXP-0095](./EXP-0095.md) production binding; [EXP-0025](./EXP-0025.md) physical baseline; [EXP-0070](./EXP-0070.md) shared one-Step observer context only. +- `Transfers question to`: None. +- `Produces input for`: [EXP-0066](./EXP-0066.md) O3 bounded heterogeneous service synthesis. +- `Reopen trigger`: Changed identity or concrete class-accounting counterexample; cost alone does not reopen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0073.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0073.md new file mode 100644 index 00000000..4cfe5f8f --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0073.md @@ -0,0 +1,204 @@ +# EXP-0073 — Lifecycle Outcome and Prefix Durability + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | [EXP-0066](./EXP-0066.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-08 pre-fission reconstruction | +| Affected domain | Bounded lifecycle service outcome classification | +| Physical mechanism | Full-Executive Actor lifecycle and committed-prefix continuation | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0025](./EXP-0025.md) — physical architecture only | +| Successor experiment | None | + +## Decision Question + +Does the finite W5 lifecycle witness distinguish committed progress from failed attempts and terminal control outcomes while preserving its committed prefix on the retained production binding? + +## Governing Specification + +- `Owners`: `template/pallets/actors/docs/specification.en.md` (lifecycle, retry, completion, custody and prefix durability); `docs/actors-resource-policy.specification.en.md` (complete Pipeline admission and Action accounting); `docs/actors-performance-assurance.specification.en.md` (evidence scope). +- `Invariant`: Q1, committed-Step durability, atomic effects, custody-neutral close and distinct Trigger/Pipeline/Action owners remain unchanged. +- `Reopen trigger`: A concrete semantic violation must be classified, never ratified as a faster lifecycle design. + +## Context and Hypothesis + +- `Current architecture`: Frozen C1 under the accepted EXP-0095 production binding. +- `Hypothesis`: W5's lifecycle timeline reconciles attempts, commits, completion and close without treating suspension or admission refusal as continuously runnable service. +- `Materiality`: One duplicate/compensated prefix Transfer, wrong terminal classification or timeline/state mismatch falsifies this bounded witness. + +## Baseline and Candidates + +| ID | Architecture | Source tree | Runtime/config identity | +| --- | --- | --- | --- | +| B0 | Frozen C1 provenance | Commit `5b46b19004b62a723d6c45a610dfe1e5c79982b0`; template tree `98af72ffbb55fe8840b5bb44b5b01d65c8e775e1` | Reference 12/24/48, equal thirds | +| Witness | Same architecture, W5 lifecycle matrix | Initial and refresh digests below | Same production bindings | + +## Controls + +- `Controlled variables`: Reference preset with 15 retained System identities, production artifacts, full Executive/finalization and exact replay. +- `Changed variables`: Five bounded lifecycle roles; observer reads cloned state outside measured execution. +- `Confounders`: Block authoring advances 6,000-ms slots, not individual 500-ms Actor ticks; finite matrix is not sustained retry demand. + +## Benchmark Class, Workloads, and Criteria + +- `Benchmark class`: Full-runtime block profile on exact production Wasm. +- `Workload`: Five Manual Actors for zero-Step, three-Step Running, retrying middle SwapOut after committed prefix, productive cleanup and underfunded User admission; blocks `2..=4`. +- `Controls`: Signer seed `[66; 32]`, Aura slots matching blocks, relay `max_pov_size` 5,000,000 bytes. +- `Metrics`: Ordered lifecycle events, Run state, attempted/committed Steps, custody balance, Control/effect Weight and proof sizes. +- `Acceptance criteria`: Reconciled finite lifecycle outcomes and exact retained-prefix balance. +- `Rejection criteria`: Duplicate/compensated prefix, mismatch between event and state classification, invalid replay or generic liveness extrapolation. + +## Environment and Artifact Identity + +| Artifact | Identity | +| --- | --- | +| Migration source | `/tmp/deos-exp0066-prefission.md`, SHA-256 `973bb0dd5f63430da3bff5aa1cc74a6df35b22b48510f47c44abf7dd8519f923`, 170875 bytes, mode `100644`; inclusive lines 371–415 | +| Migration baseline | Dirty worktree atop commit `a8d9ec1a07fb7fdc383148d1dbd37c83874d62be`, tree `a1cb8c8fa19dd790af5032038a352bdfd2dc673d`; not a clean experimental source identity | +| Migration manifest | [Extraction provenance](./experiments.md#current-decision-critical-path) | +| Accepted input source | EXP-0065 `c5345d6ba9574b9f16d0c62541400df027edd954039167f413f9b0aa9297c26d` | +| Initial W5 source | `3298afed17fd3156144875b518b29b1789e31dcee1bc07a187c7b191cd578cb5` | +| Rust toolchain | `1.94.1` | +| Benchmark runtime Wasm | No new benchmark build; preserved production replay | +| Post-generation production runtime Wasm | `155749e22a6235570d2bb97d4a42b1327b86f4f5d82742df36494fca0fd88c1d`; runtime code hash retained below | +| Final release production runtime Wasm | Not established by relocation | +| Generated Actors Weight | `20d56df225eca3ff52857f2170eccf50587c5ca17f997d18d083e61e8afbeed9` | +| Metadata | `75868ea75fd85e7b79db12650a7407e89e249f5564b2157d55ac46cd26cb5d12` | +| Database/backend | Not separately recorded in source preamble; full-state and proof replay scope only | +| Benchmark command/config | `scripts/actors-assurance.sh --integrated-w5-lifecycle-retry-cleanup`; verbose refresh and output digest below | +| Raw evidence | Exact pre-fission W5 subsection below; temporary-log availability not newly certified | + +## Proof Obligations + +- `Freeze`: Frozen via explicit 2026-09-08 pre-fission reconstruction, not historical freeze. +- `Review triggers`: Compound parent fission; new reachable domain, Weight owner, selector or criterion requires a separate Leaf. +- `Decomposition review`: Five terminal/continuation cases are bounded witnesses of one lifecycle-service classification and prefix-durability claim, not an invitation to grow a lifecycle audit here. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | W5 lifecycle service preserves and correctly classifies the committed prefix and terminal outcomes | One duplicate/compensated prefix or event/state mismatch | Full-runtime production-Wasm profile | Self | Required | EXP-0066 O4 bounded lifecycle input | Open; measured, no acceptance decision at relocation | + +## Measurements + +Exact extraction from pre-fission EXP-0066, W5 Lifecycle, Retry and Cleanup Measurement. Historical reporting-closure statements below do not confer Accepted status. + + +### W5 Lifecycle, Retry and Cleanup Measurement + +W5 added five Manual Actors to the retained production preset: a persistent zero-Step Actor, a persistent three-Transfer Running Actor, a three-Step Actor whose first Transfer commits before a middle Router SwapOut reaches its two-attempt retry limit, a one-Transfer `CloseAfterProductiveCycle` Actor, and a User Actor funded for its Manual Trigger but not complete Pipeline admission. The matrix converged in three linked full-Executive blocks, and all three independently replayed under production Wasm SHA-256 `155749e22a6235570d2bb97d4a42b1327b86f4f5d82742df36494fca0fd88c1d` (runtime code hash `bdaffa4716ada1f89ff8fd0b4150ed52b6c4f4d24794ce46488d68ff651ce259`). + +| Metric | Result | +| --- | --- | +| Progress | Seven Q1-preserving Step attempts across blocks `3, 2, 2`: five successful Transfers and two failed attempts at the same retrying middle Step | +| Phase / cycle outcomes | Three Opening, three middle and one final attempt; three completed Cycles and one failed Cycle summary; zero-Step completion emitted no Action attempt | +| Retry / prefix durability | One suspension and one continuation at cursor one; retry exhaustion closed the Actor after two temporary failures. Its sovereign retained exactly the initial balance less the one committed existential-deposit prefix, with no compensation or duplicate Transfer | +| Cleanup | Exactly one productive-completion close, one `CycleAdmissionInsufficient` minimal apoptosis close and one `RetryAttemptsExhausted` close; the persistent zero-Step and successful Running Actors remained active | +| Actor Control actual | RefTime min 8,191,370,387; median 15,677,082,549; max 42,152,907,366. ProofSize min 120,663; median 184,897; max 467,856 | +| Actor effect actual | RefTime min/median 5,575,780,000; max 6,725,250,000. ProofSize min/median 48,475; max 87,666 | +| Execution StorageProof / CompactProof bytes | Storage min 13,979; max 14,991. Compact min 9,669; max 10,467 | +| Verification StorageProof / CompactProof bytes | Storage min 1,497,151; max 1,498,163. Compact min 1,492,779; max 1,493,577 | + +The lifecycle-timeline refresh used source `cb2c2538a5b4a270e43f0d7e14d285e9ba9bf080dbc609e3f1e707fe90a236a4`, unchanged production bindings and `DEOS_VERBOSE=1 scripts/actors-assurance.sh --integrated-w5-lifecycle-retry-cleanup`. Output `/tmp/deos-0725-w5-lifecycle-timeline.log` has SHA-256 `cf0f4db75fd3b4ded84ac295a81d0e8e59e022ef57ea7c980fb438e6aadad230`. Native and exact-Wasm timelines are identical after excluding their execution-mode label; all three production blocks and the prior tabulated outcomes, Control/effect and proof values reproduce. + +`actor_lifecycle_observation` reads cloned setup/post-finalization storage outside measured execution. It retains selected readiness, lifecycle and Step events in their original `System::events` positions, with extrinsic phase and source-bound Debug details, plus each workload Actor's active/cycle/pending state and retained Run cursor, attempt, suspension and eligibility fields. Ordinal gaps represent intentionally omitted events, not reordered or lost selected events. The projection test preserves positions 0 and 2 while filtering another Actor. This is a bounded test diagnostic, not a new runtime receipt or stable public schema; `active: false` alone means absence of active state, while closure requires its event. + +Setup block 1 is a fixture snapshot, not a newly finalized production block. All five Actors have known Manual readiness there, with FIFO tickets 0–4 in the role order below. Its timestamp is zero. Finalized blocks 2/3/4 have timestamps 12,000/18,000/24,000 ms: authoring uses the runtime's 6,000-ms `SLOT_DURATION`, while Actor cadence ticks are 500 ms. Tick and block counts must therefore remain distinct; these slot-spaced blocks advance 12 Actor ticks each. + +| Role / Actor ID / class | Successful Step blocks | Failed attempt blocks | Terminal result / latency from block-1 signal | +| --- | --- | --- | --- | +| Zero-Step / 15 / System | None: no Action | None | Cycle completed at 2, latency 1; Actor remains active/Idle | +| Running / 16 / System | `[2,3,4]`, cursors `[0,1,2]` | None | First Step latency 1; completion latency 3; continuous Running gaps `[1,1]` | +| Retry prefix / 17 / System | `[2]`, cursor 0 | `[3,4]`, cursor 1 both times | Suspension at 3; continuation, failed Cycle and retry-exhaustion close at 4; first success latency 1, terminal-failure latency 3, no successful completion latency | +| Productive cleanup / 18 / System | `[2]`, cursor 0 | None | First Step and completion latency 1; productive-completion close at 2 | +| Admission apoptosis / 19 / User | None | None | Close at 2 with `CycleAdmissionInsufficient`, latency 1; no `CycleStarted` or Step, not a censored runnable Actor | + +All selected measured events are in mandatory Prepass `ApplyExtrinsic(2)`. Every Cycle uses nonce 1. Every successful Transfer is one native existential deposit to the fixture recipient. Failed attempts retain `retry_class: Temporary` and encoded `Other("")`; no unavailable error message is inferred. `Start`, `Done`, `FailSummary`, `Transfer`, `Fail`, `Suspend`, `Continue` and `Close` below name their corresponding typed Cycle/Step events; numbers before each event are original block-local ordinals. + +| Block | Complete selected event order | +| ---: | --- | +| 2 | `2 Start(15) → 3 Done(15) → 4 Start(16) → 8 Transfer(16,0) → 10 Start(17) → 12 Transfer(17,0) → 14 Start(18) → 16 Transfer(18,0) → 17 Done(18) → 18 Close(18,ProductiveCycleCompleted) → 21 Close(19,CycleAdmissionInsufficient)` | +| 3 | `3 Transfer(16,1) → 5 Fail(17,1) → 6 Suspend(17,1,Temporary)` | +| 4 | `3 Transfer(16,2) → 4 Done(16) → 6 Continue(17,1) → 7 Fail(17,1) → 8 FailSummary(17) → 9 Close(17,RetryAttemptsExhausted)` | + +| Post-block active Run state | Running Actor 16 | Retry Actor 17 | +| --- | --- | --- | +| 2 | Running; cursor 1; eligible 3; last attempt/commit 2/2; unsuccessful attempts 0; ticket 5 | Running; cursor 1; eligible 3; last attempt/commit 2/2; unsuccessful attempts 0; ticket 6 | +| 3 | Running; cursor 2; eligible 4; last attempt/commit 3/3; unsuccessful attempts 0; ticket 7 | Suspended/Temporary; cursor 1; eligible 4; last attempt/commit 3/2; unsuccessful attempts 1; ticket 8 | +| 4 | Active/Idle; no Run or ticket | Inactive after typed retry-exhaustion close | + +Actor 15 remains active/Idle without a Run or pending signal after block 2; Actors 18/19 are inactive after their observed block-2 closes. The retained Run distinguishes last attempted block from last committed Step block: failed retries do not extend the successful prefix. The existing terminal balance assertion proves Actor 17 retains its original balance less exactly one existential deposit. No generic inter-Step gap is assigned across suspension, zero-Step completion or admission refusal. This closes W5's bounded lifecycle timeline, not sustained retry fairness or a global liveness bound. + + + +### Current EXP-0095 Refresh + +The same bounded fixture was rerun against generated Actors Weight SHA-256 `9ce37ade391a29d4f60779f1887ad242dfeb4919d25cf0047ffba85d18f33901` and exact production Wasm SHA-256 `25b9695fd9e900f17ae1f3fb0b815ac1403830264d9c31f7cee54e29f434b700`. Command: `DEOS_VERBOSE=1 REQUIRE_WASM_IDENTITY=1 SKIP_WASM_BUILD=1 ./scripts/actors-assurance.sh --integrated-w5-lifecycle-retry-cleanup`. Raw log `/tmp/deos-0725-exp0073-current-w5.log` has SHA-256 `2743aae5245305c5a2aeb088a9759160d2144fd74cb20494abc29d6129c07010`. + +All lifecycle outcomes reproduce: seven ordered attempts across blocks `3/2/2`, five successful Transfers, two failed attempts at retry Actor 17 cursor one, three completed Cycles, one failed summary, one suspension and continuation, and exactly one productive close, admission-apoptosis close and retry-exhaustion close. The exact retained-prefix assertion remains one existential deposit with no duplicate or compensation. Native-authored bodies and pre-states replay under the exact Wasm with matching headers, post-state roots and proof-only verification for all three blocks. + +Current Control RefTime min/p50/max is `8,555,490,142 / 16,089,364,725 / 43,777,492,344`; ProofSize remains `120,663 / 184,897 / 471,448`. Current effect RefTime min/p50/max is `5,708,130,000 / 5,708,130,000 / 6,880,299,000`; ProofSize remains `48,475 / 48,475 / 87,666`. Execution StorageProof remains `13,979..14,991` bytes and CompactProof `9,669..10,467`; verification StorageProof is `1,497,224..1,498,236` and CompactProof `1,492,852..1,493,650` bytes. + +## Derived Metrics + +Original latency and phase counts are retained; no new derived pricing, throughput or Pareto classification. + +## Statistical / Variance Notes + +Three deterministic production blocks and matched native timeline, not a noisy latency distribution or sustained retry workload. Setup block 1 is not a finalized production sample. + +## Result + +The source reports five successful Transfers, two failed middle-Step attempts, three completed Cycles, one failed Cycle and exact retained-prefix custody after retry exhaustion. + +## Interpretation + +The finite timeline distinguishes committed service, unsuccessful attempts and control-only termination. It does not certify general retry fairness or funded User Opening coverage. Historical “closes” refers to this bounded reporting gap only. + +## Decision + +Accepted for its bounded current-production claim. The EXP-0095 refresh preserves every lifecycle, terminal and custody outcome while rebinding the quantitative Control/effect and proof values to the current generated Weight and exact Wasm. This does not accept sustained retry fairness, funded User Opening or complete lifecycle coverage. + +## Rejected Alternatives and Tradeoffs + +- `Unsupported accounting`: Zero-Step completion and admission apoptosis are not Action attempts; suspension is not a generic Running gap. +- `Unsupported custody inference`: Failed attempts do not extend or compensate the successful prefix. + +## Validity + +- `Establishes`: Retained W5 lifecycle observations and bounded prefix-durability witness. +- `Does not establish`: Global liveness, sustained retry fairness, complete lifecycle coverage or funded User Opening qualification. +- `Known limitations`: No new runtime validation or temporary-output availability proof. +- `Invalidation triggers`: Changed lifecycle/adapter semantics, bindings, fixture, observer or clock assumptions. +- `Residual qualitative insight`: Last attempt, last commit and terminal control outcomes require separate accounting. + +## Follow-Up / Next Gradient + +- `Next owner`: EXP-0066 consumes O4; independent lifecycle/resource domains must be transferred to their own Leaves, not added here. +- `Stop condition`: Stop after evidence relocation; no rerun without changed relevant inputs. + +## Architecture Impact + +- `Affected physical truth`: None changed. +- `Architecture update`: None from relocation. +- `Provenance citation`: EXP-0073 for the bounded W5 lifecycle/prefix witness. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: [EXP-0066](./EXP-0066.md) O4. +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: None; parent decomposition is not a prerequisite. +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: [EXP-0065](./EXP-0065.md) production binding; [EXP-0025](./EXP-0025.md) physical baseline. +- `Transfers question to`: None. +- `Produces input for`: [EXP-0066](./EXP-0066.md) O4 lifecycle outcome and prefix-durability synthesis. +- `Reopen trigger`: Changed relevant identity or concrete lifecycle/prefix counterexample; cost alone does not reopen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0074.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0074.md new file mode 100644 index 00000000..6336602c --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0074.md @@ -0,0 +1,217 @@ +# EXP-0074 — Arrival Eligibility and Generation + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | [EXP-0066](./EXP-0066.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-08 pre-fission reconstruction | +| Affected domain | Bounded mixed-arrival eligibility | +| Physical mechanism | Full-Executive readiness materialization with lifecycle and generation authority | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0025](./EXP-0025.md) — physical architecture only | +| Successor experiment | None | + +## Decision Question + +Does the recorded W6 mixed-arrival timeline attribute service to actual readiness and current lifecycle/generation authority, without treating pre-window, paused or pre-materialization time as runnable service delay? + +## Governing Specification + +- `Owners`: `template/pallets/actors/docs/specification.en.md` (Triggers, lifecycle, generation and scheduler); `docs/actors-resource-policy.specification.en.md` (admission); `docs/actors-performance-assurance.specification.en.md` (service classification). +- `Invariant`: FIFO, Q1, next-block causal floor, current generation authority and one pending latch remain unchanged. +- `Reopen trigger`: A demonstrated violation of readiness/authority semantics requires explicit defect classification rather than a measurement-driven semantic change. + +## Context and Hypothesis + +- `Current architecture`: Frozen C1 on the accepted EXP-0095 production binding. +- `Hypothesis`: W6's finite trace reconciles mixed block/tick readiness and lifecycle interruptions with current-generation service. +- `Materiality`: One pre-eligibility, paused, closed or stale-generation execution falsifies this bounded witness. + +## Baseline and Candidates + +| ID | Architecture | Source tree | Runtime/config identity | +| --- | --- | --- | --- | +| B0 | Frozen C1 provenance | Commit `5b46b19004b62a723d6c45a610dfe1e5c79982b0`; template tree `98af72ffbb55fe8840b5bb44b5b01d65c8e775e1` | Reference 12/24/48, equal thirds | +| Witness | Same architecture, W6 arrivals/lifecycle | Initial and refreshed source digests below | Same production binding | + +## Controls + +- `Controlled variables`: Retained reference preset, production identities, full Executive/finalization/replay and deterministic seed. +- `Changed variables`: Bounded arrival and lifecycle roles, not new scheduler mechanisms; timeline observation outside measured execution. +- `Confounders`: 6,000-ms authored slots advance twelve 500-ms ticks, coalescing overdue short temporal deadlines. + +## Benchmark Class, Workloads, and Criteria + +- `Benchmark class`: Full-runtime block profile on exact production Wasm. +- `Workload`: 13 User Actors plus retained 15 reference System identities, blocks `2..=17`; dense Manual, block-window, AtTime, seeded Cadenced and ObservationCrossing roles. +- `Controls`: Seed `0x0066c10c5eed`, signer seed `[66; 32]`, Aura slots matching block, relay `max_pov_size` 5,000,000 bytes; signed lifecycle and Oracle calls through Executive. +- `Metrics`: Readiness/service/event chronology, lifecycle and generation state, Control/effect Weight and proof bytes. +- `Acceptance criteria`: Reconciled finite eligibility and current-generation timeline; no stale or ineligible service. +- `Rejection criteria`: Invalid replay, stale authority execution or equating temporal ticks with produced blocks. + +## Environment and Artifact Identity + +| Artifact | Identity | +| --- | --- | +| Migration source | `/tmp/deos-exp0066-prefission.md`, SHA-256 `973bb0dd5f63430da3bff5aa1cc74a6df35b22b48510f47c44abf7dd8519f923`, 170875 bytes, mode `100644`; inclusive lines 416–473 | +| Migration baseline | Dirty worktree atop commit `a8d9ec1a07fb7fdc383148d1dbd37c83874d62be`, tree `a1cb8c8fa19dd790af5032038a352bdfd2dc673d`; not a clean experimental source identity | +| Migration manifest | [Extraction provenance](./experiments.md#current-decision-critical-path) | +| Accepted input source | EXP-0065 `c5345d6ba9574b9f16d0c62541400df027edd954039167f413f9b0aa9297c26d` | +| Initial W6 source | `c1aef5e3ea69cb8400e7091e3d8d408d2f31b13b133523beb0e57ccf90091750` | +| Rust toolchain | `1.94.1` | +| Benchmark runtime Wasm | No new benchmark build; preserved production replay | +| Post-generation production runtime Wasm | `155749e22a6235570d2bb97d4a42b1327b86f4f5d82742df36494fca0fd88c1d` | +| Final release production runtime Wasm | Not established by relocation | +| Generated Actors Weight | `20d56df225eca3ff52857f2170eccf50587c5ca17f997d18d083e61e8afbeed9` | +| Metadata | `75868ea75fd85e7b79db12650a7407e89e249f5564b2157d55ac46cd26cb5d12` | +| Database/backend | Not separately recorded in source preamble; full-state and proof replay scope only | +| Benchmark command/config | `scripts/actors-assurance.sh --integrated-w6-mixed-arrival-lifecycle`; refresh/output identity below | +| Raw evidence | Exact pre-fission W6 subsection below; no new temporary-log availability certification | + +## Proof Obligations + +- `Freeze`: Frozen via explicit 2026-09-08 pre-fission reconstruction, not historical freeze. +- `Review triggers`: Compound parent fission; a new domain, owner, selector or criterion requires another Leaf. +- `Decomposition review`: These roles are bounded witnesses of one arrival-eligibility/current-authority classification claim; no expansion into sustained fairness, per-tick throughput or resource-owner qualification. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | W6 service follows actual eligibility and current lifecycle/generation authority | One stale-generation, pre-window, paused or closed Actor Step | Full-runtime production-Wasm profile | Self | Required | EXP-0066 O5 bounded arrival input | Open; measured, no acceptance decision at relocation | + +## Measurements + +Exact extraction from pre-fission EXP-0066, W6 Mixed Arrival, Trigger Clock and Lifecycle Churn Measurement. Historical references to the W5 observer refer to the sibling evidence now in EXP-0073. + + +### W6 Mixed Arrival, Trigger Clock and Lifecycle Churn Measurement + +W6 used source `c1aef5e3ea69cb8400e7091e3d8d408d2f31b13b133523beb0e57ccf90091750` and deterministic seed `0x0066c10c5eed`. The 13-Actor User fixture combined three dense Manual arrivals, three sparse block-window arrivals, three AtTime deadlines, three seeded Cadenced arrivals with periods `[6,4,1]`, and one ObservationCrossing Actor. Sixteen linked blocks carried signed control and Oracle calls through normal Executive dispatch: a pending Manual Actor paused in block 2 and resumed in block 6; an Actor waiting for block-window start 9 closed in block 5; a Crossing transition was published before same-block Contract replacement rotated generation authority, followed by one crossing of the replacement threshold. Every block independently replayed under the accepted production Wasm. + +| Metric | Result | +| --- | --- | +| Arrival and progress | 12 of 13 Actors progressed for 33 successful Transfer Steps; the owner-closed future-window Actor never executed. All three dense Manual, all three AtTime, all three Cadenced and both retained block-window Actors progressed | +| Trigger observations | Three AtTime, 24 Cadenced and one post-replacement ObservationCrossing occurrence; the stale pre-replacement transition executed no Actor Step | +| Lifecycle / tombstone | Exactly one pause, one resume and one `OwnerInitiated` close. The resumed Manual Actor first progressed only after resume and retained its pending arrival; the closed block-clock bucket ended with zero live occupancy | +| Generation / faults | Crossing generation advanced from 1 to 2; only the replacement threshold fired. Crossing, wakeup and observation-fanout workers recorded no fault | +| Steps / Q1 | 33 Steps across 16 blocks, 12 distinct Actors; Q1 held and no Step failed | +| Actor Control actual | RefTime min 7,928,832,717; median 21,726,339,628; max 65,025,332,275. ProofSize min 134,975; median 265,508; max 573,155 | +| Actor effect actual | RefTime min 0; median 2,241,750,000; max 15,692,250,000. ProofSize min 0; median 29,222; max 204,554 | +| Execution StorageProof / CompactProof bytes | Storage min 14,920; max 25,429. Compact min 10,780; max 18,317 | +| Verification StorageProof / CompactProof bytes | Storage min 1,498,092; max 1,508,601. Compact min 1,493,890; max 1,501,427 | + +The arrival/eligibility refresh used source `67d8dda095230fefac401063c79d3cf5e74be5a39ad3ac53b02137baa5518167`, unchanged production bindings and `DEOS_VERBOSE=1 scripts/actors-assurance.sh --integrated-w6-mixed-arrival-lifecycle`. Output `/tmp/deos-0725-w6-arrival-timeline.log` has SHA-256 `a39321d0b3997d614c9148099c378abd620c0e3f7da921bc002096ab097ad9d0`. All 16 production-Wasm blocks passed; native and production timelines are identical apart from the execution-mode label, and the tabled outcomes, Control/effect and proof bounds reproduce. `EXP_0066_W6_TIMELINE_V1` extends the W5 observer with temporal anchors, AtTime consumption and Crossing generation. W6 `V2` uses `atTimeDeadlineActors` and `randomizedCadencePeriodsTicks`, avoiding the unsupported sparse-block interpretation. The command's help was subsequently clarified without changing execution or fixtures. + +All 13 workload Actors are User Actors. In setup block 1, timestamp zero, Actors 15–20 have paid Manual readiness. Actors 18/19/20 wait for block windows `[3,120]`, `[6,120]`, `[9,120]`; Actors 21–26 have temporal anchor tick zero. AtTime initial deadlines are ticks 1/4/7 for Actors 21/22/23; Cadenced initial deadlines and periods are 6/4/1 for Actors 24/25/26. Actor 27 starts at Crossing generation 1 with no pending signal. + +| Actor / role | Materialized readiness blocks | Successful Step and completion blocks | Delay classification | +| --- | --- | --- | --- | +| 15 / dense Manual | Setup 1 | `[2]` | Signal-to-completion 1 block | +| 16 / dense Manual | Setup 1 | `[3]` | Signal-to-completion 2 blocks | +| 17 / paused Manual | Setup 1 | `[7]` | Signal-to-completion 6 blocks includes intentional pause; resume at 6, service at 7 | +| 18 / windowed Manual | Setup 1 | `[4]` | Raw signal delay 3; window opens at 3, then service one block later | +| 19 / windowed Manual | Setup 1 | `[7]` | Raw signal delay 6; window opens at 6, then service one block later | +| 20 / owner-closed window | Setup 1 | None | Closed at 5 before window start 9; not a censored eligible Actor | +| 21–23 / AtTime | `[2]` each | `[3]` each | Materialization-to-completion 1 block; distinct deadlines collapse into one arrival block | +| 24–26 / Cadenced | `[2,4,6,8,10,12,14,16]` each | `[3,5,7,9,11,13,15,17]` each | Each signal-to-completion delay 1; seven raw inter-cycle gaps of 2 per Actor include waiting for new readiness | +| 27 / Crossing | `[5]` | `[6]` | Replacement generation 2; new-threshold publication at 4, materialization at 5, service at 6 | + +The full selected measured chronology is retained compactly below. Occurrences and service are all in `ApplyExtrinsic(2)`; `Actor:start/Transfer/summary` gives original event ordinals for a successful one-Step Cycle. Non-Cadenced Cycles have nonce 1; Cadenced service in odd block `b` has nonce `(b-1)/2`. Each Transfer is one native existential deposit and each summary is Completed. Repeated block lists have identical event positions and Actor order; no selected event is omitted by that grouping. + +| Block(s) | Occurrences: ordinal→Actor | Service: Actor:start/Transfer/summary ordinals | Signed lifecycle: ordinal / extrinsic | +| --- | --- | --- | --- | +| 2 | `4→21, 7→26, 10→22, 13→25, 16→24, 19→23` | `15:22/26/27` | `ActorPaused(17) 32 / ApplyExtrinsic(3)` | +| 3 | None | `16:4/6/7, 21:12/14/15, 26:20/22/23, 22:29/31/32, 25:37/39/40, 24:46/48/49, 23:55/57/58` | `ContractUpdated(27) 68 / ApplyExtrinsic(4)` | +| 4 | `4→26, 7→25, 10→24` | `18:13/15/16` | None | +| 5 | `3→27` | `26:6/8/9, 25:15/17/18, 24:24/26/27` | `ActorClosed(20) 34 / ApplyExtrinsic(3)` | +| 6 | `4→26, 7→25, 10→24` | `27:13/15/16` | `ActorResumed(17) 21 / ApplyExtrinsic(3)` | +| 7 | None | `19:4/6/7, 26:12/14/15, 25:21/23/24, 24:30/32/33, 17:39/41/42` | None | +| 8, 10, 12, 14, 16 | `4→26, 7→25, 10→24` | None | None | +| 9, 11, 13, 15, 17 | None | `26:4/6/7, 25:13/15/16, 24:22/24/25` | None | + +| State boundary | Observed state / meaning | +| --- | --- | +| Paused Actor 17 | Post-blocks 2–5: Paused, pending signal retained, no ticket or Run. Resume occurs after Prepass in block 6; post-state is Active/pending with ticket 18. Service at 7 clears readiness. The pause interval is not resource starvation | +| Windowed Actors 18/19 | Their initial block wakeups are 3/6, not temporal ticks. Promotion in those blocks precedes service at 4/7; pre-window waiting is excluded from a continuously-runnable gap | +| Closed Actor 20 | Remains pending with `Block(9)` wakeup through block 4; owner-close at 5 removes active state and final live bucket occupancy is zero. No Cycle or Step ever opens | +| AtTime Actors 21–23 | All are consumed/pending after block 2 with no remaining temporal wakeup, then Idle/unlatched after block 3. Anchor tick zero remains explicit | +| Cadenced Actors 24–26 | After even materialization blocks: pending, no temporal wakeup, no Run. After odd service block `b`: Idle/unlatched with next ticks `12*b + [6,4,1]`; at 17 these are 210/208/205, beyond current tick 204 | +| Crossing Actor 27 | Generation 1 through block 2; generation 2 with replacement thresholds after block 3. No readiness at 3/4; pending only after 5, cleared by service at 6. The pre-replacement transition never starts a Cycle | + +Timestamp advances from zero in setup to 12,000 ms at block 2, then by 6,000 ms per block; Actor tick quantum is 500 ms. Thus the first measured block is already tick 24 and all six initial temporal deadlines have passed. Short periods 6/4/1 all rearm before the next produced block, giving the same observed two-block materialize/service rhythm despite different tick values. The narrowed claim is mixed block/tick readiness with sparse block-window arrivals, overdue temporal coalescence and lifecycle/generation safety—not staggered temporal block arrivals or preserved per-tick execution rates. No fixture parameter changed to manufacture such evidence. + +All 12 retained Actors have progressed by the horizon; Actor 20 is intentionally closed, and no pending signal remains at block 17. No Actor has a between-block Running Pipeline in this one-Step matrix, so the Cadenced two-block inter-cycle gaps are not continuously Running `ServiceGapBlocks`. This closes W6's bounded arrival/eligibility timeline while leaving sustained fairness, User/reactive scale acceptance and complete executed-path resource accounting outside its scope. + + + +### Current EXP-0095 Refresh + +The unchanged 16-block fixture was rerun against generated Actors Weight SHA-256 `9ce37ade391a29d4f60779f1887ad242dfeb4919d25cf0047ffba85d18f33901` and exact production Wasm SHA-256 `25b9695fd9e900f17ae1f3fb0b815ac1403830264d9c31f7cee54e29f434b700`. Command: `DEOS_VERBOSE=1 REQUIRE_WASM_IDENTITY=1 SKIP_WASM_BUILD=1 ./scripts/actors-assurance.sh --integrated-w6-mixed-arrival-lifecycle`. Raw log `/tmp/deos-0725-exp0074-current-w6.log` has SHA-256 `2bfe50ba321473f53154f42491af58d7b663a8b8d06bb531db5d01ab3b1e3bae`. + +All deciding chronology reproduces: 33 successful Steps, 12 distinct progressed User Actors, three AtTime and 24 Cadenced occurrences, one replacement-generation Crossing occurrence, one pause/resume and one pre-window owner close. No Step failed, no stale generation executed and the closed Actor never became eligible. Native and exact-Wasm summaries agree; every exact-Wasm block completed execution and proof verification. + +Current Control RefTime min/p50/max is `9,683,122,870 / 25,834,700,740 / 66,524,997,122`; ProofSize is `145,751 / 265,508 / 576,747`. Effect RefTime is `0 / 2,293,433,000 / 16,054,031,000`; ProofSize remains `0 / 29,222 / 204,554`. Execution StorageProof is `14,920..25,429` bytes and CompactProof `10,780..18,317`; verification StorageProof is `1,498,165..1,508,674` and CompactProof `1,493,963..1,501,500` bytes. Historical EXP-0065 quantitative values remain provenance only. + +## Derived Metrics + +Retained block/tick and delay classifications are historical derivations; relocation computes no new service rate or Pareto ranking. + +## Statistical / Variance Notes + +Deterministic seed and 16-block horizon, not sustained stochastic arrival evidence. Temporal coalescence is explained by the actual slot/tick ratio. + +## Result + +The source records 33 successful Steps from 12 Actors, one intentional pre-window close, one pause/resume and only replacement-generation Crossing service. + +## Interpretation + +This proves neither staggered temporal block arrivals nor per-tick execution rates. Historical “closes” is bounded timeline reporting, not production User/reactive acceptance. Later User Opening qualification remains a separate parent input and cannot be resolved by this trace. + +## Decision + +Accepted for its bounded current-production arrival/eligibility claim. The EXP-0095 refresh preserves current-generation authority, lifecycle exclusions and block/tick classification while rebinding quantitative Weight and proof values. It does not accept sustained fairness, temporal rates or User/reactive scale targets. + +## Rejected Alternatives and Tradeoffs + +- `Clock interpretation`: Sparse temporal block arrivals are unsupported because short deadlines coalesce in the first produced block. +- `Service classification`: Paused/pre-window time and inter-Cycle waits are not continuously Running service gaps. + +## Validity + +- `Establishes`: The recorded bounded arrival-eligibility/current-generation observations. +- `Does not establish`: Sustained fairness, per-tick rates, User/reactive scale acceptance or complete resource accounting. +- `Known limitations`: No new runtime validation or temporary-log availability proof; source-bound Debug timeline is not a public schema. +- `Invalidation triggers`: Changed clock, fixture, observer, lifecycle/generation logic or production bindings. +- `Residual qualitative insight`: Materialization, eligibility and service are separate temporal events. + +## Follow-Up / Next Gradient + +- `Next owner`: EXP-0066 consumes O5; new resource/selector or arrival domains require separate Leaves. +- `Stop condition`: Preserve the finite witness without expansion or reruns from relocation alone. + +## Architecture Impact + +- `Affected physical truth`: None changed. +- `Architecture update`: None from relocation. +- `Provenance citation`: EXP-0074 for W6 arrival-eligibility evidence. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: [EXP-0066](./EXP-0066.md) O5. +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: None; parent decomposition is not a prerequisite. +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: [EXP-0065](./EXP-0065.md) production binding; [EXP-0025](./EXP-0025.md) physical baseline; [EXP-0073](./EXP-0073.md) shared timeline observer context only. +- `Transfers question to`: None. +- `Produces input for`: [EXP-0066](./EXP-0066.md) O5 bounded arrival-eligibility synthesis. +- `Reopen trigger`: Changed relevant identity or concrete eligibility/generation counterexample; cost alone does not reopen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0075.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0075.md new file mode 100644 index 00000000..b64787ae --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0075.md @@ -0,0 +1,166 @@ +# EXP-0075 — Due-Frontier Population Isolation + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | [EXP-0066](./EXP-0066.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-08 | +| Affected domain | Due-frontier population isolation | +| Physical mechanism | Due-only active-frontier service | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0025](./EXP-0025.md) | +| Successor experiment | None | + +## Decision Question + +Does adding non-due population at the identity cap change service or charged Actor Control for the same due frontier? + +## Governing Specification + +- `Owners`: `template/pallets/actors/docs/specification.en.md` (placement and scheduler); `docs/actors-resource-policy.specification.en.md` (component-wise admission); `docs/actors-performance-assurance.specification.en.md` (evidence). +- `Invariant`: Frozen C1, strict FIFO, Q1, cutoff, causal floor and equal thirds remain unchanged. +- `Reopen trigger`: Evidence of semantic drift requires specification review, not optimization acceptance. + +## Context and Hypothesis + +- `Hypothesis`: Non-due identities do not add charged active-frontier work for this bounded witness. +- `Materiality`: Any nonzero charged Control or service-sequence delta falsifies isolation for this comparison. + +## Baseline and Candidates + +| ID | Architecture | Source tree | Runtime/config identity | +| --- | --- | --- | --- | +| Control | 100 due workload Actors plus 15 reference identities | Source digest in extraction | Frozen C1, reference 12/24/48, equal thirds | +| Maximum | Same frontier at 10,000 identities | Same source | Same production binding | + +## Controls + +- `Controlled variables`: Same due frontier, runtime, ordered inherents, finalization and production-Wasm replay; setup outside measured interval. +- `Changed variables`: Non-due population only. +- `Confounders`: Raw proof size can change with population despite identical charged Control. + +## Benchmark Class, Workloads, and Criteria + +- `Benchmark class`: Full-runtime production-Wasm block profile; two six-block W7 profiles. +- `Metrics`: Steps, FIFO/Q1, charged Control/effects and execution/verification proof bytes, retained below. +- `Acceptance criteria`: Identical due service and charged Control; no non-due progress. +- `Rejection criteria`: Drift, invalid block or treating a finalized remainder as a phase-local stop meter. + +## Environment and Artifact Identity + +| Artifact | Identity | +| --- | --- | +| Pre-fission source | `/tmp/deos-exp0066-prefission.md`, SHA-256 `973bb0dd5f63430da3bff5aa1cc74a6df35b22b48510f47c44abf7dd8519f923`, 170875 bytes, mode `100644`; lines 474–492 | +| Migration provenance | [2026-09-08 fission manifest](./experiments.md#current-decision-critical-path) | +| Git baseline | Dirty source atop commit `a8d9ec1a07fb7fdc383148d1dbd37c83874d62be`, tree `a1cb8c8fa19dd790af5032038a352bdfd2dc673d`; not a clean measured source tree | +| Historical baseline | Commit `5b46b19004b62a723d6c45a610dfe1e5c79982b0`, template tree `98af72ffbb55fe8840b5bb44b5b01d65c8e775e1` | +| Accepted binding source | EXP-0065 source `c5345d6ba9574b9f16d0c62541400df027edd954039167f413f9b0aa9297c26d` | +| Production Wasm | `155749e22a6235570d2bb97d4a42b1327b86f4f5d82742df36494fca0fd88c1d`; preserved replay artifact, not regenerated | +| Generated Actors Weight | `20d56df225eca3ff52857f2170eccf50587c5ca17f997d18d083e61e8afbeed9` | +| Live metadata | `75868ea75fd85e7b79db12650a7407e89e249f5564b2157d55ac46cd26cb5d12` | +| Toolchain/config | Rust `1.94.1`; signer `[66; 32]`, Aura slots matching block, relay `max_pov_size` 5,000,000 bytes; database/cache not separately recorded | +| Benchmark / final release Wasm | No distinct new benchmark or final release artifact produced by extraction | +| Commands/raw evidence | Exact refresh command and raw-log digest retained below; no rerun during fission | + +## Proof Obligations + +- `Freeze`: Frozen via explicit 2026-09-08 pre-fission reconstruction; not a retrospective original freeze. +- `Review triggers`: Independent population-isolation claim extracted from compound EXP-0066. +- `Decomposition review`: One owner/changed variable; raw proof attribution belongs to [EXP-0080](./EXP-0080.md), phase-local Manual stops to [EXP-0081](./EXP-0081.md). + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | Non-due population leaves due service and charged Control unchanged | One paired block with a service or charged-Control delta | Full Executive production-Wasm | Self | Required | EXP-0066 O6 | Open; measured, no new acceptance | + +## Measurements + +Exact pre-fission EXP-0066 subsection follows. Its “above” production bindings resolve to this Environment section. + + +### W7 Due-Only Active-Frontier Measurement + +W7 used source `13dfadf5a8fe1d5d9b1aa707e4b9036dc73caf9180cc2d697542ba5994fef0b3` and compared the same 100 due one-Step Manual Actors in two production-preset profiles. The control held only those 100 workload Actors plus the 15 reference identities. The maximum profile filled the exact 10,000-identity cap: 4,943 workload Actors held one manually latched future block-window at block 100, 4,942 workload Actors remained Manual-unsignaled, 100 workload Actors were due, and 15 reference identities completed the declared 9,900 non-due population. Setup remained outside the measured interval. Each profile drained the due frontier through six linked blocks and independently replayed every block under the accepted production Wasm. + +| Metric | Result | +| --- | --- | +| Due throughput / fairness | Both profiles completed all 100 distinct due Actors in exact FIFO order with Q1 and no failed Step; identical Steps/block `[16, 19, 19, 19, 19, 8]` across blocks `2..=7` | +| Non-due isolation | No future or unsignaled workload Actor progressed. All 4,943 future memberships remained at block 100 and all 4,942 unsignaled Actors retained cycle nonce zero | +| Active-frontier comparison | Adding 9,885 non-due workload Actors to the retained 15-reference topology changed neither per-block throughput nor Actor Control charged Weight: maximum RefTime delta 0 and ProofSize delta 0 | +| Stop attribution | The first five blocks retained due FIFO work and finalized with less ProofSize than the fixture's `12,677,308,536 / 127,063` Control maximum. The sixth drained the frontier; the former comparison is not a captured phase-local stop meter | +| Actor Control actual | RefTime min 14,849,293,822; median 32,549,432,253; max 36,837,034,590. ProofSize min 255,052; median/max 590,327 | +| Actor effect actual | RefTime min 17,934,000,000; median/max 42,593,250,000. ProofSize min 233,776; median/max 555,218 | +| Maximum-population execution StorageProof / CompactProof bytes | Storage min 56,044; max 114,997. Compact min 50,194; max 106,573 | +| Maximum-population verification StorageProof / CompactProof bytes | Storage min 1,539,216; max 1,598,169. Compact min 1,533,304; max 1,589,683 | +| Due-frontier control execution StorageProof / CompactProof bytes | Storage min 23,089; max 40,659. Compact min 19,340; max 37,109 | +| Due-frontier control verification StorageProof / CompactProof bytes | Storage min 1,506,261; max 1,523,831. Compact min 1,502,450; max 1,520,219 | + +Chronological refresh: `DEOS_VERBOSE=1 scripts/actors-assurance.sh --integrated-w7-due-only-active-frontier` at source `64ca34a45568e3e3ed9b05c4939bbba7ea3fc038cc5e596e89945e688d74a01c` passed the native maximum-population fixture and both six-block exact-production-Wasm profiles. The unchanged production Weight/Wasm/metadata bindings remain those above. `/tmp/deos-0725-chronological-w7.log` has SHA-256 `1ccd5555f5058a9d9a995bc4ff65c4478f575be436756ff06501dfbe52d9df82`. The collector now preserves event chronology, so the entire 100-Actor ordered prefix is checked rather than a sorted projection. Both service sequences, the tabled Control/effect values and all tabled proof bounds reproduce. This closes the observer-order refresh for W7, not future due-herd convergence or sustained-ingress fairness. + + + +### Current EXP-0095 Refresh + +The paired frontier profiles were rerun against generated Actors Weight SHA-256 `9ce37ade391a29d4f60779f1887ad242dfeb4919d25cf0047ffba85d18f33901` and exact production Wasm SHA-256 `25b9695fd9e900f17ae1f3fb0b815ac1403830264d9c31f7cee54e29f434b700`. Command: `DEOS_VERBOSE=1 REQUIRE_WASM_IDENTITY=1 SKIP_WASM_BUILD=1 ./scripts/actors-assurance.sh --integrated-w7-due-only-active-frontier`. Raw log `/tmp/deos-0725-exp0075-current-w7.log` has SHA-256 `904105c1911d1af6cc88c1554c6e3dca456a4714f0444eb358f4d975073fece0`. + +Both the 115-identity control and 10,000-identity maximum population complete the same 100 due Actors in exact FIFO order over seven blocks, with Steps/block `[14,17,17,17,17,17,1]`, zero failed Steps and no non-due progress. Per-block Control RefTime and ProofSize deltas remain exactly zero. Current Control RefTime min/p50/max is `4,528,011,122 / 39,148,517,106 / 41,856,931,984`; ProofSize is `64,513 / 595,925 / 595,925`. Effect RefTime is `2,293,433,000 / 38,988,361,000 / 38,988,361,000`; ProofSize is `29,222 / 496,774 / 496,774`. + +Maximum-population execution StorageProof is `19,975..103,430` bytes and CompactProof `16,325..95,556`; verification StorageProof is `1,503,220..1,586,675` and CompactProof `1,499,508..1,578,739`. Control-population execution StorageProof is `13,863..41,300` and CompactProof `10,450..37,443`; verification StorageProof is `1,497,108..1,524,545` and CompactProof `1,493,633..1,520,626`. The changed service envelope and proof values supersede only the historical EXP-0065 numerics, not the zero-delta isolation result. + +## Derived Metrics + +Charged Control deltas are exactly zero; proof-byte ranges remain distinct. No new Pareto selection. + +## Statistical / Variance Notes + +Deterministic linked-block witness with chronological reproduction, not independent host-timing samples or sustained ingress. + +## Result + +Both populations drain 100 due Actors in six blocks with identical service and charged Control. + +## Interpretation + +The bounded active-frontier witness supports population isolation, not population-independent raw proof size or general herd convergence. + +## Decision + +Accepted for the bounded current due-frontier isolation claim. The EXP-0095 refresh preserves identical service and charged Control under 9,885 additional non-due workload identities. It does not establish population-independent raw proof, sustained fairness or the governing throughput target. + +## Rejected Alternatives and Tradeoffs + +- `Rejected inference`: Finalized headroom is not a phase-local stop meter; raw proof growth is not charged Control growth. + +## Validity + +- `Establishes`: Exact bounded comparison and chronological reproduction. +- `Does not establish`: Sustained-ingress fairness, future due-herd convergence or throughput acceptance. +- `Invalidation triggers`: Changed source, population geometry, runtime, Weight or replay methodology. + +## Follow-Up / Next Gradient + +Stop this extraction; parent integrates the scoped result. New herd/ingress claims require their own owner. + +## Architecture Impact + +No architecture change; frozen C1 remains unchanged. Provenance: EXP-0075. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: [EXP-0066](./EXP-0066.md) O6. +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: Governing specifications and [EXP-0025](./EXP-0025.md), physical architecture only. +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: [EXP-0065](./EXP-0065.md), exact production bindings. +- `Transfers question to`: None. +- `Produces input for`: [EXP-0066](./EXP-0066.md), due-frontier population isolation. +- `Reopen trigger`: Changed controlled population or binding invalidates this numeric comparison; cost alone does not reopen frozen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0076.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0076.md new file mode 100644 index 00000000..e8be1080 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0076.md @@ -0,0 +1,173 @@ +# EXP-0076 — Ready Tombstone Prefix Pressure + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | [EXP-0066](./EXP-0066.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-08 | +| Affected domain | Ready tombstone prefix pressure | +| Physical mechanism | Canonical Ready tombstone reclamation | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0025](./EXP-0025.md) | +| Successor experiment | None | + +## Decision Question + +How does a legal tombstone prefix consume service capacity at fixed C32 geometry? + +## Governing Specification + +- `Owners`: `template/pallets/actors/docs/specification.en.md` (scheduler/lifecycle); `docs/actors-resource-policy.specification.en.md` (admission); `docs/actors-performance-assurance.specification.en.md` (evidence). +- `Invariant`: Strict FIFO, Q1, cutoff, causal floor and equal thirds; no layout change. +- `Reopen trigger`: Semantic drift requires specification review rather than candidate acceptance. + +## Context and Hypothesis + +- `Hypothesis`: Increasing legal prefix reclamation pressure reduces the admitted live FIFO prefix under fixed Control capacity. +- `Materiality`: Measured live service and Control changes, not speculative geometry savings. + +## Baseline and Candidates + +| ID | Architecture | Source tree | Runtime/config identity | +| --- | --- | --- | --- | +| W8 | Seven legal prefix lengths, fixed C32 | Source digest in extraction | Frozen C1 reference 12/24/48, equal thirds | + +## Controls + +- `Controlled variables`: 100 due Manual one-Step Actors, block 2, full Executive/finalization, exact production replay; closed identities removed before adding live Actors. +- `Changed variables`: Tombstone prefix `[1,4,8,16,32,64,128]`. +- `Confounders`: Finalized headroom is not the live-head phase-local admission meter. + +## Benchmark Class, Workloads, and Criteria + +- `Benchmark class`: Seven independent one-block production-Wasm profiles. +- `Metrics`: Reclaimed chunks, live FIFO prefix, charged Control/effects and proof bytes. +- `Acceptance criteria`: Exact legal reclamation and live-prefix accounting without semantic drift. +- `Rejection criteria`: Alternative-layout or steady-state claims inferred from this bounded prefix witness. + +## Environment and Artifact Identity + +| Artifact | Identity | +| --- | --- | +| Pre-fission source | `/tmp/deos-exp0066-prefission.md`, SHA-256 `973bb0dd5f63430da3bff5aa1cc74a6df35b22b48510f47c44abf7dd8519f923`, 170875 bytes, mode `100644`; lines 493–510 | +| Migration provenance | [2026-09-08 fission manifest](./experiments.md#current-decision-critical-path) | +| Git baseline | Dirty atop commit `a8d9ec1a07fb7fdc383148d1dbd37c83874d62be`, tree `a1cb8c8fa19dd790af5032038a352bdfd2dc673d`; not clean measured source | +| Historical baseline | Commit `5b46b19004b62a723d6c45a610dfe1e5c79982b0`, template tree `98af72ffbb55fe8840b5bb44b5b01d65c8e775e1` | +| Accepted binding source | EXP-0065 `c5345d6ba9574b9f16d0c62541400df027edd954039167f413f9b0aa9297c26d` | +| Production Wasm | `155749e22a6235570d2bb97d4a42b1327b86f4f5d82742df36494fca0fd88c1d`, preserved replay artifact | +| Actors Weight | `20d56df225eca3ff52857f2170eccf50587c5ca17f997d18d083e61e8afbeed9` | +| Metadata | `75868ea75fd85e7b79db12650a7407e89e249f5564b2157d55ac46cd26cb5d12` | +| Chronology-refresh source | `64ca34a45568e3e3ed9b05c4939bbba7ea3fc038cc5e596e89945e688d74a01c`, the W7 cross-reference below | +| Toolchain/config | Rust `1.94.1`; 15 retained reference identities, signer `[66; 32]`, matching Aura/block slots, relay `max_pov_size` 5,000,000 bytes; database/cache not separately recorded | +| Benchmark / final release Wasm | No new artifact generated by extraction | +| Command/raw evidence | Exact command/log digest below; no fission rerun | + +## Proof Obligations + +- `Freeze`: Frozen via explicit 2026-09-08 pre-fission reconstruction, not retrospective original freeze. +- `Review triggers`: Independent tombstone-pressure owner split from compound EXP-0066. +- `Decomposition review`: All seven prefixes share one mechanism/variable/criterion; no candidate layout sweep. W7 references resolve to [EXP-0075](./EXP-0075.md); exact Manual stop ownership to [EXP-0081](./EXP-0081.md). + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | Legal tombstone reclamation pressure bounds the retained live FIFO service prefix at C32 | One mismatched reclaimed prefix or live service/accounting row | Full Executive production-Wasm | Self | Required | EXP-0066 O7 | Open; measured, no new acceptance | + +## Measurements + +Exact pre-fission EXP-0066 subsection; “same chronology-refresh source/binding as W7” resolves to the Environment section and EXP-0075. + + +### W8 Tombstone-Prefix and Chunk-Pressure Measurement + +W8 used source `8f8068f27667afe1bb7b418f402fc465bb8ae2cb424f97fdf61255aa6cf0fbbc`. Each independent production-preset profile legally created, manually latched and owner-closed the declared prefix Actors before adding 100 due Manual one-Step Transfer Actors. This setup left only canonical Ready tombstones ahead of the live FIFO and did not retain the closed identities. Block 2 then ran through full Executive/finalization and independently replayed under the accepted production Wasm. The fixed C32 Ready chunk width was an observed frozen-C1 input, not a candidate layout sweep. + +| Tombstone prefix | C32 chunks touched / fully reclaimed | Due Steps and distinct Actors | Final head (chunk, offset) | Actor Control RefTime / ProofSize | +| ---: | ---: | ---: | ---: | ---: | +| 1 | 1 / 0 | 16 / 16 | 17 (0, 17) | 37,145,908,617 / 587,679 | +| 4 | 1 / 0 | 15 / 15 | 19 (0, 19) | 36,509,188,477 / 569,279 | +| 8 | 1 / 0 | 15 / 15 | 23 (0, 23) | 37,744,684,585 / 581,723 | +| 16 | 1 / 0 | 14 / 14 | 30 (0, 30) | 38,652,334,580 / 578,878 | +| 32 | 1 / 1 | 12 / 12 | 44 (1, 12) | 40,467,634,570 / 573,188 | +| 64 | 2 / 2 | 9 / 9 | 73 (2, 9) | 45,661,576,771 / 589,541 | +| 128 | 4 / 4 | 2 / 2 | 130 (4, 2) | 54,486,118,952 / 594,514 | + +Every profile reclaimed the exact tombstone prefix, committed only the corresponding first live FIFO prefix, preserved Q1 with no failed Step, and retained live work. The next complete one-Step Control maximum was `12,677,308,536 / 127,063`; remaining Control ProofSize ranged from 81,636 to 106,871, so the fixture maximum exceeded finalized ProofSize headroom in all seven profiles while its RefTime fit. This does not capture the precise meter value at a live-head stop. Actor effect usage ranged from `4,483,500,000 / 58,444` to `35,868,000,000 / 467,552`. Execution StorageProof ranged 24,383–33,537 bytes and CompactProof 21,370–30,994; verification StorageProof ranged 1,507,555–1,516,709 and CompactProof 1,504,480–1,514,104. + +Chronological refresh: `DEOS_VERBOSE=1 scripts/actors-assurance.sh --integrated-w8-tombstone-prefix-chunk-pressure` passed native and exact-production-Wasm checks for all seven prefixes at the same chronology-refresh source/binding as W7. `/tmp/deos-0725-chronological-w8.log` has SHA-256 `64a012f698df19e42d504e8b6ff6ec5191868a7098b511030fd5e7abc6ce2fbd`. Every table row reproduces, and each remaining live prefix is now compared in actual event order. This closes W8's observer-order refresh without claiming steady-state churn or alternative geometry. + + + +### Current EXP-0095 Refresh + +The seven legal-prefix profiles were rerun against generated Actors Weight SHA-256 `9ce37ade391a29d4f60779f1887ad242dfeb4919d25cf0047ffba85d18f33901` and exact production Wasm SHA-256 `25b9695fd9e900f17ae1f3fb0b815ac1403830264d9c31f7cee54e29f434b700`. Command: `DEOS_VERBOSE=1 REQUIRE_WASM_IDENTITY=1 SKIP_WASM_BUILD=1 ./scripts/actors-assurance.sh --integrated-w8-tombstone-prefix-chunk-pressure`. Raw log `/tmp/deos-0725-exp0076-current-w8.log` has SHA-256 `0799027df5a2a504d80e43d6d0e1af95cb63fc55127bf39aecaee996e281b186`. + +| Tombstone prefix | Chunks touched / reclaimed | Due Steps | Final head (chunk, offset) | Control RefTime / ProofSize | +| ---: | ---: | ---: | ---: | ---: | +| 1 | `1 / 0` | 14 | `15 (0,15)` | `42,192,595,358 / 582,501` | +| 4 | `1 / 0` | 14 | `18 (0,18)` | `43,199,585,480 / 591,834` | +| 8 | `1 / 0` | 13 | `21 (0,21)` | `42,414,693,602 / 572,953` | +| 16 | `1 / 0` | 13 | `29 (0,29)` | `45,100,000,594 / 597,841` | +| 32 | `1 / 1` | 11 | `43 (1,11)` | `46,215,523,830 / 584,967` | +| 64 | `2 / 2` | 8 | `72 (2,8)` | `50,574,115,676 / 590,544` | +| 128 | `4 / 4` | 1 | `129 (4,1)` | `57,163,753,994 / 570,373` | + +Every profile reclaims the exact prefix, preserves the live FIFO prefix and Q1, and records no failed Step. Effect usage ranges from `2,293,433,000 / 29,222` to `32,108,062,000 / 409,108`. Execution StorageProof ranges `23,146..32,569` bytes and CompactProof `20,154..29,869`; verification StorageProof ranges `1,506,391..1,515,814` and CompactProof `1,503,337..1,513,052`. The current rows replace historical quantitative values while preserving the fixed-C32 pressure conclusion. + +## Derived Metrics + +No additional derived metric or Pareto ranking; this is fixed-geometry pressure evidence. + +## Statistical / Variance Notes + +Seven deterministic state profiles with chronological reproduction; not a steady-state churn distribution. + +## Result + +All exact prefixes reclaimed; live service ranges from 16 to 2 successful Steps with larger prefixes. + +## Interpretation + +Reclamation is measured current-topology work. Finalized ProofSize headroom supports a conservative bound, not an exact stop deficit or irreducible minimum. + +## Decision + +Accepted for the bounded current tombstone-prefix claim. The EXP-0095 refresh confirms exact legal reclamation and decreasing admitted live FIFO prefixes under fixed C32 capacity. It does not establish steady-state churn, alternate geometry or an exact phase-local stop deficit. + +## Rejected Alternatives and Tradeoffs + +- `Rejected inference`: No steady-state churn, alternative geometry or exact phase-local stop claim. + +## Validity + +- `Establishes`: Seven legal bounded-prefix witnesses on exact bindings. +- `Does not establish`: Removability, steady-state capacity or release throughput acceptance. +- `Invalidation triggers`: Ready layout, lifecycle legality, Weight, runtime or replay changes. + +## Follow-Up / Next Gradient + +Return measured pressure to parent; no layout candidate is authorized. Raw proof ownership remains [EXP-0080](./EXP-0080.md). + +## Architecture Impact + +No change to C32 or frozen C1; provenance EXP-0076 only. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: [EXP-0066](./EXP-0066.md) O7. +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: Governing specifications and [EXP-0025](./EXP-0025.md), physical architecture only. +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: [EXP-0065](./EXP-0065.md), production bindings; [EXP-0075](./EXP-0075.md), shared chronology-refresh identity. +- `Transfers question to`: None. +- `Produces input for`: [EXP-0066](./EXP-0066.md), tombstone-prefix pressure. +- `Reopen trigger`: Changed prefix legality, geometry or bindings; cost alone does not reopen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0077.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0077.md new file mode 100644 index 00000000..eaa03e2f --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0077.md @@ -0,0 +1,178 @@ +# EXP-0077 — User Resource Independence + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | [EXP-0066](./EXP-0066.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-08 | +| Affected domain | User resource independence | +| Physical mechanism | Separate User and Actor resource turns | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0025](./EXP-0025.md) | +| Successor experiment | None | + +## Decision Question + +Does bounded valid User demand preserve the same due Actor service and accounting under separate resource turns? + +## Governing Specification + +- `Owners`: `docs/actors-resource-policy.specification.en.md` (equal-thirds, component-wise admission); `template/pallets/actors/docs/specification.en.md` (FIFO/Q1); `docs/actors-performance-assurance.specification.en.md` (demand evidence). +- `Invariant`: Valid signed demand, full Executive/finalization, unchanged C1 and causal floor. +- `Reopen trigger`: Resource-policy or semantic drift requires specification review. + +## Context and Hypothesis + +- `Hypothesis`: User ProofSize saturation and the measured RefTime-heavy fallback do not change the bounded Actor service prefix. +- `Materiality`: Any service or Actor-accounting difference in the matched state falsifies this witness. + +## Baseline and Candidates + +| ID | Architecture | Source tree | Runtime/config identity | +| --- | --- | --- | --- | +| W9 | Actor-only, remarks, Router demand from same state | `0768922edef5d606dbf1608f22ca38dc996af2feaf70cee6a6891e8fb0163c7a` | C1, reference 12/24/48, equal thirds | + +## Controls + +- `Controlled variables`: Same 100 due Manual one-Step Actors and pool, block 2, valid production replay. +- `Changed variables`: Signed demand profile only. +- `Confounders`: Both User profiles bind ProofSize; Router is not RefTime saturation. + +## Benchmark Class, Workloads, and Criteria + +- `Benchmark class`: Three one-block full-runtime production-Wasm profiles. +- `Metrics`: Valid calls, User remainders, next-call admission, Actor service/Weight and proof bytes. +- `Acceptance criteria`: Identical Actor FIFO prefix and accounting under the stated demand modes. +- `Rejection criteria`: Invalid Transfer fixture, invalid demand, fallback mislabeled saturation or complete-surface impossibility. + +## Environment and Artifact Identity + +| Artifact | Identity | +| --- | --- | +| Pre-fission source | `/tmp/deos-exp0066-prefission.md`, SHA-256 `973bb0dd5f63430da3bff5aa1cc74a6df35b22b48510f47c44abf7dd8519f923`, 170875 bytes, mode `100644`; lines 511–532 | +| Migration provenance | [2026-09-08 fission manifest](./experiments.md#current-decision-critical-path) | +| Git baseline | Dirty atop commit `a8d9ec1a07fb7fdc383148d1dbd37c83874d62be`, tree `a1cb8c8fa19dd790af5032038a352bdfd2dc673d`; not clean measured source | +| Historical baseline | Commit `5b46b19004b62a723d6c45a610dfe1e5c79982b0`, template tree `98af72ffbb55fe8840b5bb44b5b01d65c8e775e1` | +| Accepted binding source | EXP-0065 `c5345d6ba9574b9f16d0c62541400df027edd954039167f413f9b0aa9297c26d` | +| Production Wasm | `155749e22a6235570d2bb97d4a42b1327b86f4f5d82742df36494fca0fd88c1d`, preserved replay artifact | +| Actors Weight | `20d56df225eca3ff52857f2170eccf50587c5ca17f997d18d083e61e8afbeed9` | +| Metadata | `75868ea75fd85e7b79db12650a7407e89e249f5564b2157d55ac46cd26cb5d12` | +| Chronology-refresh source | `64ca34a45568e3e3ed9b05c4939bbba7ea3fc038cc5e596e89945e688d74a01c`, shared with W7 | +| Toolchain/config | Rust `1.94.1`; 15 retained reference identities, signer `[66; 32]`, Aura slots matching block, relay `max_pov_size` 5,000,000 bytes; database/cache not separately recorded | +| Benchmark / final release Wasm | No new artifact produced by extraction | +| Command/raw evidence | Exact refresh command/log digest below; no fission rerun | + +## Proof Obligations + +- `Freeze`: Frozen via explicit 2026-09-08 pre-fission reconstruction; not retrospective original freeze. +- `Review triggers`: Independent User-resource witness extracted from compound EXP-0066. +- `Decomposition review`: One changed demand variable and service-independence criterion. Broader signed-demand accounting belongs to [EXP-0085](./EXP-0085.md); W7 identity belongs to [EXP-0075](./EXP-0075.md). + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | Measured valid User demand preserves bounded Actor service/accounting | One different Actor prefix or charged Actor Weight from matched state | Full Executive production-Wasm | Self | Required | EXP-0066 O8 | Open; measured, no new acceptance | + +## Measurements + +Exact pre-fission EXP-0066 subsection; its shared W7 binding resolves above and to EXP-0075. + + +### W9 User-Resource Independence and RefTime-Frontier Fallback Measurement + +W9 prepared one production-preset state with 100 due Manual one-Step Transfer Actors plus a canonical Native/test-asset Router pool, then independently authored block 2 from that exact state under Actor-only demand, proof-saturated signed remarks, and alternating signed Router swaps. Every block used full Executive/finalization and independently replayed under the accepted production Wasm. + +| Metric | Actor-only | Proof-saturated remarks | RefTime-heavy Router swaps | +| --- | ---: | ---: | ---: | +| Valid signed calls | 0 | 77 | 22 | +| User RefTime / ProofSize | 0 / 0 | 38,334,246,720 / 673,442 | 109,283,350,000 / 673,376 | +| User remainder | 305,126,456,576 / 676,150 | 266,792,209,856 / 2,708 | 195,843,106,576 / 2,774 | +| Next valid call | 0 / 0 | 497,847,360 / 8,746 | 4,967,425,000 / 30,608 | +| Due Steps / distinct Actors | 16 / 16 | 16 / 16 | 16 / 16 | +| Actor Control | 36,837,034,590 / 584,568 | 36,837,034,590 / 584,568 | 36,837,034,590 / 584,568 | +| Actor effect | 35,868,000,000 / 467,552 | 35,868,000,000 / 467,552 | 35,868,000,000 / 467,552 | +| Execution StorageProof / CompactProof bytes | 33,036 / 30,523 | 33,536 / 30,865 | 36,584 / 32,970 | +| Verification StorageProof / CompactProof bytes | 1,516,208 / 1,513,633 | 1,516,708 / 1,513,975 | 1,519,756 / 1,516,080 | + +All three profiles committed the identical first 16 Actor IDs with Q1, no failed Step and identical Actor Control/effect accounting. Both valid-user profiles stopped because the next call exceeded User ProofSize while fitting RefTime. Router demand consumed 2.85 times the remark RefTime with only a 66-byte ProofSize difference, but still left 195,843,106,576 RefTime; therefore it is an honest RefTime-heavy valid-business-call fallback, not RefTime saturation. + +Chronological refresh: `DEOS_VERBOSE=1 scripts/actors-assurance.sh --integrated-w9-resource-independence` passed native and exact-production-Wasm checks for all three demand profiles at the same chronology-refresh source/binding as W7. `/tmp/deos-0725-chronological-w9.log` has SHA-256 `86668cbc47a3a39135785fa63fa5f33c784785d3a84dabddfdef3943a38f9cf3`. Every numerical cell above reproduces; the identical first 16 Actors are now verified in actual event order. Both valid-user workloads still bind User ProofSize rather than RefTime. This closes W9's observer-order refresh, not a standalone RefTime-saturation or complete callable-surface impossibility claim. + +An initial native W9 probe reused a one-unit Transfer into an unfunded recipient and failed `Token(BelowMinimum)`. It was discarded; the retained W9 fixture transfers one existential deposit. No W9 evidence from the invalid probe is retained. + + + +### Current EXP-0095 Refresh + +The three matched-state profiles were rerun against generated Actors Weight SHA-256 `9ce37ade391a29d4f60779f1887ad242dfeb4919d25cf0047ffba85d18f33901` and exact production Wasm SHA-256 `25b9695fd9e900f17ae1f3fb0b815ac1403830264d9c31f7cee54e29f434b700`. Command: `DEOS_VERBOSE=1 REQUIRE_WASM_IDENTITY=1 SKIP_WASM_BUILD=1 ./scripts/actors-assurance.sh --integrated-w9-resource-independence`. Raw log `/tmp/deos-0725-exp0077-current-w9.log` has SHA-256 `d85d98d5d92234047756f0126e988ca0f2c6a1b5f89955161cf586307470f0d8`. + +| Metric | Actor-only | Proof-saturated remarks | RefTime-heavy Router swaps | +| --- | ---: | ---: | ---: | +| Valid signed calls | 0 | 77 | 22 | +| User RefTime / ProofSize | `0 / 0` | `38,350,416,720 / 673,442` | `109,287,970,000 / 673,376` | +| User remainder | `302,102,663,243 / 676,150` | `263,752,246,523 / 2,708` | `192,814,693,243 / 2,774` | +| Next valid call | `0 / 0` | `498,057,360 / 8,746` | `4,967,635,000 / 30,608` | +| Due Steps / distinct Actors | `14 / 14` | `14 / 14` | `14 / 14` | +| Actor Control | `41,856,931,984 / 579,390` | same | same | +| Actor effect | `32,108,062,000 / 409,108` | same | same | + +All three profiles commit the same ordered 14-Actor FIFO prefix with Q1 and identical Actor accounting. Both valid-demand profiles remain User-ProofSize-bound rather than RefTime-bound. Exact-Wasm execution/proof verification succeeds for each profile; execution StorageProof is `31,743 / 32,243 / 35,291` bytes and CompactProof `29,208 / 29,550 / 31,655`, while verification StorageProof is `1,514,988 / 1,515,488 / 1,518,536` and CompactProof `1,512,391 / 1,512,733 / 1,514,838`. + +## Derived Metrics + +The retained 2.85-times RefTime comparison does not change the binding User ProofSize dimension; no new Pareto selection. + +## Statistical / Variance Notes + +Deterministic matched-state profiles and chronological reproduction; no sustained-demand or wall-clock distribution inferred. + +## Result + +All profiles commit the same 16 Actors with identical Actor Control/effect accounting; both valid-user profiles exhaust ProofSize first. + +## Interpretation + +Bounded resource independence is supported only for these demand profiles. Unused User RefTime prevents a RefTime-saturation conclusion. + +## Decision + +Accepted for the bounded current User/Actor resource-independence claim. The EXP-0095 refresh preserves identical Actor service and accounting under both valid signed-demand profiles. It does not establish RefTime saturation, sustained-demand fairness or complete callable-surface impossibility. + +## Rejected Alternatives and Tradeoffs + +- `Invalid probe`: One-unit unfunded-recipient Transfer failed `Token(BelowMinimum)`; no measurement retained. +- `Rejected inference`: Router fallback is neither RefTime saturation nor complete callable-surface impossibility evidence. + +## Validity + +- `Establishes`: Exact bounded User/Actor resource comparison. +- `Does not establish`: Standalone RefTime saturation, sustained-demand target acceptance or all-callable-surface impossibility. +- `Invalidation triggers`: Demand validity, resource policy, adapter, Weight, runtime or replay changes. + +## Follow-Up / Next Gradient + +Stop extraction and return scoped evidence to parent. Broader signed-demand sufficiency remains separately owned by EXP-0085. + +## Architecture Impact + +No architecture or resource-policy change. Provenance EXP-0077. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: [EXP-0066](./EXP-0066.md) O8. +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: Governing specifications and [EXP-0025](./EXP-0025.md), physical architecture only. +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: [EXP-0065](./EXP-0065.md), production bindings; [EXP-0075](./EXP-0075.md), chronology-refresh identity. +- `Transfers question to`: None. +- `Produces input for`: [EXP-0066](./EXP-0066.md), bounded User resource independence. +- `Reopen trigger`: Changed valid-demand or production binding invalidates comparison; cost alone does not reopen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0078.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0078.md new file mode 100644 index 00000000..55f2d1b9 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0078.md @@ -0,0 +1,161 @@ +# EXP-0078 — Temporal Materialization Phase Ownership + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | [EXP-0066](./EXP-0066.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-08 | +| Affected domain | Temporal materialization phase attribution | +| Physical mechanism | Mandatory-Prepass temporal materialization and service | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0025](./EXP-0025.md) | +| Successor experiment | None | + +## Decision Question + +Does temporal materialization share the mandatory-Prepass Control budget with service while the captured cutoff defers newly materialized Actors? + +## Governing Specification + +- `Owners`: `template/pallets/actors/docs/specification.en.md` (temporal readiness, scheduler, cutoff); `docs/actors-resource-policy.specification.en.md` (Control accounting); `docs/actors-performance-assurance.specification.en.md` (evidence). +- `Invariant`: FIFO, Q1, causal floor, equal thirds and committed Steps remain unchanged. +- `Reopen trigger`: Any proposal changing phase authority or causal order must reopen the owning specification, not masquerade as optimization. + +## Context and Hypothesis + +- `Hypothesis`: Temporal occurrence materialization competes with service in the same mandatory Prepass; new readiness cannot cross the cutoff in that block. +- `Materiality`: Exact placement and disjointness are required, independent of any cost-saving proposal. + +## Baseline and Candidates + +| ID | Architecture | Source tree | Runtime/config identity | +| --- | --- | --- | --- | +| Matched phases | 100 Manual versus 100 cadence-one Actors | Source digest in extraction | Frozen C1, reference 12/24/48, equal thirds | + +## Controls + +- `Controlled variables`: Successful one-ED Transfers, blocks `2..=10`, ordered inherents/finalization and preserved production-Wasm replay. +- `Changed variables`: Manual versus cadence-one readiness. +- `Confounders`: Different occurrence/service counts do not isolate removable overhead. + +## Benchmark Class, Workloads, and Criteria + +- `Benchmark class`: Matched nine-block full-runtime production-Wasm phase profiles. +- `Metrics`: Steps, temporal occurrences, prepass/final Control proof and cutoff disjointness. +- `Acceptance criteria`: Observed work assigned to its actual phase; no newly materialized/progressed overlap. +- `Rejection criteria`: Invalid Transfer, inferred removability, or alternative-scheduler acceptance from phase placement alone. + +## Environment and Artifact Identity + +| Artifact | Identity | +| --- | --- | +| Pre-fission source | `/tmp/deos-exp0066-prefission.md`, SHA-256 `973bb0dd5f63430da3bff5aa1cc74a6df35b22b48510f47c44abf7dd8519f923`, 170875 bytes, mode `100644`; lines 533–548 | +| Migration provenance | [2026-09-08 fission manifest](./experiments.md#current-decision-critical-path) | +| Git baseline | Dirty atop commit `a8d9ec1a07fb7fdc383148d1dbd37c83874d62be`, tree `a1cb8c8fa19dd790af5032038a352bdfd2dc673d`; not clean measured source | +| Historical baseline | Commit `5b46b19004b62a723d6c45a610dfe1e5c79982b0`, template tree `98af72ffbb55fe8840b5bb44b5b01d65c8e775e1` | +| Accepted binding source | EXP-0065 `c5345d6ba9574b9f16d0c62541400df027edd954039167f413f9b0aa9297c26d` | +| Production Wasm | `155749e22a6235570d2bb97d4a42b1327b86f4f5d82742df36494fca0fd88c1d`, preserved replay artifact | +| Actors Weight | `20d56df225eca3ff52857f2170eccf50587c5ca17f997d18d083e61e8afbeed9` | +| Metadata | `75868ea75fd85e7b79db12650a7407e89e249f5564b2157d55ac46cd26cb5d12` | +| Toolchain/config | Rust `1.94.1`; reference preset with 15 retained identities; signer `[66; 32]`, matching Aura/block slots, relay `max_pov_size` 5,000,000 bytes; database/cache not separately recorded | +| Command/raw evidence | Allocated source subsection records phase checkpoints and source digest, not a separate exact command/log digest; none invented here | +| Benchmark / final release Wasm | No new artifact produced by extraction | + +## Proof Obligations + +- `Freeze`: Frozen via explicit 2026-09-08 pre-fission reconstruction; not retrospective original freeze. +- `Review triggers`: Independent phase-owner claim extracted from compound EXP-0066. +- `Decomposition review`: Phase placement only; generated temporal charge/admission belongs to [EXP-0079](./EXP-0079.md), later schedule accounting to [EXP-0084](./EXP-0084.md), Manual stops to [EXP-0081](./EXP-0081.md). + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | Temporal materialization shares mandatory Prepass with service and cutoff defers new work | One occurrence/Step outside claimed phase or newly materialized/progressed overlap | Full Executive production-Wasm phase profile | Self | Required | EXP-0066 O9 | Open; measured, no new acceptance | + +## Measurements + +Exact pre-fission EXP-0066 subsection. W7 now belongs to [EXP-0075](./EXP-0075.md); the production identity is above. + + +### Schedule-Control Phase Attribution + +The exact-Wasm-replayed attribution cohort used source `6541ad342c2c48face4d2304fd5f3a34744417752175960c20c989f3a02c7d6f` and matched 100-Actor Manual-only and cadence-one fixtures over blocks `2..=10`. Every Transfer used one existential deposit and succeeded. The harness checkpointed resource state after all ordered inherents and before signed demand, then compared it with finalized telemetry. + +| Metric | Manual-only | Cadenced-only | +| --- | --- | --- | +| Successful Steps/block | `[16,19,19,19,19,8,0,0,0]` | `[0,0,4,0,0,4,2,15,4]` | +| Successful Steps total | 100 | 29 | +| Temporal occurrences/block | `[0,0,0,0,0,0,0,0,0]` | `[16,19,13,18,19,13,10,2,13]` (123 total) | +| Prepass Control ProofSize | `[563231,568990,568990,568990,568990,248821,26957,26957,26957]` | `[623021,619771,553451,619771,619771,553451,566185,565040,553451]` | +| Final Control ProofSize | `[584568,590327,590327,590327,590327,255052,33188,33188,33188]` | `[629252,641108,574788,641108,641108,574788,587522,586377,574788]` | +| Phase placement | Every observed Step was committed during mandatory Prepass | Every occurrence and observed Step was committed during mandatory Prepass | +| Cutoff relation | No temporal materialization | In every block, newly materialized Actor IDs were disjoint from progressed IDs; captured FIFO cutoff deferred them | + +The matched successful-effect control reproduces the due Manual sequence already observed by W7 and narrows the material schedule owner: temporal wakeup occurrence materialization consumes the same mandatory-Prepass Actor Control budget as service, while cutoff ordering prevents newly materialized work from using that block's service pass. This establishes phase ownership and ordering pressure, not removability, accidental overhead, or an alternative scheduler topology. An initial attribution probe reused the generic one-unit control fixture, observed `Token(BelowMinimum)`, and was discarded before the existential-deposit fixture was measured. + + + +### Current EXP-0095 Refresh + +The matched nine-block profiles were rerun against generated Actors Weight SHA-256 `9ce37ade391a29d4f60779f1887ad242dfeb4919d25cf0047ffba85d18f33901` and exact production Wasm SHA-256 `25b9695fd9e900f17ae1f3fb0b815ac1403830264d9c31f7cee54e29f434b700`. Command: `DEOS_VERBOSE=1 REQUIRE_WASM_IDENTITY=1 SKIP_WASM_BUILD=1 ./scripts/actors-assurance.sh --integrated-control-attribution`. Raw log `/tmp/deos-0725-control-current.log` has SHA-256 `4de0eecc1a8d6bb131c63e552c3e25eeb162c34c347e165a40e9e0964c0fbc1f`. + +Manual service is `[14,17,17,17,17,17,1,0,0]`, totaling 100 Steps. Cadenced service is `[0,0,4,0,0,4,2,13,5]`, totaling 28 Steps; occurrences are `[16,19,13,18,19,13,10,2,12]`, totaling 122. Every observed occurrence and Step remains in mandatory Prepass, and newly materialized Actor IDs remain disjoint from progressed IDs under the captured cutoff. All 18 exact-Wasm blocks execute and verify with complete key-path plus committed-root reconstruction. + +## Derived Metrics + +No new derived cost subtraction or Pareto ranking; the extraction retains original block vectors and totals. + +## Statistical / Variance Notes + +Nine deterministic linked blocks per matched schedule; no independent host-latency sample distribution claimed. + +## Result + +Manual completes 100 Steps and cadence-one 29; all observed service and occurrences happen in mandatory Prepass, with newly materialized IDs disjoint from progressed IDs. + +## Interpretation + +Phase ownership and ordering pressure are observed. The comparison does not isolate removable temporal overhead or authorize a scheduler replacement. + +## Decision + +Accepted for the bounded current phase-attribution claim. The EXP-0095 refresh preserves mandatory-Prepass ownership and cutoff deferral while replacing historical service and resource vectors. It does not isolate removable temporal overhead or establish sustained throughput. + +## Rejected Alternatives and Tradeoffs + +- `Invalid probe`: One-unit generic control Transfer failed `Token(BelowMinimum)` and was discarded. +- `Rejected inference`: Shared budget pressure does not prove accidental overhead or removability. + +## Validity + +- `Establishes`: Phase placement and cutoff behavior in the matched cohort. +- `Does not establish`: Universal minimum temporal cost, sustained throughput or alternative topology superiority. +- `Invalidation triggers`: Changed phase ordering, cutoff, runtime, Weight, workload or replay identity. + +## Follow-Up / Next Gradient + +Stop phase investigation at extraction; EXP-0079 owns generated temporal envelopes and EXP-0084 owns schedule accounting. + +## Architecture Impact + +No architecture change. Provenance EXP-0078 records measured placement only. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: [EXP-0066](./EXP-0066.md) O9. +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: Governing specifications and [EXP-0025](./EXP-0025.md), physical architecture only. +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: [EXP-0065](./EXP-0065.md), exact production binding; [EXP-0075](./EXP-0075.md), due Manual service sequence. +- `Transfers question to`: None. +- `Produces input for`: [EXP-0066](./EXP-0066.md), temporal phase ownership. +- `Reopen trigger`: Changed phase/cutoff or artifact binding; cost alone does not reopen frozen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0079.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0079.md new file mode 100644 index 00000000..606e738d --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0079.md @@ -0,0 +1,202 @@ +# EXP-0079 — Temporal Generated Charge and Admission Ownership + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | [EXP-0066](./EXP-0066.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-08 | +| Affected domain | Temporal generated charge and admission ownership | +| Physical mechanism | Component-wise temporal branch and contingency selectors | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0025](./EXP-0025.md) | +| Successor experiment | None | + +## Decision Question + +Does the temporal ledger identify the actual component-wise charged selector separately from the complete pre-mutation admission envelope? + +## Governing Specification + +- `Owners`: `docs/actors-resource-policy.specification.en.md` (component-wise admission and settlement); `template/pallets/actors/docs/specification.en.md` (temporal readiness, fees, lifecycle and cutoff). +- `Invariant`: Complete admission before mutation; useful readiness, failure-preserving future readiness and fee ownership remain unchanged. +- `Reopen trigger`: Any proposed semantic or safety relaxation requires specification reopening, not coefficient adjustment. + +## Context and Hypothesis + +- `Hypothesis`: Correct selectors distinguish the charged temporal maximum from reserved close/fault contingencies and expose bounded current-topology pressure without implying removability. +- `Materiality`: Exact equality with production selectors; even a one-byte diagnostic mismatch fails the ledger. +- `Scope`: The semantic-classification extraction is source-derived cross-owner context, not additional research or acceptance claims owned by this Leaf. + +## Baseline and Candidates + +| ID | Architecture | Source tree | Runtime/config identity | +| --- | --- | --- | --- | +| Corrected ledger | Component-wise temporal charge and full admission | Source identities below and in extraction | Unchanged frozen C1 production selectors | +| Former diagnostics | Cadenced-only sum / admission-only V2 correction | Retained historical failure evidence below | Diagnostic defects, not changed runtime charging | + +## Controls + +- `Controlled variables`: Generated production Weight and runtime selector remain unchanged; native diagnostic corrections only. +- `Changed variables`: Ledger branch reconstruction and labels, not production coefficients. +- `Confounders`: Generated I/O is not executed invocation frequency; estimated proof is not recorded StorageProof; RefTime and ProofSize maxima have different temporal owners. + +## Benchmark Class, Workloads, and Criteria + +- `Benchmark class`: Native generated-Weight selector regression and inspected production selector evidence; not a new production-Wasm campaign. +- `Workloads`: Retained/removed Tick branches with complete close/fault contingencies, including genesis-anchor-only processing. +- `Metrics`: Generated reads/writes, RefTime, estimated ProofSize, charged/admitted equality. +- `Acceptance criteria`: Exact production selector agreement while preserving distinct charge/admission and evidence classes. +- `Rejection criteria`: Cadenced-only charged sum, arbitrary shared-proof allocation, unproved contingency removal or a source-inspected hypothesis presented as an isolated saving. + +## Environment and Artifact Identity + +| Artifact | Identity | +| --- | --- | +| Pre-fission source | `/tmp/deos-exp0066-prefission.md`, SHA-256 `973bb0dd5f63430da3bff5aa1cc74a6df35b22b48510f47c44abf7dd8519f923`, 170875 bytes, mode `100644`; lines 549–570, 675–690 and 755–756 | +| Migration provenance | [2026-09-08 fission manifest](./experiments.md#current-decision-critical-path) | +| Git baseline | Dirty atop commit `a8d9ec1a07fb7fdc383148d1dbd37c83874d62be`, tree `a1cb8c8fa19dd790af5032038a352bdfd2dc673d`; not a clean measured source tree | +| Historical baseline | Commit `5b46b19004b62a723d6c45a610dfe1e5c79982b0`, template tree `98af72ffbb55fe8840b5bb44b5b01d65c8e775e1` | +| Accepted binding source | EXP-0065 `c5345d6ba9574b9f16d0c62541400df027edd954039167f413f9b0aa9297c26d` | +| Production Wasm | `155749e22a6235570d2bb97d4a42b1327b86f4f5d82742df36494fca0fd88c1d`, unchanged preserved artifact; native selector checks do not remeasure it | +| Actors Weight | `20d56df225eca3ff52857f2170eccf50587c5ca17f997d18d083e61e8afbeed9` | +| Metadata | `75868ea75fd85e7b79db12650a7407e89e249f5564b2157d55ac46cd26cb5d12` | +| Toolchain/config | Rust `1.94.1`, reference 12/24/48, equal thirds; database/cache not separately recorded for these native checks | +| Benchmark / final release Wasm | No new artifact produced by extraction | +| Commands and raw evidence | Exact native command, source-file/selector hashes and failure-log identities retained below; no rerun during fission | + +## Proof Obligations + +- `Freeze`: Frozen via explicit 2026-09-08 pre-fission reconstruction; not retrospective original freeze. +- `Review triggers`: Independent temporal generated-Weight owner split from compound EXP-0066; cross-owner semantic context is not another owning claim. +- `Decomposition review`: Retained/removed branches and correction failures are bounded witnesses of one selector-accounting claim. Phase timing is [EXP-0078](./EXP-0078.md); raw key/root proof is [EXP-0080](./EXP-0080.md); Manual stops are [EXP-0081](./EXP-0081.md). + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | Temporal ledger exactly distinguishes generated charge from complete admission | One component mismatch with the production branch/unit selector | Native generated-Weight regression and source inspection | Self | Required | EXP-0066 O10 | Open; measured, no new acceptance | + +## Measurements + +Exact pre-fission EXP-0066 subsections follow. The “current table” includes the retained V3 correction. “Matched-phase proof” resolves to EXP-0078. The later “This accounting” refers to schedule owner-frequency/settlement accounting now owned by [EXP-0084](./EXP-0084.md), not a new campaign here. + + +### Temporal Control I/O and Admission-Envelope Attribution + +On source `6ef5d7e01b03f7085ab4046f93f0a98b44195081dae3e3df185c457ab892aca2`, the project-owned Control-attribution gate introduced the generated production-Weight read/write ledger. The current table incorporates the subsequent admission and charged-branch selector corrections. These are benchmark-accounted database operations and estimated ProofSize envelopes, not a claim that additive estimated proof equals the complete block's recorded StorageProof. + +| Owner | Accounted reads / writes | RefTime / estimated ProofSize | Classification | +| --- | ---: | ---: | --- | +| Materialization coordinator | `10 / 1` | `376,051,000 / 5,982` | Fixed every-Prepass coordination across wakeup/Crossing/fanout families | +| Two-clock no-due probes | `12 / 0` | `346,656,000 / 13,132` | Two `6 / 0` cursor probes for independent block/tick readiness; current implementation cost, not a universal minimum | +| Charged retained Tick branch | `48 / 29` | `4,796,399,000 / 16,410` | Partial consume plus temporal maximum; I/O follows its Cadenced RefTime owner, ProofSize follows AtTime | +| Charged removed Tick branch | `92 / 51` | `8,630,973,000 / 64,308` | Final-bucket consume plus the same component-wise temporal maximum | +| Cadenced-only rearm-topology delta over AtTime | `7 / 7` | Not separable by RefTime subtraction | Waiting occupancy/head/tail/frame, reverse index, cursor length and cursor page | +| Retained occurrence admission envelope | `115 / 95` | `13,902,523,000 / 99,799` | Physical retained branch plus component-wise temporal maximum and close/fault contingencies | +| Removed occurrence admission envelope | `159 / 117` | `17,737,097,000 / 147,697` | Physical removed branch plus the same temporal maximum and contingencies | + +The `7 / 7` Cadenced delta is the exact generated-storage difference from the one-shot AtTime occurrence (`20 / 9`) to Cadenced (`27 / 16`); it names the rearm topology rather than treating all temporal work as one opaque owner. The close (`66 / 65`, `81,886` proof) and fault (`1 / 1`, `1,503` proof) components are admission contingencies: the scheduler must prove the complete unit fits before mutation, but non-closing/non-faulting Tick units charge the physical wakeup branch plus the component-wise temporal maximum, including genesis-anchor-only processing without a useful occurrence. Thus the retained branch admits against 6.08 times its actual estimated ProofSize and the removed branch against 2.30 times its actual estimated ProofSize. This identifies conservative contingency reservation as a distinct measured pressure candidate; it does not establish that either contingency is removable or authorize a geometry change. + +Selector correction: the original diagnostic summed the Cadenced actual branch into admission and understated both ProofSize envelopes by one byte (`99,798` and `147,696`). Runtime `scheduler.rs::wakeup_cursor_drain_branch_weight` already selects component-wise maxima: Cadenced owns maximum RefTime, but AtTime owns `8,451` ProofSize versus Cadenced's `8,450`. `wakeup_cursor_drain_unit_weight_for` then adds close/fault contingencies. The current table uses that maximum for both charged branches and admission; the former V2 diagnostic corrected admission but still understated charged Tick-branch ProofSize by one byte. Production measurements and runtime charging remain unchanged. This is a diagnostic defect, not an undercharged runtime path. + +The earlier admission-only revision of `control_temporal_weight_io_ledger_matches_production_selectors` checked both reconstructed envelopes against `Actors::wakeup_cursor_drain_unit_weight_upper`. It failed before correction (`99,798 != 99,799`) and passed afterward; `EXP_0066_CONTROL_IO_LEDGER_V2` additionally reports the temporal occurrence admission ProofSize. Focused native evidence uses `DEOS_VERBOSE=1 RUST_TEST_NOCAPTURE=1 SKIP_WASM_BUILD=1 scripts/ci-local.sh --only tests --package deos-runtime --test-filter control_temporal_weight_io_ledger`. Output `/tmp/deos-0725-temporal-ledger-selector.log` has SHA-256 `4b7e993da3682a3bdbe8b4f96dc106f01cb6fba6d1f658758542a98d0b60fa88`. + +This selector check is native generated-Weight evidence, not a new production-Wasm campaign. Source-file SHA-256 is `00b2e69d819cddf4f8ee5cb057e46905efb65308de324c7735cc5f83e8440c13` for `template/runtime/src/tests/production_block_replay.rs`; the inspected production selector owner `template/pallets/actors/src/scheduler.rs` is `20a0188594958caf42b46ce5cb4177bb64193ca37575c979432f9c6455395f9b`, and generated Weight remains `20d56df225eca3ff52857f2170eccf50587c5ca17f997d18d083e61e8afbeed9`. The native phase-attribution fixture also passes. No production source, Weight or Wasm changed, so the closed matched-phase proof witnesses are reused rather than rerun. + + + +### Source-Derived Semantic Context + +This table preserves classification context only. Task/Transfer attribution belongs to [EXP-0082](./EXP-0082.md), baseline/no-progress accounting to [EXP-0083](./EXP-0083.md), schedule accounting to EXP-0084, signed-demand accounting to [EXP-0085](./EXP-0085.md), lifecycle accounting to [EXP-0086](./EXP-0086.md), raw shared/root proof to EXP-0080 and live Manual admission stops to EXP-0081. Governing specifications own required semantics. This Leaf owns only the temporal generated charge/admission rows and their limitations; no new cross-owner experiment is conducted here. + + +The semantic classification below links the already measured owners without pretending that raw-node ownership is execution-phase timing or a counterfactual removable cost. + +| Cost class | Evidence / implementation owner | Supported boundary | +| --- | --- | --- | +| Task effects | W0–W9 separate effect telemetry and typed successful/failed Task events | Economic effect Weight stays separate from Actor Control; ledger-account proof nodes can serve both and are not split arbitrarily | +| Required semantics | Useful Trigger latching, Pipeline/Step execution, Q1/cutoff, fee and lifecycle ownership in the governing specifications | These behaviors must survive any candidate; their mathematically minimal implementation cost has not been measured | +| Frozen-topology work | Ready/Waiting frame and locator ranking; generated physical wakeup retained/removed selectors | Necessary owners of the current C1 implementation, not proof that every read/write or byte is irreducible | +| Admission safety | `wakeup_cursor_drain_unit_weight_for`, corrected selector regression and close/fault envelopes | Complete pre-mutation capacity is required; successful settlement excludes unused contingencies, and any narrower selector needs its own reachable-path proof | +| Transient rearm | `process_due_temporal_occurrence_loaded` schedules the next Cadenced deadline before fee/activation handling; successful latching removes temporal detection | A concrete transient-work hypothesis, not proven removable overhead; underfunding/collection failure retains future readiness and must not lose it | +| Repeated activation preparation | The temporal owner preflights terminal outcomes before charging, then `request_activation_inner` reloads and preflights before commit | Repeated preparation is visible in source, but intervening state/fee changes preclude assuming the first plan remains reusable; no isolated cost saving is established | +| Shared/root proof | Reproduced storage-prefix partition and independent committed-root witness | Complete raw-node coverage, retaining cross-owner sharing; neither phase-exclusive cost nor invocation counts | + +The Cadenced rearm hypothesis is constrained by `tests/scheduling.rs::cadenced_latch_disables_detection_until_pipeline_opening`, `underfunded_cadenced_occurrence_advances_without_fee_readiness_or_apoptosis` and `cadenced_collection_failure_advances_deadline_without_readiness`. The generated `7 / 7` Cadenced-versus-AtTime difference does not isolate the cost of transient publication/deletion or repeated preflight. A future G8 comparison must preserve these branches, source/destination authority and complete admission; this inspection does not allocate a candidate or waive G7. + +The temporal selector ledger covers generated per-unit I/O and admission, not executed selector frequencies or database invocation counts for every W0–W9 block. Dividing root/key pairs by Steps would not fill that gap. Likewise, a fixture-wide next-attempt maximum gives a conservative headroom bound, not automatically the exact live-head deficit. These distinctions keep full resource-ledger closure open despite complete raw proof coverage. + + + +### Retained Settlement Correction + +The original “This accounting” points to EXP-0084's schedule accounting. The correction remains here because it decides this Leaf's charged selector, not a second schedule-throughput claim. + + +This accounting exposes the remaining V2 temporal diagnostic defect. `drain_overdue_wakeups_cursor_resuming` consumes `wakeup_cursor_drain_branch_weight` before processing the unit, and no refund replaces its temporal maximum with the Cadenced-only sum. The new guard failed with `4,796,399,000 / 16,409` versus `4,796,399,000 / 16,410`; `/tmp/deos-0725-temporal-settlement-red.log` has SHA-256 `1e0cbed209ed3cd5eafb61e664f1a6cbeaebc77881732836315af989ebdaa0a8`. `EXP_0066_CONTROL_IO_LEDGER_V3` fixes charged retained/removed ProofSize to 16,410/64,308, retains correct admission 99,799/147,697, and labels branch I/O by the RefTime selector. Earlier 16,409/64,307 values remain valid Cadenced component sums only, not charged Tick-branch totals. + + + +### Current EXP-0095 Refresh + +The generated-owner regression was rebound to Actors Weight SHA-256 `9ce37ade391a29d4f60779f1887ad242dfeb4919d25cf0047ffba85d18f33901` and executed with the EXP-0095 phase campaign recorded by EXP-0078. Current generated I/O remains `48 / 29` for the retained charged branch, `92 / 51` for removed, `115 / 95` for retained admission and `159 / 117` for removed admission. Current charged Weight is `4,872,038,000 / 16,410` retained and `8,742,441,000 / 64,308` removed; complete admission is `14,130,838,000 / 99,799` retained and `18,001,241,000 / 147,697` removed. The component-wise AtTime ProofSize maximum remains one byte above Cadenced and is selected without additive overlap. + +## Derived Metrics + +Retained 6.08/2.30 admission-to-charge proof ratios describe reservation pressure, not removable costs. The generated `7 / 7` rearm delta is not an isolated transient-operation saving. No new Pareto selection. + +## Statistical / Variance Notes + +Deterministic generated-Weight/selector equality; host latency distributions are inapplicable. Historical failing guards and diagnostic corrections remain distinct from production measurements. + +## Result + +Charged retained/removed ProofSize is 16,410/64,308; admission is 99,799/147,697. Cadenced owns RefTime and associated generated I/O, AtTime owns maximum temporal ProofSize. The one-byte discrepancies were diagnostic, not runtime undercharging. + +## Interpretation + +Complete admission conservatively includes contingencies absent from successful charge. Current-topology costs and inspected repeated preparation are not universal minima or isolated removable costs. Shared/root proof cannot supply invocation counts or phase-local stop deficits. + +## Decision + +Accepted for the bounded current temporal-selector accounting claim. The generated-owner regression exactly distinguishes charged branch Weight from complete admission contingencies on EXP-0095. It does not convert generated I/O into physical operations or establish removable cost. + +## Rejected Alternatives and Tradeoffs + +- `Rejected diagnostic`: Cadenced-only sums understate charged/admitted ProofSize by one byte where the component-wise AtTime maximum applies. +- `Rejected inference`: No runtime undercharge, raw-proof discount, arbitrary per-Step shared allocation or automatic contingency removal. +- `Unproven alternatives`: Transient rearm removal and reusable preflight need independent reachable-path evidence preserving underfunding, collection failure, authority and admission; G7/G8 remain governing gates. + +## Validity + +- `Establishes`: Generated temporal selector accounting on the exact inspected source/Weight and retained native regression evidence. +- `Does not establish`: Every block's executed owner frequencies, actual database invocation counts, phase-exclusive raw proof, minimum semantic cost or production acceptance. +- `Known limitations`: Cross-owner classification is source-derived context, not new measurement; later ledgers belong to their separate owners. +- `Invalidation triggers`: Selector logic, Weight, temporal/lifecycle semantics, runtime binding or diagnostic reconstruction changes. + +## Follow-Up / Next Gradient + +Temporal reservation pressure → generated owner distinction → reachable-path proof required before any narrower admission candidate. Stop this extraction; no optimization or new candidate is allocated. + +## Architecture Impact + +No production architecture change. EXP-0079 provides scoped generated charge/admission provenance; specifications retain semantic authority. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: [EXP-0066](./EXP-0066.md) O10. +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: Governing specifications and [EXP-0025](./EXP-0025.md), physical architecture only. +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: [EXP-0065](./EXP-0065.md), exact production binding; [EXP-0078](./EXP-0078.md), unchanged matched-phase witness; [EXP-0080](./EXP-0080.md), shared/root classification context; [EXP-0084](./EXP-0084.md), settlement correction context. +- `Transfers question to`: None. +- `Produces input for`: [EXP-0066](./EXP-0066.md), temporal generated charge/admission ownership. +- `Reopen trigger`: A changed production selector or failed component equality requires evidence refresh; cost alone does not reopen C1 and optimization requires its own admitted proof owner. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0080.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0080.md new file mode 100644 index 00000000..0c2cb083 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0080.md @@ -0,0 +1,269 @@ +# EXP-0080 — Matched-Phase StorageProof Node Coverage + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | [EXP-0066](./EXP-0066.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-08 | +| Affected domain | Matched-phase raw StorageProof node coverage | +| Physical mechanism | Root-scoped keyed paths and independent committed-root reconstruction | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0065](./EXP-0065.md) | +| Successor experiment | None | + +## Decision Question + +Do recorded key-path unions plus an independent committed-root witness cover every execution-proof node in the retained 18-block matched Manual/Cadenced cohort? + +## Governing Specification + +- `Owners`: `docs/actors-performance-assurance.specification.en.md` (production evidence); `docs/actors-resource-policy.specification.en.md` (independent resource dimensions); `template/pallets/actors/docs/specification.en.md` (scheduler invariants). +- `Invariant`: Preserve frozen C1, FIFO, Q1, cutoff and separate proof bytes from generated Weight and invocation counts. +- `Reopen trigger`: Demonstrated semantic contradiction requires specification review; raw proof sharing does not authorize a Weight discount. + +## Context and Hypothesis + +- `Current architecture`: SDK recorder paths share trie nodes; keyless execution nodes require independent coverage. +- `Reason`: Key counts and total proof size cannot explain node overlap or the remainder. +- `Hypothesis`: Root-scoped keyed paths plus committed-root reconstruction cover the complete execution-node set in the matched cohort. +- `Materiality`: Zero uncovered raw-node bytes in every retained block, not a throughput improvement. + +## Baseline and Candidates + +| ID | Architecture | Source tree | Runtime/config identity | +| --- | --- | --- | --- | +| Baseline | Existing production proof with recorded paths | Initial phase/I/O sources below | EXP-0065 bindings | +| A | Same execution; independent key-path and committed-root diagnostic reconstruction | Exact successive diagnostic source hashes in Measurements | Unchanged production Wasm/Weight/metadata | + +## Controls + +- `Controlled variables`: Matched Manual/Cadenced phase fixture, exact production bindings and proof database. +- `Changed variables`: Diagnostic reconstruction and owner classification only; no production mechanism change. +- `Confounders`: Cohorts perform different numbers of Steps and temporal materializations; shared nodes are not disjoint marginal costs. + +## Benchmark Class, Workloads, and Criteria + +- `Benchmark class`: Full-runtime production-Wasm proof diagnostics with independent SDK reconstruction and repeated normalized evidence. + +| Workload | State/population geometry | Hot/cold assumptions | Purpose | +| --- | --- | --- | --- | +| Matched phase cohort | 100 workload Actors each for Manual and cadence-one, blocks 2–10, one existential-deposit Transfer | Preserved full-state and compact-proof-only fixture; cache not separately reported | Complete raw-node coverage in 18 blocks | + +- `Metrics`: Root/key pairs, raw node sums/unions/sharing, owner partitions, execution boundary and committed-root remainder; raw bytes exclude SCALE/CompactProof framing. +- `Statistical method`: Retained repeated deterministic normalized rows and root equality, not noisy timing inference. +- `Acceptance criteria`: Every reconstructed node belongs to the execution proof, committed root matches accepted post-state root, and no node bytes remain outside the combined witness in all 18 blocks. Reconstructed explicitly during fission from prior repeated closure, not claimed as historical predeclaration. +- `Rejection criteria`: Missing proof, wrong root, uncovered nodes or treating overlap as exclusive execution cost. + +## Environment and Artifact Identity + +| Artifact | Identity | +| --- | --- | +| Source commit | Pre-fission dirty file atop `a8d9ec1a07fb7fdc383148d1dbd37c83874d62be`; original campaign baseline `5b46b19004b62a723d6c45a610dfe1e5c79982b0` | +| Source tree | Pre-fission baseline `a1cb8c8fa19dd790af5032038a352bdfd2dc673d`; original template `98af72ffbb55fe8840b5bb44b5b01d65c8e775e1` | +| Rust toolchain | `1.94.1` | +| Benchmark runtime Wasm | Not newly generated | +| Post-generation production runtime Wasm | `155749e22a6235570d2bb97d4a42b1327b86f4f5d82742df36494fca0fd88c1d` | +| Final release production runtime Wasm | Not attested by extraction | +| Generated Weight | Actors `20d56df225eca3ff52857f2170eccf50587c5ca17f997d18d083e61e8afbeed9` | +| Metadata | `75868ea75fd85e7b79db12650a7407e89e249f5564b2157d55ac46cd26cb5d12` | +| Accepted entry source | EXP-0065 `c5345d6ba9574b9f16d0c62541400df027edd954039167f413f9b0aa9297c26d` | +| Initial phase source | `6541ad342c2c48face4d2304fd5f3a34744417752175960c20c989f3a02c7d6f` | +| Initial I/O-ledger source | `6ef5d7e01b03f7085ab4046f93f0a98b44195081dae3e3df185c457ab892aca2` | +| Database/backend | SDK full-state and compact-proof replay; committed-root reconstruction uses execution-proof-only backend and artifact state version | +| Benchmark command/config | `DEOS_VERBOSE=1 scripts/actors-assurance.sh --integrated-control-attribution`; deterministic reference preset with 15 retained System identities (three active, twelve dormant), signer `[66; 32]`, Aura slots matching block, relay `max_pov_size` 5,000,000 bytes | +| Raw evidence | Complete decision-relevant tables and successive source/output SHA-256 identities below; no new runtime artifact or replay | + +## Proof Obligations + +- `Freeze`: Frozen via explicit 2026-09-08 pre-fission reconstruction, not a historical freeze. +- `Review triggers`: Independently reusable proof-coverage owner extracted from compound parent. +- `Decomposition review`: Key paths, storage-owner partition, boundary and committed-root reconstruction are bounded witnesses of the same complete raw-node coverage claim; phase-exclusive accounting and Weight remain outside this Leaf. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | Key paths plus independent committed-root witness cover every execution node in the retained matched cohort | One uncovered node or mismatched reconstruction root in a retained block | Repeated production-Wasm proof diagnostics and independent SDK reconstruction | Self | Required | EXP-0066 O11 raw-node coverage | Satisfied with scope: closed 18-block matched cohort only | + +## Measurements + +Pre-fission EXP-0066 **571–674**, inclusive (`proof-node-coverage`), from immutable `/tmp/deos-exp0066-prefission.md`, SHA-256 `973bb0dd5f63430da3bff5aa1cc74a6df35b22b48510f47c44abf7dd8519f923`, 170875 bytes, mode `100644`, dirty atop commit/tree above. [Migration provenance](./experiments.md#current-decision-critical-path) owns extraction coverage. This preserves prior repeated closure without remeasurement. + +Historical “above/below” refers to the pre-fission ordering: phase [EXP-0078](./EXP-0078.md), temporal [EXP-0079](./EXP-0079.md), proof [EXP-0080](./EXP-0080.md), stops [EXP-0081](./EXP-0081.md), Transfer [EXP-0082](./EXP-0082.md), baseline [EXP-0083](./EXP-0083.md), schedule [EXP-0084](./EXP-0084.md), signed [EXP-0085](./EXP-0085.md), lifecycle [EXP-0086](./EXP-0086.md). Intermediate “open” limitations inside the preserved historical sequence remain historical; the final committed-root witness closes only the stated raw-node remainder. + + +The same phase cohort was independently replayed again on source `d17beb80cb2501dd365ed1125299334e401522e546db5499127a04cd9fa27a59` with root-scoped recorder-key capture. `DEOS_VERBOSE=1 scripts/actors-assurance.sh --integrated-control-attribution` retained the same production Wasm, Weight and metadata identities and reproduced every recorded Step, occurrence and phase-Weight sequence. Full-state and compact-proof-only execution agreed on the complete `(trie root, key)` set after execution and root calculation in all 18 blocks. The helper regression `recorded_keys_retain_trie_root_identity` separately proves that repeated access deduplicates within one root without collapsing the same key under different roots. Raw command output SHA-256 is `0e81e69d7bf49b4fdc4598da34b499da3da6b3ccd25f6ef0e78c68a4de45e90d`; the complete decision-relevant per-block diagnostic is retained below. + +| Block | Manual root/key pairs | Cadenced root/key pairs | Manual StorageProof / CompactProof bytes | Cadenced StorageProof / CompactProof bytes | +| ---: | ---: | ---: | ---: | ---: | +| 2 | 192 | 195 | 32,933 / 30,453 | 28,584 / 26,366 | +| 3 | 200 | 206 | 40,002 / 37,109 | 39,752 / 37,435 | +| 4 | 199 | 195 | 36,870 / 33,223 | 41,470 / 38,424 | +| 5 | 200 | 202 | 40,692 / 37,058 | 48,844 / 45,712 | +| 6 | 199 | 207 | 34,687 / 31,076 | 40,708 / 37,585 | +| 7 | 120 | 199 | 23,089 / 19,340 | 42,778 / 39,393 | +| 8 | 59 | 180 | 6,558 / 4,364 | 32,292 / 28,110 | +| 9 | 59 | 199 | 6,588 / 4,394 | 36,534 / 33,168 | +| 10 | 59 | 208 | 6,668 / 4,442 | 35,164 / 31,248 | + +These are SDK recorder path identities, not read/write invocation counts or independent child-trie namespace identities: identical trie roots share content identity. Proof bytes include execution/root-calculation nodes, not only Actor Control. Capturing keys and total proof size alone does not attribute per-key node bytes, shared-node overlap, or removable work; the independent node-path measurement below addresses overlap separately. This repeat strengthens only the matched phase cohort, not whole-matrix reproducibility or production acceptance. + +The node-path diagnostic then used source `0cd9be35b9a00e3bcd6990bf64313f49d649c2b8890ca2b30f3ae1f63a23b9ce` and repeated on `1def5be1f23498a48a18581ba26ce730afa8b47eeafcb991092aac49bd86165b` after adding the iterator-remainder regression and byte-semantics comments only. Both commands were `DEOS_VERBOSE=1 scripts/actors-assurance.sh --integrated-control-attribution`; production Wasm, Weight and metadata remained unchanged. Raw output SHA-256 values are `b595865e50e16dce65ffec7bd89bcc282b0db8b0e297571816b9dc00c43002f8` and `34dac79d939089edc793f1f27c8d27dcd2382fc54e6fd99d01556a22a0be3458`. Every normalized key-proof, phase-attribution and comparison row was byte-identical across both runs. + +Each `(trie root, key)` path is independently reconstructed from the execution proof's own database, preserving the SDK-recorded hash-only versus value/absence access kind. Every recovered node must belong to that execution proof. `Per-key sum` counts node bytes in each independent path; `Union` counts each recovered node once; `Shared` counts once only nodes present in at least two paths; repeated appearances equal `Per-key sum - Union`. `Unassigned` is the execution-node byte remainder outside that union. These are raw node bytes, excluding SCALE framing and CompactProof encoding. No key-count, shared-byte or repeated-byte figure is a runtime read/write count, duplicate execution claim, or production Weight discount. + +| Schedule | Block | Per-key sum | Union | Shared | Unassigned | +| --- | ---: | ---: | ---: | ---: | ---: | +| Manual | 2 | 252,566 | 32,151 | 11,195 | 461 | +| Manual | 3 | 285,606 | 37,946 | 12,628 | 1,687 | +| Manual | 4 | 279,777 | 33,850 | 11,362 | 2,610 | +| Manual | 5 | 284,490 | 37,341 | 11,518 | 2,943 | +| Manual | 6 | 277,355 | 31,051 | 12,146 | 3,232 | +| Manual | 7 | 146,058 | 20,057 | 8,094 | 2,751 | +| Manual | 8 | 48,103 | 5,538 | 3,506 | 912 | +| Manual | 9 | 48,103 | 5,538 | 3,506 | 942 | +| Manual | 10 | 48,103 | 5,538 | 3,506 | 1,021 | +| Cadenced | 2 | 247,597 | 27,867 | 8,931 | 461 | +| Cadenced | 3 | 291,614 | 38,519 | 10,903 | 942 | +| Cadenced | 4 | 275,135 | 40,116 | 11,870 | 1,032 | +| Cadenced | 5 | 293,272 | 47,247 | 11,312 | 1,273 | +| Cadenced | 6 | 290,935 | 39,009 | 10,269 | 1,362 | +| Cadenced | 7 | 277,798 | 41,085 | 10,227 | 1,348 | +| Cadenced | 8 | 237,630 | 30,859 | 11,151 | 1,102 | +| Cadenced | 9 | 274,071 | 35,027 | 12,090 | 1,128 | +| Cadenced | 10 | 285,654 | 33,598 | 10,667 | 1,195 | + +Independent per-key path sums are 7.28–8.93 times their per-block unions for Manual and 6.21–8.88 times for Cadenced. This establishes material trie-path sharing inside actual execution proofs, not an equal throughput gain or removable generated admission charge. Unassigned execution nodes remain 461–3,232 bytes per block; the iterator-only unit witness proves that SDK-recorded nodes need not have a recorded key identity, but does not assign these integrated remainders to an iterator or root-calculation owner. The access-kind/shared-node regression also covers absent and inline values, external shared values, hash-only paths and repeated access. The storage-prefix partition below further narrows this result; phase/semantic attribution and wider-matrix reproducibility remain open. + +Storage-prefix attribution used source `86c208a54f03dd799c48663c76eb1e28a09cab33aa7d8dcd6ea2f353a7a5693f`, unchanged production identities and two executions of `DEOS_VERBOSE=1 scripts/actors-assurance.sh --integrated-control-attribution`. Raw output SHA-256 values are `b764f6e785a3ee714c79978be3896b72ed3ed04165e71248f187b96b7b399e8e` and `94c4c706b492b9d38d00c893c70d0e640999b070537ebf9c589affd7035bee5f`. Every normalized storage-owner, partition, key-overlap and phase row was identical between runs. The helper maps top-trie key prefixes through `AllPalletsWithSystem::storage_info()` and requires exactly one declared owner per matching prefix. Non-top, well-known and unmatched keys have explicit separate categories rather than inferred pallet ownership. + +`Exclusive` counts nodes reached by only one storage-owner category in that block, not a counterfactual removable cost. Cross-owner shared nodes remain a separate once-counted union. Each block asserts `sum(exclusive owner bytes) + cross-owner shared bytes = key-path union`, `key-path union + unassigned bytes = execution-node bytes`, and complete key-path coverage. The classification regression preserves repeated paths within one owner, cross-owner sharing, a same-prefix non-top key, well-known keys and unmapped top keys. No byte is assigned to an execution phase by storage prefix alone. + +The following table sums per-block observations across each nine-block cohort; it is not one cross-block proof. All owners with at least 1,024 exclusive bytes in either cohort are retained individually and sorted by maximum observed exclusive total. Each cohort's 56 smaller known owners are retained as an exact aggregate of their individual rows. `Other known owners` union is therefore a sum of owner unions, not a newly deduplicated group union; shared bytes for any row equal `Union - Exclusive` and cannot be summed as disjoint block cost. + +| Owner | Manual paths | Manual union | Manual exclusive | Cadenced paths | Cadenced union | Cadenced exclusive | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | +| Actors.ActorReadyFrameChunks | 17 | 68,650 | 59,142 | 19 | 86,342 | 76,764 | +| Actors.ActorWaitingFrameChunks | 2 | 1,446 | 426 | 25 | 76,942 | 67,420 | +| Actors.ActorContractHead | 106 | 31,522 | 25,253 | 161 | 48,559 | 39,158 | +| System.Account | 115 | 40,849 | 34,375 | 43 | 23,901 | 17,427 | +| Actors.ActorControlLocators | 106 | 26,319 | 20,596 | 161 | 41,879 | 33,068 | +| Actors.ActorFunding | 101 | 21,922 | 16,097 | 153 | 33,924 | 24,402 | +| Actors.ActorUnsignaledControlCells | 0 | 0 | 0 | 23 | 12,920 | 9,830 | +| Actors.ActorIdentities | 0 | 0 | 0 | 123 | 14,836 | 5,314 | +| Actors.ActorStateHolds | 100 | 9,664 | 3,276 | 152 | 14,492 | 4,914 | +| Actors.SovereignIndex | 6 | 10,864 | 4,284 | 5 | 9,079 | 3,479 | +| Assets.Asset | 6 | 5,064 | 1,860 | 5 | 4,220 | 1,550 | +| Actors.ActorWaitingTails | 2 | 970 | 52 | 22 | 9,923 | 1,319 | +| Actors.ActorWaitingHeads | 2 | 1,060 | 51 | 22 | 10,712 | 1,311 | +| Actors.ActorReadyHead | 9 | 9,643 | 186 | 9 | 10,617 | 1,218 | +| System.BlocksTillUpgrade | 9 | 7,527 | 1,053 | 9 | 7,527 | 1,053 | +| Actors.ActorWaitingOccupancies | 2 | 1,060 | 47 | 27 | 10,466 | 1,033 | +| Other known owners | 686 | 385,194 | 9,354 | 814 | 400,270 | 8,795 | +| WellKnownTop | 9 | 5,168 | 362 | 9 | 5,168 | 362 | +| XcmpQueue.InboundXcmpStatus | 9 | 5,313 | 0 | 9 | 5,313 | 0 | + +| Summed per-block partition | Manual bytes | Cadenced bytes | +| --- | ---: | ---: | +| Exclusive storage-owner nodes | 176,414 | 298,417 | +| Cross-owner shared nodes, counted once per block | 32,596 | 34,910 | +| Unassigned execution nodes | 16,559 | 9,843 | +| Complete execution-node bytes | 225,569 | 343,170 | + +Ready frames lead exclusive raw-node attribution in both cohorts; Waiting frames are the second-largest Cadenced owner and differ materially from the Manual witness. Contract heads, control locators, funding and ledger accounts remain separately visible. These cohorts perform different numbers of temporal materializations and Steps, so neither the storage totals nor their differences isolate a per-occurrence or per-Step marginal. Zero exclusive bytes also do not imply zero reads, Weight or execution cost. The initially unmatched top-trie key is resolved below and named in the table without changing its nine paths or 5,313 shared-only bytes per cohort; no non-top key was observed. The committed-root witness below closes the raw-node remainder for these phase cohorts; phase/semantic ledger closure and wider-matrix reproducibility remain open, without admitting a new geometry candidate. + +The boundary/identity witness first used source `c9f7663824695bdad100f21a8dcf7459a310b32caa6c4d0b358708fadcd016f7` and raw output SHA-256 `122216ac7458bbca84210e16b152c7ffbe62cfb0eb337322efb0f3bfcc74de42`, then resolved the key descriptor on source `abbcb7853f9c93810c4b2c44fe5f348af10a022b07704ec250ef38befb2d99de` with output SHA-256 `1be5600d1f2965f8afc92dea4a53f267ad8951920ea0a0d5acc817ddcc8ec36f`. Both used `DEOS_VERBOSE=1 scripts/actors-assurance.sh --integrated-control-attribution` and unchanged production identities. Normalizing only the resolved owner name makes every storage-owner, partition, key-overlap and phase row identical to the preceding repeated partition evidence. + +The previously unmatched key is `0x7b3237373ffdfeb1cab4222e3b520d6b345d8e88afa015075c945637c07e8f20`, exactly `storage_prefix("XcmpQueue", "InboundXcmpStatus")`. In pinned `cumulus-pallet-xcmp-queue 0.30.0`, `src/lib.rs::Hooks::on_idle` calls `src/migration/mod.rs::v3::lazy_migrate_inbound_queue`, whose first operation reads that private storage alias and returns when it is absent. The SDK Weight file also names this exact previously unknown key. `runtime_proof_storage_info_covers_upstream_idle_probe` pins the key derivation, unique diagnostic descriptor and absence in the fresh reference preset. This is existing upstream `on_idle` housekeeping, not Actor state, current public metadata or a new DEOS migration obligation. Unknown future keys remain visible as complete hex identities. + +A recorder-only snapshot immediately after successful `Core_execute_block` returns is a subset of the recorder after host post-root verification in all 18 blocks, with exactly zero newly added node bytes in every block. Therefore all 461–3,232 unassigned node bytes per block were already recorded inside `Core_execute_block`; post-return host root extraction explains none of them. This does not distinguish runtime-internal root calculation from other keyless trie traversal, assign them to one pallet, or authorize their removal. The final alias-resolved replay contains no unmatched top-trie key. The independent committed-root witness below resolves coverage of the internal execution-node remainder without assigning chronological first-touch ownership. + +Committed-root reconstruction used source `01ce39a89f80da1e41940429d0a38360feb0d4cf86e622e966374f04c42b1dad` and repeated on `e5a13687771fcf3cfb2f8604f67118100dcd4236402428afcc67d8051d64e6a1` after adding the matched-cohort zero-remainder assertion. Both executions used `DEOS_VERBOSE=1 scripts/actors-assurance.sh --integrated-control-attribution`, unchanged production identities and byte-identical normalized root-delta, storage-owner, partition, key-overlap and phase rows. Raw output SHA-256 values are `4371b7fc4311cb8edb0eeef9ca63b5c12a2f62a8712ccaec014d22bc300db393` and `a2ac9a2178150dff30bc3957c1fbfc34831633bb4e0ef9738f95dc3ebbe52c0d`. + +The helper retains the actual committed top/child storage deltas from full-state execution, creates a fresh backend containing only its execution proof, and independently computes `full_storage_root` with the artifact's state version. The resulting root must equal the accepted post-state root, and every recorded reconstruction node must already belong to the execution proof. The regression covers top updates, creation, deletion, a child delta, missing proof and a mismatched expected root. This reconstructs a deterministic SDK root-calculation witness; it does not rerun Actor effects or change the production Wasm. + +| Block | Manual committed-root node bytes | Cadenced committed-root node bytes | Remaining bytes outside key paths and root witness, both cohorts | +| ---: | ---: | ---: | ---: | +| 2 | 15,322 | 8,515 | 0 | +| 3 | 17,741 | 10,254 | 0 | +| 4 | 19,399 | 13,867 | 0 | +| 5 | 19,715 | 11,077 | 0 | +| 6 | 19,124 | 11,467 | 0 | +| 7 | 14,822 | 13,942 | 0 | +| 8 | 5,240 | 12,817 | 0 | +| 9 | 5,270 | 17,779 | 0 | +| 10 | 5,349 | 14,532 | 0 | + +Every byte previously labelled `Unassigned` is outside recorded key paths but covered by this committed-root witness: 16,559 Manual and 9,843 Cadenced bytes summed across their nine blocks. Thus, for each measured block, `execution nodes = key-path union ∪ committed-root witness`; the matched-phase gate now asserts zero remaining bytes. Root-witness bytes overlap keyed paths and must not be added wholesale to the earlier union. This closes raw proof-node coverage for these 18-block witnesses, not chronological first-touch attribution, a uniquely minimal proof theorem, a per-Actor byte allocation, a FRAME Weight refund or wider-workload coverage. The remaining G6 work is phase/semantic resource-ledger closure, wider-matrix reproducibility and service-gap/fairness evidence. + + + +## Derived Metrics + +Only historical path ratios and per-block partition totals retained above. No new numeric research, scalar performance score or marginal allocation. + +## Statistical / Variance Notes + +The preserved repeated runs have byte-identical normalized rows at the declared stages. This establishes matched-cohort repeatability only; no wider-matrix or timing conclusion. + +## Result + +Prior repeated committed-root reconstruction matched the accepted post-state roots and left zero raw-node bytes outside key paths plus root witness in all 18 retained blocks. The complete historical tables, hashes, intermediate remainders and limitations remain unchanged. + +## Interpretation + +The prior repeated evidence closes this bounded raw-node coverage claim. Key-path sharing and cross-owner/root overlap prevent treating sums as exclusive costs. Neither chronology, per-Actor allocation, universal Weight soundness nor wider production acceptance follows. No performance Pareto selection is made: this is diagnostic coverage of unchanged execution. + +## Decision + +EXP-0095 does not invalidate this record's explicitly historical 18-block raw-proof scope. It remains Accepted only for the retained EXP-0065 artifact and supplies no current-production proof-size claim; any current G6 proof claim requires fresh matched-phase reconstruction. + +Accepted only for the already closed 18-block matched-phase raw-node coverage, explicitly recognized during the 2026-09-08 pre-fission reconstruction. No new runtime baseline, production Weight acceptance, phase-exclusive cost claim or universal proof theorem is selected. This recognition does not reopen or rerun the closed witness. + +## Rejected Alternatives and Tradeoffs + +| Candidate | Decision | Evidence-backed reason | +| --- | --- | --- | +| Key counts alone as node attribution | Rejected inference | Shared paths and keyless execution remainder require independent reconstruction | +| Post-return host extraction as remainder owner | Rejected inference | Zero new node bytes after successful Core execution in every retained block | +| Add root witness wholesale or discount Weight by sharing | Rejected inference | Root/key overlap and raw-byte versus generated-Weight boundaries | +| Storage-prefix ownership as phase-exclusive/removable cost | Rejected inference | Shared owner nodes and non-counterfactual classification | + +## Validity + +- `Establishes`: Complete raw-node coverage and retained repeated partition evidence in the exact 18-block cohort. +- `Does not establish`: Universal Weight soundness, phase-exclusive attribution, physical invocation counts, minimal proof, removable overhead, throughput success or release acceptance. +- `Known limitations`: Identical roots share content identity; proof includes execution/root-calculation nodes; matched schedules perform differing work. +- `Invalidation triggers`: Changed proof method, SDK trie behavior, layout, artifact binding or workload outside this bounded domain. +- `Residual qualitative insight`: Shared and keyless trie work requires union-based coverage, not arbitrary division. + +## Follow-Up / Next Gradient + +- `Eliminated hypotheses`: Key count alone supplies complete attribution; post-return host root extraction explains the retained remainder. +- `Remaining hypotheses`: Phase/semantic resource attribution and wider evidence belong to independent owners. +- `Stop condition`: Closed matched raw-node coverage; no rerun solely for fission or more corner-case coverage. + +## Architecture Impact + +- `Affected physical truth`: Diagnostic proof coverage of unchanged C1 only. +- `Architecture update`: None from evidence relocation; no production optimization selected. +- `Provenance citation`: EXP-0080, scoped 18-block raw-node coverage. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: [EXP-0066](./EXP-0066.md) O11. +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: None. +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: [EXP-0065](./EXP-0065.md) unchanged exact production binding; [EXP-0078](./EXP-0078.md) matched phase cohort. +- `Transfers question to`: None. +- `Produces input for`: [EXP-0066](./EXP-0066.md) closed matched-phase raw-node coverage. +- `Reopen trigger`: A concrete uncovered execution node, wrong reconstructed root or changed identity invalidating this bounded witness; cost alone does not reopen frozen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0081.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0081.md new file mode 100644 index 00000000..51d51fff --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0081.md @@ -0,0 +1,212 @@ +# EXP-0081 — Manual Service Stop Reconstruction + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | [EXP-0066](./EXP-0066.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-08 | +| Affected domain | Manual phase-local service eligibility and Control stops | +| Physical mechanism | Live-head ordinary gate and phase-meter reconstruction | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0065](./EXP-0065.md) | +| Successor experiment | None | + +## Decision Question + +Can the bounded Manual cohort's ordinary Control stops be reconstructed from actual eligible live heads and phase-local budgets rather than finalized headroom alone? + +## Governing Specification + +- `Owners`: `template/pallets/actors/docs/specification.en.md` (placement and scheduler); `docs/actors-resource-policy.specification.en.md` (component-wise admission/actual settlement); `docs/actors-performance-assurance.specification.en.md` (evidence). +- `Invariant`: Preserve FIFO, cutoff, Q1, causal floor and complete admission. +- `Reopen trigger`: Evidence of a semantic conflict, not a cost miss. + +## Context and Hypothesis + +- `Current architecture`: Frozen C1 uses one live FIFO head and separate Prepass/idle meters. +- `Reason`: Finalized headroom alone cannot identify a historical stop. +- `Hypothesis`: Actual eligible Manual heads and source-bound phase budgets reconstruct the ordinary Control rejection in the retained nine-block cohort. +- `Materiality`: Distinguish capacity failure from service ineligibility without extrapolation. + +## Baseline and Candidates + +| ID | Architecture | Source tree | Runtime/config identity | +| --- | --- | --- | --- | +| Baseline | Frozen C1; finalized-head diagnostic | Historical file hashes in Measurements; pre-fission dirty baseline below | Unchanged EXP-0065 bindings | +| A | Same runtime; native prefix/head and meter reconstruction | Producer hashes in Measurements | Same bindings; no production candidate | + +## Controls + +- `Controlled variables`: Frozen runtime, generated Weight, metadata and existing Manual cohort. +- `Changed variables`: Native observational boundary and source-bound meter reconstruction only. +- `Confounders`: Native prefix/state evidence is not Wasm host instrumentation; exceptional and User paths differ. + +## Benchmark Class, Workloads, and Criteria + +- `Benchmark class`: Native full-runtime diagnostic with reuse of exact-production-Wasm cohort evidence. + +| Workload | State/population geometry | Hot/cold assumptions | Purpose | +| --- | --- | --- | --- | +| Frontier counterexample | Empty and three 128-System-Actor block-2 fixtures | Not separately reported | Falsify finalized deficit as sole stop cause | +| Manual phase stops | 100 Actors, nine blocks | Preserved source fixture | Reconstruct actual ordinary gates | + +- `Metrics`: Head/ticket/cutoff/eligibility, phase Control/effect capacity and deficits. +- `Statistical method`: Deterministic reconstruction; no timing inference. +- `Acceptance criteria`: Actual eligible head, exact phase budget and complete attempt expression agree within the bounded cohort; reconstructed during fission, not a claimed historical predeclaration. +- `Rejection criteria`: Ineligible head treated as capacity-only rejection or final remainder substituted for a phase stop. + +## Environment and Artifact Identity + +| Artifact | Identity | +| --- | --- | +| Source commit | Pre-fission dirty file atop `a8d9ec1a07fb7fdc383148d1dbd37c83874d62be`; original campaign baseline `5b46b19004b62a723d6c45a610dfe1e5c79982b0` | +| Source tree | Pre-fission baseline `a1cb8c8fa19dd790af5032038a352bdfd2dc673d`; original template `98af72ffbb55fe8840b5bb44b5b01d65c8e775e1` | +| Rust toolchain | `1.94.1` | +| Benchmark runtime Wasm | Not newly generated | +| Post-generation production runtime Wasm | `155749e22a6235570d2bb97d4a42b1327b86f4f5d82742df36494fca0fd88c1d` | +| Final release production runtime Wasm | Not attested by this extraction | +| Generated Weight | Actors `20d56df225eca3ff52857f2170eccf50587c5ca17f997d18d083e61e8afbeed9` | +| Metadata | `75868ea75fd85e7b79db12650a7407e89e249f5564b2157d55ac46cd26cb5d12` | +| Initial phase / I/O sources | `6541ad342c2c48face4d2304fd5f3a34744417752175960c20c989f3a02c7d6f` / `6ef5d7e01b03f7085ab4046f93f0a98b44195081dae3e3df185c457ab892aca2` | +| Database/backend | Preserved full-state/proof replay fixture; cache not separately reported | +| Benchmark command/config | Exact commands retained below; reference preset retains 15 System identities (three active, twelve dormant), signer `[66; 32]`, Aura slots matching block, relay `max_pov_size` 5,000,000 bytes | +| Raw evidence | Verbatim ranges below retain log paths/digests; logs not copied or rerun | + +## Proof Obligations + +- `Freeze`: Frozen via explicit 2026-09-08 pre-fission reconstruction, not a historical freeze. +- `Review triggers`: Independent stop/eligibility evidence separated from compound parent. +- `Decomposition review`: Both ranges test the same phase-stop reconstruction claim; the counterexample bounds the Manual conclusion. Other owners remain separate. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | The bounded Manual ordinary stops can be reconstructed from actual eligible heads and phase budgets | One mismatched head, eligibility or stage budget | Native prefix/state plus source-bound meter reconstruction | Self | Required | EXP-0066 O12 stop attribution | Open; measured, not automatically accepted | + +## Measurements + +Pre-fission EXP-0066 source ranges **691–706** (`frontier-counterexample`) and **761–787** (`manual-phase-stops`), inclusive, from immutable `/tmp/deos-exp0066-prefission.md`, SHA-256 `973bb0dd5f63430da3bff5aa1cc74a6df35b22b48510f47c44abf7dd8519f923`, 170875 bytes, mode `100644`, dirty atop commit/tree recorded above. [Migration provenance](./experiments.md#current-decision-critical-path) owns coverage. Extraction moves evidence without remeasurement. + +Historical “above/below” references inside verbatim payloads use the pre-fission ordering: phase [EXP-0078](./EXP-0078.md), temporal [EXP-0079](./EXP-0079.md), proof [EXP-0080](./EXP-0080.md), stops [EXP-0081](./EXP-0081.md), Transfer [EXP-0082](./EXP-0082.md), baseline [EXP-0083](./EXP-0083.md), schedule [EXP-0084](./EXP-0084.md), signed [EXP-0085](./EXP-0085.md), lifecycle [EXP-0086](./EXP-0086.md). + + +The native regression `full_executive_control_frontier_separates_capacity_from_service_eligibility` now reads the existing canonical Ready cell, physical ticket, frozen cutoff and finalized Control remainder from cloned post-state outside execution. An empty control plus three 128-System-Actor first-block fixtures pass via `RUST_TEST_NOCAPTURE=1 DEOS_VERBOSE=1 SKIP_WASM_BUILD=1 scripts/ci-local.sh --only tests --package deos-runtime --test-filter full_executive_control_frontier`. Log `/tmp/deos-0725-control-frontier.log` has SHA-256 `47ac073ca50cbe0b1c386067889418e840375bdd83cc8a528b6b69953945adc4`. This is native diagnostic evidence, not a new exact-Wasm campaign or a historical stop trace. + +The producer `production_block_replay.rs` has file SHA-256 `4f2f17ae970fd66ae6befef7b272dd7bcda7184b3792e6422f38f86ccbeddddd`; fixture/maximum helper `actors_integration_tests.rs` has `a5bfb9b104e943e5022481e3f39d72a0862ba930e663257dfb151d9b66dd8df9`. The inspected production `scheduler.rs` remains `20a0188594958caf42b46ce5cb4177bb64193ca37575c979432f9c6455395f9b`, with unchanged Weight binding above. These are file identities, not a replacement full-tree production attestation. + +| Native block 2 / 128 System Actors | Steps | Live Actor / ticket / cutoff | Stored eligible-at / pre-cutoff? | Final Control remainder RefTime / ProofSize | Final ordinary-gate deficit RefTime / ProofSize | +| --- | --- | --- | --- | --- | --- | +| Manual-only | 16 | `31 / 16 / 128` | `1 / yes` | `268,289,421,986 / 91,582` | `0 / 35,481` | +| Cadenced-only | 0 | `15 / 0 / 0` | `2 / no` | `201,577,937,522 / 46,898` | `0 / 80,165` | +| Mixed Manual/Cadenced | 0 | `15 / 0 / 64` | `1 / yes` | `201,074,563,522 / 16,686` | `0 / 110,377` | + +Each observed head carries `3,113,843,536 / 40,059` stored Control. The exact ordinary precheck expression in `service_live_queue_entry` adds the component-wise maximum of preserve/delete FIFO consume plus complete close cleanup, yielding `12,677,308,536 / 127,063`, equal to the fixture maximum in these three snapshots. The regression checks physical-location/ticket and eligibility consistency and matching effect envelopes. This equality is proved for these heads, not assumed for every workload, reference Actor or lifecycle branch. + +The Cadenced witness falsifies equating `eligible_at <= block` with service eligibility: tick materialization has already made the head due in block 2, but ticket 0 is outside cutoff 0. The causal floor is enforced by that cutoff; a later stored eligible-at value is not required. Both zero-Step-workload blocks have a positive final capacity deficit, yet only Mixed has a pre-cutoff head. Neither is a zero-Step Contract completion. A deficit is not proof that capacity was the sole cause of no progress. + +`live_queue_head` spends discovery before `service_live_queue_entry` spends the state probe and reaches the ordinary capacity precheck. Exceptional/lifecycle paths have additional classification, and finalization follows both Prepass and idle work. Thus the table's component-wise final deficit is exact for the named stored-head expression at the named snapshot, not the missing amount at the historical stop, not successful settlement, and not a complete discovery-plus-attempt quote. Existing `controlBoundBlocks` counters compare fixture maxima with finalized remainders; retain their numerical evidence under that narrower definition. Phase-local owner frequencies, probes, remainder boundaries and generated I/O/Step normalization remain open. Closed raw proof and full campaigns were not rerun for this diagnostic counterexample. + + + + +The Manual phase-stop witness now adds actual native prefix heads to the ledger. `full_executive_manual_phase_control_stops_use_live_head_and_stage_budget` authors the same 100-Actor/nine-block Manual fixture, then replays only each block's three exact inherents on a disposable cloned pre-state. It verifies the prefix Control/effect usage against the authoring boundary before reading the head, and compares that head with a separate finalized-state snapshot. Observation reads never enter the original block's recorder or precede its subsequent execution. This is native prefix/state evidence plus source-bound meter reconstruction, not new Wasm host instrumentation. + +The native route is `RUST_TEST_NOCAPTURE=1 DEOS_VERBOSE=1 SKIP_WASM_BUILD=1 scripts/ci-local.sh --only tests --package deos-runtime --test-filter full_executive_manual_phase_control_stops`. Producer file SHA-256 is `a3e04fa6c72beb068d2c8166e3a5a32ad774afc2bb0aa8f2e76b31266fe75560`; log `/tmp/deos-0725-manual-phase-stops.log` has SHA-256 `6191e8f2084b4e924adba7f2b46aa26c87d904a88ba8d4179fa189422445b059`. Production bindings remain unchanged, and the existing exact-Wasm Manual phase records are reused rather than executed again. + +| Block | Successful Steps | Live head Actor / ticket | Cutoff / stored eligible-at | +| --- | --- | --- | --- | +| 2 | 16 | `31 / 16` | `100 / 1` | +| 3 | 19 | `50 / 35` | `100 / 1` | +| 4 | 19 | `69 / 54` | `100 / 1` | +| 5 | 19 | `88 / 73` | `100 / 1` | +| 6 | 19 | `107 / 92` | `100 / 1` | +| 7 | 8 | None | FIFO empty in both snapshots | +| 8–10 | 0 each | None | FIFO empty in both snapshots | + +Every live head is the next unserved fixture Actor and remains identical across Prepass and finalization. The check pins System authority, Idle/latched state, nonce zero, zero failure streak, no pause/terminal/window/auto-close condition, a one-Step Persistent Manual Contract, and identical head/cell resource envelopes. Thus the ordinary Opening precheck applies, its ticket is pre-cutoff and its block eligibility is satisfied. `pipeline_capacity_sufficient_with_envelope` returns true for System authority, so capacity rejection takes the ordinary deferred branch without a User-admission alternative. + +`execute_mandatory_prepass` withholds `scheduler_on_idle_base + block_resource_finalize = 627,727,000 / 3,120` before creating its pass meter. No further meter charge follows the terminal ordinary gate in `execute_cycle_to_cutoff_inner`. Therefore its remaining Control at that gate is `Control limit - captured Prepass usage - withheld idle/finalization`. Idle settles that fixed work before its pass and makes one discovery/state probe at the same head; its terminal Control remainder is `Control limit - final Control usage`. Their difference is exactly `560,561,027 / 18,217`, not zero. + +| Blocks | Prepass Control remainder RefTime / ProofSize | Idle Control remainder RefTime / ProofSize | Prepass deficit RefTime / ProofSize | Idle deficit RefTime / ProofSize | +| --- | --- | --- | --- | --- | +| 2 | `268,849,983,013 / 109,799` | `268,289,421,986 / 91,582` | `0 / 17,264` | `0 / 35,481` | +| 3–6 | `273,137,585,350 / 104,040` | `272,577,024,323 / 85,823` | `0 / 23,023` | `0 / 41,240` | + +The required Control expression at all five heads is `12,677,308,536 / 127,063`, reconstructed from each actual stored envelope plus consume and cleanup. Prepass effect capacity and its combined Control-plus-effect meter both fit the complete next attempt; Control ProofSize alone fails among these budget checks. In idle, the positive Control deficit is independently sufficient to reject the ordinary attempt; no claim is made that other idle-hook/block limits were independently measured. Blocks 7–10 have no head deficit rather than a fabricated next-attempt failure. + +This closes phase-local Control stop reconstruction for the bounded Manual cohort. It does not generalize to W1/W2's entire 100-block horizon, post-cutoff temporal heads, User admission/apoptosis, suspended/terminal alternatives, external Actor mutation or other Task shapes. It also does not make a repeated probe removable: those other paths can change service eligibility or the head between phases. The original finalized-headroom warning remains applicable outside this now-verified scope. + + + +### Current EXP-0095 Refresh + +Current generated Weight and the 100-Actor Manual fixture were rerun through the canonical focused routes. The frontier counterexample log `/tmp/deos-0725-exp0081-current-frontier.log` has SHA-256 `65d9ecaa34cc483595e03846a220b114f92a150d84d6f8fc0f668c849f3df45f`; the phase-stop log `/tmp/deos-0725-exp0081-current-stops.log` has SHA-256 `b339c5da7db362ebf5a2330601aa8e6eafe6e95dadc5fd6f1c575562b8a035bd`. + +Current Manual service is `[14,17,17,17,17,17,1,0,0]`. Six phase-local stops occur after blocks 2–7 at next live Actors/tickets `29/14`, `46/31`, `63/48`, `80/65`, `97/82` and `114/99`, all before cutoff 100 and eligible since block 1. The complete next Control requirement is `12,924,472,858 / 127,063`. Prepass/idle ProofSize deficits are `12,086 / 30,303` after block 2 and `28,621 / 46,838` after blocks 3–7; RefTime fits, as do effect and combined capacity. Block 8 commits the final Step and blocks 8–10 have no fabricated head deficit. Idle/finalization reservation is `628,636,000 / 3,120`. + +## Derived Metrics + +Only the historical source-derived deficits and meter equations above are retained; no new numeric research. + +## Statistical / Variance Notes + +Deterministic head and meter reconstruction; no wall-clock distribution or new execution. + +## Result + +The retained native evidence reconstructs eligible ordinary Manual heads and both service-phase Control remainders. The counterexample distinguishes due time from cutoff eligibility. + +## Interpretation + +The historical bounded closure is preserved as measured evidence, not promoted to general stop attribution. Control ProofSize fails in the specified ordinary heads; unrelated idle limits were not independently measured. + +## Decision + +Accepted for the bounded current Manual stop-reconstruction claim. The EXP-0095 refresh identifies six actual eligible live heads and their phase-local Control ProofSize deficits without substituting finalized headroom. Exceptional, User, temporal and other Task stops remain outside scope. + +## Rejected Alternatives and Tradeoffs + +| Candidate | Decision | Evidence-backed reason | +| --- | --- | --- | +| Finalized deficit as sole service-stop cause | Rejected inference | Cadenced due head is post-cutoff; phase remainders differ | +| Universal Manual-stop extrapolation | Not admitted | Other horizons, authority and lifecycle branches are unmeasured | + +## Validity + +- `Establishes`: Recorded bounded native reconstruction and its exact source/artifact identities. +- `Does not establish`: Universal stop cause, complete idle bottleneck attribution, or removable probes. +- `Known limitations`: Exact-Wasm final-state evidence is reused; native phase snapshots are not Wasm traces. +- `Invalidation triggers`: Head semantics, meter order, Weight or fixture changes. +- `Residual qualitative insight`: Capacity and service eligibility require separate evidence. + +## Follow-Up / Next Gradient + +- `Eliminated hypotheses`: Finalized deficits alone identify historical stops. +- `Remaining hypotheses`: Other cohorts need their own earned phase-stop evidence. +- `Stop condition`: This extraction is complete; broader work belongs to parent obligations. + +## Architecture Impact + +- `Affected physical truth`: Ordinary gate and phase-meter attribution only. +- `Architecture update`: None from evidence relocation. +- `Provenance citation`: EXP-0081. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: [EXP-0066](./EXP-0066.md) O12. +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: None. +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: [EXP-0065](./EXP-0065.md) exact production binding; [EXP-0078](./EXP-0078.md) retained phase cohort; [EXP-0082](./EXP-0082.md) successful Step ledger; [EXP-0083](./EXP-0083.md) baseline ledger. +- `Transfers question to`: None. +- `Produces input for`: [EXP-0066](./EXP-0066.md) bounded Manual service-stop attribution. +- `Reopen trigger`: A changed ordinary-gate, eligibility or phase-meter assumption evidenced in the bounded cohort; no cost-only reopening of C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0082.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0082.md new file mode 100644 index 00000000..f6005ef0 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0082.md @@ -0,0 +1,194 @@ +# EXP-0082 — Successful System Transfer Charge Ownership + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | [EXP-0066](./EXP-0066.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-08 | +| Affected domain | Successful System Transfer incremental resource ledger | +| Physical mechanism | Charged Control owner frequencies and separate Task effects | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0065](./EXP-0065.md) | +| Successor experiment | None | + +## Decision Question + +Does the first-block successful unpredicated one-Step Persistent System Transfer ledger close exactly as baseline plus successful-Step and no-progress probe terms, with Task effects separate? + +## Governing Specification + +- `Owners`: `template/pallets/actors/docs/specification.en.md` (Pipeline/Action and scheduler ownership); `docs/actors-resource-policy.specification.en.md` (admission and actual settlement). +- `Invariant`: Preserve complete admission, actual settlement, separate Control/effects and fee owners. +- `Reopen trigger`: A demonstrated conflict with these semantics, not an accounting cost miss. + +## Context and Hypothesis + +- `Current architecture`: Frozen C1 settles consume plus returned actual Step Control. +- `Reason`: Generated owner frequencies must not be confused with physical I/O or admission alternatives. +- `Hypothesis`: The bounded successful-System-Transfer increments close both final and native Prepass equations. +- `Materiality`: Exact owner accounting without arbitrary baseline allocation. + +## Baseline and Candidates + +| ID | Architecture | Source tree | Runtime/config identity | +| --- | --- | --- | --- | +| Baseline | Empty workload with retained reference topology | Original campaign baseline below | EXP-0065 binding | +| A | Same runtime, successful-System-Transfer accounting | Source `c723c6819726868f915384a6156dd525d895f6d9cd5c84e7d76bed2cd7fac75c` | Unchanged production binding | + +## Controls + +- `Controlled variables`: Production Weight/Wasm/metadata and reference topology. +- `Changed variables`: One or 128 workload Actors and valid user demand as bounded witnesses of the same ledger. +- `Confounders`: Native phase boundary is not a Wasm host trace; generated I/O is not backend invocation counting. + +## Benchmark Class, Workloads, and Criteria + +- `Benchmark class`: Native preparation and full-Executive production-Wasm first-block evidence. + +| Workload | State/population geometry | Hot/cold assumptions | Purpose | +| --- | --- | --- | --- | +| First-block Transfer ledger | Empty, one System Actor, 128 System Actors, same frontier with 77 valid user calls | Preserved fixture; not independently profiled | Exact incremental ownership | + +- `Metrics`: Successful Steps/Cycles, Control and effect Weight, charged frequencies, generated reads/writes. +- `Statistical method`: Deterministic equality; identical retained ledger records, no new timing analysis. +- `Acceptance criteria`: Exact final and Prepass equations with explicit baseline and no-progress terms; reconstructed from source during fission, not historical predeclaration. +- `Rejection criteria`: Mixing Task effects, admission alternatives or physical I/O with charged Control frequencies. + +## Environment and Artifact Identity + +| Artifact | Identity | +| --- | --- | +| Source commit | Pre-fission dirty file atop `a8d9ec1a07fb7fdc383148d1dbd37c83874d62be`; original baseline `5b46b19004b62a723d6c45a610dfe1e5c79982b0` | +| Source tree | Pre-fission baseline `a1cb8c8fa19dd790af5032038a352bdfd2dc673d`; original template `98af72ffbb55fe8840b5bb44b5b01d65c8e775e1` | +| Rust toolchain | `1.94.1` | +| Benchmark runtime Wasm | Not newly generated | +| Post-generation production runtime Wasm | `155749e22a6235570d2bb97d4a42b1327b86f4f5d82742df36494fca0fd88c1d` | +| Final release production runtime Wasm | Not attested by extraction | +| Generated Weight | Actors `20d56df225eca3ff52857f2170eccf50587c5ca17f997d18d083e61e8afbeed9` | +| Metadata | `75868ea75fd85e7b79db12650a7407e89e249f5564b2157d55ac46cd26cb5d12` | +| Accepted entry source | EXP-0065 `c5345d6ba9574b9f16d0c62541400df027edd954039167f413f9b0aa9297c26d` | +| Database/backend | Preserved full-state/compact-proof replay; cache not separately reported | +| Benchmark command/config | `DEOS_VERBOSE=1 scripts/actors-assurance.sh --integrated-w0-w1`; 15 retained System identities (three active, twelve dormant), signer `[66; 32]`, Aura slots matching block, relay `max_pov_size` 5,000,000 bytes | +| Raw evidence | Source/file/log hashes and exact evidence retained below; no new execution | + +## Proof Obligations + +- `Freeze`: Frozen via explicit 2026-09-08 pre-fission reconstruction, not a historical freeze. +- `Review triggers`: Independently decidable charge-owner ledger extracted from compound parent. +- `Decomposition review`: Only first-block successful System Transfer increments; baseline decomposition, phase stops and other authority/Task paths remain separate. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | The bounded successful System Transfer increments close both ledger equations with separate effects | One mismatch of observed Weight and charged-owner sum | Native phase boundary and production-Wasm final replay | Self | Required | EXP-0066 O13 resource attribution | Open; measured, not automatically accepted | + +## Measurements + +Pre-fission EXP-0066 **707–735**, inclusive (`transfer-ledger`), from immutable `/tmp/deos-exp0066-prefission.md`, SHA-256 `973bb0dd5f63430da3bff5aa1cc74a6df35b22b48510f47c44abf7dd8519f923`, 170875 bytes, mode `100644`, dirty atop commit/tree above. [Migration provenance](./experiments.md#current-decision-critical-path) owns coverage. No remeasurement. + +Historical “above/below” refers to the pre-fission ordering: phase [EXP-0078](./EXP-0078.md), temporal [EXP-0079](./EXP-0079.md), proof [EXP-0080](./EXP-0080.md), stops [EXP-0081](./EXP-0081.md), Transfer [EXP-0082](./EXP-0082.md), baseline [EXP-0083](./EXP-0083.md), schedule [EXP-0084](./EXP-0084.md), signed [EXP-0085](./EXP-0085.md), lifecycle [EXP-0086](./EXP-0086.md). + + +The successful-System-Transfer delta ledger adds a narrower executed-path result without runtime instrumentation. `assert_successful_transfer_resource_ledger` runs inside the existing W0/W1 preparation gate, using source `c723c6819726868f915384a6156dd525d895f6d9cd5c84e7d76bed2cd7fac75c` and unchanged production bindings. `DEOS_VERBOSE=1 scripts/actors-assurance.sh --integrated-w0-w1` passes native preparation and four exact-production-Wasm blocks. Log `/tmp/deos-0725-transfer-resource-ledger.log` has SHA-256 `ec951c0826b5343347aa1d7a54088a85937a5e3711468a0bde266b9d6386065a`; both `ACTOR_TRANSFER_RESOURCE_LEDGER_V1` records are identical, and prior W0/W1 preparation totals reproduce. The closed 18-block matched-phase raw-proof witness was not rerun. + +For these unpredicated, one-Step Persistent System Transfers, `RuntimeStepControlWeight::base_actual_control_weight` selects `scheduler_inner_opening_complete_min(0)`, with a separate invocation receipt and no User Action collection. The scheduler settles the consume envelope plus returned actual Step Control, not the prospective close/continuation maximum. Discovery and the state probe precede that settlement. `actor_config.rs` has SHA-256 `e2d1efb3276363f1e7e8352c55be2214880df12c41cc70f2751fc26d4fcfda82`; `service_live_queue_entry`, `live_queue_head` and the unchanged generated Weight file own the remaining mappings. + +| Charged owner | RefTime / estimated ProofSize per selection | Generated reads / writes | Incremental frequency over empty-workload block | +| --- | --- | --- | --- | +| `scheduler_paged_tombstone_drain(1)` / discovery | `308,874,027 / 3,111` | `4 / 2` | `n` | +| `scheduler_actor_state_probe` | `251,687,000 / 15,106` | `7 / 0` | `n + p` | +| FIFO consume envelope | `591,769,000 / 5,118` | `5 / 4` | `n` | +| `scheduler_inner_opening_complete_min(0)` | `406,682,194 / 4,398` | `2 / 3` | `n` | +| `action_invocation_receipt` | `4,330,000 / 0` | `0 / 0` | `n` | +| Task effect: `task_transfer` | `2,241,750,000 / 29,222` | `25 / 12` | `n`, separately metered | + +Here `n` is the observed successful Transfer/Completed-Cycle count and `p` counts additional state probes that do not commit a Step. The consume reservation uses a component-wise preserve/delete maximum; preserve dominates both dimensions in this binding, and both generated variants carry `5 / 4` I/O. The frequency is of the charged envelope, not a claim that every physical consume preserves its page. Generated component tests verify the recorded arithmetic against the production DbWeight and methods. + +Each successful Step contributes `1,563,342,221 / 27,733` Control and `18 / 9` generated Control I/O, separately from `2,241,750,000 / 29,222` effect and `25 / 12` generated effect I/O. These are incremental benchmark-accounted quantities, not physical database invocation counts, unique keys or proof nodes. Admission-only alternatives are not added as executed operations; repeated calls that merely compute a maximum are not charged-owner frequencies. + +| First-block witness | `n / p` | Control delta RefTime / ProofSize | Generated Control delta reads / writes | Effect RefTime / ProofSize | Generated effect reads / writes | +| --- | --- | --- | --- | --- | --- | +| One System Actor | `1 / 0` | `1,563,342,221 / 27,733` | `18 / 9` | `2,241,750,000 / 29,222` | `25 / 12` | +| 128 System Actors, Actor-only | `16 / 2` | `25,516,849,536 / 473,940` | `302 / 144` | `35,868,000,000 / 467,552` | `400 / 192` | +| Same frontier, 77 valid user calls | `16 / 2` | `25,516,849,536 / 473,940` | `302 / 144` | `35,868,000,000 / 467,552` | `400 / 192` | + +The empty-workload block retains `11,320,185,054 / 110,628` total Control, zero workload Steps/Cycles and zero effect. Its selected reference lifecycle-event counts are empty and remain identical in the paired fixtures. This is a measured context baseline with 15 reference identities, not a zero-Step Contract completion or proof that coordination does no work. The baseline decomposition below now classifies its generated I/O; it remains separate from successful-Step increments and is never divided by zero. + +Both the final and native phase-capture equations close exactly: `final Control = baseline final Control + n * Step Control + p * state probe`; `Prepass Control = baseline Prepass Control + n * Step Control + (p / 2) * state probe`. All workload Steps occur in Prepass. The two retained-frontier probes add `503,374,000 / 30,212` and `14 / 0` generated I/O without progress, one in each service phase. Task effect independently equals `n * task_transfer`. Full exact-Wasm replay validates final state/events; phase-specific usage comes from the native authoring boundary, not a new Wasm host trace. + +This closes the first-block successful-System-Transfer incremental ledger, with explicit baseline and no-progress terms. It does not allocate baseline costs, settle historical stop-point remainders, classify every temporal/lifecycle path or extrapolate to User fees, other Tasks, retained Runs or all 100 campaign blocks. The full residual ledger remains open; no admission discount, removable-owner claim or optimization follows from this decomposition. + + + +### Current EXP-0095 Refresh + +The current W0/W1 gate recorded two identical `ACTOR_TRANSFER_RESOURCE_LEDGER_V1` witnesses in `/tmp/deos-0725-exp0070-current-w0.log` (SHA-256 `a17bf9c1752401b57aa4acfc7f8660f32687736328345901cab5e893d6f3b932`) against generated Actors Weight SHA-256 `9ce37ade391a29d4f60779f1887ad242dfeb4919d25cf0047ffba85d18f33901` and exact production Wasm SHA-256 `25b9695fd9e900f17ae1f3fb0b815ac1403830264d9c31f7cee54e29f434b700`. + +The current inclusive opening-completion owner is `scheduler_inner_opening_user_complete_header_max`, not the historical System minimum. One successful Step contributes `2,127,545,374 / 31,325` Control with generated I/O `25 / 11`, independently from Transfer effect `2,293,433,000 / 29,222` and `25 / 12` I/O. The one-Step witness closes exactly; both 14-Step Actor-only and continuous-user witnesses close at Control delta `30,365,415,236 / 468,762`, including two non-progress state probes totaling `579,780,000 / 30,212`. Baseline remains separately owned by EXP-0083. + +## Derived Metrics + +The retained per-Step and witness totals above are historical calculations only; no new numeric research. + +## Statistical / Variance Notes + +Both retained ledger records agree; no broader repeatability or timing claim. + +## Result + +Historical final and Prepass equations close for the specified first-block System Transfer witnesses, retaining baseline, probes and separate effects. + +## Interpretation + +This is benchmark-accounted incremental ownership, not physical I/O or universal System/User coverage. Historical closure language remains scoped evidence, not a new acceptance decision. + +## Decision + +Accepted for the bounded current successful-System-Transfer ledger. Current baseline, inclusive completion, consume, probe and receipt owners close final and Prepass equations while Task effects remain separate. This does not establish User fee or other Task/lifecycle coverage. + +## Rejected Alternatives and Tradeoffs + +| Candidate | Decision | Evidence-backed reason | +| --- | --- | --- | +| Counting admission alternatives as executed owners | Rejected inference | Actual settlement and selected frequencies differ from maximum computation | +| Per-Actor baseline allocation or universal fee extrapolation | Not admitted | Baseline and User fees lie outside this incremental witness | + +## Validity + +- `Establishes`: Exact retained first-block incremental equations. +- `Does not establish`: User fee coverage, all Tasks/lifecycle paths, physical I/O or removable overhead. +- `Known limitations`: Native phase capture; bounded successful System witness. +- `Invalidation triggers`: Binding, settlement selector, Task semantics or fixture changes. +- `Residual qualitative insight`: Baseline, progress and no-progress accounting need separate terms. + +## Follow-Up / Next Gradient + +- `Eliminated hypotheses`: Admission-only alternatives should be added as executed operations. +- `Remaining hypotheses`: Other authority and Task ledgers remain separately owned. +- `Stop condition`: Reuse this bounded measured input; no closed proof rerun from relocation. + +## Architecture Impact + +- `Affected physical truth`: Existing charge ownership only. +- `Architecture update`: None from relocation. +- `Provenance citation`: EXP-0082. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: [EXP-0066](./EXP-0066.md) O13. +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: None. +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: [EXP-0065](./EXP-0065.md) unchanged exact production binding. +- `Transfers question to`: None. +- `Produces input for`: [EXP-0066](./EXP-0066.md) bounded successful-System-Transfer charge ledger. +- `Reopen trigger`: A demonstrated mismatch in this exact ledger or changed selector/binding; cost alone does not reopen frozen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0083.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0083.md new file mode 100644 index 00000000..f86ee924 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0083.md @@ -0,0 +1,184 @@ +# EXP-0083 — No-Due and Genesis Control Baseline + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | [EXP-0066](./EXP-0066.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-08 | +| Affected domain | Empty-workload Control accounting | +| Physical mechanism | No-due coordinator owners and deferred genesis clock initialization | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0065](./EXP-0065.md) | +| Successor experiment | None | + +## Decision Question + +Can the retained empty-workload Control baseline be decomposed exactly into no-due charged owners plus a one-time genesis-clock term without calling either a completed Cycle? + +## Governing Specification + +- `Owners`: `template/pallets/actors/docs/specification.en.md` (Trigger/lifecycle and scheduling); `docs/actors-resource-policy.specification.en.md` (charged admission owners). +- `Invariant`: Preserve useful-readiness semantics and complete temporal admission; no fictitious progress or refunds. +- `Reopen trigger`: Demonstrated semantic conflict rather than diagnostic accounting correction. + +## Context and Hypothesis + +- `Current architecture`: Frozen C1 coordinates no-due blocks and initializes a deferred Fee Sink genesis clock. +- `Reason`: Empty workload is neither zero coordination cost nor zero-Step Contract completion. +- `Hypothesis`: No-due owners plus the startup term exactly reproduce the bounded baseline. +- `Materiality`: Keep startup, steady no-due and useful progress independently accountable. + +## Baseline and Candidates + +| ID | Architecture | Source tree | Runtime/config identity | +| --- | --- | --- | --- | +| Baseline | Existing empty-workload production context | Original campaign baseline below | EXP-0065 binding | +| A | Same runtime, explicit baseline ledger | Producer `4f9bf9bb399e9fcab38e4e06c0100ef52abee8407dac56e0996d2a5a33b96113` | Unchanged production binding | + +## Controls + +- `Controlled variables`: Production runtime/Weight, reference identities and retained drained Manual proof cohort. +- `Changed variables`: No-due versus first-block deferred genesis initialization; diagnostic temporal selector correction only. +- `Confounders`: Generated I/O uses the declared RefTime selector; it is not physical backend I/O. + +## Benchmark Class, Workloads, and Criteria + +- `Benchmark class`: Native full-Executive baseline accounting with retained exact-Wasm cohort comparison. + +| Workload | State/population geometry | Hot/cold assumptions | Purpose | +| --- | --- | --- | --- | +| Empty workload | Reference identities at initial block 2 and no-due block 3; drained Manual blocks 8–10 | Preserved fixture; cache not separately reported | Separate startup from no-due baseline | + +- `Metrics`: Control Weight, generated owner frequencies/I/O, native phase subtotals and reference lifecycle state. +- `Statistical method`: Deterministic accounting equality, not latency sampling. +- `Acceptance criteria`: Exact no-due and initial-baseline equations with no false readiness/Cycle classification; reconstructed during fission, not historical predeclaration. +- `Rejection criteria`: Repeating startup cost across every block, claiming a universal idle minimum, or treating component sum as charged maximum. + +## Environment and Artifact Identity + +| Artifact | Identity | +| --- | --- | +| Source commit | Pre-fission dirty file atop `a8d9ec1a07fb7fdc383148d1dbd37c83874d62be`; original baseline `5b46b19004b62a723d6c45a610dfe1e5c79982b0` | +| Source tree | Pre-fission baseline `a1cb8c8fa19dd790af5032038a352bdfd2dc673d`; original template `98af72ffbb55fe8840b5bb44b5b01d65c8e775e1` | +| Rust toolchain | `1.94.1` | +| Benchmark runtime Wasm | Not newly generated | +| Post-generation production runtime Wasm | `155749e22a6235570d2bb97d4a42b1327b86f4f5d82742df36494fca0fd88c1d` | +| Final release production runtime Wasm | Not attested by extraction | +| Generated Weight | Actors `20d56df225eca3ff52857f2170eccf50587c5ca17f997d18d083e61e8afbeed9` | +| Metadata | `75868ea75fd85e7b79db12650a7407e89e249f5564b2157d55ac46cd26cb5d12` | +| Initial phase / I/O sources | `6541ad342c2c48face4d2304fd5f3a34744417752175960c20c989f3a02c7d6f` / `6ef5d7e01b03f7085ab4046f93f0a98b44195081dae3e3df185c457ab892aca2` | +| Database/backend | Preserved full-state/proof fixture; no independent cache characterization | +| Benchmark command/config | Exact native command below; deterministic reference preset retains 15 System identities (three active, twelve dormant), signer `[66; 32]`, Aura slots matching block, relay `max_pov_size` 5,000,000 bytes | +| Raw evidence | Historical source/log identities retained verbatim below; no new execution | + +## Proof Obligations + +- `Freeze`: Frozen via explicit 2026-09-08 pre-fission reconstruction, not a historical freeze. +- `Review triggers`: Independently reusable baseline owner extracted from compound parent. +- `Decomposition review`: No-due and genesis are bounded witnesses of one empty-workload decomposition; subsequent whole-cohort normalization belongs to its separate schedule owner. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | No-due owners plus the one-time genesis term exactly explain the bounded empty-workload baseline | One baseline subtotal mismatch or false lifecycle classification | Native accounting with retained production-Wasm comparison | Self | Required | EXP-0066 O14 baseline attribution | Open; measured, not automatically accepted | + +## Measurements + +Pre-fission EXP-0066 **736–754**, inclusive (`baseline-ledger`), from immutable `/tmp/deos-exp0066-prefission.md`, SHA-256 `973bb0dd5f63430da3bff5aa1cc74a6df35b22b48510f47c44abf7dd8519f923`, 170875 bytes, mode `100644`, dirty atop commit/tree above. [Migration provenance](./experiments.md#current-decision-critical-path) owns coverage. No remeasurement. + +Historical “above/below” refers to pre-fission ordering: phase [EXP-0078](./EXP-0078.md), temporal [EXP-0079](./EXP-0079.md), proof [EXP-0080](./EXP-0080.md), stops [EXP-0081](./EXP-0081.md), Transfer [EXP-0082](./EXP-0082.md), baseline [EXP-0083](./EXP-0083.md), schedule [EXP-0084](./EXP-0084.md), signed [EXP-0085](./EXP-0085.md), lifecycle [EXP-0086](./EXP-0086.md). + + +The empty-workload baseline is now decomposed by `full_executive_empty_workload_control_baseline_has_explicit_owners`. It passes through `RUST_TEST_NOCAPTURE=1 DEOS_VERBOSE=1 SKIP_WASM_BUILD=1 scripts/ci-local.sh --only tests --package deos-runtime --test-filter full_executive_empty_workload_control_baseline`, also invoking the corrected temporal-component regression. Producer file SHA-256 is `4f9bf9bb399e9fcab38e4e06c0100ef52abee8407dac56e0996d2a5a33b96113`; log `/tmp/deos-0725-control-baseline.log` has SHA-256 `6a912132cdf7144017d98bebcfd63b3d7791ad194dba36c819ab06845a925913`. Production source, Weight and Wasm remain unchanged. + +| No-due baseline owner | Selections per block | Generated reads / writes per selection | RefTime / ProofSize per selection | +| --- | --- | --- | --- | +| Cutoff capture | 1 | `2 / 2` | `260,965,000 / 1,560` | +| Materialization coordinator | 1 | `10 / 1` | `376,051,000 / 5,982` | +| Clock cursor probe | 2 | `6 / 0` | `173,328,000 / 6,566` | +| Crossing base | 1 | `2 / 0` | `57,124,000 / 1,543` | +| Observation fanout base | 1 | `2 / 0` | `56,285,000 / 1,629` | +| Empty FIFO discovery | 2, one per service phase | `4 / 2` | `308,874,027 / 3,111` | +| Idle base | 1 | `7 / 2` | `394,136,000 / 1,560` | +| Resource finalization | 1 | `1 / 2` | `233,591,000 / 1,560` | + +Their sum is `2,342,556,054 / 33,188` and `44 / 11` generated I/O, independently matching native empty-workload block 3 and retained exact-Wasm Manual blocks 8–10 after the workload drained. The native Prepass subtotal is `1,405,955,027 / 26,957`; idle contributes `936,601,027 / 6,231`. Both dimensions of both subtotals match those closed production-cohort records without replaying them. These are charged generated owners, not unique backend accesses or a universal idle minimum. + +First measured block 2 additionally initializes the Fee Sink System Actor's deferred genesis clock. Actor 1 starts with `Cadenced { anchor_tick: None }` and a `Tick(0)` primary. At timestamp 12,000 ms, `process_due_temporal_occurrence_loaded` sets anchor tick 24 and moves its deadline to 144 (24 plus the configured 120 ticks), then returns before useful-readiness handling. It remains unlatched, unqueued and without a Run; no selected lifecycle event or Cycle occurs. Native block 3 retains that state. This is real genesis-anchor work, not stale-reference cleanup, an actual Trigger occurrence or a zero-Step Contract completion. + +The first-block addition is one removed Tick-branch charge plus two extra cursor probes: `8,977,629,000 / 77,440`, with `104 / 51` RefTime-selected generated I/O. Thus the full initial baseline is `11,320,185,054 / 110,628` and `148 / 62` generated I/O. Its Prepass subtotal is `10,383,584,027 / 104,397`, while idle stays unchanged. The charged temporal maximum applies even though no useful readiness is produced. Consequently this startup baseline must not be repeated mechanically across all campaign blocks. + + + +### Current EXP-0095 Refresh + +The baseline ledger was rerun against generated Actors Weight SHA-256 `9ce37ade391a29d4f60779f1887ad242dfeb4919d25cf0047ffba85d18f33901`. Command: `RUST_TEST_NOCAPTURE=1 DEOS_VERBOSE=1 SKIP_WASM_BUILD=1 ./scripts/ci-local.sh --only tests --package deos-runtime --test-filter full_executive_empty_workload_control_baseline`. Raw log `/tmp/deos-0725-exp0083-current-baseline.log` has SHA-256 `0127acd8df0ec162bb951ffe6f0f89879965db26bb4bfd8b0837bb2f12d64ff0`. + +The current no-due sum remains `2,400,465,748 / 33,188` with generated I/O `44 / 11`; Prepass is `1,436,166,374 / 26,957`. The one-time Fee Sink genesis addition is `9,091,051,000 / 77,440` with `104 / 51` generated I/O, producing initial total `11,491,516,748 / 110,628` and initial Prepass `10,527,217,374 / 104,397`. The Actor remains unlatched with no Run or Cycle, and the deadline advances from tick 0 to 144 after anchor tick 24. The current generated component regression passes exactly. + +## Derived Metrics + +Historical baseline and phase sums above only; no new numeric research or cohort projection. + +## Statistical / Variance Notes + +Deterministic generated-owner arithmetic and native state assertions; retained exact-Wasm comparisons were not rerun. + +## Result + +The retained no-due and initial baseline equations close; initial Fee Sink clock work produces no readiness or Cycle. The related temporal diagnostic correction is owned by [EXP-0079](./EXP-0079.md), not this payload. + +## Interpretation + +Current-topology accounting is not a universal idle minimum. The startup addition is not recurring. The temporal correction belongs to charged-selector semantics, not a production Weight change. + +## Decision + +Accepted for the bounded current empty-workload decomposition. Current generated owners exactly reconstruct steady no-due and one-time genesis-clock Control without classifying either as useful readiness or a Cycle. This does not establish physical database I/O or a universal idle minimum. + +## Rejected Alternatives and Tradeoffs + +| Candidate | Decision | Evidence-backed reason | +| --- | --- | --- | +| Useful readiness as prerequisite for temporal charge | Rejected inference | The complete temporal maximum applies during genesis anchoring without useful readiness | +| Recurring startup or zero-Step completion classification | Rejected inference | Deferred clock initialization produces no readiness, Run or Cycle | + +## Validity + +- `Establishes`: Bounded existing baseline charge decomposition and recorded correction. +- `Does not establish`: Universal idle minimum, actual backend I/O, removable coordination or wider campaign ownership. +- `Known limitations`: Generated selector I/O and native phase state, not new Wasm trace. +- `Invalidation triggers`: Reference genesis, temporal selector, coordinator or Weight changes. +- `Residual qualitative insight`: Separate startup from steady baseline and progress. + +## Follow-Up / Next Gradient + +- `Eliminated hypotheses`: No workload means no coordination work; startup repeats every block. +- `Remaining hypotheses`: Wider resource ledgers remain separately owned. +- `Stop condition`: Reuse this baseline within its unchanged domain; do not rerun closed proof because of fission. + +## Architecture Impact + +- `Affected physical truth`: Existing no-due and genesis charge owners. +- `Architecture update`: None from relocation. +- `Provenance citation`: EXP-0083. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: [EXP-0066](./EXP-0066.md) O14. +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: None. +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: [EXP-0065](./EXP-0065.md) unchanged production binding; [EXP-0078](./EXP-0078.md) drained Manual phase records; [EXP-0079](./EXP-0079.md) temporal selector ledger. +- `Transfers question to`: None. +- `Produces input for`: [EXP-0066](./EXP-0066.md) no-due/genesis baseline accounting. +- `Reopen trigger`: An evidenced mismatch or changed baseline/selector assumption; no cost-only reopening of C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0084.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0084.md new file mode 100644 index 00000000..3a418d0c --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0084.md @@ -0,0 +1,275 @@ +# EXP-0084 — System Schedule Charged-Owner Accounting + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | [EXP-0066](./EXP-0066.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-08 | +| Affected domain | System schedule Control and generated-I/O accounting | +| Physical mechanism | Charged-owner equations for Manual, Cadenced and Mixed System schedules | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0065](./EXP-0065.md) | +| Successor experiment | None | + +## Decision Question + +Do source-bound charged-owner equations reconstruct the retained System schedule cohorts' complete Control and generated I/O, keeping temporal work, successful Steps, baseline and no-progress work distinct? + +## Governing Specification + +- `Owners`: `template/pallets/actors/docs/specification.en.md` (Trigger latching, scheduler and lifecycle ownership); `docs/actors-resource-policy.specification.en.md` (component-wise admission and settlement); `docs/actors-performance-assurance.specification.en.md` (production evidence). +- `Invariant`: Frozen C1 class-neutral FIFO, ticket chronology/cutoff, Q1, causal floor, complete-owner accounting and separate Task effects. +- `Reopen trigger`: Evidenced semantic conflict, not cost or stale evidence alone. + +## Context and Hypothesis + +- `Current architecture`: Native full-block authors and restricted public-worker replays reconstruct generated charges against retained production phase totals and headers. +- `Hypothesis`: The schedule-specific owner frequencies exactly explain each retained cohort without allocating shared work to individual Actors or equating generated I/O with physical calls. +- `Materiality`: A mismatching owner equation, occurrence order, phase total or required header falsifies scoped accounting; no new schedule selector or optimization is proposed. + +## Baseline and Candidates + +| ID | Architecture | Source tree | Runtime/config identity | +| --- | --- | --- | --- | +| Baseline | Retained W1/W2 and nine-block phase cohorts | Initial W1 Actor-only `5045db07983dc501641816c5fa6221ff081b4d98d00d6d76261da74a37003950`; W2 `a1390ec4dfd7406674c6bf98a2c52c81d0e1a90f14c12654bc973a7ea992c044` | EXP-0065 exact production binding | +| A | Source-bound charged-owner reconciliation | Initial phase `6541ad342c2c48face4d2304fd5f3a34744417752175960c20c989f3a02c7d6f`; I/O ledger `6ef5d7e01b03f7085ab4046f93f0a98b44195081dae3e3df185c457ab892aca2`; later producer hashes below | Same frozen C1; test-side observations only | + +## Controls + +- `Controlled variables`: Accepted code/Weight/metadata, retained System identities, workload schedule, native-author occurrence sequence and exact retained phase/header evidence. +- `Changed variables`: Test-side accounting and schedule witness; no production owner or instrumentation changed. +- `Confounders`: Small Cadenced bucket drains while full-population bucket does not; restricted workers exclude nonempty Crossing/fanout, stale entries, cleanup pressure and scan-cap exhaustion. + +## Benchmark Class, Workloads, and Criteria + +- `Benchmark class`: Native source-bound accounting with retained exact-production-Wasm phase/header comparisons; not an instrumented Wasm coordinator trace. +- `Workloads`: Nine-block 100-Actor Manual/Cadenced cohorts and 100-block System Manual/Mixed/Cadenced campaigns. Large fixtures retain 15 reference identities and add 9,985 Actors, blocks 2–101; small fixtures use existential-deposit Transfers, blocks 2–10. +- `Configuration`: Rust `1.94.1`, signer seed `[66; 32]`, Aura slots matching block, 5,000,000-byte relay `max_pov_size`; reference 12/24/48 geometry and equal thirds remain unchanged. +- `Metrics`: RefTime, estimated ProofSize, generated reads/writes, processed units, removed buckets, probes/refusals, successful Steps, head/cutoff/eligibility and reference terminal state. +- `Statistical method`: Exact owner equations and retained distributions, not noisy wall-clock inference or a fresh benchmark sweep. +- `Acceptance criteria`: Equation, occurrence-order and retained phase/header agreement within each declared cohort and its exclusions. +- `Rejection criteria`: Physical-I/O/per-Actor attribution from generated charges, small-to-large extrapolation, or production/optimization acceptance from accounting alone. + +## Environment and Artifact Identity + +| Artifact | Identity | +| --- | --- | +| Pre-fission source | `/tmp/deos-exp0066-prefission.md`, SHA-256 `973bb0dd5f63430da3bff5aa1cc74a6df35b22b48510f47c44abf7dd8519f923`, 170875 bytes, mode `100644`; lines 757–760 and 788–897 inclusive | +| Git baseline | Dirty atop commit `a8d9ec1a07fb7fdc383148d1dbd37c83874d62be`, tree `a1cb8c8fa19dd790af5032038a352bdfd2dc673d`; not a clean measured-source claim | +| Migration provenance | [2026-09-08 fission manifest](./experiments.md#current-decision-critical-path) | +| Accepted production source | `c5345d6ba9574b9f16d0c62541400df027edd954039167f413f9b0aa9297c26d` | +| Rust toolchain | `1.94.1` | +| Benchmark runtime Wasm | Not used for reconstruction | +| Post-generation production runtime Wasm | `155749e22a6235570d2bb97d4a42b1327b86f4f5d82742df36494fca0fd88c1d` | +| Final release production runtime Wasm | Not established by fission | +| Generated Actors Weight | `20d56df225eca3ff52857f2170eccf50587c5ca17f997d18d083e61e8afbeed9` | +| Metadata | `75868ea75fd85e7b79db12650a7407e89e249f5564b2157d55ac46cd26cb5d12` | +| Database/backend | Native test externalities; generated I/O is not physical database-call evidence | +| Initial commands | `scripts/actors-assurance.sh --integrated-w1-actor-only`; `scripts/actors-assurance.sh --integrated-w2-schedules` | +| Reconstruction commands / raw evidence | Exact historical commands, producer identities and log digests below; no reexecution or newly asserted raw-log availability | + +## Proof Obligations + +- `Freeze`: Frozen via explicit 2026-09-08 pre-fission reconstruction from the immutable source above; not a historical freeze claim. +- `Review triggers`: Independent charged-owner accounting separated from phase/proof/service evidence and signed-state linkage. +- `Decomposition review`: Manual/Cadenced/Mixed are bounded witnesses of the same System schedule accounting claim. Signed linkage transfers to EXP-0085, lifecycle accounting to EXP-0086, phase-local Manual stops to EXP-0081. No additional profile is admitted. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | System schedule owner frequencies reconstruct retained Control and generated I/O without conflating progress and shared work | One retained cohort with mismatched owner equation, occurrence order or phase/header identity | Native source-bound accounting linked to retained production-Wasm evidence | Self | Required | EXP-0066 O15 schedule accounting | Satisfied with scope: measured System schedule cohorts only; no acceptance promotion | + +## Measurements + +Verbatim pre-fission EXP-0066 Measurements passages, lines 757–760 and 788–897. Historical “resulting ledger” and “baseline” refer to [baseline owners](./EXP-0083.md) and [Transfer](./EXP-0082.md); “following witness” is [Manual stops](./EXP-0081.md), not the next paragraph here. Historical phase/raw-proof material belongs to [phase](./EXP-0078.md), [temporal](./EXP-0079.md), [proof](./EXP-0080.md); W1/W2 retained service evidence belongs to [EXP-0070](./EXP-0070.md). Later signed-demand and lifecycle continuations are [EXP-0085](./EXP-0085.md) and [EXP-0086](./EXP-0086.md), with W5 timeline in [EXP-0073](./EXP-0073.md). “Above”, “below” and historical remaining-work claims inside the extraction retain original locality/evidence horizon; they do not broaden this Leaf. + + +The resulting ledger also closes the entire retained nine-block Manual cohort algebraically, not by a new execution. Reuse `/tmp/deos-0725-committed-root-proof-repeat.log` with SHA-256 `a2ac9a2178150dff30bc3957c1fbfc34831633bb4e0ef9738f95dc3ebbe52c0d`. For block index `i`, take its recorded successful Steps `n_i` and `p_i = 2` while the 100-Actor frontier remains, otherwise zero. Every recorded final and Prepass Weight matches the corresponding no-due baseline plus the startup addition only at block 2, plus `n_i` successful-Step units and respectively `p_i` or `p_i / 2` state probes. All 100 Steps occur in Prepass, and ten additional state probes make no progress. + +Across those nine blocks, total Control is `188,911,725,586 / 3,300,492`. Generated Control I/O is `9 * (44 / 11) + (104 / 51) + 100 * (18 / 9) + 10 * (7 / 0) = 2,370 / 1,050`. Whole-cohort normalization is 23.7 reads and 10.5 writes per committed Step, explicitly including baseline and three zero-progress blocks; it is an aggregate accounting ratio, not per-Actor causal allocation or physical I/O. Task effects stay separate. Temporal/Crossing/fanout activity and other Task/lifecycle shapes still require their own path accounting. The following witness resolves Manual phase-local stops only. + + + + +The Cadenced path ledger now closes the retained nine-block Tick-only cohort through a restricted composition of real public workers. The original producer `full_executive_cadenced_ledger_counts_processed_units_and_refusals` (now generalized as `full_executive_schedule_ledger_counts_processed_units_and_refusals`) authors the full blocks, separately replays their parachain/timestamp prefix, and invokes `Actors::drain_overdue_wakeups_cursor` with budgets reconstructed from the published family minimum envelopes. Crossing/fanout must be empty and fault-free; Ready cleanup/capacity pressure, stale entries, closing and reaching the shared wakeup scan cap are excluded by assertions. Resetting the public wrapper's counters between calls is equivalent here only because their sum stays below that cap. Only restricted test-side composition was added; production scheduling and instrumentation are unchanged. + +Every public-worker pass must match its charged-owner equation in both Weight dimensions, and the full native author's useful occurrence sequence must match exactly, not merely by count. The reconstructed total and Prepass Control then match the author. The same Steps, occurrence counts, final Control and Prepass Control also match every retained exact-Wasm Cadenced row in `/tmp/deos-0725-committed-root-proof-repeat.log`; those executions and raw-proof partitions were reused, not rerun. + +The captured nine-block run used `RUST_TEST_NOCAPTURE=1 DEOS_VERBOSE=1 SKIP_WASM_BUILD=1 scripts/ci-local.sh --only tests --package deos-runtime --test-filter full_executive_cadenced_ledger`; current reruns use the generalized schedule-ledger route documented below. Producer file SHA-256 is `77d1ef2ff67b622fbee4f3cb31c1a92ad562f65dcb80c0c43a38c3d7d43792b3`; log `/tmp/deos-0725-cadenced-path-ledger.log` has SHA-256 `a795227871e7afe1510dc4914ec30da387edc383ec3c9da8b87695ab1df4551f`. Production source, Weight, Wasm and metadata remain unchanged. `ACTOR_CADENCED_PATH_LEDGER_V1` is native worker-replay evidence, not a Wasm coordinator trace or a general model for nonempty peer families. + +| Block / starting family | Processed units per Tick pass | Removed source buckets per pass | Clock probes per pass | Admission refusals per pass | Successful Steps / no-progress FIFO state probes | Generated Control reads / writes | +| --- | --- | --- | --- | --- | --- | --- | +| 2 / Tick | `[1,16]` | `[1,0]` | `[4,34]` | `[1,1]` | `0 / 0` | `1,216 / 584` | +| 3 / Crossing | `[19]` | `[0]` | `[40]` | `[1]` | `0 / 2` | `1,246 / 591` | +| 4 / fanout | `[13]` | `[0]` | `[28]` | `[1]` | `4 / 2` | `958 / 453` | +| 5 / Tick | `[2,16]` | `[0,0]` | `[6,34]` | `[1,1]` | `0 / 2` | `1,246 / 591` | +| 6 / Crossing | `[19]` | `[0]` | `[40]` | `[1]` | `0 / 2` | `1,246 / 591` | +| 7 / fanout | `[13]` | `[0]` | `[28]` | `[1]` | `4 / 2` | `958 / 453` | +| 8 / Tick | `[1,9]` | `[0,3]` | `[4,20]` | `[1,0]` | `2 / 2` | `886 / 414` | +| 9 / Crossing | `[2]` | `[1]` | `[6]` | `[0]` | `15 / 2` | `492 / 226` | +| 10 / fanout | `[13]` | `[0]` | `[28]` | `[1]` | `4 / 2` | `958 / 453` | + +These are 124 processed live source entries: 123 useful workload occurrences plus the Fee Sink's one genesis-anchor initialization. There are five removed source buckets and 119 retained-bucket units. Removed buckets are identified from due-key disappearance, not confused with deleted pages inside a retained bucket. No stale entries occur. Twelve real worker calls include the coordinator's second Tick pass in blocks 2, 5 and 8; their 272 clock probes include terminal probes after processing. In this restricted fixture Block-clock membership is absent, the first clock is Block, and Tick callbacks create only future deadlines, so every processed entry and terminal iteration probes both clocks. + +When due work remains, the worker has charged the retained branch at its failed complete-admission check without processing another unit. This yields ten refusals, not ten failed Steps or useful occurrences. The test adds that final charge back to the returned meter remainder to recover the exact pre-refusal budget, selects the next source bucket's complete retained/removed envelope, and requires a positive deficit. All ten deficits are ProofSize-only. + +| Block / Tick pass | Pass budget ProofSize | Next due tick / occupancy | Remaining ProofSize before refusal charge | Required ProofSize | Deficit | +| --- | --- | --- | --- | --- | --- | +| 2 / first | 162,073 | `1 / 100` | 71,501 | 99,799 | 28,298 | +| 2 / second | 558,454 | `1 / 84` | 72,650 | 99,799 | 27,149 | +| 3 / only | 665,436 | `1 / 65` | 91,006 | 99,799 | 8,793 | +| 4 / only | 470,913 | `1 / 52` | 73,735 | 99,799 | 26,064 | +| 5 / first | 162,073 | `1 / 50` | 89,857 | 99,799 | 9,942 | +| 5 / second | 576,810 | `1 / 34` | 91,006 | 99,799 | 8,793 | +| 6 / only | 665,436 | `1 / 15` | 91,006 | 99,799 | 8,793 | +| 7 / only | 470,913 | `1 / 2` | 73,735 | 99,799 | 26,064 | +| 8 / first | 162,073 | `1 / 1` | 119,399 | 147,697 | 28,298 | +| 10 / only | 470,913 | `109 / 2` | 73,735 | 99,799 | 26,064 | + +Block 8's second pass has 606,352 ProofSize available and drains its remaining due work; block 9's sole pass has 665,436 and also ends without a refusal. In block 2 the first Tick pass performs only genesis anchoring, while the second processes all 16 useful occurrences. Returned peer-family reservations therefore are reused; they are not permanently lost capacity. The additional calls and terminal checks remain charged work, not proof that their fairness or safety obligations can be deleted. + +For each pass, charged temporal Control is `retained branch * (processed - removed + refusals) + removed branch * removed + clock probe * probes`. Summed generated temporal I/O is `129 * (48 / 29) + 5 * (92 / 51) + 272 * (6 / 0) = 8,284 / 3,996`. The full cohort adds nine non-temporal baselines of `32 / 11`, 29 successful-Step increments of `18 / 9`, and 16 no-progress FIFO state probes of `7 / 0`, yielding `9,206 / 4,356` Control I/O and `776,362,568,895 / 5,450,839` Control Weight. Native Prepass/idle equations and retained production phase totals agree exactly. + +| Charged Control category | Summed estimated ProofSize | Share of cohort Control ProofSize | +| --- | --- | --- | +| Processed Tick units, including genesis anchoring | 2,274,330 | 41.72% | +| Clock probes | 1,785,952 | 32.76% | +| Successful-Step Control | 804,257 | 14.75% | +| No-progress FIFO state probes | 241,696 | 4.43% | +| Non-temporal baseline | 180,504 | 3.31% | +| Refused Tick-unit charge | 164,100 | 3.01% | + +Whole-cohort normalization is approximately 317.45 generated Control reads and 150.21 writes per successful Step, including all baseline/no-progress terms and four zero-Step-workload blocks. It is not a physical database-operation count or per-Actor attribution. In particular, the ten refusals carry 290 benchmark-accounted writes without processing another temporal unit. Task effects, raw proof-node sharing and unused admission contingencies stay separate. This closes the controlled nine-block Cadenced ledger and its temporal-unit refusal budgets. That result alone establishes neither User/reactive, nonempty Crossing/fanout, lifecycle/other-Task nor full 100-block accounting, and grants no optimization or release acceptance. + +The full W2 System-Cadenced population is now separately reconciled over all 100 blocks. That captured producer used `DEOS_LARGE_CADENCED_LEDGER=1` plus `DEOS_PRODUCTION_WASM` and the optimized canonical route `scripts/ci-local.sh --only tests --release --package deos-runtime --test-filter full_executive_cadenced_ledger`, with `RUST_TEST_NOCAPTURE=1 DEOS_VERBOSE=1 SKIP_WASM_BUILD=1`. Default execution remains the small nine-block regression; malformed mode values and non-accepted genesis code fail before fixture preparation. `scripts/README.md` owns the human invocation contract. + +This mode uses the accepted Wasm only as genesis code, without executing it again. It adds 9,985 System Actors alongside the reference identities, authors blocks 2–101, and records each header hash, final head/cutoff/eligibility, initial Tick-bucket occupancy, both Control phases and real-worker pass ledger. All 100 header hashes match the Cadenced half of the retained W2 exact-Wasm log `/tmp/deos-0725-service-waits-w2.log` (SHA-256 `06cc548a2ede1ca4c744597c4eaae730ec9f047b393efef6bcbe58d0e462cff7`); source runs Manual before Cadenced, disambiguating their shared per-block profile labels. This binds the fresh native diagnostics to those already verified block states, not to a newly instrumented Wasm trace. + +Producer file SHA-256 is `2e5627758fe9faf25ea19c18e160f3a7b579eb4d2712ef2bf5eda184eae7374c`. Log `/tmp/deos-0725-cadenced-large-ledger.log` has SHA-256 `2d4839a710ee006bdf7c7d2d09659f586a0660b32f5cb8a0fd3301801e538f4f`. The new source also reproduces every prior small-profile pass/count/Control/I/O field in `/tmp/deos-0725-cadenced-ledger-small-regression.log` (SHA-256 `761d1143e6ad141ae7f6c0b99b9fbea5b5d0087941338af1d57d2b9367e4f57d`). Live Ready occupancy is bounded by the unchanged active count and the single-ticket invariant; the earlier conservative `2 * active` fixture guard is replaced by that actual capacity boundary, while saturated physical Ready cleanup remains excluded. + +| Starting family / cohort | Blocks | Processed units per pass | Successful Steps per block | Control ProofSize per block | Generated Control reads / writes per block | Temporal admission ProofSize deficits per pass | +| --- | --- | --- | --- | --- | --- | --- | +| Initial wakeups | 1 | `[1,16]` | 0 | 629,252 | `1,216 / 584` | `[28298,27149]` | +| Crossing | 33 | `[19]` | 0 | 641,108 | `1,246 / 591` | `[8793]` | +| Fanout | 33 | `[13]` | 4 | 574,788 | `958 / 453` | `[26064]` | +| Later wakeups | 33 | `[2,16]` | 0 | 641,108 | `1,246 / 591` | `[9942,8793]` | + +The 1,667 processed source entries comprise 1,666 useful workload occurrences and the one Fee Sink genesis-anchor initialization. The initialization removes the sole drained bucket; 1,666 workload units retain the initial bucket. All 134 real worker passes end with a failed complete admission for another entry in Tick bucket 1, each with zero RefTime deficit and positive ProofSize deficit. There are 3,602 clock probes and 198 no-progress FIFO state probes; block 2's head is post-cutoff, and every later final head is pre-cutoff and due. This preserves the distinction between temporal-unit refusals, FIFO service eligibility and historical finalized-headroom comparisons. + +Summed temporal generated I/O is `1,800 * (48 / 29) + 1 * (92 / 51) + 3,602 * (6 / 0) = 108,104 / 52,251`. Adding 100 non-temporal baselines, 132 successful-Step increments and 198 no-progress state probes gives `115,066 / 54,539` generated Control I/O and `9,722,261,833,572 / 61,910,384` Control Weight. The complete Control, effect and Control-remainder min/percentile/max distributions reproduce all 30 corresponding retained W2 fields. All 132 workload Steps succeed and execute in Prepass; no new Wasm or raw-proof run was needed. + +Whole-cohort normalization is approximately 871.71 generated Control reads and 413.17 writes per successful Step, including 67 zero-Step-workload blocks. The small nine-block cohort cannot substitute for this normalization: its initial bucket drains and later temporal paths change, whereas the full population retains that bucket throughout. At block 101 it still contains 8,319 entries; the Ready queue contains 1,534 Actors, headed by Actor 147 / ticket 132, eligible at block 9 below cutoff 1,648. Those observations agree with the retained censored service-wait counts, rather than implying unmeasured inter-Cycle gaps. + +The final native reference snapshot, bound to the same retained final header, shows Fee Sink Actor 1 active and Idle, with anchor tick 24, deadline tick 144, no pending signal, no queue ticket and no Run. Current tick is 1,212 (`606,000 ms / 500 ms`); under this fixture's six-second timestamp progression the deadline was due at block 12. The initial Tick bucket remains ahead of it, so this finite horizon does not materialize the reference Actor's readiness. This is bounded observed delay, not a starvation theorem or a claim that a cadence promises execution at its deadline. + +This closes full-population System-Cadenced Control/I/O accounting and its reference terminal observation, not the remaining Manual/Mixed, User/reactive, lifecycle or other-Task paths. Generated I/O is still charged benchmark accounting, not physical database calls; final-root reuse does not turn native phase reconstruction into a Wasm phase trace. Target throughput and release acceptance remain unmet. + +The same ledger now also closes the full Manual W2 and Actor-only Mixed W1 campaigns. The generalized producer is `full_executive_schedule_ledger_counts_processed_units_and_refusals`, with `DEOS_SCHEDULE_LEDGER=manual`, `mixed` or `cadenced` selecting one full profile; unset or `cadenced-small` selects the unchanged small regression. This replaces the earlier Cadenced-only test name and opt-in variable, without a compatibility alias. The current route is `RUST_TEST_NOCAPTURE=1 DEOS_VERBOSE=1 SKIP_WASM_BUILD=1 DEOS_PRODUCTION_WASM= DEOS_SCHEDULE_LEDGER= scripts/ci-local.sh --only tests --release --package deos-runtime --test-filter full_executive_schedule_ledger`. `scripts/README.md` owns current invocation rather than the historical command spellings above. + +`ACTOR_SCHEDULE_PATH_LEDGER_V1` preserves the charged-owner equations and adds schedule identity plus separate reference occurrences. Public-worker occurrence ordering matches the full author for both workload and reference Actors. The only admitted non-workload occurrence here is the Fee Sink in Manual; genesis initialization remains a processed unit without an occurrence. The producer source file has SHA-256 `4047fc3c0411e02962385c9e1fbfe9d47b08919c0c78457e15244fd0a2afc191`. + +| Full Actor-only profile | Fresh native log / SHA-256 | Reused exact-Wasm owner | Header matches | +| --- | --- | --- | --- | +| Manual W2 | `/tmp/deos-0725-manual-large-ledger.log` / `344e52d40a57f47441a8f3ebd64ad1d3a86aff0e7efa03e929d6676c32c9cd7c` | Manual half of `/tmp/deos-0725-service-waits-w2.log` | 100 / 100 | +| Mixed W1 | `/tmp/deos-0725-mixed-large-ledger.log` / `e80296203acb424f2c8c08d6a5c653b359ed7a6c2cd7658a6bd6172cabf4a411` | `/tmp/deos-0725-service-waits-w1-actor-only.log`, SHA-256 `274b4409030b85e5b26aae23497404ab811c4e19c788896a4fce881f0605f7c6` | 100 / 100 | + +Both cohorts retain 9,985 added System Actors, the same reference identities and accepted genesis code. Every native block hash matches its retained Wasm counterpart, and all 30 Control/effect/Control-remainder distribution fields per profile reproduce. The generalized small regression preserves every prior Cadenced block field in `/tmp/deos-0725-schedule-ledger-small.log` (SHA-256 `25984669871c81408123faa34c33b12b53bb0ad3cbb8273c4f2ccaa6259ce65d`). The closed large Cadenced measurement was reused, not rerun. + +| Profile | Successful Steps | Useful workload / reference occurrences | Processed units / removed source buckets | Worker passes / temporal refusals | Clock probes | No-progress FIFO state probes | +| --- | --- | --- | --- | --- | --- | --- | +| Manual | 1,894 | `0 / 1` | `2 / 2` | `100 / 0` | 204 | 200 | +| Mixed | 132 | `1,666 / 0` | `1,667 / 1` | `134 / 134` | 3,602 | 200 | + +| Profile | Total Control RefTime / ProofSize | Generated Control reads / writes | Whole-cohort generated reads / writes per successful Step | +| --- | --- | --- | --- | +| Manual | `3,263,518,429,974 / 59,021,182` | `40,100 / 18,248` | `21.17 / 9.63` | +| Mixed | `9,722,765,207,572 / 61,940,596` | `115,080 / 54,539` | `871.82 / 413.17` | + +Manual's two processed units are genesis anchoring at block 2 and one useful Fee Sink occurrence at block 12. Each of those blocks executes 16 successful workload Steps and charges Control `36,837,034,590 / 584,568`, with generated I/O `450 / 206`. The other 98 blocks execute 19 Steps and charge `32,549,432,253 / 590,327`, with I/O `400 / 182`. No temporal admission refusal occurs. The full ledger is 100 non-temporal baselines, 204 clock probes, two removed Tick-unit charges, 1,894 Step increments and 200 no-progress head probes; setup, occurrence materialization and committed Steps are not conflated. + +Mixed's temporal pass ledger matches full Cadenced throughout, except for the different source occupancies. Its first block additionally charges two no-progress state probes because the initial Manual head is already below cutoff 4,993; the pure Cadenced first head is post-cutoff. This explains exactly the whole-cohort delta `503,374,000 / 30,212` Control and `14 / 0` generated I/O. Mixed block 2 charges `104,051,893,054 / 659,464` and `1,230 / 584` I/O; later block counts, Steps and Control totals follow the three 33-block Cadenced groups above. Normalization includes 67 zero-Step-workload blocks and work for Actors other than those whose Steps commit; it is not a per-Actor physical cost. + +| Final state at block 101 | Manual | Mixed | +| --- | --- | --- | +| Workload head Actor / ticket / eligible block / cutoff | `1909 / 1894 / 1 / 9986` | `279 / 132 / 1 / 6641` | +| Live Ready occupancy | 8,092 | 6,527 | +| Initial Tick bucket remaining | 0 | 3,326 | +| Fee Sink pending / ticket / temporal pointer | `true / 9985 / none` | `false / none / Tick(144)` | +| Fee Sink lifecycle / cycle / Run | `Active / Idle / none` | `Active / Idle / none` | + +Manual materializes Fee Sink readiness at block 12, disables its temporal detector while latched, and appends its ticket behind the initial workload. Its final Ready occupancy is the 8,091 unserved workload Actors plus the Fee Sink. Mixed never drains the initial Tick bucket, so the reference deadline remains overdue but unmaterialized at the same final tick 1,212. The two finite-horizon states distinguish readiness waiting from FIFO service waiting; neither cadence nor System authority promises an immediate execution slot. No production scheduling, Weight, geometry or economic policy changed. + +These header-matched results close the three Actor-only W1/W2 schedule ledgers. They alone establish neither continuous signed-user demand, User Actor fees, reactive/lifecycle/other-Task accounting nor remaining phase-local service-stop proofs. No full runtime-suite, new Wasm execution, optimization or release acceptance is claimed. + + + +### Current EXP-0095 Small-Cohort Refresh + +The nine-block Cadenced regression now uses current generated temporal owners and the inclusive completion owner. Command: `RUST_TEST_NOCAPTURE=1 DEOS_VERBOSE=1 SKIP_WASM_BUILD=1 ./scripts/ci-local.sh --only tests --package deos-runtime --test-filter full_executive_schedule_ledger_counts_processed_units_and_refusals`. Raw log `/tmp/deos-0725-exp0084-current-small.log` has SHA-256 `4d2cfcee4c919775a86c9fb2cfa5983118378c0f76a72c63016fa8dbf69cd38c`. + +The refreshed small cohort closes all native full/Prepass equations: 124 processed source units, 28 successful Steps, 122 useful occurrences, five removed buckets and ten ProofSize-only admission refusals. Generated I/O and every pass budget remain source-bound. + +The full 10,000-identity Manual, Cadenced, Mixed and Mixed-with-user profiles also pass on the current binding. Commands use the documented optimized schedule-ledger route with the corresponding `DEOS_SCHEDULE_LEDGER` mode and `DEOS_PRODUCTION_WASM` set to the EXP-0095 artifact. Raw logs are `/tmp/deos-0725-exp0084-current-manual.log`, SHA-256 `541770ad79cc17904d724dfee61f1bc193e101ff24073df62e944fe01ef5ff33`; `/tmp/deos-0725-exp0084-current-cadenced.log`, SHA-256 `f674fe687806b9b082ebc9c2c51a456691a9002a1fa129b2a5847d23694cbc19`; `/tmp/deos-0725-exp0084-current-mixed.log`, SHA-256 `2242428d2e69b6091c92ba998a4248cd7c03f8053c2c02f7d256203e9a7217f3`; and `/tmp/deos-0725-exp0084-current-mixed-user.log`, SHA-256 `4827aa318fc24e2851473b42c1a744822283b9c01b74a6f7a44d1f8f0f6ae14a`. Each authors 100 native blocks for 9,985 workload System Actors, binds genesis code to Wasm SHA-256 `25b9695fd9e900f17ae1f3fb0b815ac1403830264d9c31f7cee54e29f434b700`, and closes all source-bound full/Prepass equations. Mixed-with-user additionally preserves real signature/nonce execution and records a ProofSize-only next-call deficit without claiming exact-Wasm header identity. + +## Derived Metrics + +Whole-cohort ratios and the Mixed/Cadenced delta remain verbatim above. They include baseline, no-progress and noncommitting Actors' work; no new per-Actor normalization is introduced. + +## Statistical / Variance Notes + +Exact equation/header/phase agreement, not an independent timing sample. The retained 30-field distributions are reused; small and full populations have different temporal paths and cannot substitute for each other. + +## Result + +The retained Manual/Cadenced small cohorts and three full Actor-only System schedule ledgers reconcile within the stated controls. Refusal deficits in the temporal witnesses are ProofSize-only; generated owner counts preserve zero-progress work. + +## Interpretation + +Temporal processing and clock probes dominate the measured Cadenced accounting; this is current-topology charged work, not a universal minimum or removable-cost proof. Returned family reservations are reused. Neither Pareto improvement nor throughput acceptance follows from accounting closure. + +## Decision + +Accepted for the bounded current System schedule accounting claim. The small Cadenced and all four full schedule/demand profiles close their source-bound Control, effect, generated-I/O and phase equations on EXP-0095 genesis code. Mixed-with-user remains native state/accounting evidence rather than exact-Wasm header identity, and no User Actor fee-path, optimization authorization or release acceptance follows. + +## Rejected Alternatives and Tradeoffs + +- `Small-cohort extrapolation`: Rejected; draining versus retained source buckets materially changes full-cohort normalization. +- `Physical/per-Actor I/O`: Rejected; generated charges include envelopes, refusals, shared baseline and work for noncommitting Actors. +- `Lost-reservation claim`: Rejected by second Tick-pass reuse; charged checks do not prove fairness/safety work removable. +- `Deadline/starvation inference`: Finite-horizon reference delay is neither a deadline execution promise nor starvation theorem. + +## Validity + +- `Establishes`: Scoped System schedule charged-owner accounting, retained header/phase linkage and reference terminal observations. +- `Does not establish`: User Actor fees, signed-Wasm linkage, reactive/lifecycle/other-Task accounting, universal minima, removable cost or release acceptance. +- `Known limitations`: Restricted public-worker composition, historical raw-log availability not newly verified, native reconstruction not Wasm phase instrumentation. +- `Invalidation triggers`: Owner equations, schedule state geometry, production identity, peer-family population, scan cap, cleanup topology or observation method change. +- `Residual qualitative insight`: Occurrence materialization, FIFO service, successful Steps and charged refusals remain distinct owners. + +## Follow-Up / Next Gradient + +Parent attribution → declared System schedule ledgers reconciled → stop this claim. Signed-state linkage belongs to EXP-0085; lifecycle accounting to EXP-0086. Any new independently decidable owner requires its own boundary, not arbitrary additional profiles here. + +## Architecture Impact + +No scheduling, Weight, geometry or economic policy change. EXP-0084 is scoped attribution provenance; it does not reopen frozen C1. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: [EXP-0066](./EXP-0066.md) O15. +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: None. +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: [EXP-0065](./EXP-0065.md) exact binding; [EXP-0070](./EXP-0070.md) W1/W2 service/header evidence; [EXP-0078](./EXP-0078.md) phase rows; [EXP-0079](./EXP-0079.md) temporal owners; [EXP-0080](./EXP-0080.md) retained repeated root-proof cohort; [EXP-0082](./EXP-0082.md) Step/Transfer owners; [EXP-0083](./EXP-0083.md) baseline/startup equations. +- `Transfers question to`: [EXP-0085](./EXP-0085.md) signed-demand state linkage; [EXP-0086](./EXP-0086.md) lifecycle charged-owner accounting; [EXP-0081](./EXP-0081.md) Manual phase-local stops. +- `Produces input for`: [EXP-0066](./EXP-0066.md) O15 System schedule attribution; [EXP-0085](./EXP-0085.md) Mixed Control/I/O reuse. +- `Reopen trigger`: Reproducible equation/header mismatch or changed declared owner/state/artifact; cost alone never reopens frozen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0085.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0085.md new file mode 100644 index 00000000..1875f1f3 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0085.md @@ -0,0 +1,183 @@ +# EXP-0085 — Signed-Demand Accounting Evidence Linkage + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | [EXP-0066](./EXP-0066.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-08 | +| Affected domain | Signed-demand accounting evidence identity | +| Physical mechanism | Signed Executive block state linkage to charged-owner reconstruction | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0095](./EXP-0095.md) | +| Successor experiment | None | + +## Decision Question + +Can the native signed-demand charged-owner ledger be bound to the retained signed production-Wasm states rather than only matching resource distributions? + +## Governing Specification + +- `Owners`: `docs/actors-performance-assurance.specification.en.md` (production evidence and target); `docs/actors-resource-policy.specification.en.md` (component-wise accounting); `template/pallets/actors/docs/specification.en.md` (fee and execution ownership). +- `Invariant`: Real valid signed dispatch, exact artifact identity, separate User transactions and User Actor fee authority; frozen C1 FIFO/Q1 and causal floor remain unchanged. +- `Reopen trigger`: Demonstrated semantic conflict, not signature randomness or an identity-linkage gap. + +## Context and Hypothesis + +- `Current architecture`: Native Executive replay reconstructs the System workload ledger while signing fresh user remarks. +- `Hypothesis`: Equal aggregate resource observations can be accompanied by exact signed-state linkage. Header mismatch is the smallest falsifier of reuse by header identity. +- `Materiality`: Evidence sufficiency for parent O16, not an additional throughput profile or optimization. + +## Baseline and Candidates + +| ID | Architecture | Source tree | Runtime/config identity | +| --- | --- | --- | --- | +| Baseline | Retained signed-Wasm W1 | Historical W1 source `1cb1efc86d22dd869677fffeb63ccfeff75d5ae3a18a0582b0f57dbb34afd19b` | EXP-0065 production binding | +| A | Native signed-demand schedule ledger | Producer `68fd9ef6dbd649e50658ce4d15da61d10de2be0c5593e7274ad54fbe9722e190` | Same binding; fresh signatures | + +## Controls + +- `Controlled variables`: Accepted production genesis code, System workload, 100-block horizon, generated Weight, valid signed calls and monotonic nonces. +- `Changed variables`: Native observation and freshly constructed signed extrinsics versus retained Wasm run. +- `Confounders`: Signatures and headers differ; equal distributions do not prove identical states. + +## Benchmark Class, Workloads, and Criteria + +- `Benchmark class`: Source-bound native full-Executive accounting compared with retained production-Wasm evidence. +- `Workload`: `mixed-user`, 9,985 added System Actors and 15 reference identities, blocks 2–101, 77 valid signed remarks per block; signer seed `[66; 32]`, Aura slots matching blocks, relay `max_pov_size` 5,000,000 bytes. +- `Metrics`: Header identity, resource distributions, charged-owner counts and preflight operands. Host timing and physical I/O are not inferred. +- `Statistical method`: Deterministic accounting equality; matching distributions are not state equivalence. +- `Acceptance criteria`: Adequate exact signed-state linkage for production acceptance, explicitly adjudicated beyond native totals. +- `Rejection criteria`: Promote aggregate equality or System workload fees to signed-Wasm state/User Actor fee proof. + +## Environment and Artifact Identity + +| Artifact | Identity | +| --- | --- | +| Pre-fission source | `/tmp/deos-exp0066-prefission.md`, SHA-256 `973bb0dd5f63430da3bff5aa1cc74a6df35b22b48510f47c44abf7dd8519f923`, 170875 bytes, mode `100644`; lines 898–914 inclusive | +| Git baseline | Dirty atop commit `a8d9ec1a07fb7fdc383148d1dbd37c83874d62be`, tree `a1cb8c8fa19dd790af5032038a352bdfd2dc673d`; not a clean measured-source claim | +| Migration provenance | [2026-09-08 fission manifest](./experiments.md#current-decision-critical-path) | +| Accepted production source | `c5345d6ba9574b9f16d0c62541400df027edd954039167f413f9b0aa9297c26d` | +| Rust toolchain | `1.94.1` | +| Benchmark runtime Wasm | Not used for this reconstruction | +| Post-generation production runtime Wasm | `155749e22a6235570d2bb97d4a42b1327b86f4f5d82742df36494fca0fd88c1d` | +| Final release production runtime Wasm | Not established by fission | +| Generated Actors Weight | `20d56df225eca3ff52857f2170eccf50587c5ca17f997d18d083e61e8afbeed9` | +| Metadata | `75868ea75fd85e7b79db12650a7407e89e249f5564b2157d55ac46cd26cb5d12` | +| Database/backend | Native test externalities; no physical database-operation claim | +| Command | `RUST_TEST_NOCAPTURE=1 DEOS_VERBOSE=1 SKIP_WASM_BUILD=1 DEOS_PRODUCTION_WASM= DEOS_SCHEDULE_LEDGER=mixed-user scripts/ci-local.sh --only tests --release --package deos-runtime --test-filter full_executive_schedule_ledger` | +| Raw evidence | Exact historical paths/digests in the extraction; no artifacts regenerated or availability inferred | + +## Proof Obligations + +- `Freeze`: Frozen via explicit 2026-09-08 pre-fission reconstruction from the immutable source above; not a claim of historical freeze. +- `Review triggers`: Independent signed-state artifact identity/evidence class separated from System schedule accounting. +- `Decomposition review`: One linkage claim; schedule totals belong to EXP-0084, production acceptance to EXP-0066. No new workload admitted. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | Native signed-demand accounting is sufficiently linked to retained production-Wasm states | Header mismatch despite equal resource totals | Native accounting versus production-Wasm state identity | Self | Required | EXP-0066 O16 evidence sufficiency | Satisfied with current-production scope | + +## Measurements + +Verbatim pre-fission EXP-0066 Measurements signed-demand accounting passage, lines 898–914. Historical “same route”, “existing Mixed” and “above” refer to [schedule accounting](./EXP-0084.md); retained service evidence belongs to [EXP-0070](./EXP-0070.md). Adjacent domains are [phase](./EXP-0078.md), [temporal](./EXP-0079.md), [proof](./EXP-0080.md), [stops](./EXP-0081.md), [Transfer](./EXP-0082.md), [baseline](./EXP-0083.md), [lifecycle accounting](./EXP-0086.md) and [lifecycle timeline](./EXP-0073.md). These links interpret historical locality without changing the payload. + + +Signed-demand accounting is now separately available through `DEOS_SCHEDULE_LEDGER=mixed-user` on the same optimized schedule-ledger route. It retains real signed `System::remark` extrinsics, monotonically advanced nonces and `Executive::apply_extrinsic` validation; it does not replace signatures or bypass transaction execution. The test records actual User and effect usage plus the fixture's next-candidate preflight operands. Workload Actors remain System Actors, explicitly labelled in the output; signed user transactions do not prove User Actor fee paths. + +The new native producer file SHA-256 is `68fd9ef6dbd649e50658ce4d15da61d10de2be0c5593e7274ad54fbe9722e190`; `/tmp/deos-0725-signed-user-ledger.log` has SHA-256 `961dbad32490b6baa329d48ad2e7d11a9f743d131298ac64b9baf87040501ccc`. Its 100-block control/worker/head observations reproduce the Actor-only Mixed ledger exactly, including all 134 passes, 132 successful workload Steps and the reference lifecycle snapshot. Thus the existing Mixed Control/I/O totals apply unchanged; this is source-bound native accounting under signed demand, not another Actor-only measurement or a new Wasm execution. + +| User demand observation in every block | RefTime | ProofSize | +| --- | --- | --- | +| Actual usage of 77 successfully dispatched signed calls | 38,334,246,720 | 673,442 | +| Remaining User base-turn budget | 266,792,209,856 | 2,708 | +| Next constructed candidate's declared Weight | 497,847,360 | 8,746 | +| Candidate deficit | 0 | 6,038 | + +The fixture submits 7,700 valid signed calls across 100 blocks. The next candidate in each block is withheld by `saturate_user_dispatch` before applying it: this table proves a ProofSize-only preflight deficit, not an observed failed on-chain dispatch or a fee charged for the unsubmitted candidate. All 40 retained Control/effect/User/Control-remainder min/percentile/max fields reproduce `/tmp/deos-0725-service-waits-w1-continuous-user.log` (SHA-256 `4258173c235f673a2c7629ce8d0f3865e404c76344e94f828daba6d7387b71ee`). + +Header identity is explicitly not reused here: zero of the 100 fresh native headers match the retained signed-Wasm run. Even the two previously retained signed runs have different first headers (`0x50d00a5776214fddc502511b393aa31f9d81a296c08ed861dcda9a4f1a7547ea` versus `0x204ff1070645c48ae00fb39efc5268264b7e5afd93e45a1e713eb248b4cec12f`) despite equal resource observations. The signing helper constructs each signature afresh with `sr25519::Pair::sign`; it does not retain signed block bodies for replay. Matching aggregate distributions and the native Actor-only control pattern therefore do not establish identical signed-Wasm states or instrumented Wasm phase counts. Production acceptance must assess that evidence boundary rather than silently promoting this native result. + +The default small regression also preserves every prior block field in `/tmp/deos-0725-signed-ledger-small.log` (SHA-256 `b1a3af818538611a944d589054aeb45e0885a535d56d96151ef6c3b6f58669a8`). Closed large Actor-only campaigns were not repeated. Production runtime, Weight, Wasm and geometry remain unchanged; User Actor, reactive, lifecycle and other-Task accounting and overall acceptance remain open. + + + +### Current-Production Measurement + +The accepted EXP-0095 cohort was rerun through the dedicated continuous-valid-user campaign. Each of 100 linked blocks contains 77 freshly signed `System::remark` extrinsics with monotonically increasing nonces. The same authored block, pre-state and signed body execute natively and are independently replayed by production Wasm; every block asserts the authored header/state root against Wasm execution and proof-only verification. This directly links the accounting observations to those 100 retained block identities rather than comparing fresh native headers with another run. + +| Artifact / result | Identity | +| --- | --- | +| Source identity | `f4007411eea86c1fbc36b670dd36f994b4eb0d6debcbd7ab1459abd63717c0e9` | +| Actors Weight | `9ce37ade391a29d4f60779f1887ad242dfeb4919d25cf0047ffba85d18f33901` | +| Production Wasm | `25b9695fd9e900f17ae1f3fb0b815ac1403830264d9c31f7cee54e29f434b700` | +| Metadata | `75868ea75fd85e7b79db12650a7407e89e249f5564b2157d55ac46cd26cb5d12` | +| Raw log | `/tmp/deos-0725-exp0085-current-signed-wasm.log`, SHA-256 `e7d11a4ec9c11d3fb73728081c2c39f9631e91fbec57017fd90c35bd9857f393` | +| Linked blocks | 100 `EXP_0066_WASM_BLOCK_PREP_V1` records, campaign blocks 2–101 | +| Signed calls | 7,700 successful calls; exactly 77 per block | +| User usage per block | `38,350,416,720 / 673,442` | +| Workload result | 132 successful System Transfer Steps, zero failed/skipped, target unmet | + +The run reports code hash `0x16f95188d3ce591c43ea500bbb14e1a889051421625c39cd5144fb9f8432dcf9` inside every block-proof record. Proof metrics and current Control distributions are retained in the raw log; they are current evidence, not aliases for the invalidated historical numeric ledger. + +## Derived Metrics + +No cross-run arithmetic or Pareto ranking is used. The current linked cohort owns its own Control/proof distributions; invalidated EXP-0084 totals are not substituted. + +## Statistical / Variance Notes + +Fresh signatures confound equality across independently authored runs, which is why the accepted campaign compares no cross-run headers. Each signed body is instead retained in its authored block and replayed against the same pre-state through exact Wasm. No host-timing conclusion is drawn. + +## Result + +The historical 0/100 cross-run header mismatch remains a valid warning against aggregate substitution. The current campaign removes that confounder by replaying each freshly signed native-authored block itself through the exact accepted Wasm and matching state/proof identity for all 100 blocks. + +## Interpretation + +Resource coincidence supports scoped native accounting, not identical signed-Wasm states or instrumented phase counts. No physical candidate comparison/Pareto winner is established. + +## Decision + +Accepted for EXP-0066 O16. Select the current 100-block signed native-authoring plus exact-Wasm replay cohort as sufficient state linkage. Preserve the historical mismatch as a rejected aggregate-equality shortcut; do not infer User Actor fee coverage because workload Actors remain System Actors. + +## Rejected Alternatives and Tradeoffs + +- `Aggregate-equality shortcut`: Rejected as state-identity proof by the explicit header mismatch. +- `Unsigned substitute`: Not admitted; the retained evidence uses real signed calls. +- `User Actor inference`: Rejected; the workload Actors are System Actors. + +## Validity + +- `Establishes`: Current-production signed-demand accounting linked block-by-block to exact Wasm execution, authored headers, post-state roots and proof-only verification. +- `Does not establish`: An on-chain rejection/fee for the withheld candidate, User Actor fees, the 10,000-Cycle target or release acceptance. +- `Known limitations`: Workload Actors are System Actors; the next candidate remains preflight-withheld after User ProofSize exhaustion. +- `Invalidation triggers`: Changed signer/body identity, source, Weight, runtime, workload or accounting method. +- `Residual qualitative insight`: Identity and aggregate equality are distinct proof surfaces. + +## Follow-Up / Next Gradient + +Current signed linkage accepted → return O16 to EXP-0066 → refresh only remaining invalidated resource/service owners. Do not repeat the closed Actor-only campaign or treat signed transactions as User Actor fee evidence. + +## Architecture Impact + +No runtime, Weight, geometry or semantic change. The retained test now pins the continuous-user exact replay entrypoint to EXP-0095 Wasm; EXP-0085 supplies evidence sufficiency, not architecture selection. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: [EXP-0066](./EXP-0066.md) O16. +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: None. +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: [EXP-0095](./EXP-0095.md) exact binding; [EXP-0084](./EXP-0084.md) historical Mixed ledger; [EXP-0070](./EXP-0070.md) historical service campaign. +- `Transfers question to`: None. +- `Produces input for`: [EXP-0066](./EXP-0066.md) O16 signed-demand evidence sufficiency. +- `Reopen trigger`: Exact state linkage or changed artifact identity requires reevaluation; cost alone never reopens frozen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0086.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0086.md new file mode 100644 index 00000000..c0ea01f9 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0086.md @@ -0,0 +1,198 @@ +# EXP-0086 — Lifecycle Charged-Owner Accounting + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | [EXP-0066](./EXP-0066.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-08 | +| Affected domain | Bounded W5 lifecycle resource accounting | +| Physical mechanism | Source-bound lifecycle charged-owner reconstruction | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0065](./EXP-0065.md) | +| Successor experiment | None | + +## Decision Question + +Does the bounded W5 lifecycle ledger reconstruct complete charged Control/effect owners and generated I/O without double-counting provisional entry, cleanup or continuation work? + +## Governing Specification + +- `Owners`: `template/pallets/actors/docs/specification.en.md` (lifecycle, custody, Pipeline/Action fees and committed prefixes); `docs/actors-resource-policy.specification.en.md` (component-wise accounting); `docs/actors-performance-assurance.specification.en.md` (evidence classification). +- `Invariant`: Frozen C1 FIFO/Q1, causal floor, committed-Step durability, atomic Task effects, complete-owner settlement and custody-neutral close. +- `Reopen trigger`: Evidenced semantic contradiction, not a generated-I/O or cost difference. + +## Context and Hypothesis + +- `Current architecture`: Production `StepControlWeight` selects lifecycle owners from bounded pre-state geometry and phase. +- `Hypothesis`: Their frequencies reconstruct the native author's full and Prepass Control and independent Task effects on the header-matched W5 states. +- `Materiality`: Any omitted or double-counted term falsifies the accounting claim; no optimization threshold or extra lifecycle profile is introduced. + +## Baseline and Candidates + +| ID | Architecture | Source tree | Runtime/config identity | +| --- | --- | --- | --- | +| Baseline | Retained W5 production blocks | Initial W5 source `3298afed17fd3156144875b518b29b1789e31dcee1bc07a187c7b191cd578cb5` | EXP-0065 production binding | +| A | Native source-bound W5 ledger | Producer `95d56b10f367a07083ea390e583040ebc91a7e25715f2bff2748a38d243fe143` | Same production owners; no selector change | + +## Controls + +- `Controlled variables`: Five Manual workload Actors, three measured blocks, reference identities, accepted genesis code, production Weight and lifecycle states. +- `Changed variables`: Test-side resource reconstruction only. +- `Confounders`: Generated I/O counts charged envelopes, not physical operations; setup fees are outside measured blocks. + +## Benchmark Class, Workloads, and Criteria + +- `Benchmark class`: Source-bound native lifecycle accounting linked by all three headers to retained full-Executive production-Wasm W5 evidence. +- `Workload`: Five Manual Actors covering zero-Step completion, three-Step Running, committed Transfer prefix before retrying middle SwapOut, productive cleanup and underfunded Pipeline-admission apoptosis; blocks 2–4, signer seed `[66; 32]`, matching Aura slots and 5,000,000-byte relay `max_pov_size`. +- `Metrics`: Charged Control/effect RefTime, ProofSize, generated reads/writes, owner frequencies, phase totals and header identity. +- `Statistical method`: Exact accounting equations, not noisy latency inference. +- `Acceptance criteria`: Complete owner sums and phase totals equal the real author with independent state/selector guards. +- `Rejection criteria`: Extra ordinary entry on complete apoptosis, inferred continuation/wakeup charge, or Task failure treated as no attempt. + +## Environment and Artifact Identity + +| Artifact | Identity | +| --- | --- | +| Pre-fission source | `/tmp/deos-exp0066-prefission.md`, SHA-256 `973bb0dd5f63430da3bff5aa1cc74a6df35b22b48510f47c44abf7dd8519f923`, 170875 bytes, mode `100644`; lines 915–952 inclusive | +| Git baseline | Dirty atop commit `a8d9ec1a07fb7fdc383148d1dbd37c83874d62be`, tree `a1cb8c8fa19dd790af5032038a352bdfd2dc673d`; not a clean measured-source claim | +| Migration provenance | [2026-09-08 fission manifest](./experiments.md#current-decision-critical-path) | +| Accepted production source | `c5345d6ba9574b9f16d0c62541400df027edd954039167f413f9b0aa9297c26d` | +| Rust toolchain | `1.94.1` | +| Benchmark runtime Wasm | Not used for this reconstruction | +| Post-generation production runtime Wasm | `155749e22a6235570d2bb97d4a42b1327b86f4f5d82742df36494fca0fd88c1d` | +| Final release production runtime Wasm | Not established by fission | +| Generated Actors Weight | `20d56df225eca3ff52857f2170eccf50587c5ca17f997d18d083e61e8afbeed9` | +| Metadata | `75868ea75fd85e7b79db12650a7407e89e249f5564b2157d55ac46cd26cb5d12` | +| Database/backend | Native test externalities; generated I/O is not physical database calls | +| Initial campaign command | `scripts/actors-assurance.sh --integrated-w5-lifecycle-retry-cleanup` | +| Reconstruction command / raw evidence | Exact native command, producer and log identities retained below; no reexecution or newly asserted artifact availability | + +## Proof Obligations + +- `Freeze`: Frozen via explicit 2026-09-08 pre-fission reconstruction from the immutable source above, not a historical freeze claim. +- `Review triggers`: Lifecycle charged-owner evidence is independently decidable from timeline and schedule accounting. +- `Decomposition review`: One bounded W5 reconstruction claim; other lifecycle states and funded User fee paths are excluded rather than appended. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | Bounded W5 charged-owner equations reconstruct complete Control/effect usage without duplicate ownership | One guarded W5 block with mismatching owner or phase sum | Native source-bound accounting, header-linked to production-Wasm W5 | Self | Required | EXP-0066 O17 bounded lifecycle ledger | Satisfied with scope: measured bounded W5 only; no acceptance promotion | + +## Measurements + +Verbatim pre-fission EXP-0066 Measurements W5 resource-ledger passage, lines 915–952. Historical “above” baseline/Control references resolve to [baseline](./EXP-0083.md), [Transfer](./EXP-0082.md), [phase](./EXP-0078.md) and [temporal](./EXP-0079.md); W5 timeline belongs to [EXP-0073](./EXP-0073.md). Adjacent independent owners are [proof](./EXP-0080.md), [stops](./EXP-0081.md), [schedule](./EXP-0084.md), [signed linkage](./EXP-0085.md) and [service](./EXP-0070.md). Historical open-work statements below describe their original evidence horizon, not new obligations. + + +W5 now has a complete source-bound lifecycle resource ledger in `assert_w5_resource_ledger`, executed by the existing lifecycle campaign. The helper reconstructs bounded control contexts from pre-state Actor geometry and phase, then calls the production `StepControlWeight` provider and checks the selected named owners. It separately checks full-block Control, Prepass Control and effect totals against the real author. Newly used generated components have explicit base/ProofSize/database-I/O guards; no production instrumentation or selector changed. + +The native route is `DEOS_PRODUCTION_WASM= RUST_TEST_NOCAPTURE=1 DEOS_VERBOSE=1 SKIP_WASM_BUILD=1 scripts/ci-local.sh --only tests --package deos-runtime --test-filter full_executive_w5_lifecycle_retry_cleanup_fixture`. Without the optional code input it remains a standalone native test. With accepted genesis code, all three headers match the retained exact-Wasm W5 blocks in `/tmp/deos-0725-w5-lifecycle-timeline.log` (SHA-256 `cf0f4db75fd3b4ded84ac295a81d0e8e59e022ef57ea7c980fb438e6aadad230`), without replaying Wasm. Every prior setup/finalized timeline field reproduces; the later observer's three added temporal/Crossing fields are explicitly null for these Manual Actors. + +Producer file SHA-256 is `95d56b10f367a07083ea390e583040ebc91a7e25715f2bff2748a38d243fe143`; `/tmp/deos-0725-w5-resource-ledger.log` has SHA-256 `4cd19421aa4fc3e8af3862a26111d4763cf0cce644b2421b15754091d9064381`. `ACTOR_LIFECYCLE_RESOURCE_LEDGER_V1` identifies native source-bound reconstruction and records per-block headers, named charged terms, frequencies and generated Control/effect I/O. + +| Charged Control term | RefTime / ProofSize per term | Generated reads / writes | Frequency in blocks 2 / 3 / 4 | +| --- | --- | --- | --- | +| No-due baseline | `2,342,556,054 / 33,188` | `44 / 11` | `1 / 1 / 1` | +| Genesis-anchor extra | `8,977,629,000 / 77,440` | `104 / 51` | `1 / 0 / 0` | +| Ordinary entry discovery + state probe + consume envelope | `1,152,330,027 / 23,335` | `16 / 6` | `4 / 2 / 2` | +| Zero-Step admission envelope | `12,196,475,054 / 143,858` | `113 / 82` | `1 / 0 / 0` | +| Opening progress, one tail chunk | `1,176,353,978 / 9,320` | `12 / 7` | `2 / 0 / 0` | +| Opening completion, no tail chunks | `406,682,194 / 4,398` | `2 / 3` | `1 / 0 / 0` | +| Running progress, two-Step tail fragment | `1,179,926,699 / 12,546` | `18 / 5` | `0 / 1 / 0` | +| Running completion, two-Step tail fragment | `795,287,861 / 10,974` | `11 / 4` | `0 / 0 / 1` | +| Current tail plan | `280,594,580 / 5,942` | `7 / 0` | `0 / 1 / 1` | +| Run suspension | `1,444,654,000 / 5,871` | `13 / 9` | `0 / 1 / 0` | +| Fallback Run completion | `973,628,000 / 6,237` | `8 / 6` | `0 / 0 / 1` | +| Ready append envelope | `630,319,000 / 16,446` | `6 / 4` | `0 / 1 / 0` | +| Invoked Action receipt | `4,330,000 / 0` | `0 / 0` | `3 / 2 / 2` | +| Authored terminal cleanup | `8,971,696,000 / 81,886` | `66 / 65` | `1 / 0 / 1` | +| Complete admission apoptosis | `2,282,851,000 / 15,106` | `19 / 16` | `1 / 0 / 0` | + +Zero-Step Actor 15 charges the ordinary entry prefix plus `contract_steps_admission_weight_upper`: scheduler admission overhead, `scheduler_inner_zero_step_complete` and a close-cleanup envelope. It completes without an Action or closure. The envelope's generated I/O is `45 / 14 + 2 / 3 + 66 / 65 = 113 / 82`; those upper-bound terms are charged accounting, not evidence that their named storage operations or cleanup actually execute. In contrast, User Actor 19 selects complete `pipeline_admission_apoptosis`. `ProvisionalEntryAccounting::settle_complete` replaces the entry's provisional discovery/state-probe charge, so adding an ordinary prefix or another generic close would double-count it. Creation and the useful Manual Trigger payment occurred in fixture setup, outside these measured blocks. + +The two three-Step System Actors select minimal Opening progress in block 2. Running Actor 16 then selects direct Running progress and completion. Retry Actor 17 selects the composed Running-tail/suspend/Ready-append path in block 3, followed by the composed Suspended-tail/failed-completion path and terminal cleanup in block 4. Its retained ticket is 8, eligible at block 4, with no wakeup pointer: this short retry does not execute a Block wakeup worker. The independent state/selector guards reject substituting a Wakeup placement or assigning an extra continuation charge merely because a `CycleContinued` event exists. + +| Block | Action attempts / successful Transfers | Control RefTime / ProofSize | Generated Control reads / writes | Effect RefTime / ProofSize | Generated effect reads / writes | +| --- | --- | --- | --- | --- | --- | +| 2 | `3 / 3` | `42,152,907,366 / 467,856` | `436 / 266` | `6,725,250,000 / 87,666` | `75 / 36` | +| 3 | `2 / 1` | `8,191,370,387 / 120,663` | `120 / 41` | `5,575,780,000 / 48,475` | `65 / 29` | +| 4 | `2 / 1` | `15,677,082,549 / 184,897` | `168 / 98` | `5,575,780,000 / 48,475` | `65 / 29` | + +Total Control is `66,021,360,302 / 773,416`, with generated I/O `724 / 405`. Effects independently total `17,876,810,000 / 184,616` and `205 / 94` I/O: five Transfers use `task_transfer` (`25 / 12` each), and two failed exact-output attempts still charge `task_dex_exact_out` (`3,334,030,000 / 19,253`, `40 / 17` each). Failure does not mean the Task was uninvoked. Idle contributes only the baseline `936,601,027 / 6,231` per block; the reconstructed Prepass totals match exactly. Whole-cohort Control normalization is `144.8 / 81` generated reads/writes per successful Step, including zero-Step completion, apoptosis, failed attempts and baseline work—not a per-Actor physical allocation. + +This closes W5's bounded zero-Step, Running, short-retry, terminal and admission-apoptosis accounting. It does not establish funded User Action/Machine fee paths, longer retry/wakeup variants, reactive or other lifecycle/Task profiles, a removable-cost claim, or release acceptance. + + + +### Current EXP-0095 Refresh + +The ledger was rerun with generated Actors Weight SHA-256 `9ce37ade391a29d4f60779f1887ad242dfeb4919d25cf0047ffba85d18f33901` and exact production Wasm SHA-256 `25b9695fd9e900f17ae1f3fb0b815ac1403830264d9c31f7cee54e29f434b700`. Command and raw-log identity are recorded by EXP-0073. The ledger now explicitly selects `scheduler_inner_opening_user_complete_header_max` for the User complete-Opening owner; substituting the old zero-tail System minimum is rejected. + +| Block | Attempts / successful Transfers | Control RefTime / ProofSize | Generated Control reads / writes | Effect RefTime / ProofSize | Generated effect reads / writes | +| --- | --- | --- | --- | --- | --- | +| 2 | `3 / 3` | `43,777,492,344 / 471,448` | `436 / 266` | `6,880,299,000 / 87,666` | `75 / 36` | +| 3 | `2 / 1` | `8,555,490,142 / 120,663` | `120 / 41` | `5,708,130,000 / 48,475` | `65 / 29` | +| 4 | `2 / 1` | `16,089,364,725 / 184,897` | `168 / 98` | `5,708,130,000 / 48,475` | `65 / 29` | + +Current total Control is `68,422,347,211 / 777,008`, with generated I/O `724 / 405`. Effects total `18,296,559,000 / 184,616`, with generated I/O `205 / 94`. Each native ledger is linked to its exact-Wasm execution by the same authored body and pre-state, matching header and post-state root, and successful proof-only verification. The owner frequencies and generated I/O are unchanged; changed generated RefTime and the block-2 completion ProofSize are not carried forward from EXP-0065. + +## Derived Metrics + +Retain the extraction's whole-cohort normalization only. No per-Actor allocation, physical I/O estimate or new Pareto claim is derived. + +## Statistical / Variance Notes + +Exact generated-owner equations and three header matches support this bounded reconstruction; no new runtime sampling occurred in fission. + +## Result + +All three W5 headers and Control/effect phase sums match the retained evidence; named terms preserve complete apoptosis settlement and separately charged failed Task attempts. + +## Interpretation + +The bounded accounting closes without treating charged envelope operations as actual storage operations. It does not isolate removable lifecycle cost or a universal minimum; Pareto comparison is not applicable. + +## Decision + +Accepted for the bounded current W5 accounting claim. The EXP-0095 refresh reconstructs all three native phase sums with current generated owners and links the same bodies and pre-states to successful exact-Wasm execution and proof verification. It preserves complete apoptosis replacement, independently charged failed Tasks and the exclusion of invented continuation/wakeup charges; broader lifecycle and funded User paths remain outside scope. + +## Rejected Alternatives and Tradeoffs + +- `Double-counted apoptosis`: Ordinary entry or generic close atop complete settlement is rejected by owner reconstruction. +- `Invented continuation/wakeup`: A continuation event is not an additional charged owner; the short retry retains Ready placement. +- `Failure means uninvoked`: Rejected by both failed exact-output attempts' effect accounting. + +## Validity + +- `Establishes`: Bounded W5 zero-Step, Running, short-retry, terminal and admission-apoptosis charged accounting. +- `Does not establish`: Funded User fees, long retry/wakeup, reactive/other Task profiles, optimization or release acceptance. +- `Known limitations`: Native reconstruction is not instrumented Wasm execution; historical raw-log availability is not newly verified. +- `Invalidation triggers`: Production selectors, owner models, lifecycle state geometry, Weight, source or runtime identity change. +- `Residual qualitative insight`: Complete-owner settlement replaces provisional work; generated charges need not equal executed storage operations. + +## Follow-Up / Next Gradient + +Parent resource attribution → bounded W5 accounted → no new measured gap in this claim → stop. Parent decides remaining independently owned obligations; do not broaden this Leaf to arbitrary lifecycle coverage. + +## Architecture Impact + +No production instrumentation, selector, Weight or geometry changed. EXP-0086 is scoped accounting provenance only. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: [EXP-0066](./EXP-0066.md) O17. +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: None. +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: [EXP-0065](./EXP-0065.md) exact binding; [EXP-0073](./EXP-0073.md) W5 lifecycle timeline/headers; [EXP-0082](./EXP-0082.md) Transfer effect accounting; [EXP-0083](./EXP-0083.md) no-due/startup owners. +- `Transfers question to`: None. +- `Produces input for`: [EXP-0066](./EXP-0066.md) O17 bounded lifecycle accounting. +- `Reopen trigger`: A changed owner/state/artifact or reproducible accounting mismatch; cost alone never reopens frozen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0087.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0087.md new file mode 100644 index 00000000..7d72b01e --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0087.md @@ -0,0 +1,126 @@ +# EXP-0087 — User and Reactive Target Population Coverage + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | [EXP-0066](./EXP-0066.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-08 | +| Affected domain | Required production target population | +| Physical mechanism | Production population admission and service | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | None | +| Successor experiment | None | + +## Decision Question + +Can production population coverage support the required User/reactive target decision rather than substitute System schedule proxies? + +## Governing Specification + +- `Exact section`: `docs/actors-performance-assurance.specification.en.md` §§1.2, 2 owns target metrics and populations; `template/pallets/actors/docs/specification.en.md` §2.1 owns invariants. BACKLOG owns open G6/G8 gates. +- `Invariant`: Class-neutral FIFO, Q1, causal floor and component-wise admission remain unchanged. +- `Reopen trigger`: Evidence that the required population demands semantic changes; route to specification, not acceptance here. + +## Context and Hypothesis + +- `Reason`: Already-open G6 population coverage is not certified by System witnesses. +- `Hypothesis`: Qualified admitted User/reactive populations can provide the required target evidence. +- `Materiality`: Governing production target or an explicit evidenced reachable-population constraint, not a proxy success. + +## Baseline and Candidates + +The governing target is fixed by performance-assurance §2.1: 10,000 prefunded one-Step User ObservationCrossing Actors complete within 100 eligible blocks. Current production admission is the candidate; System schedules are excluded. + +## Controls + +- `Controls`: Bind source, Weight, Wasm, demand and eligibility before Prepared. +- `Changed variables`: Population coverage; comparison not prepared. +- `Confounders`: User Opening qualification and unequal eligibility can invalidate comparison. + +## Benchmark Class, Workloads, and Criteria + +- `Benchmark class`: Intended full-runtime production profiles; fixtures not prepared. +- `Workload`: Required User/reactive Actor-only and signed-demand populations; fixture not prepared. +- `Metrics`: Completed Cycles, committed Steps, distinct progressed Actors, eligible blocks and independent Weight dimensions. +- `Acceptance criteria`: Must bind the governing target to its actual population; acceptance domain not prepared. +- `Rejection criteria`: Proxy substitution, altered invariants or unsupported extrapolation. + +## Environment and Artifact Identity + +- `Initial inspected source`: Pre-fission EXP-0066, SHA-256 `973bb0dd5f63430da3bff5aa1cc74a6df35b22b48510f47c44abf7dd8519f923`; [migration manifest](./experiments.md#current-decision-critical-path) owns baseline provenance. +- `Constraint evidence`: `/tmp/deos-0725-exp0087-constraint.log`, SHA-256 `b645901e5f60c5668b5de2eed45102c64b5a9561afad1ffe1348e0b3d3a179bb`. + +## Proof Obligations + +- `Freeze`: Frozen for the governing 10,000-User ObservationCrossing / 100-eligible-block target against current EXP-0095 production bounds and generated admission. +- `Review triggers`: Independent population, selector, mechanism or acceptance domain discovered during preparation. +- `Decomposition review`: One coverage claim only; narrow or split independent User/reactive owners BEFORE Prepared. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | Required target population is covered | System proxy substituted for required population | Production population profile | Self | Required | Parent O18 target decision | Accepted negative result: current target is unreachable | + +## Measurements + +Current runtime permits at most 9,000 User Crossing memberships on one feed, below the required 10,000-User fixture. Independently, generated component-wise Crossing admission permits four candidates per block. Therefore 100 eligible blocks can materialize at most 400 candidates, only 4% of the required population, before scheduler/effect service is considered. + +The configured 9,000-User reachable population requires at least 2,250 uncontended detector blocks; the 10,000 total-member bound requires 2,500. These are generated admission lower bounds, not extrapolations from System schedule throughput. + +## Derived Metrics + +`400 / 10,000 = 4%` maximum target coverage in the required window. Both fixture admission (`9,000 < 10,000`) and detector horizon (`2,500 > 100`) independently falsify the current target. + +## Statistical / Variance Notes + +Deterministic configured bounds and generated two-dimensional admission; no sampling, censoring or sustained-service inference. + +## Result + +The required User/reactive target is not constructible and cannot complete under current production Crossing admission. Existing System witnesses cannot repair either falsifier. + +## Interpretation + +This is an explicit evidenced reachable-population constraint, not an architecture regression claim or permission to substitute a smaller/System profile. + +## Decision + +Accepted negative result. Current production cannot satisfy performance-assurance §2.1/§2.2 and the same Crossing ingress bound blocks §2.3 before Task outcome. Parent acceptance must retain this explicit constraint or reopen target/geometry through its owning contract. + +## Rejected Alternatives and Tradeoffs + +- `System proxy`: Rejected because target identity is User ObservationCrossing. +- `9,000 as 10,000`: Rejected because configured admission makes the fixture nonconstructible. +- `Scheduler throughput substitution`: Rejected because detector materialization alone exceeds the window. + +## Validity + +Establishes target impossibility under current bounds and binding. Does not measure a smaller accepted target, sustained fairness or Task throughput. Population, target, binding or Crossing admission changes require review. + +## Follow-Up / Next Gradient + +Carry the explicit constraint into EXP-0066 and G8; do not claim the historical target or replace it with System evidence. + +## Architecture Impact + +None. C1 freeze and current architecture remain unchanged. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: [EXP-0066](./EXP-0066.md) O18. +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: [EXP-0067](./EXP-0067.md) qualified User Opening binding before production acceptance. +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: [EXP-0070](./EXP-0070.md) System schedule proxies, [EXP-0071](./EXP-0071.md) bounded geometry, [EXP-0074](./EXP-0074.md) arrival eligibility and [EXP-0077](./EXP-0077.md) User resource limitations; no raw evidence copied. +- `Transfers question to`: None. +- `Produces input for`: [EXP-0066](./EXP-0066.md) required population coverage decision. +- `Reopen trigger`: Governing target, User feed cap, Crossing admission or exact binding changes; cost alone does not reopen frozen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0088.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0088.md new file mode 100644 index 00000000..29530cd3 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0088.md @@ -0,0 +1,127 @@ +# EXP-0088 — Funded User Fee-Path Resource Coverage + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | [EXP-0066](./EXP-0066.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-08 | +| Affected domain | Funded User resource accounting | +| Physical mechanism | Disjoint Pipeline and Action charge mapping | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | None | +| Successor experiment | None | + +## Decision Question + +Does the qualified funded User path have a complete, disjoint Pipeline/Action resource mapping? + +## Governing Specification + +- `Exact section`: `template/pallets/actors/docs/specification.en.md` §§3.4, 7.3–7.6 owns fee/control boundaries; `docs/actors-resource-policy.specification.en.md` §§1, 4 owns resource domains and admission/actual Weight. BACKLOG owns open G4/G6 gates. +- `Invariant`: Pipeline authority and pay-as-attempted Action fees remain separate; complete-owner settlement cannot borrow another owner's charge. +- `Reopen trigger`: A required semantic fee change must reopen specification, not become a resource optimization. + +## Context and Hypothesis + +- `Reason`: G6 funded User accounting remains open after the G4 selector gap. +- `Hypothesis`: Qualified production bindings can map every executed funded User charge exactly once. +- `Materiality`: A missing, duplicated or misattributed charge changes resource soundness. + +## Baseline and Candidates + +The bounded baseline is the current funded one-Step StopCycle pair: identical System/User Contract geometry, Manual readiness and sovereign funding, differing only in authority and fee collection. A separate current header-domain witness compares OwnerOnly with maximum SignedAllowlist funding heads under the same source-derived completion context. + +## Controls + +- `Controlled variables`: Prepare qualified binding, source/Wasm, funding, Contract, outcome and admission state together. +- `Changed variables`: Funded User charge-map coverage; comparison not prepared. +- `Confounders`: Unqualified Opening settlement, maximum/actual aliasing and signed-state evidence mismatch. + +## Benchmark Class, Workloads, and Criteria + +- `Benchmark class`: Intended qualified production-path resource evidence. +- `Workload`: The measured StopCycle pair exposes useful Trigger and complete Pipeline collection without Action invocation. A successful funded User Action path remains required before decision. +- `Metrics`: Separate RefTime, ProofSize, generated I/O, admission/actual settlement and fee owners; generated I/O is not physical accesses. +- `Acceptance criteria`: Complete disjoint executed-owner mapping after G4 qualification; acceptance domain not prepared. +- `Rejection criteria`: Missing owner, overlap, unqualified selector or unsupported native-to-Wasm attribution. + +## Environment and Artifact Identity + +- `Initial inspected source`: Pre-fission EXP-0066 SHA-256 `973bb0dd5f63430da3bff5aa1cc74a6df35b22b48510f47c44abf7dd8519f923`; [migration manifest](./experiments.md#current-decision-critical-path) owns baseline provenance. +- `Current binding`: Generated Actors Weight SHA-256 `9ce37ade391a29d4f60779f1887ad242dfeb4919d25cf0047ffba85d18f33901`; EXP-0095 production Wasm SHA-256 `25b9695fd9e900f17ae1f3fb0b815ac1403830264d9c31f7cee54e29f434b700` is not yet executed by these native witnesses. +- `Current raw evidence`: `/tmp/deos-0725-exp0088-funded-user.log`, SHA-256 `285585d8e1547ce646c1c9acaf23691808231f46486574a4828c361bb0a6a653`; `/tmp/deos-0725-exp0088-header-domain.log`, SHA-256 `2eedd8179734a074597ea2a8eba6aaf72754bf7cd6dcf2dceab15e5263765005`. + +## Proof Obligations + +- `Freeze`: Frozen for the two declared O1/O2 proof obligations and current EXP-0095 binding before further Action-path measurement. +- `Review triggers`: Independent fee owner, selector, reachable domain or acceptance criterion. +- `Decomposition review`: One disjoint mapping claim; narrow or split independently decidable branches BEFORE Prepared. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | Funded User Pipeline collection maps completely and separately from Trigger and Action | Missing, duplicated or incorrectly attributed Pipeline charge | Native full-Executive monetary ledger on current generated binding | Self | Required | Parent O19 Pipeline owner | Accepted for bounded StopCycle witness | + +## Measurements + +`full_executive_funded_pipeline_collection_witness` passes through the canonical runtime test route. The User StopCycle row charges Trigger fee `10,351,120` and Pipeline fee `96,179,991`; the System row charges neither. Both settle `13,614,382,122 / 141,953` Control, `12,650,082,748 / 135,722` in Prepass and zero effect. Sovereign and Fee Sink balance deltas equal each typed fee exactly once, and no `ActionFeeCharged` event occurs because StopCycle invokes no Action. + +`full_executive_user_completion_header_domain_witness` also passes. OwnerOnly and maximum SignedAllowlist heads differ in encoded bytes (`157/1` versus `670/514`) but share source-derived completion context `[0,1,0,0,0,0,40]`, stored Control `2,898,615,858 / 33,863` and block Control `13,614,382,122 / 141,953`. This supports context-domain aliasing only; encoded-byte growth is not proof growth. + +These are native full-Executive monetary/state witnesses. They do not execute EXP-0095 Wasm and do not cover attempted Action collection. + +## Derived Metrics + +Pipeline fee is positive only for User authority while Control is identical in the paired StopCycle geometry. No Action fee can be derived from a zero-Action Task. + +## Statistical / Variance Notes + +Two deterministic native witnesses; model/generated Weight, monetary balances and exact-Wasm execution remain distinct evidence classes. + +## Result + +The bounded funded User Trigger/Pipeline path is measured and nonduplicated. The independent Action-path question is transferred to EXP-0097 because this witness intentionally invokes no Action. + +## Interpretation + +Current evidence closes bounded Pipeline collection and funding-header aliasing only. It does not decide EXP-0097's Action path or support a numerical underweight claim. + +## Decision + +Accepted for the bounded funded User Trigger/Pipeline collection claim. The successful funded Action question is independently owned by EXP-0097; no C1 reopening follows. + +## Rejected Alternatives and Tradeoffs + +No candidate adjudicated. Equal System/User charged Weight alone is not numerical underweight evidence. + +## Validity + +Open gap only, not complete accounting. Fixtures and acceptance domain not prepared. Binding, fee, funding or Weight changes require review. + +## Follow-Up / Next Gradient + +Stop. Preserve O1 without rerunning it unless a relevant input changes; EXP-0097 owns the successful funded User Action follow-up. + +## Architecture Impact + +None; no coefficient patch, overlapping aggregate or C1 reopening. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: [EXP-0066](./EXP-0066.md) O19. +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: [EXP-0067](./EXP-0067.md) User Opening binding qualification. +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: [EXP-0082](./EXP-0082.md) Transfer charge separation, [EXP-0084](./EXP-0084.md) System ledger and [EXP-0085](./EXP-0085.md) signed-demand linkage limitations; reuse only in scope. +- `Transfers question to`: [EXP-0097](./EXP-0097.md) — successful funded User Action fee-path coverage. +- `Produces input for`: [EXP-0066](./EXP-0066.md) funded User accounting decision. +- `Reopen trigger`: Changed fee semantics, owner mapping, binding or reproducible monetary/resource mismatch; cost alone does not reopen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0089.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0089.md new file mode 100644 index 00000000..a0a16f76 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0089.md @@ -0,0 +1,128 @@ +# EXP-0089 — Reactive Executed-Path Resource Coverage + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | [EXP-0066](./EXP-0066.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-08 | +| Affected domain | Reactive resource composition | +| Physical mechanism | Detector/fanout/service executed-owner mapping | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | None | +| Successor experiment | None | + +## Decision Question + +Can one qualified reactive executed path compose detector, deferred fanout and scheduled service charges without omission or duplication? + +## Governing Specification + +- `Exact section`: `template/pallets/actors/docs/specification.en.md` §§4.2, 4.3, 4.6 owns reactive semantics; `docs/actors-resource-policy.specification.en.md` §§4, 6 owns admission/actual Weight and phase composition. BACKLOG owns open G6 gates. +- `Invariant`: One paid useful latch transition; deferred fanout never executes Actors; scheduler alone owns service. +- `Reopen trigger`: A proposed change to causal or fee semantics belongs to specification. + +## Context and Hypothesis + +- `Reason`: Temporal and System accounting do not establish remaining reactive executed-path composition. +- `Hypothesis`: Qualified reactive resource ownership composes exactly once across detector/fanout/service. +- `Materiality`: Any omitted or duplicated executed owner defeats complete-path accounting. + +## Baseline and Candidates + +The selected family is broad observation fanout, whose deferred pages and service path share one composition question. ObservationCrossing uses an independent transition worker and is transferred to EXP-0098 rather than borrowing broad-fanout owners. + +## Controls + +- `Controlled variables`: Prepare exact source, bindings, Wasm, Trigger family, subscribers, latch state and service eligibility. +- `Changed variables`: Reactive executed-path coverage; comparison not prepared. +- `Confounders`: Shared versus Actor-specific work, latched redundancy and User Opening qualification. + +## Benchmark Class, Workloads, and Criteria + +- `Benchmark class`: Intended qualified reactive production-path evidence. +- `Workload`: Smallest User broad-observation occurrence through deferred fanout and scheduled one-Step Transfer service; exact fixture remains to be prepared. +- `Metrics`: Independent RefTime/ProofSize, generated I/O, shared and Actor-specific charges, actual/admitted owners. +- `Acceptance criteria`: Complete disjoint composition for a declared path; acceptance domain not prepared. +- `Rejection criteria`: Missing/duplicate owner or extrapolation across an independently decidable family. + +## Environment and Artifact Identity + +- `Initial inspected source`: Pre-fission EXP-0066 SHA-256 `973bb0dd5f63430da3bff5aa1cc74a6df35b22b48510f47c44abf7dd8519f923`; [migration manifest](./experiments.md#current-decision-critical-path) owns provenance. +- `Current binding`: EXP-0095 generated Actors Weight and production Wasm identities. +- `Complete native ledger`: `/tmp/deos-0725-exp0089-complete.log`, SHA-256 `857da0a634425cec3972b83034409f966362ec70f08619c087bd9c81e0ef12c4`. +- `Crossing boundary`: EXP-0074 chronology transfers to EXP-0098; it is not evidence for this broad-fanout path. + +## Proof Obligations + +- `Freeze`: Frozen for one broad-observation detector/fanout/service composition claim; fixture preparation may narrow workload but cannot add another family or owner. +- `Review triggers`: Independent Trigger family, Weight owner, reachable state or acceptance domain. +- `Decomposition review`: One composition claim; narrow or split independent owners BEFORE Prepared, not a detector benchmark portfolio. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | Reactive executed owners compose exactly once | Missing or duplicate detector/fanout/service charge | Qualified reactive production-path evidence | Self | Required | Parent O20 reactive resource coverage | Accepted for selected broad-observation path | + +## Measurements + +`reactive_delivery_envelopes_follow_production_weights_and_topology_bounds` now pins current generated fanout envelopes: base `56,775,000 / 1,629`, branch probe `64,247,000 / 3,587`, component-wise page unit `174,549,555,000 / 304,734`, and fault contingency `209,851,000 / 4,106`. Two page units fit the configured `400,000,000,000 / 1,000,000` fanout limit. This is generated admission/topology evidence, not executed owner frequency or physical I/O. + +`observation_change_single_user_binds_fanout_and_scheduled_service` executes one funded User ObservationChange path. The fixture binds ingress to `observation_change_ingress`, then settles fanout as generated base plus branch probe plus one ordinary admitted branch unit: `174,670,577,000 / 309,950`. Fanout only latches one signal and ready ticket. Next-block scheduler service owns Control `12,954,769,748 / 141,918`, separate Transfer effect `2,293,433,000 / 29,222`, zero User dispatch and exactly one Trigger occurrence; the recipient gains one unit and latch/ticket clear. Crossing chronology and costs remain excluded. + +## Derived Metrics + +At most two maximum fanout page units fit the configured envelope. The selected one-page path consumes exactly one admitted ordinary branch unit plus shared base/probe; service executes only in the next block. + +## Statistical / Variance Notes + +One deterministic native production-runtime path plus generated owner identities; generated I/O is not relabeled physical access or exact-Wasm proof. + +## Result + +The selected broad-observation ingress/fanout/service composition is complete without Crossing-owner borrowing. + +## Interpretation + +Ingress, fanout and service retain separate owners. Fanout does not execute the Actor, and scheduler service does not repay fanout. + +## Decision + +Accepted for the selected funded User ObservationChange-to-Transfer path. EXP-0098 independently owns Crossing; no broader population or fairness acceptance follows. + +## Rejected Alternatives and Tradeoffs + +- `Crossing substitution`: Rejected because Crossing has an independent worker and accepted EXP-0098 owner. +- `Fanout-as-service`: Rejected because deferred fanout only latches scheduler authority. +- `Generated I/O as physical proof`: Rejected; this is Weight-owner evidence. + +## Validity + +Establishes one broad ObservationChange complete-path resource composition. Does not establish population-scale service, exact-Wasm proof, Crossing or sustained fairness. Trigger, binding, latch or Weight changes require review. + +## Follow-Up / Next Gradient + +Proceed to remaining User/reactive population, fairness and stop obligations. + +## Architecture Impact + +None; no parallel authority or proof discount. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: [EXP-0066](./EXP-0066.md) O20. +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: [EXP-0067](./EXP-0067.md) qualification for funded User paths. +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: [EXP-0078](./EXP-0078.md) temporal phase ownership, [EXP-0079](./EXP-0079.md) generated charge/admission and [EXP-0080](./EXP-0080.md) matched-phase proof limits; no raw evidence copied. +- `Transfers question to`: [EXP-0098](./EXP-0098.md) — independent ObservationCrossing resource composition. +- `Produces input for`: [EXP-0066](./EXP-0066.md) reactive resource decision. +- `Reopen trigger`: Changed Trigger family, generation/latch semantics, binding or reproducible owner mismatch; cost alone does not reopen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0090.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0090.md new file mode 100644 index 00000000..ff14df3c --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0090.md @@ -0,0 +1,133 @@ +# EXP-0090 — Heavy Economic Task Resource Coverage + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | [EXP-0066](./EXP-0066.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-08 | +| Affected domain | Heavy economic effect capacity | +| Physical mechanism | Liquidity-heavy Task Shared Economic admission | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | None | +| Successor experiment | None | + +## Decision Question + +Does a qualified liquidity-heavy effect path have complete resource coverage at its Shared Economic capacity boundary? + +## Governing Specification + +- `Exact section`: `template/pallets/actors/docs/specification.en.md` §§6.6, 11.1 owns Task/effect semantics; `docs/actors-resource-policy.specification.en.md` §§1, 3, 4 owns Shared Economic capacity and settlement; `docs/actors-performance-assurance.specification.en.md` §1.3 owns workload coverage. BACKLOG owns open G6/G8 gates. +- `Invariant`: Atomic Task effects, independent committed prefixes and component-wise Weight admission remain unchanged. +- `Reopen trigger`: Required economic or admission semantics changes must reopen specification. + +## Context and Hypothesis + +- `Reason`: Transfer and heterogeneous-service witnesses do not cover required liquidity-heavy effect capacity. +- `Hypothesis`: Qualified effect ownership contains actual liquidity-heavy work at the Shared Economic boundary. +- `Materiality`: An uncovered effect or capacity limit defeats complete-path resource soundness. + +## Baseline and Candidates + +Selected successful native-staking `DonateLiquidity` as the first liquidity-heavy candidate. Add/remove liquidity and capacity-frontier claims remain independent until evidence selects whether they are required. + +## Controls + +- `Controlled variables`: Prepare source/Wasm, adapter and Weight identity, pool state, Task bounds and local capacity. +- `Changed variables`: Liquidity-heavy effect coverage; comparison not prepared. +- `Confounders`: Pool health, slippage, adapter branches and maximum/actual confusion. + +## Benchmark Class, Workloads, and Criteria + +- `Benchmark class`: Intended qualified complete production-path resource evidence. +- `Workload`: Smallest liquidity-heavy effect reaching its Shared Economic boundary; fixture not prepared. +- `Metrics`: Effect RefTime/ProofSize, actual/admitted capacity, generated I/O and atomic outcome. +- `Acceptance criteria`: Complete effect coverage and capacity containment in one declared domain; domain not prepared. +- `Rejection criteria`: Missing owner, adverse semantic change or extrapolation from cheap Tasks. + +## Environment and Artifact Identity + +- `Initial inspected source`: Pre-fission EXP-0066 SHA-256 `973bb0dd5f63430da3bff5aa1cc74a6df35b22b48510f47c44abf7dd8519f923`; [migration manifest](./experiments.md#current-decision-critical-path) owns provenance. +- `Accepted production-prepass frontier evidence`: `/tmp/deos-0725-exp0092-prepass-stop.log`, SHA-256 `459e92a79aae1c316ab2b594c104797c85820a58f249dc20f8c4d2cb7b9818e8`. + +## Proof Obligations + +- `Freeze`: Frozen for one successful System native-staking `DonateLiquidity` complete path; the Shared Economic capacity-frontier domain remains open and cannot be inferred from one execution. +- `Review triggers`: Independent Task, adapter, Weight owner or capacity acceptance domain. +- `Decomposition review`: Narrowed Leaf, not Task/lifecycle Synthesis. Split independently decidable economic owners BEFORE Prepared; lifecycle evidence remains EXP-0086 and long retry EXP-0093. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | Heavy effect coverage contains Shared Economic work | Uncovered liquidity-heavy effect or capacity limit | Qualified complete-path resource evidence | Self | Required | Parent O21 economic Task coverage | Accepted for selected donation cohort | + +## Measurements + +The existing production-runtime `system_actor_executes_native_staking_lp_donation_task` now binds the successful donation to complete block-local resource usage: + +| Owner | Weight | +| --- | ---: | +| Actor Control | `14,152,748,935 / 155,278` | +| Liquidity effect | `1,206,242,000 / 14,035` | +| User dispatch | `0 / 0` | + +The effect fits both Shared Economic `604,205,326,486 / 1,352,300` and Actor base-turn `302,102,663,243 / 676,150` limits component-wise. Pool balances each increase from 400 to 440, LP issuance remains unchanged, the sovereign retains the one-unit base remainder and `LiquidityDonated` reports exact 40/40 amounts. + +A 100-Actor homogeneous continuation exposes the first actual stop: exactly the FIFO prefix of 12 donations executes; 88 retain pending signals and tickets. Production-prepass actual usage is Control `33,660,842,618 / 577,863` and effect `14,474,904,000 / 168,420`. Both remain below their block limits because admission reserves complete prospective owners rather than settling from historical actual usage. The next complete prospective owner is Control `12,924,472,858 / 127,063` plus effect `1,206,242,000 / 14,035`; its stored Step envelope is Control `3,271,396,813 / 45,638` and the same effect owner. After the 12th actual settlement, the next Control RefTime fits but its `127,063` ProofSize exceeds remaining Actor Control ProofSize, while the complete effect still fits remaining Shared Economic. The first stop is therefore Actor Control ProofSize-bound, not Shared Economic-bound. + +## Derived Metrics + +The exact prospective owner proves the 12-Actor stop is Actor Control ProofSize-bound. The independent liquidity effect remains contained in Shared Economic and Actor base-turn capacity; no final-remainder inference is used. + +## Statistical / Variance Notes + +One deterministic native production-runtime execution; no host timing distribution or exact-Wasm state identity inferred. + +## Result + +Successful `DonateLiquidity` ownership, limit containment, 12-of-100 actual FIFO frontier and prospective binding component are qualified. + +## Interpretation + +The selected liquidity effect is complete but does not bind first: conservative Actor Control ProofSize admission stops the homogeneous cohort after 12. This is an explicit capacity result, not a claim that Shared Economic saturates. + +## Decision + +Accepted for the selected homogeneous System `DonateLiquidity` cohort. The first stop is source-bound and the complete effect owner remains covered; Add/RemoveLiquidity and sustained fairness remain outside scope. + +## Rejected Alternatives and Tradeoffs + +- `Transfer substitution`: Rejected because it does not exercise liquidity state. +- `Single-path frontier claim`: Rejected because one contained effect does not identify the binding cohort dimension. + +## Validity + +Establishes one successful System donation's resource ownership, state outcome and component-wise containment. Does not establish Add/RemoveLiquidity, capacity frontier, exact-Wasm linkage or sustained service. + +## Follow-Up / Next Gradient + +Proceed to remaining broad-observation resource and service/target obligations. + +## Architecture Impact + +None; lifecycle and economic effect owners remain separate. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: [EXP-0066](./EXP-0066.md) O21. +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: [EXP-0067](./EXP-0067.md) qualification before funded User complete-path acceptance. +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: [EXP-0072](./EXP-0072.md) heterogeneous-service limits, [EXP-0082](./EXP-0082.md) Transfer accounting and [EXP-0086](./EXP-0086.md) separate lifecycle scope; no raw evidence copied. +- `Transfers question to`: None. +- `Produces input for`: [EXP-0066](./EXP-0066.md) heavy economic coverage decision. +- `Reopen trigger`: Selected adapter, Task, pool semantics, Weight or capacity domain changes; cost alone does not reopen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0091.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0091.md new file mode 100644 index 00000000..914fe99c --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0091.md @@ -0,0 +1,130 @@ +# EXP-0091 — Sustained Service Evidence Sufficiency + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | [EXP-0066](./EXP-0066.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-08 | +| Affected domain | Sustained service evidence | +| Physical mechanism | Eligibility-aware FIFO service observation | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | None | +| Successor experiment | None | + +## Decision Question + +Can eligibility-aware sustained observations support the required service fairness claim beyond bounded censoring? + +## Governing Specification + +- `Exact section`: `docs/actors-performance-assurance.specification.en.md` §1.2 owns service metrics; `template/pallets/actors/docs/specification.en.md` §2.1 owns invariants. BACKLOG owns open G6 gates. +- `Invariant`: Class-neutral FIFO, immutable cutoff, Q1 and causal floor remain unchanged. +- `Reopen trigger`: A different fairness promise belongs to specification, not interpretation of censored data. + +## Context and Hypothesis + +- `Reason`: Bounded outcomes and censored waits do not prove the required sustained service scope. +- `Hypothesis`: Correctly classified eligibility and observation windows can decide service sufficiency without treating inter-cycle waits as Running bounds. +- `Materiality`: Unsupported fairness scope prevents the parent service decision. + +## Baseline and Candidates + +The required fairness scope is the 10,000-User ObservationCrossing target over 100 eligible blocks. Accepted bounded candidates are EXP-0077's one-cycle 100-due-Actor frontier, EXP-0084's sustained System schedules and EXP-0087's exact current User/reactive reachability constraint. + +## Controls + +- `Controlled variables`: Prepare source/Wasm, binding, arrivals, eligibility, demand and observation window. +- `Changed variables`: Eligibility-aware evidence coverage; comparison not prepared. +- `Confounders`: Left/right censoring, dormant/inter-cycle waits, starvation versus ineligibility and signed-demand linkage. + +## Benchmark Class, Workloads, and Criteria + +- `Benchmark class`: Intended eligibility-aware production service evidence. +- `Workload`: Smallest sustained eligible cohort that can falsify the claimed fairness scope; fixture not prepared. +- `Metrics`: Eligible waiting intervals, progress, cutoff/head history and explicit censored observations. +- `Acceptance criteria`: Evidence adequate for a declared required service claim; domain/window not prepared. +- `Rejection criteria`: Censoring or inter-cycle waits mislabeled as bounded Running service. + +## Environment and Artifact Identity + +- `Initial inspected source`: Pre-fission EXP-0066 SHA-256 `973bb0dd5f63430da3bff5aa1cc74a6df35b22b48510f47c44abf7dd8519f923`; [migration manifest](./experiments.md#current-decision-critical-path) owns provenance. +- `Bounded User/frontier input`: EXP-0077 log SHA-256 `904105c1911d1af6cc88c1554c6e3dca456a4714f0444eb358f4d975073fece0`. +- `Sustained System input`: EXP-0084 accepted Manual/Cadenced/Mixed/Mixed-user ledgers. +- `Required-population constraint`: EXP-0087 log SHA-256 `b645901e5f60c5668b5de2eed45102c64b5a9561afad1ffe1348e0b3d3a179bb`. + +## Proof Obligations + +- `Freeze`: Frozen for deciding whether current accepted evidence supports the governing 10,000-User/100-eligible-block fairness scope; no smaller or System proxy may replace it. +- `Review triggers`: Independent fairness claim, eligibility domain, selector or evidence class. +- `Decomposition review`: One evidence-sufficiency claim; narrow or split independent domains BEFORE Prepared. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | Sustained evidence supports required fairness scope | Censored wait reported as a service bound | Eligibility-aware production service evidence | Self | Required | Parent O22 fairness sufficiency | Accepted negative: current evidence is insufficient | + +## Measurements + +EXP-0077 services one bounded cohort of 100 due Actors over `[14,17,17,17,17,17,1]` and proves 9,885 non-due identities add zero charged-Control delta. It does not reactivate those Actors, so its final observations are right-censored and establish no sustained `ServiceGapBlocks` distribution. + +EXP-0084 observes 100 blocks across full 10,000-identity System Manual, Cadenced, Mixed and Mixed-with-user populations. Those schedules establish sustained System accounting only; they do not instantiate the required prefunded User ObservationCrossing population or its trigger-to-service intervals. + +EXP-0087 independently proves the required fairness population is nonconstructible at the 9,000 User/feed cap and that Crossing detection can materialize at most 400 of 10,000 candidates within the 100-block window. Therefore no current observation window can support the governing fairness claim. + +## Derived Metrics + +No finite required-population `ServiceGapBlocks` bound is accepted. The 2,250-block minimum detector horizon for the reachable 9,000-User population is a materialization lower bound, not a scheduler service-gap estimate. + +## Statistical / Variance Notes + +EXP-0077's terminal one-cycle tail is explicitly right-censored for sustained fairness. System schedule distributions are population-mismatched. No p95/p99/maximum or unobserved-tail bound is inferred for the required User cohort. + +## Result + +Current evidence is insufficient for the required sustained User/reactive fairness scope, and the governing population/window is independently unreachable. + +## Interpretation + +The correct result is an explicit unsupported fairness claim, not a smaller inferred bound. Bounded FIFO progress and sustained System service remain valid within their own evidence scopes. + +## Decision + +Accepted negative result. Do not claim required-population sustained fairness for current production; carry the constraint into EXP-0066/G8. + +## Rejected Alternatives and Tradeoffs + +- `Final remainder as bound`: Rejected because EXP-0077's one-cycle tail is censored. +- `System schedule proxy`: Rejected because Actor type, trigger and funding population differ. +- `Detector horizon as service gap`: Rejected because materialization precedes scheduler eligibility. + +## Validity + +Establishes evidence insufficiency for the governing scope. Does not invalidate bounded FIFO/Q1 results or sustained System ledgers. Target, population, detector capacity or new eligibility-aware User evidence requires review. + +## Follow-Up / Next Gradient + +Compose EXP-0066 with the explicit target and fairness constraints; do not invent a replacement target. + +## Architecture Impact + +None; no service-priority policy is introduced. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: [EXP-0066](./EXP-0066.md) O22. +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: [EXP-0067](./EXP-0067.md) qualification for required funded User production service. +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: [EXP-0070](./EXP-0070.md) censored schedule waits, [EXP-0071](./EXP-0071.md) geometry service, [EXP-0072](./EXP-0072.md) heterogeneous service, [EXP-0074](./EXP-0074.md) eligibility and [EXP-0076](./EXP-0076.md) prefix pressure; bounded evidence only. +- `Transfers question to`: None. +- `Produces input for`: [EXP-0066](./EXP-0066.md) fairness sufficiency decision. +- `Reopen trigger`: Governing fairness target, User population reachability, Crossing admission or a new sustained eligibility-aware User witness; cost alone does not reopen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0092.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0092.md new file mode 100644 index 00000000..daa7cd67 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0092.md @@ -0,0 +1,126 @@ +# EXP-0092 — Remaining Service-Stop Evidence + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | [EXP-0066](./EXP-0066.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-08 | +| Affected domain | Production service-stop attribution | +| Physical mechanism | Phase-local service admission stop reconstruction | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | None | +| Successor experiment | None | + +## Decision Question + +Can the remaining production service stop be explained by its actual phase-local admission state rather than finalized maximum comparisons? + +## Governing Specification + +- `Exact section`: `docs/actors-resource-policy.specification.en.md` §§4–6 owns admission, FIFO fragmentation and block phases; `docs/actors-performance-assurance.specification.en.md` §1.2 owns exhausted-resource/capacity metrics; pallet specification §8.3 owns service ordering. BACKLOG owns open G6 gates. +- `Invariant`: Actual head, immutable cutoff, eligibility and component-wise local budgets govern service. +- `Reopen trigger`: Changing service order or admission semantics requires specification reopening. + +## Context and Hypothesis + +- `Reason`: Bounded Manual stop reconstruction does not certify remaining production paths. +- `Hypothesis`: Phase-matched head/eligibility/budget evidence can identify the actual remaining stop owner. +- `Materiality`: A false stop attribution misdirects resource and target decisions. + +## Baseline and Candidates + +Selected EXP-0090's 100-Actor homogeneous System `DonateLiquidity` queue under the production `actor_prepass` phase. This is non-Manual service and keeps its resource question in EXP-0090. + +## Controls + +- `Controlled variables`: Prepare exact source/Wasm, Weight, phase, head, cutoff, eligibility and local budget snapshots. +- `Changed variables`: Remaining production-path stop coverage; comparison not prepared. +- `Confounders`: Finalized totals, changing head/cutoff and unmatched proof phases. + +## Benchmark Class, Workloads, and Criteria + +- `Benchmark class`: Intended production-path phase-local admission evidence. +- `Workload`: Smallest non-Manual production stop witness; path and fixture not prepared. +- `Metrics`: Actual stop reason, local RefTime/ProofSize budgets, admitted owner, head and eligibility. +- `Acceptance criteria`: One actual stop reconstructed from matched-phase state; acceptance domain not prepared. +- `Rejection criteria`: Maximum-versus-finalized comparison or Manual extrapolation substituted for actual stop evidence. + +## Environment and Artifact Identity + +- `Initial inspected source`: Pre-fission EXP-0066 SHA-256 `973bb0dd5f63430da3bff5aa1cc74a6df35b22b48510f47c44abf7dd8519f923`; [migration manifest](./experiments.md#current-decision-critical-path) owns provenance. +- `Phase-local stop evidence`: `/tmp/deos-0725-exp0092-prepass-stop.log`, SHA-256 `459e92a79aae1c316ab2b594c104797c85820a58f249dc20f8c4d2cb7b9818e8`. + +## Proof Obligations + +- `Freeze`: Frozen for the selected production-prepass donation stop with exact cutoff, next live head, eligibility and local component budgets. +- `Review triggers`: Independent stop mechanism, phase, selector or acceptance domain. +- `Decomposition review`: One stop-sufficiency claim outside bounded Manual; narrow or split independent paths BEFORE Prepared. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | Remaining stop is supported by actual phase-local state | Finalized maximum comparison substituted for a stop | Production head/cutoff/eligibility and local-budget evidence | Self | Required | Parent O23 stop attribution | Accepted for selected non-Manual prepass | + +## Measurements + +The production prepass freezes the current ready tail as block-2 cutoff. Exactly the first 12 eligible donations execute in FIFO order; the 13th Actor remains the live ready head, its retained ticket is below the immutable cutoff, and all later Actors retain pending signal plus ticket. + +Phase-local actual usage is Control `33,660,842,618 / 577,863` and effect `14,474,904,000 / 168,420`. The next complete prospective owner is Control `12,924,472,858 / 127,063` plus effect `1,206,242,000 / 14,035`. At the stop, next-owner Control RefTime fits but ProofSize exceeds the local remaining Actor Control budget; the effect still fits remaining Shared Economic. This reconstructs the stop at admission time rather than comparing a finalized total with an unrelated maximum. + +## Derived Metrics + +The selected stop is Actor Control ProofSize-bound. The retained 13th eligible head disproves cutoff, eligibility and empty-queue explanations. + +## Statistical / Variance Notes + +One deterministic production-runtime prepass fixture; no sustained-service or host-timing distribution inferred. + +## Result + +The remaining selected non-Manual stop has exact phase, cutoff, head, eligibility, local-budget and binding-owner evidence. + +## Interpretation + +Prospective complete-owner ProofSize, not final remainder, controls the stop while FIFO and immutable cutoff remain intact. + +## Decision + +Accepted for the selected production-prepass donation stop. Manual and other phase-local stops retain their existing owners. + +## Rejected Alternatives and Tradeoffs + +- `Finalized-total comparison`: Rejected because the assertion uses live phase-local usage and next owner. +- `Cutoff/eligibility explanation`: Rejected because the retained head is eligible below the frozen cutoff. +- `Shared Economic stop`: Rejected because the complete next effect still fits. + +## Validity + +Establishes one non-Manual production-prepass stop. Does not establish all phases, sustained fairness or another Task family. Admission, phase, selected Task or budget changes require review. + +## Follow-Up / Next Gradient + +Proceed to remaining target-population and sustained-fairness obligations. + +## Architecture Impact + +None; no budget change or proof discount is introduced. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: [EXP-0066](./EXP-0066.md) O23. +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: [EXP-0067](./EXP-0067.md) qualification before funded User stop acceptance. +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: [EXP-0079](./EXP-0079.md) charge/admission separation, [EXP-0080](./EXP-0080.md) matched-phase proof and [EXP-0081](./EXP-0081.md) bounded Manual stop; scoped reuse only. +- `Transfers question to`: None. +- `Produces input for`: [EXP-0066](./EXP-0066.md) remaining stop decision. +- `Reopen trigger`: Prepass, cutoff, FIFO, selected Task, admission owner or budget changes; cost alone does not reopen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0093.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0093.md new file mode 100644 index 00000000..f737c0a7 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0093.md @@ -0,0 +1,133 @@ +# EXP-0093 — Long Retry Resource Placement + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | [EXP-0066](./EXP-0066.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-08 | +| Affected domain | Long retry resource accounting | +| Physical mechanism | Wakeup placement for long retry | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | None | +| Successor experiment | None | + +## Decision Question + +Does the long retry path through Wakeup have complete resource ownership distinct from retained short Ready retry evidence? + +## Governing Specification + +- `Exact section`: `template/pallets/actors/docs/specification.en.md` §§6.5, 8.2, 9.2 owns retry outcomes, readiness and terminal precedence; `docs/actors-resource-policy.specification.en.md` §§4, 6 owns complete admission/settlement and phases. BACKLOG owns open G6 gates. +- `Invariant`: Temporary Mutable-only retry, one Actor-owned Continuation and existing capped retry/wakeup path; committed prefixes survive. +- `Reopen trigger`: A second retry authority or changed retry semantics requires specification reopening. + +## Context and Hypothesis + +- `Reason`: Retained short Ready retry accounting does not cover long retry placement through Wakeup. +- `Hypothesis`: Qualified Wakeup placement and subsequent service can account for the long retry path without borrowing short-retry charges. +- `Materiality`: An omitted placement/service owner invalidates complete retry accounting. + +## Baseline and Candidates + +Selected the existing production-configured System `SwapIn` reference-deviation failure: attempt 1 retains Ready retry, attempt 2 places Wakeup at block 4, block 4 returns through Wakeup without same-block execution, and block 5 performs the terminal third attempt. + +## Controls + +- `Controlled variables`: Prepare source/Wasm, Weight, temporary failure, retry delay, Continuation, Wakeup and service state. +- `Changed variables`: Long-retry coverage; comparison not prepared. +- `Confounders`: Retry eligibility, delay boundary, prefix durability and maximum/actual settlement. + +## Benchmark Class, Workloads, and Criteria + +- `Benchmark class`: Intended qualified long-retry complete production-path evidence. +- `Workload`: Smallest temporary retry entering Wakeup then returning to service; boundary and fixture not prepared. +- `Metrics`: Placement/return/service RefTime, ProofSize, generated I/O, settlement and single continuation ownership. +- `Acceptance criteria`: Complete disjoint long-path accounting in a declared retry domain; domain not prepared. +- `Rejection criteria`: Short Ready charges substituted for Wakeup or duplicate continuation authority. + +## Environment and Artifact Identity + +- `Initial inspected source`: Pre-fission EXP-0066 SHA-256 `973bb0dd5f63430da3bff5aa1cc74a6df35b22b48510f47c44abf7dd8519f923`; [migration manifest](./experiments.md#current-decision-critical-path) owns provenance. +- `Execution artifact`: `/tmp/deos-0725-exp0093-ledger.log`, SHA-256 `22b82d4dd5e51f7aaa01955b1c5e86acf9098cf3a5d34886a66623af02d3ba0c`. + +## Proof Obligations + +- `Freeze`: Frozen for the selected three-attempt System `SwapIn` temporary reference-deviation path across Ready retry, long-Wakeup placement, return and terminal service. +- `Review triggers`: Independent retry owner, reachable domain, selector or acceptance criterion. +- `Decomposition review`: One retry placement claim. Split independent domains BEFORE Prepared; lifecycle excluded. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | Long retry has complete Wakeup resource ownership | Short Ready retry charge substituted for Wakeup path | Qualified long-retry complete-path evidence | Self | Required | Parent O24 retry coverage | Accepted | + +## Measurements + +The source-bound runtime fixture records four disjoint block-local settlements: + +| Phase | Actor Control | Actor effect | Outcome | +| --- | ---: | ---: | --- | +| First failed attempt / Ready retry | `15,253,398,980 / 165,779` | `3,443,678,000 / 19,253` | One continuation, Ready ticket | +| Second failed attempt / long placement | `6,526,601,980 / 90,386` | `3,443,678,000 / 19,253` | Same continuation, Wakeup block 4 | +| Wakeup return | `8,506,170,748 / 95,611` | `0 / 0` | Control-only return; no same-block Task | +| Returned terminal service | `15,650,484,980 / 172,272` | `3,443,678,000 / 19,253` | Third failed attempt, terminal cleanup | + +The actor balance is unchanged across all three atomic failed effects. The first two attempts retain one cursor-0 Continuation; Wakeup replaces rather than duplicates the Ready ticket. The block-4 return obeys `N -> N+1`, and terminal cleanup removes process/hot authority. + +## Derived Metrics + +Long placement differs from short Ready retry and cannot borrow its Control total. Wakeup return owns zero effect; the returned Task effect belongs only to the following block. + +## Statistical / Variance Notes + +One deterministic native production-runtime fixture; generated Weight settlement is not classified as physical I/O or host timing. + +## Result + +The selected long retry path has complete disjoint placement, return, resumed-service and terminal ownership. + +## Interpretation + +Short Ready retry, long-Wakeup placement, Control-only return and later service are distinct owners under one Continuation and one canonical scheduler. + +## Decision + +Accepted for the selected System `SwapIn` temporary-failure path. No broader Task, retry delay or User-path acceptance follows. + +## Rejected Alternatives and Tradeoffs + +- `Short Ready substitution`: Rejected because attempt 2 removes the ticket and places Wakeup. +- `Same-block return execution`: Rejected by the causal floor and zero-effect block-4 ledger. +- `Second continuation/scheduler`: Rejected; one cursor-0 Continuation traverses the canonical Ready/Wakeup substrate. + +## Validity + +Establishes the selected System long-retry resource placement and atomic no-fill behavior. Does not establish other Tasks, delays, User fees or sustained service. Retry, Wakeup, adapter or Weight changes require review. + +## Follow-Up / Next Gradient + +Proceed to remaining heavy-Task and reactive-fanout resource obligations. + +## Architecture Impact + +None; one canonical FIFO/wakeup substrate and Continuation remain unchanged. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: [EXP-0066](./EXP-0066.md) O24. +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: [EXP-0067](./EXP-0067.md) qualification before funded User complete-path acceptance. +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: [EXP-0073](./EXP-0073.md) prefix durability, [EXP-0079](./EXP-0079.md) temporal admission and [EXP-0086](./EXP-0086.md) short-retry accounting limits; no raw evidence copied. +- `Transfers question to`: None. +- `Produces input for`: [EXP-0066](./EXP-0066.md) long-retry resource decision. +- `Reopen trigger`: Retry semantics, delay, Wakeup, selected adapter or generated Weight changes; cost alone does not reopen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0094.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0094.md new file mode 100644 index 00000000..ceb0d54f --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0094.md @@ -0,0 +1,163 @@ +# EXP-0094 — Combined User Completion Header and Tail Envelope + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | None | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-08 | +| Affected domain | Funded nonzero-Step User Opening completion cross-product | +| Physical mechanism | Consumed-source User completion with maximum FundingSourcePolicy head and positive tail hold release | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0068](./EXP-0068.md) and [EXP-0069](./EXP-0069.md) scoped accepted axes | +| Successor experiment | [EXP-0067](./EXP-0067.md) — live selector qualification | + +## Decision Question + +Does a complete consumed-source User completion owner cover the reachable cross-product of maximum `SignedAllowlist` header width and positive Contract tails, without adding separately measured overlapping envelopes? + +## Governing Specification + +- `Semantics`: `template/pallets/actors/docs/specification.en.md` §§3.1, 3.4–3.5, 7.3–7.7. +- `Resource ownership`: `docs/actors-resource-policy.specification.en.md` §§1, 4–6 requires complete component-wise admission and actual settlement. +- `Invariant`: Preserve C1, FIFO, cutoff, Q1, causal floor, custody, Pipeline/Action separation, funding-only hold release and committed-Step durability. +- `Reopen trigger`: A semantic change belongs to specification; this Leaf may qualify only physical coverage. + +## Context and Hypothesis + +EXP-0068 accepts positive-tail completion for its attained minimal FundingSourcePolicy head. EXP-0069 accepts maximum-header completion only at zero tails. Both dimensions are simultaneously reachable, so selecting their component-wise maximum would be an unsupported aggregate unless one owner is shown to cover the cross-product. + +The hypothesis is that one source-owned maximum-header positive-tail benchmark over `t=1..=3` can cover the combined branch. This is a new reachable-state domain and therefore cannot be inferred or measured in either predecessor. + +## Baseline and Candidates + +| ID | Architecture | Source tree | Runtime/config identity | +| --- | --- | --- | --- | +| B0 | EXP-0068 positive-tail OwnerOnly profile | Current worktree atop `a6d4bc926dae7f657902de5d0877a693aa05e891` | Reference maximum three tail chunks | +| A | Same inner body with maximum 16-source SignedAllowlist at every positive tail count | Benchmark-only T16 profile implemented; Wasm measurement pending | Same fresh benchmark Wasm as a repeated B0 control | + +## Controls + +- `Controlled variables`: Mutable Manual Persistent User, StopCycle at cursor zero, public admission, real Pipeline accounting, positive tail funding ingress/release, consumed source boundary, toolchain, runtime and Wasm. +- `Changed variables`: Funding policy width only at each `t=1..=3`. +- `Excluded`: Zero-tail decision owned by EXP-0069, outer FIFO, other header fields, Tasks/outcomes and production selector implementation. +- `Confounders`: Header and tail proof paths may combine non-additively; no predecessor subtraction is accepted. + +## Benchmark Class, Workloads, and Criteria + +- `Benchmark class`: Native FRAME construction then same-Wasm pallet benchmark. + +| Workload | State/population geometry | Hot/cold assumptions | Purpose | +| --- | --- | --- | --- | +| T | Existing OwnerOnly positive-tail profile, `t=1..=3` | Fresh admitted User Actor | Same-Wasm control | +| T16 | Maximum SignedAllowlist positive-tail profile, `t=1..=3` | Same geometry plus maximum policy head | Combined-domain falsifier | + +- `Metrics`: Raw time, tracked I/O, recorded/estimated proof, generated formulas and exact fee/completion/funding-only hold postconditions. +- `Statistical method`: Canonical 50 steps/20 repeats and default minimum-duration behavior; one fresh Wasm, no filtering, nearest-rank P50/P95, every DB sample checked against generated formulas. +- `Acceptance criteria`: T16 covers every combined sample and postcondition across `t=1..=3`; generated component-wise owner is nonduplicated and can be selected from the existing context. +- `Rejection criteria`: Any underfit, altered hold branch, invalid allowlist/admission, cross-artifact subtraction or need for an unmeasured selector distinction. + +## Environment and Artifact Identity + +- `Source checkpoint`: `a6d4bc926dae7f657902de5d0877a693aa05e891`; T16 candidate `template/pallets/actors/src/benchmarking.rs` SHA-256 `adf56be431b05e4f53a2f684608014721b9e716958a68b96dae790da370035d7`. +- `Accepted inputs`: EXP-0068 benchmark Wasm `1a55dd53472cd1669cf7e1050ceebfcca0a735f3f2e75cdb4565d75dfce6f087`; EXP-0069 benchmark Wasm `36a5cc44b0a3a323cba6135ac03e79e449236267bd675ccb2e01e18c2cc2e56e`; neither substitutes for the required same-Wasm cross-product comparison. +- `Production preservation`: Weight `20d56df225eca3ff52857f2170eccf50587c5ca17f997d18d083e61e8afbeed9`; Wasm `155749e22a6235570d2bb97d4a42b1327b86f4f5d82742df36494fca0fd88c1d`. +- `Measurement route`: T ran fresh and T16 reused the same Wasm with `--skip-build`. Shared benchmark Wasm SHA-256 `be52827f35b2938e55bf991bc9febe6430bc5b276d442335996a0ad221396662`. T `.rs/.json/.log`: `f49069266d12a109fa0eece47a5fd6d526a02a4042839479849792650fd8157f` / `968033e0445da314472d256e86bced9dc355dc9350a44f25a323e2a99b4b1321` / `0199dfbb53a7cd972d1f7f3cea88c046a51aca76db0a98d95d736abded4dcf1f`; T16: `88ec91522aa3fa2337099472f4dd394096bddffa1f0b1c65e1e0a5427c2cf831` / `ed1c94c5bd339ec6220f18c0f41270939c42d4090257e3754759fecca2dd41cf` / `f077172a506b4fa451b4d9eb81bef979dc5226426db8e65eef4315dcf9818e24`. + +## Proof Obligations + +- `Freeze`: Frozen on 2026-09-08 at Prepared: one combined header/tail reachable domain, T/T16 over `t=1..=3`, same-Wasm evidence and criteria above. +- `Review triggers`: EXP-0067 live composition exposed a reachable cross-product absent from both accepted axis owners. +- `Decomposition review`: One combined branch owner and criterion. Zero tails remain EXP-0069; selector binding remains EXP-0067. Different header fields or outcomes transfer. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | One complete inner owner covers maximum funding-policy head plus positive tails | T16 at any `t=1..=3` outside its generated/tracked envelope | Native construction and same-Wasm T/T16 measurement | Self | Required | Final numeric input to EXP-0067 live composition | Satisfied for reference `MaxWhitelistSize = 16`, positive tails `t=1..=3`: T16 covers every tracked sample and component-wise dominates T | + +## Measurements + +T and T16 ran sequentially on shared fresh benchmark Wasm `be52827f35b2938e55bf991bc9febe6430bc5b276d442335996a0ad221396662`. Both pass public admission, exact Pipeline accounting, zero Action effect/receipt, completed Cycle and funding-only hold decrease; T16 additionally requires a maximum owner-containing SignedAllowlist. + +| Profile | t | Time / DB samples | Raw time min / P50 / P95 / max (ns) | Tracked reads / repeat reads / writes | Recorded proof bytes | +| --- | ---: | --- | --- | --- | ---: | +| T | 1 | `1,000 / 50` | `170,066 / 194,302 / 261,350 / 550,777` | `12 / 30 / 8`; repeat writes 0 | 2,105 | +| T | 2 | `960 / 48` | `188,435 / 215,254 / 280,138 / 396,007` | `13 / 30 / 8`; repeat writes 0 | 2,475 | +| T | 3 | `40 / 2` | `197,794 / 226,778 / 301,090 / 396,705` | `14 / 30 / 8`; repeat writes 0 | 2,783 | +| T16 | 1 | `1,000 / 50` | `183,825 / 214,696 / 282,862 / 554,759` | `12 / 30 / 8`; repeat writes 0 | 2,618 | +| T16 | 2 | `960 / 48` | `198,142 / 231,458 / 313,942 / 503,214` | `13 / 30 / 8`; repeat writes 0 | 2,988 | +| T16 | 3 | `40 / 2` | `211,902 / 239,629 / 310,450 / 317,713` | `14 / 30 / 8`; repeat writes 0 | 3,296 | + +| Generated model | Pre-DB RefTime | Estimated ProofSize | Reads / writes | +| --- | --- | --- | --- | +| T | `176,351,829 + 19,549,484*t` | `10,164 + 2,834*t` | `11+t / 8` | +| T16 | `200,241,463 + 16,015,310*t` | `10,677 + 2,834*t` | `11+t / 8` | + +All 100 T and 100 T16 DB samples exactly match their generated I/O. At every component, T16 recorded and estimated ProofSize exceed T by exactly 513 bytes. No fit underprices the combined branch. + +## Derived Metrics + +With reference RocksDbWeight, complete T16 totals are `1,316,256,773 / 13,511`, `1,357,272,083 / 16,345` and `1,398,287,393 / 19,179` for `t=1,2,3`. They component-wise dominate same-Wasm T by RefTime `20,355,460`, `16,821,286`, `13,287,112` and exactly 513 ProofSize bytes respectively. Select T16 directly; do not add predecessor coefficients. + +## Statistical / Variance Notes + +Both profiles use one Wasm and canonical 50-step/20-repeat settings with default minimum-duration repetition. Nearest-rank P50/P95 are unfiltered. The small `t=3` sample count is explicit. Timing remains host-scoped; exact I/O coverage and component-wise dominance decide acceptance. + +## Result + +Measured and interpreted. T/T16 complete on one fresh Wasm; T16 retains the same generated I/O shape, adds the exact 513-byte policy proof at every tail count and dominates T in both Weight dimensions. Production artifacts remain unchanged. + +## Interpretation + +Neither predecessor was wrong; each remains Accepted within its axis. T16 supplies the missing simultaneous-path owner. The exact repeated 513-byte delta supports funding-policy-width attribution in this bounded domain, not arbitrary Contract-head fields. Existing scalar tail context can select T16 without a new public dimension. + +## Decision + +Accepted for reference `MaxWhitelistSize = 16` and positive tails `t=1..=3`. Select T16's generated model as the complete combined-path input to EXP-0067. Do not install it until live selector composition proves non-overlap and maximum/actual containment. + +## Rejected Alternatives and Tradeoffs + +| Candidate | Decision | Evidence-backed reason | +| --- | --- | --- | +| `max(EXP-0068 tail, EXP-0069 H16)` | Rejected as proof | Max of overlapping partial-domain owners does not establish simultaneous-path coverage | +| Add both coefficients | Rejected | Double-counts shared Pipeline/completion/hold work | +| Add context fields now | Rejected | Combined benchmark may provide one scalar inclusive owner | + +## Validity + +- `Establishes`: Same-Wasm combined maximum-header/positive-tail coverage, exact semantic postconditions, generated/tracked I/O agreement and direct T16 coefficients for the reference domain. +- `Does not establish`: Production selector correctness, other header fields/outcomes, generic-host maxima, production artifacts or release acceptance. +- `Known limitations`: Reference `MaxWhitelistSize = 16`, positive tails `1..=3`, StopCycle and attained funding geometry only; host timing is secondary. +- `Invalidation triggers`: Header encoding/bounds, tail geometry, hold/collection body, benchmark method or toolchain changes. +- `Residual qualitative insight`: Independent extrema cannot be composed when their worst cases are simultaneously reachable unless one measured owner covers the product. + +## Follow-Up / Next Gradient + +Accepted axis inputs → combined T/T16 benchmark → generated/tracked containment → EXP-0067 scalar selector composition → production regeneration only if selected. + +Stop after O1. Transfer any other cross-product before measurement. + +## Architecture Impact + +- `Affected physical truth`: None while Prepared. +- `Architecture update`: Only after a selected EXP-0067 correction converges. +- `Provenance citation`: `actors/EXP-0094` for combined maximum-header/positive-tail completion coverage. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: None. +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: [EXP-0068](./EXP-0068.md), [EXP-0069](./EXP-0069.md) +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: [EXP-0068](./EXP-0068.md), [EXP-0069](./EXP-0069.md) +- `Transfers question to`: [EXP-0067](./EXP-0067.md) — live selection after combined coverage. +- `Produces input for`: [EXP-0067](./EXP-0067.md), [EXP-0066](./EXP-0066.md) +- `Reopen trigger`: A reachable combined funding-policy/tail path outside T/T16 or changed relevant encoding/bound. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0095.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0095.md new file mode 100644 index 00000000..2c8cfe2d --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0095.md @@ -0,0 +1,152 @@ +# EXP-0095 — Production Binding Refresh + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | None | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-08 | +| Affected domain | Generated Actors Weight and exact runtime binding after User completion correction | +| Physical mechanism | Source-to-Weight-to-Wasm-to-metadata consumer convergence | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0065](./EXP-0065.md) — prior exact binding, invalidated by changed source/Weight | +| Successor experiment | None | + +## Decision Question + +Do the regenerated Actors Weight, production Wasm, metadata and all exact-identity consumers converge after EXP-0067's User completion selector correction? + +## Governing Specification + +- `Owners`: `docs/actors-resource-policy.specification.en.md`, `docs/actors-performance-assurance.specification.en.md`, pallet specification and project production-Weight/runtime validation contracts. +- `Invariant`: Preserve frozen C1 and semantics; generated artifacts must derive from one source and every consumer must bind the same final identities. +- `Reopen trigger`: Semantic or geometry drift returns to its owner; this Leaf refreshes binding evidence only. + +## Context and Hypothesis + +EXP-0067 now selects generated maximum-header User completion owners for zero and positive tails. Full Actors generation changed the production Weight, and a production runtime build changed Wasm. EXP-0065's exact old bundle therefore no longer certifies current source. The hypothesis is that affected artifacts and consumers can converge without changing C1 or unrelated semantics. + +## Baseline and Candidates + +| ID | Architecture | Source tree | Runtime/config identity | +| --- | --- | --- | --- | +| B0 | EXP-0065 exact old binding | Checkpoint `a6d4bc926dae7f657902de5d0877a693aa05e891` | Weight `20d56d…`, Wasm `155749…` | +| A | Regenerated User-completion-aware binding plus EXP-0096 correction | Current worktree | Weight `9ce37a…`, current Wasm `25b969…`; metadata and identity consumers converged | + +## Controls + +- `Controlled variables`: Toolchain, reference runtime, C1 geometry, semantics, host bounds and generation workflow. +- `Changed variables`: User completion benchmark methods, selector and generated descendants. +- `Confounders`: Full pallet regeneration refreshes all Actors coefficients; affected evidence must be classified rather than represented as a two-method patch. + +## Benchmark Class, Workloads, and Criteria + +- `Benchmark class`: Full production pallet Weight generation plus exact release-tree artifact validation. +- `Workload`: All production Actors benchmarks and existing production binding/consumer gates. +- `Metrics`: Generated method coverage, Weight/Wasm/metadata hashes, ABI/PAPI/descriptors, consumer pins, compile/tests and artifact identity. +- `Acceptance criteria`: One final source-bound artifact cohort and every required consumer/gate agree; no placeholder/default method selected in production. +- `Rejection criteria`: Partial fragment, stale identity, mismatched consumer, failed generated method coverage or unsupported release claim. + +## Environment and Artifact Identity + +- `Generation`: `./scripts/benchmarks.sh --min-duration 0 pallet_deos_actors`; 50 steps/20 repeats, explicit repeat-only mode; log SHA-256 `2e63a0a8dc82b775ffdf2c7f28fedf736cef17921401ef228b7554787be1beb1`. +- `Generated Weight`: `template/runtime/src/weights/pallet_deos_actors.rs`, SHA-256 `9ce37ade391a29d4f60779f1887ad242dfeb4919d25cf0047ffba85d18f33901`. +- `Production builds`: Initial post-Weight build log `d0085130b0715e943f4533e394f19497cb9d15003ad765951d7aa9897e6c6f87`, Wasm `8e03c2f1d305af662f9722ef54a7433a0ad19b708fec2f5f69e2054f4e918567`; post-EXP-0096 canonical build log `c44973199b64b1bb49d14bcd5be7c1af15bb69ca2aaff382153759069ac777c4`, current Wasm `25b9695fd9e900f17ae1f3fb0b815ac1403830264d9c31f7cee54e29f434b700`. +- `Current source identities`: benchmarking `2ae4cbcecda244091e8fd53958606b0c20669c953b66a5bf158baf13e1c05222`; Weight trait `289c9ec5124d89445d4d47e85f335b62d9b687b5a9fec418193ac7dbc11b72e0`; selector `a44f7ff60b28bcbd8e86f65e5ed7a52eac51f8928e7c5d0936d457693e15d0cf`. +- `Metadata and descriptors`: Metadata remains SHA-256 `75868ea75fd85e7b79db12650a7407e89e249f5564b2157d55ac46cd26cb5d12`; descriptor manifest remains `84aafcbb7b65f3dc01d307efd94f585e5b5b645ed146e644b7674590320b4505`; post-EXP-0096 export log `5f17cbce5f25156c1e2a9c3c3d065f2e896b27cc639069db695a3516f7eb3725`. +- `Client artifacts`: ABI `7c645cde7c5405ee805281dace5cddd0e80fa60be6fdf45ac06fbeabe3403e71`; bounds `0cd7716d1e694b516a33a47c40f16cf97ba90510e8b4792feddac89826e29d15`; cost vectors `ffc5f801a6ac469cace362e65125ec3eda2718eccab03e1215f7cbe58f6e3c0b`; fee vectors `9718850dbd55cb1732d725dd16724d34747c2a88621a51d0fa8b957801527abf`; observation evidence `9e97a864833114cd2c5c5ca587f62ffa2164493d4d57d23bb3a84397eb830dec`; ingress evidence remains `ea9697460ec0ad05c89b68f263b60ead719e26cc9164d240c1da3e2bd347c67a`. +- `Runtime replay`: Current reference full-block proof replay is pinned to EXP-0095 Wasm and passes. Historical EXP-0066 campaign entrypoints remain explicitly pinned to invalidated EXP-0065 Wasm until affected evidence is refreshed or retained as historical. +- `Impact classification`: EXP-0070 through EXP-0079, EXP-0081 through EXP-0084 and EXP-0086 are Invalidated for current-production use because their quantitative Control, schedule, stop, resource or lifecycle claims consume the changed generated completion owner and old Wasm. EXP-0085 remains Measuring with both its prior state-linkage gap and stale quantitative input. EXP-0080 remains Accepted only for its explicitly historical 18-block EXP-0065 raw-proof scope and supplies no current-production proof claim. +- `Final-gate attempt`: Full `actors-assurance.sh` passed source identity, ABI/normative/evidence drift, cross-language parity, package/archive/embedding contracts and the first six exact heavy profiles, then failed `transfer_10k_homogeneous_predicate_attribution`: current result used 1,900 blocks where the historical assertion expected 1,662 for the two-Predicate cohort. Full log `/tmp/deos-deos-actors-assurance.XBW44v.log`; focused log `/tmp/deos-actors-gate-exact-heavy-profile-transfer-10k-homogeneous-predicate-attribution.In4uIP.log`. +- `Heavy-profile refresh`: Two independently repeated full-gate traversals recovered deterministic replacement baselines for `transfer_10k_homogeneous_predicate_attribution`. Two predicates: 1,900 blocks, 10,002 Steps, p50/p95 5/8, maximum service gap 999, 9,997 Cadenced occurrences. Four predicates: 1,345 blocks, 10,002 Steps, p50/p95 7/15, maximum service gap 731, 9,996 Cadenced occurrences. Both reports retain zero failed Steps and 10,002 Openings/completions. Exact second logs: `/tmp/deos-actors-gate-exact-heavy-profile-transfer-10k-homogeneous-predicate-attribution.rlGARH.log` and `/tmp/deos-deos-actors-assurance.twFNrr.log`. +- `Heavy-profile falsification`: A third full gate passed both refreshed homogeneous Predicate cohorts exactly, then passed all three SwapOut heavy profiles. It first failed at `transfer_10k_manual_and_reactive_first_traversal`, where complete first traversal retained 9,997 Cadenced occurrences rather than the stale 9,989 assertion. Full log `/tmp/deos-deos-actors-assurance.lReP3j.log`; focused log `/tmp/deos-actors-gate-exact-heavy-profile-transfer-10k-manual-and-reactive-first-traversal.dG4w2l.log`. +- `Mixed-profile envelope defect`: The next full gate independently confirmed the refreshed 9,997 mixed Manual/Cadenced Transfer occurrence baseline and the following continuous-user Transfer profile. It then failed before mixed 9,500/400/100 execution because the fixture assumed SwapOut effect Weight component-wise dominated Transfer. The new completion owner makes their RefTime/ProofSize dimensions cross, so the heterogeneous fixture now selects `transfer_effect_maximum.max(swap_effect_maximum)` rather than weakening either dimension. Full log `/tmp/deos-deos-actors-assurance.5pPAHt.log`; focused log `/tmp/deos-actors-gate-exact-heavy-profile-mixed-9500-transfer-400-swapout-100-control-first-traversal.tn05S4.log`. +- `Mixed-profile execution`: The corrected component-wise envelope admitted and completed all 10,000 mixed Actors. The next falsifier was the historical Cadenced-occurrence assertion: 9,998 current occurrences versus 9,989 old. The replacement is encoded and diagnostics now print before quantitative assertions. Full log `/tmp/deos-deos-actors-assurance.Yngx2N.log`; focused log `/tmp/deos-actors-gate-exact-heavy-profile-mixed-9500-transfer-400-swapout-100-control-first-traversal.AuMt2B.log`. +- `Remaining-gate traversal`: The next full gate independently accepted 9,998, all remaining runtime heavy profiles, occupancy, and the first package heavy profile. The second package profile failed because its Cadenced Actor retained `trigger_wakeup_pointer: Tick(11)` with `pending_signal: false` after Crossing/change fanout drained at block 1,253 under the breaker. The retained trigger placement proves no disappearance, but neither the test's expected latch nor current-production acceptance. Full log `/tmp/deos-deos-actors-assurance.UqrASw.log`; focused failure `/tmp/deos-actors-gate-exact-package-heavy-profile-breaker-materializes-maximum-mixed-wakeup-crossing-and-broad-fanout-without-execution-loss.b1oZWK.log`. +- `Focused route`: `actors-assurance.sh --exact-heavy-profile NAME` now validates one declared runtime/package heavy profile with exact test resolution and the ordinary source/artifact identity boundary, avoiding repeated unrelated 35-minute predecessors. Script self-test, help, missing/unknown argument failures and real package-profile routing pass; the selected behavior test remains red. +- `EXP-0096 decision`: The discrepancy was a runtime ordering defect. A due Cadenced path scheduled its next detector wakeup, then capacity-deferred activation to the canonical process wakeup, and only afterward tried to remove the detector. That temporary dual-pointer state was unloadable and rolled back as a sticky wakeup invariant fault. EXP-0096 moves detector invalidation before activation inside the same outer transaction and passes the exact heavy profile twice, including after diagnostic removal. +- `Post-EXP-0096 convergence`: Canonical Wasm rebuild, unchanged metadata/descriptors/ABI/bounds/cost/fee/ingress identities, refreshed observation evidence, full 151-test client validation and the explicitly ignored current production-Wasm block/proof replay all pass. `--production-reference-replay` now prevents an ordinary filtered command from misclassifying a skipped ignored test as evidence. +- `Final exact-tree gate`: One uninterrupted full Actors assurance passed all identity/drift checks, package/archive/embedding contracts, 14 runtime/occupancy heavy profiles and all four package heavy profiles in 2,447 seconds on the post-EXP-0096 cohort. Successful compact script output does not retain a failure log; the canonical command and exact artifact identities in this record define the accepted cohort. +- `Pending`: None inside G5; EXP-0067 may now decide the User Opening binding and invalidated G6 owners remain separate. + +## Proof Obligations + +- `Freeze`: Frozen at Prepared on the changed User completion methods/selector and all directly generated/identity-bound descendants. +- `Review triggers`: EXP-0067 production correction changed EXP-0065's exact source and artifact assumptions. +- `Decomposition review`: One artifact-convergence decision. New semantic, benchmark-owner or consumer behavior defects transfer rather than accumulate here. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | Current User-completion-aware source has one converged exact production binding | Any required generated method, artifact or consumer identity disagrees | Full production generation/build and exact consumer validation | Self | Required | Restored G5 input for EXP-0067/0066 | Satisfied with scope | + +## Measurements + +Full generation completed in 2,870 seconds using explicit 50-step/20-repeat mode. Generated production methods include zero-tail `175,025,000` pre-DB RefTime, estimated ProofSize 7,990 and 9/5 I/O; positive-tail `221,092,770 + 17,027,764*t` pre-DB RefTime, `10,677 + 2,834*t` estimated ProofSize and `11+t / 8` I/O. Initial production Wasm build completed in 503 seconds. Canonical metadata export completed in 58 seconds and remained byte-identical because runtime APIs/storage metadata did not change. PAPI descriptors and ABI/bounds likewise remained unchanged. Regenerated cost/fee vectors and observation runtime evidence bind the new Weight/Wasm identities; ingress evidence remains unchanged. Actors quick assurance and the complete 151-test browser automation/check/build validation pass. + +## Derived Metrics + +With reference RocksDbWeight, selected zero-tail total is `900,025,000 / 7,990`; positive-tail total is `1,296,092,770 + 42,027,764*t` RefTime and `10,677 + 2,834*t` ProofSize. These are generated binding inputs, not integrated throughput acceptance. + +## Statistical / Variance Notes + +Production generation used all declared component steps and repeats with `--min-duration 0`; this differs from the earlier minimum-duration diagnostic cohorts. Generated model and database coverage require review; do not merge timing distributions across methods. + +## Result + +Accepted. Generated Weight, current production Wasm, unchanged metadata/ABI/descriptors/vectors, refreshed runtime evidence, executed block/proof replay, client validation and one uninterrupted full assurance gate converge. Historical EXP-0066 evidence remains classified separately. + +## Interpretation + +The correction is now represented by generated production methods rather than handwritten coefficients. Because the full pallet artifact changed, old exact-Wasm throughput evidence is historical until affected compatibility/replay scope is refreshed or explicitly classified. + +## Decision + +Accepted. Select the post-EXP-0096 cohort as the current exact production binding for G5. This restores the binding prerequisite for EXP-0067 without promoting invalidated G6 measurements. + +## Rejected Alternatives and Tradeoffs + +| Candidate | Decision | Evidence-backed reason | +| --- | --- | --- | +| Merge two focused fragments manually | Rejected | Would present a mixed artifact without canonical full-generation provenance | +| Keep EXP-0065 current | Rejected | Its exact source, Weight and Wasm identities changed | +| Reopen C1 | Rejected | Binding correction preserves frozen geometry and semantics | + +## Validity + +- `Establishes`: Successful full Actors generation and initial production Wasm build on current source. +- `Does not establish`: Refreshed integrated G6 evidence or release Geometry Freeze. +- `Known limitations`: Full regeneration changes many numeric coefficients; affected descendants are retained as historical evidence and require scoped refresh. +- `Invalidation triggers`: Any further source, Weight, runtime, metadata or consumer change. +- `Residual qualitative insight`: Exact binding closure is artifact-cohort identity, not successful generation alone. + +## Follow-Up / Next Gradient + +Return to EXP-0067 for the User Opening binding decision, then refresh only invalidated or still-Measuring G6 owners. + +Stop when every required descendant agrees or a specific owner blocks convergence. + +## Architecture Impact + +- `Affected physical truth`: Generated production binding only; C1 unchanged. +- `Architecture update`: None until artifact convergence is accepted. +- `Provenance citation`: `actors/EXP-0095` after decision. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: None. +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: [EXP-0067](./EXP-0067.md), [EXP-0069](./EXP-0069.md), [EXP-0094](./EXP-0094.md), [EXP-0096](./EXP-0096.md) +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: [EXP-0065](./EXP-0065.md) as current binding; [EXP-0070](./EXP-0070.md), [EXP-0071](./EXP-0071.md), [EXP-0072](./EXP-0072.md), [EXP-0073](./EXP-0073.md), [EXP-0074](./EXP-0074.md), [EXP-0075](./EXP-0075.md), [EXP-0076](./EXP-0076.md), [EXP-0077](./EXP-0077.md), [EXP-0078](./EXP-0078.md), [EXP-0079](./EXP-0079.md), [EXP-0081](./EXP-0081.md), [EXP-0082](./EXP-0082.md), [EXP-0083](./EXP-0083.md), [EXP-0084](./EXP-0084.md) and [EXP-0086](./EXP-0086.md) for current-production quantitative use; historical evidence remains scoped. +- `Uses evidence from`: [EXP-0065](./EXP-0065.md), [EXP-0069](./EXP-0069.md), [EXP-0094](./EXP-0094.md) +- `Transfers question to`: None. +- `Produces input for`: [EXP-0067](./EXP-0067.md), [EXP-0066](./EXP-0066.md) +- `Reopen trigger`: Any mismatch in the regenerated source/artifact/consumer cohort. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0096.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0096.md new file mode 100644 index 00000000..c63c5448 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0096.md @@ -0,0 +1,164 @@ +# EXP-0096 — Cadenced Latch Authority Under Ready Backpressure + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | None | +| Architecture release / experiment campaign | Campaign 0.7.25 | +| Date | 2026-09-08 | +| Affected domain | Temporal wakeup materialization under breaker and Ready-capacity backpressure | +| Physical mechanism | Atomic Cadenced detector disable before activation placement | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | None | +| Successor experiment | None | + +## Decision Question + +Can a due Cadenced occurrence disable its detector, latch once and defer through the canonical process wakeup when Ready publication is capacity-blocked, without creating an authority state that its own loader rejects? + +## Governing Specification + +- `Exact section`: `template/pallets/actors/docs/specification.en.md` sections 7.3 and 8.6. +- `Invariant`: One useful Trigger latch disables family detection while pending; capacity backpressure preserves one canonical process placement and the containing transaction rolls back every partial transition on failure. +- `Reopen trigger`: Evidence that detector invalidation before activation loses a due occurrence, duplicates authority, changes fee finality or cannot roll back to the original Trigger wakeup. + +## Context and Hypothesis + +- `Current architecture`: Due Cadenced processing scheduled the next detector wakeup, requested activation, then removed the detector after successful latching. +- `Reason`: The required maximum mixed breaker profile recorded a sticky wakeup invariant fault and retained overdue Tick(11) without latching. +- `Hypothesis`: Removing the next detector while source authority is still loadable, immediately before activation inside the existing outer transaction, prevents transient dual Trigger/process wakeup authority and retains rollback safety. +- `Materiality`: The maximum mixed profile must latch all three trigger families without execution or loss under the breaker. + +## Baseline and Candidates + +| ID | Architecture | Source tree | Runtime/config identity | +| --- | --- | --- | --- | +| Baseline | Remove Cadenced detector after activation placement | Checkpoint `da9df33` plus diagnostic-only test output | Package `Test` with 10,000 active identities and Ready cap 1,024 | +| A | Remove Cadenced detector before activation placement in the same transaction | Current candidate | Same package config and workload | + +## Controls + +- `Controlled variables`: Rust toolchain, release profile, package mock, 10,000-identity population, breaker state, observation transitions, cadence Tick(11), Weight limits and focused command. +- `Changed variables`: Ordering of Cadenced detector invalidation relative to activation only. +- `Confounders`: Production Wasm was intentionally excluded; EXP-0095 owns post-change artifact convergence. + +## Benchmark Class, Workloads, and Criteria + +- `Benchmark class`: Package-native release heavy profile. + +| Workload | State/population geometry | Hot/cold assumptions | Purpose | +| --- | --- | --- | --- | +| W1 | 5,000 Crossing, 4,999 ObservationChange and one Cadenced Actor under the breaker | Ready capacity reaches 1,024 while Tick(11) is due | Exercise detector-to-process backpressure transition at maximum active population | + +- `Metrics`: Trigger pointer, pending latch, process placement, wakeup fault, cycle nonce and complete Crossing/change fanout. +- `Statistical method`: Deterministic state transition; run once with diagnostics and repeat after diagnostics are removed. +- `Acceptance criteria`: Both candidate runs pass with all occurrences retained, Cadenced pending, canonical queue or process-wakeup placement, zero Cycle execution and no worker fault. +- `Rejection criteria`: Any sticky fault, overdue detector retention, missing occurrence, duplicate placement or Cycle execution. + +## Environment and Artifact Identity + +| Artifact | Identity | +| --- | --- | +| Source commit | Baseline checkpoint `da9df33`; candidate uncommitted at measurement | +| Source tree | Focused wrapper reported exact source identity per run | +| Rust toolchain | Repository-pinned toolchain | +| Benchmark runtime Wasm | Not applicable | +| Post-generation production runtime Wasm | Not measured; invalidated candidate belongs to EXP-0095 | +| Final release production runtime Wasm | Not measured | +| Generated Weight | Existing `9ce37ade391a29d4f60779f1887ad242dfeb4919d25cf0047ffba85d18f33901`; no Weight method changed | +| Database/backend | FRAME package mock externalities | +| Benchmark command/config | `REQUIRE_WASM_IDENTITY=0 SKIP_WASM_BUILD=1 ./scripts/actors-assurance.sh --exact-heavy-profile breaker_materializes_maximum_mixed_wakeup_crossing_and_broad_fanout_without_execution_loss` | +| Raw evidence | `/tmp/deos-deos-actors-assurance.fcNUeK.log` diagnostic failure; two subsequent focused successful wrapper runs | + +## Proof Obligations + +- `Freeze`: Frozen before candidate measurement to detector invalidation ordering only on 2026-09-08. +- `Review triggers`: Activation transactionality, temporal fee order, process-wakeup backpressure, detector re-arm or another authority loader. +- `Decomposition review`: One mechanism, workload and acceptance criterion; production artifact refresh transfers to EXP-0095. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | Pre-activation detector invalidation preserves one loadable deferred Cadenced authority under Ready backpressure | W1 faults, loses work, duplicates placement or executes under breaker | Repeated exact package heavy profile | Self | Required | EXP-0095 may regenerate the corrected production artifact | Satisfied with scope | + +## Measurements + +| Candidate | Workload | RefTime | ProofSize | Reads | Writes | Other decision metrics | +| --- | --- | ---: | ---: | ---: | ---: | --- | +| Baseline | W1 | Not interpreted | Not interpreted | Not interpreted | Not interpreted | At block 1,253: Ready/combined 1,024; Tick cursor 1; overdue Tick(11); `pending_signal: false`; sticky `WakeupWorkerFault::Invariant`; error `cadence latch authority disappeared` | +| A | W1 | Not interpreted | Not interpreted | Not interpreted | Not interpreted | Passed twice; once with diagnostics and once after diagnostic removal | + +## Derived Metrics + +| Candidate | Workload | RefTime delta | ProofSize delta | Throughput / latency | Marginal or amortized cost | Pareto class | +| --- | --- | ---: | ---: | --- | --- | --- | +| A | W1 | Not derived | Not derived | All required materialization completes | No new Weight owner | Semantically dominant | + +## Statistical / Variance Notes + +The outcome is deterministic. Wall-clock duration varied with compilation and is not a decision metric. The second passing run removed all temporary diagnostic output, preventing instrumentation from serving as hidden support. + +## Result + +Baseline diagnostic output proved that Ready capacity, not active-identity capacity, caused activation to defer while the next Cadenced detector remained live. The resulting Trigger plus process-wakeup intermediate could not be loaded for post-activation detector removal and the whole occurrence rolled back into a sticky worker fault. Candidate A passed the exact maximum profile twice. + +## Interpretation + +The retained Tick(11) pointer was not lawful retry evidence because `WakeupWorkerFault::Invariant` disabled further worker progress. Moving detector removal before activation avoids an unloadable intermediate owner. Because both actions remain inside the wakeup-drain storage transaction, any later fee or placement failure restores the original due detector and state. + +## Decision + +Accepted. Select Candidate A. Keep queue capacity, cadence arithmetic, fees, public context and C1 unchanged; regenerate the production artifact under EXP-0095. + +## Rejected Alternatives and Tradeoffs + +| Candidate | Decision | Evidence-backed reason | +| --- | --- | --- | +| Classify Tick(11) retention as backpressure | Rejected | Sticky invariant fault made it non-live | +| Relax active-identity occupancy guard | Rejected | Focused rerun still faulted and observed Ready/combined occupancy 1,024, not 10,000 | +| Change the heavy assertion | Rejected | It correctly requires latching and canonical process backpressure without execution | +| Add another retry queue | Rejected | Existing activation backpressure already owns next-block retry | + +## Validity + +- `Establishes`: Package-level due Cadenced latching remains loadable and duplicate-free when Ready publication defers under the breaker. +- `Does not establish`: New production Wasm identity, integrated throughput or final EXP-0095 acceptance. +- `Known limitations`: Evidence is package-native and uses the reference mock limits. +- `Invalidation triggers`: Activation transaction boundaries, temporal detector invalidation, process wakeup authority or Cadenced re-arm changes. +- `Residual qualitative insight`: Transactional rollback prevents permanent corruption but does not make an internally unloadable intermediate authority acceptable. + +## Follow-Up / Next Gradient + +```text +accepted ordering fix → regenerate production Wasm and consumers +→ run remaining package profiles → exact-tree EXP-0095 decision +``` + +- `Eliminated hypotheses`: Lawful backpressure, active-identity cap off-by-one and stale test expectation. +- `Remaining hypotheses`: None inside this package-level claim. +- `Stop condition`: Met by two passing exact W1 runs including one without diagnostics. + +## Architecture Impact + +- `Affected physical truth`: Ordering inside due Cadenced trigger latching. +- `Architecture update`: Existing detector-latching and canonical-backpressure contracts remain correct; implementation now satisfies them. +- `Provenance citation`: `actors/EXP-0096`. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: None. +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: None. +- `Baseline of`: None. +- `Confirms`: Existing Trigger-latching and canonical process-backpressure contracts. +- `Invalidates`: None. +- `Uses evidence from`: [EXP-0095](./EXP-0095.md) remaining package-heavy failure only. +- `Transfers question to`: None. +- `Produces input for`: [EXP-0095](./EXP-0095.md) corrected scheduler source requiring production artifact regeneration. +- `Reopen trigger`: Any failed due-Cadenced occurrence, duplicate authority or non-rollback under activation backpressure. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0097.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0097.md new file mode 100644 index 00000000..71a4a275 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0097.md @@ -0,0 +1,126 @@ +# EXP-0097 — Funded User Action Fee-Path Coverage + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | [EXP-0066](./EXP-0066.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-08 | +| Affected domain | Funded User Action resource accounting | +| Physical mechanism | Pay-as-attempted Action fee collection | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0088](./EXP-0088.md) — funded Pipeline collection only | +| Successor experiment | None | + +## Decision Question + +Does one successful funded User Action map its Action fee and resource effect separately from Trigger and complete Pipeline owners on the qualified production binding? + +## Governing Specification + +- `Owners`: `template/pallets/actors/docs/specification.en.md` §§3.4 and 7.3–7.6; `docs/actors-resource-policy.specification.en.md` §§1 and 4. +- `Invariant`: Pipeline authority and pay-as-attempted Action fees remain separate; no owner borrows or duplicates another charge. +- `Reopen trigger`: A semantic fee change must reopen the specification rather than become an accounting correction. + +## Context and Hypothesis + +EXP-0088 closes bounded Trigger/Pipeline collection with StopCycle, which intentionally invokes no Action. The remaining independently falsifiable claim is successful funded User Action collection. + +## Baseline and Candidates + +The selected witness is one funded Mutable User Actor with Manual Trigger, one unpredicated native Transfer of one existential deposit, Persistent completion and successful execution. EXP-0088 supplies the independently qualified Pipeline baseline. + +## Controls + +- `Controlled variables`: EXP-0095 binding, funded User authority, Manual Trigger, complete Pipeline admission and exact Task outcome. +- `Changed variables`: Action invocation and collection only. +- `Confounders`: System fee exemption, failed/uninvoked Tasks, native-only evidence and overlapping maximum/actual owners. + +## Benchmark Class, Workloads, and Criteria + +- `Class`: Qualified production-path monetary and resource evidence. +- `Workload`: One successful funded User native Transfer after paid Manual Trigger and complete Pipeline admission. +- `Metrics`: Trigger, Pipeline and Action fees; Control/effect Weight; payer/Fee Sink deltas; typed receipts. +- `Acceptance`: Every owner occurs exactly once and monetary/resource equations close without overlap. +- `Rejection`: Missing receipt, duplicated debit, borrowed charge, failed Task or unsupported native-to-Wasm attribution. + +## Environment and Artifact Identity + +- `Current input`: EXP-0095 generated Weight SHA-256 `9ce37ade391a29d4f60779f1887ad242dfeb4919d25cf0047ffba85d18f33901` and production Wasm SHA-256 `25b9695fd9e900f17ae1f3fb0b815ac1403830264d9c31f7cee54e29f434b700`. +- `Native evidence`: `/tmp/deos-0725-exp0097-native.log`, SHA-256 `125637f08f79cab412797250307a742c25abe9cea888cec49e9c199b5128d719`. +- `Integrated evidence`: `/tmp/deos-0725-exp0097-exact.log`, SHA-256 `1038317bc15e501448d77d00875bfadec3cb6b410393d0454c17546b205fc07a`. + +## Proof Obligations + +- `Freeze`: Frozen for the selected one-Transfer workload and exact O1 falsifier before exact-Wasm execution. +- `Review triggers`: Independent Task, Action owner, outcome or evidence-linkage domain. +- `Decomposition review`: One successful-Action mapping claim; retry/failure belongs to EXP-0093 and heavy economic Task coverage to EXP-0090. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | A funded User Action maps its fee and effect separately from Trigger/Pipeline owners | One missing, duplicated or borrowed owner | Qualified production-path monetary/resource evidence | Self | Required | EXP-0066 O25 complete User fee coverage | Accepted for selected Transfer path | + +## Measurements + +`full_executive_funded_action_collection_witness` passes through the canonical runtime test route. The funded User pays Trigger `10,351,120`, Pipeline `98,617,487` and Action `18,347,464`; each typed receipt occurs exactly once. The sovereign debit after Trigger equals one transferred existential deposit plus Pipeline and Action fees, the recipient gains exactly that deposit and Fee Sink gains exactly both execution fees. Full-Executive Control is `13,919,069,122 / 148,149`; separate Transfer effect is `2,293,433,000 / 29,222`. + +The native monetary/resource equation is closed. `./scripts/actors-assurance.sh --integrated-funded-user-action` resolves exactly one native and one ignored replay test, pins the EXP-0095 artifact and demonstrably executes both. Exact Wasm reproduces the authored header and post-state root, verifies the storage proof with zero residual nodes, and reports execution/verification StorageProof `8,668 / 1,491,913` bytes. + +## Derived Metrics + +None. + +## Statistical / Variance Notes + +One deterministic native full-Executive witness linked to one exact-Wasm execution/proof replay; no timing distribution is inferred. + +## Result + +Native monetary/resource owners close without overlap and the same authored state transition verifies under exact EXP-0095 Wasm. + +## Interpretation + +The selected successful funded User Action closes independently of EXP-0088's zero-Action Pipeline witness. Broader User Tasks, failures and retries remain outside scope. + +## Decision + +Accepted for the selected funded User Transfer path. Trigger, Pipeline, Action, transfer amount and effect owners occur exactly once and exact-Wasm state/proof replay succeeds. No C1 reopening or broader User Task acceptance follows. + +## Rejected Alternatives and Tradeoffs + +- `StopCycle substitution`: Rejected because no Action is invoked. +- `System substitution`: Rejected because System authority does not exercise User fee collection. + +## Validity + +- `Establishes`: An explicit owner for the remaining Action-path question. +- `Does not establish`: Any Action fee or resource result. +- `Invalidation triggers`: Binding, fee semantics, Task mapping or evidence method changes. + +## Follow-Up / Next Gradient + +Stop. Reopen only for a changed binding/fee mapping or reproducible monetary, state-root or proof mismatch. + +## Architecture Impact + +None. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: [EXP-0066](./EXP-0066.md) O25. +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: [EXP-0067](./EXP-0067.md), [EXP-0088](./EXP-0088.md), [EXP-0095](./EXP-0095.md). +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: None. +- `Transfers question to`: None. +- `Produces input for`: [EXP-0066](./EXP-0066.md) complete funded User fee-path decision. +- `Reopen trigger`: Changed fee semantics, Task mapping, binding or reproducible monetary/resource mismatch; cost alone does not reopen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0098.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0098.md new file mode 100644 index 00000000..49e9faac --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0098.md @@ -0,0 +1,131 @@ +# EXP-0098 — Crossing Executed-Path Resource Coverage + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | [EXP-0066](./EXP-0066.md) | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-08 | +| Affected domain | ObservationCrossing resource composition | +| Physical mechanism | Crossing transition detection, latching and scheduled service | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0074](./EXP-0074.md) — accepted W6 generation chronology | +| Successor experiment | None | + +## Decision Question + +Does the selected current-generation ObservationCrossing path compose detector/latch and scheduled service owners exactly once while stale generation authority performs no Actor work? + +## Governing Specification + +- `Owners`: `template/pallets/actors/docs/specification.en.md` §§4.2, 4.3 and 4.6; `docs/actors-resource-policy.specification.en.md` §§4 and 6. +- `Invariant`: One useful latch, generation authority and scheduler-only execution remain unchanged. +- `Reopen trigger`: A causal or fee semantic change belongs to specification. + +## Context and Hypothesis + +EXP-0089 preparation incorrectly treated Crossing and broad deferred fanout as one path. Crossing has its own transition worker and must not borrow broad-fanout owners. + +## Baseline and Candidates + +Use W6 Actor 27: replacement from generation 1 to 2, one post-replacement transition, materialization in block 5 and successful Transfer service in block 6. The pre-replacement transition is the stale-authority negative control. + +## Controls + +- `Controlled variables`: EXP-0095 binding, W6 fixture, generation, threshold, Task and exact-Wasm chronology. +- `Changed variables`: Source-bound Crossing detector/latch resource accounting only. +- `Confounders`: Broad observation fanout, temporal work and scheduled service are separate owners. + +## Benchmark Class, Workloads, and Criteria + +- `Class`: Source-bound native accounting linked to accepted exact-Wasm W6 states. +- `Workload`: One generation-2 User ObservationCrossing occurrence and one scheduled Transfer. +- `Metrics`: Detector/latch Control, generated I/O, service Control/effect and state/proof linkage. +- `Acceptance`: Complete nonduplicated equation; stale generation produces no Actor-specific charge or service. +- `Rejection`: Broad-fanout owner borrowed, missing detector/service owner or unmatched state. + +## Environment and Artifact Identity + +- `Current input`: EXP-0074 W6 evidence on EXP-0095 Weight/Wasm. +- `Capacity evidence`: `/tmp/deos-0725-exp0098-capacity.log`, SHA-256 `e4eb094b14420758af05c0c3a54d60749631591509abad86dd197bc3b44f18a4`. +- `Complete native ledger`: `/tmp/deos-0725-exp0098-complete.log`, SHA-256 `fac5c5c7f300e67af28f098144e9901a979d53778f1cf32751a2251fcf9406c3`. +- `Resource artifact`: `crossing_single_user_fire_binds_executed_detector_latch_control` binds one funded User Crossing occurrence, generated worker meter, next-block service Control and Transfer effect. +- `W6 exact-link evidence`: `/tmp/deos-0725-exp0098-w6-exact-link.log`, SHA-256 `a620760c2ee8cec559d7911ba4a0c71f0177b5304c3e4ae218f274480f72bcae`. + +## Proof Obligations + +- `Freeze`: Frozen for W6 Actor 27's generation-2 Crossing detector/latch plus scheduled Transfer service and the exact O1 falsifier. +- `Review triggers`: Crossing branch, generation state, fee owner, Task or evidence-linkage change. +- `Decomposition review`: One Crossing family path; broad deferred fanout remains EXP-0089. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | Crossing detector/latch and scheduled service compose exactly once | One missing, duplicate or borrowed owner | Source-bound ledger linked to W6 exact states | Self | Required | EXP-0066 O26 Crossing coverage | Accepted | + +## Measurements + +The current runtime `crossing_single_user_fire_binds_executed_detector_latch_control` fixture executes one funded User Rising Crossing against generation-current membership. The complete worker invocation consumes `14,876,034,000 / 386,687`, emits exactly one ObservationCrossing occurrence, leaves exactly one pending signal and owns one ready ticket. This meter total includes worker base, probes, traversal and generated branch units exactly as admitted by `service_crossing_transitions`; it does not include subsequent scheduler Control or Transfer effect. + +The current runtime `crossing_capacity_policy_is_bound_to_measured_minimum_progress_and_explicit_horizons` regression passes on generated EXP-0095 Weight. Component-wise admission permits four Crossing candidates per block under the configured worker limit, preserving explicit 9,000 User and 10,000 total members per feed and corresponding 2,250/2,500-block uncontended horizons. This is capacity-envelope evidence, not executed owner frequency. + +EXP-0074 independently proves W6 Actor 27's stale generation does not execute, generation 2 materializes exactly once and its Transfer services exactly once under exact Wasm. The isolated funded path records detector/latch Control `14,876,034,000 / 386,687`, then next-block prepass plus one User service Control `12,954,769,748 / 141,918` and separate Transfer effect `2,293,433,000 / 29,222`. The recipient gains exactly one unit, the latch and ticket clear, and no broad-fanout owner enters the equation. + +Detector/latch Control has an independent source-bound native ledger. The refreshed canonical W6 route asserts the generation-2 occurrence in block 5 and its sole Transfer service in block 6, preserving the `N -> N+1` causal floor. Both the native full-Executive fixture and exact EXP-0095 Wasm replay pass through the same assertions. The native ledger and exact-Wasm chronology remain separate evidence classes: the ledger decides resource ownership while W6 decides exact production-state chronology. + +## Derived Metrics + +None. + +## Statistical / Variance Notes + +Deterministic capacity regression plus bounded exact-Wasm chronology input; no timing distribution or resource equation inferred. + +## Result + +The selected Crossing detector/latch/service equation is complete without broad-fanout borrowing, and W6 binds the same generation-safe causal sequence to exact production Wasm. + +## Interpretation + +Capacity and chronology cannot substitute for detector/latch resource ownership. Broad fanout remains excluded. + +## Decision + +Accepted for the selected funded User Crossing-to-Transfer path. No broader reactive population, Task or fairness acceptance follows. No C1 reopening. + +## Rejected Alternatives and Tradeoffs + +- `Broad-fanout substitution`: Rejected because ObservationCrossing uses independent worker ownership. + +## Validity + +- `Establishes`: Independent ownership of the Crossing accounting question. +- `Does not establish`: Resource closure. +- `Invalidation triggers`: Binding, generation, worker or Task changes. + +## Follow-Up / Next Gradient + +Proceed to EXP-0090 heavy economic Task and EXP-0093 retry-placement resource obligations. + +## Architecture Impact + +None. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: [EXP-0066](./EXP-0066.md) O26. +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: [EXP-0074](./EXP-0074.md), [EXP-0095](./EXP-0095.md). +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: [EXP-0074](./EXP-0074.md) chronology only. +- `Transfers question to`: None. +- `Produces input for`: [EXP-0066](./EXP-0066.md) reactive coverage. +- `Reopen trigger`: Changed Crossing worker, generation semantics, binding or reproducible accounting mismatch; cost alone does not reopen C1. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0099.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0099.md new file mode 100644 index 00000000..f3f6c61a --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0099.md @@ -0,0 +1,138 @@ +# EXP-0099 — Continuation Authority Ownership Audit + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | G7 Continuation Ownership Audit | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-08 | +| Affected domain | Actor continuation and transition authority | +| Physical mechanism | Contract, Actor, scheduler, Trigger and adapter ownership across process transitions | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0028](./EXP-0028.md) — accepted phase-accounting synthesis | +| Successor experiment | None | + +## Decision Question + +Does every reachable Opening, Running, Suspended, wakeup, latch, lifecycle and failure transition retain exactly one Actor-owned continuation while the scheduler owns only order, cutoff, Q1, admission, fairness and accounting? + +## Governing Specification + +- `Exact section`: `template/pallets/actors/docs/specification.en.md` §§2, 4, 6–9 owns Contract, Pipeline, Continuation, Trigger, scheduling and lifecycle semantics; `docs/actors-resource-policy.specification.en.md` owns scheduler admission/accounting. +- `Invariant`: Actor owns continuation, lifecycle, cursor, attempt, retry and latch state; scheduler owns service order and resource admission; adapters return typed outcomes and own no retry authority. +- `Reopen trigger`: Any second continuation/retry owner or reconstructed process state requires specification reopening. + +## Context and Hypothesis + +- `Reason`: Accepted EXP-0028 decides phase-accounting ownership but does not itself certify the later G7 transition matrix against the current EXP-0095 implementation. +- `Hypothesis`: Current transitions preserve one canonical Actor-owned continuation and never reconstruct it from Contract, scheduler, Trigger or adapter state. +- `Materiality`: Duplicate or missing continuation authority can replay committed effects, lose prefixes or create a parallel scheduler. + +## Baseline and Candidates + +The sole candidate is current frozen C1 on EXP-0095. EXP-0028 is historical scoped input, not a substitute for current transition evidence. + +## Controls + +- `Controlled variables`: EXP-0095 source/binding, C1 FIFO/cutoff/Q1/causal semantics and existing typed adapter outcomes. +- `Changed variables`: None; this is an ownership audit. +- `Confounders`: Resource Weight ownership is not process-state ownership; Contract data is not mutable Continuation; scheduler references are not cursor authority. + +## Benchmark Class, Workloads, and Criteria + +- `Class`: Source-and-test ownership audit with a compact transition matrix and one regression owner. +- `Workload`: Opening, Running, short/long Suspended retry, Wakeup return, Manual/reactive latch, pause/resume, cancel/close, Contract replacement, exhaustion and typed failure. +- `Metrics`: Source authority, transition atom, destination authority, cursor/prefix preservation, scheduler reference count and external effect count. +- `Acceptance`: Every row names one source, one transactional transition, one destination and one existing falsification test; no adapter or scheduler stores an independent Continuation. +- `Rejection`: Reconstructed cursor/attempt state, duplicate retry scheduling, compensation/replay, or lifecycle mutation that leaves two process authorities. + +## Environment and Artifact Identity + +- `Current binding`: [EXP-0095](./EXP-0095.md). +- `Initial evidence`: Accepted EXP-0028 ownership synthesis, EXP-0073 lifecycle/prefix witness, EXP-0093 long-retry placement and current pallet source/tests. +- `Consolidated regression`: `/tmp/deos-0725-exp0099-regression.log`, SHA-256 `b807c03771ae17b7d4c71c8901ca6279a4eda1d28760dcaf6640b534c1a5c6d2`. + +## Proof Obligations + +- `Freeze`: Frozen for the ten transition families below on EXP-0095; an independently owned backpressure/adapter commitment question remains outside this Leaf. +- `Review triggers`: Independent authority store, transition family or external commitment owner. +- `Decomposition review`: One sole-continuation claim. Transfer independently reusable adapter/backpressure claims to a separate Leaf before measurement. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | Every reachable process transition has one Actor-owned Continuation and no reconstructed peer | One transition with duplicate/missing cursor, attempt, retry or latch authority | Current source/test matrix | Self | Required | G7 continuation closure | Accepted | + +## Measurements + +| Transition | Source authority | Transaction / destination authority | Scheduler role | Existing falsifier | +| --- | --- | --- | --- | --- | +| Idle/Unsignaled → latched Ready | Actor hot/control authority with no Run | `request_activation` sets one pending latch and ticket | Allocates FIFO ticket only | `control_observation_change_disables_on_latch_and_rearms_at_opening` | +| Ready → Opening | Consumed frame/control cell | Opening creates Actor-owned Run or completes to one lifecycle destination | Consumes eligible head under cutoff/admission | `control_ready_user_opening_projects_exact_scalar_authority_without_event_drift` | +| Running → Running | `ActorRunHead` plus `ActorRunPayload` | Committed Step advances the same Run cursor and cumulative outcomes | Republishes service reference under Q1 | `control_running_middle_step_continuation_matches_immutable_oracle` | +| Suspended → short Ready retry | Same Actor Run with attempts/cursor | Temporary outcome increments attempts and retains one Ready successor | Orders retry; owns no attempt state | `scheduler_retries_manual_continuation_after_cooldown_without_new_signal` | +| Suspended → long Wakeup | Same Actor Run with attempts/cursor | Temporary outcome places one Wakeup pointer and no Ready ticket | Stores bounded wakeup reference only | EXP-0093 `excessive_system_reference_deviation_suspends_without_fill_and_backs_off` | +| Wakeup → Ready | Actor Run plus Actor hot wakeup pointer | Due worker clears pointer and publishes one Ready ticket | Materializes eligibility; does not execute/reconstruct Run | EXP-0093 Control-only return and `N -> N+1` witness | +| Pause / resume | Existing Actor Run or idle authority | Lifecycle state changes scheduling eligibility; Run bytes/cursor remain Actor-owned | Removes/restores service reference | EXP-0073/EXP-0074 pause-resume timelines | +| Cancel / close | Supplied loaded Run and identity/control authority | `cancel_run_internal_loaded` consumes Run and emits terminal summary; close removes process authority | Removes references; performs no unwind | `loaded_cancellation_uses_supplied_run_despite_missing_or_stale_backing` | +| Contract replacement | Existing Contract generation plus optional Run | Replacement cancels Contract-bound Run before installing new generation | Invalidates stale detector/service references | `completion_policy_only_replacement_cancels_contract_bound_run` and EXP-0074 stale generation | +| Exhaustion / typed failure | Same Actor Run and adapter `TaskFailure` outcome | Actor increments/terminates retry state or advances cursor; adapter persists none | Selects one Ready/Wakeup/terminal destination | `frame_only_retry_attempt_exhaustion_closes_from_consumed_authority` | + +Current source places mutable Continuation in `ActorRunHead`/`ActorRunPayload`; `write_run_state` is test/benchmark-only and validates coherence. Production transitions use loaded authority and transactional source/successor methods. Contract stores authored immutable plan, hot/control state owns lifecycle/latch/reference metadata, scheduler stores order references, and adapters return typed effects/failures without cursor, attempt or retry storage. + +## Derived Metrics + +Ten required transition families map to one Actor-owned Run authority and zero adapter-owned continuation stores. The consolidated retry regression now asserts paired Run tiers and mutually exclusive Ready/Wakeup references across short retry, long retry and terminal completion while preserving cursor/prefix and one Cycle start. + +## Statistical / Variance Notes + +Deterministic source/test audit; throughput, timing and Weight sufficiency are outside scope. + +## Result + +The current source/test matrix and consolidated regression support sole Actor continuation ownership across all declared transition families. + +## Interpretation + +Scheduler references decide when an Actor may run, never what cursor/attempt state exists. Trigger and lifecycle state can gate or terminate a process but do not reconstruct Continuation. Adapter outcomes are inputs to Actor-owned transition logic. + +## Decision + +Accepted. Actor owns the sole mutable Continuation; scheduler references are mutually exclusive eligibility/order authority, and Contract, Trigger and adapters do not reconstruct process state. + +## Rejected Alternatives and Tradeoffs + +- `EXP-0028 automatic reuse`: Rejected because current transition ownership needs explicit source/test linkage. +- `Storage-name inference`: Rejected because a reference, Contract or hot state can participate without owning Continuation. +- `Adapter retry state`: Rejected by the governing one-Continuation contract unless current evidence falsifies it. + +## Validity + +Establishes current per-row ownership and consolidated representative Ready/short-retry/long-Wakeup/terminal regression closure. Does not establish independent backpressure safety or release acceptance. + +## Follow-Up / Next Gradient + +Proceed to the independently owned G7 existing-path backpressure audit. + +## Architecture Impact + +None; audit current frozen C1 before considering any change. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: G7 Continuation Ownership Audit. +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: [EXP-0028](./EXP-0028.md) only for current continuation-transition evidence. +- `Contradicts`: None. +- `Depends on`: [EXP-0095](./EXP-0095.md), [EXP-0066](./EXP-0066.md). +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: [EXP-0028](./EXP-0028.md), [EXP-0073](./EXP-0073.md), [EXP-0093](./EXP-0093.md); no raw evidence copied. +- `Transfers question to`: None. +- `Produces input for`: G7/G8 release decision. +- `Reopen trigger`: New continuation store, retry authority, transition family or binding change. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0100.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0100.md new file mode 100644 index 00000000..9045fb4c --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0100.md @@ -0,0 +1,135 @@ +# EXP-0100 — Existing-Path Backpressure Safety Audit + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | G7 Existing-Path Backpressure Safety | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-08 | +| Affected domain | Existing adapter and queue backpressure | +| Physical mechanism | Typed failure rollback into one Actor-owned retry/wakeup path | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0099](./EXP-0099.md) — accepted continuation authority audit | +| Successor experiment | None | + +## Decision Question + +Do existing DEX/Router, XCM/message, observation, funding/resource, bounded-queue and adapter-capacity failures preserve atomic external commitment and route retry through one Actor-owned Continuation without independent pending/retry state? + +## Governing Specification + +- `Exact section`: `template/pallets/actors/docs/specification.en.md` §§6–9 owns Task atomicity, Temporary retry, scheduler and lifecycle; root integration documents own adapter composition; `docs/actors-resource-policy.specification.en.md` owns fail-closed admission. +- `Invariant`: No duplicate external commitment, one Actor-owned Continuation, one capped typed retry/wakeup path and explicit live/pending/fanout/queue bounds. +- `Reopen trigger`: Any independently persisted adapter retry/commitment state or unbounded failure queue requires owning-contract review. + +## Context and Hypothesis + +- `Reason`: EXP-0099 establishes continuation ownership inside Actors but does not independently decide whether existing adapters or ingress paths retain commitment/retry authority under backpressure. +- `Hypothesis`: Every in-scope failure either rolls back atomically, returns a typed failure to Actors, or remains in its existing bounded canonical queue without creating parallel execution authority. +- `Materiality`: Duplicate external effects or adapter-owned retry state can violate committed-prefix durability and exactly-once Task effects. + +## Baseline and Candidates + +The sole candidate is current EXP-0095 C1 composition. New commitment registries, phases or scheduler semantics are excluded. + +## Controls + +- `Controlled variables`: Current runtime/pallet source, configured bounds, transactional wrappers and typed failure taxonomy. +- `Changed variables`: None; this is an existing-path audit. +- `Confounders`: A provider's bounded transport queue is not Actor Continuation; an external failure code is not retry authority; successful ingress does not prove rollback. + +## Benchmark Class, Workloads, and Criteria + +- `Class`: Source/test ownership and rollback audit. +- `Workload`: DEX/Router market rejection, XCM/message delivery failure, observation fanout pressure, funding/resource admission failure, Ready/Waiting/Wakeup capacity and adapter-local bounded errors. +- `Metrics`: Pre/post state root or balances, typed failure, Actor cursor/attempt state, queue cardinality/bounds, retry pointer count and external effect count. +- `Acceptance`: Every category has an exact code owner and falsifier proving no partial/duplicate commitment and no independent retry authority. +- `Rejection`: Untracked external success before Actor rollback, duplicate queue/reference, adapter retry store, unbounded pending state or silent semantics change. + +## Environment and Artifact Identity + +- `Current binding`: [EXP-0095](./EXP-0095.md). +- `Accepted inputs`: [EXP-0099](./EXP-0099.md) sole Continuation, [EXP-0093](./EXP-0093.md) long retry, EXP-0063/EXP-0064 bounded placement/admission. +- `Audit artifact`: `/tmp/deos-0725-exp0100-audit.log`, SHA-256 `ef441273fbcdf19596b9dd5886d83af50acc1d06862dee28c801bf5aa6e217f8`. +- `Canonical route`: `scripts/actors-assurance.sh --backpressure-audit`. + +## Proof Obligations + +- `Freeze`: Frozen for the six existing-path categories below on EXP-0095. A newly discovered external commitment protocol requires decomposition before measurement. +- `Review triggers`: Independent commitment owner, queue, transport phase or retry store. +- `Decomposition review`: One cross-path no-parallel-retry claim. Split a category before measurement if it has an independently decidable commitment protocol. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | Existing backpressure paths preserve atomic effects and one Actor retry authority | One partial/duplicate commitment, adapter retry store or unbounded pending queue | Current source/test matrix plus consolidated regression | Self | Required | G7 backpressure closure | Accepted | + +## Measurements + +| Category | Commitment / pending owner | Failure disposition | Exact existing falsifier | +| --- | --- | --- | --- | +| DEX/Router market execution | Transactional Task adapter plus Router/AMM canonical state | Typed Temporary/Permanent result returns to Actor; no adapter retry store | `temporary_oracle_capacity_failure_rolls_back_economics_and_has_one_retry_owner`; `swap_exact_out_liquidity_boundary_fails_without_partial_execution` | +| XCM/message ingress | XCM executor holding and bounded message queues; certified Actor ingress is transactional precommit | Failed deposit returns exact holding and removes precommit events; no Actor retry fabricated | `xcm_deposit_failure_rolls_back_precommit_events_and_exact_holding`; `xcm_deposit_rejects_before_value_movement_when_funding_pending_overflows` | +| Observation ingress/fanout | Oracle publication transaction plus Actors dirty-feed/list/page cursors | Publication rollback on hook/queue rejection; fanout retains bounded cursor/fault state, never executes Actor | `oracle_publication_rolls_back_when_actor_change_hook_rejects`; `oracle_publication_rolls_back_when_crossing_transition_queue_is_full`; package `fanout_structural_fault_is_bounded_and_requires_repair_before_resume` | +| Funding/resource admission | Actor funding ledger and block resource reservation | Prevalidation/transaction rollback or custody-neutral terminal path; no effect before admission | `internal_asset_transfer_rolls_back_when_funding_pending_overflows`; `actor_fee_collector_rolls_back_completely_on_ledger_failure`; package `user_retry_insolvency_closes_before_effect_capacity_deferral` | +| Ready/Waiting/Wakeup capacity | Canonical scheduler references with bounded occupancy and one Actor hot pointer | Typed deferral/failure preserves or terminates the same Actor authority; no second queue | EXP-0093 long-retry ledger; package `loaded_cancellation_uses_supplied_run_despite_missing_or_stale_backing`; scheduler capacity regressions | +| Adapter-local rollback/simulation | Host pallet state inside storage transaction; adapter persists no retry state | Typed result settles through Actor-owned Run; simulation rolls back all host effects | `runtime_simulation_core_rolls_back_deos_adapter_effects`; `anchored_split_transfer_rolls_back_when_a_later_recipient_is_unavailable` | + +Source inspection finds no adapter-owned cursor, retry counter or Actor commitment registry. Bounded transport/fanout queues retain only their own delivery work; EXP-0099's Actor Run remains the sole process retry authority. + +## Derived Metrics + +Six existing categories map to canonical transactional or bounded-queue owners and zero independent Actor retry stores. The canonical route executes eight runtime and three package falsifiers under one source-identity guard. + +## Statistical / Variance Notes + +Deterministic rollback/ownership audit; provider liveness, latency and timing distributions are outside scope. + +## Result + +Current source/test ownership and the passing consolidated route support atomic effects and one Actor retry authority in every declared category. + +## Interpretation + +Transport and fanout queues own delivery continuation only, not Actor process continuation. Typed adapter failure becomes input to the one Actor Run transition; it does not authorize a parallel retry. + +## Decision + +Accepted. Existing backpressure paths fail atomically or retain work in their bounded owning queue, while Actor Run remains the sole process retry authority. + +## Rejected Alternatives and Tradeoffs + +- `Success-only tests`: Rejected because they cannot falsify partial commitment. +- `Transport queue as Actor retry`: Rejected unless current source proves shared authority, which would violate the governing contract. +- `New registry`: Excluded unless an existing path cannot satisfy atomicity with current owners. + +## Validity + +Establishes current source/test ownership and consolidated execution evidence by category. Does not establish provider liveness, untested future adapters or release acceptance. + +## Follow-Up / Next Gradient + +Proceed to G8 residual convergence and explicit production-constraint disposition. + +## Architecture Impact + +None; audit existing paths before considering new state or phases. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: G7 Existing-Path Backpressure Safety. +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: [EXP-0095](./EXP-0095.md), [EXP-0099](./EXP-0099.md). +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: [EXP-0093](./EXP-0093.md), [EXP-0063](./EXP-0063.md), [EXP-0064](./EXP-0064.md); no raw evidence copied. +- `Transfers question to`: None. +- `Produces input for`: G7/G8 release decision. +- `Reopen trigger`: New external commitment, adapter retry store, unbounded queue or binding change. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0101.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0101.md new file mode 100644 index 00000000..6d384185 --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0101.md @@ -0,0 +1,138 @@ +# EXP-0101 — Reactive Target Residual Convergence + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | G8 Evidence-Selected Residual Convergence | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-08 | +| Affected domain | ObservationCrossing target throughput | +| Physical mechanism | User/feed admission and Crossing detector candidate admission | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0087](./EXP-0087.md) — accepted target constraint | +| Successor experiment | None | + +## Decision Question + +Does the measured ObservationCrossing target constraint earn a conforming residual change for 0.7.25, or should G8 retain the exact limitation and reject speculative optimization? + +## Governing Specification + +- `Exact section`: `docs/actors-performance-assurance.specification.en.md` §§1.1–1.3, 2.1–2.3 owns the 10,000-User/100-eligible-block targets; pallet specification §§4.3, 8 owns Crossing semantics and bounded scheduler authority. +- `Invariant`: FIFO, immutable cutoff, Q1, causal floor, generation safety, one scheduler and component-wise admission remain unchanged. +- `Entry rule`: A micro change must improve a complete path by at least 5% or remove a measured owner without semantic/resource/fairness regression. +- `Reopen trigger`: Accepted EXP-0087 target impossibility permits investigation but does not authorize a geometry change. + +## Context and Hypothesis + +- `Measured owner`: Crossing materialization admits four candidates/block on current generated Weight; User membership is capped at 9,000/feed. +- `Old blocking reason`: Before G6, required User/reactive population and sustained fairness evidence were unresolved rather than falsified. +- `Changed assumption`: EXP-0087 now proves two independent target blockers on exact current production bounds. +- `Hypothesis`: No micro residual can close a 25-fold detector gap plus a separate population-cap gap without reopening physical geometry and target policy. +- `Materiality`: Speculative optimization would consume release risk while still failing the governing target. + +## Baseline and Candidates + +- `C0 Retain`: Preserve current C1 and publish the explicit target constraint. +- `C1 Micro owner reduction`: Reduce one existing Crossing component by at least 5% without changing topology. +- `C2 Geometry reopening`: Change batching/index/proof topology and User/feed admission sufficiently to construct and materialize 10,000 Users in 100 blocks. + +C2 is not a residual micro change. It is included only as the semantic classification of what target satisfaction would require. + +## Controls + +- `Controlled variables`: EXP-0095 binding, target definition, User identity, one-feed Crossing cause and all frozen scheduler invariants. +- `Changed variables`: Candidate disposition only; no implementation is authorized. +- `Confounders`: System throughput, broad ObservationChange fanout, scheduler service after detection and generated I/O versus physical proof. + +## Benchmark Class, Workloads, and Criteria + +- `Class`: Evidence-selected candidate-admission decision. +- `Workload`: Governing 10,000 prefunded one-Step User ObservationCrossing profile over 100 eligible blocks. +- `Metrics`: Constructible User population, candidates/block, target-window materialization, complete-path RefTime/ProofSize and semantic changes. +- `Acceptance`: A candidate must admit 10,000 Users/feed and at least 100 Crossing candidates/block while preserving all frozen invariants and passing G3–G7 refresh. +- `Rejection`: Less than 100 candidates/block, cap below 10,000, independent-maxima composition, proof discount or new priority/retry authority. + +## Environment and Artifact Identity + +- `Current binding`: [EXP-0095](./EXP-0095.md). +- `Constraint evidence`: [EXP-0087](./EXP-0087.md), SHA-256 `b645901e5f60c5668b5de2eed45102c64b5a9561afad1ffe1348e0b3d3a179bb`. +- `Candidate artifact`: None; C0 changes no code and C2 is rejected from the 0.7.25 residual lane. + +## Proof Obligations + +- `Freeze`: Frozen for the C0/C1/C2 disposition on EXP-0095. A separately authorized post-0.7.25 target-closing architecture may reopen through the declared falsifier. +- `Review triggers`: A conforming ≥100-candidate/block mechanism, 10,000-User admission proof or changed governing target. +- `Decomposition review`: One target-convergence decision. Split implementation candidates before measurement; do not accumulate designs here. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | A 0.7.25 residual candidate either closes both target blockers or is rejected | One conforming candidate constructs 10,000 Users and admits ≥100 candidates/block | Exact constraint/candidate disposition | Self | Required | G8 residual closure | Accepted: C0 retained, C1/C2 rejected | + +## Measurements + +Current baseline admits 9,000 User Crossing members/feed and four candidates/block. The target requires 10,000 and at least 100/block before scheduler service. A 5% micro improvement yields at most 4.2 candidates/block conceptually and does not change the membership cap; even a 100% detector improvement reaches only eight candidates/block. Target closure requires at least 25× current detector admission plus a separate 11.12% User-cap increase. + +## Derived Metrics + +- `Detector coverage`: `4 * 100 / 10,000 = 4%`. +- `Required detector multiplier`: `100 / 4 = 25×`. +- `User-cap deficit`: `1,000` identities, or `10,000 / 9,000 - 1 ≈ 11.12%`. + +These independent blockers are not composed into one Weight estimate. + +## Statistical / Variance Notes + +Deterministic configured bounds and generated admission; no timing sample or unobserved throughput is inferred. + +## Result + +C0 is retained: no residual implementation and the exact target constraint advances to production disposition. C1 is rejected because any plausible micro improvement remains target-irrelevant and leaves the independent User cap blocker. C2 is rejected from 0.7.25 because it is a new architecture/policy campaign requiring ≥25× detector admission, higher User/feed capacity and complete G3–G7 evidence regeneration, not a bounded residual. + +## Interpretation + +The impossibility result earned investigation but not implementation. Preserving C1 is safer and more truthful than a coefficient patch or unbounded late geometry rewrite. C0 does not grant release permission; it makes the unresolved governing-policy choice explicit. + +## Decision + +Accepted C0 for G8 residual convergence. Implement no reactive geometry change in 0.7.25; carry the precise constraint to Production Acceptance or Explicit Constraint. This is candidate disposition, not governing-policy permission to freeze or release. + +## Rejected Alternatives and Tradeoffs + +- `C1 / 5% coefficient patch`: Rejected as target-irrelevant and unsupported by physical proof. +- `System proxy`: Rejected by EXP-0087. +- `Broad fanout substitution`: Rejected because the target requires ObservationCrossing. +- `Independent maxima`: Rejected because User cap and detector Weight are separate blockers. +- `C2 late geometry reopening`: Rejected from 0.7.25 because it requires a new bounded architecture campaign and full downstream evidence refresh. + +## Validity + +Establishes C0 residual disposition and rejects C1/C2 for 0.7.25. Does not change C1, satisfy the target or grant production/release constraint permission. + +## Follow-Up / Next Gradient + +Request the owning governing-policy decision for the explicit production constraint. Without permission, Geometry Freeze remains blocked. + +## Architecture Impact + +No implementation change; C1 remains frozen and C0 carries the exact limitation forward. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: G8 Evidence-Selected Residual Convergence. +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: [EXP-0087](./EXP-0087.md), [EXP-0066](./EXP-0066.md), [EXP-0100](./EXP-0100.md). +- `Baseline of`: None. +- `Confirms`: None. +- `Invalidates`: None. +- `Uses evidence from`: [EXP-0087](./EXP-0087.md); no raw evidence copied. +- `Transfers question to`: None. +- `Produces input for`: G8 Production Acceptance or Explicit Constraint. +- `Reopen trigger`: Conforming target-closing candidate, target change or binding change. diff --git a/.agents/skills/architecture-experiments/tracks/actors/EXP-0102.md b/.agents/skills/architecture-experiments/tracks/actors/EXP-0102.md new file mode 100644 index 00000000..4f00b0aa --- /dev/null +++ b/.agents/skills/architecture-experiments/tracks/actors/EXP-0102.md @@ -0,0 +1,122 @@ +# EXP-0102 — Production Constraint Disposition + +| Field | Value | +| --- | --- | +| Status | Accepted | +| Record kind | Leaf | +| Former IDs | None | +| Parent question | G8 Production Acceptance or Explicit Constraint | +| Architecture release / experiment campaign | 0.7.25 frozen C1 production evidence | +| Date | 2026-09-08 | +| Affected domain | Production target disposition | +| Physical mechanism | None; governing-policy acceptance of an evidenced constraint | +| Primary track | [actors](./experiments.md) | +| Related tracks | None | +| Baseline experiment | [EXP-0066](./EXP-0066.md) — accepted negative constraint Synthesis | +| Successor experiment | None | + +## Decision Question + +May DEOS 0.7.25 retain frozen C1 and proceed toward Geometry Freeze with the exact User/reactive target and fairness limitations published rather than claiming performance-target compliance? + +## Governing Specification + +- `Evidence`: EXP-0066 owns G6 composition; EXP-0087 owns target impossibility; EXP-0091 owns fairness insufficiency; EXP-0101 owns C0 residual disposition. +- `Invariant`: No target rewrite, System proxy, hidden constraint, C1 semantic change or release claim beyond verified evidence. +- `Policy gate`: BACKLOG requires explicit governing-policy permission before constraint-based release closure. + +## Context and Hypothesis + +The user selected option 1 in the active 0.7.25 release-governance turn: permit the C0 constraint release path, publish the exact limitation and continue Geometry Freeze. This permission resolves policy disposition only; it does not waive validation or release gates. + +## Baseline and Candidates + +- `P0 Deny`: Keep 0.7.25 blocked pending a separately scoped geometry campaign. +- `P1 Permit C0`: Preserve C1, publish exact constraints and continue release closure without target-compliance claims. + +## Controls + +- `Controlled variables`: EXP-0095 binding, accepted G3–G7 evidence, governing targets and C1 invariants. +- `Changed variable`: Governing-policy permission only. +- `Confounders`: Permission is not evidence, target satisfaction, validation waiver or release authorization. + +## Benchmark Class, Workloads, and Criteria + +- `Class`: Policy disposition over accepted evidence; no benchmark. +- `Acceptance`: Explicit choice of P1 with constraints retained verbatim through release surfaces. +- `Rejection`: Claiming target compliance, changing evidence status or treating permission as authorization to publish/release. + +## Environment and Artifact Identity + +- `Production binding`: [EXP-0095](./EXP-0095.md). +- `Evidence synthesis`: [EXP-0066](./EXP-0066.md). +- `Policy evidence`: Explicit user reply `1` selecting “Permit C0 constraint release path” in the active session. + +## Proof Obligations + +- `Freeze`: Frozen to P1 on the accepted EXP-0066/EXP-0101 evidence set. +- `Review triggers`: Changed target evidence, binding, C1 semantics or withdrawal of policy permission. +- `Decomposition review`: Policy disposition only; release validation and publication remain separate owners. + +| Obligation ID | Claim | Smallest falsifier | Evidence class | Owning Experiment | Required/conditional | Downstream consequence | Status | +| --- | --- | --- | --- | --- | --- | --- | --- | +| O1 | Governing policy permits C0 constraint-based release closure | Permission denied/withdrawn or constraint omitted | Explicit policy choice plus accepted evidence | Self | Required | Geometry Freeze may proceed | Accepted | + +## Measurements + +No new measurement. Current constraint remains: 9,000 User Crossing members/feed, four candidates/block and at most 400/10,000 candidates materialized in the required 100-block window; required-population sustained fairness is unsupported. + +## Derived Metrics + +None beyond EXP-0087. + +## Statistical / Variance Notes + +Not applicable to policy disposition. + +## Result + +P1 selected. Geometry Freeze preparation may proceed while retaining the exact production constraint. + +## Interpretation + +DEOS 0.7.25 may be released as a truthful constrained framework checkpoint, not as compliant with performance-assurance §§2.1–2.3. + +## Decision + +Accepted P1. Preserve C1 and continue Geometry Freeze. Do not publish, tag, release or push without separate explicit authorization. + +## Rejected Alternatives and Tradeoffs + +- `P0 indefinite block`: Rejected by explicit policy choice. +- `Target-compliance wording`: Rejected because evidence remains negative. +- `Validation waiver`: Rejected; all release checks remain mandatory. + +## Validity + +Establishes policy permission for constraint-based release closure only. Does not establish target success, Geometry Freeze completion or publication authorization. + +## Follow-Up / Next Gradient + +Reconcile specification, implementation, tests, architecture, Wiki and release evidence while preserving the constraint; complete the Geometry Freeze gate. + +## Architecture Impact + +No physical change. Release truth must expose the limitation. + +## Relations + +- `Decomposes into`: None. +- `Satisfies obligation`: G8 Production Acceptance or Explicit Constraint. +- `Replaces`: None. +- `Supersedes`: None. +- `Refines`: None. +- `Contradicts`: None. +- `Depends on`: [EXP-0066](./EXP-0066.md), [EXP-0087](./EXP-0087.md), [EXP-0091](./EXP-0091.md), [EXP-0101](./EXP-0101.md). +- `Baseline of`: None. +- `Confirms`: [EXP-0101](./EXP-0101.md) C0 release-path disposition. +- `Invalidates`: None. +- `Uses evidence from`: EXP-0066, EXP-0087, EXP-0091 and EXP-0101; no raw evidence copied. +- `Transfers question to`: None. +- `Produces input for`: G8 Geometry Freeze and Release Closure. +- `Reopen trigger`: Permission withdrawal, changed target evidence, binding or C1 semantics. diff --git a/.agents/skills/architecture-experiments/tracks/actors/experiments.md b/.agents/skills/architecture-experiments/tracks/actors/experiments.md index bdff7568..edb4a449 100644 --- a/.agents/skills/architecture-experiments/tracks/actors/experiments.md +++ b/.agents/skills/architecture-experiments/tracks/actors/experiments.md @@ -1,4 +1,6 @@ -# Actors Experiment Track +# Actors Experiments + +This is the sole Actors experiment entrypoint and owner of shared track metadata, navigation, dependency overview and conditional research portfolio. Individual EXP records own one question, decision and its necessary evidence; do not create separate track maps or retrospective archives. | Field | Value | | --- | --- | @@ -19,6 +21,7 @@ ## Accepted Physical Baseline +- Current 0.7.25 physical baseline: [EXP-0025](./EXP-0025.md), Accepted with decision scope physical architecture only; **C1 PHYSICAL GEOMETRY: FROZEN**. Production Weight, bindings, throughput and release Geometry Freeze remain separate gates. - 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 @@ -27,6 +30,8 @@ - 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. +- Sealed Ready payload versus compact per-page or directory liveness, including physical-write flattening and independent committed-prefix durability, only after sole-owner C1 production Weight and W0–W9 expose Ready rewrite cost. +- Trie/StorageProof/PoV anatomy, truthful storage cardinality, proof-oriented pages, canonical-byte compression, shared immutable Contract authority, safe runtime-generated key locality and actual-PoV-aware Weight reclaim require a material measured owner from EXP-0066 or a prerequisite production experiment and a dedicated smallest falsifier. ## Cross-Track Dependencies @@ -40,14 +45,14 @@ ## 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. +Sealed historical decision IDs are stable. Provisional IDs may change only in an explicit provenance-preserving graph normalization. Former provisional navigation IDs are qualified by their exact Git baseline in the migrated records and manifest; explicit compaction gives the active campaign a continuous numeric sequence. The three projections below distinguish decomposition, hard prerequisite ordering and evidence reuse. | 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-0004](./EXP-0004.md) | 0.7.25 | Invalidated | Which unique-owner Step-tail representation clears the nonterminal current-Step proof gate? | B0/C1/C2/C3 manifests `fbbcbae9` / `8fedf45e` / `45a93afa` / `a00c4ab9` | P32 measurements remain valid; W1 criterion ownership and runtime `12/24/48` assumptions changed | EXP-0026 owns the new baseline | | [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 | @@ -64,29 +69,914 @@ IDs are immutable monotonic identities within this track, not execution-order nu | [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-0021](./EXP-0021.md) | 0.7.24 | Superseded | 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 | Later 12-Step reference-profile decision supersedes the expressiveness premise; EXP-0026 | +| [EXP-0022](./EXP-0022.md) | 0.7.24 | Invalidated | Does exact one-third Control beat accepted 30/35/35? | EXP-0006 accepted 30/70 | Historical W1/W2 never finalized; repaired baseline is 9.1075 Steps/block and Actor-Control-bound | Bind exact identity, saturate users, then EXP-0025 | | [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 | +| [EXP-0025](./EXP-0025.md) | 0.7.25 | Accepted | Did ticket-addressed frame-owned control preserve semantics and bounded ownership sufficiently to replace scalar geometry? | A0 `5c35415`; retained C1 `5b46b19` | Physical architecture only: C1 FROZEN; G1/G2 closed, production acceptance unresolved | EXP-0028, EXP-0029 and EXP-0063 through EXP-0066 | +| [EXP-0026](./EXP-0026.md) | 0.7.25 | Rejected | How much do a 12-Step reference ceiling and lockstep `24/48` Opening bounds reduce EXP-0025 middle-Step and maximum geometry? | Corrected P12 Wasm `d555298a`; five focused artifacts | P12-only middle claim rejected at unchanged `6,876.52`; product policy retained; maximum lifecycle proof falls about 51% | Baseline of EXP-0027 | +| [EXP-0027](./EXP-0027.md) | 0.7.25 | Rejected | Can the existing immutable Contract-body hold charge replace per-Step whole-body tail rescans? | C1 Wasm `67accb9a`; exact B0/C1 maximum artifacts | W2 proof/read set is identical because execution independently reconstructs the full Contract | B0 restored; no automatic successor | +| [EXP-0028](./EXP-0028.md) | 0.7.25 | Accepted | Synthesis: Actor Control Weight Ownership and Phase Accounting | EXP-0025; accepted finite child map | Phase ownership, no-double-charge composition and settlement containment accepted; numeric admission and exact artifacts remain downstream | EXP-0064 | +| [EXP-0029](./EXP-0029.md) | 0.7.25 | Accepted | Synthesis: Reachable Actor State Geometry | EXP-0025; O1–O9 child composition | Constrained mock/reference reachable domain accepted; numeric admission and exact artifacts remain downstream | EXP-0064 | +| [EXP-0030](./EXP-0030.md) | 0.7.25 | Accepted | Leaf: Action Invocation Receipt Ownership | EXP-0025; isolated owner and admitted reference-Wasm CompleteMax | Exactly one Control owner; inclusive CompleteMax and zero-fee System selection accepted in scope | EXP-0028 | +| [EXP-0031](./EXP-0031.md) | 0.7.25 | Accepted | Leaf: Pipeline Opening Collection Ownership | EXP-0025; reference-Wasm large-head and complete User Opening witnesses | Exactly one inclusive Pipeline collection owner for admitted User Opening | EXP-0028 | +| [EXP-0032](./EXP-0032.md) | 0.7.25 | Accepted | Leaf: Current Predicate Component Bound | EXP-0025; generated three-input envelope and V10 composed selector | Reference Current component and maximum/actual selector accepted in scope | EXP-0028 | +| [EXP-0033](./EXP-0033.md) | 0.7.25 | Accepted | Leaf: Opening Predicate Capture Bound | EXP-0025; exhaustive reference-Wasm allocation domain and V10 composed selector | Reference Opening predicate capture component and maximum/actual selector accepted in scope | EXP-0028 | +| [EXP-0034](./EXP-0034.md) | 0.7.25 | Accepted | Leaf: Opening Amount Capture Bound | EXP-0025; complete typed reference allocation/host-state evidence and V9 selector | Reference Opening amount capture component and maximum/actual selector accepted in scope | EXP-0028 | +| [EXP-0035](./EXP-0035.md) | 0.7.25 | Accepted | Leaf: Zero-Result Predicate Traversal Ownership | EXP-0025; generated fixed full-Contract traversal and composed selector | One zero-result predicate owner selected mutually exclusively from nonzero and inclusive paths | EXP-0028 | +| [EXP-0036](./EXP-0036.md) | 0.7.25 | Accepted | Leaf: Zero-Result Amount Traversal Ownership | EXP-0025; generated fixed full-Contract traversal and composed selector | One zero-result amount owner selected mutually exclusively from nonzero and inclusive paths | EXP-0028 | +| [EXP-0037](./EXP-0037.md) | 0.7.25 | Accepted | Leaf: User and System Zero-Step Base Ownership | EXP-0025; controlled System/User comparison and exact maximum-head reference-Wasm witness | Manual actor-class partition and maximum-head User owner accepted in scope | EXP-0028 | +| [EXP-0038](./EXP-0038.md) | 0.7.25 | Accepted | Leaf: Temporal Zero-Step Opening Ownership | EXP-0025; retained four-branch reference-Wasm evidence | AtTime/Cadenced persistence and authored-close owners accepted in scope | EXP-0028 | +| [EXP-0039](./EXP-0039.md) | 0.7.25 | Accepted | Leaf: Crossing Opening Rearm Ownership | EXP-0025; retained membership/cursor/availability reference-Wasm evidence | Available persistence/close envelopes and unavailable refusal accepted in scope | EXP-0028 | +| [EXP-0040](./EXP-0040.md) | 0.7.25 | Accepted | Leaf: Observation Subscription Cleanup Ownership | EXP-0025; retained dense/sparse unlink, dirty-cursor and free-page reference-Wasm evidence | One component-wise Observation authored-close cleanup owner accepted in scope | EXP-0028 | +| [EXP-0041](./EXP-0041.md) | 0.7.25 | Accepted | Leaf: Pipeline Admission Apoptosis Ownership | EXP-0025; maximum-Contract Crossing/future-window reference-Wasm witness | One inclusive custody-neutral apoptosis owner accepted in scope | EXP-0028 | +| [EXP-0042](./EXP-0042.md) | 0.7.25 | Accepted | Leaf: Public Close Cleanup Ownership | EXP-0025; admitted joint maximum-Step retry/Crossing reference-Wasm witness | One inclusive public-close owner accepted in scope | EXP-0028 | +| [EXP-0043](./EXP-0043.md) | 0.7.25 | Accepted | Leaf: Window Expiry Close Ownership | EXP-0025; singleton-Block unsignaled Immutable zero-Step User reference-Wasm witness | One inclusive source-consumption and custody-neutral expiry owner accepted in scope | EXP-0028 | +| [EXP-0044](./EXP-0044.md) | 0.7.25 | Accepted | Leaf: User State-Hold Reconciliation Bound | EXP-0025; complete User Opening reference-Wasm witness | Inclusive post-publication User hold owner accepted in scope | EXP-0028 | +| [EXP-0045](./EXP-0045.md) | 0.7.25 | Measured | Leaf: Authored Amount and LastFunding Algebra | EXP-0025; exact child/source provenance | Scoped evidence retained; complete release acceptance open | EXP-0029 | +| [EXP-0046](./EXP-0046.md) | 0.7.25 | Measured | Leaf: Predicate and DNF Reachable Geometry | EXP-0025; exact child/source provenance | Scoped evidence retained; complete release acceptance open | EXP-0029 | +| [EXP-0047](./EXP-0047.md) | 0.7.25 | Accepted | Leaf: Funding Snapshot Consumption Bound | EXP-0025; reference-Wasm 1..24 authored-source component and certified Signed ingress | Reference funding-consumption component accepted in scope | EXP-0028 | +| [EXP-0048](./EXP-0048.md) | 0.7.25 | Accepted | Leaf: Run and Contract Head-Tail Reachability | EXP-0025; public mock/reference constructors | Admitted zero-Step/head/tail/cursor/Run geometry accepted; refused dense maxima excluded | EXP-0029 | +| [EXP-0049](./EXP-0049.md) | 0.7.25 | Accepted | Leaf: Host Active Identity and Sovereign Population | EXP-0025; retained-genesis host histories | Joint active/identity/sovereign additional-System bound accepted in scope | EXP-0029 | +| [EXP-0050](./EXP-0050.md) | 0.7.25 | Accepted | Leaf: Legal Waiting Primary Population | EXP-0025; ordinary saturated lifecycle histories | Existing/new/full-page publication and upward/tail removal populations accepted | EXP-0029 | +| [EXP-0051](./EXP-0051.md) | 0.7.25 | Accepted | Leaf: Legal Waiting Reference Population | EXP-0025; ordinary Cadenced/close histories | Mixed-page unlink and saturated final-Tick removal populations accepted | EXP-0029 | +| [EXP-0052](./EXP-0052.md) | 0.7.25 | Accepted | Leaf: Joint Suspended and User Close Geometry | EXP-0025; admitted reference User-close probes | Real suspension/capture/detector/hold geometry accepted for admitted subdomain | EXP-0029 | +| [EXP-0053](./EXP-0053.md) | 0.7.25 | Measured | Leaf: V10 Admission Refusal Lower Bound | EXP-0025; exact child/source provenance | Scoped evidence retained; complete release acceptance open | EXP-0029 | +| [EXP-0054](./EXP-0054.md) | 0.7.25 | Measured | Leaf: Native Admission Refusal and Precedence | EXP-0025; exact child/source provenance | Scoped evidence retained; complete release acceptance open | EXP-0064 | +| [EXP-0055](./EXP-0055.md) | 0.7.25 | Accepted | Leaf: Waiting Publication Production Bound | EXP-0025; common-Wasm complete branch and 334-byte cell evidence | Component-wise publication envelope accepted for current reference binding | EXP-0063 | +| [EXP-0056](./EXP-0056.md) | 0.7.25 | Accepted | Leaf: Waiting Removal and Heap Repair Production Bound | EXP-0025; common-Wasm complete public-close removal family | Inclusive final-Tick/downward-repair bound accepted in scope | EXP-0063 | +| [EXP-0057](./EXP-0057.md) | 0.7.25 | Accepted | Leaf: Useful Trigger Detection Weight Coverage | EXP-0020/EXP-0025; six current-source reference-Wasm occurrence owners | Six-family useful-only ownership and redundant-latch exclusion accepted in scope | EXP-0028 | +| [EXP-0058](./EXP-0058.md) | 0.7.25 | Accepted | Leaf: Scheduler Source and Destination Weight Coverage | EXP-0025, EXP-0039, EXP-0041–0043; complete-owner settlement | Source/destination and Waiting overlap matrix accepted; EXP-0063 supplies numeric envelopes | EXP-0028 | +| [EXP-0059](./EXP-0059.md) | 0.7.25 | Accepted | Leaf: Running and Suspended Control Weight Coverage | EXP-0025; admitted extrema, trace audit and exact successors | Exact finite successor envelope accepted; low-capture Continued→Wakeup contains dense `S=3` | EXP-0028 | +| [EXP-0060](./EXP-0060.md) | 0.7.25 | Accepted | Leaf: Control and Effect Settlement Containment | EXP-0025; production provider audit, finite selector matrix and one-unit rollback witnesses | Typed effect/control actuals and transactional component-wise containment accepted in scope | EXP-0028 | +| [EXP-0061](./EXP-0061.md) | 0.7.25 | Accepted | Leaf: Joint Encoded Actor Control Cell Reachability | EXP-0025; pre-fission uncovered owner | Satisfied: reference-binding 334-byte maximum with public Wasm attainment | EXP-0029 | +| [EXP-0062](./EXP-0062.md) | 0.7.25 | Accepted | Leaf: Nonzero Action Fee Collection Ownership | EXP-0025; reference-Wasm isolated ledger transfer and phase selector | One native-ledger Control owner selected independently of receipt and Pipeline collection | EXP-0028 | +| [EXP-0063](./EXP-0063.md) | 0.7.25 | Accepted | Synthesis: Waiting / Deadline Heap Production Envelope | EXP-0025; accepted publication/removal children and phase attribution | Mutually exclusive current-binding publication and removal envelopes accepted | EXP-0064 | +| [EXP-0064](./EXP-0064.md) | 0.7.25 | Accepted | Synthesis: Actor Admission Envelope | EXP-0025; all four required child scopes satisfied | Contract-level product admission, finite Waiting selector containment and pre-G5 genesis fit accepted | EXP-0065 | +| [EXP-0065](./EXP-0065.md) | 0.7.25 | Invalidated | Leaf: Production Binding Closure | EXP-0025; accepted G4 inputs and pinned exact-tree artifact cohort | Exact Weight/Wasm/live metadata/PAPI/ABI/bounds/vector bundle and finalized browser/staking consumers converge | EXP-0066 | +| [EXP-0066](./EXP-0066.md) | 0.7.25 | Accepted | Synthesis: Integrated Actor Throughput | EXP-0025/EXP-0065; User coverage prerequisite EXP-0067 | Finite child-obligation map; primary evidence relocated without remeasurement; targets and integrated acceptance remain open | Child decisions and G4 qualification | +| [EXP-0067](./EXP-0067.md) | 0.7.25 | Accepted | Leaf: User Opening Settlement Binding | Accepted EXP-0069/0094 inclusive owners and EXP-0095 exact binding | Zero-tail H16 and positive-tail T16 are shared by admission/actual settlement without overlap or new context | Invalidated/current G6 owners | +| [EXP-0068](./EXP-0068.md) | 0.7.25 | Accepted | Leaf: User Opening Completion Inner Owner | EXP-0067 classification; existing System inner completion boundary | Partitioned single/positive-tail models match all 447 DB samples and fee/hold postconditions; attained reference domain only; unsplit fit rejected | EXP-0067 live geometry/selection qualification | +| [EXP-0069](./EXP-0069.md) | 0.7.25 | Accepted | Leaf: User Completion Header Envelope | EXP-0067 native 157/670-byte alias; EXP-0068 scoped inner models | Same-Wasm H16 retains 9/5 I/O, adds exact 513-byte proof and dominates H0 at `893,949,000 / 7,990` | EXP-0067 live selector composition | +| [EXP-0070](./EXP-0070.md) | 0.7.25 | Accepted | Leaf: System Schedule Throughput and Censored Service | EXP-0066 W0–W2; EXP-0095 current binding | Current W1/Cadenced W2 retain 132 successes, Manual W2 1,694; zero failures and all targets missed | EXP-0066 O1 | +| [EXP-0071](./EXP-0071.md) | 0.7.25 | Accepted | Leaf: Contract Geometry Service | EXP-0066 W3; EXP-0095 current binding | 126 Cycles/252 ordered Steps complete across all nine cells; maximum bounded Running gap 15 | EXP-0066 O2 | +| [EXP-0072](./EXP-0072.md) | 0.7.25 | Accepted | Leaf: Heterogeneous Task Service | EXP-0066 W4; EXP-0095 current binding | Each demand mode: 123 Transfer, six SwapOut, three StopCycle; 132 total successes, zero failures, target missed | EXP-0066 O3 | +| [EXP-0073](./EXP-0073.md) | 0.7.25 | Accepted | Leaf: Lifecycle Outcome and Prefix Durability | EXP-0066 pre-fission W5 | Bounded event/Run chronology and retained prefix | EXP-0066 O4 | +| [EXP-0074](./EXP-0074.md) | 0.7.25 | Accepted | Leaf: Arrival Eligibility and Generation | EXP-0066 pre-fission W6 | Block/tick, pause/window and generation boundaries | EXP-0066 O5 | +| [EXP-0075](./EXP-0075.md) | 0.7.25 | Accepted | Leaf: Due-Frontier Population Isolation | EXP-0066 pre-fission W7 | Matched due service despite non-due population | EXP-0066 O6 | +| [EXP-0076](./EXP-0076.md) | 0.7.25 | Accepted | Leaf: Ready Tombstone Prefix Pressure | EXP-0066 pre-fission W8 | Bounded reclaim and chronological live prefix | EXP-0066 O7 | +| [EXP-0077](./EXP-0077.md) | 0.7.25 | Accepted | Leaf: User Resource Independence | EXP-0066 pre-fission W9 | Separate Actor accounting; RefTime-heavy fallback only | EXP-0066 O8 | +| [EXP-0078](./EXP-0078.md) | 0.7.25 | Accepted | Leaf: Temporal Materialization Phase Ownership | EXP-0066 matched phase cohort | Materialization/service phase and captured-cutoff deferral | EXP-0066 O9 | +| [EXP-0079](./EXP-0079.md) | 0.7.25 | Accepted | Leaf: Temporal Generated Charge and Admission Ownership | EXP-0066 production-selector ledger | Component-wise charged/admitted owners; not physical I/O | EXP-0066 O10 | +| [EXP-0080](./EXP-0080.md) | 0.7.25 | Accepted | Leaf: Matched-Phase StorageProof Node Coverage | EXP-0066 repeated key/root cohort | Closed raw-node coverage in retained 18-block domain only | EXP-0066 O11 | +| [EXP-0081](./EXP-0081.md) | 0.7.25 | Accepted | Leaf: Manual Service Stop Reconstruction | EXP-0066 native prefix diagnostics | Eligible live heads and phase-local budgets; no universal stop trace | EXP-0066 O12 | +| [EXP-0082](./EXP-0082.md) | 0.7.25 | Accepted | Leaf: Successful System Transfer Charge Ownership | EXP-0066 preparation ledger | Separated successful-Step and no-progress charge delta | EXP-0066 O13 | +| [EXP-0083](./EXP-0083.md) | 0.7.25 | Accepted | Leaf: No-Due and Genesis Control Baseline | EXP-0066 baseline ledger | Fixed coordination separate from genesis anchoring | EXP-0066 O14 | +| [EXP-0084](./EXP-0084.md) | 0.7.25 | Accepted | Leaf: System Schedule Charged-Owner Accounting | EXP-0066 native worker/retained-Wasm cohorts | Bounded and full Actor-only schedule equations and linkage | EXP-0066 O15 | +| [EXP-0085](./EXP-0085.md) | 0.7.25 | Accepted | Leaf: Signed-Demand Accounting Evidence Linkage | EXP-0066 signed native ledger plus EXP-0095 exact binding | 100 signed native-authored blocks replay the same bodies/state through exact Wasm with matching headers, roots and proofs | EXP-0066 O16 | +| [EXP-0086](./EXP-0086.md) | 0.7.25 | Accepted | Leaf: Lifecycle Charged-Owner Accounting | EXP-0066 W5 resource ledger | Bounded nonduplicated lifecycle/effect settlement | EXP-0066 O17 | +| [EXP-0087](./EXP-0087.md) | 0.7.25 | Accepted | Leaf: User and Reactive Target Population Coverage | EXP-0067 qualification before execution | Existing open target-domain obligation; no new measurement | EXP-0066 O18 | +| [EXP-0088](./EXP-0088.md) | 0.7.25 | Accepted | Leaf: Funded User Fee-Path Resource Coverage | Current funded StopCycle pair and completion-header alias | Bounded Trigger/Pipeline monetary and resource separation; Action transferred to EXP-0097 | EXP-0066 O19 | +| [EXP-0089](./EXP-0089.md) | 0.7.25 | Accepted | Leaf: Reactive Executed-Path Resource Coverage | Qualified binding and EXP-0074 outcomes | Existing open detector/fanout/service accounting obligation | EXP-0066 O20 | +| [EXP-0090](./EXP-0090.md) | 0.7.25 | Accepted | Leaf: Heavy Economic Task Resource Coverage | EXP-0072 bounded Task mix | Existing open liquidity-heavy/Shared Economic coverage | EXP-0066 O21 | +| [EXP-0091](./EXP-0091.md) | 0.7.25 | Accepted | Leaf: Sustained Service Evidence Sufficiency | EXP-0070–EXP-0077 bounded observations | Existing open fairness obligation; censoring is not a service bound | EXP-0066 O22 | +| [EXP-0092](./EXP-0092.md) | 0.7.25 | Accepted | Leaf: Remaining Service-Stop Evidence | EXP-0081 bounded Manual proof | Existing open phase-local stop obligation; no extrapolated proof | EXP-0066 O23 | +| [EXP-0093](./EXP-0093.md) | 0.7.25 | Accepted | Leaf: Long Retry Resource Placement | EXP-0073/EXP-0086 short Ready retry | Existing open longer retry/wakeup accounting | EXP-0066 O24 | +| [EXP-0094](./EXP-0094.md) | 0.7.25 | Accepted | Leaf: Combined User Completion Header and Tail Envelope | Accepted EXP-0068 tail and EXP-0069 header axes | Same-Wasm T16 covers every DB sample and adds exact 513-byte proof over T across `t=1..3` | EXP-0067 live selector composition | +| [EXP-0095](./EXP-0095.md) | 0.7.25 | Accepted | Leaf: Production Binding Refresh | EXP-0067 selector correction plus EXP-0096 cadence fix invalidated prior candidates | Weight `9ce37a…`, Wasm `25b969…`, unchanged metadata/client identities, refreshed observation evidence and full exact-tree gate converge | EXP-0067 decision | +| [EXP-0096](./EXP-0096.md) | 0.7.25 | Accepted | Leaf: Cadenced Latch Authority Under Ready Backpressure | EXP-0095 package-heavy breaker failure | Detector invalidation before activation preserves one loadable deferred authority; focused maximum profile passes twice | EXP-0095 artifact regeneration | +| [EXP-0097](./EXP-0097.md) | 0.7.25 | Accepted | Leaf: Funded User Action Fee-Path Coverage | EXP-0088 bounded Pipeline closure | Existing open successful Action monetary/resource mapping | EXP-0066 O25 | +| [EXP-0098](./EXP-0098.md) | 0.7.25 | Accepted | Leaf: Crossing Executed-Path Resource Coverage | +| [EXP-0099](./EXP-0099.md) | 0.7.25 | Accepted | Leaf: Continuation Authority Ownership Audit | +| [EXP-0100](./EXP-0100.md) | 0.7.25 | Accepted | Leaf: Existing-Path Backpressure Safety Audit | +| [EXP-0101](./EXP-0101.md) | 0.7.25 | Accepted | Leaf: Reactive Target Residual Convergence | +| [EXP-0102](./EXP-0102.md) | 0.7.25 | Accepted | Leaf: Production Constraint Disposition | EXP-0074 accepted W6 chronology | Independent Crossing detector/latch and service accounting | EXP-0066 O26 | ## 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. +The three projections have separate meanings: Synthesis-to-child decomposition, required-input dependency, and scoped evidence reuse. Decomposition and hard prerequisites are independently acyclic; a parent link alone does not create a prerequisite. The private validator regenerates this canonical projection with `--write-index`. + + + +Scope: the declared campaign and directly related external inputs/consumers. Decomposition means constituent proofs; hard dependencies mean required input; dotted evidence flow means scoped reuse, not ordering. + +**Decomposition** + +```mermaid +flowchart TD + actors_EXP_0028["actors/EXP-0028: Synthesis / Accepted"] + actors_EXP_0029["actors/EXP-0029: Synthesis / Accepted"] + actors_EXP_0030["actors/EXP-0030: Leaf / Accepted"] + actors_EXP_0031["actors/EXP-0031: Leaf / Accepted"] + actors_EXP_0032["actors/EXP-0032: Leaf / Accepted"] + actors_EXP_0033["actors/EXP-0033: Leaf / Accepted"] + actors_EXP_0034["actors/EXP-0034: Leaf / Accepted"] + actors_EXP_0035["actors/EXP-0035: Leaf / Accepted"] + actors_EXP_0036["actors/EXP-0036: Leaf / Accepted"] + actors_EXP_0037["actors/EXP-0037: Leaf / Accepted"] + actors_EXP_0038["actors/EXP-0038: Leaf / Accepted"] + actors_EXP_0039["actors/EXP-0039: Leaf / Accepted"] + actors_EXP_0040["actors/EXP-0040: Leaf / Accepted"] + actors_EXP_0041["actors/EXP-0041: Leaf / Accepted"] + actors_EXP_0042["actors/EXP-0042: Leaf / Accepted"] + actors_EXP_0043["actors/EXP-0043: Leaf / Accepted"] + actors_EXP_0044["actors/EXP-0044: Leaf / Accepted"] + actors_EXP_0045["actors/EXP-0045: Leaf / Measured"] + actors_EXP_0046["actors/EXP-0046: Leaf / Measured"] + actors_EXP_0047["actors/EXP-0047: Leaf / Accepted"] + actors_EXP_0048["actors/EXP-0048: Leaf / Accepted"] + actors_EXP_0049["actors/EXP-0049: Leaf / Accepted"] + actors_EXP_0050["actors/EXP-0050: Leaf / Accepted"] + actors_EXP_0051["actors/EXP-0051: Leaf / Accepted"] + actors_EXP_0052["actors/EXP-0052: Leaf / Accepted"] + actors_EXP_0053["actors/EXP-0053: Leaf / Measured"] + actors_EXP_0054["actors/EXP-0054: Leaf / Measured"] + actors_EXP_0055["actors/EXP-0055: Leaf / Accepted"] + actors_EXP_0056["actors/EXP-0056: Leaf / Accepted"] + actors_EXP_0057["actors/EXP-0057: Leaf / Accepted"] + actors_EXP_0058["actors/EXP-0058: Leaf / Accepted"] + actors_EXP_0059["actors/EXP-0059: Leaf / Accepted"] + actors_EXP_0060["actors/EXP-0060: Leaf / Accepted"] + actors_EXP_0061["actors/EXP-0061: Leaf / Accepted"] + actors_EXP_0062["actors/EXP-0062: Leaf / Accepted"] + actors_EXP_0063["actors/EXP-0063: Synthesis / Accepted"] + actors_EXP_0064["actors/EXP-0064: Synthesis / Accepted"] + actors_EXP_0066["actors/EXP-0066: Synthesis / Accepted"] + actors_EXP_0070["actors/EXP-0070: Leaf / Accepted"] + actors_EXP_0071["actors/EXP-0071: Leaf / Accepted"] + actors_EXP_0072["actors/EXP-0072: Leaf / Accepted"] + actors_EXP_0073["actors/EXP-0073: Leaf / Accepted"] + actors_EXP_0074["actors/EXP-0074: Leaf / Accepted"] + actors_EXP_0075["actors/EXP-0075: Leaf / Accepted"] + actors_EXP_0076["actors/EXP-0076: Leaf / Accepted"] + actors_EXP_0077["actors/EXP-0077: Leaf / Accepted"] + actors_EXP_0078["actors/EXP-0078: Leaf / Accepted"] + actors_EXP_0079["actors/EXP-0079: Leaf / Accepted"] + actors_EXP_0080["actors/EXP-0080: Leaf / Accepted"] + actors_EXP_0081["actors/EXP-0081: Leaf / Accepted"] + actors_EXP_0082["actors/EXP-0082: Leaf / Accepted"] + actors_EXP_0083["actors/EXP-0083: Leaf / Accepted"] + actors_EXP_0084["actors/EXP-0084: Leaf / Accepted"] + actors_EXP_0085["actors/EXP-0085: Leaf / Accepted"] + actors_EXP_0086["actors/EXP-0086: Leaf / Accepted"] + actors_EXP_0087["actors/EXP-0087: Leaf / Accepted"] + actors_EXP_0088["actors/EXP-0088: Leaf / Accepted"] + actors_EXP_0089["actors/EXP-0089: Leaf / Accepted"] + actors_EXP_0090["actors/EXP-0090: Leaf / Accepted"] + actors_EXP_0091["actors/EXP-0091: Leaf / Accepted"] + actors_EXP_0092["actors/EXP-0092: Leaf / Accepted"] + actors_EXP_0093["actors/EXP-0093: Leaf / Accepted"] + actors_EXP_0097["actors/EXP-0097: Leaf / Accepted"] + actors_EXP_0098["actors/EXP-0098: Leaf / Accepted"] + actors_EXP_0028 --> actors_EXP_0030 + actors_EXP_0028 --> actors_EXP_0031 + actors_EXP_0028 --> actors_EXP_0032 + actors_EXP_0028 --> actors_EXP_0033 + actors_EXP_0028 --> actors_EXP_0034 + actors_EXP_0028 --> actors_EXP_0035 + actors_EXP_0028 --> actors_EXP_0036 + actors_EXP_0028 --> actors_EXP_0037 + actors_EXP_0028 --> actors_EXP_0038 + actors_EXP_0028 --> actors_EXP_0039 + actors_EXP_0028 --> actors_EXP_0040 + actors_EXP_0028 --> actors_EXP_0041 + actors_EXP_0028 --> actors_EXP_0042 + actors_EXP_0028 --> actors_EXP_0043 + actors_EXP_0028 --> actors_EXP_0044 + actors_EXP_0028 --> actors_EXP_0047 + actors_EXP_0028 --> actors_EXP_0057 + actors_EXP_0028 --> actors_EXP_0058 + actors_EXP_0028 --> actors_EXP_0059 + actors_EXP_0028 --> actors_EXP_0060 + actors_EXP_0028 --> actors_EXP_0062 + actors_EXP_0029 --> actors_EXP_0045 + actors_EXP_0029 --> actors_EXP_0046 + actors_EXP_0029 --> actors_EXP_0048 + actors_EXP_0029 --> actors_EXP_0049 + actors_EXP_0029 --> actors_EXP_0050 + actors_EXP_0029 --> actors_EXP_0051 + actors_EXP_0029 --> actors_EXP_0052 + actors_EXP_0029 --> actors_EXP_0053 + actors_EXP_0029 --> actors_EXP_0061 + actors_EXP_0063 --> actors_EXP_0055 + actors_EXP_0063 --> actors_EXP_0056 + actors_EXP_0064 --> actors_EXP_0028 + actors_EXP_0064 --> actors_EXP_0029 + actors_EXP_0064 --> actors_EXP_0054 + actors_EXP_0064 --> actors_EXP_0063 + actors_EXP_0066 --> actors_EXP_0070 + actors_EXP_0066 --> actors_EXP_0071 + actors_EXP_0066 --> actors_EXP_0072 + actors_EXP_0066 --> actors_EXP_0073 + actors_EXP_0066 --> actors_EXP_0074 + actors_EXP_0066 --> actors_EXP_0075 + actors_EXP_0066 --> actors_EXP_0076 + actors_EXP_0066 --> actors_EXP_0077 + actors_EXP_0066 --> actors_EXP_0078 + actors_EXP_0066 --> actors_EXP_0079 + actors_EXP_0066 --> actors_EXP_0080 + actors_EXP_0066 --> actors_EXP_0081 + actors_EXP_0066 --> actors_EXP_0082 + actors_EXP_0066 --> actors_EXP_0083 + actors_EXP_0066 --> actors_EXP_0084 + actors_EXP_0066 --> actors_EXP_0085 + actors_EXP_0066 --> actors_EXP_0086 + actors_EXP_0066 --> actors_EXP_0087 + actors_EXP_0066 --> actors_EXP_0088 + actors_EXP_0066 --> actors_EXP_0089 + actors_EXP_0066 --> actors_EXP_0090 + actors_EXP_0066 --> actors_EXP_0091 + actors_EXP_0066 --> actors_EXP_0092 + actors_EXP_0066 --> actors_EXP_0093 + actors_EXP_0066 --> actors_EXP_0097 + actors_EXP_0066 --> actors_EXP_0098 +``` + +**Hard dependencies** + +```mermaid +flowchart TD + actors_EXP_0014["actors/EXP-0014: external"] + actors_EXP_0015["actors/EXP-0015: external"] + actors_EXP_0020["actors/EXP-0020: external"] + actors_EXP_0025["actors/EXP-0025: external"] + actors_EXP_0028["actors/EXP-0028: Synthesis / Accepted"] + actors_EXP_0029["actors/EXP-0029: Synthesis / Accepted"] + actors_EXP_0030["actors/EXP-0030: Leaf / Accepted"] + actors_EXP_0031["actors/EXP-0031: Leaf / Accepted"] + actors_EXP_0032["actors/EXP-0032: Leaf / Accepted"] + actors_EXP_0033["actors/EXP-0033: Leaf / Accepted"] + actors_EXP_0034["actors/EXP-0034: Leaf / Accepted"] + actors_EXP_0035["actors/EXP-0035: Leaf / Accepted"] + actors_EXP_0036["actors/EXP-0036: Leaf / Accepted"] + actors_EXP_0037["actors/EXP-0037: Leaf / Accepted"] + actors_EXP_0038["actors/EXP-0038: Leaf / Accepted"] + actors_EXP_0039["actors/EXP-0039: Leaf / Accepted"] + actors_EXP_0040["actors/EXP-0040: Leaf / Accepted"] + actors_EXP_0041["actors/EXP-0041: Leaf / Accepted"] + actors_EXP_0042["actors/EXP-0042: Leaf / Accepted"] + actors_EXP_0043["actors/EXP-0043: Leaf / Accepted"] + actors_EXP_0044["actors/EXP-0044: Leaf / Accepted"] + actors_EXP_0045["actors/EXP-0045: Leaf / Measured"] + actors_EXP_0046["actors/EXP-0046: Leaf / Measured"] + actors_EXP_0047["actors/EXP-0047: Leaf / Accepted"] + actors_EXP_0048["actors/EXP-0048: Leaf / Accepted"] + actors_EXP_0049["actors/EXP-0049: Leaf / Accepted"] + actors_EXP_0050["actors/EXP-0050: Leaf / Accepted"] + actors_EXP_0051["actors/EXP-0051: Leaf / Accepted"] + actors_EXP_0052["actors/EXP-0052: Leaf / Accepted"] + actors_EXP_0053["actors/EXP-0053: Leaf / Measured"] + actors_EXP_0054["actors/EXP-0054: Leaf / Measured"] + actors_EXP_0055["actors/EXP-0055: Leaf / Accepted"] + actors_EXP_0056["actors/EXP-0056: Leaf / Accepted"] + actors_EXP_0057["actors/EXP-0057: Leaf / Accepted"] + actors_EXP_0058["actors/EXP-0058: Leaf / Accepted"] + actors_EXP_0059["actors/EXP-0059: Leaf / Accepted"] + actors_EXP_0060["actors/EXP-0060: Leaf / Accepted"] + actors_EXP_0061["actors/EXP-0061: Leaf / Accepted"] + actors_EXP_0062["actors/EXP-0062: Leaf / Accepted"] + actors_EXP_0063["actors/EXP-0063: Synthesis / Accepted"] + actors_EXP_0064["actors/EXP-0064: Synthesis / Accepted"] + actors_EXP_0065["actors/EXP-0065: Leaf / Invalidated"] + actors_EXP_0066["actors/EXP-0066: Synthesis / Accepted"] + actors_EXP_0067["actors/EXP-0067: Leaf / Accepted"] + actors_EXP_0068["actors/EXP-0068: Leaf / Accepted"] + actors_EXP_0069["actors/EXP-0069: Leaf / Accepted"] + actors_EXP_0070["actors/EXP-0070: Leaf / Accepted"] + actors_EXP_0071["actors/EXP-0071: Leaf / Accepted"] + actors_EXP_0072["actors/EXP-0072: Leaf / Accepted"] + actors_EXP_0073["actors/EXP-0073: Leaf / Accepted"] + actors_EXP_0074["actors/EXP-0074: Leaf / Accepted"] + actors_EXP_0075["actors/EXP-0075: Leaf / Accepted"] + actors_EXP_0076["actors/EXP-0076: Leaf / Accepted"] + actors_EXP_0077["actors/EXP-0077: Leaf / Accepted"] + actors_EXP_0078["actors/EXP-0078: Leaf / Accepted"] + actors_EXP_0079["actors/EXP-0079: Leaf / Accepted"] + actors_EXP_0080["actors/EXP-0080: Leaf / Accepted"] + actors_EXP_0081["actors/EXP-0081: Leaf / Accepted"] + actors_EXP_0082["actors/EXP-0082: Leaf / Accepted"] + actors_EXP_0083["actors/EXP-0083: Leaf / Accepted"] + actors_EXP_0084["actors/EXP-0084: Leaf / Accepted"] + actors_EXP_0085["actors/EXP-0085: Leaf / Accepted"] + actors_EXP_0086["actors/EXP-0086: Leaf / Accepted"] + actors_EXP_0087["actors/EXP-0087: Leaf / Accepted"] + actors_EXP_0088["actors/EXP-0088: Leaf / Accepted"] + actors_EXP_0089["actors/EXP-0089: Leaf / Accepted"] + actors_EXP_0090["actors/EXP-0090: Leaf / Accepted"] + actors_EXP_0091["actors/EXP-0091: Leaf / Accepted"] + actors_EXP_0092["actors/EXP-0092: Leaf / Accepted"] + actors_EXP_0093["actors/EXP-0093: Leaf / Accepted"] + actors_EXP_0094["actors/EXP-0094: Leaf / Accepted"] + actors_EXP_0095["actors/EXP-0095: Leaf / Accepted"] + actors_EXP_0096["actors/EXP-0096: external"] + actors_EXP_0097["actors/EXP-0097: Leaf / Accepted"] + actors_EXP_0098["actors/EXP-0098: Leaf / Accepted"] + actors_EXP_0099["actors/EXP-0099: Leaf / Accepted"] + actors_EXP_0100["actors/EXP-0100: Leaf / Accepted"] + actors_EXP_0101["actors/EXP-0101: Leaf / Accepted"] + actors_EXP_0102["actors/EXP-0102: Leaf / Accepted"] + actors_EXP_0020 --> actors_EXP_0057 + actors_EXP_0025 --> actors_EXP_0028 + actors_EXP_0025 --> actors_EXP_0029 + actors_EXP_0025 --> actors_EXP_0030 + actors_EXP_0025 --> actors_EXP_0031 + actors_EXP_0025 --> actors_EXP_0032 + actors_EXP_0025 --> actors_EXP_0033 + actors_EXP_0025 --> actors_EXP_0034 + actors_EXP_0025 --> actors_EXP_0035 + actors_EXP_0025 --> actors_EXP_0036 + actors_EXP_0025 --> actors_EXP_0037 + actors_EXP_0025 --> actors_EXP_0038 + actors_EXP_0025 --> actors_EXP_0039 + actors_EXP_0025 --> actors_EXP_0040 + actors_EXP_0025 --> actors_EXP_0041 + actors_EXP_0025 --> actors_EXP_0042 + actors_EXP_0025 --> actors_EXP_0043 + actors_EXP_0025 --> actors_EXP_0044 + actors_EXP_0025 --> actors_EXP_0045 + actors_EXP_0025 --> actors_EXP_0046 + actors_EXP_0025 --> actors_EXP_0047 + actors_EXP_0025 --> actors_EXP_0048 + actors_EXP_0025 --> actors_EXP_0049 + actors_EXP_0025 --> actors_EXP_0050 + actors_EXP_0025 --> actors_EXP_0051 + actors_EXP_0025 --> actors_EXP_0052 + actors_EXP_0025 --> actors_EXP_0054 + actors_EXP_0025 --> actors_EXP_0057 + actors_EXP_0025 --> actors_EXP_0058 + actors_EXP_0025 --> actors_EXP_0059 + actors_EXP_0025 --> actors_EXP_0060 + actors_EXP_0025 --> actors_EXP_0061 + actors_EXP_0025 --> actors_EXP_0062 + actors_EXP_0025 --> actors_EXP_0063 + actors_EXP_0025 --> actors_EXP_0067 + actors_EXP_0025 --> actors_EXP_0068 + actors_EXP_0025 --> actors_EXP_0075 + actors_EXP_0025 --> actors_EXP_0076 + actors_EXP_0025 --> actors_EXP_0077 + actors_EXP_0025 --> actors_EXP_0078 + actors_EXP_0025 --> actors_EXP_0079 + actors_EXP_0028 --> actors_EXP_0064 + actors_EXP_0028 --> actors_EXP_0065 + actors_EXP_0029 --> actors_EXP_0028 + actors_EXP_0029 --> actors_EXP_0063 + actors_EXP_0029 --> actors_EXP_0064 + actors_EXP_0029 --> actors_EXP_0065 + actors_EXP_0030 --> actors_EXP_0028 + actors_EXP_0031 --> actors_EXP_0028 + actors_EXP_0031 --> actors_EXP_0067 + actors_EXP_0031 --> actors_EXP_0068 + actors_EXP_0032 --> actors_EXP_0028 + actors_EXP_0032 --> actors_EXP_0053 + actors_EXP_0033 --> actors_EXP_0028 + actors_EXP_0033 --> actors_EXP_0053 + actors_EXP_0034 --> actors_EXP_0028 + actors_EXP_0034 --> actors_EXP_0053 + actors_EXP_0035 --> actors_EXP_0028 + actors_EXP_0035 --> actors_EXP_0053 + actors_EXP_0036 --> actors_EXP_0028 + actors_EXP_0036 --> actors_EXP_0053 + actors_EXP_0037 --> actors_EXP_0028 + actors_EXP_0038 --> actors_EXP_0028 + actors_EXP_0039 --> actors_EXP_0028 + actors_EXP_0039 --> actors_EXP_0058 + actors_EXP_0040 --> actors_EXP_0028 + actors_EXP_0041 --> actors_EXP_0028 + actors_EXP_0041 --> actors_EXP_0058 + actors_EXP_0042 --> actors_EXP_0028 + actors_EXP_0042 --> actors_EXP_0058 + actors_EXP_0043 --> actors_EXP_0028 + actors_EXP_0043 --> actors_EXP_0058 + actors_EXP_0044 --> actors_EXP_0028 + actors_EXP_0044 --> actors_EXP_0068 + actors_EXP_0045 --> actors_EXP_0029 + actors_EXP_0045 --> actors_EXP_0047 + actors_EXP_0046 --> actors_EXP_0029 + actors_EXP_0047 --> actors_EXP_0028 + actors_EXP_0048 --> actors_EXP_0029 + actors_EXP_0049 --> actors_EXP_0029 + actors_EXP_0049 --> actors_EXP_0050 + actors_EXP_0049 --> actors_EXP_0055 + actors_EXP_0049 --> actors_EXP_0056 + actors_EXP_0050 --> actors_EXP_0029 + actors_EXP_0050 --> actors_EXP_0055 + actors_EXP_0050 --> actors_EXP_0056 + actors_EXP_0051 --> actors_EXP_0029 + actors_EXP_0051 --> actors_EXP_0055 + actors_EXP_0051 --> actors_EXP_0056 + actors_EXP_0052 --> actors_EXP_0029 + actors_EXP_0053 --> actors_EXP_0029 + actors_EXP_0053 --> actors_EXP_0064 + actors_EXP_0054 --> actors_EXP_0064 + actors_EXP_0055 --> actors_EXP_0063 + actors_EXP_0056 --> actors_EXP_0063 + actors_EXP_0057 --> actors_EXP_0028 + actors_EXP_0058 --> actors_EXP_0028 + actors_EXP_0059 --> actors_EXP_0028 + actors_EXP_0060 --> actors_EXP_0028 + actors_EXP_0061 --> actors_EXP_0029 + actors_EXP_0062 --> actors_EXP_0028 + actors_EXP_0063 --> actors_EXP_0028 + actors_EXP_0063 --> actors_EXP_0064 + actors_EXP_0063 --> actors_EXP_0065 + actors_EXP_0064 --> actors_EXP_0065 + actors_EXP_0064 --> actors_EXP_0066 + actors_EXP_0066 --> actors_EXP_0014 + actors_EXP_0066 --> actors_EXP_0015 + actors_EXP_0066 --> actors_EXP_0099 + actors_EXP_0066 --> actors_EXP_0101 + actors_EXP_0066 --> actors_EXP_0102 + actors_EXP_0067 --> actors_EXP_0066 + actors_EXP_0067 --> actors_EXP_0087 + actors_EXP_0067 --> actors_EXP_0088 + actors_EXP_0067 --> actors_EXP_0089 + actors_EXP_0067 --> actors_EXP_0090 + actors_EXP_0067 --> actors_EXP_0091 + actors_EXP_0067 --> actors_EXP_0092 + actors_EXP_0067 --> actors_EXP_0093 + actors_EXP_0067 --> actors_EXP_0095 + actors_EXP_0067 --> actors_EXP_0097 + actors_EXP_0068 --> actors_EXP_0067 + actors_EXP_0068 --> actors_EXP_0069 + actors_EXP_0068 --> actors_EXP_0094 + actors_EXP_0069 --> actors_EXP_0067 + actors_EXP_0069 --> actors_EXP_0094 + actors_EXP_0069 --> actors_EXP_0095 + actors_EXP_0070 --> actors_EXP_0066 + actors_EXP_0071 --> actors_EXP_0066 + actors_EXP_0072 --> actors_EXP_0066 + actors_EXP_0073 --> actors_EXP_0066 + actors_EXP_0074 --> actors_EXP_0066 + actors_EXP_0074 --> actors_EXP_0098 + actors_EXP_0075 --> actors_EXP_0066 + actors_EXP_0076 --> actors_EXP_0066 + actors_EXP_0077 --> actors_EXP_0066 + actors_EXP_0078 --> actors_EXP_0066 + actors_EXP_0079 --> actors_EXP_0066 + actors_EXP_0080 --> actors_EXP_0066 + actors_EXP_0081 --> actors_EXP_0066 + actors_EXP_0082 --> actors_EXP_0066 + actors_EXP_0083 --> actors_EXP_0066 + actors_EXP_0084 --> actors_EXP_0066 + actors_EXP_0085 --> actors_EXP_0066 + actors_EXP_0086 --> actors_EXP_0066 + actors_EXP_0087 --> actors_EXP_0066 + actors_EXP_0087 --> actors_EXP_0101 + actors_EXP_0087 --> actors_EXP_0102 + actors_EXP_0088 --> actors_EXP_0066 + actors_EXP_0088 --> actors_EXP_0097 + actors_EXP_0089 --> actors_EXP_0066 + actors_EXP_0090 --> actors_EXP_0066 + actors_EXP_0091 --> actors_EXP_0066 + actors_EXP_0091 --> actors_EXP_0102 + actors_EXP_0092 --> actors_EXP_0066 + actors_EXP_0093 --> actors_EXP_0066 + actors_EXP_0094 --> actors_EXP_0067 + actors_EXP_0094 --> actors_EXP_0095 + actors_EXP_0095 --> actors_EXP_0066 + actors_EXP_0095 --> actors_EXP_0097 + actors_EXP_0095 --> actors_EXP_0098 + actors_EXP_0095 --> actors_EXP_0099 + actors_EXP_0095 --> actors_EXP_0100 + actors_EXP_0096 --> actors_EXP_0095 + actors_EXP_0097 --> actors_EXP_0066 + actors_EXP_0098 --> actors_EXP_0066 + actors_EXP_0099 --> actors_EXP_0100 + actors_EXP_0100 --> actors_EXP_0101 + actors_EXP_0101 --> actors_EXP_0102 +``` + +**Evidence flow** + +```mermaid +flowchart TD + actors_EXP_0011["actors/EXP-0011: external"] + actors_EXP_0012["actors/EXP-0012: external"] + actors_EXP_0020["actors/EXP-0020: external"] + actors_EXP_0024["actors/EXP-0024: external"] + actors_EXP_0025["actors/EXP-0025: external"] + actors_EXP_0026["actors/EXP-0026: external"] + actors_EXP_0027["actors/EXP-0027: external"] + actors_EXP_0028["actors/EXP-0028: Synthesis / Accepted"] + actors_EXP_0029["actors/EXP-0029: Synthesis / Accepted"] + actors_EXP_0030["actors/EXP-0030: Leaf / Accepted"] + actors_EXP_0031["actors/EXP-0031: Leaf / Accepted"] + actors_EXP_0032["actors/EXP-0032: Leaf / Accepted"] + actors_EXP_0033["actors/EXP-0033: Leaf / Accepted"] + actors_EXP_0034["actors/EXP-0034: Leaf / Accepted"] + actors_EXP_0035["actors/EXP-0035: Leaf / Accepted"] + actors_EXP_0036["actors/EXP-0036: Leaf / Accepted"] + actors_EXP_0037["actors/EXP-0037: Leaf / Accepted"] + actors_EXP_0038["actors/EXP-0038: Leaf / Accepted"] + actors_EXP_0039["actors/EXP-0039: Leaf / Accepted"] + actors_EXP_0040["actors/EXP-0040: Leaf / Accepted"] + actors_EXP_0041["actors/EXP-0041: Leaf / Accepted"] + actors_EXP_0042["actors/EXP-0042: Leaf / Accepted"] + actors_EXP_0043["actors/EXP-0043: Leaf / Accepted"] + actors_EXP_0044["actors/EXP-0044: Leaf / Accepted"] + actors_EXP_0045["actors/EXP-0045: Leaf / Measured"] + actors_EXP_0046["actors/EXP-0046: Leaf / Measured"] + actors_EXP_0047["actors/EXP-0047: Leaf / Accepted"] + actors_EXP_0048["actors/EXP-0048: Leaf / Accepted"] + actors_EXP_0049["actors/EXP-0049: Leaf / Accepted"] + actors_EXP_0050["actors/EXP-0050: Leaf / Accepted"] + actors_EXP_0051["actors/EXP-0051: Leaf / Accepted"] + actors_EXP_0052["actors/EXP-0052: Leaf / Accepted"] + actors_EXP_0053["actors/EXP-0053: Leaf / Measured"] + actors_EXP_0054["actors/EXP-0054: Leaf / Measured"] + actors_EXP_0055["actors/EXP-0055: Leaf / Accepted"] + actors_EXP_0056["actors/EXP-0056: Leaf / Accepted"] + actors_EXP_0057["actors/EXP-0057: Leaf / Accepted"] + actors_EXP_0058["actors/EXP-0058: Leaf / Accepted"] + actors_EXP_0059["actors/EXP-0059: Leaf / Accepted"] + actors_EXP_0060["actors/EXP-0060: Leaf / Accepted"] + actors_EXP_0061["actors/EXP-0061: Leaf / Accepted"] + actors_EXP_0062["actors/EXP-0062: Leaf / Accepted"] + actors_EXP_0063["actors/EXP-0063: Synthesis / Accepted"] + actors_EXP_0064["actors/EXP-0064: Synthesis / Accepted"] + actors_EXP_0065["actors/EXP-0065: Leaf / Invalidated"] + actors_EXP_0066["actors/EXP-0066: Synthesis / Accepted"] + actors_EXP_0067["actors/EXP-0067: Leaf / Accepted"] + actors_EXP_0068["actors/EXP-0068: Leaf / Accepted"] + actors_EXP_0069["actors/EXP-0069: Leaf / Accepted"] + actors_EXP_0070["actors/EXP-0070: Leaf / Accepted"] + actors_EXP_0071["actors/EXP-0071: Leaf / Accepted"] + actors_EXP_0072["actors/EXP-0072: Leaf / Accepted"] + actors_EXP_0073["actors/EXP-0073: Leaf / Accepted"] + actors_EXP_0074["actors/EXP-0074: Leaf / Accepted"] + actors_EXP_0075["actors/EXP-0075: Leaf / Accepted"] + actors_EXP_0076["actors/EXP-0076: Leaf / Accepted"] + actors_EXP_0077["actors/EXP-0077: Leaf / Accepted"] + actors_EXP_0078["actors/EXP-0078: Leaf / Accepted"] + actors_EXP_0079["actors/EXP-0079: Leaf / Accepted"] + actors_EXP_0080["actors/EXP-0080: Leaf / Accepted"] + actors_EXP_0081["actors/EXP-0081: Leaf / Accepted"] + actors_EXP_0082["actors/EXP-0082: Leaf / Accepted"] + actors_EXP_0083["actors/EXP-0083: Leaf / Accepted"] + actors_EXP_0084["actors/EXP-0084: Leaf / Accepted"] + actors_EXP_0085["actors/EXP-0085: Leaf / Accepted"] + actors_EXP_0086["actors/EXP-0086: Leaf / Accepted"] + actors_EXP_0087["actors/EXP-0087: Leaf / Accepted"] + actors_EXP_0088["actors/EXP-0088: Leaf / Accepted"] + actors_EXP_0089["actors/EXP-0089: Leaf / Accepted"] + actors_EXP_0090["actors/EXP-0090: Leaf / Accepted"] + actors_EXP_0091["actors/EXP-0091: Leaf / Accepted"] + actors_EXP_0092["actors/EXP-0092: Leaf / Accepted"] + actors_EXP_0093["actors/EXP-0093: Leaf / Accepted"] + actors_EXP_0094["actors/EXP-0094: Leaf / Accepted"] + actors_EXP_0095["actors/EXP-0095: Leaf / Accepted"] + actors_EXP_0096["actors/EXP-0096: external"] + actors_EXP_0097["actors/EXP-0097: Leaf / Accepted"] + actors_EXP_0098["actors/EXP-0098: Leaf / Accepted"] + actors_EXP_0099["actors/EXP-0099: Leaf / Accepted"] + actors_EXP_0100["actors/EXP-0100: Leaf / Accepted"] + actors_EXP_0101["actors/EXP-0101: Leaf / Accepted"] + actors_EXP_0102["actors/EXP-0102: Leaf / Accepted"] + actors_EXP_0020 -. uses .-> actors_EXP_0057 + actors_EXP_0024 -. produces .-> actors_EXP_0028 + actors_EXP_0024 -. produces .-> actors_EXP_0029 + actors_EXP_0025 -. produces .-> actors_EXP_0028 + actors_EXP_0025 -. produces .-> actors_EXP_0029 + actors_EXP_0025 -. produces .-> actors_EXP_0063 + actors_EXP_0025 -. produces .-> actors_EXP_0064 + actors_EXP_0025 -. produces .-> actors_EXP_0065 + actors_EXP_0025 -. produces .-> actors_EXP_0066 + actors_EXP_0025 -. uses .-> actors_EXP_0070 + actors_EXP_0025 -. uses .-> actors_EXP_0071 + actors_EXP_0025 -. uses .-> actors_EXP_0072 + actors_EXP_0025 -. uses .-> actors_EXP_0073 + actors_EXP_0025 -. uses .-> actors_EXP_0074 + actors_EXP_0026 -. produces .-> actors_EXP_0028 + actors_EXP_0026 -. produces .-> actors_EXP_0029 + actors_EXP_0026 -. produces .-> actors_EXP_0064 + actors_EXP_0026 -. produces .-> actors_EXP_0065 + actors_EXP_0026 -. produces .-> actors_EXP_0066 + actors_EXP_0027 -. produces .-> actors_EXP_0028 + actors_EXP_0027 -. produces .-> actors_EXP_0029 + actors_EXP_0027 -. produces .-> actors_EXP_0064 + actors_EXP_0027 -. produces .-> actors_EXP_0065 + actors_EXP_0027 -. produces .-> actors_EXP_0066 + actors_EXP_0028 -. produces .-> actors_EXP_0064 + actors_EXP_0028 -. uses .-> actors_EXP_0064 + actors_EXP_0028 -. uses .-> actors_EXP_0065 + actors_EXP_0028 -. uses .-> actors_EXP_0099 + actors_EXP_0029 -. produces .-> actors_EXP_0064 + actors_EXP_0029 -. uses .-> actors_EXP_0064 + actors_EXP_0029 -. uses .-> actors_EXP_0065 + actors_EXP_0030 -. produces .-> actors_EXP_0028 + actors_EXP_0030 -. uses .-> actors_EXP_0028 + actors_EXP_0030 -. uses .-> actors_EXP_0062 + actors_EXP_0031 -. produces .-> actors_EXP_0028 + actors_EXP_0031 -. uses .-> actors_EXP_0028 + actors_EXP_0031 -. uses .-> actors_EXP_0037 + actors_EXP_0031 -. uses .-> actors_EXP_0044 + actors_EXP_0031 -. produces .-> actors_EXP_0067 + actors_EXP_0031 -. uses .-> actors_EXP_0067 + actors_EXP_0031 -. produces .-> actors_EXP_0068 + actors_EXP_0032 -. produces .-> actors_EXP_0028 + actors_EXP_0032 -. uses .-> actors_EXP_0028 + actors_EXP_0033 -. produces .-> actors_EXP_0028 + actors_EXP_0033 -. uses .-> actors_EXP_0028 + actors_EXP_0034 -. produces .-> actors_EXP_0028 + actors_EXP_0034 -. uses .-> actors_EXP_0028 + actors_EXP_0035 -. produces .-> actors_EXP_0028 + actors_EXP_0035 -. uses .-> actors_EXP_0028 + actors_EXP_0036 -. produces .-> actors_EXP_0028 + actors_EXP_0036 -. uses .-> actors_EXP_0028 + actors_EXP_0037 -. produces .-> actors_EXP_0028 + actors_EXP_0037 -. uses .-> actors_EXP_0028 + actors_EXP_0038 -. produces .-> actors_EXP_0028 + actors_EXP_0038 -. uses .-> actors_EXP_0028 + actors_EXP_0039 -. produces .-> actors_EXP_0028 + actors_EXP_0039 -. uses .-> actors_EXP_0028 + actors_EXP_0039 -. uses .-> actors_EXP_0058 + actors_EXP_0040 -. produces .-> actors_EXP_0028 + actors_EXP_0040 -. uses .-> actors_EXP_0028 + actors_EXP_0041 -. produces .-> actors_EXP_0028 + actors_EXP_0041 -. uses .-> actors_EXP_0028 + actors_EXP_0041 -. uses .-> actors_EXP_0058 + actors_EXP_0042 -. produces .-> actors_EXP_0028 + actors_EXP_0042 -. uses .-> actors_EXP_0028 + actors_EXP_0042 -. uses .-> actors_EXP_0056 + actors_EXP_0042 -. uses .-> actors_EXP_0058 + actors_EXP_0043 -. produces .-> actors_EXP_0028 + actors_EXP_0043 -. uses .-> actors_EXP_0028 + actors_EXP_0043 -. uses .-> actors_EXP_0058 + actors_EXP_0044 -. produces .-> actors_EXP_0028 + actors_EXP_0044 -. uses .-> actors_EXP_0028 + actors_EXP_0044 -. produces .-> actors_EXP_0068 + actors_EXP_0045 -. produces .-> actors_EXP_0029 + actors_EXP_0045 -. uses .-> actors_EXP_0029 + actors_EXP_0045 -. uses .-> actors_EXP_0047 + actors_EXP_0046 -. produces .-> actors_EXP_0029 + actors_EXP_0046 -. uses .-> actors_EXP_0029 + actors_EXP_0047 -. produces .-> actors_EXP_0028 + actors_EXP_0047 -. uses .-> actors_EXP_0028 + actors_EXP_0048 -. produces .-> actors_EXP_0029 + actors_EXP_0048 -. uses .-> actors_EXP_0029 + actors_EXP_0048 -. uses .-> actors_EXP_0061 + actors_EXP_0049 -. produces .-> actors_EXP_0029 + actors_EXP_0049 -. uses .-> actors_EXP_0029 + actors_EXP_0049 -. produces .-> actors_EXP_0050 + actors_EXP_0049 -. uses .-> actors_EXP_0050 + actors_EXP_0050 -. produces .-> actors_EXP_0029 + actors_EXP_0050 -. uses .-> actors_EXP_0029 + actors_EXP_0050 -. produces .-> actors_EXP_0055 + actors_EXP_0050 -. produces .-> actors_EXP_0056 + actors_EXP_0050 -. uses .-> actors_EXP_0056 + actors_EXP_0051 -. produces .-> actors_EXP_0029 + actors_EXP_0051 -. uses .-> actors_EXP_0029 + actors_EXP_0051 -. produces .-> actors_EXP_0055 + actors_EXP_0051 -. produces .-> actors_EXP_0056 + actors_EXP_0051 -. uses .-> actors_EXP_0056 + actors_EXP_0052 -. produces .-> actors_EXP_0029 + actors_EXP_0052 -. uses .-> actors_EXP_0029 + actors_EXP_0052 -. uses .-> actors_EXP_0042 + actors_EXP_0053 -. produces .-> actors_EXP_0029 + actors_EXP_0053 -. uses .-> actors_EXP_0029 + actors_EXP_0054 -. produces .-> actors_EXP_0064 + actors_EXP_0054 -. uses .-> actors_EXP_0064 + actors_EXP_0055 -. uses .-> actors_EXP_0058 + actors_EXP_0055 -. produces .-> actors_EXP_0063 + actors_EXP_0055 -. uses .-> actors_EXP_0063 + actors_EXP_0056 -. uses .-> actors_EXP_0058 + actors_EXP_0056 -. produces .-> actors_EXP_0063 + actors_EXP_0056 -. uses .-> actors_EXP_0063 + actors_EXP_0057 -. produces .-> actors_EXP_0028 + actors_EXP_0057 -. uses .-> actors_EXP_0028 + actors_EXP_0057 -. produces .-> actors_EXP_0064 + actors_EXP_0057 -. produces .-> actors_EXP_0065 + actors_EXP_0058 -. produces .-> actors_EXP_0028 + actors_EXP_0058 -. uses .-> actors_EXP_0028 + actors_EXP_0058 -. uses .-> actors_EXP_0059 + actors_EXP_0058 -. uses .-> actors_EXP_0063 + actors_EXP_0058 -. produces .-> actors_EXP_0064 + actors_EXP_0058 -. produces .-> actors_EXP_0065 + actors_EXP_0059 -. produces .-> actors_EXP_0028 + actors_EXP_0059 -. uses .-> actors_EXP_0028 + actors_EXP_0059 -. produces .-> actors_EXP_0058 + actors_EXP_0059 -. produces .-> actors_EXP_0065 + actors_EXP_0060 -. produces .-> actors_EXP_0028 + actors_EXP_0060 -. uses .-> actors_EXP_0028 + actors_EXP_0061 -. produces .-> actors_EXP_0029 + actors_EXP_0061 -. uses .-> actors_EXP_0029 + actors_EXP_0062 -. produces .-> actors_EXP_0028 + actors_EXP_0062 -. uses .-> actors_EXP_0028 + actors_EXP_0062 -. produces .-> actors_EXP_0067 + actors_EXP_0062 -. uses .-> actors_EXP_0067 + actors_EXP_0063 -. uses .-> actors_EXP_0011 + actors_EXP_0063 -. uses .-> actors_EXP_0042 + actors_EXP_0063 -. uses .-> actors_EXP_0059 + actors_EXP_0063 -. produces .-> actors_EXP_0064 + actors_EXP_0063 -. uses .-> actors_EXP_0064 + actors_EXP_0063 -. uses .-> actors_EXP_0065 + actors_EXP_0063 -. uses .-> actors_EXP_0100 + actors_EXP_0064 -. produces .-> actors_EXP_0065 + actors_EXP_0064 -. uses .-> actors_EXP_0065 + actors_EXP_0064 -. uses .-> actors_EXP_0100 + actors_EXP_0065 -. produces .-> actors_EXP_0066 + actors_EXP_0065 -. uses .-> actors_EXP_0066 + actors_EXP_0065 -. uses .-> actors_EXP_0070 + actors_EXP_0065 -. uses .-> actors_EXP_0071 + actors_EXP_0065 -. uses .-> actors_EXP_0073 + actors_EXP_0065 -. uses .-> actors_EXP_0074 + actors_EXP_0065 -. uses .-> actors_EXP_0075 + actors_EXP_0065 -. uses .-> actors_EXP_0076 + actors_EXP_0065 -. uses .-> actors_EXP_0077 + actors_EXP_0065 -. uses .-> actors_EXP_0078 + actors_EXP_0065 -. uses .-> actors_EXP_0079 + actors_EXP_0065 -. uses .-> actors_EXP_0080 + actors_EXP_0065 -. uses .-> actors_EXP_0081 + actors_EXP_0065 -. uses .-> actors_EXP_0082 + actors_EXP_0065 -. uses .-> actors_EXP_0083 + actors_EXP_0065 -. uses .-> actors_EXP_0084 + actors_EXP_0065 -. uses .-> actors_EXP_0086 + actors_EXP_0065 -. invalidates .-> actors_EXP_0095 + actors_EXP_0065 -. produces .-> actors_EXP_0095 + actors_EXP_0065 -. uses .-> actors_EXP_0095 + actors_EXP_0066 -. uses .-> actors_EXP_0012 + actors_EXP_0066 -. uses .-> actors_EXP_0067 + actors_EXP_0066 -. uses .-> actors_EXP_0102 + actors_EXP_0067 -. produces .-> actors_EXP_0066 + actors_EXP_0067 -. uses .-> actors_EXP_0068 + actors_EXP_0067 -. uses .-> actors_EXP_0069 + actors_EXP_0068 -. produces .-> actors_EXP_0067 + actors_EXP_0068 -. uses .-> actors_EXP_0069 + actors_EXP_0068 -. uses .-> actors_EXP_0094 + actors_EXP_0069 -. produces .-> actors_EXP_0066 + actors_EXP_0069 -. produces .-> actors_EXP_0067 + actors_EXP_0069 -. produces .-> actors_EXP_0094 + actors_EXP_0069 -. uses .-> actors_EXP_0094 + actors_EXP_0069 -. uses .-> actors_EXP_0095 + actors_EXP_0070 -. produces .-> actors_EXP_0066 + actors_EXP_0070 -. uses .-> actors_EXP_0066 + actors_EXP_0070 -. uses .-> actors_EXP_0072 + actors_EXP_0070 -. uses .-> actors_EXP_0084 + actors_EXP_0070 -. uses .-> actors_EXP_0085 + actors_EXP_0070 -. uses .-> actors_EXP_0087 + actors_EXP_0070 -. uses .-> actors_EXP_0091 + actors_EXP_0071 -. produces .-> actors_EXP_0066 + actors_EXP_0071 -. uses .-> actors_EXP_0066 + actors_EXP_0071 -. uses .-> actors_EXP_0087 + actors_EXP_0071 -. uses .-> actors_EXP_0091 + actors_EXP_0072 -. produces .-> actors_EXP_0066 + actors_EXP_0072 -. uses .-> actors_EXP_0066 + actors_EXP_0072 -. uses .-> actors_EXP_0090 + actors_EXP_0072 -. uses .-> actors_EXP_0091 + actors_EXP_0073 -. produces .-> actors_EXP_0066 + actors_EXP_0073 -. uses .-> actors_EXP_0066 + actors_EXP_0073 -. uses .-> actors_EXP_0074 + actors_EXP_0073 -. uses .-> actors_EXP_0086 + actors_EXP_0073 -. uses .-> actors_EXP_0093 + actors_EXP_0073 -. uses .-> actors_EXP_0099 + actors_EXP_0074 -. produces .-> actors_EXP_0066 + actors_EXP_0074 -. uses .-> actors_EXP_0066 + actors_EXP_0074 -. uses .-> actors_EXP_0087 + actors_EXP_0074 -. uses .-> actors_EXP_0091 + actors_EXP_0074 -. uses .-> actors_EXP_0098 + actors_EXP_0075 -. produces .-> actors_EXP_0066 + actors_EXP_0075 -. uses .-> actors_EXP_0066 + actors_EXP_0075 -. uses .-> actors_EXP_0076 + actors_EXP_0075 -. uses .-> actors_EXP_0077 + actors_EXP_0075 -. uses .-> actors_EXP_0078 + actors_EXP_0076 -. produces .-> actors_EXP_0066 + actors_EXP_0076 -. uses .-> actors_EXP_0066 + actors_EXP_0076 -. uses .-> actors_EXP_0091 + actors_EXP_0077 -. produces .-> actors_EXP_0066 + actors_EXP_0077 -. uses .-> actors_EXP_0066 + actors_EXP_0077 -. uses .-> actors_EXP_0087 + actors_EXP_0078 -. produces .-> actors_EXP_0066 + actors_EXP_0078 -. uses .-> actors_EXP_0066 + actors_EXP_0078 -. uses .-> actors_EXP_0079 + actors_EXP_0078 -. uses .-> actors_EXP_0080 + actors_EXP_0078 -. uses .-> actors_EXP_0081 + actors_EXP_0078 -. uses .-> actors_EXP_0083 + actors_EXP_0078 -. uses .-> actors_EXP_0084 + actors_EXP_0078 -. uses .-> actors_EXP_0089 + actors_EXP_0079 -. produces .-> actors_EXP_0066 + actors_EXP_0079 -. uses .-> actors_EXP_0066 + actors_EXP_0079 -. uses .-> actors_EXP_0083 + actors_EXP_0079 -. uses .-> actors_EXP_0084 + actors_EXP_0079 -. uses .-> actors_EXP_0089 + actors_EXP_0079 -. uses .-> actors_EXP_0092 + actors_EXP_0079 -. uses .-> actors_EXP_0093 + actors_EXP_0080 -. produces .-> actors_EXP_0066 + actors_EXP_0080 -. uses .-> actors_EXP_0066 + actors_EXP_0080 -. uses .-> actors_EXP_0079 + actors_EXP_0080 -. uses .-> actors_EXP_0084 + actors_EXP_0080 -. uses .-> actors_EXP_0089 + actors_EXP_0080 -. uses .-> actors_EXP_0092 + actors_EXP_0081 -. produces .-> actors_EXP_0066 + actors_EXP_0081 -. uses .-> actors_EXP_0066 + actors_EXP_0081 -. uses .-> actors_EXP_0092 + actors_EXP_0082 -. produces .-> actors_EXP_0066 + actors_EXP_0082 -. uses .-> actors_EXP_0066 + actors_EXP_0082 -. uses .-> actors_EXP_0081 + actors_EXP_0082 -. uses .-> actors_EXP_0084 + actors_EXP_0082 -. uses .-> actors_EXP_0086 + actors_EXP_0082 -. uses .-> actors_EXP_0088 + actors_EXP_0082 -. uses .-> actors_EXP_0090 + actors_EXP_0083 -. produces .-> actors_EXP_0066 + actors_EXP_0083 -. uses .-> actors_EXP_0066 + actors_EXP_0083 -. uses .-> actors_EXP_0081 + actors_EXP_0083 -. uses .-> actors_EXP_0084 + actors_EXP_0083 -. uses .-> actors_EXP_0086 + actors_EXP_0084 -. produces .-> actors_EXP_0066 + actors_EXP_0084 -. uses .-> actors_EXP_0066 + actors_EXP_0084 -. uses .-> actors_EXP_0079 + actors_EXP_0084 -. produces .-> actors_EXP_0085 + actors_EXP_0084 -. uses .-> actors_EXP_0085 + actors_EXP_0084 -. uses .-> actors_EXP_0088 + actors_EXP_0085 -. produces .-> actors_EXP_0066 + actors_EXP_0085 -. uses .-> actors_EXP_0066 + actors_EXP_0085 -. uses .-> actors_EXP_0088 + actors_EXP_0086 -. produces .-> actors_EXP_0066 + actors_EXP_0086 -. uses .-> actors_EXP_0066 + actors_EXP_0086 -. uses .-> actors_EXP_0090 + actors_EXP_0086 -. uses .-> actors_EXP_0093 + actors_EXP_0087 -. produces .-> actors_EXP_0066 + actors_EXP_0087 -. uses .-> actors_EXP_0101 + actors_EXP_0087 -. uses .-> actors_EXP_0102 + actors_EXP_0088 -. produces .-> actors_EXP_0066 + actors_EXP_0089 -. produces .-> actors_EXP_0066 + actors_EXP_0090 -. produces .-> actors_EXP_0066 + actors_EXP_0091 -. produces .-> actors_EXP_0066 + actors_EXP_0091 -. uses .-> actors_EXP_0102 + actors_EXP_0092 -. produces .-> actors_EXP_0066 + actors_EXP_0093 -. produces .-> actors_EXP_0066 + actors_EXP_0093 -. uses .-> actors_EXP_0099 + actors_EXP_0093 -. uses .-> actors_EXP_0100 + actors_EXP_0094 -. produces .-> actors_EXP_0066 + actors_EXP_0094 -. produces .-> actors_EXP_0067 + actors_EXP_0094 -. uses .-> actors_EXP_0095 + actors_EXP_0095 -. invalidates .-> actors_EXP_0065 + actors_EXP_0095 -. produces .-> actors_EXP_0066 + actors_EXP_0095 -. produces .-> actors_EXP_0067 + actors_EXP_0095 -. uses .-> actors_EXP_0067 + actors_EXP_0095 -. invalidates .-> actors_EXP_0070 + actors_EXP_0095 -. invalidates .-> actors_EXP_0071 + actors_EXP_0095 -. invalidates .-> actors_EXP_0072 + actors_EXP_0095 -. uses .-> actors_EXP_0072 + actors_EXP_0095 -. invalidates .-> actors_EXP_0073 + actors_EXP_0095 -. invalidates .-> actors_EXP_0074 + actors_EXP_0095 -. invalidates .-> actors_EXP_0075 + actors_EXP_0095 -. invalidates .-> actors_EXP_0076 + actors_EXP_0095 -. invalidates .-> actors_EXP_0077 + actors_EXP_0095 -. invalidates .-> actors_EXP_0078 + actors_EXP_0095 -. invalidates .-> actors_EXP_0079 + actors_EXP_0095 -. invalidates .-> actors_EXP_0081 + actors_EXP_0095 -. invalidates .-> actors_EXP_0082 + actors_EXP_0095 -. invalidates .-> actors_EXP_0083 + actors_EXP_0095 -. invalidates .-> actors_EXP_0084 + actors_EXP_0095 -. uses .-> actors_EXP_0085 + actors_EXP_0095 -. invalidates .-> actors_EXP_0086 + actors_EXP_0095 -. uses .-> actors_EXP_0096 + actors_EXP_0096 -. produces .-> actors_EXP_0095 + actors_EXP_0097 -. produces .-> actors_EXP_0066 + actors_EXP_0098 -. produces .-> actors_EXP_0066 + actors_EXP_0101 -. uses .-> actors_EXP_0102 + actors_EXP_0102 -. confirms .-> actors_EXP_0101 +``` + +Conditional residual experiments remain unallocated until a measured owner and changed assumption earn a distinct proof question. + + + +- `Frozen decision`: EXP-0025 remains Accepted for C1 physical architecture only. A0, Q1, FIFO and the causal floor remain unchanged; EXP-0026/0027 remain sealed negative evidence. +- `Current proof owners`: EXP-0028 and EXP-0029 are accepted finite Weight/reachability syntheses; EXP-0063 composes Waiting publication/removal bounds, Accepted EXP-0064 composes Contract-level product admission, and EXP-0065 retains the invalidated prior exact binding; Prepared EXP-0095 owns current generated binding and consumer reconvergence. +- `Production decisions`: [EXP-0065](./EXP-0065.md) owns historical production binding identities; [EXP-0095](./EXP-0095.md) owns the current refresh after EXP-0067's selector correction. [EXP-0066](./EXP-0066.md) is the integrated Synthesis; EXP-0070–EXP-0086 own extracted primary evidence and independent statuses, while Proposed EXP-0087–EXP-0093 name already-open coverage obligations without inventing measurement readiness. EXP-0067 gates wider User execution. Numerical claims cite their Leaf; release gates stay in BACKLOG. +- `Integrated fission`: The validated migration manifest is archived at Git commit `7cd1ee69244e44f92794aba8c913a6d74a31c478`, repository path `.agents/skills/architecture-experiments/migrations/actors-integrated-fission-2026-09-08.json`. It distinguishes the original Git baseline from then-uncommitted evidence and binds exact extraction payloads; Git now owns the retired working manifest. Primary evidence remains in its Leaves. Existing IDs and sealed decisions are unchanged; no measurement was rerun. +- `Identity migration`: Former provisional actors/EXP-0030@a60de7748132211d9e58cecd5be82b41f76ce230 → EXP-0064, former actors/EXP-0031@a60de7748132211d9e58cecd5be82b41f76ce230 → EXP-0063, former actors/EXP-0032@a60de7748132211d9e58cecd5be82b41f76ce230 → EXP-0065, former actors/EXP-0033@a60de7748132211d9e58cecd5be82b41f76ce230 → EXP-0066. Former numbers are baseline-qualified; no alias records or sealed renumberings exist. The completed migration manifest is archived at Git commit `6c650695568a6e5d05437351ef014e8ee950c484`, repository path `.agents/skills/architecture-experiments/migrations/actors-proof-graph-2026-09-06.json`; it preserves the exact Git baseline and extraction coverage. +- `Conditional work`: Index-only historical portfolio questions remain dormant until their stated trigger is met. No residual optimization experiment is allocated by this migration. G7/G8 correctness or residual findings confirm/invalidate the affected proof, or earn a new independent node; they never turn a Synthesis into a debugging notebook. + +## Conditional Residual Entry Contracts + +These are future triggers, not experiments or release work items. BACKLOG owns the release gates. Create only the next earned record, with an exact measured owner and distinct hypothesis; a prerequisite production experiment may expose the owner before EXP-0066, but plausibility never suffices. + +| Possible question | Entry condition / blocking evidence | Smallest falsifier | +| --- | --- | --- | +| Ready payload/liveness separation | Complete production proof/write attribution isolates mutable Ready payload cost | Mutable C32 versus bounded sealed payload/mask on partial/cross-page prefixes with committed-prefix failure | +| Proof anatomy / proof-oriented pages | Recorded/compact/PoV versus charged proof reveals a specific binding owner | Exact key/value/trie-node decomposition on 1/4/8/16/32/64/100 complete Transfer-P0 paths; compare avoided bytes against extra paths/writes | +| Truthful storage cardinality | Measured estimate excess is caused by a provably overconservative cardinality | Enforced admission/cleanup/TryRuntime bound versus storage annotation and complete production proof | +| Canonical cell compression | Complete proof identifies redundant encoded bytes with a unique derivation owner | Round-trip/corruption witness plus complete-path proof and read/write comparison | +| Shared immutable Contract body | Repeated identical bodies measurably bind proof/state after attribution | Exact-hash/reference-count lifecycle and short/divergent Contract comparison without another mutable owner | +| Storage-key locality | Production compact-proof anatomy isolates navigation cost after stable layout | Collision-resistant key comparison under sequential, randomized and adversarial population | +| Actual-PoV reclaim | Recorded/compact proof and SDK reclaim identify safely reclaimable unused capacity | Complete Prepass/Drain/extension/finalization comparison, fail-closed meters and unchanged FIFO/Q1 | + +## Index-Only Proposed Relations + +These unprepared questions retain their IDs without new record scaffolding. None has a selected implementation or new measurement. The common negative boundary is frozen C1 in EXP-0025; an unmet cost target is not a macrogeometry reopen trigger. -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. +| ID | Depends on | Uses evidence from | Refines | Confirms | Invalidates | Supersedes | Transfers question to | Produces input for | Reopen trigger | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| EXP-0007 | Resource specification | EXP-0022 invalidation | None | None | None | None | None | None until prepared | EXP-0066 isolates a distinct allocation question within fixed policy | +| EXP-0011 | EXP-0025 | EXP-0063 when decided | None | None | None | None | EXP-0063 for current heap envelope | None until prepared | Measured cadence owner outside the current heap question | +| EXP-0012 | EXP-0025 | EXP-0023, EXP-0066 when measured | Canonical FIFO only | No external successor staging | None | None | EXP-0025 physical choice already closed | Conditional residual | A measured FIFO owner and qualifying within-C1 falsifier | +| EXP-0014 | EXP-0066 | EXP-0013 | None | None | None | None | None | None until prepared | Equivalent external-operation comparison changes an evidenced decision | +| EXP-0015 | EXP-0066 | EXP-0027 negative result | None | None | None | None | None | None until prepared | Structural convergence leaves measured generic-lowering overhead | ## 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`. +- Allocate IDs from the single sequence across all tracks under [Record Identity and Layout](../../SKILL.md#record-identity-and-layout); retain this track as the ownership path and never restart numbering here. - 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 index 3590fabb..87d8f8b9 100644 --- a/.agents/skills/architecture-experiments/tracks/adapters/experiments.md +++ b/.agents/skills/architecture-experiments/tracks/adapters/experiments.md @@ -47,6 +47,6 @@ No experiment is allocated. Add the first record only when a decision-relevant h ## Maintenance -- Allocate `EXP-0001` when the first real Adapters experiment is opened; IDs are monotonic within this track. +- Allocate IDs from the single sequence across all tracks under [Record Identity and Layout](../../SKILL.md#record-identity-and-layout); retain this track as the ownership path and never restart numbering here. - 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 index 39a7f9b5..5a62b20b 100644 --- a/.agents/skills/architecture-experiments/tracks/router/experiments.md +++ b/.agents/skills/architecture-experiments/tracks/router/experiments.md @@ -46,6 +46,6 @@ No Router experiment has been opened or measured. The portfolio above is directi ## Maintenance -- Allocate `EXP-0001` only when the first real Router hypothesis has a comparable baseline/candidate, materiality threshold, and smallest falsifier. +- Allocate IDs from the single sequence across all tracks under [Record Identity and Layout](../../SKILL.md#record-identity-and-layout); retain this track as the ownership path and never restart numbering here. - 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/domain-dag/AGENTS.md b/.agents/skills/domain-dag/AGENTS.md index 8a663fa0..c4286225 100644 --- a/.agents/skills/domain-dag/AGENTS.md +++ b/.agents/skills/domain-dag/AGENTS.md @@ -1,50 +1,6 @@ -# AGENTS.md (domain-dag) +# Domain DAG Maintenance -## Knowledge & Conventions - -### Meta-Protocol Principles - -- 'Domain Ownership': Every durable responsibility should have a single owning source module or package. -- 'Acyclic Direction': Local source imports should form a directed acyclic graph. -- 'Composition Boundary': Entrypoints compose ports and runtimes; domain modules do not import entrypoints. -- 'Transportability': The skill and validator must avoid project-specific names, absolute local paths, stack-only assumptions, and external dependencies. -- 'Explicit Non-Ownership': Durable module contracts should state what they own and what they reject. -- 'Interface Surface Pressure': Wide callback/parameter/export surfaces are architectural pressure to group related concepts into named contracts, not automatic failures. -- 'Shape Before Rules': First identify whether the project is flat, layered, package-based, hexagonal, or hybrid; then choose validation rules. - -### Operating Principles - -- Use `scripts/validate-domain-dag.sh` for graph audits. -- Prefer config-driven project rules over hard-coded repository assumptions. -- Keep validator defaults portable; make stack-specific checks opt-in via config. -- Use import aliases from config when a project routes local imports through bundler/tsconfig/package aliases. -- Treat cycles and reverse entrypoint imports as hard failures. -- Treat generic shared-bucket names as warnings unless a project config upgrades them. -- Add custom alias, layer, forbidden-edge, header-clause, and surface-width rules only for discovered project constraints. - -### Discovered Constraints - -- Shared-bucket names are heuristic, not proof of bad architecture. - - Trigger: `utils`, `types`, `shared`, or similar names appear. - - Action: Audit ownership and either move content to an owner or allow the exception. -- Header labels are a navigation aid, not the graph itself. - - Trigger: A project lacks module headers but has clean boundaries. - - Action: Warn by default; use strict/config only when headers are adopted standard. -- Layer checks must be opt-in. - - Trigger: Generic validation across unknown projects. - - Action: Validate universal graph invariants by default; enforce local direction only from config. -- Header clauses are earned standards. - - Trigger: A project adopts explicit ownership headers such as owner/exclusion clauses. - - Action: Configure `headerRequiredClauses`; do not bake local wording into the skill default. -- Wide interfaces are pressure, not proof. - - Trigger: A child boundary accumulates many callbacks, methods, exports, or parameters. - - Action: Prefer grouped contracts/ports when stable; use `surfaceRules` as warnings unless noise-free. -- Decomposition has a stop rule. - - Trigger: Further extraction would mostly pass host state through or hide control flow. - - Action: Stop, review, and document the boundary rather than creating one-use wrappers. -- Flat and layered DAGs are both first-class. - - Trigger: An extension/CLI has one entrypoint plus cohesive flat domain files, or a frontend/service has earned layered route/use-case/domain separation. - - Action: Preserve the local shape if dependency direction and ownership are clear; do not impose the other shape for aesthetic consistency. -- Severity is a ladder. - - Trigger: A heuristic finds pressure but not a concrete break. - - Action: Keep it as a warning until repeated evidence proves it should fail builds. +- Architecture semantics, calibration, extraction, severity, and configuration are owned by [SKILL.md](./SKILL.md); keep this file limited to development constraints for the skill and validator. +- Keep the skill and validator portable: no project-specific names, absolute local paths, stack-only assumptions, or external dependencies. Express project-specific checks through configuration rather than hard-coded repository rules. +- Validate this package with `bash scripts/validate-domain-dag.sh --root .`; its local `domain-dag.json` owns self-validation settings, not defaults for consuming projects. +- In human-readable validator output, separate section banners and final summaries from diagnostics with a blank line. Keep `--json` output free of those text banners and spacing rules. diff --git a/.agents/skills/domain-dag/SKILL.md b/.agents/skills/domain-dag/SKILL.md index 69791a17..c2567620 100644 --- a/.agents/skills/domain-dag/SKILL.md +++ b/.agents/skills/domain-dag/SKILL.md @@ -1,8 +1,6 @@ --- name: domain-dag description: Validates and guides Domain DAG architecture for domain ownership, acyclic local dependency graphs, composition roots, boundary direction, public contracts, interface-surface pressure, and shared-bucket drift. Use when auditing, refactoring, or extending modular codebases across frontend, backend, extensions, CLIs, SDKs, and service packages. -metadata: - version: 1.0.16 --- # Domain DAG @@ -98,9 +96,9 @@ Before adding or enforcing rules, calibrate the project: 1. Identify the shape: flat, layered, package, hexagonal, or hybrid. 2. Find the true composition roots: app bootstrap, extension entrypoint, HTTP router, worker runner, CLI command, SDK facade, test harness. 3. Identify the project’s public-contract mechanism: barrels, package exports, interfaces, ports, route handlers, generated clients, or documented namespaces. -4. Run generic validation first; treat warnings as leads, not facts. -5. Add project-local config only for constraints the project has actually earned. -6. Keep hard errors for low-noise invariants; keep heuristics as warnings. +4. Run [validation](#validation) with generic defaults; treat warnings as leads, not facts. +5. Add [project-local configuration](#configuration) only where aliases, layers, or public boundaries are invisible to defaults, or a real boundary violation warrants a custom rule. +6. Apply the [Rule-Severity Ladder](#rule-severity-ladder). ## Extraction Protocol @@ -112,7 +110,7 @@ When decomposing or extending a module: 4. Keep persistence, runtime lifecycle, routing, external effects, and screen/job orchestration in the host unless the extracted module explicitly owns that policy. 5. Replace cross-feature or cross-use-case reuse with lower-layer contracts rather than peer imports. 6. Run the validator and the project's normal checks. -7. Stop when the host mainly coordinates state/effects and the next extraction would hide control flow or create a one-use wrapper. +7. Apply the [Stop Rules](#stop-rules) before extracting further. ## Stop Rules @@ -241,24 +239,10 @@ A mature Domain DAG has few hard rules and good explanations. It does not need m ## Operating Protocol -1. Identify composition roots, delivery surfaces, domain roots, and support/foundation roots. -2. Classify the project shape: flat, layered, package, hexagonal, or hybrid. -3. Build or update the ownership map: owner, public contract, explicit exclusions. -4. Run `scripts/validate-domain-dag.sh` with generic defaults. -5. If aliases, layers, or public boundaries are invisible to generic validation, add project-local config. -6. Fix hard failures before cosmetic refactors. -7. Move misplaced types/constants/helpers/policies to their owning module. -8. Replace broad concrete reach-through with narrow ports/contracts when dependency direction is wrong. -9. Group wide interfaces into named contracts when the surface is stable. -10. Add custom forbidden edges, header clauses, or surface rules only after a real boundary has been violated. -11. Stop after the smallest behavior-preserving slice that improves the graph. - -## Output Policy - -- **Failures**: Cycles, entrypoint reach-through, invalid configured layer edges, invalid configured forbidden edges. -- **Warnings**: Missing headers, missing configured header clauses, shared-bucket candidates, flat-root drift, configured surface pressure. -- **Pass**: The checked graph is acyclic and configured boundary rules hold. -- **Human-readable spacing**: In non-JSON mode, separate section banners from diagnostic lines with a blank line and separate final summaries from the diagnostic stream with a blank line. Example: `--- DOMAIN DAG VALIDATOR ---`, blank line, `[INFO] ...`; after the last `[PASS]` / `[WARN]` / `[FAIL]`, blank line, `Result: N error(s), M warning(s)`. Do not add this spacing inside `--json` output. +1. Apply the [Calibration Protocol](#calibration-protocol). +2. Map domain and support owners, public contracts, and explicit exclusions beneath the identified composition roots. +3. Fix hard failures before cosmetic refactors. Use [Placement Heuristics](#placement-heuristics) for misplaced responsibilities and [Extraction Protocol](#extraction-protocol) when decomposition is warranted. +4. Stop after the smallest behavior-preserving slice that improves the graph. ## Review Lens diff --git a/.agents/skills/release-assurance/evidence/candidate-attestation.md b/.agents/skills/release-assurance/evidence/candidate-attestation.md new file mode 100644 index 00000000..c809933e --- /dev/null +++ b/.agents/skills/release-assurance/evidence/candidate-attestation.md @@ -0,0 +1,33 @@ +# 0.7.25 Candidate Attestation Preparation + +## Evidence Boundary + +This local preparation binds the validated artifact-bearing checkpoint before release-history rewrite, signing, push, tag, or publication. It proves local identity agreement only; it is not a signed release attestation and does not claim production-network safety or the unmet User reactive target. + +- Candidate checkpoint: `4a4bb07a6e807deb3693e7544cc0389eb00a6046`. +- Candidate tree: `f98374eeee93011379d546bc30eb269573ee0a4b`. +- Sorted tracked-content manifest digest: `8f5a44632a28fabc4f1949f76601a2ba4dc229fd8c61e69d076a2d9feeb8cd44`. +- Exact-tree `./scripts/validate-local.sh full` log: SHA-256 `1b594c30529cb0a02dedc7992bb0994199c9d8729886635502a92a4e12bb8615`. +- Dependency-provenance log: SHA-256 `fc99520e835defdce4a51cca6720f64453b3ad5fe060b3caecdbb3970b31e70e`. + +## Artifact Identities + +| Artifact | SHA-256 | +| --- | --- | +| Production compact Wasm | `25b9695fd9e900f17ae1f3fb0b815ac1403830264d9c31f7cee54e29f434b700` | +| Runtime metadata | `75868ea75fd85e7b79db12650a7407e89e249f5564b2157d55ac46cd26cb5d12` | +| PAPI descriptor manifest | `84aafcbb7b65f3dc01d307efd94f585e5b5b645ed146e644b7674590320b4505` | +| Actors production Weight | `9ce37ade391a29d4f60779f1887ad242dfeb4919d25cf0047ffba85d18f33901` | +| Actors ABI manifest | `7c645cde7c5405ee805281dace5cddd0e80fa60be6fdf45ac06fbeabe3403e71` | +| Actors protocol bounds | `0cd7716d1e694b516a33a47c40f16cf97ba90510e8b4792feddac89826e29d15` | +| Actors cost vectors | `ffc5f801a6ac469cace362e65125ec3eda2718eccab03e1215f7cbe58f6e3c0b` | +| Actors fee vectors | `9718850dbd55cb1732d725dd16724d34747c2a88621a51d0fa8b957801527abf` | +| Actors semantic manifest | `15ab53a31193e8a03686db673b0c57929f8425d92ea14868e7be7da831819342` | +| Observation evidence | `9e97a864833114cd2c5c5ca587f62ffa2164493d4d57d23bb3a84397eb830dec` | +| Ingress evidence | `ea9697460ec0ad05c89b68f263b60ead719e26cc9164d240c1da3e2bd347c67a` | + +`validate-local.sh full` regenerated the production runtime, metadata, descriptors and generated client evidence without candidate-worktree drift. EXP-0095 owns benchmark and artifact provenance; the current v0.7.24-to-v0.7.25 Weight ledger owns multidimensional source comparison. + +## Remaining External Gate + +The authorized release flow must rewrite the dedicated version branch to its final single release commit, rerun exact-tree full validation against that rewritten tree, package the declared release artifacts, generate and verify release `SHA256SUMS`, and obtain platform or signing attestation. No local preparation authorizes those actions. 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 e467c577..66cb8ccc 100644 --- a/.agents/skills/release-assurance/evidence/runtime-weight-delta-ledger.md +++ b/.agents/skills/release-assurance/evidence/runtime-weight-delta-ledger.md @@ -2,190 +2,185 @@ ## Evidence Boundary -This generated ledger compares the production Weight implementations in Git tag `v0.7.22` with the candidate worktree. RefTime formulas exclude database Weight; reads and writes are therefore recorded independently. ProofSize is the generated conservative estimate. A parameterized formula records its generated slope rather than collapsing it to an unstated component value. +This generated ledger compares the production Weight implementations in Git tag `v0.7.24` with the candidate worktree. RefTime formulas exclude database Weight; reads and writes are therefore recorded independently. ProofSize is the generated conservative estimate. A parameterized formula records its generated slope rather than collapsing it to an unstated component value. -Candidate release: `0.7.23`. The locally validated production runtime was generated with `./scripts/03-build-runtime.sh`; compact Wasm SHA-256 is `4b04e98b598cb0e72516e12382b742858ba720631f769b60be433d7e1acd989a`. The accepted benchmark owners use `frame-omni-bencher 0.22.0` / CLI `58.0.0`, `50` steps, `20` repeats, compiled Wasm execution, RocksDB, 1,024 MiB cache, host `fedora`, and CPU `AMD Ryzen 7 4800H with Radeon Graphics`; each generated method records date, reads, writes, measured ProofSize, and conservative ProofSize in its authoritative source. The benchmark-runtime Wasm and production Wasm are distinct evidence identities. Exact candidate commit/tree identity remains unavailable until the validated worktree is committed through the authorized release gate. +Candidate release: `0.7.25`. The locally validated production runtime was generated with `./scripts/03-build-runtime.sh`; compact Wasm SHA-256 is `25b9695fd9e900f17ae1f3fb0b815ac1403830264d9c31f7cee54e29f434b700`. The accepted benchmark owners use `frame-omni-bencher 0.22.0` / CLI `58.0.0`, `50` steps, `20` repeats, compiled Wasm execution, RocksDB, 1,024 MiB cache, host `fedora`, and CPU `AMD Ryzen 7 4800H with Radeon Graphics`; each generated method records date, reads, writes, measured ProofSize, and conservative ProofSize in its authoritative source. The benchmark-runtime Wasm and production Wasm are distinct evidence identities. Exact candidate commit/tree identity remains unavailable until the validated worktree is committed through the authorized release gate. Interpretation codes classify changed paths only: `I` identity guard; `C` correctness; `P` bounded service topology; `M` merged canonical work; `O` measured optimization. ## Changed Production Paths -| 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 | +| Pallet | Weight method | RefTime: v0.7.24 → 0.7.25 candidate | Base delta | ProofSize: v0.7.24 → 0.7.25 candidate | Reads: v0.7.24 → 0.7.25 candidate | Writes: v0.7.24 → 0.7.25 candidate | Code | | --- | --- | ---: | ---: | ---: | ---: | ---: | --- | -| 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 | -| Oracle | `resume_feed` | `15,365,000 → 15,435,000` | +0.46% | `3,551 → 3,551` | `1 → 1` | `1 → 1` | C | -| Oracle | `publish_last_value` | `30,033,000 → 33,733,000` | +12.32% | `3,559 → 3,559` | `5 → 6` | `1 → 1` | C | -| Oracle | `publish_ema_changed` | `33,385,000 → 37,156,000` | +11.30% | `3,559 → 3,559` | `5 → 6` | `1 → 1` | C | -| Oracle | `publish_ema_changed_primary_first` | `— → 46,795,000` | new | `— → 3,559` | `— → 7` | `— → 4` | C | -| Oracle | `publish_ema_changed_primary_existing` | `— → 46,864,000` | new | `— → 3,559` | `— → 6` | `— → 3` | C | -| Oracle | `publish_ema_changed_secondary_first` | `— → 49,239,000` | new | `— → 6,060` | `— → 9` | `— → 4` | C | -| Oracle | `publish_ema_changed_secondary_existing` | `— → 49,099,000` | new | `— → 6,060` | `— → 9` | `— → 4` | C | -| 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 → 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 | +| Actors | `create_user_actor` | `489,874,000 → 563,908,000` | +15.11% | `81,886 → 81,886` | `65 → 63` | `60 → 55` | C | +| Actors | `create_user_actor_at_slot` | `481,145,000 → 604,277,000` | +25.59% | `81,886 → 81,886` | `65 → 63` | `60 → 55` | C | +| Actors | `create_system_actor` | `541,837,000 → 594,079,000` | +9.64% | `81,886 → 81,886` | `63 → 61` | `58 → 53` | C | +| Actors | `create_system_actor_at_sovereign_id` | `528,846,000 → 743,612,000` | +40.61% | `81,886 → 81,886` | `61 → 59` | `56 → 51` | C | +| Actors | `create_user_actor_crossing_new_page` | `540,999,000 → 755,066,000` | +39.57% | `53,350 → 27,170` | `33 → 31` | `28 → 23` | C | +| Actors | `create_dormant_system_actor` | `70,751,000 → 139,895,000` | +97.73% | `5,736 → 7,535` | `13 → 17` | `7 → 7` | C | +| Actors | `activate_actor` | `520,675,000 → 878,618,000` | +68.75% | `81,886 → 81,886` | `57 → 55` | `52 → 47` | C | +| Actors | `deactivate_actor` | `561,742,000 → 1,255,487,000` | +123.50% | `81,886 → 81,886` | `57 → 58` | `55 → 57` | C | +| Actors | `pause_actor` | `70,541,000 → 140,663,000` | +99.41% | `5,736 → 5,736` | `7 → 8` | `2 → 1` | C | +| Actors | `resume_actor` | `69,214,000 → 123,482,000` | +78.41% | `5,736 → 5,736` | `7 → 8` | `2 → 2` | C | +| Actors | `manual_trigger` | `155,120,000 → 243,890,000` | +57.23% | `9,635 → 15,106` | `14 → 14` | `7 → 7` | C | +| Actors | `address_event_trigger_occurrence` | `171,114,000 → 292,779,000` | +71.10% | `8,367 → 8,394` | `14 → 14` | `7 → 7` | C | +| Actors | `pipeline_admission_apoptosis` | `140,872,000 → 387,346,000` | +174.96% | `5,736 → 15,106` | `15 → 19` | `15 → 16` | C | +| Actors | `close_actor` | `708,551,000 → 973,883,000` | +37.45% | `81,886 → 81,886` | `64 → 66` | `64 → 65` | C | +| Actors | `update_contract` | `984,638,000 → 1,352,289,000` | +37.34% | `81,886 → 162,782` | `67 → 101` | `63 → 94` | C | +| Actors | `set_global_circuit_breaker` | `6,845,000 → 7,683,000` | +12.24% | `0 → 0` | `0 → 0` | `1 → 1` | C | +| Actors | `record_crossing_worker_fault` | `11,105,000 → 11,874,000` | +6.92% | `1,529 → 1,529` | `1 → 1` | `1 → 1` | C | +| Actors | `record_observation_fanout_worker_fault` | `20,464,000 → 34,851,000` | +70.30% | `4,106 → 4,106` | `3 → 3` | `1 → 1` | C | +| Actors | `record_wakeup_worker_fault` | `10,057,000 → 9,917,000` | -1.39% | `1,503 → 1,503` | `1 → 1` | `1 → 1` | C | +| Actors | `clear_crossing_worker_fault` | `13,759,000 → 14,597,000` | +6.09% | `1,529 → 1,529` | `1 → 1` | `1 → 1` | C | +| Actors | `clear_observation_fanout_worker_fault` | `13,968,000 → 15,155,000` | +8.50% | `1,629 → 1,629` | `1 → 1` | `1 → 1` | C | +| Actors | `clear_wakeup_worker_fault` | `12,501,000 → 12,851,000` | +2.80% | `1,503 → 1,503` | `1 → 1` | `1 → 1` | C | +| Actors | `set_active_actor_limit` | `9,918,000 → 11,454,000` | +15.49% | `1,489 → 1,489` | `2 → 2` | `0 → 0` | C | +| Actors | `permissionless_sweep` | `48,610,000 → 89,608,000` | +84.34% | `5,736 → 5,736` | `7 → 8` | `0 → 0` | C | +| Actors | `permissionless_sweep_many` | `26,061,500 + 140,666,863·n → 86,378,643 + 173,665,591·n` | +231.44% | `1,489 + 4,746·n → 1,489 + 4,746·n` | `3 + 14·n → 3 + 15·n` | `2 + 14·n → 2 + 13·n` | C | +| Actors | `fee_collection` | `43,512,000 → 50,007,000` | +14.93% | `3,593 → 6,196` | `1 → 2` | `1 → 2` | O | +| Actors | `action_invocation_receipt` | `— → 4,680,000` | new | `— → 0` | `— → 0` | `— → 0` | C | +| Actors | `task_transfer` | `338,037,000 → 468,433,000` | +38.57% | `18,280 → 29,222` | `21 → 25` | `8 → 12` | C | +| Actors | `task_burn` | `18,997,000 → 20,045,000` | +5.52% | `3,593 → 3,593` | `1 → 1` | `1 → 1` | C | +| Actors | `task_mint` | `292,011,000 → 343,415,000` | +17.60% | `18,280 → 29,222` | `18 → 22` | `6 → 10` | C | +| Actors | `predicate_set_evaluation` | `7,264,000 + 6,569,170·c → 6,906,425 + 11,187,530·c` | -4.92% | `3,675 + 674·c → 3,839 + 1,740·c` | `1 + 1·c → 0 + 2·c` | `0 → 0` | C | +| Actors | `task_stop_cycle` | `4,260,000 → 6,705,000` | +57.39% | `0 → 0` | `0 → 0` | `0 → 0` | C | +| Actors | `task_split_transfer` | `157,986,014 + 196,105,670·l → 163,392,331 + 310,098,141·l` | +3.42% | `18,280 + 4,746·l → 29,222 + 4,746·l` | `11 + 9·l → 14 + 10·l` | `4 + 3·l → 7 + 4·l` | C | +| Actors | `xcm_asset_deposit` | `326,303,000 → 458,445,000` | +40.50% | `18,280 → 29,222` | `21 → 25` | `8 → 12` | C | +| Actors | `task_add_liquidity` | `281,953,000 → 316,317,000` | +12.19% | `34,255 → 34,255` | `16 → 16` | `15 → 15` | C | +| Actors | `task_donate_liquidity` | `164,968,000 → 181,242,000` | +9.86% | `14,035 → 14,035` | `9 → 9` | `8 → 8` | C | +| Actors | `task_remove_liquidity` | `152,675,000 → 164,758,000` | +7.91% | `8,817 → 8,817` | `8 → 8` | `6 → 6` | C | +| Actors | `task_stake` | `85,976,000 → 91,005,000` | +5.85% | `8,817 → 8,817` | `7 → 7` | `7 → 7` | C | +| Actors | `task_unstake` | `101,900,000 → 112,935,000` | +10.83% | `8,817 → 8,817` | `7 → 7` | `7 → 7` | C | +| Actors | `task_dex_exact_in` | `536,949,000 → 718,678,000` | +33.84% | `19,253 → 19,253` | `41 → 41` | `17 → 17` | O | +| Actors | `task_dex_exact_out` | `530,243,000 → 714,697,000` | +34.79% | `19,253 → 19,253` | `40 → 40` | `17 → 17` | O | +| Actors | `scheduler_on_initialize_cutoff` | `10,616,000 → 12,292,000` | +15.79% | `1,560 → 1,560` | `2 → 2` | `2 → 2` | C | +| Actors | `scheduler_on_idle_base` | `16,832,000 → 19,626,000` | +16.60% | `1,560 → 1,560` | `7 → 7` | `2 → 2` | C | +| Actors | `materialization_coordinator_base` | `26,680,000 → 25,842,000` | -3.14% | `5,982 → 5,982` | `10 → 10` | `1 → 1` | C | +| Actors | `contract_geometry_create` | `29,462,359 + 5,346,654·c → 35,052,172 + 6,036,087·c` | +18.97% | `4,494 + 2,475·c → 4,499 + 2,475·c` | `3 + 1·c → 4 + 1·c` | `2 + 1·c → 1 + 1·c` | C | +| Actors | `contract_geometry_close` | `27,029,761 + 6,953,989·c → 34,244,305 + 8,784,131·c` | +26.69% | `4,557 + 2,670·c → 4,740 + 2,669·c` | `2 + 1·c → 3 + 1·c` | `3 + 1·c → 2 + 1·c` | C | +| Actors | `contract_geometry_reconstruct` | `23,313,535 + 5,451,102·c → 31,311,645 + 6,659,243·c` | +34.31% | `4,557 + 2,669·c → 4,740 + 2,669·c` | `2 + 1·c → 3 + 1·c` | `0 → 0` | C | +| Actors | `current_step_load_head` | `20,254,000 → 30,451,000` | +50.35% | `4,513 → 4,731` | `2 → 3` | `0 → 0` | C | +| Actors | `current_step_load_tail` | `25,950,252 + 79,315·s → 39,333,653` | +51.57% | `4,729 + 14·s → 4,913 + 14·s` | `3 → 4` | `0 → 0` | C | +| Actors | `current_step_plan_opening_head` | `43,442,000 → 81,297,000` | +87.14% | `5,223 → 4,968` | `6 → 5` | `0 → 0` | C | +| Actors | `current_step_plan_suspended_head` | `71,030,000 → 123,132,000` | +73.35% | `8,058 → 5,707` | `7 → 6` | `0 → 0` | C | +| Actors | `current_step_plan_running_tail` | `76,192,916 + 131,564·s → 117,787,560 + 1,082,421·s` | +54.59% | `8,238 + 14·s → 5,796 + 73·s` | `8 → 7` | `0 → 0` | C | +| Actors | `opening_snapshot_traversal` | `— → 1,397,000` | new | `— → 0` | `— → 0` | `— → 0` | C | +| Actors | `opening_snapshot_capture` | `2,403,773 + 9,035,928·e → 6,280,406 + 10,546,366·e` | +161.27% | `4,373 + 2,260·e → 1,366 + 2,836·e` | `1 + 2·e → 0 + 2·e` | `0 → 0` | C | +| Actors | `opening_target_snapshot_capture` | `— → 11,715,105 + 8,545,944·e` | new | `— → 1,377 + 2,834·e` | `— → 0 + 2·e` | `— → 0` | C | +| Actors | `opening_share_mixed_capture` | `— → 6,913,373 + 10,801,886·e` | new | `— → 1,308 + 2,838·e` | `— → 0 + 2·e` | `— → 0` | C | +| Actors | `opening_predicate_traversal` | `— → 1,466,000` | new | `— → 0` | `— → 0` | `— → 0` | C | +| Actors | `opening_predicate_capture` | `3,046,526 + 7,474,282·p → 5,665,087 + 9,642,493·p` | +85.95% | `4,235 + 2,524·p → 4,184 + 2,673·p` | `1 + 2·p → 0 + 2·p` | `0 → 0` | C | +| Actors | `predicate_asset_evaluation` | `— → 8,967,155 + 8,774,724·p` | new | `— → 1,344 + 2,838·p` | `— → 0 + 2·p` | `— → 0` | C | +| Actors | `predicate_observation_heavy_evaluation` | `— → 20,339,756 + 10,257,124·o` | new | `— → 4,364 + 2,673·o` | `— → 2 + 2·o` | `— → 0` | C | +| Actors | `opening_max_encoded_balance_capture` | `— → 8,117,762 + 8,776,187·p` | new | `— → 1,358 + 2,836·p` | `— → 0 + 2·p` | `— → 0` | C | +| Actors | `opening_observation_heavy_capture` | `— → 12,070,595 + 10,272,731·o` | new | `— → 4,329 + 2,667·o` | `— → 2 + 2·o` | `— → 0` | C | +| Actors | `scheduler_actor_state_probe` | `68,586,000 → 114,890,000` | +67.51% | `5,998 → 15,106` | `7 → 7` | `0 → 0` | M | +| Actors | `scheduler_paged_append_existing_page` | `118,313,000 → 124,040,000` | +4.84% | `14,048 → 13,660` | `11 → 5` | `5 → 4` | C | +| Actors | `scheduler_paged_append_new_page` | `111,538,000 → 109,653,000` | -1.69% | `16,435 → 16,446` | `12 → 6` | `5 → 4` | C | +| Actors | `scheduler_wakeup_append_existing_page` | `93,309,000 → 162,035,000` | +73.65% | `7,566 → 14,366` | `9 → 7` | `3 → 5` | C | +| Actors | `scheduler_wakeup_append_new_page` | `98,688,000 → 138,637,000` | +40.48% | `7,739 → 17,169` | `9 → 8` | `4 → 6` | C | +| Actors | `scheduler_wakeup_replace_exact` | `105,043,000 → 202,054,000` | +92.35% | `8,001 → 7,820` | `11 → 17` | `7 → 13` | C | +| Actors | `scheduler_wakeup_invalidate_middle_page` | `132,631,000 → 201,705,000` | +52.08% | `13,538 → 21,681` | `11 → 11` | `5 → 5` | C | +| Actors | `scheduler_wakeup_drain_partial_page` | `453,347,000 → 2,162,182,000` | +376.94% | `53,971 → 56,539` | `99 → 70` | `18 → 19` | C | +| Actors | `scheduler_wakeup_drain_full_page` | `833,220,000 → 3,699,832,000` | +344.04% | `101,637 → 99,272` | `196 → 135` | `36 → 39` | C | +| Actors | `scheduler_wakeup_drain_dense_boundary` | `882,669,000 → 3,698,296,000` | +318.99% | `104,784 → 102,394` | `203 → 140` | `38 → 41` | C | +| Actors | `scheduler_wakeup_drain_stale_page` | `771,200,000 → 1,357,457,000` | +76.02% | `101,029 → 82,600` | `196 → 39` | `4 → 7` | C | +| Actors | `scheduler_wakeup_cursor_insert` | `345,650,000 → 442,243,000` | +27.95% | `42,706 → 41,816` | `25 → 39` | `25 → 25` | C | +| Actors | `scheduler_wakeup_cursor_pop_min` | `450,902,000 → 571,450,000` | +26.73% | `55,232 → 54,358` | `34 → 49` | `26 → 26` | C | +| Actors | `scheduler_wakeup_cursor_remove_exact` | `420,102,000 → 539,812,000` | +28.50% | `54,699 → 53,887` | `33 → 47` | `25 → 25` | C | +| Actors | `scheduler_wakeup_cursor_worker_partial` | `127,323,000 → 235,997,000` | +85.35% | `7,976 → 7,959` | `16 → 21` | `8 → 13` | C | +| Actors | `at_time_trigger_occurrence` | `214,136,000 → 349,561,000` | +63.24% | `8,552 → 8,451` | `20 → 20` | `9 → 9` | C | +| Actors | `cadenced_trigger_occurrence` | `241,445,000 → 536,041,000` | +122.01% | `8,505 → 8,450` | `22 → 27` | `11 → 16` | C | +| Actors | `scheduler_wakeup_cursor_worker_remove` | `575,780,000 → 806,400,000` | +40.05% | `56,912 → 55,857` | `50 → 65` | `33 → 35` | C | +| Actors | `scheduler_wakeup_cursor_worker_future` | `25,283,000 → 24,305,000` | -3.87% | `6,608 → 6,566` | `6 → 6` | `0 → 0` | C | +| Actors | `scheduler_paged_consume_preserve_page` | `57,620,000 → 72,287,000` | +25.45% | `5,528 → 5,118` | `10 → 5` | `3 → 4` | C | +| Actors | `scheduler_paged_consume_delete_page` | `59,086,000 → 67,258,000` | +13.83% | `5,428 → 4,809` | `10 → 5` | `5 → 4` | C | +| Actors | `scheduler_paged_tombstone_drain` | `33,874,000 + 3,778,028·n → 35,340,000 + 323,374·n` | +4.33% | `3,778 + 2,572·n → 3,032 + 79·n` | `5 + 2·n → 4` | `4 → 2` | C | +| Actors | `scheduler_paged_mixed_scan` | `37,156,000 + 47,657,638·n → 35,550,000 + 80,648,201·n` | -4.32% | `5,121 + 2,866·n → 4,675 + 1,505·n` | `3 + 4·n → 2 + 2·n` | `3 + 1·n → 2 + 1·n` | C | +| Actors | `scheduler_inner_zero_step_complete` | `60,973,000 → 58,109,000` | -4.70% | `5,537 → 4,388` | `7 → 2` | `3 → 3` | C | +| Actors | `scheduler_paged_zero_step_user_crossing_unavailable` | `— → 161,755,000` | new | `— → 6,127` | `— → 12` | `— → 0` | C | +| Actors | `scheduler_paged_execute_opening_max` | `549,800,000 → 593,660,000` | +7.98% | `27,824 → 14,158` | `30 → 24` | `15 → 13` | C | +| Actors | `scheduler_inner_opening_failed_min` | `89,459,955 + 6,187,462·t → 109,517,654 + 25,013,024·t` | +22.42% | `6,729 + 2,670·t → 5,237 + 3,000·t` | `6 + 1·t → 6 + 1·t` | `3 → 5` | C | +| Actors | `scheduler_inner_opening_retry_min` | `135,268,177 + 6,539,368·t → 195,144,203 + 29,296,426·t` | +44.26% | `6,780 + 2,669·t → 5,946 + 2,950·t` | `11 + 1·t → 15 + 1·t` | `8 → 11` | C | +| Actors | `scheduler_inner_opening_failed_max` | `129,307,530 + 129,086,543·t → 165,770,741 + 94,996,343·t` | +28.20% | `8,347 + 22,129·t → 12,845 + 10,106·t` | `9 + 17·t → 12 + 8·t` | `3 → 5` | C | +| Actors | `scheduler_inner_opening_retry_max` | `174,001,916 + 131,385,472·t → 228,451,910 + 47,226,607·t` | +31.29% | `8,179 + 22,125·t → 8,291 + 5,515·t` | `13 + 17·t → 18 + 5·t` | `8 → 11` | C | +| Actors | `scheduler_inner_opening_complete_min` | `89,448,696 + 6,148,376·t → 68,359,831 + 11,019,188·t` | -23.58% | `6,694 + 2,670·t → 4,398` | `6 + 1·t → 2` | `3 → 3` | C | +| Actors | `scheduler_inner_opening_user_complete_header_max` | `— → 175,025,000` | new | `— → 7,990` | `— → 9` | `— → 5` | C | +| Actors | `scheduler_inner_opening_user_complete_header_max_tail` | `— → 221,092,770 + 17,027,764·t` | new | `— → 10,677 + 2,834·t` | `— → 11 + 1·t` | `— → 8` | C | +| Actors | `scheduler_inner_opening_progress_min` | `151,658,315 + 6,196,436·t → 185,578,808 + 14,888,868·t` | +22.37% | `7,045 + 2,670·t → 6,363 + 2,957·t` | `13 + 1·t → 11 + 1·t` | `8 → 7` | C | +| Actors | `scheduler_inner_opening_complete_max` | `126,799,350 + 127,843,620·t → 153,228,555 + 90,160,651·t` | +20.84% | `8,312 + 22,129·t → 9,978 + 10,416·t` | `9 + 17·t → 11 + 8·t` | `3 → 3` | C | +| Actors | `scheduler_inner_opening_progress_max` | `202,040,691 + 132,879,336·t → 313,030,635 + 15,870,597·t` | +54.93% | `8,640 + 22,090·t → 49,359 + 3,202·t` | `15 + 17·t → 44 + 1·t` | `8 → 7` | C | +| Actors | `scheduler_inner_running_complete` | `95,782,916 + 2,552,077·s + 11,013,042·p → 133,921,853 + 2,058,487·s + 20,721,195·p` | +39.82% | `7,016 + 2,718·p + 49·s → 10,952 + 2,914·p + 11·s` | `9 + 2·p → 11 + 2·p` | `4 → 4` | C | +| Actors | `scheduler_inner_running_progress` | `112,950,315 + 1,651,829·s + 8,886,496·p → 284,303,244 + 6,766,806·p` | +151.71% | `6,762 + 2,693·p + 20·s → 12,546 + 2,220·p` | `13 + 2·p → 18 + 2·p` | `6 → 5` | C | +| Actors | `scheduler_inner_suspended_tail_retry` | `127,671,072 + 1,427,388·s + 8,247,122·p → 224,662,450 + 3,771,914·s + 15,334,382·p` | +75.97% | `6,244 + 2,693·p + 12·s → 6,937 + 2,210·p + 101·s` | `9 + 2·p → 15 + 2·p` | `6 → 9` | C | +| Actors | `scheduler_inner_suspended_tail_complete` | `135,401,769 + 1,926,065·s + 9,564,179·p → 213,795,680 + 15,584,645·p` | +57.90% | `7,294 + 2,718·p + 26·s → 8,925 + 2,207·p + 204·s` | `10 + 2·p → 8 + 2·p` | `4 → 8` | C | +| Actors | `scheduler_inner_suspended_tail_progress` | `145,948,500 + 2,112,826·s + 9,053,092·p → 244,666,435 + 853,649·s + 21,015,964·p` | +67.64% | `6,764 + 2,693·p + 20·s → 9,954 + 2,192·p + 491·s` | `13 + 2·p → 14 + 2·p` | `6 → 9` | C | +| Actors | `scheduler_inner_suspended_head_retry` | `101,216,218 + 624,373·n + 77,816·r + 605,547·f + 7,436,390·p → 233,830,626 + 244,723·n + 9,611,569·p` | +131.02% | `5,133 + 21·f + 22·n + 2,693·p + 2·r → 6,848 + 2,793·p` | `9 + 2·p → 14 + 2·p` | `6 → 9` | C | +| Actors | `scheduler_inner_suspended_head_complete` | `109,542,628 + 544,051·n + 34,686·r + 520,243·f + 8,244,070·p → 145,300,991 + 1,898,621·p` | +32.64% | `6,038 + 21·f + 22·n + 2,716·p + 2·r → 6,004 + 2,701·p` | `9 + 2·p → 6 + 2·p` | `4 → 4` | C | +| Actors | `scheduler_inner_suspended_head_progress` | `111,036,829 + 803,981·n + 91,680·r + 827,461·f + 7,824,145·p → 224,981,329 + 39,366·n + 11,797,163·p` | +102.62% | `5,721 + 21·f + 22·n + 2,693·p + 2·r → 7,784 + 2,793·p` | `12 + 2·p → 12 + 2·p` | `6 → 5` | C | +| Actors | `scheduler_inner_suspended_head_opening_retry` | `122,805,125 + 662,864·n + 77,292·r + 715,051·f → 273,071,818 + 6,709,813·p` | +122.36% | `11,197 + 21·f + 22·n + 2·r → 6,832 + 2,786·p` | `15 → 14 + 2·p` | `6 → 9` | C | +| Actors | `scheduler_inner_suspended_head_opening_complete` | `128,627,138 + 574,669·n + 64,436·r + 599,401·f → 128,652,913 + 4,574,010·p` | +0.02% | `11,985 + 21·f + 22·n + 2·r → 6,017 + 2,694·p` | `14 → 6 + 2·p` | `4 → 4` | C | +| Actors | `scheduler_inner_suspended_head_opening_progress` | `135,507,223 + 806,693·n + 104,938·r + 820,306·f → 236,649,836 + 11,253,145·p` | +74.64% | `11,578 + 21·f + 22·n + 2·r → 7,919 + 2,736·p` | `17 → 12 + 2·p` | `6 → 5` | C | +| Actors | `scheduler_paged_execute_cheap` | `139,405,000 + 122,349,209·n → 199,820,000 + 251,803,938·n` | +43.34% | `4,210 + 3,171·n → 3,993 + 3,009·n` | `6 + 7·n → 5 + 5·n` | `4 + 3·n → 3 + 3·n` | C | +| Actors | `scheduler_paged_execute_cheap_mixed` | `357,034,000 + 190,370,288·n → 601,203,000 + 358,077,352·n` | +68.39% | `4,772 + 3,361·n → 6,958 + 3,206·n` | `5 + 11·n → 7 + 8·n` | `4 + 5·n → 4 + 4·n` | C | +| Actors | `run_progress` | `104,904,000 → 279,510,000` | +166.44% | `8,567 → 6,456` | `9 → 14` | `2 → 9` | C | +| Actors | `run_suspend` | `103,646,000 → 286,075,000` | +176.01% | `8,376 → 5,871` | `8 → 13` | `2 → 9` | C | +| Actors | `run_complete` | `75,429,000 → 201,984,000` | +167.78% | `7,276 → 6,237` | `8 → 8` | `4 → 6` | C | +| Actors | `run_cancel` | `288,868,000 → 486,871,000` | +68.54% | `9,635 → 43,539` | `19 → 22` | `12 → 10` | C | +| Actors | `observation_change_trigger_occurrence` | `116,846,000 → 191,298,000` | +63.72% | `8,296 → 8,299` | `14 → 13` | `8 → 8` | C | +| Actors | `observation_change_ingress` | `34,852,000 → 43,232,000` | +24.04% | `6,184 → 6,184` | `5 → 5` | `4 → 4` | C | +| Actors | `observation_fanout_base` | `6,285,000 → 6,775,000` | +7.80% | `1,629 → 1,629` | `2 → 2` | `0 → 0` | C | +| Actors | `observation_fanout_branch_probe` | `13,340,000 → 14,247,000` | +6.80% | `3,587 → 3,587` | `2 → 2` | `0 → 0` | C | +| Actors | `observation_fanout_page` | `4,651,713,000 → 7,859,158,000` | +68.95% | `304,734 → 304,734` | `461 → 461` | `201 → 265` | C | +| Actors | `observation_fanout_wakeup_page` | `5,061,897,000 → 10,472,871,000` | +106.90% | `304,734 → 304,734` | `457 → 462` | `200 → 267` | C | +| Actors | `observation_fanout_coalesced_page` | `1,961,593,000 → 4,301,175,000` | +119.27% | `304,734 → 304,734` | `389 → 327` | `2 → 2` | C | +| Actors | `observation_fanout_terminal` | `228,035,000 → 385,810,000` | +69.19% | `5,736 → 15,173` | `27 → 35` | `25 → 30` | C | +| Actors | `observation_fanout_blocked_page` | `116,219,631,000 → 136,174,555,000` | +17.17% | `304,734 → 304,734` | `591 → 467` | `200 → 267` | C | +| Actors | `crossing_worker_base` | `7,543,000 → 6,984,000` | -7.41% | `1,543 → 1,543` | `2 → 2` | `0 → 0` | C | +| Actors | `crossing_work_probe` | `62,090,000 → 80,668,000` | +29.92% | `11,729 → 15,106` | `12 → 11` | `0 → 0` | C | +| Actors | `observation_crossing_trigger_occurrence` | `508,662,000 → 713,371,000` | +40.24% | `164,204 → 164,188` | `90 → 90` | `81 → 81` | C | +| Actors | `crossing_search_probe` | `125,926,000 → 134,168,000` | +6.55% | `81,886 → 81,886` | `33 → 33` | `0 → 0` | C | +| Actors | `crossing_fire_probe` | `66,770,000 → 89,119,000` | +33.47% | `11,729 → 11,729` | `14 → 13` | `0 → 0` | C | +| Actors | `crossing_fire_pair_probe` | `216,651,000 → 350,888,000` | +61.96% | `11,729 → 15,106` | `25 → 26` | `0 → 0` | C | +| Actors | `crossing_tail_refill_probe` | `20,114,000 → 20,324,000` | +1.04% | `11,729 → 11,729` | `1 → 1` | `0 → 0` | C | +| Actors | `crossing_fire_cohort_preflight` | `41,975,000 + 23,857,544·c → 44,350,000 + 26,177,365·c` | +5.66% | `1,493 + 2,699·c → 1,493 + 2,861·c` | `2 + 7·c → 2 + 6·c` | `0 → 0` | C | +| Actors | `crossing_coalesced_cohort_preflight` | `44,280,000 + 24,157,448·c → 49,029,000 + 38,580,241·c` | +10.72% | `1,493 + 2,699·c → 9,858 + 2,699·c` | `2 + 7·c → 3 + 5·c` | `0 → 0` | C | +| Actors | `crossing_terminal_cohort_preflight` | `43,721,000 + 23,827,571·c → 21,381,679 + 25,141,280·c` | -51.10% | `1,493 + 2,699·c → 1,493 + 2,861·c` | `2 + 7·c → 2 + 6·c` | `0 → 0` | C | +| Actors | `crossing_skip_cohort_preflight` | `40,718,000 + 22,509,580·c → 31,379,277 + 23,988,770·c` | -22.94% | `990 + 2,699·c → 990 + 2,861·c` | `0 + 7·c → 0 + 6·c` | `0 → 0` | C | +| Actors | `crossing_rearm_cohort_preflight` | `40,928,000 + 22,797,266·c → 55,735,000 + 36,930,066·c` | +36.18% | `990 + 2,699·c → 9,858 + 2,699·c` | `0 + 7·c → 1 + 5·c` | `0 → 0` | C | +| Actors | `crossing_rearm_pair_probe` | `93,449,000 → 101,690,000` | +8.82% | `11,729 → 15,106` | `19 → 16` | `0 → 0` | C | +| Actors | `crossing_skip_pair_probe` | `89,188,000 → 89,119,000` | -0.08% | `11,729 → 11,729` | `19 → 17` | `0 → 0` | C | +| Actors | `crossing_transition_unit` | `33,105,000 → 46,306,000` | +39.88% | `6,636 → 6,636` | `5 → 5` | `2 → 2` | C | +| Actors | `crossing_leaf_unit` | `553,082,000 → 741,098,000` | +33.99% | `162,782 → 162,782` | `90 → 90` | `81 → 81` | C | +| Actors | `crossing_page_unit` | `553,710,000 → 713,021,000` | +28.77% | `162,782 → 162,782` | `90 → 90` | `81 → 81` | C | +| Actors | `crossing_rearm_unit` | `420,591,000 → 537,717,000` | +27.85% | `162,782 → 162,782` | `80 → 83` | `74 → 74` | C | +| Actors | `crossing_rearm_pair_unit` | `472,623,000 → 667,764,000` | +41.29% | `162,782 → 162,782` | `86 → 91` | `76 → 75` | C | +| Actors | `crossing_coalesced_unit` | `439,728,000 → 575,362,000` | +30.84% | `162,782 → 162,782` | `84 → 85` | `74 → 74` | C | +| Actors | `crossing_coalesced_pair_unit` | `521,862,000 → 748,851,000` | +43.50% | `162,782 → 162,782` | `92 → 93` | `76 → 75` | C | +| Actors | `crossing_placed_unit` | `555,946,000 → 718,679,000` | +29.27% | `162,782 → 162,782` | `90 → 90` | `81 → 81` | C | +| Actors | `crossing_placed_pair_unit` | `586,537,000 → 794,527,000` | +35.46% | `162,782 → 162,782` | `97 → 98` | `85 → 86` | C | +| Actors | `crossing_placed_maximum_unit` | `9,904,350,000 → 20,066,635,000` | +102.60% | `346,462 → 608,478` | `982 → 1,111` | `466 → 595` | C | +| Actors | `crossing_placed_non_tail_emptied_unit` | `4,094,371,000 → 9,507,020,000` | +132.20% | `327,262 → 327,262` | `563 → 627` | `367 → 431` | C | +| Actors | `crossing_placed_non_tail_trimmed_unit` | `4,067,622,000 → 9,394,853,000` | +130.97% | `327,262 → 327,262` | `563 → 627` | `369 → 433` | C | +| Actors | `crossing_skip_unit` | `168,390,000 → 235,090,000` | +39.61% | `81,886 → 81,886` | `41 → 47` | `2 → 2` | C | +| Actors | `crossing_skip_pair_unit` | `77,665,000 → 195,559,000` | +151.80% | `11,729 → 11,729` | `12 → 24` | `2 → 2` | C | +| Actors | `crossing_actor_unit` | `730,831,000 → 877,570,000` | +20.08% | `162,782 → 162,782` | `97 → 97` | `91 → 90` | C | +| Actors | `transaction_extension_ingress_base` | `13,410,000 → 14,667,000` | +9.37% | `6,052 → 6,052` | `2 → 2` | `0 → 0` | C | +| Actors | `transaction_extension_ingress_notify` | `290,055,000 → 473,182,000` | +63.14% | `5,998 → 20,625` | `13 → 13` | `6 → 2` | C | +| Actors | `funding_snapshot_open` | `13,240,086 + 122,803·a → 15,152,818 + 358,026·a` | +14.45% | `4,531 → 4,531` | `1 → 1` | `1 → 1` | C | +| Actors | `maximum_context_inherent` | `191,559,950,000 → 204,517,874,000` | +6.76% | `3,517 → 3,517` | `13 → 13` | `23 → 23` | C | +| Actors | `block_resource_finalize` | `8,590,000 → 9,010,000` | +4.89% | `1,560 → 1,560` | `1 → 1` | `2 → 2` | C | +| Actors | `block_resource_meter_extension` | `10,616,000 → 11,035,000` | +3.95% | `1,560 → 1,560` | `1 → 1` | `1 → 1` | C | +| Actors | `maximum_xcm_version_discovery` | `446,572,000 → 465,709,000` | +4.29% | `248,490 → 248,490` | `102 → 102` | `1 → 1` | C | +| Router | `direct_xyk_exact_input` | `389,092,000 → 495,601,000` | +27.37% | `9,635 → 15,106` | `31 → 31` | `18 → 18` | C | +| Router | `direct_mint_exact_input` | `390,349,000 → 561,603,000` | +43.87% | `21,862 → 21,862` | `34 → 38` | `14 → 14` | C | +| Router | `native_anchored_exact_input` | `562,930,000 → 646,251,000` | +14.80% | `19,253 → 19,253` | `45 → 45` | `27 → 27` | C | +| Router | `direct_xyk_exact_output` | `387,486,000 → 481,842,000` | +24.35% | `9,635 → 15,106` | `30 → 30` | `18 → 18` | C | +| Router | `native_anchored_exact_output` | `556,714,000 → 656,309,000` | +17.89% | `19,253 → 19,253` | `44 → 44` | `27 → 27` | C | +| Router | `create_pool` | `144,923,000 → 151,628,000` | +4.63% | `34,255 → 34,255` | `13 → 13` | `10 → 10` | C | +| Router | `update_router_fee` | `8,591,000 → 8,661,000` | +0.81% | `1,489 → 1,489` | `1 → 1` | `1 → 1` | C | +| TMC | `create_curve` | `26,680,000 → 26,610,000` | -0.26% | `6,360 → 6,360` | `3 → 3` | `1 → 1` | C | +| TMC | `mint_with_distribution` | `164,758,000 → 254,994,000` | +54.77% | `6,208 → 7,535` | `11 → 15` | `4 → 4` | C | ## Interpretation @@ -193,11 +188,12 @@ Every listed dimension requires review against the owning implementation and ben ## Retired Weight Owners -- Actors `continuation_suspend` -- Actors `continuation_retry` -- Actors `continuation_complete` -- Actors `continuation_cancel` -- Actors `continuation_suffix_admission` +- Actors `cycle_orchestration` +- Actors `step_orchestration` +- Actors `scheduler_inner_opening_close_min` +- Actors `scheduler_inner_opening_close_max` +- Actors `run_retry` +- Actors `run_suffix_admission` Any retired owner requires implementation review before release acceptance; absence from the candidate alone does not prove safe replacement. @@ -207,5 +203,5 @@ Any retired owner requires implementation review before release acceptance; abse - Verify freshness: `./.agents/skills/release-assurance/scripts/weight-delta-ledger.sh --check` - 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: `e3615c4896619eabba068ba980988810f7c2ec6ebaa893f3539a9919a1bfa3c9`. +Candidate weight source identity: `a55b874f82a52fe176f49d1d86c2531031f98dd09c4e895123140ab5f532939d`. diff --git a/.agents/skills/release-assurance/references/dependency-provenance.md b/.agents/skills/release-assurance/references/dependency-provenance.md index aad47c59..b1dc69b4 100644 --- a/.agents/skills/release-assurance/references/dependency-provenance.md +++ b/.agents/skills/release-assurance/references/dependency-provenance.md @@ -14,9 +14,9 @@ Every retained material finding states runtime reachability, rationale, and an e ## 3. Current Release Evidence -The 2026-08-21 review used Cargo Audit `0.22.2`, Cargo Deny `0.20.2`, RustSec database commit `bf5c0d245a92671908518d7e765914d437954ed6`, Node `22.22.0`, and npm `11.7.0`. Cargo metadata resolved with `--locked`, npm's lock graph reproduced with `npm ci --ignore-scripts --dry-run`, Cargo licenses and sources passed, and every npm registry package carried lockfile integrity. +The 2026-09-05 review used Cargo Audit `0.22.2`, Cargo Deny `0.20.2`, RustSec database commit `5a0ebedfe8bdd2e295b171f4162f8c977bcad9a5`, Node `22.22.0`, and npm `11.7.0`. It includes the runtime's dev-only Wasm replay dependencies. Cargo metadata resolved with `--locked`, npm's lock graph reproduced with `npm ci --ignore-scripts --dry-run`, Cargo licenses and sources passed, and every npm registry package carried lockfile integrity. -The review found 17 unique material finding identities: 13 RustSec vulnerability/unsoundness IDs and four npm high aggregate package entries. No npm critical finding existed. The active h2 line was advanced from `0.4.15` to `0.4.16`, and the native Wasmtime toolchain line from `36.0.12` to `36.0.13`; both known patched versions remain fixed in `template/Cargo.lock`. +The review retained 17 unique material finding identities: 13 RustSec vulnerability/unsoundness IDs and four npm high aggregate package entries. No npm critical finding existed. The active h2 line remains `0.4.16`; the native Wasmtime toolchain and coupled Pulley/Winch/Cranelift family were advanced to Wasmtime `36.0.14` for `RUSTSEC-2026-0269`, without adding an advisory exception. The affected WASI filesystem capability is absent from the resolved DEOS graph; the patch nevertheless removes the affected Wasmtime version. `template/Cargo.lock` owns the exact patched identities. No retained RustSec finding reaches the no-std consensus runtime graph. Twelve IDs occur only in lockfile alternatives absent from the all-feature, all-target workspace graph. `RUSTSEC-2025-0055` reaches native Polkadot SDK tracing only, is excluded from runtime Wasm, and cannot be patched independently because `sc-tracing` pins `tracing-subscriber = 0.3.19` exactly. diff --git a/.agents/skills/release-assurance/references/runtime-threat-boundaries.md b/.agents/skills/release-assurance/references/runtime-threat-boundaries.md index 40a325d3..b3acadf0 100644 --- a/.agents/skills/release-assurance/references/runtime-threat-boundaries.md +++ b/.agents/skills/release-assurance/references/runtime-threat-boundaries.md @@ -55,6 +55,7 @@ The protected outcome is deterministic bounded protocol behavior under malformed - Registered Oracle publishers are trusted only for their admitted feed identities; publication does not grant broader authority and local reserve fallback is not an independent market oracle. - Slippage, output bounds, and reference deviation limit protocol fills but do not guarantee fair price, MEV resistance, deep liquidity, execution liveness, or profitable economic outcomes. - Bounded retry and catch-up guarantee a finite service path only while blocks continue and required markets, adapters, budgets, and dependencies eventually become available. +- Current production admission permits four ObservationCrossing candidates per block. It does not satisfy or imply the 10,000-User/100-block assurance target, and System schedule or broad ObservationChange measurements cannot substitute for User reactive fairness evidence. - Canonical browser views cover bounded current chain truth. Archive, search, and unbounded history require an explicitly materialized provider. - Dependency review proves the recorded graph and reachability classification, not the absence of unknown upstream vulnerabilities. - DEOS `0.x` is fresh-genesis source. No production storage lineage, `Live` preset, or downstream network-launch assurance exists before `1.0`. 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 3d44a217..bec7514c 100755 --- a/.agents/skills/release-assurance/scripts/generate-weight-delta-ledger.mjs +++ b/.agents/skills/release-assurance/scripts/generate-weight-delta-ledger.mjs @@ -8,8 +8,8 @@ import process from 'node:process'; const skill = path.resolve(import.meta.dirname, '..'); const root = path.resolve(skill, '../../..'); -const baseline = 'v0.7.22'; -const candidate = '0.7.23'; +const baseline = 'v0.7.24'; +const candidate = '0.7.25'; const output = path.join(skill, 'evidence/runtime-weight-delta-ledger.md'); const check = process.argv.includes('--check'); const weightFiles = [ @@ -144,7 +144,7 @@ const lines = [ '', `This generated ledger compares the production Weight implementations in Git tag \`${baseline}\` with the candidate worktree. RefTime formulas exclude database Weight; reads and writes are therefore recorded independently. ProofSize is the generated conservative estimate. A parameterized formula records its generated slope rather than collapsing it to an unstated component value.`, '', - `Candidate release: \`${candidate}\`. The locally validated production runtime was generated with \`./scripts/03-build-runtime.sh\`; compact Wasm SHA-256 is \`4b04e98b598cb0e72516e12382b742858ba720631f769b60be433d7e1acd989a\`. The accepted benchmark owners use \`frame-omni-bencher 0.22.0\` / CLI \`58.0.0\`, \`50\` steps, \`20\` repeats, compiled Wasm execution, RocksDB, 1,024 MiB cache, host \`fedora\`, and CPU \`AMD Ryzen 7 4800H with Radeon Graphics\`; each generated method records date, reads, writes, measured ProofSize, and conservative ProofSize in its authoritative source. The benchmark-runtime Wasm and production Wasm are distinct evidence identities. Exact candidate commit/tree identity remains unavailable until the validated worktree is committed through the authorized release gate.`, + `Candidate release: \`${candidate}\`. The locally validated production runtime was generated with \`./scripts/03-build-runtime.sh\`; compact Wasm SHA-256 is \`25b9695fd9e900f17ae1f3fb0b815ac1403830264d9c31f7cee54e29f434b700\`. The accepted benchmark owners use \`frame-omni-bencher 0.22.0\` / CLI \`58.0.0\`, \`50\` steps, \`20\` repeats, compiled Wasm execution, RocksDB, 1,024 MiB cache, host \`fedora\`, and CPU \`AMD Ryzen 7 4800H with Radeon Graphics\`; each generated method records date, reads, writes, measured ProofSize, and conservative ProofSize in its authoritative source. The benchmark-runtime Wasm and production Wasm are distinct evidence identities. Exact candidate commit/tree identity remains unavailable until the validated worktree is committed through the authorized release gate.`, '', 'Interpretation codes classify changed paths only: `I` identity guard; `C` correctness; `P` bounded service topology; `M` merged canonical work; `O` measured optimization.', '', diff --git a/.agents/skills/release-assurance/scripts/weight-delta-ledger.sh b/.agents/skills/release-assurance/scripts/weight-delta-ledger.sh index 0e4b7a5f..0ea79b21 100755 --- a/.agents/skills/release-assurance/scripts/weight-delta-ledger.sh +++ b/.agents/skills/release-assurance/scripts/weight-delta-ledger.sh @@ -14,7 +14,7 @@ usage() { cat <<'EOF' Usage: weight-delta-ledger.sh [--check] -Generates or verifies the v0.7.22-to-v0.7.23-candidate production Weight delta ledger. +Generates or verifies the v0.7.24-to-v0.7.25-candidate production Weight delta ledger. Options: --check Fail when the committed ledger differs from current weights diff --git a/.agents/skills/wiki-sync/references/okf-reference.md b/.agents/skills/wiki-sync/references/okf-reference.md index 7521a676..08004f7e 100644 --- a/.agents/skills/wiki-sync/references/okf-reference.md +++ b/.agents/skills/wiki-sync/references/okf-reference.md @@ -18,10 +18,10 @@ lock: revision_date: "2026-07-24T16:45:43Z" blob_sha1: a516d50128f5aa1f5746d1464661a39f7143e875 source_sha256: 5a3311d270bebb16d558010e75064f5b75323f284992641732b1c8097511f948 - source_trailing_newlines: 1 + source_trailing_newlines: 0 reference: embedding: deos-reference-v2-trim-final-newlines - body_sha256: c1c693e9beaa86e390cfc038ce40cf77f1b05014e2cc0c32c1378d0f5795b466 + body_sha256: 02c7651ef5726b4a1b1e29877948592793a63e839fcea146df2bfc1b0e9a70d3 adoption: bundle: wiki/index.md okf_version: "0.2" @@ -1031,4 +1031,4 @@ sources: Gross profit by segment per the cost-allocation standard.[^cost-alloc] [^cost-alloc]: Cost allocation standard -``` \ No newline at end of file +``` diff --git a/AGENTS.md b/AGENTS.md index 60db9b9d..41484bf5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -42,14 +42,17 @@ - `Delivery History`: Record meaningful completed outcomes and impact in `CHANGELOG.md`, not in backlog or durable protocol prose. - `Changelog Shape`: Format delivery entries as `- \`Domain\`: Description`, using slash-separated domain qualifiers when needed; keep at most 8 single-line entries per release and at most 512 characters per entry; exclude package-marker chores, intermediate implementation diaries, and duplicated architecture explanations. - `Spec Ownership`: Specifications own intended subsystem contracts, rationale, invariants, and public semantics. +- `Contract Closure`: Follow specification → implementation → tests → evidence-selected correction of specification and implementation → architectural description of the verified implementation. Classify discrepancies as contract, implementation, or test defects; never ratify accidental behavior by rewriting the specification. A release freeze requires these surfaces to agree, with affected evidence refreshed after corrections. - `Package Architecture Ownership`: Package-owned architecture docs describe the shipped implementation of the independently reusable crate: storage topology, bounded algorithms, modules, generic interfaces, package evidence, and code anchors. - `Integration Documentation Ownership`: Root `docs/*.integration.en.md` documents concrete DEOS reference composition across runtime bindings, adapters, cross-pallet flows, System actor topology, production weights, client realization, and operational watchpoints. - `Code Ownership`: Code and tests own executable behavior; documentation claims must remain subordinate to verified implementation truth. -- `Skill Ownership`: Repo-local skills own specialized workflows and audits; do not duplicate their internal procedures here. +- `Skill Ownership`: Repo-local skills own agent workflows and audits; project build, runtime, tests, benchmarks, CI, and release tooling must work without installed skills or files under `.agents/skills`. Skills may inspect and invoke project-owned tools, never supply a required project dependency. +- `Experiment Causality`: Give each materially distinct hypothesis one experiment owner, each decision explicit downstream relations, each measurement exact evidence/source identity, and each release gate explicit evidence owners. Frozen architectural facts reopen only through their declared evidenced invariant/impossibility trigger; separate architecture selection from production measurement and release acceptance. The architecture-experiments skill owns the record method. - `README Ownership`: Root and subtree READMEs own human orientation, setup, navigation, and current workspace purpose. - `Read-Model Ownership`: `docs/read-model.contract.en.md` owns chain/materialized data classification; `web-client/docs/architecture.en.md` owns browser realization. - `Actors Control-Plane Ownership`: `docs/actors-control-plane.contract.en.md` owns off-chain Actor Contract artifacts, typed projection/diff, forecast/simulation provenance, governance composition inputs, and materialized Actor history boundaries. - `Framework Boundary Ownership`: `docs/framework-instance.contract.en.md` owns the reusable mechanism versus downstream policy contract. +- `Builder Economy Ownership`: `docs/builder-economy.contract.en.md` owns the reference composition of second-order TMCTOL, `$BLDR` governance, invoices, BLDR Anchor, BLDR Treasury, and the parent-capital bridge; TMCTOL and Governance specifications retain their underlying mathematics and governance semantics. ## 3. Repository Topology @@ -80,7 +83,9 @@ - `Actor Casing`: In prose, title-case established role names such as `Burn Actor`, `Liquidity Actor`, and `System Actor`; keep ordinary descriptions lowercase and code identifiers idiomatic. - `Legacy Names`: Keep manager names only for historical orientation or explicit compatibility aliases at public boundaries. - `TMC`: The unidirectional issuance engine implementing the configured minting curve. -- `TOL`: The protocol-owned liquidity accumulator and bucketed reserve topology of the TMCTOL standard. +- `TOL`: The asset-scoped topology through which a treasury owns protocol liquidity and related strategic capital; anchor, bucket, and treasury counts are configuration, with the TOL component of reference first-order Native/foreign TMCTOL using A/B/C/D plus paired B/C/D treasuries and the TOL component of second-order `$BLDR` TMCTOL using BLDR Anchor plus BLDR Treasury. +- `Bucket Types`: The reference first-order aliases are Bucket Anchor (`A`), Bucket Builder (`B`), Bucket Capital (`C`), and Bucket Dormant (`D`). Anchor is the reusable immutable bucket type; the second-order `$BLDR` TOL component uses the short human name `BLDR Anchor` because it has no sibling lettered family. Implementation identifiers belong to their owning code surface and never to the economic vocabulary. +- `BLDR Anchor`: The sole immutable Anchor-type bucket owning protocol-created `$NTVE/$BLDR` LP; never qualify its human name with a bucket letter and never call it Builder Bucket. Bucket Builder (`B`) is the distinct first-order spendable/buyback lane, while BLDR Anchor communicates the second-order immutable role without the `BB` ambiguity. - `DEOS Router`: The fee-burning execution gateway selecting the candidate with maximum recipient output across XYK, TMC, and bounded Native-anchored routes. - `DEOS Oracle`: The bounded current typed observation owner; runtime implementation remains `pallet-oracle`. - `DEOS Staking`: The reference staking subsystem; runtime implementation remains `pallet-staking`. @@ -116,7 +121,7 @@ - `Unified Primitives`: Keep shared asset taxonomy and ecosystem constants in `template/primitives`; avoid duplicated magic numbers. - `AssetKind`: Preserve bitmask-based O(1) classification and dedicated staked-local/staked-foreign namespaces. - `Arithmetic`: Use `Perbill`/`Permill` for ratios and `U256` intermediates where curve arithmetic can overflow native widths. -- `Logical-First Naming`: Name stable abstractions by role before representation; a time-ordered wakeup index need not promise a particular storage structure. +- `Logical-First Naming`: Name stable abstractions by role before representation; a time-ordered wakeup index need not promise a particular storage structure. Project identifiers and code comments describe domain mechanisms, not Skill names, experiment IDs, candidate labels, or implementation-session history; keep experimental provenance in its research owner, pointing to project code rather than making code depend on it. - `Cadence`: Keep block-duration assumptions explicit, benchmarked, and configuration-driven rather than fixing DEOS to one block speed. - `Protected Complexity`: Preserve complexity earned by real constraints and invariants; remove accidental complexity and speculative indirection. - `No Premature Optimization`: Prefer contract correctness and honest product flows over speculative loading, bundle, storage, or scheduler indirection. @@ -133,10 +138,13 @@ - `Compression Terminology`: Every compression claim identifies its analysis axis and metric; do not conflate inversion, relative parity, absolute-gap compression, and arbitrage overtake. - `Economic Claim Honesty`: Never state market immunity or unconditional guarantees beyond shipped runtime behavior and falsifiable evidence. - `TOL Accounting`: Keep reserve scope, bucket state, supply basis, sellable-pressure assumptions, and governance conditions explicit in floor claims. -- `Bucket Policy`: Treat bucket topology and percentages as TMCTOL/reference-instance policy rather than mandatory DEOS kernel law. +- `Bucket Policy`: Treat anchor, bucket, and treasury topology, order, and percentages as TMCTOL/reference-instance policy rather than mandatory DEOS kernel law; TOL order names collateral dependency, not execution hierarchy. +- `Anchor Strong Immutability`: Reference Bucket A and BLDR Anchor are sealed dormant Immutable System Actors with no Contract, hot state, scheduler work, funding state, or fee dependency. Their LP-namespace balances admit inbound LP but expose zero reducible balance to ordinary, admin-forced, and runtime-internal transfer or burn paths; the LP namespace cannot enter asset destruction while this consensus freezer exists. Only an explicit runtime upgrade or fork may revise this guarantee. +- `Layered TMCTOL Classification`: First- and second-order classify complete TMCTOL economies because issuance begins in TMC; TOL names only their liquidity and strategic-capital component. The first-order Native/foreign TMCTOL and second-order `$BLDR` TMCTOL references share only the anchor-directed issuance invariant of approximately one third of total issuance; collateral allocation is topology-specific, with half entering first-order Bucket A and all `$NTVE` collateral directed to BLDR Anchor. - `Burn Liveness`: Burn effects depend on funded, configured, schedulable execution; do not describe fee capture as automatic supply reduction before the Burn Actor completes it. - `Liquidity Liveness`: Liquidity effects depend on healthy pools, configured execution plans, bounded slippage, and valid reserve accounting. - `Simulator Authority`: Use the simulator for economic math and hypotheses, not as a shadow runtime for storage, weights, Actors, governance, XCM, or client parity. +- `Simulator Minimalism`: Keep simulator models and scenarios readable, elegant, and economics-centered; test formulas, invariants, and explicit hypotheses rather than defensive implementation machinery, and retain lifecycle detail only when an economic question requires it. - `Deterministic Simulation`: Use fixed cases or explicit seeded PRNGs in correctness suites and keep wall-clock measurement in benchmark tooling. ## 7. Runtime Subsystem Contracts @@ -146,7 +154,7 @@ - `Token Bootstrap`: Asset-registration and curve-creation hooks must remain deterministic and idempotent. - `Sovereign Liquidity`: Foreign assets enter local `pallet-assets` through XCM reserve-transfer assumptions; DEOS does not delegate its liquidity accounting to foreign chains. - `Liquid Staking`: Keep one staking pallet, `stXXX` receipts, and native `stNTVE`; do not add a parallel nomination-token tier without evidence. -- `Native Security`: Native collator backing uses explicit locked `NTVE/stNTVE` LP custody rather than liquid receipt ownership. +- `Native Security`: Native collator backing uses explicit locked `$NTVE/stNTVE` LP custody rather than liquid receipt ownership. - `Actors Staking Portability`: Keep `Stake` and `Unstake` tasks generic; runtime adapters decide native, non-native, or local representation behavior. ## 7A. Governance and Reward Contracts @@ -155,6 +163,7 @@ - `Governance Shape`: Prefer `GovernanceDomain + CadenceMode + ProposalPayloadKind`; add richer proposal classes only after measured pressure. - `Urgent Policy`: Fast-track eligibility defaults deny and must be opted in per domain/payload combination. - `Strategic Governance Ingress`: A protocol-domain `L1RootAction` may enter only through the existing signed proposal surface gated by runtime-defined nonzero primary-track power, retaining ordinary fee/cap/rollback semantics; this grants no direct Root dispatch, gives `$VETO` no agenda-setting authority, and leaves Root-equivalent enactment inside the existing bounded payload executor. `template/pallets/governance/docs/specification.en.md` owns the full contract. +- `Builder Invoice Settlement`: The Governance specification owns `BaseFloorCapped` settlement semantics: an above-base scalar target may clip only to enactment-time capacity at or above the complete base amount, while below-floor capacity produces typed execution failure and zero payout; targets at or below base require their complete amount. The Builder Economy contract selects that policy and binds each invoice to a bounded canonical IPFS CID plus one governance-approved Mutable System Actor treasury account, leaving its Actor Contract and lifecycle unchanged. Builder governance gains domain-local debit authority over that declared custody, not arbitrary Actor or Root authority. - `L2 Parameters`: Treat delegated parameter changes as explicit bounded domain-owned surfaces, not permission to call arbitrary admin setters. - `Safety Bias`: Protection governance may fail closed; `$VETO` is negative constitutional power rather than a second positive-governance path. - `Governance Reward Memory`: Keep windows, expiry buckets, uniqueness, retention, and proposal maturity bounded; avoid full-account or full-proposal scans. @@ -175,6 +184,7 @@ - `Validation Layers`: Mathematical truth lives in the simulator, behavioral truth in pallets/tests, systemic truth in runtime integration tests/XCM, client truth in `web-client`, project/release composition in root scripts and workflows, and agent-method validation inside its owning Skill. - `Validation Ownership`: Each validation and test stays with the surface whose truth it decides: package/domain checks with that package or project domain, client checks in `web-client`, CI checks in workflows/root scripts, development profiles in root scripts, and Skill-method checks inside the Skill. The project MUST expose one project-owned comprehensive validation entrypoint that may compose all project surfaces but never Skill internals; if it needs a check currently inside a Skill, move that check to the project owner rather than introducing the dependency. - `Validation Scope`: Run the smallest meaningful changed-scope check first and escalate only when the diff crosses boundaries. +- `Tool Self-Validation`: Keep a repository tool's own regression checks in its explicit self-test mode rather than adding a separate test tool; reuse that mode from the owning validation flow and isolate fixture mutations from project state. - `Stateful Tests`: Use realistic stateful mocks for AMM, TMC, and cross-component mechanism verification. - `Benchmark Metrics`: Measure both RefTime and ProofSize with explicit bounded components and worst-case setup. - `Weight Bridge`: Generate pallet weight templates and bind runtime-specific implementations under `template/runtime/src/weights`. @@ -195,6 +205,7 @@ ## 9. Documentation Contract - `Spec Purity`: Specifications define intended source-of-truth contracts only; implementation status, migration notes, and rollout caveats belong in architecture docs. +- `TMCTOL Specification Boundary`: `docs/tmctol.specification.en.md` is the project-independent mathematical entry to the standard; keep concrete asset tickers, named project tokens, Actor topology, invoice semantics, and instance product flows in the Builder Economy, framework-instance, or integration owners. - `Delivery Sequence`: For non-trivial subsystems, refine specification, implement and validate code, then update package architecture and any affected DEOS integration document from shipped truth. - `Paired Docs`: Non-trivial reusable pallets should have a specification and a separate package implementation architecture map; add a root integration document only when concrete DEOS composition crosses the package boundary. - `Embedding Docs`: Reusable host-runtime obligations live at `template/pallets//docs/embedding.md`; do not create uppercase package-root `EMBEDDING.md` aliases or redirect shims. @@ -241,7 +252,7 @@ - `Wiki Role`: `/wiki` is an isolated strict OKF v0.2 bundle and concise, provenance-aware learning lens over canonical root and package-owned project truth, not a release-note mirror or docs dump; its root `index.md` declares the bundle while unrelated repository Markdown remains outside strict OKF scope. - `Wiki Owner Naming`: Name each page after the semantic object it owns, not after its directory or implementation package. Before naming, identify whether the owner is a domain, concept, mechanism, concrete subsystem, or product surface; state what readers expect there; test whether the title survives an implementation replacement; and split subsystem from mechanism only when both have enough independent content. The `overview/` directory marks an entry role, not a mandatory `DEOS X` title pattern. - `Wiki Locales`: Human pages use explicit locale suffixes and mirrored page IDs/topology; shared metadata represents localized fields; Russian prose prefers natural Russian terminology and minimizes English borrowings except canonical identifiers, code symbols, and terms whose translation would reduce precision. -- `Wiki Navigation`: New pages need provenance, related links, locale mirrors, and graph/index reachability; merge weak leaflets into stronger owner pages. +- `Wiki Navigation`: Articles explain established facts and current capability boundaries, never backlog tasks, implementation plans, or unverified completion. Link to Wiki peers or concrete non-document resources, never to source documentation in the repository. Keep source-document provenance in non-rendered structured metadata, not article links or source-path panels. New pages need provenance, related links, locale mirrors, and graph/index reachability; merge weak leaflets into stronger owner pages. - `Wiki Trust`: Treat repo-local wiki Markdown as reviewed content and enforce the trust boundary with the wiki validation skill. - `Wiki Growth`: Large wiki expansion requires consolidation; provenance, graph reachability, and explicit page status must drive merge/remove work rather than decorative freshness dates or subjective scores. diff --git a/BACKLOG.md b/BACKLOG.md index c5999e48..151d41a6 100644 --- a/BACKLOG.md +++ b/BACKLOG.md @@ -4,502 +4,41 @@ > > 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.26 — Local Causal Introspection -# DEOS 0.7.24 — Topology Scaling +Implementation starts only after 0.7.25 Geometry Freeze. The accepted [causal-introspection specification](./docs/actors-causal-introspection.specification.en.md) remains prepared future architecture, not a 0.7.25 implementation gate. -> **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. - ---- +Architectural progression: frozen execution physics → exact local cause/effect/resource truth → later whole-network Network Physiology. Causal Introspection is its epistemic prerequisite, not a renaming of physiology. Global SCCs, feedback loops, cascade analysis, centrality, capital-circulation interpretation and homeostasis/pathology classification remain Network Physiology concerns, outside this local implementation. -# Canonical Delivery Sequence +Dependency: fix shared cause/descriptor/API types and binding identities first; topology extraction, host descriptors and System-role catalog may proceed under disjoint ownership. Coordinate cause propagation and resource/receipt hot paths. Stable runtime API/event/schema fixtures unlock parallel materializer/client work; integrated semantic/threat and scale assurance then precede correction, exact-tree convergence and architecture/Wiki closure. -The release work must proceed in this exact order: +- [ ] `Actors Introspection / Cause Identity and Propagation`: Define a typed domain-separated `CauseRef` for Manual, certified AddressEvent, Observation, Temporal, ActorEffect, ExternalProtocol, Host, and explicit gaps; bind exactly one cause to each useful `pending_signal: false -> true`, retain at most one distinct deferred next-Cycle cause, consume the right cause at Opening, preserve deterministic consequence ordinals/finality, and never alter readiness, FIFO, fees, or scheduler authority. +- [ ] `Actors Introspection / Local Topology and Dependencies`: Implement one bounded mutation-free extractor for stored and proposed Contracts that separates activation inputs, ordered Step precondition/amount/host dependencies, effect bundles, and error policy; preserve Opening versus Current timing, DNF, funding, completion, and `MaxContractSteps`, use typed dependency targets, and leave unresolved dynamic routes explicitly `Unknown`. +- [ ] `Actors Introspection / Host Effect Descriptors`: Add pure identity-bound descriptors for AssetOps, DexOps, LiquidityOps, StakingOps, FeeCollector, certified ingress, Oracle, TMC, and Router that report implicit dependencies, possible atomic effect legs, induced causes, atomicity/Weight/fee owners, and unknown dynamic surfaces, failing closed on unsupported capability without becoming a second host-semantic owner. +- [ ] `Actors Introspection / Atomic Effect Bundles`: Project at most one atomic `EffectBundle` per Step with Task kind, structural legs, amount truth, induced cause classes, rollback and native-fee owners, explicit mint/burn issuance delta, no bundle for `StopCycle` or rolled-back Tasks, finalized identity for resolved/committed truth, and no cross-asset value aggregation. +- [ ] `Actors Introspection / Resource Attribution and Receipt Decision`: Separate source, shared detector, Actor-specific control, Task effect, transaction/Trigger/Pipeline/Action/Task-native fees, state holds, and subsidy; compare event reconstruction, compact receipt event, and bounded receipt state by exactness and production cost, retain no state unless justified, label maximum versus actual, and never canonically divide shared work among Actors. +- [ ] `Actors Introspection / System Role Catalog`: Add stable typed role-to-sovereign-to-current-Actor bindings and catalog identity for active, dormant, custody-only, and vacant System roles, independent of display name, Actor ID, locator reuse, and Contract identity; preserve the current Activation DAG unchanged and validate genesis/TryRuntime cardinality and host binding. +- [ ] `Actors Introspection / Binding Identity and Runtime APIs`: Derive an introspection identity from the Actor semantic model, host descriptor identities, System-role catalog, and relevant runtime metadata, then expose bounded local methods for Actor topology, proposed Contract topology, current cause, System-role bindings, and binding identity without population scans, execution-block prediction, or raw physical storage geometry. +- [ ] `Actors Introspection / Causal Materializer`: Build a reference `Cause -> TriggerOccurrence -> Cycle -> Step -> ProducedCause` artifact from finalized canonical state/events, binding genesis, range, runtime code, metadata, introspection identity, Contract and role-at-event; support deterministic full/incremental equivalence, pre-finality rollback, explicit gaps, stable artifact hashes, and CLI/library access without protocol dependence or temporal-proximity inference. +- [ ] `Actors Introspection / Local Explainability Client`: Show role, activation input, current Pipeline/cursor and cause, Step dependencies, atomic effect bundle, possible produced causes, resource maxima, observed actual evidence, truth class, and runtime/introspection identity; remain local-only, distinguish structural/current/observed truth, expose stale materialization, and degrade to bounded runtime truth during provider failure. +- [ ] `Actors Introspection / Semantic and Threat Assurance`: Exhaust every Trigger, Predicate, amount/funding mode, Task, completion/error policy, cause family, descriptor, effect bundle, resource owner, System role, API, reorg/gap, alias/collision/staleness, unknown route, and scheduler-influence threat; prove stored/proposed parity, no duplicate lineage, exact rollback, no-double-charge, bounded APIs, no new hot-path read, and unchanged FIFO/Q1/Economic Zipper/class neutrality. +- [ ] `Actors Introspection / Materializer Scale Assurance`: Test full versus incremental deterministic output, input reordering, long Pipelines, high fanout, finalized reorgs, peak memory and rebuild time at 10,000, 100,000 synthetic, and 1,000,000 mostly dormant historical identities, while keeping off-chain scale evidence separate from runtime capacity claims. +- [ ] `Actors Introspection / Correction and Final Convergence`: Classify failures as specification, implementation, test, descriptor, evidence, or materializer defects; reopen semantics rather than weakening tests, remove unjustified hot-path or receipt state, correct cause/topology/resource ownership, and finish with package/runtime/materializer/client, no-std, Clippy, TryRuntime, embedding, security-review, production-Weight, Wasm, metadata, PAPI, and ABI evidence green on one frozen tree. +- [ ] `Actors Introspection / Architecture and Wiki Closure`: Publish causal-introspection architecture and update Actors integration, host-domain descriptors, read-model/client boundaries, and bilingual Wiki with local cause/topology/effect/resource/System-role truth, canonical-chain versus materialized provenance, provider degradation, and an explicit boundary deferring Network Physiology and global graph interpretation beyond Local Causal Introspection. -```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. -``` +## DEOS 0.7.26 — Conditional Candidates ---- +These two candidates remain conditional, not commitments created by the release heading or prerequisites for 0.7.25. Same-looking Task parameters do not establish the same economic intent. No universal Obligation registry, generic primary obligation references, coalesced economic intent, WaitingCapacity, second retry lane or capacity-priority scheduler is admitted without operation-specific evidence and an accepted owning specification. Do not reopen FIFO/Q1 geometry to fit new vocabulary. -# Release Thesis +- [ ] `Actors 0.7.26 / Operation-Specific Obligation Identity`: Start only from a concrete duplicate-commitment risk observed in production-shaped evidence. Classify the owning operation as idempotent, deduplicable, joinable, non-deduplicable, or uniqueness-critical; derive exact consensus-visible identity without duplicating Actor continuation; compare `N` independent requests against one obligation plus bounded dependents for state, ProofSize, RefTime, adapter invocations, external commitments, retry, and cleanup; specify and implement only an accepted operation-local owner rather than a universal registry. +- [ ] `Actors 0.7.26 / Capacity-Release Scheduling`: Consider a distinct capacity state only if `0.7.25` evidence proves materially different scheduler semantics from ordinary retry, such as event-driven release, no exponential backoff, or a separately bounded fairness/resource domain. Specify maximum live obligations, pending resolutions, external intents, retained retry state, fanout, and queue contribution before implementation; preserve one FIFO, Actor-owned continuation, Q1, cutoff, causal floor, and Step durability. -`0.7.24` is not merely a performance release. It establishes a new physical model for DEOS Actors: +## Unscheduled Framework Convergence -```text -rich Contracts -+ lazy local state -+ one-Step causal clock -+ symmetric economic contention -+ bounded control physiology -+ active-frontier scaling -``` +Outside the Geometry Freeze dependency and without a release assignment. Preserve these contracts unchanged; only mechanically necessary frozen-C1 correctness and binding closure belongs to G3–G5. Shared lifecycle/runtime changes must not enter the 0.7.25 critical path by implication. -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. +- [ ] `Runtime / $BLDR Capital Bridge`: Align the Treasury B runtime plan with the Builder Economy capital bridge by gradually consuming both LP reserve assets through DEOS Router, splitting recipient `$BLDR` equally between burn and BLDR Treasury, preserving retained-prefix custody and bounded retry semantics, and regenerating runtime tests, benchmarks, Weight, metadata, integration truth, and client liveness projection. +- [ ] `Runtime / Builder Invoice Settlement`: Replace the fixed BLDR Treasury funding-source payload with a validated bounded CIDv1 invoice identity and explicit governance-approved Mutable System Actor treasury account, implement `BaseFloorCapped` payout with typed below-floor `InsufficientTreasuryCapacity` failure, expose target/actual/capacity-limited receipts, and regenerate tests, benchmarks, Weight, metadata, integration truth, and client projections. +- [ ] `Actors / User Creation Gate`: Add a governance-owned `UserActorCreationEnabled` flag that is false at genesis, gates only signed User Actor creation and exact-slot recreation, can be enabled or disabled through the Root-governed pallet surface, and leaves existing Active/Dormant User Actors, owner controls, execution, close, and custody recovery unchanged; expose canonical status and regenerate tests, benchmarks, Weight, metadata, and client authoring state. +- [ ] `Actors / Vacant-Slot Custody Claim`: Add a signed immediate non-FIFO call that derives the caller's vacant User sovereign slot, rejects any occupied identity/index binding, accepts a bounded duplicate-free list of `AssetKind`, reads current balances on-chain, and atomically transfers every selected available balance to the owner under ordinary transaction Weight/fee only, independent of User creation enablement; cover stale UI input, zero balances, ledger minima, transfer failure rollback, weights, metadata, and client checkbox flow. +- [ ] `Actors / Emergency Breaker Closure`: Specify and prove the exact stop matrix of the existing Root-controlled `GlobalCircuitBreaker` instead of adding a second overlapping switch: halt User/System creation, dormant activation, Pipeline/Step execution, and economic Tasks while preserving custody, bounded topology housekeeping, breaker recovery, and explicitly admitted owner recovery/control paths; test every Trigger/materialization/lifecycle path, expose canonical breaker status to clients, and harden any path that can still advance forbidden Actor semantics while active. diff --git a/CHANGELOG.md b/CHANGELOG.md index 825cc733..bc5cf0ba 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.25: Canonical Actor Control Geometry + +- `Wiki / Truth Boundary`: Removed planned causal-introspection content and release task lists from bilingual articles, retained current capabilities and limitations, and removed the reader's source-document panel. Source provenance stays in non-rendered metadata; Wiki navigation and concrete resource links remain available. +- `Actors / Introspection Contract`: Defined bounded local cause, topology, host-effect, resource, System-role, API and materializer semantics with per-claim truth classes, finalized identities, explicit gaps and no new scheduler authority; retained the design outside the Wiki, with implementation deferred until after 0.7.25 Geometry Freeze. +- `Dependencies / Native Wasm Toolchain`: Updated Wasmtime and its coupled compiler family to the patched 36.0.14 line for RUSTSEC-2026-0269; refreshed dependency provenance against the September advisory database without adding an exception. +- `Validation / Provenance and Integration`: Bound Actors source identity to content and executable modes across all worktree states. Restored package-architecture audit routing without project dependencies on Skills. Runtime fixtures preserve monotonic time and distinguish production fee/security/voting policy from benchmark substitutions. Validated Actors corruption checks with try-runtime alone, production and benchmark runtime profiles, and independent embedding with DEX, TryRuntime and no-std. +- `Architecture / Experiment Proof Graph`: Added Leaf/Synthesis ownership, frozen obligations and separate proof graphs with globally unique IDs. Decomposed integrated Actor evidence into independently scoped outcome, resource, stop and proof owners without benchmark reruns or renumbering. Preserved C1/A0 decisions and exact extraction provenance in Git; retired the validated working manifest. Measured results have independent statuses; production and release acceptance remain open. +- `Actors / Control Integrity`: Closed G3 schema, cardinality and sole-owner reconciliation. TryRuntime rejects shifted Opening cursors, orphan cold records and invalid subscriptions; Crossing ingress rejects revision exhaustion atomically. Suspended retries preserve User liability, terminal precedence and the last attempt. AtTime Opening consumes retained-latch replacements without a second Trigger charge. Release acceptance remains open. +- `Actors / Canonical Transitions`: Completed sole-owner Ready/Waiting, lifecycle and Run substitution; retained dormant, funding and cold Run owners. Removed duplicate reload paths and reused temporal authority within callback freshness boundaries. Corrected Waiting fallback's actual-Weight classification. Simulation shares the Step owner with explicit budgets and one-record rollback. Staking bootstrap uses bounded eligibility. Final-tree, artifact and integrated acceptance remain open. +- `Actors / Production Binding`: Bound funded User completion to generated maximum-header zero/tail owners without overlap or new context. Regenerated Weight/Wasm and consumers, corrected Cadenced detector/backpressure authority, accepted G6–G8 evidence and froze C1. Published that four Crossing candidates per block cannot meet the 10,000-User/100-block target; full exact-tree validation and local release assurance pass without a fairness claim. + ## 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. diff --git a/README.md b/README.md index c0023453..90d1804a 100644 --- a/README.md +++ b/README.md @@ -75,11 +75,12 @@ See [`scripts/README.md`](./scripts/README.md) for individual operator workflows | `web-client/` | Browser reference client | [`web-client/README.md`](./web-client/README.md) | | `simulator/` | Deterministic TMCTOL mathematical reference | [`simulator/README.md`](./simulator/README.md) | | `scripts/` | Bootstrap, validation, benchmarks, administration | [`scripts/README.md`](./scripts/README.md) | -| `wiki/` | Guided newcomer and frontend knowledge layer | [`wiki/index.en.md`](./wiki/index.en.md) | +| `wiki/` | Guided newcomer and frontend knowledge layer | [`wiki/index.en.md`](./wiki/index.en.md) [`wiki/index.ru.md`](./wiki/index.ru.md) | ### Core contracts - [TMCTOL Specification](./docs/tmctol.specification.en.md) +- [Builder Economy Contract](./docs/builder-economy.contract.en.md) - [DEOS Actors Specification](./template/pallets/actors/docs/specification.en.md) - [DEOS Actors External Runtime Embedding Guide](./template/pallets/actors/docs/embedding.md) - [DEOS Actors Control-Plane Contract](./docs/actors-control-plane.contract.en.md) diff --git a/docs/README.md b/docs/README.md index 48f955c6..52e2959a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -9,19 +9,21 @@ This hub routes readers to canonical contracts and shipped architecture. `DEOS` - [Read-Model Contract](./read-model.contract.en.md) - [Manifesto](./manifesto.en.md) -## Economic Standard +## Economic Standard and Reference Economy - [TMCTOL Specification](./tmctol.specification.en.md) +- [Builder Economy Contract](./builder-economy.contract.en.md) - [TMC Architecture](../template/pallets/tmc/docs/architecture.en.md) - [Asset Registry Architecture](../template/pallets/asset-registry/docs/architecture.en.md) -The simulator is the executable authority for TMCTOL formulas, thresholds, and invariant scenarios. +The TMCTOL specification owns project-independent standard mathematics. The Builder Economy contract composes its second-order reference contour with DEOS Governance, while the simulator remains executable authority for formulas, thresholds, and invariant scenarios. ## 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) +- [DEOS Actors Local Causal Introspection Specification](./actors-causal-introspection.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) diff --git a/docs/actors-causal-introspection.specification.en.md b/docs/actors-causal-introspection.specification.en.md new file mode 100644 index 00000000..a9e7d30f --- /dev/null +++ b/docs/actors-causal-introspection.specification.en.md @@ -0,0 +1,172 @@ +# DEOS Actors Local Causal Introspection Specification + +> Intended cross-system contract for bounded local explanation and finalized causal evidence. This specification does not claim that its APIs, cause propagation, descriptors, or materializer are already implemented. + +## 1. Scope and Authority + +Local introspection answers what one Actor Contract can depend on and affect, which identified occurrence opened its current Pipeline, and which effects have committed. It MUST preserve the existing Actor language, admission, fees, custody, Trigger latching, FIFO, Q1, Economic Zipper, and task-scoped transaction boundaries. + +The [Actors specification](../template/pallets/actors/docs/specification.en.md) owns execution semantics. The [resource policy](./actors-resource-policy.specification.en.md) owns resource admission and service. The [control-plane contract](./actors-control-plane.contract.en.md) owns Contract artifacts, forecasts, simulation, and existing off-chain feedback analysis. The [read-model contract](./read-model.contract.en.md) owns chain/materialized classification. This document owns only the cross-system introspection boundary and its acceptance criteria. + +No global graph, population traversal, automatic multi-hop exploration, Network Physiology, economic value aggregation, execution-block prediction, priority, feedback controller, or new authority is introduced. An application MAY compose separately obtained local results off-chain, but that composition is materialized analysis and cannot inherit canonical-chain completeness. + +Implementation follows accepted Actor control geometry. A representation choice for causes or receipts MUST NOT become a second mutable owner of lifecycle, latch, cursor, ticket, funding, or Contract state. Pure query descriptors are optional for an embedding runtime; the reference runtime must cover its admitted host capabilities before claiming complete local introspection. + +## 2. Ownership and Dependency Direction + +| Owner | Owns | Consumes; excludes | +| --- | --- | --- | +| Actors package | Pure Contract topology extraction; bounded cause association at existing transitions; public local projection types | Its typed language and host ports; no concrete DEOS role or market policy | +| Host domain | Descriptor of its public operation, atomicity, implicit effects, resources, and evidence limits | Its executable semantics; no copied Actor scheduler or alternate execution model | +| Reference runtime | Descriptor composition, producer certification, System roles, identity binding, runtime APIs | Package and host public contracts; no descriptor-driven execution | +| Materializer | Finalized lineage, historical joins, gaps, deterministic artifact format | Canonical state/events and identified schemas; no consensus dependency | +| Client | Local explanation, truth labels, unavailable/stale presentation | Identified projections; no handwritten host semantics or inferred causation | + +The extractor consumes canonical typed Contract values. It MUST reuse semantic helpers where those helpers decide dependency timing, task surfaces, admission, or bounds. Host descriptors live with the relevant operation or its reference adapter, and conformance tests bind each descriptor to that operation. Runtime composition supplies identities and typed host mappings; the package never imports the reference runtime. + +## 3. Truth Classes and Evidence + +Each claim has its own truth class; a response-wide label cannot promote unresolved fields. These classes are orthogonal to the chain/materialized and trusted-provider/verified-execution classifications in the read-model and control-plane contracts. + +| Class | Required evidence | Meaning and limit | +| --- | --- | --- | +| `StructuralCapability` | Exact Contract bytes or typed proposed Contract, semantic binding, descriptor identities | A possible dependency or effect; neither readiness nor execution success | +| `CurrentResolved` | One named state hash, relevant code/metadata/binding, exact bounded inputs | A value resolved at that state; not a promise about later execution | +| `ObservedCommitted` | Identified committed transition and block, runtime/schema, exact event or state witness | The evidenced effect committed on that branch; finality is a separate required status | +| `Unknown` | Typed reason and known partial identities | Required evidence is absent, unsupported, stale, pruned, ambiguous, or outside the bounded scope | + +Every query envelope identifies genesis, block hash, state root, runtime code hash, metadata hash, and introspection identity. The runtime returns only identities it can establish; the caller binds its result to the selected block/header and independently verifies code and metadata when claiming verified execution. Remote RPC alone remains provider evidence. Provisional results explicitly say unfinalized. Finalized publication requires a finality-verified block and never relies on a caller-supplied `finalized` boolean. + +Static proposed-Contract analysis binds exact canonical SCALE and actor type/mutability. It need not invent an on-chain Actor or state evidence. Current resolution for a proposal additionally requires an explicit account/admission context; without that context, account-dependent fields stay structural or unknown. Contract artifact identity remains the existing `contractId`, not a new competing hash. + +## 4. Cause Identity + +`CauseRef` is a bounded typed source reference, not a free-form string, timestamp heuristic, or list of ancestors. Its canonical envelope consists of a schema version, producer domain identity, one typed source variant, and a bounded occurrence coordinate. The source discriminant and payload are encoded by the identified canonical SCALE schema. Reference hashes use `blake2_256("deos:actor-cause:v1" || SCALE(envelope))`; string concatenation of display values is forbidden. + +The runtime occurrence coordinate uses block number, execution phase, and a deterministic producer-local consequence ordinal under an identified parent operation. Ordinals reset per parent, follow actual canonical consequence order, and never wrap. They require no global historical counter. Finalized external identity adds genesis hash and block hash around the runtime reference: the current block hash is unavailable while executing that block and MUST NOT be fabricated inside consensus execution. + +| Cause family | Source evidence | +| --- | --- | +| `Manual` | Accepted manual activation call coordinate and runtime-verified caller | +| `AddressEvent` | Certified ingress producer, verified source, destination, asset, and movement coordinate | +| `Observation` | Complete feed identity and the exact revision/transition selected by the existing materialization owner | +| `Temporal` | `AtTime` or `Cadenced`, Actor installation context, and the consumed logical deadline | +| `ActorEffect` | Producing Actor incarnation, Cycle nonce, Step index, attempt coordinate, and committed consequence ordinal | +| `ExternalProtocol` | Certified protocol adapter and its bounded authenticated operation reference | +| `Host` | Identified host operation and its committed consequence coordinate | +| `Gap` | Typed unavailable-origin reason and the bounded known occurrence context | + +`ExternalProtocol` and `Host` do not add Trigger families or permit arbitrary callers to certify provenance. They describe the origin carried through an existing admitted ingress path. Actor-produced certified ingress carries its `ActorEffect` origin without duplicating the movement as two independent causes. One effect may cause multiple useful Trigger occurrences; each occurrence references the same source and its own recipient context. + +If an upstream producer cannot certify a unique parent operation, the source remains `Gap` with `Unknown` origin. Balance changes, adjacent events, shared assets, equal amounts, or nearby timestamps never establish an ActorEffect parent. Producer labels and application declarations alone cannot upgrade a gap. + +## 5. Cause Association and Lifecycle + +Cause association is an annotation of a useful `pending_signal: false -> true` transition. It MUST commit atomically with that transition and its existing Trigger fee. Failed fee collection, rejected ingress, or rolled-back movement cannot leave an occurrence or cause behind. Redundant activity while latched neither replaces the cause nor creates an occurrence, evaluation, fee, ordinal allocation, or Actor-specific storage operation. + +At most one active-Cycle cause and one pending cause exist for an Actor. While Idle, only the pending cause exists. Opening consumes that pending cause into the admitted Cycle; while Running/Suspended, a later useful latch may retain one pending cause for the next Cycle. Distinct occurrences can reference the same source cause; deduplication MUST NOT merge two legitimate cycles merely because their source references compare equal. + +Continuation and Retry retain the active cause and existing Cycle nonce. Failed Task attempts have distinct attempt coordinates but create no committed Task consequences. Completion/cancellation removes active association while preserving or removing pending association exactly as the owning lifecycle transition preserves or removes readiness. Insufficient Pipeline admission records the existing terminal outcome and clears process annotations without reversing the useful Trigger fee or moving custody. + +Deactivation, Contract replacement, close, and exact-slot recreation follow existing cancellation/invalidation semantics. No annotation survives as live authority after its owning process is deleted. Historical evidence remains materialized. Actor incarnation identity uses the creation event coordinate plus genesis and Actor ID, so reused locators, sovereign accounts, IDs, or reset nonces cannot alias history. A snapshot without creation evidence may identify a current Actor but MUST leave its historical incarnation linkage unknown. + +Observation causes report the revision/transition actually selected at useful materialization, including installation/processing revision context when needed. Coalesced intermediate revisions and latched-period history are explicitly absent; no event queue, old-value archive, or guarantee of delivery per revision is added. Temporal causes identify the consumed deadline, not every missed cadence point. Cause metadata never participates in matching, rearming, cohort grouping, ticket allocation, eligibility, or service order. + +## 6. Local Topology Projection + +One mutation-free extractor serves stored and proposed Contracts. For equal canonical bytes, actor type/mutability, and binding, their structural projections MUST be identical. The stored wrapper additionally attaches the current Actor/Contract context. It never changes extraction semantics based on physical storage geometry. + +The result contains one activation input, Contract-level funding/completion/window constraints, and ordered Step projections. Each Step carries exact index, bounded DNF clause/Predicate positions and timing, amount surfaces, typed host dependencies, error policy, and at most one structural atomic effect bundle. A zero-Step Contract produces an empty Step list and retains its control-only Opening/completion explanation. + +Dependency targets are typed: `AccountAsset`, `ObservationFeed`, `Pool`, `Reserve`, `Tmc`, `StakingPosition`, `Actor`, or `Unknown`. Each target includes its owning domain and exact identity when proven. Asset identity alone cannot stand in for an account balance or a pool; unresolved account, receipt namespace, route, or market selection is explicitly unknown. Account derivation uses the runtime sovereign-account owner. + +Opening Predicates and frozen amount snapshots stay distinct from Current reads. DNF is preserved as clauses and atoms; the extractor cannot flatten it into an apparent conjunction or skip evaluated atoms based on optimistic short-circuiting. `Fixed`, `PercentageOfCurrent`, `PercentageAtOpening`, `PercentageOfLastFunding`, `AllAvailable`, and input limits retain their canonical semantics. Funding coverage/authorization is a dependency, not proof of available funds. + +Forward resource dependency means an earlier Step may write a resource read by a later Step. It is not a scheduler dependency or evidence of activation. False Precondition, resolution skip, funding unavailability, Temporary/Permanent failure, retry, and successful `StopCycle` remain separate outcomes. Suffix resource envelopes reuse canonical cost owners and remain maxima unless separately evidenced path-sensitive results apply. + +## 7. Host Descriptors and Atomic Effects + +A pure descriptor identifies operation capability, implicit resource reads, possible effect legs, possible induced cause classes, rollback owner, generated Weight owner, fee owners, and unsupported or dynamically unresolved surfaces. Descriptor construction performs no dispatch, quote execution, mutation, network request, or population scan. A current resolver may read only declared bounded keys at one snapshot and cannot turn a possible route into an observed execution. + +| Descriptor family | Required boundary | +| --- | --- | +| `AssetOps` | Exact account/asset debits and credits; mint/burn issuance deltas; native anchor/freezer restrictions | +| `DexOps`, Router, TMC | Candidate/route uncertainty, recipient versus gross amounts, trading fees, issuance and reserve effects, Oracle/certified-ingress consequences | +| `LiquidityOps` | Reserve legs, LP mint/burn, retained amounts, pool identity, atomicity and namespace restrictions | +| `StakingOps` | Input/receipt identities, custody and maturity dependencies, adapter-defined native/non-native effects | +| `FeeCollector` | Collection destination and fee kind; allocation is a separate later Actor effect | +| Certified ingress | Producer identity, authenticated source, movement/notify atomicity, possible useful-latch consequence | +| Oracle | Typed producer/feed/revision, freshness and coalescing; no external fair-price claim | + +The table names coverage obligations, not a second implementation of these domains. Unknown capability fails closed for that explanation: it emits typed unsupported information and never guesses economic legs. It does not disable an otherwise valid Task or change its execution semantics. + +An `EffectBundle` identifies one Task attempt and its atomic effect domain. Structural bundles describe possible successful legs; committed bundles exist only for successful committed Tasks. `StopCycle`, skipped Steps, and rolled-back Tasks have no committed bundle. Prefix bundles survive a later failure. A failed Task may still have a separately evidenced Action fee/control outcome under the existing fee boundary; that charge is not a successful economic Task bundle. + +Each leg reports domain, exact or unknown resource, debit/credit/mint/burn kind, amount truth, and evidence. Mint and burn explicitly report issuance delta in the affected asset. No cross-asset sum, USD valuation, or netting away retained balances is canonical. Split legs preserve canonical authored order; implicit legs use descriptor-defined order. A concrete route or receipt conversion is committed truth only when its actual identity and amount are evidenced. + +## 8. Resource Attribution and Receipt Decision + +Resource reporting separates source operation, shared detection/materialization, Actor-specific Trigger control, Pipeline Machine control, Action attempt, and host Task effect. It names transaction, Trigger, Pipeline, Action, and Task-native/trading fees independently, alongside state holds and runtime subsidy. Both RefTime and ProofSize are mandatory. Maxima, registered/charged Weight, measured execution, and actual fee transfers have distinct labels. + +Shared work is attributed to its shared owner and evidence coordinate. Canonical reports MUST NOT divide it equally among recipients, assign it all to the first Actor, or silently count it again within every Step. Analytical allocations are optional materialized policy with a declared formula and cannot claim measured per-Actor cost. + +Implementation must first test reconstruction from existing Cycle/Step/fee events and state. Compare that result with a compact receipt event and bounded receipt state for exactness, event bytes, RefTime, ProofSize, reads/writes, and lifecycle cleanup. Retain state only if a required current canonical datum cannot be obtained through an accepted cheaper owner. Unknown actual cost remains unknown; maximum Weight is not an actual measurement. + +No newly introduced hot-path read is accepted. Any required added event bytes, writes, larger proof, or computation needs explicit production-Weight and full-runtime differential evidence against frozen geometry, preserving its acceptance gates. If a cause or receipt design fails that gate, revise its encoding/owner or the specification explicitly; never silently weaken causality or carry unaccepted overhead into release. + +## 9. System Role Catalog + +The reference runtime owns a finite typed `SystemRole` catalog, with stable role identity, sovereign account, optional current Actor identity, lifecycle class, and catalog identity. Classes distinguish active, dormant, custody-only, and vacant bindings. Generic Actors owns none of the concrete role names or topology. + +Role identity is independent of display name, Actor ID, storage locator, Contract hash, and incarnation. Historical views resolve role-at-event under the event's runtime/catalog identity. Rebinding a role changes current binding without rewriting history. A custody-only or vacant role must not fabricate a runnable Actor. Genesis and TryRuntime validate unique role keys, admitted sovereign bindings, cardinality, and correspondence with concrete host adapters. Existing Activation DAG and sealed Anchor behavior remain unchanged. + +## 10. Binding and Bounded APIs + +`introspectionIdentity` is `blake2_256("deos:actor-introspection:v1" || SCALE(binding))`. The binding contains schema version, Actor semantic-model identity, ordered host descriptor identities, System-role catalog identity, and relevant runtime metadata identity. Its canonical order is schema-defined; duplicate keys and unknown required versions fail closed. Runtime code and production Weight identities travel in the snapshot envelope even when unchanged schema permits reuse of structural extraction. + +The reference API exposes these logical methods; concrete SCALE names/version numbers belong to implementation metadata. Every method is read-only and returns bounded typed errors (`NotRegistered`, `UnsupportedBinding`, `UnsupportedCapability`, `InvalidContract`, `LimitExceeded`, or `StateUnavailable`) rather than partial unlabeled success. + +| Method | Input and bound | Result | +| --- | --- | --- | +| Binding identity | No population input | Binding and published bound constants | +| Actor topology | One Actor ID | One Contract's bounded topology and current context, or absent/dormant status | +| Proposed topology | One bounded canonical Contract plus type/mutability | Structural topology; no installation or execution | +| Current cause | One Actor ID | At most active and pending cause references, with current Cycle context | +| System-role bindings | One role or bounded page cursor/limit | Runtime-owned finite catalog page and catalog identity | + +Let `S` be `MaxContractSteps`, `P` the admitted Predicate total, `A` the maximum amount/dependency surfaces per Task, `H` the maximum descriptor legs per Task, and `R` the runtime catalog bound. The implementation publishes concrete encoded-byte and work bounds for all inputs and outputs; topology size/work is bounded by `O(1 + P + S * (A + H))`, cause count by two, and role traversal by `R` with a per-call page cap. Host expansion MUST have explicit finite bounds before a descriptor is admitted. Requests exceeding a bound reject before unbounded allocation. + +No API returns raw frame/page/heap coordinates, follows effects recursively, resolves all Actors sharing an asset, walks history, or predicts FIFO service time. Runtime execution cannot call the query path as a prerequisite. Current cost/eligibility projections reuse the existing `ActorCostApi`, `ActorResourceApi`, and `ActorEligibilityApi` owners instead of reconstructing their answers in the client. + +## 11. Materializer, Finality, and Invalidation + +The reference artifact records `Cause -> TriggerOccurrence -> Cycle -> Step -> ProducedCause` only for evidenced links. It binds genesis, covered block range, finalized block hashes, code/metadata/introspection identities, Contract identity, Actor incarnation, and role-at-event. TriggerOccurrence identity includes its recipient and useful-latch coordinate; Step attempt identity includes block/phase/event coordinates, so retries cannot collide. + +Input records are normalized by chain position and canonical consequence order. Identical duplicates are idempotent; conflicting duplicates reject. Artifact bytes use one versioned canonical encoding with stable ordering and exact integer/byte representation; their domain-separated hash excludes presentation text and processing timestamps. Full rebuild and incremental ingestion over identical finalized input MUST produce identical bytes and hashes. CLI and library use the same transform. + +Provisional ingestion is held separately and may be rolled back to the common ancestor. A claimed reorganization of already accepted finalized history halts ingestion with a finality-conflict error; it is not silently repaired. Missing ranges, unavailable creation/Contract history, pruned events, unsupported runtime epochs, and uncertified parents create typed gaps. A gap cannot be joined by temporal proximity or erased by a later matching balance. + +Caches key by genesis, block hash, runtime/binding, and queried Contract or Actor context. A new head invalidates current resolutions; a code/metadata/descriptor/catalog change invalidates affected interpretations. Stored historical claims retain their original identities and are reinterpreted only with an explicitly matching decoder. Replaced Contracts and recreated Actors never inherit old current causes. Unavailable providers leave direct bounded state available and mark historical continuity unavailable or stale. + +## 12. Acceptance and Threat Witnesses + +Specification acceptance establishes ownership and implementable boundaries only. Implementation acceptance requires these witnesses against executable package/runtime behavior and the accepted production tree. + +| Surface | Required falsification witnesses | +| --- | --- | +| Language | Every Trigger family, DNF/timing, amount/funding mode, Task, completion/error policy; zero and maximum Steps; stored/proposed equality | +| Causes | Fee/ingress rollback, busy deferred latch, repeated source, redundant latch no-op, coalesced observations, missed cadence, retry, close/recreate, Contract replacement | +| Effects | Successful prefix plus failed suffix, atomic multi-leg rollback, separate fee outcome, explicit issuance, unknown route, exact adapter identity | +| Resources | No double charge, shared-owner attribution, maximum/actual distinction, receipt alternatives, bounded bytes and both Weight dimensions | +| Identities | Wrong genesis/code/metadata/binding, source spoofing, ordinal collision/overflow, slot/ID reuse, role rebinding, stale proposal | +| APIs | Oversized and malformed input, bounded expansion, unsupported host, missing Actor, no writes/events/fees, no physical geometry or population scan | +| Materializer | Full/incremental byte equality, reordered/duplicate/conflicting input, reorg/finality conflict, gaps, long Pipelines, high fanout | +| Noninterference | Identical admission, FIFO, Q1, Economic Zipper, class neutrality, committed state/effects/fees with introspection instrumentation; no new hot-path read | + +Materializer scale evidence uses 10,000, 100,000 synthetic, and 1,000,000 mostly dormant historical identities with declared ranges, distributions, peak memory, and rebuild time. This off-chain evidence does not establish runtime capacity. Production acceptance includes package/runtime/client/materializer tests, embedding, no-std, TryRuntime, Clippy, production Weight/Wasm, metadata and client ABI convergence under one binding. + +## 13. Implementation Anchors + +The starting executable surfaces are [Actor typed Contracts](../template/pallets/actors/src/types/contract.rs), [host ports and certified ingress](../template/pallets/actors/src/adapters.rs), [runtime APIs and Cycle/Task events](../template/pallets/actors/src/lib.rs), and [reference Actor configuration](../template/runtime/src/configs/actor_config.rs). Existing events already expose Actor/Cycle/Step coordinates and several economic amounts; they do not by themselves prove complete upstream cause identity, implicit host legs, or actual per-Actor resource attribution. + +These anchors constrain implementation, not the reverse: changes to physical control geometry must not change the public local semantics in this document. Shipped storage, modules, descriptors, event coverage, and integration limitations belong in package architecture and the reference integration documentation after implementation and validation. diff --git a/docs/actors-performance-assurance.specification.en.md b/docs/actors-performance-assurance.specification.en.md index 4165288e..39da82c2 100644 --- a/docs/actors-performance-assurance.specification.en.md +++ b/docs/actors-performance-assurance.specification.en.md @@ -29,6 +29,8 @@ Setup, genesis construction, prefunding, and deterministic population generation 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. +Independent RefTime-heavy and ProofSize-heavy user witnesses MUST be attempted. When the production call surface cannot saturate one component before another production bound rejects the next valid call, retain the highest justified reachable witness, identify the binding component and next-call deficit, report the unused component without calling it saturated, and keep the impossibility claim subordinate to an explicit callable-surface audit. + 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. @@ -221,13 +223,13 @@ The target fails on any combined Trigger/Pipeline charge, Actor-specific evaluat ### 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. +Benchmark semantically identical one-, two-, and three-Step Contracts under `MaxContractSteps = 4`, `8`, and `12`. 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. +A maximum 12-Step DEOS Actor with every Step executable MUST use exactly 12 distinct committed-Step blocks, with at most one committed Step in each block, and MUST NOT complete before its twelfth 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 @@ -236,7 +238,7 @@ Construct: ```text 9,500 one-Step Actors 400 three-Step Actors -100 thirty-two-Step Actors +100 twelve-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. @@ -245,7 +247,7 @@ No ten-minute completion promise applies to all Steps in this mixed workload unl ### 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. +Construct one shared Trigger instance whose affected causal cohort contains an equal deterministic mix of Contracts with lengths `0`, `1`, `4`, `8`, and `12`. 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. @@ -275,7 +277,7 @@ TryRuntime or offline integrity cost for constructing/checking the full stress s 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. +- 10,000 maximum-cost 12-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. diff --git a/docs/actors.integration.en.md b/docs/actors.integration.en.md index 4b9e3024..ce05877f 100644 --- a/docs/actors.integration.en.md +++ b/docs/actors.integration.en.md @@ -50,7 +50,7 @@ The complete DEOS deterministic System account map follows. | 9 | Treasury D | `0x1a01084c8c17375cf01299a8f492de6023bc29b78e56024510630be56b5c38f3` | `5CeoQfeA6zkG7yToYZm3L8g5gjR5aMikm4b1gVLK69CgYzsC` | | 10 | BLDR Splitter | `0xdc201c83f1db632704da438c2fe7e6212c4a25921c48cd9294f6dde633ef1d85` | `5H3KvwhcEmU5QZNcXWjwwmtduXdrKTrR5WYZqjrJm23KK14u` | | 11 | BLDR Liquidity Actor | `0x2e699b4acc26bcf078237dc13eda2470505c8bd99450269eeb7eb4c5f5472968` | `5D7ZRz4hMphgVdq9UYBA9Gtk1q2cBjKTgoDCqpBETQi6Ziq4` | -| 12 | BLDR Bucket A | `0x791ec3fe30f34d005232cdf3bb5abdc0ae14e51fe3caeb62914d35f7c81ae544` | `5EoWnoVuB925BHs9UwHUfLkcm5rSbmqzrHgFZRzY5nA4M5B6` | +| 12 | BLDR Anchor | `0x791ec3fe30f34d005232cdf3bb5abdc0ae14e51fe3caeb62914d35f7c81ae544` | `5EoWnoVuB925BHs9UwHUfLkcm5rSbmqzrHgFZRzY5nA4M5B6` | | 13 | BLDR Treasury | `0x07297bfba697b7593a93b6bc2c52f7dc4452d968c1e2c3badb09f2fafb8d1709` | `5CE6WsJ12vyyjAPMuvaqf2cdSQMVzAAxVjZDvXZK99VswFGe` | | 14 | Native staking LP provisioning actor | `0x14292af3e9e70acb4c39cfe83317039c1f2111b475b99e660d87b16948edc339` | `5CX93X5agA9cbvbv4JKpXmR8RF9ywdLbyg6WR9qY15evri5L` | @@ -61,20 +61,20 @@ The complete DEOS deterministic System account map follows. | Core | Burn Actor | 0 | Active burn plan | | Core | Fee Sink | 1 | Active 120-tick/60-second 10% buffer allocation | | Core | Liquidity Actor | 2 | Dormant | -| TOL | Bucket A | 3 | Custody-only | +| TOL | Bucket A | 3 | Dormant Immutable Anchor | | TOL | Buckets B/C/D | 4–6 | Dormant | | Treasury | Treasuries B/C/D | 7–9 | Dormant | -| BLDR | BLDR Splitter | 10 | Active 50/50 split | -| BLDR | BLDR Liquidity Actor | 11 | Dormant | -| BLDR | BLDR Bucket A | 12 | Custody-only | -| BLDR | BLDR Treasury | 13 | Dormant | +| `$BLDR` | BLDR Splitter | 10 | Active 50/50 split | +| `$BLDR` | BLDR Liquidity Actor | 11 | Dormant | +| `$BLDR` | BLDR Anchor | 12 | Dormant Immutable Anchor | +| `$BLDR` | BLDR Treasury | 13 | Dormant | | Staking | Native staking LP provisioning actor | 14 | Dormant | -Active genesis actors use the runtime System cooldown, `ActorType::System`, `Mutability::Mutable`, and no schedule window. Fee Sink's tick-zero bootstrap wakeup anchors its 120-tick period from the first consensus timestamp without executing allocation. Dormant entries occupy `ActorIdentities` and `SovereignIndex` without hot program, queue, wakeup, funding, fee, or Active-epoch state. Custody-only accounts occupy no actor identity. +Active genesis actors use the runtime System cooldown, `ActorType::System`, `Mutability::Mutable`, and no schedule window. Fee Sink's tick-zero bootstrap wakeup anchors its 120-tick period from the first consensus timestamp without executing allocation. Twelve dormant entries occupy `ActorIdentities` and `SovereignIndex` without hot program, queue, wakeup, funding, fee, or Active-epoch state: ten are Mutable activation candidates, while Bucket A and BLDR Anchor are sealed Immutable identities that reject activation, mutation, deactivation, and close. The runtime LP freezer admits incoming LP to both Anchors, exposes zero reducible LP balance to ordinary, admin-forced, and internal transfer/burn paths, and blocks destruction of every LP asset class. -The reference runtime configures no System Immutable actor, so it has no actor-specific emergency migration or custody disposition to execute. A downstream runtime that admits an indefinite System Immutable actor must ship its migration-specific source/target actor set, bounded Close or Deactivate disposition, custody handling, terminal invariant, and Continuation policy with the same upgrade. The ordinary DEOS Governance path exposes 3-day lead-in, 7-day vote, 7-day protection, and 3-day enactment delay—20 days before bounded maturity/operational delay. Protocol `L1RootAction` can use the separately governed 24-hour urgent path only with unanimous raw protection-track `Pass`; Actors promises neither path completes within a finite time. +The reference runtime configures two sealed dormant System Immutable identities and no active System Immutable Contract. Bucket A and BLDR Anchor therefore have no Actor-level emergency close, deactivation, or custody disposition: changing their identity or LP-freeze contract requires an explicit runtime upgrade or fork. A downstream runtime that admits an indefinite active System Immutable actor must ship its migration-specific source/target actor set, bounded Close or Deactivate disposition, custody handling, terminal invariant, and Continuation policy with the same upgrade. The ordinary DEOS Governance path exposes 3-day lead-in, 7-day vote, 7-day protection, and 3-day enactment delay—20 days before bounded maturity/operational delay. Protocol `L1RootAction` can use the separately governed 24-hour urgent path only with unanimous raw protection-track `Pass`; Actors promises neither path completes within a finite time. -`ActorIdentityCount` covers thirteen active plus dormant identities. `NextActorId = 15` preserves the reserved address range. Every expected small-native-flow System or custody account receives one persistent free-balance ED anchor because a provider or reserved balance alone does not make a zero-free account eligible for sub-ED native ingress under `pallet-balances` v50. +`ActorIdentityCount` covers all fifteen active plus dormant identities. `NextActorId = 15` preserves the reserved address range. Every expected small-native-flow System sovereign account receives one persistent free-balance ED anchor because a provider or reserved balance alone does not make a zero-free account eligible for sub-ED native ingress under `pallet-balances` v50. ## Actor Contract Families @@ -87,13 +87,17 @@ The runtime keeps TMCTOL policy declarative through builders in `actor_config.rs | `build_zap_contract_steps` | Liquidity Actor | Add LP → surplus swap → split LP to buckets | | `build_bucket_lp_transfer_contract_steps` | Buckets B/C/D | Transfer bounded LP fraction to paired Treasury | | `build_treasury_lp_unwind_contract_steps` | Treasuries B/C/D | Return typed failure unless the asset is a registered local LP; otherwise remove it into Treasury custody | -| `build_bldr_splitter_contract_steps` | BLDR Splitter | Split minted BLDR share between liquidity and treasury lanes | -| `build_bldr_liquidity_contract_steps` | BLDR Liquidity Actor | Add NTVE/BLDR liquidity → transfer LP to BLDR Bucket A | -| `build_treasury_b_buyback_contract_steps` | Treasury B | Optional NTVE buyback → burn acquired target | -| `build_native_staking_liquidity_contract_steps` | Native Staking Liquidity Actor | Donate balanced `NTVE/stNTVE` without minting LP | +| `build_bldr_splitter_contract_steps` | BLDR Splitter | Split minted `$BLDR` share between liquidity and treasury lanes | +| `build_bldr_liquidity_contract_steps` | BLDR Liquidity Actor | Add `$NTVE/$BLDR` liquidity → transfer LP to BLDR Anchor | +| `build_treasury_b_buyback_contract_steps` | Treasury B | Optional `$NTVE` buyback → burn acquired target | +| `build_native_staking_liquidity_contract_steps` | Native Staking Liquidity Actor | Donate balanced `$NTVE/stNTVE` without minting LP | These builders configure the reusable task language; they do not create pallet-level roles or Actors-id policy branches. +The Builder Economy contract classifies BLDR Treasury as a Mutable System Actor treasury whose sovereign account may be debited by domain governance without mutating its Actor Contract. Genesis currently retains actor id `13` as a Dormant System identity, so a later activation may install an independent bounded treasury plan while governance and Actor execution remain serialized consumers of the same custody. Governance architecture owns the current invoice-settlement convergence gap. + +The current Treasury B builder is narrower than the [Builder Economy contract](./builder-economy.contract.en.md): it consumes only a bounded percentage of Native and burns all target output. The target composition preserves gradual Bucket B LP transfer and paired Treasury unwind, then routes both resulting reserve assets into `$BLDR` and divides recipient output equally between burn and BLDR Treasury. Because existing Steps commit independently, the runtime change must specify route order, retained-prefix custody, retry behavior, liveness inspection, and Weight before this integration document can describe that target as shipped. + ## System Activation DAG The DEOS runtime owns one bounded System activation manifest over known ids `0..=14`. Its nodes and ranks are descriptive host metadata, not Actor Contract fields. The only declared edge effect is a successful certified Actor `Transfer` or `SplitTransfer` into a known System sovereign whose active Contract selects `AddressEvent`. @@ -104,8 +108,7 @@ The DEOS runtime owns one bounded System activation manifest over known ids `0.. | Liquidity Actor | TOL Buckets A/B/C/D | | TOL Buckets B/C/D | Treasuries B/C/D respectively | | BLDR Splitter | BLDR Liquidity Actor; BLDR Treasury | -| BLDR Liquidity Actor | BLDR Bucket A | -| BLDR Bucket A | BLDR Treasury | +| BLDR Liquidity Actor | BLDR Anchor | `DeosSystemActorContractValidator` checks every Active System installation and replacement against this manifest before Contract mutation. The runtime integrity gate ranks all manifest nodes with bounded Kahn traversal, rejects a cycle, and validates every genesis System Contract. The derived projection scans only the bounded known catalog, includes edges whose target currently has an active `AddressEvent` Contract, and remains read-only; runtime tests require every projected edge to belong to the manifest and prove an undeclared back-edge is rejected without changing the stored Contract. @@ -119,7 +122,7 @@ The package architecture owns the exhaustive public reachability matrix. DEOS pr `Foreign asset + TOL lane`: register the foreign asset, create the Native/foreign pool, extend the Burn Actor, activate the Liquidity Actor, then optionally activate paired Bucket transfer and Treasury unwind plans. -`BLDR lane`: retain the BLDR Splitter at genesis, create the NTVE/BLDR pool, activate the BLDR Liquidity Actor, then optionally activate Treasury buyback/burn policy. +`$BLDR lane`: retain the BLDR Splitter at genesis, create the `$NTVE/$BLDR` pool, activate the BLDR Liquidity Actor, then optionally activate the current Native-only Treasury B buyback/burn policy. The dual-reserve burn/treasury bridge remains an explicit runtime convergence item. `Native staking LP lane`: register native staking, initialize `stNTVE`, create and seed the AMM, then call `activate_native_staking_liquidity_actor`. Activation fails until receipt asset, staking pool, actor, and nonempty AMM all exist. @@ -215,6 +218,8 @@ Fee-collector ledger movements are intentionally excluded from certified Address ## Fee Composition +`RuntimeStepControlWeight` reserves one generated `fee_collection` allowance in Actor Control for each non-StopCycle Step, independently of Actor class. Opening, Running and Suspended actual control settle it only when a nonzero User Action fee must be collected; System and non-invoked attempts reclaim it. The existing bounded Pipeline machine envelope prepays this control work, while Action fees still derive solely from Task-effect Weight. This is separate from Pipeline admission collection. Base-selector Opening identity and component-wise reservation checks remain fail-closed. Maximum Opening retains the composed bound; matching maximal Opening progress settles the admitted base envelope rather than substituting an under-covering direct profile. Control binding identity includes selection-policy version 3. Complete Weight/Wasm regeneration and remaining invocation overhead acceptance are still pending. + 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 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. @@ -227,6 +232,14 @@ The runtime-upgrade integrity gate re-derives the complete Fee Sink/native-secur Pure lifecycle cleanup charges no execution fee and runs no plan. User fee admission reserves transient Native fees without consuming the persistent sovereign ED anchor. +The reference precondition limits are four clauses, four predicates per clause and four total predicates per Step. The composed `RuntimeStepControlWeight` selectors price the hint `total + Opening` at `n = min(hint, 4)`, with zero cost at zero. They use the component-wise maximum of generated asset-heavy mixed and pure-asset models at `n`, plus the observation-heavy model at `n - 1` only when `n >= 2`. The control identity binds all three inputs. This parameter is at least the number of Step predicate visits; Opening visits use saved results, while Current visits evaluate host state. Opening capture is priced separately. Reference host/state evidence and selector tests cover these staged components; direct aggregate profiles, complete admission and full production regeneration remain unqualified. + +Composed Opening control includes separate generated traversals of the full bounded Contract when there are zero Opening predicate results or zero amount-snapshot entries. Predicate capture still visits Steps, clauses and skipped Current predicates; amount capture still classifies each Step's authored amounts. Each zero-result traversal is reserved only for head contexts and charged only during Opening, replacing its corresponding nonzero capture charge. Inclusive direct Opening owners receive no additive traversal. + +Nonzero amount capture uses the component-wise maximum of generated Preservable, Target and one-share mixed models. Target input is capped at the 12-Step domain and share-mixed input at 23 entries; the Preservable model covers all 24 entries. These are alternative complete capture models, never additive traversal charges. The control identity binds all three input models, their selected result and the zero-result alternatives. The scoped binding consumes typed, Native, unfunded and mixed capture evidence; complete phase/admission reconciliation and final production regeneration remain open. + +Nonzero Opening predicate capture selects the component-wise maximum of full-Contract mixed and pure maximum-account models at the captured count, plus one-account/Oracle-heavy capture at `count - 1` only for counts of at least two. The first two models cover 0..48; the Oracle parameter covers 1..47. Zero selects only the independent traversal. V10 binds every applicable input and selected result; maximum and actual use the same selector. Generated fits, recorded proof and sampled timings remain distinct evidence classes; these staged components do not certify complete production admission. + ## Runtime Bounds and Block Budget | Bound | DEOS value and role | @@ -243,7 +256,7 @@ Pure lifecycle cleanup charges no execution fee and runs no plan. User fee admis | `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` | Configured maximum remains within `0..=255`; each User or System Contract admits `0..=32` Steps under the DEOS production binding | +| `MaxContractSteps` | Configured maximum remains within `0..=255`; each User or System Contract admits `0..=12` Steps under the DEOS production binding, while independent hosts may select another bounded value | | `MaxRetryAttempts` | 10 cursor-local unsuccessful attempts | | `MaxConsecutiveFailures` | 10 | | `MaxAutoCloseNonceHorizon` | 10,000 | @@ -292,7 +305,7 @@ The current Actors production generation used `frame-omni-bencher 0.22.0`, bench `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. 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. +`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 `f54ae4e3a9d9ad99fc25b61b262785c09338e3cf974ae889475354cf3e3d738f`, and metadata `0116be51f4994c77687d50d49418bcbf7d2b95f044404a8b091e33412be9653c`. `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 @@ -365,7 +378,7 @@ The current production policy has a `44%` shared materialization envelope, exact | 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. +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. EXP-0095 binds these selected values to the production Weight source and Wasm; larger candidate-count cohorts or sparse-search amortization would require a later geometry campaign and complete evidence refresh. Every drain figure assumes one ready homogeneous branch, solvent Actors, available queue positions and tickets, no materialization fault, production block reserve, and no competing family consumption beyond its protected minimum. Producer rate, queue occupancy, mixed thresholds, paused or already-latched Actors, insolvency, branch changes, and the rotated lending order can change observed completion. These are bounded capacity facts, not a publication-rate, activation-latency, task-success, wall-clock, or economic-success SLA. @@ -375,6 +388,10 @@ The runtime cross-pallet hook-rejection anchor fills Actors dirty capacity, atte `ACTORS_OBSERVATION_PUBLISHER_INVENTORY` closes the reference-runtime publisher set to `DEOS Oracle::OnObservationChanged`. The Oracle hook reaches Actors through `ObservationTransitionIngress` with the exact revision and previous/current scalar values; no second runtime publisher owns transition progression. Generated observation evidence scans the complete runtime-config Rust tree, requires exactly one Oracle-owned typed ingress call, and rejects direct broad-fanout bypasses. +`TmctolObservationProvider::current` supplies Crossing rearm with the retained canonical value/revision even when its feed is Paused or its value is old; age-sensitive predicate observations use the separate `observe` interface. Deactivated feeds instead return `Unavailable`. Oracle deactivation is terminal, retains the feed identity and last observation, and has no ordinary resume path. A previously admitted Crossing therefore cannot become `Uninitialized` through ordinary Oracle calls, but can lose availability after deactivation. + +Deactivating a feed used by a latched Crossing can prevent Opening at the live FIFO head and stop later Actors despite spare Weight. Opening retains paid readiness and commits no Pipeline fee or cycle. Authorized Mutable owner close, independently due terminal semantics, or an explicit runtime upgrade can resolve the obstruction within their existing authority; observation loss grants no new Immutable close permission. See Actors specification §5.2/§8.5 and `scheduler_paged_zero_step_user_crossing_unavailable` for the measured refusal and Mutable-close recovery path. + Task rollback and lifecycle rollback remain distinct corpus boundaries. A DEX adapter that fails after input transfer restores actor/pool task writes while `ContinueNextStep` permits the following transfer and cycle summary to commit. Corrupt dirty-list linkage makes deactivation fail closed and restores actor, subscription, dirty-feed, list, and event pre-state; explicit linkage repair permits a fresh deactivation attempt to finish cleanup. Operational-recovery fixtures bind exact runtime version identity, breaker deferral/recovery, Continuation wakeup, and bounded repair. Version drift yields `EvidenceMismatch` without changing factual reactive state. Breaker activation produces no partial cycle/close evidence; recovery admits the deferred close. A suspended retry wakes at cooldown without another signal, while one-actor permissionless repair remains available under the breaker and performs no hidden task work. diff --git a/docs/builder-economy.contract.en.md b/docs/builder-economy.contract.en.md new file mode 100644 index 00000000..dc1945cc --- /dev/null +++ b/docs/builder-economy.contract.en.md @@ -0,0 +1,243 @@ +# Builder Economy Contract + +- **Component**: DEOS reference Builder economy +- **Status**: Durable reference-economy contract +- **Audience**: economic designers, governance contributors, runtime integrators, client authors, and downstream fork teams + +This document is the canonical entry point for the DEOS reference Builder economy. It composes the project-independent TMCTOL standard with DEOS Governance into one optional second-order economic domain without moving Builder-specific policy into either dependency. + +## 1. Authority and Dependencies + +This contract owns the cross-system composition of `$BLDR`, Builder governance, Builder invoices, BLDR Anchor, BLDR Treasury, and the parent-capital bridge. + +Its two primary normative dependencies are: + +- [`tmctol.specification.en.md`](./tmctol.specification.en.md), which owns TMC pricing, mint conservation, TOL reserve mathematics, XYK floor analysis, burn compression, and floor-reporting preconditions. +- [`template/pallets/governance/docs/specification.en.md`](../template/pallets/governance/docs/specification.en.md), which owns governance domains, primary and protection tracks, vote power, lifecycle, invoice resolution, payout execution, boundedness, and observability. + +Supporting ownership remains separate: + +- [`framework-instance.contract.en.md`](./framework-instance.contract.en.md) owns the mechanism-versus-instance-policy boundary. +- [`core.architecture.en.md`](./core.architecture.en.md) and [`actors.integration.en.md`](./actors.integration.en.md) describe the shipped DEOS composition and any implementation gap from this contract. +- [`../simulator/`](../simulator/) owns executable project-independent TMCTOL formulas, conservation checks, floor/compression scenarios, and parameter hypotheses; it does not model the Builder composition or its runtime execution. +- Runtime code and tests own executable behavior; Wiki pages are explanatory projections rather than normative owners. + +If this document conflicts with TMCTOL mathematics, the TMCTOL specification wins for the standard mechanism. If it conflicts with governance lifecycle or vote semantics, the Governance specification wins. This document decides only how those two contracts compose for the reference Builder domain. + +## 2. Purpose + +The Builder economy is an optional L2 tactical domain for coordinating and funding completed useful work. + +Its reference loop is: + +```text +useful completed work +→ bounded public invoice +→ $BLDR primary evaluation +→ Native protection gate +→ transactional payout from BLDR Treasury +``` + +The mechanism separates economic participation from permanent status. Founder, employee, team, or agent identity creates no protocol claim by itself; any compensation policy must enter through an explicit allocation or the same bounded funding path available to the declared contributor class. + +This contract defines mechanics and invariants. It does not define what work is valuable, who should perform it, how teams organize, what invoice etiquette is socially acceptable, or what demand strategy a downstream product should use. + +## 3. Second-Order TMCTOL Composition + +The `$BLDR` economy specializes TMCTOL as a second-order contour: its TMC accepts the first-order minted asset `$NTVE` as collateral, and its protocol-owned liquidity pairs `$BLDR` with `$NTVE`. + +`Second-order` classifies the complete TMCTOL economy because `$BLDR` issuance begins in its TMC and depends on collateral issued by first-order TMCTOL. TOL names only the liquidity and strategic-capital component inside that economy. The order does not grant execution priority, governance superiority, or authority over the first-order economy. + +The Builder contour inherits these TMCTOL properties rather than redefining them: + +- Unidirectional TMC issuance with deterministic curve pricing. +- Transaction-local mint conservation. +- Protocol-owned XYK liquidity and live pro-rata reserve accounting. +- Explicit separation of spot price, stress-floor analysis, and public guarantee claims. +- Burn effects that remain conditional on funded and live execution. +- Deterministic rounding and ledger conservation. + +The Builder contour adds one concrete allocation and custody topology described below. + +## 4. `$BLDR` Mint and Custody Topology + +For total `$BLDR` issuance `M`, recipient output `U`, and protocol output `T = M - U`, the reference split is: + +```text +direct_treasury = floor(T / 2) +anchor_issuance = T - direct_treasury +U + anchor_issuance + direct_treasury = M +``` + +With the reference TMCTOL one-third/two-thirds mint split, this yields approximately: + +```text +1/3 M → recipient +1/3 M → BLDR Anchor liquidity lane +1/3 M → BLDR Treasury +``` + +All `$NTVE` collateral paid into the `$BLDR` TMC is anchor-directed. The collateral is sent to the BLDR Liquidity Actor rather than divided with BLDR Treasury. + +The two capital owners are independent: + +- `BLDR Anchor` owns every protocol-created `$NTVE/$BLDR` LP token as a sealed dormant Immutable System Actor. Its runtime LP freeze admits incoming LP while exposing zero reducible balance to ordinary, admin-forced, internal transfer, burn, and LP-class destruction paths. Only its live pro-rata LP reserve claim counts as anchor support. +- `BLDR Treasury` owns spendable capital used by Builder governance, including invoice funding. Treasury spending cannot withdraw or mutate BLDR Anchor custody. + +The `$BLDR` topology instantiates one technical Anchor-type bucket. Because it has no sibling A/B/C/D family, its human name is simply `BLDR Anchor`; `BLDR Treasury` is a separate treasury instance rather than a sub-position of that bucket. `Builder Bucket` is not an alias: Bucket Builder (`B`) is the distinct first-order spendable lane that funds `$BLDR` acquisition, whereas BLDR Anchor names immutable second-order LP custody. + +Unmatched balances waiting in the liquidity lane remain visible custody but do not count as deployed anchor reserves until represented by live LP. + +## 5. Relationship to First-Order TMCTOL + +The first-order and Builder TMCTOL contours share one reference invariant: approximately one third of total issuance is directed toward immutable protocol-owned anchor liquidity. + +They do not share a collateral percentage: + +- First-order Bucket Anchor (`A`) receives half of first-order collateral under the reference four-bucket split. +- BLDR Anchor receives all `$NTVE` collateral paid into the `$BLDR` TMC. + +No conclusion about second-order collateral may be inferred solely from the first-order bucket percentages. Every floor report must use the live reserves and sellable-pressure assumptions of the contour being reported. + +## 6. Parent Capital Bridge + +The reference economy may recycle first-order Bucket Builder (`B`) capital into the Builder contour through the paired Treasury B lane. + +The target bridge preserves this sequence: + +```text +Bucket B releases a bounded LP portion +→ Treasury B receives both pro-rata reserve assets +→ each reserve asset routes independently into $BLDR +→ recipient $BLDR splits between burn and BLDR Treasury +``` + +For recipient acquisition output `Q`: + +```text +burn = floor(Q / 2) +recycled_treasury = Q - burn +burn + recycled_treasury = Q +``` + +Route accounting must remain explicit: + +- An XYK or other market route acquires existing `$BLDR`; net issuance changes by `-burn`. +- A `$BLDR` TMC route creates full issuance `M`; net issuance changes by `M - burn`, while its ordinary direct-treasury and anchor allocations remain separately attributable. +- A report must not describe both branches as uniformly deflationary. + +Runtime execution must advance bounded portions and preserve retained custody across retries. Analytical evaluation may collapse the sequence into one economic round only when it preserves reserve release, route choice, mint or market acquisition, burn, treasury funding, and anchor effects independently; the current simulator carries no dedicated Builder bridge model. + +## 7. Governance Topology + +The reference Builder domain is an L2 tactical governance domain protected by L1 Native economic power. + +Its two tracks are: + +- `Primary track`: `$BLDR` domain governance power. Under the Governance specification this is the runtime-resolved same-domain staking surface, not an unqualified free-balance vote. +- `Protection track`: Native `Veto / Pass` power resolved from the runtime-declared `NativeVotePower` surface. + +This topology means Builder participants evaluate tactical work with `$BLDR`-backed power, while Native economic stakeholders retain a constitutional brake over the domain they collateralize and protect. + +The protection track is not a second positive-governance lane: + +- `Veto` can cancel under the Governance specification's immediate or final protection rules. +- `Pass` permits the primary track to decide and may authorize urgent handling only where an explicit domain/payload policy allows it. +- Native protection never prices an invoice and never substitutes for `$BLDR` primary approval. + +Builder governance has no direct Root-equivalent authority over L1. A Builder-domain request for strategic action must use the Governance specification's bounded L2-to-L1 signal surface unless L1 explicitly delegates the target parameter or authority. + +## 8. Invoice Contract + +A Builder invoice selects the Governance specification's `L2TreasurySpend` payload and `Invoice` primary family. The Governance specification exclusively owns that family's choices, scalars, tie handling, protection interaction, and resolution order. + +For Builder composition, a successful positive result determines one scalar target from the submitted base amount, while primary rejection produces no payout. Native `Veto / Pass` remains the separate protection track and never prices the work. + +Every invoice payload declares: + +- Beneficiary. +- Payout asset. +- Base amount. +- Treasury account. +- Required bounded IPFS CID for the canonical invoice document and supporting evidence. + +The payload commits the content-addressed CID, not a mutable web URL. The referenced document may contain human-readable rationale and links to GitHub or other work evidence, but changing that document changes its CID and therefore requires a different proposal payload. IPFS availability remains an external materialization concern rather than canonical on-chain storage. + +The treasury account is explicit but not arbitrary. The runtime composition must resolve it to a governance-approved sovereign account of a Mutable System Actor in the same Builder domain. Admission rejects an ordinary user account, an Immutable Actor such as BLDR Anchor, an unregistered Actor, or a treasury belonging to another governance domain. + +BLDR Treasury is a Mutable System Actor treasury. It may hold an independently governed Actor Contract that spends bounded portions through ordinary economic flows. Invoice settlement is a separate governance-authorized debit of the same sovereign account: it requires no Actor-owner signature or Actor Step and does not mutate, pause, replace, or bypass the treasury's Actor Contract. + +The Builder domain selects the Governance specification's `BaseFloorCapped` invoice-settlement policy. That specification exclusively owns target calculation, minimum payable capacity, bounded clipping, atomic transfer, failure, and receipt semantics. + +For a scalar target above `1.0x`, enactment may pay less than the target only when the treasury can still cover at least the complete submitted base amount. Thus an `Amplify` target of `2.0x` may settle at `1.5x`, while capacity below `1.0x` fails with zero payout. Targets at or below `1.0x` require their complete target and never clip further. + +Treasury capacity is evaluated from authoritative state at enactment because the Mutable Treasury Actor or an earlier invoice may spend between proposal submission, voting, and execution. Opening an invoice creates no balance reservation. The proposer must inspect treasury capacity and accept that concurrent spending can cause settlement failure or reduce only an above-base premium within the range authorized by the selected Governance policy. + +Builder clients must distinguish target from actual payout and primary approval from successful enactment. Before execution they must not present either the target or current treasury capacity as a guaranteed payout. + +## 9. Economic and Governance Invariants + +A conforming Builder realization preserves all of these invariants: + +- `Mint conservation`: recipient, anchor-directed, and direct-treasury `$BLDR` allocations conserve full issuance after deterministic rounding. +- `Collateral direction`: all `$BLDR` TMC `$NTVE` collateral remains assigned to the anchor-liquidity lane. +- `Anchor custody`: every protocol-created `$NTVE/$BLDR` LP token belongs to the sealed Immutable BLDR Anchor; treasury payout and every ledger debit path observe zero reducible Anchor LP balance. +- `Floor honesty`: only live LP reserve claims count as anchor support. +- `Bridge conservation`: every released parent reserve asset is consumed by a declared route or remains visibly attributed to Treasury B. +- `Acquisition conservation`: recipient `$BLDR` output conserves exactly across burn and recycled treasury destinations. +- `Route honesty`: market and TMC acquisition branches report distinct issuance effects. +- `Track separation`: `$BLDR` primary evaluation and Native protection retain independent ballots, power surfaces, and meanings. +- `Treasury authorization`: the payload-selected account resolves to an approved Mutable System Actor treasury in the Builder domain and never to BLDR Anchor or an arbitrary account. +- `Invoice settlement`: the Builder domain uses the Governance specification's `BaseFloorCapped` policy; above-base premium may clip to spendable capacity, while capacity below the complete base floor produces explicit execution failure and zero payout. +- `Invoice atomicity`: the one Governance-computed actual amount transfers completely or remains absent; no Builder adapter independently clips, prorates, reconstructs, or weakens that result. +- `Actor independence`: governance debit changes treasury custody only and does not mutate the treasury Actor Contract or lifecycle. +- `Authority containment`: Builder governance receives domain-local authority over declared treasury custody but cannot synthesize L1 or general Root-equivalent authority. +- `Boundedness`: invoice CID, proposal, ballot, execution, retry, projection, and retained-history surfaces obey their owning subsystem bounds. + +## 10. Read and Evidence Surfaces + +Canonical on-chain Builder truth should expose the bounded current state needed to verify: + +- `$BLDR` curve and issuance state. +- BLDR Anchor account, LP ownership, and live reserve claim. +- BLDR Treasury account and spendable balances. +- Active and recent Builder governance lifecycle. +- Primary and protection track identity, power profiles, ballots, and tallies. +- Invoice CID, treasury account, base amount, target amount, actual amount, capacity-limited status, outcome, enactment or execution-failure status, and successful execution receipt. +- Current burn and capital-bridge liveness or explicit unavailability where the runtime supports those plans. + +Permanent invoice archives, contributor histories, searchable work records, and long-range governance analytics are materialized views. Product clients must not present those surfaces as direct canonical-chain history. + +Conformance evidence is distributed by truth owner: + +- Simulator tests prove the inherited project-independent TMCTOL curve, reserve, conservation, floor, and compression mathematics; the Builder-specific allocation and bridge equations remain analytical contract claims until a dedicated executable owner is introduced. +- Governance pallet and runtime tests prove shipped track semantics, invoice resolution, transactional payout, authority, and bounded lifecycle; the architecture document identifies the remaining invoice-settlement gap from this contract. +- Runtime integration tests prove concrete accounts, asset routing, System Actor topology, and cross-pallet composition. +- Client tests prove provenance labels, current-state projection, transaction feedback, and explicit materialized dependencies. + +## 11. Framework and Instance Boundary + +DEOS owns the reusable Builder mechanism and the reference composition described here. A downstream instance decides whether to enable it and owns: + +- Builder-domain name and token symbol. +- Accepted work and evidence norms. +- Invoice etiquette, review culture, and payout appetite. +- Founder, team, and contributor allocation policy. +- Demand generation, product loops, and market narrative. +- Any parameter changes that remain inside declared governance and economic bounds. + +A fork may replace `$BLDR`, invoices, or the entire Builder domain without violating DEOS. It must stop claiming this contract when its replacement no longer preserves the stated topology and invariants. + +## 12. Non-Goals and Non-Guarantees + +This contract does not define: + +- The general TMCTOL standard or its project-independent mathematics. +- The internal Governance pallet lifecycle, tally algorithms, or storage topology. +- A universal labor philosophy or objective measure of useful work. +- A founder entitlement, payroll promise, or mandatory contributor structure. +- Guaranteed demand, appreciation, liquidity, invoice quality, or governance wisdom. +- Unbounded on-chain work records, reputation history, or invoice archives. +- Automatic activation of optional capital-bridge plans. + +The Builder economy is a bounded reference mechanism for funding useful work. Its economic outcomes remain conditional on utility, participation, treasury funding, market liquidity, correct execution, and honest governance. diff --git a/docs/core.architecture.en.md b/docs/core.architecture.en.md index b45cba47..ce850f3b 100644 --- a/docs/core.architecture.en.md +++ b/docs/core.architecture.en.md @@ -112,7 +112,7 @@ _The intellectual layer atop raw liquidity._ - `Market Liquidity`: Direct XYK swaps. - `Protocol Liquidity`: Direct minting via TMC when that path delivers more output. - `Complex Paths`: Multi-hop Native-anchored XYK routes. -- `Multi-Curve Routing`: For any pair where `has_curve(to) && supports_collateral(to, from)`, the Router considers TMC as a candidate route. This generalizes beyond Native-only minting to support BLDR and future protocol tokens. +- `Multi-Curve Routing`: For any pair where `has_curve(to) && supports_collateral(to, from)`, the Router considers TMC as a candidate route. This generalizes beyond Native-only minting to support `$BLDR` and future protocol tokens. - `Security mitigations (not complete MEV defense)`: Every actual XYK leg validates its prepared directional quote against the current reference, then publishes the leg's pre-execution reserve ratio immediately before market execution. Authored exact-input/output bounds remain mandatory. This launch line has no commit/reveal or ordering protection — do not claim flash-loan or sandwich immunity. - `Execution`: Uses `Balance-Delta Verification` — measures the physical change in the recipient's balance rather than relying on theoretical quotes. @@ -124,7 +124,7 @@ _The algorithmic issuer._ - `Multi-Curve Architecture`: Each curve is identified by its `minted_asset` (formerly `native_asset`). The system supports multiple concurrent curves: - L1: `minted_asset = Native`, `foreign_asset = Foreign` (e.g., USDC) - L2: `minted_asset = Local(BLDR_ASSET_ID)`, `foreign_asset = Native` -- `MintOutputResolver`: Maps each `minted_asset` to explicit collateral and minted-liquidity output accounts. The reference L1 route sends both outputs to the Liquidity Actor; the BLDR route sends NTVE collateral directly to the BLDR Liquidity Actor and the minted BLDR share to the BLDR Splitter. +- `MintOutputResolver`: Maps each `minted_asset` to explicit collateral and minted-liquidity output accounts. The reference L1 route sends both outputs to the Liquidity Actor; the `$BLDR` route sends `$NTVE` collateral directly to the BLDR Liquidity Actor and the minted `$BLDR` share to the BLDR Splitter. - `Role`: Sets the "Hard Ceiling" on price. If market price > curve price, the Router automatically routes trades through TMC, creating arbitrage that feeds the protocol. #### System Actors Composition @@ -241,7 +241,7 @@ Permissionless collators and a stronger randomness contract remain gated on a pr The interaction of actors creates a conditionally bounded economy: -- `Ceiling`: Enforced by TMC. Multiple curves such as Native and BLDR each define independent ceilings. +- `Ceiling`: Enforced by TMC. Multiple curves such as Native and `$BLDR` each define independent ceilings. - `Floor`: Reported from TOL bucket reserves that qualify under the TMCTOL floor metric and bucket-accounting rules. The concrete custody accounts and active-policy lanes belong to the Actors integration map. - `Compression`: Burn Actor execution reduces live supply and liquidity-actor LP provisioning can strengthen counted reserves. Bidirectional compression holds only under the named preconditions: protected counted reserves, explicit sellable-pressure assumptions, live burn execution, and healthy liquidity accounting. @@ -252,18 +252,27 @@ The interaction of actors creates a conditionally bounded economy: - `Mechanism`: High base fee (e.g., 0.5%) + Protocol Priority Routing. - `Outcome`: System value capture is prioritized over LP revenue. The protocol captures the spread to burn its own supply. -### 6.3 Multi-Token Flywheel +### 6.3 Second-Order `$BLDR` TMCTOL and Capital Loop -The BLDR economy creates a self-reinforcing loop: +The [Builder Economy Contract](./builder-economy.contract.en.md) owns the normative composition of second-order TMCTOL and Builder governance; this section maps that contract into the current DEOS architecture. TOL is an asset-scoped treasury-owned liquidity topology, not a fixed four-bucket shape. The TOL component of reference first-order Native/foreign TMCTOL uses Bucket Anchor (`A`), Bucket Builder (`B`), Bucket Capital (`C`), and Bucket Dormant (`D`), with paired treasuries for B/C/D. The TOL component of second-order `$BLDR` TMCTOL has no lettered bucket family. Its bucket has reusable Anchor type and the human name BLDR Anchor; BLDR Treasury is a separate multi-asset treasury owner rather than another bucket. Their sovereign accounts hold consensus-frozen LP and spendable invoice custody respectively; the former belongs to a sealed dormant Immutable identity, while the latter belongs to a dormant Mutable treasury identity. +```text +User acquires $BLDR through Router + → TMC branch: about 1/3 recipient + 1/3 immutable anchor lane + 1/3 direct BLDR Treasury + → XYK branch: existing $BLDR moves from market reserves to recipient + +Parent Bucket B releases LP gradually + → Treasury B receives $NTVE + foreign reserves + → both assets route into $BLDR + → half of recipient $BLDR burns + → half enters BLDR Treasury ``` -User buys BLDR (via Router TMC) - → NTVE collateral → BLDR Liquidity Actor → LP → BLDR Bucket A (floor ↑) - → BLDR liquidity share → 50% to Liquidity Actor (more LP), 50% to Treasury (ecosystem fund) - → Optional Bucket B LP transfer → Treasury B LP removal → buyback BLDR on XYK → burn (downstream policy target) -``` -BLDR floor support and ceiling pressure can compress over time when LP accumulation remains counted as support and buyback-burn execution remains live. +All `$NTVE` collateral received by the `$BLDR` TMC is directed to the BLDR Liquidity Actor, and all resulting LP is assigned to BLDR Anchor. The shared first-/second-order anchor invariant is approximately one third of total issuance directed to immutable protocol-owned liquidity. Collateral is deliberately asymmetric: first-order Bucket A receives half under the reference four-bucket allocation, while BLDR Anchor receives all Native collateral. + +Route effects remain distinct. An XYK acquisition is issuance-contracting by the burned half. A TMC acquisition creates full curve issuance, sends its protocol allocation through the existing BLDR Splitter, then burns only half of the recipient output; it can remain net issuance-positive while strengthening anchor liquidity and BLDR Treasury. + +The shipped runtime currently realizes the `$BLDR` TMC, BLDR Splitter, and anchor/treasury path and an optional Treasury B plan that spends only Native and burns all acquired `$BLDR`. The Builder Economy contract defines the target dual-reserve 50/50 capital bridge analytically; the simulator does not model that Builder composition. Runtime composition, liveness projection, and production evidence remain open until the plan consumes both Treasury B reserve assets and separates recipient burn from recycled treasury funding. ## 7. Runtime Topology @@ -308,7 +317,7 @@ By separating dedicated economic mechanisms from production-admitted `pallet-deo 1. `Bounded trade safety`: Direct routes combine slippage with pre-swap EMA deviation guards; multi-hop and full MEV/sandwich resistance are out of scope for this launch line (see DEOS Router architecture). 2. `Composable Automation`: bounded Actors tasks and runtime adapters compose reconfigurable flows only when the resulting plan fits production admission and preserves custody/atomicity. New actor graphs avoid runtime code changes when existing primitives satisfy that contract; new mechanics require an admitted adapter or core-task review. -3. `Multi-Token Economy`: L1 (Native) and L2 (BLDR) economies operate independently with shared infrastructure. Each has its own TMC curve, liquidity pools, liquidity actor, and TOL buckets. +3. `Multi-Token Economy`: L1 (Native) and L2 (`$BLDR`) economies operate independently with shared infrastructure. Each has its own TMC curve, liquidity pools, liquidity actor, and TOL buckets. 4. `Bounded Background Execution`: Actors uses budget-capped `on_idle`, bounded `on_initialize` bookkeeping, and durable carry-over rather than claiming zero congestion under every workload. 5. `Conditional Automation`: Activated, funded actors can burn, provision liquidity, or route treasury value when adapters, pools, triggers, and safety conditions remain healthy; dormant or unadmitted policy targets do not produce effects. 6. `Focused Ownership`: Dedicated pallets own minting, routing, staking, assets, and AMM mechanics while Actors owns bounded orchestration, avoiding duplicated manager loops without turning the actor kernel into a universal VM. diff --git a/docs/framework-instance.contract.en.md b/docs/framework-instance.contract.en.md index 3d1b54d3..d4dffd53 100644 --- a/docs/framework-instance.contract.en.md +++ b/docs/framework-instance.contract.en.md @@ -63,46 +63,15 @@ The framework may ship reference defaults and examples. Those defaults help a fo ## 5. Builder Pattern Boundary -The builder pattern is an optional governance-mediated labor-funding primitive. +The builder pattern is an optional governance-mediated labor-funding primitive. [`builder-economy.contract.en.md`](./builder-economy.contract.en.md) owns the complete DEOS reference composition across second-order TMCTOL, `$BLDR` governance, invoices, BLDR Anchor, BLDR Treasury, and the parent-capital bridge. -Generic mechanism: +DEOS owns safe reusable invoice, governance, treasury, liquidity, and execution mechanisms. An instance decides whether to enable the pattern and owns work norms, invoice etiquette, contributor culture, payout appetite, naming, and demand strategy. -1. Useful completed work is represented as a public invoice payload. -2. Governance evaluates the invoice under explicit domain rules. -3. Governance may approve, reject, reduce, or amplify payout. -4. Execution pays from a declared funding source under bounded caps and transactional rules. +## 6. TOL Capital Boundary -Framework responsibility: +TOL is an asset-scoped treasury-owned liquidity topology, not a synonym for one fixed bucket count. The TMCTOL specification owns the project-independent standard mathematics and reference first-order model; the Builder Economy contract owns the concrete second-order `$BLDR` specialization. -- Define safe invoice payloads, lifecycle, execution, observability, caps, and failure semantics. -- Keep invoice voting bounded and auditable. -- Avoid hardcoding one instance's contributor culture into generic pallets. - -Instance responsibility: - -- Decide whether builder invoices are used at all. -- Define accepted work norms, individual/team invoice expectations, social review culture, and payout appetite. -- Decide whether contributor funding uses a builder token, a treasury asset, another domain token, or no invoice system. - -## 6. Bucketed Capital Boundary - -TMCTOL's reference buckets are a canonical standard-level topology, not a requirement that every DEOS fork must preserve identical names or percentages. - -Reusable pattern: - -- Protocol capital is segmented into explicit roles. -- Each segment has a clear policy boundary. -- Governance authority over each segment is bounded and named. -- Automation moves capital only through declared Actor Contracts. - -Reference TMCTOL semantics: - -- Bucket A: anchor / protection / floor-support gravity. -- Bucket B: building / contributor-development funding. -- Bucket C: capital / operations. -- Bucket D: dormant reserve or future demand/expansion policy. - -A fork may rename, resize, merge, remove, or add buckets if it updates the claimed standard and validation surface honestly. +Reusable framework requirements remain limited to explicit capital roles, bounded governance authority, protected custody, honest floor accounting, and declared execution paths. A fork may rename, resize, merge, remove, or add anchors, buckets, and treasuries if it updates its claimed contract and validation surface honestly. ## 7. Actors Extraction Rule diff --git a/docs/read-model.contract.en.md b/docs/read-model.contract.en.md index 7cde67cd..ac55a3bb 100644 --- a/docs/read-model.contract.en.md +++ b/docs/read-model.contract.en.md @@ -77,10 +77,10 @@ Each subsystem below uses the same two buckets: #### On-Chain Canonical Projection -- Current System Immutable Bucket A anchor status for TOL and BLDR domains -- Current anchor-pool existence, reserves, LP issuance, and Bucket A LP balance +- Current sealed Immutable System Actor status for first-order Bucket A and BLDR Anchor +- Current anchor-pool existence, reserves, LP issuance, and owning anchor LP balance - Bounded live inputs needed to recompute the reported floor metric -- Separate live burn-liveness status for framework native burn and reference BLDR buyback/burn paths +- Separate live burn-liveness status for framework native burn and reference `$BLDR` buyback/burn paths - Current Zap postcondition status for add-liquidity, foreign-to-native swap, and LP bucket-split configuration - Explicit live statuses for guarantee classes, including `NotInitialized` and `NotGuaranteed` diff --git a/docs/tmctol.specification.en.md b/docs/tmctol.specification.en.md index 12116797..9c5c3559 100644 --- a/docs/tmctol.specification.en.md +++ b/docs/tmctol.specification.en.md @@ -2,7 +2,7 @@ ## Abstract -TMCTOL (Token Minting Curve + Treasury-Owned Liquidity) is a tokenomic standard for DEOS, establishing mathematically defined price boundaries through treasury-controlled liquidity accumulation. The system combines unidirectional token emission with automated XYK reserve allocation to create calculable downside protection ranging from 11% to 25% of equilibrium price, contingent on governance maintaining specified system parameters. +TMCTOL (Token Minting Curve + Treasury-Owned Liquidity) is a project-independent tokenomic standard establishing mathematically defined price boundaries through treasury-controlled liquidity accumulation. The system combines unidirectional token emission with automated XYK reserve allocation to create calculable downside protection ranging from 11% to 25% of equilibrium price, contingent on governance maintaining specified system parameters. `Key Properties`: @@ -30,14 +30,14 @@ TMCTOL (Token Minting Curve + Treasury-Owned Liquidity) is a tokenomic standard - `TBC`: Bidirectional mint/redeem curve model, typically symmetric in formula-space (reserve-extraction path exists) - `TMC`: Unidirectional mint-only curve model, inherently asymmetric in market structure (reserve-extraction path does not exist) - `POL`: Protocol-Owned Liquidity — ownership class where LP inventory is held by protocol accounts; permanence depends on policy (hard-locked or governance-withdrawable) -- `TOL`: Treasury-Owned Liquidity — treasury policy framework over protocol-owned liquidity with explicit bucket roles; in TMCTOL, Bucket_A is the anchor permanence layer while B/C/D are policy-flex buckets +- `TOL`: Treasury-Owned Liquidity — treasury policy framework over protocol-owned liquidity with explicit reserve roles; a realization may use one or several accounting positions while preserving declared protection and reporting rules `Combination Profiles`: - `TBC + POL`: Possible only with an explicit accumulation source such as fees, spread, surplus, or seed capital. Otherwise the redeem flow tends to consume reserves and POL cannot grow sustainably - `TBC + TOL`: Adds treasury discipline and segmentation, but still requires dedicated LP inflow into treasury logic. The redeem path continues to create extraction pressure - `TMC + POL`: Naturally supports reserve accumulation when mint-side allocations route into protocol LP. Floor quality then depends on treasury policy strictness -- `TMC + TOL`: Combines unidirectional minting with explicit treasury floor policy across Anchor, Building, Capital, and Dormant buckets. This maximizes floor hardness and governance clarity +- `TMC + TOL`: Combines unidirectional minting with explicit treasury floor policy. A realization declares its own custody topology without changing the underlying curve, reserve, and conservation mathematics `Relationship note`: TOL is a structured policy layer over POL. All TOL LP is POL by ownership, but not every POL design is TOL. @@ -140,12 +140,12 @@ Distribution occurs atomically within minting transaction; no newly minted token `Bucket Structure (66.6% total allocation)`: -- `Bucket_A (33.3% total supply)`: Anchor liquidity — primary floor protection mechanism. Bucket_A is protected floor capital while it remains in an anchor-qualified state under Section 6.2. If Bucket_A exceeds its required anchor target due to supply compression or strategic accumulation, governance may migrate only the excess under the migration rules below -- `Bucket_B (11.1% total supply)`: Building budget — ecosystem construction spending through governed LP unwind, buyback, treasury, or deployment actions -- `Bucket_C (11.1% total supply)`: Capital bucket — operational liquidity reserve for controlled redeployment between LP positions and treasury balances -- `Bucket_D (11.1% total supply)`: Dormant LP reserve — governance-controlled LP held in a passive or delayed-use state until a later policy activates it +- `Bucket_A / Anchor (33.3% total supply)`: Immutable anchor liquidity — primary floor protection mechanism. Bucket_A is protected floor capital under Section 6.2 +- `Bucket_B / Builder (11.1% total supply)`: Ecosystem-construction budget for governed LP unwind, buyback, treasury, or deployment actions +- `Bucket_C / Capital (11.1% total supply)`: Operational liquidity reserve for controlled redeployment between LP positions and treasury balances +- `Bucket_D / Dormant (11.1% total supply)`: Sleeping LP reserve for later demand, distribution, or marketing policy activated through explicit governance -`Allocation vs. Circulating Share`: Initial bucket allocations represent fixed percentages of total supply at minting. However, circulating shares (percentage of current supply held by each bucket) evolve dynamically through token burning and strategic reallocations. This distinction enables the system to maintain floor protection guarantees while allowing treasury expansion. +`Allocation vs. Circulating Share`: Initial bucket allocations represent fixed percentages of total supply at minting. Circulating shares evolve through token burning and non-Anchor strategic reallocations, while Anchor custody remains immutable. This distinction permits treasury activity without converting protected Anchor capital into a spending reserve. `Capital Efficiency`: Four independent bucket positions target near-full reserve deployment rather than idle treasury custody. This does not imply one deep pool or identical user slippage across buckets. Any realization that advertises aggregate utilization MUST also report whether floor depth is concentrated, fragmented, migrated, or externally deployed. @@ -154,9 +154,9 @@ Distribution occurs atomically within minting transaction; no newly minted token - `Maximum model case (25%)`: All TOL buckets are in floor-supporting reserve state under the same stress metric - `Minimum model case (11%)`: Only Bucket_A is in floor-supporting reserve state and other buckets are treated as sellable pressure or non-supporting deployment - `Typical live state`: Governance balances protection, development, capital use, and external deployment; clients MUST report the current bucket-state classification rather than assuming either endpoint -- `Excess Liquidity Migration`: Token burning increases Bucket_A's relative share of circulating supply. Governance MAY migrate only Bucket_A liquidity above the required anchor target, and the migrated portion MUST stop counting toward in-domain floor support unless it satisfies the external LP reporting rules in Section 6.2 +- `Anchor immutability`: Token burning may increase Bucket_A's relative share of circulating supply, but no resulting excess becomes withdrawable or migratable under ordinary governance; changing this guarantee requires a fork or explicit standard revision -### 2.5 DEOS Router Mechanism +### 2.5 Router Mechanism `Price Discovery Gateway`: @@ -293,6 +293,8 @@ A conforming report MUST state: - `Sellable pressure`: the chosen `λ_reported` assumption and rationale - `Governance state`: intact, degraded, emergency, or forked guarantee surface +Counted reserves MUST derive from each support position's current pro-rata reserve claim. Historical contribution or cost-basis telemetry MUST NOT be added to the same live pool reserves. Missing live Bucket_A anchor support automatically degrades the governance state, and a named pressure preset MUST derive from the configured allocation shares rather than hardcoded default percentages. + `Reference-state assumption for ratio estimates`: To express scenario estimates using only `s` and `a`, we normalize at a parity reference state: @@ -603,7 +605,7 @@ where burn_rate = f_router × V_trade `Flexibility`: Bucket independence enables: -- `Bucket_A`: Dedicated baseline floor support target; governance policy should treat it as protected capital and minimize withdrawal paths +- `Bucket_A / Anchor`: Immutable baseline floor support with no ordinary governance withdrawal path - `Buckets 2-4`: Strategic deployment per governance decisions - Effective floor ranges 11% minimum (only Bucket_A) to 25% maximum (all buckets) based on deployment choices @@ -691,7 +693,7 @@ Narrower range → Reduced volatility → Increased confidence → Adoption `Maturing Growth Phase`: -- Governance deploys TOL strategically (parachain expansion, development funding) +- Governance deploys TOL strategically (external expansion, development funding) - Framework flexibility enables growth without sacrificing floor protection - Burn effects create deflationary pressure @@ -759,7 +761,7 @@ Narrower range → Reduced volatility → Increased confidence → Adoption - `XYK fee`: bounded-mutable by governance only when the floor and best-execution impact is disclosed - `Burn threshold and cooldown`: bounded-mutable operational parameters; changes MUST preserve the burn liveness contract - `Zap slippage bounds`: bounded-mutable operational parameters; changes MUST preserve the Zap postconditions -- `Bucket migration policy`: governance-mutable only for non-anchor or anchor-excess liquidity; protected anchor liquidity follows the TOL Anchor Invariant +- `Bucket migration policy`: governance-mutable only for non-Anchor buckets; Anchor liquidity follows the immutable TOL Anchor Invariant - `Emergency controls`: unavailable by default unless a realization defines scope, duration, authority, veto/timelock, and reporting semantics ### 6.2 Critical Invariants @@ -786,9 +788,9 @@ Bucket_A counted as protected anchor support MUST satisfy all conditions below: - It is held in protocol-owned or treasury-owned accounts governed by the default protected governance surface - It is deployed in an in-domain XYK pool or another explicitly qualifying floor-supporting reserve position -- It is held by a System Immutable Actors or equivalent hard protocol anchor that runtime extrinsics, including governance/root, cannot mutate, pause, close, or reopen +- It is held by an immutable protocol anchor that ordinary or privileged implementation calls cannot mutate, pause, close, or reopen - It cannot be withdrawn, migrated, spent, or reclassified by unilateral admin authority -- Any migration is limited to anchor-excess liquidity above the required anchor target unless a runtime upgrade, fork, or explicit standard revision degrades the guarantee surface +- No burn-created or strategically accumulated Anchor excess becomes withdrawable; only a fork or explicit standard revision may replace this guarantee surface - Any emergency path that can bypass this invariant MUST classify the floor state as emergency or degraded before users can rely on the former reported floor `Bucket and LP Accounting`: @@ -848,7 +850,7 @@ Current required coverage: - `Elasticity inversion`: analytical unless the simulator/runtime test suite explicitly covers the selected formula and parameters - `Relative compression, absolute-gap compression, and overtake`: analytical until each regime has dedicated conformance vectors -A conforming reference runtime SHOULD expose a bounded live guarantee-state projection for inspection. This projection MUST NOT add dashboard/history storage; it MAY report uninitialized or explicitly non-guaranteed classes separately from violations. Native burn liveness and BLDR buyback/burn liveness MUST be reported as separate domains when both exist. Zap postcondition inspection MUST verify the configured liquidity-add, residual swap, and LP bucket split as one coherent plan before reporting the Zap domain as satisfied. +A conforming implementation SHOULD expose a bounded live guarantee-state projection for inspection. This projection MUST NOT add unbounded history storage; it MAY report uninitialized or explicitly non-guaranteed classes separately from violations. Distinct burn domains MUST be reported independently when a realization defines more than one. Zap postcondition inspection MUST verify the configured liquidity-add, residual swap, and LP bucket split as one coherent plan before reporting the Zap domain as satisfied. ### 6.4 Economic Conditions diff --git a/scripts/README.md b/scripts/README.md index 4cc18649..e07471a7 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -2,7 +2,7 @@ This directory is the deterministic operator/developer automation layer of the DEOS reference stack. It supports testing, validation, benchmarking, generation, build, deployment preparation, local-network operation, release work, and repeatable coordination across `/docs`, `/template`, and `/web-client`; it is not the primary conceptual control plane. -The `0.7.22` release accepts only a fresh-genesis candidate. Local full validation does not certify an upgrade from `0.7.21`, a deployed storage lineage, or a network launch; those remain outside the pre-`1.0` release boundary. +The pre-`1.0` release line accepts only fresh-genesis candidates. Local full validation does not certify an upgrade from an earlier release, a deployed storage lineage, or a network launch. This directory contains deterministic command surfaces, not agent strategy: @@ -82,7 +82,11 @@ Each numbered command is independently callable by a human or CI from any workin 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. 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. + Shared current-tree Actors proof contract for semantic-manifest and fee-envelope-vector freshness, cross-language semantics, scheduler stress, capacity, and independent-runtime embedding. `--exact-heavy-profile NAME` runs one profile from the gate's declared runtime/package heavy inventory, verifies that it resolves exactly once, and preserves the same source/artifact identity boundary without replaying unrelated heavy predecessors. `--production-reference-replay` separately runs the one ignored full-block StorageProof replay against the currently pinned production Wasm rather than allowing an ordinary filtered test command to report a skipped test as evidence. By default the broad gate 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. + + `--integrated-w0-w1` selects EXP-0066's bounded preparation gate instead of the broad suite. It authors complete W0 empty/one-Step blocks and a 128-Actor W1 falsifier through runtime-created inherents, full Executive hooks and finalization under Actor-only and continuously saturated valid user demand, then independently replays the accepted production Wasm with recorded StorageProof and CompactProof. This proves the measurement path and identity binding, not the 10,000-Actor target. `--integrated-w1-actor-only` and `--integrated-w1-continuous-user` preserve the reference preset's 15 System Actor identities (three active and twelve dormant), fill the remaining identity capacity with 9,985 mixed Manual/Cadenced workload Actors, author 100 linked finalized blocks, and exact-Wasm proof-replay every block against the 10,000-Cycle and 100-distinct-Actors-per-block target. The continuous-user cohort also fills every User base turn with valid signed remarks and carries the signer nonce monotonically across blocks. `--integrated-w2-schedules` runs separate Actor-only Manual-only and Cadenced-only populations through the same 100 linked full-Executive blocks and exact-Wasm proof contract, isolating execution from temporal scheduling cost without changing the accepted binding. `--integrated-w3-opening-matrix` runs 14 equal repetitions of every 0/2/4 Opening-predicate by 1/2/3-Step Manual-only cell through linked full-Executive blocks until all 126 Actors and 252 Steps complete, reporting Opening, strict-middle and final Steps separately and exact-Wasm proof-replaying every retained block. `--integrated-w4-heterogeneous-effects` preserves the reference preset's 15 identities and fills the remaining production capacity with a deterministic interleaving of 9,485 Transfer, 400 Router SwapOut, and 100 StopCycle Actors (the nominal 9,500 Transfer slots less the 15 retained production identities), then measures 100 linked blocks under both Actor-only and continuously saturated valid-user demand with exact-Wasm proof replay. `--integrated-w5-lifecycle-retry-cleanup` runs one five-Actor Manual matrix through zero-Step completion, successful Opening/middle/final Running, temporary retry and exhaustion, productive cleanup, minimal admission apoptosis, and uncompensated committed-prefix durability across linked native and exact-production-Wasm blocks. `--integrated-w6-mixed-arrival-lifecycle` runs deterministic dense Manual, sparse block-window/AtTime, and seeded Cadenced arrivals across both scheduler clocks while signed pause/resume, owner close, Oracle publication and Crossing Contract replacement produce and safely consume normal tombstone and stale-generation churn; all retained blocks use the same native/full-Executive and exact-Wasm proof contract. `--integrated-w7-due-only-active-frontier` compares a 100-due-Actor control against the same FIFO frontier at the exact 10,000-identity production cap. The maximum profile keeps 4,943 workload Actors behind one future block-clock window and 4,942 Manual Actors unsignaled; together with the 15 retained reference identities they form the declared 9,900 non-due population, while both profiles must complete the same 100 due Actors through native and exact-Wasm blocks. `--integrated-w8-tombstone-prefix-chunk-pressure` places legal closed-Actor tombstone prefixes of 1/4/8/16/32/64/128 physical Ready entries ahead of 100 due Manual Actors, then measures one complete block per profile. Every profile must reclaim the exact tombstone prefix, preserve the live FIFO/Q1 committed prefix, and stop pending live work only at the Actor Control frontier without comparing alternative page layouts. `--integrated-w9-resource-independence` compares one identical 100-due-Actor FIFO frontier under Actor-only, proof-saturated signed remarks, and RefTime-heavy signed Router swaps. It requires identical Actor progress and accounting, then records the honest fallback when the highest retained valid business-call profile still exhausts User ProofSize before RefTime. `--integrated-control-attribution` compares matched 100-Actor Manual and cadence-one fixtures for nine linked blocks, records mandatory-Prepass and finalized Actor Control separately, and proves temporal occurrence materialization is Prepass-owned while the captured FIFO cutoff defers newly materialized service. The same gate pins the generated production-Weight read/write ledger for fixed coordination, dual-clock probes, retained/removed wakeup consumption, the Cadenced rearm-topology delta, and reserved close/fault contingencies; these additive admission envelopes remain distinct from actual complete-block proof. + + Its built-in `self-test` mode checks source-content identity in temporary Git fixtures: committed, staged, unstaged, untracked, deleted and executable-mode changes, stable commit packaging, artifact exclusions and fail-closed symlink handling. The assurance gate runs the same self-test automatically; standalone self-test needs no Cargo, network access or build artifacts. - [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. @@ -90,17 +94,33 @@ Each numbered command is independently callable by a human or CI from any workin - [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. -Project-local audit leaves and targeted routes are documented in `/.agents/skills/alignment/SKILL.md`. Use the diff-aware completion gate for changed-scope work and `./scripts/validate-local.sh fast|heavy|full` only for the corresponding release-validation boundary. +Use the owning tool's focused checks for changed-scope work and `./scripts/validate-local.sh fast|heavy|full` for the corresponding project-validation boundary. These commands do not require agent skills; any agent-specific audits are optional additional review. -Before creating a release tag, run `./scripts/validate-local.sh full` against the intended commit. After that commit is accepted as local `main` and tagged, run `./.agents/skills/alignment/scripts/audit-release-line.sh --validated-release-commit `; the audit requires `main`, `vX.Y.Z`, and the explicitly supplied validated commit/tree to be identical and rejects a parallel plain version ref. It records no hidden validation state and does not discover branch or GitHub authority. +Before creating a release tag, run `./scripts/validate-local.sh full` against the intended commit. After that commit is accepted as `main` and tagged, verify with Git that `main`, the peeled `vX.Y.Z` tag, and the validated commit/tree are identical and that no parallel plain version tag exists. Release verification must not depend on an agent skill, hidden validation state, or inferred GitHub authority. 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. `--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. + 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. Add `--json-file FILE` to an exact extrinsic run to preserve raw component samples alongside its generated model; this requires frame-omni-bencher. Unbound User zero-Step and empty/large-head Pipeline collection profiles require exact `--extrinsic` selection and remain excluded from default production generation. 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. + + Use paired `--low VALUES --high VALUES` for selected component ranges, with comma-separated unsigned values in the benchmark's component order. Bounds require an exact extrinsic, explicit output file and frame-omni-bencher; the bencher validates ranges and samples each axis with other components at their upper bounds. This does not exhaust a Cartesian grid. `--min-duration 0` makes quick reachability probes use only the requested repeat count; omitted duration retains the bencher default. Restricted sampling cannot establish complete production coverage. - [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. + Reproduce local CI or select one compact check with `--only`; narrow Cargo work further with `--package NAME`, `--test-filter NAME`, and explicit feature mode. Use `--only tests --release` for optimized large-fixture tests; other checks reject `--release`. Apply Rust formatting with `--only format --fix`. Agents should prefer this entrypoint over raw Cargo commands. + +The native schedule-accounting test defaults to 100 added System Actors over nine Cadenced blocks. Set `DEOS_SCHEDULE_LEDGER` to `manual`, `mixed` or `cadenced` to select one full identity-bound, 100-block Actor-only profile; set `DEOS_PRODUCTION_WASM` to the accepted production Wasm file. Run `ci-local.sh --only tests --release --package deos-runtime --test-filter full_executive_schedule_ledger` with `RUST_TEST_NOCAPTURE=1 DEOS_VERBOSE=1`. The Wasm is genesis code only: the test authors and reconstructs native execution, emits code identity and block hashes for retained-evidence comparison, and does not execute Wasm or certify a new production artifact. Unset the profile (or select `cadenced-small`) for the small regression; other values are rejected. Workload and reference occurrences remain distinct. + +`DEOS_SCHEDULE_LEDGER=mixed-user` selects the same full Mixed population with continuous valid signed remarks, reports User usage and the next candidate's preflight budget deficit, and preserves signature verification and nonce progression. Fresh signing does not promise retained header identity: compare the available evidence honestly rather than treating summary equality as exact-state equality. The workload Actors remain System Actors; this mode does not cover User Actor fees. + +`actors-assurance.sh --integrated-funded-user-action` executes both the native monetary/resource witness and its ignored exact-production-Wasm replay under one pinned identity gate. It proves the selected funded User Transfer's Trigger, Pipeline and Action fee separation; it is not broader User Task or retry coverage. + +`actors-assurance.sh --backpressure-audit` executes EXP-0100's focused runtime and package falsifiers for DEX/Router, XCM, observation, funding/resource, scheduler-capacity and adapter rollback ownership. It validates existing atomicity and sole Actor retry authority without introducing a new queue, registry or production-Wasm claim. + +The W5 lifecycle test runs through `ci-local.sh --only tests --package deos-runtime --test-filter full_executive_w5_lifecycle_retry_cleanup_fixture`. It also checks charged Control/effect ownership for zero-Step, Running, retry, terminal cleanup and User admission apoptosis. Optional `DEOS_PRODUCTION_WASM` pins accepted genesis code for retained-header comparison without executing Wasm; unset it for the standalone native fixture. + +`ci-local.sh --only tests --package deos-runtime --test-filter full_executive_funded_pipeline_collection_witness` compares funded System/User StopCycle fee ledgers and reports charged Control. It verifies monetary deltas and receipts; a passing witness is not proof of complete User Weight coverage. The companion filter `full_executive_user_completion_header_domain_witness` checks ordinary admission and execution of OwnerOnly/maximum-allowlist heads sharing a source-derived completion context; it is a domain-alias witness, not a numeric Weight bound. + +The extra `scheduler_inner_opening_user_complete_single` and `scheduler_inner_opening_user_complete_tail` Actors benchmarks measure User StopCycle after real source consumption, including Pipeline collection and hold reconciliation but not outer FIFO work. Zero tails and positive tails use separate models because funding-hold release changes I/O. Native construction uses `ci-local.sh --only tests --package pallet-deos-actors --all-features --test-filter scheduler_inner_opening_user_complete_`. Reference measurement requires fresh benchmark Wasm and `benchmarks.sh --extra --extrinsic NAME --output /tmp/user-completion.rs pallet_deos_actors`; both diagnostics remain outside default production generation and do not qualify arbitrary User Contract geometry. ## Admin Utilities @@ -108,10 +128,10 @@ 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 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. + 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. The exporter initializes the canonical reference ParaId before reading storage-backed metadata constants, so its bytes match Metadata API output from the reference composition. 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. + Consolidated native staking bootstrap helper. `check` reads the mode-shaped canonical `NativeSecurityView`, pool readiness and bounded `ActorEligibilityApi` state at one finalized block. JSON `stakingLiquidityActor` contains the Active activation projection, or null for Dormant/NotRegistered; classification errors fail closed. Matching runtime metadata/descriptors are required. `prepare-calls` emits plan-only registration, pool and liquidity calls without mode-inactive LP-security actions. It never signs or submits transactions; the staking specification and package architecture own sequencing and authority boundaries. - [authorized-upgrade-local.sh](./authorized-upgrade-local.sh) Consolidated authorized runtime-upgrade helper. `check` pins one finalized block, compares live and local runtime code, inspects protocol `L1RootAction` submission authority and `$VETO` issuance, verifies the pending authorized hash, and can emit offline relay call data. `prepare-authorization` emits candidate-bound stake, preimage, and signed proposal call data only after reporting finalized item, balance, fee, and authority checks; it withholds the protection `Pass` call unless the lifecycle is ready. `apply` stays plan-only unless explicit `--submit` relays matching code bytes through `System.apply_authorized_upgrade { code }`. `snapshot` captures finalized non-empty Router/Oracle/Actors baseline state, and `verify` checks runtime identity, selected-state preservation, and live code equality with the candidate Wasm. diff --git a/scripts/actors-assurance.sh b/scripts/actors-assurance.sh index 969c51b1..ec914519 100755 --- a/scripts/actors-assurance.sh +++ b/scripts/actors-assurance.sh @@ -6,11 +6,29 @@ source "$(dirname "${BASH_SOURCE[0]}")/_common.sh" CARGO_PROFILE="${CARGO_PROFILE:-release}" INCLUDE_OCCUPANCY_PROFILE="${INCLUDE_OCCUPANCY_PROFILE:-1}" QUICK_MODE="${QUICK_MODE:-0}" +INTEGRATED_W0_W1=0 +INTEGRATED_W1_ACTOR_ONLY=0 +INTEGRATED_W1_CONTINUOUS_USER=0 +INTEGRATED_W2_SCHEDULES=0 +INTEGRATED_W3_OPENING_MATRIX=0 +INTEGRATED_W4_HETEROGENEOUS_EFFECTS=0 +INTEGRATED_W5_LIFECYCLE_RETRY_CLEANUP=0 +INTEGRATED_W6_MIXED_ARRIVAL_LIFECYCLE=0 +INTEGRATED_W7_DUE_ONLY_ACTIVE_FRONTIER=0 +INTEGRATED_W8_TOMBSTONE_PREFIX_CHUNK_PRESSURE=0 +INTEGRATED_W9_RESOURCE_INDEPENDENCE=0 +INTEGRATED_CONTROL_ATTRIBUTION=0 +INTEGRATED_FUNDED_USER_ACTION=0 +BACKPRESSURE_AUDIT=0 +EXACT_HEAVY_PROFILE="" +PRODUCTION_REFERENCE_REPLAY=0 REQUIRE_WASM_IDENTITY="${REQUIRE_WASM_IDENTITY:-1}" EVIDENCE_SOURCE_IDENTITY="" EVIDENCE_WEIGHT_IDENTITY="" EVIDENCE_WASM_IDENTITY="" EVIDENCE_METADATA_IDENTITY="" +WASM_ARTIFACT="$TEMPLATE_DIR/target/release/wbuild/deos-runtime/deos_runtime.compact.compressed.wasm" +WASM_SNAPSHOT="" REQUIRED_HEAVY_PROFILES=( "scheduler_stress_fifo_over_capacity_fairness_matrix" @@ -18,6 +36,11 @@ 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" + "control_only_10k_first_traversal_with_continuous_user_dispatch" + "transfer_10k_homogeneous_predicate_attribution" + "swapout_10k_manual_and_reactive_first_traversal" + "swapout_10k_first_traversal_with_continuous_user_dispatch" + "swapout_10k_first_traversal_with_ref_time_heavy_user_dispatch" "transfer_10k_manual_and_reactive_first_traversal" "transfer_10k_first_traversal_with_continuous_user_dispatch" "mixed_9500_transfer_400_swapout_100_control_first_traversal" @@ -34,13 +57,90 @@ DIAGNOSTIC_HEAVY_PROFILES=("profile_scheduler_wallclock_matrix") usage() { cat <<'EOF' Usage: actors-assurance.sh [OPTIONS] + actors-assurance.sh self-test Runs the DEOS Actors assurance contract across the package archive, external-consumer fixture, and deos-runtime. +Source identity hashes tracked and untracked source bytes and executable mode in +template, scripts, web-client, simulator, docs, .github, .cargo, and root build configuration. +It includes staged changes and committed content, is independent of checkout path +and commit packaging, and excludes build outputs, .papi metadata/descriptors, and Skills. +Weight, production Wasm, and metadata retain their separate artifact identities. + +self-test checks source identity in an isolated temporary Git repository, without +Cargo, network access, or build artifacts. The assurance gate also runs it automatically. + Options: --skip-occupancy-profile Skip the gating 10k occupancy profile --quick Run only fast checks (Clippy + light tests) + --exact-heavy-profile NAME Run one declared runtime/package heavy profile + --production-reference-replay + Run the current pinned production-Wasm block/proof replay + --integrated-w0-w1 Run only EXP-0066's full-Executive W0/W1 preparation gate + --integrated-w1-actor-only Run the exact-Wasm 100-block Actor-only W1 campaign + --integrated-w1-continuous-user Run the exact-Wasm 100-block continuous-user W1 campaign + --integrated-w2-schedules Run exact-Wasm 100-block Manual-only and Cadenced-only W2 campaigns + --integrated-w3-opening-matrix Run the exact-Wasm W3 Opening-predicate/mixed-length matrix + --integrated-w4-heterogeneous-effects + Run exact-Wasm Actor-only and valid-user W4 effects + --integrated-w5-lifecycle-retry-cleanup + Run native and exact-Wasm W5 lifecycle/retry/cleanup + --integrated-w6-mixed-arrival-lifecycle + Run native and exact-Wasm W6 clocks/Triggers/churn + --integrated-w7-due-only-active-frontier + Run native and exact-Wasm W7 active-frontier scaling + --integrated-w8-tombstone-prefix-chunk-pressure + Run native and exact-Wasm W8 queue-prefix pressure + --integrated-w9-resource-independence + Run native and exact-Wasm W9 User-resource isolation + --integrated-control-attribution + Run native and exact-Wasm schedule-Control phase attribution + --integrated-funded-user-action + Run native and exact-Wasm funded User Action fee evidence + --backpressure-audit Run EXP-0100 existing-path atomicity/retry falsifiers +The preparation gate authors real ordered inherents, finalizes actor-only and +continuous-valid-user-demand blocks, then independently replays the exact production Wasm. +The W1 campaign preserves the reference preset's 15 System Actor identities (three +active and twelve dormant), fills remaining identity capacity with 9,985 mixed +Manual/Cadenced workload Actors, and proof-replays 100 linked finalized blocks +against the 10,000-Cycle target. The continuous-user campaign additionally fills +each User base turn with valid signed remarks and proves the next call is inadmissible. +The W2 schedule campaign separately measures Manual-only and Cadenced-only +actor-demand cohorts against the same target and exact production binding. +The W3 campaign runs equal 0/2/4 Opening-predicate by 1/2/3-Step Manual-only +cells to completion and reports Opening, middle, and final Steps separately. +The W4 campaign preserves all 15 reference identities, fills the remaining slots +with 9,485 Transfer, 400 Router SwapOut, and 100 StopCycle Actors, and runs both +Actor-only and continuously saturated valid-user demand for 100 linked blocks. +The W5 campaign completes one five-Actor Manual matrix across zero-Step, +Opening/middle/final Running, retry exhaustion, productive cleanup, minimal +admission apoptosis, and uncompensated committed-prefix durability. +The W6 campaign combines dense Manual readiness, sparse block windows, AtTime +tick deadlines and seeded Cadenced periods with pause/resume, close-created +tombstones and normal Crossing generation rotation without stale execution. +Its timeline distinguishes deadline, materialization and service: distinct short +tick periods can become due together in the same produced block. +The W7 campaign compares one 100-due-Actor control with the same due frontier at +10,000 total identities: 4,943 future, 4,942 unsignaled, and 15 retained reference +identities form the 9,900 non-due population. Both profiles must preserve exact +FIFO/Q1 throughput and the complete-attempt Control frontier. +The W8 campaign measures legal closed-Actor tombstone prefixes of +1/4/8/16/32/64/128 entries ahead of the same 100 due Manual Actors. Each complete +block must reclaim the exact prefix, preserve the live FIFO/Q1 committed prefix, +and stop remaining live work only at the Actor Control frontier without opening a +page-layout comparison. +The W9 campaign compares the same 100-due-Actor FIFO frontier under Actor-only, +proof-saturated remarks, and RefTime-heavy valid Router demand. It requires equal +Actor service/accounting and records the explicit fallback when production-valid +business calls still reach User ProofSize before the RefTime frontier. +The Control-attribution campaign compares matched 100-Actor Manual and cadence-one +fixtures for nine linked blocks, records mandatory-Prepass versus final Control, +and proves temporal occurrence materialization is Prepass-owned and cannot service +newly materialized work past that block's captured FIFO cutoff. It also pins the +production-Weight read/write ledger for fixed coordination, dual-clock probes, +retained/removed wakeup consumption, Cadenced rearm topology, and close/fault +admission contingencies without treating additive envelopes as actual block proof. The wall-clock matrix remains diagnostic and does not run in this contract. -h, --help Show this help message @@ -63,6 +163,59 @@ parse_args() { --quick) QUICK_MODE=1 ;; + --exact-heavy-profile) + [[ $# -ge 2 ]] || { + log_error "--exact-heavy-profile requires a profile name" + exit 2 + } + EXACT_HEAVY_PROFILE="$2" + shift + ;; + --production-reference-replay) + PRODUCTION_REFERENCE_REPLAY=1 + ;; + --integrated-w0-w1) + INTEGRATED_W0_W1=1 + ;; + --integrated-w1-actor-only) + INTEGRATED_W1_ACTOR_ONLY=1 + ;; + --integrated-w1-continuous-user) + INTEGRATED_W1_CONTINUOUS_USER=1 + ;; + --integrated-w2-schedules) + INTEGRATED_W2_SCHEDULES=1 + ;; + --integrated-w3-opening-matrix) + INTEGRATED_W3_OPENING_MATRIX=1 + ;; + --integrated-w4-heterogeneous-effects) + INTEGRATED_W4_HETEROGENEOUS_EFFECTS=1 + ;; + --integrated-w5-lifecycle-retry-cleanup) + INTEGRATED_W5_LIFECYCLE_RETRY_CLEANUP=1 + ;; + --integrated-w6-mixed-arrival-lifecycle) + INTEGRATED_W6_MIXED_ARRIVAL_LIFECYCLE=1 + ;; + --integrated-w7-due-only-active-frontier) + INTEGRATED_W7_DUE_ONLY_ACTIVE_FRONTIER=1 + ;; + --integrated-w8-tombstone-prefix-chunk-pressure) + INTEGRATED_W8_TOMBSTONE_PREFIX_CHUNK_PRESSURE=1 + ;; + --integrated-w9-resource-independence) + INTEGRATED_W9_RESOURCE_INDEPENDENCE=1 + ;; + --integrated-control-attribution) + INTEGRATED_CONTROL_ATTRIBUTION=1 + ;; + --integrated-funded-user-action) + INTEGRATED_FUNDED_USER_ACTION=1 + ;; + --backpressure-audit) + BACKPRESSURE_AUDIT=1 + ;; -h|--help) usage exit 0 @@ -75,6 +228,42 @@ parse_args() { esac shift done + local integrated_count=$((INTEGRATED_W0_W1 + INTEGRATED_W1_ACTOR_ONLY + INTEGRATED_W1_CONTINUOUS_USER + INTEGRATED_W2_SCHEDULES + INTEGRATED_W3_OPENING_MATRIX + INTEGRATED_W4_HETEROGENEOUS_EFFECTS + INTEGRATED_W5_LIFECYCLE_RETRY_CLEANUP + INTEGRATED_W6_MIXED_ARRIVAL_LIFECYCLE + INTEGRATED_W7_DUE_ONLY_ACTIVE_FRONTIER + INTEGRATED_W8_TOMBSTONE_PREFIX_CHUNK_PRESSURE + INTEGRATED_W9_RESOURCE_INDEPENDENCE + INTEGRATED_CONTROL_ATTRIBUTION + INTEGRATED_FUNDED_USER_ACTION)) + if [[ "$integrated_count" -gt 1 ]]; then + log_error "Select exactly one integrated EXP-0066 cohort" + exit 2 + fi + if [[ "$QUICK_MODE" == "1" && ( "$integrated_count" -gt 0 || "$BACKPRESSURE_AUDIT" == "1" ) ]]; then + log_error "Selected focused cohorts and --quick are mutually exclusive" + exit 2 + fi + if [[ "$BACKPRESSURE_AUDIT" == "1" && ( "$integrated_count" -gt 0 || "$PRODUCTION_REFERENCE_REPLAY" == "1" ) ]]; then + log_error "--backpressure-audit selects one independent assurance cohort" + exit 2 + fi + if [[ -n "$EXACT_HEAVY_PROFILE" && ( "$QUICK_MODE" == "1" || "$integrated_count" -gt 0 || "$PRODUCTION_REFERENCE_REPLAY" == "1" || "$BACKPRESSURE_AUDIT" == "1" ) ]]; then + log_error "--exact-heavy-profile selects one independent assurance cohort" + exit 2 + fi + if [[ "$PRODUCTION_REFERENCE_REPLAY" == "1" && ( "$QUICK_MODE" == "1" || "$integrated_count" -gt 0 ) ]]; then + log_error "--production-reference-replay selects one independent assurance cohort" + exit 2 + fi + if [[ -n "$EXACT_HEAVY_PROFILE" ]]; then + local exact_matches=0 profile + [[ "$EXACT_HEAVY_PROFILE" == "$OCCUPANCY_HEAVY_PROFILE" ]] && exact_matches=$((exact_matches + 1)) + for profile in "${REQUIRED_HEAVY_PROFILES[@]}" "${PACKAGE_HEAVY_PROFILES[@]}"; do + [[ "$EXACT_HEAVY_PROFILE" == "$profile" ]] && exact_matches=$((exact_matches + 1)) + done + if [[ "$exact_matches" -ne 1 ]]; then + log_error "Exact heavy profile '$EXACT_HEAVY_PROFILE' is not declared exactly once" + exit 2 + fi + fi + if [[ "$REQUIRE_WASM_IDENTITY" != "1" && ( "$integrated_count" -gt 0 || "$PRODUCTION_REFERENCE_REPLAY" == "1" ) ]]; then + log_error "The selected production replay cohort requires the exact production Wasm identity" + exit 2 + fi } check_prerequisites() { @@ -87,7 +276,7 @@ check_prerequisites() { return 1 } if [[ "$REQUIRE_WASM_IDENTITY" == "1" ]]; then - [[ -f "$TEMPLATE_DIR/target/release/wbuild/deos-runtime/deos_runtime.compact.compressed.wasm" ]] || { + [[ -f "$WASM_ARTIFACT" ]] || { log_error "Production runtime Wasm not found" return 1 } @@ -102,19 +291,101 @@ check_prerequisites() { } 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}' + ( + cd "$PROJECT_ROOT" || exit 1 + git ls-files --cached --others --exclude-standard -z -- \ + template scripts web-client simulator docs .github .cargo \ + ':/*.toml' ':/*.json' .gitignore .gitattributes .npmrc \ + ':(exclude,glob)**/target/**' ':(exclude,glob)**/node_modules/**' \ + ':(exclude,glob)**/build/**' ':(exclude,glob)**/dist/**' \ + ':(exclude,glob)**/.svelte-kit/**' ':(exclude)web-client/.papi' \ + ':(exclude)template/chain_spec.json' \ + | LC_ALL=C sort -zu \ + | while IFS= read -r -d '' path; do + if [[ -L "$path" ]]; then + log_error "Source identity cannot certify a symlink: $path" >&2 + exit 1 + fi + # A tracked deletion has the same source tree before and after staging. + [[ -e "$path" ]] || continue + if [[ -x "$path" ]]; then + printf 'executable\0' + else + printf 'regular\0' + fi + sha256sum --zero -- "$path" || exit 1 + done \ + | sha256sum | awk '{print $1}' + ) } +run_self_test() ( + local fixture + require_commands bash git sha256sum sort mktemp + fixture="$(mktemp -d "${TMPDIR:-/tmp}/deos-source-identity.XXXXXX")" + trap 'rm -rf -- "$fixture"' EXIT + + source_identity() { PROJECT_ROOT="$fixture" worktree_evidence_identity; } + assert_equal() { [[ "$1" == "$2" ]] || { log_error "$3"; exit 1; }; } + assert_changed() { [[ "$1" != "$2" ]] || { log_error "$3"; exit 1; }; } + + git -C "$fixture" init -q + git -C "$fixture" config user.name 'Source identity test' + git -C "$fixture" config user.email 'source-identity@example.invalid' + mkdir -p "$fixture/template/runtime/src" + local source="$fixture/template/runtime/src/lib.rs" + printf 'baseline\n' > "$source" + git -C "$fixture" add . + git -C "$fixture" -c commit.gpgsign=false commit -qm baseline + local baseline committed staged unstaged untracked deleted + baseline="$(source_identity)" + printf 'committed successor\n' > "$source" + git -C "$fixture" add . + git -C "$fixture" -c commit.gpgsign=false commit -qm successor + committed="$(source_identity)" + assert_changed "$baseline" "$committed" 'Different clean committed sources shared an identity' + git -C "$fixture" -c commit.gpgsign=false commit --allow-empty -qm packaging + assert_equal "$committed" "$(source_identity)" 'Commit packaging changed identical source bytes' + printf 'staged successor\n' > "$source" + git -C "$fixture" add . + staged="$(source_identity)" + assert_changed "$committed" "$staged" 'Staged source change was omitted' + printf 'unstaged successor\n' > "$source" + unstaged="$(source_identity)" + assert_changed "$staged" "$unstaged" 'Unstaged source change was omitted' + local unusual="$fixture/template/runtime/src/line"$'\n'"break.rs" + printf 'untracked bytes\n' > "$unusual" + untracked="$(source_identity)" + assert_changed "$unstaged" "$untracked" 'Untracked source change was omitted' + git -C "$fixture" add . + assert_equal "$untracked" "$(source_identity)" 'Staging identical source bytes changed identity' + rm -- "$unusual" + deleted="$(source_identity)" + assert_changed "$untracked" "$deleted" 'Deleted source remained in identity' + git -C "$fixture" add -u + assert_equal "$deleted" "$(source_identity)" 'Staging deletion changed identical source tree' + mkdir -p "$fixture/template/target" "$fixture/web-client/.papi" "$fixture/.agents/skills" + printf 'artifact\n' > "$fixture/template/target/runtime.wasm" + printf 'metadata\n' > "$fixture/web-client/.papi/deos.scale" + printf 'skill\n' > "$fixture/.agents/skills/SKILL.md" + assert_equal "$deleted" "$(source_identity)" 'Excluded artifacts or Skills changed source identity' + chmod +x "$source" + assert_changed "$deleted" "$(source_identity)" 'Executable-mode source change was omitted' + chmod -x "$source" + assert_equal "$deleted" "$(source_identity)" 'Restoring executable mode changed source identity' + ln -s lib.rs "$fixture/template/runtime/src/link.rs" + if source_identity >/dev/null 2>&1; then + log_error 'Symlink source must fail closed instead of certifying external bytes' + exit 1 + fi + log_success 'Actors source identity self-test passed' +) + 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}')" + EVIDENCE_WASM_IDENTITY="$(sha256sum "$WASM_ARTIFACT" | awk '{print $1}')" else EVIDENCE_WASM_IDENTITY="not-required" fi @@ -125,12 +396,31 @@ capture_evidence_identity() { log_info "Evidence metadata: $EVIDENCE_METADATA_IDENTITY" } +snapshot_wasm_artifact() { + if [[ "$REQUIRE_WASM_IDENTITY" != "1" ]]; then + return + fi + WASM_SNAPSHOT="$(mktemp)" + cp "$WASM_ARTIFACT" "$WASM_SNAPSHOT" +} + +restore_wasm_artifact() { + if [[ -z "$WASM_SNAPSHOT" || ! -f "$WASM_SNAPSHOT" ]]; then + return + fi + local replacement="${WASM_ARTIFACT}.assurance-restore" + cp "$WASM_SNAPSHOT" "$replacement" + mv "$replacement" "$WASM_ARTIFACT" + rm -f "$WASM_SNAPSHOT" + WASM_SNAPSHOT="" +} + 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}')" + wasm_identity="$(sha256sum "$WASM_ARTIFACT" | awk '{print $1}')" else wasm_identity="not-required" fi @@ -199,8 +489,297 @@ verify_heavy_profiles_resolve_exactly_once() { log_success "All package heavy profiles resolve to exactly one test" } +run_integrated_w0_w1_gate() { + local native_profile="full_executive_w0_w1_fixture_covers_actor_only_and_valid_user_demand" + local wasm_profile="full_executive_w0_w1_replays_exact_production_wasm" + local listing profile matches + listing="$(cd "$TEMPLATE_DIR" && cargo test -p deos-runtime --locked -- --list 2>/dev/null)" + for profile in "$native_profile" "$wasm_profile"; do + matches="$(printf '%s\n' "$listing" | grep -c "${profile}:" || true)" + if [[ "$matches" -ne 1 ]]; then + log_error "Integrated profile '${profile}' resolved to ${matches} test(s); expected exactly 1" + return 1 + fi + done + run_shell_step \ + "EXP-0066 gate: native full-Executive W0/W1 fixture" \ + "" \ + "cd \"$TEMPLATE_DIR\" && cargo test -p deos-runtime --locked '$native_profile' -- --nocapture" + run_shell_step \ + "EXP-0066 gate: exact production-Wasm W0/W1 replay" \ + "" \ + "cd \"$TEMPLATE_DIR\" && DEOS_PRODUCTION_WASM='$WASM_SNAPSHOT' cargo test --release -p deos-runtime --locked '$wasm_profile' -- --ignored --nocapture" +} + +run_integrated_w1_campaign_gate() { + local demand="$1" + local profile label + case "$demand" in + actor-only) + profile="full_executive_w1_actor_only_100_block_campaign_replays_exact_production_wasm" + label="Actor-only" + ;; + continuous-user) + profile="full_executive_w1_continuous_valid_user_100_block_campaign_replays_exact_production_wasm" + label="continuous valid-user" + ;; + *) + log_error "Unknown EXP-0066 W1 campaign: $demand" + return 2 + ;; + esac + local listing matches + listing="$(cd "$TEMPLATE_DIR" && cargo test --release -p deos-runtime --locked -- --list 2>/dev/null)" + matches="$(printf '%s\n' "$listing" | grep -c "${profile}:" || true)" + if [[ "$matches" -ne 1 ]]; then + log_error "Integrated profile '${profile}' resolved to ${matches} test(s); expected exactly 1" + return 1 + fi + run_shell_step \ + "EXP-0066 gate: exact production-Wasm ${label} W1 campaign" \ + "" \ + "cd \"$TEMPLATE_DIR\" && DEOS_PRODUCTION_WASM='$WASM_SNAPSHOT' cargo test --release -p deos-runtime --locked '$profile' -- --ignored --nocapture" +} + +run_integrated_w2_schedule_gate() { + local profile="full_executive_w2_manual_and_cadenced_only_100_block_campaigns_replay_exact_production_wasm" + local listing matches + listing="$(cd "$TEMPLATE_DIR" && cargo test --release -p deos-runtime --locked -- --list 2>/dev/null)" + matches="$(printf '%s\n' "$listing" | grep -c "${profile}:" || true)" + if [[ "$matches" -ne 1 ]]; then + log_error "Integrated profile '${profile}' resolved to ${matches} test(s); expected exactly 1" + return 1 + fi + run_shell_step \ + "EXP-0066 gate: exact production-Wasm Manual-only and Cadenced-only W2 campaigns" \ + "" \ + "cd \"$TEMPLATE_DIR\" && DEOS_PRODUCTION_WASM='$WASM_SNAPSHOT' cargo test --release -p deos-runtime --locked '$profile' -- --ignored --nocapture" +} + +run_integrated_w3_opening_matrix_gate() { + local profile="full_executive_w3_opening_predicate_mixed_length_campaign_replays_exact_production_wasm" + local listing matches + listing="$(cd "$TEMPLATE_DIR" && cargo test --release -p deos-runtime --locked -- --list 2>/dev/null)" + matches="$(printf '%s\n' "$listing" | grep -c "${profile}:" || true)" + if [[ "$matches" -ne 1 ]]; then + log_error "Integrated profile '${profile}' resolved to ${matches} test(s); expected exactly 1" + return 1 + fi + run_shell_step \ + "EXP-0066 gate: exact production-Wasm W3 Opening-predicate and mixed-length matrix" \ + "" \ + "cd \"$TEMPLATE_DIR\" && DEOS_PRODUCTION_WASM='$WASM_SNAPSHOT' cargo test --release -p deos-runtime --locked '$profile' -- --ignored --nocapture" +} + +run_integrated_w4_heterogeneous_effects_gate() { + local profile="full_executive_w4_heterogeneous_effect_campaigns_replay_exact_production_wasm" + local listing matches + listing="$(cd "$TEMPLATE_DIR" && cargo test --release -p deos-runtime --locked -- --list 2>/dev/null)" + matches="$(printf '%s\n' "$listing" | grep -c "${profile}:" || true)" + if [[ "$matches" -ne 1 ]]; then + log_error "Integrated profile '${profile}' resolved to ${matches} test(s); expected exactly 1" + return 1 + fi + run_shell_step \ + "EXP-0066 gate: exact production-Wasm W4 heterogeneous effect campaigns" \ + "" \ + "cd \"$TEMPLATE_DIR\" && DEOS_PRODUCTION_WASM='$WASM_SNAPSHOT' cargo test --release -p deos-runtime --locked '$profile' -- --ignored --nocapture" +} + +run_integrated_w5_lifecycle_retry_cleanup_gate() { + local native_profile="full_executive_w5_lifecycle_retry_cleanup_fixture_preserves_prefixes" + local wasm_profile="full_executive_w5_lifecycle_retry_cleanup_campaign_replays_exact_production_wasm" + local listing profile matches + listing="$(cd "$TEMPLATE_DIR" && cargo test -p deos-runtime --locked -- --list 2>/dev/null)" + for profile in "$native_profile" "$wasm_profile"; do + matches="$(printf '%s\n' "$listing" | grep -c "${profile}:" || true)" + if [[ "$matches" -ne 1 ]]; then + log_error "Integrated profile '${profile}' resolved to ${matches} test(s); expected exactly 1" + return 1 + fi + done + run_shell_step \ + "EXP-0066 gate: native full-Executive W5 lifecycle/retry/cleanup fixture" \ + "" \ + "cd \"$TEMPLATE_DIR\" && cargo test -p deos-runtime --locked '$native_profile' -- --nocapture" + run_shell_step \ + "EXP-0066 gate: exact production-Wasm W5 lifecycle/retry/cleanup campaign" \ + "" \ + "cd \"$TEMPLATE_DIR\" && DEOS_PRODUCTION_WASM='$WASM_SNAPSHOT' cargo test --release -p deos-runtime --locked '$wasm_profile' -- --ignored --nocapture" +} + +run_integrated_funded_user_action_gate() { + local native_profile="full_executive_funded_action_collection_witness" + local wasm_profile="full_executive_funded_action_collection_replays_exact_production_wasm" + local listing profile matches + listing="$(cd "$TEMPLATE_DIR" && cargo test -p deos-runtime --locked -- --list 2>/dev/null)" + for profile in "$native_profile" "$wasm_profile"; do + matches="$(printf '%s\n' "$listing" | grep -c "${profile}:" || true)" + if [[ "$matches" -ne 1 ]]; then + log_error "Funded User Action profile '${profile}' resolved to ${matches} test(s); expected exactly 1" + return 1 + fi + done + run_shell_step \ + "EXP-0097 gate: native funded User Action monetary/resource witness" \ + "" \ + "cd \"$TEMPLATE_DIR\" && cargo test -p deos-runtime --locked '$native_profile' -- --nocapture" + run_shell_step \ + "EXP-0097 gate: exact production-Wasm funded User Action replay" \ + "" \ + "cd \"$TEMPLATE_DIR\" && DEOS_PRODUCTION_WASM='$WASM_SNAPSHOT' cargo test --release -p deos-runtime --locked '$wasm_profile' -- --ignored --nocapture" +} + +run_integrated_w6_mixed_arrival_lifecycle_gate() { + local native_profile="full_executive_w6_mixed_arrival_lifecycle_fixture_handles_stale_generations" + local wasm_profile="full_executive_w6_mixed_arrival_lifecycle_campaign_replays_exact_production_wasm" + local listing profile matches + listing="$(cd "$TEMPLATE_DIR" && cargo test -p deos-runtime --locked -- --list 2>/dev/null)" + for profile in "$native_profile" "$wasm_profile"; do + matches="$(printf '%s\n' "$listing" | grep -c "${profile}:" || true)" + if [[ "$matches" -ne 1 ]]; then + log_error "Integrated profile '${profile}' resolved to ${matches} test(s); expected exactly 1" + return 1 + fi + done + run_shell_step \ + "EXP-0066 gate: native full-Executive W6 clocks/Triggers/lifecycle fixture" \ + "" \ + "cd \"$TEMPLATE_DIR\" && cargo test -p deos-runtime --locked '$native_profile' -- --nocapture" + run_shell_step \ + "EXP-0066 gate: exact production-Wasm W6 clocks/Triggers/lifecycle campaign" \ + "" \ + "cd \"$TEMPLATE_DIR\" && DEOS_PRODUCTION_WASM='$WASM_SNAPSHOT' cargo test --release -p deos-runtime --locked '$wasm_profile' -- --ignored --nocapture" +} + +run_integrated_w7_due_only_active_frontier_gate() { + local native_profile="full_executive_w7_due_only_active_frontier_fixture_scales_independently" + local wasm_profile="full_executive_w7_due_only_active_frontier_campaign_replays_exact_production_wasm" + local listing profile matches + listing="$(cd "$TEMPLATE_DIR" && cargo test -p deos-runtime --locked -- --list 2>/dev/null)" + for profile in "$native_profile" "$wasm_profile"; do + matches="$(printf '%s\n' "$listing" | grep -c "${profile}:" || true)" + if [[ "$matches" -ne 1 ]]; then + log_error "Integrated profile '${profile}' resolved to ${matches} test(s); expected exactly 1" + return 1 + fi + done + run_shell_step \ + "EXP-0066 gate: native full-Executive W7 due-only active-frontier fixture" \ + "" \ + "cd \"$TEMPLATE_DIR\" && RUST_TEST_THREADS=1 cargo test -p deos-runtime --locked '$native_profile' -- --nocapture" + run_shell_step \ + "EXP-0066 gate: exact production-Wasm W7 due-only active-frontier campaign" \ + "" \ + "cd \"$TEMPLATE_DIR\" && RUST_TEST_THREADS=1 DEOS_PRODUCTION_WASM='$WASM_SNAPSHOT' cargo test --release -p deos-runtime --locked '$wasm_profile' -- --ignored --nocapture" +} + +run_integrated_w8_tombstone_prefix_chunk_pressure_gate() { + local native_profile="full_executive_w8_tombstone_prefix_chunk_pressure_fixture_preserves_fifo" + local wasm_profile="full_executive_w8_tombstone_prefix_chunk_pressure_campaign_replays_exact_production_wasm" + local listing profile matches + listing="$(cd "$TEMPLATE_DIR" && cargo test -p deos-runtime --locked -- --list 2>/dev/null)" + for profile in "$native_profile" "$wasm_profile"; do + matches="$(printf '%s\n' "$listing" | grep -c "${profile}:" || true)" + if [[ "$matches" -ne 1 ]]; then + log_error "Integrated profile '${profile}' resolved to ${matches} test(s); expected exactly 1" + return 1 + fi + done + run_shell_step \ + "EXP-0066 gate: native full-Executive W8 tombstone-prefix/chunk pressure" \ + "" \ + "cd \"$TEMPLATE_DIR\" && RUST_TEST_THREADS=1 cargo test -p deos-runtime --locked '$native_profile' -- --nocapture" + run_shell_step \ + "EXP-0066 gate: exact production-Wasm W8 tombstone-prefix/chunk pressure" \ + "" \ + "cd \"$TEMPLATE_DIR\" && RUST_TEST_THREADS=1 DEOS_PRODUCTION_WASM='$WASM_SNAPSHOT' cargo test --release -p deos-runtime --locked '$wasm_profile' -- --ignored --nocapture" +} + +run_integrated_w9_resource_independence_gate() { + local native_profile="full_executive_w9_resource_independence_fixture_preserves_actor_service" + local wasm_profile="full_executive_w9_resource_independence_campaign_replays_exact_production_wasm" + local listing profile matches + listing="$(cd "$TEMPLATE_DIR" && cargo test -p deos-runtime --locked -- --list 2>/dev/null)" + for profile in "$native_profile" "$wasm_profile"; do + matches="$(printf '%s\n' "$listing" | grep -c "${profile}:" || true)" + if [[ "$matches" -ne 1 ]]; then + log_error "Integrated profile '${profile}' resolved to ${matches} test(s); expected exactly 1" + return 1 + fi + done + run_shell_step \ + "EXP-0066 gate: native full-Executive W9 resource independence" \ + "" \ + "cd \"$TEMPLATE_DIR\" && RUST_TEST_THREADS=1 cargo test -p deos-runtime --locked '$native_profile' -- --nocapture" + run_shell_step \ + "EXP-0066 gate: exact production-Wasm W9 resource independence" \ + "" \ + "cd \"$TEMPLATE_DIR\" && RUST_TEST_THREADS=1 DEOS_PRODUCTION_WASM='$WASM_SNAPSHOT' cargo test --release -p deos-runtime --locked '$wasm_profile' -- --ignored --nocapture" +} + +run_integrated_control_attribution_gate() { + local native_profile="full_executive_control_phase_attribution_fixture_isolates_materialization" + local wasm_profile="full_executive_control_phase_attribution_replays_exact_production_wasm" + local listing profile matches + listing="$(cd "$TEMPLATE_DIR" && SKIP_WASM_BUILD=1 cargo test -p deos-runtime --locked -- --list 2>/dev/null)" + for profile in "$native_profile" "$wasm_profile"; do + matches="$(printf '%s\n' "$listing" | grep -c "${profile}:" || true)" + if [[ "$matches" -ne 1 ]]; then + log_error "Integrated profile '${profile}' resolved to ${matches} test(s); expected exactly 1" + return 1 + fi + done + run_shell_step \ + "EXP-0066 gate: native schedule-Control phase attribution" \ + "" \ + "cd \"$TEMPLATE_DIR\" && SKIP_WASM_BUILD=1 RUST_TEST_THREADS=1 cargo test -p deos-runtime --locked '$native_profile' -- --nocapture" + run_shell_step \ + "EXP-0066 gate: exact production-Wasm schedule-Control phase attribution" \ + "" \ + "cd \"$TEMPLATE_DIR\" && SKIP_WASM_BUILD=1 RUST_TEST_THREADS=1 DEOS_PRODUCTION_WASM='$WASM_SNAPSHOT' cargo test --release -p deos-runtime --locked '$wasm_profile' -- --ignored --nocapture" +} + +run_production_reference_replay() { + local profile="reference_full_block_replays_in_production_wasm_with_verified_storage_proof" + local listing matches + listing="$(cd "$TEMPLATE_DIR" && SKIP_WASM_BUILD=1 cargo test -p deos-runtime --locked -- --list 2>/dev/null)" + matches="$(printf '%s\n' "$listing" | grep -c "${profile}:" || true)" + if [[ "$matches" -ne 1 ]]; then + log_error "Production reference replay '$profile' resolved to ${matches} test(s); expected exactly 1" + return 1 + fi + run_shell_step \ + "Actors gate: current production-Wasm reference block replay" \ + "" \ + "cd \"$TEMPLATE_DIR\" && SKIP_WASM_BUILD=1 DEOS_PRODUCTION_WASM='$WASM_SNAPSHOT' cargo test --release -p deos-runtime --locked '$profile' -- --ignored --nocapture" +} + +run_exact_heavy_profile() { + local package="deos-runtime" + local profile + for profile in "${PACKAGE_HEAVY_PROFILES[@]}"; do + if [[ "$EXACT_HEAVY_PROFILE" == "$profile" ]]; then + package="pallet-deos-actors" + break + fi + done + local listing matches + listing="$(cd "$TEMPLATE_DIR" && SKIP_WASM_BUILD=1 cargo test -p "$package" --locked -- --list 2>/dev/null)" + matches="$(printf '%s\n' "$listing" | grep -c "${EXACT_HEAVY_PROFILE}:" || true)" + if [[ "$matches" -ne 1 ]]; then + log_error "Exact heavy profile '$EXACT_HEAVY_PROFILE' resolved to ${matches} test(s); expected exactly 1" + return 1 + fi + run_shell_step \ + "Actors gate: exact heavy profile ${EXACT_HEAVY_PROFILE}" \ + "" \ + "cd \"$TEMPLATE_DIR\" && cargo test --$CARGO_PROFILE -p '$package' --locked '$EXACT_HEAVY_PROFILE' -- --ignored --nocapture" +} + 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_self_test + run_shell_step "Actors gate: fee-envelope vector freshness" "" "cd \"$TEMPLATE_DIR\" && cargo run -q --locked -p pallet-deos-actors --example fee_envelope_vectors -- --metadata ../web-client/.papi/metadata/deos.scale --weights runtime/src/weights/pallet_deos_actors.rs --check ../web-client/src/lib/automation/actors-fee-envelope-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" @@ -266,19 +845,97 @@ run_gate() { log_info "Non-gating diagnostics (not resolved or executed by this gate): ${DIAGNOSTIC_HEAVY_PROFILES[*]}" } +run_backpressure_audit() { + local runtime_profiles=( + temporary_oracle_capacity_failure_rolls_back_economics_and_has_one_retry_owner + swap_exact_out_liquidity_boundary_fails_without_partial_execution + xcm_deposit_failure_rolls_back_precommit_events_and_exact_holding + oracle_publication_rolls_back_when_crossing_transition_queue_is_full + internal_asset_transfer_rolls_back_when_funding_pending_overflows + actor_fee_collector_rolls_back_completely_on_ledger_failure + runtime_simulation_core_rolls_back_deos_adapter_effects + anchored_split_transfer_rolls_back_when_a_later_recipient_is_unavailable + ) + local package_profiles=( + fanout_structural_fault_is_bounded_and_requires_repair_before_resume + user_retry_insolvency_closes_before_effect_capacity_deferral + retry_later_resumes_same_cursor_without_replaying_committed_prefix + ) + local profile + for profile in "${runtime_profiles[@]}"; do + run_shell_step \ + "EXP-0100 runtime falsifier: ${profile}" \ + "" \ + "cd \"$TEMPLATE_DIR\" && cargo test -p deos-runtime --locked '$profile'" + done + for profile in "${package_profiles[@]}"; do + run_shell_step \ + "EXP-0100 package falsifier: ${profile}" \ + "" \ + "cd \"$TEMPLATE_DIR\" && cargo test -p pallet-deos-actors --locked '$profile'" + done +} + 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 + snapshot_wasm_artifact + trap restore_wasm_artifact EXIT + log_info "Profile: $CARGO_PROFILE | quick: $QUICK_MODE | exact-heavy-profile: ${EXACT_HEAVY_PROFILE:-none} | production-reference-replay: $PRODUCTION_REFERENCE_REPLAY | occupancy: $INCLUDE_OCCUPANCY_PROFILE | integrated-w0-w1: $INTEGRATED_W0_W1 | integrated-w1-actor-only: $INTEGRATED_W1_ACTOR_ONLY | integrated-w1-continuous-user: $INTEGRATED_W1_CONTINUOUS_USER | integrated-w2-schedules: $INTEGRATED_W2_SCHEDULES | integrated-w3-opening-matrix: $INTEGRATED_W3_OPENING_MATRIX | integrated-w4-heterogeneous-effects: $INTEGRATED_W4_HETEROGENEOUS_EFFECTS | integrated-w5-lifecycle-retry-cleanup: $INTEGRATED_W5_LIFECYCLE_RETRY_CLEANUP | integrated-w6-mixed-arrival-lifecycle: $INTEGRATED_W6_MIXED_ARRIVAL_LIFECYCLE | integrated-w7-due-only-active-frontier: $INTEGRATED_W7_DUE_ONLY_ACTIVE_FRONTIER | integrated-w8-tombstone-prefix-chunk-pressure: $INTEGRATED_W8_TOMBSTONE_PREFIX_CHUNK_PRESSURE | integrated-w9-resource-independence: $INTEGRATED_W9_RESOURCE_INDEPENDENCE | integrated-control-attribution: $INTEGRATED_CONTROL_ATTRIBUTION | integrated-funded-user-action: $INTEGRATED_FUNDED_USER_ACTION | backpressure-audit: $BACKPRESSURE_AUDIT" + if [[ "$BACKPRESSURE_AUDIT" == "1" ]]; then + run_backpressure_audit + elif [[ -n "$EXACT_HEAVY_PROFILE" ]]; then + run_exact_heavy_profile + elif [[ "$PRODUCTION_REFERENCE_REPLAY" == "1" ]]; then + run_production_reference_replay + elif [[ "$INTEGRATED_W0_W1" == "1" ]]; then + run_integrated_w0_w1_gate + elif [[ "$INTEGRATED_W1_ACTOR_ONLY" == "1" ]]; then + run_integrated_w1_campaign_gate actor-only + elif [[ "$INTEGRATED_W1_CONTINUOUS_USER" == "1" ]]; then + run_integrated_w1_campaign_gate continuous-user + elif [[ "$INTEGRATED_W2_SCHEDULES" == "1" ]]; then + run_integrated_w2_schedule_gate + elif [[ "$INTEGRATED_W3_OPENING_MATRIX" == "1" ]]; then + run_integrated_w3_opening_matrix_gate + elif [[ "$INTEGRATED_W4_HETEROGENEOUS_EFFECTS" == "1" ]]; then + run_integrated_w4_heterogeneous_effects_gate + elif [[ "$INTEGRATED_W5_LIFECYCLE_RETRY_CLEANUP" == "1" ]]; then + run_integrated_w5_lifecycle_retry_cleanup_gate + elif [[ "$INTEGRATED_W6_MIXED_ARRIVAL_LIFECYCLE" == "1" ]]; then + run_integrated_w6_mixed_arrival_lifecycle_gate + elif [[ "$INTEGRATED_W7_DUE_ONLY_ACTIVE_FRONTIER" == "1" ]]; then + run_integrated_w7_due_only_active_frontier_gate + elif [[ "$INTEGRATED_W8_TOMBSTONE_PREFIX_CHUNK_PRESSURE" == "1" ]]; then + run_integrated_w8_tombstone_prefix_chunk_pressure_gate + elif [[ "$INTEGRATED_W9_RESOURCE_INDEPENDENCE" == "1" ]]; then + run_integrated_w9_resource_independence_gate + elif [[ "$INTEGRATED_CONTROL_ATTRIBUTION" == "1" ]]; then + run_integrated_control_attribution_gate + elif [[ "$INTEGRATED_FUNDED_USER_ACTION" == "1" ]]; then + run_integrated_funded_user_action_gate + else + run_gate + fi + restore_wasm_artifact + trap - EXIT verify_evidence_identity_unchanged phase_banner "Summary" log_success "Actors scheduler assurance completed successfully" } run_entrypoint() { + if [[ "${1:-}" == "self-test" ]]; then + shift + case "$#:${1:-}" in + 0:) run_self_test ;; + 1:--help|1:-h) usage ;; + *) usage >&2; return 2 ;; + esac + return + fi if [[ "${1:-}" == "--internal" ]]; then shift main "$@" diff --git a/scripts/benchmarks.sh b/scripts/benchmarks.sh index 75f8be0f..723fe5d4 100755 --- a/scripts/benchmarks.sh +++ b/scripts/benchmarks.sh @@ -8,6 +8,7 @@ BENCHMARK_TARGET_DIR="$TEMPLATE_DIR/target/benchmarks" PRODUCTION_RUNTIME_WASM="$TEMPLATE_DIR/target/release/wbuild/deos-runtime/deos_runtime.compact.compressed.wasm" STEPS=50 REPEAT=20 +MIN_DURATION="" HEAP_PAGES=4096 CHAIN="dev" INCLUDE_EXTRA_BENCHMARKS=0 @@ -28,6 +29,9 @@ ACTION="" TARGET_PALLET="" EXTRINSIC_PATTERN="*" OUTPUT_OVERRIDE="" +JSON_OUTPUT="" +COMPONENT_LOW="" +COMPONENT_HIGH="" SKIP_BUILD=0 usage() { @@ -39,13 +43,19 @@ Run benchmarks and generate weight files for the current DEOS reference runtime Options: --steps N Number of steps per benchmark (default: $STEPS) --repeat N Number of repetitions per benchmark (default: $REPEAT) + --min-duration N + Minimum seconds per benchmark (bencher default: 10; 0 uses --repeat) --all Benchmark all custom pallets --list List available pallets --check Verify benchmark compilation and generated storage names (no execution) - --extra Include Actors circular-chain diagnostic benchmarks excluded from production weights + --extra Include diagnostic benchmarks excluded from production weights --extrinsic NAME Benchmark one extrinsic (requires one PALLET_NAME) --output FILE Write generated weights to FILE (requires --extrinsic) + --json-file FILE + Save raw component samples (requires one exact --extrinsic) + --low VALUES Comma-separated component minima (requires --high and --output) + --high VALUES Comma-separated component maxima (requires --low and --output) --skip-build Reuse an already-built benchmark runtime -h, --help Show this help message @@ -57,7 +67,7 @@ Examples: $(basename "$0") --all # Benchmark all pallets $(basename "$0") pallet_deos_router # Benchmark one pallet $(basename "$0") --check # Verify compilation only - $(basename "$0") --extra pallet_deos_actors # Include Actors circular-chain diagnostics + $(basename "$0") --extra pallet_deos_actors # Include Actors diagnostics $(basename "$0") --extrinsic scheduler_wakeup_replace_exact --output /tmp/wakeup.rs pallet_deos_actors $(basename "$0") --steps 100 --repeat 50 --all # Production-quality run @@ -83,6 +93,14 @@ parse_args() { REPEAT="$2" shift 2 ;; + --min-duration) + if [[ $# -lt 2 || ! "$2" =~ ^[0-9]+$ ]]; then + log_error "--min-duration requires unsigned whole seconds" + exit 2 + fi + MIN_DURATION="$2" + shift 2 + ;; --all) ACTION="all" shift @@ -107,10 +125,30 @@ parse_args() { OUTPUT_OVERRIDE="$2" shift 2 ;; + --json-file) + if [[ $# -lt 2 || -z "$2" || "$2" == --* ]]; then + log_error "--json-file requires a file path" + exit 2 + fi + JSON_OUTPUT="$2" + shift 2 + ;; --skip-build) SKIP_BUILD=1 shift ;; + --low|--high) + if [[ $# -lt 2 || ! "$2" =~ ^[0-9]+(,[0-9]+)*$ ]]; then + log_error "$1 requires comma-separated unsigned component values" + exit 2 + fi + if [[ "$1" == "--low" ]]; then + COMPONENT_LOW="$2" + else + COMPONENT_HIGH="$2" + fi + shift 2 + ;; -h|--help) usage ;; @@ -133,6 +171,16 @@ parse_args() { log_error "--output requires --extrinsic" exit 2 fi + if [[ -n "$JSON_OUTPUT" && ( "$EXTRINSIC_PATTERN" == "*" || -n "$ACTION" ) ]]; then + log_error "--json-file requires one exact --extrinsic and PALLET_NAME" + exit 2 + fi + if [[ -n "$COMPONENT_LOW$COMPONENT_HIGH" \ + && ( -z "$COMPONENT_LOW" || -z "$COMPONENT_HIGH" || -z "$OUTPUT_OVERRIDE" \ + || "$EXTRINSIC_PATTERN" == "*" || -n "$ACTION" ) ]]; then + log_error "Component bounds require --low, --high, --output and one exact --extrinsic" + exit 2 + fi } check_prerequisites() { @@ -377,6 +425,16 @@ verify_weight_file_contract() { "crossing_page_unit" "crossing_actor_unit" "predicate_set_evaluation" + "predicate_asset_evaluation" + "predicate_observation_heavy_evaluation" + "opening_predicate_traversal" + "opening_predicate_capture" + "opening_max_encoded_balance_capture" + "opening_observation_heavy_capture" + "opening_snapshot_traversal" + "opening_snapshot_capture" + "opening_target_snapshot_capture" + "opening_share_mixed_capture" "observation_fanout_blocked_page" ) for benchmark in "${required_runtime_benchmarks[@]}"; do @@ -410,11 +468,44 @@ run_pallet_benchmark() { if [[ "$pallet_name" == "pallet_deos_actors" ]]; then local diagnostic_benchmarks=( + "scheduler_inner_zero_step_user_complete" + "scheduler_inner_zero_step_user_max_head" + "scheduler_inner_zero_step_user_cadenced" + "scheduler_inner_zero_step_user_cadenced_close" + "scheduler_inner_zero_step_user_at_time" + "scheduler_inner_zero_step_user_at_time_close" + "scheduler_wakeup_zero_step_user_expiry" + "scheduler_paged_zero_step_user_insolvency" + "scheduler_paged_zero_step_user_crossing_insolvency" + "scheduler_paged_user_crossing_insolvency_max_contract" + "scheduler_paged_user_crossing_insolvency_max_contract_window" + "pipeline_admission_apoptosis_crossing" + "scheduler_inner_zero_step_user_crossing_armed" + "scheduler_inner_zero_step_user_crossing_armed_pages" + "scheduler_inner_zero_step_user_crossing_armed_new_page" + "scheduler_inner_zero_step_user_crossing_armed_pages_close" + "scheduler_inner_zero_step_user_crossing_armed_new_page_close" + "scheduler_inner_zero_step_user_crossing_cursor" + "scheduler_inner_zero_step_user_crossing_cursor_close" + "scheduler_inner_zero_step_user_crossing_waiting" + "scheduler_inner_zero_step_user_crossing_close" + "scheduler_inner_zero_step_user_crossing_page" + "scheduler_inner_zero_step_user_crossing_page_close" + "scheduler_inner_zero_step_user_crossing_page_full" + "scheduler_inner_zero_step_user_crossing_page_full_close" + "scheduler_inner_zero_step_user_observation" + "scheduler_inner_zero_step_user_observation_close" + "scheduler_inner_zero_step_user_observation_page_close" + "scheduler_inner_zero_step_user_observation_unlink" + "scheduler_inner_zero_step_user_observation_feed_close" + "pipeline_zero_step_opening_collection" + "pipeline_large_head_opening_collection" "process_remove_liquidity_indexed" "scheduler_on_idle_healthy_empty" "scheduler_cooldown_ineligible_idle" "scheduler_wakeup_sparse_gap_recovery" "close_actor_system_pure" + "close_actor_opening_partition" "close_actor_crossing_page" "close_actor_crossing_tail" "close_actor_crossing_cursor_repair" @@ -424,6 +515,8 @@ run_pallet_benchmark() { "update_contract_observation_change" "precondition_all_max" "precondition_observation" + "scheduler_wakeup_cursor_remove_upward_depth" + "scheduler_wakeup_cursor_remove_upward_pages" "benchmark_monolithic_create" "benchmark_chunked_create" "benchmark_monolithic_close" @@ -462,6 +555,14 @@ run_pallet_benchmark() { log_info "Benchmarking: $pallet_name (steps=$STEPS, repeat=$REPEAT)" if [[ "$BENCHER_MODE" != "omni" ]]; then + if [[ -n "$COMPONENT_LOW$COMPONENT_HIGH" ]]; then + log_error "Component bounds require frame-omni-bencher; compile-only fallback cannot select samples" + return 1 + fi + if [[ -n "$JSON_OUTPUT" ]]; then + log_error "--json-file requires frame-omni-bencher; compile-only fallback cannot produce samples" + return 1 + fi log_warning "Running benchmark tests (dry run without weight generation)" if [[ "$INCLUDE_EXTRA_BENCHMARKS" == "1" ]]; then log_warning "--extra requires frame-omni-bencher for actual extra-benchmark execution; cargo fallback remains compile-only" @@ -479,6 +580,15 @@ run_pallet_benchmark() { runtime_wasm="$(resolve_runtime_wasm_path)" || return 1 output_dir="$(dirname "$output_file")" require_directory "$output_dir" "Weight output directory" + if [[ -n "$JSON_OUTPUT" ]]; then + require_commands realpath + require_directory "$(dirname "$JSON_OUTPUT")" "Raw benchmark output directory" + if [[ "$(realpath -m -- "$JSON_OUTPUT")" == "$(realpath -m -- "$output_file")" \ + || "$JSON_OUTPUT" -ef "$output_file" ]]; then + log_error "Raw samples and generated weights require separate output files" + return 1 + fi + fi staged_output="$(mktemp "$output_dir/.${pallet_name}.weights.XXXXXX")" local bencher_args=( --runtime "$runtime_wasm" @@ -491,6 +601,16 @@ run_pallet_benchmark() { --output "$staged_output" ) + if [[ -n "$JSON_OUTPUT" ]]; then + bencher_args+=(--json-file "$JSON_OUTPUT") + fi + if [[ -n "$COMPONENT_LOW" ]]; then + bencher_args+=(--low "$COMPONENT_LOW" --high "$COMPONENT_HIGH") + fi + if [[ -n "$MIN_DURATION" ]]; then + bencher_args+=(--min-duration "$MIN_DURATION") + fi + if [[ "$INCLUDE_EXTRA_BENCHMARKS" == "1" ]]; then bencher_args+=(--extra) fi @@ -511,6 +631,11 @@ run_pallet_benchmark() { rm -f "$staged_output" return 1 fi + if [[ -n "$JSON_OUTPUT" && ! -s "$JSON_OUTPUT" ]]; then + log_error "Raw benchmark samples not generated for $pallet_name" + rm -f "$staged_output" + return 1 + fi normalize_weight_file "$pallet_name" "$staged_output" if [[ "$EXTRINSIC_PATTERN" == "*" ]] \ diff --git a/scripts/bootstrap-native-staking-local.sh b/scripts/bootstrap-native-staking-local.sh index 8c964d7e..e1983bc1 100755 --- a/scripts/bootstrap-native-staking-local.sh +++ b/scripts/bootstrap-native-staking-local.sh @@ -307,17 +307,23 @@ try { throw new Error(`Native security view rejected bounded state: ${securityViewResult.value.type}`); } const securityView = securityViewResult.value; - const securityMode = securityView.mode; - const securityReadiness = securityView.readiness; - const securityEpoch = securityView.current_epoch; + const securityState = securityView.value; + const securityMode = { type: securityView.type }; + const securityReadiness = securityView.type === "LpBackedSelection" + ? securityState.readiness + : null; + const securityEpoch = securityState.current_epoch; if (mode === "check") { - const [stakingLiquidityHot, stakingLiquidityProgram] = await Promise.all([ - api.query.Actors.ActorHot.getValue(actorId, { at: block.hash }), - api.query.Actors.ActorContract.getValue(actorId, { at: block.hash }), - ]); - const stakingLiquidityActor = stakingLiquidityHot != null && stakingLiquidityProgram != null - ? { hot: stakingLiquidityHot, program: stakingLiquidityProgram } - : null; + const actorEligibility = await api.apis.ActorEligibilityApi.actor_eligibility(actorId, { at: block.hash }); + if (!actorEligibility.success) { + throw new Error(`Actor eligibility rejected bounded state: ${actorEligibility.value.type}`); + } + const actorState = actorEligibility.value; + if (!["Active", "Dormant", "NotRegistered"].includes(actorState.type)) { + throw new Error(`Unsupported Actor eligibility variant: ${actorState.type}`); + } + // Preserve the active-only check using the runtime's canonical bounded projection. + const stakingLiquidityActor = actorState.type === "Active" ? actorState.value : null; const checks = { securityModeKnown: securityMode?.type === "TrustedSet" || securityMode?.type === "LpBackedSelection", lpBackedSelectionActive: securityMode?.type === "LpBackedSelection", diff --git a/scripts/ci-local.sh b/scripts/ci-local.sh index f3aea2d7..49104326 100755 --- a/scripts/ci-local.sh +++ b/scripts/ci-local.sh @@ -7,6 +7,7 @@ ONLY_CHECK="all" FIX_FORMAT=0 TARGET_PACKAGE="" TEST_FILTER="" +RELEASE_TESTS=0 FEATURE_MODE="auto" SKIP_WASM_BUILD="${SKIP_WASM_BUILD:-1}" if [[ "$SKIP_WASM_BUILD" == "1" ]]; then @@ -28,6 +29,7 @@ Options: --only CHECK Run one compact check: clippy, tests, docs, format, or check --package NAME Scope clippy, tests, docs, or check to one Cargo package --test-filter NAME Scope --only tests to one Cargo test-name filter + --release Use optimized tests; requires --only tests --all-features Enable all Cargo features (Clippy default) --default-features Use package default features, including for Clippy --fix Apply formatting; requires --only format @@ -77,6 +79,9 @@ parse_args() { shift 2 continue ;; + --release) + RELEASE_TESTS=1 + ;; --all-features) FEATURE_MODE="all" ;; @@ -110,6 +115,10 @@ parse_args() { log_error "--package does not apply to formatting" exit 2 fi + if (( RELEASE_TESTS == 1 )) && [[ "$ONLY_CHECK" != "tests" ]]; then + log_error "--release requires --only tests" + exit 2 + fi if [[ -n "$TEST_FILTER" && "$ONLY_CHECK" != "tests" ]]; then log_error "--test-filter requires --only tests" exit 2 @@ -170,11 +179,14 @@ run_primary_checks() { fi if selected tests; then - local feature_args + local feature_args profile_args="" feature_args="$(cargo_feature_args tests)" + if (( RELEASE_TESTS == 1 )); then + profile_args="--release" + fi run_shell_step "Tests" \ "15" \ - "cargo test --locked $feature_args $scope_args$test_filter" + "cargo test --locked $profile_args $feature_args $scope_args$test_filter" fi if selected docs; then diff --git a/scripts/validate-local.sh b/scripts/validate-local.sh index a7731ff2..a169f9cf 100755 --- a/scripts/validate-local.sh +++ b/scripts/validate-local.sh @@ -109,7 +109,7 @@ 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" + 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 -- --metadata ../web-client/.papi/metadata/deos.scale --weights runtime/src/weights/pallet_deos_actors.rs --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 diff --git a/simulator/README.md b/simulator/README.md index fd3924ee..2703b08d 100644 --- a/simulator/README.md +++ b/simulator/README.md @@ -63,13 +63,17 @@ const buy = system.router.swap_foreign_to_native(100n * PRECISION); console.log(buy.route, buy.native_out?.toString()); ``` +Configuration overrides follow the nested model shape, for example `tmc: { slope: ... }`. + --- ## Unit and scaling conventions - Token balances and prices use fixed-point `PRECISION` (`10^12`) - Fractions/ratios use `PPB` (`10^9`) and `_ppb` naming -- Arithmetic is done in `BigInt` only +- Arithmetic is done in unbounded JavaScript `BigInt` only +- `contributed_native` and `contributed_foreign` are historical cost-basis telemetry and must not be added to the same assets already counted in live XYK reserves +- Floor ratios use `PRECISION`, while pressure fractions use `PPB` This mirrors the project rule: spec model prioritizes mathematical clarity and precision before runtime constraints. @@ -93,6 +97,8 @@ It is **not** authoritative for: - Actors scheduling, actor lifecycle, or runtime adapter behavior - Governance execution, XCM, collator/session logic, or frontend flows - Market/MEV guarantees beyond the explicit economic assumptions modeled in the tests +- Runtime `U256` overflow behavior; unbounded `BigInt` scenarios prove formula determinism, not runtime arithmetic limits +- Actor retry, cooldown, scheduling, or liveness state machines For runtime behavior and pallet wiring, see implementation docs in `docs/` and tests under `template/`. diff --git a/simulator/model.js b/simulator/model.js index f229cd3e..17a42df9 100644 --- a/simulator/model.js +++ b/simulator/model.js @@ -17,6 +17,7 @@ /** @typedef {import('./types').XykConfig} XykConfig */ /** @typedef {import('./types').RouterConfig} RouterConfig */ /** @typedef {import('./types').SystemConfig} SystemConfig */ +/** @typedef {import('./types').SystemConfigOverride} SystemConfigOverride */ // --- Helpers --- @@ -220,10 +221,11 @@ export class Tol { this.bucket_keys = Object.keys(config.bucket_shares).map((key) => key.replace(/_ppb$/, ""), ); - const sum_buckets = Object.values(config.bucket_shares).reduce( - (sum, val) => sum + val, - 0n, - ); + const bucket_shares = Object.values(config.bucket_shares); + if (bucket_shares.some((share) => share < 0n || share > PPB)) { + throw new Error("Bucket shares must each be in [0, 1]"); + } + const sum_buckets = bucket_shares.reduce((sum, val) => sum + val, 0n); if (sum_buckets !== PPB) { throw new Error(`Bucket shares must sum to ${PPB}, got ${sum_buckets}`); } @@ -306,7 +308,7 @@ export class Tol { /** @type {bigint} */ total_foreign, ) { if (total_native === 0n || total_foreign === 0n) { - return Ok(this.#create_empty_zap_result()); + return Ok(this.#create_empty_zap_result(total_native, total_foreign)); } try { const result = this.xyk.add_liquidity(total_native, total_foreign); @@ -425,7 +427,7 @@ export class Tol { const fractions = {}; let sum_shares = 0n; this.bucket_keys.forEach((key) => { - const ppb_key = `${key}_ppb`; + const ppb_key = /** @type {keyof TolBucketConfig} */ (`${key}_ppb`); const share = BigMath.mul_div(total, this.bucket_config[ppb_key], PPB); shares[key] = share; sum_shares += share; @@ -441,13 +443,16 @@ export class Tol { return shares; } - #create_empty_zap_result() { + #create_empty_zap_result( + /** @type {bigint} */ leftover_native = 0n, + /** @type {bigint} */ leftover_foreign = 0n, + ) { return { total_lp_minted: 0n, total_native_used: 0n, total_foreign_used: 0n, - leftover_native: 0n, - leftover_foreign: 0n, + leftover_native, + leftover_foreign, }; } @@ -463,8 +468,8 @@ export class Tol { export class Xyk { constructor(/** @type {XykConfig} */ config) { - if (config.fee_xyk_ppb >= PPB) { - throw new Error("Fee must be < 100%"); + if (config.fee_xyk_ppb < 0n || config.fee_xyk_ppb >= PPB) { + throw new Error("Fee must be in [0, 100%)"); } this.fee_ppb = config.fee_xyk_ppb; this.reserve_native = 0n; @@ -695,6 +700,14 @@ export class Tmc { this.tol_ppb = config.mint_shares.tol_ppb; this.tol = tol; this.supply = 0n; + if ( + this.user_ppb < 0n || + this.user_ppb > PPB || + this.tol_ppb < 0n || + this.tol_ppb > PPB + ) { + throw new Error("Mint shares must each be in [0, 1]"); + } const sum_shares = this.user_ppb + this.tol_ppb; if (sum_shares !== PPB) { throw new Error(`Shares must sum to ${PPB}, got ${sum_shares}`); @@ -1008,6 +1021,12 @@ export class Router { /** @type {FeeManager} */ fee_manager, /** @type {RouterConfig} */ config, ) { + if (config.fee_router_ppb < 0n || config.fee_router_ppb >= PPB) { + throw new Error("Router fee must be in [0, 100%)"); + } + if (config.min_swap_foreign <= 0n || config.min_initial_foreign <= 0n) { + throw new Error("Router minimum amounts must be positive"); + } this.xyk = xyk; this.tmc = tmc; this.fee_manager = fee_manager; @@ -1122,7 +1141,9 @@ export function distribute_fee_sink_trusted_set(/** @type {bigint} */ amount) { }; } -export function distribute_fee_sink_permissionless_target(/** @type {bigint} */ amount) { +export function distribute_fee_sink_permissionless_target( + /** @type {bigint} */ amount, +) { const one_third = amount / 3n; return { security_rewards: one_third, @@ -1135,13 +1156,27 @@ export function distribute_fee_sink_permissionless_target(/** @type {bigint} */ // --- Factory --- export function create_system( - /** @type {Partial} */ config_override = {}, + /** @type {SystemConfigOverride} */ config_override = {}, ) { const config = { router: { ...DEFAULT_CONFIG.router, ...config_override.router }, xyk: { ...DEFAULT_CONFIG.xyk, ...config_override.xyk }, - tmc: { ...DEFAULT_CONFIG.tmc, ...config_override.tmc }, - tol: { ...DEFAULT_CONFIG.tol, ...config_override.tol }, + tmc: { + ...DEFAULT_CONFIG.tmc, + ...config_override.tmc, + mint_shares: { + ...DEFAULT_CONFIG.tmc.mint_shares, + ...config_override.tmc?.mint_shares, + }, + }, + tol: { + ...DEFAULT_CONFIG.tol, + ...config_override.tol, + bucket_shares: { + ...DEFAULT_CONFIG.tol.bucket_shares, + ...config_override.tol?.bucket_shares, + }, + }, }; const xyk = new Xyk(config.xyk); const tol = new Tol(xyk, config.tol); diff --git a/simulator/tests.js b/simulator/tests.js index a878f55f..6c80a302 100644 --- a/simulator/tests.js +++ b/simulator/tests.js @@ -141,7 +141,11 @@ const calculateRelativeSpread = (ceiling, floor) => { */ const calculateArithmeticSpread = (ceiling, floor) => ceiling - floor; -const calculateStressFloor = (reserveNative, reserveForeign, sellablePressure) => { +const calculateStressFloor = ( + reserveNative, + reserveForeign, + sellablePressure, +) => { if (reserveNative <= 0n || reserveForeign <= 0n) return 0n; if (sellablePressure < 0n) { throw new Error("Sellable pressure must be non-negative"); @@ -192,6 +196,7 @@ const TEST_SECTIONS = [ [11, 4], // Economic Security & Attack Resistance [12, 8], // Adaptive System Behaviors [13, 5], // Reporting & Conformance Metrics + [14, 3], // Custody & Floor Scenarios ]; let testCount = 0; @@ -643,8 +648,7 @@ runTest("Scaling Rules - Property Fuzz", () => { for (let i = 0; i < cases; i++) { const price_initial = BigInt(randomInt(1_000_000) + 1) * (PRECISION / 1_000_000n); - const slope = - BigInt(randomInt(1_000_000) + 1) * (PRECISION / 1_000_000n); + const slope = BigInt(randomInt(1_000_000) + 1) * (PRECISION / 1_000_000n); const system = create_system({ tmc: { price_initial, slope } }); const tmc = system.tmc; // Random supply point @@ -771,8 +775,10 @@ runTest("XYK Fee Tracking - Native to Foreign", () => { runTest("Smart Router Path Selection", () => { const system = create_system({ - price_initial: PRECISION, - slope: 100_000n, + tmc: { + price_initial: PRECISION, + slope: 100_000n, + }, }); const router = system.router; const minter = system.tmc; @@ -816,11 +822,13 @@ runTest("Edge Cases", () => { runTest("Full Integration Flow", () => { const system = create_system({ - price_initial: PRECISION, - slope: PRECISION, - shares: { - user_ppb: 333_333_333n, - tol_ppb: 666_666_667n, + tmc: { + price_initial: PRECISION, + slope: PRECISION, + mint_shares: { + user_ppb: 333_333_333n, + tol_ppb: 666_666_667n, + }, }, }); const minter = system.tmc; @@ -833,19 +841,18 @@ runTest("Full Integration Flow", () => { assert(burn_result.supply_before > 0n, "Burn successful"); }); -runTest("Overflow Protection Testing", () => { - const max_uint256 = (1n << 256n) - 1n; - const half_max = max_uint256 / 2n; - try { - const system = create_system({ - price_initial: half_max, +runTest("Large BigInt Inputs Remain Deterministic", () => { + const half_uint256 = ((1n << 256n) - 1n) / 2n; + const system = create_system({ + tmc: { + price_initial: half_uint256, slope: PRECISION / 1_000n, - }); - const result = system.tmc.mint_native(1_000n * PRECISION); - assert(result.total_minted > 0n, "Large price_initial handled"); - } catch (e) { - assert(true, "Overflow protected"); - } + }, + }); + const first = system.tmc.calculate_mint(1_000n * PRECISION); + const second = system.tmc.calculate_mint(1_000n * PRECISION); + assert(first === second, "Large BigInt calculation is deterministic"); + assert(first === 0n, "Sub-token output rounds down explicitly"); }); runTest("Safe Operating Ranges", () => { @@ -887,11 +894,13 @@ runTest("Formula Batch Execution Smoke", () => { let total_minted = 0n; for (let i = 0; i < iterations; i++) { const system = create_system({ - price_initial, - slope, - shares: { - user_ppb: 1_000_000_000n, - tol_ppb: 0n, + tmc: { + price_initial, + slope, + mint_shares: { + user_ppb: 1_000_000_000n, + tol_ppb: 0n, + }, }, }); system.tmc.supply = current_supply; @@ -973,33 +982,35 @@ runTest("Mass Conservation - System-Wide Token Accounting", () => { const total_supply = system.tmc.supply; const alice_balance = alice.get_balance().native; const bob_balance = bob.get_balance().native; - const tol_balance = system.tol.get_balance(); - const tol_native = - tol_balance.bucket_a.contributed_native + - tol_balance.bucket_b.contributed_native + - tol_balance.bucket_c.contributed_native + - tol_balance.bucket_d.contributed_native; const xyk_native = system.xyk.reserve_native; - // Mass conservation: current supply equals sum of all token balances - // Burned tokens are already excluded from supply, so no need to add them back - const total_accounted = alice_balance + bob_balance + tol_native + xyk_native; - assertApprox( - total_supply, - total_accounted, - total_accounted / 1000n, // 0.1% tolerance for rounding + const tol_buffer_native = system.tol.buffer_native; + const fee_buffer_native = system.fee_manager.buffer_native; + // Historical contribution fields describe cost basis and must not be counted + // in addition to the same native currently held by the XYK pool. + const total_accounted = + alice_balance + + bob_balance + + xyk_native + + tol_buffer_native + + fee_buffer_native; + assert( + total_supply === total_accounted, `Mass conservation violated: supply=${total_supply}, accounted=${total_accounted}`, ); // Additional verification: no negative balances assert(alice_balance >= 0n, "Alice balance cannot be negative"); assert(bob_balance >= 0n, "Bob balance cannot be negative"); - assert(tol_native >= 0n, "TOL balance cannot be negative"); + assert(tol_buffer_native >= 0n, "TOL buffer cannot be negative"); assert(xyk_native >= 0n, "XYK reserves cannot be negative"); // Verify supply is positive after operations assert(total_supply > 0n, "Total supply must be positive after mints"); // Verify all participants have accumulated tokens assert(alice_balance > 0n, "Alice should have received tokens"); assert(bob_balance > 0n, "Bob should have received tokens"); - assert(tol_native > 0n, "TOL should have accumulated tokens"); + assert( + tol_buffer_native > 0n || xyk_native > 0n, + "TOL should have accumulated native custody", + ); assert(xyk_native > 0n, "XYK should have TOL reserves"); }); @@ -1202,8 +1213,10 @@ runTest("Distribution Remainder Handling", () => { runTest("System Invariants After Heavy Use", () => { const system = create_system({ - price_initial: PRECISION / 100n, - slope: 10_000n, + tmc: { + price_initial: PRECISION / 100n, + slope: 10_000n, + }, }); for (let i = 0; i < 50; i++) { system.tmc.mint_native((100n + BigInt(i)) * PRECISION); @@ -1320,8 +1333,10 @@ runTest("Multi-User Concurrent Simulation", () => { const random = createDeterministicRandom(0xde05_2001); const randomInt = (maxExclusive) => Math.floor(random() * maxExclusive); const system = create_system({ - price_initial: PRECISION / 100n, - slope: 1_000n, + tmc: { + price_initial: PRECISION / 100n, + slope: 1_000n, + }, }); let total_minted = 0n; let total_burned = 0n; @@ -1387,14 +1402,10 @@ runTest("Extreme Load Stress Test", () => { case 1: // Buy operation const buy_amount = BigInt(randomInt(500) + 5) * PRECISION; try { - const swap_result = system.router.swap_foreign_to_native( - buy_amount, - 0n, - ); + const supply_before = system.tmc.supply; + system.router.swap_foreign_to_native(buy_amount, 0n); total_swapped += buy_amount; - if (swap_result.route === "TMC" && swap_result.tol) { - total_minted += swap_result.tol.total_minted; - } + total_minted += system.tmc.supply - supply_before; } catch (e) {} break; case 2: // Sell operation @@ -1443,8 +1454,10 @@ runTest("Extreme Load Stress Test", () => { runTest("Bootstrap Gravity Well Detection", () => { // Test the critical TOL threshold where system becomes stable const system = create_system({ - price_initial: PRECISION / 1_000n, - slope: 1_000n, + tmc: { + price_initial: PRECISION / 1_000n, + slope: 1_000n, + }, }); let tol_market_share = 0n; let stability_achieved = false; @@ -1475,8 +1488,10 @@ runTest("Bootstrap Gravity Well Detection", () => { runTest("Supply Elasticity Inversion Point", () => { // Test that after critical supply, increasing supply raises minimum price const system = create_system({ - price_initial: PRECISION / 1_000n, - slope: 500n, + tmc: { + price_initial: PRECISION / 1_000n, + slope: 500n, + }, }); const initial_supply = system.tmc.supply; let pre_inversion_floor = 0n; @@ -1522,11 +1537,17 @@ runTest("Vesting Cliff Math Trap Detection", () => { system.tmc.burn_native(burn_amount); const ceiling = system.tmc.get_price(); const floor = calculateFloorPrice(system); - const preBurnGap = calculateArithmeticSpread(pre_burn_ceiling, pre_burn_floor); + const preBurnGap = calculateArithmeticSpread( + pre_burn_ceiling, + pre_burn_floor, + ); const convergenceGap = calculateArithmeticSpread(ceiling, floor); assert(ceiling > floor, "Ceiling should exceed floor"); assert(convergenceGap > 0n, "Convergence gap exists"); - assert(convergenceGap < preBurnGap, "Burn should tighten the vesting cliff gap"); + assert( + convergenceGap < preBurnGap, + "Burn should tighten the vesting cliff gap", + ); }); runTest("Mint-Swap Feedback Loop Analysis", () => { @@ -2125,32 +2146,61 @@ runTest("Router Intelligence - XYK Route Selection", () => { runTest("Mode-Named Reward Routing - Unified Fee Collection", () => { const amount = 100n * PRECISION; const collected = collect_protocol_fee(amount); - assert(collected.fee_sink === amount, "Fee Sink should receive 100% of protocol fees"); + assert( + collected.fee_sink === amount, + "Fee Sink should receive 100% of protocol fees", + ); }); runTest("TrustedSet Reward Routing - Pool Halves", () => { const feeSinkAmount = 100n * PRECISION; const split = distribute_fee_sink_trusted_set(feeSinkAmount); - assert(split.staking_pool === 50n * PRECISION, "TrustedSet staking pool should receive half of Fee Sink flow"); - assert(split.liquidity_pool === 50n * PRECISION, "TrustedSet liquidity pool should receive half of Fee Sink flow"); - assert(split.staking_pool + split.liquidity_pool === feeSinkAmount, "TrustedSet distribution must conserve Fee Sink amount"); + assert( + split.staking_pool === 50n * PRECISION, + "TrustedSet staking pool should receive half of Fee Sink flow", + ); + assert( + split.liquidity_pool === 50n * PRECISION, + "TrustedSet liquidity pool should receive half of Fee Sink flow", + ); + assert( + split.staking_pool + split.liquidity_pool === feeSinkAmount, + "TrustedSet distribution must conserve Fee Sink amount", + ); }); runTest("Permissionless Target Reward Routing - Equal Thirds", () => { const feeSinkAmount = 60n * PRECISION; const split = distribute_fee_sink_permissionless_target(feeSinkAmount); - assert(split.security_rewards === 20n * PRECISION, "Permissionless target security rewards should receive one third"); - assert(split.staking_pool === 20n * PRECISION, "Permissionless target staking pool should receive one third"); - assert(split.liquidity_pool === 20n * PRECISION, "Permissionless target liquidity pool should receive one third"); - assert(split.fee_sink_remainder === 0n, "Divisible permissionless target flow should leave no remainder"); + assert( + split.security_rewards === 20n * PRECISION, + "Permissionless target security rewards should receive one third", + ); + assert( + split.staking_pool === 20n * PRECISION, + "Permissionless target staking pool should receive one third", + ); + assert( + split.liquidity_pool === 20n * PRECISION, + "Permissionless target liquidity pool should receive one third", + ); + assert( + split.fee_sink_remainder === 0n, + "Divisible permissionless target flow should leave no remainder", + ); }); runTest("Mode-Named Reward Routing - Remainder Conservation", () => { const collected = collect_protocol_fee(7n); assert(collected.fee_sink === 7n, "Collection must conserve dust amounts"); const trusted = distribute_fee_sink_trusted_set(collected.fee_sink); - assert(trusted.staking_pool + trusted.liquidity_pool === collected.fee_sink, "TrustedSet dust split must conserve Fee Sink amount"); - const permissionless = distribute_fee_sink_permissionless_target(collected.fee_sink); + assert( + trusted.staking_pool + trusted.liquidity_pool === collected.fee_sink, + "TrustedSet dust split must conserve Fee Sink amount", + ); + const permissionless = distribute_fee_sink_permissionless_target( + collected.fee_sink, + ); assert( permissionless.security_rewards + permissionless.staking_pool + @@ -2159,7 +2209,10 @@ runTest("Mode-Named Reward Routing - Remainder Conservation", () => { collected.fee_sink, "Permissionless target dust split plus retained Fee Sink remainder must conserve the collected amount", ); - assert(permissionless.fee_sink_remainder === 1n, "Indivisible thirds should retain their remainder in Fee Sink"); + assert( + permissionless.fee_sink_remainder === 1n, + "Indivisible thirds should retain their remainder in Fee Sink", + ); }); runTest("Economic Incentive Alignment", () => { @@ -2226,9 +2279,22 @@ runTest("Reported Floor Metric Scenario Ratios", () => { referenceCeiling, sellableSupply: 666_666n * PRECISION, }); - assertApprox(userExit.ratioPpb, 250_000_000n, 1n, "User exit should report ~25% floor ratio"); - assertApprox(systemExit.ratioPpb, 111_111_111n, 1n, "System exit should report ~11.11% floor ratio"); - assert(userExit.sellablePressure === 333_333n * PRECISION, "Default λ should use full sellable supply"); + assertApprox( + userExit.ratioPpb, + 250_000_000n, + 1n, + "User exit should report ~25% floor ratio", + ); + assertApprox( + systemExit.ratioPpb, + 111_111_111n, + 1n, + "System exit should report ~11.11% floor ratio", + ); + assert( + userExit.sellablePressure === 333_333n * PRECISION, + "Default λ should use full sellable supply", + ); }); runTest("Ledger Conservation With Burns", () => { @@ -2260,11 +2326,23 @@ runTest("Burn Liveness Threshold Behavior", () => { }); system.tmc.mint_native(1_000n * PRECISION); system.fee_manager.receive_fee_foreign(5n * PRECISION); - assert(system.fee_manager.buffer_foreign === 5n * PRECISION, "Sub-threshold foreign fees should remain buffered"); - assert(system.fee_manager.total_native_burned === 0n, "Sub-threshold foreign fees should not burn native"); + assert( + system.fee_manager.buffer_foreign === 5n * PRECISION, + "Sub-threshold foreign fees should remain buffered", + ); + assert( + system.fee_manager.total_native_burned === 0n, + "Sub-threshold foreign fees should not burn native", + ); system.fee_manager.receive_fee_foreign(10n * PRECISION); - assert(system.fee_manager.buffer_foreign === 0n, "Threshold-crossing foreign fees should be consumed"); - assert(system.fee_manager.total_native_burned > 0n, "Threshold-crossing foreign fees should burn native when liquidity exists"); + assert( + system.fee_manager.buffer_foreign === 0n, + "Threshold-crossing foreign fees should be consumed", + ); + assert( + system.fee_manager.total_native_burned > 0n, + "Threshold-crossing foreign fees should burn native when liquidity exists", + ); }); runTest("Zap Bucket Accounting Conservation", () => { @@ -2277,12 +2355,30 @@ runTest("Zap Bucket Accounting Conservation", () => { result.tol.bucket_d, ]; const lpTotal = buckets.reduce((sum, bucket) => sum + bucket.lp_tokens, 0n); - const nativeTotal = buckets.reduce((sum, bucket) => sum + bucket.contributed_native, 0n); - const foreignTotal = buckets.reduce((sum, bucket) => sum + bucket.contributed_foreign, 0n); - assert(lpTotal === result.tol.total_lp_minted, "Bucket LP accounting should conserve total LP minted"); - assert(nativeTotal === result.tol.total_native_used, "Bucket native accounting should conserve total native used"); - assert(foreignTotal === result.tol.total_foreign_used, "Bucket foreign accounting should conserve total foreign used"); - assert(result.tol.bucket_a.lp_tokens > 0n, "Bucket_A should be classifiable as anchor-support LP after initial mint"); + const nativeTotal = buckets.reduce( + (sum, bucket) => sum + bucket.contributed_native, + 0n, + ); + const foreignTotal = buckets.reduce( + (sum, bucket) => sum + bucket.contributed_foreign, + 0n, + ); + assert( + lpTotal === result.tol.total_lp_minted, + "Bucket LP accounting should conserve total LP minted", + ); + assert( + nativeTotal === result.tol.total_native_used, + "Bucket native accounting should conserve total native used", + ); + assert( + foreignTotal === result.tol.total_foreign_used, + "Bucket foreign accounting should conserve total foreign used", + ); + assert( + result.tol.bucket_a.lp_tokens > 0n, + "Bucket_A should receive its configured LP share after initial mint", + ); }); runTest("Stress Floor Monotonicity", () => { @@ -2298,7 +2394,10 @@ runTest("Stress Floor Monotonicity", () => { reserveForeign, 300n * PRECISION, ); - assert(smallSell > largeSell, "Stress floor should decrease as sellable pressure increases"); + assert( + smallSell > largeSell, + "Stress floor should decrease as sellable pressure increases", + ); }); // FINAL STATISTICS diff --git a/simulator/tests.md b/simulator/tests.md index 812e5244..e76576cd 100644 --- a/simulator/tests.md +++ b/simulator/tests.md @@ -204,12 +204,12 @@ Multi-component workflows and system-level behavior validation. - Validates: End-to-end flow produces consistent state - Failure Criteria: State inconsistency, balance mismatch, or operation failure -### 5.3 Overflow Protection Testing +### 5.3 Large BigInt Inputs Remain Deterministic -- Nature: Tests near-maximum uint256 operations -- Necessity: Critical for preventing financial exploits -- Validates: Safe multiplication, no silent overflows -- Failure Criteria: Overflow occurs or result wraps around +- Nature: Repeats one near-uint256-sized price calculation with unbounded JavaScript `BigInt`. +- Necessity: Confirms deterministic rounding without pretending to reproduce runtime `U256` overflow behavior. +- Validates: Equal inputs produce equal outputs and sub-token mint output rounds to zero explicitly. +- Failure Criteria: Repeated outputs differ, the operation throws, or a sub-token amount becomes a positive token. ### 5.4 Safe Operating Ranges @@ -238,10 +238,10 @@ Validation of system invariants and participant class integration with core toke ### 6.2 Mass Conservation - System-Wide Token Accounting -- Nature: Fundamental conservation law for token supply -- Necessity: Proves tokens cannot be created or destroyed outside designed mechanisms (minting, TOL lock) -- Validates: Total supply exactly equals user balances + TOL reserves (across all 4 buckets); no tokens lost to rounding; supply tracking remains consistent across all operations -- Failure Criteria: Supply mismatch >1 wei; tokens disappear or appear unexpectedly; balance sum ≠ total supply +- Nature: Fundamental conservation law for live native custody. +- Necessity: Prevents historical TOL contribution telemetry from being double-counted with the same assets held in XYK. +- Validates: Total supply exactly equals user balances, live XYK reserve, TOL buffer, and fee buffer. +- Failure Criteria: Exact live-custody sum differs from total supply or any balance becomes negative. ### 6.3 TOL Independence - Participant Sales Don't Touch TOL @@ -532,14 +532,14 @@ Executable vectors for durable TMCTOL reporting metrics and conformance boundari ### 13.4 Zap Bucket Accounting Conservation - Nature: Tests bucket-level LP/native/foreign accounting after Zap allocation. -- Necessity: Supports the floor-reporting bucket state model with executable accounting conservation. -- Validates: Sum of bucket LP/native/foreign fields equals aggregate Zap totals and Bucket_A receives classifiable LP. -- Failure Criteria: Bucket sums diverge from aggregate totals or Bucket_A cannot be classified as anchor LP. +- Necessity: Keeps configured TOL allocation accounting exact without introducing lifecycle semantics. +- Validates: Sum of bucket LP/native/foreign fields equals aggregate Zap totals and Bucket_A receives its configured share. +- Failure Criteria: Bucket sums diverge from aggregate totals or Bucket_A receives no LP. ### 13.5 Stress Floor Monotonicity - Nature: Tests stress-floor response to larger sellable pressure. -- Necessity: Pins the canonical `P_stress(x)` direction used by floor reports. +- Necessity: Pins the canonical `P_stress(x)` direction used by floor scenarios. - Validates: Larger sellable pressure lowers the stress floor for fixed reserves. - Failure Criteria: Stress floor increases or stays equal as sellable pressure grows. diff --git a/simulator/types.d.ts b/simulator/types.d.ts index cf2f5c64..efbe239b 100644 --- a/simulator/types.d.ts +++ b/simulator/types.d.ts @@ -32,8 +32,19 @@ export type SystemConfig = { tol: TolConfig; }; +export type SystemConfigOverride = { + router?: Partial; + xyk?: Partial; + tmc?: Omit, "mint_shares"> & { + mint_shares?: Partial; + }; + tol?: { + bucket_shares?: TolBucketConfig; + }; +}; + export type BucketBalance = { - lp_tokens: bigint; + balance_lp: bigint; contributed_native: bigint; contributed_foreign: bigint; }; diff --git a/template/Cargo.lock b/template/Cargo.lock index 66a18036..a668874a 100644 --- a/template/Cargo.lock +++ b/template/Cargo.lock @@ -2848,9 +2848,9 @@ dependencies = [ [[package]] name = "cranelift-codegen" -version = "0.123.13" +version = "0.123.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daa4a357d030bdd586d8fe3da56b394f7f6b6ded506e59f945e7b32b1e126b71" +checksum = "9f30aeb2de7f97d6f26b4a1642615834daad58e2e4d7c027810010a3a32f22be" dependencies = [ "bumpalo", "cranelift-assembler-x64", @@ -2875,9 +2875,9 @@ dependencies = [ [[package]] name = "cranelift-codegen-meta" -version = "0.123.13" +version = "0.123.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4121e36a8757dea6fb237435ee5095eba25bc13ecc2eaee57eb9bffd4b27784f" +checksum = "cd5dd137fcdedef33b6fd40edf1ced024460d764ceb75833e8198a843395945c" dependencies = [ "cranelift-assembler-x64-meta", "cranelift-codegen-shared", @@ -2914,9 +2914,9 @@ dependencies = [ [[package]] name = "cranelift-frontend" -version = "0.123.13" +version = "0.123.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90a3a277b1a0aff1123f6bae61c080a4bcb6df964829ed427f98e18dff14257f" +checksum = "ab5976c0ff5bfadf61cd8bda81fea78ee5a07018b9cd03e66c0952c56684928b" dependencies = [ "cranelift-codegen", "log", @@ -2926,15 +2926,15 @@ dependencies = [ [[package]] name = "cranelift-isle" -version = "0.123.13" +version = "0.123.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be53dd9b3a4cbeb9ced45c4a543ea8417ccfb334c3ba1cbb2233f4b25fb2531f" +checksum = "77b4f73d2288e9480fd2d1d9ab576394dce4805443d6148c6d819dbf78865ce4" [[package]] name = "cranelift-native" -version = "0.123.13" +version = "0.123.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca62ab1d9f48cad97da5843b913ccf96c3dfde935af5d750cb5a6d367ccfb262" +checksum = "fe9650c2baf22fa1e2542a5bdd8152616ec2023d929c4cbb450ff677ad8d9c21" dependencies = [ "cranelift-codegen", "libc", @@ -4097,7 +4097,7 @@ dependencies = [ [[package]] name = "deos-primitives" -version = "0.7.24" +version = "0.7.25" dependencies = [ "parity-scale-codec", "polkadot-sdk", @@ -4109,10 +4109,11 @@ dependencies = [ [[package]] name = "deos-runtime" -version = "0.7.24" +version = "0.7.25" dependencies = [ "cumulus-pallet-parachain-system", "cumulus-primitives-parachain-inherent", + "cumulus-primitives-proof-size-hostfunction", "cumulus-test-relay-sproof-builder", "deos-primitives", "docify", @@ -4128,6 +4129,7 @@ dependencies = [ "pallet-deos-tmc", "parity-scale-codec", "polkadot-sdk", + "sc-executor", "scale-info", "serde_json", "smallvec", @@ -4679,7 +4681,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -6592,7 +6594,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -6601,7 +6603,7 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82cb6a9f675da968c63b6208c641b9dca58fc0133ae53375736b1767b0cab8bd" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -9404,7 +9406,7 @@ dependencies = [ [[package]] name = "pallet-deos-actors" -version = "0.7.24" +version = "0.7.25" dependencies = [ "parity-scale-codec", "polkadot-sdk", @@ -9418,7 +9420,7 @@ dependencies = [ [[package]] name = "pallet-deos-actors-embedding-fixture" -version = "0.7.24" +version = "0.7.25" dependencies = [ "pallet-deos-actors", "parity-scale-codec", @@ -9428,7 +9430,7 @@ dependencies = [ [[package]] name = "pallet-deos-asset-registry" -version = "0.7.24" +version = "0.7.25" dependencies = [ "deos-primitives", "parity-scale-codec", @@ -9440,7 +9442,7 @@ dependencies = [ [[package]] name = "pallet-deos-governance" -version = "0.7.24" +version = "0.7.25" dependencies = [ "parity-scale-codec", "polkadot-sdk", @@ -9450,7 +9452,7 @@ dependencies = [ [[package]] name = "pallet-deos-oracle" -version = "0.7.24" +version = "0.7.25" dependencies = [ "parity-scale-codec", "polkadot-sdk", @@ -9460,7 +9462,7 @@ dependencies = [ [[package]] name = "pallet-deos-router" -version = "0.7.24" +version = "0.7.25" dependencies = [ "deos-primitives", "pallet-deos-tmc", @@ -9475,7 +9477,7 @@ dependencies = [ [[package]] name = "pallet-deos-router-embedding-fixture" -version = "0.7.24" +version = "0.7.25" dependencies = [ "pallet-deos-router", "parity-scale-codec", @@ -9485,7 +9487,7 @@ dependencies = [ [[package]] name = "pallet-deos-session-rotation" -version = "0.7.24" +version = "0.7.25" dependencies = [ "parity-scale-codec", "polkadot-sdk", @@ -9495,7 +9497,7 @@ dependencies = [ [[package]] name = "pallet-deos-staking" -version = "0.7.24" +version = "0.7.25" dependencies = [ "parity-scale-codec", "polkadot-sdk", @@ -9505,7 +9507,7 @@ dependencies = [ [[package]] name = "pallet-deos-tmc" -version = "0.7.24" +version = "0.7.25" dependencies = [ "deos-primitives", "parity-scale-codec", @@ -10102,7 +10104,7 @@ dependencies = [ [[package]] name = "pallet-oracle-embedding-fixture" -version = "0.7.24" +version = "0.7.25" dependencies = [ "pallet-deos-oracle", "parity-scale-codec", @@ -13565,9 +13567,9 @@ dependencies = [ [[package]] name = "pulley-interpreter" -version = "36.0.13" +version = "36.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2662666315cb90dfb4d99a652ee053d4d8598f71c474209e84da031ca56ae5a4" +checksum = "eb0a4b56042e461cc64456650182938e2d1ede98fa0c8a975027416a2809c414" dependencies = [ "cranelift-bitset", "log", @@ -13577,9 +13579,9 @@ dependencies = [ [[package]] name = "pulley-macros" -version = "36.0.13" +version = "36.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb9a7d9ed2618f94b6d054aba3eb2768c9b489fe16b4ef8847fbc6ed41b707bb" +checksum = "244667bea2e214273442a71f26adb12b88a41f66718fb2c6eea47c00f0dc325f" dependencies = [ "proc-macro2", "quote", @@ -13683,7 +13685,7 @@ dependencies = [ "once_cell", "socket2 0.5.10", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -14568,7 +14570,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -14581,7 +14583,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -14639,7 +14641,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs 0.26.11", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -18639,7 +18641,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix 1.1.4", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -18658,7 +18660,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix 1.1.4", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -19837,9 +19839,9 @@ dependencies = [ [[package]] name = "wasmtime" -version = "36.0.13" +version = "36.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d881c5dcff5f230368d84fcf110ca25fe47badc694e7d559c3891492159916" +checksum = "7d05c745dc0978e589ef295958f3130122afc33d96af6bad3f0f06dbe7ac43a8" dependencies = [ "addr2line", "anyhow", @@ -19885,9 +19887,9 @@ dependencies = [ [[package]] name = "wasmtime-environ" -version = "36.0.13" +version = "36.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4b25534a3ff9dd844701c2bc997f76cb1f97bf2af0546a7066ae96f49c2e068" +checksum = "9fd1d43cfaa1a0859d2f4fccc15e7e571e2a88b357e81bc88ba6c501b83d925d" dependencies = [ "anyhow", "cpp_demangle", @@ -19910,18 +19912,18 @@ dependencies = [ [[package]] name = "wasmtime-internal-asm-macros" -version = "36.0.13" +version = "36.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d02832d760351fb3a1aa99eb8f7b95596c0f4e4e52df1547fcdb295ea5c780c" +checksum = "515dd7158bf1719b41290cd2e6a2a46ec944484146816992f195af3720e49b3f" dependencies = [ "cfg-if", ] [[package]] name = "wasmtime-internal-cache" -version = "36.0.13" +version = "36.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "651f8a16b51cde3ee8bd5b775bcedc24b66040d7dca1f13ad855b10c660e1d01" +checksum = "12a53145473629ea40f445235ed95182b76940f00a67c3c9c6c4857dae0ad823" dependencies = [ "anyhow", "base64", @@ -19939,9 +19941,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-cranelift" -version = "36.0.13" +version = "36.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65821bab751956cddfc6ee957beb13a0e2a6cf682050d75dfb7a0228db2ed499" +checksum = "5ba1736927b58e50e741e407da7c037c0250f3e213833a09c89dcd8f73ae2eac" dependencies = [ "anyhow", "cfg-if", @@ -19966,9 +19968,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-fiber" -version = "36.0.13" +version = "36.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18cf73e5e8d28a2b30d86454430c7dea3b593598dcbbc0ebb927ca2716222d41" +checksum = "7b238e4c20bddb900ec0cb380252d63e8d0644fd94de001119574f5921e895d9" dependencies = [ "anyhow", "cc", @@ -19982,9 +19984,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-jit-debug" -version = "36.0.13" +version = "36.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4279dc3147ddaa21e5b3d2c0eaff117881c4f937747bdd2379eb361665843bd3" +checksum = "8f259b13685ad51e3dcf58cb69031279ed0d79c25bc3ccc8b50e7160ed04fbfe" dependencies = [ "cc", "object 0.37.3", @@ -19994,9 +19996,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-jit-icache-coherence" -version = "36.0.13" +version = "36.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8eb677944201839c0be19b39b0f19dcd663efbcbc05a09c73f26fec7bdf0331" +checksum = "41fed85537936b16460bac352ad149052c025db50467c7bc539dd47b31439374" dependencies = [ "anyhow", "cfg-if", @@ -20006,24 +20008,24 @@ dependencies = [ [[package]] name = "wasmtime-internal-math" -version = "36.0.13" +version = "36.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db9a153e184878df396a11f8912444531c2e4d0c7a1d9e9a52b30d9853d89cb9" +checksum = "82fff10da41d0d15d90ebba70946a0aa16ed0957ae7b77e0b6d2a46e8221e555" dependencies = [ "libm", ] [[package]] name = "wasmtime-internal-slab" -version = "36.0.13" +version = "36.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b771494bead25e1f0c4c89a9476dd0b65eacca314ed82125f1e197fbc3f0396b" +checksum = "e44a8c097bab08d349d57dce1ab818859fefbe261ab3632b38fe127b1b551108" [[package]] name = "wasmtime-internal-unwinder" -version = "36.0.13" +version = "36.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90199caed6925420434a923861d8ad813689ca8533d2c679f805d12e35b40a4b" +checksum = "7f40a57d5e7c221ce56391d7dca0a918ba17ea00185462c7facbf534d7745184" dependencies = [ "anyhow", "cfg-if", @@ -20034,9 +20036,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-versioned-export-macros" -version = "36.0.13" +version = "36.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae71687aa834f9cc9eb5b0f97c85184d7dc70b84d32fd8927af0887998a1ba35" +checksum = "e085bfce1cb2089dbeef6e280a5d598666923d3dcd308712fe429fe43c9d19f5" dependencies = [ "proc-macro2", "quote", @@ -20045,9 +20047,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-winch" -version = "36.0.13" +version = "36.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0ba45c0d766dd56257d97702d3284b6f69efdd4c53ca981a0754cc0a139df0" +checksum = "c4916cd526e1ce294984cc5b70264cfc0ca103b41ca665b58728bde250b6b82f" dependencies = [ "anyhow", "cranelift-codegen", @@ -20263,7 +20265,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.48.0", ] [[package]] @@ -20274,9 +20276,9 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "winch-codegen" -version = "36.0.13" +version = "36.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82ea9625459ce35d6a4188cf0f07c9095cbb0e5afd86f711d63955bfc4216e64" +checksum = "e826c012c68403725e77adf6b904c2ea809e5d464aaf25aa6eda14559300b3df" dependencies = [ "anyhow", "cranelift-assembler-x64", diff --git a/template/Cargo.toml b/template/Cargo.toml index 1b8bf546..850e2531 100644 --- a/template/Cargo.toml +++ b/template/Cargo.toml @@ -1,5 +1,5 @@ [workspace.package] -version = "0.7.24" +version = "0.7.25" license = "MIT-0" authors = ["DEOS Contributors"] homepage = "https://github.com/atmo-network/deos" diff --git a/template/README.md b/template/README.md index 660c4279..c0aa403a 100644 --- a/template/README.md +++ b/template/README.md @@ -3,7 +3,7 @@ This directory is the Rust/FRAME implementation workspace for DEOS. It contains the reusable parachain runtime components that realize the living contracts described in `../docs/` and, when tokenomics or invariants change, can be cross-checked against the mathematical reference in `../simulator/`. The current flagship economic standard shipped on this runtime kernel is TMCTOL. -The `0.7.22` reference runtime supports fresh genesis only. It is not an upgrade artifact for a `0.7.21` chain, carries no deployed storage lineage, and provides no `0.7.21`-to-`0.7.22` migration contract. +The pre-`1.0` reference runtime supports fresh genesis only. It is not an upgrade artifact for an earlier release, carries no deployed storage lineage, and provides no production migration contract. ## Purpose diff --git a/template/pallets/actors/Cargo.toml b/template/pallets/actors/Cargo.toml index 02fdaf00..5495bd19 100644 --- a/template/pallets/actors/Cargo.toml +++ b/template/pallets/actors/Cargo.toml @@ -12,7 +12,7 @@ keywords = ["substrate", "frame", "automation", "actors"] categories = ["cryptography::cryptocurrencies"] edition.workspace = true publish = false -include = ["Cargo.toml", "README.md", "docs/**/*.md", "src/**/*.rs", "examples/**/*.rs"] +include = ["Cargo.toml", "README.md", "docs/**/*.md", "src/**/*.rs", "src/tests/fixtures/*.tsv", "tests/fixtures/*.json", "examples/**/*.rs"] [lib] name = "pallet_deos_actors" @@ -47,6 +47,7 @@ polkadot-sdk = { workspace = true, features = [ "sp-core", "sp-io", "sp-runtime", + "sp-trie", ] } [features] diff --git a/template/pallets/actors/README.md b/template/pallets/actors/README.md index 9346d607..78e7235c 100644 --- a/template/pallets/actors/README.md +++ b/template/pallets/actors/README.md @@ -75,6 +75,18 @@ Minimal checklist: - 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 run suspension with runtime-local tests when adapters perform multi-step mutations. +## Fee-envelope evidence example + +The package-owned example generates deterministic fee-envelope vectors bound to an embedding runtime's metadata and production Actors weights. It requires explicit artifact paths and does not require a DEOS repository layout: + +```sh +cargo run -p pallet-deos-actors --example fee_envelope_vectors -- \ + --metadata /path/to/metadata.scale --weights /path/to/actors_weights.rs \ + --check /path/to/fee-envelope-vectors.json +``` + +Replace `--check PATH` with `PATH` to write the artifact, or omit it to print JSON. `--help` describes the interface without loading artifacts. Missing inputs fail closed; supplied files are hashed exactly, not validated as runtime metadata or benchmark output by this utility. + ## Non-goals of the current slice The current kernel does not yet include: diff --git a/template/pallets/actors/docs/architecture.en.md b/template/pallets/actors/docs/architecture.en.md index c27b0f0f..7d6699a4 100644 --- a/template/pallets/actors/docs/architecture.en.md +++ b/template/pallets/actors/docs/architecture.en.md @@ -20,9 +20,9 @@ The crate assigns no economic roles, assets, recipients, routes, actor IDs, or c 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 +5. `Hot-state decomposition`: the primary control cell carries bounded scheduler/admission facts; certified Contract, funding, and run payloads remain separate cold owners. -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. +Canonical writes target one locator-resolved `ActorControlCell`, certified Contract geometry, and bounded `ActorFunding`. Funding retains tracked-asset state without Trigger-family amounts or fee reserves. `ActorStateHolds` records a refundable User geometry quote: its run component reserves installed-lifetime capacity, so autonomous Opening requires no new owner hold. Dormant Actors release that component; 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. @@ -68,37 +68,39 @@ Current owner-slot representation is fixed-width and runtime-shaped: 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`, one queue membership, independent Pipeline-service and Trigger-temporal wakeup pointers, terminal block, `schedule_anchor`, and optional `last_cycle_block` +- `ActorIdentities`: dormant identity only; active identity belongs to the unique primary control cell +- `ActorControlCell`: active identity, lifecycle/run state, failure counter, readiness latch, independent Block/Tick pointers, terminal block, schedule anchor, cursor/eligibility, admission certificate, and resource envelope; `ActorControlLocators` resolves its Unsignaled/Ready/Waiting owner - `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 +- `ActorControlCell.admission`: compact runtime semantics/layout/Weight and lifecycle identity, 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. +`ActorCreated` carries `actor_id`, owner, `actor_class`, mutability, sovereign account, and `initial_lifecycle`; User slot or System custody locator lives inside `ActorClass`. `actor_id` identifies the primary and its locator; dormant identity retains its last control-mutation block. Public creation admits Dormant only as Mutable, while host genesis may install a sealed Immutable Dormant System identity that generic activation and owner-close control reject. Activation or schedule replacement derives block eligibility from `schedule_anchor` and window start, while `ActorControlCell.hot.trigger_runtime_state::Cadenced` owns the optional timestamp-ceiled anchor tick. The typed lifecycle forbids contradictory pause state. -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. +Package internals reconstruct execution state from the locator-resolved primary, certified Contract geometry, `ActorFunding`, and optional `ActorRunHeads`/`ActorRunPayloads`. Observation fanout uses fixed activation authority and the mutable run head without decoding generic funding, immutable run payload, or unreached Steps. `active_actor_state` exposes 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 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. +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 host-configured `MaxContractSteps` in `1..=255` applies identically to User and System actors across creation, activation, replacement, simulation, genesis, and benchmark construction. For a nonempty N-Step Contract, the Step-0 control context charges exactly `ceil((N - 1) / 4)` authored Opening tail chunks; 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` - `on_error: StepErrorPolicy` (`AbortCycle` / `ContinueNextStep` / Mutable-only `RetryLater { max_attempts }`) -`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. +`None` is the sole unconditional Step form. `Some(Precondition { clauses })` stores one bounded DNF: outer clauses are OR and each inner clause is AND. Admission rejects empty outer and inner vectors and enforces the configured `MaxPreconditionClauses`, `MaxPredicatesPerClause` and total `MaxPredicatesPerStep` bounds. Evaluation visits 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; 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. -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. +`Precondition::evaluation_units` supplies `total predicates + Opening predicates` to the host's `StepControlWeight` resource-envelope provider. It is a pricing hint, not a count of current host reads. `evaluate_step_precondition` visits each predicate once: Current evaluates the atomic predicate, while Opening reads its saved result and advances the result cursor. Opening capture has a separate control-context count. The prepaid Pipeline Machine envelope sums admitted Step control bounds and cleanup; the package does not split this hint into repeated predicate-component charges. + +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. @@ -181,13 +183,31 @@ 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`. -`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. +`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 currently uses generated `scheduler_inner_zero_step_complete`; EXP-0037 accepts a scoped Manual System/User partition and complete maximum-head User owner, EXP-0038 accepts the mutually exclusive AtTime/Cadenced persistence/authored-close owners, and EXP-0039 accepts available Crossing persistence/authored-close envelopes plus a state-preserving unavailable refusal owner. Neither temporal nor Crossing Opening charges a second Trigger fee; unavailable Crossing collects no Pipeline fee. + +EXP-0040 accepts one component-wise Observation authored-close owner across dense/sparse subscription pages, middle subscriber/feed unlink, dirty-cursor repair and existing/new free-page publication. That inclusive owner already contains Pipeline collection, User hold reconciliation and terminal cleanup. Outer source/Waiting composition and complete production binding still require G4/G5 closure before numerical release acceptance. + +EXP-0041 accepts the attained reference maximum-Step Crossing plus singleton future-window Pipeline-apoptosis atom at `709,110,000 / 86,870` before database additions, with 70 reads and 65 writes. It includes canonical Ready consumption, three cold Contract-tail chunks, Crossing cleanup, Block-reference invalidation and applicable User hold release, with no Pipeline receipt or custody movement. This complete branch must not receive a second generic source or cleanup charge. Populated Waiting/heap composition remains EXP-0063 work, and EXP-0065 owns exact regeneration. + +EXP-0042 accepts the admitted joint maximum-Step Suspended Run/Crossing public-close atom at `(O=4, P=12)`: `742,998,816 / 38,096` before database additions, with 34 reads and 32 writes. It includes Contract/tail and Run deletion, a singleton future Waiting removal, real deferred-latch/live-cursor repair and ordinary User Account/Hold release, while preserving custody and charging no new fee. Observation cleanup is a mutually exclusive branch. Populated Waiting/heap alternatives remain EXP-0063 work and must not duplicate this atom's Contract/Run/lifecycle cleanup; EXP-0065 owns exact regeneration. + +EXP-0058 retains corrected generic Ready-consume and Queue-publication primitives whose fixtures begin from loaded or consumed authority. The per-entry settlement seam replaces provisional discovery/probe/source accounting when admission selects a complete apoptosis, terminal-close, or Crossing-unavailable refusal owner, while retaining unrelated earlier and later FIFO work. + +Crossing availability is selected only after terminal and User Pipeline-capacity precedence; an available value/revision is carried into transactional rearm without a second provider read. Pause already demotes its Ready primary to Unsignaled atomically, so later service sees only a stale tombstone; breaker refusal leaves the live Ready owner and exact FIFO position unchanged after discovery/probe rather than consuming and republishing it. The experiment remains Measuring because executed-Step Run/placement fallbacks and Waiting input are open. No synthetic coefficient may be derived by subtraction, and production regeneration remains EXP-0065 work. + +EXP-0043 accepts the unsignaled Immutable zero-Step Manual User singleton-Block expiry atom at `261,001,000 / 6,367` before database additions, with 24 reads and 21 writes. It includes canonical Block-source and Waiting authority removal, loaded expiry classification, lifecycle/process/deadline cleanup and applicable User hold release while preserving custody and producing no Pipeline receipt. Outer clock selection and populated Waiting/heap alternatives remain EXP-0063 work; generic source or terminal cleanup must not be charged again, and EXP-0065 owns exact regeneration. + +EXP-0044 accepts post-publication User hold reconciliation only inside its matching complete transition owner. The attained maximum-Step User Opening owner is `536,459,000 / 14,158` before database additions, with 24 reads and 13 writes. It publishes Running state, preserves identity/Contract/detector/active-lifetime Run hold components, decreases consumed funding geometry, and reads/writes both `ActorStateHolds` and the host hold ledger once. No standalone hold coefficient may be added or derived by subtraction; EXP-0065 owns exact regeneration. + +EXP-0047 accepts the nonzero funding-consumption component across the complete reference authored-source domain `1..24`: `14,154,244 + 288,800a` pre-database RefTime, fixed `4,531` estimated ProofSize and one `ActorFunding` read/write. Zero selects no component. The 40-entry configured capacity remains a conservative storage/admission input rather than a reachable actual snapshot; complete configured-capacity composition and generated-file regeneration remain EXP-0064/EXP-0065 work. + +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. +Insufficient `MinUserBalance + Pipeline total` selects `CycleAdmissionInsufficient` before Opening; Running/Suspended reuse prepaid machine authority. A Suspended User separately checks current Action liability before effect-capacity deferral: insolvency closes fee-free with custody unchanged, while a solvent deferred retry retains its complete Run and FIFO head (`user_retry_insolvency_closes_before_effect_capacity_deferral`). `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. `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. +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 prices bounded hot/control capacity with the actual Contract 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: @@ -205,17 +225,37 @@ 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. +Ready tombstone discovery validates physical pages, exact locators and canonical cell projection without loading cold Actor partitions. Service rejects an ordinary future head from inline eligibility, preserving paused/breaker handling and inline terminal exceptions. A nonterminal Running head rejects insufficient aggregate/control Weight for consume, inline Step and one cleanup allowance, or insufficient effect-domain capacity, before cold loading. Checked sums fall through on overflow. The capacity probe reuses reservation logic on a fixed local copy and reports only `LimitExceeded`; phase, halt and overflow retain their ordinary owners. This negative preflight never admits execution; cold binding validation, actual admission and effect reservations remain authoritative. + +Suspended resource refusal first reads the Run head and current certified Step to preserve retry exhaustion and Action liability. A coherent solvent retry needs neither Funding nor Run payload; User liability still reads its native balance. Insolvency and exceptional paths retain full classification and cleanup. `suspended_weight_refusal_avoids_run_payload_and_funding_reads` records trie reads for Temporary/FundingUnavailable, User/System, head/tail cursor and independently scarce aggregate/control/effect RefTime and ProofSize before the last allowed attempt, with unchanged state and later progress controls. Expired-window tests retain cleanup precedence under pause and insolvency, while the breaker preserves Run. These native witnesses do not establish a complete production Weight envelope. + +A trie-recorder regression excludes Contract head/tail, Funding and Run payload reads for a real future Q1 successor and independently insufficient RefTime/ProofSize in aggregate, control, Actor base-turn and user-saturated Shared Economic budgets. It also covers aggregate/control budgets that fit the Step but lack cleanup, then completes the same Run with an ample positive control. These local service cases do not establish full Prepass or Executive throughput. Cold binding corruption may remain undiagnosed until service passes these preflights. Execution loads only the certificate-gated current Step without reconstructing an unreached tail; consumption validates ticket/topology and passes exact source authority to the evaluator. + +Next-work scheduling requires caller-supplied hot state, identity, Run, admission and Step resources. Running/Suspended placement rejects a missing or incoherent supplied Run without falling back to storage; Idle may carry no Run. Retained entry points validate source authority before calling the same kernel used by consumed sources. Window and retry placement pass that authority directly to the transactional wakeup publisher; neither branch reconstructs it. Successful Ready-capacity fallback reports `StepControlPlacement::Wakeup` to actual Weight selection, matching the published Waiting primary. Running coherence and Suspended retry validation remain shared with their existing owners. + +The loaded effectful execution core returns the exact persisted successor Run for progress and suspension, or no Run for terminal outcomes. Production placement consumes that returned value without reloading Run backing; the attempted ticket and Step remain unchanged for resource and fee settlement. Fresh Idle execution rejects either existing raw Run partition before snapshots or effects, so a missing or corrupt payload cannot be mistaken for absent continuation authority. + +Opening prepares Trigger rearm within the consumed-source execution transaction. Cadenced prepares a fresh Tick reference; Crossing rebuilds membership from current observation while preserving the incoming latch until the common core consumes it; ObservationChange removes its disabled marker. AtTime sets its consumed flag, including when Contract replacement retained another Trigger's paid latch. Manual and AddressEvent require no rearm mutation. The execution successor publishes the resulting hot state; no separate retained-primary rearm publisher intervenes. + +Ready invalidation loads complete service authority once and uses its current Step resources, identity, Run and admission certificate to remove the primary and restore Unsignaled state in one transaction. Optional invalidation returns no ticket for an absent or non-Ready source; the temporal caller requires an actual Ready transition. Corrupt active authority fails without changing storage, and temporal invalidation resolves state after activation mutations. + +Queue-capacity fallback checks next-block overflow, then derives its placement view, Run, admission and Step resources from one complete retained-state load before invoking the transactional wakeup publisher. Initial installation retains its validated service tuple through Trigger and window placement; after Tick publication it refreshes canonical hot authority before subsequent placement, preserving the new pointer without another Contract/Run load. Prime paths share initial AtTime/Cadenced deadline calculation but retain distinct placement order and pointer guards. Frame priming publishes fresh Tick placement directly from entry-loaded authority. The shared publisher applies the earlier window-terminal bound; admission forbids temporal Triggers with ScheduleWindow. + +Activation plans carry the validated admission certificate and original source state as mandatory values. Cancellation likewise always carries admission, but retains distinct retained-source and consumed-source variants because publication must respect whether the original primary still exists. Loaded cancellation uses the supplied Run rather than selecting it again from backing storage; no Run is a no-op only for Idle state, while non-Idle without Run fails before writes. No mode selector chooses between scalar and canonical authority. + +Activation preflight and commit are synchronous, with no intervening Actor mutation. Deferred activation uses the plan's source identity, Run and admission rather than reloading complete service state; current-Step resources remain certificate-validated inside the wakeup transaction, with the dedicated control envelope for zero-Step Contracts. Capacity fallback and future-window placement retain prospective latch publication, exact deadlines and rollback. An activation plan is not a durable command valid after its source changes. + +Retained enqueue and wakeup ingress pass their first loaded service tuple into canonical preparation instead of discarding its certificate/current Step and loading them again. Prospective-hot callers retain their separate supplied-state path because their intended hot state may differ from the stored primary. 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. +The execution kernel produces one `StepOutcome` for the current Step after canonical precondition, amount, fee, and task evaluation. `StepOutcome::Failed(TaskFailure)` retains the concrete `DispatchError` cause and orthogonal `RetryClass`; `execute_loaded_single_step_core` applies the authored policy without a simulation-only failure vocabulary. 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. +Production and simulation share current-Step transition owners, 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. ### Market Adapter Boundary @@ -259,33 +299,35 @@ A cycle is admitted only when all checks pass: 1. actor is ready (`trigger`, cooldown, pause/breaker/window checks) 2. per-block execution cap (`MaxExecutionsPerBlock`) not exceeded 3. a two-dimensional `WeightMeter` can consume the complete attempt plus measured pure-cleanup weight without exceeding RefTime or ProofSize -4. for a User Actor: fee preflight covers the opening plan or unresolved retry suffix plus `MinUserBalance` +4. For an Idle User Actor consuming paid readiness: complete Pipeline Machine payment preserves `MinUserBalance`; each invoked Action independently admits its current effect fee + +The certified Contract head stores the complete bounded `PipelineMachineEnvelope`; Idle admission reads that authority without rescanning the full Contract. Opening captures only declared Opening dependencies. Running/Suspended service reuses paid machine authority, loads the current Step, and independently admits its control, Task effect, and current Action liability. -Attempt Weight and User fees are derived from the current bounded contract at every use. Fresh attempts scan the full plan, while suspended attempts scan only the bounded `cursor..plan.len()` suffix and compose generated retry/suffix-admission classes. +Weight or scan deferral remains silent and state-preserving: no candidate identity, event, nonce, cursor, funding snapshot, or task effect changes. Unavailable or uninitialized canonical observation during Crossing rearm atomically refuses Opening, retaining the paid latch and live FIFO head without Pipeline collection. Persistent live-head Weight blockage, fee-collection failure, or invariant refusal becomes observable through sparse starvation transition events; starvation does not by itself identify Weight exhaustion. -Weight or scan deferral remains silent and state-preserving: no candidate identity, event, nonce, cursor, funding snapshot, or task effect changes. Persistent live-head Weight blockage, fee-collection failure, or invariant stall becomes observable only through sparse starvation transition events. +`src/crossing.rs::prepare_crossing_rearm_hot` owns the required current value/revision check; `src/scheduler.rs::execute_cycle` stops at the refused head. Native `crossing_rearm_unavailability_` tests prove exact state and balance preservation, service after host recovery, and independent insolvency cleanup without rearm. The runtime benchmark `scheduler_paged_zero_step_user_crossing_unavailable` verifies the unavailable branch and authorized Mutable owner close followed by FIFO progress. 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` precedes `CycleAdmissionInsufficient`, then independently owned nonce/failure/lifecycle terminals +- After circuit-breaker refusal, admission handles the classified terminal reason before testing capacity for a new User Pipeline. Already-due nonce/failure/lifecycle closure therefore cannot be relabelled as `CycleAdmissionInsufficient`; terminal cleanup retains its own Weight admission. - 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 +- Post-failure close selects retry-local exhaustion before the inclusive global bound `unsuccessful_attempt_streak >= MaxConsecutiveFailures` when both are reached by one Attempt. The shared finalizer emits the 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. +- Owner-initiated Active or Dormant 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. A host-genesis Immutable Dormant System identity has no authored source and rejects activation. 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 +- Pipeline-admission insufficiency selects generated `pipeline_admission_apoptosis`; other terminal reasons and the economically collected cleanup upper select `close_actor`. Actual cleanup retains the Actor's Contract, detector, placement, and owner-hold geometry. The one-Step Manual apoptosis fixture alone does not establish an all-context cleanup bound; the Crossing diagnostic includes retained threshold membership and ordinary owner hold access. +- Close removes the exact primary and invalidates remaining Block/Tick references transactionally. Ready removal leaves an ordered empty slot until bounded head advancement; Waiting removal releases its slot and unlinks an empty page. +- Every bounded window validates checked `end + 1` representability and stores that exact terminal block in primary hot state. Block service/terminal reasons share one exact Block pointer and retain the earlier target; AtTime/Cadenced detection has its independent Tick pointer. - Paused actors remain hot-only before terminal time and load `ActorContract` only when closure is due - With `GlobalCircuitBreaker` active, normal cycles and scheduler-owned terminal cleanup defer; bounded housekeeping plus explicit lifecycle/sweep cleanup remain available `ActorContract.completion_policy` defaults to `Persistent`. `CloseAfterProductiveCycle` checks cumulative `committed_effectful_tasks` only after successful logical-cycle completion, including a resumed Continuation. False latest-state Precondition results, skipped Steps, rolled-back failures, bare `StopCycle`, suspension, abort, cancellation, and retry exhaustion cannot select `ProductiveCycleCompleted`. The pure close path remains valid for Immutable System actors and preserves their sovereign balances. -Code anchor: `src/execution.rs::execute_single_cycle_traced` increments the cumulative count after task commit and applies productive close after `CycleSummary`. Pallet tests prefixed `close_after_productive_cycle_` falsify false-state, latest-state race, bare stop, retry, exhaustion, balance, and Immutable closure claims. +Code anchors: `src/execution.rs::execute_loaded_single_step_core` owns cumulative outcomes; the consumed-frame execution owner in `src/scheduler.rs` applies productive close after cycle completion. Pallet tests prefixed `close_after_productive_cycle_` falsify false-state, latest-state race, bare stop, retry, exhaustion, balance, and Immutable closure claims. Lifecycle lease-by-cycles is authored by `ActorContract.auto_close_at_cycle_nonce`: after a successful cycle reaches the configured target, the actor closes with `AutoCloseNonceReached`. Complete Mutable Contract replacement may set, shorten, extend, or clear the target; every non-empty target must remain strictly ahead of current `cycle_nonce` and within `MaxAutoCloseNonceHorizon`. No field-specific setter or increment call exists. @@ -295,23 +337,30 @@ The generic pallet collects creation, successfully processed Manual, AddressEven 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. +EXP-0057 accepts the current reference-Wasm useful-occurrence matrix: Manual `207,502,000 / 15,106` with 14 reads/7 writes; AddressEvent `230,410,000 / 8,394` with 14/7; ObservationChange `153,933,000 / 8,299` with 13/8; ObservationCrossing `618,734,000 / 164,188` with 90/81; AtTime `301,091,000 / 8,451` with 20/9; and Cadenced `469,271,000 / 8,450` with 27/16. Values are pre-database RefTime/estimated ProofSize. Physical host, fanout, Crossing and Tick workers retain their inclusive source/traversal owner, so these economic coefficients are not added again. The temporary fragments remain uninstalled; EXP-0065 owns complete regeneration. + - Predicate and Task preparation run read-only before collection determines the Step outcome. - 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. +- `StepControlWeightProvider` receives the starting phase, committed outcome, canonical post-placement class (`None`, `Queue`, or `Wakeup`), typed Task invocation evidence, and independently whether nonzero Action fee collection is required for the attempt to commit. The runtime keeps invocation-receipt Control separate from fee collection. A receipt-free estimate adds the generated receipt owner for general-path invocation, including System zero-fee attempts; a direct estimate already containing receipt deposition adds nothing. Maximum composition compares complete envelopes, and conservative maximum fallback retains its included receipt allowance. Specialized unpredicated `StopCycle` reports invocation without that receipt; predicated `StopCycle` uses the general path and emits a zero-fee receipt. These additions are staged components; complete generated Weight rebinding remains required. +- Missing or component-wise greater-than-reserved actual control evidence rolls back the shared Step/placement transaction. The host binds generated owners to the admitted Contract geometry; independent storage maxima do not prove a jointly reachable context. Opening and last-funding amount sources occupy mutually exclusive authored fields, so benchmark coverage must preserve their tradeoffs. Independently estimated ProofSize cannot be subtracted soundly to remove overlapping owners. + +Admission and Opening control contexts use the configured funding capacity as a conservative model input, not physical snapshot cardinality. A resumed head supplies the retained funding count from its already-loaded Run; tail contexts normalize payload counts to zero. This distinction adds no storage or adapter read, but changes resumed actual Weight evaluation and therefore requires regenerated production evidence before numeric acceptance. + +Running, Suspended, and fresh-Opening progress select direct inner owners. Fresh-Opening progress has separate minimal and maximum geometry owners at each authored tail-chunk count. Execution reuses the transaction-prevalidated Contract, hot state, Run and admission identity for persistence and FIFO successor placement without reloading the next tail fragment. Suspended-head fixtures now reach their source through real Opening, failure and due wakeup: terminal heads have one Step; other profiles couple tail Opening and funding sources, with Current-only or one-Opening plus variable Current predicates. These lawful profiles do not establish dominance over every runtime-selected composition; their changed benchmark parameter signatures still require production Weight regeneration and envelope reconciliation. Fresh-Opening completion selects separate minimal and maximum direct owners; ordinary completion no longer reconstructs cold tails merely to prove that no terminal close condition applies. -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. +Running inner completion/progress fixtures use User Actors with separately funded Manual readiness and Pipeline capacity, Q1 prefixes, and no-effect current Steps. Native mock admission and hold reconciliation pass, but production benchmark Wasm rejects their authored geometry with `ContractStepsExceedOnIdleBudget` before measurement. Production reachability and coefficients therefore remain unresolved. The inner dispatcher includes User state-hold reconciliation after destination publication; native successor ownership and TryRuntime validate the retained hold, without establishing invoked-Task coverage or worst-case hold adjustment. -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. +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. Ordinary lifecycle tests verify auto-close event ordering and complete Contract-fragment cleanup without introducing a second close Weight owner. +- `fee_native_protected_minimum` accepts `FeeAssetClass::{FeeNative, Other}` and 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. Public callers cannot invert this safety distinction through an unlabeled boolean. 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 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. +`ActorControlCell.hot.cycle_state` selects sparse run state. Idle has no Run; Running and Suspended retain matching `ActorRunHeads`/`ActorRunPayloads`. The mutable head carries cursor, retry count, causal commit block, eligibility, and cumulative outcomes; the payload holds immutable Opening/funding snapshots. A nonterminal Step persists Running with eligibility at least `now + 1`, preserving Q1 and the committed prefix. Suspended service retries only its current cursor under the authored Mutable-only bounded policy. Try-state checks marker/store equivalence, cursor/retry bounds, admission identity, and payload 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`. @@ -329,19 +378,25 @@ Eligibility uses the larger of this delay and schedule cooldown, omits external `Timer phase decision`: `tests/fixtures/timer-jitter-decision.v1.json` preserves the 10,000-actor comparison. Historical jitter expanded two targets to 128 while leaving tail service, `PassExhausted`, serviced Weight, FIFO, suspension, and retry recovery unchanged; peak queue improved by less than 2.5%. The implementation therefore uses exact cadence from `schedule_anchor` with no phase constant, hash, arithmetic, metadata, or host configuration. -`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`. +Production `service_live_queue_entry` dispatches the consumed-frame zero-Step owner and routes StopCycle/effectful attempts through `execute_current_step_and_place`, shared with inner benchmarks; effectful execution uses `execute_loaded_single_step_core`. Rollback simulation uses these same owners for one selected Ready Actor, without consuming earlier peers or advancing a virtual block. Inline attempt evidence reports the actual outcome and finalizer result without becoming persisted Run authority. `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. +The matrix compares one rollback-only current-Step transition with independently observed production events, counters, cursors, failure streaks, fees, balances, custody, untouched suffix effects, and final disposition. Fresh cases exercise Opening; CurrentRun cases establish their committed prefix and retry history through real prior service. Separate witnesses exercise both RefTime and ProofSize shortages in each synthetic resource lane, non-head selection, zero-Step auto-close and non-Ready terminal cleanup. These are behavioral checks, not integrated throughput evidence. Task-scoped rollback leaves earlier successful steps committed. The named `SwapIn → AddLiquidity → Transfer` and Burn-prefix regressions prove same-cursor retry, prefix non-replay, cumulative outcomes, and no cancellation compensation. The fixed-seed model `0xDE05_0730` independently checks sparse state, cursor progress, queue/wakeup uniqueness, funding accumulation, frozen cycle snapshots, and cancellation after each transition. `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 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. +`ActorSimulationApi` version 2 accepts `SimulationBudget { actor_control, shared_economic }`, checks component-wise limit arithmetic and uses local resource reservation/settlement with the production admission owners. Insufficient resources return `ResourceDeferred` without a fabricated attempt. `SimulationResult.steps` is bounded to one record independently of Contract length. CurrentRun accepts Running or Suspended authority only when semantically ready; waiting work returns `NotReady`. A due Block Waiting primary may be promoted through checked canonical publication inside rollback, preserving independent temporal references. Terminal closure reports the actual `Closed(CloseReason)` finalizer. The whole operation uses `TransactionOutcome::Rollback`; storage-backed Actor, custody, fee, event and adapter effects remain unchanged. Synthetic budgets do not establish production block capacity, and generated metadata/Wasm identity requires separate production binding evidence. -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. +Semantic Contract Steps, funding-policy, schedule, window, deactivation, terminal, and close transitions share `cancel_run_internal`. Cancellation validates and mutates canonical primary hot state through one fallible owner; missing or contradictory state returns `ActorRunInvariant`. Actor run writes require coherent primary, Contract, funding, and run authority, 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. + +Retained run cancellation loads canonical authority directly. `LoadedCancellationContext` distinguishes retained and consumed primary provenance, carrying complete admission and state in both variants. The consumed-primary finalizer rejects a surviving locator, so retained authority cannot be silently handled as consumed. A retained close supplies complete primary state and admission before removing its owner. This lets underfunded retry select custody-neutral minimal apoptosis from Ready and lets expired Manual activation close from Unsignaled without a transient destination. + +Activation namespace exhaustion closes from the retained source state and admission already carried by the activation plan; failed placement is transactional and does not publish a successor. Due temporal admission, placement/rearm, and generic wakeup-materialization failures instead close from the supplied consumed wakeup source; these modes are never inferred from the close reason. + +Temporal progression refreshes the canonical service tuple after placement and state-hold reconciliation, then passes its admission certificate directly into activation preflight. Terminal-before-fee handling commits that prepared plan without a second preflight; plan commit opens a transaction only when its caller has none. Successful fee collection is a separate mutation boundary, so subsequent activation uses fresh authority rather than reusing the pre-fee plan. Other supplied-state callers retain the validating preflight entry. 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. @@ -365,7 +420,7 @@ Code anchor: `src/scheduler.rs::actor_eligibility`; package tests prefixed `elig `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. +The quote returns the configured Creation Fee separately from the current refundable state hold. User hold provenance separates identity, Contract head, actual retained tail body, detector, funding, and run components with base/per-byte pricing and a checked total; System Actors are explicitly exempt. Active identity uses the canonical control identity, while Dormant identity uses its registry value. The head component includes actual certified head/admission bytes and type-derived maximum mutable control, cursor, eligibility and resource-envelope capacity; it is not a claim about the current SCALE payload length. Ordinary reconciliation and the direct benchmark path use the same quote, and detector accounting follows the resulting reference/re-arm state. Zero/one-Step Contracts reserve no maximum tail body. The bounded read-only method exposes no remaining machine budget and returns typed absence, corruption, overflow or missing-Weight errors. 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. @@ -379,18 +434,145 @@ 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**: `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 +3. **Block-start cutoff**: `on_initialize` stores `(block, ActorReadyTail)` 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. +5. **Strict ticket order**: bounded tombstone advancement cannot bypass a live or blocked head. Capacity uses the checked `ActorReadyTail - ActorReadyHead` physical span; `ActorReadyOccupancy` counts live cells only. Append, exact-head consumption, and head advancement validate their page/slot and count authority transactionally. Corruption produces an invariant stall, never false absence or successful progress. 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. +`ActorControlLocators::Ready { ticket }` and its matching C32 slot jointly own live FIFO membership; `actor_hot` derives the queue-ticket projection. Removal clears the exact slot without moving surviving tickets. Ready service checks inline eligibility and resources before loading current service state; consumption and successor publication carry explicit authority rather than reconstructing duplicate active storage. + +Each live FIFO entry snapshots the cycle and Actor Control meters before its discovery. Generic nonterminal service retains discovery, loaded-state probe, Ready consumption and one inner/destination owner. Admission-time apoptosis, terminal close, or state-preserving Crossing-unavailable refusal instead evaluates and settles from that snapshot, replacing the provisional entry prefix with one complete owner without coefficient subtraction. Breaker refusal is distinct: it retains the live Ready source in place and charges only its discovery/probe. A coherent paused Actor has already moved to Unsignaled, so its old Ready slot is only a tombstone; a live paused Ready cell is malformed rather than a benchmarkable transition. Tombstones completed before an entry and later next-head or empty-queue probes remain separately charged physical operations. -Wakeup schedule, invalidation, drain, and materialization use the same canonical boundary; only NotRegistered, Dormant, or Active-with-no-pointer entries drain as lazy temporal tombstones, while another live pointer or corrupt partitions roll back. Continuation cancellation exactly removes the retained Active actor's current wakeup slot before re-priming a latched next run, so an old slot cannot conflict with the replacement pointer. The production-Wasm `scheduler_actor_state_probe` benchmark at 100 steps and 50 repeats measures `38,413,000 / 12,200` with five reads. The reference runtime regenerated every changed scheduler and ingress branch at 50 steps and 20 repeats against the same loader; branch weights now carry their complete read sets without a separate partial-probe surcharge. +Wakeup schedule, invalidation, drain, and materialization validate canonical primary authority and exact clock/page/slot pointers. References also bind admission identity; malformed live authority fails closed. Cancellation removes the retained temporal placement before re-priming a deferred latch. Pointer, page, count, or cursor failure preserves the prior schedule transactionally. 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. +The production `execute_cycle_to_cutoff_with_resources` path pre-reserves the complete pass Actor Control maximum and enforces that same component-wise limit through a dedicated control meter across queue scan, actor probe, Opening, Step, cleanup, and placement work. It separately reserves each Action's phase-owned effect maximum before semantic mutation, settles committed actual effects transactionally, and settles the full reconciled pass control afterward. Prepass admission withholds the generated `scheduler_on_idle_base + block_resource_finalize` control envelope so ordinary head-of-line exhaustion cannot strand the mandatory ExternalPhase-to-Finalizable transition. + +Coverage includes successful Action, zero-Step Opening, precondition skip, paused/tombstone consumption, proof-budget probe stall, saturated Prepass, current-block telemetry, and successful finalization 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. + +The deterministic production-resource collector separates base and Drain dispositions, checks Q1 `S == A`, records failures, Opening/completion/closure events, finalized resource domains, service gaps, exact FIFO stop reasons against the next head envelope, and emits a stable JSON histogram/percentile report. The repaired 10,000-Actor W1/W2 baseline spans 1,098 measured blocks with mean `9.1075`, p50 `7`, p95/p99 `21`, range `0..=21`, 10,000 Openings/completions, zero failures/closures/Drain Steps/halts, and Actor Control as every measured stop reason. + +Replacing native Transfer with control-only `StopCycle` produces the exact same histogram and zero effect Weight, directly falsifying effect capacity as the binding baseline owner. Homogeneous Router-backed `SwapOut` also preserves every histogram bin with zero failures while the final measured block consumes `35,666,356,000 / 211,783` Actor effect. Each proof-saturated W2 profile submits 77 successful signed remarks per block, consumes `673,442 / 676,150` User ProofSize with 2,708 remaining, and leaves the control-only, Transfer, or `SwapOut` histogram unchanged. + +The independent RefTime-heavy W3 witness executes 22 alternating signed direct Router swaps per measured block. Every block consumes `107,444,150,000 / 673,376`, leaves `198,691,868,910 / 2,774`, and rejects the next `4,883,825,000 / 30,608` swap on ProofSize while it still fits RefTime. Across 1,098 blocks and 24,156 valid swaps, the 10,000-Actor SwapOut profile retains the exact W1 histogram, zero failures, zero halts, and valid finalization. This is the highest retained repeatable business-call witness, not a false RefTime saturation claim. + +The callable-surface audit excludes Root/internal hooks, rejects the `u64::MAX` disabled XCM extrinsic surface, classifies bounded storage-heavy calls below the Router RefTime-per-proof frontier, and treats Preimage separately under its 4 MiB call and 5 MiB block-length bounds. Even a theoretical full 5 MiB Preimage allocation plus the remaining admissible Router calls remains below the User Base RefTime turn and binds ProofSize first. RefTime saturation is therefore unreachable on the current production signed-call surface; W3 is the explicit frontier fallback required by the assurance contract. + +The retained mixed proof-saturated profile contains exactly 9,500 Transfer, 400 Router-backed `SwapOut`, and 100 `StopCycle` Actors. It completes at block 1,099 with all class counts exact, zero failures/closures/halts, 9,989 Cadenced occurrences, and the unchanged 1,098-block W1 histogram. All 1,098 blocks reject the next signed remark on User ProofSize and stop Actor service on Actor Control, proving that the heterogeneous Task mix changes neither the scalar control frontier nor FIFO service geometry. + +After the queue scan and actor probe, the one-Step FIFO head check reserves queue consumption, stored Step control, and reachable cleanup, distinct from the larger lifecycle admission envelope. Successful settlement releases unused maximum, but the next head must fit the complete reservation. The retained W1 measurements below used `11,513,795,349 / 125,005`, before explicit Action collection accounting. Current non-StopCycle reservations additionally include the host collection allowance; these historical measurements do not describe that new reservation frontier. + +| FIFO attempt owner | RefTime | ProofSize | Share of reserved proof | +| --- | ---: | ---: | ---: | +| Queue consume maximum | `809,855,000` | `5,528` | 4.4% | +| One-Step control maximum | `1,983,656,349` | `37,591` | 30.1% | +| Conservative close cleanup | `8,720,284,000` | `81,886` | 65.5% | +| Complete post-probe reservation | `11,513,795,349` | `125,005` | 100% | + +The final measured block retains `249,629,611,530 / 95,231` Actor Control, so ProofSize alone rejects the next head while RefTime remains abundant. The separate lifecycle envelope is `14,643,430,575 / 175,144`; using it as the FIFO stop classifier would overstate the live head requirement. Conservative close cleanup is the provisional per-head owner, subject to branch-reachability falsification before any geometry candidate is admitted. + +Homogeneous Trigger attribution separates FIFO attempt pressure from reactive detection, materialization, and successor work. Manual Actors begin from an already admitted queue and sustain 21 Steps in every saturated block; Cadenced Actors share Actor Control with tick wakeup/materialization and successor scheduling, producing 466 zero-Step blocks before first-population coverage. The mixed baseline lies between these profiles, so cleanup-reservation and Cadenced-path counterfactual gains are non-additive. + +| 10,000-Actor Transfer profile | Measured blocks | Committed Steps | Mean | p50 | p95 | Zero-Step blocks | Maximum service gap | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| Manual only | 477 | 10,000 | `20.9644` | 21 | 21 | 0 | 0 | +| Manual/Cadenced | 1,098 | 10,000 | `9.1075` | 7 | 21 | 200 | 0 | +| Cadenced only | 1,726 | 10,002 | `5.7949` | 6 | 13 | 466 | 1,723 | + +Every saturated Manual block and 1,725 of 1,726 Cadenced blocks stop on Actor Control; the final Cadenced block stops at its causal cutoff after population coverage. The two recurrent Cadenced completions are included in committed-Step throughput but not double-counted in the 10,000 distinct-Actor coverage assertion. + +The frequency ledger counts 0 Cadenced occurrences for 10,000 Manual commits, 19,992 occurrences for 10,002 Cadenced commits, and 9,989 occurrences for the mixed first traversal. The Cadenced first-traversal window includes initial readiness plus almost one successor latch per Actor at the terminal boundary; those deferred successors are not mislabeled as committed Steps. In steady recurring service, each Cadenced completion creates approximately one next temporal transition, while the initial 10,000-occurrence fill amortizes only over later traversals. + +| Homogeneous Opening Predicates | Blocks | Commits | Mean | p50 | p95 | Maximum service gap | Cadenced occurrences | +| ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| 0 | 1,098 | 10,000 | `9.1075` | 7 | 21 | 0 | 9,989 | +| 2 | 1,662 | 10,002 | `6.0181` | 6 | 10 | 881 | 9,996 | +| 4 | 1,167 | 10,002 | `8.5707` | 7 | 19 | 624 | 9,997 | + +The 10,000-Actor homogeneous Predicate profiles retain zero failures, closures, and accounting halts. The non-monotonic 2-versus-4 throughput is an exact consequence of current generated branch weights and component-wise fragmentation, not a semantic claim that evaluating four Predicates is intrinsically cheaper than evaluating two. It is retained as a falsification constraint for any macrogeometry candidate. + +A converged paused/empty block consumes `2,954,966,226 / 39,980` fixed Actor Control. One successful Manual Opening adds `2,207,317,923 / 24,685`; its generated components sum exactly to that increment. No leaf dominates proof, so the binding macro owner is the repeated scalar per-Actor FIFO/authority/Opening commit frame rather than Transfer, one isolated storage call, or close reservation. + +| Incremental scalar owner | RefTime | ProofSize | Reads | Writes | Share of incremental proof | +| --- | ---: | ---: | ---: | ---: | ---: | +| Queue scan | `613,752,613` | `6,465` | 7 | 4 | 26.2% | +| Actor state probe | `243,096,000` | `5,998` | 7 | 0 | 24.3% | +| Queue consume maximum | `809,855,000` | `5,528` | 10 | 5 | 22.4% | +| Opening completion | `540,614,310` | `6,694` | 6 | 3 | 27.1% | +| Complete incremental frame | `2,207,317,923` | `24,685` | 30 | 12 | 100% | + +Read/write counts are generated owner maxima and may revisit the same physical key across owners; they are not distinct-key counts. Queue consume is the component-wise maximum of preserve-page and delete-page branches, so its RefTime/proof pair and `10/5` access ceiling are a safe composite envelope rather than one observed branch tuple. + +After the empty baseline, the one-third control envelope can carry at most 25 such proof increments even before head-of-line reservation fragmentation; the observed Manual ceiling is 21. The controlled cleanup counterfactual removes `81,886` proof from the `125,005` head reservation, leaving `43,119`; after 21 actual increments the remaining `117,785` proof could admit that narrower head, but the `24,685` actual increment still caps service at 25. Cleanup removal therefore has an isolated upper bound of four additional Steps/block. Reaching 100 requires at most approximately 6,361 incremental proof bytes per committed Actor, a 3.88-fold reduction, and Cadenced materialization remains a separate non-additive counterfactual. + +A single Cadenced Actor alternates a materialization-only block at `10,329,600,226 / 118,726` with a completion block exactly equal to Manual at `5,162,284,149 / 64,665`. Relative to the stable empty baseline, Cadenced materialization adds `7,374,634,000 / 78,746`; completion adds the same `2,207,317,923 / 24,685` scalar frame as Manual. Pausing first drains stale temporal work before converging to the empty baseline, so stale cleanup is not mislabeled as quiescence. + +The due Cadenced occurrence decomposes into one scalar wakeup-cursor removal plus one component-wise temporal occurrence owner. A separate rotated-family baseline delta accounts for the difference from the converged empty block. The admission envelope adds close and worker-fault reserves, but successful materialization settles only the actual branch. + +| Cadenced materialization owner | RefTime | ProofSize | Share of measured incremental proof | +| --- | ---: | ---: | ---: | +| Physical wakeup-cursor removal | `5,134,092,000` | `56,945` | 72.3% | +| Temporal occurrence maximum | `1,889,838,000` | `8,585` | 10.9% | +| Rotated-family baseline delta | `350,704,000` | `13,216` | 16.8% | +| Measured materialization increment | `7,374,634,000` | `78,746` | 100% | +| Complete admission envelope | `15,879,272,000` | `148,919` | n/a | + +The physical cursor owner carries 50 generated reads and 33 writes; the component-wise temporal occurrence ceiling carries 22 reads and 11 writes. Their successful due-occurrence composition therefore repeats 72 reads and 44 writes before the rotated-family baseline delta, directly identifying the page-amortization pressure. + +Removing occurrence evaluation alone saves only `8,585` proof; removing the physical cursor branch alone still leaves `21,801` scalar proof, above the approximately `6,361` proof/Actor target. Reactive service therefore requires amortizing the complete scalar temporal transition rather than adding its gain to the independent cleanup counterfactual. + +Terminal branch accounting includes cleanup whenever `CompletionPolicy::CloseAfterProductiveCycle` or the configured auto-close nonce can close on success. The scheduler previously reserved success cleanup only for the nonce branch, so a productive close mutated full lifecycle state while settling persistent-completion control. The corrected branch reserves and settles the existing generated close maximum; no Actor semantics or close behavior changes. + +| Complete control branch | Actor Control RefTime / proof | Actor effect RefTime / proof | +| --- | --- | --- | +| Persistent Transfer Opening | `5,162,284,149 / 64,665` | `1,670,789,000 / 18,280` | +| Productive Transfer cleanup | `13,882,568,149 / 146,551` | `1,670,789,000 / 18,280` | +| Persistent zero-Step | `17,817,905,065 / 201,061` | `0 / 0` | +| Minimal Pipeline-admission apoptosis | `4,971,257,226 / 45,716` | `0 / 0` | + +The current runtime fixture includes fixed on-idle work, one complete apoptosis entry owner, and the later empty-queue probe. Per-entry replacement removes the formerly additive discovery/state-probe/Ready-consume prefix (`1,666,703,613 / 17,991`) from this exact telemetry without deriving a coefficient. The generated narrow `pipeline_admission_apoptosis` remains stale pending EXP-0065; the accepted complete replacement value is not yet installed. The transition emits `CycleAdmissionInsufficient`, deletes process semantics, preserves sovereign custody, and invokes no Task effect. + +| Generated branch owner | Reads | Writes | Event/fee consequence | +| --- | ---: | ---: | --- | +| Opening completion minimum | 6 | 3 | `CycleStarted`, Step outcome, final `CycleSummary`; System fee-exempt | +| Running middle progress | 13 | 6 | Step outcome only; RunState remains live | +| Running completion | 9 | 4 | Step outcome plus final `CycleSummary` | +| Opening retry minimum | 11 | 8 | `CycleSuspended`; Action effect is independent | +| Suspended head Opening retry | 15 | 6 | Next-attempt `CycleSuspended`; cursor remains zero | +| Minimal Pipeline apoptosis | 15 | 15 | Useful Trigger fee remains final; no Pipeline/Action fee; `ActorClosed` | +| Conservative close cleanup | 64 | 64 | Lifecycle close event; no custody transfer | +| Zero-Step completion | 7 | 3 | Opening/completion events; no Action fee | + +Counts are generated owner maxima rather than distinct physical keys or an additive complete-block total. Generic branches retain the shared outer scan/probe/consume frame. Complete admission-close and Crossing-unavailable refusal branches replace that entry frame through the saved meter baseline and a typed complete-result selector. + +The finalized one-Actor complete-path matrix separates Fresh Opening from a second Running Step. Each cell executes the same native Transfer effect at `1,670,789,000 / 18,280`; native backend cache warmth is not a consensus Weight dimension. Fresh Opening with two predicates selects a conservative intermediate actual-control composition rather than a monotonic interpolation between endpoint branches. + +| Phase | Predicates | Actor Control RefTime / proof | Actor effect RefTime / proof | +| --- | ---: | --- | --- | +| Fresh Opening | 0 | `5,162,284,149 / 64,665` | `1,670,789,000 / 18,280` | +| Fresh Opening | 2 | `5,940,778,143 / 90,653` | `1,670,789,000 / 18,280` | +| Fresh Opening | 4 | `5,277,070,638 / 66,283` | `1,670,789,000 / 18,280` | +| Running completion | 0 | `5,347,343,198 / 65,036` | `1,670,789,000 / 18,280` | +| Running middle progress | 0 | `5,669,242,017 / 64,773` | `1,670,789,000 / 18,280` | +| Running completion | 2 | `5,471,929,878 / 70,472` | `1,670,789,000 / 18,280` | +| Running completion | 4 | `5,596,516,558 / 75,908` | `1,670,789,000 / 18,280` | + +The Router-backed `SwapOut` matrix uses the same settled infrastructure baseline. Successful Opening has the same charged control cells as Transfer; this does not establish identical executed control work. A typed input-capacity failure suspends through `RetryLater`; the next-block resumption stays at cursor zero under the same single wakeup owner. Every invoked branch charges the canonical `3,242,396,000 / 19,253` effect envelope independently of its control branch. + +| Outcome | Predicates | Actor Control RefTime / proof | Actor effect RefTime / proof | +| --- | ---: | --- | --- | +| Success | 0 | `5,162,284,149 / 64,665` | `3,242,396,000 / 19,253` | +| Success | 2 | `5,940,778,143 / 90,653` | `3,242,396,000 / 19,253` | +| Success | 4 | `5,277,070,638 / 66,283` | `3,242,396,000 / 19,253` | +| Temporary | 0 | `6,380,116,188 / 92,536` | `3,242,396,000 / 19,253` | +| Temporary | 2 | `6,684,863,143 / 108,188` | `3,242,396,000 / 19,253` | +| Temporary | 4 | `6,801,634,895 / 113,238` | `3,242,396,000 / 19,253` | +| Retry | 0 | `5,544,105,092 / 63,104` | `3,242,396,000 / 19,253` | +| Retry | 2 | `5,778,173,654 / 73,880` | `3,242,396,000 / 19,253` | +| Retry | 4 | `5,731,684,946 / 69,176` | `3,242,396,000 / 19,253` | + +A native Transfer rejected before invocation for `FundingUnavailable` consumes zero Task effect. Its Opening suspension is `6,380,116,188 / 92,536` control and its next-block Suspended retry is `5,544,105,092 / 63,104`, matching the zero-predicate Temporary/Retry control branches above while removing the Router effect. Resumed values use the empty retained funding snapshot, not the configured admission cap. A matched two-Step fixture with equal custody and zero/one retained funding entry preserves Opening charge and isolates exactly the generated retry entry contribution. These are native charged-model results, not regenerated C1 production evidence or measured proof savings. `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. @@ -412,16 +594,20 @@ Package coverage proves the hook-ordered `NextBlock` cutoff: a block-N Manual oc 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`. +- `ActorWaitingFrameChunks<(WakeupKey, page_id)>` owns linked nonempty C32 pages for Block or Tick deadlines. Entries are primary cells or compact actor/admission references; per-key Heads, Tails, and Occupancies form the same Waiting directory. +- `WakeupCursorPages<(clock, page_id)>` plus `WakeupCursorLen` provide separate paged Block/Tick min-heaps; `ActorWaitingCursorIndices` owns the exact per-key reverse 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.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. +- Primary `hot.wakeup_pointer` owns exact Block service/terminal membership, while `hot.trigger_wakeup_pointer` owns independent Tick detection membership. Cadenced runtime state retains its 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. - The coordinator gives the rotated first family its generated maximum-unit minimum plus the shared lendable remainder, while reserving one complete maximum-unit minimum for each later family. The wakeup worker consumes only that grant and remains independently bounded by `MaxWakeupsPerBlock`; a complete cursor/drain unit must fit before mutation, and returned hook Weight cannot exceed the caller budget. - The production drain primitive bounds work by slots scanned, preserves a partial head cursor, crosses linked page boundaries, deletes exhausted pages, clears only matching live pointers, discards stale slots, and removes an exhausted bucket from the cursor in the same transaction. -- Try-state reconciles links, counts, slots, unique pointers, and active-actor capacity. +- Zero-Step, StopCycle, and effectful service consume explicit primary authority and publish their final successor within the same transaction as Trigger rearm, run updates, fees, events, and User hold reconciliation. +- Opening captures Predicate/funding snapshots once; Running/Suspended service reuse them. Q1, the deferred readiness latch, authored error policies, and committed-prefix durability remain independent of placement. +- Cancellation and lifecycle transitions distinguish retained from consumed source state. A surviving destination must be published before hold reconciliation; no scalar compatibility store or alternate scheduler exists. +- Window-terminal drain closes directly from consumed Waiting authority under its admitted cleanup bound, with no intermediate Ready owner or custody movement. +- Try-state checks primary uniqueness/locator bijection, exact reference admission/pointers, linked pages, directory/cursor reverse indices, and live counts. One primary plus at most one Block and one Tick reference bounds total control entries by three times the active Actor count. Package benchmarks isolate append, exact replacement, middle-page unlink, partial/full/stale drain, cursor insert/pop/removal, one-slot worker progress, full-depth repair, and future-minimum stop. Every branch reserves complete RefTime and ProofSize before mutation. Hosts select page size and generate production coefficients against their configured depth and database schedule. @@ -468,7 +654,9 @@ Successful temporal enqueue carries single-Actor authority into activation commi 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. +Crossing phase and installation revision live only in `ActorControlCell.hot.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. + +Crossing ingress requires a checked successor of the last retained transition revision and rejects exhaustion atomically. TryRuntime checks positive revisions and the same strict succession throughout each retained queue; the certified publisher owns revision history across drained queues. 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. @@ -488,15 +676,21 @@ The release load profile installs 10,000 members on one feed and proves a zero-m 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. +During FIFO Opening, `prepare_opening_rearm_hot` preserves the admitted pending latch while preparing the Crossing detector's new phase and revision. The common Step core validates and consumes that latch; detector preparation cannot consume it prematurely or fabricate readiness. `crossing_opening_executes_effectful_step_and_rearms_detector` covers ordinary FIFO effect execution, Running continuation and detector rearm together. + +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 current EXP-0057 economic owner `observation_crossing_trigger_occurrence` is `618,734,000 / 164,188` with 90 reads and 81 writes; it begins from an already published transition and retained threshold, so Oracle publication/signaller work is excluded. This staged fragment supersedes the stale installed coefficient as current evidence but remains uninstalled until EXP-0065 regenerates the complete file. 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. It 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. +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, and groups equal destination obligations so each touched destination page is written once. It 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. + +A route-heterogeneous third candidate truncates a four-candidate prefix after its first two homogeneous immediate-FIFO authorities. It consumes the pair 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. -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. +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. @@ -504,7 +698,7 @@ A candidate whose installation revision equals the pending revision truncates a | Crossing capacity dimension | Reference value | Current evidence | | --- | --- | --- | -| Actor Control envelope | `304,686,077,576 / 676,150` | Every generated complete branch fits component-wise after fixed-context composition | +| Actor Control envelope | `302,102,663,242 / 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 | @@ -513,9 +707,11 @@ A candidate whose installation revision equals the pending revision truncates a | 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 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. +The 10,000-member single-threshold load 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. +Current generated component-wise admission permits four Crossing candidates per block. The reference 9,000-User population therefore requires at least 2,250 uncontended detector blocks, and 100 eligible blocks can materialize at most 400 candidates. The implementation does not satisfy the performance-assurance 10,000-User/100-block profiles; System schedule throughput and broad ObservationChange fanout do not substitute for that target. + +`MaterializationFamilyCursor` rotates the block-start family across overdue wakeups, Crossing, and broad fanout as `0 → 1 → 2 → 0`. `MaterializationMinimumReservation::{ReserveAllFamilies, Unavailable}` records whether one minimum quantum can be protected for every family; budget calculation no longer receives an unlabeled boolean. 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. @@ -529,21 +725,40 @@ Filter surface: - Asset: `Any` / `Whitelist` Each producer event evaluates every configured source atom without short-circuit. Several atoms matching one event fold into one readiness decision; funding/provenance mutation runs once outside that fold. Distinct producer events still apply funding independently, while pending readiness shares one latch and one bounded actor-queue membership without coalescing value effects. + +Active control has one physical owner: an `ActorControlCell` containing identity, hot lifecycle state, current cursor and eligibility, admission, and the current Step's control/effect resource envelope. `ActorControlLocation` identifies its Unsignaled, Ready, or Waiting placement. Public projections and lifecycle operations resolve that primary; `ActorIdentities` contains dormant identities only. Activation transfers dormant identity into the primary, deactivation restores its latest identity, and close leaves sovereign custody untouched. + +`ActorReadyFrameChunks` stores ticket-addressed C32 cells under one monotonic `ActorReadyTail`. `ActorReadyHead` and `ActorReadyOccupancy` distinguish the bounded physical span from live occupancy: removed cells leave ordered tombstones, without moving surviving tickets or changing FIFO, cutoff, or Q1 service. Saturation cleanup uses the checked physical span before materialization, so a zero-live full span still receives bounded head cleanup. `ActorUnsignaledControlCells` stores active Actors without current process readiness. + +Ready consumption validates the exact locator and ticket, removes the primary and decrements live occupancy before advancing the head or reclaiming its page. This also applies to terminal Idle cells without a pending Trigger; latching does not determine physical ownership. Loaded production consumption and closed-tombstone cleanup retain their own checked inputs. The full-state discovery adapter exists only for tests and measured benchmark fixtures. + +`ActorWaitingFrameChunks` stores linked nonempty C32 pages under the existing shared deadline heap. Each active Actor has one primary and at most one Block and one Tick reference; references carry only Actor identity and admission identity, never mutable process state. Exact clock/page/slot pointers connect references to their primary. Empty pages are unlinked; a deadline is removed after its final entry. Close invalidates remaining references, and Contract replacement rebinds retained reference admission atomically. + +Stable mutations update an existing primary. Moving transitions carry validated source state and admission explicitly into successor publication after source consumption; they do not reconstruct state from duplicate storage. `ActorControlTransitionError` classifies placement failure. AddressEvent funding and useful readiness retain their independent atomicity and fee boundaries while using this same control owner. + +`QueueAppendPlan` carries one prepared canonical cell and ticket per publication, plus the expected final tail and occupancy. Commit validates any retained source, preserves temporal references during detachment, and checks each ticket against the current tail; there is no fallback reconstruction of a missing prepared cell. A caller-owned storage transaction must cover detachment and publication when a subsequent check can fail. Canonical hot/service loaders provide projections rather than a second mutable store. + +FIFO execution uses `service_live_queue_entry` and shares `execute_current_step_and_place` with inner benchmarks. The shared dispatcher owns the attempt state clone and no-precondition StopCycle versus effectful selection; source consumption and settlement remain in the caller's transaction. Semantic regression snapshots exercise ordinary FIFO against an immutable reference oracle. Synthetic geometry models and isolated storage benchmarks do not establish production execution, cold-read counts or throughput. + +Package tests exercise logical transition equivalence, committed-prefix durability, corrupt ownership, lifecycle changes, and bounded Ready/Waiting geometry. Runtime benchmarks use explicit retained or consumed source fixtures and separately measure control, effects, and storage geometry. Retained enqueue and wakeup setup/invalidation adapters compile only for tests or runtime benchmarks; they preserve their measured transaction boundaries and call canonical owners, not an alternative scheduler. Host adapters, asset/account types, genesis topology, resource limits, and generated production weights remain embedding-runtime responsibilities. + When a signalled cycle starts, the latch is consumed atomically. `ObservationChange` is the scalar SCALE trigger at index `2`. Its payload contains one typed feed identity only; thresholds remain Predicate-owned. `note_observation_changed` enters Actors dirty-feed state without a subscriber walk, amount payload, readiness mutation, or execution path. Deferred fanout owns conversion of latest revisions into the existing readiness latch. Opening-snapshot surface validation runs at genesis, creation, activation, and plan replacement. It requires every staking-share mapping to exist but remains independent from trigger kind, signal payload, and event amount. -`ActorObservationFeeds` stores each active actor's canonical source-derived feed set. A reusable dense slot has exact `ObservationSubscriptionSlot` and reverse-owner entries. `ObservationFreeSlotPages` provides one-page LIFO allocation bounded by `MaxActiveActors`, so identity churn reuses slots rather than growing consensus keys. +`ActorObservationFeeds` stores each active actor's canonical source-derived feed set. TryRuntime compares that complete set with the Contract Trigger through the admission derivation helper; internally consistent missing, foreign or phantom subscriptions are invalid. ObservationChange latching retains membership and disables detection through its existing marker. A reusable dense slot has exact `ObservationSubscriptionSlot` and reverse-owner entries. `ObservationFreeSlotPages` provides one-page LIFO allocation bounded by `MaxActiveActors`, so identity churn reuses slots rather than growing consensus keys. -`ObservationSubscriberPages(feed, slot / QueuePageSize)` stores an optional actor at `slot % QueuePageSize`. Each page also stores previous/next occupied page ids, while `ObservationSubscriberPageLists(feed)` stores exact head, tail, and live page count. First insertion appends one page; last removal unlinks it through at most two neighbors. Schedule updates still touch only old/new feed differences, and Contract Steps updates touch none. +`ObservationSubscriberPages(feed, slot / ObservationPageSize)` stores an optional actor at `slot % ObservationPageSize`. Each page also stores previous/next occupied page ids, while `ObservationSubscriberPageLists(feed)` stores exact head, tail, and live page count. First insertion appends one page; last removal unlinks it through at most two neighbors. Schedule updates still touch only old/new feed differences, and Contract Steps updates touch none. -`ObservationSubscriberCount` and `ObservationSubscriptionCount` expose bounded current cardinality. Try-state reconciles actor/feed ownership, free slots, page cells, reciprocal occupied links, list bounds, and counts. Fanout initializes from the feed-local head and follows exact page links, so an isolated subscriber in a historically high global slot costs one occupied-page unit rather than every lower page id. +`ObservationSubscriberCount` and `ObservationSubscriptionCount` expose bounded current cardinality. Try-state reconciles actor/feed ownership, free slots, page cells, reciprocal occupied links, list bounds, and counts; a count-bounded head walk rejects disconnected pages and cycles. Owned and free slots must lie below the allocation frontier; free-page lengths must match their exact LIFO stack positions, not just the aggregate free count. Fanout initializes from the feed-local head and follows exact page links, so an isolated subscriber in a historically high global slot costs one occupied-page unit rather than every lower page id. `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` 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. +`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 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. @@ -557,7 +772,7 @@ Terminal classification persists `TerminalDeferred`; a later scalar terminal tur 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. +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 control or funding storage directly; the host integration owns protocol inventory and rollback evidence. The private transition has one full-ingress contract; dead `apply_trigger` and `apply_funding` switches are removed, so a caller cannot partially apply one admitted event. `IngressFailure { error, retry }` classifies recoverable queue/wakeup capacity or placement unavailability as Temporary. Monotonic ticket/index exhaustion, topology corruption, invalid provenance, and invariant failure are Permanent. Actor tasks preserve the classification through `TaskFailure`; non-Actor producers map it to their outer dispatch error. @@ -565,9 +780,13 @@ The package never scans host events, fingerprints value transfers, or defers ing Host-decided `RuntimePolicy` receives both optional fields unchanged. Every accepted tracked transfer checked-adds into `funding_accumulated`; preflight rejects overflow before supported movement. Fresh-run opening takes and clears the accumulator atomically only after all fallible admission checks. Retry retains the frozen funding snapshot, later ingress accumulates for the next run, and completion, failure, or cancellation neither promotes nor restores funding. +TryRuntime reuses admission derivation to check the exact tracked-asset set when all funding references name assets directly, before checking accumulator and Run-snapshot membership. `Unstake` receipt identities are fixed at admission and their host mapping may subsequently become unavailable; current host lookup cannot prove historical identity. For these Contracts, the audit requires every directly named source, a nonempty tracked set, and at most one additional source per distinct staking position. This qualified check cannot detect every substitution of a historical receipt; it adds no production service read or duplicate mapping state. + +Funding accumulators and Run funding snapshots contain positive amounts only: Opening copies certified accumulation and keeps it immutable. TryRuntime checks this value constraint as well as tracked-key membership; a coherent payload commitment alone does not establish reachable funding state. Opening balance snapshots independently retain zero balances for admitted surfaces. + ### Manual -`manual_trigger` sets `ActorHot.pending_signal` only when the Actor's scalar trigger is `Manual`. Every other trigger fails with `ManualSourceDisabled`; paused calls fail with `ActorPaused`, and System Immutable calls fail with `ImmutableActor`. Manual readiness requests the FIFO under block cooldown/window gates. The latch clears when a signalled cycle starts and survives deferrals. +`manual_trigger` sets `ActorControlCell.hot.pending_signal` only when the Actor's scalar trigger is `Manual`. Every other trigger fails with `ManualSourceDisabled`; paused calls fail with `ActorPaused`, and System Immutable calls fail with `ImmutableActor`. Manual readiness requests the FIFO under block cooldown/window gates. The latch clears when a signalled cycle starts and survives deferrals. --- @@ -576,22 +795,22 @@ Host-decided `RuntimePolicy` receives both optional fields unchanged. Every acce Primary storage follows explicit owners. Section 13's stable behavioral stores constrain compatibility, while bounded scheduler and ingress machinery remains replaceable implementation state. No synchronized readiness mirror remains. - `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` +- `ActorIdentities`: dormant identities, retaining owner, class/custody locator, mutability, cycle nonce, and last control-mutation block +- `ActorUnsignaledControlCells` / `ActorReadyFrameChunks` / `ActorWaitingFrameChunks`: exclusive active primary placements; Waiting additionally holds bounded exact-reference entries - `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 +- `ActorControlLocators`: exact active primary location; active identity, hot state, and admission have no duplicate scalar stores - `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` +- `ActorRunHeads` / `ActorRunPayloads`: mutable cursor/outcome/authority state separated from immutable Opening and funding snapshots; payload commitment and predicate-result count bind the pair. TryRuntime additionally requires the Opening-result cursor to equal the sum of authored Opening-predicate counts before the current Step, rejecting in-range shifts that would select another Step's frozen facts. +- `ActorIdentityCount`: O(1) total of active primary identities plus dormant registry identities, bounded by `MaxActorIdentities` - `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. -- Wakeup replacement is one closed storage transaction across existing pointer validation, page-slot removal, checked page/bucket live-count release, reciprocal neighbor unlinking, cursor removal, new cursor/page insertion, checked live-count acquisition, and the actor pointer rewrite. Missing pages or slots, non-reciprocal links, cursor disagreement, count underflow/overflow, or insertion exhaustion rolls back the exact existing schedule and returns topology corruption; intentional actor-local invalidation on terminal cleanup remains the only lazy stale-entry path. +- `ActorReadyTail`: shared monotonic ticket allocator and block-start cutoff source; empty-queue cleanup never resets chronology +- `PrepassExecutionCutoff`: optional `(block, ticket frontier)` authority frozen by `on_initialize`; matching-block `NextBlock` Drain consumes it, TryRuntime rejects a frontier beyond `ActorReadyTail`, and absence/staleness is reserved for direct physical service fixtures rather than runtime timing conformance +- `ActorReadyHead` / `ActorReadyOccupancy`: next physical FIFO position and live-cell count. Ticket-addressed C32 slots retain ordered tombstones; bounded head advancement reclaims consumed pages. An empty partial-tail page may be reconstructed by the next append without reusing tickets. +- Ready capacity includes tombstones through the physical `ActorReadyTail - ActorReadyHead` span, not the smaller live count. Exhaustion fails closed without changing the prior placement. +- `ActorWaitingHeads` / `ActorWaitingTails` / `ActorWaitingOccupancies` / `ActorWaitingCursorIndices`: exact per-deadline directory for linked nonempty C32 pages. `WakeupCursorPages` / `WakeupCursorLen` retain the shared Block/Tick heaps; `NextWakeupClock` arbitrates simultaneously due domains. +- Wakeup replacement transactionally validates exact pointer/admission identity, removes the slot, releases checked page/key counts, unlinks empty neighbors, updates the cursor, inserts the replacement, and updates the primary pointer. Missing slots, link/cursor disagreement, count errors, or exhaustion roll back the existing schedule; close invalidates references explicitly. - Cross-path falsifiers compare complete storage roots and event vectors around manual-trigger capacity fallback, schedule replacement, fresh post-attempt rearm, and Continuation retry rejection. Terminal-only wakeup plus live-ticket coexistence runs try-state, while the fixed-seed corruption corpus injects queue occupancy/page and wakeup cursor/slot/live-count contradictions and requires exact rollback on every rejected transition. - `ActiveActorLimit`: explicit nonzero governance-configurable active cap bounded by `min(MaxActiveActors, MaxQueueLength)` and never below `ActiveActorCount`; zero has no fallback meaning and fails try-state - `OwnerSlotBitmaps`: one fixed 256-bit User owner-slot bitmap per owner; all-zero values are absent and System Actors never consume it @@ -624,6 +843,8 @@ Lifecycle calls preserve the split-store boundary: 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. +Public Dormant creation requires Mutable. TryRuntime permits a sealed Immutable Dormant System identity only when its actor/sovereign id, owner and mutability match the host's dormant genesis declaration; ordinary locator and absence-of-active-state checks remain mandatory. + Package lifecycle interpretation: - `Normal cycle`: scheduler-owned `contract_steps` run; checked-increments the stored nonce before events, so a new actor's first run emits nonce `1` and the run from `u64::MAX - 1` emits and executes nonce `u64::MAX`; a later Active installation or run at stored exhaustion executes no normal steps or cycle events and closes either class with `CycleNonceExhausted` @@ -658,18 +879,20 @@ This subsystem follows the project-wide [`read-model.contract.en.md`](../../../. The current pallet already provides chain-native bounded reads for live actor and scheduler truth through: -- `actor_hot(actor_id)` for lifecycle, identity/control, queue membership, cycle state, and cached bounds +- `actor_hot(actor_id)` for the primary's lifecycle, queue-ticket, cycle and Trigger-state projection; this partial read does not certify complete Contract, funding or temporal-reference integrity - `actor_contract(actor_id)` for schedule/window and bounded `ContractSteps` - `actor_funding(actor_id)` for funding policy, tracked assets, and the bounded accumulated-delta map - `owner_slot_bitmap(owner)` plus deterministic `sovereign_account_id(owner, owner_slot)` recovery and `sovereign_index(sovereign)` lookup for bounded per-owner discovery/recovery - Deterministic `sovereign_account_id_system(actor_id)` for System Actor addressing against the known runtime catalog -- Bounded scheduler / readiness / breaker surfaces such as `ActorHot.pending_signal`, `ActorHot.queue_ticket`, `ActorHot.wakeup_pointer`, `NextQueueTicket`, `QueueHead`, `QueueTail`, `QueueOccupancy`, `QueuePages`, paged wakeup stores, `ActiveActorLimit`, `GlobalCircuitBreaker`, and `IdleStarvationState` +- Bounded readiness projections from the primary and canonical Ready/Waiting directories, plus `ActiveActorLimit`, `GlobalCircuitBreaker`, and `IdleStarvationState` - Live execution-side effects and bounded operational events `SovereignAccountDeriver` maps User custody from `SCALE(ActorsPalletId, b"user", owner, owner_slot)` and System custody from `SCALE(ActorsPalletId, b"system", sovereign_id)`. The host owns the concrete infallible `AccountId` mapping; explicit tags separate the two deterministic account domains before hashing, and the DEOS runtime preserves its established 32-byte identities. These are the authoritative bounded surfaces for known-actor inspection, per-owner recovery, scheduler state, and current operator observability. +Full active-state classification additionally validates each declared Block/Tick slot against the primary's actor and admission identity, including exact locator coordinates when the slot owns the primary. It reads at most two temporal slots and never scans a population; hot head admission remains separate. TryRuntime reconciles the primary's inline cursor and resource envelope with canonical Run and current-Step authority, including zero-Step control cost. Reverse audits reject persisted tail keys outside their Contract head's declared geometry and activation-authority keys without a Contract head owner. + ### Indexed / materialized Actor views The pallet intentionally does **not** promise these as canonical on-chain surfaces: @@ -728,7 +951,7 @@ The ignored release profiles add 10,000-scale zero-match and homogeneous-herd ev Mandatory reactive falsifiers cover partial fanout followed by subscriber deactivation, subscriber removal and late re-addition during fanout, stale close entries draining as tombstones before a recreated slot runs, newer revision during page progress, queue saturation, protected User fee-native floor, fee-collector failure after admission, invalid `Fresh`, and nonce exhaustion for both classes, all ending in try-state. -FRAME benchmarks construct bounded worst-case package branches; every production host must generate and bind runtime-specific weights. +FRAME benchmarks isolate bounded package branches; every production host must prove fixture reachability and envelope coverage before generating and binding runtime-specific weights. A Fixed-Transfer Suspended-head assurance matrix uses authored maximum-length Contracts, real funding ingress, Opening, due promotion and a second retry across host-feasible Opening/funding allocations and Opening/Current predicate counts. It checks immutable snapshots, custody and canonical reconciliation; it does not establish a Weight envelope for other head Tasks or all host configurations. External-consumer profiles prove that the crate composes without DEOS types. Concrete runtime adapters, generated artifacts, stress SLOs, and operational gates belong to the integration architecture. diff --git a/template/pallets/actors/docs/embedding.md b/template/pallets/actors/docs/embedding.md index 8b397f1f..9080027a 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.24` +- **Release line**: `0.7.25` - **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.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. +The 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. @@ -32,6 +32,7 @@ The default profile proves deterministic unsupported optional adapters and all t | 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, dedicated refundable Actor-state hold, coordinator cursor, fresh storage version, try-state reconciliation, and nonzero production-derived classes | +| Host-qualified geometry | Every local Step count, Opening/tracked-funding allocation boundaries, over-cap admission rejection and frozen capture retention through each Step; empty funding remains distinct from zero-valued Opening balances | 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. @@ -41,7 +42,7 @@ An embedding runtime must provide only the bounded host surface that Actors cann - `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. +- `StepControlWeight`: Typed authority returning its production Weight identity, maximum Actor-control Weight for an exact Step/context, and valid actual Weight for committed phase/outcome/placement plus independent Task invocation and nonzero Action-fee collection facts. `StepControlExecution::task_effect` identifies Invoked receipts even for fee-exempt System Actors; `action_fee_collected` must not imply invocation when the Task was NotInvoked. The context distinguishes the inline head from its bounded tail fragment 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 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. @@ -65,9 +66,13 @@ An embedding runtime must provide only the bounded host surface that Actors cann - 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 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. +- Under `runtime-benchmarks`, `setup_predicate_assets` supplies distinct assets for general predicate fixtures. Production Current benchmarks require `setup_max_encoded_predicate_assets` and `setup_max_encoded_observation_feeds`: `predicate_set_evaluation` measures asset-heavy mixed totals, `predicate_asset_evaluation` measures pure non-native accounts, and `predicate_observation_heavy_evaluation` measures one fixed asset plus its parameter's number of fresh observations. Host records must have maximum encoded width. Default unsupported errors fail measurement explicitly. Repeated keys or short host records do not establish worst-case ProofSize. Additional host/refusal and cached-result diagnostics qualify the components beyond these mixtures. +- Opening predicate benchmarks hold the complete admitted Step/DNF traversal constant while varying the number and family of captured predicates. `opening_predicate_capture` uses the maximum host helpers above; `opening_predicate_traversal` measures the zero-result case with skipped Current predicates and no host reads. A zero capture count does not imply zero traversal work. The latter is an alternative zero-result owner, not an additional charge on nonzero capture or a direct aggregate that already covers the path. +- Production predicate-capture generation also requires `opening_max_encoded_balance_capture` for pure accounts and `opening_observation_heavy_capture` for one account plus the parameter's number of observations. Hosts combine applicable models within their measured domains; the observation parameter is total captures minus one and is valid only for totals of at least two. Review host refusal and mixed-state coverage separately. Generic fallback methods remain unmeasured estimates. +- Opening amount benchmarks retain the full configured Step count and verify exact typed snapshot values. `opening_snapshot_capture` requires maximum encoded non-native accounts through `setup_max_encoded_predicate_assets`; repeated surfaces exercise deduplication without increasing capture count. Target-asset diagnostics use the same account preparation. Staking-share diagnostics require `setup_max_encoded_staking_positions` to return distinct funded positions and their expected shares with maximum encoded receipt accounts; unsupported hosts fail explicitly. These component fixtures do not certify whole-Actor resource admission. +- Production amount-capture generation includes `opening_target_snapshot_capture` and `opening_share_mixed_capture` alongside the Preservable profile. Target has at most one entry per Step; the share-mixed profile holds one share surface and admits at most `2 * MaxContractSteps - 1` total entries. Each host must review its adapters and combine applicable generated models within their measured domains, preserving the separate zero-entry traversal. The standalone fallback values remain unmeasured estimates. -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. +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 and explicit mutability under the total-identity bound but own no contract or scheduler state; an Immutable dormant identity rejects activation and owner close. Runtime-specific custody-only addresses may remain outside generic actor storage when no Actor identity is intended. Deterministic User and System custody derivation survives host account-provider removal. Reattachment recovers the same address but does not recreate balances removed by external reaping, dust handling, or other host policy. diff --git a/template/pallets/actors/docs/specification.en.md b/template/pallets/actors/docs/specification.en.md index 11fdb7a8..bd0b6c1a 100644 --- a/template/pallets/actors/docs/specification.en.md +++ b/template/pallets/actors/docs/specification.en.md @@ -356,7 +356,7 @@ Semantic owners: Composite Actor values and runtime API views are read-only and MUST NOT become write models. -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. +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. Public creation admits Dormant only as Mutable; a host genesis configuration MAY declare a sealed Immutable System identity, which can never activate or close through Actor control. --- @@ -503,6 +503,8 @@ Opening consumes the current latch and re-arms the Trigger (§5.2): | `AtTime` | Never re-arm; `consumed == true`. | | `Cadenced` | Install the first canonical cadence deadline strictly after the current authoritative tick; no catch-up. | +These rules also apply when semantic Contract replacement preserves a latch acquired by the previous Trigger. Opening under `AtTime` consumes that one-shot source without installing another deadline. + 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 @@ -649,6 +651,8 @@ If the Actor cannot pay the Pipeline Machine fee, or its active-installed run-st 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). +If Trigger rearm requires a current authoritative observation (§4.5) and that observation is unavailable or uninitialized, Opening MUST atomically refuse. The latch, placement, cycle nonce, and prior Trigger payment remain unchanged; no Pipeline fee, Run, or Task effect commits. This refusal grants no bypass, retry Continuation, or observation-loss close authority. Independently applicable terminal checks, including insufficient Pipeline capacity, retain their existing precedence and do not require successful rearm (§9.2, §9.4). + ### 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. @@ -1016,7 +1020,7 @@ Rules: - 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; +- Zero/one-Step Contracts MUST NOT reserve a maximum-size 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; @@ -1238,6 +1242,8 @@ After bounded stale-head cleanup, the valid live FIFO head is authoritative: 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). +Required Opening observation unavailability likewise preserves the live head and stops the pass (§5.2), even when Weight remains. Later tickets cannot bypass that refusal. + ### 8.4 Detector workers, cohorts, and faults Detector geometry is source-specific: @@ -1273,12 +1279,15 @@ Given: - Recurring conforming Actor Control and Shared Economic capacity; - Finite stale churn; - Eventual placement capacity; +- Eventual availability of required authoritative observations and successful fee collection when valid payment capacity exists; - No structural invariant fault; all live tickets receive service in FIFO order. The protocol promises no fixed block latency. Increasing runnable population MAY increase inter-Step service gaps while preserving order and eventual service. +Without the required host observation or fee-collection prerequisite, a live head MAY prevent later service despite spare Weight. Only restored prerequisites or already-authorized lifecycle transitions can resolve that obstruction; liveness creates no additional close or scheduling authority. + Starvation telemetry MUST NOT change priority, order, or execution authority. --- @@ -1298,7 +1307,7 @@ Actor-scoped authorization: User Mutable MAY update Contract, pause/resume, cancel a run, deactivate, reactivate, and explicitly close. -Dormant creation requires `Mutable`. Immutable creation MUST install an Active Contract because no later activation authority exists. +Public Dormant creation requires `Mutable`. Public Immutable creation MUST install an Active Contract because no later activation authority exists. A host genesis configuration MAY declare an Immutable Dormant System identity as a permanently sealed no-Contract role; activation and owner close MUST reject it as Immutable. User Immutable: @@ -1928,12 +1937,12 @@ A deployed change MUST NOT make previously reattachable custody unreachable with Required relations: -1. `0 < MaxContractSteps <= 255`; production reference is 32. +1. `0 < MaxContractSteps <= 255`; each host runtime selects its bounded value. 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. +6. `MaxCrossingMembersPerFeed` covers the configured User membership allowance plus every separately bounded host-owned membership reserved for the feed; the reference split is 9,000 User and 1,000 System positions within 10,000 total. 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. @@ -1952,12 +1961,12 @@ TargetBlockTime = 6 seconds CadenceTick = 500 milliseconds MaxActiveActors = 10_000 MaxOwnerSlots = 255 -MaxContractSteps = 32 +MaxContractSteps = 12 MaxRetryAttempts = 10 MaxConsecutiveFailures = 10 MaxFundingTrackedAssets = 40 -MaxOpeningSnapshotEntries = 64 -MaxOpeningPredicateResults = 128 +MaxOpeningSnapshotEntries = 24 +MaxOpeningPredicateResults = 48 MaxPreconditionClauses = 4 MaxPredicatesPerClause = 4 MaxPredicatesPerStep = 4 diff --git a/template/pallets/actors/embedding-runtime/README.md b/template/pallets/actors/embedding-runtime/README.md index 68eb739d..0335e1c5 100644 --- a/template/pallets/actors/embedding-runtime/README.md +++ b/template/pallets/actors/embedding-runtime/README.md @@ -12,6 +12,7 @@ The fixture deliberately uses: - 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 `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. +- Every local Contract length from zero through eight Steps, mixed Opening/tracked-funding bounds, admission rejection above the funding cap, and immutable Opening captures through every nonterminal Step under the independent 8/16/16/4 limits. - No DEOS primitives, TMCTOL topology, governance catalog, DEOS Router, TMC, or staking pallet. Run its focused evidence from `template/`: diff --git a/template/pallets/actors/embedding-runtime/src/lib.rs b/template/pallets/actors/embedding-runtime/src/lib.rs index c349e080..8bfbbffa 100644 --- a/template/pallets/actors/embedding-runtime/src/lib.rs +++ b/template/pallets/actors/embedding-runtime/src/lib.rs @@ -1027,17 +1027,28 @@ mod tests { } #[test] - fn independent_runtime_metadata_exposes_split_actor_storage() { + fn independent_runtime_metadata_exposes_canonical_actor_storage() { let encoded = Runtime::metadata().encode(); for expected in [ b"Actors".as_slice(), - b"ActorHot".as_slice(), + b"ActorIdentities".as_slice(), + b"ActorControlLocators".as_slice(), + b"ActorUnsignaledControlCells".as_slice(), b"ActorContract".as_slice(), b"ActorFunding".as_slice(), b"ActorRunHead".as_slice(), b"ActorRunPayload".as_slice(), - b"QueuePages".as_slice(), - b"WakeupPages".as_slice(), + b"ActorReadyFrameChunks".as_slice(), + b"ActorReadyHead".as_slice(), + b"ActorReadyTail".as_slice(), + b"ActorReadyOccupancy".as_slice(), + b"ActorWaitingFrameChunks".as_slice(), + b"ActorWaitingHeads".as_slice(), + b"ActorWaitingTails".as_slice(), + b"ActorWaitingOccupancies".as_slice(), + b"ActorWaitingCursorIndices".as_slice(), + b"WakeupCursorPages".as_slice(), + b"WakeupCursorLen".as_slice(), b"Precondition".as_slice(), b"precondition".as_slice(), b"All".as_slice(), @@ -1105,7 +1116,7 @@ mod tests { assert_eq!(Actors::actor_identity_count(), 0); assert_eq!(Actors::active_actor_count(), 0); assert_eq!( - pallet_deos_actors::ActorHot::::iter_keys().count(), + pallet_deos_actors::ActorControlLocators::::iter_keys().count(), 0 ); }); @@ -1121,10 +1132,11 @@ mod tests { None, )); let actor_id = pallet_deos_actors::NextActorId::::get().saturating_sub(1); - let identity = Actors::actor_identities(actor_id).expect("identity exists"); + let identity = Actors::actor_identity(actor_id).expect("identity exists"); assert!(Actors::active_actor_state(actor_id).is_none()); assert_eq!(Actors::active_actor_count(), 0); - assert!(pallet_deos_actors::ActorHot::::get(actor_id).is_none()); + assert!(Actors::actor_hot(actor_id).is_none()); + assert!(!pallet_deos_actors::ActorControlLocators::::contains_key(actor_id)); // Spec 7.1: the existing dormant sovereign must cover the activation // prefunding requirement before the Active Actor Contract commits. let activate_plan = contract_steps(pallet_deos_actors::Task::Transfer { @@ -1157,7 +1169,7 @@ mod tests { let expected = user_prefunding_requirement(&activate_plan).saturating_add(1_000); assert_eq!(Balances::free_balance(identity.sovereign_account), expected); assert_ok!(Actors::close_actor(RuntimeOrigin::signed(ALICE), actor_id)); - assert!(Actors::actor_identities(actor_id).is_none()); + assert!(Actors::actor_identity(actor_id).is_none()); assert_eq!(Actors::actor_identity_count(), 0); assert_eq!(Actors::owner_slot_bitmap(ALICE), [0; 32]); assert_eq!(Balances::free_balance(identity.sovereign_account), expected); @@ -1414,6 +1426,138 @@ mod tests { }); } + #[test] + fn admitted_geometry_follows_local_bounds_through_every_step() { + use pallet_deos_actors::{AmountResolution, ObservationTiming, Predicate, TimedPredicate}; + let max_steps = ::MaxContractSteps::get(); + let max_funding = ::MaxFundingTrackedAssets::get(); + for step_count in 0..=max_steps { + let amount_positions = 2 * step_count; + let funding_boundary = amount_positions.min(max_funding); + let mut funding_counts = alloc::vec![0, funding_boundary]; + if amount_positions > max_funding { + funding_counts.push(max_funding + 1); + } + funding_counts.dedup(); + for funding_count in funding_counts { + new_test_ext().execute_with(|| { + System::set_block_number(1); + let amount = |position| { + if position < funding_count { + AmountResolution::PercentageOfLastFunding(Perbill::one()) + } else { + AmountResolution::PercentageAtOpening(Perbill::one()) + } + }; + let steps = (0..step_count) + .map(|index| pallet_deos_actors::Step { + precondition: Some(pallet_deos_actors::Precondition { + clauses: alloc::vec![ + alloc::vec![ + TimedPredicate { + timing: ObservationTiming::Opening, + predicate: Predicate::BlockNumberBelow { threshold: 0 } + }, + TimedPredicate { + timing: ObservationTiming::Opening, + predicate: Predicate::BlockNumberAbove { threshold: 0 } + }, + ] + .try_into() + .expect("two predicates fit") + ] + .try_into() + .expect("one clause fits"), + }), + task: pallet_deos_actors::Task::AddLiquidity { + asset_a: 10 + index * 2, + asset_b: 11 + index * 2, + amount_a: amount(index * 2), + amount_b: amount(index * 2 + 1), + min_lp_out: 1, + }, + on_error: pallet_deos_actors::StepErrorPolicy::AbortCycle, + }) + .collect(); + let contract = active_contract(pallet_deos_actors::Trigger::manual(), 0, steps); + if funding_count > max_funding { + assert_noop!( + Actors::create_system_actor( + RuntimeOrigin::root(), + ALICE, + pallet_deos_actors::Mutability::Mutable, + Some(contract) + ), + pallet_deos_actors::Error::::TooManyContractSteps + ); + return; + } + assert_ok!(Actors::create_system_actor( + RuntimeOrigin::root(), + ALICE, + pallet_deos_actors::Mutability::Mutable, + Some(contract) + )); + let actor_id = pallet_deos_actors::NextActorId::::get() - 1; + assert_eq!( + pallet_deos_actors::ActorFunding::::get(actor_id) + .expect("admitted funding state") + .funding_tracked_assets + .len(), + funding_count as usize + ); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + let mut opening = None; + for block in 1..=step_count.max(1) { + System::set_block_number(block.into()); + service_actor_idle(block.into()); + if block < step_count { + let run = + Actors::actor_run_state(actor_id).expect("nonterminal Step retains its Run"); + assert_eq!(run.cursor, block); + assert_eq!( + run.opening_snapshot.len(), + (amount_positions - funding_count) as usize + ); + assert!(run.opening_snapshot.values().all(|value| *value == 0)); + assert_eq!( + run.opening_predicate_results.len(), + (2 * step_count) as usize + ); + assert_eq!(run.opening_predicate_cursor, 2 * block); + assert!( + run.funding_snapshot.is_empty(), + "Opening cannot invent certified ingress" + ); + let captured = (run.opening_snapshot, run.opening_predicate_results); + if let Some(expected) = &opening { + assert_eq!( + &captured, expected, + "later Steps never recapture Opening facts" + ); + } else { + opening = Some(captured); + } + } else { + assert!(Actors::actor_run_state(actor_id).is_none()); + assert_eq!( + Actors::actor_identity(actor_id) + .expect("persistent Actor survives") + .cycle_nonce, + 1 + ); + } + #[cfg(feature = "try-runtime")] + assert_ok!(Actors::try_state(block.into())); + } + }); + } + } + } + #[test] fn direct_runtime_ingress_is_exact_once() { new_test_ext().execute_with(|| { @@ -1457,7 +1601,7 @@ mod tests { new_test_ext().execute_with(|| { System::set_block_number(1); let mut actor_ids = alloc::vec::Vec::new(); - for _ in 0..9 { + for _ in 0..33 { assert_ok!(Actors::create_system_actor( RuntimeOrigin::root(), ALICE, @@ -1477,12 +1621,19 @@ mod tests { actor_ids.push(actor_id); } assert_eq!( - pallet_deos_actors::WakeupPages::::iter().count(), + pallet_deos_actors::ActorWaitingFrameChunks::::iter().count(), 2 ); - for block in 20..=30 { + for block in 20..=60 { System::set_block_number(block); service_actor_idle(block); + if actor_ids + .iter(/* deos-bypass: bounded-iter — thirty-three-element embedding fixture. */) + .all(|actor_id| { + Actors::active_actor_state(*actor_id).is_some_and(|state| state.identity.cycle_nonce >= 1) + }) { + break; + } } for actor_id in &actor_ids { assert_eq!( @@ -1493,16 +1644,19 @@ mod tests { 1 ); } - assert_eq!(pallet_deos_actors::QueuePages::::iter().count(), 0); + assert_eq!( + pallet_deos_actors::ActorReadyFrameChunks::::iter().count(), + 0 + ); for actor_id in &actor_ids { assert!( - pallet_deos_actors::ActorHot::::get(*actor_id) + Actors::actor_hot(*actor_id) .expect("hot state remains") .trigger_wakeup_pointer .is_some() ); } - assert!(pallet_deos_actors::WakeupPages::::iter().count() >= 2); + assert!(pallet_deos_actors::ActorWaitingFrameChunks::::iter().count() >= 2); }); } @@ -1532,7 +1686,7 @@ mod tests { pallet_deos_actors::Mutability::Mutable, None, )); - let identity = Actors::actor_identities(fresh_id).expect("fresh system identity reattaches"); + let identity = Actors::actor_identity(fresh_id).expect("fresh system identity reattaches"); assert_eq!(identity.sovereign_account, sovereign); assert_eq!(Balances::free_balance(sovereign), 777); }); @@ -1775,8 +1929,7 @@ mod tests { )); 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"); + let before_hot = Actors::actor_hot(actor_id).expect("hot state exists"); assert_eq!( before.hot.cycle_state, pallet_deos_actors::CycleState::Suspended @@ -1802,8 +1955,7 @@ mod tests { Ok(Ok(_)) )); let after = Actors::active_actor_state(actor_id).expect("actor remains suspended"); - let after_hot = - pallet_deos_actors::ActorHot::::get(actor_id).expect("hot state remains"); + let after_hot = Actors::actor_hot(actor_id).expect("hot state remains"); assert!(after.hot.pending_signal); assert_eq!( after @@ -1824,27 +1976,35 @@ mod tests { Executive::apply_extrinsic(signed_extrinsic(ALICE, 1, repeated_call)), Ok(Ok(_)) )); - let repeated_hot = - pallet_deos_actors::ActorHot::::get(actor_id).expect("hot state remains"); + let repeated_hot = Actors::actor_hot(actor_id).expect("hot state remains"); 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_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); + let reprime_hot = Actors::actor_hot(actor_id).expect("hot state remains"); + assert!(reprime_hot.wakeup_pointer.is_none()); + assert!(reprime_hot.queue_ticket.is_some()); + assert_eq!( + Actors::active_actor_state(actor_id) + .expect("cancellation preserves the deferred Pipeline") + .identity + .cycle_nonce, + 1 + ); + #[cfg(feature = "try-runtime")] + assert!(Actors::try_state(2).is_ok()); System::set_block_number(3); 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 after cancelled cycle remains uncommitted") + .expect("latched run completes after the cancelled Cycle commits its nonce") .identity .cycle_nonce, - 1 + 2 ); }); } @@ -1986,12 +2146,9 @@ mod tests { #[test] fn independent_runtime_binds_nonzero_run_weights() { let suspend = as pallet_deos_actors::WeightInfo>::run_suspend(); - let retry = - as pallet_deos_actors::WeightInfo>::run_retry( - ); let cancel = as pallet_deos_actors::WeightInfo>::run_cancel(); - assert!(suspend.ref_time() > retry.ref_time()); - assert!(cancel.proof_size() > retry.proof_size()); + assert!(suspend.ref_time() > 0 && suspend.proof_size() > 0); + assert!(cancel.ref_time() > 0 && cancel.proof_size() > 0); } #[cfg(all(feature = "dex-fixture", feature = "try-runtime"))] diff --git a/template/pallets/actors/examples/fee_envelope_vectors.rs b/template/pallets/actors/examples/fee_envelope_vectors.rs index 8d6dc372..070b378f 100644 --- a/template/pallets/actors/examples/fee_envelope_vectors.rs +++ b/template/pallets/actors/examples/fee_envelope_vectors.rs @@ -1,5 +1,5 @@ use pallet_deos_actors::{ - ActorType, FeeChargeKind, FeeEnvelopeInput, compose_attempt_fee_envelope, + ActorType, FeeAssetClass, FeeChargeKind, FeeEnvelopeInput, compose_attempt_fee_envelope, fee_native_protected_minimum, settle_attempt_fee_step, }; use polkadot_sdk::frame_support::{BoundedVec, traits::ConstU32}; @@ -14,8 +14,8 @@ fn hex(bytes: &[u8]) -> String { bytes.iter().map(|byte| format!("{byte:02x}")).collect() // deos-bypass: bounded-iter -- fixed SHA-256 digest } -/// Hash a committed runtime evidence file into a hex identity. The metadata and -/// Actors weights files are the final release candidate's artifacts; a missing file +/// Hash a caller-supplied runtime evidence file into a hex identity. The metadata and +/// Actors weights files identify the embedding runtime's artifacts; a missing file /// fails closed so the fee-envelope binding cannot silently unbind. fn file_identity(path: &Path) -> String { let bytes = fs::read(path).unwrap_or_else(|error| { @@ -209,7 +209,11 @@ fn floor_case( min_user_balance: min_user_balance.to_string(), protected_minimum: fee_native_protected_minimum( actor_type, - is_fee_native, + if is_fee_native { + FeeAssetClass::FeeNative + } else { + FeeAssetClass::Other + }, asset_minimum, min_user_balance, ) @@ -217,12 +221,12 @@ fn floor_case( } } -fn manifest() -> FeeEnvelopeVectors { +fn manifest(metadata: &Path, weights: &Path) -> FeeEnvelopeVectors { FeeEnvelopeVectors { format: "deos.actor.fee-envelope-vectors", format_version: 2, - metadata_sha256: file_identity(Path::new("../web-client/.papi/metadata/deos.scale")), - weight_sha256: file_identity(Path::new("runtime/src/weights/pallet_deos_actors.rs")), + metadata_sha256: file_identity(metadata), + weight_sha256: file_identity(weights), vectors: vec![ vector(ActorType::User, 0), vector(ActorType::User, 1), @@ -265,9 +269,16 @@ fn manifest() -> FeeEnvelopeVectors { } fn main() { - let rendered = serde_json::to_string(&manifest()).expect("vectors serialize") + "\n"; let args = env::args().skip(1).collect::>(); - match args.as_slice() { + if matches!(args.as_slice(), [flag] if flag == "--help" || flag == "-h") { + println!("{USAGE}"); + return; + } + let (metadata, weights, output) = parse_args(&args).unwrap_or_else(|error| panic!("{error}")); + let rendered = serde_json::to_string(&manifest(Path::new(metadata), Path::new(weights))) + .expect("vectors serialize") + + "\n"; + match output { [] => print!("{rendered}"), [path] => { fs::write(Path::new(path), rendered).expect("fee-envelope vector artifact is writable"); @@ -276,10 +287,68 @@ fn main() { let actual = fs::read_to_string(Path::new(path)).expect("vector artifact is readable"); assert_eq!(actual, rendered, "fee-envelope vector artifact is stale"); } - _ => { - panic!( - "usage: cargo run -p pallet-deos-actors --example fee_envelope_vectors -- [PATH | --check PATH]" - ) + _ => unreachable!("output arguments were validated before reading artifacts"), + } +} + +const USAGE: &str = "usage: cargo run -p pallet-deos-actors --example fee_envelope_vectors -- --metadata METADATA_PATH --weights PRODUCTION_WEIGHTS_PATH [OUTPUT_PATH | --check OUTPUT_PATH]\nBoth evidence inputs are required. Omit OUTPUT_PATH to print JSON; --check compares exact bytes. Relative paths resolve from the current working directory."; + +fn parse_args(args: &[String]) -> Result<(&str, &str, &[String]), &'static str> { + let [metadata_flag, metadata, weights_flag, weights, output @ ..] = args else { + return Err(USAGE); + }; + if metadata_flag != "--metadata" + || weights_flag != "--weights" + || metadata.is_empty() + || weights.is_empty() + { + return Err(USAGE); + } + match output { + [] => {} + [path] if !path.is_empty() && !path.starts_with('-') => {} + [flag, path] if flag == "--check" && !path.is_empty() => {} + _ => return Err(USAGE), + } + Ok((metadata, weights, output)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn evidence_paths_are_explicit_and_output_modes_are_bounded() { + let inputs = [ + "--metadata", + "host/metadata.scale", + "--weights", + "host/weights.rs", + ]; + for output in [ + &[][..], + &["vectors.json"][..], + &["--check", "vectors.json"][..], + ] { + let args = inputs + .iter() + .chain(output) + .map(|arg| (*arg).to_owned()) + .collect::>(); + let (metadata, weights, actual_output) = parse_args(&args).expect("explicit paths accepted"); + assert_eq!(metadata, "host/metadata.scale"); + assert_eq!(weights, "host/weights.rs"); + assert_eq!(actual_output, output); + } + for invalid in [ + vec![], + vec!["--check", "vectors.json"], + vec!["--metadata", "file"], + vec!["--metadata", "file", "--weights", "weights", "--check"], + vec!["--metadata", "", "--weights", "weights"], + ] { + let args = invalid.into_iter().map(str::to_owned).collect::>(); + assert!(parse_args(&args).is_err()); } } } diff --git a/template/pallets/actors/proptest-regressions/tests.txt b/template/pallets/actors/proptest-regressions/tests.txt index 4702e35d..da37df94 100644 --- a/template/pallets/actors/proptest-regressions/tests.txt +++ b/template/pallets/actors/proptest-regressions/tests.txt @@ -4,4 +4,6 @@ # # It is recommended to check this file in to source control so that # everyone who runs the test benefits from these saved cases. -cc b75878323467f6484601fcbc14334befb1e1b2ac285e031ced1ad3857643853d # shrinks to operations = [Activate, Suspend, Deactivate] +cc f1606ed2dc2acfab8ba7e2c45ac51b96e430648bcff0603ecdb39f7e1b62aa64 # shrinks to corruption = 0 +cc cec77c2e2c7b28a0a6227814cabff859bea014cbca5daa134844ad95cdfbd8c7 # shrinks to creates = 1, closes = 0 +cc 68229b0922b63a921eec14025e36e8d803cc3c27730964461ac99cc24eced587 # shrinks to actor_count = 2 diff --git a/template/pallets/actors/src/adapters.rs b/template/pallets/actors/src/adapters.rs index 9807b89f..541fa0e7 100644 --- a/template/pallets/actors/src/adapters.rs +++ b/template/pallets/actors/src/adapters.rs @@ -327,7 +327,7 @@ impl AdmissionCertificateAuthorityProvider for () { } } -/// Exact physical location facts needed to price one lazy current-Step load. +/// Physical geometry and admission bounds for current-Step control pricing. #[derive( Clone, Copy, Debug, Decode, DecodeWithMemTracking, Encode, Eq, MaxEncodedLen, PartialEq, TypeInfo, )] @@ -338,6 +338,7 @@ pub struct StepControlWeightContext { pub predicate_evaluation_units: u32, pub opening_snapshot_entries: u32, pub opening_predicate_results: u32, + /// Configured bound for admission/Opening; retained count for a resumed head. pub funding_snapshot_entries: u32, } @@ -377,6 +378,10 @@ pub struct StepControlExecution { pub phase: StepControlPhase, pub outcome: StepControlOutcome, pub placement: StepControlPlacement, + /// Exact Task invocation branch, including zero-fee System receipts. + pub task_effect: TaskEffectExecution, + /// Nonzero Action fee collection required for this attempt to commit, separate from effects. + pub action_fee_collected: bool, } pub trait StepControlWeightProvider { @@ -410,7 +415,9 @@ impl StepControlWeightProvider for () { } /// Exact post-dispatch branch owned by one current-Step Task effect. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[derive( + Clone, Copy, Debug, Decode, DecodeWithMemTracking, Encode, Eq, MaxEncodedLen, PartialEq, TypeInfo, +)] pub enum TaskEffectExecution { /// Predicate evaluation, amount resolution, or funding classification ended before the Task /// operation was invoked. The exact effect Weight is zero. diff --git a/template/pallets/actors/src/benchmarking.rs b/template/pallets/actors/src/benchmarking.rs index c09abe28..9db80588 100644 --- a/template/pallets/actors/src/benchmarking.rs +++ b/template/pallets/actors/src/benchmarking.rs @@ -12,6 +12,49 @@ use polkadot_sdk::frame_support::traits::Hooks; use polkadot_sdk::frame_system::RawOrigin; use polkadot_sdk::sp_runtime::Perbill; +const BENCHMARK_ADMISSION_CELL_BYTES: usize = 171; +type BenchmarkAdmissionCell = [u8; BENCHMARK_ADMISSION_CELL_BYTES]; + +#[frame::storage_alias] +type BenchmarkAdmissionChunk4 = StorageMap< + Pallet, + Blake2_128Concat, + u32, + BoundedVec>, + OptionQuery, +>; +#[frame::storage_alias] +type BenchmarkAdmissionChunk8 = StorageMap< + Pallet, + Blake2_128Concat, + u32, + BoundedVec>, + OptionQuery, +>; +#[frame::storage_alias] +type BenchmarkAdmissionChunk16 = StorageMap< + Pallet, + Blake2_128Concat, + u32, + BoundedVec>, + OptionQuery, +>; +#[frame::storage_alias] +type BenchmarkAdmissionChunk32 = StorageMap< + Pallet, + Blake2_128Concat, + u32, + BoundedVec>, + OptionQuery, +>; +#[frame::storage_alias] +type BenchmarkAdmissionChunk64 = StorageMap< + Pallet, + Blake2_128Concat, + u32, + BoundedVec>, + OptionQuery, +>; #[benchmarks] mod benches { use super::*; @@ -20,6 +63,72 @@ mod benches { const CROSSING_NON_TAIL_BENCHMARK_MAX: u32 = 64; const CROSSING_TRIMMED_BENCHMARK_TAIL: u32 = CROSSING_NON_TAIL_BENCHMARK_MAX + 2; + fn control_named_cell(actor_id: ActorId) -> ActorControlCellOf { + let owner: T::AccountId = account("control-cell", actor_id as u32, 0); + ActorControlCell { + actor_id, + identity: ActorControlIdentity { + owner, + actor_class: ActorClass::System { + sovereign_id: actor_id, + }, + mutability: Mutability::Mutable, + cycle_nonce: 0, + last_control_mutation_block: 0u32.into(), + }, + hot: ActorControlHotState { + lifecycle: ActiveLifecycle::Active, + cycle_state: CycleState::Idle, + trigger_runtime_state: TriggerRuntimeState::Stateless, + unsuccessful_attempt_streak: 0, + pending_signal: true, + wakeup_pointer: None, + trigger_wakeup_pointer: None, + terminal_at: None, + schedule_anchor: 0u32.into(), + last_cycle_block: None, + }, + cursor: 0, + eligible_at: Some(1u32.into()), + admission: ActorAdmissionCertificate::>::new( + [1u8; 32], + [2u8; 32], + 1, + [3u8; 32], + 1, + [4u8; 32], + Weight::from_parts(1, 1), + ), + resources: ActorStepResourceEnvelope { + control: Weight::from_parts(1, 1), + effect: Weight::from_parts(1, 1), + }, + } + } + + fn control_named_chunk(first_actor_id: ActorId, len: u32) -> ActorControlChunkOf { + BoundedVec::try_from( + (0..len) + .map(|offset| { + Some(control_named_cell::( + first_actor_id.saturating_add(offset as u64), + )) + }) + .collect::>(), + ) + .expect("Actor control named C32 chunk fits") + } + + fn control_waiting_page(entries: ActorWaitingChunkOf) -> ActorWaitingPageOf { + ActorWaitingPageOf:: { + live_entries: entries.iter().filter(|entry| entry.is_some()).count() as u32, + entries, + scan_slot: 0, + previous_page: None, + next_page: None, + } + } + #[derive(Clone)] struct Schedule { trigger: Trigger, @@ -97,57 +206,24 @@ mod benches { .total } - fn current_all( - predicates: alloc::vec::Vec>, - ) -> PreconditionOf { - let clause = BoundedVec::try_from( - predicates - .into_iter() - .map(|predicate| TimedPredicate { - timing: ObservationTiming::Current, - predicate, - }) - .collect::>(), - ) - .expect("benchmark predicates fit"); - Precondition { - clauses: BoundedVec::try_from(alloc::vec![clause]).expect("clause fits"), - } - } - - 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 benchmark_predicate_capacity() -> u32 { + T::MaxPredicatesPerStep::get() + .min(T::MaxPreconditionClauses::get().saturating_mul(T::MaxPredicatesPerClause::get())) } - fn current_any( - predicates: alloc::vec::Vec>, + fn packed_predicate_clauses( + predicates: Vec>>, + width: u32, ) -> PreconditionOf { + assert!(width > 0 && width <= T::MaxPredicatesPerClause::get()); + assert!(predicates.len() as u32 <= benchmark_predicate_capacity::()); let clauses = predicates - .into_iter() - .map(|predicate| { - BoundedVec::try_from(alloc::vec![TimedPredicate { - timing: ObservationTiming::Current, - predicate, - }]) - .expect("benchmark predicate fits") - }) - .collect::>(); + .chunks(width as usize) + .map(|chunk| BoundedVec::try_from(chunk.to_vec()).expect("benchmark predicate clause fits")) + .collect::>(); Precondition { - clauses: BoundedVec::try_from(clauses).expect("clauses fit"), + clauses: BoundedVec::try_from(clauses) + .expect("host cannot represent benchmark predicate count"), } } @@ -178,6 +254,43 @@ mod benches { .expect("maximum Contract Steps must fit") } + /// Retains the requested structural geometry and protected measured Steps while reducing only + /// unrelated setup payload until ordinary component-wise admission accepts the Contract. + fn retain_admitted_contract_geometry( + actor_type: ActorType, + steps: &mut ContractSteps, + protected: &[u32], + ) -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let service = Pallet::::guaranteed_actor_service_weight().ok_or( + polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "host does not expose a valid Actor service floor", + ), + )?; + if Pallet::::contract_steps_admission_weight_upper(actor_type, steps).all_lte(service) { + return Ok(()); + } + for index in (0..steps.len()).rev() { + if protected.contains(&(index as u32)) { + continue; + } + steps[index] = Step { + precondition: None, + task: ActorTask::Transfer { + to: account("admitted-contract-filler", index as u32, 0), + asset: T::FeeNativeAssetId::get(), + amount: AmountResolution::AllAvailable, + }, + on_error: StepErrorPolicy::AbortCycle, + }; + if Pallet::::contract_steps_admission_weight_upper(actor_type, steps).all_lte(service) { + return Ok(()); + } + } + Err(polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "protected benchmark branch exceeds ordinary idle-budget admission", + )) + } + fn assert_max_contract_geometry(actor_id: ActorId) { let expected_tail_chunks = T::MaxContractSteps::get() .saturating_sub(1) @@ -234,6 +347,22 @@ mod benches { target_slot } + fn prefill_reachable_owner_slots(owner: &T::AccountId) -> u8 { + let target_slot = T::MaxOwnerSlots::get() + .checked_sub(1) + .expect("host must admit at least one owner slot"); + for slot in 0..target_slot { + Pallet::::create_user_actor_at_slot( + RawOrigin::Signed(owner.clone()).into(), + slot, + Mutability::Mutable, + None, + ) + .expect("dormant User slot guard is admitted"); + } + target_slot + } + // Observation scaffolding for lifecycle benchmarks. // // Each Actor owns at most one observation subscription. The helpers below keep the measured @@ -279,19 +408,359 @@ mod benches { ); } - /// Marks `feeds` dirty in list order. Ingress appends to the tail, so passing a slice ordered as - /// `[guard_low, measured.., guard_high]` leaves every measured feed a middle node. Must run after - /// the measured actor exists, because ingress skips feeds with no subscriber. - fn mark_observation_chain_dirty(feeds: &[T::ObservationFeedId]) { - for feed in feeds { - Pallet::::note_observation_changed(*feed, 1) - .expect("observation change ingress must succeed for a subscribed feed"); + fn benchmark_fixture_hot(actor_id: ActorId) -> Option> { + Pallet::::load_frame_control_authority(actor_id).map(|(_, _, hot, _)| hot) + } + + fn benchmark_fixture_scalar_hot(actor_id: ActorId) -> Option> { + benchmark_fixture_hot::(actor_id) + } + + fn benchmark_fixture_mutate_hot( + actor_id: ActorId, + mutate: impl FnOnce(&mut ActorHotStateOf), + ) { + let mut hot = + benchmark_fixture_hot::(actor_id).expect("benchmark fixture hot authority exists"); + let previous_ticket = hot.queue_ticket; + mutate(&mut hot); + let requested_ticket = hot.queue_ticket; + if let Some(ticket) = requested_ticket.filter(|ticket| Some(*ticket) != previous_ticket) { + // Prepare the successor from captured primary authority, before publishing the latch or Run + // transition. An Unsignaled pending/Running cell is not a valid reload boundary. + let mut cell = Pallet::::remove_primary_control_cell_inner(actor_id) + .expect("benchmark source primary exists"); + hot.queue_ticket = None; + hot.wakeup_pointer = None; + let run = ActorRunStateStore::::get(actor_id); + if hot.cycle_state == CycleState::Idle { + hot.pending_signal = true; + } + cell.hot = Pallet::::control_hot_from_scalar(hot); + cell.cursor = run.as_ref().map_or(0, |run| run.cursor); + cell.eligible_at = Some( + run + .as_ref() + .map_or_else(frame_system::Pallet::::block_number, |run| { + run.eligible_at + }), + ); + benchmark_fixture_advance_ready_tail::(ticket); + Pallet::::control_append_ready(cell).expect("benchmark Ready placement fits"); + return; } - assert_eq!( - DirtyObservationListState::::get().count, - feeds.len() as u32, - "every prepared observation feed must be dirty" - ); + if previous_ticket != requested_ticket { + if previous_ticket.is_some() { + assert_eq!(Pallet::::paged_invalidate(actor_id), previous_ticket); + } + hot.queue_ticket = None; + } + Pallet::::update_existing_frame_control_hot(actor_id, &hot) + .expect("benchmark fixture updates the existing primary"); + } + + fn benchmark_fixture_identity(actor_id: ActorId) -> Option> { + Pallet::::load_frame_control_authority(actor_id).map(|(_, identity, _, _)| identity) + } + + fn benchmark_fixture_scalar_identity(actor_id: ActorId) -> Option> { + Pallet::::actor_identity(actor_id) + } + + fn benchmark_fixture_contains_scalar_identity(actor_id: ActorId) -> bool { + Pallet::::actor_identity(actor_id).is_some() + } + + fn benchmark_fixture_mutate_identity( + actor_id: ActorId, + mutate: impl FnOnce(&mut ActorIdentityOf), + ) { + let mut identity = benchmark_fixture_scalar_identity::(actor_id) + .expect("benchmark fixture identity authority exists"); + mutate(&mut identity); + if ActorControlLocators::::contains_key(actor_id) { + Pallet::::update_existing_frame_control_identity(actor_id, &identity) + .expect("benchmark fixture updates active identity"); + } else { + ActorIdentities::::insert(actor_id, identity); + } + } + + fn benchmark_fixture_admission( + actor_id: ActorId, + ) -> Option> { + Pallet::::load_frame_control_authority(actor_id).map(|(_, _, _, admission)| admission) + } + + fn benchmark_fixture_scalar_admission( + actor_id: ActorId, + ) -> Option> { + benchmark_fixture_admission::(actor_id) + } + + fn benchmark_fixture_align_primary_control(actor_id: ActorId) { + let identity = + benchmark_fixture_scalar_identity::(actor_id).expect("benchmark identity exists"); + let hot = benchmark_fixture_scalar_hot::(actor_id).expect("benchmark hot state exists"); + let admission = + benchmark_fixture_scalar_admission::(actor_id).expect("benchmark admission exists"); + let location = ActorControlLocators::::get(actor_id).expect("benchmark primary exists"); + assert!(Pallet::::store_frame_control_authority( + actor_id, location, identity, hot, admission, + )); + } + + fn benchmark_fixture_publish_trigger_waiting( + actor_id: ActorId, + wakeup_tick: SchedulerTick, + ) { + let mut cell = Pallet::::remove_primary_control_cell_inner(actor_id) + .expect("benchmark temporal source primary exists"); + cell.cursor = ActorRunStateStore::::get(actor_id).map_or(0, |run| run.cursor); + cell.eligible_at = None; + Pallet::::control_append_waiting( + cell, + WakeupKey::Tick(wakeup_tick), + crate::scheduler::ActorWaitingAuthority::Trigger, + ) + .expect("benchmark temporal Waiting placement fits"); + } + + fn benchmark_fixture_schedule_service_waiting( + actor_id: ActorId, + wakeup_block: BlockNumberFor, + ) { + benchmark_fixture_prepare_service_waiting::(actor_id, wakeup_block)(); + } + + fn benchmark_fixture_prepare_consumed_service_waiting( + actor_id: ActorId, + wakeup_block: BlockNumberFor, + ) -> impl FnOnce() { + let (state, admission, _) = Pallet::::load_frame_actor_service_state(actor_id) + .expect("benchmark consumed source service authority exists"); + let (_, cell) = + Pallet::::actor_control_cell(actor_id).expect("benchmark consumed source primary exists"); + let mut hot = state.hot; + hot.pending_signal = true; + Pallet::::remove_primary_control_cell_inner(actor_id) + .expect("benchmark service source is consumed before Waiting publication"); + move || { + Pallet::::try_wakeup_substrate_schedule_transition_with_authority( + actor_id, + WakeupKey::Block(wakeup_block), + hot, + &state.identity, + cell.cursor, + &admission, + cell.resources, + ) + .expect("benchmark consumed service Waiting placement fits") + } + } + + fn benchmark_fixture_prepare_service_waiting( + actor_id: ActorId, + wakeup_block: BlockNumberFor, + ) -> impl FnOnce() { + let (state, admission, _) = Pallet::::load_frame_actor_service_state(actor_id) + .expect("benchmark source service authority exists"); + let (_, cell) = + Pallet::::actor_control_cell(actor_id).expect("benchmark source primary exists"); + let mut hot = state.hot; + hot.pending_signal = true; + move || { + Pallet::::try_wakeup_substrate_schedule_transition_with_authority( + actor_id, + WakeupKey::Block(wakeup_block), + hot, + &state.identity, + cell.cursor, + &admission, + cell.resources, + ) + .expect("benchmark latched service Waiting placement fits") + } + } + + fn benchmark_fixture_active_count() -> u32 { + ActorControlLocators::::iter_keys().count() as u32 + } + + fn benchmark_fixture_hot_actor_ids( + select: impl Fn(&ActorHotStateOf) -> bool, + ) -> alloc::vec::Vec { + ActorControlLocators::::iter_keys() + .filter_map(|actor_id| { + benchmark_fixture_hot::(actor_id) + .filter(|hot| select(hot)) + .map(|_| actor_id) + }) + .collect() + } + + fn benchmark_fixture_advance_ready_tail(ticket: QueueTicket) { + let tail = ActorReadyTail::::get(); + assert!(ticket >= tail, "fixture preserves global ticket chronology"); + if ActorReadyHead::::get() == tail { + ActorReadyHead::::put(ticket); + } else { + assert!(ticket - ActorReadyHead::::get() < u64::from(T::MaxQueueLength::get())); + for page_id in tail / 32..=ticket / 32 { + if !ActorReadyFrameChunks::::contains_key(page_id) { + ActorReadyFrameChunks::::insert( + page_id, + ActorControlChunkOf::::try_from(vec![None; 32]).expect("fixed Ready chunk"), + ); + } + } + } + ActorReadyTail::::put(ticket); + } + + fn benchmark_fixture_ready_page_len(page_id: QueuePageId) -> Option { + ActorReadyFrameChunks::::get(page_id).map(|page| page.len()) + } + + fn benchmark_fixture_contains_ready_page(page_id: QueuePageId) -> bool { + ActorReadyFrameChunks::::contains_key(page_id) + } + + fn benchmark_fixture_ready_enqueue(actor_id: ActorId) -> bool { + let Some((state, admission, _)) = Pallet::::load_frame_actor_service_state(actor_id) else { + return false; + }; + let Some((_, cell)) = Pallet::::actor_control_cell(actor_id) else { + return false; + }; + let mut hot = state.hot; + if hot.cycle_state == CycleState::Idle { + hot.pending_signal = true; + } + Pallet::::preflight_paged_enqueue_authority( + actor_id, + hot, + &state.identity, + state.run_state.as_ref(), + &admission, + cell.resources, + ) + .and_then(Pallet::::commit_paged_enqueue) + .is_ok() + } + + fn benchmark_fixture_prepare_consumed_ready_enqueue( + actor_id: ActorId, + ) -> impl FnOnce() -> bool { + let (state, admission, _) = Pallet::::load_frame_actor_service_state(actor_id) + .expect("benchmark destination authority exists"); + let (_, cell) = + Pallet::::actor_control_cell(actor_id).expect("benchmark destination primary exists"); + let mut hot = state.hot; + if hot.cycle_state == CycleState::Idle { + hot.pending_signal = true; + } + Pallet::::remove_primary_control_cell_inner(actor_id) + .expect("benchmark destination source is consumed before measurement"); + move || { + Pallet::::preflight_paged_enqueue_authority( + actor_id, + hot, + &state.identity, + state.run_state.as_ref(), + &admission, + cell.resources, + ) + .and_then(Pallet::::commit_paged_enqueue) + .is_ok() + } + } + + #[inline(always)] + fn benchmark_fixture_ready_consume_head(ticket: QueueTicket) -> bool { + Pallet::::paged_consume_head(ticket) + } + + #[inline(always)] + fn benchmark_fixture_ready_consume_loaded_head( + position: QueueTicket, + actor_id: ActorId, + ticket: QueueTicket, + hot: ActorHotStateOf, + ) -> bool { + Pallet::::paged_consume_loaded_head_at(position, actor_id, ticket, hot).is_ok() + } + + #[inline(always)] + fn benchmark_fixture_ready_drain_tombstones( + cutoff: QueueTicket, + scan_limit: u32, + ) -> Result { + Pallet::::paged_drain_tombstones(cutoff, scan_limit) + } + + #[inline(always)] + fn benchmark_fixture_ready_head_entry() + -> Option<(QueueTicket, QueueEntry>)> { + Pallet::::paged_head_entry() + } + + fn benchmark_fixture_ready_head() -> u64 { + ActorReadyHead::::get() + } + + fn benchmark_fixture_ready_tail() -> u64 { + ActorReadyTail::::get() + } + + fn benchmark_fixture_ready_occupancy() -> u32 { + ActorReadyOccupancy::::get() + } + + fn benchmark_fixture_next_ready_ticket() -> QueueTicket { + ActorReadyTail::::get() + } + + fn benchmark_fixture_set_next_ready_ticket(ticket: QueueTicket) { + ActorReadyTail::::put(ticket); + } + + fn benchmark_fixture_set_ready_queue_state(head: u64, tail: u64, occupancy: u32) { + ActorReadyHead::::put(head); + ActorReadyTail::::put(tail); + ActorReadyOccupancy::::put(occupancy); + } + + fn benchmark_fixture_seed_saturated_ready_tombstones() { + let pages = ActorReadyFrameChunks::::iter_keys().collect::>(); + for page in pages { + ActorReadyFrameChunks::::remove(page); + } + let capacity = T::MaxQueueLength::get(); + for page in 0..capacity.div_ceil(32) { + ActorReadyFrameChunks::::insert( + u64::from(page), + ActorControlChunkOf::::try_from(vec![None; 32]) + .expect("canonical saturated tombstone chunk fits"), + ); + } + ActorReadyHead::::put(0); + ActorReadyTail::::put(u64::from(capacity)); + ActorReadyOccupancy::::put(0); + } + + fn benchmark_fixture_reset_ready_queue() { + let ids = ActorControlLocators::::iter() + .filter_map(|(id, location)| { + matches!(location, ActorControlLocation::Ready { .. }).then_some(id) + }) + .collect::>(); + for id in ids { + assert!(Pallet::::paged_invalidate(id).is_some()); + } + let tail = ActorReadyTail::::get(); + let _ = ActorReadyFrameChunks::::clear(u32::MAX, None); + benchmark_fixture_set_ready_queue_state::(tail, tail, 0); } fn seed_actor_for_cycle(actor_id: ActorId) { @@ -560,7 +1029,7 @@ mod benches { .expect("dormant System identity creation must succeed"); } let actor_id = NextActorId::::get().saturating_sub(1); - assert!(ActorIdentities::::contains_key(actor_id)); + assert!(benchmark_fixture_contains_scalar_identity::(actor_id)); assert!(!Pallet::::active_actor_exists(actor_id)); } @@ -591,17 +1060,16 @@ mod benches { contract.expect("benchmark active contract"), ); 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()); } #[benchmark] - fn deactivate_actor() { + fn deactivate_actor() -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + frame_system::Pallet::::set_block_number(0u32.into()); let owner: T::AccountId = whitelisted_caller(); - let recipient: T::AccountId = account("deactivate-recipient", 0, 0); - let contract_steps = make_max_contract_steps::(recipient); + let (contract_steps, (asset_a, asset_b, amount_a, amount_b)) = reachable_retry_contract::()?; let feed = observation_feed_pool::(1)[0]; Pallet::::create_system_actor( RawOrigin::Root.into(), @@ -617,17 +1085,70 @@ mod benches { ) .expect("System Actors creation must succeed"); let actor_id = NextActorId::::get().saturating_sub(1); - install_run_state::(actor_id, T::MaxOpeningSnapshotEntries::get()); + let sovereign = Pallet::::actor_identity(actor_id) + .expect("created actor identity exists") + .sovereign_account; + T::AssetOps::mint(&sovereign, asset_a, amount_a).expect("first liquidity leg is funded"); + T::AssetOps::mint(&sovereign, asset_b, amount_b).expect("second liquidity leg is funded"); + let custody_before = ( + T::AssetOps::balance(&sovereign, asset_a), + T::AssetOps::balance(&sovereign, asset_b), + ); + let now: BlockNumberFor = 1u32.into(); + frame_system::Pallet::::set_block_number(now); + GlobalCircuitBreaker::::put(false); + Pallet::::note_observation_transition( + feed, + ObservationTransition { + revision: 2, + previous: Some(u128::MAX - 1), + current: u128::MAX, + }, + ) + .expect("real Crossing transition is admitted"); + while CrossingPendingFeedListState::::get().count > 0 { + Pallet::::crossing_work_unit().expect("Crossing transition materializes"); + } + assert!(benchmark_fixture_hot::(actor_id).is_some_and(|hot| hot.pending_signal)); + let (_, ready) = Pallet::::actor_control_cell(actor_id) + .expect("Crossing occurrence retains canonical Ready authority"); + let eligible_at = ready + .eligible_at + .expect("Ready authority has an eligibility boundary"); + frame_system::Pallet::::set_block_number( + frame_system::Pallet::::block_number().max(eligible_at), + ); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("real Crossing Ready fixture passes full state audit"); + Pallet::::execute_cycle(Weight::MAX); + assert_reachable_retry::(actor_id); + assert!( + CrossingMemberships::::contains_key(actor_id), + "Opening rearmed the detector removed by deactivate" + ); + assert_eq!( + ( + T::AssetOps::balance(&sovereign, asset_a), + T::AssetOps::balance(&sovereign, asset_b) + ), + custody_before, + "rejected liquidity attempt preserves custody" + ); #[extrinsic_call] deactivate_actor(RawOrigin::Signed(owner), actor_id); assert!(!Pallet::::active_actor_exists(actor_id)); - assert!(ActorIdentities::::contains_key(actor_id)); + assert!(benchmark_fixture_contains_scalar_identity::(actor_id)); assert!( ActorContractTailChunks::::iter_prefix(actor_id) .next() .is_none() ); assert!(!CrossingMemberships::::contains_key(actor_id)); + assert!(!ActorRunStateStore::::contains_key(actor_id)); + assert!(!ActorControlLocators::::contains_key(actor_id)); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("Crossing deactivation leaves no orphan state"); + Ok(()) } #[benchmark] @@ -696,17 +1217,18 @@ mod benches { 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"); + let hot = benchmark_fixture_hot::(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, + /// pending Pipeline. A one-Step Manual Contract is retained, while Crossing, ObservationChange, /// temporal membership, and Run state are intentionally absent from this apoptosis branch. + /// This direct cleanup fixture does not qualify the affordability decision or other Triggers. #[benchmark] fn pipeline_admission_apoptosis() { - let owner: T::AccountId = whitelisted_caller(); + let owner: T::AccountId = account("pipeline-apoptosis-owner", 0, 0); let recipient: T::AccountId = account("pipeline-apoptosis-recipient", 0, 0); let contract_steps = make_contract_steps::(recipient); ensure_creation_balance::(&owner); @@ -740,17 +1262,17 @@ mod benches { } #[benchmark] - fn close_actor() { - let owner: T::AccountId = whitelisted_caller(); + fn close_actor() -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + // Automatic cleanup reuses this Weight, including the owner's state-hold ledger access. + let owner: T::AccountId = account("close-owner", 0, 0); ensure_creation_balance::(&owner); - let owner_slot = prefill_owner_slots_for_worst_case::(&owner); - let recipient: T::AccountId = account("close-recipient", 0, 0); + let owner_slot = prefill_reachable_owner_slots::(&owner); let feed = observation_feed_pool::(1)[0]; let schedule = Schedule { trigger: Trigger::observation_crossing(feed, CrossingDirection::Rising, u128::MAX, 0), - cooldown_blocks: 1, + cooldown_blocks: 100, }; - let contract_steps = make_max_contract_steps::(recipient); + let (contract_steps, retry_funding) = reachable_retry_contract::()?; prefund_user_sovereign::(&owner, owner_slot, &contract_steps); Pallet::::create_user_actor_at_slot( RawOrigin::Signed(owner.clone()).into(), @@ -760,29 +1282,16 @@ mod benches { ) .expect("create_user_actor_at_slot must succeed in close_actor benchmark setup"); let actor_id = NextActorId::::get().saturating_sub(1); - let locator = CrossingMemberships::::get(actor_id).expect("Crossing membership exists"); - Pallet::::note_observation_transition( - feed, - ObservationTransition { - revision: 2, - previous: Some(1), - current: 2, - }, - ) - .expect("pending Crossing transition must be admitted"); - CrossingRangeCursors::::insert( - feed, - CrossingRangeCursor { - revision: 2, - traversal: locator.key.traversal, - search_bound: 2, - current_threshold: None, - page: 0, - offset: 0, - exhausted: false, - }, + open_reachable_retry::(actor_id, retry_funding); + assert_reachable_retry::(actor_id); + frame_system::Pallet::::set_block_number( + frame_system::Pallet::::block_number().saturating_add(One::one()), ); - install_run_state::(actor_id, T::MaxOpeningSnapshotEntries::get()); + publish_zero_step_observation::(feed, 1); + assert!(CrossingTransitionQueues::::contains_key(feed)); + assert!(!CrossingRangeCursors::::contains_key(feed)); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("reachable close premeasurement state is valid"); #[extrinsic_call] close_actor(RawOrigin::Signed(owner), actor_id); assert!(!Pallet::::active_actor_exists(actor_id)); @@ -796,26 +1305,294 @@ mod benches { assert!(!CrossingTransitionQueues::::contains_key(feed)); assert!(!CrossingRangeCursors::::contains_key(feed)); assert!(!CrossingPendingFeeds::::contains_key(feed)); + assert!(!ActorRunStateStore::::contains_key(actor_id)); + assert!(!ActorControlLocators::::contains_key(actor_id)); + assert!( + ActorContractTailChunks::::iter_prefix(actor_id) + .next() + .is_none() + ); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("close leaves no orphan state"); + Ok(()) } - // Diagnostic removal branch: delete the last page while the Crossing leaf survives. - #[benchmark] - fn close_actor_crossing_page() { - let owner: T::AccountId = whitelisted_caller(); + fn prepare_user_retry_opening_partition( + opening_amounts: u32, + opening_predicates: u32, + ) -> Result { + let owner: T::AccountId = account("close-owner", 0, 0); ensure_creation_balance::(&owner); + let owner_slot = prefill_reachable_owner_slots::(&owner); + let (steps, funding) = + retry_contract_with_opening_partition::(opening_amounts, opening_predicates)?; let feed = observation_feed_pool::(1)[0]; - let threshold = u128::MAX; - 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); - } - let owner_slot = prefill_owner_slots_for_worst_case::(&owner); - let recipient: T::AccountId = account("crossing-remove-page-recipient", 0, 0); - let contract_steps = make_contract_steps::(recipient); + prefund_user_sovereign::(&owner, owner_slot, &steps); + Pallet::::create_user_actor_at_slot( + RawOrigin::Signed(owner).into(), + owner_slot, + Mutability::Mutable, + user_contract::( + Schedule { + trigger: Trigger::observation_crossing(feed, CrossingDirection::Rising, 4, 1), + cooldown_blocks: 100, + }, + steps, + ), + ) + .expect("maximum-Step retry Opening partition is ordinarily admitted"); + let actor_id = NextActorId::::get().saturating_sub(1); + open_reachable_retry::(actor_id, funding); + let run = ActorRunStateStore::::get(actor_id).unwrap(); + assert_eq!(run.opening_snapshot.len() as u32, opening_amounts); + assert_eq!( + run.opening_predicate_results.len() as u32, + opening_predicates + ); + Ok(actor_id) + } + + /// Maximum Step count with independent Opening captures; other positions retain Current reads. + #[benchmark(pov_mode = Measured)] + fn close_actor_opening_partition( + o: Linear<0, { T::MaxContractSteps::get() * 2 }>, + p: Linear<0, { T::MaxContractSteps::get() * benchmark_predicate_capacity::() }>, + ) -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let actor_id = prepare_user_retry_opening_partition::(o, p)?; + let contract = Pallet::::load_actor_contract(actor_id).unwrap(); + let Trigger::ObservationCrossing { feed, .. } = contract.trigger else { + panic!("partition fixture owns a Crossing Trigger") + }; + let identity = benchmark_fixture_identity::(actor_id).unwrap(); + let retained_run = ActorRunStateStore::::get(actor_id).unwrap().encode(); + let occurrence_fee = Pallet::::trigger_fee_for_weight( + ActorType::User, + TriggerFamily::ObservationCrossing, + T::WeightInfo::observation_crossing_trigger_occurrence(), + ) + .trigger_fee; + T::AssetOps::mint( + &identity.sovereign_account, + T::FeeNativeAssetId::get(), + occurrence_fee, + ) + .unwrap(); + for value in [1, 4] { + frame_system::Pallet::::set_block_number( + frame_system::Pallet::::block_number().saturating_add(One::one()), + ); + if value == 1 { + publish_observation_at_most::(feed, 1); + } else { + assert!(publish_zero_step_observation::(feed, 1_000) >= value); + } + while CrossingPendingFeedListState::::get().count > 0 { + Pallet::::crossing_work_unit().expect("real retry occurrence materializes"); + } + assert!( + matches!(benchmark_fixture_hot::(actor_id).unwrap().trigger_runtime_state, + TriggerRuntimeState::ObservationCrossing { phase, .. } + if phase == if value == 1 { CrossingPhase::Armed } else { CrossingPhase::WaitingForRearm }), + "real transition changes the suspended Actor detector phase" + ); + } + assert!(benchmark_fixture_hot::(actor_id).unwrap().pending_signal); + assert_eq!( + ActorRunStateStore::::get(actor_id).unwrap().encode(), + retained_run + ); + let source = CrossingMemberships::::get(actor_id).unwrap(); + assert_eq!((source.page, source.offset), (0, 0)); + let page_size = T::CrossingPageSize::get(); + for index in 0..2 * page_size - 1 { + let guard = create_zero_step_crossing_guard::(index, feed, 4); + assert_eq!( + CrossingMemberships::::get(guard).unwrap().key, + source.key + ); + } + frame_system::Pallet::::set_block_number( + frame_system::Pallet::::block_number().saturating_add(One::one()), + ); + publish_observation_at_most::(feed, 1); + let mut candidates = 0u32; + for _ in 0..T::MaxCrossingTransitionsPerBlock::get() { + let (_, counters) = Pallet::::service_crossing_transitions_with_counters(Weight::MAX); + assert_eq!( + (counters.faults, counters.activations, counters.closes), + (0, 0, 0) + ); + candidates += counters.candidates; + if candidates > 1 { + break; + } + frame_system::Pallet::::set_block_number( + frame_system::Pallet::::block_number().saturating_add(One::one()), + ); + } + assert!(candidates > 1); + let cursor_before = CrossingRangeCursors::::get(feed).unwrap(); + assert_eq!(cursor_before.current_threshold, Some(source.key.threshold)); + assert_eq!(cursor_before.traversal, source.key.traversal); + assert!((source.page, source.offset) < (cursor_before.page, cursor_before.offset)); + let leaf = CrossingLeafStates::::get(source.key).unwrap(); + assert_eq!(leaf.page_count, 2); + assert_eq!(leaf.member_count, 2 * page_size - (candidates - 1)); + let tail = CrossingMemberPages::::get(source.key, leaf.tail_page).unwrap(); + assert!( + (leaf.tail_page, tail.entries.len() as u32 - 1) >= (cursor_before.page, cursor_before.offset) + ); + let moved_actor = tail.entries.last().unwrap().actor_id; + assert_ne!(moved_actor, actor_id); + assert_eq!( + ActorRunStateStore::::get(actor_id).unwrap().encode(), + retained_run + ); + assert!(matches!( + ActorControlLocators::::get(actor_id), + Some(ActorControlLocation::Waiting { .. }) + )); + assert!( + ActorRunStateStore::::get(actor_id).unwrap().eligible_at + > frame_system::Pallet::::block_number() + ); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("joint retry, deferred latch and real cursor are coherent"); + let native = T::FeeNativeAssetId::get(); + let custody = T::AssetOps::balance(&identity.sovereign_account, native); + let sink = T::AssetOps::balance(&T::FeeSink::get(), native); + #[block] + { + Pallet::::close_actor(RawOrigin::Signed(identity.owner).into(), actor_id).unwrap(); + } + assert!(!Pallet::::active_actor_exists(actor_id)); + assert!(!ActorStateHolds::::contains_key(actor_id)); + assert!(!ActorRunStateStore::::contains_key(actor_id)); + assert!(!ActorControlLocators::::contains_key(actor_id)); + assert!(!CrossingMemberships::::contains_key(actor_id)); + assert!(!IndexedTriggerDetectionDisabled::::contains_key( + actor_id + )); + let moved = CrossingMemberships::::get(moved_actor).unwrap(); + assert_eq!((moved.page, moved.offset), (0, 0)); + let mut expected_cursor = cursor_before; + expected_cursor.page = moved.page; + expected_cursor.offset = moved.offset; + assert_eq!(CrossingRangeCursors::::get(feed), Some(expected_cursor)); + assert_eq!( + CrossingLeafStates::::get(source.key) + .unwrap() + .member_count, + leaf.member_count - 1 + ); + assert!( + ActorContractTailChunks::::iter_prefix(actor_id) + .next() + .is_none() + ); + assert_eq!( + T::AssetOps::balance(&identity.sovereign_account, native), + custody + ); + assert_eq!(T::AssetOps::balance(&T::FeeSink::get(), native), sink); + frame_system::Pallet::::set_block_number( + frame_system::Pallet::::block_number().saturating_add(One::one()), + ); + let (_, resumed) = Pallet::::service_crossing_transitions_with_counters(Weight::MAX); + assert_eq!( + (resumed.faults, resumed.activations, resumed.closes), + (0, 0, 0) + ); + assert_ne!( + CrossingMemberships::::get(moved_actor).unwrap().key, + moved.key + ); + assert_eq!( + benchmark_fixture_identity::(moved_actor) + .unwrap() + .cycle_nonce, + 0 + ); + let guard_hot = benchmark_fixture_hot::(moved_actor).unwrap(); + assert!(!guard_hot.pending_signal && guard_hot.queue_ticket.is_none()); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("partitioned retry close leaves no orphan state"); + Ok(()) + } + + #[benchmark(extra, pov_mode = Measured)] + fn close_actor_mixed_waiting_reference() + -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let fixture = prepare_mixed_waiting_close::(false)?; + #[block] + { + Pallet::::close_actor(RawOrigin::Root.into(), fixture.actor_id).unwrap(); + } + assert_mixed_waiting_close::(&fixture, false); + Ok(()) + } + + #[benchmark(extra, pov_mode = Measured)] + fn close_actor_mixed_waiting_unlink() + -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let fixture = prepare_mixed_waiting_close::(true)?; + #[block] + { + Pallet::::close_actor(RawOrigin::Root.into(), fixture.actor_id).unwrap(); + } + assert_mixed_waiting_close::(&fixture, true); + Ok(()) + } + + #[benchmark(extra, pov_mode = Measured)] + fn close_actor_mixed_waiting_deadline() + -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let (fixture, keys) = prepare_mixed_waiting_deadline_close::()?; + #[block] + { + Pallet::::close_actor(RawOrigin::Root.into(), fixture.actor_id).unwrap(); + } + assert_mixed_waiting_deadline_close::(&fixture, keys); + Ok(()) + } + + #[benchmark(extra, pov_mode = Measured)] + fn close_actor_full_waiting_upward() { + let fixture = prepare_full_waiting_heap_close::(true); + #[block] + { + Pallet::::close_actor(RawOrigin::Root.into(), fixture.actor_id).unwrap(); + } + assert_full_waiting_heap_close::(&fixture); + } + + #[benchmark(extra, pov_mode = Measured)] + fn close_actor_full_waiting_tail() { + let fixture = prepare_full_waiting_heap_close::(false); + #[block] + { + Pallet::::close_actor(RawOrigin::Root.into(), fixture.actor_id).unwrap(); + } + assert_full_waiting_heap_close::(&fixture); + } + + // Diagnostic removal branch: delete the last page while the Crossing leaf survives. + #[benchmark] + fn close_actor_crossing_page() -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let owner: T::AccountId = account("close-owner", 0, 0); + ensure_creation_balance::(&owner); + let feed = observation_feed_pool::(1)[0]; + let threshold = u128::MAX; + 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); + } + let owner_slot = prefill_reachable_owner_slots::(&owner); + let (contract_steps, retry_funding) = reachable_retry_contract::()?; prefund_user_sovereign::(&owner, owner_slot, &contract_steps); let schedule = Schedule { trigger: Trigger::observation_crossing(feed, CrossingDirection::Rising, threshold, 0), - cooldown_blocks: 1, + cooldown_blocks: 100, }; Pallet::::create_user_actor_at_slot( RawOrigin::Signed(owner.clone()).into(), @@ -825,9 +1602,22 @@ mod benches { ) .expect("page-removal User setup must succeed"); let actor_id = NextActorId::::get().saturating_sub(1); + let pause_at = frame_system::Pallet::::block_number().saturating_add(One::one()); + frame_system::Pallet::::set_block_number(pause_at); + Pallet::::pause_actor(RawOrigin::Signed(owner.clone()).into(), actor_id) + .expect("target pauses before its real occurrence"); + open_reachable_retry::(actor_id, retry_funding); + assert_reachable_retry::(actor_id); + frame_system::Pallet::::set_block_number( + frame_system::Pallet::::block_number().saturating_add(One::one()), + ); let removed = CrossingMemberships::::get(actor_id).expect("tail-page membership exists"); assert_eq!(removed.page, 1); - install_run_state::(actor_id, T::MaxOpeningSnapshotEntries::get()); + let leaf = CrossingLeafStates::::get(removed.key).expect("full guard leaf survives Opening"); + assert_eq!(leaf.page_count, 2); + assert_eq!(leaf.member_count, T::CrossingPageSize::get() + 1); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("reachable close premeasurement state is valid"); #[block] { Pallet::::close_actor(RawOrigin::Signed(owner).into(), actor_id) @@ -839,24 +1629,33 @@ mod benches { assert_eq!(leaf.tail_page, 0); assert_eq!(leaf.page_count, 1); assert_eq!(leaf.member_count, T::CrossingPageSize::get()); + assert!(!ActorRunStateStore::::contains_key(actor_id)); + assert!(!ActorControlLocators::::contains_key(actor_id)); + assert!( + ActorContractTailChunks::::iter_prefix(actor_id) + .next() + .is_none() + ); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("close leaves no orphan state"); + Ok(()) } // Diagnostic removal branch: remove the tail member while its leaf page survives. #[benchmark] - fn close_actor_crossing_tail() { - let owner: T::AccountId = whitelisted_caller(); + fn close_actor_crossing_tail() -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let owner: T::AccountId = account("close-owner", 0, 0); ensure_creation_balance::(&owner); let feed = observation_feed_pool::(1)[0]; let threshold = u128::MAX; let guard_owner: T::AccountId = account("crossing-tail-guard", 0, 0); let guard = bench_create_system_crossing::(guard_owner, feed, threshold); - let owner_slot = prefill_owner_slots_for_worst_case::(&owner); - let recipient: T::AccountId = account("crossing-tail-recipient", 0, 0); - let contract_steps = make_contract_steps::(recipient); + let owner_slot = prefill_reachable_owner_slots::(&owner); + let (contract_steps, retry_funding) = reachable_retry_contract::()?; prefund_user_sovereign::(&owner, owner_slot, &contract_steps); let schedule = Schedule { trigger: Trigger::observation_crossing(feed, CrossingDirection::Rising, threshold, 0), - cooldown_blocks: 1, + cooldown_blocks: 100, }; Pallet::::create_user_actor_at_slot( RawOrigin::Signed(owner.clone()).into(), @@ -866,8 +1665,11 @@ mod benches { ) .expect("tail-removal User setup must succeed"); let actor_id = NextActorId::::get().saturating_sub(1); + open_reachable_retry::(actor_id, retry_funding); + assert_reachable_retry::(actor_id); let removed = CrossingMemberships::::get(actor_id).expect("tail membership exists"); - install_run_state::(actor_id, T::MaxOpeningSnapshotEntries::get()); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("reachable close premeasurement state is valid"); #[block] { Pallet::::close_actor(RawOrigin::Signed(owner).into(), actor_id) @@ -878,75 +1680,75 @@ mod benches { let leaf = CrossingLeafStates::::get(removed.key).expect("surviving leaf exists"); assert_eq!(leaf.page_count, 1); assert_eq!(leaf.member_count, 1); + assert!(!ActorRunStateStore::::contains_key(actor_id)); + assert!(!ActorControlLocators::::contains_key(actor_id)); + assert!( + ActorContractTailChunks::::iter_prefix(actor_id) + .next() + .is_none() + ); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("close leaves no orphan state"); + Ok(()) } - // Diagnostic removal branch: repair an in-progress range cursor after dense compaction. - #[benchmark] - fn close_actor_crossing_cursor_repair() { - let owner: T::AccountId = whitelisted_caller(); - ensure_creation_balance::(&owner); - let feed = observation_feed_pool::(1)[0]; - let threshold = u128::MAX; - let guard_owner: T::AccountId = account("crossing-cursor-guard", 0, 0); - let _ = bench_create_system_crossing::(guard_owner, feed, threshold); - let owner_slot = prefill_owner_slots_for_worst_case::(&owner); - let recipient: T::AccountId = account("crossing-cursor-recipient", 0, 0); - let contract_steps = make_contract_steps::(recipient); - prefund_user_sovereign::(&owner, owner_slot, &contract_steps); - let schedule = Schedule { - trigger: Trigger::observation_crossing(feed, CrossingDirection::Rising, threshold, 0), - cooldown_blocks: 1, - }; - Pallet::::create_user_actor_at_slot( - RawOrigin::Signed(owner.clone()).into(), - owner_slot, - Mutability::Mutable, - user_contract::(schedule, contract_steps), - ) - .expect("cursor-repair User setup must succeed"); - let actor_id = NextActorId::::get().saturating_sub(1); - let tail_owner: T::AccountId = account("crossing-cursor-tail", 0, 0); - let _ = bench_create_system_crossing::(tail_owner, feed, threshold); - let removed = CrossingMemberships::::get(actor_id).expect("middle membership exists"); - CrossingRangeCursors::::insert( - feed, - CrossingRangeCursor { - revision: 1, - traversal: removed.key.traversal, - search_bound: threshold, - current_threshold: Some(threshold), - page: removed.page, - offset: removed.offset.saturating_add(1), - exhausted: false, - }, - ); - install_run_state::(actor_id, T::MaxOpeningSnapshotEntries::get()); + // Public close removes a latched User before Opening, preserving a real detector cursor. + #[benchmark(pov_mode = Measured)] + fn close_actor_crossing_cursor_repair() + -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let fixture = prepare_zero_step_crossing_cursor::(true); + let actor_id = fixture.actor_id; + let identity = benchmark_fixture_identity::(actor_id).unwrap(); + let native = T::FeeNativeAssetId::get(); + let custody = T::AssetOps::balance(&identity.sovereign_account, native); + let sink = T::AssetOps::balance(&T::FeeSink::get(), native); + assert!(ActorStateHolds::::contains_key(actor_id)); + assert!(benchmark_fixture_hot::(actor_id).unwrap().pending_signal); + frame_system::Pallet::::reset_events(); #[block] { - Pallet::::close_actor(RawOrigin::Signed(owner).into(), actor_id) - .expect("cursor-repair Crossing close must succeed"); + Pallet::::close_actor(RawOrigin::Signed(identity.owner).into(), actor_id) + .expect("public close repairs the published Crossing traversal"); } - let cursor = CrossingRangeCursors::::get(feed).expect("range cursor survives"); - assert_eq!(cursor.page, removed.page); - assert_eq!(cursor.offset, removed.offset); + assert!(!Pallet::::active_actor_exists(actor_id)); + assert!(!ActorStateHolds::::contains_key(actor_id)); + assert!(!ActorRunStateStore::::contains_key(actor_id)); + assert!(!ActorControlLocators::::contains_key(actor_id)); + assert!(!CrossingMemberships::::contains_key(actor_id)); + assert!(!IndexedTriggerDetectionDisabled::::contains_key( + actor_id + )); + assert_eq!( + T::AssetOps::balance(&identity.sovereign_account, native), + custody + ); + assert_eq!(T::AssetOps::balance(&T::FeeSink::get(), native), sink); + frame_system::Pallet::::assert_has_event( + Event::::ActorClosed { + actor_id, + reason: CloseReason::OwnerInitiated, + } + .into(), + ); + assert_zero_step_crossing_cursor_topology::(&fixture, true); + Ok(()) } // Diagnostic removal branch: remove a dense middle member and repair the moved tail locator. #[benchmark] - fn close_actor_crossing_middle() { - let owner: T::AccountId = whitelisted_caller(); + fn close_actor_crossing_middle() -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let owner: T::AccountId = account("close-owner", 0, 0); ensure_creation_balance::(&owner); let feed = observation_feed_pool::(1)[0]; let threshold = u128::MAX; let guard_owner: T::AccountId = account("crossing-middle-guard", 0, 0); let _ = bench_create_system_crossing::(guard_owner, feed, threshold); - let owner_slot = prefill_owner_slots_for_worst_case::(&owner); - let recipient: T::AccountId = account("crossing-middle-recipient", 0, 0); - let contract_steps = make_contract_steps::(recipient); + let owner_slot = prefill_reachable_owner_slots::(&owner); + let (contract_steps, retry_funding) = reachable_retry_contract::()?; prefund_user_sovereign::(&owner, owner_slot, &contract_steps); let schedule = Schedule { trigger: Trigger::observation_crossing(feed, CrossingDirection::Rising, threshold, 0), - cooldown_blocks: 1, + cooldown_blocks: 100, }; Pallet::::create_user_actor_at_slot( RawOrigin::Signed(owner.clone()).into(), @@ -956,10 +1758,13 @@ mod benches { ) .expect("middle-removal User setup must succeed"); let actor_id = NextActorId::::get().saturating_sub(1); + open_reachable_retry::(actor_id, retry_funding); + assert_reachable_retry::(actor_id); 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_run_state::(actor_id, T::MaxOpeningSnapshotEntries::get()); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("reachable close premeasurement state is valid"); #[block] { Pallet::::close_actor(RawOrigin::Signed(owner).into(), actor_id) @@ -969,25 +1774,35 @@ mod benches { let moved = CrossingMemberships::::get(tail_id).expect("moved tail membership exists"); assert_eq!(moved.page, removed.page); assert_eq!(moved.offset, removed.offset); + assert!(!ActorRunStateStore::::contains_key(actor_id)); + assert!(!ActorControlLocators::::contains_key(actor_id)); + assert!( + ActorContractTailChunks::::iter_prefix(actor_id) + .next() + .is_none() + ); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("close leaves no orphan state"); + Ok(()) } // Diagnostic counterpart for broad ObservationChange cleanup; compare it with the production // Crossing close before accepting one conservative public close owner. #[benchmark] - fn close_actor_observation_change() { - let owner: T::AccountId = whitelisted_caller(); + fn close_actor_observation_change() -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> + { + let owner: T::AccountId = account("close-owner", 0, 0); ensure_creation_balance::(&owner); - let owner_slot = prefill_owner_slots_for_worst_case::(&owner); - let recipient: T::AccountId = account("close-observation-recipient", 0, 0); + let owner_slot = prefill_reachable_owner_slots::(&owner); let feeds = observation_feed_pool::(3); let measured = feeds[1]; install_observation_guard::(feeds[0], 0); install_observation_guard::(feeds[feeds.len() - 1], 1); let schedule = Schedule { trigger: observation_trigger::(measured), - cooldown_blocks: 1, + cooldown_blocks: 100, }; - let contract_steps = make_contract_steps::(recipient); + let (contract_steps, retry_funding) = reachable_retry_contract::()?; prefund_user_sovereign::(&owner, owner_slot, &contract_steps); Pallet::::create_user_actor_at_slot( RawOrigin::Signed(owner.clone()).into(), @@ -997,8 +1812,14 @@ mod benches { ) .expect("ObservationChange close benchmark setup must succeed"); let actor_id = NextActorId::::get().saturating_sub(1); - install_run_state::(actor_id, T::MaxOpeningSnapshotEntries::get()); - mark_observation_chain_dirty::(&feeds); + open_reachable_retry::(actor_id, retry_funding); + assert_reachable_retry::(actor_id); + for feed in &feeds { + Pallet::::note_observation_changed(*feed, 3).expect("close dirty topology is admitted"); + } + assert_eq!(DirtyObservationListState::::get().count, 3); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("reachable close premeasurement state is valid"); #[block] { Pallet::::close_actor(RawOrigin::Signed(owner).into(), actor_id) @@ -1006,6 +1827,16 @@ mod benches { } assert!(!Pallet::::active_actor_exists(actor_id)); assert_eq!(DirtyObservationListState::::get().count, 2); + assert!(!ActorRunStateStore::::contains_key(actor_id)); + assert!(!ActorControlLocators::::contains_key(actor_id)); + assert!( + ActorContractTailChunks::::iter_prefix(actor_id) + .next() + .is_none() + ); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("close leaves no orphan state"); + Ok(()) } // Diagnostic counterpart for the System branch; production close pricing uses the heavier User path. @@ -1034,140 +1865,70 @@ mod benches { } #[benchmark] - fn update_contract_observation_change() { - let caller: T::AccountId = whitelisted_caller(); - // Diagnostic broad branch replaces one dirty middle-node subscription with a disjoint feed. - // Pool layout: [slot donor, guard_low, replaced, installed, guard_high]. - let feeds = observation_feed_pool::(5); - let guard_high = feeds[4]; - seed_recycled_observation_slot::(feeds[0]); - install_observation_guard::(feeds[1], 0); - install_observation_guard::(guard_high, 1); - let replaced = feeds[2]; - let installed = feeds[3]; - let actor_id = - 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_run_state::(actor_id, T::MaxOpeningSnapshotEntries::get()); - ActorHot::::mutate(actor_id, |maybe_hot| { - maybe_hot - .as_mut() - .expect("benchmark actor hot state exists") - .pending_signal = true; - }); - let funding_assets = T::BenchmarkHelper::funding_assets(T::MaxFundingTrackedAssets::get()); - ActorFunding::::mutate(actor_id, |maybe| { - let funding = maybe.as_mut().expect("benchmark actor funding exists"); - for asset in funding_assets { - funding - .funding_accumulated - .try_insert(asset, One::one()) - .expect("funding accumulator benchmark bound fits"); - } - }); - let recipient = account("recipient", 0, 0); - let replacement = make_contract_steps::(recipient); - let mut allowed: BoundedBTreeSet = - BoundedBTreeSet::default(); - for index in 0..T::MaxWhitelistSize::get() { - allowed - .try_insert(account("funding-source", index, 0)) - .expect("funding source must fit benchmark bound"); + fn update_contract_observation_change() + -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + if T::MaxFundingTrackedAssets::get() == 0 { + return Err(polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "host cannot represent the nonempty funding-prune ObservationChange profile", + )); } - let funding = FundingSourcePolicy::SignedAllowlist(allowed); - #[extrinsic_call] - update_contract( - RawOrigin::Signed(caller), - actor_id, - ActorContract { - trigger: observation_trigger::(installed), - cooldown_blocks: 20, - window: None, - steps: replacement.clone(), - funding, - completion: CompletionPolicy::Persistent, - auto_close_at_cycle_nonce: None, - }, - ); - assert_eq!( - ActorObservationFeeds::::get(actor_id).map(|feeds| feeds.to_vec()), - Some(alloc::vec![installed]), - "measured update must replace the one observation source" - ); - let inst = - Pallet::::active_actor_view(actor_id).expect("Actors must exist after update_contract"); - assert_eq!(inst.steps, replacement); - assert_eq!(inst.cooldown_blocks, 20); - assert!( - ActorFunding::::get(actor_id) - .expect("actor funding exists") - .funding_accumulated - .is_empty() - ); + let opening_legs = T::MaxContractSteps::get() + .saturating_mul(2) + .saturating_sub(1); + let (owner, actor_id, replacement, old_feed) = + prepare_reachable_update::(opening_legs, TriggerFamily::ObservationChange)?; + let expected = replacement.clone(); + #[block] + { + execute_reachable_update::(owner, actor_id, replacement); + } + assert_reachable_update::(actor_id, &expected, old_feed); + Ok(()) } - // Production update owner: focused comparison proves unique-leaf Crossing replacement dominates - // the excluded broad ObservationChange branch in RefTime, ProofSize, reads, and writes. + // Reachable funding-heavy reference corner, not a proved envelope over the allocation frontier. + // Production Weight selection remains open until the extra diagnostic is measured in Wasm. #[benchmark] - fn update_contract() { - let caller: T::AccountId = whitelisted_caller(); - let feed = observation_feed_pool::(1)[0]; - 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_run_state::(actor_id, T::MaxOpeningSnapshotEntries::get()); - ActorHot::::mutate(actor_id, |maybe_hot| { - maybe_hot - .as_mut() - .expect("benchmark actor hot state exists") - .pending_signal = true; - }); - let funding_assets = T::BenchmarkHelper::funding_assets(T::MaxFundingTrackedAssets::get()); - ActorFunding::::mutate(actor_id, |maybe| { - let funding = maybe.as_mut().expect("benchmark actor funding exists"); - for asset in funding_assets { - funding - .funding_accumulated - .try_insert(asset, One::one()) - .expect("funding accumulator benchmark bound fits"); - } - }); - let recipient = account("crossing-update-recipient", 0, 0); - let replacement = make_max_contract_steps::(recipient); - let mut allowed: BoundedBTreeSet = - BoundedBTreeSet::default(); - for index in 0..T::MaxWhitelistSize::get() { - allowed - .try_insert(account("crossing-funding-source", index, 0)) - .expect("funding source must fit benchmark bound"); + fn update_contract() -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let opening_legs = T::MaxContractSteps::get() + .saturating_mul(2) + .saturating_sub(T::MaxFundingTrackedAssets::get()); + let (owner, actor_id, replacement, old_feed) = + prepare_reachable_update::(opening_legs, TriggerFamily::ObservationCrossing)?; + let expected = replacement.clone(); + #[block] + { + execute_reachable_update::(owner, actor_id, replacement); } - let funding = FundingSourcePolicy::SignedAllowlist(allowed); + assert_reachable_update::(actor_id, &expected, old_feed); + Ok(()) + } + + #[benchmark(extra)] + fn update_contract_reachable_allocation( + o: Linear< + { + T::MaxContractSteps::get() + .saturating_mul(2) + .saturating_sub(T::MaxFundingTrackedAssets::get()) + }, + { T::MaxContractSteps::get().saturating_mul(2) }, + >, + t: Linear<0, 1>, + ) -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let family = if t == 0 { + TriggerFamily::ObservationCrossing + } else { + TriggerFamily::ObservationChange + }; + let (owner, actor_id, replacement, old_feed) = prepare_reachable_update::(o, family)?; + let expected = replacement.clone(); #[block] { - Pallet::::update_contract( - RawOrigin::Signed(caller).into(), - actor_id, - ActorContract { - trigger: Trigger::observation_crossing(feed, CrossingDirection::Rising, u128::MAX, 0), - cooldown_blocks: 20, - window: None, - steps: replacement, - funding, - completion: CompletionPolicy::Persistent, - auto_close_at_cycle_nonce: None, - }, - ) - .expect("Crossing update must succeed"); + execute_reachable_update::(owner, actor_id, replacement); } - 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()); + assert_reachable_update::(actor_id, &expected, old_feed); + Ok(()) } #[benchmark] @@ -1322,12 +2083,10 @@ mod benches { let actor_id = NextActorId::::get().saturating_sub(1); // 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; + benchmark_fixture_mutate_identity::(actor_id, |identity| { + identity.cycle_nonce = u64::MAX; }); + benchmark_fixture_align_primary_control::(actor_id); actor_ids .try_push(actor_id) .expect("benchmark n must fit MaxSweepBatch"); @@ -1343,31 +2102,49 @@ mod benches { #[benchmark] fn fee_collection() { - let payer: T::AccountId = whitelisted_caller(); - let owner: T::AccountId = account("fee-sink-owner", 0, 0); - let schedule = Schedule { - trigger: Trigger::cadenced(1), - cooldown_blocks: 0, - }; - Pallet::::create_system_actor( - RawOrigin::Root.into(), - owner, - Mutability::Mutable, - system_contract::(schedule, make_inert_contract_steps::()), - ) - .expect("fee-collection benchmark sink must be created"); - let fee_sink_id = NextActorId::::get().saturating_sub(1); - let fee_sink = Pallet::::sovereign_account_id_system(fee_sink_id); + let payer: T::AccountId = account("fee-payer", 0, 0); + let fee_sink = T::FeeSink::get(); + assert_ne!(payer, fee_sink); let native = T::FeeNativeAssetId::get(); - let amount = T::MinUserBalance::get().saturating_add(One::one()); + let minimum = T::AssetOps::minimum_balance(native); + let amount = T::MinUserBalance::get() + .max(minimum) + .saturating_add(One::one()); T::AssetOps::mint(&payer, native, amount.saturating_mul(2u32.into())) .expect("fee-collection benchmark payer must be funded"); + T::AssetOps::mint(&fee_sink, native, minimum.max(One::one())) + .expect("configured fee sink must retain its account minimum"); + let payer_before = T::AssetOps::balance(&payer, native); + let sink_before = T::AssetOps::balance(&fee_sink, native); #[block] { - T::FeeCollector::collect_fee(&payer, &fee_sink, native, amount) - .expect("fee collection must succeed"); + Pallet::::collect_user_step_fee(&payer, amount).expect("fee collection must succeed"); } - assert!(T::AssetOps::balance(&fee_sink, native) >= amount); + assert_eq!(T::AssetOps::balance(&payer, native), payer_before - amount); + assert_eq!( + T::AssetOps::balance(&fee_sink, native), + sink_before + amount + ); + } + + /// Isolated invocation-receipt owner at maximum encoded field width. No Actor lifecycle, + /// Task effect, fee transfer or state-hold work is included in this measurement. + #[benchmark] + fn action_invocation_receipt() { + frame_system::Pallet::::set_block_number(1u32.into()); + let fee = ::max_value(); + let expected = Event::::ActionFeeCharged { + actor_id: ActorId::MAX, + cycle_nonce: u64::MAX, + step_index: u32::MAX, + actual_effect_weight: Weight::MAX, + fee, + }; + #[block] + { + Pallet::::deposit_action_fee_receipt(ActorId::MAX, u64::MAX, u32::MAX, Weight::MAX, fee); + } + frame_system::Pallet::::assert_last_event(expected.into()); } #[benchmark] @@ -1384,7 +2161,7 @@ mod benches { T::AssetOps::transfer(&caller, &recipient, native, amount) .expect("ingress-aware transfer must succeed"); } - assert!(ActorHot::::get(target_id).is_some_and(|hot| hot.wakeup_pointer.is_some())); + assert!(benchmark_fixture_hot::(target_id).is_some_and(|hot| hot.wakeup_pointer.is_some())); } #[benchmark] @@ -1415,13 +2192,18 @@ mod benches { { T::AssetOps::mint(&recipient, native, amount).expect("ingress-aware mint must succeed"); } - assert!(ActorHot::::get(target_id).is_some_and(|hot| hot.wakeup_pointer.is_some())); + assert!(benchmark_fixture_hot::(target_id).is_some_and(|hot| hot.wakeup_pointer.is_some())); } #[benchmark] - fn precondition_all_max() { + fn precondition_all_max() -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + if benchmark_predicate_capacity::() == 0 { + return Err(polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "host has no representable predicate capacity for this benchmark", + )); + } let actor: T::AccountId = account("condition-all", 0, 0); - let max_predicates = T::MaxPredicatesPerStep::get(); + let max_predicates = benchmark_predicate_capacity::(); let assets = T::BenchmarkHelper::setup_predicate_assets(&actor, max_predicates) .expect("predicate benchmark assets must be available"); assert!(assets.len() >= max_predicates as usize); @@ -1437,7 +2219,16 @@ mod benches { } }) .collect::>(); - let precondition = current_all::(predicates); + let precondition = packed_predicate_clauses::( + predicates + .into_iter() + .map(|predicate| TimedPredicate { + timing: ObservationTiming::Current, + predicate, + }) + .collect(), + T::MaxPredicatesPerClause::get(), + ); #[block] { assert_eq!( @@ -1445,12 +2236,20 @@ mod benches { Ok(true) ); } + Ok(()) } #[benchmark] - fn precondition_observation(c: Linear<1, 4>) { + fn precondition_observation( + c: Linear<1, { benchmark_predicate_capacity::().max(1) }>, + ) -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + if benchmark_predicate_capacity::() == 0 { + return Err(polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "host has no representable predicate capacity for this benchmark", + )); + } let actor: T::AccountId = account("condition-observation", 0, 0); - let bounded = c.min(T::MaxPredicatesPerStep::get()); + let bounded = c; let feeds = T::BenchmarkHelper::setup_observation_feeds(bounded) .expect("observation benchmark feeds must be available"); assert!(feeds.len() >= bounded as usize); @@ -1463,40 +2262,38 @@ mod benches { max_age_blocks: 100, }) .collect::>(); - let precondition = current_all::(predicates); + let precondition = packed_predicate_clauses::( + predicates + .into_iter() + .map(|predicate| TimedPredicate { + timing: ObservationTiming::Current, + predicate, + }) + .collect(), + T::MaxPredicatesPerClause::get(), + ); #[block] { let _ = Pallet::::evaluate_precondition(&precondition, &actor, T::Balance::zero()); } + Ok(()) } - #[benchmark] - fn predicate_set_evaluation(c: Linear<1, 4>) { - let actor: T::AccountId = account("condition-any", 0, 0); - let bounded = c.min(T::MaxPredicatesPerStep::get()); - let assets = T::BenchmarkHelper::setup_predicate_assets(&actor, bounded) - .expect("predicate benchmark assets must be available"); - assert!(assets.len() >= bounded as usize); - let predicates = assets - .into_iter() - .take(bounded as usize) - .map(|asset| { - T::AssetOps::mint(&actor, asset, T::MinUserBalance::get()) - .expect("condition benchmark asset must be funded"); - Predicate::BalanceAbove { - asset, - threshold: T::Balance::zero(), - } - }) - .collect::>(); - let precondition = current_any::(predicates); + #[benchmark(pov_mode = Measured)] + fn predicate_set_evaluation( + c: Linear<1, { benchmark_predicate_capacity::().max(1) }>, + ) -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let observations = u32::from(c >= 2); + let (actor, precondition) = + prepare_max_encoded_step_precondition::(c - observations, observations, false)?; + let result; #[block] { - assert_eq!( - Pallet::::evaluate_precondition(&precondition, &actor, T::Balance::zero()), - Ok(true) - ); + result = Pallet::::evaluate_precondition(&precondition, &actor, Zero::zero()); + core::hint::black_box(&result); } + assert_eq!(result, Ok(true)); + Ok(()) } #[benchmark] @@ -1536,7 +2333,9 @@ mod benches { } } for (target_id, _) in targets { - assert!(ActorHot::::get(target_id).is_some_and(|hot| hot.wakeup_pointer.is_some())); + assert!( + benchmark_fixture_hot::(target_id).is_some_and(|hot| hot.wakeup_pointer.is_some()) + ); } } @@ -1552,7 +2351,7 @@ mod benches { T::BenchmarkHelper::run_xcm_asset_deposit(&recipient, &source, amount) .expect("Actors-aware XCM deposit must succeed"); } - assert!(ActorHot::::get(target_id).is_some_and(|hot| hot.wakeup_pointer.is_some())); + assert!(benchmark_fixture_hot::(target_id).is_some_and(|hot| hot.wakeup_pointer.is_some())); } #[benchmark] @@ -1776,35 +2575,17 @@ mod benches { (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); + let identity = benchmark_fixture_identity::(actor_id).expect("benchmark identity exists"); + let certificate = + benchmark_fixture_admission::(actor_id).expect("benchmark canonical admission exists"); + let (head, chunks) = Pallet::::decompose_admitted_contract_geometry( + actor_id, + identity.actor_class.actor_type(), + contract, + &certificate, + ) + .expect("benchmark canonical Contract decomposes"); ActorContractHeads::::insert(actor_id, head); for (chunk_index, chunk) in chunks { ActorContractTailChunks::::insert(actor_id, chunk_index, chunk); @@ -1972,5277 +2753,10244 @@ mod benches { NextActorId::::get().saturating_sub(1) } - 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 = RunOpeningSnapshotOf::::default(); - for asset in assets { - if opening_snapshot.len() as u32 >= bounded { - break; - } - opening_snapshot - .try_insert(OpeningSurface::PreservableAsset(asset), One::one()) - .expect("benchmark snapshot asset entry fits"); - if opening_snapshot.len() as u32 >= bounded { - break; - } - opening_snapshot - .try_insert(OpeningSurface::StakingShares(asset), One::one()) - .expect("benchmark snapshot staking entry fits"); - } - assert_eq!(opening_snapshot.len() as u32, bounded); - 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() - .expect("benchmark actor hot state exists"); - hot.cycle_state = CycleState::Suspended; - hot.pending_signal = false; - hot.queue_ticket = None; - hot.wakeup_pointer = None; - }); - 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, - 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, - 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 fund_reachable_update_assets(actor_id: ActorId, amount: T::Balance) { + fund_reachable_assets_except::(actor_id, amount, None); } - 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"); + fn fund_reachable_assets_except( + actor_id: ActorId, + amount: T::Balance, + excluded: Option, + ) { + let identity = Pallet::::actor_identity(actor_id).expect("update Actor identity exists"); + let funding = ActorFunding::::get(actor_id).expect("update funding exists"); + T::BenchmarkHelper::enable_asset_ops_ingress(); + for asset in &funding.funding_tracked_assets { + if excluded == Some(*asset) { + continue; + } + T::AssetOps::mint(&identity.owner, *asset, amount.saturating_mul(2u32.into())) + .expect("authorized funding source has transferable custody"); + T::BenchmarkHelper::transfer_signed( + &identity.owner, + &identity.sovereign_account, + *asset, + amount, + ) + .expect("real owner ingress accumulates tracked funding"); } + let accumulated = ActorFunding::::get(actor_id).expect("funded Actor exists"); assert_eq!( - state.funding_snapshot.len() as u32, - T::MaxFundingTrackedAssets::get() + accumulated.funding_accumulated.len(), + funding + .funding_tracked_assets + .iter() + .filter(|asset| excluded != Some(**asset)) + .count() ); - for index in 0..T::MaxOpeningPredicateResults::get() { - state - .opening_predicate_results - .try_push(if index % 2 == 0 { - Ok(true) + assert!( + funding + .funding_tracked_assets + .iter() + .all(|asset| if excluded == Some(*asset) { + !accumulated.funding_accumulated.contains_key(asset) } else { - Err(PredicateError::InvalidObservation) + accumulated.funding_accumulated.get(asset) == Some(&amount) }) - .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) - .expect("benchmark cursor page id fits u32") - .saturating_mul(page_size); - let mut page = WakeupCursorPageOf::::default(); - for slot in 0..len { - let index = page_start.saturating_add(slot); - let block: BlockNumberFor = 1_000_000u32.saturating_add(index).into(); - page - .try_push(WakeupKey::Block(block)) - .expect("benchmark cursor page must fit configured bound"); - WakeupBuckets::::insert( - WakeupKey::Block(block), - WakeupBucketState { - head_page: 0, - tail_page: 0, - next_page_id: 1, - live_entries: 1, - cursor_index: Some(index), + fn prepare_reachable_update( + opening_legs: u32, + family: TriggerFamily, + ) -> Result< + ( + T::AccountId, + ActorId, + ActorContractOf, + T::ObservationFeedId, + ), + polkadot_sdk::frame_benchmarking::BenchmarkError, + > { + let caller: T::AccountId = whitelisted_caller(); + ensure_creation_balance::(&caller); + let (steps, liquidity_funding) = reachable_retry_contract_allocation::(opening_legs, 0)?; + let feeds = observation_feed_pool::(if family == TriggerFamily::ObservationChange { + 5 + } else { + 1 + }); + let (old_feed, trigger, replacement_trigger) = match family { + TriggerFamily::ObservationCrossing => ( + feeds[0], + Trigger::observation_crossing(feeds[0], CrossingDirection::Rising, 4, 1), + Trigger::observation_crossing(feeds[0], CrossingDirection::Rising, u128::MAX, 0), + ), + TriggerFamily::ObservationChange => { + seed_recycled_observation_slot::(feeds[0]); + install_observation_guard::(feeds[1], 0); + install_observation_guard::(feeds[4], 1); + ( + feeds[2], + observation_trigger::(feeds[2]), + observation_trigger::(feeds[3]), + ) + } + _ => panic!("update diagnostic admits only its two measured Trigger families"), + }; + prefund_active_user_creation::(&caller, &steps); + Pallet::::create_user_actor( + RawOrigin::Signed(caller.clone()).into(), + Mutability::Mutable, + user_contract::( + Schedule { + trigger, + cooldown_blocks: 100, }, - ); + steps, + ), + ) + .expect("reachable update Contract is admitted"); + let actor_id = NextActorId::::get().saturating_sub(1); + let funding_amount = liquidity_funding.2.min(liquidity_funding.3); + assert!(!funding_amount.is_zero()); + fund_reachable_update_assets::(actor_id, funding_amount); + open_reachable_retry::(actor_id, liquidity_funding); + let state = Pallet::::active_actor_state(actor_id).expect("update Run is live"); + let run = state.run_state.as_ref().expect("real update Run exists"); + assert_eq!( + run.opening_snapshot.len(), + Pallet::::opening_surfaces(&state.contract.steps, 0).len() + ); + assert_eq!( + run.funding_snapshot.len(), + state.funding.funding_tracked_assets.len() + ); + assert!( + run + .funding_snapshot + .values() + .all(|amount| *amount == funding_amount) + ); + assert!(state.funding.funding_accumulated.is_empty()); + let retained_run = run.encode(); + // A second paid family occurrence establishes the real deferred latch of the active Run. + match family { + TriggerFamily::ObservationCrossing => { + for rearm in [true, false] { + frame_system::Pallet::::set_block_number( + frame_system::Pallet::::block_number().saturating_add(One::one()), + ); + if rearm { + publish_observation_at_most::(old_feed, 1); + } else { + assert!(publish_zero_step_observation::(old_feed, 1_000) >= 4); + } + while CrossingPendingFeedListState::::get().count > 0 { + Pallet::::crossing_work_unit() + .expect("real deferred Crossing occurrence materializes"); + } + } + } + TriggerFamily::ObservationChange => { + Pallet::::note_observation_changed(old_feed, 3) + .expect("deferred observation occurrence is admitted"); + while DirtyObservationListState::::get().count > 0 { + Pallet::::do_fanout_dirty_observation_page() + .expect("deferred observation occurrence materializes"); + } + } + _ => unreachable!(), } - WakeupCursorPages::::insert((WakeupClock::Block, page_id), page); - } - - fn clear_host_genesis_wakeup_placements() { - let block_actor_ids = ActorHot::::iter() - .filter_map(|(actor_id, hot)| hot.wakeup_pointer.map(|_| actor_id)) - .collect::>(); - for actor_id in block_actor_ids { - Pallet::::wakeup_substrate_invalidate(actor_id) - .expect("host genesis Pipeline wakeup placement must be removable"); + assert!( + benchmark_fixture_hot::(actor_id) + .expect("deferred Actor exists") + .pending_signal + ); + assert_eq!( + ActorRunStateStore::::get(actor_id) + .expect("deferred Run remains live") + .encode(), + retained_run + ); + fund_reachable_update_assets::(actor_id, funding_amount); + if family == TriggerFamily::ObservationChange { + for feed in [feeds[1], old_feed, feeds[4]] { + Pallet::::note_observation_changed(feed, 4) + .expect("dirty middle-node topology is admitted"); + } + assert_eq!(DirtyObservationListState::::get().count, 3); } - 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"); + let mut allowed: BoundedBTreeSet = + BoundedBTreeSet::default(); + for index in 0..T::MaxWhitelistSize::get() { + allowed + .try_insert(account("update-funding-source", index, 0)) + .expect("full allowlist fits"); } - assert_eq!(WakeupCursorLen::::get(WakeupClock::Block), 0); - assert_eq!(WakeupCursorLen::::get(WakeupClock::Tick), 0); + let replacement = ActorContract { + trigger: replacement_trigger, + cooldown_blocks: 20, + window: None, + steps: make_max_contract_steps::(account("update-recipient", 0, 0)), + funding: FundingSourcePolicy::SignedAllowlist(allowed), + completion: CompletionPolicy::Persistent, + auto_close_at_cycle_nonce: None, + }; + assert_max_contract_geometry::(actor_id); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("reachable update premeasurement state passes full audit"); + Ok((caller, actor_id, replacement, old_feed)) } - fn add_wakeup_cursor_page(page_ids: &mut alloc::vec::Vec, index: u32, size: u32) { - let page_id = u64::from(index / size); - if !page_ids.contains(&page_id) { - page_ids.push(page_id); - } + fn execute_reachable_update( + owner: T::AccountId, + actor_id: ActorId, + replacement: ActorContractOf, + ) { + Pallet::::update_contract(RawOrigin::Signed(owner).into(), actor_id, replacement) + .expect("real Contract replacement succeeds"); } - fn prepare_wakeup_cursor_repair(start_index: u32) -> BlockNumberFor { - let page_size = T::WakeupPageSize::get(); - let cursor_len = T::MaxActiveActors::get(); + fn assert_reachable_update( + actor_id: ActorId, + expected: &ActorContractOf, + old_feed: T::ObservationFeedId, + ) { + assert_eq!( + Pallet::::load_actor_contract(actor_id) + .expect("replaced Contract exists") + .encode(), + expected.encode() + ); + assert!(!ActorRunStateStore::::contains_key(actor_id)); assert!( - page_size > 0 && cursor_len > start_index.saturating_add(1), - "benchmark requires bounded cursor depth" + benchmark_fixture_hot::(actor_id) + .expect("updated Actor is active") + .pending_signal ); - let last_index = cursor_len.saturating_sub(1); - let tail_page = u64::from(last_index / page_size); - let tail_len = (last_index % page_size).saturating_add(1); - let mut page_ids = alloc::vec::Vec::new(); - add_wakeup_cursor_page(&mut page_ids, last_index, page_size); - let mut current = start_index; - loop { - add_wakeup_cursor_page(&mut page_ids, current, page_size); - let left = current.saturating_mul(2).saturating_add(1); - if left >= cursor_len { - break; + let funding = ActorFunding::::get(actor_id).expect("updated funding exists"); + assert!(funding.funding_tracked_assets.is_empty()); + assert!(funding.funding_accumulated.is_empty()); + assert_max_contract_geometry::(actor_id); + match expected.trigger { + Trigger::ObservationCrossing { threshold, .. } => { + assert_eq!( + CrossingMemberships::::get(actor_id) + .expect("replacement Crossing membership exists") + .key + .threshold, + threshold + ); + assert!(ActorObservationFeeds::::get(actor_id).is_none()); } - add_wakeup_cursor_page(&mut page_ids, left, page_size); - let right = left.saturating_add(1); - if right < cursor_len { - add_wakeup_cursor_page(&mut page_ids, right, page_size); + Trigger::ObservationChange { feed } => { + assert_eq!( + ActorObservationFeeds::::get(actor_id).map(|feeds| feeds.to_vec()), + Some(vec![feed]) + ); + assert!(!DirtyObservationFeeds::::contains_key(old_feed)); + assert_eq!(DirtyObservationListState::::get().count, 2); } - current = left; + _ => unreachable!(), } - for page_id in page_ids { - let len = if page_id == tail_page { - tail_len - } else { - page_size + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("Contract replacement preserves full state invariants"); + } + + fn reachable_retry_contract_allocation( + opening_legs: u32, + opening_start: u32, + ) -> Result< + ( + ContractSteps, + (T::AssetId, T::AssetId, T::Balance, T::Balance), + ), + polkadot_sdk::frame_benchmarking::BenchmarkError, + > { + let (mut steps, liquidity_funding) = retry_contract_with_opening_partition::( + T::MaxContractSteps::get() * 2, + T::MaxContractSteps::get() * benchmark_predicate_capacity::(), + )?; + let leg_count = T::MaxContractSteps::get() + .checked_mul(2) + .expect("host amount surface bound fits"); + assert!(opening_legs <= leg_count); + assert!(opening_start < leg_count); + for (index, step) in steps.iter_mut().enumerate() { + let ActorTask::AddLiquidity { + amount_a, amount_b, .. + } = &mut step.task + else { + panic!("reachable retry allocation requires the admitted two-leg Contract"); }; - install_wakeup_cursor_page::(page_id, len); + for (offset, amount) in [amount_a, amount_b].into_iter().enumerate() { + let position = ((index * 2 + offset) as u32 + leg_count - opening_start) % leg_count; + if position >= opening_legs { + *amount = AmountResolution::PercentageOfLastFunding(Perbill::from_percent(50)); + } + } } - WakeupCursorLen::::insert(WakeupClock::Block, cursor_len); - 1_000_000u32.saturating_add(start_index).into() + retain_admitted_contract_geometry::(ActorType::System, &mut steps, &[0])?; + Ok((steps, liquidity_funding)) } - fn install_saturated_tombstone_queue() { - let page_size = T::QueuePageSize::get(); - let capacity = T::MaxQueueLength::get(); - for page_id in 0..capacity.div_ceil(page_size) { - let first = page_id.saturating_mul(page_size); - let len = page_size.min(capacity.saturating_sub(first)); - let entries = (0..len) - .map(|offset| { - benchmark_queue_entry::( - u64::from(first.saturating_add(offset)), - u64::MAX.saturating_sub(u64::from(first.saturating_add(offset))), - ) - }) - .collect::>(); - QueuePages::::insert( - u64::from(page_id), - BoundedVec::try_from(entries).expect("saturated queue page fits"), - ); - } - QueueHead::::put(0); - QueueTail::::put(u64::from(capacity)); - QueueOccupancy::::put(capacity); - NextQueueTicket::::put(u64::from(capacity)); + fn create_reachable_manual_retry( + opening_legs: u32, + opening_start: u32, + cooldown_blocks: u32, + ) -> Result< + (ActorId, (T::AssetId, T::AssetId, T::Balance, T::Balance)), + polkadot_sdk::frame_benchmarking::BenchmarkError, + > { + let owner: T::AccountId = account("manual-retry-owner", 0, 0); + ensure_creation_balance::(&owner); + let (steps, funding) = reachable_retry_contract_allocation::(opening_legs, opening_start)?; + let mut contract = system_contract::( + Schedule { + trigger: Trigger::Manual, + cooldown_blocks, + }, + steps, + ) + .expect("active Manual Contract exists"); + contract.funding = FundingSourcePolicy::OwnerOnly; + Pallet::::create_system_actor( + RawOrigin::Root.into(), + owner, + Mutability::Mutable, + Some(contract), + ) + .expect("real Manual retry Contract is admitted"); + let actor_id = NextActorId::::get().saturating_sub(1); + fund_reachable_update_assets::(actor_id, funding.2.min(funding.3)); + Ok((actor_id, funding)) } - fn prepare_saturated_address_actor( - seed: u32, - matched_source: Option, - ) -> (ActorId, T::AccountId) { - let owner: T::AccountId = account("ingress_owner", seed, 0); - let source_filter = if let Some(matched_source) = matched_source { - let mut allowed_sources = (0..T::MaxWhitelistSize::get().saturating_sub(2)) - .map(|index| account("ingress-source", index, 0)) - .collect::>(); - allowed_sources.push(owner.clone()); - allowed_sources.push(matched_source); - allowed_sources.sort_by_key(Encode::encode); - allowed_sources.dedup(); - assert_eq!( - allowed_sources.len() as u32, - T::MaxWhitelistSize::get(), - "benchmark source whitelist must saturate MaxWhitelistSize" - ); - SourceFilter::Whitelist( - BoundedVec::try_from(allowed_sources).expect("source whitelist must fit runtime bound"), - ) - } else { - SourceFilter::Any - }; - let schedule = Schedule { - trigger: Trigger::address_event(source_filter, AssetFilter::Any), - cooldown_blocks: 0, + fn prepare_reachable_suspended_head( + opening_legs: u32, + opening_start: u32, + ) -> Result<(ActorId, ActorStepTicketOf), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let (actor_id, funding) = create_reachable_manual_retry::(opening_legs, opening_start, 100)?; + open_reachable_retry::(actor_id, funding); + let run = ActorRunStateStore::::get(actor_id).expect("real retry Run exists"); + let retained_run = run.encode(); + frame_system::Pallet::::set_block_number(run.eligible_at); + let mut meter = polkadot_sdk::sp_weights::WeightMeter::with_limit(Weight::MAX); + let stats = Pallet::::drain_overdue_wakeups_cursor(run.eligible_at, &mut meter); + assert!(stats.ready_entries >= 1); + assert!(matches!( + ActorControlLocators::::get(actor_id), + Some(ActorControlLocation::Ready { .. }) + )); + assert_eq!( + ActorRunStateStore::::get(actor_id) + .expect("due Run remains") + .encode(), + retained_run + ); + let state = + Pallet::::active_actor_state(actor_id).expect("due Actor has canonical authority"); + let Some(ActorControlLocation::Ready { ticket }) = ActorControlLocators::::get(actor_id) + else { + panic!("actual due wakeup must publish a Ready ticket"); }; + let (_, cell) = Pallet::::actor_control_cell(actor_id).expect("Ready control cell exists"); + let ticket = Pallet::::build_actor_step_ticket( + actor_id, + ticket, + run.eligible_at, + &state.identity, + &state.hot, + state.run_state.as_ref(), + &cell.admission, + ) + .expect("canonical due authority produces the measured Step ticket"); + assert_eq!(state.hot.cycle_state, CycleState::Suspended); + assert_eq!( + run.opening_snapshot.len(), + Pallet::::opening_surfaces(&state.contract.steps, 0).len() + ); + assert_eq!( + run.funding_snapshot.len(), + state.funding.funding_tracked_assets.len() + ); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("real due Ready fixture passes full state audit"); + Ok((actor_id, ticket)) + } + + fn create_reachable_waiting_guard(seed: u32, wakeup_at: BlockNumberFor) -> ActorId { + let owner: T::AccountId = account("cancel-waiting-guard", seed, 0); + let mut contract = system_contract::( + Schedule { + trigger: Trigger::Manual, + cooldown_blocks: 0, + }, + make_inert_contract_steps::(), + ) + .expect("active guard Contract exists"); + contract.window = Some(ScheduleWindow { + start: wakeup_at, + end: wakeup_at.saturating_add(T::MinWindowLength::get()), + }); Pallet::::create_system_actor( RawOrigin::Root.into(), owner.clone(), Mutability::Mutable, - system_contract::(schedule, make_tracked_funding_contract_steps::(owner)), + Some(contract), ) - .expect("create_system_actor must succeed in ingress benchmark setup"); + .expect("future-window guard is admitted"); 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()); - 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 - .funding_accumulated - .try_insert(T::FeeNativeAssetId::get(), One::one()) - .expect("tracked funding accumulator fits"); - }); - install_saturated_tombstone_queue::(); - (actor_id, recipient) + Pallet::::manual_trigger(RawOrigin::Signed(owner).into(), actor_id) + .expect("guard occurrence waits for its authored window"); + assert!( + matches!(ActorControlLocators::::get(actor_id), Some(ActorControlLocation::Waiting { key: WakeupKey::Block(at), .. }) if at == wakeup_at) + ); + actor_id } - #[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)) + fn prepare_reachable_waiting_opening( + destination_entries: u32, + ) -> Result<(ActorId, u32, BlockNumberFor), polkadot_sdk::frame_benchmarking::BenchmarkError> + { + let (actor_id, count, keys, due) = prepare_reachable_waiting_publication::( + destination_entries, + 0, + ReachableOpeningProfile::RetryMin, + )?; + assert_eq!(count, 1); + Ok((actor_id, keys, due)) + } + + fn prepare_reachable_waiting_publication( + destination_entries: u32, + tail_chunks: u32, + profile: ReachableOpeningProfile, + ) -> Result< + (ActorId, u32, u32, BlockNumberFor), + polkadot_sdk::frame_benchmarking::BenchmarkError, + > { + let (actor_id, count) = prepare_reachable_opening::(tail_chunks, profile)?; + assert_eq!(WakeupCursorLen::::get(WakeupClock::Block), 0); + let due = frame_system::Pallet::::block_number().saturating_add(2u32.into()); + let active_room = Pallet::::effective_active_actor_limit() + .checked_sub(ActiveActorCount::::get()) + .expect("admitted Actors fit host capacity"); + let identity_room = T::MaxActorIdentities::get() + .checked_sub(ActorIdentityCount::::get()) + .expect("admitted identities fit host capacity"); + let sovereign_room = T::MaxSystemSovereigns::get() + .checked_sub(SystemSovereignCount::::get()) + .expect("admitted sovereigns fit host capacity"); + let guards = active_room.min(identity_room).min(sovereign_room); + assert!(guards > 0 && destination_entries <= guards && due < 1_000u32.into()); + frame::log::info!( + "Waiting population: active_room={}, identity_room={}, sovereign_room={}, guards={}", + active_room, + identity_room, + sovereign_room, + guards, ); + for index in 0..guards { + let deadline = if index < destination_entries { + due + } else { + 1_000u32.saturating_add(index).into() + }; + create_reachable_waiting_guard::(index, deadline); + } + let keys = guards - destination_entries.saturating_sub(1); + assert_waiting_population_capacity::(); + assert_eq!(WakeupCursorLen::::get(WakeupClock::Block), keys); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("complete populated pre-retry ownership"); + Ok((actor_id, count, keys, due)) + } + + fn assert_reachable_waiting_reference_publication( + actor_id: ActorId, + keys: u32, + due: BlockNumberFor, + ) { + let key = WakeupKey::Block(due); + assert_eq!(WakeupCursorLen::::get(WakeupClock::Block), keys + 1); + assert_eq!(ActorWaitingOccupancies::::get(key), 1); + let (location, cell) = + Pallet::::actor_control_cell(actor_id).expect("retained Ready primary exists"); + assert!(matches!(location, ActorControlLocation::Ready { .. })); + let pointer = cell + .hot + .wakeup_pointer + .expect("Block reference pointer exists"); + assert_eq!((pointer.block, pointer.page_id, pointer.slot), (key, 0, 0)); + assert!(matches!( + &ActorWaitingFrameChunks::::get((key, 0)) + .expect("new reference page exists") + .entries[0], + Some(ActorWaitingEntry::Reference(reference)) + if reference.actor_id == actor_id + && reference.admission_identity == cell.admission.admission_identity + )); + assert_waiting_population_capacity::(); + assert_wakeup_cursor_page_indices::(); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("complete populated Reference publication is legal"); + } + + fn assert_reachable_waiting_publication( + actor_id: ActorId, + count: u32, + keys: u32, + due: BlockNumberFor, + destination_entries: u32, + ) { + let expected_keys = keys + u32::from(destination_entries == 0); + let key = WakeupKey::Block(due); + assert_eq!(WakeupCursorLen::::get(WakeupClock::Block), expected_keys); assert_eq!( - CurrentBlockResourceState::::get().map(|state| state.phase()), - Some(BlockResourcePhase::ExternalPhase) + ActorWaitingOccupancies::::get(key), + destination_entries + 1 ); + assert!(matches!( + ActorControlLocators::::get(actor_id), + Some(ActorControlLocation::Waiting { key: stored, page, slot }) + if stored == key && page == u64::from(destination_entries / 32) + && u32::from(slot) == destination_entries % 32 + )); + let state = Pallet::::active_actor_state(actor_id) + .expect("consumed service authority is republished in Waiting"); + assert_eq!(state.contract.steps.len() as u32, count); + assert_eq!(state.hot.cycle_state, CycleState::Idle); + assert!(state.hot.pending_signal && state.run_state.is_none()); + assert_waiting_population_capacity::(); + assert_wakeup_cursor_page_indices::(); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("complete populated Waiting publication is legal"); } - #[benchmark] - fn scheduler_on_idle_base() { - let threshold = T::MaxIdleStarvationBlocks::get().max(1); - let now: BlockNumberFor = threshold.into(); - frame_system::Pallet::::set_block_number(now); - GlobalCircuitBreaker::::put(false); - 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()); - core::hint::black_box(QueueOccupancy::::get()); - core::hint::black_box(DirtyObservationListState::::get()); - Pallet::::update_idle_starvation_state(now, true); + fn prepare_reachable_cadenced_cross_clock_publication() -> Result< + ( + ActorId, + ActorControlLocation>, + SchedulerTick, + u32, + ), + polkadot_sdk::frame_benchmarking::BenchmarkError, + > { + frame_system::Pallet::::set_block_number(1u32.into()); + T::BenchmarkHelper::advance_to_scheduler_tick(2)?; + Pallet::::create_system_actor( + RawOrigin::Root.into(), + account("cadenced-cross-clock-target", 0, 0), + Mutability::Mutable, + system_contract::( + Schedule { + trigger: Trigger::cadenced(100), + cooldown_blocks: 0, + }, + make_inert_contract_steps::(), + ), + ) + .expect("ordinary Cadenced target is admitted"); + let actor_id = NextActorId::::get().saturating_sub(1); + let first = Pallet::::actor_hot(actor_id) + .and_then(|hot| hot.trigger_wakeup_pointer) + .expect("Cadenced target owns its initial Tick authority") + .tick; + T::BenchmarkHelper::finalize_scheduler_clock()?; + T::BenchmarkHelper::advance_to_scheduler_tick(first)?; + let now = frame_system::Pallet::::block_number(); + let mut meter = polkadot_sdk::sp_weights::WeightMeter::with_limit(Weight::MAX); + Pallet::::drain_overdue_wakeups_cursor(now, &mut meter); + let (location, cell) = + Pallet::::actor_control_cell(actor_id).expect("due Cadenced target is Ready"); + assert!(matches!(location, ActorControlLocation::Ready { .. })); + assert!(cell.hot.pending_signal && cell.hot.trigger_wakeup_pointer.is_none()); + let service_due = now.saturating_add(2u32.into()); + benchmark_fixture_prepare_consumed_service_waiting::(actor_id, service_due)(); + let block_location = ActorControlLocators::::get(actor_id) + .expect("consumed Cadenced source republishes one Block primary"); + assert!(matches!( + block_location, + ActorControlLocation::Waiting { + key: WakeupKey::Block(at), + .. + } if at == service_due + )); + + let active_room = Pallet::::effective_active_actor_limit() + .checked_sub(ActiveActorCount::::get()) + .expect("Cadenced guards fit active capacity"); + let identity_room = T::MaxActorIdentities::get() + .checked_sub(ActorIdentityCount::::get()) + .expect("Cadenced guards fit identity capacity"); + let sovereign_room = T::MaxSystemSovereigns::get() + .checked_sub(SystemSovereignCount::::get()) + .expect("Cadenced guards fit sovereign capacity"); + let guards = active_room.min(identity_room).min(sovereign_room); + for index in 0..guards { + Pallet::::create_system_actor( + RawOrigin::Root.into(), + account("cadenced-cross-clock-guard", index, 0), + Mutability::Mutable, + system_contract::( + Schedule { + trigger: Trigger::cadenced(u64::from(1_000 + index)), + cooldown_blocks: 0, + }, + make_inert_contract_steps::(), + ), + ) + .expect("ordinary Cadenced guard is admitted"); } + assert_waiting_population_capacity::(); + let next = first.checked_add(100).expect("next cadence fits"); + assert!( + Pallet::::wakeup_cursor_peek_key(WakeupClock::Tick) + .is_none_or(|minimum| minimum > WakeupKey::Tick(next)) + ); + let keys = WakeupCursorLen::::get(WakeupClock::Tick); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("complete cross-clock source population is legal"); + Ok((actor_id, block_location, next, keys)) } - #[benchmark] - fn materialization_coordinator_base() { - MaterializationFamilyCursor::::put(0); - let now = frame_system::Pallet::::block_number(); - #[block] - { - core::hint::black_box(Pallet::::materialization_family_has_work(0, now)); - core::hint::black_box(Pallet::::materialization_family_has_work(1, now)); - core::hint::black_box(Pallet::::materialization_family_has_work(2, now)); - let cursor = MaterializationFamilyCursor::::get(); - MaterializationFamilyCursor::::put(cursor.saturating_add(1) % 3); - } - assert_eq!(MaterializationFamilyCursor::::get(), 1); + fn prepare_reachable_max_cell_waiting_publication() -> Result< + (ActorId, usize, u32, BlockNumberFor), + polkadot_sdk::frame_benchmarking::BenchmarkError, + > { + let actor_id = prepare_temporal_control_cell::(false); + Pallet::::execute_cycle(Weight::MAX); + let first = ActorRunStateStore::::get(actor_id).expect("first Cadenced retry exists"); + frame_system::Pallet::::set_block_number(first.eligible_at); + Pallet::::execute_cycle(Weight::MAX); + let second = ActorRunStateStore::::get(actor_id).expect("second Cadenced retry exists"); + let (_, maximum) = + Pallet::::actor_control_cell(actor_id).expect("maximal suspended control cell exists"); + assert_eq!(maximum.hot.cycle_state, CycleState::Suspended); + assert!(maximum.hot.wakeup_pointer.is_some()); + assert!(maximum.hot.trigger_wakeup_pointer.is_some()); + assert!(maximum.hot.last_cycle_block.is_some() && maximum.eligible_at.is_some()); + let maximum_size = maximum.encoded_size(); + + frame_system::Pallet::::set_block_number(second.eligible_at); + let mut meter = polkadot_sdk::sp_weights::WeightMeter::with_limit(Weight::MAX); + let stats = Pallet::::drain_overdue_wakeups_cursor(second.eligible_at, &mut meter); + assert_eq!(stats.ready_entries, 1); + let (location, ready) = + Pallet::::actor_control_cell(actor_id).expect("due maximal-cell target is Ready"); + assert!(matches!(location, ActorControlLocation::Ready { .. })); + assert!(ready.hot.wakeup_pointer.is_none() && ready.hot.trigger_wakeup_pointer.is_some()); + + let due = second.eligible_at.saturating_add(2u32.into()); + let active_room = Pallet::::effective_active_actor_limit() + .checked_sub(ActiveActorCount::::get()) + .expect("maximal-cell population fits active capacity"); + let identity_room = T::MaxActorIdentities::get() + .checked_sub(ActorIdentityCount::::get()) + .expect("maximal-cell population fits identity capacity"); + let sovereign_room = T::MaxSystemSovereigns::get() + .checked_sub(SystemSovereignCount::::get()) + .expect("maximal-cell population fits sovereign capacity"); + let guards = active_room.min(identity_room).min(sovereign_room); + assert!(guards > 0 && due < 1_000u32.into()); + for index in 0..guards { + create_reachable_waiting_guard::(index, 1_000u32.saturating_add(index).into()); + } + assert_waiting_population_capacity::(); + let keys = WakeupCursorLen::::get(WakeupClock::Block); + assert_eq!(keys, guards); + assert!(Pallet::::wakeup_cursor_peek().is_none_or(|minimum| minimum > due)); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("maximal-cell publication source population is legal"); + Ok((actor_id, maximum_size, keys, due)) } - // Non-dispatch diagnostic benchmark excluded from runtime weight artifact generation - #[benchmark] - fn scheduler_on_idle_healthy_empty() { - let now: BlockNumberFor = 1u32.into(); - frame_system::Pallet::::set_block_number(now); - GlobalCircuitBreaker::::put(false); - QueueHead::::put(0); - QueueTail::::put(0); - QueueOccupancy::::put(0); - WakeupCursorLen::::insert(WakeupClock::Block, 0); - IdleStarvationState::::kill(); - #[block] - { - core::hint::black_box(Pallet::::on_idle(now, Weight::MAX)); - } - assert!(!IdleStarvationState::::exists()); + fn assert_reachable_waiting_opening( + actor_id: ActorId, + keys: u32, + due: BlockNumberFor, + ) { + assert_reachable_opening::(actor_id, 1, ReachableOpeningProfile::RetryMin); + assert_eq!(WakeupCursorLen::::get(WakeupClock::Block), keys); + assert_eq!(Pallet::::wakeup_cursor_peek(), Some(due)); + assert_waiting_population_capacity::(); + assert!(matches!( + ActorControlLocators::::get(actor_id), + Some(ActorControlLocation::Waiting { key: WakeupKey::Block(at), .. }) if at == due + )); + assert_wakeup_cursor_page_indices::(); } - #[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)); + fn assert_waiting_population_capacity() { + assert!( + ActiveActorCount::::get() == Pallet::::effective_active_actor_limit() + || ActorIdentityCount::::get() == T::MaxActorIdentities::get() + || SystemSovereignCount::::get() == T::MaxSystemSovereigns::get(), + "complete System population exhausts an explicit host capacity" + ); } - #[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 - )); - } + struct WaitingHeapClose { + actor_id: ActorId, + removed_key: WakeupKey>, + expected_heap: Vec>>, + active_count: u32, } - #[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"), - ); + fn prepare_full_waiting_heap_close(upward: bool) -> WaitingHeapClose { + frame_system::Pallet::::set_block_number(1u32.into()); + assert_eq!(WakeupCursorLen::::get(WakeupClock::Block), 0); + let guards = (Pallet::::effective_active_actor_limit() - ActiveActorCount::::get()) + .min(T::MaxActorIdentities::get() - ActorIdentityCount::::get()) + .min(T::MaxSystemSovereigns::get() - SystemSovereignCount::::get()); + let size = T::WakeupPageSize::get(); + let shared = (guards - 1) % size; + let keys = guards - shared; + assert!(keys > size * 2); + let horizon: BlockNumberFor = keys + .checked_mul(3) + .and_then(|range| range.checked_add(10)) + .expect("bounded fixture deadline range") + .into(); + assert!(horizon <= T::MaxExecutionDelayBlocks::get()); + let root_branch = |mut index: u32| { + while index > 2 { + index = (index - 1) / 2; + } + index + }; + let low_branch = root_branch(keys - 1); + let removed_index = if upward { + (3..keys - 1) + .rev() + .find(|index| root_branch(*index) != low_branch) + .expect("opposite subtree contains a deep removal target") + } else { + keys - 1 + }; + let mut actor_id = None; + let mut heap = Vec::new(); + for index in 0..keys { + let deadline = if index == 0 { + 10 + } else if root_branch(index) == low_branch { + keys + 10 + index + } else { + 2 * keys + 10 + index + }; + let actor = create_reachable_waiting_guard::(index, deadline.into()); + if index == removed_index { + actor_id = Some(actor); + } + let key = WakeupKey::Block(deadline.into()); + assert_eq!(ActorWaitingCursorIndices::::get(key), Some(index)); + heap.push(key); + } + for index in 0..shared { + create_reachable_waiting_guard::(keys + index, 10u32.into()); + } + assert_waiting_population_capacity::(); + assert_eq!(WakeupCursorLen::::get(WakeupClock::Block), keys); + assert_eq!(keys % size, 1); + assert_wakeup_cursor_page_indices::(); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("complete public-created Waiting heap is legal"); + let removed_key = heap.swap_remove(removed_index as usize); + if upward { + // The tail is in the low subtree; every ancestor below the opposite root is high. + // It replaces the removed key and moves to that root, shifting only this path down. + let mut index = removed_index; + assert!(heap[index as usize] < heap[((index - 1) / 2) as usize]); + while index > 2 { + let parent = (index - 1) / 2; + heap.swap(index as usize, parent as usize); + index = parent; + } + } + frame::log::info!( + "Waiting removal population: guards={}, keys={}, removed_index={}, upward={}", + guards, + keys, + removed_index, + upward, + ); + WaitingHeapClose { + actor_id: actor_id.unwrap(), + removed_key, + expected_heap: heap, + active_count: ActiveActorCount::::get(), } - 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 - )); + fn assert_full_waiting_heap_close(fixture: &WaitingHeapClose) { + assert!(Pallet::::actor_identity(fixture.actor_id).is_none()); + assert!(!ActorControlLocators::::contains_key(fixture.actor_id)); + assert_eq!(ActiveActorCount::::get(), fixture.active_count - 1); + let key = fixture.removed_key; + assert!(!ActorWaitingOccupancies::::contains_key(key)); + assert!(!ActorWaitingHeads::::contains_key(key)); + assert!(!ActorWaitingTails::::contains_key(key)); + assert!(!ActorWaitingCursorIndices::::contains_key(key)); + assert!(!ActorWaitingFrameChunks::::contains_key((key, 0))); + let size = T::WakeupPageSize::get(); + let len = fixture.expected_heap.len() as u32; + assert_eq!(WakeupCursorLen::::get(WakeupClock::Block), len); + assert!(!WakeupCursorPages::::contains_key(( + WakeupClock::Block, + u64::from(len / size) + ))); + for (index, expected) in fixture.expected_heap.chunks(size as usize).enumerate() { + let page = WakeupCursorPages::::get((WakeupClock::Block, index as u64)).unwrap(); + assert_eq!(page.as_slice(), expected); } + assert_wakeup_cursor_page_indices::(); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("exact close preserves the complete Waiting heap"); } - #[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"); + struct MixedWaitingClose { + actor_id: ActorId, + tick: SchedulerTick, + sovereign: T::AccountId, + custody: T::Balance, + active_count: u32, + head_entries: Vec, + tail_entries: Vec, + retired_sovereigns: Vec, + } + + fn prepare_mixed_waiting_close( + unlink: bool, + ) -> Result, polkadot_sdk::frame_benchmarking::BenchmarkError> { + if T::MaxContractSteps::get() < 2 { + return Err(polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "mixed Waiting reference requires a two-Step Contract", + )); + } + frame_system::Pallet::::set_block_number(1u32.into()); + T::BenchmarkHelper::advance_to_scheduler_tick(2)?; + let create = |seed: u32, every_ticks: u64, steps: ContractSteps| { + Pallet::::create_system_actor( + RawOrigin::Root.into(), + account("mixed-waiting-owner", seed, 0), + Mutability::Mutable, + system_contract::( + Schedule { + trigger: Trigger::cadenced(every_ticks), + cooldown_blocks: 0, + }, + steps, + ), + ) + .expect("ordinary Cadenced Actor is admitted"); + NextActorId::::get().saturating_sub(1) + }; + let mut tick = 0; + for seed in 0..32 { + let actor = create(seed, 200, make_inert_contract_steps::()); + let pointer = Pallet::::actor_hot(actor) + .and_then(|hot| hot.trigger_wakeup_pointer) + .expect("Cadenced guard owns a temporal primary"); + if seed == 0 { + tick = pointer.tick; + } + assert_eq!(pointer.tick, tick); + } + let mut steps = inert_contract_steps_of_len::(2); + steps[0].precondition = Some(packed_predicate_clauses::( + vec![TimedPredicate { + timing: ObservationTiming::Current, + predicate: Predicate::BlockNumberBelow { threshold: 0 }, + }], + 1, + )); + let actor_id = create(32, 100, steps); + let first = Pallet::::actor_hot(actor_id) + .and_then(|hot| hot.trigger_wakeup_pointer) + .expect("target owns its first cadence") + .tick; + T::BenchmarkHelper::finalize_scheduler_clock()?; + T::BenchmarkHelper::advance_to_scheduler_tick(first)?; + let mut meter = polkadot_sdk::sp_weights::WeightMeter::with_limit(Weight::MAX); + Pallet::::drain_overdue_wakeups_cursor( + frame_system::Pallet::::block_number(), + &mut meter, + ); + let (location, cell) = Pallet::::actor_control_cell(actor_id).expect("due target exists"); + assert!(matches!(location, ActorControlLocation::Ready { .. })); + frame_system::Pallet::::set_block_number( + frame_system::Pallet::::block_number().max(cell.eligible_at.unwrap()), + ); + Pallet::::execute_cycle(Weight::MAX); + let run = Pallet::::actor_run_state(actor_id).expect("real Opening retains its suffix"); + assert_eq!(run.cursor, 1); + let retained_run = run.encode(); + let hot = Pallet::::actor_hot(actor_id).expect("Running target exists"); + assert_eq!(hot.cycle_state, CycleState::Running); + let pointer = hot.trigger_wakeup_pointer.expect("Opening rearms cadence"); + assert_eq!((pointer.tick, pointer.page_id, pointer.slot), (tick, 1, 0)); + let key = WakeupKey::Tick(tick); + let period = tick + .checked_sub(Pallet::::current_scheduler_tick().unwrap()) + .expect("shared deadline remains in the future"); + let mut later = Vec::new(); + for seed in 33..96 { + later.push(create(seed, period, make_inert_contract_steps::())); + } + let middle = ActorWaitingFrameChunks::::get((key, 1)).expect("mixed middle page exists"); + assert_eq!(middle.live_entries, 32); + assert!(matches!(&middle.entries[0], + Some(ActorWaitingEntry::Reference(reference)) if reference.actor_id == actor_id)); + for (entry, actor) in middle.entries.iter().skip(1).zip(&later[..31]) { + assert!(matches!(entry, + Some(ActorWaitingEntry::Primary(cell)) if cell.actor_id == *actor)); + } + if unlink { + for actor in &later[..31] { + Pallet::::close_actor(RawOrigin::Root.into(), *actor) + .expect("ordinary close leaves the reference as the last middle entry"); + } } assert_eq!( - Pallet::::load_actor_contract(actor_id).map(|contract| contract.cooldown_blocks), - Some(1) + ActorWaitingOccupancies::::get(key), + if unlink { 65 } else { 96 } + ); + assert_eq!( + Pallet::::actor_run_state(actor_id).unwrap().encode(), + retained_run ); + let head = ActorWaitingFrameChunks::::get((key, 0)).unwrap(); + let tail = ActorWaitingFrameChunks::::get((key, 2)).unwrap(); + assert_eq!((head.live_entries, tail.live_entries), (32, 32)); + let sovereign = Pallet::::actor_identity(actor_id) + .unwrap() + .sovereign_account; + let native = T::FeeNativeAssetId::get(); + let amount = T::AssetOps::minimum_balance(native) + .max(One::one()) + .saturating_mul(10u32.into()); + T::AssetOps::mint(&sovereign, native, amount).expect("surviving custody is funded"); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("real mixed Waiting and Running authority agree"); + Ok(MixedWaitingClose { + actor_id, + tick, + custody: T::AssetOps::balance(&sovereign, native), + sovereign, + active_count: ActiveActorCount::::get(), + head_entries: head.entries.encode(), + tail_entries: tail.entries.encode(), + retired_sovereigns: if unlink { + later[..31].to_vec() + } else { + Vec::new() + }, + }) } - #[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)); - } + fn assert_mixed_waiting_close(fixture: &MixedWaitingClose, unlink: bool) { + let key = WakeupKey::Tick(fixture.tick); + assert_mixed_waiting_target_closed::(fixture); + assert_eq!( + ActorWaitingOccupancies::::get(key), + if unlink { 64 } else { 95 } + ); + let head = ActorWaitingFrameChunks::::get((key, 0)).unwrap(); + let tail = ActorWaitingFrameChunks::::get((key, 2)).unwrap(); + assert_eq!(head.entries.encode(), fixture.head_entries); + assert_eq!(tail.entries.encode(), fixture.tail_entries); + assert_eq!(head.next_page, Some(if unlink { 2 } else { 1 })); + assert_eq!(tail.previous_page, Some(if unlink { 0 } else { 1 })); + if unlink { + assert!(!ActorWaitingFrameChunks::::contains_key((key, 1))); + } else { + let middle = ActorWaitingFrameChunks::::get((key, 1)).unwrap(); + assert_eq!(middle.live_entries, 31); + assert!(middle.entries[0].is_none()); + } + assert!(ActorWaitingCursorIndices::::contains_key(key)); + assert_wakeup_clock_page_indices::(WakeupClock::Tick); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("public close preserves mixed Waiting ownership"); } - #[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); + fn assert_mixed_waiting_target_closed(fixture: &MixedWaitingClose) { + assert!(Pallet::::actor_identity(fixture.actor_id).is_none()); + assert!(!ActorControlLocators::::contains_key(fixture.actor_id)); + assert!(!ActorRunHeads::::contains_key(fixture.actor_id)); + assert!(!ActorRunPayloads::::contains_key(fixture.actor_id)); + assert!(!ActorContractHeads::::contains_key(fixture.actor_id)); + assert!(!ActorFunding::::contains_key(fixture.actor_id)); + assert!(!ActorStateHolds::::contains_key(fixture.actor_id)); + assert!( + ActorContractTailChunks::::iter_prefix(fixture.actor_id) + .next() + .is_none() + ); + assert_eq!(ActiveActorCount::::get(), fixture.active_count - 1); assert_eq!( - u32::try_from(n.saturating_sub(1).div_ceil(4)).expect("chunk count fits"), - c + T::AssetOps::balance(&fixture.sovereign, T::FeeNativeAssetId::get()), + fixture.custody ); - #[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")); + fn prepare_mixed_waiting_deadline_close() + -> Result<(MixedWaitingClose, u32), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let mut fixture = prepare_mixed_waiting_close::(true)?; + let key = WakeupKey::Tick(fixture.tick); + let run = Pallet::::actor_run_state(fixture.actor_id) + .unwrap() + .encode(); + for page in [0, 2] { + let chunk = ActorWaitingFrameChunks::::get((key, page)).unwrap(); + for entry in chunk.entries.into_iter().flatten() { + let ActorWaitingEntry::Primary(cell) = entry else { + panic!("neighbor guards own primary cells"); + }; + Pallet::::close_actor(RawOrigin::Root.into(), cell.actor_id).unwrap(); + fixture.retired_sovereigns.push(cell.actor_id); + } } - } - - #[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"), + assert_eq!(fixture.retired_sovereigns.len(), 95); + assert_eq!(ActorWaitingOccupancies::::get(key), 1); + let active_room = Pallet::::effective_active_actor_limit() - ActiveActorCount::::get(); + let identity_room = T::MaxActorIdentities::get() - ActorIdentityCount::::get(); + let fresh_room = T::MaxSystemSovereigns::get() - SystemSovereignCount::::get(); + let guards = active_room.min(identity_room).min(fresh_room + 95); + let size = T::WakeupPageSize::get(); + let maximum_keys = WakeupCursorLen::::get(WakeupClock::Tick) + guards; + let shared = (maximum_keys - 1) % size; + assert!(guards > 95 && guards > shared && guards > size); + frame::log::info!( + "Tick cleanup population: active_room={}, identity_room={}, fresh_room={}, reused=95, guards={}, keys={}", + active_room, + identity_room, + fresh_room, + guards, + maximum_keys - shared, + ); + for index in 0..guards { + let period = 1_000 + if index <= shared { 0 } else { u64::from(index) }; + let contract = system_contract::( + Schedule { + trigger: Trigger::cadenced(period), + cooldown_blocks: 0, + }, + make_inert_contract_steps::(), ); + let owner = account("waiting-deadline-owner", index, 0); + if let Some(sovereign_id) = fixture.retired_sovereigns.get(index as usize) { + assert_eq!( + SystemSovereigns::::get(sovereign_id), + Some(SystemSovereignState::Vacant) + ); + Pallet::::create_system_actor_at_sovereign_id( + RawOrigin::Root.into(), + *sovereign_id, + owner, + Mutability::Mutable, + contract, + ) + .expect("ordinary creation reuses a known retired guard sovereign"); + assert_eq!( + SystemSovereigns::::get(sovereign_id), + Some(SystemSovereignState::Occupied(NextActorId::::get() - 1)) + ); + } else { + Pallet::::create_system_actor( + RawOrigin::Root.into(), + owner, + Mutability::Mutable, + contract, + ) + .expect("ordinary creation fills remaining host capacity"); + } } + fixture.active_count = ActiveActorCount::::get(); + assert_waiting_population_capacity::(); + let keys = WakeupCursorLen::::get(WakeupClock::Tick); + assert_eq!(keys, maximum_keys - shared); + assert_eq!(keys % size, 1); + assert_eq!(ActorWaitingCursorIndices::::get(key), Some(0)); + assert_eq!( + WakeupCursorPages::::get((WakeupClock::Tick, 0)).unwrap()[0], + key + ); + assert_eq!( + Pallet::::actor_run_state(fixture.actor_id) + .unwrap() + .encode(), + run + ); + assert_eq!( + WakeupCursorPages::::get((WakeupClock::Tick, u64::from(keys / size))) + .unwrap() + .len(), + 1 + ); + assert_wakeup_clock_page_indices::(WakeupClock::Tick); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("complete Tick heap follows ordinary lifecycle history"); + Ok((fixture, keys)) } - #[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"), - ); - } + fn assert_mixed_waiting_deadline_close(fixture: &MixedWaitingClose, keys: u32) { + assert_mixed_waiting_target_closed::(fixture); + let key = WakeupKey::Tick(fixture.tick); + assert!(!ActorWaitingOccupancies::::contains_key(key)); + assert!(!ActorWaitingHeads::::contains_key(key)); + assert!(!ActorWaitingTails::::contains_key(key)); + assert!(!ActorWaitingCursorIndices::::contains_key(key)); + assert!(!ActorWaitingFrameChunks::::iter_keys().any(|(stored_key, _)| stored_key == key)); + assert_eq!(WakeupCursorLen::::get(WakeupClock::Tick), keys - 1); + assert!(!WakeupCursorPages::::contains_key(( + WakeupClock::Tick, + u64::from(keys / T::WakeupPageSize::get()) + ))); + assert_wakeup_clock_page_indices::(WakeupClock::Tick); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("last-reference close repairs the complete Tick heap"); } - #[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] + fn open_reachable_retry( + actor_id: ActorId, + funding: (T::AssetId, T::AssetId, T::Balance, T::Balance), + ) { + let (asset_a, asset_b, amount_a, amount_b) = funding; + let identity = Pallet::::actor_identity(actor_id).expect("retry Actor identity exists"); + T::AssetOps::mint(&identity.sovereign_account, asset_a, amount_a) + .expect("retry first liquidity leg is funded"); + T::AssetOps::mint(&identity.sovereign_account, asset_b, amount_b) + .expect("retry second liquidity leg is funded"); + let contract = Pallet::::load_actor_contract(actor_id).expect("retry Contract exists"); + let (trigger_family, trigger_weight) = match contract.trigger { + Trigger::Manual => (TriggerFamily::Manual, T::WeightInfo::manual_trigger()), + Trigger::AddressEvent { .. } => ( + TriggerFamily::AddressEvent, + T::WeightInfo::address_event_trigger_occurrence(), + ), + Trigger::ObservationCrossing { .. } => ( + TriggerFamily::ObservationCrossing, + T::WeightInfo::observation_crossing_trigger_occurrence(), + ), + Trigger::ObservationChange { .. } => ( + TriggerFamily::ObservationChange, + T::WeightInfo::observation_change_trigger_occurrence(), + ), + _ => panic!("reachable retry fixture requires a supported Trigger"), + }; + let fee_reserve = full_attempt_fee::(&contract.steps) + .checked_add( + &Pallet::::trigger_fee_for_weight(ActorType::User, trigger_family, trigger_weight) + .trigger_fee, + ) + .expect("host fee reserve is representable"); + T::AssetOps::mint( + &identity.sovereign_account, + T::FeeNativeAssetId::get(), + fee_reserve, + ) + .expect("real Trigger and current Action fees have independent funding"); + let now = frame_system::Pallet::::block_number().saturating_add(One::one()); + frame_system::Pallet::::set_block_number(now); + GlobalCircuitBreaker::::put(false); + let occurrence_cohort = match contract.trigger { + Trigger::Manual => { + Pallet::::manual_trigger(RawOrigin::Signed(identity.owner.clone()).into(), actor_id) + .expect("real Manual occurrence is admitted"); + vec![actor_id] + } + Trigger::AddressEvent { .. } => { + T::BenchmarkHelper::setup_address_event_ingress( + &identity.sovereign_account, + &identity.owner, + One::one(), + ) + .expect("host prepares the initial AddressEvent"); + assert!(T::BenchmarkHelper::run_address_event_ingress( + &identity.sovereign_account, + &identity.owner, + One::one(), + )); + vec![actor_id] + } + Trigger::ObservationCrossing { + feed, threshold, .. + } => { + assert!(threshold > 0, "retry Crossing threshold has a rearm side"); + publish_observation_at_most::(feed, threshold - 1); + while CrossingPendingFeedListState::::get().count > 0 { + Pallet::::crossing_work_unit().expect("Crossing rearm materializes"); + } + frame_system::Pallet::::set_block_number( + frame_system::Pallet::::block_number().saturating_add(One::one()), + ); + let CanonicalObservationState::Available { revision, .. } = + T::ObservationProvider::current(&feed) + else { + panic!("retry Crossing feed remains available") + }; + Pallet::::note_observation_transition( + feed, + ObservationTransition { + revision: revision.saturating_add(1), + previous: Some(threshold - 1), + current: threshold, + }, + ) + .expect("real Crossing fire transition is admitted"); + while CrossingPendingFeedListState::::get().count > 0 { + Pallet::::crossing_work_unit().expect("Crossing cohort materializes"); + } + CrossingMemberships::::iter() + .filter_map(|(id, locator)| (locator.key.feed == feed).then_some(id)) + .collect::>() + } + Trigger::ObservationChange { feed } => { + Pallet::::note_observation_changed(feed, 2) + .expect("real observation occurrence is admitted"); + while DirtyObservationListState::::get().count > 0 { + Pallet::::do_fanout_dirty_observation_page() + .expect("observation occurrence materializes"); + } + vec![actor_id] + } + _ => panic!("reachable retry fixture requires a supported Trigger"), + }; + assert!( + benchmark_fixture_hot::(actor_id) + .expect("latched Actor exists") + .pending_signal + ); + if benchmark_fixture_hot::(actor_id) + .expect("latched Actor exists") + .lifecycle + .is_paused() { - core::hint::black_box( - Pallet::::load_current_step_from_storage(actor_id, current) - .expect("benchmark tail Step loads coherently"), + // A paused target retains its paid latch while the existing guard cohort finishes Opening. + for _ in 0..T::MaxQueueLength::get() { + if occurrence_cohort + .iter() + .filter(|id| **id != actor_id) + .all(|id| benchmark_fixture_hot::(*id).is_some_and(|hot| !hot.pending_signal)) + { + break; + } + Pallet::::execute_cycle(Weight::MAX); + } + assert!( + occurrence_cohort + .iter() + .filter(|id| **id != actor_id) + .all(|id| benchmark_fixture_hot::(*id).is_some_and(|hot| !hot.pending_signal)), + "guard cohort finishes before the target resumes" ); + Pallet::::resume_actor(RawOrigin::Signed(identity.owner.clone()).into(), actor_id) + .expect("target resumes its retained occurrence through the public lifecycle"); + } + let (_, ready) = Pallet::::actor_control_cell(actor_id) + .expect("real occurrence retains canonical control authority"); + let eligible_at = ready + .eligible_at + .expect("latched service has an eligibility boundary"); + frame_system::Pallet::::set_block_number(now.max(eligible_at)); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("real close occurrence preserves canonical state"); + for _ in 0..T::MaxQueueLength::get() { + Pallet::::execute_cycle(Weight::MAX); + if !Pallet::::active_actor_exists(actor_id) { + break; + } + if benchmark_fixture_hot::(actor_id) + .is_some_and(|hot| hot.cycle_state == CycleState::Suspended) + && matches!( + ActorControlLocators::::get(actor_id), + Some(ActorControlLocation::Waiting { .. }) + ) + && occurrence_cohort + .iter() + .all(|id| benchmark_fixture_hot::(*id).is_some_and(|hot| !hot.pending_signal)) + { + break; + } + if let Some((ActorControlLocation::Ready { .. }, cell)) = + Pallet::::actor_control_cell(actor_id) + { + let current = frame_system::Pallet::::block_number(); + if let Some(next) = cell.eligible_at.filter(|next| *next > current) { + frame_system::Pallet::::set_block_number(next); + } + } } + assert!( + occurrence_cohort + .iter() + .all(|id| benchmark_fixture_hot::(*id).is_some_and(|hot| !hot.pending_signal)), + "every fired guard completes its Opening before topology measurement" + ); + assert_contract_derived_retry::(actor_id); } - #[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), + fn assert_reachable_retry(actor_id: ActorId) { + assert_contract_derived_retry::(actor_id); + let state = Pallet::::active_actor_state(actor_id).expect("retry Actor remains active"); + let run = state.run_state.as_ref().expect("retry Run exists"); + assert_eq!( + run.opening_snapshot.len(), + Pallet::::opening_surfaces(&state.contract.steps, 0).len() ); - 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)); + assert_eq!( + run.opening_predicate_results.len() as u32, + state + .contract + .steps + .iter() + .map(|step| step + .precondition + .as_ref() + .map_or(0, Precondition::opening_predicate_count)) + .sum::() + ); + assert!(state.funding.funding_tracked_assets.is_empty()); + assert!(run.funding_snapshot.is_empty()); } - #[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), + fn assert_contract_derived_retry(actor_id: ActorId) { + let state = Pallet::::active_actor_state(actor_id).expect("retry Actor remains active"); + assert_eq!(state.hot.cycle_state, CycleState::Suspended); + assert!(!state.hot.pending_signal); + assert!(matches!( + ActorControlLocators::::get(actor_id), + Some(ActorControlLocation::Waiting { .. }) + )); + let contract = + Pallet::::load_actor_contract(actor_id).expect("retry Contract remains admitted"); + let surfaces = Pallet::::opening_surfaces(&contract.steps, 0); + let run = state.run_state.as_ref().expect("Opening published the Run"); + assert_eq!(run.cursor, 0); + assert!(matches!( + run.last_step_outcome, + Some(StepOutcome::Failed(TaskFailure { + retry: RetryClass::Temporary, + .. + })) + )); + assert_eq!(run.opening_snapshot.len(), surfaces.len()); + assert!( + surfaces + .iter() + .all(|surface| run.opening_snapshot.contains_key(surface)) ); - 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)); + assert_eq!( + run.opening_predicate_results.len(), + contract + .steps + .iter() + .filter_map(|step| step.precondition.as_ref()) + .flat_map(|precondition| precondition.clauses.iter()) + .flat_map(|clause| clause.iter()) + .filter(|predicate| predicate.timing == ObservationTiming::Opening) + .count() + ); + assert!( + run + .opening_predicate_results + .iter() + .all(|result| *result == Ok(true)) + ); + assert_eq!( + run.funding_snapshot.len(), + state.funding.funding_tracked_assets.len() + ); + assert!( + state + .funding + .funding_tracked_assets + .iter() + .all(|asset| run.funding_snapshot.contains_key(asset)) + ); + assert_max_contract_geometry::(actor_id); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("reachable retry fixture passes full state audit"); } - #[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"), - ); - } + fn reachable_retry_contract() -> Result< + ( + ContractSteps, + (T::AssetId, T::AssetId, T::Balance, T::Balance), + ), + polkadot_sdk::frame_benchmarking::BenchmarkError, + > { + let (mut steps, funding) = retry_contract_with_opening_partition::( + T::MaxContractSteps::get() * 2, + T::MaxContractSteps::get() * benchmark_predicate_capacity::(), + )?; + retain_admitted_contract_geometry::(ActorType::System, &mut steps, &[0])?; + Ok((steps, funding)) } - #[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] + fn retry_contract_with_opening_partition( + opening_amounts: u32, + opening_predicates: u32, + ) -> Result< + ( + ContractSteps, + (T::AssetId, T::AssetId, T::Balance, T::Balance), + ), + polkadot_sdk::frame_benchmarking::BenchmarkError, + > { + let step_count = T::MaxContractSteps::get(); + let predicates_per_step = benchmark_predicate_capacity::(); + assert!(opening_amounts <= step_count * 2); + assert!(opening_predicates <= step_count * predicates_per_step); + if step_count < 2 + || predicates_per_step == 0 + || predicates_per_step != T::MaxPredicatesPerStep::get() { - core::hint::black_box( - Pallet::::load_current_step_from_storage(actor_id, 0) - .expect("benchmark head Step loads coherently"), - ); + return Err(polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "host cannot represent the Crossing retry Contract with maximum Opening predicates", + )); + } + let asset_owner: T::AccountId = account("reachable-opening-assets", 0, 0); + let (asset_a, asset_b, amount_a, amount_b) = + T::BenchmarkHelper::setup_add_liquidity(&asset_owner) + .expect("host supplies a valid funded liquidity pair"); + let mut assets = vec![asset_a, asset_b]; + let candidates = T::BenchmarkHelper::setup_predicate_assets(&asset_owner, step_count * 2) + .expect("Crossing retry assets exist"); + for asset in candidates { + if !assets.contains(&asset) && assets.len() < (step_count * 2) as usize { + assets.push(asset); + } + } + assert_eq!(assets.len() as u32, step_count * 2); + let mut contract_steps = ContractSteps::::default(); + for (step_index, pair) in assets.chunks_exact(2).enumerate() { + let predicates = (0..predicates_per_step) + .map(|index| TimedPredicate { + timing: if step_index as u32 * predicates_per_step + index < opening_predicates { + ObservationTiming::Opening + } else { + ObservationTiming::Current + }, + predicate: Predicate::BalanceBelow { + asset: pair[index as usize % 2], + threshold: ::max_value() + .saturating_sub(index.saturated_into()), + }, + }) + .collect(); + let amount = |position: u32| { + if step_index as u32 * 2 + position < opening_amounts { + AmountResolution::PercentageAtOpening(Perbill::from_percent(50)) + } else { + AmountResolution::PercentageOfCurrent(Perbill::from_percent(50)) + } + }; + let task = ActorTask::AddLiquidity { + asset_a: pair[0], + asset_b: pair[1], + amount_a: amount(0), + amount_b: amount(1), + min_lp_out: if step_index == 0 { + ::max_value() + } else { + One::one() + }, + }; + contract_steps + .try_push(Step { + precondition: Some(packed_predicate_clauses::( + predicates, + T::MaxPredicatesPerClause::get(), + )), + task, + on_error: if step_index == 0 { + StepErrorPolicy::RetryLater { + max_attempts: T::MaxRetryAttempts::get(), + } + } else { + StepErrorPolicy::AbortCycle + }, + }) + .expect("Crossing retry Contract fits"); } + // The funded first attempt rejects its output bound transactionally and enters service Waiting. + // Amount and predicate prefixes independently select immutable captures. + let surfaces = Pallet::::opening_surfaces(&contract_steps, 0); + assert_eq!(surfaces.len() as u32, opening_amounts); + Ok((contract_steps, (asset_a, asset_b, amount_a, amount_b))) } - #[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"), - ); - } + fn benchmark_fixture_consume_frame_current_step_service_state( + actor_id: ActorId, + ) -> ( + ActiveActorStateOf, + ActorAdmissionCertificateOf, + LoadedActorStepOf, + ) { + let (state, admission, loaded_step) = Pallet::::load_frame_actor_service_state(actor_id) + .expect("benchmark frame service state is coherent"); + Pallet::::remove_primary_control_cell_inner(actor_id) + .expect("benchmark frame source primary is consumed"); + ( + state, + admission, + loaded_step.expect("benchmark frame current Step exists"), + ) } - #[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"), - ); + fn install_wakeup_cursor_page(page_id: WakeupPageId, len: u32) { + let page_size = T::WakeupPageSize::get(); + let page_start = u32::try_from(page_id) + .expect("benchmark cursor page id fits u32") + .saturating_mul(page_size); + let mut page = WakeupCursorPageOf::::default(); + for slot in 0..len { + let index = page_start.saturating_add(slot); + let block: BlockNumberFor = 1_000_000u32.saturating_add(index).into(); + page + .try_push(WakeupKey::Block(block)) + .expect("benchmark cursor page must fit configured bound"); + // Consumed buckets retain their cursor inverse until atomic heap repair. + ActorWaitingCursorIndices::::insert(WakeupKey::Block(block), index); } + WakeupCursorPages::::insert((WakeupClock::Block, page_id), page); } - #[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"), - ); + fn clear_host_genesis_wakeup_placements() { + let block_actor_ids = benchmark_fixture_hot_actor_ids::(|hot| hot.wakeup_pointer.is_some()); + for actor_id in block_actor_ids { + Pallet::::wakeup_substrate_invalidate(actor_id) + .expect("host genesis Pipeline wakeup placement must be removable"); + } + let trigger_actor_ids = + benchmark_fixture_hot_actor_ids::(|hot| hot.trigger_wakeup_pointer.is_some()); + 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); } - #[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"), - ); + fn add_wakeup_cursor_page(page_ids: &mut alloc::vec::Vec, index: u32, size: u32) { + let page_id = u64::from(index / size); + if !page_ids.contains(&page_id) { + page_ids.push(page_id); } } - #[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()); + fn prepare_wakeup_cursor_repair(start_index: u32) -> BlockNumberFor { + let page_size = T::WakeupPageSize::get(); + let cursor_len = T::MaxActiveActors::get(); + assert!( + page_size > 0 && cursor_len > start_index.saturating_add(1), + "benchmark requires bounded cursor depth" + ); + let last_index = cursor_len.saturating_sub(1); + let tail_page = u64::from(last_index / page_size); + let tail_len = (last_index % page_size).saturating_add(1); + let mut page_ids = alloc::vec::Vec::new(); + add_wakeup_cursor_page(&mut page_ids, last_index, page_size); + if start_index > 0 { + add_wakeup_cursor_page(&mut page_ids, (start_index - 1) / 2, page_size); } - 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"); + let mut current = start_index; + loop { + add_wakeup_cursor_page(&mut page_ids, current, page_size); + let left = current.saturating_mul(2).saturating_add(1); + if left >= cursor_len { + break; + } + add_wakeup_cursor_page(&mut page_ids, left, page_size); + let right = left.saturating_add(1); + if right < cursor_len { + add_wakeup_cursor_page(&mut page_ids, right, page_size); + } + current = left; } - #[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); + for page_id in page_ids { + let len = if page_id == tail_page { + tail_len + } else { + page_size + }; + install_wakeup_cursor_page::(page_id, len); } + WakeupCursorLen::::insert(WakeupClock::Block, cursor_len); + 1_000_000u32.saturating_add(start_index).into() } - #[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()); + // Leaf fixtures install only accessed heap pages, not complete Actor/Waiting state. + fn prepare_upward_cursor_removal( + removed_index: u32, + ) -> Result, polkadot_sdk::frame_benchmarking::BenchmarkError> { + if T::WakeupPageSize::get() != 32 || T::MaxActiveActors::get() != 10_000 { + return Err(polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "upward heap diagnostics require the reference 10000-key, 32-slot geometry", + )); } - 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"); + clear_host_genesis_wakeup_placements::(); + let mut tail_ancestors = Vec::new(); + let mut index = 9_999u32; + loop { + tail_ancestors.push(index); + if index == 0 { + break; + } + index = (index - 1) / 2; } - #[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); + let mut page_ids = Vec::new(); + add_wakeup_cursor_page(&mut page_ids, 9_999, 32); + let mut index = removed_index; + loop { + add_wakeup_cursor_page(&mut page_ids, index, 32); + if index == 0 { + break; + } + index = (index - 1) / 2; + } + // Partial accessed pages of the same legal global heap as the complete native witness. + for page_id in page_ids { + let first = u32::try_from(page_id).expect("reference page fits u32") * 32; + let mut page = WakeupCursorPageOf::::default(); + for index in first..(first + 32).min(10_000) { + let block: BlockNumberFor = if tail_ancestors.contains(&index) { + u32::BITS - (index + 1).leading_zeros() + } else { + 100_000 + index + } + .into(); + let key = WakeupKey::Block(block); + page.try_push(key).expect("reference heap page fits"); + ActorWaitingCursorIndices::::insert(key, index); + } + WakeupCursorPages::::insert((WakeupClock::Block, page_id), page); } + WakeupCursorLen::::insert(WakeupClock::Block, 10_000); + assert_wakeup_cursor_page_indices::(); + Ok((100_000 + removed_index).into()) } - #[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"); + // Leaf fixtures install only accessed heap pages, not complete Actor/Waiting state. + fn assert_wakeup_cursor_page_indices() { + assert_wakeup_clock_page_indices::(WakeupClock::Block); + } + + fn assert_wakeup_clock_page_indices(expected_clock: WakeupClock) { + let size = T::WakeupPageSize::get(); + let len = WakeupCursorLen::::get(expected_clock); + for ((clock, page_id), page) in WakeupCursorPages::::iter() { + if clock != expected_clock { + continue; } - }); - #[block] - { - core::hint::black_box( - Pallet::::load_current_step_service_state(actor_id) - .expect("benchmark current-Step service state loads coherently"), + let first = u32::try_from(page_id).expect("fixture page fits u32") * size; + assert!(first < len, "empty tail pages must be reclaimed"); + assert_eq!(page.len() as u32, (len - first).min(size)); + for (slot, key) in page.iter().enumerate() { + let index = first + slot as u32; + assert_eq!(key.clock(), expected_clock); + assert_eq!(ActorWaitingCursorIndices::::get(key), Some(index)); + if index > 0 { + let parent = (index - 1) / 2; + if let Some(parent_page) = WakeupCursorPages::::get((clock, u64::from(parent / size))) + { + assert!(parent_page[(parent % size) as usize] <= *key); + } + } + } + } + for (key, index) in ActorWaitingCursorIndices::::iter() { + if key.clock() != expected_clock { + continue; + } + assert!(index < len); + let page = WakeupCursorPages::::get((expected_clock, u64::from(index / size))) + .expect("retained inverse resolves to an installed page"); + assert_eq!(page.get((index % size) as usize), Some(&key)); + } + } + + fn install_saturated_tombstone_queue() { + let capacity = u64::from(T::MaxQueueLength::get()); + let head = ActorReadyTail::::get(); + let tail = head + .checked_add(capacity) + .expect("bounded fixture ticket span"); + for page_id in head / 32..tail.div_ceil(32) { + ActorReadyFrameChunks::::insert( + page_id, + ActorControlChunkOf::::try_from(vec![None; 32]).expect("fixed Ready chunk"), ); } - assert!(matches!( - Pallet::::load_actor_state(actor_id), - LoadedActorStateOf::Active(_) - )); + benchmark_fixture_set_ready_queue_state::(head, tail, 0); + } + + fn saturated_address_source_filter( + owner: &T::AccountId, + matched_source: Option, + ) -> SourceFilter { + if let Some(matched_source) = matched_source { + let mut allowed_sources = (0..T::MaxWhitelistSize::get().saturating_sub(2)) + .map(|index| account("ingress-source", index, 0)) + .collect::>(); + allowed_sources.push(owner.clone()); + allowed_sources.push(matched_source); + allowed_sources.sort_by_key(Encode::encode); + allowed_sources.dedup(); + assert_eq!( + allowed_sources.len() as u32, + T::MaxWhitelistSize::get(), + "benchmark source whitelist must saturate MaxWhitelistSize" + ); + SourceFilter::Whitelist( + BoundedVec::try_from(allowed_sources).expect("source whitelist must fit runtime bound"), + ) + } else { + SourceFilter::Any + } + } + + fn prepare_saturated_address_actor( + seed: u32, + matched_source: Option, + ) -> (ActorId, T::AccountId) { + let owner: T::AccountId = account("ingress_owner", seed, 0); + let source_filter = saturated_address_source_filter::(&owner, matched_source); + let schedule = Schedule { + trigger: Trigger::address_event(source_filter, AssetFilter::Any), + cooldown_blocks: 0, + }; + Pallet::::create_system_actor( + RawOrigin::Root.into(), + owner.clone(), + Mutability::Mutable, + system_contract::(schedule, make_tracked_funding_contract_steps::(owner)), + ) + .expect("create_system_actor must succeed in ingress benchmark setup"); + 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()); + 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 + .funding_accumulated + .try_insert(T::FeeNativeAssetId::get(), One::one()) + .expect("tracked funding accumulator fits"); + }); + install_saturated_tombstone_queue::(); + (actor_id, recipient) } - /// One complete minimal cycle execution over one inert StopCycle step (fixed cycle - /// orchestration plus finalization), measured on the execution path only; queue probes and - /// head consumption are separate scheduler classes. + #[benchmark] - fn cycle_orchestration() { - let actor_id = bench_create_system_with_plan::(3_100, make_inert_contract_steps::()); - let now: BlockNumberFor = 1u32.into(); - frame_system::Pallet::::set_block_number(now); - let instance = Pallet::::active_actor_view(actor_id).expect("cycle actor exists"); + fn scheduler_on_initialize_cutoff() { + benchmark_fixture_set_next_ready_ticket::(7); + CurrentBlockResourceState::::kill(); #[block] { - core::hint::black_box(Pallet::::execute_single_cycle(actor_id, instance, now)); + 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, benchmark_fixture_next_ready_ticket::())); + state + .settle(&mut reservation, Weight::zero()) + .expect("zero cutoff owner settles"); + state + .open_external_phase() + .expect("empty benchmark prepass closes"); + CurrentBlockResourceState::::put(state); } - let updated = Pallet::::active_actor_view(actor_id).expect("cycle actor survives"); - assert_eq!(updated.cycle_nonce, 1); + assert_eq!( + PrepassExecutionCutoff::::get(), + Some((frame_system::Pallet::::block_number(), 7)) + ); + assert_eq!( + CurrentBlockResourceState::::get().map(|state| state.phase()), + Some(BlockResourcePhase::ExternalPhase) + ); } - /// One complete cycle execution over `steps` inert StopCycle steps: the linear model prices - /// the fixed cycle orchestration plus per-step bookkeeping, the exact cycle overhead the - /// admission composition uses for arbitrary plans. #[benchmark] - fn step_orchestration(n: Linear<1, 8>) { - let actor_id = bench_create_system_with_plan::(3_200, inert_contract_steps_of_len::(n)); - let now: BlockNumberFor = 1u32.into(); + fn scheduler_on_idle_base() { + let threshold = T::MaxIdleStarvationBlocks::get().max(1); + let now: BlockNumberFor = threshold.into(); frame_system::Pallet::::set_block_number(now); - let instance = Pallet::::active_actor_view(actor_id).expect("cycle actor exists"); + GlobalCircuitBreaker::::put(false); + 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] { - core::hint::black_box(Pallet::::execute_single_cycle(actor_id, instance, now)); + 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(benchmark_fixture_ready_head::()); + core::hint::black_box(benchmark_fixture_ready_tail::()); + core::hint::black_box(benchmark_fixture_ready_occupancy::()); + core::hint::black_box(DirtyObservationListState::::get()); + Pallet::::update_idle_starvation_state(now, true); } - let updated = Pallet::::active_actor_view(actor_id).expect("cycle actor survives"); - assert_eq!(updated.cycle_nonce, 1); } - #[benchmark(pov_mode = Measured)] - fn scheduler_paged_append_existing_page() { - let page_size = T::QueuePageSize::get(); - assert!( - page_size >= 2, - "benchmark requires a non-trivial queue page" - ); - for i in 0..page_size.saturating_sub(1) { - let actor_id = bench_create_system_manual::(31_000_000u32.saturating_add(i)); - assert!(Pallet::::paged_enqueue(actor_id)); - } - let actor_id = bench_create_system_manual::(32_000_000); + #[benchmark] + fn materialization_coordinator_base() { + MaterializationFamilyCursor::::put(0); + let now = frame_system::Pallet::::block_number(); #[block] { - assert!(Pallet::::paged_enqueue(actor_id)); + core::hint::black_box(Pallet::::materialization_family_has_work(0, now)); + core::hint::black_box(Pallet::::materialization_family_has_work(1, now)); + core::hint::black_box(Pallet::::materialization_family_has_work(2, now)); + let cursor = MaterializationFamilyCursor::::get(); + MaterializationFamilyCursor::::put(cursor.saturating_add(1) % 3); } - assert_eq!(QueueTail::::get(), u64::from(page_size)); - assert_eq!( - ActorHot::::get(actor_id).and_then(|hot| hot.queue_ticket), - Some(u64::from(page_size - 1)) - ); + assert_eq!(MaterializationFamilyCursor::::get(), 1); } - #[benchmark(pov_mode = Measured)] - fn scheduler_paged_append_new_page() { - let page_size = T::QueuePageSize::get(); - for i in 0..page_size { - let actor_id = bench_create_system_manual::(33_000_000u32.saturating_add(i)); - assert!(Pallet::::paged_enqueue(actor_id)); - } - let actor_id = bench_create_system_manual::(34_000_000); + // Non-dispatch diagnostic benchmark excluded from runtime weight artifact generation + #[benchmark] + fn scheduler_on_idle_healthy_empty() { + let now: BlockNumberFor = 1u32.into(); + frame_system::Pallet::::set_block_number(now); + GlobalCircuitBreaker::::put(false); + benchmark_fixture_set_ready_queue_state::(0, 0, 0); + WakeupCursorLen::::insert(WakeupClock::Block, 0); + IdleStarvationState::::kill(); #[block] { - assert!(Pallet::::paged_enqueue(actor_id)); + core::hint::black_box(Pallet::::on_idle(now, Weight::MAX)); } - assert_eq!( - QueueTail::::get(), - u64::from(page_size).saturating_add(1) - ); - assert_eq!(QueuePages::::get(1).map(|page| page.len()), Some(1)); + assert!(!IdleStarvationState::::exists()); } #[benchmark(pov_mode = Measured)] - fn scheduler_wakeup_append_existing_page() { - let page_size = T::WakeupPageSize::get(); - assert!( - page_size >= 2, - "benchmark requires a non-trivial wakeup page" - ); - let wakeup_block = 100u32.into(); - for i in 0..page_size.saturating_sub(1) { - let actor_id = bench_create_system_manual::(41_000_000u32.saturating_add(i)); - assert!(Pallet::::wakeup_substrate_schedule( + 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, - wakeup_block + &contract, + &certificate, )); } - let actor_id = bench_create_system_manual::(42_000_000); - #[block] - { - assert!(Pallet::::wakeup_substrate_schedule( + // Geometry insertion does not publish process authority or own its admission certificate. + assert!(!ActorControlLocators::::contains_key(actor_id)); + assert!(ActorContractHeads::::contains_key(actor_id)); + for chunk_index in 0..chunk_count { + assert!(ActorContractTailChunks::::contains_key( actor_id, - wakeup_block + chunk_index )); } - let pointer = ActorHot::::get(actor_id) - .and_then(|hot| hot.wakeup_pointer) - .expect("benchmark wakeup pointer must exist"); - assert_eq!((pointer.page_id, pointer.slot), (0, page_size - 1)); } #[benchmark(pov_mode = Measured)] - fn scheduler_wakeup_append_new_page() { - let page_size = T::WakeupPageSize::get(); - let wakeup_block = 100u32.into(); - for i in 0..page_size { - let actor_id = bench_create_system_manual::(43_000_000u32.saturating_add(i)); - assert!(Pallet::::wakeup_substrate_schedule( - actor_id, - wakeup_block - )); + 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"), + ); } - let actor_id = bench_create_system_manual::(44_000_000); + 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; + let authority_before = Pallet::::actor_control_cell(actor_id); #[block] { - assert!(Pallet::::wakeup_substrate_schedule( + core::hint::black_box( + Pallet::::remove_admitted_contract_geometry(actor_id) + .expect("benchmark geometry removes coherently"), + ); + } + assert_eq!(Pallet::::actor_control_cell(actor_id), authority_before); + assert!(!ActorContractHeads::::contains_key(actor_id)); + for chunk_index in 0..chunk_count { + assert!(!ActorContractTailChunks::::contains_key( actor_id, - wakeup_block + chunk_index )); } - let pointer = ActorHot::::get(actor_id) - .and_then(|hot| hot.wakeup_pointer) - .expect("benchmark wakeup pointer must exist"); - assert_eq!((pointer.page_id, pointer.slot), (1, 0)); } #[benchmark(pov_mode = Measured)] - fn scheduler_wakeup_replace_exact() { - let actor_id = bench_create_system_manual::(45_000_000); - let old_block = 100u32.into(); - let replacement_block = 200u32.into(); - assert!(Pallet::::wakeup_substrate_schedule(actor_id, old_block)); + 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] { - assert!(Pallet::::wakeup_substrate_schedule( - actor_id, - replacement_block - )); + Pallet::::store_actor_contract(actor_id, replacement) + .expect("benchmark replacement remains admitted"); } - let pointer = ActorHot::::get(actor_id) - .and_then(|hot| hot.wakeup_pointer) - .expect("replacement wakeup pointer must exist"); assert_eq!( - (pointer.block, pointer.page_id, pointer.slot), - (WakeupKey::Block(replacement_block), 0, 0) + Pallet::::load_actor_contract(actor_id).map(|contract| contract.cooldown_blocks), + Some(1) ); - assert!(!WakeupBuckets::::contains_key(WakeupKey::Block( - old_block - ))); } #[benchmark(pov_mode = Measured)] - fn scheduler_wakeup_invalidate_middle_page() { - let page_size = T::WakeupPageSize::get(); - let wakeup_block = 100u32.into(); - let count = page_size.saturating_mul(2).saturating_add(1); - let mut actors = alloc::vec::Vec::with_capacity(count as usize); - for i in 0..count { - let actor_id = bench_create_system_manual::(46_000_000u32.saturating_add(i)); - assert!(Pallet::::wakeup_substrate_schedule( - actor_id, - wakeup_block - )); - actors.push(actor_id); - } - let middle_start = page_size as usize; - let middle_end = middle_start.saturating_add(page_size as usize); - for actor_id in &actors[middle_start..middle_end.saturating_sub(1)] { - assert!(Pallet::::wakeup_substrate_invalidate(*actor_id).is_some()); - } - let actor_id = actors[middle_end - 1]; + 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] { - assert!(Pallet::::wakeup_substrate_invalidate(actor_id).is_some()); + 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)); } - assert!(!WakeupPages::::contains_key(( - WakeupKey::Block(wakeup_block), - 1, - ))); - assert_eq!( - WakeupPages::::get((WakeupKey::Block(wakeup_block), 0)).and_then(|page| page.next_page), - Some(2) - ); + } + + #[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!( - WakeupPages::::get((WakeupKey::Block(wakeup_block), 2)) - .and_then(|page| page.previous_page), - Some(0) + 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 scheduler_wakeup_drain_partial_page() { - let page_size = T::WakeupPageSize::get(); - assert!(page_size >= 2, "benchmark requires a partial page"); - let wakeup_block = 100u32.into(); - for i in 0..page_size { - let actor_id = bench_create_system_manual::(47_000_000u32.saturating_add(i)); - assert!(Pallet::::wakeup_substrate_schedule( - actor_id, - wakeup_block - )); - } - let scan_limit = page_size / 2; + fn benchmark_monolithic_load_first() { + let actor_id = bench_create_system_with_plan::(2_980, inert_contract_steps_of_len::(1)); #[block] { - let (ready, stats) = Pallet::::wakeup_substrate_drain_block(wakeup_block, scan_limit); - assert_eq!(ready.len(), scan_limit as usize); - assert_eq!(stats.entries_scanned, scan_limit); - assert_eq!(stats.pages_deleted, 0); + let contract = Pallet::::load_actor_contract(actor_id).expect("benchmark Contract exists"); + core::hint::black_box(contract.steps.first().expect("benchmark Step 0 exists")); } - assert_eq!( - WakeupPages::::get((WakeupKey::Block(wakeup_block), 0)).map(|page| page.scan_slot), - Some(scan_limit) - ); } #[benchmark(pov_mode = Measured)] - fn scheduler_wakeup_drain_full_page() { - let page_size = T::WakeupPageSize::get(); - let wakeup_block = 100u32.into(); - for i in 0..page_size { - let actor_id = bench_create_system_manual::(48_000_000u32.saturating_add(i)); - assert!(Pallet::::wakeup_substrate_schedule( - actor_id, - wakeup_block - )); + 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] { - let (ready, stats) = Pallet::::wakeup_substrate_drain_block(wakeup_block, page_size); - assert_eq!(ready.len(), page_size as usize); - assert_eq!(stats.entries_scanned, page_size); - assert_eq!(stats.pages_deleted, 1); + core::hint::black_box( + Pallet::::load_current_step_from_storage(actor_id, 0) + .expect("benchmark head Step loads coherently"), + ); } - assert!(!WakeupBuckets::::contains_key(WakeupKey::Block( - wakeup_block - ))); - assert!(!WakeupPages::::contains_key(( - WakeupKey::Block(wakeup_block), - 0, - ))); } #[benchmark(pov_mode = Measured)] - fn scheduler_wakeup_drain_dense_boundary() { - let page_size = T::WakeupPageSize::get(); - let count = page_size.saturating_add(1); - assert!( - count <= T::MaxWakeupsPerBlock::get(), - "benchmark requires one boundary-crossing drain" - ); - let wakeup_block = 100u32.into(); - for i in 0..count { - let actor_id = bench_create_system_manual::(49_000_000u32.saturating_add(i)); - assert!(Pallet::::wakeup_substrate_schedule( - actor_id, - wakeup_block + fn benchmark_chunked_load_tail( + s: Linear< + 1, + { + T::MaxContractSteps::get() + .saturating_sub(1) + .clamp(1, MAX_STEPS_PER_TAIL_CHUNK) + }, + >, + ) -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + if T::MaxContractSteps::get() <= 1 { + return Err(polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "host Contract bound cannot represent this tail benchmark branch", )); } + 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] { - let (ready, stats) = Pallet::::wakeup_substrate_drain_block(wakeup_block, count); - assert_eq!(ready.len(), count as usize); - assert_eq!(stats.entries_scanned, count); - assert_eq!(stats.pages_touched, 2); - assert_eq!(stats.pages_deleted, 2); + core::hint::black_box( + Pallet::::load_current_step_from_storage(actor_id, current) + .expect("benchmark tail Step loads coherently"), + ); } - assert!(!WakeupBuckets::::contains_key(WakeupKey::Block( - wakeup_block - ))); + Ok(()) } #[benchmark(pov_mode = Measured)] - fn scheduler_wakeup_drain_stale_page() { - let page_size = T::WakeupPageSize::get(); - let wakeup_block = 100u32.into(); - for i in 0..page_size { - let actor_id = bench_create_system_manual::(50_000_000u32.saturating_add(i)); - assert!(Pallet::::wakeup_substrate_schedule( + fn contract_geometry_create(c: Linear<0, 3>) { + let actor_id = 2_994; + let n = 1u32.saturating_add( + c.saturating_mul(MAX_STEPS_PER_TAIL_CHUNK) + .min(T::MaxContractSteps::get().saturating_sub(1)), + ); + 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, - wakeup_block + &contract, + &certificate, )); - ActorHot::::mutate(actor_id, |maybe_hot| { - maybe_hot - .as_mut() - .expect("benchmark actor hot state must exist") - .wakeup_pointer = None; - }); } + assert!(!ActorControlLocators::::contains_key(actor_id)); + assert!(ActorContractHeads::::contains_key(actor_id)); + } + + #[benchmark(pov_mode = Measured)] + fn contract_geometry_close(c: Linear<0, 3>) { + let n = 1u32.saturating_add( + c.saturating_mul(MAX_STEPS_PER_TAIL_CHUNK) + .min(T::MaxContractSteps::get().saturating_sub(1)), + ); + 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); + let authority_before = Pallet::::actor_control_cell(actor_id); #[block] { - let (ready, stats) = Pallet::::wakeup_substrate_drain_block(wakeup_block, page_size); - assert!(ready.is_empty()); - assert_eq!(stats.entries_scanned, page_size); - assert_eq!(stats.stale_entries, page_size); - assert_eq!(stats.pages_deleted, 1); + core::hint::black_box( + Pallet::::remove_admitted_contract_geometry(actor_id) + .expect("benchmark geometry removes coherently"), + ); } - assert!(!WakeupBuckets::::contains_key(WakeupKey::Block( - wakeup_block - ))); + assert_eq!(Pallet::::actor_control_cell(actor_id), authority_before); + assert!(!ActorContractHeads::::contains_key(actor_id)); } #[benchmark(pov_mode = Measured)] - fn scheduler_wakeup_cursor_insert() { - let page_size = T::WakeupPageSize::get(); - let max_active = T::MaxActiveActors::get(); - assert!( - page_size > 0 && max_active > 1, - "benchmark requires bounded cursor depth" - ); - let insert_index = max_active.saturating_sub(1); - let tail_page = u64::from(insert_index / page_size); - let tail_len = insert_index % page_size; - let mut page_ids = alloc::vec::Vec::new(); - let mut current = insert_index; - loop { - add_wakeup_cursor_page(&mut page_ids, current, page_size); - if current == 0 { - break; - } - current = current.saturating_sub(1) / 2; - } - for page_id in page_ids { - let len = if page_id == tail_page { - tail_len - } else { - page_size - }; - if len > 0 { - install_wakeup_cursor_page::(page_id, len); - } - } - WakeupCursorLen::::insert(WakeupClock::Block, insert_index); - let inserted_block: BlockNumberFor = 1u32.into(); - WakeupBuckets::::insert( - WakeupKey::Block(inserted_block), - WakeupBucketState { - head_page: 0, - tail_page: 0, - next_page_id: 1, - live_entries: 1, - cursor_index: None, - }, + fn contract_geometry_reconstruct(c: Linear<0, 3>) { + let n = 1u32.saturating_add( + c.saturating_mul(MAX_STEPS_PER_TAIL_CHUNK) + .min(T::MaxContractSteps::get().saturating_sub(1)), ); + 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] { - assert!(Pallet::::wakeup_cursor_insert(inserted_block)); + core::hint::black_box( + Pallet::::load_admitted_contract_geometry(actor_id) + .expect("benchmark geometry reconstructs coherently"), + ); } - assert_eq!(WakeupCursorLen::::get(WakeupClock::Block), max_active); - assert_eq!(Pallet::::wakeup_cursor_peek(), Some(inserted_block)); } #[benchmark(pov_mode = Measured)] - fn scheduler_wakeup_cursor_pop_min() { - let cursor_len = T::MaxActiveActors::get(); - let expected_min = prepare_wakeup_cursor_repair::(0); + 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] { - assert_eq!(Pallet::::wakeup_cursor_pop_min(), Some(expected_min)); + core::hint::black_box( + Pallet::::load_current_step_from_storage(actor_id, 0) + .expect("benchmark head Step loads coherently"), + ); } - assert_eq!( - WakeupCursorLen::::get(WakeupClock::Block), - cursor_len.saturating_sub(1) - ); - assert_eq!(Pallet::::wakeup_cursor_peek(), Some(1_000_001u32.into())); - assert_eq!( - WakeupBuckets::::get(WakeupKey::Block(expected_min)) - .and_then(|bucket| bucket.cursor_index), - None - ); } #[benchmark(pov_mode = Measured)] - fn scheduler_wakeup_cursor_remove_exact() { - let cursor_len = T::MaxActiveActors::get(); - let removed_block = prepare_wakeup_cursor_repair::(1); + fn current_step_load_tail( + s: Linear< + 1, + { + T::MaxContractSteps::get() + .saturating_sub(1) + .clamp(1, MAX_STEPS_PER_TAIL_CHUNK) + }, + >, + ) -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + if T::MaxContractSteps::get() <= 1 { + return Err(polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "host Contract bound cannot represent this tail benchmark branch", + )); + } + 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] { - assert!(Pallet::::wakeup_cursor_remove(removed_block)); + core::hint::black_box( + Pallet::::load_current_step_from_storage(actor_id, s) + .expect("benchmark tail Step loads coherently"), + ); } - assert_eq!( - WakeupCursorLen::::get(WakeupClock::Block), - cursor_len.saturating_sub(1) - ); - assert_eq!(Pallet::::wakeup_cursor_peek(), Some(1_000_000u32.into())); - assert_eq!( - WakeupBuckets::::get(WakeupKey::Block(removed_block)) - .and_then(|bucket| bucket.cursor_index), - None - ); + Ok(()) } #[benchmark(pov_mode = Measured)] - fn scheduler_wakeup_cursor_worker_partial() { - clear_host_genesis_wakeup_placements::(); - let first = bench_create_system_manual::(34_100_000); - let second = bench_create_system_manual::(34_100_001); - for actor_id in [first, second] { - 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() - .expect("benchmark actor hot state exists") - .trigger_runtime_state = TriggerRuntimeState::Cadenced { anchor_tick: None }; - }); - Pallet::::benchmark_defer_tick_wakeup(actor_id, 0) - .expect("benchmark bootstrap wakeup fits"); + 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); + benchmark_fixture_mutate_hot::(actor_id, |hot| { + hot.queue_ticket = Some(9); + }); + let identity = benchmark_fixture_identity::(actor_id).expect("benchmark identity exists"); + let certificate = + benchmark_fixture_admission::(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"), + ); } - let limit = T::WeightInfo::scheduler_wakeup_cursor_worker_future() + } + + #[benchmark(pov_mode = Measured)] + fn current_step_plan_suspended_head() + -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + // Funding-heavy host-feasible corner; frontier cases verify reachability, not a Weight envelope. + let opening_legs = T::MaxContractSteps::get() .saturating_mul(2) - .saturating_add(Pallet::::wakeup_cursor_drain_unit_weight_upper(false)); - let mut meter = polkadot_sdk::sp_weights::WeightMeter::with_limit(limit); + .saturating_sub(T::MaxFundingTrackedAssets::get()); + let (actor_id, ticket) = prepare_reachable_suspended_head::(opening_legs, 0)?; + let retained = ActorRunStateStore::::get(actor_id) + .expect("due Run exists") + .encode(); #[block] { - let stats = Pallet::::drain_overdue_wakeups_cursor(10u32.into(), &mut meter); - assert_eq!(stats.entries_scanned, 1); - assert_eq!(stats.ready_entries, 1); + core::hint::black_box( + Pallet::::load_current_step_plan_from_storage(ticket) + .expect("real Suspended head plan loads coherently"), + ); } assert_eq!( - WakeupBuckets::::get(WakeupKey::Tick(0)).map(|bucket| bucket.live_entries), - Some(1) - ); - let rearmed_tick = ActorHot::::get(first) - .and_then(|hot| hot.trigger_runtime_state.temporal_anchor_tick()) - .and_then(|anchor| anchor.checked_add(1)) - .expect("benchmark cadence re-anchors"); - assert_eq!( - WakeupBuckets::::get(WakeupKey::Tick(rearmed_tick)).map(|bucket| bucket.live_entries), - Some(1) + ActorRunStateStore::::get(actor_id) + .expect("read-only plan load retains Run") + .encode(), + retained ); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("read-only plan loading preserves full state audit"); + Ok(()) } - /// 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 at_time_trigger_occurrence() { - clear_host_genesis_wakeup_placements::(); - 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"); + fn current_step_plan_running_tail( + s: Linear< + 1, + { + T::MaxContractSteps::get() + .saturating_sub(1) + .clamp(1, MAX_STEPS_PER_TAIL_CHUNK) + }, + >, + ) -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let (actor_id, ticket) = prepare_reachable_running_tail::(s)?; + let retained = ActorRunStateStore::::get(actor_id) + .expect("real tail Run exists") + .encode(); #[block] { - assert_eq!( - Pallet::::process_due_temporal_occurrence(actor_id, 1), - Ok(false) + core::hint::black_box( + Pallet::::load_current_step_plan_from_storage(ticket) + .expect("real Running plan loads coherently"), ); } - 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, .. } - )); + assert_reachable_running_tail_unchanged::(actor_id, &retained); + Ok(()) } - /// 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, + fn opening_snapshot_traversal() -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let actor: T::AccountId = account("opening_snapshot_actor", 0, 0); + let assets = T::BenchmarkHelper::setup_predicate_assets(&actor, 2).map_err(|_| { + polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "host cannot prepare two distinct Opening assets", + ) + })?; + if assets.len() != 2 || assets[0] == assets[1] { + return Err(polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "Opening traversal requires two distinct assets", + )); + } + let steps: ContractSteps = vec![ + Step { + precondition: None, + task: ActorTask::AddLiquidity { + asset_a: assets[0], + asset_b: assets[1], + amount_a: AmountResolution::Fixed(One::one()), + amount_b: AmountResolution::Fixed(One::one()), + min_lp_out: One::one(), }, - 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), + on_error: StepErrorPolicy::AbortCycle, }; - }); - 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"); + T::MaxContractSteps::get() as usize + ] + .try_into() + .expect("full Contract Step count fits"); + let snapshot; #[block] { - assert_eq!( - Pallet::::process_due_temporal_occurrence(actor_id, 0), - Ok(false) - ); + snapshot = + Pallet::::capture_opening_snapshot(ActorType::System, &actor, &steps, Zero::zero()); + core::hint::black_box(&snapshot); } - 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()); + assert!(snapshot.is_empty()); + Ok(()) } - #[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, - }, - ); - WakeupBuckets::::mutate(WakeupKey::Block(wakeup_block), |maybe_bucket| { - let bucket = maybe_bucket.as_mut().expect("cursor bucket exists"); - bucket.head_page = 0; - bucket.tail_page = 0; - bucket.next_page_id = 1; - bucket.live_entries = 1; - }); - ActorHot::::mutate(actor_id, |maybe_hot| { - maybe_hot.as_mut().expect("actor hot state").wakeup_pointer = Some(WakeupPointer { - block: WakeupKey::Block(wakeup_block), - page_id: 0, - slot: 0, - }); - }); - let mut meter = polkadot_sdk::sp_weights::WeightMeter::with_limit(Weight::MAX); - #[block] - { - let stats = Pallet::::drain_overdue_wakeups_cursor(wakeup_block, &mut meter); - assert_eq!(stats.entries_scanned, 1); - assert_eq!(stats.ready_entries, 1); - } + fn prepare_opening_snapshot_assets(e: u32) -> (T::AccountId, Vec) { + let actor: T::AccountId = account("opening_snapshot_actor", 0, 0); + let assets: Vec<_> = + T::BenchmarkHelper::setup_max_encoded_predicate_assets(&actor, e.max(2) + 1) + .expect("maximum encoded Opening assets exist") + .into_iter() + .filter(|asset| *asset != T::FeeNativeAssetId::get()) + .take(e.max(2) as usize) + .collect(); + assert_eq!(assets.len() as u32, e.max(2)); assert_eq!( - WakeupCursorLen::::get(WakeupClock::Block), - cursor_len.saturating_sub(1) + assets + .iter() + .collect::>() + .len(), + assets.len() ); - assert_eq!(Pallet::::wakeup_cursor_peek(), Some(1_000_001u32.into())); - assert!(WakeupBuckets::::get(WakeupKey::Block(wakeup_block)).is_none()); + for asset in assets.iter().take(e as usize) { + let minimum = T::AssetOps::minimum_balance(*asset); + let amount = minimum + .checked_add(&One::one()) + .expect("funded capture amount fits"); + T::AssetOps::mint(&actor, *asset, amount).expect("Opening capture asset is funded"); + assert!(T::AssetOps::balance(&actor, *asset) > minimum); + } + (actor, assets) } #[benchmark(pov_mode = Measured)] - fn scheduler_wakeup_cursor_worker_future() { - clear_host_genesis_wakeup_placements::(); - let wakeup_block: BlockNumberFor = 1_000_000u32.into(); - let actor_id = bench_create_system_manual::(34_300_000); - assert!(Pallet::::wakeup_substrate_schedule( - actor_id, - wakeup_block - )); - let mut meter = polkadot_sdk::sp_weights::WeightMeter::with_limit(Weight::MAX); - #[block] - { - let stats = Pallet::::drain_overdue_wakeups_cursor(10u32.into(), &mut meter); - assert_eq!(stats.entries_scanned, 0); + fn opening_snapshot_capture( + e: Linear< + 1, + { T::MaxOpeningSnapshotEntries::get().min(T::MaxContractSteps::get().saturating_mul(2)) }, + >, + ) { + let (actor, assets) = prepare_opening_snapshot_assets::(e); + let mut steps = ContractSteps::::default(); + for index in 0..T::MaxContractSteps::get() { + let asset_a = assets[(index * 2).min(e - 1) as usize]; + let asset_b = if e == 1 { + assets[1] + } else { + assets[if index * 2 + 1 < e { + index * 2 + 1 + } else { + e - 2 + } as usize] + }; + assert_ne!(asset_a, asset_b); + steps + .try_push(Step { + precondition: None, + task: ActorTask::AddLiquidity { + asset_a, + asset_b, + amount_a: AmountResolution::PercentageAtOpening(Perbill::one()), + amount_b: if e == 1 { + AmountResolution::Fixed(One::one()) + } else { + AmountResolution::PercentageAtOpening(Perbill::one()) + }, + min_lp_out: One::one(), + }, + on_error: StepErrorPolicy::AbortCycle, + }) + .expect("benchmark Opening Step fits"); } - assert_eq!(Pallet::::wakeup_cursor_peek(), Some(wakeup_block)); - assert!( - ActorHot::::get(actor_id) - .and_then(|hot| hot.wakeup_pointer) - .is_some() - ); - } - - #[benchmark(pov_mode = Measured)] - fn scheduler_paged_consume_preserve_page() { - let first = bench_create_system_manual::(35_000_000); - let second = bench_create_system_manual::(35_000_001); - assert!(Pallet::::paged_enqueue(first)); - assert!(Pallet::::paged_enqueue(second)); + let snapshot; #[block] { - assert!(Pallet::::paged_consume_head(0)); + snapshot = + Pallet::::capture_opening_snapshot(ActorType::System, &actor, &steps, Zero::zero()); + core::hint::black_box(&snapshot); } - assert_eq!(QueueHead::::get(), 1); - assert!(QueuePages::::contains_key(0)); assert_eq!( - ActorHot::::get(first).and_then(|hot| hot.queue_ticket), - None + u32::try_from(snapshot.len()).expect("snapshot count fits"), + e ); - } - - #[benchmark(pov_mode = Measured)] - fn scheduler_paged_consume_delete_page() { - let actor_id = bench_create_system_manual::(36_000_000); - assert!(Pallet::::paged_enqueue(actor_id)); - #[block] - { - assert!(Pallet::::paged_consume_head(0)); + for asset in assets.iter().take(e as usize) { + let expected = + T::AssetOps::balance(&actor, *asset).saturating_sub(T::AssetOps::minimum_balance(*asset)); + assert!(!expected.is_zero()); + assert_eq!( + snapshot.get(&OpeningSurface::PreservableAsset(*asset)), + Some(&expected) + ); } - assert_eq!(QueueHead::::get(), u64::from(T::QueuePageSize::get())); - assert_eq!(QueueTail::::get(), u64::from(T::QueuePageSize::get())); - assert!(!QueuePages::::contains_key(0)); } #[benchmark(pov_mode = Measured)] - fn scheduler_paged_tombstone_drain(n: Linear<1, 10_000>) { - let bounded = n.min(T::MaxQueueLength::get()); - let page_size = T::QueuePageSize::get(); - let mut ticket = 0u64; - while ticket < u64::from(bounded) { - let page_id = ticket / u64::from(page_size); - let remaining = u64::from(bounded).saturating_sub(ticket); - let entries = remaining.min(u64::from(page_size)); - let page = (0..entries) - .map(|offset| { - benchmark_queue_entry::( - ticket.saturating_add(offset), - 37_000_000u64.saturating_add(ticket).saturating_add(offset), - ) - }) - .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); - } - QueueHead::::put(0); - QueueTail::::put(u64::from(bounded)); - QueueOccupancy::::put(bounded); - NextQueueTicket::::put(u64::from(bounded)); + fn opening_target_snapshot_capture( + e: Linear<1, { T::MaxOpeningSnapshotEntries::get().min(T::MaxContractSteps::get()) }>, + ) { + let (actor, assets) = prepare_opening_snapshot_assets::(e); + let steps: ContractSteps = (0..T::MaxContractSteps::get()) + .map(|index| Step { + precondition: None, + task: ActorTask::Mint { + asset: assets[index.min(e - 1) as usize], + amount: AmountResolution::PercentageAtOpening(Perbill::one()), + }, + on_error: StepErrorPolicy::AbortCycle, + }) + .collect::>() + .try_into() + .expect("full target-surface Contract fits"); + let snapshot; #[block] { - core::hint::black_box( - Pallet::::paged_drain_tombstones(u64::from(bounded), bounded) - .expect("benchmark queue topology is valid"), + snapshot = + Pallet::::capture_opening_snapshot(ActorType::System, &actor, &steps, Zero::zero()); + core::hint::black_box(&snapshot); + } + assert_eq!(snapshot.len() as u32, e); + for asset in assets.iter().take(e as usize) { + assert_eq!( + snapshot.get(&OpeningSurface::TargetAsset(*asset)), + Some(&T::AssetOps::balance(&actor, *asset)), ); } - assert!(QueueHead::::get() >= u64::from(bounded)); - assert_eq!(QueueHead::::get(), QueueTail::::get()); - assert_eq!(QueueOccupancy::::get(), 0); } - #[benchmark(pov_mode = Measured)] - fn scheduler_paged_mixed_scan(n: Linear<1, 10_000>) { - let bounded = n.min(T::MaxQueueLength::get()); - let page_size = T::QueuePageSize::get(); - let template_id = bench_create_system_manual::(38_000_000); - let hot_template = ActorHot::::get(template_id).expect("benchmark hot template"); - let 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"); - identity_template.actor_class = ActorClass::User { owner_slot: 0 }; - let mut ticket = 0u64; - while ticket < u64::from(bounded) { - let page_id = ticket / u64::from(page_size); - let remaining = u64::from(bounded).saturating_sub(ticket); - let entries = remaining.min(u64::from(page_size)); - let page = (0..entries) - .map(|offset| { - let logical_ticket = ticket.saturating_add(offset); - let actor_id = 39_000_000u64.saturating_add(logical_ticket); - if logical_ticket % 2 == 1 { - let mut hot = hot_template.clone(); - hot.queue_ticket = Some(logical_ticket); - ActorIdentities::::insert(actor_id, identity_template.clone()); - ActorHot::::insert(actor_id, hot); - Pallet::::store_actor_contract(actor_id, contract_template.clone()) - .expect("benchmark mixed Contract remains admitted"); - ActorFunding::::insert(actor_id, funding_template.clone()); - } - 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); + #[benchmark(extra, pov_mode = Measured)] + fn opening_staking_snapshot_capture( + e: Linear<1, { T::MaxOpeningSnapshotEntries::get().min(T::MaxContractSteps::get()) }>, + ) -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let actor: T::AccountId = account("opening_snapshot_actor", 0, 0); + let positions = T::BenchmarkHelper::setup_max_encoded_staking_positions(&actor, e)?; + assert_eq!(positions.len() as u32, e); + assert_eq!( + positions + .iter() + .map(|(asset, _)| asset) + .collect::>() + .len(), + positions.len() + ); + let steps: ContractSteps = (0..T::MaxContractSteps::get()) + .map(|index| Step { + precondition: None, + task: ActorTask::Unstake { + asset: positions[index.min(e - 1) as usize].0, + shares: AmountResolution::PercentageAtOpening(Perbill::one()), + }, + on_error: StepErrorPolicy::AbortCycle, + }) + .collect::>() + .try_into() + .expect("full staking-surface Contract fits"); + let snapshot; #[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)); - } - } + snapshot = + Pallet::::capture_opening_snapshot(ActorType::System, &actor, &steps, Zero::zero()); + core::hint::black_box(&snapshot); } - assert!(QueueHead::::get() >= cutoff); - assert_eq!(QueueHead::::get(), QueueTail::::get()); - assert_eq!(QueueOccupancy::::get(), 0); + assert_eq!(snapshot.len() as u32, e); + for (asset, shares) in positions { + assert!(!shares.is_zero()); + assert_eq!( + snapshot.get(&OpeningSurface::StakingShares(asset)), + Some(&shares) + ); + } + Ok(()) } - /// 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, + #[benchmark(extra, pov_mode = Measured)] + fn opening_native_snapshot_capture( + c: Linear<0, 17>, + ) -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + if T::MaxContractSteps::get() < 2 || T::MaxOpeningSnapshotEntries::get() < 2 { + return Err(polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "host cannot represent two typed Native Opening surfaces", + )); + } + let actor: T::AccountId = account("opening_native_snapshot_actor", 0, 0); + let native = T::FeeNativeAssetId::get(); + let other = T::BenchmarkHelper::setup_predicate_assets(&actor, 2)? + .into_iter() + .find(|asset| *asset != native) + .expect("distinct fixed auxiliary asset exists"); + let amount = T::MinUserBalance::get() + .checked_add(&T::AssetOps::minimum_balance(native)) + .and_then(|amount| amount.checked_add(&100u32.into())) + .expect("benchmark Native funding fits"); + T::AssetOps::mint(&actor, native, amount).expect("Native snapshot custody is funded"); + let raw = T::AssetOps::balance(&actor, native); + assert!(!raw.is_zero()); + let actor_type = if c / 9 == 0 { + ActorType::System + } else { + ActorType::User }; - 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 family = (c % 9) / 3; + let reserved = match c % 3 { + 0 => Zero::zero(), + 1 => raw / 2u32.into(), + _ => raw, }; - 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, - >(())) + let steps: ContractSteps = (0..T::MaxContractSteps::get()) + .map(|index| Step { + precondition: None, + task: if family == 0 || (family == 2 && index % 2 == 0) { + ActorTask::AddLiquidity { + asset_a: native, + asset_b: other, + amount_a: AmountResolution::PercentageAtOpening(Perbill::one()), + amount_b: AmountResolution::Fixed(One::one()), + min_lp_out: One::one(), } - Err(error) => { - polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)) + } else { + ActorTask::SwapOut { + asset_out: native, + amount_out: AmountResolution::PercentageAtOpening(Perbill::one()), + asset_in: other, + input_limit: InputLimit::Absolute(One::one()), + slippage_tolerance: Perbill::zero(), } - } + }, + on_error: StepErrorPolicy::AbortCycle, }) - .expect("zero-Step inner atom commits"); + .collect::>() + .try_into() + .expect("full Native snapshot Contract fits"); + let snapshot; + #[block] + { + snapshot = Pallet::::capture_opening_snapshot(actor_type, &actor, &steps, reserved); + core::hint::black_box(&snapshot); + } + assert_eq!(snapshot.len(), if family == 2 { 2 } else { 1 }); + let spendable = raw.saturating_sub(reserved); + if family != 1 { + let minimum = if actor_type == ActorType::User { + T::MinUserBalance::get() + } else { + T::AssetOps::minimum_balance(native) + }; + assert_eq!( + snapshot.get(&OpeningSurface::PreservableAsset(native)), + Some(&spendable.saturating_sub(minimum)), + ); } - 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() - })); + if family != 0 { + assert_eq!( + snapshot.get(&OpeningSurface::TargetAsset(native)), + Some(&spendable) + ); + } + Ok(()) } - /// 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"); + #[benchmark(extra, pov_mode = Measured)] + fn opening_unfunded_snapshot_capture( + c: Linear< + 0, + { + T::MaxOpeningSnapshotEntries::get().min(T::MaxContractSteps::get().saturating_mul(2)) + + 2 * T::MaxOpeningSnapshotEntries::get().min(T::MaxContractSteps::get()) + }, + >, + ) -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let paired = + T::MaxOpeningSnapshotEntries::get().min(T::MaxContractSteps::get().saturating_mul(2)); + let single = T::MaxOpeningSnapshotEntries::get().min(T::MaxContractSteps::get()); + if single == 0 { + return Err(polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "host cannot represent an Opening surface", + )); } - 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, + let (family, count) = if c < paired { + (0, c + 1) + } else if c < paired + single { + (1, c - paired + 1) + } else if c < paired + 2 * single { + (2, c - paired - single + 1) + } else { + (3, 1) }; - 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"); + let owner: T::AccountId = account("opening_snapshot_reference_owner", 0, 0); + let actor: T::AccountId = account("opening_unfunded_snapshot_actor", 0, 0); + assert_ne!(owner, actor); + let assets: Vec<_> = match family { + 0 | 1 => T::BenchmarkHelper::setup_max_encoded_predicate_assets(&owner, count.max(2) + 1)? + .into_iter() + .filter(|asset| *asset != T::FeeNativeAssetId::get()) + .take(count.max(2) as usize) + .collect(), + 2 => T::BenchmarkHelper::setup_max_encoded_staking_positions(&owner, count)? + .into_iter() + .map(|(asset, _)| asset) + .collect(), + _ => { + let (asset, _) = T::BenchmarkHelper::setup_stake(&owner)?; + assert!(T::StakingOps::share_asset(asset).is_some()); + T::BenchmarkHelper::remove_empty_staking_receipt(&owner, asset)?; + assert!(T::StakingOps::share_asset(asset).is_none()); + vec![asset] } - }); + }; assert_eq!( - ActorFunding::::get(actor_id) - .expect("maximum Opening funding exists") - .funding_accumulated + assets.len() as u32, + if family < 2 { count.max(2) } else { count } + ); + assert_eq!( + assets + .iter() + .collect::>() .len(), - T::MaxFundingTrackedAssets::get() as usize, + assets.len() ); - 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); + let steps: ContractSteps = (0..T::MaxContractSteps::get()) + .map(|index| Step { + precondition: None, + task: match family { + 0 => ActorTask::AddLiquidity { + asset_a: assets[(index * 2).min(count - 1) as usize], + asset_b: assets[if count == 1 { + 1 + } else if index * 2 + 1 < count { + index * 2 + 1 + } else { + count - 2 + } as usize], + amount_a: AmountResolution::PercentageAtOpening(Perbill::one()), + amount_b: if count == 1 { + AmountResolution::Fixed(One::one()) + } else { + AmountResolution::PercentageAtOpening(Perbill::one()) + }, + min_lp_out: One::one(), + }, + 1 => ActorTask::Mint { + asset: assets[index.min(count - 1) as usize], + amount: AmountResolution::PercentageAtOpening(Perbill::one()), + }, + _ => ActorTask::Unstake { + asset: assets[index.min(count - 1) as usize], + shares: AmountResolution::PercentageAtOpening(Perbill::one()), + }, + }, + on_error: StepErrorPolicy::AbortCycle, + }) + .collect::>() + .try_into() + .expect("full unfunded snapshot Contract fits"); + let snapshot; #[block] { - core::hint::black_box(Pallet::::execute_cycle(Weight::MAX)); + snapshot = + Pallet::::capture_opening_snapshot(ActorType::System, &actor, &steps, Zero::zero()); + core::hint::black_box(&snapshot); + } + assert_eq!(snapshot.len() as u32, count); + for asset in assets.into_iter().take(count as usize) { + let surface = match family { + 0 => OpeningSurface::PreservableAsset(asset), + 1 => OpeningSurface::TargetAsset(asset), + _ => OpeningSurface::StakingShares(asset), + }; + assert_eq!(snapshot.get(&surface), Some(&Zero::zero())); } - 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); + Ok(()) } - /// 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)) + fn opening_mixed_allocations(step_limit: u32, entry_limit: u32) -> Vec<(u32, u32, u32)> { + let mut allocations = Vec::new(); + for total in 2..=entry_limit.min(step_limit.saturating_mul(2)) { + for preservable in 0..=total { + for target in 0..=total.saturating_sub(preservable).min(step_limit) { + let shares = total - preservable - target; + if preservable.div_ceil(2) + target + shares <= step_limit + && u32::from(preservable > 0) + u32::from(target > 0) + u32::from(shares > 0) >= 2 + { + allocations.push((preservable, target, shares)); } } - }) - .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, - ); + allocations } - /// 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"); + fn prepare_mixed_opening_snapshot( + preservable: u32, + target: u32, + share_count: u32, + reverse: bool, + ) -> Result< + (T::AccountId, ContractSteps, RunOpeningSnapshotOf), + polkadot_sdk::frame_benchmarking::BenchmarkError, + > { + let total = preservable + target + share_count; + let actor: T::AccountId = account("opening_snapshot_actor", 0, 0); + let available = + T::BenchmarkHelper::setup_max_encoded_predicate_assets(&actor, total + share_count + 1)?; + let positions = if share_count == 0 { + Vec::new() + } else { + T::BenchmarkHelper::setup_max_encoded_staking_positions(&actor, share_count)? }; - let execution_instance = Pallet::::derive_active_actor_view( - full_state.identity, - full_state.hot, - full_state.contract, + assert_eq!(positions.len() as u32, share_count); + let mut position_assets = alloc::collections::BTreeSet::new(); + for (asset, shares) in &positions { + assert!(!shares.is_zero()); + assert!(position_assets.insert(*asset)); + } + let receipts: alloc::collections::BTreeSet<_> = positions + .iter() + .map(|(asset, _)| T::StakingOps::share_asset(*asset).expect("funded position has a receipt")) + .collect(); + assert_eq!(receipts.len(), positions.len()); + let assets: Vec<_> = available + .into_iter() + .filter(|asset| { + *asset != T::FeeNativeAssetId::get() + && !position_assets.contains(asset) + && !receipts.contains(asset) + }) + .take((preservable + target).max(2) as usize) + .collect(); + assert!(assets.len() as u32 >= preservable + target); + assert_eq!( + assets + .iter() + .collect::>() + .len(), + assets.len() ); - 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 mut expected = RunOpeningSnapshotOf::::default(); + for (index, asset) in assets + .iter() + .take((preservable + target) as usize) + .enumerate() { - 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"); + let minimum = T::AssetOps::minimum_balance(*asset); + let amount = minimum + .checked_add(&One::one()) + .expect("mixed capture funding fits"); + T::AssetOps::mint(&actor, *asset, amount).expect("mixed capture asset is funded"); + let balance = T::AssetOps::balance(&actor, *asset); + let (surface, value) = if (index as u32) < preservable { + ( + OpeningSurface::PreservableAsset(*asset), + balance.saturating_sub(minimum), + ) + } else { + (OpeningSurface::TargetAsset(*asset), balance) + }; + assert!(!value.is_zero()); + expected + .try_insert(surface, value) + .expect("mixed expected snapshot fits"); + } + let mut steps = Vec::new(); + for pair in assets[..preservable as usize].chunks(2) { + let other = pair.get(1).copied().unwrap_or_else(|| { + assets + .iter() + .copied() + .chain(position_assets.iter().copied()) + .find(|asset| *asset != pair[0]) + .expect("fixed auxiliary asset is distinct") + }); + steps.push(Step { + precondition: None, + task: ActorTask::AddLiquidity { + asset_a: pair[0], + asset_b: other, + amount_a: AmountResolution::PercentageAtOpening(Perbill::one()), + amount_b: if pair.len() == 2 { + AmountResolution::PercentageAtOpening(Perbill::one()) + } else { + AmountResolution::Fixed(One::one()) + }, + min_lp_out: One::one(), + }, + on_error: StepErrorPolicy::AbortCycle, + }); } - 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); + let repeated = steps.last().cloned(); + for asset in assets + .iter() + .skip(preservable as usize) + .take(target as usize) + { + steps.push(Step { + precondition: None, + task: ActorTask::Mint { + asset: *asset, + amount: AmountResolution::PercentageAtOpening(Perbill::one()), + }, + on_error: StepErrorPolicy::AbortCycle, + }); + } + for (asset, shares) in positions { + expected + .try_insert(OpeningSurface::StakingShares(asset), shares) + .expect("mixed share snapshot fits"); + steps.push(Step { + precondition: None, + task: ActorTask::Unstake { + asset, + shares: AmountResolution::PercentageAtOpening(Perbill::one()), + }, + on_error: StepErrorPolicy::AbortCycle, + }); + } + let repeated = repeated.unwrap_or_else(|| steps.last().expect("mixed Steps exist").clone()); + assert!(steps.len() as u32 <= T::MaxContractSteps::get()); + steps.resize(T::MaxContractSteps::get() as usize, repeated); + if reverse { + steps.reverse(); + } + let steps: ContractSteps = steps.try_into().expect("full mixed Contract fits"); + assert_eq!(expected.len() as u32, total); + Ok((actor, steps, expected)) } - /// 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, + #[benchmark(extra, pov_mode = Measured)] + fn opening_mixed_snapshot_capture( + c: Linear< + 0, + { + (opening_mixed_allocations( + T::MaxContractSteps::get(), + T::MaxOpeningSnapshotEntries::get(), + ) + .len() as u32) + .saturating_mul(2) + .saturating_sub(1) }, - 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, + >, + ) -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let allocations = opening_mixed_allocations( + T::MaxContractSteps::get(), + T::MaxOpeningSnapshotEntries::get(), ); - 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; - }); + let &(preservable, target, share_count) = allocations.get((c / 2) as usize).ok_or( + polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "host cannot represent this mixed Opening allocation", + ), + )?; + let (actor, steps, expected) = + prepare_mixed_opening_snapshot::(preservable, target, share_count, c % 2 == 1)?; + let snapshot; #[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"); + snapshot = + Pallet::::capture_opening_snapshot(ActorType::System, &actor, &steps, Zero::zero()); + core::hint::black_box(&snapshot); } - 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() - })); + assert_eq!(snapshot.len() as u32, preservable + target + share_count); + assert_eq!(snapshot, expected); + Ok(()) } - /// 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), + fn opening_share_mixed_capture( + e: Linear< + 1, + { + T::MaxOpeningSnapshotEntries::get().min( + T::MaxContractSteps::get() + .saturating_mul(2) + .saturating_sub(1), + ) }, - ]) - .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() + >, + ) -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let (actor, steps, expected) = prepare_mixed_opening_snapshot::(e - 1, 0, 1, true)?; + let snapshot; + #[block] { - 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(), - } - }; + snapshot = + Pallet::::capture_opening_snapshot(ActorType::System, &actor, &steps, Zero::zero()); + core::hint::black_box(&snapshot); + } + assert_eq!(snapshot.len() as u32, e); + assert_eq!(snapshot, expected); + Ok(()) + } + + fn opening_predicate_capture_steps( + predicates: Vec>, + ) -> ContractSteps { + let mut steps = ContractSteps::::default(); + for chunk in predicates.chunks(benchmark_predicate_capacity::() as usize) { + let predicates = chunk + .iter() + .cloned() + .map(|predicate| TimedPredicate { + timing: ObservationTiming::Opening, + predicate, + }) + .collect(); steps .try_push(Step { - precondition: Some(opening_all::(predicates)), - task, + precondition: Some(packed_predicate_clauses::( + predicates, + T::MaxPredicatesPerClause::get(), + )), + task: ActorTask::StopCycle, on_error: StepErrorPolicy::AbortCycle, }) - .expect("Opening-failure Contract fits"); + .expect("benchmark predicate Step fits"); } - let actor_id = bench_create_system_manual::(41_615_625); - let contract = system_contract::( - Schedule { - trigger: Trigger::manual(), - cooldown_blocks: 0, + steps + } + + #[benchmark(pov_mode = Measured)] + fn opening_predicate_traversal() -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let (actor, steps) = prepare_full_opening_predicate_capture::(0, 0, false)?; + let results; + #[block] + { + results = Pallet::::capture_opening_predicate_results(&actor, &steps, Zero::zero()); + core::hint::black_box(&results); + } + assert!(results.is_empty()); + Ok(()) + } + + #[benchmark(pov_mode = Measured)] + fn opening_predicate_capture( + p: Linear< + 0, + { + T::MaxOpeningPredicateResults::get() + .min(T::MaxContractSteps::get().saturating_mul(benchmark_predicate_capacity::())) + .max(1) }, - 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) + >, + ) -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let maximum = T::MaxOpeningPredicateResults::get() + .min(T::MaxContractSteps::get().saturating_mul(benchmark_predicate_capacity::())); + if maximum == 0 { + return Err(polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "host has no representable predicate capacity for this benchmark", + )); + } + let observations = u32::from(p >= 2); + let (actor, steps) = + prepare_full_opening_predicate_capture::(p - observations, observations, false)?; + let results; + #[block] + { + results = Pallet::::capture_opening_predicate_results(&actor, &steps, Zero::zero()); + core::hint::black_box(&results); + } + assert_eq!(u32::try_from(results.len()).expect("result count fits"), p); + assert!(results.iter().all(|result| *result == Ok(true))); + Ok(()) + } + + #[benchmark(extra, pov_mode = Measured)] + fn opening_observation_capture( + p: Linear< + 1, { - 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), - ); + T::MaxOpeningPredicateResults::get() + .min(T::MaxContractSteps::get().saturating_mul(benchmark_predicate_capacity::())) + .max(1) + }, + >, + ) -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + if benchmark_predicate_capacity::() == 0 { + return Err(polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "host has no representable predicate capacity for this benchmark", + )); + } + let actor: T::AccountId = account("opening_predicate_actor", 0, 0); + let feeds = T::BenchmarkHelper::setup_max_encoded_observation_feeds(p) + .expect("maximum-encoded Opening observation feeds are published"); + assert_eq!(feeds.len() as u32, p); assert_eq!( - control_context.opening_predicate_results, - step_count.saturating_mul(T::MaxPredicatesPerStep::get()), + feeds + .iter() + .collect::>() + .len() as u32, + p ); - 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 steps = opening_predicate_capture_steps::( + feeds + .into_iter() + .map(|feed| Predicate::ObservationAbove { + feed, + threshold: 0, + max_age_blocks: 100, + }) + .collect(), ); - ActorHot::::mutate(actor_id, |maybe| { - maybe - .as_mut() - .expect("Opening-failure hot state remains") - .queue_ticket = None; - }); + let results; #[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"); + results = Pallet::::capture_opening_predicate_results(&actor, &steps, Zero::zero()); + core::hint::black_box(&results); } - 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); + assert_eq!(results.len() as u32, p); + assert!(results.iter().all(|result| *result == Ok(true))); + Ok(()) } - /// 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), + fn prepare_observation_read_state( + p: u32, + s: u32, + max_age_blocks: u32, + ) -> Result<(Vec, bool), polkadot_sdk::frame_benchmarking::BenchmarkError> + { + let state = match s { + 0 => BenchmarkObservationReadState::Missing, + 1 => BenchmarkObservationReadState::Deactivated, + 2 => BenchmarkObservationReadState::Uninitialized, + 3 => BenchmarkObservationReadState::Stale, + _ => BenchmarkObservationReadState::Fresh, + }; + let feeds = T::BenchmarkHelper::setup_observation_read_state(p, state, max_age_blocks)?; + assert_eq!(feeds.len() as u32, p); + assert_eq!( + feeds + .iter() + .collect::>() + .len(), + feeds.len() ); - 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 now = frame_system::Pallet::::block_number(); + for feed in &feeds { + assert!(matches!( + ( + state, + T::ObservationProvider::observe(feed, now, max_age_blocks) + ), + ( + BenchmarkObservationReadState::Missing | BenchmarkObservationReadState::Deactivated, + ScalarObservationState::Unavailable + ) | ( + BenchmarkObservationReadState::Uninitialized, + ScalarObservationState::Uninitialized + ) | ( + BenchmarkObservationReadState::Stale, + ScalarObservationState::Stale + ) | ( + BenchmarkObservationReadState::Fresh, + ScalarObservationState::Fresh { .. } + ) + )); } - 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) + Ok((feeds, state == BenchmarkObservationReadState::Fresh)) + } + + #[benchmark(extra, pov_mode = Measured)] + fn opening_observation_read_state( + p: Linear< + 1, { - 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), + T::MaxOpeningPredicateResults::get() + .min(T::MaxContractSteps::get().saturating_mul(benchmark_predicate_capacity::())) + .max(1) + }, + >, + s: Linear<0, 4>, + ) -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let maximum = T::MaxOpeningPredicateResults::get() + .min(T::MaxContractSteps::get().saturating_mul(benchmark_predicate_capacity::())); + if maximum == 0 { + return Err(polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "host cannot represent Opening predicates", + )); + } + let max_age_blocks = 100; + let (feeds, expected) = prepare_observation_read_state::(p, s, max_age_blocks)?; + let actor: T::AccountId = account("opening_predicate_actor", 0, 0); + let steps = opening_predicate_capture_steps::( + feeds + .into_iter() + .map(|feed| Predicate::ObservationAbove { + feed, + threshold: 0, + max_age_blocks, + }) + .collect(), ); - ActorHot::::mutate(actor_id, |maybe| { - maybe - .as_mut() - .expect("Opening-retry hot state remains") - .queue_ticket = None; - }); + let results; #[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"); + results = Pallet::::capture_opening_predicate_results(&actor, &steps, Zero::zero()); + core::hint::black_box(&results); } - 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!(results.len() as u32, p); + assert!(results.iter().all(|result| *result == Ok(expected))); + Ok(()) + } + + #[benchmark(extra, pov_mode = Measured)] + fn opening_mixed_predicate_capture( + p: Linear< + 2, + { + T::MaxOpeningPredicateResults::get() + .min(T::MaxContractSteps::get().saturating_mul(benchmark_predicate_capacity::())) + .max(2) + }, + >, + ) -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let maximum = T::MaxOpeningPredicateResults::get() + .min(T::MaxContractSteps::get().saturating_mul(benchmark_predicate_capacity::())); + if maximum < 2 { + return Err(polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "host cannot represent mixed Opening predicates", + )); + } + let actor: T::AccountId = account("opening_predicate_actor", 0, 0); + let native = T::FeeNativeAssetId::get(); + let non_native = T::BenchmarkHelper::setup_predicate_assets(&actor, 2)? + .into_iter() + .find(|asset| *asset != native) + .expect("mixed capture has a non-native asset"); + let assets = if p == 2 { + vec![non_native] + } else { + vec![native, non_native] + }; + let mut predicates = Vec::with_capacity(p as usize); + for asset in assets { + let amount = T::AssetOps::minimum_balance(asset) + .checked_add(&One::one()) + .expect("mixed capture funding fits"); + T::AssetOps::mint(&actor, asset, amount).expect("mixed capture asset is funded"); + predicates.push(Predicate::BalanceAbove { + asset, + threshold: Zero::zero(), + }); + } + let observation_count = p - predicates.len() as u32; + let feeds = T::BenchmarkHelper::setup_max_encoded_observation_feeds(observation_count)?; + assert_eq!(feeds.len() as u32, observation_count); assert_eq!( - run.opening_predicate_results.len(), - step_count.saturating_mul(T::MaxPredicatesPerStep::get()) as usize, + feeds + .iter() + .collect::>() + .len() as u32, + observation_count ); - assert!(ActorHot::::get(actor_id).is_some_and(|hot| { - hot.cycle_state == CycleState::Suspended && hot.wakeup_pointer.is_some() + predicates.extend(feeds.into_iter().map(|feed| Predicate::ObservationAbove { + feed, + threshold: 0, + max_age_blocks: 100, })); + let steps = opening_predicate_capture_steps::(predicates); + let results; + #[block] + { + results = Pallet::::capture_opening_predicate_results(&actor, &steps, Zero::zero()); + core::hint::black_box(&results); + } + assert_eq!(results.len() as u32, p); + assert!(results.iter().all(|result| *result == Ok(true))); + Ok(()) } - /// 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, + #[benchmark(extra, pov_mode = Measured)] + fn opening_asset_heavy_predicate_capture( + p: Linear< + 2, + { + T::MaxOpeningPredicateResults::get() + .min(T::MaxContractSteps::get().saturating_mul(benchmark_predicate_capacity::())) + .max(2) }, - 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, + >, + ) -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let maximum = T::MaxOpeningPredicateResults::get() + .min(T::MaxContractSteps::get().saturating_mul(benchmark_predicate_capacity::())); + if maximum < 2 { + return Err(polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "host cannot represent mixed Opening predicates", + )); + } + let actor: T::AccountId = account("opening_predicate_actor", 0, 0); + let assets = T::BenchmarkHelper::setup_max_encoded_predicate_assets(&actor, (p - 1).max(2))?; + let assets: Vec<_> = assets + .into_iter() + .filter(|asset| p != 2 || *asset != T::FeeNativeAssetId::get()) + .collect(); + assert_eq!(assets.len() as u32, p - 1); + assert_eq!( + assets + .iter() + .collect::>() + .len(), + assets.len() ); - 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; + let mut predicates = Vec::with_capacity(p as usize); + for asset in assets { + let amount = T::AssetOps::minimum_balance(asset) + .checked_add(&One::one()) + .expect("asset-heavy capture funding fits"); + T::AssetOps::mint(&actor, asset, amount).expect("asset-heavy capture asset is funded"); + predicates.push(Predicate::BalanceAbove { + asset, + threshold: Zero::zero(), + }); + } + let feeds = T::BenchmarkHelper::setup_max_encoded_observation_feeds(1)?; + assert_eq!(feeds.len(), 1); + predicates.push(Predicate::ObservationAbove { + feed: feeds[0], + threshold: 0, + max_age_blocks: 100, }); + let steps = opening_predicate_capture_steps::(predicates); + let results; #[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"); + results = Pallet::::capture_opening_predicate_results(&actor, &steps, Zero::zero()); + core::hint::black_box(&results); } - 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() - })); + assert_eq!(results.len() as u32, p); + assert!(results.iter().all(|result| *result == Ok(true))); + Ok(()) } - /// 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, + fn prepare_max_encoded_opening_predicates( + balances: u32, + observations: u32, + include_native: bool, + ) -> Result<(T::AccountId, ContractSteps), polkadot_sdk::frame_benchmarking::BenchmarkError> + { + let actor: T::AccountId = account("opening_predicate_actor", 0, 0); + let native = T::FeeNativeAssetId::get(); + let mut assets = Vec::new(); + if balances != 0 { + let requested = + balances + .checked_add(1) + .ok_or(polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "asset fixture count overflows", + ))?; + assets = T::BenchmarkHelper::setup_max_encoded_predicate_assets(&actor, requested)? + .into_iter() + .filter(|asset| *asset != native) + .take(balances as usize) + .collect(); + assert_eq!(assets.len() as u32, balances); + assert_eq!( + assets + .iter() + .collect::>() + .len(), + assets.len() + ); + } + if include_native { + assets.insert(0, native); + } + let mut predicates = Vec::new(); + for asset in assets { + let amount = T::AssetOps::minimum_balance(asset) + .checked_add(&One::one()) + .expect("maximum capture funding fits"); + T::AssetOps::mint(&actor, asset, amount).expect("maximum capture asset is funded"); + predicates.push(Predicate::BalanceAbove { + asset, + threshold: Zero::zero(), + }); + } + if observations != 0 { + let feeds = T::BenchmarkHelper::setup_max_encoded_observation_feeds(observations)?; + assert_eq!(feeds.len() as u32, observations); + assert_eq!( + feeds + .iter() + .collect::>() + .len(), + feeds.len() + ); + predicates.extend(feeds.into_iter().map(|feed| Predicate::ObservationAbove { + feed, + threshold: 0, + max_age_blocks: 100, + })); + } + Ok((actor, opening_predicate_capture_steps::(predicates))) + } + + fn prepare_full_opening_predicate_capture( + balances: u32, + observations: u32, + include_native: bool, + ) -> Result<(T::AccountId, ContractSteps), polkadot_sdk::frame_benchmarking::BenchmarkError> + { + let per_step = benchmark_predicate_capacity::(); + let clause_count = T::MaxPreconditionClauses::get().min(per_step) as usize; + if clause_count == 0 || T::MaxContractSteps::get() == 0 { + return Err(polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "host cannot represent predicate traversal", + )); + } + let (actor, opening_steps) = + prepare_max_encoded_opening_predicates::(balances, observations, include_native)?; + let mut predicates = opening_steps + .iter() + .filter_map(|step| step.precondition.as_ref()) + .flat_map(|precondition| precondition.clauses.iter().flat_map(|clause| clause.iter())) + .cloned() + .collect::>(); + predicates.resize( + T::MaxContractSteps::get().saturating_mul(per_step) as usize, + TimedPredicate { + timing: ObservationTiming::Current, + predicate: Predicate::BlockNumberAbove { threshold: 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"); + ); + let mut steps = ContractSteps::::default(); + for chunk in predicates.chunks(per_step as usize) { + let mut clauses = vec![Vec::new(); clause_count]; + for (index, predicate) in chunk.iter().enumerate() { + clauses[index % clause_count].push(*predicate); } - }); - 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 clauses = clauses + .into_iter() + .map(|clause| clause.try_into().expect("balanced predicate clause fits")) + .collect::>() + .try_into() + .expect("maximum predicate clause count fits"); + steps + .try_push(Step { + precondition: Some(Precondition { clauses }), + task: ActorTask::StopCycle, + on_error: StepErrorPolicy::AbortCycle, + }) + .expect("full predicate traversal fits Contract"); + } + assert_eq!(steps.len() as u32, T::MaxContractSteps::get()); + Ok((actor, steps)) + } + + fn prepare_max_encoded_step_precondition( + balances: u32, + observations: u32, + include_native: bool, + ) -> Result<(T::AccountId, PreconditionOf), polkadot_sdk::frame_benchmarking::BenchmarkError> + { + let count = balances + .saturating_add(observations) + .saturating_add(u32::from(include_native)); + if count == 0 || count > benchmark_predicate_capacity::() { + return Err(polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "host cannot represent Step predicate count", + )); + } + let (actor, steps) = + prepare_max_encoded_opening_predicates::(balances, observations, include_native)?; + assert_eq!(steps.len(), 1); + let predicates = steps[0] + .precondition + .as_ref() + .expect("prepared Step has predicates") + .clauses + .iter() + .flat_map(|clause| clause.iter()) + .cloned() + .map(|mut predicate| { + predicate.timing = ObservationTiming::Current; + predicate + }) + .collect(); + let precondition = packed_predicate_clauses::( + predicates, + count.div_ceil(T::MaxPreconditionClauses::get()).max(1), ); - 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; - }); + Ok((actor, precondition)) + } + + #[benchmark(extra, pov_mode = Measured)] + fn predicate_host_allocation( + b: Linear<0, { benchmark_predicate_capacity::().max(1) }>, + ) -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let count = benchmark_predicate_capacity::(); + if b > count { + return Err(polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "host cannot represent Step predicate allocation", + )); + } + let (actor, precondition) = prepare_max_encoded_step_precondition::(b, count - b, false)?; + let result; #[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"); + result = Pallet::::evaluate_precondition(&precondition, &actor, Zero::zero()); + core::hint::black_box(&result); } - 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); + assert_eq!(result, Ok(true)); + Ok(()) + } + + #[benchmark(extra, pov_mode = Measured)] + fn predicate_native_host_allocation( + b: Linear<0, { benchmark_predicate_capacity::().saturating_sub(1).max(1) }>, + ) -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let count = benchmark_predicate_capacity::(); + if b >= count { + return Err(polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "host cannot represent native Step predicate allocation", + )); + } + let (actor, precondition) = prepare_max_encoded_step_precondition::(b, count - 1 - b, true)?; + let result; + #[block] + { + result = Pallet::::evaluate_precondition(&precondition, &actor, Zero::zero()); + core::hint::black_box(&result); + } + assert_eq!(result, Ok(true)); + Ok(()) } - /// 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() + fn predicate_asset_evaluation( + p: Linear<1, { benchmark_predicate_capacity::().max(1) }>, + ) -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let (actor, precondition) = prepare_max_encoded_step_precondition::(p, 0, false)?; + let result; + #[block] { - 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(), + result = Pallet::::evaluate_precondition(&precondition, &actor, Zero::zero()); + core::hint::black_box(&result); + } + assert_eq!(result, Ok(true)); + Ok(()) + } + + #[benchmark(extra, pov_mode = Measured)] + fn predicate_observation_evaluation( + p: Linear<1, { benchmark_predicate_capacity::().max(1) }>, + ) -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let (actor, precondition) = prepare_max_encoded_step_precondition::(0, p, false)?; + let result; + #[block] + { + result = Pallet::::evaluate_precondition(&precondition, &actor, Zero::zero()); + core::hint::black_box(&result); + } + assert_eq!(result, Ok(true)); + Ok(()) + } + + #[benchmark(pov_mode = Measured)] + fn predicate_observation_heavy_evaluation( + o: Linear<1, { benchmark_predicate_capacity::().saturating_sub(1).max(1) }>, + ) -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let (actor, precondition) = prepare_max_encoded_step_precondition::(1, o, false)?; + let result; + #[block] + { + result = Pallet::::evaluate_precondition(&precondition, &actor, Zero::zero()); + core::hint::black_box(&result); + } + assert_eq!(result, Ok(true)); + Ok(()) + } + + #[benchmark(extra, pov_mode = Measured)] + fn predicate_result_branch( + r: Linear<0, 2>, + ) -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let count = benchmark_predicate_capacity::(); + if count < 2 || (r == 2 && count > T::MaxOpeningPredicateResults::get()) { + return Err(polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "host cannot represent predicate result branch", + )); + } + let (actor, mut precondition) = + prepare_max_encoded_step_precondition::(count - 1, 1, false)?; + for timed in precondition + .clauses + .iter_mut() + .flat_map(|clause| clause.iter_mut()) + { + if r == 1 { + match &mut timed.predicate { + Predicate::BalanceAbove { asset, threshold } => { + *threshold = T::AssetOps::balance(&actor, *asset); + } + Predicate::ObservationAbove { threshold, .. } => *threshold = u128::MAX, + _ => { + return Err(polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "expected mixed comparison predicates", + )); + } } - }; - steps - .try_push(Step { - precondition: Some(opening_all::(predicates)), - task, - on_error: StepErrorPolicy::AbortCycle, - }) - .expect("Opening-close Contract fits"); + } else if r == 2 { + timed.timing = ObservationTiming::Opening; + } } - let actor_id = bench_create_system_manual::(41_671_875); - let mut contract = system_contract::( - Schedule { - trigger: Trigger::manual(), - cooldown_blocks: 0, + let opening_results: OpeningPredicateResultsOf = if r == 2 { + vec![Err(PredicateError::InvalidObservation); T::MaxOpeningPredicateResults::get() as usize] + .try_into() + .expect("maximum Opening result vector fits") + } else { + Default::default() + }; + let mut index = if r == 2 { + opening_results.len() - count as usize + } else { + 0 + }; + let result; + #[block] + { + result = Pallet::::evaluate_step_precondition( + Some(&precondition), + &actor, + Zero::zero(), + &opening_results, + &mut index, + ); + core::hint::black_box(&result); + } + let expected = match r { + 0 => Ok(true), + 1 => Ok(false), + _ => Err(Error::::InvalidPredicate.into()), + }; + assert_eq!(result, expected); + assert_eq!(index, opening_results.len()); + Ok(()) + } + + #[benchmark(extra, pov_mode = Measured)] + fn predicate_mixed_read_state( + s: Linear<0, 4>, + ) -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let count = benchmark_predicate_capacity::(); + if count < 2 { + return Err(polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "host cannot represent mixed Step predicates", + )); + } + let (actor, precondition) = prepare_max_encoded_step_precondition::(count - 1, 0, false)?; + let mut predicates: Vec<_> = precondition + .clauses + .iter() + .flat_map(|clause| clause.iter()) + .cloned() + .collect(); + let (feeds, fresh) = prepare_observation_read_state::(1, s, 100)?; + predicates.push(TimedPredicate { + timing: ObservationTiming::Current, + predicate: Predicate::ObservationAbove { + feed: feeds[0], + threshold: 0, + max_age_blocks: 100, }, - 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), + let precondition = packed_predicate_clauses::( + predicates, + count.div_ceil(T::MaxPreconditionClauses::get()).max(1), ); - ActorHot::::mutate(actor_id, |maybe| { - maybe - .as_mut() - .expect("Opening-close hot state remains") - .queue_ticket = None; - }); + let expected = precondition.clauses.len() > 1 || fresh; + let result; #[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"); + result = Pallet::::evaluate_precondition(&precondition, &actor, Zero::zero()); + core::hint::black_box(&result); } - 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(), + assert_eq!(result, Ok(expected)); + Ok(()) + } + + #[benchmark(extra, pov_mode = Measured)] + fn predicate_cached_evaluation( + o: Linear<0, { benchmark_predicate_capacity::().max(1) }>, + ) -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let count = benchmark_predicate_capacity::(); + if o > T::MaxOpeningPredicateResults::get() { + return Err(polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "host cannot retain requested Opening results", + )); + } + let (actor, mut precondition) = prepare_max_encoded_step_precondition::(count, 0, false)?; + for timed in precondition + .clauses + .iter_mut() + .flat_map(|clause| clause.iter_mut()) + .take(o as usize) + { + timed.timing = ObservationTiming::Opening; + } + let opening_results: OpeningPredicateResultsOf = + vec![Ok(true); T::MaxOpeningPredicateResults::get() as usize] + .try_into() + .expect("maximum Opening result vector fits"); + let mut index = opening_results.len() - o as usize; + let result; + #[block] + { + result = Pallet::::evaluate_step_precondition( + Some(&precondition), + &actor, + Zero::zero(), + &opening_results, + &mut index, + ); + core::hint::black_box(&result); + } + assert_eq!(result, Ok(true)); + assert_eq!(index, opening_results.len()); + Ok(()) + } + + #[benchmark(pov_mode = Measured)] + fn opening_max_encoded_balance_capture( + p: Linear< 0, - ); + { + T::MaxOpeningPredicateResults::get() + .min(T::MaxContractSteps::get().saturating_mul(benchmark_predicate_capacity::())) + .max(1) + }, + >, + ) -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let maximum = T::MaxOpeningPredicateResults::get() + .min(T::MaxContractSteps::get().saturating_mul(benchmark_predicate_capacity::())); + if maximum == 0 { + return Err(polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "host cannot represent Opening predicates", + )); + } + let (actor, steps) = prepare_full_opening_predicate_capture::(p, 0, false)?; + let results; + #[block] + { + results = Pallet::::capture_opening_predicate_results(&actor, &steps, Zero::zero()); + core::hint::black_box(&results); + } + assert_eq!(results.len() as u32, p); + assert!(results.iter().all(|result| *result == Ok(true))); + Ok(()) } - /// 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() + fn opening_observation_heavy_capture( + o: Linear< + 1, + { + T::MaxOpeningPredicateResults::get() + .min(T::MaxContractSteps::get().saturating_mul(benchmark_predicate_capacity::())) + .saturating_sub(1) + .max(1) + }, + >, + ) -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let maximum = T::MaxOpeningPredicateResults::get() + .min(T::MaxContractSteps::get().saturating_mul(benchmark_predicate_capacity::())); + if maximum < 2 { + return Err(polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "host cannot represent mixed Opening predicates", + )); + } + let (actor, steps) = prepare_full_opening_predicate_capture::(1, o, false)?; + let results; + #[block] { - 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"); + results = Pallet::::capture_opening_predicate_results(&actor, &steps, Zero::zero()); + core::hint::black_box(&results); } - 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, + assert_eq!(results.len() as u32, o + 1); + assert!(results.iter().all(|result| *result == Ok(true))); + Ok(()) + } + + #[benchmark(extra, pov_mode = Measured)] + fn opening_predicate_composition( + c: Linear< + 0, + { + T::MaxOpeningPredicateResults::get() + .min(T::MaxContractSteps::get().saturating_mul(benchmark_predicate_capacity::())) + .saturating_add(1) + .saturating_pow(2) + .saturating_sub(1) }, - 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) + >, + ) -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let maximum = T::MaxOpeningPredicateResults::get() + .min(T::MaxContractSteps::get().saturating_mul(benchmark_predicate_capacity::())); + let total = (0..=maximum) + .find(|total| c < total.saturating_add(1).saturating_pow(2)) + .ok_or(polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "Opening composition index exceeds the host domain", + ))?; + let offset = c - total * total; + let include_native = offset > total; + let balances = if include_native { + offset - total - 1 + } else { + offset + }; + let observations = total - balances - u32::from(include_native); + let (actor, steps) = + prepare_full_opening_predicate_capture::(balances, observations, include_native)?; + let results; + #[block] + { + results = Pallet::::capture_opening_predicate_results(&actor, &steps, Zero::zero()); + core::hint::black_box(&results); + } + assert_eq!(results.len() as u32, total); + assert!(results.iter().all(|result| *result == Ok(true))); + Ok(()) + } + + #[benchmark(extra, pov_mode = Measured)] + fn opening_predicate_allocation( + b: Linear< + 0, { - 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"); + T::MaxOpeningPredicateResults::get() + .min(T::MaxContractSteps::get().saturating_mul(benchmark_predicate_capacity::())) + .max(1) + }, + >, + ) -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let maximum = T::MaxOpeningPredicateResults::get() + .min(T::MaxContractSteps::get().saturating_mul(benchmark_predicate_capacity::())); + if maximum == 0 { + return Err(polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "host cannot represent Opening predicates", + )); + } + let (actor, steps) = prepare_max_encoded_opening_predicates::(b, maximum - b, false)?; + let results; + #[block] + { + results = Pallet::::capture_opening_predicate_results(&actor, &steps, Zero::zero()); + core::hint::black_box(&results); + } + assert_eq!(results.len() as u32, maximum); + assert!(results.iter().all(|result| *result == Ok(true))); + Ok(()) + } + + #[benchmark(extra, pov_mode = Measured)] + fn opening_predicate_native_allocation( + b: Linear< + 0, + { + T::MaxOpeningPredicateResults::get() + .min(T::MaxContractSteps::get().saturating_mul(benchmark_predicate_capacity::())) + .saturating_sub(1) + .max(1) + }, + >, + ) -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let maximum = T::MaxOpeningPredicateResults::get() + .min(T::MaxContractSteps::get().saturating_mul(benchmark_predicate_capacity::())); + if maximum < 2 { + return Err(polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "host cannot represent mixed Opening predicates", + )); + } + let (actor, steps) = prepare_max_encoded_opening_predicates::(b, maximum - 1 - b, true)?; + let results; + #[block] + { + results = Pallet::::capture_opening_predicate_results(&actor, &steps, Zero::zero()); + core::hint::black_box(&results); + } + assert_eq!(results.len() as u32, maximum); + assert!(results.iter().all(|result| *result == Ok(true))); + Ok(()) + } + + // This pure-Opening corner maximizes Contract length compatible with the measured fragment, + // not the independent Opening/funding allocation frontier. + fn prepare_reachable_running_tail( + s: u32, + ) -> Result<(ActorId, ActorStepTicketOf), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let maximum = T::MaxContractSteps::get(); + let chunk = MAX_STEPS_PER_TAIL_CHUNK; + if s == 0 || s > chunk || maximum <= s { + return Err(polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "host cannot represent the requested tail fragment", + )); + } + let step_count = if s == chunk { + maximum + } else { + 1 + s + ((maximum - 1 - s) / chunk) * chunk }; - 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); + let cursor = if s == chunk { 1 } else { step_count - s }; + let actor_id = prepare_reachable_running::(step_count)?; + for expected_cursor in 1..cursor { + let run = ActorRunStateStore::::get(actor_id).expect("committed prefix retains Run"); + assert_eq!(run.cursor, expected_cursor); + frame_system::Pallet::::set_block_number(run.eligible_at); + Pallet::::execute_cycle(Weight::MAX); + } + let state = Pallet::::active_actor_state(actor_id).expect("real Running authority exists"); + let run = state + .run_state + .as_ref() + .expect("real Running payload exists"); + assert_eq!(state.hot.cycle_state, CycleState::Running); + assert_eq!(run.cursor, cursor); assert_eq!( - control_context.opening_snapshot_entries, - step_count.saturating_sub(1).saturating_mul(2), + run.opening_snapshot.len(), + Pallet::::opening_surfaces(&state.contract.steps, 0).len() ); assert_eq!( - control_context.opening_predicate_results, - step_count - .checked_mul(T::MaxPredicatesPerStep::get()) - .expect("Opening-completion predicate count fits"), + run.opening_predicate_results.len() as u32, + state + .contract + .steps + .iter() + .map(|step| step + .precondition + .as_ref() + .map_or(0, Precondition::opening_predicate_count)) + .sum::() ); - assert_eq!(control_context.predicate_evaluation_units, 8); + assert!(run.funding_snapshot.is_empty()); + frame_system::Pallet::::set_block_number(run.eligible_at); + let (location, cell) = + Pallet::::actor_control_cell(actor_id).expect("real Ready authority exists"); + let ActorControlLocation::Ready { ticket } = location else { + panic!("Q1 continuation must have a real Ready ticket"); + }; + let context = Pallet::::step_control_weight_context(step_count, cursor, 0, 0, 0, 0) + .expect("tail context exists"); + assert_eq!(context.steps_in_fragment, s); + let ticket = Pallet::::build_actor_step_ticket( + actor_id, + ticket, + run.eligible_at, + &state.identity, + &state.hot, + Some(run), + &cell.admission, + ) + .expect("real Ready authority builds a ticket"); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("real Running tail passes full premeasurement audit"); + Ok((actor_id, ticket)) + } + + fn assert_reachable_running_tail_unchanged(actor_id: ActorId, retained: &[u8]) { assert_eq!( - T::StepControlWeight::maximum_control_weight(control_context, &loaded_step.step), - Some(loaded_step.resources.control), + ActorRunStateStore::::get(actor_id) + .expect("read-only tail load retains Run") + .encode(), + retained ); - 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() - })); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("read-only tail load preserves full state audit"); } - /// 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(), + #[derive(Clone, Copy)] + enum RunningInnerBranch { + Complete, + Progress, + } + + fn prepare_reachable_running_inner( + s: u32, + p: u32, + branch: RunningInnerBranch, + ) -> Result<(ActorId, u32), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let maximum = T::MaxContractSteps::get(); + let chunk = MAX_STEPS_PER_TAIL_CHUNK; + let minimum = if matches!(branch, RunningInnerBranch::Complete) { + 1 + } else { + 2 + }; + if s < minimum || s > chunk || maximum <= s || p > benchmark_predicate_capacity::() { + return Err(polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "host cannot represent the requested Running inner branch", + )); + } + let step_count = if matches!(branch, RunningInnerBranch::Progress) && s == chunk { + maximum + } else { + 1 + s + ((maximum - 1 - s) / chunk) * chunk + }; + let cursor = if matches!(branch, RunningInnerBranch::Complete) { + step_count - 1 + } else if s == chunk { + 1 + } else { + step_count - s + }; + let predicate_owner: T::AccountId = account("running-current-predicate-assets", 0, 0); + let mut assets = T::BenchmarkHelper::setup_predicate_assets(&predicate_owner, p + 2) + .expect("current predicate assets exist"); + assert_eq!(assets.len() as u32, p + 2); + let zero_asset_index = assets + .iter() + .position(|asset| *asset != T::FeeNativeAssetId::get()) + .expect("host supplies a non-native zero-balance asset"); + let zero_asset = assets.remove(zero_asset_index); + assets.retain(|asset| *asset != T::FeeNativeAssetId::get()); + assets.truncate(p as usize); + assert_eq!(assets.len() as u32, p); + let precondition = if p == 0 { + None + } else { + Some(packed_predicate_clauses::( + assets + .into_iter() + .map(|asset| TimedPredicate { + timing: ObservationTiming::Current, + predicate: Predicate::BalanceAbove { + asset, + threshold: One::one(), + }, + }) + .collect(), + p.div_ceil(T::MaxPreconditionClauses::get()).max(1), + )) + }; + assert_eq!( + precondition + .as_ref() + .map_or(0, Precondition::evaluation_units), + p + ); + let task = match branch { + RunningInnerBranch::Complete => ActorTask::StopCycle, + RunningInnerBranch::Progress => ActorTask::Transfer { + to: account("running-zero-recipient", 0, 0), + asset: zero_asset, + amount: AmountResolution::PercentageOfCurrent(Perbill::from_percent(50)), + }, + }; + let actor_id = prepare_reachable_running_with_cooldown::( + ActorType::User, + step_count, + Some(( + cursor, + Step { + precondition, + task, + on_error: StepErrorPolicy::AbortCycle, }, - ]; + )), + 0, + )?; + for expected_cursor in 1..cursor { + let run = ActorRunStateStore::::get(actor_id).expect("committed prefix retains Run"); + assert_eq!(run.cursor, expected_cursor); + frame_system::Pallet::::set_block_number(run.eligible_at); + Pallet::::execute_cycle(Weight::MAX); + } + let state = Pallet::::active_actor_state(actor_id).expect("real Running state exists"); + assert!(T::AssetOps::balance(&state.identity.sovereign_account, zero_asset).is_zero()); + let run = state + .run_state + .as_ref() + .expect("real Running payload exists"); + assert_eq!(state.hot.cycle_state, CycleState::Running); + assert_eq!(run.cursor, cursor); + let opening_before_cursor: u32 = state.contract.steps[..cursor as usize] + .iter() + .map(|step| { + step + .precondition + .as_ref() + .map_or(0, Precondition::opening_predicate_count) + }) + .sum(); + assert_eq!(run.opening_predicate_cursor, opening_before_cursor); + assert_eq!( + run.opening_snapshot.len(), + Pallet::::opening_surfaces(&state.contract.steps, 0).len() + ); + assert_eq!( + run.opening_predicate_results.len() as u32, + state + .contract + .steps + .iter() + .map(|step| step + .precondition + .as_ref() + .map_or(0, Precondition::opening_predicate_count)) + .sum::() + ); + assert!(run.funding_snapshot.is_empty()); + assert!(state.funding.funding_tracked_assets.is_empty()); + assert!(state.funding.funding_accumulated.is_empty()); + frame_system::Pallet::::set_block_number(run.eligible_at); + assert!(matches!( + ActorControlLocators::::get(actor_id), + Some(ActorControlLocation::Ready { .. }) + )); + let context = Pallet::::step_control_weight_context(step_count, cursor, p, 0, 0, 0) + .expect("real tail context exists"); + assert_eq!(context.steps_in_fragment, s); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state() + .expect("real Running inner source passes full audit before consumption"); + Ok((actor_id, cursor)) + } + + fn execute_reachable_step_inner( + actor_id: ActorId, + state: ActiveActorStateOf, + admission: ActorAdmissionCertificateOf, + loaded_step: LoadedActorStepOf, + now: BlockNumberFor, + ) -> Weight { + let ticket = Pallet::::build_actor_step_ticket( + actor_id, + state + .hot + .queue_ticket + .expect("real consumed Ready ticket exists"), + now, + &state.identity, + &state.hot, + state.run_state.as_ref(), + &admission, + ) + .expect("real Running Step ticket builds"); + let maximum_fee = Pallet::::maximum_current_action_fee( + state.identity.actor_class.actor_type(), + &loaded_step.step, + loaded_step.resources, + ) + .expect("current Action fee matches production"); + let plan = Pallet::::build_current_step_plan( + actor_id, + state.identity.clone(), + state.hot.clone(), + state.run_state.clone(), + state.funding.clone(), + admission.clone(), + ticket, + loaded_step, + maximum_fee, + ) + .expect("real carried Running plan builds"); + polkadot_sdk::frame_support::storage::with_transaction(|| { + let result = + Pallet::::execute_current_step_and_place(actor_id, &state, plan, &admission, now); + match result { + Ok(evidence) => { + let effect_weight = evidence.actual_effect_weight; + core::hint::black_box(evidence); + polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok::< + Weight, + AttemptTransactionError, + >( + effect_weight + )) + } + Err(error) => { + polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)) + } + } + }) + .expect("real carried Step inner atom commits") + } + + fn assert_reachable_running_inner( + actor_id: ActorId, + cursor: u32, + now: BlockNumberFor, + branch: RunningInnerBranch, + ) { + let state = Pallet::::active_actor_state(actor_id).expect("real successor authority exists"); + assert_eq!(state.identity.actor_class.actor_type(), ActorType::User); + let hold = ActorStateHolds::::get(actor_id).expect("User state hold remains owned"); + assert_eq!(hold.owner, state.identity.owner); + match branch { + RunningInnerBranch::Complete => { + assert!(ActorRunStateStore::::get(actor_id).is_none()); + assert_eq!(state.identity.cycle_nonce, 1); + assert_eq!(state.hot.cycle_state, CycleState::Idle); + } + RunningInnerBranch::Progress => { + let run = state + .run_state + .as_ref() + .expect("real Running successor remains"); + assert_eq!(run.cursor, cursor + 1); + assert_eq!(run.last_committed_step_block, Some(now)); + let expected_skip = if state.contract.steps[cursor as usize].precondition.is_none() { + StepSkippedReason::ResolutionSkipped + } else { + StepSkippedReason::PreconditionFalse + }; + assert_eq!( + run.last_step_outcome, + Some(StepOutcome::Skipped(expected_skip)) + ); + let ActorTask::Transfer { asset, ref to, .. } = state.contract.steps[cursor as usize].task + else { + panic!("Progress target retains its authored Transfer"); + }; + assert!(T::AssetOps::balance(&state.identity.sovereign_account, asset).is_zero()); + assert!(T::AssetOps::balance(to, asset).is_zero()); + assert_eq!(benchmark_fixture_ready_occupancy::(), 1); + assert!(state.hot.queue_ticket.is_some()); + } + } + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("real Running inner result preserves full state audit"); + } + + fn prepare_reachable_running( + step_count: u32, + ) -> Result { + prepare_reachable_running_with_step::(step_count, None) + } + + fn make_reachable_opening_steps( + step_count: u32, + ) -> Result, polkadot_sdk::frame_benchmarking::BenchmarkError> { + let predicates_per_step = benchmark_predicate_capacity::(); + if step_count == 0 + || step_count > T::MaxContractSteps::get() + || step_count.saturating_mul(2) > T::MaxOpeningSnapshotEntries::get() + || step_count.saturating_mul(predicates_per_step) > T::MaxOpeningPredicateResults::get() + || predicates_per_step == 0 + || predicates_per_step != T::MaxPredicatesPerStep::get() + { + return Err(polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "host cannot represent the maximum Opening payload with a Running successor", + )); + } + let asset_owner: T::AccountId = account("reachable-state-assets", 0, 0); + let assets_per_step = 2 + predicates_per_step; + let assets = + T::BenchmarkHelper::setup_predicate_assets(&asset_owner, step_count * assets_per_step) + .expect("reachable Opening assets exist"); + assert_eq!(assets.len() as u32, step_count * assets_per_step); + assert_eq!( + assets + .iter() + .copied() + .collect::>() + .len(), + assets.len(), + "Opening amounts and predicates use independent asset identities" + ); + let mut steps = ContractSteps::::default(); + for pair in assets.chunks_exact(assets_per_step as usize) { + let predicates = (0..predicates_per_step) + .map(|index| TimedPredicate { + timing: ObservationTiming::Opening, + predicate: Predicate::BalanceAbove { + asset: pair[2 + index as usize], + threshold: index.saturating_add(1).saturated_into(), + }, + }) + .collect(); steps .try_push(Step { - precondition: Some(opening_all::(predicates)), + precondition: Some(packed_predicate_clauses::( + predicates, + T::MaxPredicatesPerClause::get(), + )), task: ActorTask::AddLiquidity { - asset_a, - asset_b, + asset_a: pair[0], + asset_b: pair[1], 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"); + .expect("reachable maximum 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, + Ok(steps) + } + + #[derive(Clone, Copy)] + enum ReachableOpeningProfile { + UserPaged, + UserCompleteMin, + UserCompleteHeaderMax, + Minimal, + Predicated, + RetryMin, + RetryMax, + CompleteMin, + CompleteMax, + FailedMin, + FailedMax, + } + + fn prepare_reachable_opening( + tail_chunks: u32, + profile: ReachableOpeningProfile, + ) -> Result<(ActorId, u32), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let maximum = T::MaxContractSteps::get(); + let max_tails = maximum.saturating_sub(1).div_ceil(MAX_STEPS_PER_TAIL_CHUNK); + let failed = matches!( + profile, + ReachableOpeningProfile::FailedMin | ReachableOpeningProfile::FailedMax + ); + let retry = matches!( + profile, + ReachableOpeningProfile::RetryMin | ReachableOpeningProfile::RetryMax + ); + let complete = matches!( + profile, + ReachableOpeningProfile::CompleteMin + | ReachableOpeningProfile::CompleteMax + | ReachableOpeningProfile::UserCompleteMin + | ReachableOpeningProfile::UserCompleteHeaderMax + ); + let terminal_predicates = matches!( + profile, + ReachableOpeningProfile::RetryMax + | ReachableOpeningProfile::CompleteMax + | ReachableOpeningProfile::FailedMax + ); + if (tail_chunks == 0 && !retry && !complete && !failed) + || tail_chunks > max_tails + || (retry && T::MaxRetryAttempts::get() < 2) + || (failed && T::MaxFundingTrackedAssets::get() == 0) + { + return Err(polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "host cannot represent the Opening progress tail profile", + )); + } + let count = (1 + tail_chunks * MAX_STEPS_PER_TAIL_CHUNK).min(maximum); + let mut steps = make_reachable_opening_steps::(count)?; + let owner: T::AccountId = account("reachable-opening-owner", 0, 0); + ensure_creation_balance::(&owner); + let missing_receipt = if failed { + let (position, _) = T::BenchmarkHelper::setup_stake(&owner).map_err(|_| { + polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "host cannot prepare an empty staking receipt", + ) + })?; + let receipt = T::StakingOps::share_asset(position).ok_or( + polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "host did not create an admissible staking receipt", + ), + )?; + // A host may alias position/receipt identity; neither may overlap any tail funding leg. + for step in steps.iter().skip(1) { + let ActorTask::AddLiquidity { + asset_a, asset_b, .. + } = step.task + else { + unreachable!() + }; + assert!( + asset_a != receipt && asset_b != receipt && asset_a != position && asset_b != position, + "staking position and receipt are distinct from tail assets" + ); + } + Some((position, receipt)) + } else { + None }; - 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"); + if terminal_predicates { + for step in steps.iter_mut() { + let ActorTask::AddLiquidity { + asset_a, asset_b, .. + } = step.task + else { + unreachable!() + }; + step.precondition = Some(packed_predicate_clauses::( + (0..benchmark_predicate_capacity::()) + .map(|index| TimedPredicate { + timing: ObservationTiming::Opening, + predicate: Predicate::BalanceBelow { + asset: if index % 2 == 0 { asset_a } else { asset_b }, + threshold: ::max_value() + .saturating_sub(index.saturated_into()), + }, + }) + .collect(), + T::MaxPredicatesPerClause::get(), + )); } - }); - 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 funding_count = + if matches!(profile, ReachableOpeningProfile::Predicated) || terminal_predicates { + 0 + } else { + (2 * (count - 1)).min(T::MaxFundingTrackedAssets::get().saturating_sub(u32::from(failed))) + }; + if !matches!(profile, ReachableOpeningProfile::Predicated) { + let ActorTask::AddLiquidity { + asset_a, asset_b, .. + } = steps[0].task + else { + unreachable!() + }; + let zero_asset = if asset_a != T::FeeNativeAssetId::get() { + asset_a + } else { + asset_b + }; + assert_ne!( + zero_asset, + T::FeeNativeAssetId::get(), + "zero resolution uses non-native custody" + ); + steps[0] = Step { + precondition: if terminal_predicates { + steps[0].precondition.clone() + } else { + None + }, + task: if let Some((position, _)) = missing_receipt { + ActorTask::Unstake { + asset: position, + shares: AmountResolution::PercentageOfLastFunding(Perbill::from_percent(50)), + } + } else if complete { + ActorTask::StopCycle + } else { + ActorTask::Transfer { + to: account("opening-zero-recipient", 0, 0), + asset: zero_asset, + amount: if retry { + AmountResolution::Fixed(One::one()) + } else if matches!(profile, ReachableOpeningProfile::UserPaged) { + AmountResolution::PercentageAtOpening(Perbill::from_percent(50)) + } else { + AmountResolution::PercentageOfCurrent(Perbill::from_percent(50)) + }, + } + }, + on_error: if retry { + StepErrorPolicy::RetryLater { + max_attempts: T::MaxRetryAttempts::get(), + } + } else { + StepErrorPolicy::AbortCycle + }, + }; + for (index, step) in steps.iter_mut().skip(1).enumerate() { + if terminal_predicates { + continue; + } + step.precondition = None; + let ActorTask::AddLiquidity { + amount_a, amount_b, .. + } = &mut step.task + else { + unreachable!() + }; + for (offset, amount) in [amount_a, amount_b].into_iter().enumerate() { + *amount = if ((2 * index + offset) as u32) < funding_count { + AmountResolution::PercentageOfLastFunding(Perbill::from_percent(50)) + } else { + AmountResolution::PercentageOfCurrent(Perbill::from_percent(50)) + }; + } + } + } + let actor_type = if matches!( + profile, + ReachableOpeningProfile::UserPaged + | ReachableOpeningProfile::UserCompleteMin + | ReachableOpeningProfile::UserCompleteHeaderMax + ) { + ActorType::User + } else { + ActorType::System }; - 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, + retain_admitted_contract_geometry::(actor_type, &mut steps, &[0])?; + let mut contract = user_contract::( + Schedule { + trigger: Trigger::Manual, + cooldown_blocks: if retry { 2 } else { 0 }, + }, + steps, ) - .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 Contract exists"); + if matches!(profile, ReachableOpeningProfile::UserCompleteHeaderMax) { + let maximum = T::MaxWhitelistSize::get(); + if maximum == 0 { + return Err(polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "host cannot represent a nonempty maximum funding allowlist", + )); + } + let mut sources = alloc::collections::BTreeSet::from([owner.clone()]); + for index in 0..maximum.saturating_sub(1) { + sources.insert(account("user-complete-funding-source", index, 0)); + } + if sources.len() as u32 != maximum { + return Err(polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "benchmark accounts do not fill the maximum funding allowlist", + )); + } + contract.funding = FundingSourcePolicy::SignedAllowlist( + sources + .try_into() + .expect("maximum funding allowlist obeys its configured bound"), + ); + } + let fee_reserve = full_attempt_fee::(&contract.steps) + .checked_add( + &Pallet::::trigger_fee_for_weight( + ActorType::User, + TriggerFamily::Manual, + T::WeightInfo::manual_trigger(), + ) + .trigger_fee, ) - .expect("Opening-progress Step ticket builds"); - let maximum_fee = Pallet::::maximum_current_step_fee( - state.identity.actor_class.actor_type(), - loaded_step.resources, + .expect("real User Opening reserve fits"); + if actor_type == ActorType::User { + prefund_active_user_creation::(&owner, &contract.steps); + Pallet::::create_user_actor( + RawOrigin::Signed(owner.clone()).into(), + Mutability::Mutable, + Some(contract), ) - .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("real User Opening Contract is admitted"); + } else { + Pallet::::create_system_actor( + RawOrigin::Root.into(), + owner.clone(), + Mutability::Mutable, + Some(contract), ) - .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"); + .expect("real System Opening Contract is admitted"); } - 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, + let actor_id = NextActorId::::get().saturating_sub(1); + let identity = Pallet::::actor_identity(actor_id).expect("real Opening identity exists"); + let funding_amount: T::Balance = 1_000_000_000_000u128.saturated_into(); + assert!(!funding_amount.is_zero()); + fund_reachable_assets_except::( + actor_id, + funding_amount, + missing_receipt.map(|(_, receipt)| receipt), ); + let funding = ActorFunding::::get(actor_id).expect("real Opening funding exists"); assert_eq!( - run.opening_predicate_results.len(), - step_count - .checked_mul(T::MaxPredicatesPerStep::get()) - .expect("Opening-progress predicate count fits") as usize, + funding.funding_tracked_assets.len() as u32, + funding_count + u32::from(failed) ); - 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)); + assert_eq!(funding.funding_accumulated.len() as u32, funding_count); + if actor_type == ActorType::User { + T::AssetOps::mint( + &identity.sovereign_account, + T::FeeNativeAssetId::get(), + fee_reserve, + ) + .expect("User Trigger and Pipeline have independent funding"); } - 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(), + frame_system::Pallet::::set_block_number(1u32.into()); + GlobalCircuitBreaker::::put(false); + Pallet::::manual_trigger(RawOrigin::Signed(owner.clone()).into(), actor_id) + .expect("real Manual occurrence publishes Opening readiness"); + let state = Pallet::::active_actor_state(actor_id).expect("real Idle authority exists"); + if let ActorTask::Transfer { asset, .. } = state.contract.steps[0].task { + assert!(T::AssetOps::balance(&state.identity.sovereign_account, asset).is_zero()); + } + assert_eq!(state.hot.cycle_state, CycleState::Idle); + assert!(state.hot.pending_signal && state.run_state.is_none()); + assert!(matches!( + ActorControlLocators::::get(actor_id), + Some(ActorControlLocation::Ready { .. }) + )); + let (_, cell) = Pallet::::actor_control_cell(actor_id).expect("Idle Ready cell exists"); + frame_system::Pallet::::set_block_number( + cell.eligible_at.expect("Opening eligibility exists"), ); - 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)) - } - } + let surfaces = Pallet::::opening_surfaces(&state.contract.steps, 0); + assert!(surfaces.len() as u32 <= T::MaxOpeningSnapshotEntries::get()); + assert_eq!(state.contract.steps.len() as u32, count); + let opening_results: u32 = state + .contract + .steps + .iter() + .map(|step| { + step + .precondition + .as_ref() + .map_or(0, Precondition::opening_predicate_count) }) - .expect("Running benchmark inner atom commits"); - } - assert!(ActorRunStateStore::::get(actor_id).is_none()); + .sum(); + assert!(opening_results <= T::MaxOpeningPredicateResults::get()); assert_eq!( - ActorIdentities::::get(actor_id) - .expect("Running benchmark identity remains") - .cycle_nonce, - 1, + count.saturating_sub(1).div_ceil(MAX_STEPS_PER_TAIL_CHUNK), + tail_chunks ); - } - - /// 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. + let predicate_units = state.contract.steps[0] + .precondition + .as_ref() + .map_or(0, Precondition::evaluation_units); + assert_eq!( + predicate_units, + if matches!(profile, ReachableOpeningProfile::Predicated) || terminal_predicates { + 2 * benchmark_predicate_capacity::() + } else { + 0 + } + ); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("real Idle Opening source passes full audit"); + if let Some((position, receipt)) = missing_receipt { + assert!(T::AssetOps::balance(&identity.sovereign_account, receipt).is_zero()); + T::BenchmarkHelper::remove_empty_staking_receipt(&owner, position).map_err(|_| { + polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "host cannot remove the empty admitted staking receipt", + ) + })?; + assert!(T::StakingOps::share_asset(position).is_none()); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state() + .expect("host receipt removal preserves Actor authority invariants"); + } + Ok((actor_id, count)) + } + + fn assert_reachable_opening( + actor_id: ActorId, + count: u32, + profile: ReachableOpeningProfile, + ) { + if matches!( + profile, + ReachableOpeningProfile::RetryMin + | ReachableOpeningProfile::RetryMax + | ReachableOpeningProfile::CompleteMin + | ReachableOpeningProfile::CompleteMax + | ReachableOpeningProfile::UserCompleteMin + | ReachableOpeningProfile::UserCompleteHeaderMax + | ReachableOpeningProfile::FailedMin + | ReachableOpeningProfile::FailedMax + ) { + assert_reachable_terminal_opening::(actor_id, count, profile); + return; + } + let state = + Pallet::::active_actor_state(actor_id).expect("Opening publishes real Running state"); + assert_eq!(state.hot.cycle_state, CycleState::Running); + let run = state.run_state.as_ref().expect("real Opening Run exists"); + assert_eq!(run.cursor, 1); + assert_eq!( + run.last_committed_step_block, + Some(frame_system::Pallet::::block_number()) + ); + let opening = Pallet::::opening_surfaces(&state.contract.steps, 0).len() as u32; + let results: u32 = state + .contract + .steps + .iter() + .map(|step| { + step + .precondition + .as_ref() + .map_or(0, Precondition::opening_predicate_count) + }) + .sum(); + let funding = state.funding.funding_tracked_assets.len() as u32; + let skip = match profile { + ReachableOpeningProfile::Predicated => StepSkippedReason::PreconditionFalse, + ReachableOpeningProfile::Minimal | ReachableOpeningProfile::UserPaged => { + StepSkippedReason::ResolutionSkipped + } + _ => unreachable!("terminal Opening profiles are checked separately"), + }; + assert_eq!(run.opening_snapshot.len() as u32, opening); + assert_eq!(run.opening_predicate_results.len() as u32, results); + assert_eq!(run.funding_snapshot.len() as u32, funding); + let surfaces = Pallet::::opening_surfaces(&state.contract.steps, 0); + assert!( + surfaces + .iter() + .all(|surface| run.opening_snapshot.contains_key(surface)) + ); + assert!( + run + .opening_predicate_results + .iter() + .all(|result| *result == Ok(false)) + ); + assert!( + run + .funding_snapshot + .values() + .all(|amount| *amount == 1_000_000_000_000u128.saturated_into()) + ); + assert!(state.funding.funding_accumulated.is_empty()); + assert!( + run + .funding_snapshot + .keys() + .all(|asset| state.funding.funding_tracked_assets.contains(asset)) + ); + assert_eq!(run.last_step_outcome, Some(StepOutcome::Skipped(skip))); + assert_eq!(benchmark_fixture_ready_occupancy::(), 1); + assert!(state.hot.queue_ticket.is_some()); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("real Opening successor passes full audit"); + } + + fn assert_reachable_terminal_opening( + actor_id: ActorId, + _count: u32, + profile: ReachableOpeningProfile, + ) { + let state = Pallet::::active_actor_state(actor_id) + .expect("terminal/retry Opening retains Actor authority"); + let failed = matches!( + profile, + ReachableOpeningProfile::FailedMin | ReachableOpeningProfile::FailedMax + ); + let funding = state + .funding + .funding_tracked_assets + .len() + .saturating_sub(usize::from(failed)) as u32; + assert_eq!( + state.funding.funding_tracked_assets.len() as u32, + funding + u32::from(failed) + ); + assert!(state.funding.funding_accumulated.is_empty()); + assert!(!state.hot.pending_signal); + assert_eq!(benchmark_fixture_ready_occupancy::(), 0); + let now = frame_system::Pallet::::block_number(); + if failed { + assert_eq!(state.hot.cycle_state, CycleState::Idle); + assert_eq!(state.identity.cycle_nonce, 1); + assert_eq!(state.hot.unsuccessful_attempt_streak, 1); + assert!(state.run_state.is_none()); + assert!(state.hot.queue_ticket.is_none() && state.hot.wakeup_pointer.is_none()); + assert!(matches!( + ActorControlLocators::::get(actor_id), + Some(ActorControlLocation::Unsignaled) + )); + let failure: ::RuntimeEvent = Event::::StepFailed { + actor_id, + cycle_nonce: 1, + step_index: 0, + retry_class: RetryClass::Permanent, + error: Error::::InvalidAmountResolution.into(), + } + .into(); + assert!( + frame_system::Pallet::::events() + .iter() + .any(|record| record.event == failure) + ); + let ActorTask::Unstake { asset, .. } = state.contract.steps[0].task else { + unreachable!() + }; + assert!(T::StakingOps::share_asset(asset).is_none()); + assert!(T::StakingOps::share_balance(&state.identity.sovereign_account, asset).is_zero()); + } else if matches!( + profile, + ReachableOpeningProfile::CompleteMin + | ReachableOpeningProfile::CompleteMax + | ReachableOpeningProfile::UserCompleteMin + | ReachableOpeningProfile::UserCompleteHeaderMax + ) { + assert_eq!(state.hot.cycle_state, CycleState::Idle); + assert_eq!(state.identity.cycle_nonce, 1); + assert_eq!(state.hot.unsuccessful_attempt_streak, 0); + let stopped: ::RuntimeEvent = Event::::CycleStopped { + actor_id, + cycle_nonce: 1, + step_index: 0, + } + .into(); + assert!( + frame_system::Pallet::::events() + .iter() + .any(|record| record.event == stopped) + ); + let receipt: ::RuntimeEvent = Event::::ActionFeeCharged { + actor_id, + cycle_nonce: 1, + step_index: 0, + actual_effect_weight: T::TaskEffectWeight::actual_effect_weight( + &ActorTask::StopCycle, + TaskEffectExecution::Invoked, + ) + .expect("StopCycle has host effect evidence"), + fee: Zero::zero(), + } + .into(); + assert_eq!( + frame_system::Pallet::::events() + .iter() + .filter(|record| record.event == receipt) + .count(), + usize::from(matches!(profile, ReachableOpeningProfile::CompleteMax)), + "only the predicated completion profile includes invocation-receipt deposition" + ); + assert!(state.run_state.is_none()); + assert!(state.hot.queue_ticket.is_none() && state.hot.wakeup_pointer.is_none()); + assert!(matches!( + ActorControlLocators::::get(actor_id), + Some(ActorControlLocation::Unsignaled) + )); + } else { + assert_eq!(state.hot.cycle_state, CycleState::Suspended); + let run = state.run_state.as_ref().expect("real retry Run persists"); + assert_eq!(run.cursor, 0); + assert_eq!(run.unsuccessful_attempts_at_cursor, 1); + assert_eq!(run.last_attempt_block, now); + assert_eq!(run.last_step_outcome, Some(StepOutcome::FundingUnavailable)); + assert_eq!(run.suspension, Some(SuspensionReason::FundingUnavailable)); + let due = Pallet::::suspension_eligible_at(2, None, now, 1) + .expect("retry eligibility is representable"); + assert_eq!(run.eligible_at, due); + assert!( + matches!(ActorControlLocators::::get(actor_id), Some(ActorControlLocation::Waiting { key: WakeupKey::Block(at), .. }) if at == due) + ); + assert_eq!( + run.opening_snapshot.len(), + Pallet::::opening_surfaces(&state.contract.steps, 0).len() + ); + assert_eq!( + run.opening_predicate_results.len() as u32, + state + .contract + .steps + .iter() + .map(|step| step + .precondition + .as_ref() + .map_or(0, Precondition::opening_predicate_count)) + .sum::() + ); + assert!( + run + .opening_predicate_results + .iter() + .all(|result| *result == Ok(true)) + ); + assert_eq!(run.funding_snapshot.len() as u32, funding); + assert!( + run + .funding_snapshot + .keys() + .all(|asset| state.funding.funding_tracked_assets.contains(asset)) + ); + assert!( + run + .funding_snapshot + .values() + .all(|amount| *amount == 1_000_000_000_000u128.saturated_into()) + ); + let ActorTask::Transfer { asset, ref to, .. } = state.contract.steps[0].task else { + unreachable!() + }; + assert!(T::AssetOps::balance(&state.identity.sovereign_account, asset).is_zero()); + assert!(T::AssetOps::balance(to, asset).is_zero()); + } + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("real retry/completion preserves full state audit"); + } + + struct ReachableSuspendedSkip { + actor_id: ActorId, + cursor: u32, + ready_others: u32, + owner: T::AccountId, + frozen_asset: T::AssetId, + balances: [(T::AssetId, T::Balance); 2], + fixture_funding: [(T::AssetId, T::Balance, T::Balance); 2], + } + + // A terminal head has no tail. For the other branches, `n` allocates the tail's + // two-leg amount sources between Opening and LastFunding; it is not a free Run axis. + fn prepare_reachable_suspended_head_inner( + n: u32, + current: u32, + opening: u32, + branch: Option, + ) -> Result< + (ActorId, Option>), + polkadot_sdk::frame_benchmarking::BenchmarkError, + > { + let complete = matches!(branch, Some(RunningInnerBranch::Complete)); + let count = if complete { + 1 + } else { + T::MaxContractSteps::get() + }; + let capacity = benchmark_predicate_capacity::(); + let tail_legs = count.saturating_sub(1) * 2; + if count == 0 + || (!complete && count < 2) + || n > tail_legs + || tail_legs - n > T::MaxFundingTrackedAssets::get() + || current + opening > capacity + || T::MaxRetryAttempts::get() < 3 + { + return Err(polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "host cannot represent the coupled Suspended-head profile", + )); + } + frame_system::Pallet::::set_block_number(1u32.into()); + GlobalCircuitBreaker::::put(false); + let owner: T::AccountId = account("suspended-head-owner", 0, 0); + ensure_creation_balance::(&owner); + let liquidity = if branch.is_some() { + Some( + T::BenchmarkHelper::setup_add_liquidity(&owner).map_err(|_| { + polkadot_sdk::frame_benchmarking::BenchmarkError::Stop("host liquidity setup unsupported") + })?, + ) + } else { + None + }; + let mut steps = make_reachable_opening_steps::(count)?; + for (index, step) in steps.iter_mut().skip(1).enumerate() { + let ActorTask::AddLiquidity { + amount_a, amount_b, .. + } = &mut step.task + else { + unreachable!() + }; + for (offset, amount) in [amount_a, amount_b].into_iter().enumerate() { + if (index * 2 + offset) as u32 >= n { + *amount = AmountResolution::PercentageOfLastFunding(Perbill::one()); + } + } + } + let auxiliary_start = count * (2 + capacity); + let assets = T::BenchmarkHelper::setup_predicate_assets(&owner, auxiliary_start + capacity + 4) + .map_err(|_| { + polkadot_sdk::frame_benchmarking::BenchmarkError::Stop("host predicate setup unsupported") + })?; + let auxiliary = assets + .into_iter() + .skip(auxiliary_start as usize) + .filter(|asset| { + *asset != T::FeeNativeAssetId::get() + && liquidity.is_none_or(|(a, b, _, _)| *asset != a && *asset != b) + }) + .take((1 + current + opening) as usize) + .collect::>(); + assert_eq!(auxiliary.len() as u32, 1 + current + opening); + steps[0] = Step { + precondition: (current + opening > 0).then(|| { + packed_predicate_clauses::( + auxiliary + .iter() + .skip(1) + .enumerate() + .map(|(index, asset)| TimedPredicate { + timing: if (index as u32) < opening { + ObservationTiming::Opening + } else { + ObservationTiming::Current + }, + predicate: Predicate::BalanceBelow { + asset: *asset, + threshold: One::one(), + }, + }) + .collect(), + T::MaxPredicatesPerClause::get(), + ) + }), + task: if let Some((asset_a, asset_b, _, _)) = liquidity { + ActorTask::AddLiquidity { + asset_a, + asset_b, + amount_a: AmountResolution::PercentageOfCurrent(Perbill::from_percent(50)), + amount_b: AmountResolution::PercentageOfCurrent(Perbill::from_percent(50)), + min_lp_out: ::max_value(), + } + } else { + ActorTask::Transfer { + to: account("suspended-head-recipient", 0, 0), + asset: auxiliary[0], + amount: AmountResolution::Fixed(One::one()), + } + }, + on_error: StepErrorPolicy::RetryLater { + max_attempts: T::MaxRetryAttempts::get(), + }, + }; + retain_admitted_contract_geometry::(ActorType::System, &mut steps, &[0])?; + let mut contract = system_contract::( + Schedule { + trigger: Trigger::Manual, + cooldown_blocks: 2, + }, + steps, + ) + .expect("active head Contract exists"); + contract.funding = FundingSourcePolicy::OwnerOnly; + Pallet::::create_system_actor( + RawOrigin::Root.into(), + owner.clone(), + Mutability::Mutable, + Some(contract), + ) + .expect("coupled head Contract is admitted"); + let actor_id = NextActorId::::get().saturating_sub(1); + let actor = Pallet::::actor_identity(actor_id) + .expect("head identity exists") + .sovereign_account; + fund_reachable_update_assets::(actor_id, 10u32.into()); + assert!( + auxiliary + .iter() + .all(|asset| T::AssetOps::balance(&actor, *asset).is_zero()) + ); + let mut skip = if let Some((asset_a, asset_b, amount_a, amount_b)) = liquidity { + let frozen_asset = [asset_a, asset_b] + .into_iter() + .find(|asset| *asset != T::FeeNativeAssetId::get()) + .expect("liquidity has a non-native leg"); + let fixture_funding = [ + (asset_a, T::AssetOps::balance(&actor, asset_a), amount_a), + (asset_b, T::AssetOps::balance(&actor, asset_b), amount_b), + ]; + T::AssetOps::mint(&actor, asset_a, amount_a).expect("head first liquidity leg funded"); + T::AssetOps::mint(&actor, asset_b, amount_b).expect("head second liquidity leg funded"); + T::BenchmarkHelper::set_asset_account_frozen(&owner, &actor, frozen_asset, false).map_err( + |_| polkadot_sdk::frame_benchmarking::BenchmarkError::Stop("host account thaw unsupported"), + )?; + Some(ReachableSuspendedSkip { + actor_id, + cursor: 0, + ready_others: 0, + owner: owner.clone(), + frozen_asset, + balances: [ + (asset_a, T::AssetOps::balance(&actor, asset_a)), + (asset_b, T::AssetOps::balance(&actor, asset_b)), + ], + fixture_funding, + }) + } else { + None + }; + Pallet::::manual_trigger(RawOrigin::Signed(owner).into(), actor_id) + .expect("real head occurrence admits Opening"); + Pallet::::execute_cycle(Weight::MAX); + let state = Pallet::::active_actor_state(actor_id).expect("Opening retains Actor"); + let run = state.run_state.expect("real failure retains Run"); + assert_eq!(state.hot.cycle_state, CycleState::Suspended); + assert_eq!(run.cursor, 0); + assert_eq!(run.unsuccessful_attempts_at_cursor, 1); + assert_eq!(run.last_committed_step_block, None); + assert_eq!( + run.opening_snapshot.len(), + Pallet::::opening_surfaces(&state.contract.steps, 0).len() + ); + assert_eq!( + run.funding_snapshot.len(), + state.funding.funding_tracked_assets.len() + ); + assert_eq!( + run.opening_predicate_results.len() as u32, + state + .contract + .steps + .iter() + .map(|step| step + .precondition + .as_ref() + .map_or(0, Precondition::opening_predicate_count)) + .sum::() + ); + assert!( + run + .opening_predicate_results + .iter() + .take(opening as usize) + .all(|result| *result == Ok(true)) + ); + assert!(state.funding.funding_accumulated.is_empty()); + if let Some(fixture) = &skip { + assert_eq!(run.suspension, Some(SuspensionReason::Temporary)); + T::BenchmarkHelper::set_asset_account_frozen( + &fixture.owner, + &actor, + fixture.frozen_asset, + true, + ) + .map_err(|_| { + polkadot_sdk::frame_benchmarking::BenchmarkError::Stop("host account freeze unsupported") + })?; + assert!(T::AssetOps::balance(&actor, fixture.frozen_asset).is_zero()); + } else { + assert_eq!(run.last_step_outcome, Some(StepOutcome::FundingUnavailable)); + } + frame_system::Pallet::::set_block_number(run.eligible_at); + let mut meter = polkadot_sdk::sp_weights::WeightMeter::with_limit(Weight::MAX); + Pallet::::drain_overdue_wakeups_cursor(run.eligible_at, &mut meter); + assert!(matches!( + ActorControlLocators::::get(actor_id), + Some(ActorControlLocation::Ready { .. }) + )); + if let Some(fixture) = &mut skip { + // Host asset bootstrap may create other due Actors. Preserve their real topology. + fixture.ready_others = benchmark_fixture_ready_occupancy::() + .checked_sub(1) + .expect("target owns one live Ready slot"); + } + assert_eq!( + ActorRunStateStore::::get(actor_id) + .expect("due Run persists") + .encode(), + run.encode() + ); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("real Suspended-head source passes full audit"); + Ok((actor_id, skip)) + } + + fn assert_reachable_suspended_head_retry( + actor_id: ActorId, + before: ActorRunStateOf, + ) { + let after = ActorRunStateStore::::get(actor_id).expect("retry retains Run"); + assert_eq!(after.cursor, 0); + assert_eq!(after.unsuccessful_attempts_at_cursor, 2); + assert_eq!(after.last_attempt_block, before.eligible_at); + assert_eq!(after.last_committed_step_block, None); + assert_eq!( + after.last_step_outcome, + Some(StepOutcome::FundingUnavailable) + ); + assert_eq!(after.opening_snapshot, before.opening_snapshot); + assert_eq!( + after.opening_predicate_results, + before.opening_predicate_results + ); + assert_eq!(after.funding_snapshot, before.funding_snapshot); + assert!( + matches!(ActorControlLocators::::get(actor_id), Some(ActorControlLocation::Waiting { key: WakeupKey::Block(at), .. }) if at == after.eligible_at) + ); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("head retry successor passes full audit"); + } + + #[cfg(test)] + fn prepare_reachable_suspended_tail_skip( + s: u32, + p: u32, + branch: RunningInnerBranch, + ) -> Result, polkadot_sdk::frame_benchmarking::BenchmarkError> { + let maximum = T::MaxContractSteps::get(); + let chunk = MAX_STEPS_PER_TAIL_CHUNK; + let complete = matches!(branch, RunningInnerBranch::Complete); + if s < if complete { 1 } else { 2 } + || s > chunk + || maximum <= s + || p > benchmark_predicate_capacity::() + || T::MaxRetryAttempts::get() < 2 + { + return Err(polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "host cannot represent the Suspended skip tail", + )); + } + let count = if !complete && s == chunk { + maximum + } else { + 1 + s + ((maximum - 1 - s) / chunk) * chunk + }; + let cursor = if complete { + count - 1 + } else if s == chunk { + 1 + } else { + count - s + }; + let owner: T::AccountId = account("suspended-skip-assets", 0, 0); + let (asset_a, asset_b, amount_a, amount_b) = T::BenchmarkHelper::setup_add_liquidity(&owner) + .map_err(|_| { + polkadot_sdk::frame_benchmarking::BenchmarkError::Stop("host liquidity setup unsupported") + })?; + let frozen_asset = [asset_a, asset_b] + .into_iter() + .find(|asset| *asset != T::FeeNativeAssetId::get()) + .ok_or(polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "host needs a freezable non-native liquidity leg", + ))?; + let assets = T::BenchmarkHelper::setup_predicate_assets(&owner, p + 3).map_err(|_| { + polkadot_sdk::frame_benchmarking::BenchmarkError::Stop("host predicate setup unsupported") + })?; + let assets = assets + .into_iter() + .filter(|asset| { + *asset != asset_a && *asset != asset_b && *asset != T::FeeNativeAssetId::get() + }) + .take(p as usize) + .collect::>(); + if assets.len() != p as usize { + return Err(polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "host needs distinct unfunded predicate assets", + )); + } + let precondition = if p == 0 { + None + } else { + Some(packed_predicate_clauses::( + assets + .iter() + .map(|asset| TimedPredicate { + timing: ObservationTiming::Current, + predicate: Predicate::BalanceBelow { + asset: *asset, + threshold: One::one(), + }, + }) + .collect(), + p.div_ceil(T::MaxPreconditionClauses::get()).max(1), + )) + }; + let actor_id = prepare_reachable_running_with_cooldown::( + ActorType::System, + count, + Some(( + cursor, + Step { + precondition, + task: ActorTask::AddLiquidity { + asset_a, + asset_b, + amount_a: AmountResolution::PercentageOfCurrent(Perbill::from_parts(1)), + amount_b: AmountResolution::PercentageOfCurrent(Perbill::from_parts(1)), + min_lp_out: ::max_value(), + }, + on_error: StepErrorPolicy::RetryLater { + max_attempts: T::MaxRetryAttempts::get(), + }, + }, + )), + 2, + )?; + let sovereign = Pallet::::actor_identity(actor_id) + .expect("admitted identity exists") + .sovereign_account; + // Funding occurs after Opening, so previously captured false predicates stay authoritative. + let fixture_funding = [ + (asset_a, T::AssetOps::balance(&sovereign, asset_a), amount_a), + (asset_b, T::AssetOps::balance(&sovereign, asset_b), amount_b), + ]; + T::AssetOps::mint(&sovereign, asset_a, amount_a).expect("first real liquidity leg funded"); + T::AssetOps::mint(&sovereign, asset_b, amount_b).expect("second real liquidity leg funded"); + T::BenchmarkHelper::set_asset_account_frozen(&owner, &sovereign, frozen_asset, false).map_err( + |_| { + polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "host liquidity account thaw unsupported", + ) + }, + )?; + let balances = [ + (asset_a, T::AssetOps::balance(&sovereign, asset_a)), + (asset_b, T::AssetOps::balance(&sovereign, asset_b)), + ]; + assert!(balances.iter().all(|(_, balance)| *balance > One::one())); + assert!( + assets + .iter() + .all(|asset| T::AssetOps::balance(&sovereign, *asset).is_zero()) + ); + for expected_cursor in 1..cursor { + let run = ActorRunStateStore::::get(actor_id).expect("real prefix Run exists"); + assert_eq!(run.cursor, expected_cursor); + frame_system::Pallet::::set_block_number(run.eligible_at); + Pallet::::execute_cycle(Weight::MAX); + } + let run = ActorRunStateStore::::get(actor_id).expect("real target Run exists"); + assert_eq!(run.cursor, cursor); + frame_system::Pallet::::set_block_number(run.eligible_at); + Pallet::::execute_cycle(Weight::MAX); + let state = + Pallet::::active_actor_state(actor_id).expect("real Temporary retry remains active"); + let run = state + .run_state + .as_ref() + .expect("Temporary retry retains Run"); + assert_eq!(state.hot.cycle_state, CycleState::Suspended); + assert_eq!(run.cursor, cursor); + assert_eq!(run.unsuccessful_attempts_at_cursor, 1); + assert_eq!(run.suspension, Some(SuspensionReason::Temporary)); + assert!(matches!( + run.last_step_outcome, + Some(StepOutcome::Failed(TaskFailure { + retry: RetryClass::Temporary, + .. + })) + )); + assert!( + matches!(ActorControlLocators::::get(actor_id), Some(ActorControlLocation::Waiting { key: WakeupKey::Block(at), .. }) if at == run.eligible_at) + ); + assert_eq!( + run.opening_snapshot.len(), + Pallet::::opening_surfaces(&state.contract.steps, 0).len() + ); + assert_eq!( + run.opening_predicate_results.len() as u32, + state + .contract + .steps + .iter() + .map(|step| step + .precondition + .as_ref() + .map_or(0, Precondition::opening_predicate_count)) + .sum::() + ); + assert_eq!( + run.opening_predicate_cursor, + state.contract.steps[..cursor as usize] + .iter() + .map(|step| step + .precondition + .as_ref() + .map_or(0, Precondition::opening_predicate_count)) + .sum::() + ); + assert!( + run + .opening_predicate_results + .iter() + .all(|result| *result == Ok(false)) + ); + assert!( + run.funding_snapshot.is_empty() + && state.funding.funding_tracked_assets.is_empty() + && state.funding.funding_accumulated.is_empty() + ); + for (asset, balance) in balances { + assert_eq!(T::AssetOps::balance(&sovereign, asset), balance); + } + let context = Pallet::::step_control_weight_context(count, cursor, p, 0, 0, 0) + .expect("tail context exists"); + assert_eq!(context.steps_in_fragment, s); + assert_eq!(context.predicate_evaluation_units, p); + let retained = run.encode(); + T::BenchmarkHelper::set_asset_account_frozen(&owner, &sovereign, frozen_asset, true).map_err( + |_| { + polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "host liquidity account freeze unsupported", + ) + }, + )?; + assert!(T::AssetOps::balance(&sovereign, frozen_asset).is_zero()); + frame_system::Pallet::::set_block_number(run.eligible_at); + let mut meter = polkadot_sdk::sp_weights::WeightMeter::with_limit(Weight::MAX); + assert_eq!( + Pallet::::drain_overdue_wakeups_cursor(run.eligible_at, &mut meter).ready_entries, + 1 + ); + assert_eq!( + ActorRunStateStore::::get(actor_id) + .expect("due Run retained") + .encode(), + retained + ); + assert!(matches!( + ActorControlLocators::::get(actor_id), + Some(ActorControlLocation::Ready { .. }) + )); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("real frozen due source passes full audit"); + Ok(ReachableSuspendedSkip { + actor_id, + cursor, + ready_others: benchmark_fixture_ready_occupancy::() + .checked_sub(1) + .expect("target owns one live Ready slot"), + owner, + frozen_asset, + balances, + fixture_funding, + }) + } + + struct ReachableSuspendedSuccess { + actor_id: ActorId, + cursor: u32, + ready_others: u32, + asset: T::AssetId, + amount: T::Balance, + recipient: T::AccountId, + } + + fn prepare_reachable_suspended_tail_success( + s: u32, + p: u32, + branch: RunningInnerBranch, + ) -> Result, polkadot_sdk::frame_benchmarking::BenchmarkError> { + let maximum = T::MaxContractSteps::get(); + let chunk = MAX_STEPS_PER_TAIL_CHUNK; + let complete = matches!(branch, RunningInnerBranch::Complete); + if s < if complete { 1 } else { 2 } + || s > chunk + || maximum <= s + || p > benchmark_predicate_capacity::() + || T::MaxRetryAttempts::get() < 2 + { + return Err(polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "host cannot represent the successful Suspended tail", + )); + } + let count = if !complete && s == chunk { + maximum + } else { + 1 + s + ((maximum - 1 - s) / chunk) * chunk + }; + let cursor = if complete { + count - 1 + } else if s == chunk { + 1 + } else { + count - s + }; + let owner: T::AccountId = account("suspended-success-assets", 0, 0); + let mut assets = T::BenchmarkHelper::setup_predicate_assets(&owner, p + 2).map_err(|_| { + polkadot_sdk::frame_benchmarking::BenchmarkError::Stop("host predicate setup unsupported") + })?; + let position = assets + .iter() + .position(|asset| *asset != T::FeeNativeAssetId::get()) + .ok_or(polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "successful Suspended tail needs a non-native asset", + ))?; + let asset = assets.remove(position); + assets.retain(|candidate| *candidate != asset); + assets.truncate(p as usize); + if assets.len() != p as usize { + return Err(polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "host needs distinct current-predicate assets", + )); + } + let precondition = (p > 0).then(|| { + packed_predicate_clauses::( + assets + .into_iter() + .map(|candidate| TimedPredicate { + timing: ObservationTiming::Current, + predicate: Predicate::BalanceBelow { + asset: candidate, + threshold: One::one(), + }, + }) + .collect(), + p.div_ceil(T::MaxPreconditionClauses::get()).max(1), + ) + }); + let recipient: T::AccountId = account("suspended-success-recipient", 0, 0); + let amount = T::AssetOps::minimum_balance(asset).max(One::one()); + let actor_id = prepare_reachable_running_with_cooldown::( + ActorType::System, + count, + Some(( + cursor, + Step { + precondition, + task: ActorTask::Transfer { + to: recipient.clone(), + asset, + amount: AmountResolution::Fixed(amount), + }, + on_error: StepErrorPolicy::RetryLater { + max_attempts: T::MaxRetryAttempts::get(), + }, + }, + )), + 2, + )?; + for expected_cursor in 1..cursor { + let run = ActorRunStateStore::::get(actor_id).expect("real prefix Run exists"); + assert_eq!(run.cursor, expected_cursor); + frame_system::Pallet::::set_block_number(run.eligible_at); + Pallet::::execute_cycle(Weight::MAX); + } + let run = ActorRunStateStore::::get(actor_id).expect("target Run exists"); + frame_system::Pallet::::set_block_number(run.eligible_at); + Pallet::::execute_cycle(Weight::MAX); + assert_reachable_suspended_tail_retry_state::(actor_id, cursor, 1); + let run = ActorRunStateStore::::get(actor_id).expect("suspension persists"); + frame_system::Pallet::::set_block_number(run.eligible_at); + let mut meter = polkadot_sdk::sp_weights::WeightMeter::with_limit(Weight::MAX); + assert!( + Pallet::::drain_overdue_wakeups_cursor(run.eligible_at, &mut meter).ready_entries >= 1 + ); + let sovereign = Pallet::::actor_identity(actor_id) + .expect("Suspended identity exists") + .sovereign_account; + let retry_funding = amount.saturating_mul(2u32.into()); + T::AssetOps::mint(&sovereign, asset, retry_funding) + .expect("successful retry receives spendable fixed funding"); + Ok(ReachableSuspendedSuccess { + actor_id, + cursor, + ready_others: benchmark_fixture_ready_occupancy::().saturating_sub(1), + asset, + amount, + recipient, + }) + } + + fn assert_reachable_suspended_tail_success( + fixture: ReachableSuspendedSuccess, + now: BlockNumberFor, + branch: RunningInnerBranch, + ) { + let state = Pallet::::active_actor_state(fixture.actor_id) + .expect("successful retry retains active authority"); + assert_eq!( + T::AssetOps::balance(&fixture.recipient, fixture.asset), + fixture.amount + ); + match branch { + RunningInnerBranch::Complete => { + assert!(state.run_state.is_none()); + assert_eq!(state.identity.cycle_nonce, 1); + assert_eq!(state.hot.cycle_state, CycleState::Idle); + assert_eq!( + benchmark_fixture_ready_occupancy::(), + fixture.ready_others + ); + } + RunningInnerBranch::Progress => { + let run = state + .run_state + .as_ref() + .expect("progress retry retains Run"); + assert_eq!(state.hot.cycle_state, CycleState::Running); + assert_eq!(run.cursor, fixture.cursor + 1); + assert_eq!(run.last_committed_step_block, Some(now)); + assert_eq!(run.last_step_outcome, Some(StepOutcome::Executed)); + assert_eq!( + benchmark_fixture_ready_occupancy::(), + fixture.ready_others + 1 + ); + } + } + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("successful Suspended tail preserves full state audit"); + } + + fn assert_reachable_suspended_tail_skip( + fixture: ReachableSuspendedSkip, + now: BlockNumberFor, + branch: RunningInnerBranch, + ) { + let state = + Pallet::::active_actor_state(fixture.actor_id).expect("skip successor remains active"); + assert!(frame_system::Pallet::::events().iter().any(|record| { + record.event + == Event::::StepSkipped { + actor_id: fixture.actor_id, + cycle_nonce: 1, + step_index: fixture.cursor, + reason: StepSkippedReason::ResolutionSkipped, + } + .into() + })); + assert!(!state.hot.pending_signal); + match branch { + RunningInnerBranch::Complete => { + assert!(state.run_state.is_none()); + assert_eq!(state.identity.cycle_nonce, 1); + assert_eq!(state.hot.cycle_state, CycleState::Idle); + assert_eq!( + ActorControlLocators::::get(fixture.actor_id), + Some(ActorControlLocation::Unsignaled) + ); + assert_eq!( + benchmark_fixture_ready_occupancy::(), + fixture.ready_others + ); + } + RunningInnerBranch::Progress => { + let run = state.run_state.as_ref().expect("progress retains real Run"); + assert_eq!(state.hot.cycle_state, CycleState::Running); + assert_eq!(state.identity.cycle_nonce, 0); + assert_eq!(run.cursor, fixture.cursor + 1); + assert_eq!(run.last_committed_step_block, Some(now)); + assert_eq!( + run.last_step_outcome, + Some(StepOutcome::Skipped(StepSkippedReason::ResolutionSkipped)) + ); + assert_eq!(run.unsuccessful_attempts_at_cursor, 0); + assert!(run.suspension.is_none()); + assert_eq!( + benchmark_fixture_ready_occupancy::(), + fixture.ready_others + 1 + ); + assert!(matches!( + ActorControlLocators::::get(fixture.actor_id), + Some(ActorControlLocation::Ready { .. }) + )); + } + } + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("frozen skip successor passes full audit"); + T::BenchmarkHelper::set_asset_account_frozen( + &fixture.owner, + &state.identity.sovereign_account, + fixture.frozen_asset, + false, + ) + .expect("supported fixture thaws after measurement"); + for (asset, balance) in fixture.balances { + assert_eq!( + T::AssetOps::balance(&state.identity.sovereign_account, asset), + balance + ); + } + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("custody-preserving thaw passes full audit"); + // Explicit fixture teardown after all measured-transition and custody assertions. The Actor + // lifecycle does not burn or unwind custody; this removes only this setup's minted balances. + for (asset, baseline, minted) in fixture.fixture_funding { + T::AssetOps::burn(&state.identity.sovereign_account, asset, minted) + .expect("fixture-only funding teardown succeeds"); + assert_eq!( + T::AssetOps::balance(&state.identity.sovereign_account, asset), + baseline + ); + } + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("fixture funding teardown preserves lifecycle invariants"); + } + + fn prepare_reachable_suspended_tail_retry( + s: u32, + p: u32, + ) -> Result<(ActorId, u32), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let maximum = T::MaxContractSteps::get(); + let chunk = MAX_STEPS_PER_TAIL_CHUNK; + if s == 0 + || s > chunk + || maximum <= s + || p > benchmark_predicate_capacity::() + || T::MaxRetryAttempts::get() < 3 + { + return Err(polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "host cannot represent two nonterminal tail retries", + )); + } + let count = if s == chunk { + maximum + } else { + 1 + s + ((maximum - 1 - s) / chunk) * chunk + }; + let cursor = if s == chunk { 1 } else { count - s }; + let asset_owner: T::AccountId = account("suspended-retry-assets", 0, 0); + let mut assets = T::BenchmarkHelper::setup_predicate_assets(&asset_owner, p + 2) + .expect("retry predicate assets exist"); + let position = assets + .iter() + .position(|asset| *asset != T::FeeNativeAssetId::get()) + .expect("retry requires a non-native asset"); + let retry_asset = assets.remove(position); + assets.truncate(p as usize); + assert_eq!(assets.len() as u32, p); + assert!(assets.iter().all(|asset| *asset != retry_asset)); + let precondition = if p == 0 { + None + } else { + Some(packed_predicate_clauses::( + assets + .into_iter() + .map(|asset| TimedPredicate { + timing: ObservationTiming::Current, + predicate: Predicate::BalanceBelow { + asset, + threshold: One::one(), + }, + }) + .collect(), + p.div_ceil(T::MaxPreconditionClauses::get()).max(1), + )) + }; + let actor_id = prepare_reachable_running_with_cooldown::( + ActorType::System, + count, + Some(( + cursor, + Step { + precondition, + task: ActorTask::Transfer { + to: account("suspended-retry-recipient", 0, 0), + asset: retry_asset, + amount: AmountResolution::Fixed(One::one()), + }, + on_error: StepErrorPolicy::RetryLater { + max_attempts: T::MaxRetryAttempts::get(), + }, + }, + )), + 2, + )?; + for expected_cursor in 1..cursor { + let run = ActorRunStateStore::::get(actor_id).expect("real committed prefix retains Run"); + assert_eq!(run.cursor, expected_cursor); + frame_system::Pallet::::set_block_number(run.eligible_at); + Pallet::::execute_cycle(Weight::MAX); + } + let state = + Pallet::::active_actor_state(actor_id).expect("target has real Running authority"); + let run = state.run_state.as_ref().expect("target Run exists"); + assert_eq!(run.cursor, cursor); + assert!(T::AssetOps::balance(&state.identity.sovereign_account, retry_asset).is_zero()); + assert_eq!( + state.contract.steps[cursor as usize] + .precondition + .as_ref() + .map_or(0, Precondition::evaluation_units), + p + ); + let context = Pallet::::step_control_weight_context(count, cursor, p, 0, 0, 0) + .expect("tail context exists"); + assert_eq!(context.steps_in_fragment, s); + assert_eq!(context.predicate_evaluation_units, p); + frame_system::Pallet::::set_block_number(run.eligible_at); + Pallet::::execute_cycle(Weight::MAX); + assert_reachable_suspended_tail_retry_state::(actor_id, cursor, 1); + let run = ActorRunStateStore::::get(actor_id).expect("first real retry persists"); + let retained = run.encode(); + frame_system::Pallet::::set_block_number(run.eligible_at); + let mut meter = polkadot_sdk::sp_weights::WeightMeter::with_limit(Weight::MAX); + let drained = Pallet::::drain_overdue_wakeups_cursor(run.eligible_at, &mut meter); + assert!(drained.ready_entries >= 1); + assert!(matches!( + ActorControlLocators::::get(actor_id), + Some(ActorControlLocation::Ready { .. }) + )); + assert_eq!( + ActorRunStateStore::::get(actor_id) + .expect("due retry Run persists") + .encode(), + retained + ); + assert!(matches!( + ActorControlLocators::::get(actor_id), + Some(ActorControlLocation::Ready { .. }) + )); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("real due retry Ready source passes full audit"); + Ok((actor_id, cursor)) + } + + fn assert_reachable_suspended_tail_retry_state( + actor_id: ActorId, + cursor: u32, + attempts: u32, + ) { + let state = Pallet::::active_actor_state(actor_id).expect("real retry authority remains"); + let run = state.run_state.as_ref().expect("real retry Run remains"); + assert_eq!(state.hot.cycle_state, CycleState::Suspended); + assert!(!state.hot.pending_signal); + assert_eq!(state.identity.cycle_nonce, 0); + assert_eq!(run.cycle_nonce, 1); + assert_eq!(run.cursor, cursor); + assert_eq!(run.unsuccessful_attempts_at_cursor, attempts); + assert_eq!(run.last_step_outcome, Some(StepOutcome::FundingUnavailable)); + assert_eq!(run.suspension, Some(SuspensionReason::FundingUnavailable)); + let now = frame_system::Pallet::::block_number(); + assert_eq!(run.last_attempt_block, now); + let due = + Pallet::::suspension_eligible_at(2, None, now, attempts).expect("retry due boundary fits"); + assert_eq!(run.eligible_at, due); + assert!( + matches!(ActorControlLocators::::get(actor_id), Some(ActorControlLocation::Waiting { key: WakeupKey::Block(at), .. }) if at == due) + ); + assert_eq!( + run.opening_snapshot.len(), + Pallet::::opening_surfaces(&state.contract.steps, 0).len() + ); + assert_eq!( + run.opening_predicate_results.len() as u32, + state + .contract + .steps + .iter() + .map(|step| step + .precondition + .as_ref() + .map_or(0, Precondition::opening_predicate_count)) + .sum::() + ); + assert_eq!( + run.opening_predicate_cursor, + state.contract.steps[..cursor as usize] + .iter() + .map(|step| step + .precondition + .as_ref() + .map_or(0, Precondition::opening_predicate_count)) + .sum::() + ); + assert!( + run + .opening_predicate_results + .iter() + .all(|result| *result == Ok(false)) + ); + assert!( + run.funding_snapshot.is_empty() + && state.funding.funding_accumulated.is_empty() + && state.funding.funding_tracked_assets.is_empty() + ); + let ActorTask::Transfer { asset, ref to, .. } = state.contract.steps[cursor as usize].task + else { + unreachable!() + }; + assert!(T::AssetOps::balance(&state.identity.sovereign_account, asset).is_zero()); + assert!(T::AssetOps::balance(to, asset).is_zero()); + assert_eq!(benchmark_fixture_ready_occupancy::(), 0); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("real Suspended Waiting retry passes full audit"); + } + + fn prepare_reachable_running_with_step( + step_count: u32, + current_step: Option<(u32, StepOf)>, + ) -> Result { + prepare_reachable_running_with_cooldown::(ActorType::System, step_count, current_step, 0) + } + + fn prepare_reachable_running_with_cooldown( + actor_type: ActorType, + step_count: u32, + current_step: Option<(u32, StepOf)>, + cooldown_blocks: u32, + ) -> Result { + if step_count < 2 { + return Err(polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "Running fixture requires a successor Step", + )); + } + let now: BlockNumberFor = 1u32.into(); + frame_system::Pallet::::set_block_number(now); + GlobalCircuitBreaker::::put(false); + let owner: T::AccountId = account("reachable-state-owner", 0, 0); + let mut steps = make_reachable_opening_steps::(step_count)?; + if actor_type == ActorType::User { + // Native prefunding must not turn the authored no-effect prefix into an invoked Task. + for step in &mut steps { + for clause in &mut step + .precondition + .as_mut() + .expect("Opening predicates exist") + .clauses + { + for timed in clause { + if let Predicate::BalanceAbove { asset, threshold } = &mut timed.predicate { + if *asset == T::FeeNativeAssetId::get() { + *threshold = ::max_value() + .saturating_sub(*threshold); + } + } + } + } + } + } + let protected_cursor = current_step.as_ref().map(|(cursor, _)| *cursor); + if let Some((cursor, step)) = current_step { + assert!(cursor > 0 && cursor < step_count); + steps[cursor as usize] = step; + } + let mut protected = vec![0]; + if let Some(cursor) = protected_cursor { + protected.push(cursor); + } + retain_admitted_contract_geometry::(actor_type, &mut steps, &protected)?; + let surfaces = Pallet::::opening_surfaces(&steps, 0); + let opening_predicates = steps + .iter() + .filter_map(|step| step.precondition.as_ref()) + .flat_map(|precondition| precondition.clauses.iter()) + .flat_map(|clause| clause.iter()) + .filter(|timed| timed.timing == ObservationTiming::Opening) + .count() as u32; + assert!(steps.iter().all(|step| matches!( + &step.task, + ActorTask::AddLiquidity { + amount_a: AmountResolution::PercentageAtOpening(_), + amount_b: AmountResolution::PercentageAtOpening(_), + .. + } | ActorTask::AddLiquidity { + amount_a: AmountResolution::PercentageOfCurrent(_), + amount_b: AmountResolution::PercentageOfCurrent(_), + .. + } | ActorTask::StopCycle + | ActorTask::Transfer { + amount: AmountResolution::PercentageOfCurrent(_), + .. + } + | ActorTask::Transfer { + amount: AmountResolution::Fixed(_) | AmountResolution::AllAvailable, + .. + } + ))); + assert!(surfaces.len() as u32 <= T::MaxOpeningSnapshotEntries::get()); + assert!(opening_predicates <= T::MaxOpeningPredicateResults::get()); + let schedule = Schedule { + trigger: Trigger::manual(), + cooldown_blocks, + }; + if actor_type == ActorType::User { + ensure_creation_balance::(&owner); + prefund_active_user_creation::(&owner, &steps); + Pallet::::create_user_actor( + RawOrigin::Signed(owner.clone()).into(), + Mutability::Mutable, + user_contract::(schedule, steps), + ) + .expect("reachable maximum User Contract is admitted"); + } else { + Pallet::::create_system_actor( + RawOrigin::Root.into(), + owner.clone(), + Mutability::Mutable, + system_contract::(schedule, steps), + ) + .expect("reachable maximum System Contract is admitted"); + } + let actor_id = NextActorId::::get().saturating_sub(1); + if actor_type == ActorType::User { + let sovereign = Pallet::::actor_identity(actor_id) + .expect("admitted User identity exists") + .sovereign_account; + let trigger_fee = Pallet::::trigger_fee_for_weight( + ActorType::User, + TriggerFamily::Manual, + T::WeightInfo::manual_trigger(), + ) + .trigger_fee; + T::AssetOps::mint(&sovereign, T::FeeNativeAssetId::get(), trigger_fee) + .expect("Manual readiness has funding independent of Pipeline capacity"); + } + Pallet::::manual_trigger(RawOrigin::Signed(owner).into(), actor_id) + .expect("real Manual occurrence latches the actor"); + // Normal Q1 service consumes only Step 0 at this block; false predicates invoke no Task. + Pallet::::execute_cycle(Weight::MAX); + let state = Pallet::::active_actor_state(actor_id).expect("reachable actor remains active"); + assert_eq!(state.hot.cycle_state, CycleState::Running); + let run = state + .run_state + .as_ref() + .expect("Opening published a real Run"); + assert_eq!(run.cursor, 1); + assert_eq!(run.last_committed_step_block, Some(now)); + assert_eq!(run.cycle_nonce, state.identity.cycle_nonce + 1); + assert_eq!(run.opening_snapshot.len(), surfaces.len()); + assert!( + surfaces + .iter() + .all(|surface| run.opening_snapshot.contains_key(surface)) + ); + assert_eq!( + run.opening_predicate_results.len() as u32, + opening_predicates + ); + assert!(state.funding.funding_tracked_assets.is_empty()); + assert!(state.funding.funding_accumulated.is_empty()); + assert!(run.funding_snapshot.is_empty()); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("reachable probe fixture passes full state audit"); + Ok(actor_id) + } + + #[benchmark] + fn scheduler_actor_state_probe() -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let actor_id = prepare_reachable_running::(T::MaxContractSteps::get())?; + let state = Pallet::::active_actor_state(actor_id).expect("real probe state exists"); + let run = state.run_state.as_ref().expect("real probe Run exists"); + assert_eq!( + run.opening_snapshot.len(), + Pallet::::opening_surfaces(&state.contract.steps, 0).len() + ); + assert_eq!( + run.opening_predicate_results.len() as u32, + state + .contract + .steps + .iter() + .map(|step| step + .precondition + .as_ref() + .map_or(0, Precondition::opening_predicate_count)) + .sum::() + ); + #[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(_) + )); + Ok(()) + } + #[benchmark(pov_mode = Measured)] + fn scheduler_paged_append_existing_page() { + let page_size = 32u32; + assert!( + page_size >= 2, + "benchmark requires a non-trivial queue page" + ); + for i in 0..page_size.saturating_sub(1) { + let actor_id = bench_create_system_manual::(31_000_000u32.saturating_add(i)); + assert!(benchmark_fixture_ready_enqueue::(actor_id)); + } + let actor_id = bench_create_system_manual::(32_000_000); + let append = benchmark_fixture_prepare_consumed_ready_enqueue::(actor_id); + #[block] + { + assert!(append()); + } + assert_eq!(benchmark_fixture_ready_tail::(), u64::from(page_size)); + assert_eq!( + benchmark_fixture_scalar_hot::(actor_id).and_then(|hot| hot.queue_ticket), + Some(u64::from(page_size - 1)) + ); + } + + #[benchmark(pov_mode = Measured)] + fn scheduler_paged_append_new_page() { + let page_size = 32u32; + for i in 0..page_size { + let actor_id = bench_create_system_manual::(33_000_000u32.saturating_add(i)); + assert!(benchmark_fixture_ready_enqueue::(actor_id)); + } + let actor_id = bench_create_system_manual::(34_000_000); + let append = benchmark_fixture_prepare_consumed_ready_enqueue::(actor_id); + #[block] + { + assert!(append()); + } + assert_eq!( + benchmark_fixture_ready_tail::(), + u64::from(page_size).saturating_add(1) + ); + assert_eq!(benchmark_fixture_ready_page_len::(1), Some(32)); + let page = ActorReadyFrameChunks::::get(1).expect("new Ready page exists"); + assert_eq!(page[0].as_ref().map(|cell| cell.actor_id), Some(actor_id)); + assert!(page[1..].iter().all(Option::is_none)); + } + + #[benchmark(pov_mode = Measured)] + fn scheduler_wakeup_append_existing_page() { + let page_size = 32u32; + assert!( + page_size >= 2, + "benchmark requires a non-trivial wakeup page" + ); + let wakeup_block = 100u32.into(); + for i in 0..page_size.saturating_sub(1) { + let actor_id = bench_create_system_manual::(41_000_000u32.saturating_add(i)); + benchmark_fixture_schedule_service_waiting::(actor_id, wakeup_block); + } + let actor_id = bench_create_system_manual::(42_000_000); + let append = benchmark_fixture_prepare_service_waiting::(actor_id, wakeup_block); + #[block] + { + append(); + } + let pointer = benchmark_fixture_scalar_hot::(actor_id) + .and_then(|hot| hot.wakeup_pointer) + .expect("benchmark wakeup pointer must exist"); + assert_eq!((pointer.page_id, pointer.slot), (0, page_size - 1)); + } + + #[benchmark(pov_mode = Measured)] + fn scheduler_wakeup_append_new_page() { + let page_size = 32u32; + let wakeup_block = 100u32.into(); + for i in 0..page_size { + let actor_id = bench_create_system_manual::(43_000_000u32.saturating_add(i)); + benchmark_fixture_schedule_service_waiting::(actor_id, wakeup_block); + } + let actor_id = bench_create_system_manual::(44_000_000); + let append = benchmark_fixture_prepare_service_waiting::(actor_id, wakeup_block); + #[block] + { + append(); + } + let pointer = benchmark_fixture_scalar_hot::(actor_id) + .and_then(|hot| hot.wakeup_pointer) + .expect("benchmark wakeup pointer must exist"); + assert_eq!((pointer.page_id, pointer.slot), (1, 0)); + let key = WakeupKey::Block(wakeup_block); + let previous = + ActorWaitingFrameChunks::::get((key, 0)).expect("full preceding Waiting page remains"); + let page = ActorWaitingFrameChunks::::get((key, 1)).expect("new linked Waiting page exists"); + assert_eq!( + (previous.previous_page, previous.next_page), + (None, Some(1)) + ); + assert_eq!(previous.live_entries, page_size); + assert_eq!((page.previous_page, page.next_page), (Some(0), None)); + assert_eq!(page.live_entries, 1); + assert_eq!(page.entries.len(), page_size as usize); + assert_eq!( + page.entries[0] + .as_ref() + .and_then(ActorWaitingEntry::primary) + .map(|cell| cell.actor_id), + Some(actor_id) + ); + assert!(page.entries[1..].iter().all(Option::is_none)); + assert_eq!(ActorWaitingOccupancies::::get(key), page_size + 1); + assert!(Pallet::::wakeup_page_entry_matches(pointer, actor_id)); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state() + .expect("new Waiting page reconciles primary and directory ownership"); + } + + #[benchmark(pov_mode = Measured)] + fn scheduler_wakeup_replace_exact() { + let actor_id = bench_create_system_manual::(45_000_000); + let old_block = 100u32.into(); + let replacement_block = 200u32.into(); + benchmark_fixture_schedule_service_waiting::(actor_id, old_block); + #[block] + { + assert!(Pallet::::wakeup_substrate_schedule( + actor_id, + replacement_block + )); + } + let pointer = benchmark_fixture_scalar_hot::(actor_id) + .and_then(|hot| hot.wakeup_pointer) + .expect("replacement wakeup pointer must exist"); + assert_eq!( + (pointer.block, pointer.page_id, pointer.slot), + (WakeupKey::Block(replacement_block), 0, 0) + ); + assert!(!ActorWaitingOccupancies::::contains_key( + WakeupKey::Block(old_block) + )); + } + + #[benchmark(pov_mode = Measured)] + fn scheduler_wakeup_invalidate_middle_page() { + let page_size = 32u32; + let wakeup_block = 100u32.into(); + let count = page_size.saturating_mul(2).saturating_add(1); + let mut actors = alloc::vec::Vec::with_capacity(count as usize); + for i in 0..count { + let actor_id = bench_create_system_manual::(46_000_000u32.saturating_add(i)); + benchmark_fixture_schedule_service_waiting::(actor_id, wakeup_block); + actors.push(actor_id); + } + let middle_start = page_size as usize; + let middle_end = middle_start.saturating_add(page_size as usize); + for actor_id in &actors[middle_start..middle_end.saturating_sub(1)] { + assert!(Pallet::::wakeup_substrate_invalidate(*actor_id).is_some()); + } + let actor_id = actors[middle_end - 1]; + #[block] + { + assert!(Pallet::::wakeup_substrate_invalidate(actor_id).is_some()); + } + assert!(!ActorWaitingFrameChunks::::contains_key(( + WakeupKey::Block(wakeup_block), + 1 + ))); + assert_eq!( + ActorWaitingFrameChunks::::get((WakeupKey::Block(wakeup_block), 0)) + .and_then(|page| page.next_page), + Some(2) + ); + assert_eq!( + ActorWaitingFrameChunks::::get((WakeupKey::Block(wakeup_block), 2)) + .and_then(|page| page.previous_page), + Some(0) + ); + } + + #[benchmark(pov_mode = Measured)] + fn scheduler_wakeup_drain_partial_page() { + let page_size = 32u32; + assert!(page_size >= 2, "benchmark requires a partial page"); + let wakeup_block = 100u32.into(); + for i in 0..page_size { + let actor_id = bench_create_system_manual::(47_000_000u32.saturating_add(i)); + benchmark_fixture_schedule_service_waiting::(actor_id, wakeup_block); + } + let scan_limit = page_size / 2; + #[block] + { + let (ready, stats) = Pallet::::wakeup_substrate_drain_block(wakeup_block, scan_limit); + assert_eq!(ready.len(), scan_limit as usize); + assert_eq!(stats.entries_scanned, scan_limit); + assert_eq!(stats.pages_deleted, 0); + } + assert_eq!( + ActorWaitingFrameChunks::::get((WakeupKey::Block(wakeup_block), 0)) + .map(|page| page.scan_slot), + Some(scan_limit) + ); + } + + #[benchmark(pov_mode = Measured)] + fn scheduler_wakeup_drain_full_page() { + let page_size = 32u32; + let wakeup_block = 100u32.into(); + for i in 0..page_size { + let actor_id = bench_create_system_manual::(48_000_000u32.saturating_add(i)); + benchmark_fixture_schedule_service_waiting::(actor_id, wakeup_block); + } + #[block] + { + let (ready, stats) = Pallet::::wakeup_substrate_drain_block(wakeup_block, page_size); + assert_eq!(ready.len(), page_size as usize); + assert_eq!(stats.entries_scanned, page_size); + assert_eq!(stats.pages_deleted, 1); + } + assert!(!ActorWaitingOccupancies::::contains_key( + WakeupKey::Block(wakeup_block) + )); + assert!(!ActorWaitingFrameChunks::::contains_key(( + WakeupKey::Block(wakeup_block), + 0 + ))); + } + + #[benchmark(pov_mode = Measured)] + fn scheduler_wakeup_drain_dense_boundary() { + let page_size = 32u32; + let count = page_size.saturating_add(1); + assert!( + count <= T::MaxWakeupsPerBlock::get(), + "benchmark requires one boundary-crossing drain" + ); + let wakeup_block = 100u32.into(); + for i in 0..count { + let actor_id = bench_create_system_manual::(49_000_000u32.saturating_add(i)); + benchmark_fixture_schedule_service_waiting::(actor_id, wakeup_block); + } + #[block] + { + let (ready, stats) = Pallet::::wakeup_substrate_drain_block(wakeup_block, count); + assert_eq!(ready.len(), count as usize); + assert_eq!(stats.entries_scanned, count); + assert_eq!(stats.pages_touched, 2); + assert_eq!(stats.pages_deleted, 2); + } + assert!(!ActorWaitingOccupancies::::contains_key( + WakeupKey::Block(wakeup_block) + )); + } + + #[benchmark(pov_mode = Measured)] + fn scheduler_wakeup_drain_stale_page() { + let page_size = 32u32; + let wakeup_block = 100u32.into(); + for i in 0..page_size { + let actor_id = bench_create_system_manual::(50_000_000u32.saturating_add(i)); + benchmark_fixture_schedule_service_waiting::(actor_id, wakeup_block); + let (location, cell) = + Pallet::::actor_control_cell(actor_id).expect("benchmark Waiting primary exists"); + let ActorControlLocation::Waiting { key, page, slot } = location else { + panic!("benchmark service primary is Waiting"); + }; + // Explicit orphan corruption exercises stale-reference removal, not normal close. + ActorWaitingFrameChunks::::mutate((key, page), |stored| { + stored + .as_mut() + .expect("benchmark Waiting page exists") + .entries[slot as usize] = Some(ActorWaitingEntry::Reference(ActorWakeupReference { + actor_id, + admission_identity: cell.admission.admission_identity, + })); + }); + ActorControlLocators::::remove(actor_id); + } + #[block] + { + let (ready, stats) = Pallet::::wakeup_substrate_drain_block(wakeup_block, page_size); + assert!(ready.is_empty()); + assert_eq!(stats.entries_scanned, page_size); + assert_eq!(stats.stale_entries, page_size); + assert_eq!(stats.pages_deleted, 1); + } + assert!(!ActorWaitingOccupancies::::contains_key( + WakeupKey::Block(wakeup_block) + )); + } + + #[benchmark(pov_mode = Measured)] + fn scheduler_wakeup_cursor_insert() { + clear_host_genesis_wakeup_placements::(); + let inserted_block: BlockNumberFor = 1u32.into(); + let actor_id = bench_create_system_manual::(50_100_000); + benchmark_fixture_schedule_service_waiting::(actor_id, inserted_block); + // Preserve the live Waiting owner at the boundary before its cursor is installed. + ActorWaitingCursorIndices::::remove(WakeupKey::Block(inserted_block)); + let page_size = T::WakeupPageSize::get(); + let max_active = T::MaxActiveActors::get(); + assert!( + page_size > 0 && max_active > 1, + "benchmark requires bounded cursor depth" + ); + let insert_index = max_active.saturating_sub(1); + let tail_page = u64::from(insert_index / page_size); + let tail_len = insert_index % page_size; + let mut page_ids = alloc::vec::Vec::new(); + let mut current = insert_index; + loop { + add_wakeup_cursor_page(&mut page_ids, current, page_size); + if current == 0 { + break; + } + current = current.saturating_sub(1) / 2; + } + for page_id in page_ids { + let len = if page_id == tail_page { + tail_len + } else { + page_size + }; + if len > 0 { + install_wakeup_cursor_page::(page_id, len); + } + } + WakeupCursorLen::::insert(WakeupClock::Block, insert_index); + #[block] + { + assert!(Pallet::::wakeup_cursor_insert(inserted_block)); + } + assert_eq!(WakeupCursorLen::::get(WakeupClock::Block), max_active); + assert_eq!(Pallet::::wakeup_cursor_peek(), Some(inserted_block)); + assert_wakeup_cursor_page_indices::(); + } + + #[benchmark(pov_mode = Measured)] + fn scheduler_wakeup_cursor_pop_min() { + clear_host_genesis_wakeup_placements::(); + let cursor_len = T::MaxActiveActors::get(); + let expected_min = prepare_wakeup_cursor_repair::(0); + #[block] + { + assert_eq!(Pallet::::wakeup_cursor_pop_min(), Some(expected_min)); + } + assert_eq!( + WakeupCursorLen::::get(WakeupClock::Block), + cursor_len.saturating_sub(1) + ); + assert_eq!(Pallet::::wakeup_cursor_peek(), Some(1_000_001u32.into())); + assert_eq!( + ActorWaitingCursorIndices::::get(WakeupKey::Block(expected_min)), + None + ); + assert_wakeup_cursor_page_indices::(); + } + + #[benchmark(pov_mode = Measured)] + fn scheduler_wakeup_cursor_remove_exact() { + clear_host_genesis_wakeup_placements::(); + let cursor_len = T::MaxActiveActors::get(); + let removed_block = prepare_wakeup_cursor_repair::(1); + #[block] + { + assert!(Pallet::::wakeup_cursor_remove(removed_block)); + } + assert_eq!( + WakeupCursorLen::::get(WakeupClock::Block), + cursor_len.saturating_sub(1) + ); + assert_eq!(Pallet::::wakeup_cursor_peek(), Some(1_000_000u32.into())); + assert_eq!( + ActorWaitingCursorIndices::::get(WakeupKey::Block(removed_block)), + None + ); + assert_wakeup_cursor_page_indices::(); + } + + #[benchmark(extra, pov_mode = Measured)] + fn scheduler_wakeup_cursor_remove_upward_depth() + -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let removed = prepare_upward_cursor_removal::(6_143)?; + #[block] + { + assert!(Pallet::::wakeup_cursor_remove(removed)); + } + assert_eq!(WakeupCursorLen::::get(WakeupClock::Block), 9_999); + assert_eq!( + ActorWaitingCursorIndices::::get(WakeupKey::Block(removed)), + None + ); + assert_eq!( + ActorWaitingCursorIndices::::get(WakeupKey::Block(14u32.into())), + Some(2) + ); + assert_wakeup_cursor_page_indices::(); + Ok(()) + } + + #[benchmark(extra, pov_mode = Measured)] + fn scheduler_wakeup_cursor_remove_upward_pages() + -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let removed = prepare_upward_cursor_removal::(8_447)?; + #[block] + { + assert!(Pallet::::wakeup_cursor_remove(removed)); + } + assert_eq!(WakeupCursorLen::::get(WakeupClock::Block), 9_999); + assert_eq!( + ActorWaitingCursorIndices::::get(WakeupKey::Block(removed)), + None + ); + assert_eq!( + ActorWaitingCursorIndices::::get(WakeupKey::Block(14u32.into())), + Some(7) + ); + assert_wakeup_cursor_page_indices::(); + Ok(()) + } + + #[benchmark(pov_mode = Measured)] + fn scheduler_wakeup_cursor_worker_partial() { + clear_host_genesis_wakeup_placements::(); + let first = bench_create_system_manual::(34_100_000); + let second = bench_create_system_manual::(34_100_001); + for actor_id in [first, second] { + 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"); + benchmark_fixture_mutate_hot::(actor_id, |hot| { + hot.trigger_runtime_state = TriggerRuntimeState::Cadenced { anchor_tick: None }; + }); + Pallet::::benchmark_defer_tick_wakeup(actor_id, 0) + .expect("benchmark bootstrap wakeup fits"); + } + let limit = T::WeightInfo::scheduler_wakeup_cursor_worker_future() + .saturating_mul(2) + .saturating_add(Pallet::::wakeup_cursor_drain_unit_weight_upper( + crate::scheduler::WakeupBucketDisposition::Retain, + )); + let mut meter = polkadot_sdk::sp_weights::WeightMeter::with_limit(limit); + #[block] + { + let stats = Pallet::::drain_overdue_wakeups_cursor(10u32.into(), &mut meter); + assert_eq!(stats.entries_scanned, 1); + assert_eq!(stats.ready_entries, 1); + } + assert_eq!( + ActorWaitingOccupancies::::try_get(WakeupKey::Tick(0)).ok(), + Some(1) + ); + let rearmed_tick = benchmark_fixture_hot::(first) + .and_then(|hot| hot.trigger_runtime_state.temporal_anchor_tick()) + .and_then(|anchor| anchor.checked_add(1)) + .expect("benchmark cadence re-anchors"); + assert_eq!( + ActorWaitingOccupancies::::try_get(WakeupKey::Tick(rearmed_tick)).ok(), + Some(1) + ); + } + + /// 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 at_time_trigger_occurrence() { + clear_host_genesis_wakeup_placements::(); + 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"); + benchmark_fixture_mutate_hot::(actor_id, |hot| { + hot.trigger_runtime_state = TriggerRuntimeState::AtTime { + anchor_tick: Some(0), + consumed: false, + }; + }); + Pallet::::benchmark_defer_tick_wakeup(actor_id, 1).expect("due AtTime occurrence fits"); + benchmark_fixture_publish_trigger_waiting::(actor_id, 1); + let (mut ready, stats) = Pallet::::wakeup_substrate_drain_key(WakeupKey::Tick(1), 1); + assert_eq!(stats.entries_scanned, 1); + let (_, state, admission, loaded_step) = ready + .pop() + .expect("due AtTime source authority is consumed"); + #[block] + { + assert_eq!( + Pallet::::process_due_temporal_occurrence_loaded( + actor_id, + state, + admission, + loaded_step, + 1, + ), + Ok(false) + ); + } + let actor = Pallet::::active_actor_view(actor_id).expect("AtTime Actor remains active"); + assert!(actor.pending_signal); + assert!(actor.queue_ticket.is_some()); + assert!(actor.trigger_wakeup_pointer.is_none()); + assert!(matches!( + actor.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"); + benchmark_fixture_mutate_hot::(actor_id, |hot| { + hot.trigger_runtime_state = TriggerRuntimeState::Cadenced { + anchor_tick: Some(0), + }; + }); + Pallet::::benchmark_defer_tick_wakeup(actor_id, 0).expect("due Cadenced occurrence fits"); + benchmark_fixture_publish_trigger_waiting::(actor_id, 0); + let (mut ready, stats) = Pallet::::wakeup_substrate_drain_key(WakeupKey::Tick(0), 1); + assert_eq!(stats.entries_scanned, 1); + let (_, state, admission, loaded_step) = ready + .pop() + .expect("due Cadenced source authority is consumed"); + #[block] + { + assert_eq!( + Pallet::::process_due_temporal_occurrence_loaded( + actor_id, + state, + admission, + loaded_step, + 0, + ), + Ok(false) + ); + } + let actor = Pallet::::active_actor_view(actor_id).expect("Cadenced Actor remains active"); + assert!(actor.pending_signal); + assert!(actor.queue_ticket.is_some()); + assert!(actor.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: BlockNumberFor = 1_000_000u32.into(); + let actor_id = bench_create_system_manual::(34_200_000); + benchmark_fixture_schedule_service_waiting::(actor_id, wakeup_block); + assert_eq!(prepare_wakeup_cursor_repair::(0), wakeup_block); + let mut meter = polkadot_sdk::sp_weights::WeightMeter::with_limit(Weight::MAX); + #[block] + { + let stats = Pallet::::drain_overdue_wakeups_cursor(wakeup_block, &mut meter); + assert_eq!(stats.entries_scanned, 1); + assert_eq!(stats.ready_entries, 1); + } + assert_eq!( + WakeupCursorLen::::get(WakeupClock::Block), + cursor_len.saturating_sub(1) + ); + assert_eq!(Pallet::::wakeup_cursor_peek(), Some(1_000_001u32.into())); + assert!(!ActorWaitingOccupancies::::contains_key( + WakeupKey::Block(wakeup_block) + )); + } + + #[benchmark(pov_mode = Measured)] + fn scheduler_wakeup_cursor_worker_future() { + clear_host_genesis_wakeup_placements::(); + let wakeup_block: BlockNumberFor = 1_000_000u32.into(); + let actor_id = bench_create_system_manual::(34_300_000); + benchmark_fixture_schedule_service_waiting::(actor_id, wakeup_block); + let mut meter = polkadot_sdk::sp_weights::WeightMeter::with_limit(Weight::MAX); + #[block] + { + let stats = Pallet::::drain_overdue_wakeups_cursor(10u32.into(), &mut meter); + assert_eq!(stats.entries_scanned, 0); + } + assert_eq!(Pallet::::wakeup_cursor_peek(), Some(wakeup_block)); + assert!( + benchmark_fixture_scalar_hot::(actor_id) + .and_then(|hot| hot.wakeup_pointer) + .is_some() + ); + } + + #[benchmark(pov_mode = Measured)] + fn scheduler_paged_consume_preserve_page() { + let first = bench_create_system_manual::(35_000_000); + let second = bench_create_system_manual::(35_000_001); + assert!(benchmark_fixture_ready_enqueue::(first)); + assert!(benchmark_fixture_ready_enqueue::(second)); + let hot = benchmark_fixture_hot::(first).expect("benchmark loaded source exists"); + #[block] + { + assert!(benchmark_fixture_ready_consume_loaded_head::( + 0, first, 0, hot, + )); + } + assert_eq!(benchmark_fixture_ready_head::(), 1); + assert!(benchmark_fixture_contains_ready_page::(0)); + assert!(!ActorControlLocators::::contains_key(first)); + } + + #[benchmark(pov_mode = Measured)] + fn scheduler_paged_consume_delete_page() { + let actor_id = bench_create_system_manual::(36_000_000); + assert!(benchmark_fixture_ready_enqueue::(actor_id)); + let hot = benchmark_fixture_hot::(actor_id).expect("benchmark loaded source exists"); + #[block] + { + assert!(benchmark_fixture_ready_consume_loaded_head::( + 0, actor_id, 0, hot, + )); + } + assert_eq!(benchmark_fixture_ready_head::(), 1); + assert_eq!(benchmark_fixture_ready_tail::(), 1); + assert!(!benchmark_fixture_contains_ready_page::(0)); + assert!(!ActorControlLocators::::contains_key(actor_id)); + } + + #[benchmark(pov_mode = Measured)] + fn scheduler_paged_tombstone_drain(n: Linear<1, 10_000>) { + let bounded = n.min(T::MaxQueueLength::get()); + let head = ActorReadyTail::::get(); + let tail = head + .checked_add(u64::from(bounded)) + .expect("bounded fixture span"); + for page_id in head / 32..tail.div_ceil(32) { + ActorReadyFrameChunks::::insert( + page_id, + ActorControlChunkOf::::try_from(vec![None; 32]).expect("fixed Ready chunk"), + ); + } + benchmark_fixture_set_ready_queue_state::(head, tail, 0); + #[block] + { + core::hint::black_box( + benchmark_fixture_ready_drain_tombstones::(tail, bounded) + .expect("benchmark queue topology is valid"), + ); + } + assert_eq!(benchmark_fixture_ready_head::(), tail); + assert_eq!(benchmark_fixture_ready_tail::(), tail); + assert_eq!(benchmark_fixture_ready_occupancy::(), 0); + } + + #[benchmark(pov_mode = Measured)] + fn scheduler_paged_mixed_scan(n: Linear<1, 10_000>) { + let bounded = n.min(T::MaxQueueLength::get()); + let head = ActorReadyTail::::get(); + let tail = head + .checked_add(u64::from(bounded)) + .expect("bounded fixture span"); + for offset in 0..bounded { + let ticket = head + u64::from(offset); + if offset % 2 == 1 { + let actor_id = bench_create_system_manual::(39_000_000u32.saturating_add(offset)); + assert!(benchmark_fixture_ready_enqueue::(actor_id)); + } else { + let page_id = ticket / 32; + if !ActorReadyFrameChunks::::contains_key(page_id) { + ActorReadyFrameChunks::::insert( + page_id, + ActorControlChunkOf::::try_from(vec![None; 32]).expect("fixed Ready chunk"), + ); + } + ActorReadyTail::::put(ticket + 1); + } + } + assert_eq!(ActorReadyTail::::get(), tail); + #[block] + { + while benchmark_fixture_ready_head::() < tail { + core::hint::black_box( + benchmark_fixture_ready_drain_tombstones::(tail, bounded) + .expect("benchmark mixed queue topology is valid"), + ); + if let Some((_, entry)) = benchmark_fixture_ready_head_entry::() { + assert!(benchmark_fixture_ready_consume_head::(entry.ticket)); + } + } + } + assert_eq!(benchmark_fixture_ready_head::(), tail); + assert_eq!(benchmark_fixture_ready_tail::(), tail); + assert_eq!(benchmark_fixture_ready_occupancy::(), 0); + } + + fn prepare_zero_step_opening(actor_type: ActorType) -> ActorId { + frame_system::Pallet::::set_block_number(1u32.into()); + let owner: T::AccountId = account("zero-step-opening-owner", 0, 0); + ensure_creation_balance::(&owner); + let steps = ContractSteps::::default(); + let schedule = Schedule { + trigger: Trigger::manual(), + cooldown_blocks: 0, + }; + if actor_type == ActorType::User { + prefund_active_user_creation::(&owner, &steps); + Pallet::::create_user_actor( + RawOrigin::Signed(owner.clone()).into(), + Mutability::Mutable, + user_contract::(schedule, steps), + ) + .expect("zero-Step User Contract is admitted"); + } else { + Pallet::::create_system_actor( + RawOrigin::Root.into(), + owner.clone(), + Mutability::Mutable, + system_contract::(schedule, steps), + ) + .expect("zero-Step System Contract is admitted"); + } + let actor_id = NextActorId::::get().saturating_sub(1); + let identity = benchmark_fixture_identity::(actor_id).expect("admitted identity exists"); + if actor_type == ActorType::User { + let fee = full_attempt_fee::(&ContractSteps::::default()) + .checked_add( + &Pallet::::trigger_fee_for_weight( + ActorType::User, + TriggerFamily::Manual, + T::WeightInfo::manual_trigger(), + ) + .trigger_fee, + ) + .expect("User readiness and Pipeline fees fit"); + T::AssetOps::mint(&identity.sovereign_account, T::FeeNativeAssetId::get(), fee) + .expect("User readiness and Pipeline have funding"); + } + Pallet::::manual_trigger(RawOrigin::Signed(owner).into(), actor_id) + .expect("real Manual occurrence publishes zero-Step readiness"); + let (_, cell) = Pallet::::actor_control_cell(actor_id).expect("zero-Step Ready cell exists"); + assert!(cell.hot.pending_signal); + frame_system::Pallet::::set_block_number( + cell.eligible_at.expect("zero-Step eligibility exists"), + ); + actor_id + } + + fn consume_zero_step_opening( + actor_id: ActorId, + ) -> (ActiveActorStateOf, ActorAdmissionCertificateOf) { + let (state, admission, step) = + Pallet::::load_frame_actor_service_state(actor_id).expect("zero-Step frame is coherent"); + assert!(step.is_none() && state.contract.steps.is_empty()); + Pallet::::remove_primary_control_cell_inner(actor_id) + .expect("zero-Step primary is consumed outside the inner measurement"); + (state, admission) + } + + fn execute_zero_step_inner( + actor_id: ActorId, + state: ActiveActorStateOf, + admission: &ActorAdmissionCertificateOf, + now: BlockNumberFor, + ) { + polkadot_sdk::frame_support::storage::with_transaction(|| { + match Pallet::::execute_zero_step_from_consumed_fixture(actor_id, state, admission, 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"); + } + + fn assert_zero_step_completion( + actor_id: ActorId, + actor_type: ActorType, + fee: T::Balance, + ) { + let identity = benchmark_fixture_identity::(actor_id).expect("zero-Step identity survives"); + assert_eq!(identity.cycle_nonce, 1); + assert!(ActorRunStateStore::::get(actor_id).is_none()); + assert!(benchmark_fixture_hot::(actor_id).is_some_and(|hot| { + hot.cycle_state == CycleState::Idle + && !hot.pending_signal + && hot.queue_ticket.is_none() + && hot.wakeup_pointer.is_none() + })); + let receipt: ::RuntimeEvent = + Event::::PipelineFeeCharged { actor_id, fee }.into(); + assert_eq!( + frame_system::Pallet::::events() + .iter() + .filter(|record| record.event == receipt) + .count(), + usize::from(actor_type == ActorType::User) + ); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("complete zero-Step state and User holds are coherent"); + } + + /// Persistent System zero-Step Opening after a real Manual occurrence and source consumption. + /// This complete System inner owner excludes User Pipeline collection and hold settlement. + #[benchmark(pov_mode = Measured)] + fn scheduler_inner_zero_step_complete() { + let actor_id = prepare_zero_step_opening::(ActorType::System); + let (state, admission) = consume_zero_step_opening::(actor_id); + let now = frame_system::Pallet::::block_number(); + #[block] + { + execute_zero_step_inner::(actor_id, state, &admission, now); + } + assert_zero_step_completion::(actor_id, ActorType::System, Zero::zero()); + } + + /// Complete User zero-Step inner owner, including Pipeline collection/receipt and state holds. + #[benchmark(pov_mode = Measured)] + fn scheduler_inner_zero_step_user_complete() { + let actor_id = prepare_zero_step_opening::(ActorType::User); + let hold_before = ActorStateHolds::::get(actor_id) + .expect("zero-Step User state hold exists") + .encode(); + let (state, admission) = consume_zero_step_opening::(actor_id); + let native = T::FeeNativeAssetId::get(); + let sovereign = state.identity.sovereign_account.clone(); + let sink = T::FeeSink::get(); + let payer_before = T::AssetOps::balance(&sovereign, native); + let sink_before = T::AssetOps::balance(&sink, native); + let fee = Pallet::::pipeline_fee_for_actor(actor_id, ActorType::User) + .expect("certified Pipeline fee exists") + .total_fee; + let now = frame_system::Pallet::::block_number(); + #[block] + { + execute_zero_step_inner::(actor_id, state, &admission, now); + } + assert_eq!(T::AssetOps::balance(&sovereign, native), payer_before - fee); + assert_eq!(T::AssetOps::balance(&sink, native), sink_before + fee); + assert_zero_step_completion::(actor_id, ActorType::User, fee); + assert_eq!( + ActorStateHolds::::get(actor_id).map(|hold| hold.encode()), + Some(hold_before), + "persistent zero-Step Opening preserves the installed state hold" + ); + } + + fn create_zero_step_system_follower() -> ActorId { + let owner: T::AccountId = account("crossing-unavailable-follower", 0, 0); + Pallet::::create_system_actor( + RawOrigin::Root.into(), + owner.clone(), + Mutability::Mutable, + system_contract::( + Schedule { + trigger: Trigger::manual(), + cooldown_blocks: 0, + }, + ContractSteps::::default(), + ), + ) + .unwrap(); + let follower = NextActorId::::get().saturating_sub(1); + Pallet::::manual_trigger(RawOrigin::Signed(owner).into(), follower).unwrap(); + frame_system::Pallet::::set_block_number( + Pallet::::actor_control_cell(follower) + .unwrap() + .1 + .eligible_at + .unwrap(), + ); + follower + } + + /// Full FIFO refusal when paid Crossing readiness loses its required authoritative value. + #[benchmark(pov_mode = Measured)] + fn scheduler_paged_zero_step_user_crossing_unavailable() { + let (actor_id, feed) = + prepare_zero_step_observation::(Some(CrossingPhase::WaitingForRearm), false); + let follower = create_zero_step_system_follower::(); + T::BenchmarkHelper::deactivate_observation_feed(feed).unwrap(); + assert!(matches!( + T::ObservationProvider::current(&feed), + CanonicalObservationState::Unavailable + )); + let state = Pallet::::active_actor_state(actor_id).unwrap(); + let encoded_state = state.encode(); + let head = ActorReadyHead::::get(); + let events = frame_system::Pallet::::events().encode(); + let membership = CrossingMemberships::::get(actor_id); + let holds = ActorStateHolds::::get(actor_id); + let before = user_pipeline_accounting::(actor_id); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("host unavailability is not Actor topology corruption"); + #[block] + { + let pass = Pallet::::execute_cycle(Weight::MAX); + assert!(!pass.consumed.is_zero()); + assert!(pass.effect_consumed.is_zero() && pass.starved); + } + assert_eq!( + Pallet::::active_actor_state(actor_id).unwrap().encode(), + encoded_state + ); + assert_eq!(ActorReadyHead::::get(), head); + assert_eq!(ActorReadyOccupancy::::get(), 2); + assert_eq!( + benchmark_fixture_identity::(follower) + .unwrap() + .cycle_nonce, + 0 + ); + assert_eq!(CrossingMemberships::::get(actor_id), membership); + assert_eq!(ActorStateHolds::::get(actor_id), holds); + assert_eq!(frame_system::Pallet::::events().encode(), events); + let native = T::FeeNativeAssetId::get(); + assert_eq!( + T::AssetOps::balance(&before.payer, native), + before.payer_balance + ); + assert_eq!( + T::AssetOps::balance(&T::FeeSink::get(), native), + before.sink_balance + ); + Pallet::::close_actor(RawOrigin::Signed(state.identity.owner).into(), actor_id).unwrap(); + Pallet::::execute_cycle(Weight::MAX); + assert!(benchmark_fixture_identity::(actor_id).is_none()); + assert!(!ActorStateHolds::::contains_key(actor_id)); + assert_eq!( + benchmark_fixture_identity::(follower) + .unwrap() + .cycle_nonce, + 1 + ); + assert_eq!(ActorReadyHead::::get(), ActorReadyTail::::get()); + assert_eq!( + T::AssetOps::balance(&before.payer, native), + before.payer_balance + ); + assert_eq!( + T::AssetOps::balance(&T::FeeSink::get(), native), + before.sink_balance + ); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("authorized Mutable close releases the unavailable head"); + } + + fn prepare_zero_step_user_insolvency( + crossing: bool, + ) -> (ActorId, UserPipelineAccounting) { + prepare_user_pipeline_insolvency::(crossing, ContractSteps::::default(), false) + } + + fn prepare_user_pipeline_insolvency( + crossing: bool, + steps: ContractSteps, + with_window: bool, + ) -> (ActorId, UserPipelineAccounting) { + frame_system::Pallet::::set_block_number(1u32.into()); + assert_eq!(ActorReadyOccupancy::::get(), 0); + let feed = crossing.then(|| { + T::BenchmarkHelper::setup_observation_feeds(1) + .unwrap() + .into_iter() + .next() + .unwrap() + }); + let (trigger, family, weight) = match feed { + Some(feed) => { + assert!(matches!( + T::ObservationProvider::current(&feed), + CanonicalObservationState::Available { value: 1, .. } + )); + ( + Trigger::observation_crossing(feed, CrossingDirection::Rising, 2, 1), + TriggerFamily::ObservationCrossing, + T::WeightInfo::observation_crossing_trigger_occurrence(), + ) + } + None => ( + Trigger::manual(), + TriggerFamily::Manual, + T::WeightInfo::manual_trigger(), + ), + }; + let owner: T::AccountId = account("zero-step-insolvent-owner", 0, 0); + ensure_creation_balance::(&owner); + let required = Pallet::::user_pipeline_machine_capacity_requirement(&steps).unwrap(); + prefund_active_user_creation::(&owner, &steps); + let mut contract = user_contract::( + Schedule { + trigger, + cooldown_blocks: 0, + }, + steps, + ) + .unwrap(); + if with_window { + let start = frame_system::Pallet::::block_number(); + contract.window = Some(ScheduleWindow { + start, + end: start.checked_add(&T::MinWindowLength::get()).unwrap(), + }); + } + Pallet::::create_user_actor( + RawOrigin::Signed(owner.clone()).into(), + Mutability::Mutable, + Some(contract), + ) + .expect("User Contract is ordinarily admitted before its paid Trigger"); + let actor_id = NextActorId::::get().saturating_sub(1); + let payer = benchmark_fixture_identity::(actor_id) + .unwrap() + .sovereign_account; + let native = T::FeeNativeAssetId::get(); + assert_eq!(T::AssetOps::balance(&payer, native), required); + let trigger_fee = + Pallet::::trigger_fee_for_weight(ActorType::User, family, weight).trigger_fee; + assert!(!trigger_fee.is_zero()); + T::AssetOps::mint(&payer, native, trigger_fee - One::one()).unwrap(); + let sink_before = T::AssetOps::balance(&T::FeeSink::get(), native); + match feed { + Some(feed) => { + frame_system::Pallet::::set_block_number(2u32.into()); + assert!(publish_zero_step_observation::(feed, 1_000) >= 2); + Pallet::::service_crossing_transitions(Weight::MAX); + assert!(CrossingMemberships::::contains_key(actor_id)); + assert!(IndexedTriggerDetectionDisabled::::contains_key(actor_id)); + } + None => { + Pallet::::manual_trigger(RawOrigin::Signed(owner).into(), actor_id).unwrap(); + } + } + let (_, cell) = Pallet::::actor_control_cell(actor_id).unwrap(); + assert!(cell.hot.pending_signal); + assert_eq!(cell.identity.cycle_nonce, 0); + frame_system::Pallet::::set_block_number(cell.eligible_at.unwrap()); + assert_eq!(ActorReadyOccupancy::::get(), 1); + assert_eq!(T::AssetOps::balance(&payer, native), required - One::one()); + assert_eq!( + T::AssetOps::balance(&T::FeeSink::get(), native), + sink_before + trigger_fee + ); + assert!(!Pallet::::pipeline_capacity_sufficient(actor_id, ActorType::User, &payer).unwrap()); + frame_system::Pallet::::assert_has_event( + Event::::TriggerOccurrenceProcessed { + actor_id, + trigger_family: family, + fee: trigger_fee, + } + .into(), + ); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("paid readiness may survive insufficient Pipeline capacity"); + (actor_id, user_pipeline_accounting::(actor_id)) + } + + fn assert_user_pipeline_insolvency( + actor_id: ActorId, + before: UserPipelineAccounting, + ) { + assert!(benchmark_fixture_identity::(actor_id).is_none()); + assert!(benchmark_fixture_hot::(actor_id).is_none()); + assert!(!ActorContractHeads::::contains_key(actor_id)); + assert!(!ActorStateHolds::::contains_key(actor_id)); + assert!(!ActorFunding::::contains_key(actor_id)); + assert!( + ActorContractTailChunks::::iter_prefix(actor_id) + .next() + .is_none() + ); + assert!(ActorRunStateStore::::get(actor_id).is_none()); + assert!(!CrossingMemberships::::contains_key(actor_id)); + assert!(!IndexedTriggerDetectionDisabled::::contains_key( + actor_id + )); + assert_eq!(ActorReadyOccupancy::::get(), 0); + assert_eq!(ActorReadyHead::::get(), ActorReadyTail::::get()); + let native = T::FeeNativeAssetId::get(); + assert_eq!( + T::AssetOps::balance(&before.payer, native), + before.payer_balance + ); + assert_eq!( + T::AssetOps::balance(&T::FeeSink::get(), native), + before.sink_balance + ); + let receipt: ::RuntimeEvent = Event::::PipelineFeeCharged { + actor_id, + fee: before.fee, + } + .into(); + assert!( + !frame_system::Pallet::::events() + .iter() + .any(|record| record.event == receipt) + ); + frame_system::Pallet::::assert_has_event( + Event::::ActorClosed { + actor_id, + reason: CloseReason::CycleAdmissionInsufficient, + } + .into(), + ); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("Pipeline apoptosis releases all process resources"); + } + + /// Direct cleanup-owner comparison with the existing Manual one-Step apoptosis fixture. + #[benchmark] + fn pipeline_admission_apoptosis_crossing() { + let (actor_id, before) = prepare_zero_step_user_insolvency::(true); + let instance = Pallet::::active_actor_view(actor_id).unwrap(); + let cutoff = ActorReadyTail::::get(); + #[block] + { + Pallet::::finalize_actor(actor_id, &instance, CloseReason::CycleAdmissionInsufficient) + .unwrap(); + } + assert_eq!(ActorReadyOccupancy::::get(), 0); + assert!(ActorReadyHead::::get() < cutoff); + benchmark_fixture_ready_drain_tombstones::(cutoff, 1) + .expect("outer FIFO retires the closed primary's tombstone after cleanup measurement"); + assert_user_pipeline_insolvency::(actor_id, before); + } + + /// Complete FIFO liability check and Manual User apoptosis one balance unit below admission. + #[benchmark(pov_mode = Measured)] + fn scheduler_paged_zero_step_user_insolvency() { + let (actor_id, before) = prepare_zero_step_user_insolvency::(false); + #[block] + { + let pass = Pallet::::execute_cycle(Weight::MAX); + assert!(pass.effect_consumed.is_zero() && !pass.starved); + } + assert_user_pipeline_insolvency::(actor_id, before); + } + + /// The same paid-readiness insolvency with retained Crossing cleanup inside the FIFO atom. + #[benchmark(pov_mode = Measured)] + fn scheduler_paged_zero_step_user_crossing_insolvency() { + let (actor_id, before) = prepare_zero_step_user_insolvency::(true); + #[block] + { + let pass = Pallet::::execute_cycle(Weight::MAX); + assert!(pass.effect_consumed.is_zero() && !pass.starved); + } + assert_user_pipeline_insolvency::(actor_id, before); + } + + fn prepare_max_contract_user_insolvency( + with_window: bool, + ) -> ( + ActorId, + UserPipelineAccounting, + Option>>, + ) { + let steps = make_max_contract_steps::(account("insolvent-contract-recipient", 0, 0)); + let (actor_id, before) = prepare_user_pipeline_insolvency::(true, steps, with_window); + assert_max_contract_geometry::(actor_id); + let hot = benchmark_fixture_hot::(actor_id).unwrap(); + let pointer = hot.wakeup_pointer; + assert_eq!(pointer.is_some(), with_window); + if let Some(pointer) = pointer { + let WakeupKey::Block(at) = pointer.block else { + panic!("window expiry has a Block reference") + }; + assert!(at > frame_system::Pallet::::block_number()); + assert!(Pallet::::wakeup_page_entry_matches(pointer, actor_id)); + assert_eq!(ActorWaitingOccupancies::::get(pointer.block), 1); + } + (actor_id, before, pointer) + } + + /// Full FIFO insolvency at the host Step-count maximum, including retained Crossing cleanup. + #[benchmark(pov_mode = Measured)] + fn scheduler_paged_user_crossing_insolvency_max_contract() { + let (actor_id, before, _) = prepare_max_contract_user_insolvency::(false); + #[block] + { + let pass = Pallet::::execute_cycle(Weight::MAX); + assert!(pass.effect_consumed.is_zero() && !pass.starved); + } + assert_user_pipeline_insolvency::(actor_id, before); + } + + /// The same maximum-Step Contract also retains one future window reference before apoptosis. + #[benchmark(pov_mode = Measured)] + fn scheduler_paged_user_crossing_insolvency_max_contract_window() { + let (actor_id, before, pointer) = prepare_max_contract_user_insolvency::(true); + let pointer = pointer.unwrap(); + #[block] + { + let pass = Pallet::::execute_cycle(Weight::MAX); + assert!(pass.effect_consumed.is_zero() && !pass.starved); + } + assert_user_pipeline_insolvency::(actor_id, before); + assert!(!Pallet::::wakeup_page_entry_matches(pointer, actor_id)); + assert!(!ActorWaitingHeads::::contains_key(pointer.block)); + assert!(!ActorWaitingTails::::contains_key(pointer.block)); + assert!(!ActorWaitingCursorIndices::::contains_key(pointer.block)); + } + + /// Complete single-key Block source consumption and expiry closure, excluding clock selection. + #[benchmark(pov_mode = Measured)] + fn scheduler_wakeup_zero_step_user_expiry() { + frame_system::Pallet::::set_block_number(1u32.into()); + let owner: T::AccountId = account("zero-step-window-owner", 0, 0); + ensure_creation_balance::(&owner); + let steps = ContractSteps::::default(); + prefund_active_user_creation::(&owner, &steps); + let start = frame_system::Pallet::::block_number(); + let end = start.saturating_add(T::MinWindowLength::get()); + let mut contract = user_contract::( + Schedule { + trigger: Trigger::manual(), + cooldown_blocks: 0, + }, + steps, + ) + .unwrap(); + contract.window = Some(ScheduleWindow { start, end }); + Pallet::::create_user_actor( + RawOrigin::Signed(owner).into(), + Mutability::Immutable, + Some(contract), + ) + .expect("Immutable zero-Step User window is ordinarily admitted"); + let actor_id = NextActorId::::get().saturating_sub(1); + let identity = benchmark_fixture_identity::(actor_id).unwrap(); + assert_eq!(identity.cycle_nonce, 0); + let hot = benchmark_fixture_hot::(actor_id).unwrap(); + assert!(!hot.pending_signal && hot.queue_ticket.is_none()); + let expired = end.saturating_add(1u32.into()); + let key = WakeupKey::Block(expired); + assert_eq!(hot.wakeup_pointer.unwrap().block, key); + assert_eq!(ActorWaitingOccupancies::::get(key), 1); + assert!(ActorStateHolds::::contains_key(actor_id)); + assert!(ActorRunStateStore::::get(actor_id).is_none()); + frame_system::Pallet::::set_block_number(end); + T::BenchmarkHelper::advance_to_scheduler_tick(expired.saturated_into()).unwrap(); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("ordinary expired window and host deadlines are coherent"); + let native = T::FeeNativeAssetId::get(); + let payer_before = T::AssetOps::balance(&identity.sovereign_account, native); + let sink = T::FeeSink::get(); + let sink_before = T::AssetOps::balance(&sink, native); + let fee = Pallet::::pipeline_fee_for_actor(actor_id, ActorType::User) + .unwrap() + .total_fee; + assert!(!fee.is_zero()); + #[block] + { + polkadot_sdk::frame_support::storage::with_transaction(|| { + let (due, stats) = Pallet::::wakeup_substrate_drain_key(key, 1); + assert_eq!(stats.entries_scanned, 1); + assert_eq!(due.len(), 1); + let (due_actor, state, admission, loaded_step) = due.into_iter().next().unwrap(); + assert_eq!(due_actor, actor_id); + assert!(loaded_step.is_none()); + let view = Pallet::::derive_active_actor_view( + state.identity.clone(), + state.hot.clone(), + state.contract.clone(), + ); + assert!( + Pallet::::expiry_substitution_due_loaded(&view, state.run_state.as_ref()).unwrap() + ); + Pallet::::finalize_actor_from_consumed_state( + actor_id, + state, + &admission, + CloseReason::WindowExpired, + ) + .unwrap(); + polkadot_sdk::frame_support::storage::TransactionOutcome::Commit( + Ok::<(), DispatchError>(()), + ) + }) + .unwrap(); + } + assert!(benchmark_fixture_identity::(actor_id).is_none()); + assert!(benchmark_fixture_hot::(actor_id).is_none()); + assert!(!ActorContractHeads::::contains_key(actor_id)); + assert!(!ActorStateHolds::::contains_key(actor_id)); + assert!(!ActorFunding::::contains_key(actor_id)); + assert!(ActorRunStateStore::::get(actor_id).is_none()); + assert!(!ActorWaitingOccupancies::::contains_key(key)); + assert_eq!( + T::AssetOps::balance(&identity.sovereign_account, native), + payer_before + ); + assert_eq!(T::AssetOps::balance(&sink, native), sink_before); + let receipt: ::RuntimeEvent = + Event::::PipelineFeeCharged { actor_id, fee }.into(); + assert!( + !frame_system::Pallet::::events() + .iter() + .any(|record| record.event == receipt) + ); + frame_system::Pallet::::assert_has_event( + Event::::ActorClosed { + actor_id, + reason: CloseReason::WindowExpired, + } + .into(), + ); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("window expiry releases its source and process resources"); + } + + fn prepare_zero_step_cadenced(close: bool) -> ActorId { + prepare_zero_step_temporal::(Trigger::cadenced(1), Mutability::Mutable, close) + } + + fn prepare_zero_step_temporal( + trigger: TriggerOf, + mutability: Mutability, + close: bool, + ) -> ActorId { + let family = trigger.family(); + let occurrence_weight = match family { + TriggerFamily::AtTime => T::WeightInfo::at_time_trigger_occurrence(), + TriggerFamily::Cadenced => T::WeightInfo::cadenced_trigger_occurrence(), + _ => panic!("temporal profile requires AtTime or Cadenced"), + }; + frame_system::Pallet::::set_block_number(1u32.into()); + let owner: T::AccountId = account("zero-step-cadenced-owner", 0, 0); + ensure_creation_balance::(&owner); + let steps = ContractSteps::::default(); + prefund_active_user_creation::(&owner, &steps); + let mut contract = user_contract::( + Schedule { + trigger, + cooldown_blocks: 0, + }, + steps.clone(), + ) + .unwrap(); + contract.auto_close_at_cycle_nonce = close.then_some(1); + Pallet::::create_user_actor(RawOrigin::Signed(owner).into(), mutability, Some(contract)) + .expect("zero-Step temporal User Contract is admitted"); + let actor_id = NextActorId::::get().saturating_sub(1); + let identity = benchmark_fixture_identity::(actor_id).unwrap(); + let extra = full_attempt_fee::(&steps) + .checked_add( + &Pallet::::trigger_fee_for_weight(ActorType::User, family, occurrence_weight) + .trigger_fee, + ) + .unwrap(); + T::AssetOps::mint( + &identity.sovereign_account, + T::FeeNativeAssetId::get(), + extra, + ) + .unwrap(); + let tick = benchmark_fixture_hot::(actor_id) + .unwrap() + .trigger_wakeup_pointer + .expect("creation installed a temporal primary") + .tick; + T::BenchmarkHelper::advance_to_scheduler_tick(tick).expect("host clock reaches the due tick"); + assert!(Pallet::::current_scheduler_tick().unwrap() >= tick); + let now = frame_system::Pallet::::block_number(); + let mut meter = polkadot_sdk::sp_weights::WeightMeter::with_limit(Weight::MAX); + Pallet::::drain_overdue_wakeups_cursor(now, &mut meter); + let (_, cell) = Pallet::::actor_control_cell(actor_id).expect("due Actor is Ready"); + assert!(cell.hot.pending_signal && cell.hot.trigger_wakeup_pointer.is_none()); + frame_system::Pallet::::set_block_number( + cell.eligible_at.expect("Ready eligibility exists"), + ); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("ordinary temporal readiness and holds are coherent"); + actor_id + } + + /// Complete Mutable User AtTime Opening retains a consumed process with no recurring deadline. + #[benchmark(pov_mode = Measured)] + fn scheduler_inner_zero_step_user_at_time() { + let actor_id = prepare_zero_step_temporal::(Trigger::at_time(1), Mutability::Mutable, false); + let before = user_pipeline_accounting::(actor_id); + let (state, admission) = consume_zero_step_opening::(actor_id); + let now = frame_system::Pallet::::block_number(); + #[block] + { + execute_zero_step_inner::(actor_id, state, &admission, now); + } + assert_user_pipeline_accounting::(actor_id, before); + let hot = benchmark_fixture_hot::(actor_id).unwrap(); + assert_eq!(hot.cycle_state, CycleState::Idle); + assert!(matches!( + hot.trigger_runtime_state, + TriggerRuntimeState::AtTime { consumed: true, .. } + )); + assert!(!hot.pending_signal && hot.queue_ticket.is_none()); + assert!(hot.trigger_wakeup_pointer.is_none() && hot.wakeup_pointer.is_none()); + assert_eq!( + benchmark_fixture_identity::(actor_id) + .unwrap() + .cycle_nonce, + 1 + ); + assert!(ActorRunStateStore::::get(actor_id).is_none()); + let payer = benchmark_fixture_identity::(actor_id) + .unwrap() + .sovereign_account; + let balance = T::AssetOps::balance(&payer, T::FeeNativeAssetId::get()); + let next_tick = Pallet::::current_scheduler_tick() + .unwrap() + .saturating_add(1); + T::BenchmarkHelper::finalize_scheduler_clock().unwrap(); + T::BenchmarkHelper::advance_to_scheduler_tick(next_tick).unwrap(); + let mut meter = polkadot_sdk::sp_weights::WeightMeter::with_limit(Weight::MAX); + Pallet::::drain_overdue_wakeups_cursor( + frame_system::Pallet::::block_number(), + &mut meter, + ); + assert_eq!(benchmark_fixture_hot::(actor_id).unwrap(), hot); + assert_eq!( + benchmark_fixture_identity::(actor_id) + .unwrap() + .cycle_nonce, + 1 + ); + assert_eq!( + T::AssetOps::balance(&payer, T::FeeNativeAssetId::get()), + balance + ); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("consumed Mutable AtTime has no recurring activation"); + } + + /// Complete Immutable User AtTime Opening obeys the mandatory authored nonce-one close. + #[benchmark(pov_mode = Measured)] + fn scheduler_inner_zero_step_user_at_time_close() { + let actor_id = + prepare_zero_step_temporal::(Trigger::at_time(1), Mutability::Immutable, true); + let before = user_pipeline_accounting::(actor_id); + let (state, admission) = consume_zero_step_opening::(actor_id); + let now = frame_system::Pallet::::block_number(); + #[block] + { + execute_zero_step_inner::(actor_id, state, &admission, now); + } + assert_user_pipeline_accounting::(actor_id, before); + assert!(benchmark_fixture_identity::(actor_id).is_none()); + assert!(!ActorContractHeads::::contains_key(actor_id)); + assert!(!ActorStateHolds::::contains_key(actor_id)); + assert!(ActorRunStateStore::::get(actor_id).is_none()); + frame_system::Pallet::::assert_has_event( + Event::::ActorClosed { + actor_id, + reason: CloseReason::AutoCloseNonceReached, + } + .into(), + ); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state() + .expect("authored Immutable AtTime close releases all process resources"); + } + + fn assert_zero_step_cadenced(actor_id: ActorId, close: bool, fee: T::Balance) { + assert!( + !fee.is_zero(), + "User collection profile requires a nonzero fee" + ); + assert!(ActorRunStateStore::::get(actor_id).is_none()); + if close { + assert!(benchmark_fixture_identity::(actor_id).is_none()); + assert!(!ActorContractHeads::::contains_key(actor_id)); + assert!(!ActorStateHolds::::contains_key(actor_id)); + frame_system::Pallet::::assert_has_event( + Event::::ActorClosed { + actor_id, + reason: CloseReason::AutoCloseNonceReached, + } + .into(), + ); + } else { + assert_eq!( + benchmark_fixture_identity::(actor_id) + .unwrap() + .cycle_nonce, + 1 + ); + let hot = benchmark_fixture_hot::(actor_id).expect("Cadenced process survives"); + assert_eq!(hot.cycle_state, CycleState::Idle); + assert!(!hot.pending_signal && hot.queue_ticket.is_none()); + let pointer = hot + .trigger_wakeup_pointer + .expect("Cadenced successor has a temporal pointer"); + assert!(pointer.tick > Pallet::::current_scheduler_tick().unwrap()); + assert!(Pallet::::wakeup_page_entry_matches( + WakeupPointer { + block: WakeupKey::Tick(pointer.tick), + page_id: pointer.page_id, + slot: pointer.slot, + }, + actor_id + )); + } + let receipt: ::RuntimeEvent = + Event::::PipelineFeeCharged { actor_id, fee }.into(); + assert_eq!( + frame_system::Pallet::::events() + .iter() + .filter(|record| record.event == receipt) + .count(), + 1 + ); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("Cadenced completion/close and holds are coherent"); + } + + /// Complete User zero-Step inner owner with ordinary due readiness and Tick rearm. + #[benchmark(pov_mode = Measured)] + fn scheduler_inner_zero_step_user_cadenced() { + let actor_id = prepare_zero_step_cadenced::(false); + let (state, admission) = consume_zero_step_opening::(actor_id); + let native = T::FeeNativeAssetId::get(); + let sovereign = state.identity.sovereign_account.clone(); + let sink = T::FeeSink::get(); + let payer_before = T::AssetOps::balance(&sovereign, native); + let sink_before = T::AssetOps::balance(&sink, native); + let fee = Pallet::::pipeline_fee_for_actor(actor_id, ActorType::User) + .unwrap() + .total_fee; + let now = frame_system::Pallet::::block_number(); + #[block] + { + execute_zero_step_inner::(actor_id, state, &admission, now); + } + assert_eq!(T::AssetOps::balance(&sovereign, native), payer_before - fee); + assert_eq!(T::AssetOps::balance(&sink, native), sink_before + fee); + assert_zero_step_cadenced::(actor_id, false, fee); + } + + /// Inclusive authored close: Cadenced rearm, Pipeline collection and custody-neutral cleanup. + #[benchmark(pov_mode = Measured)] + fn scheduler_inner_zero_step_user_cadenced_close() { + let actor_id = prepare_zero_step_cadenced::(true); + let (state, admission) = consume_zero_step_opening::(actor_id); + let native = T::FeeNativeAssetId::get(); + let sovereign = state.identity.sovereign_account.clone(); + let sink = T::FeeSink::get(); + let payer_before = T::AssetOps::balance(&sovereign, native); + let sink_before = T::AssetOps::balance(&sink, native); + let fee = Pallet::::pipeline_fee_for_actor(actor_id, ActorType::User) + .unwrap() + .total_fee; + let now = frame_system::Pallet::::block_number(); + #[block] + { + execute_zero_step_inner::(actor_id, state, &admission, now); + } + assert_eq!(T::AssetOps::balance(&sovereign, native), payer_before - fee); + assert_eq!(T::AssetOps::balance(&sink, native), sink_before + fee); + assert_zero_step_cadenced::(actor_id, true, fee); + } + + struct UserPipelineAccounting { + payer: T::AccountId, + payer_balance: T::Balance, + sink_balance: T::Balance, + fee: T::Balance, + } + + fn user_pipeline_accounting(actor_id: ActorId) -> UserPipelineAccounting { + let payer = benchmark_fixture_identity::(actor_id) + .unwrap() + .sovereign_account; + let native = T::FeeNativeAssetId::get(); + let fee = Pallet::::pipeline_fee_for_actor(actor_id, ActorType::User) + .unwrap() + .total_fee; + assert!( + !fee.is_zero(), + "User profile requires a nonzero Pipeline fee" + ); + UserPipelineAccounting { + payer_balance: T::AssetOps::balance(&payer, native), + sink_balance: T::AssetOps::balance(&T::FeeSink::get(), native), + payer, + fee, + } + } + + fn assert_user_pipeline_accounting( + actor_id: ActorId, + before: UserPipelineAccounting, + ) { + let native = T::FeeNativeAssetId::get(); + assert_eq!( + T::AssetOps::balance(&before.payer, native), + before.payer_balance - before.fee + ); + assert_eq!( + T::AssetOps::balance(&T::FeeSink::get(), native), + before.sink_balance + before.fee + ); + let receipt: ::RuntimeEvent = Event::::PipelineFeeCharged { + actor_id, + fee: before.fee, + } + .into(); + assert_eq!( + frame_system::Pallet::::events() + .iter() + .filter(|record| record.event == receipt) + .count(), + 1 + ); + } + + fn publish_zero_step_observation(feed: T::ObservationFeedId, sample: u128) -> u128 { + let CanonicalObservationState::Available { + value: previous, + revision, + } = T::ObservationProvider::current(&feed) + else { + panic!("initial observation exists") + }; + T::BenchmarkHelper::publish_observation(feed, sample) + .expect("host observation publication succeeds"); + let CanonicalObservationState::Available { + value, + revision: published_revision, + } = T::ObservationProvider::current(&feed) + else { + panic!("published observation exists") + }; + assert_eq!( + published_revision, + revision.checked_add(u64::from(value != previous)).unwrap() + ); + value + } + + fn publish_observation_at_most(feed: T::ObservationFeedId, ceiling: u128) { + let CanonicalObservationState::Available { + value: mut current, .. + } = T::ObservationProvider::current(&feed) + else { + panic!("initial observation exists") + }; + for _ in 0..T::MaxCrossingTransitionsPerBlock::get() { + if current <= ceiling { + return; + } + let next = publish_zero_step_observation::(feed, ceiling); + assert!( + next < current, + "host aggregation approaches the rearm boundary" + ); + current = next; + } + assert!( + current <= ceiling, + "bounded real publications reach the rearm boundary" + ); + } + + fn prepare_zero_step_observation( + crossing_phase: Option, + close: bool, + ) -> (ActorId, T::ObservationFeedId) { + prepare_zero_step_observation_at_value::(crossing_phase, close, 2, 1) + } + + fn create_zero_step_observation_user( + feed: T::ObservationFeedId, + crossing_phase: Option, + close: bool, + threshold: u128, + ) -> (ActorId, T::AccountId) { + let owner: T::AccountId = account("zero-step-observation-owner", 0, 0); + ensure_creation_balance::(&owner); + let steps = ContractSteps::::default(); + prefund_active_user_creation::(&owner, &steps); + let (trigger, family, trigger_weight) = if crossing_phase.is_some() { + ( + Trigger::observation_crossing(feed, CrossingDirection::Rising, threshold, 1), + TriggerFamily::ObservationCrossing, + T::WeightInfo::observation_crossing_trigger_occurrence(), + ) + } else { + ( + Trigger::observation_change(feed), + TriggerFamily::ObservationChange, + T::WeightInfo::observation_change_trigger_occurrence(), + ) + }; + let mut contract = user_contract::( + Schedule { + trigger, + cooldown_blocks: 0, + }, + steps.clone(), + ) + .unwrap(); + contract.auto_close_at_cycle_nonce = close.then_some(1); + Pallet::::create_user_actor( + RawOrigin::Signed(owner).into(), + Mutability::Mutable, + Some(contract), + ) + .expect("zero-Step observation User Contract is admitted"); + let actor_id = NextActorId::::get().saturating_sub(1); + let payer = benchmark_fixture_identity::(actor_id) + .unwrap() + .sovereign_account; + let extra = full_attempt_fee::(&steps) + .checked_add( + &Pallet::::trigger_fee_for_weight(ActorType::User, family, trigger_weight).trigger_fee, + ) + .unwrap(); + T::AssetOps::mint(&payer, T::FeeNativeAssetId::get(), extra).unwrap(); + (actor_id, payer) + } + + fn prepare_zero_step_observation_at_value( + crossing_phase: Option, + close: bool, + threshold: u128, + rearm_value: u128, + ) -> (ActorId, T::ObservationFeedId) { + frame_system::Pallet::::set_block_number(1u32.into()); + let feed = T::BenchmarkHelper::setup_observation_feeds(1) + .unwrap() + .into_iter() + .next() + .unwrap(); + assert!(matches!( + T::ObservationProvider::current(&feed), + CanonicalObservationState::Available { value: 1, .. } + )); + let (actor_id, payer) = + create_zero_step_observation_user::(feed, crossing_phase, close, threshold); + frame_system::Pallet::::set_block_number(2u32.into()); + const CROSSING_SAMPLE: u128 = 1_000; + let mut current = publish_zero_step_observation::(feed, CROSSING_SAMPLE); + assert!( + (threshold..=CROSSING_SAMPLE).contains(¤t), + "host aggregation crosses the authored threshold" + ); + if crossing_phase.is_some() { + Pallet::::service_crossing_transitions(Weight::MAX); + } else { + Pallet::::do_fanout_dirty_observation_page() + .expect("published observation reaches its subscriber"); + } + assert!(benchmark_fixture_hot::(actor_id).unwrap().pending_signal); + assert!(IndexedTriggerDetectionDisabled::::contains_key(actor_id)); + if crossing_phase == Some(CrossingPhase::Armed) { + let balance = T::AssetOps::balance(&payer, T::FeeNativeAssetId::get()); + frame_system::Pallet::::set_block_number( + frame_system::Pallet::::block_number().saturating_add(1u32.into()), + ); + for _ in 0..T::MaxCrossingTransitionsPerBlock::get() { + if current <= rearm_value { + break; + } + let next = publish_zero_step_observation::(feed, rearm_value); + assert!( + next < current, + "host aggregation approaches the qualified Opening rearm value" + ); + current = next; + } + assert_eq!( + current, rearm_value, + "real publication reaches the qualified Opening rearm value" + ); + Pallet::::service_crossing_transitions(Weight::MAX); + assert_eq!( + T::AssetOps::balance(&payer, T::FeeNativeAssetId::get()), + balance + ); + assert!(IndexedTriggerDetectionDisabled::::contains_key(actor_id)); + } + let (_, cell) = Pallet::::actor_control_cell(actor_id) + .expect("published observation creates Ready primary"); + assert!(cell.hot.pending_signal); + frame_system::Pallet::::set_block_number( + cell.eligible_at.expect("Ready eligibility exists"), + ); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("published observation readiness is coherent"); + (actor_id, feed) + } + + fn assert_zero_step_observation( + actor_id: ActorId, + feed: T::ObservationFeedId, + crossing_phase: Option, + close: bool, + ) { + assert!(ActorRunStateStore::::get(actor_id).is_none()); + assert!(!IndexedTriggerDetectionDisabled::::contains_key( + actor_id + )); + if close { + assert!(benchmark_fixture_identity::(actor_id).is_none()); + assert!(!ActorContractHeads::::contains_key(actor_id)); + assert!(!ActorStateHolds::::contains_key(actor_id)); + assert!(!CrossingMemberships::::contains_key(actor_id)); + assert!(!ObservationSubscriptionSlot::::contains_key(actor_id)); + frame_system::Pallet::::assert_has_event( + Event::::ActorClosed { + actor_id, + reason: CloseReason::AutoCloseNonceReached, + } + .into(), + ); + } else { + assert_eq!( + benchmark_fixture_identity::(actor_id) + .unwrap() + .cycle_nonce, + 1 + ); + let hot = benchmark_fixture_hot::(actor_id).unwrap(); + assert_eq!(hot.cycle_state, CycleState::Idle); + assert!(!hot.pending_signal && hot.queue_ticket.is_none()); + if let Some(expected) = crossing_phase { + let CanonicalObservationState::Available { revision, .. } = + T::ObservationProvider::current(&feed) + else { + panic!("rearm retains canonical observation") + }; + assert!( + matches!(hot.trigger_runtime_state, TriggerRuntimeState::ObservationCrossing { + phase, installed_at_revision, + } if phase == expected && installed_at_revision == revision) + ); + assert!(CrossingMemberships::::contains_key(actor_id)); + } else { + assert!(ObservationSubscriptionSlot::::contains_key(actor_id)); + } + } + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("observation zero-Step successor and holds are coherent"); + } + + fn create_zero_step_crossing_guard( + index: u32, + feed: T::ObservationFeedId, + threshold: u128, + ) -> ActorId { + let owner: T::AccountId = account("zero-step-crossing-page", index, 0); + Pallet::::create_system_actor( + RawOrigin::Root.into(), + owner, + Mutability::Mutable, + system_contract::( + Schedule { + trigger: Trigger::observation_crossing(feed, CrossingDirection::Rising, threshold, 1), + cooldown_blocks: 0, + }, + ContractSteps::::default(), + ), + ) + .expect("ordinary zero-Step guard is admitted"); + let actor_id = NextActorId::::get().saturating_sub(1); + assert!(!benchmark_fixture_hot::(actor_id).unwrap().pending_signal); + actor_id + } + + fn prepare_zero_step_crossing_page( + close: bool, + tail_members: u32, + ) -> (ActorId, T::ObservationFeedId, ActorId) { + let (actor_id, feed) = + prepare_zero_step_observation::(Some(CrossingPhase::WaitingForRearm), close); + let target = CrossingMemberships::::get(actor_id).unwrap(); + assert_eq!((target.page, target.offset), (0, 0)); + let mut tail_actor = actor_id; + assert!((1..=T::CrossingPageSize::get()).contains(&tail_members)); + for index in 0..T::CrossingPageSize::get() + tail_members - 1 { + tail_actor = create_zero_step_crossing_guard::(index, feed, 2); + let guard = CrossingMemberships::::get(tail_actor).unwrap(); + assert_eq!(guard.key, target.key); + } + let leaf = CrossingLeafStates::::get(target.key).unwrap(); + assert_eq!((leaf.page_count, leaf.tail_page), (2, 1)); + assert_eq!(leaf.member_count, T::CrossingPageSize::get() + tail_members); + assert_eq!( + CrossingMemberPages::::get(target.key, 0) + .unwrap() + .entries + .len() as u32, + T::CrossingPageSize::get() + ); + let tail = CrossingMemberships::::get(tail_actor).unwrap(); + assert_eq!((tail.page, tail.offset), (1, tail_members - 1)); + assert_eq!( + CrossingMemberPages::::get(target.key, 1) + .unwrap() + .entries + .len() as u32, + tail_members + ); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("full source page and qualified tail are coherent"); + (actor_id, feed, tail_actor) + } + + fn assert_zero_step_crossing_page( + actor_id: ActorId, + feed: T::ObservationFeedId, + tail_actor: ActorId, + close: bool, + tail_members: u32, + ) { + assert_zero_step_observation::(actor_id, feed, Some(CrossingPhase::WaitingForRearm), close); + let moved = CrossingMemberships::::get(tail_actor).unwrap(); + assert_eq!((moved.page, moved.offset), (0, 0)); + let page = CrossingMemberPages::::get(moved.key, 0).unwrap(); + assert_eq!(page.entries.len() as u32, T::CrossingPageSize::get()); + assert_eq!(page.entries[0].actor_id, tail_actor); + let leaf = CrossingLeafStates::::get(moved.key).unwrap(); + assert_eq!( + leaf.member_count, + T::CrossingPageSize::get() + tail_members - u32::from(close) + ); + assert_eq!( + leaf.page_count, + if close && tail_members == 1 { 1 } else { 2 } + ); + if close && tail_members == 1 { + assert!(!CrossingMemberPages::::contains_key(moved.key, 1)); + } else if close { + assert_eq!( + CrossingMemberPages::::get(moved.key, 1) + .unwrap() + .entries + .len() as u32, + tail_members - 1 + ); + } else { + let target = CrossingMemberships::::get(actor_id).unwrap(); + assert_eq!(target.key, moved.key); + assert_eq!((target.page, target.offset), (1, tail_members - 1)); + let tail = CrossingMemberPages::::get(target.key, 1).unwrap(); + assert_eq!(tail.entries.len() as u32, tail_members); + assert_eq!(tail.entries.last().unwrap().actor_id, actor_id); + } + assert_eq!( + benchmark_fixture_identity::(tail_actor) + .unwrap() + .cycle_nonce, + 0 + ); + let hot = benchmark_fixture_hot::(tail_actor).unwrap(); + assert!(!hot.pending_signal && hot.queue_ticket.is_none()); + } + + fn prepare_zero_step_crossing_armed_pages( + close: bool, + allocate_page: bool, + ) -> (ActorId, T::ObservationFeedId, ActorId, ActorId) { + let (actor_id, feed) = + prepare_zero_step_observation_at_value::(Some(CrossingPhase::Armed), close, 4, 2); + let source = CrossingMemberships::::get(actor_id).unwrap(); + assert_eq!((source.page, source.offset), (0, 0)); + let page_size = T::CrossingPageSize::get(); + assert!(page_size > 1); + let mut moved_actor = actor_id; + for index in 0..2 * page_size - 1 { + moved_actor = create_zero_step_crossing_guard::(index, feed, 2); + assert_eq!( + CrossingMemberships::::get(moved_actor).unwrap().key, + source.key + ); + } + let source_leaf = CrossingLeafStates::::get(source.key).unwrap(); + assert_eq!( + (source_leaf.member_count, source_leaf.page_count), + (2 * page_size, 2) + ); + for page in 0..2 { + assert_eq!( + CrossingMemberPages::::get(source.key, page) + .unwrap() + .entries + .len() as u32, + page_size + ); + } + let moved = CrossingMemberships::::get(moved_actor).unwrap(); + assert_eq!((moved.page, moved.offset), (1, page_size - 1)); + let destination_members = page_size - u32::from(!allocate_page); + let mut destination_actor = actor_id; + for index in 0..destination_members { + destination_actor = create_zero_step_crossing_guard::(2 * page_size + index, feed, 4); + } + let destination = CrossingMemberships::::get(destination_actor).unwrap(); + assert_ne!(destination.key, source.key); + let leaf = CrossingLeafStates::::get(destination.key).unwrap(); + assert_eq!( + (leaf.member_count, leaf.page_count), + (destination_members, 1) + ); + assert_eq!( + (destination.page, destination.offset), + (0, destination_members - 1) + ); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("ordinary guards qualify both Crossing phase leaves"); + (actor_id, feed, moved_actor, destination_actor) + } + + fn assert_zero_step_crossing_armed_pages( + actor_id: ActorId, + feed: T::ObservationFeedId, + moved_actor: ActorId, + destination_actor: ActorId, + close: bool, + allocate_page: bool, + ) { + assert_zero_step_observation::(actor_id, feed, Some(CrossingPhase::Armed), close); + let page_size = T::CrossingPageSize::get(); + let moved = CrossingMemberships::::get(moved_actor).unwrap(); + assert_eq!((moved.page, moved.offset), (0, 0)); + let source_leaf = CrossingLeafStates::::get(moved.key).unwrap(); + assert_eq!( + (source_leaf.member_count, source_leaf.page_count), + (2 * page_size - 1, 2) + ); + assert_eq!( + CrossingMemberPages::::get(moved.key, 1) + .unwrap() + .entries + .len() as u32, + page_size - 1 + ); + let destination = CrossingMemberships::::get(destination_actor).unwrap(); + let initial_members = page_size - u32::from(!allocate_page); + assert_eq!( + (destination.page, destination.offset), + (0, initial_members - 1) + ); + let leaf = CrossingLeafStates::::get(destination.key).unwrap(); + assert_eq!(leaf.member_count, initial_members + u32::from(!close)); + assert_eq!(leaf.page_count, if allocate_page && !close { 2 } else { 1 }); + if close { + assert!(!CrossingMemberPages::::contains_key(destination.key, 1)); + } else { + let target = CrossingMemberships::::get(actor_id).unwrap(); + assert_eq!(target.key, destination.key); + assert_eq!( + (target.page, target.offset), + if allocate_page { + (1, 0) + } else { + (0, page_size - 1) + } + ); + } + for guard in [moved_actor, destination_actor] { + assert_eq!( + benchmark_fixture_identity::(guard).unwrap().cycle_nonce, + 0 + ); + let hot = benchmark_fixture_hot::(guard).unwrap(); + assert!(!hot.pending_signal && hot.queue_ticket.is_none()); + } + } + + struct ZeroStepCrossingCursor { + actor_id: ActorId, + feed: T::ObservationFeedId, + moved_actor: ActorId, + destination_actor: ActorId, + cursor_before: CrossingRangeCursor, + source_members_before: u32, + } + + fn prepare_zero_step_crossing_cursor(close: bool) -> ZeroStepCrossingCursor { + let (actor_id, feed, _, destination_actor) = + prepare_zero_step_crossing_armed_pages::(close, true); + frame_system::Pallet::::set_block_number( + frame_system::Pallet::::block_number().saturating_add(1u32.into()), + ); + assert_eq!(publish_zero_step_observation::(feed, 1), 1); + let (_, counters) = Pallet::::service_crossing_transitions_with_counters(Weight::MAX); + assert_eq!( + (counters.faults, counters.activations, counters.closes), + (0, 0, 0) + ); + assert!( + counters.candidates > 1, + "bounded worker skips the latched target and rearms guards" + ); + let source = CrossingMemberships::::get(actor_id).unwrap(); + let cursor_before = + CrossingRangeCursors::::get(feed).expect("bounded worker retains live traversal"); + assert_eq!(cursor_before.current_threshold, Some(source.key.threshold)); + assert_eq!(cursor_before.traversal, source.key.traversal); + assert!((source.page, source.offset) < (cursor_before.page, cursor_before.offset)); + let leaf = CrossingLeafStates::::get(source.key).unwrap(); + assert_eq!( + leaf.member_count, + 2 * T::CrossingPageSize::get() - (counters.candidates - 1) + ); + assert_eq!(leaf.page_count, 2); + let tail = CrossingMemberPages::::get(source.key, leaf.tail_page).unwrap(); + let tail_offset = tail.entries.len() as u32 - 1; + assert!((leaf.tail_page, tail_offset) >= (cursor_before.page, cursor_before.offset)); + let moved_actor = tail.entries.last().unwrap().actor_id; + assert_ne!(moved_actor, actor_id); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state() + .expect("real bounded worker leaves a coherent repairable range cursor"); + ZeroStepCrossingCursor { + actor_id, + feed, + moved_actor, + destination_actor, + cursor_before, + source_members_before: leaf.member_count, + } + } + + fn assert_zero_step_crossing_cursor(fixture: &ZeroStepCrossingCursor, close: bool) { + assert_zero_step_observation::( + fixture.actor_id, + fixture.feed, + Some(CrossingPhase::Armed), + close, + ); + assert_zero_step_crossing_cursor_topology::(fixture, close); + } + + fn assert_zero_step_crossing_cursor_topology( + fixture: &ZeroStepCrossingCursor, + close: bool, + ) { + let moved = CrossingMemberships::::get(fixture.moved_actor).unwrap(); + assert_eq!((moved.page, moved.offset), (0, 0)); + let mut expected_cursor = fixture.cursor_before; + expected_cursor.page = moved.page; + expected_cursor.offset = moved.offset; + assert_eq!( + CrossingRangeCursors::::get(fixture.feed), + Some(expected_cursor) + ); + assert_eq!( + CrossingLeafStates::::get(moved.key) + .unwrap() + .member_count, + fixture.source_members_before - 1 + ); + let destination = CrossingMemberships::::get(fixture.destination_actor).unwrap(); + let leaf = CrossingLeafStates::::get(destination.key).unwrap(); + assert_eq!( + leaf.member_count, + T::CrossingPageSize::get() + u32::from(!close) + ); + assert_eq!(leaf.page_count, if close { 1 } else { 2 }); + if close { + assert!(!CrossingMemberPages::::contains_key(destination.key, 1)); + } else { + let target = CrossingMemberships::::get(fixture.actor_id).unwrap(); + assert_eq!(target.key, destination.key); + assert_eq!((target.page, target.offset), (1, 0)); + } + for guard in [fixture.moved_actor, fixture.destination_actor] { + assert_eq!( + benchmark_fixture_identity::(guard).unwrap().cycle_nonce, + 0 + ); + let hot = benchmark_fixture_hot::(guard).unwrap(); + assert!(!hot.pending_signal && hot.queue_ticket.is_none()); + } + frame_system::Pallet::::set_block_number( + frame_system::Pallet::::block_number().saturating_add(1u32.into()), + ); + let (_, resumed) = Pallet::::service_crossing_transitions_with_counters(Weight::MAX); + assert_eq!( + (resumed.faults, resumed.activations, resumed.closes), + (0, 0, 0) + ); + let repaired_member = CrossingMemberships::::get(fixture.moved_actor).unwrap(); + assert_ne!( + repaired_member.key, moved.key, + "resumed traversal does not skip the moved guard" + ); + let hot = benchmark_fixture_hot::(fixture.moved_actor).unwrap(); + assert!(matches!( + hot.trigger_runtime_state, + TriggerRuntimeState::ObservationCrossing { + phase: CrossingPhase::Armed, + .. + } + )); + assert!(!hot.pending_signal && hot.queue_ticket.is_none()); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state() + .expect("resumed traversal preserves the repaired guard and all survivors"); + } + + /// Complete User Opening repairs a live range cursor after non-tail membership removal. + #[benchmark(pov_mode = Measured)] + fn scheduler_inner_zero_step_user_crossing_cursor() { + let fixture = prepare_zero_step_crossing_cursor::(false); + let before = user_pipeline_accounting::(fixture.actor_id); + let (state, admission) = consume_zero_step_opening::(fixture.actor_id); + let now = frame_system::Pallet::::block_number(); + #[block] + { + execute_zero_step_inner::(fixture.actor_id, state, &admission, now); + } + assert_user_pipeline_accounting::(fixture.actor_id, before); + assert_zero_step_crossing_cursor::(&fixture, false); + } + + /// Complete authored close preserves the repaired live range cursor and surviving guards. + #[benchmark(pov_mode = Measured)] + fn scheduler_inner_zero_step_user_crossing_cursor_close() { + let fixture = prepare_zero_step_crossing_cursor::(true); + let before = user_pipeline_accounting::(fixture.actor_id); + let (state, admission) = consume_zero_step_opening::(fixture.actor_id); + let now = frame_system::Pallet::::block_number(); + #[block] + { + execute_zero_step_inner::(fixture.actor_id, state, &admission, now); + } + assert_user_pipeline_accounting::(fixture.actor_id, before); + assert_zero_step_crossing_cursor::(&fixture, true); + } + + /// Complete User Armed rearm fills an existing destination page after full source/tail rewrite. + #[benchmark(pov_mode = Measured)] + fn scheduler_inner_zero_step_user_crossing_armed_pages() { + let (actor_id, feed, moved, destination) = + prepare_zero_step_crossing_armed_pages::(false, false); + let before = user_pipeline_accounting::(actor_id); + let (state, admission) = consume_zero_step_opening::(actor_id); + let now = frame_system::Pallet::::block_number(); + #[block] + { + execute_zero_step_inner::(actor_id, state, &admission, now); + } + assert_user_pipeline_accounting::(actor_id, before); + assert_zero_step_crossing_armed_pages::(actor_id, feed, moved, destination, false, false); + } + + /// Complete User Armed rearm allocates behind a full destination page. + #[benchmark(pov_mode = Measured)] + fn scheduler_inner_zero_step_user_crossing_armed_new_page() { + let (actor_id, feed, moved, destination) = + prepare_zero_step_crossing_armed_pages::(false, true); + let before = user_pipeline_accounting::(actor_id); + let (state, admission) = consume_zero_step_opening::(actor_id); + let now = frame_system::Pallet::::block_number(); + #[block] + { + execute_zero_step_inner::(actor_id, state, &admission, now); + } + assert_user_pipeline_accounting::(actor_id, before); + assert_zero_step_crossing_armed_pages::(actor_id, feed, moved, destination, false, true); + } + + /// Complete authored close includes Armed rearm into an existing destination page. + #[benchmark(pov_mode = Measured)] + fn scheduler_inner_zero_step_user_crossing_armed_pages_close() { + let (actor_id, feed, moved, destination) = + prepare_zero_step_crossing_armed_pages::(true, false); + let before = user_pipeline_accounting::(actor_id); + let (state, admission) = consume_zero_step_opening::(actor_id); + let now = frame_system::Pallet::::block_number(); + #[block] + { + execute_zero_step_inner::(actor_id, state, &admission, now); + } + assert_user_pipeline_accounting::(actor_id, before); + assert_zero_step_crossing_armed_pages::(actor_id, feed, moved, destination, true, false); + } + + /// Complete authored close reclaims the destination page allocated by Armed rearm. + #[benchmark(pov_mode = Measured)] + fn scheduler_inner_zero_step_user_crossing_armed_new_page_close() { + let (actor_id, feed, moved, destination) = + prepare_zero_step_crossing_armed_pages::(true, true); + let before = user_pipeline_accounting::(actor_id); + let (state, admission) = consume_zero_step_opening::(actor_id); + let now = frame_system::Pallet::::block_number(); + #[block] + { + execute_zero_step_inner::(actor_id, state, &admission, now); + } + assert_user_pipeline_accounting::(actor_id, before); + assert_zero_step_crossing_armed_pages::(actor_id, feed, moved, destination, true, true); + } + + /// Full source-page refill, singleton-tail removal and reallocation during complete User rearm. + #[benchmark(pov_mode = Measured)] + fn scheduler_inner_zero_step_user_crossing_page() { + let (actor_id, feed, tail_actor) = prepare_zero_step_crossing_page::(false, 1); + let before = user_pipeline_accounting::(actor_id); + let (state, admission) = consume_zero_step_opening::(actor_id); + let now = frame_system::Pallet::::block_number(); + #[block] + { + execute_zero_step_inner::(actor_id, state, &admission, now); + } + assert_user_pipeline_accounting::(actor_id, before); + assert_zero_step_crossing_page::(actor_id, feed, tail_actor, false, 1); + } + + /// Complete authored close after full-page Crossing rearm, retaining every guard Actor. + #[benchmark(pov_mode = Measured)] + fn scheduler_inner_zero_step_user_crossing_page_close() { + let (actor_id, feed, tail_actor) = prepare_zero_step_crossing_page::(true, 1); + let before = user_pipeline_accounting::(actor_id); + let (state, admission) = consume_zero_step_opening::(actor_id); + let now = frame_system::Pallet::::block_number(); + #[block] + { + execute_zero_step_inner::(actor_id, state, &admission, now); + } + assert_user_pipeline_accounting::(actor_id, before); + assert_zero_step_crossing_page::(actor_id, feed, tail_actor, true, 1); + } + + /// Complete User rearm rewrites two full Crossing member pages and the moved inverse locator. + #[benchmark(pov_mode = Measured)] + fn scheduler_inner_zero_step_user_crossing_page_full() { + let members = T::CrossingPageSize::get(); + let (actor_id, feed, tail_actor) = prepare_zero_step_crossing_page::(false, members); + let before = user_pipeline_accounting::(actor_id); + let (state, admission) = consume_zero_step_opening::(actor_id); + let now = frame_system::Pallet::::block_number(); + #[block] + { + execute_zero_step_inner::(actor_id, state, &admission, now); + } + assert_user_pipeline_accounting::(actor_id, before); + assert_zero_step_crossing_page::(actor_id, feed, tail_actor, false, members); + } + + /// Complete authored close after full source/tail-page rearm, preserving the surviving tail. + #[benchmark(pov_mode = Measured)] + fn scheduler_inner_zero_step_user_crossing_page_full_close() { + let members = T::CrossingPageSize::get(); + let (actor_id, feed, tail_actor) = prepare_zero_step_crossing_page::(true, members); + let before = user_pipeline_accounting::(actor_id); + let (state, admission) = consume_zero_step_opening::(actor_id); + let now = frame_system::Pallet::::block_number(); + #[block] + { + execute_zero_step_inner::(actor_id, state, &admission, now); + } + assert_user_pipeline_accounting::(actor_id, before); + assert_zero_step_crossing_page::(actor_id, feed, tail_actor, true, members); + } + + fn create_zero_step_observation_guard( + index: u32, + feed: T::ObservationFeedId, + ) -> ActorId { + let owner: T::AccountId = account("zero-step-observation-page", index, 0); + Pallet::::create_system_actor( + RawOrigin::Root.into(), + owner, + Mutability::Mutable, + system_contract::( + Schedule { + trigger: Trigger::observation_change(feed), + cooldown_blocks: 0, + }, + ContractSteps::::default(), + ), + ) + .expect("ordinary zero-Step observation guard is admitted"); + NextActorId::::get().saturating_sub(1) + } + + /// Complete User close unlinks the current middle dirty feed and allocates a new free-slot page. + #[benchmark(pov_mode = Measured)] + fn scheduler_inner_zero_step_user_observation_feed_close() { + frame_system::Pallet::::set_block_number(1u32.into()); + let feeds = observation_feed_pool::(3); + let (previous_feed, feed, next_feed) = (feeds[0], feeds[1], feeds[2]); + let page_size = T::ObservationPageSize::get(); + assert!(page_size > 1 && T::MaxObservationFanoutPagesPerBlock::get() >= 2); + assert_eq!(NextObservationSubscriptionSlot::::get(), 0); + assert_eq!(ObservationFreeSlotLen::::get(), 0); + let mut guards = Vec::new(); + for index in 0..2 * page_size - 1 { + guards.push(create_zero_step_observation_guard::( + index, + previous_feed, + )); + } + let (actor_id, _) = create_zero_step_observation_user::(feed, None, true, 2); + let slot = ObservationSubscriptionSlot::::get(actor_id).unwrap(); + assert_eq!(slot, 2 * page_size - 1); + guards.push(create_zero_step_observation_guard::( + 2 * page_size, + next_feed, + )); + let mut donors = Vec::new(); + for index in 0..page_size { + donors.push(create_zero_step_observation_guard::( + 3 * page_size + index, + previous_feed, + )); + } + frame_system::Pallet::::set_block_number(2u32.into()); + for guard in &guards { + Pallet::::pause_actor(RawOrigin::Root.into(), *guard).unwrap(); + } + for donor in donors { + Pallet::::close_actor(RawOrigin::Root.into(), donor).unwrap(); + } + assert_eq!(ObservationFreeSlotLen::::get(), page_size); + let full_free_page = ObservationFreeSlotPages::::get(0).unwrap(); + assert_eq!(full_free_page.len() as u32, page_size); + assert!(!ObservationFreeSlotPages::::contains_key(1)); + assert_eq!( + ObservationSubscriberPageLists::::get(previous_feed) + .unwrap() + .count, + 2 + ); + frame_system::Pallet::::set_block_number(3u32.into()); + let first_value = publish_zero_step_observation::(feed, 1_000); + Pallet::::do_fanout_dirty_observation_page().unwrap(); + assert!(benchmark_fixture_hot::(actor_id).unwrap().pending_signal); + assert_eq!(DirtyObservationListState::::get().count, 0); + frame_system::Pallet::::set_block_number(4u32.into()); + publish_zero_step_observation::(previous_feed, 1_000); + assert_ne!(publish_zero_step_observation::(feed, 1), first_value); + publish_zero_step_observation::(next_feed, 1_000); + Pallet::::do_fanout_dirty_observation_page().unwrap(); + let list = DirtyObservationListState::::get(); + assert_eq!( + (list.head, list.tail, list.cursor, list.count), + (Some(previous_feed), Some(next_feed), Some(feed), 3) + ); + let target_dirty = DirtyObservationFeeds::::get(feed).unwrap(); + assert_eq!( + ( + target_dirty.previous_dirty_feed, + target_dirty.next_dirty_feed + ), + (Some(previous_feed), Some(next_feed)) + ); + let previous_before = DirtyObservationFeeds::::get(previous_feed).unwrap(); + let next_before = DirtyObservationFeeds::::get(next_feed).unwrap(); + assert_eq!(previous_before.next_subscriber_page, Some(1)); + let page = ObservationSubscriberPages::::get(feed, 1).unwrap(); + assert_eq!(page.entries.len() as u32, page_size); + assert!( + page.entries[..page.entries.len() - 1] + .iter() + .all(Option::is_none) + ); + assert_eq!(page.entries.last(), Some(&Some(actor_id))); + assert_eq!(ObservationSubscriberCount::::get(feed), 1); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state() + .expect("real publications retain the target as the current middle dirty feed"); + let before = user_pipeline_accounting::(actor_id); + let (state, admission) = consume_zero_step_opening::(actor_id); + let now = frame_system::Pallet::::block_number(); + #[block] + { + execute_zero_step_inner::(actor_id, state, &admission, now); + } + assert_user_pipeline_accounting::(actor_id, before); + assert_zero_step_observation::(actor_id, feed, None, true); + assert!(!ObservationSubscriberPages::::contains_key(feed, 1)); + assert!(!ObservationSubscriberPageLists::::contains_key(feed)); + assert!(!ObservationSubscriberCount::::contains_key(feed)); + assert!(!ObservationIngressRevisions::::contains_key(feed)); + assert!(!DirtyObservationFeeds::::contains_key(feed)); + let list = DirtyObservationListState::::get(); + assert_eq!( + (list.head, list.tail, list.cursor, list.count), + (Some(previous_feed), Some(next_feed), Some(next_feed), 2) + ); + let mut expected_previous = previous_before; + expected_previous.next_dirty_feed = Some(next_feed); + let mut expected_next = next_before; + expected_next.previous_dirty_feed = Some(previous_feed); + assert_eq!( + DirtyObservationFeeds::::get(previous_feed), + Some(expected_previous) + ); + assert_eq!( + DirtyObservationFeeds::::get(next_feed), + Some(expected_next) + ); + assert_eq!(ObservationFreeSlotLen::::get(), page_size + 1); + assert_eq!(ObservationFreeSlotPages::::get(0), Some(full_free_page)); + assert_eq!( + ObservationFreeSlotPages::::get(1).unwrap().as_slice(), + &[slot] + ); + frame_system::Pallet::::set_block_number(5u32.into()); + for _ in 0..2 { + Pallet::::do_fanout_dirty_observation_page().unwrap(); + } + assert_eq!(DirtyObservationListState::::get().count, 0); + for guard in guards { + assert_eq!( + benchmark_fixture_identity::(guard).unwrap().cycle_nonce, + 0 + ); + let hot = benchmark_fixture_hot::(guard).unwrap(); + assert!(hot.lifecycle.is_paused() && hot.queue_ticket.is_none()); + } + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("both surviving dirty feeds complete after middle unlink"); + } + + /// Complete User close unlinks a sparse middle page and advances a real dirty fanout cursor. + #[benchmark(pov_mode = Measured)] + fn scheduler_inner_zero_step_user_observation_unlink() { + frame_system::Pallet::::set_block_number(1u32.into()); + let feeds = observation_feed_pool::(2); + let feed = feeds[0]; + let page_size = T::ObservationPageSize::get(); + assert!(page_size > 1 && T::MaxObservationFanoutPagesPerBlock::get() >= 3); + assert_eq!(NextObservationSubscriptionSlot::::get(), 0); + assert_eq!(ObservationFreeSlotLen::::get(), 0); + let mut guards = Vec::new(); + for index in 0..page_size { + guards.push(create_zero_step_observation_guard::(index, feed)); + } + let mut fillers = Vec::new(); + for index in 0..page_size - 1 { + fillers.push(create_zero_step_observation_guard::( + page_size + index, + feeds[1], + )); + } + let (actor_id, _) = create_zero_step_observation_user::(feed, None, true, 2); + let slot = ObservationSubscriptionSlot::::get(actor_id).unwrap(); + assert_eq!(slot, 2 * page_size - 1); + for index in 0..page_size { + guards.push(create_zero_step_observation_guard::( + 2 * page_size + index, + feed, + )); + } + frame_system::Pallet::::set_block_number(2u32.into()); + for guard in &guards { + Pallet::::pause_actor(RawOrigin::Root.into(), *guard) + .expect("ordinary pause retains guard subscriptions without Ready service"); + } + for filler in fillers { + Pallet::::close_actor(RawOrigin::Root.into(), filler) + .expect("ordinary filler close retains the target's sparse slot geometry"); + } + frame_system::Pallet::::set_block_number(3u32.into()); + let first_value = publish_zero_step_observation::(feed, 1_000); + for _ in 0..3 { + Pallet::::do_fanout_dirty_observation_page().unwrap(); + } + assert!(DirtyObservationFeeds::::get(feed).is_none()); + assert!(benchmark_fixture_hot::(actor_id).unwrap().pending_signal); + for guard in &guards { + let hot = benchmark_fixture_hot::(*guard).unwrap(); + assert!(hot.lifecycle.is_paused() && hot.queue_ticket.is_none()); + } + frame_system::Pallet::::set_block_number(4u32.into()); + assert_ne!(publish_zero_step_observation::(feed, 1), first_value); + Pallet::::do_fanout_dirty_observation_page().unwrap(); + let dirty_before = DirtyObservationFeeds::::get(feed).unwrap(); + assert_eq!(dirty_before.next_subscriber_page, Some(1)); + assert_eq!(dirty_before.next_subscriber_position, 0); + let page = ObservationSubscriberPages::::get(feed, 1).unwrap(); + assert_eq!((page.previous, page.next), (Some(0), Some(2))); + assert_eq!(page.entries.len() as u32, page_size); + assert_eq!(page.entries.last(), Some(&Some(actor_id))); + assert!( + page.entries[..page.entries.len() - 1] + .iter() + .all(Option::is_none) + ); + for page_id in [0, 2] { + let page = ObservationSubscriberPages::::get(feed, page_id).unwrap(); + assert_eq!(page.entries.len() as u32, page_size); + assert!(page.entries.iter().all(Option::is_some)); + } + assert_eq!(ObservationFreeSlotLen::::get(), page_size - 1); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("ordinary sparse middle page and live fanout are coherent"); + let before = user_pipeline_accounting::(actor_id); + let (state, admission) = consume_zero_step_opening::(actor_id); + let now = frame_system::Pallet::::block_number(); + #[block] + { + execute_zero_step_inner::(actor_id, state, &admission, now); + } + assert_user_pipeline_accounting::(actor_id, before); + assert_zero_step_observation::(actor_id, feed, None, true); + assert!(!ObservationSubscriberPages::::contains_key(feed, 1)); + assert_eq!( + ObservationSubscriberPages::::get(feed, 0).unwrap().next, + Some(2) + ); + assert_eq!( + ObservationSubscriberPages::::get(feed, 2) + .unwrap() + .previous, + Some(0) + ); + let list = ObservationSubscriberPageLists::::get(feed).unwrap(); + assert_eq!((list.head, list.tail, list.count), (0, 2, 2)); + assert_eq!(ObservationSubscriberCount::::get(feed), 2 * page_size); + let mut expected_dirty = dirty_before; + expected_dirty.next_subscriber_page = Some(2); + assert_eq!(DirtyObservationFeeds::::get(feed), Some(expected_dirty)); + assert_eq!(ObservationFreeSlotLen::::get(), page_size); + assert_eq!( + ObservationFreeSlotPages::::get(0).unwrap().last(), + Some(&slot) + ); + frame_system::Pallet::::set_block_number(5u32.into()); + Pallet::::do_fanout_dirty_observation_page().unwrap(); + assert!(DirtyObservationFeeds::::get(feed).is_none()); + for guard in guards { + assert_eq!( + benchmark_fixture_identity::(guard).unwrap().cycle_nonce, + 0 + ); + let hot = benchmark_fixture_hot::(guard).unwrap(); + assert!(hot.lifecycle.is_paused() && hot.queue_ticket.is_none()); + } + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state() + .expect("fanout resumes past the removed page without losing guards"); + } + + /// Complete User close rewrites a full subscriber page and fills an existing free-slot page. + #[benchmark(pov_mode = Measured)] + fn scheduler_inner_zero_step_user_observation_page_close() { + let (actor_id, feed) = prepare_zero_step_observation::(None, true); + let page_size = T::ObservationPageSize::get(); + assert!(page_size > 1); + let slot = ObservationSubscriptionSlot::::get(actor_id).unwrap(); + assert_eq!( + slot % page_size, + 0, + "qualified target starts a subscription page" + ); + assert_eq!(ObservationFreeSlotLen::::get(), 0); + let page_id = slot / page_size; + let mut last_guard = actor_id; + for index in 0..page_size - 1 { + last_guard = create_zero_step_observation_guard::(index, feed); + assert_eq!( + ObservationSubscriptionSlot::::get(last_guard), + Some(slot + index + 1) + ); + } + let mut donors = Vec::new(); + for index in 0..page_size - 1 { + donors.push(create_zero_step_observation_guard::( + page_size + index, + feed, + )); + } + for donor in donors { + Pallet::::close_actor(RawOrigin::Root.into(), donor) + .expect("ordinary donor close fills the reusable free-slot page"); + } + assert_eq!(ObservationFreeSlotLen::::get(), page_size - 1); + assert_eq!( + ObservationFreeSlotPages::::get(0).unwrap().len() as u32, + page_size - 1 + ); + let page = ObservationSubscriberPages::::get(feed, page_id).unwrap(); + assert_eq!(page.entries.len() as u32, page_size); + assert!(page.entries.iter().all(Option::is_some)); + assert_eq!(ObservationSubscriberCount::::get(feed), page_size); + assert!(DirtyObservationFeeds::::get(feed).is_none()); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state() + .expect("dense subscriber and reusable free-slot pages are coherent"); + let before = user_pipeline_accounting::(actor_id); + let (state, admission) = consume_zero_step_opening::(actor_id); + let now = frame_system::Pallet::::block_number(); + #[block] + { + execute_zero_step_inner::(actor_id, state, &admission, now); + } + assert_user_pipeline_accounting::(actor_id, before); + assert_zero_step_observation::(actor_id, feed, None, true); + let page = ObservationSubscriberPages::::get(feed, page_id).unwrap(); + assert_eq!(page.entries.len() as u32, page_size); + assert_eq!(page.entries[0], None); + assert_eq!(page.entries.last(), Some(&Some(last_guard))); + assert_eq!(ObservationSubscriberCount::::get(feed), page_size - 1); + assert_eq!(ObservationFreeSlotLen::::get(), page_size); + let free = ObservationFreeSlotPages::::get(0).unwrap(); + assert_eq!(free.len() as u32, page_size); + assert_eq!(free.last(), Some(&slot)); + assert_eq!( + benchmark_fixture_identity::(last_guard) + .unwrap() + .cycle_nonce, + 0 + ); + let hot = benchmark_fixture_hot::(last_guard).unwrap(); + assert!(!hot.pending_signal && hot.queue_ticket.is_none()); + } + + /// Complete User zero-Step observation inner atom on a fresh single-member feed. + #[benchmark(pov_mode = Measured)] + fn scheduler_inner_zero_step_user_crossing_armed() { + let (actor_id, feed) = prepare_zero_step_observation::(Some(CrossingPhase::Armed), false); + let before = user_pipeline_accounting::(actor_id); + let (state, admission) = consume_zero_step_opening::(actor_id); + let now = frame_system::Pallet::::block_number(); + #[block] + { + execute_zero_step_inner::(actor_id, state, &admission, now); + } + assert_user_pipeline_accounting::(actor_id, before); + assert_zero_step_observation::(actor_id, feed, Some(CrossingPhase::Armed), false); + } + + /// Complete User zero-Step observation inner atom on a fresh single-member feed. + #[benchmark(pov_mode = Measured)] + fn scheduler_inner_zero_step_user_crossing_waiting() { + let (actor_id, feed) = + prepare_zero_step_observation::(Some(CrossingPhase::WaitingForRearm), false); + let before = user_pipeline_accounting::(actor_id); + let (state, admission) = consume_zero_step_opening::(actor_id); + let now = frame_system::Pallet::::block_number(); + #[block] + { + execute_zero_step_inner::(actor_id, state, &admission, now); + } + assert_user_pipeline_accounting::(actor_id, before); + assert_zero_step_observation::(actor_id, feed, Some(CrossingPhase::WaitingForRearm), false); + } + + /// Complete User zero-Step observation inner atom on a fresh single-member feed. + #[benchmark(pov_mode = Measured)] + fn scheduler_inner_zero_step_user_crossing_close() { + let (actor_id, feed) = + prepare_zero_step_observation::(Some(CrossingPhase::WaitingForRearm), true); + let before = user_pipeline_accounting::(actor_id); + let (state, admission) = consume_zero_step_opening::(actor_id); + let now = frame_system::Pallet::::block_number(); + #[block] + { + execute_zero_step_inner::(actor_id, state, &admission, now); + } + assert_user_pipeline_accounting::(actor_id, before); + assert_zero_step_observation::(actor_id, feed, Some(CrossingPhase::WaitingForRearm), true); + } + + /// Complete User zero-Step observation inner atom on a fresh single-member feed. + #[benchmark(pov_mode = Measured)] + fn scheduler_inner_zero_step_user_observation() { + let (actor_id, feed) = prepare_zero_step_observation::(None, false); + let before = user_pipeline_accounting::(actor_id); + let (state, admission) = consume_zero_step_opening::(actor_id); + let now = frame_system::Pallet::::block_number(); + #[block] + { + execute_zero_step_inner::(actor_id, state, &admission, now); + } + assert_user_pipeline_accounting::(actor_id, before); + assert_zero_step_observation::(actor_id, feed, None, false); + } + + /// Complete User zero-Step observation inner atom on a fresh single-member feed. + #[benchmark(pov_mode = Measured)] + fn scheduler_inner_zero_step_user_observation_close() { + let (actor_id, feed) = prepare_zero_step_observation::(None, true); + let before = user_pipeline_accounting::(actor_id); + let (state, admission) = consume_zero_step_opening::(actor_id); + let now = frame_system::Pallet::::block_number(); + #[block] + { + execute_zero_step_inner::(actor_id, state, &admission, now); + } + assert_user_pipeline_accounting::(actor_id, before); + assert_zero_step_observation::(actor_id, feed, None, true); + } + + /// Exact User Pipeline collection/receipt primitive on an admitted empty Contract head. + /// It excludes cycle execution, placement and User hold reconciliation, and does not bound a larger head. + #[benchmark(pov_mode = Measured)] + fn pipeline_zero_step_opening_collection() { + let actor_id = prepare_zero_step_opening::(ActorType::User); + let opening = Pallet::::active_actor_view(actor_id).expect("Idle User view exists"); + let native = T::FeeNativeAssetId::get(); + let sink = T::FeeSink::get(); + let payer_before = T::AssetOps::balance(&opening.sovereign_account, native); + let sink_before = T::AssetOps::balance(&sink, native); + let fee = Pallet::::pipeline_fee_for_actor(actor_id, ActorType::User) + .expect("certified Pipeline fee exists") + .total_fee; + assert!( + !fee.is_zero(), + "this collection profile requires a nonzero host fee" + ); + #[block] + { + Pallet::::charge_pipeline_opening(actor_id, &opening) + .expect("funded Pipeline collection succeeds"); + } + assert_eq!( + T::AssetOps::balance(&opening.sovereign_account, native), + payer_before - fee + ); + assert_eq!(T::AssetOps::balance(&sink, native), sink_before + fee); + frame_system::Pallet::::assert_last_event( + Event::::PipelineFeeCharged { actor_id, fee }.into(), + ); + assert_eq!( + benchmark_fixture_identity::(actor_id) + .unwrap() + .cycle_nonce, + 0 + ); + } + + fn large_header_fields() -> ( + T::AccountId, + T::AssetId, + TriggerOf, + FundingSourcePolicyOf, + ) { + frame_system::Pallet::::set_block_number(1u32.into()); + let owner: T::AccountId = account("large-head-owner", 0, 0); + ensure_creation_balance::(&owner); + let count = T::MaxWhitelistSize::get(); + assert!(count > 0); + let mut sources = vec![owner.clone()]; + sources.extend((1..count).map(|index| account("large-head-source", index, 0))); + sources.sort_by_key(Encode::encode); + sources.dedup(); + assert_eq!(sources.len(), count as usize); + let mut assets = T::BenchmarkHelper::setup_predicate_assets(&owner, count + 1) + .expect("large-head assets exist"); + assets.retain(|asset| asset.encoded_size() == T::AssetId::max_encoded_len()); + assets.sort_by_key(Encode::encode); + assets.dedup(); + assets.truncate(count as usize); + assert_eq!(assets.len(), count as usize); + let asset = assets[0]; + let trigger = Trigger::AddressEvent { + source_filter: SourceFilter::Whitelist(sources.clone().try_into().unwrap()), + asset_filter: AssetFilter::Whitelist(assets.try_into().unwrap()), + }; + assert_eq!(trigger.encoded_size(), TriggerOf::::max_encoded_len()); + let funding = FundingSourcePolicyOf::::SignedAllowlist( + sources + .into_iter() + .collect::>() + .try_into() + .unwrap(), + ); + assert_eq!( + funding.encoded_size(), + FundingSourcePolicyOf::::max_encoded_len() + ); + (owner, asset, trigger, funding) + } + + /// Exact zero-Step head byte maximum, with a future window reference and complete User control. + /// This byte certificate does not certify maximum Waiting geometry or CPU branch dominance. + #[benchmark(pov_mode = Measured)] + fn scheduler_inner_zero_step_user_max_head() { + let (owner, asset, trigger, funding) = large_header_fields::(); + let steps = ContractSteps::::default(); + prefund_active_user_creation::(&owner, &steps); + let extra = full_attempt_fee::(&steps) + .checked_add( + &Pallet::::trigger_fee_for_weight( + ActorType::User, + TriggerFamily::AddressEvent, + T::WeightInfo::address_event_trigger_occurrence(), + ) + .trigger_fee, + ) + .unwrap(); + let now = frame_system::Pallet::::block_number(); + let end = now.saturating_add(T::MinWindowLength::get()); + Pallet::::create_user_actor( + RawOrigin::Signed(owner.clone()).into(), + Mutability::Mutable, + Some(ActorContract { + trigger, + cooldown_blocks: 0, + window: Some(ScheduleWindow { start: now, end }), + steps, + funding, + completion: CompletionPolicy::Persistent, + auto_close_at_cycle_nonce: Some(T::MaxAutoCloseNonceHorizon::get()), + }), + ) + .expect("maximum-header zero-Step User Contract is ordinarily admitted"); + let actor_id = NextActorId::::get().saturating_sub(1); + let head = ActorContractHeads::::get(actor_id).unwrap(); + assert_eq!(head.header.step_count, 0); + assert!(head.first_step.is_none() && head.first_step_resources.is_none()); + assert_eq!( + head.header.encoded_size(), + ActorContractHeaderOf::::max_encoded_len() + ); + assert_eq!( + head.encoded_size(), + ActorContractHeaderOf::::max_encoded_len() + 2 + ); + let sovereign = benchmark_fixture_identity::(actor_id) + .unwrap() + .sovereign_account; + T::AssetOps::mint(&sovereign, T::FeeNativeAssetId::get(), extra).unwrap(); + let ingress = T::AssetOps::minimum_balance(asset).max(1u32.into()); + T::AssetOps::mint(&owner, asset, ingress + ingress).unwrap(); + T::BenchmarkHelper::enable_asset_ops_ingress(); + T::BenchmarkHelper::transfer_signed(&owner, &sovereign, asset, ingress) + .expect("certified ingress activates the maximum-header zero-Step Actor"); + let (_, cell) = Pallet::::actor_control_cell(actor_id).unwrap(); + assert!(cell.hot.pending_signal); + frame_system::Pallet::::set_block_number(cell.eligible_at.unwrap()); + let funding = ActorFunding::::get(actor_id).unwrap(); + assert!(funding.funding_tracked_assets.is_empty() && funding.funding_accumulated.is_empty()); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("maximum-header readiness and window are coherent"); + let before = user_pipeline_accounting::(actor_id); + let (state, admission) = consume_zero_step_opening::(actor_id); + let now = frame_system::Pallet::::block_number(); + #[block] + { + execute_zero_step_inner::(actor_id, state, &admission, now); + } + assert_user_pipeline_accounting::(actor_id, before); + assert_eq!( + benchmark_fixture_identity::(actor_id) + .unwrap() + .cycle_nonce, + 1 + ); + let hot = benchmark_fixture_hot::(actor_id).unwrap(); + assert_eq!(hot.cycle_state, CycleState::Idle); + assert!(!hot.pending_signal && hot.queue_ticket.is_none()); + let pointer = hot + .wakeup_pointer + .expect("future window retains its Block reference"); + assert_eq!( + pointer.block, + WakeupKey::Block(end.saturating_add(1u32.into())) + ); + assert!(Pallet::::wakeup_page_entry_matches(pointer, actor_id)); + assert!(ActorRunStateStore::::get(actor_id).is_none()); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state() + .expect("maximum-header zero-Step completion and holds are coherent"); + } + + /// Large admitted head, with host-qualified field widths and semantic predicate cardinality. + /// Compact resource widths and decode-branch dominance remain separate envelope questions. + #[benchmark(pov_mode = Measured)] + fn pipeline_large_head_opening_collection() { + let (owner, asset, trigger, funding) = large_header_fields::(); + let feed = T::BenchmarkHelper::contract_head_observation_feed() + .expect("host provides a Contract-head feed witness"); + assert_eq!(feed.encoded_size(), T::ObservationFeedId::max_encoded_len()); + let predicate = + Predicate::::ObservationAbove { + feed, + threshold: 1, + max_age_blocks: 1, + }; + assert_eq!( + predicate.encoded_size(), + Predicate::::max_encoded_len() + ); + let predicates = benchmark_predicate_capacity::(); + assert!(predicates > 0 && predicates <= T::MaxPreconditionClauses::get()); + let precondition = packed_predicate_clauses::( + (0..predicates) + .map(|index| TimedPredicate { + timing: ObservationTiming::Current, + predicate: Predicate::ObservationAbove { + feed, + threshold: u128::from(index) + 1, + max_age_blocks: 1, + }, + }) + .collect(), + 1, + ); + let legs = T::MaxSplitTransferLegs::get(); + assert!(legs > 0 && legs <= Perbill::ACCURACY); + let task = ActorTask::SplitTransfer { + asset, + amount: AmountResolution::Fixed(1u32.into()), + legs: (0..legs) + .map(|index| SplitLeg { + to: account("large-head-recipient", index, 0), + share: Perbill::from_parts(Perbill::ACCURACY / legs), + }) + .collect::>() + .try_into() + .unwrap(), + }; + assert_eq!(task.encoded_size(), TaskOf::::max_encoded_len()); + let steps: ContractSteps = vec![Step { + precondition: Some(precondition), + task, + on_error: StepErrorPolicy::RetryLater { max_attempts: 2 }, + }] + .try_into() + .unwrap(); + prefund_active_user_creation::(&owner, &steps); + let extra = full_attempt_fee::(&steps) + .checked_add( + &Pallet::::trigger_fee_for_weight( + ActorType::User, + TriggerFamily::AddressEvent, + T::WeightInfo::address_event_trigger_occurrence(), + ) + .trigger_fee, + ) + .expect("large-head fee funding fits"); + let now = frame_system::Pallet::::block_number(); + Pallet::::create_user_actor( + RawOrigin::Signed(owner.clone()).into(), + Mutability::Mutable, + Some(ActorContract { + trigger, + cooldown_blocks: 0, + window: Some(ScheduleWindow { + start: now, + end: now.saturating_add(T::MinWindowLength::get()), + }), + steps, + funding, + completion: CompletionPolicy::Persistent, + auto_close_at_cycle_nonce: Some(T::MaxAutoCloseNonceHorizon::get()), + }), + ) + .expect("large-head User Contract is admitted without fixture mutation"); + let actor_id = NextActorId::::get().saturating_sub(1); + let opening = Pallet::::active_actor_view(actor_id).expect("Idle User view exists"); + let native = T::FeeNativeAssetId::get(); + T::AssetOps::mint(&opening.sovereign_account, native, extra).unwrap(); + let ingress = T::AssetOps::minimum_balance(asset).max(1u32.into()); + T::AssetOps::mint(&owner, asset, ingress + ingress).unwrap(); + T::BenchmarkHelper::enable_asset_ops_ingress(); + T::BenchmarkHelper::transfer_signed(&owner, &opening.sovereign_account, asset, ingress) + .expect("certified signed ingress reaches the large-head Actor"); + let (_, cell) = Pallet::::actor_control_cell(actor_id).expect("Ready cell exists"); + assert!(cell.hot.pending_signal); + frame_system::Pallet::::set_block_number( + cell.eligible_at.expect("Ready eligibility exists"), + ); + let opening = Pallet::::active_actor_view(actor_id).expect("latched Idle User view exists"); + let head = ActorContractHeads::::get(actor_id).expect("admitted head exists"); + assert_eq!(head.header.step_count, 1); + let stored = head.first_step.expect("admitted Step exists"); + assert_eq!(stored.precondition.unwrap().predicate_count(), predicates); + assert_eq!(stored.task.encoded_size(), TaskOf::::max_encoded_len()); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("large-head fixture and User holds are coherent"); + let sink = T::FeeSink::get(); + let payer_before = T::AssetOps::balance(&opening.sovereign_account, native); + let sink_before = T::AssetOps::balance(&sink, native); + let fee = Pallet::::pipeline_fee_for_actor(actor_id, ActorType::User) + .expect("certified Pipeline fee exists") + .total_fee; + assert!( + !fee.is_zero(), + "this collection profile requires a nonzero host fee" + ); + #[block] + { + Pallet::::charge_pipeline_opening(actor_id, &opening) + .expect("funded large-head Pipeline collection succeeds"); + } + assert_eq!( + T::AssetOps::balance(&opening.sovereign_account, native), + payer_before - fee + ); + assert_eq!(T::AssetOps::balance(&sink, native), sink_before + fee); + frame_system::Pallet::::assert_last_event( + Event::::PipelineFeeCharged { actor_id, fee }.into(), + ); + assert_eq!( + benchmark_fixture_identity::(actor_id) + .unwrap() + .cycle_nonce, + 0 + ); + } + + fn prepare_temporal_control_cell(at_time: bool) -> ActorId { + frame_system::Pallet::::set_block_number(1u32.into()); + let owner: T::AccountId = account("temporal-control-owner", 0, 0); + let asset = T::BenchmarkHelper::setup_predicate_assets(&owner, 2) + .expect("ordinary assets exist") + .into_iter() + .find(|asset| *asset != T::FeeNativeAssetId::get()) + .expect("ordinary non-native asset exists"); + let leg_count = T::MaxSplitTransferLegs::get(); + assert!(leg_count > 0 && leg_count <= Perbill::ACCURACY); + let steps = vec![Step { + precondition: None, + task: ActorTask::SplitTransfer { + asset, + amount: AmountResolution::Fixed(1u32.into()), + legs: (0..leg_count) + .map(|index| SplitLeg { + to: account("temporal-control-recipient", index, 0), + share: Perbill::from_parts(Perbill::ACCURACY / leg_count), + }) + .collect::>() + .try_into() + .unwrap(), + }, + on_error: StepErrorPolicy::RetryLater { max_attempts: 3 }, + }] + .try_into() + .unwrap(); + let mut contract = system_contract::( + Schedule { + trigger: if at_time { + Trigger::at_time(100) + } else { + Trigger::cadenced(100) + }, + cooldown_blocks: 0, + }, + steps, + ) + .unwrap(); + contract.cooldown_blocks = 1; + polkadot_sdk::frame_support::assert_noop!( + Pallet::::create_system_actor( + RawOrigin::Root.into(), + owner.clone(), + Mutability::Mutable, + Some(contract.clone()), + ), + Error::::InvalidTriggerConfiguration + ); + contract.cooldown_blocks = 0; + contract.window = Some(ScheduleWindow { + start: 1u32.into(), + end: T::MinWindowLength::get().saturating_add(1_000u32.into()), + }); + polkadot_sdk::frame_support::assert_noop!( + Pallet::::create_system_actor( + RawOrigin::Root.into(), + owner.clone(), + Mutability::Mutable, + Some(contract.clone()), + ), + Error::::InvalidScheduleWindow + ); + contract.window = None; + Pallet::::create_system_actor( + RawOrigin::Root.into(), + owner, + Mutability::Mutable, + Some(contract), + ) + .expect("ordinary temporal control Contract is admitted"); + let actor_id = NextActorId::::get().saturating_sub(1); + let (_, cell) = Pallet::::actor_control_cell(actor_id).expect("created control cell"); + assert!(cell.hot.wakeup_pointer.is_none() && cell.hot.trigger_wakeup_pointer.is_some()); + assert!(cell.hot.terminal_at.is_none() && cell.eligible_at.is_none()); + assert!(cell.hot.last_cycle_block.is_none()); + report_temporal_control_cell::(actor_id, "created"); + let tick = cell.hot.trigger_wakeup_pointer.unwrap().tick; + T::BenchmarkHelper::advance_to_scheduler_tick(tick).expect("ordinary due host clock"); + let now = frame_system::Pallet::::block_number(); + let mut meter = polkadot_sdk::sp_weights::WeightMeter::with_limit(Weight::MAX); + Pallet::::drain_overdue_wakeups_cursor(now, &mut meter); + let (_, cell) = Pallet::::actor_control_cell(actor_id).expect("latched temporal cell"); + assert!(cell.hot.pending_signal && cell.hot.trigger_wakeup_pointer.is_none()); + frame_system::Pallet::::set_block_number(cell.eligible_at.expect("ordinary eligibility")); + actor_id + } + + fn report_temporal_control_cell(actor_id: ActorId, phase: &str) { + let (_, cell) = Pallet::::actor_control_cell(actor_id).expect("stored control cell"); + let authority = T::AdmissionCertificateAuthority::current().expect("host authority"); + assert_eq!( + cell.admission.maximum_lifecycle_weight, + authority.maximum_lifecycle_weight + ); + frame::log::info!( + "Temporal control cell {}: total={}, nominal={}, identity={}, hot={}, cursor={}, eligible={}, certificate={}, resources={}; lifecycle={:?}, control={:?}, effect={:?}; Weight bytes={}/{}/{}; service={:?}", + phase, + cell.encoded_size(), + ActorControlCellOf::::max_encoded_len(), + cell.identity.encoded_size(), + cell.hot.encoded_size(), + cell.cursor.encoded_size(), + cell.eligible_at.encoded_size(), + cell.admission.encoded_size(), + cell.resources.encoded_size(), + cell.admission.maximum_lifecycle_weight, + cell.resources.control, + cell.resources.effect, + cell.admission.maximum_lifecycle_weight.encoded_size(), + cell.resources.control.encoded_size(), + cell.resources.effect.encoded_size(), + Pallet::::guaranteed_actor_service_weight(), + ); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("ordinary temporal cell reconciles"); + } + + fn assert_temporal_control_cell(actor_id: ActorId, at_time: bool) { + let (location, cell) = + Pallet::::actor_control_cell(actor_id).expect("suspended control cell"); + assert!(matches!( + location, + ActorControlLocation::Waiting { + key: WakeupKey::Block(_), + .. + } + )); + assert_eq!(cell.hot.cycle_state, CycleState::Suspended); + assert!(cell.hot.wakeup_pointer.is_some() && cell.hot.terminal_at.is_none()); + assert!(cell.hot.last_cycle_block.is_some() && cell.eligible_at.is_some()); + assert_eq!(cell.hot.trigger_wakeup_pointer.is_none(), at_time); + let run = ActorRunStateStore::::get(actor_id).expect("ordinary Temporary retry Run"); + assert_eq!(run.cursor, 0); + assert_eq!(run.unsuccessful_attempts_at_cursor, 2); + assert_eq!(run.last_step_outcome, Some(StepOutcome::FundingUnavailable)); + report_temporal_control_cell::(actor_id, "suspended"); + if !at_time { + let mut contract = Pallet::::actor_contract(actor_id).expect("retained temporal Contract"); + contract.trigger = Trigger::at_time(100); + let now = frame_system::Pallet::::block_number(); + frame_system::Pallet::::set_block_number(now.saturating_add(1u32.into())); + contract.cooldown_blocks = 1; + polkadot_sdk::frame_support::assert_noop!( + Pallet::::update_contract(RawOrigin::Root.into(), actor_id, contract.clone()), + Error::::InvalidTriggerConfiguration + ); + contract.cooldown_blocks = 0; + contract.window = Some(ScheduleWindow { + start: now, + end: now.saturating_add(T::MinWindowLength::get()), + }); + polkadot_sdk::frame_support::assert_noop!( + Pallet::::update_contract(RawOrigin::Root.into(), actor_id, contract.clone()), + Error::::InvalidScheduleWindow + ); + contract.window = None; + Pallet::::update_contract(RawOrigin::Root.into(), actor_id, contract) + .expect("ordinary AtTime replacement cancels the old Run"); + let (_, replaced) = Pallet::::actor_control_cell(actor_id).expect("replaced AtTime cell"); + assert_eq!(replaced.hot.cycle_state, CycleState::Idle); + assert_eq!(replaced.hot.last_cycle_block, cell.hot.last_cycle_block); + assert!(replaced.hot.wakeup_pointer.is_none() && replaced.eligible_at.is_none()); + assert!(replaced.hot.terminal_at.is_none() && replaced.hot.trigger_wakeup_pointer.is_some()); + assert!(ActorRunStateStore::::get(actor_id).is_none()); + report_temporal_control_cell::(actor_id, "replaced AtTime"); + } + } + + // These diagnostics construct two logical blocks; their timings are not production Weight. + #[benchmark(extra, pov_mode = Measured)] + fn control_cell_cadenced_reachability() { + let actor_id = prepare_temporal_control_cell::(false); + #[block] + { + Pallet::::execute_cycle(Weight::MAX); + let run = ActorRunStateStore::::get(actor_id).expect("first retry"); + frame_system::Pallet::::set_block_number(run.eligible_at); + Pallet::::execute_cycle(Weight::MAX); + } + assert_temporal_control_cell::(actor_id, false); + } + + #[benchmark(extra, pov_mode = Measured)] + fn control_cell_at_time_reachability() { + let actor_id = prepare_temporal_control_cell::(true); + #[block] + { + Pallet::::execute_cycle(Weight::MAX); + let run = ActorRunStateStore::::get(actor_id).expect("first retry"); + frame_system::Pallet::::set_block_number(run.eligible_at); + Pallet::::execute_cycle(Weight::MAX); + } + assert_temporal_control_cell::(actor_id, true); + } + + #[cfg(test)] + #[test] + fn temporal_control_cell_public_reachability() { + for at_time in [false, true] { + new_test_ext().execute_with(|| { + let actor_id = prepare_temporal_control_cell::(at_time); + Pallet::::execute_cycle(Weight::MAX); + let run = ActorRunStateStore::::get(actor_id).expect("first retry"); + frame_system::Pallet::::set_block_number(run.eligible_at); + Pallet::::execute_cycle(Weight::MAX); + assert_temporal_control_cell::(actor_id, at_time); + }); + } + } + + /// Full User FIFO Opening at maximum Contract length, one zero-balance Opening surface and + /// reachable tail funding. This declared corner is not an allocation-frontier envelope. #[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)); + fn scheduler_paged_execute_opening_max() + -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let t = T::MaxContractSteps::get() + .saturating_sub(1) + .div_ceil(MAX_STEPS_PER_TAIL_CHUNK); + let (actor_id, count) = prepare_reachable_opening::(t, ReachableOpeningProfile::UserPaged)?; + let accounting_before = user_pipeline_accounting::(actor_id); + let hold_before = + ActorStateHolds::::get(actor_id).expect("nonzero-Step User state hold exists"); + #[block] + { + core::hint::black_box(Pallet::::execute_cycle(Weight::MAX)); } - 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(), + assert_user_pipeline_accounting::(actor_id, accounting_before); + let hold_after = + ActorStateHolds::::get(actor_id).expect("nonzero-Step User state hold survives"); + assert_eq!(hold_after.owner, hold_before.owner); + assert_eq!( + hold_after.breakdown.identity, + hold_before.breakdown.identity ); - ActorHot::::mutate(actor_id, |maybe| { - maybe - .as_mut() - .expect("Running-progress hot state remains") - .queue_ticket = None; - }); + assert_eq!( + hold_after.breakdown.contract_head, + hold_before.breakdown.contract_head + ); + assert_eq!( + hold_after.breakdown.contract_body, + hold_before.breakdown.contract_body + ); + assert_eq!( + hold_after.breakdown.detector, + hold_before.breakdown.detector + ); + assert_eq!(hold_after.breakdown.run, hold_before.breakdown.run); + assert!( + hold_after.breakdown.funding < hold_before.breakdown.funding, + "Opening independently reconciles consumed funding geometry" + ); + assert_reachable_opening::(actor_id, count, ReachableOpeningProfile::UserPaged); + Ok(()) + } + + /// An admitted Unstake loses its empty host receipt before Opening and fails pre-effect. + /// Real tail ingress excludes that receipt; this is a reachable minimal-Opening failure corner. + #[benchmark(pov_mode = Measured)] + fn scheduler_inner_opening_failed_min( + t: Linear< + 0, + { + T::MaxContractSteps::get() + .saturating_sub(1) + .div_ceil(MAX_STEPS_PER_TAIL_CHUNK) + }, + >, + ) -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let (actor_id, count) = prepare_reachable_opening::(t, ReachableOpeningProfile::FailedMin)?; + let now = frame_system::Pallet::::block_number(); + let (state, admission, loaded_step) = + benchmark_fixture_consume_frame_current_step_service_state::(actor_id); + let effect_weight; #[block] { - let ticket = Pallet::::build_actor_step_ticket( + effect_weight = + execute_reachable_step_inner::(actor_id, state, admission, loaded_step, now); + } + assert_eq!(effect_weight, Weight::zero()); + assert_reachable_opening::(actor_id, count, ReachableOpeningProfile::FailedMin); + Ok(()) + } + + /// Measures direct minimal-geometry fresh-Opening retry at every tail-chunk count. An unfunded + /// fixed Transfer suspends at cursor zero and installs one wakeup without Opening host reads. + #[benchmark(pov_mode = Measured)] + fn scheduler_inner_opening_retry_min( + t: Linear< + 0, + { + T::MaxContractSteps::get() + .saturating_sub(1) + .div_ceil(MAX_STEPS_PER_TAIL_CHUNK) + }, + >, + ) -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let (actor_id, count) = prepare_reachable_opening::(t, ReachableOpeningProfile::RetryMin)?; + let now = frame_system::Pallet::::block_number(); + let (state, admission, loaded_step) = + benchmark_fixture_consume_frame_current_step_service_state::(actor_id); + #[block] + { + execute_reachable_step_inner::(actor_id, state, admission, loaded_step, now); + } + assert_reachable_opening::(actor_id, count, ReachableOpeningProfile::RetryMin); + Ok(()) + } + + #[benchmark(extra, pov_mode = Measured)] + fn scheduler_opening_retry_full_waiting_existing() + -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let (actor_id, keys, due) = prepare_reachable_waiting_opening::(1)?; + #[block] + { + Pallet::::execute_cycle(Weight::MAX); + } + assert_reachable_waiting_opening::(actor_id, keys, due); + Ok(()) + } + + #[benchmark(extra, pov_mode = Measured)] + fn scheduler_opening_retry_full_waiting_new() + -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let (actor_id, keys, due) = prepare_reachable_waiting_opening::(0)?; + #[block] + { + Pallet::::execute_cycle(Weight::MAX); + } + assert_reachable_waiting_opening::(actor_id, keys + 1, due); + Ok(()) + } + + #[benchmark(extra, pov_mode = Measured)] + fn scheduler_opening_retry_full_waiting_page() + -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let (actor_id, keys, due) = prepare_reachable_waiting_opening::(32)?; + let key = WakeupKey::Block(due); + let before = ActorWaitingFrameChunks::::get((key, 0)).expect("full Waiting page exists"); + assert_eq!(before.live_entries, 32); + assert!(before.next_page.is_none()); + assert!(!ActorWaitingFrameChunks::::contains_key((key, 1))); + #[block] + { + Pallet::::execute_cycle(Weight::MAX); + } + assert_reachable_waiting_opening::(actor_id, keys, due); + let head = ActorWaitingFrameChunks::::get((key, 0)).expect("full Waiting head remains"); + let tail = ActorWaitingFrameChunks::::get((key, 1)).expect("new Waiting tail exists"); + assert_eq!(head.entries.encode(), before.entries.encode()); + assert_eq!(head.scan_slot, before.scan_slot); + assert_eq!((head.live_entries, head.next_page), (32, Some(1))); + assert_eq!((tail.live_entries, tail.previous_page), (1, Some(0))); + assert!(head.previous_page.is_none() && tail.next_page.is_none()); + assert!(matches!( + ActorControlLocators::::get(actor_id), + Some(ActorControlLocation::Waiting { key: at, page: 1, slot: 0 }) if at == key + )); + Ok(()) + } + + /// Complete consumed-authority Waiting publication into a populated existing Block deadline. + /// The fixed RetryMin target isolates publication geometry; all background entries follow ordinary + /// System creation and future-window publication until one host capacity is saturated. + #[benchmark(extra, pov_mode = Measured)] + fn waiting_publish_existing_populated() + -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let destination_entries = 1; + let (actor_id, count, keys, due) = prepare_reachable_waiting_publication::( + destination_entries, + 0, + ReachableOpeningProfile::RetryMin, + )?; + assert_eq!(count, 1); + let publish = benchmark_fixture_prepare_consumed_service_waiting::(actor_id, due); + #[block] + { + publish(); + } + assert_reachable_waiting_publication::(actor_id, count, keys, due, destination_entries); + Ok(()) + } + + /// Complete consumed-authority Waiting publication that allocates a second linked page under the + /// same saturated host population and fixed RetryMin authority. + #[benchmark(extra, pov_mode = Measured)] + fn waiting_publish_full_page_populated() + -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let destination_entries = 32; + let (actor_id, count, keys, due) = prepare_reachable_waiting_publication::( + destination_entries, + 0, + ReachableOpeningProfile::RetryMin, + )?; + assert_eq!(count, 1); + let before = ActorWaitingFrameChunks::::get((WakeupKey::Block(due), 0)) + .expect("full populated Waiting page exists"); + assert_eq!(before.live_entries, 32); + let publish = benchmark_fixture_prepare_consumed_service_waiting::(actor_id, due); + #[block] + { + publish(); + } + assert_reachable_waiting_publication::(actor_id, count, keys, due, destination_entries); + let head = ActorWaitingFrameChunks::::get((WakeupKey::Block(due), 0)) + .expect("full populated Waiting head survives"); + assert_eq!(head.entries.encode(), before.entries.encode()); + assert_eq!((head.live_entries, head.next_page), (32, Some(1))); + Ok(()) + } + + /// Complete consumed-authority RetryMin publication of a new minimum into the maximum additional + /// ordinary System population admitted alongside retained reference-runtime genesis authority. + #[benchmark(extra, pov_mode = Measured)] + fn waiting_publish_new_minimum_populated() + -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let destination_entries = 0; + let (actor_id, count, keys, due) = prepare_reachable_waiting_publication::( + destination_entries, + 0, + ReachableOpeningProfile::RetryMin, + )?; + assert_eq!(count, 1); + assert!(Pallet::::wakeup_cursor_peek().is_none_or(|minimum| minimum > due)); + let publish = benchmark_fixture_prepare_consumed_service_waiting::(actor_id, due); + #[block] + { + publish(); + } + assert_reachable_waiting_publication::(actor_id, count, keys, due, destination_entries); + assert_eq!(Pallet::::wakeup_cursor_peek(), Some(due)); + Ok(()) + } + + /// Complete new-minimum Primary publication of the largest publicly reachable control cell. The + /// Cadenced target reaches its second real suspended attempt before its due service authority is + /// consumed; publication then restores the Block pointer beside the retained Tick pointer under + /// the maximum additional ordinary System population. + #[benchmark(extra, pov_mode = Measured)] + fn waiting_publish_max_cell_new_minimum_populated() + -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let (actor_id, maximum_size, keys, due) = + prepare_reachable_max_cell_waiting_publication::()?; + let publish = benchmark_fixture_prepare_consumed_service_waiting::(actor_id, due); + #[block] + { + publish(); + } + let (location, cell) = + Pallet::::actor_control_cell(actor_id).expect("maximal cell is published in Waiting"); + assert!(matches!( + location, + ActorControlLocation::Waiting { + key: WakeupKey::Block(at), + page: 0, + slot: 0, + } if at == due + )); + assert_eq!(cell.encoded_size(), maximum_size); + assert_eq!(cell.hot.cycle_state, CycleState::Suspended); + assert!(cell.hot.wakeup_pointer.is_some()); + assert!(cell.hot.trigger_wakeup_pointer.is_some()); + assert!(cell.hot.last_cycle_block.is_some() && cell.eligible_at.is_some()); + assert_eq!(WakeupCursorLen::::get(WakeupClock::Block), keys + 1); + assert_eq!(Pallet::::wakeup_cursor_peek(), Some(due)); + assert_waiting_population_capacity::(); + assert_wakeup_cursor_page_indices::(); + frame::log::info!( + "Maximal Waiting publication cell: total={}, nominal={}", + cell.encoded_size(), + ActorControlCellOf::::max_encoded_len(), + ); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("maximal-cell Waiting publication is legal"); + Ok(()) + } + + /// Complete new-minimum Block Reference publication while retaining the consumed service + /// authority's Ready primary. This isolates the bounded Reference entry and primary rewrite from + /// the separately measured consumed-primary publication branches. + #[benchmark(extra, pov_mode = Measured)] + fn waiting_publish_reference_new_minimum_populated() + -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let (actor_id, count, keys, due) = + prepare_reachable_waiting_publication::(0, 0, ReachableOpeningProfile::RetryMin)?; + assert_eq!(count, 1); + assert!(Pallet::::wakeup_cursor_peek().is_none_or(|minimum| minimum > due)); + let location = ActorControlLocators::::get(actor_id).expect("Ready primary exists"); + assert!(matches!(location, ActorControlLocation::Ready { .. })); + let publish = benchmark_fixture_prepare_service_waiting::(actor_id, due); + #[block] + { + publish(); + } + assert_eq!(ActorControlLocators::::get(actor_id), Some(location)); + assert_reachable_waiting_reference_publication::(actor_id, keys, due); + assert_eq!(Pallet::::wakeup_cursor_peek(), Some(due)); + Ok(()) + } + + /// Complete new-minimum Cadenced Tick Reference publication while retaining an independent Block + /// Waiting primary. The target and all Tick competitors follow ordinary temporal scheduling; the + /// measured boundary is the cross-clock publication and retained-primary rewrite only. + #[benchmark(extra, pov_mode = Measured)] + fn waiting_publish_cadenced_cross_clock_populated() + -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let (actor_id, location, next, keys) = + prepare_reachable_cadenced_cross_clock_publication::()?; + let (state, admission, loaded_step) = Pallet::::load_frame_actor_service_state(actor_id) + .expect("cross-clock target retains service authority"); + let (_, cell) = Pallet::::actor_control_cell(actor_id).expect("Block primary exists"); + let resources = loaded_step + .expect("Cadenced target retains one admitted Step") + .resources; + #[block] + { + Pallet::::try_wakeup_substrate_schedule_transition_with_authority( actor_id, - 9, - now, + WakeupKey::Tick(next), + state.hot, &state.identity, - &state.hot, - state.run_state.as_ref(), + cell.cursor, &admission, + resources, ) - .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"); + .expect("cross-clock Tick Reference publication fits"); + } + assert_eq!(ActorControlLocators::::get(actor_id), Some(location)); + let (_, retained) = + Pallet::::actor_control_cell(actor_id).expect("Block primary remains authoritative"); + let pointer = retained + .hot + .trigger_wakeup_pointer + .expect("Cadenced Tick Reference exists"); + assert_eq!((pointer.tick, pointer.page_id, pointer.slot), (next, 0, 0)); + let key = WakeupKey::Tick(next); + assert_eq!(WakeupCursorLen::::get(WakeupClock::Tick), keys + 1); + assert!(matches!( + &ActorWaitingFrameChunks::::get((key, 0)) + .expect("new Tick reference page exists") + .entries[0], + Some(ActorWaitingEntry::Reference(reference)) + if reference.actor_id == actor_id + && reference.admission_identity == retained.admission.admission_identity + )); + assert_waiting_population_capacity::(); + assert_wakeup_cursor_page_indices::(); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("complete cross-clock publication is legal"); + Ok(()) + } + + /// True host-bounded Opening predicates and tail Opening legs precede missing-receipt failure. + /// The sole tracked receipt has no accumulated ingress; no independent funding maximum is claimed. + #[benchmark(pov_mode = Measured)] + fn scheduler_inner_opening_failed_max( + t: Linear< + 0, + { + T::MaxContractSteps::get() + .saturating_sub(1) + .div_ceil(MAX_STEPS_PER_TAIL_CHUNK) + }, + >, + ) -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let (actor_id, count) = prepare_reachable_opening::(t, ReachableOpeningProfile::FailedMax)?; + let now = frame_system::Pallet::::block_number(); + let (state, admission, loaded_step) = + benchmark_fixture_consume_frame_current_step_service_state::(actor_id); + let effect_weight; + #[block] + { + effect_weight = + execute_reachable_step_inner::(actor_id, state, admission, loaded_step, now); } - 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())); + assert_eq!(effect_weight, Weight::zero()); + assert_reachable_opening::(actor_id, count, ReachableOpeningProfile::FailedMax); + Ok(()) } - /// 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. + /// True Opening predicates and two tail-Step Opening legs precede an unfunded fixed Transfer. + /// This reachable retry corner has no LastFunding legs and does not establish envelope dominance. #[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()), + fn scheduler_inner_opening_retry_max( + t: Linear< + 0, + { + T::MaxContractSteps::get() + .saturating_sub(1) + .div_ceil(MAX_STEPS_PER_TAIL_CHUNK) + }, + >, + ) -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let (actor_id, count) = prepare_reachable_opening::(t, ReachableOpeningProfile::RetryMax)?; + let now = frame_system::Pallet::::block_number(); + let (state, admission, loaded_step) = + benchmark_fixture_consume_frame_current_step_service_state::(actor_id); + #[block] + { + execute_reachable_step_inner::(actor_id, state, admission, loaded_step, now); + } + assert_reachable_opening::(actor_id, count, ReachableOpeningProfile::RetryMax); + Ok(()) + } + + /// Measures direct minimal-geometry fresh-Opening completion at every tail-chunk count. StopCycle + /// terminates the cycle without an effect, Opening host reads, run persistence, or placement. + #[benchmark(pov_mode = Measured)] + fn scheduler_inner_opening_complete_min( + t: Linear< + 0, + { + T::MaxContractSteps::get() + .saturating_sub(1) + .div_ceil(MAX_STEPS_PER_TAIL_CHUNK) + }, + >, + ) -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let (actor_id, count) = + prepare_reachable_opening::(t, ReachableOpeningProfile::CompleteMin)?; + let now = frame_system::Pallet::::block_number(); + let (state, admission, loaded_step) = + benchmark_fixture_consume_frame_current_step_service_state::(actor_id); + #[block] + { + execute_reachable_step_inner::(actor_id, state, admission, loaded_step, now); + } + assert_reachable_opening::(actor_id, count, ReachableOpeningProfile::CompleteMin); + Ok(()) + } + + /// Complete one-Step User inner completion with an unchanged hold. Outer FIFO work stays out. + #[benchmark(extra, pov_mode = Measured)] + fn scheduler_inner_opening_user_complete_single() + -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let (actor_id, count) = + prepare_reachable_opening::(0, ReachableOpeningProfile::UserCompleteMin)?; + let accounting_before = user_pipeline_accounting::(actor_id); + let hold_before = ActorStateHolds::::get(actor_id).expect("User hold exists"); + let now = frame_system::Pallet::::block_number(); + let (state, admission, loaded_step) = + benchmark_fixture_consume_frame_current_step_service_state::(actor_id); + assert_eq!(state.identity.actor_class.actor_type(), ActorType::User); + let effect_weight; + #[block] + { + effect_weight = + execute_reachable_step_inner::(actor_id, state, admission, loaded_step, now); + } + assert_eq!(effect_weight, Weight::zero()); + assert_user_pipeline_accounting::(actor_id, accounting_before); + assert_reachable_opening::(actor_id, count, ReachableOpeningProfile::UserCompleteMin); + assert_eq!(ActorStateHolds::::get(actor_id), Some(hold_before)); + Ok(()) + } + + /// Complete one-Step User inner completion with the maximum admitted funding allowlist. + /// This differs from the single profile only by Contract-head funding-policy width. + #[benchmark(pov_mode = Measured)] + fn scheduler_inner_opening_user_complete_header_max() + -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let (actor_id, count) = + prepare_reachable_opening::(0, ReachableOpeningProfile::UserCompleteHeaderMax)?; + let accounting_before = user_pipeline_accounting::(actor_id); + let state_before = + Pallet::::active_actor_state(actor_id).expect("maximum-header User authority exists"); + let FundingSourcePolicy::SignedAllowlist(sources) = &state_before.contract.funding else { + unreachable!("maximum-header profile uses SignedAllowlist") }; - contract.steps[s as usize].on_error = StepErrorPolicy::RetryLater { - max_attempts: T::MaxRetryAttempts::get(), + assert_eq!(sources.len() as u32, T::MaxWhitelistSize::get()); + assert!(sources.contains(&state_before.identity.owner)); + let hold_before = ActorStateHolds::::get(actor_id).expect("User hold exists"); + let now = frame_system::Pallet::::block_number(); + let (state, admission, loaded_step) = + benchmark_fixture_consume_frame_current_step_service_state::(actor_id); + assert_eq!(state.identity.actor_class.actor_type(), ActorType::User); + let effect_weight; + #[block] + { + effect_weight = + execute_reachable_step_inner::(actor_id, state, admission, loaded_step, now); + } + assert_eq!(effect_weight, Weight::zero()); + assert_user_pipeline_accounting::(actor_id, accounting_before); + assert_reachable_opening::( + actor_id, + count, + ReachableOpeningProfile::UserCompleteHeaderMax, + ); + assert_eq!(ActorStateHolds::::get(actor_id), Some(hold_before)); + Ok(()) + } + + /// Complete maximum-header User inner completion with positive tails and funding-hold release. + /// This reaches the simultaneous funding-policy/tail envelope; outer FIFO work stays out. + #[benchmark(pov_mode = Measured)] + fn scheduler_inner_opening_user_complete_header_max_tail( + t: Linear< + 1, + { + T::MaxContractSteps::get() + .saturating_sub(1) + .div_ceil(MAX_STEPS_PER_TAIL_CHUNK) + .max(1) + }, + >, + ) -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let (actor_id, count) = + prepare_reachable_opening::(t, ReachableOpeningProfile::UserCompleteHeaderMax)?; + let accounting_before = user_pipeline_accounting::(actor_id); + let state_before = + Pallet::::active_actor_state(actor_id).expect("maximum-header User authority exists"); + let FundingSourcePolicy::SignedAllowlist(sources) = &state_before.contract.funding else { + unreachable!("maximum-header profile uses SignedAllowlist") }; - 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)); + assert_eq!(sources.len() as u32, T::MaxWhitelistSize::get()); + assert!(sources.contains(&state_before.identity.owner)); + let mut hold_before = ActorStateHolds::::get(actor_id).expect("User hold exists"); + let now = frame_system::Pallet::::block_number(); + let (state, admission, loaded_step) = + benchmark_fixture_consume_frame_current_step_service_state::(actor_id); + assert_eq!(state.identity.actor_class.actor_type(), ActorType::User); + let effect_weight; + #[block] + { + effect_weight = + execute_reachable_step_inner::(actor_id, state, admission, loaded_step, now); } - 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(), + assert_eq!(effect_weight, Weight::zero()); + assert_user_pipeline_accounting::(actor_id, accounting_before); + assert_reachable_opening::( + actor_id, + count, + ReachableOpeningProfile::UserCompleteHeaderMax, ); - ActorHot::::mutate(actor_id, |maybe| { - maybe - .as_mut() - .expect("Suspended-tail hot state remains") - .queue_ticket = None; - }); + let hold_after = ActorStateHolds::::get(actor_id).expect("User hold survives completion"); + assert!(hold_after.breakdown.funding < hold_before.breakdown.funding); + hold_before.breakdown.funding = hold_after.breakdown.funding; + assert_eq!( + hold_after, hold_before, + "only consumed funding changes the hold" + ); + Ok(()) + } + + /// Complete User inner completion with positive tails and funding-hold release. This branch + /// cannot share a linear fit with the one-Step no-release case; outer FIFO work stays out. + #[benchmark(extra, pov_mode = Measured)] + fn scheduler_inner_opening_user_complete_tail( + t: Linear< + 1, + { + T::MaxContractSteps::get() + .saturating_sub(1) + .div_ceil(MAX_STEPS_PER_TAIL_CHUNK) + .max(1) + }, + >, + ) -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let (actor_id, count) = + prepare_reachable_opening::(t, ReachableOpeningProfile::UserCompleteMin)?; + let accounting_before = user_pipeline_accounting::(actor_id); + let mut hold_before = ActorStateHolds::::get(actor_id).expect("User hold exists"); + let now = frame_system::Pallet::::block_number(); + let (state, admission, loaded_step) = + benchmark_fixture_consume_frame_current_step_service_state::(actor_id); + assert_eq!(state.identity.actor_class.actor_type(), ActorType::User); + let effect_weight; #[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"); + effect_weight = + execute_reachable_step_inner::(actor_id, state, admission, loaded_step, now); + } + assert_eq!(effect_weight, Weight::zero()); + assert_user_pipeline_accounting::(actor_id, accounting_before); + assert_reachable_opening::(actor_id, count, ReachableOpeningProfile::UserCompleteMin); + let hold_after = ActorStateHolds::::get(actor_id).expect("User hold survives completion"); + assert!(hold_after.breakdown.funding < hold_before.breakdown.funding); + hold_before.breakdown.funding = hold_after.breakdown.funding; + assert_eq!( + hold_after, hold_before, + "only consumed funding changes the hold" + ); + Ok(()) + } + + /// Measures the direct minimal-geometry fresh-Opening progress path at every tail-chunk count. + /// Current percentage resolution skips on zero custody; real tail ingress supplies the bounded + /// funding corner without Opening surfaces or predicates. + #[benchmark(pov_mode = Measured)] + fn scheduler_inner_opening_progress_min( + t: Linear< + 1, + { + T::MaxContractSteps::get() + .saturating_sub(1) + .div_ceil(MAX_STEPS_PER_TAIL_CHUNK) + .max(1) + }, + >, + ) -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let (actor_id, count) = prepare_reachable_opening::(t, ReachableOpeningProfile::Minimal)?; + let now = frame_system::Pallet::::block_number(); + let (state, admission, loaded_step) = + benchmark_fixture_consume_frame_current_step_service_state::(actor_id); + #[block] + { + execute_reachable_step_inner::(actor_id, state, admission, loaded_step, now); + } + assert_reachable_opening::(actor_id, count, ReachableOpeningProfile::Minimal); + Ok(()) + } + + /// True Opening predicates on every Step and two Opening legs per tail Step precede StopCycle. + /// No LastFunding payload, retained Run, or successor placement is claimed by this corner. + #[benchmark(pov_mode = Measured)] + fn scheduler_inner_opening_complete_max( + t: Linear< + 0, + { + T::MaxContractSteps::get() + .saturating_sub(1) + .div_ceil(MAX_STEPS_PER_TAIL_CHUNK) + }, + >, + ) -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let (actor_id, count) = + prepare_reachable_opening::(t, ReachableOpeningProfile::CompleteMax)?; + let now = frame_system::Pallet::::block_number(); + let (state, admission, loaded_step) = + benchmark_fixture_consume_frame_current_step_service_state::(actor_id); + #[block] + { + execute_reachable_step_inner::(actor_id, state, admission, loaded_step, now); } - 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())); + assert_reachable_opening::(actor_id, count, ReachableOpeningProfile::CompleteMax); + Ok(()) } - /// Measures the direct inner Suspended-tail successful-completion owner. - /// A fixed-zero Transfer or false current predicates skips without Task effect execution. + /// Pure-Opening progress after canonical frame loading and source consumption. Two Opening legs + /// and full Opening predicates per Step imply zero LastFunding legs in this declared corner. #[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; - }); + fn scheduler_inner_opening_progress_max( + t: Linear< + 1, + { + T::MaxContractSteps::get() + .saturating_sub(1) + .div_ceil(MAX_STEPS_PER_TAIL_CHUNK) + .max(1) + }, + >, + ) -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let (actor_id, count) = prepare_reachable_opening::(t, ReachableOpeningProfile::Predicated)?; + let now = frame_system::Pallet::::block_number(); + let (state, admission, loaded_step) = + benchmark_fixture_consume_frame_current_step_service_state::(actor_id); #[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"); + execute_reachable_step_inner::(actor_id, state, admission, loaded_step, now); } - 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() - })); + assert_reachable_opening::(actor_id, count, ReachableOpeningProfile::Predicated); + Ok(()) } - /// 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. + /// 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 + /// User post-placement hold reconciliation. The host owns StopCycle's effect Weight. #[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; - }); + fn scheduler_inner_running_complete( + s: Linear< + 1, + { + T::MaxContractSteps::get() + .saturating_sub(1) + .clamp(1, MAX_STEPS_PER_TAIL_CHUNK) + }, + >, + p: Linear<0, { benchmark_predicate_capacity::() }>, + ) -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let (actor_id, cursor) = + prepare_reachable_running_inner::(s, p, RunningInnerBranch::Complete)?; + let expected_effect = T::TaskEffectWeight::actual_effect_weight( + &ActorTask::StopCycle, + if p == 0 { + TaskEffectExecution::Invoked + } else { + TaskEffectExecution::NotInvoked + }, + ) + .expect("host supplies StopCycle effect evidence"); + let now = frame_system::Pallet::::block_number(); + let (state, admission, loaded_step) = + benchmark_fixture_consume_frame_current_step_service_state::(actor_id); #[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"); + assert_eq!( + execute_reachable_step_inner::(actor_id, state, admission, loaded_step, now), + expected_effect, + ); } - 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() - }) - ); + assert_reachable_running_inner::(actor_id, cursor, now, RunningInnerBranch::Complete); + Ok(()) } - /// 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. + /// Measures the direct inner Running-progress owner through causal FIFO successor placement. + /// A percentage of a proven zero balance or false current predicates skips Task effects. #[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; - }); + fn scheduler_inner_running_progress( + s: Linear< + 2, + { + T::MaxContractSteps::get() + .saturating_sub(1) + .clamp(2, MAX_STEPS_PER_TAIL_CHUNK) + }, + >, + p: Linear<0, { benchmark_predicate_capacity::() }>, + ) -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let (actor_id, cursor) = + prepare_reachable_running_inner::(s, p, RunningInnerBranch::Progress)?; + let now = frame_system::Pallet::::block_number(); + let (state, admission, loaded_step) = + benchmark_fixture_consume_frame_current_step_service_state::(actor_id); #[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"); + assert!( + execute_reachable_step_inner::(actor_id, state, admission, loaded_step, now).is_zero() + ); } - 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())); + assert_reachable_running_inner::(actor_id, cursor, now, RunningInnerBranch::Progress); + Ok(()) } - /// 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. + /// 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_head_complete( - n: Linear<0, 64>, - r: Linear<0, 128>, - f: Linear<0, 40>, - p: Linear<0, 4>, - ) { + fn scheduler_inner_suspended_tail_retry( + s: Linear< + 1, + { + T::MaxContractSteps::get() + .saturating_sub(1) + .clamp(1, MAX_STEPS_PER_TAIL_CHUNK) + }, + >, + p: Linear<0, { benchmark_predicate_capacity::() }>, + ) -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + if T::MaxContractSteps::get() <= 1 { + return Err(polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "host Contract bound cannot represent this tail benchmark branch", + )); + } 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"), - }); + let (actor_id, cursor) = prepare_reachable_suspended_tail_retry::(s, p)?; + let now = frame_system::Pallet::::block_number(); + let (state, admission, loaded_step) = + benchmark_fixture_consume_frame_current_step_service_state::(actor_id); + let effect_weight; + #[block] + { + effect_weight = + execute_reachable_step_inner::(actor_id, state, admission, loaded_step, now); } - 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; - }); + assert_eq!(effect_weight, Weight::zero()); + assert_reachable_suspended_tail_retry_state::(actor_id, cursor, 2); + Ok(()) + } + + /// Measures the direct inner Suspended-tail successful-completion owner. + /// A real unfunded retry receives exact spendable custody and invokes its fixed Transfer. + #[benchmark(pov_mode = Measured)] + fn scheduler_inner_suspended_tail_complete( + s: Linear< + 1, + { + T::MaxContractSteps::get() + .saturating_sub(1) + .clamp(1, MAX_STEPS_PER_TAIL_CHUNK) + }, + >, + p: Linear<0, { benchmark_predicate_capacity::() }>, + ) -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let fixture = + prepare_reachable_suspended_tail_success::(s, p, RunningInnerBranch::Complete)?; + let now = frame_system::Pallet::::block_number(); + let (state, admission, loaded_step) = + benchmark_fixture_consume_frame_current_step_service_state::(fixture.actor_id); + let expected_effect = T::TaskEffectWeight::actual_effect_weight( + &loaded_step.step.task, + TaskEffectExecution::Invoked, + ) + .expect("host supplies Transfer effect evidence"); #[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_eq!( + execute_reachable_step_inner::(fixture.actor_id, state, admission, loaded_step, now), + expected_effect, + ); + } + assert_reachable_suspended_tail_success::(fixture, now, RunningInnerBranch::Complete); + Ok(()) + } + + /// Measures the direct inner Suspended-tail successful-progress owner through causal FIFO + /// successor placement after a real unfunded retry receives exact spendable custody. + #[benchmark(pov_mode = Measured)] + fn scheduler_inner_suspended_tail_progress( + s: Linear< + 2, + { + T::MaxContractSteps::get() + .saturating_sub(1) + .clamp(2, MAX_STEPS_PER_TAIL_CHUNK) + }, + >, + p: Linear<0, { benchmark_predicate_capacity::() }>, + ) -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let fixture = + prepare_reachable_suspended_tail_success::(s, p, RunningInnerBranch::Progress)?; + let now = frame_system::Pallet::::block_number(); + let (state, admission, loaded_step) = + benchmark_fixture_consume_frame_current_step_service_state::(fixture.actor_id); + let expected_effect = T::TaskEffectWeight::actual_effect_weight( + &loaded_step.step.task, + TaskEffectExecution::Invoked, + ) + .expect("host supplies Transfer effect evidence"); + #[block] + { + assert_eq!( + execute_reachable_step_inner::(fixture.actor_id, state, admission, loaded_step, now), + expected_effect, + ); } - 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() - })); + assert_reachable_suspended_tail_success::(fixture, now, RunningInnerBranch::Progress); + Ok(()) } - /// 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. + /// Measures a real Current-predicate head retry after Opening and due wakeup. + /// Tail Opening and funding cardinalities share the same authored amount-source allocation. #[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"), - }); + fn scheduler_inner_suspended_head_retry( + n: Linear< + { + (T::MaxContractSteps::get().saturating_sub(1) * 2) + .saturating_sub(T::MaxFundingTrackedAssets::get()) + }, + { T::MaxContractSteps::get().saturating_sub(1) * 2 }, + >, + p: Linear<0, { benchmark_predicate_capacity::() }>, + ) -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let (actor_id, skip) = prepare_reachable_suspended_head_inner::(n, p, 0, None)?; + let before = ActorRunStateStore::::get(actor_id).expect("real suspended Run exists"); + let now = frame_system::Pallet::::block_number(); + let (state, admission, loaded_step) = + benchmark_fixture_consume_frame_current_step_service_state::(actor_id); + let effect; + #[block] + { + effect = execute_reachable_step_inner::(actor_id, state, admission, loaded_step, now); } - 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(), + assert_eq!(effect, Weight::zero()); + assert!(skip.is_none()); + assert_reachable_suspended_head_retry::(actor_id, before); + Ok(()) + } + + /// Measures a real Current-predicate head complete after Opening and due wakeup. + /// A terminal head has one authored Step and no tail payload. + #[benchmark(pov_mode = Measured)] + fn scheduler_inner_suspended_head_complete( + p: Linear<0, { benchmark_predicate_capacity::() }>, + ) -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let (actor_id, skip) = + prepare_reachable_suspended_head_inner::(0, p, 0, Some(RunningInnerBranch::Complete))?; + let before = ActorRunStateStore::::get(actor_id).expect("real suspended Run exists"); + let now = frame_system::Pallet::::block_number(); + let (state, admission, loaded_step) = + benchmark_fixture_consume_frame_current_step_service_state::(actor_id); + let effect; + #[block] + { + effect = execute_reachable_step_inner::(actor_id, state, admission, loaded_step, now); + } + assert_eq!(effect, Weight::zero()); + assert_eq!(before.cursor, 0); + assert_reachable_suspended_tail_skip::( + skip.expect("freeze fixture exists"), + now, + RunningInnerBranch::Complete, ); - ActorHot::::mutate(actor_id, |maybe| { - maybe - .as_mut() - .expect("Suspended-head progress hot state remains") - .queue_ticket = None; - }); + Ok(()) + } + + /// Measures a real Current-predicate head progress after Opening and due wakeup. + /// Tail Opening and funding cardinalities share the same authored amount-source allocation. + #[benchmark(pov_mode = Measured)] + fn scheduler_inner_suspended_head_progress( + n: Linear< + { + (T::MaxContractSteps::get().saturating_sub(1) * 2) + .saturating_sub(T::MaxFundingTrackedAssets::get()) + }, + { T::MaxContractSteps::get().saturating_sub(1) * 2 }, + >, + p: Linear<0, { benchmark_predicate_capacity::() }>, + ) -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let (actor_id, skip) = + prepare_reachable_suspended_head_inner::(n, p, 0, Some(RunningInnerBranch::Progress))?; + let before = ActorRunStateStore::::get(actor_id).expect("real suspended Run exists"); + let now = frame_system::Pallet::::block_number(); + let (state, admission, loaded_step) = + benchmark_fixture_consume_frame_current_step_service_state::(actor_id); + let effect; #[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() - }) + effect = execute_reachable_step_inner::(actor_id, state, admission, loaded_step, now); + } + assert_eq!(effect, Weight::zero()); + assert_eq!(before.cursor, 0); + assert_reachable_suspended_tail_skip::( + skip.expect("freeze fixture exists"), + now, + RunningInnerBranch::Progress, ); + Ok(()) } - /// 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. + /// Measures a real Opening/Current-predicate head retry after Opening and due wakeup. + /// Tail Opening and funding cardinalities share the same authored amount-source allocation. #[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; - }); + n: Linear< + { + (T::MaxContractSteps::get().saturating_sub(1) * 2) + .saturating_sub(T::MaxFundingTrackedAssets::get()) + }, + { T::MaxContractSteps::get().saturating_sub(1) * 2 }, + >, + p: Linear<0, { benchmark_predicate_capacity::().saturating_sub(1) }>, + ) -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let (actor_id, skip) = prepare_reachable_suspended_head_inner::(n, p, 1, None)?; + let before = ActorRunStateStore::::get(actor_id).expect("real suspended Run exists"); + let now = frame_system::Pallet::::block_number(); + let (state, admission, loaded_step) = + benchmark_fixture_consume_frame_current_step_service_state::(actor_id); + let effect; #[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"); + effect = execute_reachable_step_inner::(actor_id, state, admission, loaded_step, now); } - 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())); + assert_eq!(effect, Weight::zero()); + assert!(skip.is_none()); + assert_reachable_suspended_head_retry::(actor_id, before); + Ok(()) } - /// 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. + /// Measures a real Opening/Current-predicate head complete after Opening and due wakeup. + /// A terminal head has one authored Step and no tail payload. #[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; - }); + p: Linear<0, { benchmark_predicate_capacity::().saturating_sub(1) }>, + ) -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let (actor_id, skip) = + prepare_reachable_suspended_head_inner::(0, p, 1, Some(RunningInnerBranch::Complete))?; + let before = ActorRunStateStore::::get(actor_id).expect("real suspended Run exists"); + let now = frame_system::Pallet::::block_number(); + let (state, admission, loaded_step) = + benchmark_fixture_consume_frame_current_step_service_state::(actor_id); + let effect; #[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)) - } - } - }) - .expect("Opening-heavy 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() - })); + effect = execute_reachable_step_inner::(actor_id, state, admission, loaded_step, now); + } + assert_eq!(effect, Weight::zero()); + assert_eq!(before.cursor, 0); + assert_reachable_suspended_tail_skip::( + skip.expect("freeze fixture exists"), + now, + RunningInnerBranch::Complete, + ); + Ok(()) } - /// 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. + /// Measures a real Opening/Current-predicate head progress after Opening and due wakeup. + /// Tail Opening and funding cardinalities share the same authored amount-source allocation. #[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(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; - }); + fn scheduler_inner_suspended_head_opening_progress( + n: Linear< + { + (T::MaxContractSteps::get().saturating_sub(1) * 2) + .saturating_sub(T::MaxFundingTrackedAssets::get()) + }, + { T::MaxContractSteps::get().saturating_sub(1) * 2 }, + >, + p: Linear<0, { benchmark_predicate_capacity::().saturating_sub(1) }>, + ) -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let (actor_id, skip) = + prepare_reachable_suspended_head_inner::(n, p, 1, Some(RunningInnerBranch::Progress))?; + let before = ActorRunStateStore::::get(actor_id).expect("real suspended Run exists"); + let now = frame_system::Pallet::::block_number(); + let (state, admission, loaded_step) = + benchmark_fixture_consume_frame_current_step_service_state::(actor_id); + let effect; #[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 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"); - } - 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() - }) + effect = execute_reachable_step_inner::(actor_id, state, admission, loaded_step, now); + } + assert_eq!(effect, Weight::zero()); + assert_eq!(before.cursor, 0); + assert_reachable_suspended_tail_skip::( + skip.expect("freeze fixture exists"), + now, + RunningInnerBranch::Progress, ); + Ok(()) } /// Measures actual scheduler admission and complete execution for up to 1,000 @@ -7253,44 +13001,18 @@ mod benches { /// execution, and consumption rather than actor creation. #[benchmark(pov_mode = Measured)] fn scheduler_paged_execute_cheap(n: Linear<1, 1_000>) { - let _ = QueuePages::::clear(u32::MAX, None); - QueueHead::::put(0); - QueueTail::::put(0); - QueueOccupancy::::put(0); - NextQueueTicket::::put(0); + benchmark_fixture_reset_ready_queue::(); GlobalCircuitBreaker::::put(false); let bounded = n .min(T::MaxExecutionsPerBlock::get()) .min(T::MaxQueueEntriesScannedPerBlock::get()) .min(T::MaxQueueLength::get()); assert!(bounded > 0, "runtime limits must admit at least one sample"); - let template_id = bench_create_system_manual::(40_000_000); - let hot_template = ActorHot::::get(template_id).expect("benchmark hot template"); - let identity_template = - ActorIdentities::::get(template_id).expect("benchmark identity template"); - let 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); - Pallet::::remove_actor_contract(template_id).expect("benchmark template Contract removes"); - ActorFunding::::remove(template_id); - - let first_id = 41_000_000u64; + let mut actors = alloc::vec::Vec::with_capacity(bounded as usize); for offset in 0..bounded { - let actor_id = first_id.saturating_add(u64::from(offset)); - let mut hot = hot_template.clone(); - let mut identity = identity_template.clone(); - identity.cycle_nonce = 0; - hot.last_cycle_block = None; - hot.pending_signal = true; - hot.queue_ticket = None; - ActorIdentities::::insert(actor_id, identity); - ActorHot::::insert(actor_id, hot); - 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)); + let actor_id = bench_create_system_manual::(41_000_000u32.saturating_add(offset)); + Pallet::::request_activation(actor_id).expect("cheap benchmark readiness must latch"); + actors.push(actor_id); } let now: BlockNumberFor = 1u32.into(); frame_system::Pallet::::set_block_number(now); @@ -7298,28 +13020,28 @@ mod benches { { core::hint::black_box(Pallet::::execute_cycle(Weight::MAX)); } - let executed = (0..bounded) - .filter(|offset| { - let actor_id = first_id.saturating_add(u64::from(*offset)); - ActorIdentities::::get(actor_id).is_some_and(|identity| identity.cycle_nonce == 1) + let executed = actors + .iter() + .filter(|actor_id| { + benchmark_fixture_identity::(**actor_id) + .is_some_and(|identity| identity.cycle_nonce == 1) }) .count() as u32; assert_eq!( executed, bounded, "unbounded diagnostic budget completed only {executed} of {bounded} requested cheap actors" ); - assert_eq!(QueueHead::::get(), QueueTail::::get()); + assert_eq!( + benchmark_fixture_ready_head::(), + benchmark_fixture_ready_tail::() + ); } /// Measures canonical FIFO execution over alternating System/User actors. /// Setup materializes one ticket-ordered queue outside the measured block. #[benchmark(pov_mode = Measured)] fn scheduler_paged_execute_cheap_mixed(n: Linear<2, 1_000>) { - let _ = QueuePages::::clear(u32::MAX, None); - QueueHead::::put(0); - QueueTail::::put(0); - QueueOccupancy::::put(0); - NextQueueTicket::::put(0); + benchmark_fixture_reset_ready_queue::(); GlobalCircuitBreaker::::put(false); let bounded = n .min(T::MaxExecutionsPerBlock::get()) @@ -7330,226 +13052,845 @@ mod benches { "runtime limits must admit alternating actor classes" ); - let system_template_id = bench_create_system_manual::(42_000_000); - let system_identity = - ActorIdentities::::take(system_template_id).expect("System identity template"); - let system_hot = ActorHot::::take(system_template_id).expect("System hot template"); - let 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; - for offset in 0..bounded { - let actor_id = first_id.saturating_add(u64::from(offset)); - let is_user = offset % 2 != 0; - let mut identity = if is_user { - let owner: T::AccountId = account("mixed_user_owner", offset, 0); - let template_id = bench_create_user::(owner); - let identity = ActorIdentities::::take(template_id).expect("User identity template"); - ActorHot::::remove(template_id); - Pallet::::remove_actor_contract(template_id).expect("User template Contract removes"); - ActorFunding::::remove(template_id); - identity - } else { - system_identity.clone() - }; - identity.cycle_nonce = 0; - let mut hot = system_hot.clone(); - hot.last_cycle_block = None; - hot.pending_signal = true; - hot.queue_ticket = None; - ActorIdentities::::insert(actor_id, identity); - ActorHot::::insert(actor_id, hot); - 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)); + let mut actors = alloc::vec::Vec::with_capacity(bounded as usize); + for offset in 0..bounded { + let actor_id = if offset % 2 == 0 { + bench_create_system_manual::(43_000_000u32.saturating_add(offset)) + } else { + let owner: T::AccountId = account("mixed_user_owner", offset, 0); + bench_create_user::(owner) + }; + Pallet::::request_activation(actor_id).expect("mixed benchmark readiness must latch"); + actors.push(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 executed = actors + .iter() + .filter(|actor_id| { + benchmark_fixture_identity::(**actor_id) + .is_some_and(|identity| identity.cycle_nonce == 1) + }) + .count() as u32; + // Whether User fee collection materializes a Fee Sink service obligation is host + // configuration, not a pallet guarantee. When the host does configure one, that obligation + // consumes a single pass slot ahead of the measured cohort while the post-cutoff ticket stays + // ordered behind it; a host without a Fee Sink runs the whole cohort. + let ceiling = bounded.min(T::MaxExecutionsPerBlock::get()); + assert!( + executed == ceiling || executed == ceiling.saturating_sub(1), + "mixed canonical-FIFO cohort must reach the pass ceiling, minus at most one slot taken by a host Fee Sink service obligation" + ); + let consumed = actors + .iter() + .filter(|actor_id| { + benchmark_fixture_hot::(**actor_id).is_some_and(|hot| hot.queue_ticket.is_none()) + }) + .count() as u32; + assert_eq!( + consumed, executed, + "every executed cohort actor must release its queue ticket" + ); + } + + /// Diagnostic classifier for dense Running-capable Contract geometry. Each singleton component + /// point uses the ordinary public System constructor and either publishes one admitted actor or + /// stops with the exact idle-budget rejection. It is excluded from production Weight output. + #[benchmark(extra)] + fn run_dense_admission_classifier( + s: Linear<1, { T::MaxContractSteps::get() }>, + ) -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let owner: T::AccountId = account("dense-run-admission-owner", s, 0); + let steps = make_reachable_opening_steps::(s)?; + let creation = Pallet::::create_system_actor( + RawOrigin::Root.into(), + owner, + Mutability::Mutable, + system_contract::( + Schedule { + trigger: Trigger::manual(), + cooldown_blocks: 0, + }, + steps, + ), + ); + if let Err(error) = creation { + assert_eq!( + error, + Error::::ContractStepsExceedOnIdleBudget.into(), + "dense Run admission classifier encountered an unrelated creation failure" + ); + return Err(polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "dense Run point rejected by ordinary idle-budget admission", + )); + } + let actor_id = NextActorId::::get().saturating_sub(1); + #[block] + { + core::hint::black_box(Pallet::::active_actor_view(actor_id)); + } + assert_eq!( + Pallet::::active_actor_view(actor_id) + .expect("admitted dense classifier actor exists") + .steps + .len() as u32, + s + ); + Ok(()) + } + + fn prepare_low_capture_running_with_type( + actor_type: ActorType, + current_step: Option>, + cooldown_blocks: u32, + ) -> Result { + if T::MaxContractSteps::get() < 2 { + return Err(polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "low-capture Running fixture requires a successor Step", + )); + } + frame_system::Pallet::::set_block_number(1u32.into()); + GlobalCircuitBreaker::::put(false); + let owner: T::AccountId = account("low-capture-run-owner", 0, 0); + let mut steps = make_max_contract_steps::(account("low-capture-run-recipient", 0, 0)); + if let Some(step) = current_step { + steps[1] = step; + } + let schedule = Schedule { + trigger: Trigger::manual(), + cooldown_blocks, + }; + if actor_type == ActorType::User { + ensure_creation_balance::(&owner); + prefund_active_user_creation::(&owner, &steps); + Pallet::::create_user_actor( + RawOrigin::Signed(owner.clone()).into(), + Mutability::Mutable, + user_contract::(schedule, steps), + ) + } else { + Pallet::::create_system_actor( + RawOrigin::Root.into(), + owner.clone(), + Mutability::Mutable, + system_contract::(schedule, steps), + ) + } + .map_err(|_| { + polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "maximum-length low-capture Contract rejected by ordinary creation", + ) + })?; + let actor_id = NextActorId::::get().saturating_sub(1); + if actor_type == ActorType::User { + let sovereign = Pallet::::actor_identity(actor_id) + .expect("admitted low-capture User identity exists") + .sovereign_account; + let trigger_fee = Pallet::::trigger_fee_for_weight( + ActorType::User, + TriggerFamily::Manual, + T::WeightInfo::manual_trigger(), + ) + .trigger_fee; + T::AssetOps::mint(&sovereign, T::FeeNativeAssetId::get(), trigger_fee) + .expect("low-capture User Manual readiness is independently funded"); + } + Pallet::::manual_trigger(RawOrigin::Signed(owner).into(), actor_id).map_err(|_| { + polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "maximum-length low-capture Contract failed Manual readiness", + ) + })?; + Pallet::::execute_cycle(Weight::MAX); + let run = ActorRunStateStore::::get(actor_id).ok_or( + polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "maximum-length low-capture Contract did not publish Running authority", + ), + )?; + assert_eq!(run.cursor, 1); + assert!(run.opening_snapshot.is_empty()); + assert!(run.opening_predicate_results.is_empty()); + assert!(run.funding_snapshot.is_empty()); + assert!(matches!( + ActorControlLocators::::get(actor_id), + Some(ActorControlLocation::Ready { .. }) + )); + if actor_type == ActorType::User { + assert!(ActorStateHolds::::contains_key(actor_id)); + } + Ok(actor_id) + } + + fn prepare_low_capture_running( + current_step: Option>, + cooldown_blocks: u32, + ) -> Result { + prepare_low_capture_running_with_type::(ActorType::System, current_step, cooldown_blocks) + } + + fn prepare_run_terminal_failure_source() + -> Result { + let owner: T::AccountId = account("low-capture-run-owner", 0, 0); + let (position, _) = T::BenchmarkHelper::setup_stake(&owner).map_err(|_| { + polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "host cannot prepare an empty terminal-failure staking receipt", + ) + })?; + let receipt = T::StakingOps::share_asset(position).ok_or( + polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "host did not create an admissible terminal-failure staking receipt", + ), + )?; + let actor_id = prepare_low_capture_running_with_type::( + ActorType::User, + Some(Step { + precondition: None, + task: ActorTask::Unstake { + asset: position, + shares: AmountResolution::PercentageOfLastFunding(Perbill::from_percent(50)), + }, + on_error: StepErrorPolicy::AbortCycle, + }), + 0, + )?; + assert!( + ActorFunding::::get(actor_id) + .expect("terminal-failure funding authority exists") + .funding_tracked_assets + .contains(&receipt) + ); + T::BenchmarkHelper::remove_empty_staking_receipt(&owner, position).map_err(|_| { + polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "host cannot remove the empty terminal-failure staking receipt", + ) + })?; + assert!(T::StakingOps::share_asset(position).is_none()); + Ok(actor_id) + } + + fn prepare_run_owner_suspension_source( + dense: bool, + cooldown_blocks: u32, + ) -> Result { + if dense && T::MaxContractSteps::get() < 3 { + return Err(polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "dense Run owner fixture requires three Steps", + )); + } + let asset_owner: T::AccountId = account("run-owner-retry-assets", 0, 0); + let capacity = benchmark_predicate_capacity::(); + let mut assets = T::BenchmarkHelper::setup_predicate_assets( + &asset_owner, + 2 + if dense { capacity } else { 0 }, + ) + .map_err(|_| { + polkadot_sdk::frame_benchmarking::BenchmarkError::Stop("host retry asset setup unsupported") + })?; + let retry_index = assets + .iter() + .position(|asset| *asset != T::FeeNativeAssetId::get()) + .ok_or(polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "Run suspension fixture needs a non-native unfunded asset", + ))?; + let retry_asset = assets.remove(retry_index); + assets.retain(|asset| *asset != T::FeeNativeAssetId::get()); + assets.truncate(if dense { capacity as usize } else { 0 }); + if dense && assets.len() != capacity as usize { + return Err(polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "host cannot represent dense suspension predicates", + )); + } + let retry_step = Step { + precondition: dense.then(|| { + packed_predicate_clauses::( + assets + .into_iter() + .map(|asset| TimedPredicate { + timing: ObservationTiming::Opening, + predicate: Predicate::BalanceBelow { + asset, + threshold: One::one(), + }, + }) + .collect(), + T::MaxPredicatesPerClause::get(), + ) + }), + task: ActorTask::Transfer { + to: account("run-owner-retry-recipient", 0, 0), + asset: retry_asset, + amount: AmountResolution::Fixed(One::one()), + }, + on_error: StepErrorPolicy::RetryLater { + max_attempts: T::MaxRetryAttempts::get(), + }, + }; + let actor_id = if dense { + prepare_reachable_running_with_cooldown::( + ActorType::System, + 3, + Some((1, retry_step)), + cooldown_blocks, + )? + } else { + prepare_low_capture_running::(Some(retry_step), cooldown_blocks)? + }; + let state = Pallet::::active_actor_state(actor_id).expect("real Running source exists"); + assert!(T::AssetOps::balance(&state.identity.sovereign_account, retry_asset).is_zero()); + Ok(actor_id) + } + + fn prepare_run_owner_suspension( + dense: bool, + ) -> Result { + let actor_id = prepare_run_owner_suspension_source::(dense, 2)?; + let run = ActorRunStateStore::::get(actor_id).expect("real Running payload exists"); + frame_system::Pallet::::set_block_number(run.eligible_at); + Pallet::::execute_cycle(Weight::MAX); + let suspended = ActorRunStateStore::::get(actor_id).expect("real suspension retains Run"); + assert_eq!(suspended.cursor, 1); + assert_eq!( + suspended.suspension, + Some(SuspensionReason::FundingUnavailable) + ); + if dense { + assert_eq!(suspended.opening_snapshot.len() as u32, 4); + assert_eq!( + suspended.opening_predicate_results.len() as u32, + 3 * benchmark_predicate_capacity::() + ); + } else { + assert!(suspended.opening_snapshot.is_empty()); + assert!(suspended.opening_predicate_results.is_empty()); + } + frame_system::Pallet::::set_block_number(suspended.eligible_at); + let mut meter = polkadot_sdk::sp_weights::WeightMeter::with_limit(Weight::MAX); + assert!( + Pallet::::drain_overdue_wakeups_cursor(suspended.eligible_at, &mut meter).ready_entries + >= 1 + ); + assert!(matches!( + ActorControlLocators::::get(actor_id), + Some(ActorControlLocation::Ready { .. }) + )); + Ok(actor_id) + } + + /// Diagnostic structural probe for a maximum-length Run with no Opening amount or predicate + /// capture. Public creation, Manual readiness and normal Q1 execution remain in force. + #[benchmark(extra)] + fn run_low_capture_max_probe() -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let actor_id = prepare_low_capture_running::(None, 0)?; + #[block] + { + core::hint::black_box(Pallet::::load_current_step_service_state(actor_id)); + } + Ok(()) + } + + /// Candidate complete consumed-source successor for the largest admitted dense Running domain. + /// The canonical Ready span is full of reachable tombstones, so ordinary Queue publication falls + /// back to a new Block wakeup without reintroducing the consumed source. This remains diagnostic + /// until EXP-0058/0059 accept the finite successor envelope. + #[benchmark(extra, pov_mode = Measured)] + fn run_successor_continued_wakeup_dense() + -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let actor_id = prepare_reachable_running::(3)?; + let source = ActorRunStateStore::::get(actor_id).expect("dense Running source exists"); + assert_eq!(source.cursor, 1); + assert_eq!(source.opening_snapshot.len() as u32, 6); + assert_eq!( + source.opening_predicate_results.len() as u32, + 3 * benchmark_predicate_capacity::() + ); + let now = source.eligible_at; + frame_system::Pallet::::set_block_number(now); + let (state, admission, loaded_step) = + benchmark_fixture_consume_frame_current_step_service_state::(actor_id); + benchmark_fixture_seed_saturated_ready_tombstones::(); + #[block] + { + assert!( + execute_reachable_step_inner::(actor_id, state, admission, loaded_step, now).is_zero() + ); + } + let successor = + ActorRunStateStore::::get(actor_id).expect("continued dense successor retains Run"); + assert_eq!(successor.cursor, 2); + assert_eq!(successor.last_committed_step_block, Some(now)); + assert!(matches!( + ActorControlLocators::::get(actor_id), + Some(ActorControlLocation::Waiting { + key: WakeupKey::Block(at), + .. + }) if at == successor.eligible_at + )); + assert_eq!(benchmark_fixture_ready_occupancy::(), 0); + Ok(()) + } + + /// Candidate complete consumed-source successor for the admitted maximum-length, low-capture + /// Running domain. The canonical Ready span is full of reachable tombstones, so ordinary Queue + /// publication falls back to a new Block wakeup without reintroducing the consumed source. + /// This remains diagnostic until EXP-0058/0059 accept the finite successor envelope. + #[benchmark(extra, pov_mode = Measured)] + fn run_successor_continued_wakeup_low_capture() + -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let actor_id = prepare_low_capture_running::(None, 0)?; + let now = ActorRunStateStore::::get(actor_id) + .expect("low-capture Running source exists") + .eligible_at; + frame_system::Pallet::::set_block_number(now); + let (state, admission, loaded_step) = + benchmark_fixture_consume_frame_current_step_service_state::(actor_id); + benchmark_fixture_seed_saturated_ready_tombstones::(); + #[block] + { + assert!( + execute_reachable_step_inner::(actor_id, state, admission, loaded_step, now).is_zero() + ); + } + let successor = + ActorRunStateStore::::get(actor_id).expect("continued low-capture successor retains Run"); + assert_eq!(successor.cursor, 2); + assert_eq!(successor.last_committed_step_block, Some(now)); + assert!(matches!( + ActorControlLocators::::get(actor_id), + Some(ActorControlLocation::Waiting { + key: WakeupKey::Block(at), + .. + }) if at == successor.eligible_at + )); + assert_eq!(benchmark_fixture_ready_occupancy::(), 0); + Ok(()) + } + + /// Candidate complete consumed-source successful terminal for the admitted maximum-length, + /// low-capture User Running domain. StopCycle clears Run authority and publishes one Unsignaled + /// primary while retaining exactly one post-publication state-hold reconciliation. + #[benchmark(extra, pov_mode = Measured)] + fn run_successor_completed_unsignaled_low_capture() + -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let actor_id = prepare_low_capture_running_with_type::( + ActorType::User, + Some(Step { + precondition: None, + task: ActorTask::StopCycle, + on_error: StepErrorPolicy::AbortCycle, + }), + 0, + )?; + let now = ActorRunStateStore::::get(actor_id) + .expect("low-capture Running completion source exists") + .eligible_at; + frame_system::Pallet::::set_block_number(now); + let (state, admission, loaded_step) = + benchmark_fixture_consume_frame_current_step_service_state::(actor_id); + let expected_effect = T::TaskEffectWeight::actual_effect_weight( + &ActorTask::StopCycle, + TaskEffectExecution::Invoked, + ) + .expect("host supplies StopCycle effect evidence"); + #[block] + { + assert_eq!( + execute_reachable_step_inner::(actor_id, state, admission, loaded_step, now), + expected_effect + ); + } + let successor = + Pallet::::active_actor_state(actor_id).expect("completed User authority remains active"); + assert!(successor.run_state.is_none()); + assert_eq!(successor.identity.cycle_nonce, 1); + assert_eq!(successor.hot.cycle_state, CycleState::Idle); + assert!(!successor.hot.pending_signal); + assert_eq!(successor.hot.unsuccessful_attempt_streak, 0); + assert!(matches!( + ActorControlLocators::::get(actor_id), + Some(ActorControlLocation::Unsignaled) + )); + assert_eq!(benchmark_fixture_ready_occupancy::(), 0); + assert!(ActorStateHolds::::contains_key(actor_id)); + Ok(()) + } + + /// Candidate complete consumed-source failed terminal for the admitted maximum-length, + /// low-capture User Running domain. A receipt mapping removed after ordinary admission causes a + /// permanent pre-effect failure, clears Run authority and publishes exactly one Unsignaled owner. + #[benchmark(extra, pov_mode = Measured)] + fn run_successor_failed_unsignaled_low_capture() + -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let actor_id = prepare_run_terminal_failure_source::()?; + let now = ActorRunStateStore::::get(actor_id) + .expect("low-capture Running failure source exists") + .eligible_at; + frame_system::Pallet::::set_block_number(now); + let (state, admission, loaded_step) = + benchmark_fixture_consume_frame_current_step_service_state::(actor_id); + #[block] + { + assert!( + execute_reachable_step_inner::(actor_id, state, admission, loaded_step, now).is_zero() + ); + } + let successor = + Pallet::::active_actor_state(actor_id).expect("failed User authority remains active"); + assert!(successor.run_state.is_none()); + assert_eq!(successor.identity.cycle_nonce, 1); + assert_eq!(successor.hot.cycle_state, CycleState::Idle); + assert!(!successor.hot.pending_signal); + assert_eq!(successor.hot.unsuccessful_attempt_streak, 1); + assert!(matches!( + ActorControlLocators::::get(actor_id), + Some(ActorControlLocation::Unsignaled) + )); + assert_eq!(benchmark_fixture_ready_occupancy::(), 0); + assert!(ActorStateHolds::::contains_key(actor_id)); + Ok(()) + } + + /// Candidate complete consumed-source suspension for the admitted maximum-length, low-capture + /// Running domain. The first retry has an exact-next-block eligibility and therefore publishes + /// directly to Queue inside the snapshotted service cutoff. + #[benchmark(extra, pov_mode = Measured)] + fn run_successor_suspended_queue_low_capture() + -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let actor_id = prepare_run_owner_suspension_source::(false, 0)?; + let now = ActorRunStateStore::::get(actor_id) + .expect("low-capture Running retry source exists") + .eligible_at; + frame_system::Pallet::::set_block_number(now); + let (state, admission, loaded_step) = + benchmark_fixture_consume_frame_current_step_service_state::(actor_id); + #[block] + { + assert!( + execute_reachable_step_inner::(actor_id, state, admission, loaded_step, now).is_zero() + ); + } + let successor = + ActorRunStateStore::::get(actor_id).expect("suspended low-capture successor retains Run"); + assert_eq!(successor.cursor, 1); + assert_eq!(successor.last_attempt_block, now); + assert_eq!(successor.eligible_at, now.saturating_add(One::one())); + assert_eq!( + successor.suspension, + Some(SuspensionReason::FundingUnavailable) + ); + assert!(matches!( + ActorControlLocators::::get(actor_id), + Some(ActorControlLocation::Ready { .. }) + )); + assert_eq!(benchmark_fixture_ready_occupancy::(), 1); + Ok(()) + } + + /// Candidate complete consumed-source suspension for the admitted maximum-length, low-capture + /// Running domain. A two-block cooldown makes the retry ineligible inside the snapshotted cutoff, + /// so the successor is published once into a new Block Waiting key. + #[benchmark(extra, pov_mode = Measured)] + fn run_successor_suspended_wakeup_low_capture() + -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let actor_id = prepare_run_owner_suspension_source::(false, 2)?; + let now = ActorRunStateStore::::get(actor_id) + .expect("low-capture Running retry source exists") + .eligible_at; + frame_system::Pallet::::set_block_number(now); + let (state, admission, loaded_step) = + benchmark_fixture_consume_frame_current_step_service_state::(actor_id); + #[block] + { + assert!( + execute_reachable_step_inner::(actor_id, state, admission, loaded_step, now).is_zero() + ); + } + let successor = + ActorRunStateStore::::get(actor_id).expect("suspended low-capture successor retains Run"); + assert_eq!(successor.cursor, 1); + assert_eq!(successor.last_attempt_block, now); + assert_eq!(successor.eligible_at, now.saturating_add(2u32.into())); + assert_eq!( + successor.suspension, + Some(SuspensionReason::FundingUnavailable) + ); + assert!(matches!( + ActorControlLocators::::get(actor_id), + Some(ActorControlLocation::Waiting { + key: WakeupKey::Block(at), + .. + }) if at == successor.eligible_at + )); + assert_eq!(benchmark_fixture_ready_occupancy::(), 0); + Ok(()) + } + + // Diagnostic overlap classifiers only: `write_run_state` starts from a retained Ready primary + // and also maintains a Block wakeup reference. These profiles bound admitted payload extrema, + // but they are not composable production Run primitives after Ready source consumption. + #[benchmark(extra, pov_mode = Measured)] + fn run_progress_dense() -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let actor_id = prepare_reachable_running::(3)?; + let state = ActorRunStateStore::::get(actor_id).expect("dense Running state exists"); + #[block] + { + Pallet::::persist_run_progress(actor_id, state) + .expect("dense Running progress must persist"); + } + Ok(()) + } + + #[benchmark(extra, pov_mode = Measured)] + fn run_progress_low_capture() -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let actor_id = prepare_low_capture_running::(None, 0)?; + let state = ActorRunStateStore::::get(actor_id).expect("low-capture Running state exists"); + #[block] + { + Pallet::::persist_run_progress(actor_id, state) + .expect("low-capture Running progress must persist"); + } + Ok(()) + } + + #[benchmark(extra, pov_mode = Measured)] + fn run_suspend_dense() -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let actor_id = prepare_run_owner_suspension::(true)?; + let state = ActorRunStateStore::::get(actor_id).expect("dense Suspended state exists"); + #[block] + { + Pallet::::persist_run_suspension(actor_id, state).expect("dense suspension must persist"); } - let now: BlockNumberFor = 1u32.into(); - frame_system::Pallet::::set_block_number(now); + Ok(()) + } + + #[benchmark(extra, pov_mode = Measured)] + fn run_suspend_low_capture() -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let actor_id = prepare_run_owner_suspension::(false)?; + let state = ActorRunStateStore::::get(actor_id).expect("low-capture Suspended state exists"); #[block] { - core::hint::black_box(Pallet::::execute_cycle(Weight::MAX)); + Pallet::::persist_run_suspension(actor_id, state) + .expect("low-capture suspension must persist"); } - let executed = (0..bounded) - .filter(|offset| { - let actor_id = first_id.saturating_add(u64::from(*offset)); - ActorIdentities::::get(actor_id).is_some_and(|identity| identity.cycle_nonce == 1) - }) - .count() as u32; - // Whether User fee collection materializes a Fee Sink service obligation is host - // configuration, not a pallet guarantee. When the host does configure one, that obligation - // consumes a single pass slot ahead of the measured cohort while the post-cutoff ticket stays - // ordered behind it; a host without a Fee Sink runs the whole cohort. - let ceiling = bounded.min(T::MaxExecutionsPerBlock::get()); - assert!( - executed == ceiling || executed == ceiling.saturating_sub(1), - "mixed canonical-FIFO cohort must reach the pass ceiling, minus at most one slot taken by a host Fee Sink service obligation" - ); - let consumed = (0..bounded) - .filter(|offset| { - let actor_id = first_id.saturating_add(u64::from(*offset)); - ActorHot::::get(actor_id).is_some_and(|hot| hot.queue_ticket.is_none()) - }) - .count() as u32; - assert_eq!( - consumed, executed, - "every executed cohort actor must release its queue ticket" - ); + Ok(()) } - #[benchmark(pov_mode = Measured)] - 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; + #[benchmark(extra, pov_mode = Measured)] + fn run_complete_dense() -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let actor_id = prepare_reachable_running::(3)?; #[block] { - Pallet::::persist_run_progress(actor_id, state) - .expect("benchmark Running progress must persist"); + Pallet::::write_run_state(actor_id, None).expect("dense completion must clear Run"); } - assert!(ActorRunStateStore::::contains_key(actor_id)); - assert!(ActorHot::::get(actor_id).is_some_and(|hot| hot.cycle_state == CycleState::Running)); + assert!(ActorRunStateStore::::get(actor_id).is_none()); + Ok(()) + } + + #[benchmark(extra, pov_mode = Measured)] + fn run_complete_low_capture() -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let actor_id = prepare_low_capture_running::(None, 0)?; + #[block] + { + Pallet::::write_run_state(actor_id, None).expect("low-capture completion must clear Run"); + } + assert!(ActorRunStateStore::::get(actor_id).is_none()); + Ok(()) } + /// Measures persistence of a real Running state, not execution of its next Step. #[benchmark(pov_mode = Measured)] - fn run_suspend() { - let actor_id = bench_create_system_manual::(50_000_000); - 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"); + fn run_progress() -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let actor_id = prepare_reachable_running::(T::MaxContractSteps::get())?; + let state = ActorRunStateStore::::get(actor_id).expect("real Running state exists"); + let eligible_at = state.eligible_at; + let location = ActorControlLocators::::get(actor_id); + assert!(matches!(location, Some(ActorControlLocation::Ready { .. }))); + let retained = state.encode(); #[block] { - Pallet::::persist_run_suspension(actor_id, state) - .expect("benchmark suspension must persist"); + Pallet::::persist_run_progress(actor_id, state) + .expect("benchmark Running progress must persist"); } - assert!(ActorRunStateStore::::contains_key(actor_id)); + assert_eq!( + ActorRunStateStore::::get(actor_id) + .expect("Running state remains") + .encode(), + retained + ); + assert!( + benchmark_fixture_hot::(actor_id) + .is_some_and(|hot| hot.cycle_state == CycleState::Running) + ); + assert_eq!(ActorControlLocators::::get(actor_id), location); + let pointer = benchmark_fixture_hot::(actor_id) + .and_then(|hot| hot.wakeup_pointer) + .expect("Running temporal reference exists"); + assert_eq!(pointer.block, WakeupKey::Block(eligible_at)); + assert!(Pallet::::wakeup_page_entry_matches(pointer, actor_id)); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("real Running persistence passes full state audit"); + Ok(()) } + /// Measures persistence of a real suspension after due collection, not a new Task attempt. #[benchmark(pov_mode = Measured)] - fn run_retry() { - let actor_id = bench_create_system_manual::(50_000_001); - install_run_state::(actor_id, T::MaxOpeningSnapshotEntries::get()); + fn run_suspend() -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let (actor_id, _) = prepare_reachable_suspended_head::(T::MaxContractSteps::get() * 2, 0)?; + let state = ActorRunStateStore::::get(actor_id).expect("real Suspended state exists"); + let eligible_at = state.eligible_at; + let location = ActorControlLocators::::get(actor_id); + assert!(matches!(location, Some(ActorControlLocation::Ready { .. }))); + let expected_event = Event::::CycleSuspended { + actor_id, + cycle_nonce: state.cycle_nonce, + cursor: state.cursor, + reason: state + .suspension + .expect("real retry has a suspension reason"), + cumulative_outcomes: state.cumulative_outcomes, + }; + let retained = state.encode(); + frame_system::Pallet::::reset_events(); #[block] { - core::hint::black_box(Pallet::::begin_run_attempt(actor_id, 2u32.into())); + Pallet::::persist_run_suspension(actor_id, state) + .expect("benchmark suspension must persist"); } assert_eq!( ActorRunStateStore::::get(actor_id) - .expect("benchmark Actor run remains") - .last_attempt_block, - 2u32.into() + .expect("Suspended state remains") + .encode(), + retained + ); + assert!( + benchmark_fixture_hot::(actor_id) + .is_some_and(|hot| hot.cycle_state == CycleState::Suspended) ); + assert_eq!(ActorControlLocators::::get(actor_id), location); + let pointer = benchmark_fixture_hot::(actor_id) + .and_then(|hot| hot.wakeup_pointer) + .expect("Suspended temporal reference exists"); + assert_eq!(pointer.block, WakeupKey::Block(eligible_at)); + assert!(Pallet::::wakeup_page_entry_matches(pointer, actor_id)); + frame_system::Pallet::::assert_last_event(expected_event.into()); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("real suspension persistence passes full state audit"); + Ok(()) } + /// Measures Run removal/Idle publication, not execution of a final Task. #[benchmark(pov_mode = Measured)] - fn run_complete() { - let actor_id = bench_create_system_manual::(50_000_002); - install_run_state::(actor_id, T::MaxOpeningSnapshotEntries::get()); + fn run_complete() -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let actor_id = prepare_reachable_running::(T::MaxContractSteps::get())?; + let state = Pallet::::active_actor_state(actor_id).expect("real completion state exists"); + let run = state + .run_state + .as_ref() + .expect("real completion Run exists"); + assert_eq!( + run.opening_snapshot.len(), + Pallet::::opening_surfaces(&state.contract.steps, 0).len() + ); + assert_eq!( + run.opening_predicate_results.len() as u32, + state + .contract + .steps + .iter() + .map(|step| step + .precondition + .as_ref() + .map_or(0, Precondition::opening_predicate_count)) + .sum::() + ); #[block] { Pallet::::write_run_state(actor_id, None) .expect("benchmark completion must clear Actor run"); } assert!(ActorRunStateStore::::get(actor_id).is_none()); - assert!(ActorHot::::get(actor_id).is_some_and(|hot| hot.cycle_state == CycleState::Idle)); + let (_, completed) = Pallet::::load_primary_control_cell(actor_id) + .expect("benchmark completed primary remains"); + assert_eq!(completed.hot.cycle_state, CycleState::Idle); + assert_eq!(completed.identity.cycle_nonce, run.cycle_nonce); + assert!(matches!( + ActorControlLocators::::get(actor_id), + Some(ActorControlLocation::Unsignaled) + )); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("completed real Run passes full state audit"); + Ok(()) } #[benchmark] - fn run_cancel() { - let page_size = T::WakeupPageSize::get(); - let wakeup_block = 100u32.into(); + fn run_cancel() -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + if T::MaxRetryAttempts::get() < 3 { + return Err(polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "middle-page cancellation requires two nonterminal retries", + )); + } + let page_size = 32u32; + let (actor_id, funding) = + create_reachable_manual_retry::(T::MaxContractSteps::get() * 2, 0, 0)?; + let second_attempt = frame_system::Pallet::::block_number().saturating_add(2u32.into()); + let wakeup_block = Pallet::::suspension_eligible_at(0, None, second_attempt, 2) + .expect("second real retry target is representable"); for i in 0..page_size { - let filler = bench_create_system_manual::(50_000_003u32.saturating_add(i)); - assert!(Pallet::::wakeup_substrate_schedule(filler, wakeup_block)); + create_reachable_waiting_guard::(i, wakeup_block); } let mut middle_fillers = alloc::vec::Vec::with_capacity(page_size.saturating_sub(1) as usize); for i in 0..page_size.saturating_sub(1) { - let filler = bench_create_system_manual::(51_000_003u32.saturating_add(i)); - assert!(Pallet::::wakeup_substrate_schedule(filler, wakeup_block)); + let filler = create_reachable_waiting_guard::(page_size + i, wakeup_block); middle_fillers.push(filler); } - let actor_id = bench_create_system_manual::(52_000_003); - install_run_state::(actor_id, T::MaxOpeningSnapshotEntries::get()); - assert!(Pallet::::wakeup_substrate_schedule( - actor_id, - wakeup_block - )); - let tail_filler = bench_create_system_manual::(53_000_003); - assert!(Pallet::::wakeup_substrate_schedule( - tail_filler, - wakeup_block - )); + open_reachable_retry::(actor_id, funding); + assert_reachable_retry::(actor_id); + let run = ActorRunStateStore::::get(actor_id).expect("real cancellation Run exists"); + assert_eq!(run.unsuccessful_attempts_at_cursor, 2); + assert_eq!(run.eligible_at, wakeup_block); + create_reachable_waiting_guard::(page_size * 2, wakeup_block); for filler in middle_fillers { - assert!(Pallet::::wakeup_substrate_invalidate(filler).is_some()); - } - ActorHot::::mutate(actor_id, |maybe_hot| { - maybe_hot - .as_mut() - .expect("benchmark actor hot state exists") - .pending_signal = true; - }); + Pallet::::close_actor(RawOrigin::Root.into(), filler) + .expect("real guard close unlinks its Waiting entry"); + } + let owner = Pallet::::actor_identity(actor_id) + .expect("target identity exists") + .owner; + Pallet::::manual_trigger(RawOrigin::Signed(owner).into(), actor_id) + .expect("second real occurrence retains a deferred latch"); + assert_eq!( + ActorRunStateStore::::get(actor_id) + .expect("deferred Run exists") + .encode(), + run.encode() + ); + let middle = ActorWaitingFrameChunks::::get((WakeupKey::Block(wakeup_block), 1)) + .expect("cancel measures unlink of a live middle Waiting page"); + assert_eq!( + (middle.previous_page, middle.next_page, middle.live_entries), + (Some(0), Some(2), 1) + ); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("real middle-page cancellation state is valid"); #[extrinsic_call] 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() - })); - assert!(!WakeupPages::::contains_key(( + assert!( + Pallet::::active_actor_view(actor_id).is_some_and(|actor| { + actor.cycle_state == CycleState::Idle + && actor.pending_signal + && actor.queue_ticket.is_some() + }) + ); + assert!(!ActorWaitingFrameChunks::::contains_key(( WakeupKey::Block(wakeup_block), 1, ))); - } - - #[benchmark] - fn run_suffix_admission(n: Linear<1, 10>) { - let bounded = n.min(T::MaxContractSteps::get()); - let recipient: T::AccountId = account("run_suffix_recipient", 0, 0); - let mut plan = ContractSteps::::default(); - for _ in 0..bounded { - plan - .try_push(Step { - precondition: None, - task: ActorTask::Transfer { - to: recipient.clone(), - asset: T::FeeNativeAssetId::get(), - amount: AmountResolution::Fixed(One::one()), - }, - on_error: StepErrorPolicy::RetryLater { max_attempts: 3 }, - }) - .expect("benchmark suffix step fits"); - } - #[block] - { - let mut total = Weight::zero(); - for step_index in 0..plan.len() { - let step = &plan[step_index]; - total = total - .saturating_add(Pallet::::weight_upper_bound(&step.task)) - .saturating_add(Weight::from_parts( - step - .precondition - .as_ref() - .map_or(0, Precondition::predicate_count) as u64, - 0, - )); - } - core::hint::black_box(total); - } + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("real cancellation preserves canonical topology"); + Ok(()) } /// Measures one affected User ObservationChange occurrence independently from publisher ingress @@ -7589,7 +13930,7 @@ mod benches { .expect("ObservationChange occurrence commits") ); } - let hot = ActorHot::::get(actor_id).expect("ObservationChange Actor remains active"); + let hot = benchmark_fixture_hot::(actor_id).expect("ObservationChange Actor remains active"); assert!(hot.pending_signal); assert!(hot.queue_ticket.is_some() || hot.wakeup_pointer.is_some()); } @@ -7677,7 +14018,7 @@ mod benches { assert!(DirtyObservationFeeds::::get(feed).is_none()); assert_eq!(DirtyObservationListState::::get().count, 2); for actor_id in actors { - assert!(ActorHot::::get(actor_id).is_some_and(|hot| hot.pending_signal)); + assert!(benchmark_fixture_hot::(actor_id).is_some_and(|hot| hot.pending_signal)); } } @@ -7692,12 +14033,10 @@ mod benches { 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()); + benchmark_fixture_mutate_hot::(actor_id, |hot| { + hot.last_cycle_block = Some(One::one()); }); + benchmark_fixture_align_primary_control::(actor_id); actors.push(actor_id); } Pallet::::note_observation_changed(feed, 1) @@ -7709,7 +14048,7 @@ mod benches { } assert!(DirtyObservationFeeds::::get(feed).is_none()); for actor_id in actors { - assert!(ActorHot::::get(actor_id).is_some_and(|hot| { + assert!(benchmark_fixture_hot::(actor_id).is_some_and(|hot| { hot.pending_signal && hot.queue_ticket.is_none() && hot.wakeup_pointer.is_some() })); } @@ -7740,7 +14079,7 @@ mod benches { assert!(DirtyObservationFeeds::::get(feed).is_none()); for actor_id in actors { assert!( - ActorHot::::get(actor_id) + benchmark_fixture_hot::(actor_id) .is_some_and(|hot| { hot.pending_signal && hot.queue_ticket.is_some() }) ); } @@ -7769,7 +14108,7 @@ mod benches { Pallet::::do_fanout_dirty_observation_page() .expect("terminal observation fanout must succeed"); } - assert!(ActorHot::::get(actor_id).is_none()); + assert!(benchmark_fixture_hot::(actor_id).is_none()); assert!(DirtyObservationFeeds::::get(feed).is_none()); } @@ -7797,7 +14136,7 @@ mod benches { } assert!(DirtyObservationFeeds::::get(feed).is_none()); for actor_id in actors { - assert!(ActorHot::::get(actor_id).is_some_and(|hot| { + assert!(benchmark_fixture_hot::(actor_id).is_some_and(|hot| { hot.pending_signal && hot.queue_ticket.is_none() && hot.wakeup_pointer.is_some() })); } @@ -7871,7 +14210,7 @@ mod benches { { Pallet::::crossing_work_unit().expect("Crossing occurrence commits"); } - let hot = ActorHot::::get(actor_id).expect("Crossing Actor remains active"); + let hot = benchmark_fixture_hot::(actor_id).expect("Crossing Actor remains active"); assert!(hot.pending_signal); assert!(hot.queue_ticket.is_some() || hot.wakeup_pointer.is_some()); } @@ -7922,7 +14261,7 @@ mod benches { let actor_id = bench_create_user_with_trigger::(owner, Trigger::manual()); Pallet::::request_activation(actor_id).expect("queue-boundary actor activation"); } - assert_eq!(QueueOccupancy::::get(), 31); + assert_eq!(benchmark_fixture_ready_occupancy::(), 31); let second_owner: T::AccountId = account("crossing-pair-probe", 0, 0); let _ = bench_create_user_with_trigger::( second_owner, @@ -8017,8 +14356,13 @@ mod benches { c, ) .expect("bounded cohort snapshot"); - let preflight = Pallet::::preflight_crossing_cohort(&snapshot, transition, true, None) - .expect("homogeneous cohort preflight"); + let preflight = Pallet::::preflight_crossing_cohort( + &snapshot, + transition, + crate::crossing::CrossingFireClassification::Resolve, + None, + ) + .expect("homogeneous cohort preflight"); assert_eq!(preflight.plan, CrossingWorkPlan::FireCohortPlaced); assert_eq!(preflight.admitted_candidates, c); } @@ -8061,7 +14405,7 @@ mod benches { let preflight = Pallet::::preflight_crossing_cohort( &snapshot, transition, - true, + crate::crossing::CrossingFireClassification::Resolve, Some(CrossingWorkPlan::FireCohortCoalesced), ) .expect("homogeneous coalesced preflight"); @@ -8080,7 +14424,7 @@ mod benches { Trigger::observation_crossing(feed, CrossingDirection::Rising, 2, 0), ); } - NextQueueTicket::::put(u64::MAX); + benchmark_fixture_set_next_ready_ticket::(u64::MAX); let locator = CrossingMemberships::::get(first).expect("first Crossing locator"); let page = CrossingMemberPages::::get(locator.key, locator.page) .expect("Crossing cohort source page"); @@ -8104,7 +14448,7 @@ mod benches { let preflight = Pallet::::preflight_crossing_cohort( &snapshot, transition, - true, + crate::crossing::CrossingFireClassification::Resolve, Some(CrossingWorkPlan::FireCohortClosed), ) .expect("homogeneous terminal preflight"); @@ -8114,74 +14458,244 @@ mod benches { } #[benchmark] - 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 { - let owner: T::AccountId = account("crossing-skip-cohort", index, 0); - actors.push(bench_create_user_with_trigger::( - owner, - Trigger::observation_crossing(feed, CrossingDirection::Rising, 2, 0), - )); - } - for actor_id in actors { - ActorHot::::mutate(actor_id, |maybe_hot| { - let hot = maybe_hot.as_mut().expect("Crossing cohort Actor"); + 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 { + let owner: T::AccountId = account("crossing-skip-cohort", index, 0); + actors.push(bench_create_user_with_trigger::( + owner, + Trigger::observation_crossing(feed, CrossingDirection::Rising, 2, 0), + )); + } + for actor_id in actors { + benchmark_fixture_mutate_hot::(actor_id, |hot| { + let TriggerRuntimeState::ObservationCrossing { + installed_at_revision, + .. + } = &mut hot.trigger_runtime_state + else { + panic!("Crossing runtime state") + }; + *installed_at_revision = 2; + }); + benchmark_fixture_align_primary_control::(actor_id); + } + let locator = CrossingMemberships::::get(first).expect("first Crossing locator"); + let page = CrossingMemberPages::::get(locator.key, locator.page) + .expect("Crossing cohort source page"); + let transition = CrossingTransitionObligation { + revision: 2, + previous: 0, + current: 2, + cause_provenance: TriggerCauseProvenance::Deferred, + cause_block: 0, + }; + #[block] + { + let snapshot = Pallet::::snapshot_crossing_source_prefix( + locator.key, + locator.page, + &page, + locator.offset, + c, + ) + .expect("bounded cohort snapshot"); + let preflight = Pallet::::preflight_crossing_cohort( + &snapshot, + transition, + crate::crossing::CrossingFireClassification::Resolve, + Some(CrossingWorkPlan::SkipPostInstallationTransition), + ) + .expect("homogeneous skip preflight"); + assert_eq!( + preflight.plan, + CrossingWorkPlan::SkipPostInstallationTransition + ); + assert_eq!(preflight.admitted_candidates, c); + } + } + + #[benchmark] + 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); + let _ = bench_create_user_with_trigger::( + owner, + Trigger::observation_crossing(feed, CrossingDirection::Rising, 2, 0), + ); + } + 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 { + revision: 3, + previous: Some(2), + current: 0, + }, + ) + .expect("Crossing rearm transition must be admitted"); + let locator = CrossingMemberships::::get(first).expect("first Crossing locator"); + let page = CrossingMemberPages::::get(locator.key, locator.page) + .expect("Crossing cohort source page"); + let transition = CrossingTransitionObligation { + revision: 3, + previous: 2, + current: 0, + cause_provenance: TriggerCauseProvenance::Deferred, + cause_block: 0, + }; + #[block] + { + let snapshot = Pallet::::snapshot_crossing_source_prefix( + locator.key, + locator.page, + &page, + locator.offset, + c, + ) + .expect("bounded cohort snapshot"); + let preflight = Pallet::::preflight_crossing_cohort( + &snapshot, + transition, + crate::crossing::CrossingFireClassification::Resolve, + Some(CrossingWorkPlan::RearmCohort), + ) + .expect("homogeneous rearm preflight"); + assert_eq!(preflight.plan, CrossingWorkPlan::RearmCohort); + assert_eq!(preflight.admitted_candidates, c); + } + } + + #[benchmark] + fn crossing_rearm_pair_probe() { + let (feed, _) = prepare_crossing_work::(2); + let second_owner: T::AccountId = account("crossing-rearm-pair-probe", 0, 0); + let _ = bench_create_user_with_trigger::( + second_owner, + Trigger::observation_crossing(feed, CrossingDirection::Rising, 2, 0), + ); + 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 { + revision: 3, + previous: Some(2), + current: 0, + }, + ) + .expect("pair rearm transition must be admitted"); + CrossingRangeCursors::::insert( + feed, + CrossingRangeCursor { + revision: 3, + traversal: CrossingTraversal::Downward, + search_bound: 0, + current_threshold: Some(0), + page: 0, + offset: 0, + exhausted: false, + }, + ); + #[block] + { + assert_eq!( + Pallet::::classify_crossing_work(), + CrossingWorkPlan::RearmCohortPair + ); + } + } + + #[benchmark] + fn crossing_skip_pair_probe() { + let (feed, first_actor) = prepare_crossing_work::(2); + let second_owner: T::AccountId = account("crossing-skip-pair-probe", 0, 0); + let second_actor = bench_create_user_with_trigger::( + second_owner, + Trigger::observation_crossing(feed, CrossingDirection::Rising, 2, 0), + ); + for actor_id in [first_actor, second_actor] { + benchmark_fixture_mutate_hot::(actor_id, |hot| { let TriggerRuntimeState::ObservationCrossing { installed_at_revision, .. } = &mut hot.trigger_runtime_state else { - panic!("Crossing runtime state") + panic!("pair Actor must use Crossing state"); }; *installed_at_revision = 2; }); } - let locator = CrossingMemberships::::get(first).expect("first Crossing locator"); - let page = CrossingMemberPages::::get(locator.key, locator.page) - .expect("Crossing cohort source page"); - let transition = CrossingTransitionObligation { - revision: 2, - previous: 0, - current: 2, - cause_provenance: TriggerCauseProvenance::Deferred, - cause_block: 0, - }; + CrossingRangeCursors::::insert( + feed, + CrossingRangeCursor { + revision: 2, + traversal: CrossingTraversal::Upward, + search_bound: 2, + current_threshold: Some(2), + page: 0, + offset: 0, + exhausted: false, + }, + ); + for actor_id in [first_actor, second_actor] { + benchmark_fixture_align_primary_control::(actor_id); + } #[block] { - let snapshot = Pallet::::snapshot_crossing_source_prefix( - locator.key, - locator.page, - &page, - locator.offset, - c, - ) - .expect("bounded cohort snapshot"); - let preflight = Pallet::::preflight_crossing_cohort( - &snapshot, - transition, - true, - Some(CrossingWorkPlan::SkipPostInstallationTransition), - ) - .expect("homogeneous skip preflight"); assert_eq!( - preflight.plan, - CrossingWorkPlan::SkipPostInstallationTransition + Pallet::::classify_crossing_work(), + CrossingWorkPlan::SkipPostInstallationPair ); - assert_eq!(preflight.admitted_candidates, c); } } #[benchmark] - 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); - let _ = bench_create_user_with_trigger::( - owner, - Trigger::observation_crossing(feed, CrossingDirection::Rising, 2, 0), - ); + fn crossing_transition_unit() { + let (feed, _) = prepare_crossing_work::(u128::MAX); + #[block] + { + Pallet::::crossing_work_unit().expect("no-match Crossing work must succeed"); + } + assert!(CrossingRangeCursors::::get(feed).is_some_and(|cursor| cursor.exhausted)); + } + + #[benchmark] + fn crossing_leaf_unit() { + let (_, actor_id) = prepare_crossing_work::(2); + #[block] + { + Pallet::::crossing_work_unit().expect("matched Crossing leaf work must succeed"); + } + assert!(benchmark_fixture_hot::(actor_id).is_some_and(|hot| hot.pending_signal)); + } + + #[benchmark] + fn crossing_page_unit() { + let (_, actor_id) = prepare_crossing_work::(2); + #[block] + { + Pallet::::crossing_work_unit().expect("matched Crossing page work must succeed"); } + assert!(matches!( + benchmark_fixture_hot::(actor_id).map(|hot| hot.trigger_runtime_state), + Some(TriggerRuntimeState::ObservationCrossing { + phase: CrossingPhase::WaitingForRearm, + .. + }) + )); + } + + #[benchmark] + fn crossing_rearm_unit() { + let (feed, actor_id) = prepare_crossing_work::(2); while CrossingPendingFeedListState::::get().count > 0 { Pallet::::crossing_work_unit().expect("initial Crossing fire must drain"); } @@ -8195,66 +14709,214 @@ mod benches { }, ) .expect("Crossing rearm transition must be admitted"); - let locator = CrossingMemberships::::get(first).expect("first Crossing locator"); - let page = CrossingMemberPages::::get(locator.key, locator.page) - .expect("Crossing cohort source page"); - let transition = CrossingTransitionObligation { - revision: 3, - previous: 2, - current: 0, - cause_provenance: TriggerCauseProvenance::Deferred, - cause_block: 0, - }; #[block] { - let snapshot = Pallet::::snapshot_crossing_source_prefix( - locator.key, - locator.page, - &page, - locator.offset, - c, - ) - .expect("bounded cohort snapshot"); - let preflight = Pallet::::preflight_crossing_cohort( - &snapshot, - transition, - true, - Some(CrossingWorkPlan::RearmCohort), - ) - .expect("homogeneous rearm preflight"); - assert_eq!(preflight.plan, CrossingWorkPlan::RearmCohort); - assert_eq!(preflight.admitted_candidates, c); + Pallet::::crossing_work_unit().expect("Crossing rearm work must succeed"); + } + assert!(matches!( + benchmark_fixture_hot::(actor_id).map(|hot| hot.trigger_runtime_state), + Some(TriggerRuntimeState::ObservationCrossing { + phase: CrossingPhase::Armed, + .. + }) + )); + } + + #[benchmark] + fn crossing_rearm_pair_unit() { + let (feed, first_actor) = prepare_crossing_work::(2); + let second_owner: T::AccountId = account("crossing-rearm-pair-unit", 0, 0); + let second_actor = bench_create_user_with_trigger::( + second_owner, + Trigger::observation_crossing(feed, CrossingDirection::Rising, 2, 0), + ); + 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 { + revision: 3, + previous: Some(2), + current: 0, + }, + ) + .expect("pair rearm transition must be admitted"); + CrossingRangeCursors::::insert( + feed, + CrossingRangeCursor { + revision: 3, + traversal: CrossingTraversal::Downward, + search_bound: 0, + current_threshold: Some(0), + page: 0, + offset: 0, + exhausted: false, + }, + ); + #[block] + { + Pallet::::crossing_pair_work_unit().expect("Crossing rearm pair must succeed"); + } + for actor_id in [first_actor, second_actor] { + assert!(matches!( + benchmark_fixture_hot::(actor_id).map(|hot| hot.trigger_runtime_state), + Some(TriggerRuntimeState::ObservationCrossing { + phase: CrossingPhase::Armed, + .. + }) + )); + } + } + + #[benchmark] + fn crossing_coalesced_unit() { + let (_, actor_id) = prepare_crossing_work::(2); + Pallet::::request_activation(actor_id).expect("benchmark actor activation must succeed"); + #[block] + { + Pallet::::crossing_work_unit().expect("coalesced Crossing fire must succeed"); + } + assert!(benchmark_fixture_hot::(actor_id).is_some_and(|hot| { + hot.pending_signal + && hot.queue_ticket.is_some() + && matches!( + hot.trigger_runtime_state, + TriggerRuntimeState::ObservationCrossing { + phase: CrossingPhase::WaitingForRearm, + .. + } + ) + })); + } + + #[benchmark] + fn crossing_coalesced_pair_unit() { + let (feed, first_actor) = prepare_crossing_work::(2); + let second_owner: T::AccountId = account("crossing-coalesced-pair", 0, 0); + let second_actor = bench_create_user_with_trigger::( + second_owner, + Trigger::observation_crossing(feed, CrossingDirection::Rising, 2, 0), + ); + Pallet::::request_activation(first_actor).expect("first pair latch must succeed"); + Pallet::::request_activation(second_actor).expect("second pair latch must succeed"); + CrossingRangeCursors::::insert( + feed, + CrossingRangeCursor { + revision: 2, + traversal: CrossingTraversal::Upward, + search_bound: 2, + current_threshold: Some(2), + page: 0, + offset: 0, + exhausted: false, + }, + ); + assert_eq!( + Pallet::::classify_crossing_work(), + CrossingWorkPlan::FireCohortCoalescedPair + ); + #[block] + { + Pallet::::crossing_pair_work_unit().expect("coalesced Crossing pair must succeed"); + } + for actor_id in [first_actor, second_actor] { + assert!(benchmark_fixture_hot::(actor_id).is_some_and(|hot| { + hot.pending_signal + && hot.queue_ticket.is_some() + && matches!( + hot.trigger_runtime_state, + TriggerRuntimeState::ObservationCrossing { + phase: CrossingPhase::WaitingForRearm, + .. + } + ) + })); + } + } + + #[benchmark] + fn crossing_placed_unit() { + let (_, actor_id) = prepare_crossing_work::(2); + #[block] + { + Pallet::::crossing_work_unit().expect("placed Crossing fire must succeed"); } + assert!(benchmark_fixture_hot::(actor_id).is_some_and(|hot| { + hot.pending_signal + && hot.queue_ticket.is_some() + && matches!( + hot.trigger_runtime_state, + TriggerRuntimeState::ObservationCrossing { + phase: CrossingPhase::WaitingForRearm, + .. + } + ) + })); } #[benchmark] - fn crossing_rearm_pair_probe() { - let (feed, _) = prepare_crossing_work::(2); - let second_owner: T::AccountId = account("crossing-rearm-pair-probe", 0, 0); - let _ = bench_create_user_with_trigger::( + fn crossing_placed_pair_unit() { + let (feed, first_actor) = prepare_crossing_work::(2); + let second_owner: T::AccountId = account("crossing-pair-unit", 0, 0); + let second_actor = bench_create_user_with_trigger::( second_owner, Trigger::observation_crossing(feed, CrossingDirection::Rising, 2, 0), ); - while CrossingPendingFeedListState::::get().count > 0 { - Pallet::::crossing_work_unit().expect("initial pair fire must drain"); - } - clear_indexed_detection_disablement::(); - Pallet::::note_observation_transition( + CrossingRangeCursors::::insert( feed, - ObservationTransition { - revision: 3, - previous: Some(2), - current: 0, + CrossingRangeCursor { + revision: 2, + traversal: CrossingTraversal::Upward, + search_bound: 2, + current_threshold: Some(2), + page: 0, + offset: 0, + exhausted: false, }, - ) - .expect("pair rearm transition must be admitted"); + ); + #[block] + { + Pallet::::crossing_placed_batch_work_unit(2).expect("placed Crossing pair must succeed"); + } + for actor_id in [first_actor, second_actor] { + assert!(benchmark_fixture_hot::(actor_id).is_some_and(|hot| { + hot.pending_signal + && hot.queue_ticket.is_some() + && matches!( + hot.trigger_runtime_state, + TriggerRuntimeState::ObservationCrossing { + phase: CrossingPhase::WaitingForRearm, + .. + } + ) + })); + } + } + + #[benchmark] + 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::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::( + owner, + Trigger::observation_crossing(feed, CrossingDirection::Rising, 2, 0), + ) + } else { + bench_create_system_crossing::(owner, feed, 2) + }); + } CrossingRangeCursors::::insert( feed, CrossingRangeCursor { - revision: 3, - traversal: CrossingTraversal::Downward, - search_bound: 0, - current_threshold: Some(0), + revision: 2, + traversal: CrossingTraversal::Upward, + search_bound: 2, + current_threshold: Some(2), page: 0, offset: 0, exhausted: false, @@ -8262,24 +14924,106 @@ mod benches { ); #[block] { - assert_eq!( - Pallet::::classify_crossing_work(), - CrossingWorkPlan::RearmCohortPair + 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(CROSSING_COHORT_BENCHMARK_MAX as usize) + { + assert!(benchmark_fixture_hot::(actor_id).is_some_and(|hot| { + hot.pending_signal + && hot.queue_ticket.is_some() + && matches!( + hot.trigger_runtime_state, + TriggerRuntimeState::ObservationCrossing { + phase: CrossingPhase::WaitingForRearm, + .. + } + ) + })); + } + } + + #[benchmark] + fn crossing_placed_non_tail_emptied_unit() { + let actors = prepare_non_tail_crossing_batch::(CROSSING_NON_TAIL_BENCHMARK_MAX); + #[block] + { + 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(CROSSING_NON_TAIL_BENCHMARK_MAX as usize) + { + assert!( + benchmark_fixture_hot::(actor_id) + .is_some_and(|hot| { hot.pending_signal && hot.queue_ticket.is_some() }) ); } } #[benchmark] - fn crossing_skip_pair_probe() { + fn crossing_placed_non_tail_trimmed_unit() { + let actors = prepare_non_tail_crossing_batch::(CROSSING_TRIMMED_BENCHMARK_TAIL); + #[block] + { + 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(CROSSING_NON_TAIL_BENCHMARK_MAX as usize) + { + assert!( + benchmark_fixture_hot::(actor_id) + .is_some_and(|hot| { hot.pending_signal && hot.queue_ticket.is_some() }) + ); + } + } + + #[benchmark] + fn crossing_skip_unit() { + let (_, actor_id) = prepare_crossing_work::(2); + benchmark_fixture_mutate_hot::(actor_id, |hot| { + let TriggerRuntimeState::ObservationCrossing { + installed_at_revision, + .. + } = &mut hot.trigger_runtime_state + else { + panic!("benchmark actor must use Crossing state"); + }; + *installed_at_revision = 2; + }); + benchmark_fixture_align_primary_control::(actor_id); + #[block] + { + Pallet::::crossing_work_unit().expect("post-installation Crossing skip must succeed"); + } + assert!(benchmark_fixture_hot::(actor_id).is_some_and(|hot| { + !hot.pending_signal + && hot.queue_ticket.is_none() + && matches!( + hot.trigger_runtime_state, + TriggerRuntimeState::ObservationCrossing { + phase: CrossingPhase::Armed, + installed_at_revision: 2, + } + ) + })); + } + + #[benchmark] + fn crossing_skip_pair_unit() { let (feed, first_actor) = prepare_crossing_work::(2); - let second_owner: T::AccountId = account("crossing-skip-pair-probe", 0, 0); + let second_owner: T::AccountId = account("crossing-skip-pair-unit", 0, 0); let second_actor = bench_create_user_with_trigger::( second_owner, Trigger::observation_crossing(feed, CrossingDirection::Rising, 2, 0), ); for actor_id in [first_actor, second_actor] { - ActorHot::::mutate(actor_id, |maybe_hot| { - let hot = maybe_hot.as_mut().expect("pair Actor hot state must exist"); + benchmark_fixture_mutate_hot::(actor_id, |hot| { let TriggerRuntimeState::ObservationCrossing { installed_at_revision, .. @@ -8302,721 +15046,2144 @@ mod benches { exhausted: false, }, ); - #[block] - { - assert_eq!( - Pallet::::classify_crossing_work(), - CrossingWorkPlan::SkipPostInstallationPair - ); + for actor_id in [first_actor, second_actor] { + benchmark_fixture_align_primary_control::(actor_id); } - } - - #[benchmark] - fn crossing_transition_unit() { - let (feed, _) = prepare_crossing_work::(u128::MAX); #[block] { - Pallet::::crossing_work_unit().expect("no-match Crossing work must succeed"); + Pallet::::crossing_pair_work_unit().expect("Crossing skip pair must succeed"); } - assert!(CrossingRangeCursors::::get(feed).is_some_and(|cursor| cursor.exhausted)); - } - - #[benchmark] - fn crossing_leaf_unit() { - let (_, actor_id) = prepare_crossing_work::(2); - #[block] - { - Pallet::::crossing_work_unit().expect("matched Crossing leaf work must succeed"); + for actor_id in [first_actor, second_actor] { + assert!(benchmark_fixture_hot::(actor_id).is_some_and(|hot| { + !hot.pending_signal + && hot.queue_ticket.is_none() + && matches!( + hot.trigger_runtime_state, + TriggerRuntimeState::ObservationCrossing { + phase: CrossingPhase::Armed, + installed_at_revision: 2, + } + ) + })); } - assert!(ActorHot::::get(actor_id).is_some_and(|hot| hot.pending_signal)); } #[benchmark] - fn crossing_page_unit() { + fn crossing_actor_unit() { let (_, actor_id) = prepare_crossing_work::(2); + benchmark_fixture_reset_ready_queue::(); + ActorReadyHead::::put(u64::MAX); + benchmark_fixture_set_next_ready_ticket::(u64::MAX); #[block] { - Pallet::::crossing_work_unit().expect("matched Crossing page work must succeed"); + Pallet::::crossing_work_unit() + .expect("matched Crossing actor terminal cleanup must succeed"); } - assert!(matches!( - ActorHot::::get(actor_id).map(|hot| hot.trigger_runtime_state), - Some(TriggerRuntimeState::ObservationCrossing { - phase: CrossingPhase::WaitingForRearm, - .. - }) - )); + assert!(benchmark_fixture_hot::(actor_id).is_none()); + assert!(!CrossingMemberships::::contains_key(actor_id)); } #[benchmark] - fn crossing_rearm_unit() { - let (feed, actor_id) = prepare_crossing_work::(2); - 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 { - revision: 3, - previous: Some(2), - current: 0, - }, - ) - .expect("Crossing rearm transition must be admitted"); + fn transaction_extension_ingress_base() { + let owner: T::AccountId = whitelisted_caller(); + let populated_actor_id = bench_create_user::(owner); + let proof_witness = Pallet::::active_actor_view(populated_actor_id) + .expect("benchmark actor exists") + .sovereign_account; + let recipient: T::AccountId = account("unmatched_ingress_recipient", 0, 0); + let source: T::AccountId = account("ingress_source", 0, 0); + T::BenchmarkHelper::setup_address_event_ingress(&recipient, &source, One::one()) + .expect("benchmark helper must prepare an unmatched producer event"); #[block] { - Pallet::::crossing_work_unit().expect("Crossing rearm work must succeed"); + // Storage benchmarking does not attribute an absent overlay lookup to its map. Read a + // populated witness first so the generated envelope includes one conservative database + // read and the map's maximum proof before exercising the real negative lookup. + assert!(SovereignIndex::::contains_key(&proof_witness)); + assert!(!T::BenchmarkHelper::run_address_event_ingress( + &recipient, + &source, + One::one(), + )); } - assert!(matches!( - ActorHot::::get(actor_id).map(|hot| hot.trigger_runtime_state), - Some(TriggerRuntimeState::ObservationCrossing { - phase: CrossingPhase::Armed, - .. - }) - )); } #[benchmark] - fn crossing_rearm_pair_unit() { - let (feed, first_actor) = prepare_crossing_work::(2); - let second_owner: T::AccountId = account("crossing-rearm-pair-unit", 0, 0); - let second_actor = bench_create_user_with_trigger::( - second_owner, - Trigger::observation_crossing(feed, CrossingDirection::Rising, 2, 0), - ); - 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 { - revision: 3, - previous: Some(2), - current: 0, + fn transaction_extension_ingress_notify() + -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + let source: T::AccountId = account("ingress_source", 0, 0); + let owner: T::AccountId = account("ingress_owner", 0, 0); + let (mut steps, funding) = reachable_retry_contract::()?; + let last = steps.last_mut().expect("retry Contract has a tail"); + last.task = make_tracked_funding_contract_steps::(owner.clone()) + .remove(0) + .task; + let mut contract = system_contract::( + Schedule { + trigger: Trigger::address_event( + saturated_address_source_filter::(&owner, Some(source.clone())), + AssetFilter::Any, + ), + cooldown_blocks: 100, }, + steps, ) - .expect("pair rearm transition must be admitted"); - CrossingRangeCursors::::insert( - feed, - CrossingRangeCursor { - revision: 3, - traversal: CrossingTraversal::Downward, - search_bound: 0, - current_threshold: Some(0), - page: 0, - offset: 0, - exhausted: false, - }, + .expect("active ingress retry Contract exists"); + contract.funding = FundingSourcePolicy::AnyVerifiedIngress; + Pallet::::create_system_actor( + RawOrigin::Root.into(), + owner, + Mutability::Mutable, + Some(contract), + ) + .expect("ingress retry Contract is admitted"); + let actor_id = NextActorId::::get().saturating_sub(1); + let recipient = Pallet::::sovereign_account_id_system(actor_id); + open_reachable_retry::(actor_id, funding); + let run = ActorRunStateStore::::get(actor_id).expect("real ingress retry Run exists"); + let retained_run = run.encode(); + let location = ActorControlLocators::::get(actor_id); + assert!(matches!( + location, + Some(ActorControlLocation::Waiting { .. }) + )); + assert!( + !benchmark_fixture_hot::(actor_id) + .expect("active ingress target") + .pending_signal + ); + assert_eq!( + ActorReadyOccupancy::::get(), + 0, + "tombstones cannot replace live Ready actors" ); + let native = T::FeeNativeAssetId::get(); + let before_funding = ActorFunding::::get(actor_id).expect("ingress funding exists"); + assert!(before_funding.funding_tracked_assets.contains(&native)); + let expected_funding = before_funding + .funding_accumulated + .get(&native) + .copied() + .unwrap_or_else(Zero::zero) + .checked_add(&One::one()) + .expect("second ingress accumulator is representable"); + install_saturated_tombstone_queue::(); + T::BenchmarkHelper::setup_address_event_ingress(&recipient, &source, One::one()) + .expect("benchmark helper must prepare a matched producer event"); #[block] { - Pallet::::crossing_pair_work_unit().expect("Crossing rearm pair must succeed"); - } - for actor_id in [first_actor, second_actor] { - assert!(matches!( - ActorHot::::get(actor_id).map(|hot| hot.trigger_runtime_state), - Some(TriggerRuntimeState::ObservationCrossing { - phase: CrossingPhase::Armed, - .. - }) + assert!(T::BenchmarkHelper::run_address_event_ingress( + &recipient, + &source, + One::one(), )); } + assert!( + benchmark_fixture_hot::(actor_id) + .is_some_and(|hot| { hot.pending_signal && hot.wakeup_pointer.is_some() }) + ); + assert_eq!(ActorControlLocators::::get(actor_id), location); + assert_eq!( + ActorFunding::::get(actor_id) + .expect("latched funding remains") + .funding_accumulated + .get(&native) + .copied(), + Some(expected_funding) + ); + assert_eq!( + ActorRunStateStore::::get(actor_id) + .expect("latched Run remains") + .encode(), + retained_run + ); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("real ingress retry latch passes state audit"); + Ok(()) } #[benchmark] - fn crossing_coalesced_unit() { - let (_, actor_id) = prepare_crossing_work::(2); - Pallet::::request_activation(actor_id).expect("benchmark actor activation must succeed"); + fn funding_snapshot_open( + a: Linear< + 1, + { + T::MaxFundingTrackedAssets::get() + .min(T::MaxContractSteps::get().saturating_mul(2)) + .max(1) + }, + >, + ) -> Result<(), polkadot_sdk::frame_benchmarking::BenchmarkError> { + if T::MaxFundingTrackedAssets::get() == 0 { + return Err(polkadot_sdk::frame_benchmarking::BenchmarkError::Stop( + "host cannot represent a nonempty funding snapshot", + )); + } + let owner: T::AccountId = whitelisted_caller(); + ensure_creation_balance::(&owner); + let assets = T::BenchmarkHelper::setup_predicate_assets(&owner, a) + .expect("funding assets exist in the host ledger"); + assert_eq!(assets.len(), a as usize); + let mut steps = ContractSteps::::default(); + for pair in assets.chunks(2) { + let amount = AmountResolution::PercentageOfLastFunding(Perbill::one()); + let task = if pair.len() == 1 { + ActorTask::Transfer { + to: account("funding-snapshot-recipient", 0, 0), + asset: pair[0], + amount, + } + } else { + ActorTask::AddLiquidity { + asset_a: pair[0], + asset_b: pair[1], + amount_a: amount, + amount_b: amount, + min_lp_out: One::one(), + } + }; + steps + .try_push(Step { + precondition: None, + task, + on_error: StepErrorPolicy::AbortCycle, + }) + .expect("reachable funding sources fit the authored Step bound"); + } + prefund_active_user_creation::(&owner, &steps); + Pallet::::create_user_actor( + RawOrigin::Signed(owner).into(), + Mutability::Mutable, + user_contract::( + Schedule { + trigger: Trigger::Manual, + cooldown_blocks: 0, + }, + steps, + ), + ) + .expect("funding snapshot Contract is admitted"); + let actor_id = NextActorId::::get() - 1; + let amount = assets + .iter() + .map(|asset| T::AssetOps::minimum_balance(*asset)) + .max() + .expect("nonempty funding assets") + .checked_add(&One::one()) + .expect("funding amount fits"); + fund_reachable_update_assets::(actor_id, amount); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("certified funding input is a reachable Actor state"); + let snapshot; + // This measures accumulator consumption only; complete Opening owns hold reconciliation. #[block] { - Pallet::::crossing_work_unit().expect("coalesced Crossing fire must succeed"); + snapshot = ActorFunding::::mutate(actor_id, |maybe| { + maybe + .as_mut() + .map(|funding| core::mem::take(&mut funding.funding_accumulated)) + .expect("benchmark actor funding exists") + }); } - assert!(ActorHot::::get(actor_id).is_some_and(|hot| { - hot.pending_signal - && hot.queue_ticket.is_some() - && matches!( - hot.trigger_runtime_state, - TriggerRuntimeState::ObservationCrossing { - phase: CrossingPhase::WaitingForRearm, - .. - } - ) - })); + assert_eq!(snapshot.len() as u32, a); + assert!( + ActorFunding::::get(actor_id) + .expect("benchmark actor funding exists") + .funding_accumulated + .is_empty() + ); + Ok(()) } - #[benchmark] - fn crossing_coalesced_pair_unit() { - let (feed, first_actor) = prepare_crossing_work::(2); - let second_owner: T::AccountId = account("crossing-coalesced-pair", 0, 0); - let second_actor = bench_create_user_with_trigger::( - second_owner, - Trigger::observation_crossing(feed, CrossingDirection::Rising, 2, 0), - ); - Pallet::::request_activation(first_actor).expect("first pair latch must succeed"); - Pallet::::request_activation(second_actor).expect("second pair latch must succeed"); - CrossingRangeCursors::::insert( - feed, - CrossingRangeCursor { - revision: 2, - traversal: CrossingTraversal::Upward, - search_bound: 2, - current_threshold: Some(2), - page: 0, - offset: 0, - exhausted: false, - }, + /// Builds a circular chain of `n` system Actors where each transfers 1% of its + /// native balance to the next actor in the ring, then runs 3 blocks and asserts zero drift. + pub(super) fn setup_and_run_circular_chain( + requested_n: u32, + ) -> alloc::vec::Vec { + let existing_active = benchmark_fixture_active_count::(); + let available = T::MaxActiveActors::get().saturating_sub(existing_active); + let n = requested_n.min(available); + assert!( + n > 0, + "benchmark requires at least one available active slot" ); + let pct = polkadot_sdk::sp_runtime::Perbill::from_percent(1); + let initial_balance = T::MinUserBalance::get().saturating_mul(1_000_000u32.into()); + let native = T::FeeNativeAssetId::get(); + let schedule = Schedule { + trigger: Trigger::cadenced(1), + cooldown_blocks: 0, + }; + let mut sovereigns: alloc::vec::Vec = alloc::vec::Vec::with_capacity(n as usize); + let mut actor_ids: alloc::vec::Vec = alloc::vec::Vec::with_capacity(n as usize); + for i in 0..n { + let owner: T::AccountId = account("owner", i, 0); + let temp_contract_steps = make_inert_contract_steps::(); + Pallet::::create_system_actor( + RawOrigin::Root.into(), + owner, + Mutability::Mutable, + system_contract::(schedule.clone(), temp_contract_steps), + ) + .expect("create_system_actor must succeed"); + let actor_id = NextActorId::::get().saturating_sub(1); + let sov = Pallet::::sovereign_account_id_system(actor_id); + let _ = T::AssetOps::mint(&sov, native, initial_balance); + sovereigns.push(sov); + actor_ids.push(actor_id); + } + // Creation stamps `last_control_mutation_block`, and the one-mutation-per-actor clock is per + // block, so the rewrite below must land strictly after the block the creation loop ran on. + let creation_block = frame_system::Pallet::::block_number(); + frame_system::Pallet::::set_block_number(creation_block + One::one()); + for (i, actor_id) in actor_ids.iter().enumerate() { + let next_sov = sovereigns[(i + 1) % sovereigns.len()].clone(); + let transfer_contract_steps: ContractSteps = BoundedVec::try_from(alloc::vec![Step { + precondition: None, + task: ActorTask::Transfer { + to: next_sov, + asset: native, + amount: AmountResolution::PercentageOfCurrent(pct), + }, + on_error: StepErrorPolicy::AbortCycle, + }]) + .expect("transfer contract_steps fits"); + let contract = Pallet::::load_actor_contract(*actor_id).expect("Actor Contract exists"); + Pallet::::update_contract( + RawOrigin::Root.into(), + *actor_id, + ActorContract { + steps: transfer_contract_steps, + completion: CompletionPolicy::Persistent, + ..contract + }, + ) + .expect("update_contract must succeed"); + } + let total_before: T::Balance = sovereigns + .iter() + .map(|sov| T::AssetOps::balance(sov, native)) + .fold(T::Balance::zero(), |acc, b| acc.saturating_add(b)); + for block in 2u32..=4 { + frame_system::Pallet::::set_block_number(block.into()); + let _ = Pallet::::on_idle(block.into(), Weight::MAX); + } + // System Actors don't pay fees → transfers are pure balance moves → zero drift + let total_after: T::Balance = sovereigns + .iter() + .map(|sov| T::AssetOps::balance(sov, native)) + .fold(T::Balance::zero(), |acc, b| acc.saturating_add(b)); assert_eq!( - Pallet::::classify_crossing_work(), - CrossingWorkPlan::FireCohortCoalescedPair + total_before, total_after, + "Balance must be exactly conserved (System Actors pay no fees)" ); + sovereigns + } + + /// Parametric stress test: circular chain of n system Actors. + /// + /// Capacity planning reference points: + /// - n=100: ~300 transfers/block (baseline) + /// - n=1_000: ~3000 transfers/block (moderate load) + /// - n=10_000: ~30000 transfers/block (high load) + #[benchmark(extra)] + fn circular_chain_stress(n: Linear<10, 10_000>) { #[block] { - Pallet::::crossing_pair_work_unit().expect("coalesced Crossing pair must succeed"); + setup_and_run_circular_chain::(n); } - for actor_id in [first_actor, second_actor] { - assert!(ActorHot::::get(actor_id).is_some_and(|hot| { - hot.pending_signal - && hot.queue_ticket.is_some() - && matches!( - hot.trigger_runtime_state, - TriggerRuntimeState::ObservationCrossing { - phase: CrossingPhase::WaitingForRearm, - .. - } - ) - })); + } + + /// Extreme stress test request: 10K-100K Actors circular chain. + /// Effective n is clamped by available `MaxActiveActors` capacity. + #[benchmark(extra)] + fn circular_chain_stress_100k(n: Linear<10_000, 100_000>) { + #[block] + { + setup_and_run_circular_chain::(n); } } + /// Fixed-size stress tests for scaling analysis. + /// Run all three and compare times to determine O(n) vs O(n²). + /// Linear: time ratio ≈ 10x when n increases 10x + /// Quadratic: time ratio ≈ 100x when n increases 10x + #[benchmark] - fn crossing_placed_unit() { - let (_, actor_id) = prepare_crossing_work::(2); + fn circular_chain_100() { #[block] { - Pallet::::crossing_work_unit().expect("placed Crossing fire must succeed"); + setup_and_run_circular_chain::(100); } - assert!(ActorHot::::get(actor_id).is_some_and(|hot| { - hot.pending_signal - && hot.queue_ticket.is_some() - && matches!( - hot.trigger_runtime_state, - TriggerRuntimeState::ObservationCrossing { - phase: CrossingPhase::WaitingForRearm, - .. - } - ) - })); } #[benchmark] - fn crossing_placed_pair_unit() { - let (feed, first_actor) = prepare_crossing_work::(2); - let second_owner: T::AccountId = account("crossing-pair-unit", 0, 0); - let second_actor = bench_create_user_with_trigger::( - second_owner, - Trigger::observation_crossing(feed, CrossingDirection::Rising, 2, 0), - ); - CrossingRangeCursors::::insert( - feed, - CrossingRangeCursor { - revision: 2, - traversal: CrossingTraversal::Upward, - search_bound: 2, - current_threshold: Some(2), - page: 0, - offset: 0, - exhausted: false, - }, - ); + fn circular_chain_1000() { #[block] { - Pallet::::crossing_placed_batch_work_unit(2).expect("placed Crossing pair must succeed"); + setup_and_run_circular_chain::(1000); } - for actor_id in [first_actor, second_actor] { - assert!(ActorHot::::get(actor_id).is_some_and(|hot| { - hot.pending_signal - && hot.queue_ticket.is_some() - && matches!( - hot.trigger_runtime_state, - TriggerRuntimeState::ObservationCrossing { - phase: CrossingPhase::WaitingForRearm, - .. - } - ) - })); + } + + #[benchmark(extra)] + fn circular_chain_10000() { + #[block] + { + setup_and_run_circular_chain::(10_000); } } #[benchmark] - 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::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::( - owner, - Trigger::observation_crossing(feed, CrossingDirection::Rising, 2, 0), - ) - } else { - bench_create_system_crossing::(owner, feed, 2) - }); - } - CrossingRangeCursors::::insert( - feed, - CrossingRangeCursor { - revision: 2, - traversal: CrossingTraversal::Upward, - search_bound: 2, - current_threshold: Some(2), - page: 0, - offset: 0, - exhausted: false, - }, - ); + fn maximum_context_inherent() { + let inherent = T::BenchmarkHelper::prepare_maximum_context_inherent(); #[block] { - Pallet::::crossing_placed_batch_work_unit(CROSSING_COHORT_BENCHMARK_MAX) - .expect("placed Crossing maximum batch must succeed"); + T::BenchmarkHelper::execute_maximum_context_inherent(inherent) + .expect("maximum context inherent execution must succeed"); } - for actor_id in actors - .into_iter() - .take(CROSSING_COHORT_BENCHMARK_MAX as usize) + 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] { - assert!(ActorHot::::get(actor_id).is_some_and(|hot| { - hot.pending_signal - && hot.queue_ticket.is_some() - && matches!( - hot.trigger_runtime_state, - TriggerRuntimeState::ObservationCrossing { - phase: CrossingPhase::WaitingForRearm, - .. - } - ) - })); + 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 crossing_placed_non_tail_emptied_unit() { - let actors = prepare_non_tail_crossing_batch::(CROSSING_NON_TAIL_BENCHMARK_MAX); + fn block_resource_meter_extension() { + T::BenchmarkHelper::prepare_block_resource_meter_extension(); #[block] { - Pallet::::crossing_placed_batch_work_unit(CROSSING_COHORT_BENCHMARK_MAX) - .expect("non-tail Crossing batch with emptied tail must succeed"); + T::BenchmarkHelper::execute_block_resource_meter_extension(); } - for actor_id in actors - .into_iter() - .take(CROSSING_NON_TAIL_BENCHMARK_MAX as usize) + T::BenchmarkHelper::verify_block_resource_meter_extension(); + } + + #[benchmark] + fn maximum_xcm_version_discovery() { + T::BenchmarkHelper::prepare_maximum_xcm_version_discovery(); + #[block] { - assert!( - ActorHot::::get(actor_id) - .is_some_and(|hot| { hot.pending_signal && hot.queue_ticket.is_some() }) - ); + T::BenchmarkHelper::execute_maximum_xcm_version_discovery(); } + T::BenchmarkHelper::verify_maximum_xcm_version_discovery(); } - #[benchmark] - fn crossing_placed_non_tail_trimmed_unit() { - let actors = prepare_non_tail_crossing_batch::(CROSSING_TRIMMED_BENCHMARK_TAIL); + // Actor control candidate-only storage probes. These five owners measure one physical chunk + // read-modify-write while varying only the bounded cell count. They are not production Weight + // methods and must be deleted with a rejected candidate or replaced by accepted runtime owners. + #[benchmark(extra)] + fn control_frame_chunk4_io(n: Linear<1, 4>) { + let chunk = BoundedVec::>::try_from(vec![[1u8; 171]; 4]) + .expect("frame C4 benchmark chunk fits"); + BenchmarkAdmissionChunk4::::insert(0, chunk); #[block] { - Pallet::::crossing_placed_batch_work_unit(CROSSING_COHORT_BENCHMARK_MAX) - .expect("non-tail Crossing batch with trimmed tail must succeed"); + let mut loaded = + BenchmarkAdmissionChunk4::::get(0).expect("frame C4 benchmark chunk exists"); + for cell in loaded.iter_mut().take(n as usize) { + cell[0] = cell[0].wrapping_add(1); + } + BenchmarkAdmissionChunk4::::insert(0, loaded); } - for actor_id in actors - .into_iter() - .take(CROSSING_NON_TAIL_BENCHMARK_MAX as usize) + let stored = BenchmarkAdmissionChunk4::::get(0).expect("frame C4 benchmark chunk remains"); + assert_eq!(stored.len(), 4); + assert!(stored.iter().take(n as usize).all(|cell| cell[0] == 2)); + } + + #[benchmark(extra)] + fn control_frame_chunk8_io(n: Linear<1, 8>) { + let chunk = BoundedVec::>::try_from(vec![[1u8; 171]; 8]) + .expect("frame C8 benchmark chunk fits"); + BenchmarkAdmissionChunk8::::insert(0, chunk); + #[block] { - assert!( - ActorHot::::get(actor_id) - .is_some_and(|hot| { hot.pending_signal && hot.queue_ticket.is_some() }) - ); + let mut loaded = + BenchmarkAdmissionChunk8::::get(0).expect("frame C8 benchmark chunk exists"); + for cell in loaded.iter_mut().take(n as usize) { + cell[0] = cell[0].wrapping_add(1); + } + BenchmarkAdmissionChunk8::::insert(0, loaded); } + let stored = BenchmarkAdmissionChunk8::::get(0).expect("frame C8 benchmark chunk remains"); + assert_eq!(stored.len(), 8); + assert!(stored.iter().take(n as usize).all(|cell| cell[0] == 2)); } - #[benchmark] - fn crossing_skip_unit() { - let (_, actor_id) = prepare_crossing_work::(2); - ActorHot::::mutate(actor_id, |maybe_hot| { - let hot = maybe_hot - .as_mut() - .expect("benchmark actor hot state must exist"); - let TriggerRuntimeState::ObservationCrossing { - installed_at_revision, - .. - } = &mut hot.trigger_runtime_state - else { - panic!("benchmark actor must use Crossing state"); - }; - *installed_at_revision = 2; - }); + #[benchmark(extra)] + fn control_frame_chunk16_io(n: Linear<1, 16>) { + let chunk = BoundedVec::>::try_from(vec![[1u8; 171]; 16]) + .expect("frame C16 benchmark chunk fits"); + BenchmarkAdmissionChunk16::::insert(0, chunk); #[block] { - Pallet::::crossing_work_unit().expect("post-installation Crossing skip must succeed"); + let mut loaded = + BenchmarkAdmissionChunk16::::get(0).expect("frame C16 benchmark chunk exists"); + for cell in loaded.iter_mut().take(n as usize) { + cell[0] = cell[0].wrapping_add(1); + } + BenchmarkAdmissionChunk16::::insert(0, loaded); } - assert!(ActorHot::::get(actor_id).is_some_and(|hot| { - !hot.pending_signal - && hot.queue_ticket.is_none() - && matches!( - hot.trigger_runtime_state, - TriggerRuntimeState::ObservationCrossing { - phase: CrossingPhase::Armed, - installed_at_revision: 2, - } - ) - })); + let stored = BenchmarkAdmissionChunk16::::get(0).expect("frame C16 benchmark chunk remains"); + assert_eq!(stored.len(), 16); + assert!(stored.iter().take(n as usize).all(|cell| cell[0] == 2)); } - #[benchmark] - fn crossing_skip_pair_unit() { - let (feed, first_actor) = prepare_crossing_work::(2); - let second_owner: T::AccountId = account("crossing-skip-pair-unit", 0, 0); - let second_actor = bench_create_user_with_trigger::( - second_owner, - Trigger::observation_crossing(feed, CrossingDirection::Rising, 2, 0), - ); - for actor_id in [first_actor, second_actor] { - ActorHot::::mutate(actor_id, |maybe_hot| { - let hot = maybe_hot.as_mut().expect("pair Actor hot state must exist"); - let TriggerRuntimeState::ObservationCrossing { - installed_at_revision, - .. - } = &mut hot.trigger_runtime_state - else { - panic!("pair Actor must use Crossing state"); - }; - *installed_at_revision = 2; - }); + #[benchmark(extra)] + fn control_frame_chunk32_io(n: Linear<1, 32>) { + ActorReadyFrameChunks::::insert(0, control_named_chunk::(0, 32)); + #[block] + { + let mut loaded = + ActorReadyFrameChunks::::get(0).expect("named frame C32 benchmark chunk exists"); + for cell in loaded.iter_mut().take(n as usize).flatten() { + cell.hot.pending_signal = false; + } + ActorReadyFrameChunks::::insert(0, loaded); } - CrossingRangeCursors::::insert( - feed, - CrossingRangeCursor { - revision: 2, - traversal: CrossingTraversal::Upward, - search_bound: 2, - current_threshold: Some(2), - page: 0, - offset: 0, - exhausted: false, - }, + let stored = + ActorReadyFrameChunks::::get(0).expect("named frame C32 benchmark chunk remains"); + assert_eq!(stored.len(), 32); + assert!( + stored + .iter() + .take(n as usize) + .flatten() + .all(|cell| !cell.hot.pending_signal) ); + } + + #[benchmark(extra)] + fn control_frame_chunk64_io(n: Linear<1, 64>) { + let chunk = BoundedVec::>::try_from(vec![[1u8; 171]; 64]) + .expect("frame C64 benchmark chunk fits"); + BenchmarkAdmissionChunk64::::insert(0, chunk); #[block] { - Pallet::::crossing_pair_work_unit().expect("Crossing skip pair must succeed"); - } - for actor_id in [first_actor, second_actor] { - assert!(ActorHot::::get(actor_id).is_some_and(|hot| { - !hot.pending_signal - && hot.queue_ticket.is_none() - && matches!( - hot.trigger_runtime_state, - TriggerRuntimeState::ObservationCrossing { - phase: CrossingPhase::Armed, - installed_at_revision: 2, - } - ) - })); + let mut loaded = + BenchmarkAdmissionChunk64::::get(0).expect("frame C64 benchmark chunk exists"); + for cell in loaded.iter_mut().take(n as usize) { + cell[0] = cell[0].wrapping_add(1); + } + BenchmarkAdmissionChunk64::::insert(0, loaded); } + let stored = BenchmarkAdmissionChunk64::::get(0).expect("frame C64 benchmark chunk remains"); + assert_eq!(stored.len(), 64); + assert!(stored.iter().take(n as usize).all(|cell| cell[0] == 2)); } - #[benchmark] - fn crossing_actor_unit() { - let (_, actor_id) = prepare_crossing_work::(2); - NextQueueTicket::::put(u64::MAX); + /// Actor control due-only owner move from one waiting C32 chunk into a fresh ready C32 chunk. + #[benchmark(extra)] + fn control_frame_move_waiting_to_fresh_ready32(n: Linear<1, 32>) { + let key = WakeupKey::Block(1u32.into()); + let waiting = ActorWaitingChunkOf::::try_from( + control_named_chunk::(0, 32) + .into_iter() + .map(|cell| cell.map(ActorWaitingEntry::Primary)) + .collect::>(), + ) + .expect("named Waiting primary chunk fits"); + ActorWaitingFrameChunks::::insert((key, 0), control_waiting_page::(waiting)); #[block] { - Pallet::::crossing_work_unit() - .expect("matched Crossing actor terminal cleanup must succeed"); + let loaded = ActorWaitingFrameChunks::::get((key, 0)) + .expect("named frame waiting C32 benchmark chunk exists"); + let mut cells = loaded.entries.into_inner(); + let moved = cells + .drain(..n as usize) + .map(|entry| entry.and_then(ActorWaitingEntry::into_primary)) + .collect::>(); + let remaining = + ActorWaitingChunkOf::::try_from(cells).expect("named frame waiting C32 remainder fits"); + let ready = + ActorControlChunkOf::::try_from(moved).expect("named frame fresh ready C32 prefix fits"); + if remaining.is_empty() { + ActorWaitingFrameChunks::::remove((key, 0)); + } else { + ActorWaitingFrameChunks::::insert((key, 0), control_waiting_page::(remaining)); + } + ActorReadyFrameChunks::::insert(0, ready); } - assert!(!ActorHot::::contains_key(actor_id)); - assert!(!CrossingMemberships::::contains_key(actor_id)); + assert_eq!(ActorReadyFrameChunks::::decode_len(0), Some(n as usize)); } - #[benchmark] - fn transaction_extension_ingress_base() { - let owner: T::AccountId = whitelisted_caller(); - let populated_actor_id = bench_create_user::(owner); - let proof_witness = Pallet::::active_actor_view(populated_actor_id) - .expect("benchmark actor exists") - .sovereign_account; - let recipient: T::AccountId = account("unmatched_ingress_recipient", 0, 0); - let source: T::AccountId = account("ingress_source", 0, 0); - T::BenchmarkHelper::setup_address_event_ingress(&recipient, &source, One::one()) - .expect("benchmark helper must prepare an unmatched producer event"); + /// Actor control worst fixed-C32 append crossing: one cell fills the existing ready tail and the + /// remaining 31 cells allocate its immediate successor without reordering the waiting cohort. + #[benchmark(extra)] + fn control_frame_move_waiting_across_ready32_tail() { + let key = WakeupKey::Block(1u32.into()); + let waiting = ActorWaitingChunkOf::::try_from( + control_named_chunk::(0, 32) + .into_iter() + .map(|cell| cell.map(ActorWaitingEntry::Primary)) + .collect::>(), + ) + .expect("named Waiting primary chunk fits"); + ActorWaitingFrameChunks::::insert((key, 0), control_waiting_page::(waiting)); + ActorReadyFrameChunks::::insert(0, control_named_chunk::(100, 31)); #[block] { - // Storage benchmarking does not attribute an absent overlay lookup to its map. Read a - // populated witness first so the generated envelope includes one conservative database - // read and the map's maximum proof before exercising the real negative lookup. - assert!(SovereignIndex::::contains_key(&proof_witness)); - assert!(!T::BenchmarkHelper::run_address_event_ingress( - &recipient, - &source, - One::one(), + let waiting = ActorWaitingFrameChunks::::get((key, 0)) + .expect("named frame crossing waiting C32 benchmark chunk exists"); + let mut waiting_cells = waiting + .entries + .into_iter() + .map(|entry| entry.and_then(ActorWaitingEntry::into_primary)) + .collect::>(); + let first = waiting_cells.remove(0); + let mut ready_tail = ActorReadyFrameChunks::::get(0) + .expect("named frame crossing ready C32 benchmark tail exists"); + ready_tail + .try_push(first) + .expect("one waiting cell fills the ready C32 tail"); + let successor = ActorControlChunkOf::::try_from(waiting_cells) + .expect("remaining waiting cells fit the successor C32 chunk"); + ActorReadyFrameChunks::::insert(0, ready_tail); + ActorReadyFrameChunks::::insert(1, successor); + ActorWaitingFrameChunks::::remove((key, 0)); + } + assert_eq!(ActorReadyFrameChunks::::decode_len(0), Some(32)); + assert_eq!(ActorReadyFrameChunks::::decode_len(1), Some(31)); + } + + /// Actor control external-boundary locator transition. Execution never reads this map, but every + /// physical Waiting/Ready move must publish the new ticket/location without mass compaction. + #[benchmark(extra)] + fn control_frame_locator_transition_batch32(n: Linear<1, 32>) { + let first_actor_id = NextActorId::::get(); + for offset in 0..n as u64 { + assert!(!ActorControlLocators::::contains_key( + first_actor_id.saturating_add(offset) )); } + #[block] + { + for offset in 0..n as u64 { + let actor_id = first_actor_id.saturating_add(offset); + ActorControlLocators::::insert( + actor_id, + ActorControlLocation::Ready { + ticket: 10_000u64.saturating_add(offset), + }, + ); + } + } + for offset in 0..n as u64 { + assert_eq!( + ActorControlLocators::::get(first_actor_id.saturating_add(offset)), + Some(ActorControlLocation::Ready { + ticket: 10_000u64.saturating_add(offset), + }) + ); + } } - #[benchmark] - 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_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"); + /// Actor control one-batch service cursor settlement after a contiguous ready prefix commits. + #[benchmark(extra)] + fn control_frame_service_cursor_batch32(n: Linear<1, 32>) { + ActorReadyHead::::put(0); + ActorReadyTail::::put(32); + ActorReadyOccupancy::::put(32); #[block] { - assert!(T::BenchmarkHelper::run_address_event_ingress( - &recipient, - &source, - One::one(), - )); + let head = ActorReadyHead::::get(); + let tail = ActorReadyTail::::get(); + let occupancy = ActorReadyOccupancy::::get(); + assert!(head.saturating_add(n as u64) <= tail); + ActorReadyHead::::put(head.saturating_add(n as u64)); + ActorReadyOccupancy::::put(occupancy.saturating_sub(n)); } - assert!( - ActorHot::::get(actor_id) - .is_some_and(|hot| { hot.pending_signal && hot.wakeup_pointer.is_some() }) - ); } - #[benchmark] - fn funding_snapshot_open(a: Linear<1, 10>) { - let owner: T::AccountId = whitelisted_caller(); - let actor_id = bench_create_user::(owner); - let assets = T::BenchmarkHelper::funding_assets(a); - ActorFunding::::mutate(actor_id, |maybe| { - let funding = maybe.as_mut().expect("benchmark actor funding exists"); - for asset in assets { - funding - .funding_accumulated - .try_insert(asset, 2u32.into()) - .expect("snapshot-open benchmark bound fits"); - } - }); - let snapshot; + /// Actor control one-batch due-page cursor settlement into the current ready tail. + #[benchmark(extra)] + fn control_frame_due_cursor_batch32(n: Linear<1, 32>) { + let key = WakeupKey::Block(1u32.into()); + ActorWaitingTails::::insert(key, n as u64); + ActorReadyTail::::put(31); + ActorReadyOccupancy::::put(31); #[block] { - snapshot = ActorFunding::::mutate(actor_id, |maybe| { - maybe - .as_mut() - .map(|funding| core::mem::take(&mut funding.funding_accumulated)) - .expect("benchmark actor funding exists") - }); + let waiting = ActorWaitingTails::::get(key); + let ready_tail = ActorReadyTail::::get(); + let occupancy = ActorReadyOccupancy::::get(); + assert_eq!(waiting, n as u64); + ActorWaitingTails::::remove(key); + ActorReadyTail::::put(ready_tail.saturating_add(n as u64)); + ActorReadyOccupancy::::put(occupancy.saturating_add(n)); } - assert_eq!(snapshot.len() as u32, a); - assert!( - ActorFunding::::get(actor_id) - .expect("benchmark actor funding exists") - .funding_accumulated - .is_empty() - ); } - /// Builds a circular chain of `n` system Actors where each transfers 1% of its - /// native balance to the next actor in the ring, then runs 3 blocks and asserts zero drift. - pub(super) fn setup_and_run_circular_chain( - requested_n: u32, - ) -> alloc::vec::Vec { - let existing_active = ActorHot::::iter_keys().count() as u32; - let available = T::MaxActiveActors::get().saturating_sub(existing_active); - let n = requested_n.min(available); - assert!( - n > 0, - "benchmark requires at least one available active slot" - ); - let pct = polkadot_sdk::sp_runtime::Perbill::from_percent(1); - let initial_balance = T::MinUserBalance::get().saturating_mul(1_000_000u32.into()); - let native = T::FeeNativeAssetId::get(); - let schedule = Schedule { - trigger: Trigger::cadenced(1), - cooldown_blocks: 0, - }; - let mut sovereigns: alloc::vec::Vec = alloc::vec::Vec::with_capacity(n as usize); - let mut actor_ids: alloc::vec::Vec = alloc::vec::Vec::with_capacity(n as usize); - for i in 0..n { - let owner: T::AccountId = account("owner", i, 0); - let temp_contract_steps = make_inert_contract_steps::(); + /// Complete minimal User Opening cohort through the canonical FIFO and Pipeline Opening. + /// Trigger source work remains setup-owned; the measured branch includes control execution, + /// fees, events, and explicit successor projection/persistence. + #[benchmark(extra, pov_mode = Measured)] + fn control_user_opening_complete_batch32(n: Linear<1, 32>) { + let now: BlockNumberFor = 1u32.into(); + frame_system::Pallet::::set_block_number(0u32.into()); + GlobalCircuitBreaker::::put(false); + let mut actors = Vec::with_capacity(n as usize); + for index in 0..n { + let owner: T::AccountId = account("control-complete-user", index, 0); + let actor_id = bench_create_user_with_trigger_and_steps::( + owner.clone(), + Trigger::manual(), + make_inert_contract_steps::(), + ); + let sovereign = Pallet::::active_actor_view(actor_id) + .map(|instance| instance.sovereign_account) + .expect("Actor control complete User exists after setup"); + let reserve: T::Balance = (u64::MAX / 4).saturated_into(); + let _ = T::AssetOps::mint(&sovereign, T::FeeNativeAssetId::get(), reserve); + actors.push((actor_id, owner)); + } + frame_system::Pallet::::set_block_number(now); + for (actor_id, owner) in actors.iter().cloned() { + Pallet::::manual_trigger(RawOrigin::Signed(owner).into(), actor_id) + .expect("Actor control complete User trigger succeeds in setup"); + let (state, admission, loaded_step) = Pallet::::load_current_step_service_state(actor_id) + .expect("Actor control ready User setup state is coherent"); + let queue_ticket = state + .hot + .queue_ticket + .expect("Actor control ready User setup ticket exists"); + let ticket = Pallet::::build_actor_step_ticket( + actor_id, + queue_ticket, + now, + &state.identity, + &state.hot, + state.run_state.as_ref(), + &admission, + ) + .expect("Actor control ready User setup ticket builds"); + let expected_identity = state.identity.clone(); + let expected_hot = state.hot.clone(); + let expected_admission = admission.clone(); + let cell = Pallet::::control_opening_cell_from_scalar( + actor_id, + state.identity, + state.hot, + admission, + &ticket, + &loaded_step, + ) + .expect("Actor control ready User setup projects to frame"); + let restored = Pallet::::project_control_cell( + &cell, + ActorControlLocation::Ready { + ticket: queue_ticket, + }, + ) + .expect("Actor control ready User setup restores from frame"); + assert_eq!(restored.0, expected_identity); + assert_eq!(restored.1, expected_hot); + assert_eq!(restored.2, expected_admission); + } + let actor_ids = actors + .iter() + .map(|(actor_id, _)| *actor_id) + .collect::>(); + #[block] + { + for _ in 0..n { + if ActorReadyOccupancy::::get() == 0 { + break; + } + core::hint::black_box(Pallet::::execute_cycle(Weight::MAX)); + } + for actor_id in actor_ids.iter().take(n as usize).copied() { + let (state, admission, loaded_step) = + Pallet::::load_current_step_service_state(actor_id).unwrap_or_else(|| { + panic!( + "Actor control completed User state remains coherent: actor={actor_id} identity={} hot={} contract={} admission={} funding={}", + ActorIdentities::::contains_key(actor_id), + benchmark_fixture_hot::(actor_id).is_some(), + ActorContractHeads::::contains_key(actor_id), + benchmark_fixture_admission::(actor_id).is_some(), + ActorFunding::::contains_key(actor_id), + ) + }); + assert_eq!(state.identity.cycle_nonce, 1); + assert_eq!(state.hot.cycle_state, CycleState::Idle); + assert!(!state.hot.pending_signal); + assert!(state.hot.queue_ticket.is_none()); + assert!(state.run_state.is_none()); + let cell = Pallet::::control_unsignaled_cell_from_scalar( + actor_id, + state.identity, + state.hot, + admission, + &loaded_step, + ) + .expect("Actor control complete User authority projects exactly"); + ActorUnsignaledControlCells::::insert(actor_id, &cell); + ActorControlLocators::::insert(actor_id, ActorControlLocation::Unsignaled); + core::hint::black_box(cell); + } + } + assert_eq!(ActorReadyOccupancy::::get(), 0); + for actor_id in actor_ids.iter().take(n as usize).copied() { + assert!(ActorUnsignaledControlCells::::contains_key(actor_id)); + assert!(!ActorIdentities::::contains_key(actor_id)); + assert!(Pallet::::actor_control_cell(actor_id).is_some()); + } + } + + fn control_install_temporal_waiting(n: u32, due_tick: SchedulerTick) -> Vec { + frame_system::Pallet::::set_block_number(1u32.into()); + GlobalCircuitBreaker::::put(false); + clear_host_genesis_wakeup_placements::(); + let mut actor_ids = Vec::with_capacity(n as usize); + for index in 0..n { + let owner: T::AccountId = account("control-temporal-system", index, 0); Pallet::::create_system_actor( RawOrigin::Root.into(), owner, Mutability::Mutable, - system_contract::(schedule.clone(), temp_contract_steps), + system_contract::( + Schedule { + trigger: Trigger::cadenced(5), + cooldown_blocks: 0, + }, + make_inert_contract_steps::(), + ), ) - .expect("create_system_actor must succeed"); + .expect("Actor control temporal System creation succeeds"); let actor_id = NextActorId::::get().saturating_sub(1); - let sov = Pallet::::sovereign_account_id_system(actor_id); - let _ = T::AssetOps::mint(&sov, native, initial_balance); - sovereigns.push(sov); + Pallet::::trigger_wakeup_substrate_invalidate_inner(actor_id) + .expect("Actor control reference Trigger wakeup invalidates"); + let (state, admission, loaded_step) = Pallet::::load_current_step_service_state(actor_id) + .expect("Actor control temporal System state is coherent"); + let cell = Pallet::::control_unsignaled_cell_from_scalar( + actor_id, + state.identity, + state.hot, + admission, + &loaded_step, + ) + .expect("Actor control temporal System projects Unsignaled"); + ActorUnsignaledControlCells::::insert(actor_id, cell); + ActorControlLocators::::insert(actor_id, ActorControlLocation::Unsignaled); + Pallet::::control_stage_unsignaled_temporal(actor_id, due_tick) + .expect("Actor control temporal System stages Waiting"); actor_ids.push(actor_id); } - // Creation stamps `last_control_mutation_block`, and the one-mutation-per-actor clock is per - // block, so the rewrite below must land strictly after the block the creation loop ran on. - let creation_block = frame_system::Pallet::::block_number(); - frame_system::Pallet::::set_block_number(creation_block + One::one()); - for (i, actor_id) in actor_ids.iter().enumerate() { - let next_sov = sovereigns[(i + 1) % sovereigns.len()].clone(); - let transfer_contract_steps: ContractSteps = BoundedVec::try_from(alloc::vec![Step { - precondition: None, - task: ActorTask::Transfer { - to: next_sov, - asset: native, - amount: AmountResolution::PercentageOfCurrent(pct), - }, - on_error: StepErrorPolicy::AbortCycle, - }]) - .expect("transfer contract_steps fits"); - let contract = Pallet::::load_actor_contract(*actor_id).expect("Actor Contract exists"); - Pallet::::update_contract( - RawOrigin::Root.into(), - *actor_id, - ActorContract { - steps: transfer_contract_steps, - completion: CompletionPolicy::Persistent, - ..contract - }, - ) - .expect("update_contract must succeed"); + actor_ids + } + + /// Actor control Trigger-due transition from one C32 Tick page into N+1 Block waiting. The measured + /// branch clears Trigger pointers, latches readiness, and publishes service pointers/locators. + #[benchmark(extra, pov_mode = Measured)] + fn control_frame_temporal_latch_batch32(n: Linear<1, 32>) { + let actor_ids = control_install_temporal_waiting::(n, 10); + #[block] + { + let moved = Pallet::::control_latch_temporal_waiting_page(10, 0, 1u32.into(), 10) + .expect("Actor control temporal C32 latches"); + assert_eq!(moved, actor_ids); + core::hint::black_box(moved); + } + assert!(!ActorWaitingFrameChunks::::contains_key(( + WakeupKey::Tick(10), + 0 + ))); + assert_eq!( + ActorWaitingOccupancies::::get(WakeupKey::Block(2u32.into())), + n + ); + } + + /// Actor control N+1 transition from one C32 Block-waiting page into immutable Ready tickets. + #[benchmark(extra, pov_mode = Measured)] + fn control_frame_waiting_ready_batch32(n: Linear<1, 32>) { + let actor_ids = control_install_temporal_waiting::(n, 10); + Pallet::::control_latch_temporal_waiting_page(10, 0, 1u32.into(), 10) + .expect("Actor control temporal C32 latches in setup"); + frame_system::Pallet::::set_block_number(2u32.into()); + #[block] + { + let moved = Pallet::::control_promote_due_waiting_page(2u32.into(), 0, 2u32.into()) + .expect("Actor control N+1 C32 promotes"); + assert_eq!( + moved, + actor_ids + .iter() + .copied() + .enumerate() + .map(|(ticket, actor_id)| (actor_id, ticket as QueueTicket)) + .collect::>() + ); + core::hint::black_box(moved); + } + assert_eq!(ActorReadyTail::::get(), n as QueueTicket); + assert_eq!(ActorReadyOccupancy::::get(), n); + } + + #[cfg(test)] + use crate::mock::{Test, new_test_ext}; + #[cfg(test)] + #[test] + fn fixture_admission_reduction_preserves_protected_branch_and_structure() { + new_test_ext().execute_with(|| { + let count = <::MaxContractSteps as Get>::get(); + let mut steps = make_reachable_opening_steps::(count) + .expect("structurally bounded dense fixture exists"); + let protected = steps[0].clone(); + retain_admitted_contract_geometry::(ActorType::System, &mut steps, &[0]) + .expect("unrelated setup payload can be reduced through ordinary admission"); + let service = Pallet::::guaranteed_actor_service_weight() + .expect("mock exposes a valid service floor"); + assert_eq!(steps.len() as u32, count); + assert_eq!(steps[0], protected); + assert!( + Pallet::::contract_steps_admission_weight_upper(ActorType::System, &steps) + .all_lte(service) + ); + }); + } + + #[cfg(test)] + #[test] + fn reachable_retry_opening_partition_boundaries() { + let steps = <::MaxContractSteps as Get>::get(); + for amounts in [0, 1, steps * 2] { + for predicates in [0, 1, steps * benchmark_predicate_capacity::()] { + new_test_ext().execute_with(|| { + let actor_id = prepare_user_retry_opening_partition::(amounts, predicates) + .expect("independent capture boundaries reach actual suspension"); + let identity = benchmark_fixture_identity::(actor_id).unwrap(); + Pallet::::close_actor(RawOrigin::Signed(identity.owner).into(), actor_id) + .expect("independent capture boundaries close"); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("partition close leaves no orphan state"); + }); + } + } + } + #[cfg(test)] + #[test] + fn reachable_opening_tail_profile_boundaries() { + let maximum = <::MaxContractSteps as Get>::get(); + for tails in 0..=maximum.saturating_sub(1).div_ceil(MAX_STEPS_PER_TAIL_CHUNK) { + for profile in [ + ReachableOpeningProfile::UserPaged, + ReachableOpeningProfile::UserCompleteMin, + ReachableOpeningProfile::UserCompleteHeaderMax, + ReachableOpeningProfile::Minimal, + ReachableOpeningProfile::Predicated, + ReachableOpeningProfile::RetryMin, + ReachableOpeningProfile::RetryMax, + ReachableOpeningProfile::CompleteMin, + ReachableOpeningProfile::CompleteMax, + ReachableOpeningProfile::FailedMin, + ReachableOpeningProfile::FailedMax, + ] { + new_test_ext().execute_with(|| { + if tails == 0 + && matches!( + profile, + ReachableOpeningProfile::UserPaged + | ReachableOpeningProfile::Minimal + | ReachableOpeningProfile::Predicated + ) + { + assert!(prepare_reachable_opening::(tails, profile).is_err()); + return; + } + let (actor_id, count) = prepare_reachable_opening::(tails, profile) + .expect("host-feasible Opening tail profile is admitted"); + let accounting = matches!( + profile, + ReachableOpeningProfile::UserCompleteMin + | ReachableOpeningProfile::UserCompleteHeaderMax + ) + .then(|| user_pipeline_accounting::(actor_id)); + if matches!(profile, ReachableOpeningProfile::UserPaged) { + Pallet::::execute_cycle(Weight::MAX); + } else { + let now = frame_system::Pallet::::block_number(); + let (state, admission, loaded_step) = + benchmark_fixture_consume_frame_current_step_service_state::(actor_id); + let effect_weight = + execute_reachable_step_inner::(actor_id, state, admission, loaded_step, now); + if matches!( + profile, + ReachableOpeningProfile::FailedMin | ReachableOpeningProfile::FailedMax + ) { + assert_eq!(effect_weight, Weight::zero()); + } + } + assert_reachable_opening::(actor_id, count, profile); + if let Some(accounting) = accounting { + assert_user_pipeline_accounting::(actor_id, accounting); + } + }); + } + } + } + #[cfg(test)] + #[test] + fn reachable_suspended_head_inner_allocation_boundaries() { + let capacity = benchmark_predicate_capacity::(); + let legs = (<::MaxContractSteps as Get>::get() - 1) * 2; + let minimum = + legs.saturating_sub(<::MaxFundingTrackedAssets as Get>::get()); + for branch in [ + None, + Some(RunningInnerBranch::Complete), + Some(RunningInnerBranch::Progress), + ] { + let allocations = if matches!(branch, Some(RunningInnerBranch::Complete)) { + vec![0] + } else { + vec![minimum, minimum + (legs - minimum) / 2, legs] + }; + for n in allocations { + for predicates in 0..=capacity { + for opening in 0..=predicates { + new_test_ext().execute_with(|| { + let (actor_id, skip) = prepare_reachable_suspended_head_inner::( + n, + predicates - opening, + opening, + branch, + ) + .expect("coupled allocation and predicate timing partition are admitted"); + let before = ActorRunStateStore::::get(actor_id).expect("real Run exists"); + let now = frame_system::Pallet::::block_number(); + let (state, admission, loaded_step) = + benchmark_fixture_consume_frame_current_step_service_state::(actor_id); + assert_eq!( + execute_reachable_step_inner::(actor_id, state, admission, loaded_step, now), + Weight::zero() + ); + if let Some(branch) = branch { + if matches!(branch, RunningInnerBranch::Progress) { + let after = + ActorRunStateStore::::get(actor_id).expect("progress retains Run"); + assert_eq!(after.opening_snapshot, before.opening_snapshot); + assert_eq!( + after.opening_predicate_results, + before.opening_predicate_results + ); + assert_eq!(after.funding_snapshot, before.funding_snapshot); + } + assert_reachable_suspended_tail_skip::( + skip.expect("freeze fixture exists"), + now, + branch, + ); + } else { + assert_reachable_suspended_head_retry::(actor_id, before); + } + }); + } + } + } + } + new_test_ext().execute_with(|| { + assert!( + prepare_reachable_suspended_head_inner::(1, 0, 0, Some(RunningInnerBranch::Complete)) + .is_err() + ); + assert!(prepare_reachable_suspended_head_inner::(legs + 1, 0, 0, None).is_err()); + assert!(prepare_reachable_suspended_head_inner::(legs, capacity, 1, None).is_err()); + assert_eq!(NextActorId::::get(), 0); + }); + } + #[cfg(test)] + #[test] + fn reachable_suspended_head_retry_payload_tradeoffs() { + type Helper = ::BenchmarkHelper; + type Assets = ::AssetOps; + let count = <::MaxContractSteps as Get>::get(); + let predicates = benchmark_predicate_capacity::(); + let legs = count * 2; + let minimum = + legs.saturating_sub(<::MaxFundingTrackedAssets as Get>::get()); + for (opening_legs, opening_start) in [ + (minimum, 0), + (minimum + 1, 0), + (legs - 1, 0), + (legs, 0), + (minimum, 2), + (minimum + 1, 2), + (legs - 2, 2), + ] { + new_test_ext().execute_with(|| { + let (actor_id, _) = prepare_reachable_suspended_head::(opening_legs, opening_start) + .expect("two-leg head reaches a real due retry"); + let state = Pallet::::active_actor_state(actor_id).expect("due source exists"); + let run = state.run_state.as_ref().expect("first failure retained Run"); + assert_eq!(run.unsuccessful_attempts_at_cursor, 1); + assert_eq!(run.opening_snapshot.len() as u32, opening_legs); + assert_eq!(run.funding_snapshot.len() as u32, legs - opening_legs); + assert_eq!(run.opening_predicate_results.len() as u32, count * predicates); + let ActorTask::AddLiquidity { asset_a, asset_b, amount_a, amount_b, .. } = + &state.contract.steps[0].task else { panic!("two-leg head remains authored") }; + let (asset, amount) = if *asset_a != ::FeeNativeAssetId::get() { + (*asset_a, amount_a) + } else { + (*asset_b, amount_b) + }; + assert_ne!(asset, ::FeeNativeAssetId::get()); + assert_eq!(matches!(amount, AmountResolution::PercentageOfLastFunding(_)), opening_start == 2); + let frozen = match amount { + AmountResolution::PercentageAtOpening(pct) => pct.mul_floor( + *run.opening_snapshot.get(&OpeningSurface::PreservableAsset(asset)) + .expect("head Opening amount is retained")), + AmountResolution::PercentageOfLastFunding(pct) => pct.mul_floor( + *run.funding_snapshot.get(&asset).expect("head funding amount is retained")), + _ => panic!("head amount is frozen by authored source"), + }; + let actor = state.identity.sovereign_account; + let owner = state.identity.owner; + let remaining = Assets::minimum_balance(asset).max(1); + assert!(frozen > remaining); + let withdrawal = Assets::balance(&actor, asset).checked_sub(remaining) + .expect("first attempt retains funded head custody"); + Assets::transfer(&actor, &owner, asset, withdrawal) + .expect("fixture ledger withdrawal leaves the minimum intact"); + assert_eq!(Assets::balance(&actor, asset), remaining); + assert_eq!(ActorRunStateStore::::get(actor_id) + .expect("ledger withdrawal retains Run").encode(), run.encode()); + let custody = (Assets::balance(&actor, asset), Assets::balance(&owner, asset)); + let (source, admission, loaded_step) = + benchmark_fixture_consume_frame_current_step_service_state::(actor_id); + assert_eq!(execute_reachable_step_inner::( + actor_id, source, admission, loaded_step, run.eligible_at, + ), Weight::zero()); + let after = ActorRunStateStore::::get(actor_id).expect("second failure retains Run"); + assert_eq!(after.cursor, 0); + assert_eq!(after.cycle_nonce, run.cycle_nonce); + assert_eq!(after.unsuccessful_attempts_at_cursor, 2); + assert_eq!(after.last_committed_step_block, None); + assert_eq!(after.last_attempt_block, run.eligible_at); + assert_eq!(after.last_step_outcome, Some(StepOutcome::FundingUnavailable)); + assert_eq!(after.opening_snapshot, run.opening_snapshot); + assert_eq!(after.opening_predicate_results, run.opening_predicate_results); + assert_eq!(after.funding_snapshot, run.funding_snapshot); + assert!(after.eligible_at > run.eligible_at); + assert_eq!((Assets::balance(&actor, asset), Assets::balance(&owner, asset)), custody); + assert!(matches!(ActorControlLocators::::get(actor_id), + Some(ActorControlLocation::Waiting { key: WakeupKey::Block(at), .. }) if at == after.eligible_at)); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("two-leg retry reconciles exact Waiting ownership"); + }); + } + let tail_legs = 2 * (count - 1); + let minimum_opening = + tail_legs.saturating_sub(<::MaxFundingTrackedAssets as Get>::get()); + for opening_legs in [ + minimum_opening, + minimum_opening + 1, + tail_legs - 1, + tail_legs, + ] { + for head_predicates in 0..=predicates { + for head_opening in 0..=head_predicates { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + GlobalCircuitBreaker::::put(false); + let owner = account("head-retry-owner", 0, 0); + ensure_creation_balance::(&owner); + let auxiliary_start = count * (2 + predicates); + let assets = Helper::setup_predicate_assets(&owner, auxiliary_start + 1 + predicates) + .expect("head and predicate assets exist beyond the distinct tail assets"); + let retry_asset = assets[auxiliary_start as usize]; + assert_ne!(retry_asset, ::FeeNativeAssetId::get()); + let mut steps = make_reachable_opening_steps::(count) + .expect("reference host admits maximum Contract"); + for (index, step) in steps.iter_mut().skip(1).enumerate() { + let ActorTask::AddLiquidity { amount_a, amount_b, .. } = &mut step.task else { + unreachable!() + }; + for (offset, amount) in [amount_a, amount_b].into_iter().enumerate() { + if (index * 2 + offset) as u32 >= opening_legs { + *amount = AmountResolution::PercentageOfLastFunding(Perbill::one()); + } + } + } + steps[0] = Step { + precondition: (head_predicates > 0).then(|| packed_predicate_clauses::( + (0..head_predicates).map(|index| TimedPredicate { + timing: if index < head_opening { + ObservationTiming::Opening + } else { + ObservationTiming::Current + }, + predicate: Predicate::BalanceBelow { + asset: assets[(auxiliary_start + 1 + index) as usize], + threshold: (index + 1).into(), + }, + }).collect(), + <::MaxPredicatesPerClause as Get>::get(), + )), + task: ActorTask::Transfer { + to: account("head-retry-recipient", 0, 0), + asset: retry_asset, + amount: AmountResolution::Fixed(One::one()), + }, + on_error: StepErrorPolicy::RetryLater { + max_attempts: <::MaxRetryAttempts as Get>::get(), + }, + }; + Pallet::::create_system_actor( + RawOrigin::Root.into(), + owner, + Mutability::Mutable, + system_contract::( + Schedule { trigger: Trigger::manual(), cooldown_blocks: 2 }, + steps, + ), + ) + .expect("authored head and coupled tail are admitted"); + let actor_id = NextActorId::::get() - 1; + fund_reachable_update_assets::(actor_id, 10); + Pallet::::manual_trigger(RawOrigin::Signed(owner).into(), actor_id) + .expect("real occurrence admits Opening"); + let (opening_state, _, opening_step) = + Pallet::::load_current_step_service_state(actor_id) + .expect("admitted Opening service state exists"); + let reserved_control = opening_step.resources.control; + let opening_instance = Pallet::::derive_active_actor_view( + opening_state.identity, opening_state.hot, opening_state.contract, + ); + let opening_context = Pallet::::execution_step_control_weight_context( + &opening_instance, None, &opening_step, + ).expect("Opening context retains admission geometry"); + assert_eq!(opening_context.funding_snapshot_entries, + <::MaxFundingTrackedAssets as Get>::get()); + Pallet::::execute_cycle(Weight::MAX); + let state = Pallet::::active_actor_state(actor_id).expect("Opening retains Actor"); + let run = state.run_state.expect("unfunded head publishes real retry"); + assert_eq!(state.hot.cycle_state, CycleState::Suspended); + assert_eq!(state.identity.cycle_nonce, 0); + assert_eq!(run.cursor, 0); + assert_eq!(run.cycle_nonce, 1); + assert_eq!(run.last_committed_step_block, None); + assert_eq!(run.unsuccessful_attempts_at_cursor, 1); + assert_eq!(run.last_step_outcome, Some(StepOutcome::FundingUnavailable)); + assert_eq!(run.opening_snapshot.len() as u32, opening_legs); + assert_eq!(run.funding_snapshot.len() as u32, tail_legs - opening_legs); + assert_eq!( + run.opening_predicate_results.len() as u32, + (count - 1) * predicates + head_opening, + ); + assert!(run.opening_predicate_results.iter() + .take(head_opening as usize).all(|value| *value == Ok(true))); + assert!(state.funding.funding_accumulated.is_empty()); + let retained = run.encode(); + frame_system::Pallet::::set_block_number(run.eligible_at); + let mut meter = polkadot_sdk::sp_weights::WeightMeter::with_limit(Weight::MAX); + let stats = Pallet::::drain_overdue_wakeups_cursor(run.eligible_at, &mut meter); + assert_eq!(stats.ready_entries, 1); + assert_eq!( + ActorRunStateStore::::get(actor_id).expect("due Run persists").encode(), + retained, + ); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("real head retry Ready source reconciles"); + let (state, admission, loaded_step) = + benchmark_fixture_consume_frame_current_step_service_state::(actor_id); + let instance = Pallet::::derive_active_actor_view( + state.identity.clone(), state.hot.clone(), state.contract.clone(), + ); + let context = Pallet::::execution_step_control_weight_context( + &instance, state.run_state.as_ref(), &loaded_step, + ).expect("Suspended context uses retained Run geometry"); + assert_eq!(context.funding_snapshot_entries, tail_legs - opening_legs); + assert_eq!(loaded_step.resources.control, reserved_control); + let actor = state.identity.sovereign_account; + let recipient = account("head-retry-recipient", 0, 0); + let custody: Vec<_> = assets.iter().map(|asset| ( + Assets::balance(&actor, *asset), Assets::balance(&recipient, *asset), + )).collect(); + assert_eq!( + loaded_step.step.precondition.as_ref().map_or(0, Precondition::evaluation_units), + head_predicates + head_opening, + ); + let effect = execute_reachable_step_inner::( + actor_id, state, admission, loaded_step, run.eligible_at, + ); + assert_eq!(effect, Weight::zero()); + let after = ActorRunStateStore::::get(actor_id).expect("second retry retains Run"); + assert_eq!(after.cursor, 0); + assert_eq!(after.cycle_nonce, run.cycle_nonce); + assert_eq!(after.last_committed_step_block, None); + assert_eq!(after.unsuccessful_attempts_at_cursor, 2); + assert_eq!(after.last_step_outcome, Some(StepOutcome::FundingUnavailable)); + assert_eq!(after.opening_snapshot, run.opening_snapshot); + assert_eq!(after.opening_predicate_results, run.opening_predicate_results); + assert_eq!(after.funding_snapshot, run.funding_snapshot); + assert_eq!(assets.iter().map(|asset| ( + Assets::balance(&actor, *asset), Assets::balance(&recipient, *asset), + )).collect::>(), custody); + assert!(matches!(ActorControlLocators::::get(actor_id), Some(ActorControlLocation::Waiting { key: WakeupKey::Block(at), .. }) if at == after.eligible_at)); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("second retry Waiting destination reconciles"); + }); + } + } + } + } + #[cfg(test)] + #[test] + fn reachable_running_inner_fragment_predicate_boundaries() { + let maximum = <::MaxContractSteps as Get>::get(); + for branch in [RunningInnerBranch::Complete, RunningInnerBranch::Progress] { + let minimum = if matches!(branch, RunningInnerBranch::Complete) { + 1 + } else { + 2 + }; + for fragment in minimum..=maximum.saturating_sub(1).min(MAX_STEPS_PER_TAIL_CHUNK) { + for predicates in [0, benchmark_predicate_capacity::()] { + new_test_ext().execute_with(|| { + let (actor_id, cursor) = + prepare_reachable_running_inner::(fragment, predicates, branch) + .expect("host-feasible Running inner boundary is reachable"); + let now = frame_system::Pallet::::block_number(); + let (state, admission, loaded_step) = + benchmark_fixture_consume_frame_current_step_service_state::(actor_id); + execute_reachable_step_inner::(actor_id, state, admission, loaded_step, now); + assert_reachable_running_inner::(actor_id, cursor, now, branch); + }); + new_test_ext().execute_with(|| { + let fixture = + prepare_reachable_suspended_tail_skip::(fragment, predicates, branch) + .expect("host-feasible Suspended skip boundary is reachable"); + let now = frame_system::Pallet::::block_number(); + let (state, admission, loaded_step) = + benchmark_fixture_consume_frame_current_step_service_state::(fixture.actor_id); + let effect_weight = execute_reachable_step_inner::( + fixture.actor_id, + state, + admission, + loaded_step, + now, + ); + assert_eq!(effect_weight, Weight::zero()); + assert_reachable_suspended_tail_skip::(fixture, now, branch); + }); + } + } + } + for fragment in 1..=maximum.saturating_sub(1).min(MAX_STEPS_PER_TAIL_CHUNK) { + for predicates in [0, benchmark_predicate_capacity::()] { + new_test_ext().execute_with(|| { + let (actor_id, cursor) = + prepare_reachable_suspended_tail_retry::(fragment, predicates) + .expect("host-feasible Suspended retry boundary is reachable"); + let now = frame_system::Pallet::::block_number(); + let (state, admission, loaded_step) = + benchmark_fixture_consume_frame_current_step_service_state::(actor_id); + let effect_weight = + execute_reachable_step_inner::(actor_id, state, admission, loaded_step, now); + assert_eq!(effect_weight, Weight::zero()); + assert_reachable_suspended_tail_retry_state::(actor_id, cursor, 2); + }); + } + } + } + #[cfg(test)] + #[test] + fn reachable_running_tail_fragment_boundaries() { + let maximum = <::MaxContractSteps as Get>::get(); + for fragment in 1..=maximum.saturating_sub(1).min(MAX_STEPS_PER_TAIL_CHUNK) { + new_test_ext().execute_with(|| { + let (actor_id, ticket) = prepare_reachable_running_tail::(fragment) + .expect("host-feasible tail fragment is reachable"); + let retained = ActorRunStateStore::::get(actor_id) + .expect("real tail Run exists") + .encode(); + Pallet::::load_current_step_plan_from_storage(ticket) + .expect("each real partial or full tail fragment loads"); + assert_reachable_running_tail_unchanged::(actor_id, &retained); + }); + } + } + #[cfg(test)] + #[test] + fn reachable_update_allocation_boundaries_match_authored_contract() { + let legs = <::MaxContractSteps as Get>::get() * 2; + let minimum = + legs.saturating_sub(<::MaxFundingTrackedAssets as Get>::get()); + if minimum > 0 { + new_test_ext().execute_with(|| { + let (steps, _) = reachable_retry_contract_allocation::(minimum - 1, 0) + .expect("the authored Contract fits Step and predicate storage bounds"); + let next_actor = NextActorId::::get(); + assert_eq!( + Pallet::::create_system_actor( + RawOrigin::Root.into(), + account("funding-bound-owner", 0, 0), + Mutability::Mutable, + system_contract::( + Schedule { + trigger: Trigger::Manual, + cooldown_blocks: 100 + }, + steps, + ), + ), + Err(Error::::TooManyContractSteps.into()), + "independent funding sources exceed the host bound by one", + ); + assert_eq!(NextActorId::::get(), next_actor); + assert!(!ActorFunding::::contains_key(next_actor)); + assert!(!ActorContractHeads::::contains_key(next_actor)); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("rejected admission publishes no Actor state"); + }); } - let total_before: T::Balance = sovereigns - .iter() - .map(|sov| T::AssetOps::balance(sov, native)) - .fold(T::Balance::zero(), |acc, b| acc.saturating_add(b)); - for block in 2u32..=4 { - frame_system::Pallet::::set_block_number(block.into()); - let _ = Pallet::::on_idle(block.into(), Weight::MAX); + for opening_legs in [minimum, minimum + (legs - minimum) / 2, legs] { + for family in [ + TriggerFamily::ObservationCrossing, + TriggerFamily::ObservationChange, + ] { + new_test_ext().execute_with(|| { + let (owner, actor_id, replacement, old_feed) = + prepare_reachable_update::(opening_legs, family) + .expect("host-feasible allocation boundary is admitted"); + let expected = replacement.clone(); + execute_reachable_update::(owner, actor_id, replacement); + assert_reachable_update::(actor_id, &expected, old_feed); + }); + } + } + } + #[cfg(test)] + #[test] + fn mixed_waiting_reference_close_uses_real_opening() { + for unlink in [false, true] { + new_test_ext().execute_with(|| { + let fixture = prepare_mixed_waiting_close::(unlink) + .expect("mixed Waiting reference is reachable"); + Pallet::::close_actor(RawOrigin::Root.into(), fixture.actor_id).unwrap(); + assert_mixed_waiting_close::(&fixture, unlink); + }); } - // System Actors don't pay fees → transfers are pure balance moves → zero drift - let total_after: T::Balance = sovereigns - .iter() - .map(|sov| T::AssetOps::balance(sov, native)) - .fold(T::Balance::zero(), |acc, b| acc.saturating_add(b)); - assert_eq!( - total_before, total_after, - "Balance must be exactly conserved (System Actors pay no fees)" - ); - sovereigns } - /// Parametric stress test: circular chain of n system Actors. - /// - /// Capacity planning reference points: - /// - n=100: ~300 transfers/block (baseline) - /// - n=1_000: ~3000 transfers/block (moderate load) - /// - n=10_000: ~30000 transfers/block (high load) - #[benchmark(extra)] - fn circular_chain_stress(n: Linear<10, 10_000>) { - #[block] - { - setup_and_run_circular_chain::(n); + #[cfg(test)] + #[test] + fn mixed_waiting_deadline_close_reclaims_full_heap_tail() { + new_test_ext().execute_with(|| { + let (fixture, keys) = prepare_mixed_waiting_deadline_close::() + .expect("last Tick reference and full heap follow ordinary lifecycle transitions"); + Pallet::::close_actor(RawOrigin::Root.into(), fixture.actor_id).unwrap(); + assert_mixed_waiting_deadline_close::(&fixture, keys); + }); + } + + #[cfg(test)] + #[test] + fn full_waiting_heap_close_covers_upward_and_tail_removal() { + for upward in [true, false] { + new_test_ext().execute_with(|| { + let fixture = prepare_full_waiting_heap_close::(upward); + Pallet::::close_actor(RawOrigin::Root.into(), fixture.actor_id).unwrap(); + assert_full_waiting_heap_close::(&fixture); + }); } } - /// Extreme stress test request: 10K-100K Actors circular chain. - /// Effective n is clamped by available `MaxActiveActors` capacity. - #[benchmark(extra)] - fn circular_chain_stress_100k(n: Linear<10_000, 100_000>) { - #[block] - { - setup_and_run_circular_chain::(n); + #[cfg(test)] + #[test] + fn cadenced_opening_rearms_tick_heap_before_ready_continuation() { + use alloc::collections::BTreeMap; + + for existing_destination in [true, false] { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let mut steps = inert_contract_steps_of_len::(2); + steps[0].task = ActorTask::Transfer { + to: account("cadence-noop-recipient", 0, 0), + asset: ::FeeNativeAssetId::get(), + amount: AmountResolution::PercentageOfCurrent(Perbill::from_percent(50)), + }; + Pallet::::create_system_actor( + RawOrigin::Root.into(), + account("cadence-rearm-target", 0, 0), + Mutability::Mutable, + system_contract::( + Schedule { + trigger: Trigger::cadenced(100), + cooldown_blocks: 0, + }, + steps, + ), + ) + .expect("real Cadenced Contract is admitted"); + let actor_id = NextActorId::::get() - 1; + let first = Pallet::::actor_hot(actor_id) + .and_then(|hot| hot.trigger_wakeup_pointer) + .expect("first cadence") + .tick; + let next = first + 100; + for index in 0..96u32 { + let every = if index == 0 && existing_destination { + 200 + } else { + 1_000 + index + }; + Pallet::::create_system_actor( + RawOrigin::Root.into(), + account("cadence-rearm-competitor", index, 0), + Mutability::Mutable, + system_contract::( + Schedule { + trigger: Trigger::cadenced(u64::from(every)), + cooldown_blocks: 0, + }, + make_inert_contract_steps::(), + ), + ) + .expect("competing Tick primary is admitted"); + } + frame_system::Pallet::::set_block_number(first); + let (mut due, stats) = + Pallet::::wakeup_substrate_drain_key(WakeupKey::Tick(first), 1); + assert_eq!(stats.entries_scanned, 1); + let (due_actor, state, admission, loaded_step) = due.pop().expect("real due primary"); + assert_eq!(due_actor, actor_id); + assert_eq!( + Pallet::::process_due_temporal_occurrence_loaded( + actor_id, + state, + admission, + loaded_step, + first, + ), + Ok(false) + ); + assert_eq!(WakeupCursorLen::::get(WakeupClock::Tick), 96); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("due collection reconciles before Opening"); + let before: BTreeMap<_, _> = WakeupCursorPages::::iter().collect(); + Pallet::::execute_cycle(Weight::MAX); + let (location, cell) = + Pallet::::actor_control_cell(actor_id).expect("Running primary"); + assert!(matches!(location, ActorControlLocation::Ready { .. })); + assert_eq!(cell.cursor, 1); + let pointer = Pallet::::actor_hot(actor_id) + .and_then(|hot| hot.trigger_wakeup_pointer) + .expect("independent rearmed reference"); + assert_eq!(pointer.tick, next); + assert!(matches!( + ActorWaitingFrameChunks::::get((WakeupKey::Tick(next), pointer.page_id)) + .expect("reference page") + .entries[pointer.slot as usize], + Some(ActorWaitingEntry::Reference(_)) + )); + assert_eq!( + crate::mock::last_step_control_execution() + .expect("actual selection") + .placement, + crate::StepControlPlacement::Queue + ); + let after: BTreeMap<_, _> = WakeupCursorPages::::iter().collect(); + if existing_destination { + assert_eq!(before, after); + } else { + let changed = after + .iter() + .filter(|(key, page)| before.get(key) != Some(page)) + .count(); + assert_eq!(changed, 3, "Opening itself repairs three Tick heap pages"); + assert_eq!(WakeupCursorLen::::get(WakeupClock::Tick), 97); + } + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state() + .expect("Ready primary and independent Tick reference reconcile"); + }); } } - /// Fixed-size stress tests for scaling analysis. - /// Run all three and compare times to determine O(n) vs O(n²). - /// Linear: time ratio ≈ 10x when n increases 10x - /// Quadratic: time ratio ≈ 100x when n increases 10x + #[cfg(test)] + #[test] + fn opening_retry_reconciles_existing_and_new_waiting_heap_keys() { + use alloc::collections::BTreeMap; - #[benchmark] - fn circular_chain_100() { - #[block] - { - setup_and_run_circular_chain::(100); + let mut logical_results = Vec::new(); + for destination_entries in [0, 1, 32] { + logical_results.push(new_test_ext().execute_with(|| { + let (actor_id, keys, due) = prepare_reachable_waiting_opening::(destination_entries) + .expect("full host population is admitted"); + let (_, source) = Pallet::::actor_control_cell(actor_id).expect("Ready source"); + let capacity = Pallet::::effective_active_actor_limit(); + assert_eq!(ActiveActorCount::::get(), capacity); + assert_eq!(keys, 1_023 - destination_entries.saturating_sub(1)); + let before: BTreeMap<_, _> = WakeupCursorPages::::iter().collect(); + Pallet::::execute_cycle(Weight::MAX); + assert_reachable_waiting_opening::( + actor_id, + if destination_entries > 0 { + keys + } else { + capacity + }, + due, + ); + let after: BTreeMap<_, _> = WakeupCursorPages::::iter().collect(); + if destination_entries > 0 { + assert_eq!(before, after, "existing deadline needs no heap repair"); + } else { + assert_eq!(Pallet::::wakeup_cursor_peek(), Some(due)); + let changed = after + .iter() + .filter(|(key, page)| before.get(key) != Some(page)) + .count(); + assert_eq!(changed, 6, "full mock population repairs six heap pages"); + } + let execution = + crate::mock::last_step_control_execution().expect("actual control selection"); + assert_eq!(execution.placement, crate::StepControlPlacement::Wakeup); + ( + source.encode(), + Pallet::::actor_run_state(actor_id).encode(), + execution, + ) + })); } + assert!(logical_results.windows(2).all(|pair| pair[0] == pair[1])); } - #[benchmark] - fn circular_chain_1000() { - #[block] - { - setup_and_run_circular_chain::(1000); + #[cfg(test)] + #[test] + fn consumed_waiting_publication_extrema_use_complete_host_populations() { + let mut source_cells = Vec::new(); + for destination_entries in [0, 1, 32] { + source_cells.push(new_test_ext().execute_with(|| { + let (actor_id, count, keys, due) = prepare_reachable_waiting_publication::( + destination_entries, + 0, + ReachableOpeningProfile::RetryMin, + ) + .expect("RetryMin publication and complete host population are admitted"); + let (_, source) = Pallet::::actor_control_cell(actor_id).expect("Ready source"); + let encoded_source = source.encode(); + let publish = benchmark_fixture_prepare_consumed_service_waiting::(actor_id, due); + publish(); + assert_reachable_waiting_publication::( + actor_id, + count, + keys, + due, + destination_entries, + ); + assert_eq!(ActiveActorCount::::get(), 1_024); + assert_eq!(count, 1); + encoded_source + })); } + assert!(source_cells.windows(2).all(|pair| pair[0] == pair[1])); } - #[benchmark(extra)] - fn circular_chain_10000() { - #[block] - { - setup_and_run_circular_chain::(10_000); + #[cfg(test)] + #[test] + fn consumed_running_continued_wakeup_extrema_are_complete_successors() { + for dense in [false, true] { + new_test_ext().execute_with(|| { + let actor_id = if dense { + prepare_reachable_running::(3).expect("largest dense Running fixture is admitted") + } else { + prepare_low_capture_running::(None, 0) + .expect("maximum-length low-capture Running fixture is admitted") + }; + let source = + ActorRunStateStore::::get(actor_id).expect("continued Running source exists"); + assert_eq!(source.cursor, 1); + if dense { + assert_eq!(source.opening_snapshot.len(), 6); + assert_eq!( + source.opening_predicate_results.len() as u32, + 3 * benchmark_predicate_capacity::() + ); + } else { + assert!(source.opening_snapshot.is_empty()); + assert!(source.opening_predicate_results.is_empty()); + } + let now = source.eligible_at; + frame_system::Pallet::::set_block_number(now); + let (state, admission, loaded_step) = + benchmark_fixture_consume_frame_current_step_service_state::(actor_id); + benchmark_fixture_seed_saturated_ready_tombstones::(); + assert_eq!( + execute_reachable_step_inner::(actor_id, state, admission, loaded_step, now), + Weight::zero() + ); + let successor = + ActorRunStateStore::::get(actor_id).expect("continued successor retains Run"); + assert_eq!(successor.cursor, 2); + assert_eq!(successor.last_committed_step_block, Some(now)); + assert_eq!( + crate::mock::last_step_control_execution() + .expect("actual selector observes complete successor") + .placement, + crate::StepControlPlacement::Wakeup + ); + assert!(matches!( + ActorControlLocators::::get(actor_id), + Some(ActorControlLocation::Waiting { + key: WakeupKey::Block(at), + .. + }) if at == successor.eligible_at + )); + assert_eq!(ActorReadyOccupancy::::get(), 0); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("complete fallback successor is canonical"); + }); } } - #[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"); + #[cfg(test)] + #[test] + fn consumed_running_terminal_successors_publish_one_unsignaled_owner() { + for failed in [false, true] { + new_test_ext().execute_with(|| { + let actor_id = if failed { + prepare_run_terminal_failure_source::() + .expect("maximum-length low-capture failure source is admitted") + } else { + prepare_low_capture_running_with_type::( + ActorType::User, + Some(Step { + precondition: None, + task: ActorTask::StopCycle, + on_error: StepErrorPolicy::AbortCycle, + }), + 0, + ) + .expect("maximum-length low-capture completion source is admitted") + }; + let before_hold = + ActorStateHolds::::get(actor_id).expect("terminal User state hold exists"); + let now = ActorRunStateStore::::get(actor_id) + .expect("low-capture terminal source retains Run") + .eligible_at; + frame_system::Pallet::::set_block_number(now); + let (state, admission, loaded_step) = + benchmark_fixture_consume_frame_current_step_service_state::(actor_id); + let effect = + execute_reachable_step_inner::(actor_id, state, admission, loaded_step, now); + assert_eq!( + effect, + if failed { + Weight::zero() + } else { + ::TaskEffectWeight::actual_effect_weight( + &ActorTask::StopCycle, + TaskEffectExecution::Invoked, + ) + .expect("mock supplies StopCycle effect evidence") + } + ); + let successor = + Pallet::::active_actor_state(actor_id).expect("terminal authority remains active"); + assert!(successor.run_state.is_none()); + assert_eq!(successor.identity.cycle_nonce, 1); + assert_eq!(successor.hot.cycle_state, CycleState::Idle); + assert!(!successor.hot.pending_signal); + assert_eq!(successor.hot.unsuccessful_attempt_streak, u32::from(failed)); + assert!(successor.hot.queue_ticket.is_none() && successor.hot.wakeup_pointer.is_none()); + assert!(matches!( + ActorControlLocators::::get(actor_id), + Some(ActorControlLocation::Unsignaled) + )); + assert_eq!(ActorReadyOccupancy::::get(), 0); + let after_hold = + ActorStateHolds::::get(actor_id).expect("terminal User state hold remains owned"); + assert_eq!(after_hold.owner, before_hold.owner); + assert_eq!(after_hold.breakdown, before_hold.breakdown); + let execution = crate::mock::last_step_control_execution() + .expect("actual selector observes complete terminal successor"); + assert_eq!(execution.phase, crate::StepControlPhase::Running); + assert_eq!( + execution.outcome, + if failed { + crate::StepControlOutcome::Failed + } else { + crate::StepControlOutcome::Completed + } + ); + assert_eq!(execution.placement, crate::StepControlPlacement::None); + assert_eq!( + execution.task_effect, + if failed { + TaskEffectExecution::NotInvoked + } else { + TaskEffectExecution::Invoked + } + ); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("complete terminal successor is canonical"); + }); } - 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); + #[cfg(test)] + #[test] + fn consumed_running_suspension_selects_queue_or_wakeup_from_exact_eligibility() { + for (cooldown_blocks, expected_placement) in [ + (0, crate::StepControlPlacement::Queue), + (2, crate::StepControlPlacement::Wakeup), + ] { + new_test_ext().execute_with(|| { + let actor_id = prepare_run_owner_suspension_source::(false, cooldown_blocks) + .expect("maximum-length low-capture suspension source is admitted"); + let now = ActorRunStateStore::::get(actor_id) + .expect("low-capture Running retry source exists") + .eligible_at; + frame_system::Pallet::::set_block_number(now); + let (state, admission, loaded_step) = + benchmark_fixture_consume_frame_current_step_service_state::(actor_id); + assert_eq!( + execute_reachable_step_inner::(actor_id, state, admission, loaded_step, now), + Weight::zero() + ); + let successor = + ActorRunStateStore::::get(actor_id).expect("suspended successor retains Run"); + assert_eq!(successor.cursor, 1); + assert_eq!(successor.last_attempt_block, now); + assert_eq!( + successor.suspension, + Some(SuspensionReason::FundingUnavailable) + ); + assert_eq!( + crate::mock::last_step_control_execution() + .expect("actual selector observes suspension successor") + .placement, + expected_placement + ); + match expected_placement { + crate::StepControlPlacement::Queue => { + assert_eq!(successor.eligible_at, now + 1); + assert!(matches!( + ActorControlLocators::::get(actor_id), + Some(ActorControlLocation::Ready { .. }) + )); + assert_eq!(ActorReadyOccupancy::::get(), 1); + } + crate::StepControlPlacement::Wakeup => { + assert_eq!(successor.eligible_at, now + 2); + assert!(matches!( + ActorControlLocators::::get(actor_id), + Some(ActorControlLocation::Waiting { + key: WakeupKey::Block(at), + .. + }) if at == successor.eligible_at + )); + assert_eq!(ActorReadyOccupancy::::get(), 0); + } + _ => unreachable!(), + } + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("complete suspension successor is canonical"); + }); } } - #[benchmark] - fn block_resource_meter_extension() { - T::BenchmarkHelper::prepare_block_resource_meter_extension(); - #[block] + #[cfg(test)] + #[test] + fn continued_step_reports_waiting_after_ready_capacity_fallback() { + new_test_ext().execute_with(|| { + let (actor_id, _) = prepare_reachable_opening::(1, ReachableOpeningProfile::Minimal) + .expect("admitted Opening fixture"); + let (state, admission, loaded_step) = + benchmark_fixture_consume_frame_current_step_service_state::(actor_id); + // Exercise the consumed-Step placement boundary with real competing Ready owners. + // This is not evidence of a complete Executive interleaving that fills the queue. + let capacity = <::MaxQueueLength as Get>::get(); + for seed in 0..capacity { + let other = bench_create_system_manual::(60_000_000 + seed); + let owner = Pallet::::actor_identity(other) + .expect("competitor identity") + .owner; + Pallet::::manual_trigger(RawOrigin::Signed(owner).into(), other) + .expect("competing Ready ticket fits"); + if seed == 1 { + frame_system::Pallet::::set_block_number(2); + Pallet::::deactivate_actor(RawOrigin::Root.into(), other) + .expect("non-head lifecycle churn frees an Active slot but retains its tombstone"); + } + } + assert_eq!( + ActorReadyTail::::get() - ActorReadyHead::::get(), + u64::from(capacity) + ); + execute_reachable_step_inner::(actor_id, state, admission, loaded_step, 2); + assert!(matches!( + ActorControlLocators::::get(actor_id), + Some(ActorControlLocation::Waiting { + key: WakeupKey::Block(3), + .. + }) + )); + assert_eq!( + crate::mock::last_step_control_execution().map(|execution| execution.placement), + Some(crate::StepControlPlacement::Wakeup) + ); + assert_eq!( + ActorRunStateStore::::get(actor_id) + .expect("retained Run") + .cursor, + 1 + ); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().expect("fallback keeps one canonical process owner"); + }); + } + + #[cfg(test)] + #[test] + fn upward_heap_repair_preserves_indices_and_exposes_page_spread() { + for (removed_index, expected_index, changed_pages) in + [(6_143u32, 2u32, 10usize), (8_447, 7, 11)] { - T::BenchmarkHelper::execute_block_resource_meter_extension(); + new_test_ext().execute_with(|| { + clear_host_genesis_wakeup_placements::(); + let size = <::WakeupPageSize as Get>::get(); + let len = <::MaxActiveActors as Get>::get(); + assert_eq!((size, len), (32, 10_000)); + let mut tail_ancestors = Vec::new(); + let mut index = len - 1; + loop { + tail_ancestors.push(index); + if index == 0 { + break; + } + index = (index - 1) / 2; + } + // Complete heap-only authority: no claim that these keys own live Actor/Waiting state. + for page_id in 0..len.div_ceil(size) { + let mut page = WakeupCursorPageOf::::default(); + for index in page_id * size..((page_id + 1) * size).min(len) { + let block = if tail_ancestors.contains(&index) { + u32::BITS - (index + 1).leading_zeros() + } else { + 100_000 + index + }; + let key = WakeupKey::Block(u64::from(block)); + page.try_push(key).expect("complete heap page fits"); + ActorWaitingCursorIndices::::insert(key, index); + } + WakeupCursorPages::::insert((WakeupClock::Block, u64::from(page_id)), page); + } + WakeupCursorLen::::insert(WakeupClock::Block, len); + assert_wakeup_cursor_page_indices::(); + let before = WakeupCursorPages::::iter() + .map(|(key, page)| (key, page.encode())) + .collect::>(); + assert_eq!(before.len(), len.div_ceil(size) as usize); + let tail_key = Pallet::::wakeup_cursor_get(WakeupClock::Block, len - 1) + .expect("complete heap tail exists"); + assert!(Pallet::::wakeup_cursor_remove(u64::from( + 100_000 + removed_index + ))); + assert_eq!(WakeupCursorLen::::get(WakeupClock::Block), len - 1); + assert_eq!( + ActorWaitingCursorIndices::::get(tail_key), + Some(expected_index) + ); + assert!(!ActorWaitingCursorIndices::::contains_key( + WakeupKey::Block(u64::from(100_000 + removed_index)) + )); + assert_wakeup_cursor_page_indices::(); + assert_eq!(WakeupCursorPages::::iter().count(), before.len()); + assert_eq!( + before + .iter() + .filter(|(key, encoded)| { + WakeupCursorPages::::get(*key) + .map(|page| page.encode()) + .as_ref() + != Some(encoded) + }) + .count(), + changed_pages + ); + }); } - 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(); + #[cfg(test)] + #[test] + fn pipeline_insolvency_cleans_each_admitted_tail_and_future_window() { + let maximum = <::MaxContractSteps as Get>::get(); + for count in 0..=maximum { + for with_window in [false, true] { + new_test_ext().execute_with(|| { + let mut steps = make_max_contract_steps::(999); + steps.truncate(count as usize); + let (actor_id, before) = + prepare_user_pipeline_insolvency::(true, steps, with_window); + assert_eq!( + ActorContractTailChunks::::iter_prefix(actor_id).count(), + count.saturating_sub(1).div_ceil(MAX_STEPS_PER_TAIL_CHUNK) as usize + ); + let hot = benchmark_fixture_hot::(actor_id).unwrap(); + assert_eq!(hot.wakeup_pointer.is_some(), with_window); + let pass = Pallet::::execute_cycle(Weight::MAX); + assert!(pass.effect_consumed.is_zero() && !pass.starved); + assert_user_pipeline_insolvency::(actor_id, before); + if let Some(pointer) = hot.wakeup_pointer { + assert!(!Pallet::::wakeup_page_entry_matches( + pointer, actor_id + )); + assert!(!ActorWaitingCursorIndices::::contains_key( + pointer.block + )); + } + }); + } } - T::BenchmarkHelper::verify_maximum_xcm_version_discovery(); } #[cfg(test)] - use crate::mock::{Test, new_test_ext}; + #[test] + fn crossing_rearm_unavailability_preserves_paid_head_until_host_recovers() { + for unavailable in [ + ScalarObservationState::Unavailable, + ScalarObservationState::Uninitialized, + ] { + new_test_ext().execute_with(|| { + let (actor_id, feed) = + prepare_zero_step_observation::(Some(CrossingPhase::WaitingForRearm), false); + let available = crate::mock::MockObservationProvider::observe(&feed, 0, u32::MAX); + let follower = create_zero_step_system_follower::(); + crate::mock::set_observation(feed, unavailable); + let state = Pallet::::active_actor_state(actor_id) + .unwrap() + .encode(); + let head = ActorReadyHead::::get(); + let events = frame_system::Pallet::::events(); + let before = user_pipeline_accounting::(actor_id); + let complete_refusal = + <::WeightInfo as WeightInfo>::scheduler_paged_zero_step_user_crossing_unavailable(); + if unavailable == ScalarObservationState::Unavailable { + for budget in [ + Weight::from_parts(complete_refusal.ref_time().saturating_sub(1), u64::MAX), + Weight::from_parts(u64::MAX, complete_refusal.proof_size().saturating_sub(1)), + ] { + let refused = Pallet::::execute_cycle(budget); + assert!(refused.consumed.all_lte(budget)); + assert!(refused.effect_consumed.is_zero() && refused.starved); + assert_eq!( + Pallet::::active_actor_state(actor_id) + .unwrap() + .encode(), + state + ); + assert_eq!(ActorReadyHead::::get(), head); + assert_eq!(frame_system::Pallet::::events(), events); + } + } + let pass = Pallet::::execute_cycle(Weight::MAX); + assert!(!pass.consumed.is_zero()); + assert!(pass.effect_consumed.is_zero() && pass.starved); + if unavailable == ScalarObservationState::Unavailable { + assert_eq!( + pass.consumed, complete_refusal, + "the complete Crossing refusal replaces provisional FIFO discovery and actor-probe accounting" + ); + } + assert_eq!( + Pallet::::active_actor_state(actor_id) + .unwrap() + .encode(), + state + ); + assert_eq!(ActorReadyHead::::get(), head); + assert_eq!( + benchmark_fixture_identity::(follower) + .unwrap() + .cycle_nonce, + 0 + ); + assert_eq!(frame_system::Pallet::::events(), events); + assert_eq!( + ::AssetOps::balance( + &before.payer, + ::FeeNativeAssetId::get() + ), + before.payer_balance + ); + assert_eq!( + ::AssetOps::balance( + &::FeeSink::get(), + ::FeeNativeAssetId::get() + ), + before.sink_balance + ); + crate::mock::set_observation(feed, available); + Pallet::::execute_cycle(Weight::MAX); + assert_user_pipeline_accounting::(actor_id, before); + assert_eq!( + benchmark_fixture_identity::(actor_id) + .unwrap() + .cycle_nonce, + 1 + ); + assert_eq!( + benchmark_fixture_identity::(follower) + .unwrap() + .cycle_nonce, + 1 + ); + assert_eq!(ActorReadyHead::::get(), ActorReadyTail::::get()); + #[cfg(feature = "try-runtime")] + Pallet::::do_try_state().unwrap(); + }); + } + } + + #[cfg(test)] + #[test] + fn crossing_rearm_unavailability_does_not_block_pipeline_insolvency_close() { + new_test_ext().execute_with(|| { + let (actor_id, before) = prepare_zero_step_user_insolvency::(true); + let feed = Pallet::::active_actor_view(actor_id) + .unwrap() + .trigger + .observation_crossing_contract() + .unwrap() + .feed + .to_owned(); + crate::mock::set_observation(feed, ScalarObservationState::Unavailable); + Pallet::::execute_cycle(Weight::MAX); + assert_user_pipeline_insolvency::(actor_id, before); + }); + } + #[cfg(test)] impl_benchmark_test_suite!(Pallet, new_test_ext(), Test, extra = false); } diff --git a/template/pallets/actors/src/crossing.rs b/template/pallets/actors/src/crossing.rs index 1a9d6dd7..1094107c 100644 --- a/template/pallets/actors/src/crossing.rs +++ b/template/pallets/actors/src/crossing.rs @@ -85,6 +85,45 @@ impl CrossingPlacedCohortAuthority { } } +#[derive(Clone, Copy)] +pub(crate) enum CrossingFireClassification { + Deferred, + Resolve, +} + +#[derive(Clone, Copy)] +enum CrossingFeedQueueDisposition { + Preserve, + ClearIfFeedEmpty, +} + +#[derive(Clone, Copy)] +pub(crate) enum CrossingRewriteDisposition { + Commit, + #[cfg(test)] + Preview, +} + +#[cfg(test)] +#[derive(Clone, Copy)] +pub(crate) enum PlacedCohortFixtureFault { + None, + MalformedLaterLocator, +} + +#[cfg(test)] +impl PlacedCohortFixtureFault { + fn malforms_later_locator(self) -> bool { + matches!(self, Self::MalformedLaterLocator) + } +} + +impl CrossingFireClassification { + fn resolves_fire(self) -> bool { + matches!(self, Self::Resolve) + } +} + struct CrossingWorkClassification { plan: CrossingWorkPlan, admitted_candidates: u32, @@ -196,8 +235,12 @@ impl Pallet { { return Err(invalid()); } - let identity = ActorIdentities::::get(member.actor_id).ok_or_else(&invalid)?; - if matches!(identity.actor_class, ActorClass::User { .. }) { + let crate::LoadedActorStateOf::Active(actor) = + Self::load_frame_actor_state(member.actor_id) + else { + return Err(invalid()); + }; + if matches!(actor.identity.actor_class, ActorClass::User { .. }) { *expected_user_feed_counts.entry(key.feed).or_insert(0u32) = expected_user_feed_counts .get(&key.feed) .copied() @@ -205,10 +248,10 @@ impl Pallet { .checked_add(1) .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 + let crossing = + Self::crossing_from_trigger(&actor.contract.trigger).ok_or_else(&invalid)?; + let TriggerRuntimeState::ObservationCrossing { phase, .. } = + actor.hot.trigger_runtime_state else { return Err(invalid()); }; @@ -257,9 +300,13 @@ impl Pallet { if actual_user_feed_counts != expected_user_feed_counts { return Err(invalid()); } - for (actor_id, hot) in ActorHot::::iter(/* deos-bypass: bounded-iter */) { - let contract = Self::load_actor_contract(actor_id).ok_or_else(&invalid)?; - let contract_crossing = Self::crossing_from_trigger(&contract.trigger); + let control_entries = Self::frame_control_entries().ok_or_else(&invalid)?; + for (actor_id, _, _) in control_entries { + let crate::LoadedActorStateOf::Active(state) = Self::load_frame_actor_state(actor_id) else { + return Err(invalid()); + }; + let hot = state.hot; + let contract_crossing = Self::crossing_from_trigger(&state.contract.trigger); let runtime_crossing = match hot.trigger_runtime_state { TriggerRuntimeState::ObservationCrossing { phase, .. } => Some(phase), TriggerRuntimeState::Stateless @@ -300,7 +347,7 @@ impl Pallet { return Err(invalid()); } for pair in queue.windows(2) { - if pair[1].revision != pair[0].revision.saturating_add(1) + if pair[0].revision.checked_add(1) != Some(pair[1].revision) || pair[1].previous != pair[0].current || pair[1].previous == pair[1].current { @@ -309,7 +356,7 @@ impl Pallet { } if queue .iter(/* deos-bypass: bounded-iter */) - .any(|transition| transition.previous == transition.current) + .any(|transition| transition.revision == 0 || transition.previous == transition.current) { return Err(invalid()); } @@ -427,13 +474,36 @@ impl Pallet { Ok(()) } + #[cfg(test)] fn insert_crossing_member( actor_id: ActorId, crossing: ObservationCrossing, phase: CrossingPhase, generation: u64, - is_user: bool, + actor_type: ActorType, + ) -> DispatchResult { + let admission_identity = Self::load_control_admission(actor_id) + .map(|certificate| certificate.admission_identity) + .unwrap_or([0; 32]); + Self::insert_crossing_member_with_admission_identity( + actor_id, + crossing, + phase, + generation, + actor_type, + admission_identity, + ) + } + + fn insert_crossing_member_with_admission_identity( + actor_id: ActorId, + crossing: ObservationCrossing, + phase: CrossingPhase, + generation: u64, + actor_type: ActorType, + admission_identity: [u8; 32], ) -> DispatchResult { + let is_user = actor_type == ActorType::User; ensure!( !CrossingMemberships::::contains_key(actor_id), Error::::CrossingIndexInvariant @@ -445,9 +515,6 @@ impl Pallet { 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 @@ -557,7 +624,12 @@ impl Pallet { ) } - fn remove_crossing_member(actor_id: ActorId, clear_feed_queue: bool) -> DispatchResult { + fn remove_crossing_member( + actor_id: ActorId, + feed_queue: CrossingFeedQueueDisposition, + actor_type: ActorType, + ) -> DispatchResult { + let is_user = actor_type == ActorType::User; let Some(locator) = CrossingMemberships::::get(actor_id) else { return Ok(()); }; @@ -641,12 +713,7 @@ impl Pallet { } state.member_count -= 1; CrossingMemberships::::remove(actor_id); - let is_user = matches!( - ActorIdentities::::get(actor_id) - .ok_or(Error::::ActorInvariant)? - .actor_class, - ActorClass::User { .. } - ); + CrossingFeedMembershipCount::::try_mutate_exists( locator.key.feed, |maybe| -> DispatchResult { @@ -675,7 +742,9 @@ impl Pallet { }, )?; } - if clear_feed_queue && CrossingFeedMembershipCount::::get(locator.key.feed) == 0 { + if matches!(feed_queue, CrossingFeedQueueDisposition::ClearIfFeedEmpty) + && CrossingFeedMembershipCount::::get(locator.key.feed) == 0 + { Self::clear_crossing_transition_queue(locator.key.feed)?; } if state.member_count == 0 { @@ -687,52 +756,116 @@ impl Pallet { Ok(()) } - pub(crate) fn remove_crossing_membership(actor_id: ActorId) -> DispatchResult { - Self::remove_crossing_member(actor_id, true) + fn remove_crossing_member_preserving_feed_queue( + actor_id: ActorId, + actor_type: ActorType, + ) -> DispatchResult { + Self::remove_crossing_member(actor_id, CrossingFeedQueueDisposition::Preserve, actor_type) + } + + pub(crate) fn remove_crossing_membership( + actor_id: ActorId, + actor_type: ActorType, + ) -> DispatchResult { + Self::remove_crossing_member( + actor_id, + CrossingFeedQueueDisposition::ClearIfFeedEmpty, + actor_type, + ) } - fn move_crossing_membership( + fn move_crossing_membership_with_authority( actor_id: ActorId, crossing: ObservationCrossing, next_phase: CrossingPhase, locator: CrossingMembershipLocator, ) -> DispatchResult { - Self::move_crossing_membership_with_hot(actor_id, crossing, next_phase, locator, true) + let (identity, _, admission) = + Self::load_control_authority_with_authority(actor_id).ok_or(Error::::ActorInvariant)?; + Self::move_crossing_membership_index_with_authority( + actor_id, + crossing, + next_phase, + locator, + identity.actor_class.actor_type(), + admission.admission_identity, + )?; + Self::try_mutate_control_hot_with_authority( + actor_id, + Error::::ActorInvariant, + |hot| -> DispatchResult { + let TriggerRuntimeState::ObservationCrossing { + installed_at_revision, + .. + } = hot.trigger_runtime_state + else { + return Err(Error::::ActorInvariant.into()); + }; + hot.trigger_runtime_state = TriggerRuntimeState::ObservationCrossing { + phase: next_phase, + installed_at_revision, + }; + Ok(()) + }, + ) } - fn move_crossing_membership_with_hot( + fn move_crossing_membership_index_with_authority( actor_id: ActorId, crossing: ObservationCrossing, next_phase: CrossingPhase, locator: CrossingMembershipLocator, - write_hot: bool, + actor_type: ActorType, + admission_identity: [u8; 32], ) -> DispatchResult { - let is_user = matches!( - ActorIdentities::::get(actor_id) - .ok_or(Error::::ActorInvariant)? - .actor_class, - ActorClass::User { .. } - ); - Self::remove_crossing_member(actor_id, false)?; - Self::insert_crossing_member(actor_id, crossing, next_phase, locator.generation, is_user)?; - if !write_hot { - return Ok(()); - } - ActorHot::::try_mutate(actor_id, |maybe_hot| -> DispatchResult { - let hot = maybe_hot.as_mut().ok_or(Error::::ActorInvariant)?; - let TriggerRuntimeState::ObservationCrossing { - installed_at_revision, - .. - } = hot.trigger_runtime_state - else { - return Err(Error::::ActorInvariant.into()); - }; - hot.trigger_runtime_state = TriggerRuntimeState::ObservationCrossing { - phase: next_phase, - installed_at_revision, - }; - Ok(()) - }) + Self::remove_crossing_member_preserving_feed_queue(actor_id, actor_type)?; + Self::insert_crossing_member_with_admission_identity( + actor_id, + crossing, + next_phase, + locator.generation, + actor_type, + admission_identity, + ) + } + + #[cfg(test)] + fn move_crossing_membership_fixture_without_hot( + actor_id: ActorId, + crossing: ObservationCrossing, + next_phase: CrossingPhase, + locator: CrossingMembershipLocator, + ) -> DispatchResult { + let actor_type = Self::load_control_identity(actor_id) + .ok_or(Error::::ActorInvariant)? + .actor_class + .actor_type(); + Self::remove_crossing_member_preserving_feed_queue(actor_id, actor_type)?; + Self::insert_crossing_member( + actor_id, + crossing, + next_phase, + locator.generation, + actor_type, + ) + } + + #[cfg(all(test, feature = "runtime-benchmarks"))] + pub(crate) fn control_move_crossing_membership_without_hot( + actor_id: ActorId, + crossing: ObservationCrossing, + next_phase: CrossingPhase, + locator: CrossingMembershipLocator, + actor_type: ActorType, + ) -> DispatchResult { + Self::remove_crossing_member_preserving_feed_queue(actor_id, actor_type)?; + Self::insert_crossing_member( + actor_id, + crossing, + next_phase, + locator.generation, + actor_type, + ) } #[cfg(test)] @@ -742,15 +875,18 @@ impl Pallet { next_phase: CrossingPhase, locator: CrossingMembershipLocator, ) -> Result { - let original_hot = ActorHot::::get(actor_id).ok_or(Error::::ActorInvariant)?; + let original_hot = Self::load_frame_control_authority(actor_id) + .map(|(_, _, hot, _)| hot) + .ok_or(Error::::ActorInvariant)?; let (expected_key, _) = Self::crossing_obligation(&crossing, next_phase); polkadot_sdk::frame_support::storage::with_transaction(|| { if let Err(error) = - Self::move_crossing_membership_with_hot(actor_id, crossing, next_phase, locator, false) + Self::move_crossing_membership_fixture_without_hot(actor_id, crossing, next_phase, locator) { return TransactionOutcome::Rollback(Err(error)); } - let preserved = ActorHot::::get(actor_id) == Some(original_hot) + let preserved = Self::load_frame_control_authority(actor_id) + .is_some_and(|(_, _, hot, _)| hot == original_hot) && CrossingMemberships::::get(actor_id).is_some_and(|moved| moved.key == expected_key); TransactionOutcome::Rollback(Ok(preserved)) }) @@ -764,12 +900,11 @@ impl Pallet { first_locator: CrossingMembershipLocator, ) -> Result { polkadot_sdk::frame_support::storage::with_transaction(|| { - if let Err(error) = Self::move_crossing_membership_with_hot( + if let Err(error) = Self::move_crossing_membership_fixture_without_hot( first, crossing, CrossingPhase::WaitingForRearm, first_locator, - false, ) { return TransactionOutcome::Rollback(Err(error)); } @@ -790,35 +925,37 @@ impl Pallet { tail_crossing: ObservationCrossing, first_locator: CrossingMembershipLocator, ) -> Result { - let first_hot = ActorHot::::get(first).ok_or(Error::::ActorInvariant)?; - let tail_hot = ActorHot::::get(tail).ok_or(Error::::ActorInvariant)?; + let first_hot = Self::load_frame_control_authority(first) + .map(|(_, _, hot, _)| hot) + .ok_or(Error::::ActorInvariant)?; + let tail_hot = Self::load_frame_control_authority(tail) + .map(|(_, _, hot, _)| hot) + .ok_or(Error::::ActorInvariant)?; polkadot_sdk::frame_support::storage::with_transaction(|| { - if let Err(error) = Self::move_crossing_membership_with_hot( + if let Err(error) = Self::move_crossing_membership_fixture_without_hot( first, first_crossing, CrossingPhase::WaitingForRearm, first_locator, - false, ) { return TransactionOutcome::Rollback(Err(error)); } let Some(derived_tail) = CrossingMemberships::::get(tail) else { return TransactionOutcome::Rollback(Err(Error::::CrossingIndexInvariant.into())); }; - if let Err(error) = Self::move_crossing_membership_with_hot( + if let Err(error) = Self::move_crossing_membership_fixture_without_hot( tail, tail_crossing, CrossingPhase::WaitingForRearm, derived_tail, - false, ) { return TransactionOutcome::Rollback(Err(error)); } let split = CrossingMemberships::::get(first) .zip(CrossingMemberships::::get(tail)) .is_some_and(|(first_moved, tail_moved)| first_moved.key != tail_moved.key) - && ActorHot::::get(first) == Some(first_hot) - && ActorHot::::get(tail) == Some(tail_hot); + && Self::load_frame_control_authority(first).is_some_and(|(_, _, hot, _)| hot == first_hot) + && Self::load_frame_control_authority(tail).is_some_and(|(_, _, hot, _)| hot == tail_hot); TransactionOutcome::Rollback(Ok(split)) }) } @@ -832,12 +969,11 @@ impl Pallet { let tail = authority.candidates[1]; let first_crossing = authority.crossings[0].clone(); let tail_crossing = authority.crossings[1].clone(); - Self::move_crossing_membership_with_hot( + Self::move_crossing_membership_with_authority( first.member.actor_id, first_crossing, CrossingPhase::WaitingForRearm, first.locator, - false, )?; let derived_tail = CrossingMemberships::::get(tail.member.actor_id) .ok_or(Error::::CrossingIndexInvariant)?; @@ -848,14 +984,13 @@ impl Pallet { && derived_tail.generation == tail.member.generation, Error::::CrossingIndexInvariant ); - Self::move_crossing_membership_with_hot( + Self::move_crossing_membership_with_authority( tail.member.actor_id, tail_crossing, CrossingPhase::WaitingForRearm, derived_tail, - false, )?; - Self::commit_paged_enqueue(authority.queue_plan); + Self::commit_paged_enqueue(authority.queue_plan).map_err(|_| Error::::ActorInvariant)?; Ok(()) } @@ -865,8 +1000,12 @@ impl Pallet { tail: ActorId, first_locator: CrossingMembershipLocator, ) -> Result { - let mut first_hot = ActorHot::::get(first).ok_or(Error::::ActorInvariant)?; - let mut tail_hot = ActorHot::::get(tail).ok_or(Error::::ActorInvariant)?; + let mut first_hot = Self::load_frame_control_authority(first) + .map(|(_, _, hot, _)| hot) + .ok_or(Error::::ActorInvariant)?; + let mut tail_hot = Self::load_frame_control_authority(tail) + .map(|(_, _, hot, _)| hot) + .ok_or(Error::::ActorInvariant)?; for hot in [&mut first_hot, &mut tail_hot] { let TriggerRuntimeState::ObservationCrossing { installed_at_revision, @@ -940,7 +1079,7 @@ impl Pallet { return TransactionOutcome::Rollback(Err(error)); } let committed = [first, tail].into_iter().all(|actor_id| { - ActorHot::::get(actor_id).is_some_and(|hot| { + Self::load_frame_control_authority(actor_id).is_some_and(|(_, _, hot, _)| { hot.pending_signal && hot.queue_ticket.is_some() && matches!( @@ -1153,7 +1292,7 @@ impl Pallet { contract: ActorContractOf, _transition: CrossingTransitionObligation, ) -> Result<(CrossingWorkPlan, bool, Option>), DispatchError> { - let mut loaded = match Self::load_actor_state(actor_id) { + let mut loaded = match Self::load_actor_state_with_authority(actor_id) { LoadedActorStateOf::Active(state) => state, _ => return Err(Error::::ActorInvariant.into()), }; @@ -1174,7 +1313,7 @@ impl Pallet { }; let activation = Self::preflight_activation_loaded(actor_id, loaded) .map_err(|_| Error::::ActorInvariant)?; - if activation.terminal_reason.is_some() || NextQueueTicket::::get() == u64::MAX { + if activation.terminal_reason.is_some() || ActorReadyTail::::get() == u64::MAX { Ok((CrossingWorkPlan::FireCohortClosed, false, None)) } else if activation.already_pending { Ok((CrossingWorkPlan::FireCohortCoalesced, false, None)) @@ -1197,7 +1336,7 @@ impl Pallet { fn classify_crossing_candidate( member: CrossingMember, transition: CrossingTransitionObligation, - classify_fire: bool, + fire_classification: CrossingFireClassification, ) -> Result<(CrossingWorkPlan, bool, Option>), DispatchError> { if IndexedTriggerDetectionDisabled::::contains_key(member.actor_id) { return Ok(( @@ -1206,7 +1345,12 @@ impl Pallet { None, )); } - let hot = ActorHot::::get(member.actor_id).ok_or(Error::::ActorInvariant)?; + let LoadedActorStateOf::Active(loaded) = Self::load_actor_state_with_authority(member.actor_id) + else { + return Err(Error::::ActorInvariant.into()); + }; + let hot = loaded.hot; + let contract = loaded.contract; let TriggerRuntimeState::ObservationCrossing { phase, installed_at_revision, @@ -1221,8 +1365,6 @@ impl Pallet { None, )); } - let contract = - 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); @@ -1238,18 +1380,18 @@ impl Pallet { if transition_kind == CrossingTransition::Rearm { return Ok((CrossingWorkPlan::RearmCohort, false, None)); } - if !classify_fire { + if !fire_classification.resolves_fire() { return Ok((CrossingWorkPlan::FireCohortPending, false, None)); } Self::classify_fire_activation(member.actor_id, hot, contract, transition) } fn classify_crossing_idle_candidate( - authority: &CrossingCandidateAuthority, + candidate: &CrossingCandidateAuthority, transition: CrossingTransitionObligation, - classify_fire: bool, + fire_classification: CrossingFireClassification, ) -> Result>)>, DispatchError> { - let member = authority.member; + let member = candidate.member; if IndexedTriggerDetectionDisabled::::contains_key(member.actor_id) { return Ok(Some(( CrossingWorkPlan::SkipPostInstallationTransition, @@ -1257,8 +1399,8 @@ impl Pallet { None, ))); } - if authority.locator.generation != member.generation - || authority.locator.key.traversal + if candidate.locator.generation != member.generation + || candidate.locator.key.traversal != if transition.current > transition.previous { CrossingTraversal::Upward } else { @@ -1267,9 +1409,10 @@ impl Pallet { { return Err(Error::::CrossingIndexInvariant.into()); } - let Some(state) = - Self::load_crossing_idle_activation_state(member.actor_id, authority.locator.key.feed) - else { + let Some(state) = Self::load_crossing_idle_activation_state_with_authority( + member.actor_id, + candidate.locator.key.feed, + ) else { return Ok(None); }; if member.admission_identity != state.authority.admission_identity { @@ -1292,12 +1435,12 @@ impl Pallet { if phase == CrossingPhase::WaitingForRearm { return Ok(Some((CrossingWorkPlan::RearmCohort, false, None))); } - if !classify_fire { + if !fire_classification.resolves_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 { + if classification.terminal_reason.is_some() || ActorReadyTail::::get() == u64::MAX { return Ok(Some((CrossingWorkPlan::FireCohortClosed, false, None))); } if state.hot.pending_signal { @@ -1322,7 +1465,7 @@ impl Pallet { pub(crate) fn preflight_crossing_cohort( snapshot: &CrossingCohortSnapshot, transition: CrossingTransitionObligation, - classify_fire: bool, + fire_classification: CrossingFireClassification, expected_plan: Option, ) -> Result, DispatchError> { let mut plan = expected_plan; @@ -1331,7 +1474,8 @@ impl Pallet { let mut admitted_candidates = 0u32; for authority in snapshot.candidates.iter(/* deos-bypass: bounded-iter */) { let (candidate_immediate_fifo, queue_hot, truncate_after_candidate) = { - let compact = Self::classify_crossing_idle_candidate(authority, transition, classify_fire)?; + let compact = + Self::classify_crossing_idle_candidate(authority, transition, fire_classification)?; if compact.is_none() && admitted_candidates > 0 { break; } @@ -1339,7 +1483,7 @@ impl Pallet { let (candidate_plan, immediate_fifo, queue_hot) = if let Some(compact) = compact { compact } else { - Self::classify_crossing_candidate(authority.member, transition, classify_fire)? + Self::classify_crossing_candidate(authority.member, transition, fire_classification)? }; if let Some(expected) = plan { if candidate_plan != expected { @@ -1403,7 +1547,7 @@ impl Pallet { .try_push(crossing) .map_err(|_| Error::::CrossingIndexInvariant)?; } - let queue_plan = Self::preflight_paged_enqueue_cohort(queue_candidates) + let queue_plan = Self::preflight_paged_enqueue_cohort_with_authority(queue_candidates) .map_err(|_| Error::::CrossingIndexInvariant)?; let authority = CrossingPlacedCohortAuthority { feed, @@ -1480,13 +1624,32 @@ impl Pallet { generation: candidate.member.generation, }, ); + Self::try_mutate_control_hot_with_authority( + candidate.member.actor_id, + Error::::ActorInvariant, + |hot| -> DispatchResult { + let TriggerRuntimeState::ObservationCrossing { + installed_at_revision, + .. + } = hot.trigger_runtime_state + else { + return Err(Error::::ActorInvariant.into()); + }; + hot.trigger_runtime_state = TriggerRuntimeState::ObservationCrossing { + phase: CrossingPhase::WaitingForRearm, + installed_at_revision, + }; + Ok(()) + }, + )?; state.member_count = state .member_count .checked_add(1) .ok_or(Error::::CrossingIndexCapacityExceeded)?; user_count = user_count.saturating_add(u32::from(matches!( - ActorIdentities::::get(candidate.member.actor_id) + Self::load_control_authority_with_authority(candidate.member.actor_id,) .ok_or(Error::::ActorInvariant)? + .0 .actor_class, ActorClass::User { .. } ))); @@ -1539,7 +1702,7 @@ impl Pallet { .map_err(|_| Error::::CrossingIndexInvariant)?, }; ensure!( - Self::rewrite_non_tail_source(&source, &tail_refill, true)?, + Self::rewrite_non_tail_source(&source, &tail_refill, CrossingRewriteDisposition::Commit,)?, Error::::CrossingIndexInvariant ); let count = authority.candidates.len() as u32; @@ -1562,9 +1725,12 @@ impl Pallet { .iter(/* deos-bypass: bounded-iter */) .try_fold(0u32, |users, candidate| -> Result { let is_user = matches!( - ActorIdentities::::get(candidate.member.actor_id) - .ok_or(Error::::ActorInvariant)? - .actor_class, + Self::load_control_authority_with_authority( + candidate.member.actor_id, + ) + .ok_or(Error::::ActorInvariant)? + .0 + .actor_class, ActorClass::User { .. } ); Ok(users.saturating_add(u32::from(is_user))) @@ -1576,7 +1742,7 @@ impl Pallet { Ok(()) })?; Self::insert_crossing_destination_cohort(&authority.candidates, &authority.crossings)?; - Self::commit_paged_enqueue(authority.queue_plan); + Self::commit_paged_enqueue(authority.queue_plan).map_err(|_| Error::::ActorInvariant)?; Ok(()) } @@ -1620,12 +1786,11 @@ impl Pallet { locator == candidate.locator, Error::::CrossingIndexInvariant ); - Self::move_crossing_membership_with_hot( + Self::move_crossing_membership_with_authority( candidate.member.actor_id, crossing.clone(), CrossingPhase::WaitingForRearm, locator, - false, )?; } if !remainder.entries.is_empty() { @@ -1646,7 +1811,7 @@ impl Pallet { } CrossingMemberPages::::insert(key, authority.cursor.page, remainder); } - Self::commit_paged_enqueue(authority.queue_plan); + Self::commit_paged_enqueue(authority.queue_plan).map_err(|_| Error::::ActorInvariant)?; Ok(()) } @@ -1654,12 +1819,12 @@ impl Pallet { pub(crate) fn test_placed_cohort_authority_count( snapshot: &CrossingCohortSnapshot, transition: CrossingTransitionObligation, - malform_later_locator: bool, + fixture_fault: PlacedCohortFixtureFault, ) -> Result { let preflight = Self::preflight_crossing_cohort( snapshot, transition, - true, + CrossingFireClassification::Resolve, Some(CrossingWorkPlan::FireCohortPlaced), )?; ensure!( @@ -1689,7 +1854,7 @@ impl Pallet { .map(|candidate| candidate.member.actor_id); Self::test_reset_queue_append_commits(); polkadot_sdk::frame_support::storage::with_transaction(|| { - if malform_later_locator { + if fixture_fault.malforms_later_locator() { let Some(actor_id) = malformed_actor else { return TransactionOutcome::Rollback(Err(Error::::CrossingIndexInvariant.into())); }; @@ -1700,11 +1865,13 @@ impl Pallet { }); } match Self::commit_tail_page_placed_cohort_authority(authority) { - Ok(()) if Self::test_queue_append_commits() == 1 && !malform_later_locator => { + Ok(()) + if Self::test_queue_append_commits() == 1 && !fixture_fault.malforms_later_locator() => + { TransactionOutcome::Rollback(Ok(count)) } Ok(()) => TransactionOutcome::Rollback(Err(Error::::CrossingIndexInvariant.into())), - Err(_) if malform_later_locator => TransactionOutcome::Rollback(Ok(count)), + Err(_) if fixture_fault.malforms_later_locator() => TransactionOutcome::Rollback(Ok(count)), Err(error) => TransactionOutcome::Rollback(Err(error)), } }) @@ -1713,7 +1880,7 @@ impl Pallet { pub(crate) fn rewrite_non_tail_source( source: &CrossingCohortSnapshot, tail_refill: &CrossingCohortSnapshot, - commit: bool, + disposition: CrossingRewriteDisposition, ) -> Result { ensure!( source.key == tail_refill.key @@ -1805,7 +1972,9 @@ impl Pallet { Ok(()) })(); match result { - Ok(()) if commit => TransactionOutcome::Commit(Ok(true)), + Ok(()) if matches!(disposition, CrossingRewriteDisposition::Commit) => { + TransactionOutcome::Commit(Ok(true)) + } Ok(()) => TransactionOutcome::Rollback(Ok(true)), Err(error) => TransactionOutcome::Rollback(Err(error)), } @@ -1827,7 +1996,7 @@ impl Pallet { let preflight = Self::preflight_crossing_cohort( source, transition, - true, + CrossingFireClassification::Resolve, Some(CrossingWorkPlan::FireCohortPlaced), )?; ensure!( @@ -1884,17 +2053,19 @@ impl Pallet { .all(|candidate| { CrossingMemberships::::get(candidate.member.actor_id) .is_some_and(|locator| locator.key != source.key) - && ActorHot::::get(candidate.member.actor_id).is_some_and(|hot| { - hot.pending_signal - && hot.queue_ticket.is_some() - && matches!( - hot.trigger_runtime_state, - TriggerRuntimeState::ObservationCrossing { - phase: CrossingPhase::WaitingForRearm, - .. - } - ) - }) + && Self::load_frame_control_authority(candidate.member.actor_id).is_some_and( + |(_, _, hot, _)| { + hot.pending_signal + && hot.queue_ticket.is_some() + && matches!( + hot.trigger_runtime_state, + TriggerRuntimeState::ObservationCrossing { + phase: CrossingPhase::WaitingForRearm, + .. + } + ) + }, + ) }) => { TransactionOutcome::Rollback(Ok(count)) @@ -1906,7 +2077,7 @@ impl Pallet { } fn do_classify_crossing_work( - classify_fire: bool, + fire_classification: CrossingFireClassification, max_candidates: u32, ) -> Result { let list = CrossingPendingFeedListState::::get(); @@ -1981,7 +2152,7 @@ impl Pallet { Error::::CrossingIndexInvariant ); let first_preflight = - Self::preflight_crossing_cohort(&snapshot, transition, classify_fire, None)?; + Self::preflight_crossing_cohort(&snapshot, transition, fire_classification, None)?; ensure!( first_preflight.admitted_candidates == 1, Error::::CrossingIndexInvariant @@ -1993,7 +2164,7 @@ impl Pallet { if !pair_shape { return Ok(CrossingWorkPlan::SkipPostInstallationTransition); } - if !classify_fire { + if !fire_classification.resolves_fire() { return Ok(CrossingWorkPlan::SkipPostInstallationPairPending); } let second_offset = page.entries.len().saturating_sub(1) as u32; @@ -2002,7 +2173,7 @@ impl Pallet { let second_preflight = Self::preflight_crossing_cohort( &second_snapshot, transition, - classify_fire, + fire_classification, Some(first_plan), )?; return Ok(if second_preflight.admitted_candidates == 1 { @@ -2017,7 +2188,7 @@ impl Pallet { if !pair_shape { return Ok(CrossingWorkPlan::RearmCohort); } - if !classify_fire { + if !fire_classification.resolves_fire() { return Ok(CrossingWorkPlan::RearmCohortPairPending); } let second_offset = page.entries.len().saturating_sub(1) as u32; @@ -2026,7 +2197,7 @@ impl Pallet { let second_preflight = Self::preflight_crossing_cohort( &second_snapshot, transition, - classify_fire, + fire_classification, Some(first_plan), )?; return Ok(if second_preflight.admitted_candidates == 1 { @@ -2035,7 +2206,7 @@ impl Pallet { CrossingWorkPlan::RearmCohort }); } - if !classify_fire { + if !fire_classification.resolves_fire() { ensure!( first_plan == CrossingWorkPlan::FireCohortPending, Error::::CrossingIndexInvariant @@ -2064,7 +2235,7 @@ impl Pallet { let second_preflight = Self::preflight_crossing_cohort( &second_snapshot, transition, - classify_fire, + fire_classification, Some(first_plan), )?; if second_preflight.admitted_candidates == 0 { @@ -2237,7 +2408,7 @@ impl Pallet { } fn classify_crossing_work_with_limit(max_candidates: u32) -> CrossingWorkClassification { - let plan = Self::do_classify_crossing_work(true, max_candidates) + let plan = Self::do_classify_crossing_work(CrossingFireClassification::Resolve, max_candidates) .unwrap_or(CrossingWorkPlan::StructuralFault); let mut classified = Self::crossing_work_classification(plan); let source_is_non_tail = CrossingPendingFeedListState::::get() @@ -2276,8 +2447,9 @@ impl Pallet { fn classify_crossing_work_preflight_with_limit( max_candidates: u32, ) -> CrossingWorkClassification { - let plan = Self::do_classify_crossing_work(false, max_candidates) - .unwrap_or(CrossingWorkPlan::StructuralFault); + let plan = + Self::do_classify_crossing_work(CrossingFireClassification::Deferred, max_candidates) + .unwrap_or(CrossingWorkPlan::StructuralFault); let mut classified = Self::crossing_work_classification(plan); if max_candidates > 2 { if let Some(feed) = CrossingPendingFeedListState::::get().cursor { @@ -2321,7 +2493,8 @@ impl Pallet { ) -> Result>, DispatchError> { use crate::weights::WeightInfo as _; - let identity = ActorIdentities::::get(actor_id).ok_or(Error::::ActorInvariant)?; + let (identity, _, _) = + Self::load_control_authority_with_authority(actor_id).ok_or(Error::::ActorInvariant)?; let actor_type = identity.actor_class.actor_type(); let breakdown = Self::trigger_fee_for_weight( actor_type, @@ -2472,7 +2645,13 @@ impl Pallet { 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 LoadedActorStateOf::Active(loaded) = + Self::load_actor_state_for_frame_control(member.actor_id) + else { + return Err(Error::::ActorInvariant.into()); + }; + let hot = loaded.hot; + let contract = loaded.contract; let TriggerRuntimeState::ObservationCrossing { phase, installed_at_revision, @@ -2489,8 +2668,6 @@ impl Pallet { Self::advance_crossing_pending_feed(feed)?; return Ok(CrossingWorkOutcome::new(true, 1, 1, 1, 1)); } - let contract = - 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); @@ -2513,7 +2690,7 @@ impl Pallet { } else { None }; - Self::move_crossing_membership(member.actor_id, crossing, next_phase, locator)?; + Self::move_crossing_membership_with_authority(member.actor_id, crossing, next_phase, locator)?; let mut activation = None; if let Some(fire_plan) = fire_plan { if fire_plan == CrossingWorkPlan::FireCohortClosed { @@ -2678,13 +2855,13 @@ impl Pallet { let first_preflight = Self::preflight_crossing_cohort( &first, transition, - true, + CrossingFireClassification::Resolve, Some(CrossingWorkPlan::FireCohortPlaced), )?; let tail_preflight = Self::preflight_crossing_cohort( &tail, transition, - true, + CrossingFireClassification::Resolve, Some(CrossingWorkPlan::FireCohortPlaced), )?; ensure!( @@ -2739,7 +2916,7 @@ impl Pallet { let preflight = Self::preflight_crossing_cohort( &snapshot, transition, - true, + CrossingFireClassification::Resolve, Some(CrossingWorkPlan::FireCohortPlaced), )?; if preflight.admitted_candidates == count @@ -3147,17 +3324,25 @@ impl Pallet { (meter.consumed(), counters) } - pub(crate) fn rearm_disabled_crossing_after_opening( + pub(crate) fn prepare_crossing_rearm_hot( actor_id: ActorId, - trigger: &TriggerOf, - ) -> DispatchResult { + instance: &ActiveActorViewOf, + admission: &ActorAdmissionCertificateOf, + opening_observation: Option, + ) -> Result>, DispatchError> { if !IndexedTriggerDetectionDisabled::::contains_key(actor_id) { - return Ok(()); + return Ok(None); } - let crossing = Self::crossing_from_trigger(trigger).ok_or(Error::::ActorInvariant)?; + let crossing = + Self::crossing_from_trigger(&instance.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) { + let TriggerRuntimeState::ObservationCrossing { .. } = &instance.trigger_runtime_state else { + return Err(Error::::ActorInvariant.into()); + }; + let current_observation = + opening_observation.unwrap_or_else(|| T::ObservationProvider::current(&crossing.feed)); + let (current, installed_at_revision) = match current_observation { crate::CanonicalObservationState::Available { value, revision } => (value, revision), crate::CanonicalObservationState::Unavailable => { return Err(Error::::ObservationUnavailable.into()); @@ -3167,20 +3352,38 @@ impl Pallet { } }; 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 { + let actor_type = instance.actor_class.actor_type(); + Self::remove_crossing_member_preserving_feed_queue(actor_id, actor_type)?; + Self::insert_crossing_member_with_admission_identity( + actor_id, + crossing, + phase, + locator.generation, + actor_type, + admission.admission_identity, + )?; + IndexedTriggerDetectionDisabled::::remove(actor_id); + Ok(Some(ActorHotState { + lifecycle: instance.lifecycle, + cycle_state: instance.cycle_state, + trigger_runtime_state: TriggerRuntimeState::ObservationCrossing { phase, installed_at_revision, - }; - Ok(()) - })?; - IndexedTriggerDetectionDisabled::::remove(actor_id); - Ok(()) - } - - pub(crate) fn preflight_crossing_membership( + }, + unsuccessful_attempt_streak: instance.unsuccessful_attempt_streak, + pending_signal: false, + queue_ticket: None, + wakeup_pointer: instance.wakeup_pointer, + trigger_wakeup_pointer: instance.trigger_wakeup_pointer, + terminal_at: instance + .window + .map(|window| Self::window_terminal_at(&window)), + schedule_anchor: instance.schedule_anchor, + last_cycle_block: instance.last_cycle_block, + })) + } + + pub(crate) fn preflight_crossing_membership_with_authority( actor_id: ActorId, trigger: &TriggerOf, ) -> Result, DispatchError> { @@ -3199,8 +3402,13 @@ impl Pallet { crossing.has_valid_hysteresis(), Error::::InvalidTriggerConfiguration ); - 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 preserved_hot = { + match Self::load_actor_state_with_authority(actor_id) { + LoadedActorStateOf::Active(state) if state.contract.trigger == *trigger => Some(state.hot), + _ => None, + } + }; + if let Some(hot) = preserved_hot { let TriggerRuntimeState::ObservationCrossing { phase, installed_at_revision, @@ -3237,11 +3445,12 @@ impl Pallet { pub(crate) fn commit_crossing_membership( actor_id: ActorId, transition: CrossingMembershipTransition, - is_user: bool, + actor_type: ActorType, + admission_identity: [u8; 32], ) -> Result, DispatchError> { match transition { CrossingMembershipTransition::Remove => { - Self::remove_crossing_membership(actor_id)?; + Self::remove_crossing_membership(actor_id, actor_type)?; Ok(None) } CrossingMembershipTransition::Preserve { @@ -3254,8 +3463,15 @@ impl Pallet { generation, installed_at_revision, } => { - Self::remove_crossing_membership(actor_id)?; - Self::insert_crossing_member(actor_id, crossing, phase, generation, is_user)?; + Self::remove_crossing_membership(actor_id, actor_type)?; + Self::insert_crossing_member_with_admission_identity( + actor_id, + crossing, + phase, + generation, + actor_type, + admission_identity, + )?; Ok(Some((phase, installed_at_revision))) } } diff --git a/template/pallets/actors/src/execution.rs b/template/pallets/actors/src/execution.rs index f1efacae..940bdf78 100644 --- a/template/pallets/actors/src/execution.rs +++ b/template/pallets/actors/src/execution.rs @@ -1,10 +1,11 @@ use super::pallet::*; use super::types::{InputLimit, Task as ActorTask}; use super::{ - AssetOps, DexOps, FeeChargeKind, FeeCollector, LiquidityOps, ObservationProvider as _, + AssetOps, DexOps, FeeAssetClass, FeeCollector, LiquidityOps, ObservationProvider as _, RetryClass, ScalarObservationState, StakingOps, TaskEffectExecution, TaskFailure, - WeightInfo as _, fee_native_protected_minimum, settle_attempt_fee_step, + WeightInfo as _, fee_native_protected_minimum, }; +use crate::scheduler::AttemptTransactionError; use frame::prelude::*; use polkadot_sdk::sp_runtime::{ Perbill, @@ -131,14 +132,6 @@ enum PreparedTaskOutcome { FundingUnavailable, } -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -enum StepControl { - Advance, - CompleteCycle, - Terminate, - SuspendCurrent, -} - pub(crate) fn evaluate_precondition_with( precondition: &Precondition, mut evaluate: Evaluate, @@ -172,48 +165,56 @@ where Ok(expression_passes) } -fn resolve_step_control(outcome: &StepOutcome, error_policy: StepErrorPolicy) -> StepControl { - match (outcome, error_policy) { - (StepOutcome::Executed | StepOutcome::Skipped(_), _) => StepControl::Advance, - (StepOutcome::Stopped, _) => StepControl::CompleteCycle, - (StepOutcome::FundingUnavailable, StepErrorPolicy::RetryLater { .. }) => { - StepControl::SuspendCurrent - } - (StepOutcome::FundingUnavailable, _) => StepControl::Advance, - (StepOutcome::Failed(_), StepErrorPolicy::ContinueNextStep) => StepControl::Advance, - ( - StepOutcome::Failed(TaskFailure { - retry: RetryClass::Temporary, - .. - }), - StepErrorPolicy::RetryLater { .. }, - ) => StepControl::SuspendCurrent, - (StepOutcome::Failed(_), StepErrorPolicy::AbortCycle | StepErrorPolicy::RetryLater { .. }) => { - StepControl::Terminate - } +#[derive(Clone, Copy)] +enum LoadedFundingDisposition { + Preserve, + Clear, +} + +impl LoadedFundingDisposition { + fn clears(self) -> bool { + matches!(self, Self::Clear) } } -pub(crate) struct AttemptExecution { - weight: Weight, - disposition: AttemptDisposition, - fee_collection_failed: bool, - effect_execution: TaskEffectExecution, - outcomes: OutcomeTotals, +pub(crate) enum LoadedCancellationContext { + RetainedFrame { + admission: ActorAdmissionCertificateOf, + state: ActiveActorStateOf, + }, + ConsumedFrame { + admission: ActorAdmissionCertificateOf, + state: ActiveActorStateOf, + }, +} + +impl LoadedCancellationContext { + pub(crate) fn admission(&self) -> &ActorAdmissionCertificateOf { + match self { + Self::RetainedFrame { admission, .. } | Self::ConsumedFrame { admission, .. } => admission, + } + } } impl Pallet { - 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(()) - }) + #[cfg(any(test, feature = "runtime-benchmarks"))] + fn commit_cycle_nonce_from_identity( + actor_id: ActorId, + identity: &ActorIdentityOf, + cycle_nonce: u64, + ) -> DispatchResult { + ensure!( + identity.cycle_nonce.checked_add(1) == Some(cycle_nonce), + Error::::ActorRunInvariant + ); + let mut updated = identity.clone(); + updated.cycle_nonce = cycle_nonce; + let frame_primary_exists = ActorControlLocators::::contains_key(actor_id); + if frame_primary_exists { + Self::update_existing_frame_control_identity(actor_id, &updated) + .map_err(|_| Error::::ActorRunInvariant)?; + } + Ok(()) } pub(crate) fn cancel_run_internal( @@ -221,30 +222,95 @@ impl Pallet { reason: CancellationReason, outcomes: Option, ) -> Result { - let Some(run_state) = ActorRunStateStore::::get(actor_id) else { + let (state, admission, _) = + Self::load_frame_actor_service_state(actor_id).ok_or(Error::::ActorRunInvariant)?; + let identity = state.identity.clone(); + Self::cancel_run_internal_loaded( + actor_id, + &identity, + reason, + outcomes, + LoadedCancellationContext::RetainedFrame { admission, state }, + ) + } + + pub(crate) fn cancel_run_internal_loaded( + actor_id: ActorId, + identity: &ActorIdentityOf, + reason: CancellationReason, + outcomes: Option, + context: LoadedCancellationContext, + ) -> Result { + let (admission, mut state, consumed) = match context { + LoadedCancellationContext::RetainedFrame { admission, state } => (admission, state, false), + LoadedCancellationContext::ConsumedFrame { admission, state } => (admission, state, true), + }; + let Some(run_state) = state.run_state.clone() else { + ensure!( + state.hot.cycle_state == CycleState::Idle, + Error::::ActorRunInvariant + ); return Ok(false); }; - let identity = ActorIdentities::::get(actor_id).ok_or(Error::::ActorRunInvariant)?; ensure!( - identity.cycle_nonce.checked_add(1) == Some(run_state.cycle_nonce), + state.identity == *identity + && (identity.cycle_nonce == run_state.cycle_nonce + || identity.cycle_nonce.checked_add(1) == Some(run_state.cycle_nonce)) + && matches!( + state.hot.cycle_state, + CycleState::Running | CycleState::Suspended + ) + && (!consumed || !ActorControlLocators::::contains_key(actor_id)), 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::::ActorRunInvariant)?; - ensure!( - matches!(hot.cycle_state, CycleState::Running | CycleState::Suspended), - Error::::ActorRunInvariant - ); - hot.cycle_state = CycleState::Idle; - hot.queue_ticket = None; - hot.wakeup_pointer = None; - Ok(()) - })?; - Self::commit_cycle_nonce(actor_id, run_state.cycle_nonce)?; + if state.hot.wakeup_pointer.is_some() { + Self::wakeup_substrate_invalidate_loaded(actor_id, state.clone(), &admission) + .map_err(|_| Error::::ActorRunInvariant)?; + } + if ActorControlLocators::::contains_key(actor_id) { + Self::remove_primary_control_cell_inner(actor_id) + .map_err(|_| Error::::ActorRunInvariant)?; + } + state.hot.cycle_state = CycleState::Idle; + state.hot.queue_ticket = None; + state.hot.wakeup_pointer = None; + state.identity.cycle_nonce = run_state.cycle_nonce; ActorRunStateStore::::remove(actor_id); - Self::reconcile_actor_state_hold(actor_id)?; - let totals = outcomes.unwrap_or(run_state.cumulative_outcomes); + if !matches!(reason, CancellationReason::Closing(_)) { + let resources = if state.contract.steps.is_empty() { + ActorStepResourceEnvelope { + control: T::WeightInfo::scheduler_inner_zero_step_complete(), + effect: Weight::zero(), + } + } else { + Self::load_current_step_with_admission(actor_id, 0, &admission) + .map(|loaded| loaded.resources) + .ok_or(Error::::ActorRunInvariant)? + }; + if state.hot.pending_signal && !state.hot.lifecycle.is_paused() { + let plan = Self::preflight_paged_enqueue_authority( + actor_id, + state.hot, + &state.identity, + None, + &admission, + resources, + ) + .map_err(Self::placement_error)?; + Self::commit_paged_enqueue(plan).map_err(Self::placement_error)?; + } else { + Self::restore_unsignaled_from_authority( + actor_id, + state.hot, + &state.identity, + None, + &admission, + resources, + ) + .map_err(|_| Error::::ActorRunInvariant)?; + } + Self::reconcile_actor_state_hold_with_authority(actor_id)?; + } Self::deposit_event(Event::CycleCancelled { actor_id, cycle_nonce: run_state.cycle_nonce, @@ -254,11 +320,12 @@ impl Pallet { actor_id, cycle_nonce: run_state.cycle_nonce, result: CycleResult::Cancelled, - outcomes: totals, + outcomes: outcomes.unwrap_or(run_state.cumulative_outcomes), }); Ok(true) } + #[cfg(any(test, feature = "runtime-benchmarks"))] pub(crate) fn write_run_state( actor_id: ActorId, state: Option>, @@ -269,6 +336,10 @@ impl Pallet { let identity = loaded.identity; let contract = loaded.contract; let existing_run = loaded.run_state; + let step_count = ActorContractHeads::::get(actor_id) + .ok_or(Error::::ActorRunInvariant)? + .header + .step_count; let target_step = state .as_ref() .and_then(|run| Self::load_current_step_from_storage(actor_id, run.cursor)); @@ -285,7 +356,7 @@ impl Pallet { && 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.cursor < step_count && run_state.has_contract_authority( admission.semantic_contract_id, admission.body_commitment, @@ -339,26 +410,104 @@ impl Pallet { CycleState::Running } }); + ensure!( + identity.actor_class.actor_type() == ActorType::System + || ActorControlLocators::::contains_key(actor_id), + Error::::StateHoldInvariant + ); polkadot_sdk::frame_support::storage::with_transaction(|| { if state.is_none() - && let Err(error) = Self::commit_cycle_nonce(actor_id, expected_cycle_nonce) + && let Err(error) = + Self::commit_cycle_nonce_from_identity(actor_id, &identity, 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 = - next_cycle_state.unwrap_or(CycleState::Idle); - Ok(()) - }); + let hot_update = Self::try_mutate_control_hot_with_authority( + actor_id, + Error::::ActorNotFound, + |hot| -> DispatchResult { + hot.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)); } + let (location, mut cell) = match Self::load_primary_control_cell(actor_id) { + Ok(primary) => primary, + Err(_) => { + return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err( + Error::::ActorRunInvariant.into(), + )); + } + }; + cell.cursor = state.as_ref().map_or(0, |run| run.cursor); + let Some(step) = Self::load_current_step_with_admission(actor_id, cell.cursor, &admission) + else { + return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err( + Error::::ActorRunInvariant.into(), + )); + }; + cell.resources = step.resources; + let mut placement_hot = loaded.hot.clone(); + placement_hot.cycle_state = next_cycle_state.unwrap_or(CycleState::Idle); + let placement_run = state.as_ref().map(|run| (run.cursor, run.eligible_at)); + if let Some(run) = state.as_ref() + && matches!(location, ActorControlLocation::Ready { .. }) + { + cell.eligible_at = Some(run.eligible_at); + } + let primary_publication = if placement_run.is_none() { + Self::remove_primary_control_cell_inner(actor_id).and_then(|_| { + if let ActorControlLocation::Waiting { key, .. } = location { + match key.clock() { + WakeupClock::Block => cell.hot.wakeup_pointer = None, + WakeupClock::Tick => cell.hot.trigger_wakeup_pointer = None, + } + } + if cell.hot.pending_signal && !cell.hot.lifecycle.is_paused() { + cell.eligible_at = Some(frame_system::Pallet::::block_number()); + Self::control_append_ready(cell).map(|_| ()) + } else { + cell.eligible_at = None; + ActorUnsignaledControlCells::::insert(actor_id, cell); + ActorControlLocators::::insert(actor_id, ActorControlLocation::Unsignaled); + Ok(()) + } + }) + } else { + Self::store_primary_control_cell(location, cell) + }; + if primary_publication.is_err() { + return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err( + Error::::ActorRunInvariant.into(), + )); + } if let Some(run_state) = state { ActorRunStateStore::::insert(actor_id, run_state); } else { ActorRunStateStore::::remove(actor_id); } - if let Err(error) = Self::reconcile_actor_state_hold(actor_id) { + if let Some((cursor, eligible_at)) = placement_run { + if Self::try_wakeup_substrate_schedule_transition_with_authority( + actor_id, + WakeupKey::Block(eligible_at), + placement_hot, + &identity, + cursor, + &admission, + step.resources, + ) + .is_err() + { + return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err( + Error::::ActorRunInvariant.into(), + )); + } + } + if identity.actor_class.actor_type() == ActorType::User + && let Err(error) = Self::reconcile_actor_state_hold_with_authority(actor_id) + { return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)); } polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(())) @@ -394,967 +543,1301 @@ impl Pallet { Ok(()) } - 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 { + pub(crate) fn record_stop_cycle_event(actor_id: ActorId, cycle_nonce: u64, step_index: u32) { + Self::deposit_event(Event::CycleStopped { actor_id, cycle_nonce, - cursor, - reason, - cumulative_outcomes, + step_index, }); - Ok(()) } - pub(crate) fn begin_run_attempt( + fn commit_loaded_single_step_suspension( actor_id: ActorId, - now: BlockNumberFor, - ) -> Option> { - let updated = ActorRunStateStore::::mutate(actor_id, |maybe| { - let Some(run_state) = maybe.as_mut() else { - return false; - }; - run_state.last_attempt_block = now; - true - }); - if !updated { - return None; + mut plan: CurrentStepPlanOf, + run: ActorRunStateOf, + unsuccessful_attempt_streak: u32, + funding_disposition: LoadedFundingDisposition, + effect_execution: TaskEffectExecution, + ) -> Result< + ( + CurrentStepPlanOf, + TaskEffectExecution, + AttemptDisposition, + OutcomeTotals, + Option>, + ), + AttemptTransactionError, + > { + if !run.suspension_is_coherent() { + return Err(AttemptTransactionError::Invariant); } - let run_state = ActorRunStateStore::::get(actor_id)?; - Self::deposit_event(Event::CycleContinued { - actor_id, - cycle_nonce: run_state.cycle_nonce, - cursor: run_state.cursor, - }); - Some(run_state) - } - - pub(crate) fn record_stop_cycle_event(actor_id: ActorId, cycle_nonce: u64, step_index: u32) { - Self::deposit_event(Event::CycleStopped { + let cycle_nonce = run.cycle_nonce; + let cursor = run.cursor; + let eligible_at = run.eligible_at; + let outcomes = run.cumulative_outcomes; + let reason = run.suspension.ok_or(AttemptTransactionError::Invariant)?; + let deferred_signal = plan.hot.cycle_state == CycleState::Suspended && plan.hot.pending_signal; + plan.hot.cycle_state = CycleState::Suspended; + plan.hot.unsuccessful_attempt_streak = unsuccessful_attempt_streak; + plan.hot.pending_signal = deferred_signal; + plan.hot.queue_ticket = None; + if funding_disposition.clears() { + plan.funding.funding_accumulated.clear(); + ActorFunding::::insert(actor_id, &plan.funding); + } + plan.last_step_outcome = run.last_step_outcome.clone(); + ActorRunStateStore::::insert(actor_id, run.clone()); + plan.run = Some(run); + Self::deposit_event(Event::CycleSuspended { actor_id, cycle_nonce, - step_index, + cursor, + reason, + cumulative_outcomes: outcomes, }); + Ok(( + plan, + effect_execution, + AttemptDisposition::Suspended, + outcomes, + Some(eligible_at), + )) } - 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, - outcome, - }); - } - } - - #[cfg(any(test, feature = "runtime-benchmarks"))] - pub(crate) fn execute_single_cycle( - actor_id: ActorId, - instance: ActiveActorViewOf, + fn prepare_loaded_resuspension( + instance: &ActiveActorViewOf, + run: &mut ActorRunStateOf, + max_attempts: u32, + unsuccessful_attempt_streak: u32, now: BlockNumberFor, - ) -> (Weight, bool) { - let result = Self::execute_single_cycle_traced(actor_id, instance, now, None, None); - (result.weight, result.fee_collection_failed) + reason: SuspensionReason, + last_step_outcome: StepOutcome, + outcomes: OutcomeTotals, + ) -> Result<(u32, bool), AttemptTransactionError> { + let attempts = run + .unsuccessful_attempts_at_cursor + .checked_add(1) + .ok_or(AttemptTransactionError::Invariant)?; + let streak = transition_failure_streak( + unsuccessful_attempt_streak, + FailureStreakTransition::UnsuccessfulAttempt, + ) + .ok_or(AttemptTransactionError::Invariant)?; + let terminal = attempts >= max_attempts || Self::failure_limit_reached(streak); + run.unsuccessful_attempts_at_cursor = attempts; + run.last_attempt_block = now; + run.cumulative_outcomes = outcomes; + run.last_step_outcome = Some(last_step_outcome); + run.suspension = Some(reason); + if terminal { + return Ok((streak, true)); + } + run.eligible_at = + Self::suspension_eligible_at(instance.cooldown_blocks, instance.window, now, attempts) + .map_err(|_| AttemptTransactionError::Invariant)?; + Ok((streak, false)) } - pub(crate) fn execute_zero_step_cycle( + fn commit_loaded_single_step_failure( actor_id: ActorId, - instance: ActiveActorViewOf, + mut plan: CurrentStepPlanOf, + cycle_nonce: u64, + unsuccessful_attempt_streak: u32, + outcomes: OutcomeTotals, + funding_disposition: LoadedFundingDisposition, + effect_execution: TaskEffectExecution, now: BlockNumberFor, - ) -> (bool, AttemptDisposition) { - let result = Self::execute_single_cycle_traced(actor_id, instance, now, None, None); - (result.fee_collection_failed, result.disposition) + ) -> Result< + ( + CurrentStepPlanOf, + TaskEffectExecution, + AttemptDisposition, + OutcomeTotals, + Option>, + ), + AttemptTransactionError, + > { + let deferred_signal = plan.hot.cycle_state != CycleState::Idle && plan.hot.pending_signal; + ActorRunStateStore::::remove(actor_id); + plan.run = None; + plan.identity.cycle_nonce = cycle_nonce; + plan.hot.cycle_state = CycleState::Idle; + plan.hot.pending_signal = deferred_signal; + plan.hot.queue_ticket = None; + plan.hot.last_cycle_block = Some(now); + plan.hot.unsuccessful_attempt_streak = unsuccessful_attempt_streak; + if funding_disposition.clears() { + plan.funding.funding_accumulated.clear(); + ActorFunding::::insert(actor_id, &plan.funding); + } + Self::deposit_event(Event::CycleSummary { + actor_id, + cycle_nonce, + result: CycleResult::Failed, + outcomes, + }); + Ok(( + plan, + effect_execution, + AttemptDisposition::Failed, + outcomes, + None, + )) } - pub(crate) fn execute_current_step_plan( + fn execute_suspended_single_step_core( actor_id: ActorId, - instance: ActiveActorViewOf, - plan: CurrentStepPlanOf, + instance: &ActiveActorViewOf, + mut plan: CurrentStepPlanOf, now: BlockNumberFor, - ) -> (Weight, bool, TaskEffectExecution, AttemptDisposition) { - let result = Self::execute_single_cycle_traced(actor_id, instance, now, Some(plan), None); + step_count: u32, + ) -> Result< ( - result.weight, - result.fee_collection_failed, - result.effect_execution, - result.disposition, + CurrentStepPlanOf, + TaskEffectExecution, + AttemptDisposition, + OutcomeTotals, + Option>, + ), + AttemptTransactionError, + > { + let step = plan.loaded_step.step.clone(); + let mut run = plan.run.take().ok_or(AttemptTransactionError::Invariant)?; + let deferred_signal = plan.hot.pending_signal; + if instance.cycle_state != CycleState::Suspended + || run.cursor >= step_count + || step_count == 0 + || plan.hot.wakeup_pointer.is_some() + || plan.ticket.actor_id != actor_id + || plan.ticket.cursor != run.cursor + || plan.loaded_step.cursor != run.cursor + || plan.ticket.cycle_nonce != run.cycle_nonce + || plan.ticket.eligible_at != run.eligible_at + || now < run.eligible_at + || run.cycle_nonce + != plan + .identity + .cycle_nonce + .checked_add(1) + .ok_or(AttemptTransactionError::Invariant)? + || !run.suspension_is_coherent() + || !matches!(step.on_error, StepErrorPolicy::RetryLater { .. }) + || !plan.admission.has_valid_identity() + { + return Err(AttemptTransactionError::Invariant); + } + let expected_fee = Self::maximum_current_action_fee( + instance.actor_class.actor_type(), + &step, + plan.loaded_step.resources, ) - } - - 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 collect_fee_in_execution = step_plan.is_none(); - let has_persisted_run = matches!( - instance.cycle_state, - CycleState::Running | CycleState::Suspended + .map_err(|_| AttemptTransactionError::Invariant)?; + if plan.maximum_fee != expected_fee { + return Err(AttemptTransactionError::Invariant); + } + let cycle_nonce = run.cycle_nonce; + let cursor = run.cursor; + run.last_attempt_block = now; + Self::deposit_event(Event::CycleContinued { + actor_id, + cycle_nonce, + cursor, + }); + let mut predicate_index = run.opening_predicate_cursor as usize; + let predicate_result = Self::evaluate_step_precondition( + step.precondition.as_ref(), + &instance.sovereign_account, + plan.maximum_fee.total_fee, + &run.opening_predicate_results, + &mut predicate_index, ); - let is_suspended_run = instance.cycle_state == CycleState::Suspended; - let actor = instance.sovereign_account.clone(); - let contract_steps = &instance.steps; - 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; + let predicate_error = predicate_result.as_ref().err().cloned(); + let predicate_matches = predicate_result.unwrap_or(true); + let mut resolution_skipped = false; + let effect_execution = if predicate_matches { + match predicate_error.map_or_else( + || { + Self::prepare_task( + &step.task, + &instance.sovereign_account, + instance.actor_class.actor_type(), + plan.maximum_fee.total_fee, + &run.opening_snapshot, + &run.funding_snapshot, + ) + }, + Err, + ) { + Err(error) => { + let failure = TaskFailure::permanent(error); + plan.last_step_outcome = Some(StepOutcome::Failed(failure.clone())); + Self::deposit_event(Event::StepFailed { + actor_id, + cycle_nonce, + step_index: cursor, + retry_class: failure.retry, + error: failure.error, + }); + let mut outcomes = run.cumulative_outcomes; + outcomes.failed_steps = checked_semantic_increment(outcomes.failed_steps) + .map_err(|_| AttemptTransactionError::Invariant)?; + let streak = transition_failure_streak( + plan.hot.unsuccessful_attempt_streak, + FailureStreakTransition::UnsuccessfulAttempt, + ) + .ok_or(AttemptTransactionError::Invariant)?; + return Self::commit_loaded_single_step_failure( + actor_id, + plan, + cycle_nonce, + streak, + outcomes, + LoadedFundingDisposition::Preserve, + TaskEffectExecution::NotInvoked, + now, + ); } - }); - ActorFunding::::mutate(actor_id, |maybe| { - if let Some(funding) = maybe.as_mut() { - funding.funding_accumulated.clear(); + Ok(PreparedTaskOutcome::Executable(prepared)) => { + if let Err(failure) = Self::execute_prepared_task( + prepared, + actor_id, + cycle_nonce, + cursor, + &instance.sovereign_account, + instance.actor_class.actor_type(), + ) { + let mut outcomes = run.cumulative_outcomes; + outcomes.failed_steps = checked_semantic_increment(outcomes.failed_steps) + .map_err(|_| AttemptTransactionError::Invariant)?; + let failed_outcome = StepOutcome::Failed(failure.clone()); + plan.last_step_outcome = Some(failed_outcome.clone()); + Self::deposit_event(Event::StepFailed { + actor_id, + cycle_nonce, + step_index: cursor, + retry_class: failure.retry, + error: failure.error, + }); + if failure.retry != RetryClass::Temporary { + let streak = transition_failure_streak( + plan.hot.unsuccessful_attempt_streak, + FailureStreakTransition::UnsuccessfulAttempt, + ) + .ok_or(AttemptTransactionError::Invariant)?; + return Self::commit_loaded_single_step_failure( + actor_id, + plan, + cycle_nonce, + streak, + outcomes, + LoadedFundingDisposition::Preserve, + TaskEffectExecution::Invoked, + now, + ); + } + let (streak, terminal) = Self::prepare_loaded_resuspension( + instance, + &mut run, + step + .on_error + .retry_max_attempts() + .ok_or(AttemptTransactionError::Invariant)?, + plan.hot.unsuccessful_attempt_streak, + now, + SuspensionReason::Temporary, + failed_outcome, + outcomes, + )?; + if terminal { + return Self::commit_loaded_single_step_failure( + actor_id, + plan, + cycle_nonce, + streak, + outcomes, + LoadedFundingDisposition::Preserve, + TaskEffectExecution::Invoked, + now, + ); + } + return Self::commit_loaded_single_step_suspension( + actor_id, + plan, + run, + streak, + LoadedFundingDisposition::Preserve, + TaskEffectExecution::Invoked, + ); + } + TaskEffectExecution::Invoked } - }); - Self::deposit_event(Event::CycleStarted { + Ok(PreparedTaskOutcome::FundingUnavailable) => { + plan.last_step_outcome = Some(StepOutcome::FundingUnavailable); + let outcomes = run.cumulative_outcomes; + let (streak, terminal) = Self::prepare_loaded_resuspension( + instance, + &mut run, + step + .on_error + .retry_max_attempts() + .ok_or(AttemptTransactionError::Invariant)?, + plan.hot.unsuccessful_attempt_streak, + now, + SuspensionReason::FundingUnavailable, + StepOutcome::FundingUnavailable, + outcomes, + )?; + if terminal { + return Self::commit_loaded_single_step_failure( + actor_id, + plan, + cycle_nonce, + streak, + outcomes, + LoadedFundingDisposition::Preserve, + TaskEffectExecution::NotInvoked, + now, + ); + } + return Self::commit_loaded_single_step_suspension( + actor_id, + plan, + run, + streak, + LoadedFundingDisposition::Preserve, + TaskEffectExecution::NotInvoked, + ); + } + Ok(PreparedTaskOutcome::Skipped) => { + resolution_skipped = true; + Self::deposit_event(Event::StepSkipped { + actor_id, + cycle_nonce, + step_index: cursor, + reason: StepSkippedReason::ResolutionSkipped, + }); + TaskEffectExecution::NotInvoked + } + } + } else { + let mut outcomes = run.cumulative_outcomes; + outcomes.precondition_skips = checked_semantic_increment(outcomes.precondition_skips) + .map_err(|_| AttemptTransactionError::Invariant)?; + Self::deposit_event(Event::StepSkipped { actor_id, cycle_nonce, + step_index: cursor, + reason: StepSkippedReason::PreconditionFalse, }); - let outcomes = OutcomeTotals::default(); + run.cumulative_outcomes = outcomes; + TaskEffectExecution::NotInvoked + }; + let mut outcomes = run.cumulative_outcomes; + if resolution_skipped { + outcomes.skipped_resolution = checked_semantic_increment(outcomes.skipped_resolution) + .map_err(|_| AttemptTransactionError::Invariant)?; + } else if matches!(effect_execution, TaskEffectExecution::Invoked) { + outcomes.executed_steps = checked_semantic_increment(outcomes.executed_steps) + .map_err(|_| AttemptTransactionError::Invariant)?; + if matches!(&step.task, ActorTask::StopCycle) { + Self::record_stop_cycle_event(actor_id, cycle_nonce, cursor); + } else { + outcomes.committed_effectful_tasks = + checked_semantic_increment(outcomes.committed_effectful_tasks) + .map_err(|_| AttemptTransactionError::Invariant)?; + } + } + plan.last_step_outcome = Some(if resolution_skipped { + StepOutcome::Skipped(StepSkippedReason::ResolutionSkipped) + } else if !predicate_matches { + StepOutcome::Skipped(StepSkippedReason::PreconditionFalse) + } else if matches!(&step.task, ActorTask::StopCycle) { + StepOutcome::Stopped + } else { + StepOutcome::Executed + }); + let next_cursor = cursor + .checked_add(1) + .ok_or(AttemptTransactionError::Invariant)?; + if (matches!(&step.task, ActorTask::StopCycle) + && matches!(effect_execution, TaskEffectExecution::Invoked)) + || next_cursor >= step_count + { + ActorRunStateStore::::remove(actor_id); + plan.run = None; + plan.identity.cycle_nonce = cycle_nonce; + plan.hot.cycle_state = CycleState::Idle; + plan.hot.pending_signal = deferred_signal; + plan.hot.queue_ticket = None; + plan.hot.last_cycle_block = Some(now); + plan.hot.unsuccessful_attempt_streak = 0; 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) + let deferred_eligible_at = if deferred_signal { + Some( + now + .checked_add(&One::one()) + .ok_or(AttemptTransactionError::Invariant)?, + ) } else { - AttemptDisposition::Completed + None }; - return AttemptExecution { - weight: base_weight, - disposition, - fee_collection_failed: false, - effect_execution: TaskEffectExecution::NotInvoked, + return Ok(( + plan, + effect_execution, + AttemptDisposition::Completed, outcomes, - }; + deferred_eligible_at, + )); } - 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) + let eligible_at = now + .checked_add(&One::one()) + .ok_or(AttemptTransactionError::Invariant)?; + run.cursor = next_cursor; + run.opening_predicate_cursor = + u32::try_from(predicate_index).map_err(|_| AttemptTransactionError::Invariant)?; + run.unsuccessful_attempts_at_cursor = 0; + run.last_committed_step_block = Some(now); + run.eligible_at = eligible_at; + run.cumulative_outcomes = outcomes; + run.last_step_outcome = Some(if resolution_skipped { + StepOutcome::Skipped(StepSkippedReason::ResolutionSkipped) + } else if predicate_matches { + StepOutcome::Executed } 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(), - }; + StepOutcome::Skipped(StepSkippedReason::PreconditionFalse) + }); + run.suspension = None; + if !run.running_is_coherent() { + return Err(AttemptTransactionError::Invariant); } - 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( + ActorRunStateStore::::insert(actor_id, run.clone()); + plan.run = Some(run); + plan.hot.cycle_state = CycleState::Running; + plan.hot.pending_signal = deferred_signal; + plan.hot.queue_ticket = None; + plan.hot.unsuccessful_attempt_streak = 0; + Ok(( + plan, + effect_execution, + AttemptDisposition::Continued, + outcomes, + Some(eligible_at), + )) + } + + fn execute_running_current_step_core( + actor_id: ActorId, + instance: &ActiveActorViewOf, + mut plan: CurrentStepPlanOf, + now: BlockNumberFor, + step_count: u32, + ) -> Result< + ( + CurrentStepPlanOf, + TaskEffectExecution, + AttemptDisposition, + OutcomeTotals, + Option>, + ), + AttemptTransactionError, + > { + let step = plan.loaded_step.step.clone(); + let direct_task_policy = matches!( + step.on_error, + StepErrorPolicy::ContinueNextStep + | StepErrorPolicy::AbortCycle + | StepErrorPolicy::RetryLater { .. } + ); + let mut run = plan.run.take().ok_or(AttemptTransactionError::Invariant)?; + let deferred_signal = plan.hot.pending_signal; + if instance.cycle_state != CycleState::Running + || plan.hot.wakeup_pointer.is_some() + || plan.ticket.actor_id != actor_id + || plan.ticket.cursor != run.cursor + || plan.loaded_step.cursor != run.cursor + || plan.ticket.cycle_nonce != run.cycle_nonce + || plan.ticket.eligible_at != run.eligible_at + || now < run.eligible_at + || run.cursor == 0 + || run.cursor >= step_count + || run.cycle_nonce + != plan + .identity + .cycle_nonce + .checked_add(1) + .ok_or(AttemptTransactionError::Invariant)? + || !run.running_is_coherent() + || !direct_task_policy + || !plan.admission.has_valid_identity() + { + return Err(AttemptTransactionError::Invariant); + } + let expected_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(), - }; + &step, + plan.loaded_step.resources, + ) + .map_err(|_| AttemptTransactionError::Invariant)?; + if plan.maximum_fee != expected_fee { + return Err(AttemptTransactionError::Invariant); } - 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 { - ($fee:expr) => {{ - let result = Self::collect_user_step_fee(&actor, $fee); - if result.is_err() { - fee_collection_failed = true; + let cycle_nonce = run.cycle_nonce; + let cursor = run.cursor; + run.last_attempt_block = now; + let mut predicate_index = run.opening_predicate_cursor as usize; + let predicate_result = Self::evaluate_step_precondition( + step.precondition.as_ref(), + &instance.sovereign_account, + plan.maximum_fee.total_fee, + &run.opening_predicate_results, + &mut predicate_index, + ); + let predicate_error = predicate_result.as_ref().err().cloned(); + let predicate_matches = predicate_result.unwrap_or(true); + let mut resolution_skipped = false; + let mut funding_unavailable = false; + let mut execution_failure = None; + let mut abort_cycle = false; + let effect_execution = if predicate_matches { + match predicate_error.map_or_else( + || { + Self::prepare_task( + &step.task, + &instance.sovereign_account, + instance.actor_class.actor_type(), + plan.maximum_fee.total_fee, + &run.opening_snapshot, + &run.funding_snapshot, + ) + }, + Err, + ) { + Err(error) => { + let failure = TaskFailure::permanent(error); + plan.last_step_outcome = Some(StepOutcome::Failed(failure.clone())); + Self::deposit_event(Event::StepFailed { + actor_id, + cycle_nonce, + step_index: cursor, + retry_class: failure.retry, + error: failure.error, + }); + abort_cycle = !matches!(step.on_error, StepErrorPolicy::ContinueNextStep); + execution_failure = Some(failure); + TaskEffectExecution::NotInvoked } - result - }}; - } - - let ( - cycle_nonce, - start_cursor, - prior_unsuccessful_attempts_at_cursor, - cumulative_outcomes, - funding_snapshot, - opening_snapshot, - opening_predicate_results, - 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(), - }; - }; - ( - 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 { - Self::finalize_actor(actor_id, &instance, CloseReason::CycleNonceExhausted) - .expect("fresh execution snapshot satisfies terminal preconditions"); - return AttemptExecution { - weight: base_weight, - disposition: AttemptDisposition::Closed(CloseReason::CycleNonceExhausted), - fee_collection_failed: false, - effect_execution: TaskEffectExecution::NotInvoked, - outcomes: OutcomeTotals::default(), - }; - } - let funding_snapshot = ActorFunding::::get(actor_id) - .map(|funding| funding.funding_accumulated) - .unwrap_or_default(); - let opening_snapshot = Self::capture_opening_snapshot( - instance.actor_class.actor_type(), - &actor, - contract_steps, - reserved_fee_remaining, - ); - let opening_predicate_results = - Self::capture_opening_predicate_results(&actor, contract_steps, reserved_fee_remaining); - 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(), - }; - }; - ActorHot::::mutate(actor_id, |maybe| { - if let Some(hot) = maybe.as_mut() { - hot.pending_signal = false; - hot.last_cycle_block = Some(now); + Ok(PreparedTaskOutcome::Executable(prepared)) => { + if let Err(failure) = Self::execute_prepared_task( + prepared, + actor_id, + cycle_nonce, + cursor, + &instance.sovereign_account, + instance.actor_class.actor_type(), + ) { + let failed_outcome = StepOutcome::Failed(failure.clone()); + plan.last_step_outcome = Some(failed_outcome.clone()); + Self::deposit_event(Event::StepFailed { + actor_id, + cycle_nonce, + step_index: cursor, + retry_class: failure.retry, + error: failure.error, + }); + if matches!(step.on_error, StepErrorPolicy::ContinueNextStep) { + execution_failure = Some(failure); + } else if matches!(step.on_error, StepErrorPolicy::AbortCycle) { + execution_failure = Some(failure); + abort_cycle = true; + } else if failure.retry != RetryClass::Temporary { + execution_failure = Some(failure); + abort_cycle = true; + } else { + let mut outcomes = run.cumulative_outcomes; + outcomes.failed_steps = checked_semantic_increment(outcomes.failed_steps) + .map_err(|_| AttemptTransactionError::Invariant)?; + let (streak, terminal) = Self::prepare_loaded_resuspension( + instance, + &mut run, + step + .on_error + .retry_max_attempts() + .ok_or(AttemptTransactionError::Invariant)?, + plan.hot.unsuccessful_attempt_streak, + now, + SuspensionReason::Temporary, + failed_outcome, + outcomes, + )?; + if terminal { + return Self::commit_loaded_single_step_failure( + actor_id, + plan, + cycle_nonce, + streak, + outcomes, + LoadedFundingDisposition::Preserve, + TaskEffectExecution::Invoked, + now, + ); + } + return Self::commit_loaded_single_step_suspension( + actor_id, + plan, + run, + streak, + LoadedFundingDisposition::Preserve, + TaskEffectExecution::Invoked, + ); + } + } + TaskEffectExecution::Invoked } - }); - ActorFunding::::mutate(actor_id, |maybe| { - if let Some(funding) = maybe.as_mut() { - funding.funding_accumulated.clear(); + Ok(PreparedTaskOutcome::FundingUnavailable) => { + plan.last_step_outcome = Some(StepOutcome::FundingUnavailable); + if step.on_error.retry_max_attempts().is_none() { + funding_unavailable = true; + Self::deposit_event(Event::StepSkipped { + actor_id, + cycle_nonce, + step_index: cursor, + reason: StepSkippedReason::FundingUnavailable, + }); + TaskEffectExecution::NotInvoked + } else { + let outcomes = run.cumulative_outcomes; + let (streak, terminal) = Self::prepare_loaded_resuspension( + instance, + &mut run, + step + .on_error + .retry_max_attempts() + .ok_or(AttemptTransactionError::Invariant)?, + plan.hot.unsuccessful_attempt_streak, + now, + SuspensionReason::FundingUnavailable, + StepOutcome::FundingUnavailable, + outcomes, + )?; + if terminal { + return Self::commit_loaded_single_step_failure( + actor_id, + plan, + cycle_nonce, + streak, + outcomes, + LoadedFundingDisposition::Preserve, + TaskEffectExecution::NotInvoked, + now, + ); + } + return Self::commit_loaded_single_step_suspension( + actor_id, + plan, + run, + streak, + LoadedFundingDisposition::Preserve, + TaskEffectExecution::NotInvoked, + ); + } } - }); - ( + Ok(PreparedTaskOutcome::Skipped) => { + resolution_skipped = true; + Self::deposit_event(Event::StepSkipped { + actor_id, + cycle_nonce, + step_index: cursor, + reason: StepSkippedReason::ResolutionSkipped, + }); + TaskEffectExecution::NotInvoked + } + } + } else { + Self::deposit_event(Event::StepSkipped { + actor_id, cycle_nonce, - 0, - 0, - OutcomeTotals::default(), - funding_snapshot, - opening_snapshot, - opening_predicate_results, - 0, - None, - ) + step_index: cursor, + reason: StepSkippedReason::PreconditionFalse, + }); + TaskEffectExecution::NotInvoked + }; + let mut outcomes = run.cumulative_outcomes; + let last_step_outcome = if let Some(failure) = execution_failure { + outcomes.failed_steps = checked_semantic_increment(outcomes.failed_steps) + .map_err(|_| AttemptTransactionError::Invariant)?; + StepOutcome::Failed(failure) + } else if funding_unavailable { + outcomes.skipped_funding_unavailable = + checked_semantic_increment(outcomes.skipped_funding_unavailable) + .map_err(|_| AttemptTransactionError::Invariant)?; + StepOutcome::FundingUnavailable + } else if resolution_skipped { + outcomes.skipped_resolution = checked_semantic_increment(outcomes.skipped_resolution) + .map_err(|_| AttemptTransactionError::Invariant)?; + StepOutcome::Skipped(StepSkippedReason::ResolutionSkipped) + } else if predicate_matches { + outcomes.executed_steps = checked_semantic_increment(outcomes.executed_steps) + .map_err(|_| AttemptTransactionError::Invariant)?; + if matches!(&step.task, ActorTask::StopCycle) { + Self::record_stop_cycle_event(actor_id, cycle_nonce, cursor); + StepOutcome::Stopped + } else { + outcomes.committed_effectful_tasks = + checked_semantic_increment(outcomes.committed_effectful_tasks) + .map_err(|_| AttemptTransactionError::Invariant)?; + StepOutcome::Executed + } + } else { + outcomes.precondition_skips = checked_semantic_increment(outcomes.precondition_skips) + .map_err(|_| AttemptTransactionError::Invariant)?; + StepOutcome::Skipped(StepSkippedReason::PreconditionFalse) }; - let is_user = instance.actor_class.actor_type() == ActorType::User; - let funding_snapshots = &funding_snapshot; - let mut executed_steps = cumulative_outcomes.executed_steps; - let mut committed_effectful_tasks = cumulative_outcomes.committed_effectful_tasks; - let mut precondition_skips = cumulative_outcomes.precondition_skips; - let mut skipped_resolution = cumulative_outcomes.skipped_resolution; - let mut skipped_funding_unavailable = cumulative_outcomes.skipped_funding_unavailable; - 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; - let mut effect_execution = TaskEffectExecution::NotInvoked; - if !has_persisted_run { - Self::deposit_event(Event::CycleStarted { + plan.last_step_outcome = Some(last_step_outcome.clone()); + if abort_cycle { + let unsuccessful_attempt_streak = transition_failure_streak( + plan.hot.unsuccessful_attempt_streak, + FailureStreakTransition::UnsuccessfulAttempt, + ) + .ok_or(AttemptTransactionError::Invariant)?; + return Self::commit_loaded_single_step_failure( + actor_id, + plan, + cycle_nonce, + unsuccessful_attempt_streak, + outcomes, + LoadedFundingDisposition::Preserve, + effect_execution, + now, + ); + } + let next_cursor = cursor + .checked_add(1) + .ok_or(AttemptTransactionError::Invariant)?; + if matches!(last_step_outcome, StepOutcome::Stopped) || next_cursor >= step_count { + ActorRunStateStore::::remove(actor_id); + plan.run = None; + plan.identity.cycle_nonce = cycle_nonce; + plan.hot.cycle_state = CycleState::Idle; + plan.hot.pending_signal = deferred_signal; + plan.hot.queue_ticket = None; + plan.hot.last_cycle_block = Some(now); + plan.hot.unsuccessful_attempt_streak = 0; + Self::deposit_event(Event::CycleSummary { actor_id, cycle_nonce, + result: CycleResult::Completed, + outcomes, }); + let deferred_eligible_at = if deferred_signal { + Some( + now + .checked_add(&One::one()) + .ok_or(AttemptTransactionError::Invariant)?, + ) + } else { + None + }; + return Ok(( + plan, + effect_execution, + AttemptDisposition::Completed, + outcomes, + deferred_eligible_at, + )); + } + let eligible_at = now + .checked_add(&One::one()) + .ok_or(AttemptTransactionError::Invariant)?; + run.cursor = next_cursor; + run.opening_predicate_cursor = + u32::try_from(predicate_index).map_err(|_| AttemptTransactionError::Invariant)?; + run.unsuccessful_attempts_at_cursor = 0; + run.last_committed_step_block = Some(now); + run.eligible_at = eligible_at; + run.cumulative_outcomes = outcomes; + run.last_step_outcome = Some(last_step_outcome); + run.suspension = None; + if !run.running_is_coherent() { + return Err(AttemptTransactionError::Invariant); + } + ActorRunStateStore::::insert(actor_id, run.clone()); + plan.run = Some(run); + plan.hot.cycle_state = CycleState::Running; + plan.hot.pending_signal = deferred_signal; + plan.hot.queue_ticket = None; + plan.hot.unsuccessful_attempt_streak = 0; + Ok(( + plan, + effect_execution, + AttemptDisposition::Continued, + outcomes, + Some(eligible_at), + )) + } + + /// Returns the persisted successor Run in `plan.run`; ticket and loaded Step still identify + /// the attempted Step. Terminal outcomes return no Run authority. + pub(crate) fn execute_loaded_single_step_core( + actor_id: ActorId, + instance: &ActiveActorViewOf, + mut plan: CurrentStepPlanOf, + now: BlockNumberFor, + step_count: u32, + ) -> Result< + ( + CurrentStepPlanOf, + TaskEffectExecution, + AttemptDisposition, + OutcomeTotals, + Option>, + ), + AttemptTransactionError, + > { + if instance.cycle_state == CycleState::Running { + return Self::execute_running_current_step_core(actor_id, instance, plan, now, step_count); + } + if instance.cycle_state == CycleState::Suspended { + return Self::execute_suspended_single_step_core(actor_id, instance, plan, now, step_count); + } + let step = plan.loaded_step.step.clone(); + let direct_task_policy = matches!( + step.on_error, + StepErrorPolicy::ContinueNextStep + | StepErrorPolicy::AbortCycle + | StepErrorPolicy::RetryLater { .. } + ); + if instance.cycle_state != CycleState::Idle + || instance.steps.is_empty() + || instance.steps.len() != step_count as usize + || plan.run.is_some() + || ActorRunHeads::::contains_key(actor_id) + || ActorRunPayloads::::contains_key(actor_id) + || !plan.hot.pending_signal + || plan.ticket.actor_id != actor_id + || plan.ticket.cursor != 0 + || plan.loaded_step.cursor != 0 + || plan.ticket.cycle_nonce + != plan + .identity + .cycle_nonce + .checked_add(1) + .ok_or(AttemptTransactionError::Invariant)? + || instance.steps.first() != Some(&step) + || step_count == 0 + || !direct_task_policy + || !plan.admission.has_valid_identity() + { + return Err(AttemptTransactionError::Invariant); + } + let expected_fee = Self::maximum_current_action_fee( + instance.actor_class.actor_type(), + &step, + plan.loaded_step.resources, + ) + .map_err(|_| AttemptTransactionError::Invariant)?; + if plan.maximum_fee != expected_fee { + return Err(AttemptTransactionError::Invariant); } - 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 - // identically: count the failure, record and emit it as Permanent, then terminate or advance - // per `StepErrorPolicy`. It stays a macro because that resolution ends in `break` or - // `continue` against this loop, which a helper function cannot express, and it lives inside - // the loop so `step` and `step_num` resolve without threading them through parameters. - macro_rules! fail_step_with_fee_error { - ($error:expr) => {{ - let step_error = $error; - failed_steps = checked_semantic_increment(failed_steps) - .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, - &mut last_step_outcome, - step_num, - outcome.clone(), - ); + let opening_snapshot = Self::capture_opening_snapshot( + instance.actor_class.actor_type(), + &instance.sovereign_account, + &instance.steps, + plan.maximum_fee.total_fee, + ); + let opening_predicate_results = Self::capture_opening_predicate_results( + &instance.sovereign_account, + &instance.steps, + plan.maximum_fee.total_fee, + ); + let funding_snapshot = plan.funding.funding_accumulated.clone(); + let cycle_nonce = plan.ticket.cycle_nonce; + plan.hot.last_cycle_block = Some(now); + Self::deposit_event(Event::CycleStarted { + actor_id, + cycle_nonce, + }); + let mut opening_predicate_index = 0usize; + let predicate_result = Self::evaluate_step_precondition( + step.precondition.as_ref(), + &instance.sovereign_account, + plan.maximum_fee.total_fee, + &opening_predicate_results, + &mut opening_predicate_index, + ); + let predicate_error = predicate_result.as_ref().err().cloned(); + let predicate_matches = predicate_result.unwrap_or(true); + let mut abort_cycle = false; + let (effect_execution, outcomes, last_step_outcome) = if predicate_matches { + match predicate_error.map_or_else( + || { + Self::prepare_task( + &step.task, + &instance.sovereign_account, + instance.actor_class.actor_type(), + plan.maximum_fee.total_fee, + &opening_snapshot, + &funding_snapshot, + ) + }, + Err, + ) { + Err(error) => { + let failure = TaskFailure::permanent(error); + plan.last_step_outcome = Some(StepOutcome::Failed(failure.clone())); Self::deposit_event(Event::StepFailed { actor_id, cycle_nonce, - step_index: step_num, + step_index: 0, retry_class: failure.retry, - error: step_error, + error: failure.error, }); - contract_steps_failed = - resolve_step_control(&outcome, step.on_error) == StepControl::Terminate; - if contract_steps_failed { - break; - } - continue; - }}; - } - match Self::evaluate_step_precondition( - step.precondition.as_ref(), - &actor, - reserved_fee_remaining, - &opening_predicate_results, - &mut opening_predicate_index, - ) { - Ok(true) => {} - Ok(false) => { - if is_user && collect_fee_in_execution { - let charged_fee = Self::settle_user_step_fee( - &mut reserved_fee_remaining, - step_fee, - FeeChargeKind::EvaluationOnly, - ); - if let Err(error) = collect_step_fee!(charged_fee) { - fail_step_with_fee_error!(error); - } - } - precondition_skips = checked_semantic_increment(precondition_skips) - .expect("semantic counter bound is precluded by admission"); - Self::record_step_outcome( - &mut trace, - &mut last_step_outcome, - step_num, - StepOutcome::Skipped(StepSkippedReason::PreconditionFalse), - ); - Self::deposit_event(Event::StepSkipped { - actor_id, - cycle_nonce, - step_index: step_num, - reason: StepSkippedReason::PreconditionFalse, - }); - continue; - } - Err(error) => { - let charged_error = if is_user && collect_fee_in_execution { - let charged_fee = Self::settle_user_step_fee( - &mut reserved_fee_remaining, - step_fee, - FeeChargeKind::EvaluationOnly, - ); - collect_step_fee!(charged_fee).err().unwrap_or(error) - } else { - error - }; - fail_step_with_fee_error!(charged_error); + abort_cycle = !matches!(step.on_error, StepErrorPolicy::ContinueNextStep); + ( + TaskEffectExecution::NotInvoked, + OutcomeTotals { + failed_steps: 1, + ..Default::default() + }, + StepOutcome::Failed(failure), + ) } - } - let prepared_task = match Self::prepare_task( - &step.task, - &actor, - instance.actor_class.actor_type(), - reserved_fee_remaining, - &opening_snapshot, - funding_snapshots, - ) { - Ok(PreparedTaskOutcome::Executable(task)) => task, Ok(PreparedTaskOutcome::Skipped) => { - if is_user && collect_fee_in_execution { - let charged_fee = Self::settle_user_step_fee( - &mut reserved_fee_remaining, - step_fee, - FeeChargeKind::EvaluationOnly, - ); - if let Err(error) = collect_step_fee!(charged_fee) { - fail_step_with_fee_error!(error); - } - } - skipped_resolution = checked_semantic_increment(skipped_resolution) - .expect("semantic counter bound is precluded by admission"); - Self::record_step_outcome( - &mut trace, - &mut last_step_outcome, - step_num, - StepOutcome::Skipped(StepSkippedReason::ResolutionSkipped), - ); Self::deposit_event(Event::StepSkipped { actor_id, cycle_nonce, - step_index: step_num, + step_index: 0, reason: StepSkippedReason::ResolutionSkipped, }); - continue; + ( + TaskEffectExecution::NotInvoked, + OutcomeTotals { + skipped_resolution: 1, + ..Default::default() + }, + StepOutcome::Skipped(StepSkippedReason::ResolutionSkipped), + ) } - Ok(PreparedTaskOutcome::FundingUnavailable) => { - if is_user && collect_fee_in_execution { - let charged_fee = Self::settle_user_step_fee( - &mut reserved_fee_remaining, - step_fee, - FeeChargeKind::EvaluationOnly, - ); - if let Err(error) = collect_step_fee!(charged_fee) { - fail_step_with_fee_error!(error); - } - } - let outcome = StepOutcome::FundingUnavailable; - 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 => { - unreachable!("FundingUnavailable cannot complete a cycle") - } - StepControl::Terminate => { - contract_steps_failed = true; - break; - } - StepControl::SuspendCurrent => { - suspended_at = Some((step_num, SuspensionReason::FundingUnavailable)); - break; - } - } - skipped_funding_unavailable = checked_semantic_increment(skipped_funding_unavailable) - .expect("semantic counter bound is precluded by admission"); + Ok(PreparedTaskOutcome::FundingUnavailable) + if step.on_error.retry_max_attempts().is_none() => + { Self::deposit_event(Event::StepSkipped { actor_id, cycle_nonce, - step_index: step_num, + step_index: 0, reason: StepSkippedReason::FundingUnavailable, }); - continue; + ( + TaskEffectExecution::NotInvoked, + OutcomeTotals { + skipped_funding_unavailable: 1, + ..Default::default() + }, + StepOutcome::FundingUnavailable, + ) } - Err(error) => { - let charged_error = if is_user && collect_fee_in_execution { - let charged_fee = Self::settle_user_step_fee( - &mut reserved_fee_remaining, - step_fee, - FeeChargeKind::EvaluationOnly, + Ok(PreparedTaskOutcome::FundingUnavailable) => { + plan.last_step_outcome = Some(StepOutcome::FundingUnavailable); + let max_attempts = step + .on_error + .retry_max_attempts() + .ok_or(AttemptTransactionError::Invariant)?; + let unsuccessful_attempts_at_cursor = 1u32; + let unsuccessful_attempt_streak = transition_failure_streak( + plan.hot.unsuccessful_attempt_streak, + FailureStreakTransition::UnsuccessfulAttempt, + ) + .ok_or(AttemptTransactionError::Invariant)?; + let outcomes = OutcomeTotals::default(); + if unsuccessful_attempts_at_cursor >= max_attempts + || Self::failure_limit_reached(unsuccessful_attempt_streak) + { + return Self::commit_loaded_single_step_failure( + actor_id, + plan, + cycle_nonce, + unsuccessful_attempt_streak, + outcomes, + LoadedFundingDisposition::Clear, + TaskEffectExecution::NotInvoked, + now, ); - collect_step_fee!(charged_fee).err().unwrap_or(error) - } else { - error + } + let eligible_at = Self::suspension_eligible_at( + instance.cooldown_blocks, + instance.window, + now, + unsuccessful_attempts_at_cursor, + ) + .map_err(|_| AttemptTransactionError::Invariant)?; + let run = ActorRunState { + contract_authority: ActorRunAuthority { + semantic_contract_id: plan.admission.semantic_contract_id, + body_commitment: plan.admission.body_commitment, + admission_identity: plan.admission.admission_identity, + }, + cycle_nonce, + cursor: 0, + opening_predicate_cursor: 0, + unsuccessful_attempts_at_cursor, + last_attempt_block: now, + last_committed_step_block: None, + eligible_at, + opening_snapshot, + opening_predicate_results, + funding_snapshot, + cumulative_outcomes: outcomes, + last_step_outcome: Some(StepOutcome::FundingUnavailable), + suspension: Some(SuspensionReason::FundingUnavailable), }; - fail_step_with_fee_error!(charged_error); - } - }; - if is_user && collect_fee_in_execution { - let charged_fee = Self::settle_user_step_fee( - &mut reserved_fee_remaining, - step_fee, - FeeChargeKind::Attempted, - ); - if let Err(error) = collect_step_fee!(charged_fee) { - fail_step_with_fee_error!(error); + return Self::commit_loaded_single_step_suspension( + actor_id, + plan, + run, + unsuccessful_attempt_streak, + LoadedFundingDisposition::Clear, + TaskEffectExecution::NotInvoked, + ); } - } - effect_execution = TaskEffectExecution::Invoked; - if let Err(failure) = Self::execute_prepared_task( - prepared_task, - actor_id, - cycle_nonce, - step_num, - &actor, - instance.actor_class.actor_type(), - ) { - failed_steps = checked_semantic_increment(failed_steps) - .expect("semantic counter bound is precluded by admission"); - 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, &mut last_step_outcome, step_num, outcome); - Self::deposit_event(Event::StepFailed { - actor_id, - cycle_nonce, - step_index: step_num, - retry_class: retry, - error: failure.error, - }); - match control { - StepControl::Advance => continue, - StepControl::CompleteCycle => { - unreachable!("task failure cannot complete a cycle") - } - StepControl::Terminate => { - contract_steps_failed = true; - break; - } - StepControl::SuspendCurrent => { - suspended_at = Some((step_num, SuspensionReason::Temporary)); - break; + Ok(PreparedTaskOutcome::Executable(prepared)) => { + if let Err(failure) = Self::execute_prepared_task( + prepared, + actor_id, + cycle_nonce, + 0, + &instance.sovereign_account, + instance.actor_class.actor_type(), + ) { + let last_step_outcome = StepOutcome::Failed(failure.clone()); + plan.last_step_outcome = Some(last_step_outcome.clone()); + Self::deposit_event(Event::StepFailed { + actor_id, + cycle_nonce, + step_index: 0, + retry_class: failure.retry, + error: failure.error, + }); + if matches!(step.on_error, StepErrorPolicy::ContinueNextStep) { + ( + TaskEffectExecution::Invoked, + OutcomeTotals { + failed_steps: 1, + ..Default::default() + }, + last_step_outcome, + ) + } else if matches!(step.on_error, StepErrorPolicy::AbortCycle) { + abort_cycle = true; + ( + TaskEffectExecution::Invoked, + OutcomeTotals { + failed_steps: 1, + ..Default::default() + }, + last_step_outcome, + ) + } else if failure.retry != RetryClass::Temporary { + abort_cycle = true; + ( + TaskEffectExecution::Invoked, + OutcomeTotals { + failed_steps: 1, + ..Default::default() + }, + last_step_outcome, + ) + } else { + let max_attempts = step + .on_error + .retry_max_attempts() + .ok_or(AttemptTransactionError::Invariant)?; + let unsuccessful_attempts_at_cursor = 1u32; + let unsuccessful_attempt_streak = transition_failure_streak( + plan.hot.unsuccessful_attempt_streak, + FailureStreakTransition::UnsuccessfulAttempt, + ) + .ok_or(AttemptTransactionError::Invariant)?; + let outcomes = OutcomeTotals { + failed_steps: 1, + ..Default::default() + }; + if unsuccessful_attempts_at_cursor >= max_attempts + || Self::failure_limit_reached(unsuccessful_attempt_streak) + { + return Self::commit_loaded_single_step_failure( + actor_id, + plan, + cycle_nonce, + unsuccessful_attempt_streak, + outcomes, + LoadedFundingDisposition::Clear, + TaskEffectExecution::Invoked, + now, + ); + } + let eligible_at = Self::suspension_eligible_at( + instance.cooldown_blocks, + instance.window, + now, + unsuccessful_attempts_at_cursor, + ) + .map_err(|_| AttemptTransactionError::Invariant)?; + let run = ActorRunState { + contract_authority: ActorRunAuthority { + semantic_contract_id: plan.admission.semantic_contract_id, + body_commitment: plan.admission.body_commitment, + admission_identity: plan.admission.admission_identity, + }, + cycle_nonce, + cursor: 0, + opening_predicate_cursor: 0, + unsuccessful_attempts_at_cursor, + last_attempt_block: now, + last_committed_step_block: None, + eligible_at, + opening_snapshot, + opening_predicate_results, + funding_snapshot, + cumulative_outcomes: outcomes, + last_step_outcome: Some(last_step_outcome), + suspension: Some(SuspensionReason::Temporary), + }; + return Self::commit_loaded_single_step_suspension( + actor_id, + plan, + run, + unsuccessful_attempt_streak, + LoadedFundingDisposition::Clear, + TaskEffectExecution::Invoked, + ); + } + } else { + let committed_effectful_tasks = if matches!(&step.task, ActorTask::StopCycle) { + Self::record_stop_cycle_event(actor_id, cycle_nonce, 0); + 0 + } else { + 1 + }; + ( + TaskEffectExecution::Invoked, + OutcomeTotals { + executed_steps: 1, + committed_effectful_tasks, + ..Default::default() + }, + if matches!(&step.task, ActorTask::StopCycle) { + StepOutcome::Stopped + } else { + StepOutcome::Executed + }, + ) } } } - let outcome = if matches!(&step.task, ActorTask::StopCycle) { - StepOutcome::Stopped - } else { - StepOutcome::Executed - }; - let successful_control = resolve_step_control(&outcome, step.on_error); - if successful_control == StepControl::CompleteCycle { - Self::record_stop_cycle_event(actor_id, cycle_nonce, step_num); - } else { - debug_assert_eq!(successful_control, StepControl::Advance); - } - 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) - .expect("semantic counter bound is precluded by admission"); - if successful_control != StepControl::CompleteCycle { - committed_effectful_tasks = checked_semantic_increment(committed_effectful_tasks) - .expect("semantic counter bound is precluded by admission"); - } - if successful_control == StepControl::CompleteCycle { - step_completed_cycle = true; - break; - } - } - let attempt_weight = if attempt_executed_steps == 0 { - base_weight } 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( + Self::deposit_event(Event::StepSkipped { 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, + cycle_nonce, + step_index: 0, + reason: StepSkippedReason::PreconditionFalse, + }); + ( + TaskEffectExecution::NotInvoked, + OutcomeTotals { + precondition_skips: 1, + ..Default::default() }, + StepOutcome::Skipped(StepSkippedReason::PreconditionFalse), ) - .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, + }; + plan.last_step_outcome = Some(last_step_outcome.clone()); + if abort_cycle { + let unsuccessful_attempt_streak = transition_failure_streak( + plan.hot.unsuccessful_attempt_streak, + FailureStreakTransition::UnsuccessfulAttempt, + ) + .ok_or(AttemptTransactionError::Invariant)?; + return Self::commit_loaded_single_step_failure( + actor_id, + plan, + cycle_nonce, + unsuccessful_attempt_streak, + outcomes, + LoadedFundingDisposition::Clear, effect_execution, - outcomes: cumulative_outcomes, - }; + now, + ); } - let mut failure_already_recorded = false; - if let Some((cursor, suspension_reason)) = suspended_at { - let unsuccessful_attempt_streak = ActorHot::::mutate(actor_id, |maybe| { - let Some(hot) = maybe.as_mut() else { - return 0; - }; - hot.unsuccessful_attempt_streak = transition_failure_streak( - hot.unsuccessful_attempt_streak, - FailureStreakTransition::UnsuccessfulAttempt, - ) - .expect("attempt admission excludes an exhausted failure counter"); - hot.unsuccessful_attempt_streak - }); - failure_already_recorded = true; - 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") - } else { - 1 + if step_count > 1 && !matches!(last_step_outcome, StepOutcome::Stopped) { + let eligible_at = now + .checked_add(&One::one()) + .ok_or(AttemptTransactionError::Invariant)?; + let run = ActorRunState { + contract_authority: ActorRunAuthority { + semantic_contract_id: plan.admission.semantic_contract_id, + body_commitment: plan.admission.body_commitment, + admission_identity: plan.admission.admission_identity, + }, + cycle_nonce, + cursor: 1, + opening_predicate_cursor: u32::try_from(opening_predicate_index) + .map_err(|_| AttemptTransactionError::Invariant)?, + unsuccessful_attempts_at_cursor: 0, + last_attempt_block: now, + last_committed_step_block: Some(now), + eligible_at, + opening_snapshot, + opening_predicate_results, + funding_snapshot, + cumulative_outcomes: outcomes, + last_step_outcome: Some(last_step_outcome), + suspension: None, }; - let max_attempts = contract_steps[cursor as usize] - .on_error - .retry_max_attempts() - .expect("suspension requires bounded RetryLater"); - 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 { - 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 { - failure_close_reason = Some(if local_limit_reached { - CloseReason::RetryAttemptsExhausted - } else { - CloseReason::ConsecutiveFailures - }); + if !run.running_is_coherent() { + return Err(AttemptTransactionError::Invariant); } - contract_steps_failed = true; - } - 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. + ActorRunStateStore::::insert(actor_id, run.clone()); + plan.run = Some(run); + plan.hot.cycle_state = CycleState::Running; + plan.hot.pending_signal = false; + plan.hot.queue_ticket = None; + plan.hot.last_cycle_block = Some(now); + plan.hot.unsuccessful_attempt_streak = 0; + plan.funding.funding_accumulated.clear(); + ActorFunding::::insert(actor_id, &plan.funding); + return Ok(( + plan, + effect_execution, + AttemptDisposition::Continued, + outcomes, + Some(eligible_at), + )); } - let post_failure_streak = ActorHot::::mutate(actor_id, |maybe| { - let Some(hot) = maybe.as_mut() else { - return 0; - }; - 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. - } - hot.unsuccessful_attempt_streak - }); - let outcomes = OutcomeTotals { - executed_steps, - committed_effectful_tasks, - precondition_skips, - skipped_resolution, - skipped_funding_unavailable, - failed_steps, - }; Self::deposit_event(Event::CycleSummary { actor_id, cycle_nonce, - result: if contract_steps_failed { - CycleResult::Failed - } else { - CycleResult::Completed - }, + result: CycleResult::Completed, 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 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) - } else if contract_steps_failed { - AttemptDisposition::Failed - } else { - AttemptDisposition::Completed - }; - AttemptExecution { - weight: attempt_weight, - disposition, - fee_collection_failed, + plan.identity.cycle_nonce = cycle_nonce; + plan.hot.cycle_state = CycleState::Idle; + plan.hot.pending_signal = false; + plan.hot.queue_ticket = None; + plan.hot.last_cycle_block = Some(now); + plan.hot.unsuccessful_attempt_streak = 0; + plan.funding.funding_accumulated.clear(); + ActorFunding::::insert(actor_id, &plan.funding); + Ok(( + plan, effect_execution, + AttemptDisposition::Completed, outcomes, - } + None, + )) } pub fn simulate_current_contract( @@ -1363,13 +1846,14 @@ impl Pallet { expected_mutability: Mutability, expected_contract: ActorContractOf, mode: SimulationMode, - ) -> Result, SimulationError> { + budget: SimulationBudget, + ) -> Result { Self::validate_trigger( &expected_contract.trigger, expected_contract.cooldown_blocks, ) .map_err(|_| SimulationError::InvalidContract)?; - let state = match Self::load_actor_state(actor_id) { + let state = match Self::load_actor_state_for_frame_control(actor_id) { LoadedActorStateOf::Active(state) => state, LoadedActorStateOf::NotRegistered | LoadedActorStateOf::Dormant(_) => { return Err(SimulationError::ActorNotFound); @@ -1380,6 +1864,9 @@ impl Pallet { )); } }; + if state.contract != expected_contract { + return Err(SimulationError::ContractMismatch); + } 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 { @@ -1388,14 +1875,16 @@ impl Pallet { if instance.mutability != expected_mutability { return Err(SimulationError::MutabilityMismatch); } - 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::CurrentRun if instance.cycle_state != CycleState::Suspended => { + SimulationMode::CurrentRun + if !matches!( + instance.cycle_state, + CycleState::Running | CycleState::Suspended + ) => + { return Err(SimulationError::ModeCycleStateMismatch); } _ => {} @@ -1405,153 +1894,22 @@ impl Pallet { if classification.execution_phase == ActorExecutionPhase::GlobalCircuitBreaker { return Err(SimulationError::GlobalCircuitBreaker); } - if let Some(reason) = classification.terminal_reason { - 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, - ) + budget + .checked_limits() + .map_err(|_| SimulationError::InvalidBudget)?; + if classification.terminal_reason.is_none() { + match classification.execution_phase { + ActorExecutionPhase::Ready => {} + ActorExecutionPhase::Paused => return Err(SimulationError::Paused), + ActorExecutionPhase::GlobalCircuitBreaker => { + return Err(SimulationError::GlobalCircuitBreaker); } - }; - return Ok(SimulationResult { - status: AttemptDisposition::Closed(reason), - cycle_nonce, - start_cursor, - 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 - .cycle_nonce - .checked_add(1) - .ok_or(SimulationError::Classification( - ActorClassificationError::ActorInvariant, - ))?, - 0, - ), - SimulationMode::CurrentRun => { - let run_state = run_state.as_ref().ok_or(SimulationError::Classification( - ActorClassificationError::RunInvariant, - ))?; - (run_state.cycle_nonce, run_state.cursor) + _ => return Err(SimulationError::NotReady), } - }; - match classification.execution_phase { - ActorExecutionPhase::Paused => return Err(SimulationError::Paused), - ActorExecutionPhase::Ready => {} - ActorExecutionPhase::WaitingRetry(_) - | ActorExecutionPhase::WaitingBlock(_) - | ActorExecutionPhase::WaitingCadenceTick(_) - | ActorExecutionPhase::WaitingSignal => return Err(SimulationError::NotReady), - ActorExecutionPhase::GlobalCircuitBreaker => unreachable!("handled above"), } - 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 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 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) = 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::RunInvariant), - )); - }; - polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Ok(SimulationResult { - status: attempt.disposition, - cycle_nonce, - start_cursor, - run_cursor, - unsuccessful_attempts_at_cursor, - cumulative_outcomes: attempt.outcomes, - steps, - })) + let result = Self::simulate_actor_service(actor_id, budget); + polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(result) }) .map_err(|()| SimulationError::TransactionDepthExceeded)? } @@ -1561,32 +1919,6 @@ impl Pallet { max_failures > 0 && unsuccessful_attempt_streak >= max_failures } - pub(crate) fn predicate_evaluation_weight(evaluation_units: u32) -> Weight { - let component_bound = T::MaxPredicatesPerStep::get(); - if component_bound == 0 { - return Weight::MAX; - } - let mut remaining = evaluation_units; - let mut weight = Weight::zero(); - while remaining > 0 { - let chunk = remaining.min(component_bound); - weight = weight.saturating_add(T::WeightInfo::predicate_set_evaluation(chunk)); - remaining = remaining.saturating_sub(chunk); - } - weight - } - - fn settle_user_step_fee( - reservation: &mut T::Balance, - step: &super::StepFeeEnvelope, - charge_kind: FeeChargeKind, - ) -> T::Balance { - let settlement = settle_attempt_fee_step(ActorType::User, *reservation, step, charge_kind) - .expect("admitted User plans preserve their fee reservation"); - *reservation = settlement.reservation_remaining; - settlement.charged - } - pub(crate) fn collect_user_step_fee(actor: &T::AccountId, fee: T::Balance) -> DispatchResult { if fee.is_zero() { return Ok(()); @@ -2392,7 +2724,7 @@ impl Pallet { ) } - fn evaluate_step_precondition( + pub(crate) fn evaluate_step_precondition( precondition: Option<&PreconditionOf>, who: &T::AccountId, reserved: T::Balance, @@ -2520,7 +2852,11 @@ impl Pallet { fn protected_minimum(actor_type: ActorType, asset: T::AssetId) -> T::Balance { fee_native_protected_minimum( actor_type, - asset == T::FeeNativeAssetId::get(), + if asset == T::FeeNativeAssetId::get() { + FeeAssetClass::FeeNative + } else { + FeeAssetClass::Other + }, T::AssetOps::minimum_balance(asset), T::MinUserBalance::get(), ) @@ -2640,83 +2976,6 @@ impl Pallet { mod step_control_tests { use super::*; - const RETRY: StepErrorPolicy = StepErrorPolicy::RetryLater { max_attempts: 3 }; - - #[test] - fn step_control_matrix_is_exhaustive() { - let permanent = StepOutcome::Failed(TaskFailure::permanent(DispatchError::Other("test"))); - let temporary = StepOutcome::Failed(TaskFailure::temporary(DispatchError::Other("test"))); - let fixed_cases = [ - ( - &StepOutcome::Executed, - StepErrorPolicy::ContinueNextStep, - StepControl::Advance, - ), - ( - &StepOutcome::Executed, - StepErrorPolicy::AbortCycle, - StepControl::Advance, - ), - (&StepOutcome::Executed, RETRY, StepControl::Advance), - ( - &StepOutcome::Stopped, - StepErrorPolicy::ContinueNextStep, - StepControl::CompleteCycle, - ), - ( - &StepOutcome::Stopped, - StepErrorPolicy::AbortCycle, - StepControl::CompleteCycle, - ), - (&StepOutcome::Stopped, RETRY, StepControl::CompleteCycle), - ( - &permanent, - StepErrorPolicy::ContinueNextStep, - StepControl::Advance, - ), - ( - &permanent, - StepErrorPolicy::AbortCycle, - StepControl::Terminate, - ), - (&permanent, RETRY, StepControl::Terminate), - ( - &temporary, - StepErrorPolicy::ContinueNextStep, - StepControl::Advance, - ), - ( - &temporary, - StepErrorPolicy::AbortCycle, - StepControl::Terminate, - ), - ]; - for (outcome, policy, expected) in fixed_cases { - assert_eq!(resolve_step_control(outcome, policy), expected); - } - assert_eq!( - resolve_step_control(&temporary, RETRY), - StepControl::SuspendCurrent, - ); - } - - #[test] - fn funding_unavailable_uses_the_same_control_owner() { - for policy in [ - StepErrorPolicy::ContinueNextStep, - StepErrorPolicy::AbortCycle, - ] { - assert_eq!( - resolve_step_control(&StepOutcome::FundingUnavailable, policy), - StepControl::Advance, - ); - } - assert_eq!( - resolve_step_control(&StepOutcome::FundingUnavailable, RETRY), - StepControl::SuspendCurrent, - ); - } - #[test] fn precondition_dnf_never_short_circuits_truth_or_error() { use polkadot_sdk::frame_support::traits::ConstU32; diff --git a/template/pallets/actors/src/lib.rs b/template/pallets/actors/src/lib.rs index d27dda67..03885738 100644 --- a/template/pallets/actors/src/lib.rs +++ b/template/pallets/actors/src/lib.rs @@ -1,5 +1,5 @@ #![cfg_attr(not(feature = "std"), no_std)] -#![recursion_limit = "256"] +#![recursion_limit = "512"] extern crate alloc; @@ -48,7 +48,7 @@ mod reactions; mod scheduler; mod subscriptions; -pub use scheduler::EnqueueOutcome; +pub use scheduler::{EnqueueOutcome, WakeupBucketDisposition}; pub mod adapters; pub use adapters::{ @@ -78,11 +78,33 @@ mod tests; #[cfg(feature = "runtime-benchmarks")] mod benchmarking; +/// Host read branches used by observation capture diagnostics. +#[cfg(feature = "runtime-benchmarks")] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum BenchmarkObservationReadState { + Missing, + Deactivated, + Uninitialized, + Stale, + Fresh, +} + #[cfg(feature = "runtime-benchmarks")] pub trait BenchmarkHelper { fn setup_add_liquidity( owner: &AccountId, ) -> Result<(AssetId, AssetId, Balance, Balance), polkadot_sdk::sp_runtime::DispatchError>; + /// Change host account spendability through its authorized lifecycle without moving custody. + fn set_asset_account_frozen( + _owner: &AccountId, + _who: &AccountId, + _asset: AssetId, + _frozen: bool, + ) -> polkadot_sdk::sp_runtime::DispatchResult { + Err(polkadot_sdk::sp_runtime::DispatchError::Other( + "BenchmarkAssetFreezeUnsupported", + )) + } fn setup_donate_liquidity( owner: &AccountId, ) -> Result<(AssetId, AssetId, Balance), polkadot_sdk::sp_runtime::DispatchError>; @@ -95,6 +117,24 @@ pub trait BenchmarkHelper { fn setup_unstake( owner: &AccountId, ) -> Result<(AssetId, Balance), polkadot_sdk::sp_runtime::DispatchError>; + /// Prepare distinct funded positions with maximum encoded host receipt-account state. + fn setup_max_encoded_staking_positions( + _owner: &AccountId, + _max: u32, + ) -> Result, polkadot_sdk::sp_runtime::DispatchError> { + Err(polkadot_sdk::sp_runtime::DispatchError::Other( + "BenchmarkMaximumStakingEncodingUnsupported", + )) + } + /// Remove an empty receipt through the host lifecycle after an Unstake Contract is admitted. + fn remove_empty_staking_receipt( + _owner: &AccountId, + _asset: AssetId, + ) -> polkadot_sdk::sp_runtime::DispatchResult { + Err(polkadot_sdk::sp_runtime::DispatchError::Other( + "BenchmarkReceiptRemovalUnsupported", + )) + } fn setup_swap_exact_in( owner: &AccountId, ) -> Result<(AssetId, AssetId, Balance), polkadot_sdk::sp_runtime::DispatchError>; @@ -106,10 +146,84 @@ pub trait BenchmarkHelper { owner: &AccountId, max: u32, ) -> Result, polkadot_sdk::sp_runtime::DispatchError>; + /// Prepare distinct assets with maximum encoded host balance-account state. + fn setup_max_encoded_predicate_assets( + _owner: &AccountId, + _max: u32, + ) -> Result, polkadot_sdk::sp_runtime::DispatchError> { + Err(polkadot_sdk::sp_runtime::DispatchError::Other( + "BenchmarkMaximumAssetEncodingUnsupported", + )) + } fn setup_observation_feeds( max: u32, ) -> Result, polkadot_sdk::sp_runtime::DispatchError>; + /// Prepare published feeds with maximum encoded host key, configuration and observation widths. + fn setup_max_encoded_observation_feeds( + _max: u32, + ) -> Result, polkadot_sdk::sp_runtime::DispatchError> { + Err(polkadot_sdk::sp_runtime::DispatchError::Other( + "BenchmarkMaximumObservationEncodingUnsupported", + )) + } + /// Prepare maximum-width observation reads, keeping populated neighbors for absent values. + fn setup_observation_read_state( + _max: u32, + _state: BenchmarkObservationReadState, + _max_age_blocks: u32, + ) -> Result, polkadot_sdk::sp_runtime::DispatchError> { + Err(polkadot_sdk::sp_runtime::DispatchError::Other( + "BenchmarkObservationReadStateUnsupported", + )) + } fn enable_asset_ops_ingress() {} + /// Publish a sample through the host observation owner, including revision and Actor ingress. + fn publish_observation( + _feed: ObservationFeedId, + _sample: u128, + ) -> polkadot_sdk::sp_runtime::DispatchResult { + Err(polkadot_sdk::sp_runtime::DispatchError::Other( + "BenchmarkObservationPublishUnsupported", + )) + } + /// Make a feed unavailable through the host observation owner's lifecycle. + fn deactivate_observation_feed( + _feed: ObservationFeedId, + ) -> polkadot_sdk::sp_runtime::DispatchResult { + Err(polkadot_sdk::sp_runtime::DispatchError::Other( + "BenchmarkObservationDeactivateUnsupported", + )) + } + /// Begin the next fixture block and advance its authoritative clock to the requested tick. + fn advance_to_scheduler_tick(_tick: u64) -> polkadot_sdk::sp_runtime::DispatchResult { + Err(polkadot_sdk::sp_runtime::DispatchError::Other( + "BenchmarkClockAdvanceUnsupported", + )) + } + /// Finalize the host clock before a fixture advances to another block. + fn finalize_scheduler_clock() -> polkadot_sdk::sp_runtime::DispatchResult { + Err(polkadot_sdk::sp_runtime::DispatchError::Other( + "BenchmarkClockFinalizeUnsupported", + )) + } + /// Encoded Contract-head witness only; collection does not require a published observation. + fn contract_head_observation_feed() + -> Result { + Self::setup_observation_feeds(1)?.into_iter().next().ok_or( + polkadot_sdk::sp_runtime::DispatchError::Other("BenchmarkContractHeadFeedUnsupported"), + ) + } + /// Move custody through the host's signed-transfer producer, including certified ingress. + fn transfer_signed( + _source: &AccountId, + _recipient: &AccountId, + _asset: AssetId, + _amount: Balance, + ) -> polkadot_sdk::sp_runtime::DispatchResult { + Err(polkadot_sdk::sp_runtime::DispatchError::Other( + "BenchmarkSignedTransferUnsupported", + )) + } fn setup_address_event_ingress( recipient: &AccountId, source: &AccountId, @@ -246,14 +360,20 @@ where Ok(AttemptFeeEnvelope { steps, total }) } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum FeeAssetClass { + FeeNative, + Other, +} + /// Returns the preserve-spend floor for a direct or adapter-reported debit surface. pub fn fee_native_protected_minimum( actor_type: types::ActorType, - is_fee_native: bool, + asset_class: FeeAssetClass, asset_minimum: Balance, min_user_balance: Balance, ) -> Balance { - if actor_type == types::ActorType::User && is_fee_native { + if actor_type == types::ActorType::User && asset_class == FeeAssetClass::FeeNative { min_user_balance } else { asset_minimum @@ -299,6 +419,7 @@ pub(crate) const fn contract_steps_bound_is_valid(bound: u32) -> bool { } sp_api::decl_runtime_apis! { + #[api_version(2)] pub trait ActorSimulationApi where Contract: codec::Codec, @@ -310,6 +431,7 @@ sp_api::decl_runtime_apis! { expected_mutability: types::Mutability, expected_contract: Contract, mode: types::SimulationMode, + budget: types::SimulationBudget, ) -> Result; } @@ -598,7 +720,6 @@ pub mod pallet { pub type AssetFilterOf = AssetFilter<::AssetId, ::MaxWhitelistSize>; pub type ActorObservationFeedsOf = BoundedVec<::ObservationFeedId, ConstU32<1>>; - pub type SimulationResultOf = SimulationResult<::MaxContractSteps>; pub type ObservationSubscriberPageOf = ObservationSubscriberPage<::ObservationPageSize>; pub type ObservationFreeSlotPageOf = BoundedVec::ObservationPageSize>; @@ -610,6 +731,18 @@ pub mod pallet { pub type CrossingTransitionQueueOf = BoundedVec::MaxCrossingTransitionsPerFeed>; + #[derive(Clone, Copy)] + pub(crate) enum MaterializationMinimumReservation { + ReserveAllFamilies, + Unavailable, + } + + impl MaterializationMinimumReservation { + fn reserves_all_families(self) -> bool { + matches!(self, Self::ReserveAllFamilies) + } + } + #[derive(Clone, Copy)] pub(crate) enum TriggerTransitionIntent { GenesisInstallation, @@ -766,6 +899,132 @@ pub mod pallet { >; pub type QueuePageOf = BoundedVec>, ::QueuePageSize>; + + /// Canonical movable identity authority. Sovereign account is derived from owner/class and is not + /// duplicated in every control cell. + #[derive( + Clone, Debug, Decode, DecodeWithMemTracking, Encode, Eq, MaxEncodedLen, PartialEq, TypeInfo, + )] + pub struct ActorControlIdentity { + pub owner: AccountId, + pub actor_class: ActorClass, + pub mutability: Mutability, + pub cycle_nonce: u64, + pub last_control_mutation_block: BlockNumber, + } + + /// Canonical mutable control authority. Ready ticket is represented by physical location; both + /// wakeup pointers remain explicit because process and Trigger temporal memberships can coexist. + #[derive( + Clone, Debug, Decode, DecodeWithMemTracking, Encode, Eq, MaxEncodedLen, PartialEq, TypeInfo, + )] + pub struct ActorControlHotState { + pub lifecycle: ActiveLifecycle, + pub cycle_state: CycleState, + pub trigger_runtime_state: TriggerRuntimeState, + pub unsuccessful_attempt_streak: u32, + pub pending_signal: bool, + pub wakeup_pointer: Option>, + pub trigger_wakeup_pointer: Option, + pub terminal_at: Option, + pub schedule_anchor: BlockNumber, + pub last_cycle_block: Option, + } + + /// Canonical single-owner control cell. + #[derive( + Clone, Debug, Decode, DecodeWithMemTracking, Encode, Eq, MaxEncodedLen, PartialEq, TypeInfo, + )] + pub struct ActorControlCell { + pub actor_id: ActorId, + pub identity: ActorControlIdentity, + pub hot: ActorControlHotState, + pub cursor: u32, + pub eligible_at: Option, + pub admission: Admission, + pub resources: ActorStepResourceEnvelope, + } + + pub type ActorControlCellOf = ActorControlCell< + ::AccountId, + BlockNumberFor, + ActorAdmissionCertificateOf, + >; + pub type ActorControlChunkOf = BoundedVec>, ConstU32<32>>; + + /// A deadline membership points at the single control owner without copying mutable authority. + /// Its clock is the containing Waiting key; the primary must carry the exact matching pointer. + #[derive( + Clone, Debug, Decode, DecodeWithMemTracking, Encode, Eq, MaxEncodedLen, PartialEq, TypeInfo, + )] + pub struct ActorWakeupReference { + pub actor_id: ActorId, + pub admission_identity: [u8; 32], + } + + #[derive( + Clone, Debug, Decode, DecodeWithMemTracking, Encode, Eq, MaxEncodedLen, PartialEq, TypeInfo, + )] + pub enum ActorWaitingEntry { + Primary(Cell), + Reference(ActorWakeupReference), + } + + impl ActorWaitingEntry { + pub fn primary(&self) -> Option<&Cell> { + match self { + Self::Primary(cell) => Some(cell), + Self::Reference(_) => None, + } + } + + pub fn primary_mut(&mut self) -> Option<&mut Cell> { + match self { + Self::Primary(cell) => Some(cell), + Self::Reference(_) => None, + } + } + + pub fn into_primary(self) -> Option { + match self { + Self::Primary(cell) => Some(cell), + Self::Reference(_) => None, + } + } + } + + pub type ActorWaitingChunkOf = + BoundedVec>>, ConstU32<32>>; + pub type ActorWaitingPageOf = WakeupPage>; + + /// External-boundary location; execution writes but never reads this index. + #[derive( + Clone, + Copy, + Debug, + Decode, + DecodeWithMemTracking, + Encode, + Eq, + MaxEncodedLen, + PartialEq, + TypeInfo, + )] + pub enum ActorControlLocation { + Unsignaled, + Waiting { + key: WakeupKey, + page: u64, + slot: u8, + }, + Ready { + ticket: QueueTicket, + }, + } + + // Temporary source aliases remain only while the atomic cutover is assembled in this + // diagnostic worktree; the retained production patch removes them after caller conversion. + pub type WakeupPageEntriesOf = BoundedVec, ::WakeupPageSize>; pub type WakeupPageOf = WakeupPage>; pub type WakeupCursorPageOf = @@ -810,6 +1069,7 @@ pub mod pallet { pub identity: ActorIdentityOf, pub hot: ActorHotStateOf, pub authority: ActorActivationAuthorityOf, + pub admission: Option>, pub run_head: Option>, pub loaded_step: Option>, } @@ -824,11 +1084,6 @@ pub mod pallet { #[pallet::getter(fn next_actor_id)] pub type NextActorId = StorageValue<_, ActorId, ValueQuery>; - #[pallet::storage] - #[pallet::getter(fn actor_hot)] - pub type ActorHot = - StorageMap<_, Blake2_128Concat, ActorId, ActorHotStateOf, OptionQuery>; - #[pallet::storage] #[pallet::storage_prefix = "ActorContractHead"] pub type ActorContractHeads = @@ -839,11 +1094,6 @@ pub mod pallet { 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< @@ -1033,13 +1283,23 @@ pub mod pallet { 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) + let actor_type = Self::load_frame_control_authority(actor_id) + .map(|(_, identity, _, _)| identity.actor_class.actor_type()) + .ok_or(Error::::ActorInvariant)?; + Self::insert_admitted_contract_geometry_with_actor_type( + actor_id, + actor_type, + &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) { + let phase = match Self::load_frame_control_authority(actor_id) + .map(|(_, _, hot, _)| hot.trigger_runtime_state) + { Some(TriggerRuntimeState::ObservationCrossing { phase, .. }) => phase, _ => return Err(Error::::ActorInvariant.into()), }; @@ -1082,6 +1342,7 @@ pub mod pallet { } } + #[cfg(feature = "runtime-benchmarks")] pub(crate) fn remove_actor_contract(actor_id: ActorId) -> DispatchResult { ensure!( Self::remove_admitted_contract_geometry(actor_id).is_some(), @@ -1090,21 +1351,56 @@ pub mod pallet { Ok(()) } - pub(crate) fn insert_admitted_contract_geometry( + #[cfg(all(test, feature = "runtime-benchmarks"))] + pub(crate) fn control_remove_frame_owned_contract_geometry( actor_id: ActorId, contract: &ActorContractOf, - certificate: &ActorAdmissionCertificateOf, ) -> bool { - if ActorContractHeads::::contains_key(actor_id) - || ActorAdmissionCertificates::::contains_key(actor_id) - { + if !ActorContractHeads::::contains_key(actor_id) { + return false; + } + let Some(chunk_count) = u32::try_from(contract.steps.len()) + .ok() + .map(|count| count.saturating_sub(1).div_ceil(MAX_STEPS_PER_TAIL_CHUNK)) + else { return false; + }; + ActorActivationAuthorities::::remove(actor_id); + ActorContractHeads::::remove(actor_id); + for chunk_index in 0..chunk_count { + ActorContractTailChunks::::remove(actor_id, chunk_index); } - // 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) + true + } + + #[cfg(any(test, feature = "runtime-benchmarks"))] + pub(crate) fn insert_admitted_contract_geometry( + actor_id: ActorId, + contract: &ActorContractOf, + certificate: &ActorAdmissionCertificateOf, + ) -> bool { + // Deliberately malformed storage fixtures may have no identity owner. They retain System's + // zero-fee projection solely as a package-test corruption seam. + let actor_type = Self::load_control_identity(actor_id) .map(|identity| identity.actor_class.actor_type()) .unwrap_or(ActorType::System); + Self::insert_admitted_contract_geometry_with_actor_type( + actor_id, + actor_type, + contract, + certificate, + ) + } + + fn insert_admitted_contract_geometry_with_actor_type( + actor_id: ActorId, + actor_type: ActorType, + contract: &ActorContractOf, + certificate: &ActorAdmissionCertificateOf, + ) -> bool { + if ActorContractHeads::::contains_key(actor_id) { + return false; + } let Some((head, chunks)) = Self::decompose_admitted_contract_geometry(actor_id, actor_type, contract, certificate) else { @@ -1118,7 +1414,6 @@ pub mod pallet { { 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); @@ -1126,15 +1421,11 @@ pub mod pallet { true } - #[allow( - dead_code, - reason = "I2 bounded reconstruction is staged behind the centralized Contract owner" - )] - pub(crate) fn load_admitted_contract_geometry( + pub(crate) fn load_contract_geometry_with_admission( actor_id: ActorId, - ) -> Option<(ActorContractOf, ActorAdmissionCertificateOf)> { + certificate: &ActorAdmissionCertificateOf, + ) -> Option> { 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 @@ -1165,20 +1456,31 @@ pub mod pallet { if resource_count != head.header.step_count as usize { return None; } - let contract = Self::reconstruct_contract_geometry(actor_id, head, &chunks)?; + Self::reconstruct_contract_geometry(actor_id, head, &chunks) + } + + pub(crate) fn load_admitted_contract_geometry( + actor_id: ActorId, + ) -> Option<(ActorContractOf, ActorAdmissionCertificateOf)> { + let certificate = Self::load_control_admission(actor_id)?; + let contract = Self::load_contract_geometry_with_admission(actor_id, &certificate)?; 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 certificate = Self::load_control_admission(actor_id)?; + Self::load_current_step_with_admission(actor_id, cursor, &certificate) + } + + pub(crate) fn load_current_step_with_admission( + actor_id: ActorId, + cursor: u32, + certificate: &ActorAdmissionCertificateOf, ) -> Option> { let head = ActorContractHeads::::get(actor_id)?; - let certificate = ActorAdmissionCertificates::::get(actor_id)?; let tail_chunk = if cursor == 0 { None } else { @@ -1191,7 +1493,7 @@ pub mod pallet { Self::load_current_step_from_geometry( actor_id, &head, - &certificate, + certificate, cursor, tail_chunk.as_ref().map(|(index, chunk)| (*index, chunk)), ) @@ -1202,12 +1504,15 @@ pub mod pallet { contract: &ActorContractOf, certificate: &ActorAdmissionCertificateOf, ) -> bool { - let Some((current_contract, _)) = Self::load_admitted_contract_geometry(actor_id) else { + let Some((_, identity, _, admission)) = Self::load_frame_control_authority(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(current_contract) = + Self::load_contract_geometry_with_admission(actor_id, &admission) + else { + return false; + }; + let actor_type = identity.actor_class.actor_type(); let Some((head, chunks)) = Self::decompose_admitted_contract_geometry(actor_id, actor_type, contract, certificate) else { @@ -1222,7 +1527,9 @@ pub mod pallet { let Ok(new_chunk_count) = u32::try_from(chunks.len()) else { return false; }; - ActorAdmissionCertificates::::insert(actor_id, certificate); + if !Self::replace_control_admission_for_transition(actor_id, certificate, contract) { + return false; + } ActorContractHeads::::insert(actor_id, head); for (chunk_index, chunk) in chunks { ActorContractTailChunks::::insert(actor_id, chunk_index, chunk); @@ -1237,12 +1544,26 @@ pub mod pallet { actor_id: ActorId, ) -> Option> { let (contract, _) = Self::load_admitted_contract_geometry(actor_id)?; + Self::remove_loaded_contract_geometry(actor_id, contract) + } + + pub(crate) fn remove_admitted_contract_geometry_with_admission( + actor_id: ActorId, + admission: &ActorAdmissionCertificateOf, + ) -> Option> { + let contract = Self::load_contract_geometry_with_admission(actor_id, admission)?; + Self::remove_loaded_contract_geometry(actor_id, contract) + } + + fn remove_loaded_contract_geometry( + actor_id: ActorId, + contract: ActorContractOf, + ) -> Option> { 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); @@ -1250,10 +1571,7 @@ pub mod pallet { Some(contract) } - #[allow( - dead_code, - reason = "I4 dual-meter admission is staged before scheduler plan wiring" - )] + #[cfg(test)] pub(crate) fn current_step_resources_fit( control_meter: &WeightMeter, effect_meter: &WeightMeter, @@ -1505,33 +1823,32 @@ pub mod pallet { .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)? + let (opening_snapshot_entries, opening_predicate_results, funding_snapshot_entries) = + if cursor == 0 { + if instance.cycle_state == CycleState::Idle { + let (snapshots, predicates) = Self::opening_control_geometry(&instance.steps)?; + (snapshots, predicates, T::MaxFundingTrackedAssets::get()) + } else { + let run = run?; + ( + u32::try_from(run.opening_snapshot.len()).ok()?, + u32::try_from(run.opening_predicate_results.len()).ok()?, + u32::try_from(run.funding_snapshot.len()).ok()?, + ) + } } else { - let run = run?; - ( - u32::try_from(run.opening_snapshot.len()).ok()?, - u32::try_from(run.opening_predicate_results.len()).ok()?, - ) - } - } else { - (0, 0) - }; + (0, 0, 0) + }; Self::step_control_weight_context( step_count, cursor, predicate_evaluation_units, opening_snapshot_entries, opening_predicate_results, - T::MaxFundingTrackedAssets::get(), + funding_snapshot_entries, ) } - #[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> { @@ -1566,10 +1883,6 @@ pub mod pallet { .ok() } - #[allow( - dead_code, - reason = "I2 certificate construction is staged before centralized Contract persistence" - )] pub(crate) fn build_admission_certificate( contract: &ActorContractOf, ) -> Option> { @@ -1585,10 +1898,6 @@ pub mod pallet { )) } - #[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, @@ -1739,6 +2048,7 @@ pub mod pallet { ticket, loaded_step, maximum_fee, + last_step_outcome: None, }) } @@ -1750,11 +2060,11 @@ pub mod pallet { return None; } let actor_id = ticket.actor_id; - let identity = ActorIdentities::::get(actor_id)?; - let hot = ActorHot::::get(actor_id)?; + let identity = Self::load_control_identity(actor_id)?; + let hot = Self::load_control_hot(actor_id)?; let run = ActorRunStateStore::::get(actor_id); let funding = ActorFunding::::get(actor_id)?; - let admission = ActorAdmissionCertificates::::get(actor_id)?; + let admission = Self::load_control_admission(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) @@ -1772,45 +2082,555 @@ pub mod pallet { ) } - pub(crate) fn validate_loaded_step_authority( + pub(crate) fn control_identity_from_scalar( + identity: ActorIdentityOf, + ) -> Option>> { + let sovereign_account = match identity.actor_class { + ActorClass::User { owner_slot } => Self::sovereign_account_id(&identity.owner, owner_slot), + ActorClass::System { sovereign_id } => Self::sovereign_account_id_system(sovereign_id), + }; + if sovereign_account != identity.sovereign_account { + return None; + } + Some(ActorControlIdentity { + owner: identity.owner, + actor_class: identity.actor_class, + mutability: identity.mutability, + cycle_nonce: identity.cycle_nonce, + last_control_mutation_block: identity.last_control_mutation_block, + }) + } + + pub(crate) fn control_hot_from_scalar( + hot: ActorHotStateOf, + ) -> ActorControlHotState> { + ActorControlHotState { + lifecycle: hot.lifecycle, + cycle_state: hot.cycle_state, + trigger_runtime_state: hot.trigger_runtime_state, + unsuccessful_attempt_streak: hot.unsuccessful_attempt_streak, + pending_signal: hot.pending_signal, + wakeup_pointer: hot.wakeup_pointer, + trigger_wakeup_pointer: hot.trigger_wakeup_pointer, + terminal_at: hot.terminal_at, + schedule_anchor: hot.schedule_anchor, + last_cycle_block: hot.last_cycle_block, + } + } + + pub(crate) fn control_identity_to_scalar( + identity: &ActorControlIdentity>, + ) -> ActorIdentityOf { + let sovereign_account = match identity.actor_class { + ActorClass::User { owner_slot } => Self::sovereign_account_id(&identity.owner, owner_slot), + ActorClass::System { sovereign_id } => Self::sovereign_account_id_system(sovereign_id), + }; + ActorIdentity { + sovereign_account, + owner: identity.owner.clone(), + actor_class: identity.actor_class, + mutability: identity.mutability, + cycle_nonce: identity.cycle_nonce, + last_control_mutation_block: identity.last_control_mutation_block, + } + } + + pub(crate) fn control_hot_to_scalar( + hot: &ActorControlHotState>, + queue_ticket: Option, + ) -> ActorHotStateOf { + ActorHotState { + lifecycle: hot.lifecycle, + cycle_state: hot.cycle_state, + trigger_runtime_state: hot.trigger_runtime_state.clone(), + unsuccessful_attempt_streak: hot.unsuccessful_attempt_streak, + pending_signal: hot.pending_signal, + queue_ticket, + wakeup_pointer: hot.wakeup_pointer, + trigger_wakeup_pointer: hot.trigger_wakeup_pointer, + terminal_at: hot.terminal_at, + schedule_anchor: hot.schedule_anchor, + last_cycle_block: hot.last_cycle_block, + } + } + + #[cfg(feature = "runtime-benchmarks")] + pub(crate) fn control_cell_from_parts( actor_id: ActorId, - queue_ticket: QueueTicket, - certificate: &ActorAdmissionCertificateOf, - ticket: &ActorStepTicketOf, + identity: ActorIdentityOf, + hot: ActorHotStateOf, + admission: ActorAdmissionCertificateOf, 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 + eligible_at: Option>, + ) -> Option> { + if !admission.has_valid_identity() { + return None; + } + Some(ActorControlCell { + actor_id, + identity: Self::control_identity_from_scalar(identity)?, + hot: Self::control_hot_from_scalar(hot), + cursor: loaded_step.cursor, + eligible_at, + admission, + resources: loaded_step.resources, + }) } - pub(crate) fn load_current_step_from_geometry( + #[cfg(feature = "runtime-benchmarks")] + pub(crate) fn control_opening_cell_from_scalar( 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 + identity: ActorIdentityOf, + hot: ActorHotStateOf, + admission: ActorAdmissionCertificateOf, + ticket: &ActorStepTicketOf, + loaded_step: &LoadedActorStepOf, + ) -> Option> { + if hot.cycle_state != CycleState::Idle + || hot.queue_ticket != Some(ticket.ticket) + || ticket.actor_id != actor_id + || ticket.cursor != 0 + || ticket.cycle_nonce != identity.cycle_nonce.checked_add(1)? + || ticket.eligible_at > frame_system::Pallet::::block_number() + || !Self::validate_loaded_step_authority( + actor_id, + ticket.ticket, + &admission, + ticket, + loaded_step, + ) { 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?, - }); - } + Self::control_cell_from_parts( + actor_id, + identity, + hot, + admission, + loaded_step, + Some(ticket.eligible_at), + ) + } + + #[cfg(feature = "runtime-benchmarks")] + #[allow( + dead_code, + reason = "zero-Step frame projection remains test-only until the atomic production cutover" + )] + pub(crate) fn control_zero_step_opening_cell_from_scalar( + actor_id: ActorId, + identity: ActorIdentityOf, + hot: ActorHotStateOf, + admission: ActorAdmissionCertificateOf, + ticket: &ActorStepTicketOf, + ) -> Option> { + if hot.cycle_state != CycleState::Idle + || hot.queue_ticket != Some(ticket.ticket) + || ticket.actor_id != actor_id + || ticket.cursor != 0 + || ticket.cycle_nonce != identity.cycle_nonce.checked_add(1)? + || ticket.eligible_at > frame_system::Pallet::::block_number() + || !admission.has_valid_identity() + { + return None; + } + Some(ActorControlCell { + actor_id, + identity: Self::control_identity_from_scalar(identity)?, + hot: Self::control_hot_from_scalar(hot), + cursor: 0, + eligible_at: Some(ticket.eligible_at), + admission, + resources: ActorStepResourceEnvelope { + control: T::WeightInfo::scheduler_inner_zero_step_complete(), + effect: Weight::zero(), + }, + }) + } + + #[cfg(feature = "runtime-benchmarks")] + #[allow( + dead_code, + reason = "zero-Step frame projection remains test-only until the atomic production cutover" + )] + pub(crate) fn control_zero_step_unsignaled_cell_from_scalar( + actor_id: ActorId, + identity: ActorIdentityOf, + hot: ActorHotStateOf, + admission: ActorAdmissionCertificateOf, + ) -> Option> { + if hot.cycle_state != CycleState::Idle + || hot.pending_signal + || hot.queue_ticket.is_some() + || hot.wakeup_pointer.is_some() + || hot.trigger_wakeup_pointer.is_some() + || !admission.has_valid_identity() + { + return None; + } + Some(ActorControlCell { + actor_id, + identity: Self::control_identity_from_scalar(identity)?, + hot: Self::control_hot_from_scalar(hot), + cursor: 0, + eligible_at: None, + admission, + resources: ActorStepResourceEnvelope { + control: T::WeightInfo::scheduler_inner_zero_step_complete(), + effect: Weight::zero(), + }, + }) + } + + #[cfg(feature = "runtime-benchmarks")] + pub(crate) fn control_unsignaled_cell_from_scalar( + actor_id: ActorId, + identity: ActorIdentityOf, + hot: ActorHotStateOf, + admission: ActorAdmissionCertificateOf, + loaded_step: &LoadedActorStepOf, + ) -> Option> { + if hot.cycle_state != CycleState::Idle + || hot.pending_signal + || hot.queue_ticket.is_some() + || hot.wakeup_pointer.is_some() + || hot.trigger_wakeup_pointer.is_some() + || loaded_step.cursor != 0 + { + return None; + } + Self::control_cell_from_parts(actor_id, identity, hot, admission, loaded_step, None) + } + + pub(crate) fn project_control_cell( + cell: &ActorControlCellOf, + location: ActorControlLocation>, + ) -> Option<( + ActorIdentityOf, + ActorHotStateOf, + ActorAdmissionCertificateOf, + )> { + if !cell.admission.has_valid_identity() { + return None; + } + let queue_ticket = match location { + ActorControlLocation::Unsignaled => { + if cell.cursor != 0 + || cell.eligible_at.is_some() + || (cell.hot.pending_signal && !cell.hot.lifecycle.is_paused()) + || cell.hot.cycle_state != CycleState::Idle + { + return None; + } + if let Some(pointer) = cell.hot.wakeup_pointer { + let WakeupKey::Block(terminal_at) = pointer.block else { + return None; + }; + if cell.hot.terminal_at != Some(terminal_at) + || !Self::wakeup_page_entry_matches(pointer, cell.actor_id) + { + return None; + } + } + if let Some(pointer) = cell.hot.trigger_wakeup_pointer { + if ActorControlLocators::::get(cell.actor_id) + != Some(ActorControlLocation::Unsignaled) + { + return None; + } + let reference = WakeupPointer { + block: WakeupKey::Tick(pointer.tick), + page_id: pointer.page_id, + slot: pointer.slot, + }; + if !Self::wakeup_page_entry_matches(reference, cell.actor_id) { + return None; + } + } + None + } + ActorControlLocation::Ready { ticket } => { + let terminal_ready = cell.hot.cycle_state == CycleState::Idle + && !cell.hot.pending_signal + && cell + .hot + .terminal_at + .zip(cell.eligible_at) + .is_some_and(|(terminal_at, eligible_at)| eligible_at >= terminal_at); + if cell.eligible_at.is_none() + || !(matches!( + cell.hot.cycle_state, + CycleState::Running | CycleState::Suspended + ) || (cell.hot.cycle_state == CycleState::Idle && cell.hot.pending_signal) + || terminal_ready) + { + return None; + } + Some(ticket) + } + ActorControlLocation::Waiting { key, .. } => { + if let Some(pointer) = cell.hot.wakeup_pointer { + let terminal_waiting = cell.hot.cycle_state == CycleState::Idle + && !cell.hot.pending_signal + && cell + .hot + .terminal_at + .zip(cell.eligible_at) + .is_some_and(|(terminal_at, eligible_at)| eligible_at >= terminal_at); + if pointer.block != key + || cell.eligible_at.is_none() + || !(matches!( + cell.hot.cycle_state, + CycleState::Running | CycleState::Suspended + ) || (cell.hot.cycle_state == CycleState::Idle && cell.hot.pending_signal) + || terminal_waiting) + { + return None; + } + } else { + let pointer = cell.hot.trigger_wakeup_pointer?; + let reference = WakeupPointer { + block: WakeupKey::Tick(pointer.tick), + page_id: pointer.page_id, + slot: pointer.slot, + }; + if key != reference.block + || cell.eligible_at.is_some() + || cell.hot.pending_signal + || cell.hot.cycle_state != CycleState::Idle + { + return None; + } + } + None + } + }; + Some(( + Self::control_identity_to_scalar(&cell.identity), + Self::control_hot_to_scalar(&cell.hot, queue_ticket), + cell.admission.clone(), + )) + } + + pub(crate) fn insert_unsignaled_control_authority( + actor_id: ActorId, + identity: ActorIdentityOf, + hot: ActorHotStateOf, + admission: ActorAdmissionCertificateOf, + resources: ActorStepResourceEnvelope, + ) -> bool { + if ActorControlLocators::::contains_key(actor_id) + || ActorUnsignaledControlCells::::contains_key(actor_id) + || hot.cycle_state != CycleState::Idle + || (hot.pending_signal && !hot.lifecycle.is_paused()) + || hot.queue_ticket.is_some() + || hot.wakeup_pointer.is_some() + || !admission.has_valid_identity() + { + return false; + } + let Some(control_identity) = Self::control_identity_from_scalar(identity.clone()) else { + return false; + }; + let cell = ActorControlCell { + actor_id, + identity: control_identity, + hot: Self::control_hot_from_scalar(hot.clone()), + cursor: 0, + eligible_at: None, + admission: admission.clone(), + resources, + }; + ActorUnsignaledControlCells::::insert(actor_id, cell); + ActorControlLocators::::insert(actor_id, ActorControlLocation::Unsignaled); + Self::load_frame_control_authority(actor_id).is_some_and( + |(location, restored_identity, restored_hot, restored_admission)| { + location == ActorControlLocation::Unsignaled + && restored_identity == identity + && restored_hot == hot + && restored_admission == admission + }, + ) + } + + pub(crate) fn load_frame_control_authority( + actor_id: ActorId, + ) -> Option<( + ActorControlLocation>, + ActorIdentityOf, + ActorHotStateOf, + ActorAdmissionCertificateOf, + )> { + let (location, cell) = Self::load_primary_control_cell(actor_id).ok()?; + let (identity, hot, admission) = Self::project_control_cell(&cell, location)?; + Some((location, identity, hot, admission)) + } + + pub(crate) fn load_control_authority_with_authority( + actor_id: ActorId, + ) -> Option<( + ActorIdentityOf, + ActorHotStateOf, + ActorAdmissionCertificateOf, + )> { + let (_, identity, hot, admission) = Self::load_frame_control_authority(actor_id)?; + Some((identity, hot, admission)) + } + + #[cfg(feature = "runtime-benchmarks")] + pub(crate) fn store_frame_control_authority( + actor_id: ActorId, + location: ActorControlLocation>, + identity: ActorIdentityOf, + hot: ActorHotStateOf, + admission: ActorAdmissionCertificateOf, + ) -> bool { + let expected_ticket = match location { + ActorControlLocation::Ready { ticket } => Some(ticket), + ActorControlLocation::Unsignaled | ActorControlLocation::Waiting { .. } => None, + }; + if hot.queue_ticket != expected_ticket || !admission.has_valid_identity() { + return false; + } + let Ok((stored_location, mut cell)) = Self::load_primary_control_cell(actor_id) else { + return false; + }; + if stored_location != location { + return false; + } + let Some(control_identity) = Self::control_identity_from_scalar(identity.clone()) else { + return false; + }; + cell.identity = control_identity; + cell.hot = Self::control_hot_from_scalar(hot.clone()); + cell.admission = admission.clone(); + let Some((restored_identity, restored_hot, restored_admission)) = + Self::project_control_cell(&cell, location) + else { + return false; + }; + if restored_identity != identity || restored_hot != hot || restored_admission != admission { + return false; + } + Self::store_primary_control_cell(location, cell).is_ok() + } + + #[cfg(all(test, feature = "runtime-benchmarks"))] + pub(crate) fn control_load_frame_contract( + actor_id: ActorId, + admission: &ActorAdmissionCertificateOf, + ) -> Option<(ActorContractOf, ActorContractHeadOf)> { + let head = ActorContractHeads::::get(actor_id)?; + if !admission.has_valid_identity() + || admission.semantic_contract_id != head.header.semantic_contract_id + || admission.body_commitment != head.header.body_commitment + || admission.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 contract = Self::reconstruct_contract_geometry(actor_id, head.clone(), &chunks)?; + Some((contract, head)) + } + + #[cfg(all(test, feature = "runtime-benchmarks"))] + pub(crate) fn control_load_current_step_contract( + actor_id: ActorId, + admission: &ActorAdmissionCertificateOf, + cursor: u32, + ) -> Option<( + ActorContractOf, + LoadedActorStepOf, + ActorContractHeadOf, + )> { + let head = ActorContractHeads::::get(actor_id)?; + if cursor >= head.header.step_count + || !admission.has_valid_identity() + || admission.semantic_contract_id != head.header.semantic_contract_id + || admission.body_commitment != head.header.body_commitment + || admission.admission_identity != head.header.admission_identity + { + 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 = Self::load_current_step_from_geometry( + actor_id, + &head, + admission, + cursor, + tail_chunk.as_ref().map(|(index, chunk)| (*index, chunk)), + )?; + let contract = ActorContract { + trigger: head.header.trigger.clone(), + cooldown_blocks: head.header.cooldown_blocks, + window: head.header.window, + steps: BoundedVec::try_from(alloc::vec![loaded_step.step.clone()]).ok()?, + funding: head.header.funding.clone(), + completion: head.header.completion, + auto_close_at_cycle_nonce: head.header.auto_close_at_cycle_nonce, + }; + Some((contract, loaded_step, head)) + } + + 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)?)?; @@ -1838,10 +2658,6 @@ pub mod pallet { }) } - #[allow( - dead_code, - reason = "I2 geometry decomposition is staged before production storage wiring" - )] pub(crate) fn decompose_contract_geometry( actor_id: ActorId, contract: &ActorContractOf, @@ -1908,10 +2724,6 @@ pub mod pallet { )) } - #[allow( - dead_code, - reason = "I2 bounded reconstruction is staged before production storage wiring" - )] pub(crate) fn reconstruct_contract_geometry( actor_id: ActorId, head: ActorContractHeadOf, @@ -2055,6 +2867,7 @@ pub mod pallet { window: contract.window, steps: contract.steps, completion: contract.completion, + trigger_runtime_state: hot.trigger_runtime_state.clone(), cycle_nonce: identity.cycle_nonce, auto_close_at_cycle_nonce: contract.auto_close_at_cycle_nonce, unsuccessful_attempt_streak: hot.unsuccessful_attempt_streak, @@ -2070,55 +2883,248 @@ pub mod pallet { } } + /// Active identity belongs to the primary; the separate registry owns dormant identities. + pub(crate) fn control_identity_exists(actor_id: ActorId) -> bool { + ActorControlLocators::::contains_key(actor_id) + || ActorUnsignaledControlCells::::contains_key(actor_id) + || ActorIdentities::::contains_key(actor_id) + } + + pub(crate) fn load_control_identity(actor_id: ActorId) -> Option> { + if ActorControlLocators::::contains_key(actor_id) { + return Self::load_frame_control_authority(actor_id).map(|(_, identity, _, _)| identity); + } + if ActorUnsignaledControlCells::::contains_key(actor_id) { + return None; + } + ActorIdentities::::get(actor_id) + } + + pub(crate) fn control_hot_exists(actor_id: ActorId) -> bool { + ActorControlLocators::::contains_key(actor_id) + || ActorUnsignaledControlCells::::contains_key(actor_id) + } + + #[cfg(any(test, feature = "runtime-benchmarks"))] + pub(crate) fn load_control_hot(actor_id: ActorId) -> Option> { + Self::load_frame_control_authority(actor_id).map(|(_, _, hot, _)| hot) + } + + #[cfg(feature = "try-runtime")] + pub(crate) fn control_hot_entries_for_try_state() -> Option)>> + { + Self::frame_control_entries()? + .into_iter() + .map(|(actor_id, location, cell)| { + let (_, hot, _) = Self::project_control_cell(&cell, location)?; + Some((actor_id, hot)) + }) + .collect() + } + + #[cfg(all(test, feature = "runtime-benchmarks"))] + pub(crate) fn load_control_head( + actor_id: ActorId, + ) -> Option<(ActorIdentityOf, ActorHotStateOf)> { + Self::load_frame_control_authority(actor_id).map(|(_, identity, hot, _)| (identity, hot)) + } + + /// In-place mutation requires a live primary; moving transitions publish a supplied successor. + pub(crate) fn try_store_control_hot_with_authority( + actor_id: ActorId, + hot: ActorHotStateOf, + ) -> Result<(), crate::scheduler::EnqueueOutcome> { + if !ActorControlLocators::::contains_key(actor_id) { + return Err(crate::scheduler::EnqueueOutcome::CorruptedTopology); + } + Self::update_existing_frame_control_hot(actor_id, &hot) + } + + /// Mutates the physical primary without introducing a second hot-state owner. + pub(crate) fn try_mutate_control_hot( + actor_id: ActorId, + missing: Error, + mutate: impl FnOnce(&mut ActorHotStateOf) -> Result, + ) -> Result { + let (_, _, mut hot, _) = Self::load_frame_control_authority(actor_id).ok_or(missing)?; + let output = mutate(&mut hot)?; + Self::update_existing_frame_control_hot(actor_id, &hot) + .map_err(|_| Error::::ActorInvariant)?; + Ok(output) + } + + #[cfg(all(test, not(feature = "runtime-benchmarks")))] + pub(crate) fn mutate_control_hot_or( + actor_id: ActorId, + fallback: R, + mutate: impl FnOnce(&mut ActorHotStateOf) -> R, + ) -> R { + let Some((_, _, mut hot, _)) = Self::load_frame_control_authority(actor_id) else { + return fallback; + }; + let output = mutate(&mut hot); + match Self::update_existing_frame_control_hot(actor_id, &hot) { + Ok(()) => output, + Err(_) => fallback, + } + } + + pub(crate) fn try_mutate_control_hot_with_authority( + actor_id: ActorId, + missing: Error, + + mutate: impl FnOnce(&mut ActorHotStateOf) -> Result, + ) -> Result { + Self::try_mutate_control_hot(actor_id, missing, mutate) + } + + /// Identity mutation follows its active-primary or dormant-registry owner. + #[cfg(test)] + pub(crate) fn try_mutate_control_identity( + actor_id: ActorId, + missing: Error, + mutate: impl FnOnce(&mut ActorIdentityOf) -> Result, + ) -> Result { + if ActorControlLocators::::contains_key(actor_id) { + let (_, mut identity, _, _) = + Self::load_frame_control_authority(actor_id).ok_or(Error::::ActorInvariant)?; + let output = mutate(&mut identity)?; + Self::update_existing_frame_control_identity(actor_id, &identity) + .map_err(|_| Error::::ActorInvariant)?; + return Ok(output); + } + ensure!( + !ActorUnsignaledControlCells::::contains_key(actor_id), + Error::::ActorInvariant + ); + ActorIdentities::::try_mutate(actor_id, |maybe| { + let identity = maybe.as_mut().ok_or(missing)?; + mutate(identity) + }) + } + + /// Admission is carried by the sole active primary. + pub(crate) fn control_admission_exists(actor_id: ActorId) -> bool { + Self::load_control_admission(actor_id).is_some() + } + + pub(crate) fn load_control_admission( + actor_id: ActorId, + ) -> Option> { + Self::load_frame_control_authority(actor_id).map(|(_, _, _, admission)| admission) + } + + /// Replaces admission and current-Step resources in the existing primary. Source-consumed + /// transitions must carry admission explicitly into their destination publication. + pub(crate) fn replace_control_admission_for_transition( + actor_id: ActorId, + certificate: &ActorAdmissionCertificateOf, + contract: &ActorContractOf, + ) -> bool { + let Ok((location, mut cell)) = Self::load_primary_control_cell(actor_id) else { + return false; + }; + let Some(resources) = Self::derive_step_resource_envelopes(contract).and_then(|resources| { + if contract.steps.is_empty() { + Some(ActorStepResourceEnvelope { + control: T::WeightInfo::scheduler_inner_zero_step_complete(), + effect: Weight::zero(), + }) + } else { + resources.get(cell.cursor as usize).copied() + } + }) else { + return false; + }; + let pointers = [ + cell.hot.wakeup_pointer, + cell + .hot + .trigger_wakeup_pointer + .map(|pointer| WakeupPointer { + block: WakeupKey::Tick(pointer.tick), + page_id: pointer.page_id, + slot: pointer.slot, + }), + ]; + let mut reference_updates = Vec::with_capacity(2); + for pointer in pointers.into_iter().flatten() { + let key = (pointer.block, pointer.page_id); + let Some(mut page) = ActorWaitingFrameChunks::::get(key) else { + return false; + }; + match page + .entries + .get_mut(pointer.slot as usize) + .and_then(Option::as_mut) + { + Some(ActorWaitingEntry::Reference(reference)) + if reference.actor_id == actor_id + && reference.admission_identity == cell.admission.admission_identity => + { + reference.admission_identity = certificate.admission_identity; + reference_updates.push((key, page)); + } + Some(ActorWaitingEntry::Primary(primary)) + if primary.actor_id == actor_id + && primary.admission.admission_identity == cell.admission.admission_identity + && location + == (ActorControlLocation::Waiting { + key: pointer.block, + page: pointer.page_id, + slot: pointer.slot as u8, + }) => {} + _ => return false, + } + } + cell.admission = certificate.clone(); + cell.resources = resources; + if Self::store_primary_control_cell(location, cell).is_err() { + return false; + } + for (key, page) in reference_updates { + ActorWaitingFrameChunks::::insert(key, page); + } + true + } + 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 admitted_contract = Self::load_admitted_contract_geometry(actor_id); + if ActorControlLocators::::contains_key(actor_id) { + let state = Self::load_frame_actor_state(actor_id); + let admission = match &state { + LoadedActorStateOf::Active(_) => Self::load_control_admission(actor_id), + _ => None, + }; + return (state, admission); + } + if ActorUnsignaledControlCells::::contains_key(actor_id) + || ActorContractHeads::::contains_key(actor_id) + || ActorActivationAuthorities::::contains_key(actor_id) + || ActorRunHeads::::contains_key(actor_id) + || ActorRunPayloads::::contains_key(actor_id) + || ActorFunding::::contains_key(actor_id) + || as polkadot_sdk::frame_support::storage::StorageDoubleMap< + ActorId, + u32, + ActorStepChunkOf, + >>::contains_prefix(actor_id) + { + return (LoadedActorStateOf::Corrupt, None); + } + let identity = Self::load_control_identity(actor_id); + if identity.is_none() && ActorIdentities::::contains_key(actor_id) { + return (LoadedActorStateOf::Corrupt, None); + } let funding = ActorFunding::::get(actor_id); 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, - run_state, - }), - Some(admission), - ) - } + match (identity, funding, run_state) { + (None, None, None) => (LoadedActorStateOf::NotRegistered, None), + (Some(identity), None, None) => (LoadedActorStateOf::Dormant(identity), None), _ => (LoadedActorStateOf::Corrupt, None), } } @@ -2127,12 +3133,112 @@ pub mod pallet { Self::load_actor_state_with_admission(actor_id).0 } - pub fn load_crossing_idle_activation_state( + /// Strict active-state loader; malformed primary authority never falls back to dormancy. + pub(crate) fn load_frame_actor_state(actor_id: ActorId) -> LoadedActorStateOf { + if ActorIdentities::::contains_key(actor_id) { + return LoadedActorStateOf::Corrupt; + } + let Some((location, identity, hot, frame_admission)) = + Self::load_frame_control_authority(actor_id) + else { + return LoadedActorStateOf::Corrupt; + }; + // Full classification validates temporal references; hot head admission does not load them. + let pointers = [ + hot.wakeup_pointer, + hot.trigger_wakeup_pointer.map(|pointer| WakeupPointer { + block: WakeupKey::Tick(pointer.tick), + page_id: pointer.page_id, + slot: pointer.slot, + }), + ]; + if let ActorControlLocation::Waiting { key, page, slot } = location { + if !pointers.contains(&Some(WakeupPointer { + block: key, + page_id: page, + slot: u32::from(slot), + })) { + return LoadedActorStateOf::Corrupt; + } + } + for pointer in pointers.into_iter().flatten() { + let Some(page) = ActorWaitingFrameChunks::::get((pointer.block, pointer.page_id)) else { + return LoadedActorStateOf::Corrupt; + }; + let coherent = match page + .entries + .get(pointer.slot as usize) + .and_then(Option::as_ref) + { + Some(ActorWaitingEntry::Reference(reference)) => { + reference.actor_id == actor_id + && reference.admission_identity == frame_admission.admission_identity + } + Some(ActorWaitingEntry::Primary(primary)) => { + primary.actor_id == actor_id + && primary.admission.admission_identity == frame_admission.admission_identity + && matches!(location, ActorControlLocation::Waiting { key, page, slot } + if key == pointer.block && page == pointer.page_id && u32::from(slot) == pointer.slot) + } + None => false, + }; + if !coherent { + return LoadedActorStateOf::Corrupt; + } + } + let Some(contract) = Self::load_contract_geometry_with_admission(actor_id, &frame_admission) + else { + return LoadedActorStateOf::Corrupt; + }; + let Some(funding) = ActorFunding::::get(actor_id) else { + return LoadedActorStateOf::Corrupt; + }; + let run_state = ActorRunStateStore::::get(actor_id); + let run_is_coherent = match (hot.cycle_state, run_state.as_ref()) { + (CycleState::Idle, None) => { + !ActorRunHeads::::contains_key(actor_id) + && !ActorRunPayloads::::contains_key(actor_id) + } + (CycleState::Running, Some(run)) => { + run.running_is_coherent() + && run.has_contract_authority( + frame_admission.semantic_contract_id, + frame_admission.body_commitment, + frame_admission.admission_identity, + ) + } + (CycleState::Suspended, Some(run)) => { + run.suspension_is_coherent() + && run.has_contract_authority( + frame_admission.semantic_contract_id, + frame_admission.body_commitment, + frame_admission.admission_identity, + ) + } + _ => false, + }; + if !run_is_coherent + || !hot + .trigger_runtime_state + .is_compatible_with(&contract.trigger) + { + return LoadedActorStateOf::Corrupt; + } + LoadedActorStateOf::Active(ActiveActorState { + identity, + hot, + contract, + funding, + run_state, + }) + } + + pub(crate) fn load_crossing_idle_activation_state_with_authority( 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)?; + let (identity, hot, certificate) = Self::load_control_authority_with_authority(actor_id)?; if hot.cycle_state != CycleState::Idle || !matches!( hot.trigger_runtime_state, @@ -2142,8 +3248,6 @@ pub mod pallet { { 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 @@ -2156,21 +3260,35 @@ pub mod pallet { identity, hot, authority, + admission: Some(certificate), run_head: None, loaded_step: None, }) } - pub(crate) fn load_observation_activation_state( + pub fn load_crossing_idle_activation_state( + actor_id: ActorId, + feed: T::ObservationFeedId, + ) -> Option> { + Self::load_crossing_idle_activation_state_with_authority(actor_id, feed) + } + + pub(crate) fn load_observation_activation_state_with_authority( 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 (identity, hot, admission) = Self::load_control_authority_with_authority(actor_id)?; + if authority.semantic_contract_id != admission.semantic_contract_id + || authority.body_commitment != admission.body_commitment + || authority.admission_identity != admission.admission_identity + { + return None; + } + let admission = Some(admission); let head = ActorContractHeads::::get(actor_id)?; if !matches!( &head.header.trigger, @@ -2255,24 +3373,46 @@ pub mod pallet { identity, hot, authority, + admission, run_head, loaded_step, }) } - pub(crate) fn load_actor_service_state( + pub(crate) fn load_observation_activation_state( actor_id: ActorId, + feed: T::ObservationFeedId, + ) -> Option> { + Self::load_observation_activation_state_with_authority(actor_id, feed) + } + + pub(crate) fn load_actor_service_state_with_control( + actor_id: ActorId, + identity: ActorIdentityOf, + hot: ActorHotStateOf, + admission: ActorAdmissionCertificateOf, ) -> Option<( ActiveActorStateOf, ActorAdmissionCertificateOf, Option>, )> { - let identity = ActorIdentities::::get(actor_id)?; - let hot = ActorHot::::get(actor_id)?; let head = ActorContractHeads::::get(actor_id)?; + Self::load_actor_service_state_with_head(actor_id, identity, hot, admission, head) + } + + pub(crate) fn load_actor_service_state_with_head( + actor_id: ActorId, + identity: ActorIdentityOf, + hot: ActorHotStateOf, + admission: ActorAdmissionCertificateOf, + head: ActorContractHeadOf, + ) -> Option<( + ActiveActorStateOf, + ActorAdmissionCertificateOf, + Option>, + )> { 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)) @@ -2360,6 +3500,28 @@ pub mod pallet { )) } + pub(crate) fn load_actor_service_state_with_authority( + actor_id: ActorId, + ) -> Option<( + ActiveActorStateOf, + ActorAdmissionCertificateOf, + Option>, + )> { + let (identity, hot, admission) = Self::load_control_authority_with_authority(actor_id)?; + Self::load_actor_service_state_with_control(actor_id, identity, hot, admission) + } + + pub(crate) fn load_frame_actor_service_state( + actor_id: ActorId, + ) -> Option<( + ActiveActorStateOf, + ActorAdmissionCertificateOf, + Option>, + )> { + Self::load_actor_service_state_with_authority(actor_id) + } + + #[cfg(any(test, feature = "runtime-benchmarks"))] pub(crate) fn load_current_step_service_state( actor_id: ActorId, ) -> Option<( @@ -2367,12 +3529,14 @@ pub mod pallet { ActorAdmissionCertificateOf, LoadedActorStepOf, )> { - let (state, admission, loaded_step) = Self::load_actor_service_state(actor_id)?; + let (state, admission, loaded_step) = Self::load_frame_actor_service_state(actor_id)?; Some((state, admission, loaded_step?)) } + #[cfg(any(test, feature = "runtime-benchmarks"))] pub(crate) fn active_actor_view(actor_id: ActorId) -> Option> { - let LoadedActorStateOf::Active(state) = Self::load_actor_state(actor_id) else { + let LoadedActorStateOf::Active(state) = Self::load_actor_state_for_frame_control(actor_id) + else { return None; }; Some(Self::derive_active_actor_view( @@ -2382,10 +3546,19 @@ pub mod pallet { )) } - pub(crate) fn active_actor_state_for_control( + pub(crate) fn load_actor_state_with_authority(actor_id: ActorId) -> LoadedActorStateOf { + Self::load_actor_state(actor_id) + } + + /// Lifecycle classification shares one strict canonical state boundary. + pub(crate) fn load_actor_state_for_frame_control(actor_id: ActorId) -> LoadedActorStateOf { + Self::load_actor_state_with_authority(actor_id) + } + + pub(crate) fn active_actor_state_for_frame_control( actor_id: ActorId, ) -> Result, Error> { - match Self::load_actor_state(actor_id) { + match Self::load_actor_state_for_frame_control(actor_id) { LoadedActorStateOf::Active(state) => Ok(state), LoadedActorStateOf::NotRegistered | LoadedActorStateOf::Dormant(_) => { Err(Error::::ActorNotFound) @@ -2395,25 +3568,66 @@ pub mod pallet { } pub fn active_actor_state(actor_id: ActorId) -> Option> { - match Self::load_actor_state(actor_id) { + match Self::load_actor_state_for_frame_control(actor_id) { LoadedActorStateOf::Active(state) => Some(state), _ => None, } } + /// Resolves identity from its lifecycle owner; malformed active authority never falls back + /// to a dormant registry row. + pub fn actor_identity(actor_id: ActorId) -> Option> { + match Self::load_actor_state_for_frame_control(actor_id) { + LoadedActorStateOf::Active(state) => Some(state.identity), + LoadedActorStateOf::Dormant(identity) => Some(identity), + LoadedActorStateOf::NotRegistered | LoadedActorStateOf::Corrupt => None, + } + } + + /// Reads the bounded control owner independently of funding and Contract projections. + pub fn actor_control_cell( + actor_id: ActorId, + ) -> Option<( + ActorControlLocation>, + ActorControlCellOf, + )> { + Self::load_primary_control_cell(actor_id).ok() + } + + pub fn actor_hot(actor_id: ActorId) -> Option> { + Self::load_frame_control_authority(actor_id).map(|(_, _, hot, _)| hot) + } + pub fn pending_signal(actor_id: ActorId) -> bool { - match Self::load_actor_state(actor_id) { + match Self::load_actor_state_for_frame_control(actor_id) { LoadedActorStateOf::Active(state) => state.hot.pending_signal, _ => false, } } - pub fn wakeup_pages(key: (BlockNumberFor, WakeupPageId)) -> Option> { - WakeupPages::::get((WakeupKey::Block(key.0), key.1)) + pub fn wakeup_pages(key: (BlockNumberFor, WakeupPageId)) -> Option> { + ActorWaitingFrameChunks::::get((WakeupKey::Block(key.0), key.1)) } pub fn wakeup_buckets(block: BlockNumberFor) -> Option { - WakeupBuckets::::get(WakeupKey::Block(block)) + Self::wakeup_bucket_state(WakeupKey::Block(block)) + } + + pub(crate) fn wakeup_bucket_state( + key: WakeupKey>, + ) -> Option { + let live_entries = ActorWaitingOccupancies::::get(key); + if live_entries == 0 { + return None; + } + let tail = ActorWaitingTails::::get(key); + Some(WakeupBucketState { + head_page: ActorWaitingHeads::::get(key) / 32, + tail_page: tail.checked_sub(1)? / 32, + next_page_id: tail.div_ceil(32), + live_entries, + cursor_index: ActorWaitingCursorIndices::::get(key), + }) } pub fn wakeup_cursor_pages(page_id: WakeupPageId) -> Option> { @@ -2426,19 +3640,27 @@ pub mod pallet { pub(crate) fn active_actor_exists(actor_id: ActorId) -> bool { matches!( - Self::load_actor_state(actor_id), + Self::load_actor_state_for_frame_control(actor_id), LoadedActorStateOf::Active(_) ) } - pub(crate) fn preflight_trigger_transition( + pub(crate) fn preflight_trigger_transition( + actor_id: ActorId, + trigger: &TriggerOf, + intent: TriggerTransitionIntent, + ) -> Result, DispatchError> { + Self::preflight_trigger_transition_with_authority(actor_id, trigger, intent) + } + + pub(crate) fn preflight_trigger_transition_with_authority( actor_id: ActorId, trigger: &TriggerOf, intent: TriggerTransitionIntent, ) -> Result, DispatchError> { Ok(TriggerTransitionPlan { intent, - crossing: Self::preflight_crossing_membership(actor_id, trigger)?, + crossing: Self::preflight_crossing_membership_with_authority(actor_id, trigger)?, observation_feeds: Self::preflight_observation_subscription_replace(actor_id, trigger)?, }) } @@ -2455,21 +3677,20 @@ pub mod pallet { Error::::ActorInvariant ); Self::preflight_remove_observation_subscriptions(actor_id)?; - Self::preflight_trigger_transition(actor_id, &TriggerOf::::Manual, intent) + Self::preflight_trigger_transition_with_authority(actor_id, &TriggerOf::::Manual, intent) } fn commit_trigger_transition( actor_id: ActorId, plan: TriggerTransitionPlan, - prospective_is_user: Option, + actor_type: ActorType, + prospective_admission_identity: Option<[u8; 32]>, ) -> 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 { .. })) - }); - let crossing_state = Self::commit_crossing_membership(actor_id, plan.crossing, is_user)?; + let admission_identity = prospective_admission_identity.unwrap_or([0; 32]); + let crossing_state = + Self::commit_crossing_membership(actor_id, plan.crossing, actor_type, admission_identity)?; Self::commit_observation_subscription_replace(actor_id, plan.observation_feeds)?; Ok(crossing_state) } @@ -2482,18 +3703,33 @@ pub mod pallet { intent: TriggerTransitionIntent, ) -> DispatchResult { let transition = Self::preflight_trigger_transition(actor_id, &contract.trigger, intent)?; + let admission = + Self::build_admission_certificate(&contract).ok_or(Error::::AdmissionBoundOverflow)?; let crossing_state = Self::commit_trigger_transition( actor_id, transition, - Some(matches!(identity.actor_class, ActorClass::User { .. })), + identity.actor_class.actor_type(), + Some(admission.admission_identity), )?; hot.trigger_runtime_state = Self::installed_trigger_runtime_state( &contract.trigger, hot.trigger_runtime_state.temporal_anchor_tick(), crossing_state, )?; - ActorIdentities::::insert(actor_id, identity); - ActorHot::::insert(actor_id, hot); + let step_resources = Self::derive_step_resource_envelopes(&contract) + .ok_or(Error::::AdmissionBoundOverflow)?; + let resources = step_resources + .first() + .copied() + .unwrap_or(ActorStepResourceEnvelope { + control: T::WeightInfo::scheduler_inner_zero_step_complete(), + effect: Weight::zero(), + }); + ensure!( + Self::insert_unsignaled_control_authority(actor_id, identity, hot, admission, resources,), + Error::::ActorInvariant + ); + ActorIdentities::::remove(actor_id); Self::store_actor_contract(actor_id, contract) } @@ -2543,13 +3779,24 @@ pub mod pallet { } } - pub(crate) fn remove_active_actor( + fn remove_active_actor_with_admission( actor_id: ActorId, trigger_transition: TriggerTransitionPlan, + admission: Option<&ActorAdmissionCertificateOf>, + actor_type: ActorType, ) -> DispatchResult { - Self::commit_trigger_transition(actor_id, trigger_transition, None)?; - ActorHot::::remove(actor_id); - Self::remove_actor_contract(actor_id)?; + Self::commit_trigger_transition(actor_id, trigger_transition, actor_type, None)?; + if ActorControlLocators::::contains_key(actor_id) { + Self::remove_primary_control_cell_inner(actor_id) + .map_err(|_| Error::::ActorInvariant)?; + } + let removed_contract = match admission { + Some(admission) => { + Self::remove_admitted_contract_geometry_with_admission(actor_id, admission) + } + None => Self::remove_admitted_contract_geometry(actor_id), + }; + ensure!(removed_contract.is_some(), Error::::ActorInvariant); ActorRunStateStore::::remove(actor_id); Ok(()) } @@ -2583,11 +3830,6 @@ pub mod pallet { #[pallet::getter(fn system_sovereign_count)] pub type SystemSovereignCount = StorageValue<_, u32, ValueQuery>; - /// 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)] @@ -2606,41 +3848,55 @@ pub mod pallet { pub type FinalizedBlockResourceTelemetry = StorageValue<_, FinalizedBlockResourceSnapshot>, OptionQuery>; - /// Physical head position of the canonical paged FIFO. - #[pallet::storage] - #[pallet::getter(fn queue_head)] - pub type QueueHead = StorageValue<_, QueueTicket, ValueQuery>; - - /// Next physical position in the canonical paged FIFO. - #[pallet::storage] - #[pallet::getter(fn queue_tail)] - pub type QueueTail = StorageValue<_, QueueTicket, ValueQuery>; - - /// Exact unconsumed physical entries, including tombstones, in the canonical FIFO. + /// Sole primary control cells for active Actors without current process readiness. #[pallet::storage] - #[pallet::getter(fn queue_occupancy)] - pub type QueueOccupancy = StorageValue<_, u32, ValueQuery>; + pub type ActorUnsignaledControlCells = + StorageMap<_, Blake2_128Concat, ActorId, ActorControlCellOf, OptionQuery>; - /// Bounded physical pages for the canonical FIFO. + /// Ticket-addressed C32 chunks for the one canonical Ready FIFO. #[pallet::storage] - #[pallet::getter(fn queue_pages)] - pub type QueuePages = - StorageMap<_, Blake2_128Concat, QueuePageId, QueuePageOf, OptionQuery>; + pub type ActorReadyFrameChunks = + StorageMap<_, Blake2_128Concat, QueuePageId, ActorControlChunkOf, OptionQuery>; - /// Fixed-size pages for block and timestamp-tick wakeups. + /// Deadline-addressed C32 chunks for non-ready active control cells. #[pallet::storage] - pub type WakeupPages = StorageMap< + pub type ActorWaitingFrameChunks = StorageMap< _, Blake2_128Concat, - (WakeupKey>, WakeupPageId), - WakeupPageOf, + (WakeupKey>, QueuePageId), + ActorWaitingPageOf, OptionQuery, >; - /// Small per-deadline ownership and allocation metadata for temporal pages. + /// External-boundary locator for the sole primary active control cell. + #[pallet::storage] + pub type ActorControlLocators = + StorageMap<_, Blake2_128Concat, ActorId, ActorControlLocation>, OptionQuery>; + + #[pallet::storage] + pub type ActorReadyHead = StorageValue<_, QueueTicket, ValueQuery>; + + #[pallet::storage] + pub type ActorReadyTail = StorageValue<_, QueueTicket, ValueQuery>; + + #[pallet::storage] + pub type ActorReadyOccupancy = StorageValue<_, u32, ValueQuery>; + #[pallet::storage] - pub type WakeupBuckets = - StorageMap<_, Blake2_128Concat, WakeupKey>, WakeupBucketState, OptionQuery>; + pub type ActorWaitingHeads = + StorageMap<_, Blake2_128Concat, WakeupKey>, u64, ValueQuery>; + + #[pallet::storage] + pub type ActorWaitingTails = + StorageMap<_, Blake2_128Concat, WakeupKey>, u64, ValueQuery>; + + #[pallet::storage] + pub type ActorWaitingOccupancies = + StorageMap<_, Blake2_128Concat, WakeupKey>, u32, ValueQuery>; + + #[pallet::storage] + pub type ActorWaitingCursorIndices = + StorageMap<_, Blake2_128Concat, WakeupKey>, WakeupCursorIndex, OptionQuery>; /// Paged binary min-heaps of distinct block and timestamp-tick deadlines. #[pallet::storage] @@ -2874,7 +4130,7 @@ pub mod pallet { pub trait GenesisSystemActors { fn system_actors() -> alloc::vec::Vec<(ActorId, AccountId, Mutability, Contract)>; - fn dormant_system_actors() -> alloc::vec::Vec<(ActorId, AccountId)> { + fn dormant_system_actors() -> alloc::vec::Vec<(ActorId, AccountId, Mutability)> { alloc::vec::Vec::new() } @@ -3054,10 +4310,13 @@ pub mod pallet { ActorIdentityCount::::get() <= T::MaxActorIdentities::get(), "genesis actor identity capacity exceeded at actor_id={actor_id}" ); - Pallet::::prime_actor_schedule(actor_id) - .expect("genesis placement preserves readiness (spec 8.1.4)"); + Pallet::::prime_initial_actor_schedule(actor_id).unwrap_or_else(|error| { + panic!( + "genesis placement preserves readiness (spec 8.1.4) at actor_id={actor_id}: {error:?}" + ) + }); } - for (actor_id, owner) in T::GenesisSystemActors::dormant_system_actors() { + for (actor_id, owner, mutability) in T::GenesisSystemActors::dormant_system_actors() { assert!( !Pallet::::active_actor_exists(actor_id) && !ActorIdentities::::contains_key(actor_id), @@ -3080,7 +4339,7 @@ pub mod pallet { actor_class: ActorClass::System { sovereign_id: actor_id, }, - mutability: Mutability::Mutable, + mutability, cycle_nonce: 0, last_control_mutation_block: Zero::zero(), }; @@ -3211,14 +4470,18 @@ pub mod pallet { 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); + let minimum_reservation = if all_minimum_quanta.all_lte(remaining) { + MaterializationMinimumReservation::ReserveAllFamilies + } else { + MaterializationMinimumReservation::Unavailable + }; 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, + minimum_reservation, ); let consumed = Self::service_materialization_family( family, @@ -3268,7 +4531,7 @@ pub mod pallet { CurrentBlockResourceState::::put(state); return Err(Error::::ResourceProtocolFailed); } - let cutoff = NextQueueTicket::::get(); + let cutoff = ActorReadyTail::::get(); let mut cutoff_reservation = state .reserve( budget.limits(), @@ -3282,8 +4545,10 @@ pub mod pallet { .map_err(|_| Error::::ResourceProtocolFailed)?; let cleanup_units = u32::from( - QueueHead::::get() < QueueTail::::get() - && Self::combined_queue_occupancy() >= u64::from(T::MaxQueueLength::get()), + Self::ready_work_exists() + && ActorReadyTail::::get() + .checked_sub(ActorReadyHead::::get()) + .is_some_and(|span| span >= u64::from(T::MaxQueueLength::get())), ); let cleanup_weight = if cleanup_units > 0 { T::WeightInfo::scheduler_paged_tombstone_drain(cleanup_units) @@ -3345,17 +4610,23 @@ pub mod pallet { .limits() .actor_control() .checked_sub(&state.usage().actor_control_used()) - .unwrap_or_else(Weight::zero); - let prepass_limit = control_remaining + .ok_or(Error::::ResourceProtocolFailed)?; + let drain_finalization_control = T::WeightInfo::scheduler_on_idle_base() + .checked_add(&T::WeightInfo::block_resource_finalize()) + .ok_or(Error::::ResourceProtocolFailed)?; + let prepass_control = control_remaining + .checked_sub(&drain_finalization_control) + .ok_or(Error::::ResourceProtocolFailed)?; + let prepass_limit = prepass_control .checked_add(&budget.limits().actor_base_turn()) - .unwrap_or_else(Weight::zero); + .ok_or(Error::::ResourceProtocolFailed)?; let pass = Self::execute_cycle_to_cutoff_with_resources( prepass_limit, cutoff, &mut state, budget.limits(), BlockResourceDomain::ActorBaseEffect, - control_remaining, + prepass_control, ); if state.open_external_phase().is_err() { state.halt_optional_actor_work(); @@ -3524,8 +4795,11 @@ pub mod pallet { wakeup_limit.ref_time() > 0 && wakeup_limit.proof_size() > 0, "wakeup worker Weight limit must be non-zero in both dimensions" ); - let wakeup_unit = T::WeightInfo::scheduler_wakeup_cursor_worker_future() - .saturating_add(Self::wakeup_cursor_drain_unit_weight_upper(true)); + let wakeup_unit = T::WeightInfo::scheduler_wakeup_cursor_worker_future().saturating_add( + Self::wakeup_cursor_drain_unit_weight_upper( + crate::scheduler::WakeupBucketDisposition::Remove, + ), + ); assert!( wakeup_unit.all_lte(wakeup_limit), "positive wakeup cap must admit one complete maximum unit" @@ -3616,14 +4890,16 @@ pub mod pallet { }; let breaker_active = GlobalCircuitBreaker::::get(); let after_base = control_available.saturating_sub(fixed_weight); - let cleanup_units = u32::from(QueueHead::::get() < QueueTail::::get()); + let cleanup_units = u32::from(Self::ready_work_exists()); let queue_cleanup_weight = T::WeightInfo::scheduler_paged_tombstone_drain(cleanup_units); let saturated_cleanup_weight = if legacy_unmetered_materialization && cleanup_units > 0 - && Self::combined_queue_occupancy() >= u64::from(T::MaxQueueLength::get()) + && ActorReadyTail::::get() + .checked_sub(ActorReadyHead::::get()) + .is_some_and(|span| span >= u64::from(T::MaxQueueLength::get())) && queue_cleanup_weight.all_lte(after_base) { - let cutoff = NextQueueTicket::::get(); + let cutoff = ActorReadyTail::::get(); // The probe reads queue topology and the head page before it can know whether anything is // drainable, so a scan that finds nothing still consumed that work. Charge the attempt // unconditionally rather than letting the empty outcome bill zero every block. @@ -3654,7 +4930,7 @@ pub mod pallet { let execution_cutoff = PrepassExecutionCutoff::::get() .filter(|(cutoff_block, _)| *cutoff_block == now) .map(|(_, cutoff)| cutoff) - .unwrap_or_else(NextQueueTicket::::get); + .unwrap_or_else(ActorReadyTail::::get); let pass = match CurrentBlockResourceState::::get() { Some(mut state) if state.ensure_block(now).is_ok() @@ -4094,7 +5370,7 @@ pub mod pallet { #[pallet::call_index(4)] #[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 state = Self::active_actor_state_for_frame_control(actor_id)?; 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)?; @@ -4112,28 +5388,47 @@ pub mod pallet { let now = frame_system::Pallet::::block_number(); Self::ensure_control_mutation_allowed(&snapshot, now)?; Self::with_control_transaction(|| { - ActorHot::::try_mutate(actor_id, |maybe| -> DispatchResult { - let inst = maybe.as_mut().ok_or(Error::::ActorNotFound)?; - ensure!( - snapshot.mutability == Mutability::Mutable, - Error::::ImmutableActor - ); - inst.lifecycle = ActiveLifecycle::Paused; - inst.queue_ticket = None; - Self::record_control_mutation(actor_id, now)?; - Self::deposit_event(Event::ActorPaused { actor_id }); - Ok(()) - })?; - Self::prime_actor_schedule(actor_id).map_err(Self::placement_error) + Self::try_mutate_control_hot_with_authority( + actor_id, + Error::::ActorNotFound, + |inst| -> DispatchResult { + ensure!( + snapshot.mutability == Mutability::Mutable, + Error::::ImmutableActor + ); + inst.lifecycle = ActiveLifecycle::Paused; + inst.queue_ticket = None; + Self::record_control_mutation_with_authority(actor_id, now)?; + Self::deposit_event(Event::ActorPaused { actor_id }); + Ok(()) + }, + )?; + Self::demote_ready_frame_to_unsignaled(actor_id).map_err(Self::placement_error)?; + Self::prime_frame_actor_schedule(actor_id).map_err(Self::placement_error) }) } #[pallet::call_index(5)] #[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.run_state; - let snapshot = Self::derive_active_actor_view(state.identity, state.hot, state.contract); + ensure!( + !ActorIdentities::::contains_key(actor_id), + Error::::ActorInvariant + ); + let (state, admission, loaded_step) = + Self::load_frame_actor_service_state(actor_id).ok_or(Error::::ActorInvariant)?; + ensure!( + state.hot.cycle_state != CycleState::Idle + || (!ActorRunHeads::::contains_key(actor_id) + && !ActorRunPayloads::::contains_key(actor_id)), + Error::::ActorInvariant + ); + let continuation = state.run_state.clone(); + let snapshot = Self::derive_active_actor_view( + state.identity.clone(), + state.hot.clone(), + state.contract.clone(), + ); Self::ensure_control_origin(origin.clone(), &snapshot)?; Self::ensure_not_system_immutable(&snapshot)?; if Self::expiry_substitution_due_loaded(&snapshot, continuation.as_ref())? { @@ -4149,25 +5444,26 @@ pub mod pallet { let now = frame_system::Pallet::::block_number(); Self::ensure_control_mutation_allowed(&snapshot, now)?; Self::with_control_transaction(|| { - ActorHot::::try_mutate(actor_id, |maybe| -> DispatchResult { - let inst = maybe.as_mut().ok_or(Error::::ActorNotFound)?; - ensure!( - snapshot.mutability == Mutability::Mutable, - Error::::ImmutableActor - ); - inst.lifecycle = ActiveLifecycle::Active; - Self::record_control_mutation(actor_id, now)?; - Self::deposit_event(Event::ActorResumed { actor_id }); - Ok(()) - })?; - Self::prime_actor_schedule(actor_id).map_err(Self::placement_error) + Self::record_control_mutation_with_authority(actor_id, now)?; + let (_, identity, _, _) = + Self::load_frame_control_authority(actor_id).ok_or(Error::::ActorInvariant)?; + let mut resumed = state.clone(); + resumed.identity = identity; + resumed.hot.lifecycle = ActiveLifecycle::Active; + resumed.hot.queue_ticket = None; + Self::detach_primary_for_successor(actor_id, &resumed.hot) + .map_err(Self::placement_error)?; + Self::publish_resumed_frame(actor_id, resumed, admission.clone(), loaded_step.clone()) + .map_err(Self::placement_error)?; + Self::deposit_event(Event::ActorResumed { actor_id }); + Ok(()) }) } #[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) -> DispatchResultWithPostInfo { - let state = Self::active_actor_state_for_control(actor_id)?; + let state = Self::active_actor_state_for_frame_control(actor_id)?; 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)?; @@ -4189,7 +5485,7 @@ pub mod pallet { ); let mut trigger_processed = false; Self::with_control_transaction(|| { - let Some(outcome) = Self::commit_trigger_occurrence( + let Some(outcome) = Self::commit_frame_trigger_occurrence( actor_id, actor_type, &snapshot.sovereign_account, @@ -4218,7 +5514,7 @@ pub mod pallet { #[pallet::call_index(8)] #[pallet::weight(Pallet::::close_dispatch_weight_upper())] pub fn close_actor(origin: OriginFor, actor_id: ActorId) -> DispatchResult { - match Self::load_actor_state(actor_id) { + match Self::load_actor_state_for_frame_control(actor_id) { LoadedActorStateOf::Active(state) => { let instance = Self::derive_active_actor_view(state.identity, state.hot, state.contract); Self::ensure_control_origin(origin, &instance)?; @@ -4230,6 +5526,10 @@ pub mod pallet { } LoadedActorStateOf::Dormant(identity) => { Self::ensure_identity_control_origin(origin, &identity)?; + ensure!( + identity.mutability == Mutability::Mutable, + Error::::ImmutableActor + ); Self::close_inactive_actor(actor_id, &identity, CloseReason::OwnerInitiated) } LoadedActorStateOf::NotRegistered => Err(Error::::ActorNotFound.into()), @@ -4250,12 +5550,11 @@ pub mod pallet { Self::validate_schedule_window(window)?; } Self::validate_future_schedule_targets(&contract)?; - let state = Self::active_actor_state_for_control(actor_id)?; + let state = Self::active_actor_state_for_frame_control(actor_id)?; let continuation = state.run_state; + let current_contract = state.contract.clone(); let snapshot = Self::derive_active_actor_view(state.identity, state.hot, state.contract); Self::ensure_control_origin(origin.clone(), &snapshot)?; - let current_contract = - Self::load_actor_contract(actor_id).ok_or(Error::::ActorInvariant)?; if current_contract == contract { return Ok(()); } @@ -4298,6 +5597,8 @@ pub mod pallet { if let Some(target_nonce) = contract.auto_close_at_cycle_nonce { Self::ensure_auto_close_target(snapshot.cycle_nonce, target_nonce)?; } + let replacement_admission = + Self::build_admission_certificate(&contract).ok_or(Error::::AdmissionBoundOverflow)?; let new_tracked = Self::derive_funding_tracked_assets(&contract.steps)?; let mut funding_state = ActorFunding::::get(actor_id).ok_or(Error::::ActorNotFound)?; funding_state.funding_tracked_assets = new_tracked.clone(); @@ -4312,7 +5613,7 @@ pub mod pallet { Self::temporal_anchor_tick(&contract.trigger).map_err(Self::placement_error)?; let trigger_transition = schedule_changed .then(|| { - Self::preflight_trigger_transition( + Self::preflight_trigger_transition_with_authority( actor_id, &contract.trigger, TriggerTransitionIntent::ReplaceActive, @@ -4326,39 +5627,49 @@ pub mod pallet { false }; let crossing_state = if let Some(transition) = trigger_transition { - Self::commit_trigger_transition(actor_id, transition, None)? + Self::commit_trigger_transition( + actor_id, + transition, + snapshot.actor_class.actor_type(), + Some(replacement_admission.admission_identity), + )? } else { None }; - 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, - temporal_anchor_tick, - crossing_state, - )?; - hot.terminal_at = contract - .window - .map(|window| Self::window_terminal_at(&window)); - } - if steps_changed { - hot.unsuccessful_attempt_streak = crate::execution::transition_failure_streak( - hot.unsuccessful_attempt_streak, - crate::execution::FailureStreakTransition::Reset, - ) - .ok_or(Error::::ActorInvariant)?; + if schedule_changed { + let (state, admission, _) = + Self::load_frame_actor_service_state(actor_id).ok_or(Error::::ActorInvariant)?; + if state.hot.trigger_wakeup_pointer.is_some() { + Self::trigger_wakeup_substrate_invalidate_loaded(actor_id, state, &admission) + .map_err(Self::placement_error)?; } - Self::record_control_mutation(actor_id, now)?; - Ok(()) - })?; + } + Self::try_mutate_control_hot_with_authority( + actor_id, + Error::::ActorNotFound, + |hot| -> DispatchResult { + if schedule_changed { + hot.schedule_anchor = schedule_anchor; + hot.trigger_runtime_state = Self::installed_trigger_runtime_state( + &contract.trigger, + temporal_anchor_tick, + crossing_state, + )?; + hot.terminal_at = contract + .window + .map(|window| Self::window_terminal_at(&window)); + } + if steps_changed { + hot.unsuccessful_attempt_streak = crate::execution::transition_failure_streak( + hot.unsuccessful_attempt_streak, + crate::execution::FailureStreakTransition::Reset, + ) + .ok_or(Error::::ActorInvariant)?; + } + Self::record_control_mutation_with_authority(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())?; @@ -4367,9 +5678,9 @@ pub mod pallet { #[cfg(test)] crate::mock::control_atomicity_checkpoint(actor_id)?; if schedule_changed || run_cancelled { - Self::prime_actor_schedule(actor_id).map_err(Self::placement_error)?; + Self::prime_frame_actor_schedule(actor_id).map_err(Self::placement_error)?; } - Self::reconcile_actor_state_hold(actor_id)?; + Self::reconcile_actor_state_hold_with_authority(actor_id)?; Ok(()) }) } @@ -4437,7 +5748,7 @@ pub mod pallet { let mut missing = 0u32; let requested = actor_ids.len() as u32; for actor_id in actor_ids { - let state = match Self::load_actor_state(actor_id) { + let state = match Self::load_actor_state_for_frame_control(actor_id) { LoadedActorStateOf::Active(state) => state, LoadedActorStateOf::NotRegistered | LoadedActorStateOf::Dormant(_) => { missing = missing.saturating_add(1); @@ -4474,7 +5785,7 @@ pub mod pallet { actor_id: ActorId, contract: ActorContractOf, ) -> DispatchResult { - let identity = match Self::load_actor_state(actor_id) { + let identity = match Self::load_actor_state_for_frame_control(actor_id) { LoadedActorStateOf::Dormant(identity) => identity, LoadedActorStateOf::Active(_) => return Err(Error::::ActorAlreadyActive.into()), LoadedActorStateOf::NotRegistered => return Err(Error::::ActorNotFound.into()), @@ -4487,7 +5798,7 @@ pub mod pallet { #[pallet::call_index(18)] #[pallet::weight(T::WeightInfo::deactivate_actor())] pub fn deactivate_actor(origin: OriginFor, actor_id: ActorId) -> DispatchResult { - let instance = match Self::load_actor_state(actor_id) { + let instance = match Self::load_actor_state_for_frame_control(actor_id) { LoadedActorStateOf::Active(state) => { Self::derive_active_actor_view(state.identity, state.hot, state.contract) } @@ -4550,7 +5861,7 @@ pub mod pallet { #[pallet::call_index(19)] #[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 state = Self::active_actor_state_for_frame_control(actor_id)?; 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)?; @@ -4575,8 +5886,8 @@ pub mod pallet { 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) + Self::record_control_mutation_with_authority(actor_id, now)?; + Self::prime_frame_actor_schedule(actor_id).map_err(Self::placement_error) }) } @@ -4661,49 +5972,6 @@ pub mod pallet { Self::close_cleanup_weight_upper() } - pub(crate) fn compute_cycle_weight_upper_from( - actor_type: ActorType, - contract_steps: &ContractSteps, - start_cursor: usize, - ) -> Weight { - let mut upper = T::WeightInfo::step_orchestration(contract_steps.len() as u32); - for step_index in start_cursor..contract_steps.len() { - let step = &contract_steps[step_index]; - let predicate_evaluation = Self::predicate_evaluation_weight( - step - .precondition - .as_ref() - .map_or(0, Precondition::evaluation_units), - ); - upper = upper - .saturating_add(predicate_evaluation) - .saturating_add(Self::weight_upper_bound(&step.task)); - if actor_type == ActorType::User { - upper = upper.saturating_add(T::WeightInfo::fee_collection()); - } - } - if (start_cursor..contract_steps.len()).any(|step_index| { - contract_steps[step_index] - .on_error - .retry_max_attempts() - .is_some() - }) { - upper = upper.saturating_add( - T::WeightInfo::run_suspend() - .max(T::WeightInfo::run_complete()) - .max(T::WeightInfo::run_cancel()), - ); - } - upper - } - - pub fn compute_cycle_weight_upper( - actor_type: ActorType, - contract_steps: &ContractSteps, - ) -> Weight { - Self::compute_cycle_weight_upper_from(actor_type, contract_steps, 0) - } - pub fn attempt_fee_envelope( actor_type: ActorType, contract_steps: &ContractSteps, @@ -4733,33 +6001,6 @@ pub mod pallet { }) } - #[cfg(test)] - pub(crate) fn attempt_weight_upper_bound( - instance: &ActiveActorViewOf, - start_cursor: usize, - ) -> Weight { - let mut upper = Self::compute_cycle_weight_upper_from( - instance.actor_class.actor_type(), - &instance.steps, - start_cursor, - ); - 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 Actor run value. The transition - // envelope already carries its maximum proof, so only incremental RefTime composes here. - 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)); - } - upper - } - - pub(crate) fn close_cycle_weight_upper_bound(_instance: &ActiveActorViewOf) -> Weight { - Self::close_cleanup_weight_upper() - } - /// 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( @@ -4838,9 +6079,9 @@ pub mod pallet { family_cursor: u8, offset: u8, remaining: Weight, - reserve_all_minima: bool, + minimum_reservation: MaterializationMinimumReservation, ) -> Weight { - if !reserve_all_minima { + if !minimum_reservation.reserves_all_families() { return remaining; } let reserved_for_later = ((offset + 1)..3).fold(Weight::zero(), |reserved, later| { @@ -4873,7 +6114,9 @@ pub mod pallet { match family { 0 => T::WeightInfo::scheduler_wakeup_cursor_worker_future() .saturating_mul(2) - .saturating_add(Self::wakeup_cursor_drain_unit_weight_upper(true)), + .saturating_add(Self::wakeup_cursor_drain_unit_weight_upper( + crate::scheduler::WakeupBucketDisposition::Remove, + )), 1 => { let branch = T::WeightInfo::crossing_transition_unit() .max(T::WeightInfo::crossing_leaf_unit()) @@ -4968,7 +6211,7 @@ pub mod pallet { true } - fn owner_slot_is_set(bitmap: &OwnerSlotBitmap, owner_slot: u8) -> bool { + pub(crate) 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 @@ -5067,13 +6310,20 @@ pub mod pallet { Ok(()) } - fn derive_actor_state_hold( + fn derive_actor_state_hold_with_authority( actor_id: ActorId, identity: &ActorIdentityOf, + authority: Option<(&ActorHotStateOf, &ActorAdmissionCertificateOf)>, ) -> 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)?; + if authority.is_some() { + let control_identity = Self::control_identity_from_scalar(identity.clone()) + .ok_or(Error::::StateHoldInvariant)?; + Self::add_state_hold_encoded_size(&mut identity_bytes, &control_identity)?; + } else { + 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 { @@ -5091,10 +6341,10 @@ pub mod pallet { }); } - let Some(hot) = ActorHot::::get(actor_id) else { + let Some((hot, admission)) = authority else { ensure!( !ActorContractHeads::::contains_key(actor_id) - && !ActorAdmissionCertificates::::contains_key(actor_id) + && !Self::control_admission_exists(actor_id) && !ActorFunding::::contains_key(actor_id) && !ActorRunStateStore::::contains_key(actor_id), Error::::StateHoldInvariant @@ -5102,14 +6352,10 @@ pub mod pallet { 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)?; + breakdown.contract_head = + Self::state_hold_component(Self::control_state_hold_head_bytes(&head, admission)?)?; let chunk_count = head .header @@ -5124,33 +6370,63 @@ pub mod pallet { } breakdown.contract_body = Self::state_hold_component(body_bytes)?; - let mut detector_bytes = 0usize; + breakdown.detector = Self::state_hold_component(Self::state_hold_detector_bytes( + actor_id, + hot.trigger_wakeup_pointer, + )?)?; + 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) + } + + fn control_state_hold_head_bytes( + head: &ActorContractHeadOf, + admission: &ActorAdmissionCertificateOf, + ) -> Result> { + let mut bytes = + > as codec::MaxEncodedLen>::max_encoded_len(); + for component in [ + ::max_encoded_len(), + > as codec::MaxEncodedLen>::max_encoded_len(), + ::max_encoded_len(), + ] { + bytes = bytes + .checked_add(component) + .ok_or(Error::::StateHoldOverflow)?; + } + Self::add_state_hold_encoded_size(&mut bytes, admission)?; + Self::add_state_hold_encoded_size(&mut bytes, head)?; + Ok(bytes) + } + + fn state_hold_detector_bytes( + actor_id: ActorId, + trigger_wakeup_pointer: Option, + ) -> Result> { + let mut bytes = 0usize; let activation = ActorActivationAuthorities::::get(actor_id); if let Some(activation) = &activation { - Self::add_state_hold_encoded_size(&mut detector_bytes, activation)?; + Self::add_state_hold_encoded_size(&mut bytes, activation)?; } if let Some(feeds) = ActorObservationFeeds::::get(actor_id) { - Self::add_state_hold_encoded_size(&mut detector_bytes, &feeds)?; + Self::add_state_hold_encoded_size(&mut bytes, &feeds)?; } if let Some(slot) = ObservationSubscriptionSlot::::get(actor_id) { - Self::add_state_hold_encoded_size(&mut detector_bytes, &slot)?; + Self::add_state_hold_encoded_size(&mut 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)?; + Self::add_state_hold_encoded_size(&mut bytes, locator)?; } if activation.is_some() || crossing_locator.is_some() { - Self::add_state_hold_encoded_size(&mut detector_bytes, &())?; + Self::add_state_hold_encoded_size(&mut bytes, &())?; } - if let Some(pointer) = hot.trigger_wakeup_pointer { - Self::add_state_hold_encoded_size(&mut detector_bytes, &pointer)?; + if let Some(pointer) = trigger_wakeup_pointer { + Self::add_state_hold_encoded_size(&mut 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) + Ok(bytes) } pub(crate) fn ensure_funding_state_hold_capacity( @@ -5179,14 +6455,35 @@ pub mod pallet { Ok(()) } - pub(crate) fn reconcile_actor_state_hold(actor_id: ActorId) -> DispatchResult { + pub(crate) fn reconcile_actor_state_hold_with_authority(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 target = if ActorControlLocators::::contains_key(actor_id) { + let (_, identity, hot, admission) = + Self::load_frame_control_authority(actor_id).ok_or(Error::::StateHoldInvariant)?; + if identity.actor_class.actor_type() == ActorType::User { + Some(( + identity.owner.clone(), + Self::derive_actor_state_hold_with_authority( + actor_id, + &identity, + Some((&hot, &admission)), + )?, + )) + } else { + None + } + } else { + ensure!( + !ActorUnsignaledControlCells::::contains_key(actor_id), + Error::::StateHoldInvariant + ); + match Self::load_control_identity(actor_id) { + Some(identity) if identity.actor_class.actor_type() == ActorType::User => Some(( + identity.owner.clone(), + Self::derive_actor_state_hold_with_authority(actor_id, &identity, None)?, + )), + Some(_) | None => None, + } }; let (owner, target_breakdown) = match (existing.as_ref(), target) { (Some(existing), Some((owner, breakdown))) => { @@ -5248,6 +6545,88 @@ pub mod pallet { Ok(()) } + #[cfg(all(test, feature = "runtime-benchmarks"))] + pub(crate) fn control_reconcile_single_step_state_hold( + actor_id: ActorId, + cell: &ActorControlCellOf, + head: &ActorContractHeadOf, + funding: &ActorFundingStateOf, + ) -> DispatchResult { + if cell.identity.actor_class.actor_type() == ActorType::System { + return Ok(()); + } + ensure!(cell.actor_id == actor_id, Error::::StateHoldInvariant); + let sovereign_account = match cell.identity.actor_class { + ActorClass::User { owner_slot } => { + Self::sovereign_account_id(&cell.identity.owner, owner_slot) + } + ActorClass::System { sovereign_id } => Self::sovereign_account_id_system(sovereign_id), + }; + 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, &cell.identity)?; + Self::add_state_hold_encoded_size(&mut identity_bytes, &sovereign_account)?; + let head_bytes = Self::control_state_hold_head_bytes(head, &cell.admission)?; + 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)?; + } + let target_breakdown = ActorStateHoldBreakdown { + identity: Self::state_hold_component(identity_bytes)?, + contract_head: Self::state_hold_component(head_bytes)?, + contract_body: Self::state_hold_component(body_bytes)?, + detector: Self::state_hold_component(Self::state_hold_detector_bytes( + actor_id, + cell.hot.trigger_wakeup_pointer, + )?)?, + funding: Self::state_hold_component(codec::Encode::encoded_size(funding))?, + run: Self::state_hold_component( + as codec::MaxEncodedLen>::max_encoded_len(), + )?, + }; + let existing = ActorStateHolds::::get(actor_id).ok_or(Error::::StateHoldInvariant)?; + ensure!( + existing.owner == cell.identity.owner, + Error::::StateHoldInvariant + ); + if existing.breakdown == target_breakdown { + return Ok(()); + } + let old_total = Self::state_hold_total(&existing.breakdown)?; + 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, &existing.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, &existing.owner, decrease, Precision::Exact) + .map_err(|_| Error::::StateHoldInvariant)?; + ensure!(released == decrease, Error::::StateHoldInvariant); + } + ActorStateHolds::::insert( + actor_id, + ActorStateHoldRecord { + owner: existing.owner, + breakdown: target_breakdown, + }, + ); + Ok(()) + } + fn charge_creation_fee(owner: &T::AccountId) -> DispatchResult { let creation_fee = T::ActorCreationFee::get(); if creation_fee.is_zero() { @@ -5300,18 +6679,26 @@ pub mod pallet { .map_err(|_| Error::::InsufficientFee.into()) } - pub(crate) fn commit_trigger_occurrence( + fn commit_trigger_occurrence_with_authority( actor_id: ActorId, actor_type: ActorType, sovereign_account: &T::AccountId, breakdown: TriggerFeeBreakdown, - _cause_provenance: TriggerCauseProvenance, + state: ActiveActorStateOf, ) -> Result, DispatchError> { - if ActorHot::::get(actor_id).is_some_and(|hot| hot.pending_signal) { + if state.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)?; + ensure!( + state.identity.actor_class.actor_type() == actor_type + && state.identity.sovereign_account == *sovereign_account, + Error::::ActorInvariant + ); + + let plan = Self::preflight_activation_loaded(actor_id, state) + .map_err(Self::activation_failure_error)?; + let outcome = Self::commit_activation_plan(plan).map_err(Self::activation_failure_error)?; if matches!(outcome, crate::scheduler::ActivationOutcome::Closed) { return Ok(None); } @@ -5324,7 +6711,24 @@ pub mod pallet { Ok(Some(outcome)) } - pub(crate) fn try_commit_automatic_trigger_occurrence( + pub(crate) fn commit_frame_trigger_occurrence( + actor_id: ActorId, + actor_type: ActorType, + sovereign_account: &T::AccountId, + breakdown: TriggerFeeBreakdown, + _cause_provenance: TriggerCauseProvenance, + ) -> Result, DispatchError> { + let state = Self::active_actor_state_for_frame_control(actor_id)?; + Self::commit_trigger_occurrence_with_authority( + actor_id, + actor_type, + sovereign_account, + breakdown, + state, + ) + } + + pub(crate) fn try_commit_frame_automatic_trigger_occurrence( actor_id: ActorId, actor_type: ActorType, sovereign_account: &T::AccountId, @@ -5332,7 +6736,7 @@ pub mod pallet { cause_provenance: TriggerCauseProvenance, ) -> Result, DispatchError> { polkadot_sdk::frame_support::storage::with_transaction(|| { - match Self::commit_trigger_occurrence( + match Self::commit_frame_trigger_occurrence( actor_id, actor_type, sovereign_account, @@ -5408,7 +6812,23 @@ pub mod pallet { if actor_type == ActorType::System { return Ok(true); } - let breakdown = Self::pipeline_fee_for_actor(actor_id, actor_type)?; + let head = ActorContractHeads::::get(actor_id).ok_or(Error::::ActorInvariant)?; + Self::pipeline_capacity_sufficient_with_envelope( + actor_type, + sovereign_account, + head.header.pipeline_machine_envelope, + ) + } + + pub(crate) fn pipeline_capacity_sufficient_with_envelope( + actor_type: ActorType, + sovereign_account: &T::AccountId, + envelope: PipelineMachineEnvelope, + ) -> Result> { + if actor_type == ActorType::System { + return Ok(true); + } + let breakdown = Self::pipeline_fee_breakdown(actor_type, envelope)?; let required = T::MinUserBalance::get() .checked_add(&breakdown.total_fee) .ok_or(Error::::AdmissionBoundOverflow)?; @@ -5609,7 +7029,10 @@ pub mod pallet { ); let actor_id = NextActorId::::get(); ensure!( - !Self::active_actor_exists(actor_id) && !ActorIdentities::::contains_key(actor_id), + matches!( + Self::load_actor_state(actor_id), + LoadedActorStateOf::NotRegistered + ), Error::::ActorIdOccupied ); let next_id = actor_id.checked_add(1).ok_or(Error::::ActorIdOverflow)?; @@ -5706,7 +7129,7 @@ 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) { + if let Err(error) = Self::reconcile_actor_state_hold_with_authority(actor_id) { return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)); } Self::deposit_event(Event::ActorCreated { @@ -5829,7 +7252,10 @@ pub mod pallet { let funding_tracked_assets = Self::derive_funding_tracked_assets(&contract.steps)?; let actor_id = NextActorId::::get(); ensure!( - !Self::active_actor_exists(actor_id) && !ActorIdentities::::contains_key(actor_id), + matches!( + Self::load_actor_state(actor_id), + LoadedActorStateOf::NotRegistered + ), Error::::ActorIdOccupied ); if actor_type == ActorType::System { @@ -6002,10 +7428,12 @@ pub mod pallet { if let Err(error) = crate::mock::control_atomicity_checkpoint(actor_id) { return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)); } - if let Err(error) = Self::prime_actor_schedule(actor_id).map_err(Self::placement_error) { + if let Err(error) = + Self::prime_initial_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) { + if let Err(error) = Self::reconcile_actor_state_hold_with_authority(actor_id) { return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)); } polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(())) @@ -6089,7 +7517,7 @@ pub mod pallet { last_cycle_block: None, }; polkadot_sdk::frame_support::storage::with_transaction(|| { - if !ActorIdentities::::contains_key(actor_id) || Self::active_actor_exists(actor_id) { + if !Self::control_identity_exists(actor_id) || Self::active_actor_exists(actor_id) { return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err( Error::::ActorAlreadyActive.into(), )); @@ -6123,10 +7551,12 @@ pub mod pallet { if let Err(error) = crate::mock::control_atomicity_checkpoint(actor_id) { return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)); } - if let Err(error) = Self::prime_actor_schedule(actor_id).map_err(Self::placement_error) { + if let Err(error) = + Self::prime_initial_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) { + if let Err(error) = Self::reconcile_actor_state_hold_with_authority(actor_id) { return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)); } polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(())) @@ -6138,7 +7568,7 @@ pub mod pallet { let trigger_transition = Self::preflight_trigger_cleanup(actor_id, TriggerTransitionIntent::Deactivate)?; polkadot_sdk::frame_support::storage::with_transaction(|| { - if let Err(error) = Self::record_control_mutation(actor_id, now) { + if let Err(error) = Self::record_control_mutation_with_authority(actor_id, now) { return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)); } if let Err(error) = @@ -6146,31 +7576,42 @@ pub mod pallet { { return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)); } - if let Err(error) = Self::remove_actor_from_queues(actor_id) { - return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)); - } - let LoadedActorStateOf::Active(state) = Self::load_actor_state(actor_id) else { + let Some((state, admission, _)) = Self::load_frame_actor_service_state(actor_id) else { return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err( Error::::ActorInvariant.into(), )); }; - if state.hot.wakeup_pointer.is_some() - && Self::wakeup_substrate_invalidate(actor_id).is_none() - { - return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err( - Error::::ActorNotFound.into(), - )); + if let Err(error) = Self::remove_actor_from_queues_with_authority(actor_id) { + return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)); + } + if state.hot.wakeup_pointer.is_some() { + let invalidated = + Self::wakeup_substrate_invalidate_loaded(actor_id, state.clone(), &admission).is_ok(); + if !invalidated { + return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err( + Error::::ActorNotFound.into(), + )); + } } - 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 state.hot.trigger_wakeup_pointer.is_some() { + let invalidated = + Self::trigger_wakeup_substrate_invalidate_loaded(actor_id, state.clone(), &admission) + .is_ok(); + if !invalidated { + return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err( + Error::::ActorNotFound.into(), + )); + } } - if let Err(error) = Self::remove_active_actor(actor_id, trigger_transition) { + if let Err(error) = Self::remove_active_actor_with_admission( + actor_id, + trigger_transition, + Some(&admission), + state.identity.actor_class.actor_type(), + ) { return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)); } + ActorIdentities::::insert(actor_id, state.identity.clone()); ActorFunding::::remove(actor_id); if let Err(error) = ActiveActorCount::::try_mutate(|count| -> DispatchResult { *count = count @@ -6180,7 +7621,7 @@ pub mod pallet { }) { return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)); } - if let Err(error) = Self::reconcile_actor_state_hold(actor_id) { + if let Err(error) = Self::reconcile_actor_state_hold_with_authority(actor_id) { return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)); } Self::deposit_event(Event::ActorDeactivated { actor_id }); @@ -6716,14 +8157,16 @@ pub mod pallet { Self::with_reused_transaction(operation) } - fn record_control_mutation(actor_id: ActorId, now: BlockNumberFor) -> DispatchResult { - ActorIdentities::::try_mutate(actor_id, |maybe| -> DispatchResult { - maybe - .as_mut() - .ok_or(Error::::ActorNotFound)? - .last_control_mutation_block = now; - Ok(()) - }) + fn record_control_mutation_with_authority( + actor_id: ActorId, + now: BlockNumberFor, + ) -> DispatchResult { + let (_, mut identity, _, _) = + Self::load_frame_control_authority(actor_id).ok_or(Error::::ActorNotFound)?; + identity.last_control_mutation_block = now; + Self::update_existing_frame_control_identity(actor_id, &identity) + .map_err(|_| Error::::ActorInvariant)?; + Ok(()) } fn ensure_control_mutation_allowed( @@ -6753,7 +8196,11 @@ pub mod pallet { Ok(()) } - fn remove_owner_slot_binding(owner: &T::AccountId, owner_slot: u8, sovereign: &T::AccountId) { + pub(crate) fn remove_owner_slot_binding( + owner: &T::AccountId, + owner_slot: u8, + sovereign: &T::AccountId, + ) { let mut bitmap = OwnerSlotBitmaps::::get(owner); Self::clear_owner_slot(&mut bitmap, owner_slot); if Self::owner_slot_bitmap_is_empty(&bitmap) { @@ -6773,19 +8220,75 @@ pub mod pallet { actor_id: ActorId, instance: &ActiveActorViewOf, reason: CloseReason, + ) -> DispatchResult { + let state = Self::active_actor_state_for_frame_control(actor_id)?; + let current = Self::derive_active_actor_view( + state.identity.clone(), + state.hot.clone(), + state.contract.clone(), + ); + ensure!(current == *instance, Error::::ActorNotFound); + let (_, _, admission) = + Self::load_control_authority_with_authority(actor_id).ok_or(Error::::ActorNotFound)?; + Self::finalize_actor_from_retained_state(actor_id, state, &admission, reason) + } + + pub(crate) fn finalize_actor_from_consumed_state( + actor_id: ActorId, + state: ActiveActorStateOf, + admission: &ActorAdmissionCertificateOf, + reason: CloseReason, ) -> DispatchResult { ensure!( - Self::active_actor_view(actor_id).as_ref() == Some(instance), - Error::::ActorNotFound + !ActorControlLocators::::contains_key(actor_id) + && !ActorUnsignaledControlCells::::contains_key(actor_id), + Error::::ActorInvariant + ); + let instance = Self::derive_active_actor_view( + state.identity.clone(), + state.hot.clone(), + state.contract.clone(), + ); + Self::finalize_actor_loaded_inner( + actor_id, + &instance, + reason, + crate::execution::LoadedCancellationContext::ConsumedFrame { + admission: admission.clone(), + state, + }, + ) + } + + pub(crate) fn finalize_actor_from_retained_state( + actor_id: ActorId, + state: ActiveActorStateOf, + admission: &ActorAdmissionCertificateOf, + reason: CloseReason, + ) -> DispatchResult { + let instance = Self::derive_active_actor_view( + state.identity.clone(), + state.hot.clone(), + state.contract.clone(), ); - Self::finalize_actor_loaded(actor_id, instance, reason) + Self::finalize_actor_loaded_inner( + actor_id, + &instance, + reason, + crate::execution::LoadedCancellationContext::RetainedFrame { + admission: admission.clone(), + state, + }, + ) } - pub(crate) fn finalize_actor_loaded( + fn finalize_actor_loaded_inner( actor_id: ActorId, instance: &ActiveActorViewOf, reason: CloseReason, + mut cancellation_context: crate::execution::LoadedCancellationContext, ) -> DispatchResult { + let admission = cancellation_context.admission().clone(); ensure!( ActorFunding::::contains_key(actor_id), Error::::ActorNotFound @@ -6820,15 +8323,59 @@ pub mod pallet { } let trigger_transition = Self::preflight_trigger_cleanup(actor_id, TriggerTransitionIntent::Close)?; + let identity = ActorIdentity { + sovereign_account: instance.sovereign_account.clone(), + owner: instance.owner.clone(), + actor_class: instance.actor_class, + mutability: instance.mutability, + cycle_nonce: instance.cycle_nonce, + last_control_mutation_block: instance.last_control_mutation_block, + }; polkadot_sdk::frame_support::storage::with_transaction(|| { let result = (|| -> DispatchResult { - Self::cancel_run_internal(actor_id, CancellationReason::Closing(reason), None)?; + let (close_state, close_admission) = match &mut cancellation_context { + crate::execution::LoadedCancellationContext::RetainedFrame { state, admission } + | crate::execution::LoadedCancellationContext::ConsumedFrame { state, admission } => { + (state, admission) + } + }; + let pointers = [ + close_state.hot.wakeup_pointer, + close_state + .hot + .trigger_wakeup_pointer + .map(|pointer| WakeupPointer { + block: WakeupKey::Tick(pointer.tick), + page_id: pointer.page_id, + slot: pointer.slot, + }), + ]; + for pointer in pointers.into_iter().flatten() { + Self::invalidate_wakeup_reference( + actor_id, + pointer, + close_admission.admission_identity, + ) + .map_err(|_| Error::::ActorInvariant)?; + } + close_state.hot.wakeup_pointer = None; + close_state.hot.trigger_wakeup_pointer = None; + Self::cancel_run_internal_loaded( + actor_id, + &identity, + CancellationReason::Closing(reason), + None, + cancellation_context, + )?; - // 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. - Self::remove_active_actor(actor_id, trigger_transition)?; - ActorIdentities::::remove(actor_id); + // Exact secondary references are gone; remove the sole primary without a population scan. + Self::remove_active_actor_with_admission( + actor_id, + trigger_transition, + Some(&admission), + instance.actor_class.actor_type(), + )?; ActorFunding::::remove(actor_id); ActiveActorCount::::try_mutate(|count| -> DispatchResult { *count = count @@ -6853,7 +8400,7 @@ pub mod pallet { SystemSovereigns::::insert(sovereign_id, SystemSovereignState::Vacant); } } - Self::reconcile_actor_state_hold(actor_id)?; + Self::reconcile_actor_state_hold_with_authority(actor_id)?; Self::deposit_event(Event::ActorClosed { actor_id, reason }); Ok(()) })(); @@ -6872,7 +8419,7 @@ pub mod pallet { reason: CloseReason, ) -> DispatchResult { ensure!( - ActorIdentities::::get(actor_id).as_ref() == Some(identity), + Self::load_control_identity(actor_id).as_ref() == Some(identity), Error::::ActorNotFound ); ensure!( @@ -6914,7 +8461,7 @@ pub mod pallet { SystemSovereigns::::insert(sovereign_id, SystemSovereignState::Vacant); } } - Self::reconcile_actor_state_hold(actor_id)?; + Self::reconcile_actor_state_hold_with_authority(actor_id)?; Self::deposit_event(Event::ActorClosed { actor_id, reason }); Ok(()) }) @@ -6963,17 +8510,23 @@ pub mod pallet { ActiveActorCount::::get() } - pub(crate) fn remove_actor_from_queues(actor_id: ActorId) -> DispatchResult { - Self::try_paged_invalidate(actor_id) - .map(|_| ()) - .map_err(Self::placement_error) + pub(crate) fn remove_actor_from_queues_with_authority(actor_id: ActorId) -> DispatchResult { + match ActorControlLocators::::get(actor_id) { + Some(ActorControlLocation::Unsignaled | ActorControlLocation::Waiting { .. }) => Ok(()), + Some(ActorControlLocation::Ready { .. }) => { + Self::remove_primary_control_cell_inner(actor_id) + .map(|_| ()) + .map_err(|_| Error::::ActorInvariant.into()) + } + None => Err(Error::::ActorInvariant.into()), + } } #[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()) + .is_some_and(|(_, cutoff)| cutoff > ActorReadyTail::::get()) { return Err(TryRuntimeError::Other( "prepass execution cutoff exceeds the allocated ticket frontier", @@ -6986,7 +8539,10 @@ pub mod pallet { } let limit = Self::effective_active_actor_limit(); let active_count = Self::active_instance_count(); - let actual_active_count = ActorHot::::iter_keys().count() as u32; + let control_entries = Self::frame_control_entries().ok_or(TryRuntimeError::Other( + "ActorControl frame topology is corrupt", + ))?; + let actual_active_count = control_entries.len() as u32; if T::MaxOwnerSlots::get() == 0 { return Err(TryRuntimeError::Other("MaxOwnerSlots must be nonzero")); } @@ -7002,19 +8558,47 @@ pub mod pallet { } if active_count != actual_active_count { return Err(TryRuntimeError::Other( - "ActiveActorCount does not match ActorHot cardinality", + "ActiveActorCount does not match ActorControl primary cardinality", )); } if active_count > limit { return Err(TryRuntimeError::Other( - "ActorHot count exceeds effective active actor limit", + "ActorControl primary count exceeds effective active actor limit", )); } - let stored_identity_count = ActorIdentities::::iter_keys().count() as u32; + let mut semantic_identities = alloc::collections::BTreeMap::new(); + for (actor_id, location, primary) in &control_entries { + let (identity, _, _) = Self::project_control_cell(primary, *location).ok_or( + TryRuntimeError::Other("ActorControl primary cannot restore its semantic identity"), + )?; + if semantic_identities.insert(*actor_id, identity).is_some() { + return Err(TryRuntimeError::Other( + "multiple ActorControl primaries own one identity", + )); + } + } + for (actor_id, identity) in ActorIdentities::::iter() { + if ActorControlLocators::::contains_key(actor_id) { + return Err(TryRuntimeError::Other( + "dormant ActorIdentity duplicates active primary authority", + )); + } + if !matches!( + Self::load_actor_state_for_frame_control(actor_id), + LoadedActorStateOf::Dormant(_) + ) || semantic_identities.insert(actor_id, identity).is_some() + { + return Err(TryRuntimeError::Other( + "scalar ActorIdentity without a primary is not uniquely dormant", + )); + } + } + let actual_identity_count = u32::try_from(semantic_identities.len()) + .map_err(|_| TryRuntimeError::Other("semantic Actor identity count exceeds u32"))?; let identity_count = ActorIdentityCount::::get(); - if identity_count != stored_identity_count { + if identity_count != actual_identity_count { return Err(TryRuntimeError::Other( - "ActorIdentityCount does not match ActorIdentities cardinality", + "ActorIdentityCount does not match active primaries plus dormant identities", )); } if identity_count > T::MaxActorIdentities::get() { @@ -7024,7 +8608,7 @@ pub mod pallet { } for actor_id in ActorContractHeads::::iter_keys() { if !matches!( - Self::load_actor_state(actor_id), + Self::load_actor_state_for_frame_control(actor_id), LoadedActorStateOf::Active(_) ) { return Err(TryRuntimeError::Other( @@ -7032,20 +8616,71 @@ pub mod pallet { )); } } + for (actor_id, chunk_index) in ActorContractTailChunks::::iter_keys() { + let head = ActorContractHeads::::get(actor_id).ok_or(TryRuntimeError::Other( + "ActorContract tail has no Contract head owner", + ))?; + let tail_count = head + .header + .step_count + .saturating_sub(1) + .div_ceil(MAX_STEPS_PER_TAIL_CHUNK); + if chunk_index >= tail_count { + return Err(TryRuntimeError::Other( + "ActorContract tail key exceeds the declared Contract geometry", + )); + } + } + for actor_id in ActorActivationAuthorities::::iter_keys() { + if !ActorContractHeads::::contains_key(actor_id) { + return Err(TryRuntimeError::Other( + "Actor activation authority has no Contract head owner", + )); + } + } let mut max_id: Option = None; - for actor_id in ActorHot::::iter_keys() { - let LoadedActorStateOf::Active(state) = Self::load_actor_state(actor_id) else { + for (actor_id, _, primary) in control_entries { + let LoadedActorStateOf::Active(state) = Self::load_actor_state_for_frame_control(actor_id) + else { return Err(TryRuntimeError::Other( - "ActorHot entry belongs to a corrupt actor partition set", + "ActorControl primary belongs to a corrupt actor partition set", )); }; + let frame_admission = primary.admission; let identity = state.identity; + if identity.last_control_mutation_block > frame_system::Pallet::::block_number() { + return Err(TryRuntimeError::Other( + "actor control mutation block is in the future", + )); + } let hot = state.hot; let contract = state.contract; let funding = state.funding; - if !ActorAdmissionCertificates::::contains_key(actor_id) { + // Receipt identities are fixed at admission; the host mapping can disappear later. + let mut static_funding_steps = contract.steps.clone(); + let mut staking_positions = alloc::collections::BTreeSet::new(); + static_funding_steps.retain(|step| { + if let ActorTask::Unstake { + asset, + shares: AmountResolution::PercentageOfLastFunding(_), + } = &step.task + { + staking_positions.insert(*asset); + false + } else { + true + } + }); + let expected_tracked = + Self::derive_funding_tracked_assets(&static_funding_steps).map_err(|_| { + TryRuntimeError::Other("Actor Contract funding sources cannot be rederived") + })?; + if !expected_tracked.is_subset(&funding.funding_tracked_assets) + || funding.funding_tracked_assets.len() > expected_tracked.len() + staking_positions.len() + || (!staking_positions.is_empty() && funding.funding_tracked_assets.is_empty()) + { return Err(TryRuntimeError::Other( - "Active actor has no admission certificate", + "ActorFunding tracked assets disagree with the Actor Contract", )); } let head = ActorContractHeads::::get(actor_id).ok_or(TryRuntimeError::Other( @@ -7054,6 +8689,29 @@ pub mod pallet { let resources = Self::derive_step_resource_envelopes(&contract).ok_or( TryRuntimeError::Other("Active actor Step resources cannot be rederived"), )?; + let expected_cursor = state.run_state.as_ref().map_or(0, |run| run.cursor); + if primary.cursor != expected_cursor { + return Err(TryRuntimeError::Other( + "ActorControl primary cursor disagrees with canonical Run authority", + )); + } + let expected_resources = if contract.steps.is_empty() { + ActorStepResourceEnvelope { + control: T::WeightInfo::scheduler_inner_zero_step_complete(), + effect: Weight::zero(), + } + } else { + *resources + .get(expected_cursor as usize) + .ok_or(TryRuntimeError::Other( + "ActorControl primary cursor has no canonical Step resources", + ))? + }; + if primary.resources != expected_resources { + return Err(TryRuntimeError::Other( + "ActorControl primary resources disagree with canonical Step authority", + )); + } let expected_pipeline_machine_envelope = Self::derive_pipeline_machine_envelope( identity.actor_class.actor_type(), &contract.steps, @@ -7075,16 +8733,13 @@ pub mod pallet { }; 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 + || authority.semantic_contract_id != frame_admission.semantic_contract_id + || authority.body_commitment != frame_admission.body_commitment + || authority.admission_identity != frame_admission.admission_identity { return Err(TryRuntimeError::Other( "indexed Observation activation authority disagrees with C6 Contract authority", @@ -7103,6 +8758,14 @@ pub mod pallet { } (None, None) => {} } + let expected_feeds = Self::derive_observation_feeds(&contract.trigger).map_err(|_| { + TryRuntimeError::Other("Actor Trigger observation feeds cannot be rederived") + })?; + if ActorObservationFeeds::::get(actor_id).unwrap_or_default() != expected_feeds { + return Err(TryRuntimeError::Other( + "Actor observation subscription membership disagrees with its Trigger", + )); + } // 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). @@ -7125,10 +8788,10 @@ pub mod pallet { consumed: false, .. } | TriggerRuntimeState::Cadenced { .. } - if hot.trigger_wakeup_pointer.is_none() => + if !hot.pending_signal && hot.trigger_wakeup_pointer.is_none() => { return Err(TryRuntimeError::Other( - "pending temporal actor has no Trigger temporal membership", + "unlatched temporal actor has no Trigger temporal membership", )); } TriggerRuntimeState::Stateless | TriggerRuntimeState::ObservationCrossing { .. } @@ -7199,7 +8862,7 @@ pub mod pallet { } for actor_id in ActorFunding::::iter_keys() { if !matches!( - Self::load_actor_state(actor_id), + Self::load_actor_state_for_frame_control(actor_id), LoadedActorStateOf::Active(_) ) { return Err(TryRuntimeError::Other( @@ -7213,7 +8876,8 @@ pub mod pallet { "ActorRunHead has no immutable ActorRunPayload", )); } - let LoadedActorStateOf::Active(state) = Self::load_actor_state(actor_id) else { + let LoadedActorStateOf::Active(state) = Self::load_actor_state_for_frame_control(actor_id) + else { return Err(TryRuntimeError::Other( "ActorRunState entry belongs to a corrupt actor partition set", )); @@ -7293,29 +8957,43 @@ pub mod pallet { "ActorRunState opening snapshot disagrees with the complete Contract", )); } - let expected_opening_predicates = contract + let mut expected_opening_predicates = 0usize; + let mut expected_opening_predicate_cursor = 0usize; + for (index, step) in contract .steps .iter() // deos-bypass: bounded-iter -- MaxSteps bounds the Contract. - .map(|step| { - step.precondition.as_ref().map_or(0, |precondition| { - precondition.opening_predicate_count() as usize - }) - }) - .sum::(); + .enumerate() + { + let count = step.precondition.as_ref().map_or(0, |precondition| { + precondition.opening_predicate_count() as usize + }); + expected_opening_predicates = + expected_opening_predicates + .checked_add(count) + .ok_or(TryRuntimeError::Other( + "Actor Contract Opening-predicate count overflows", + ))?; + if index < run_state.cursor as usize { + expected_opening_predicate_cursor = 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)) - { + if run_state.opening_predicate_cursor as usize != expected_opening_predicate_cursor { return Err(TryRuntimeError::Other( - "ActorRunState funding snapshot contains an untracked asset", + "ActorRunState opening predicate cursor disagrees with the committed Step prefix", )); } + for (asset, amount) in &run_state.funding_snapshot { + if !state.funding.funding_tracked_assets.contains(asset) || amount.is_zero() { + return Err(TryRuntimeError::Other( + "ActorRunState funding snapshot contains an untracked asset or zero amount", + )); + } + } } for actor_id in ActorRunPayloads::::iter_keys() { if !ActorRunHeads::::contains_key(actor_id) { @@ -7324,18 +9002,16 @@ pub mod pallet { )); } } - for actor_id in ActorIdentities::::iter_keys() { - let identity = ActorIdentities::::get(actor_id) - .ok_or(TryRuntimeError::Other("actor identity key has no value"))?; + for (actor_id, identity) in &semantic_identities { + if ActorControlLocators::::contains_key(actor_id) { + continue; + } if identity.last_control_mutation_block > frame_system::Pallet::::block_number() { return Err(TryRuntimeError::Other( "actor control mutation block is in the future", )); } - max_id = Some(max_id.map_or(actor_id, |prev| prev.max(actor_id))); - if Self::active_actor_exists(actor_id) { - continue; - } + max_id = Some(max_id.map_or(*actor_id, |prev| prev.max(*actor_id))); if ActorFunding::::contains_key(actor_id) || ActorRunHeads::::contains_key(actor_id) || ActorRunPayloads::::contains_key(actor_id) @@ -7345,7 +9021,7 @@ pub mod pallet { )); } match SovereignIndex::::get(&identity.sovereign_account) { - Some(mapped_id) if mapped_id == actor_id => {} + Some(mapped_id) if mapped_id == *actor_id => {} _ => { return Err(TryRuntimeError::Other( "Dormant SovereignIndex does not map sovereign_account back to actor_id", @@ -7369,13 +9045,22 @@ pub mod pallet { } } ActorClass::System { sovereign_id } => { - if identity.mutability != Mutability::Mutable { + if identity.mutability == Mutability::Immutable + && !T::GenesisSystemActors::dormant_system_actors() + .into_iter() + .any(|(genesis_id, owner, mutability)| { + genesis_id == *actor_id + && genesis_id == sovereign_id + && owner == identity.owner + && mutability == Mutability::Immutable + }) + { return Err(TryRuntimeError::Other( - "Dormant System Actors must be Mutable", + "Immutable Dormant System Actor is not declared by genesis", )); } if SystemSovereigns::::get(sovereign_id) - != Some(SystemSovereignState::Occupied(actor_id)) + != Some(SystemSovereignState::Occupied(*actor_id)) { return Err(TryRuntimeError::Other( "dormant System Actor disagrees with its sovereign locator", @@ -7401,9 +9086,9 @@ pub mod pallet { "OwnerSlotBitmaps bit has no SovereignIndex owner", )); }; - let Some(identity) = ActorIdentities::::get(actor_id) else { + let Some(identity) = semantic_identities.get(&actor_id) else { return Err(TryRuntimeError::Other( - "OwnerSlotBitmaps bit has no ActorIdentity owner", + "OwnerSlotBitmaps bit has no semantic Actor identity owner", )); }; if identity.owner != owner @@ -7429,10 +9114,10 @@ pub mod pallet { )); } let mut system_identity_owners = alloc::collections::BTreeMap::new(); - for (actor_id, identity) in ActorIdentities::::iter() { + for (actor_id, identity) in &semantic_identities { if let ActorClass::System { sovereign_id } = identity.actor_class && system_identity_owners - .insert(sovereign_id, actor_id) + .insert(sovereign_id, *actor_id) .is_some() { return Err(TryRuntimeError::Other( @@ -7464,9 +9149,11 @@ pub mod pallet { "occupied System sovereign locator disagrees with identity ownership", )); } - let identity = ActorIdentities::::get(actor_id).ok_or(TryRuntimeError::Other( - "occupied System sovereign locator has no ActorIdentity", - ))?; + let identity = semantic_identities + .get(&actor_id) + .ok_or(TryRuntimeError::Other( + "occupied System sovereign locator has no semantic Actor identity", + ))?; if identity.sovereign_account != sovereign_account || SovereignIndex::::get(&sovereign_account) != Some(actor_id) { @@ -7490,9 +9177,11 @@ pub mod pallet { .ok_or(TryRuntimeError::Other( "SovereignIndex cardinality exceeds u32", ))?; - let identity = ActorIdentities::::get(actor_id).ok_or(TryRuntimeError::Other( - "SovereignIndex owner has no ActorIdentity", - ))?; + let identity = semantic_identities + .get(&actor_id) + .ok_or(TryRuntimeError::Other( + "SovereignIndex owner has no semantic Actor identity", + ))?; if identity.sovereign_account != sovereign_account { return Err(TryRuntimeError::Other( "SovereignIndex key disagrees with ActorIdentity custody account", @@ -7501,7 +9190,7 @@ pub mod pallet { } if sovereign_index_count != identity_count { return Err(TryRuntimeError::Other( - "SovereignIndex cardinality does not match ActorIdentities", + "SovereignIndex cardinality does not match semantic Actor identities", )); } @@ -7511,239 +9200,281 @@ pub mod pallet { "MaxQueueLength is below effective active actor limit", )); } - let queue_occupancy = QueueOccupancy::::get(); + let queue_occupancy = ActorReadyOccupancy::::get(); if queue_occupancy > queue_capacity { return Err(TryRuntimeError::Other( "canonical queue physical occupancy exceeds MaxQueueLength", )); } - let next_ticket = NextQueueTicket::::get(); - let page_size = u64::from(T::QueuePageSize::get()); - let head = QueueHead::::get(); - let tail = QueueTail::::get(); - if head > tail { - return Err(TryRuntimeError::Other("canonical queue head exceeds tail")); - } - let queue_span = tail.checked_sub(head).ok_or(TryRuntimeError::Other( - "canonical queue physical span underflows", - ))?; - if head < tail && queue_span != u64::from(queue_occupancy) { + const CONTROL_PAGE_SIZE: u64 = 32; + let head = ActorReadyHead::::get(); + let tail = ActorReadyTail::::get(); + let span = tail + .checked_sub(head) + .ok_or(TryRuntimeError::Other("canonical Ready head exceeds tail"))?; + if span > u64::from(queue_capacity) + || queue_occupancy > active_count + || u64::from(queue_occupancy) > span + { return Err(TryRuntimeError::Other( - "canonical queue occupancy disagrees with its nonempty physical span", + "canonical Ready live count or physical span exceeds its bound", )); } - let mut physical_tickets = alloc::collections::BTreeMap::new(); - let mut physical_occupancy = 0u32; - for page_id in QueuePages::::iter_keys() { - let page = QueuePages::::get(page_id) - .ok_or(TryRuntimeError::Other("queue page key has no value"))?; - if page.is_empty() || page.len() > T::QueuePageSize::get() as usize { - return Err(TryRuntimeError::Other( - "canonical queue page has invalid length", - )); - } - let Some(page_start) = page_id.checked_mul(page_size) else { - return Err(TryRuntimeError::Other( - "canonical queue page range overflows", - )); - }; - let Some(page_end) = page_start.checked_add(page.len() as u64) else { - return Err(TryRuntimeError::Other( - "canonical queue page range overflows", - )); - }; - if page_end <= head || page_start >= tail { + let mut ready_pages = alloc::collections::BTreeSet::new(); + let mut ready_live = 0u32; + for (page_id, page) in ActorReadyFrameChunks::::iter(/* deos-bypass: bounded-iter -- TryRuntime-only topology audit. */) + { + let start = page_id + .checked_mul(CONTROL_PAGE_SIZE) + .ok_or(TryRuntimeError::Other( + "canonical Ready page range overflows", + ))?; + if page.len() != CONTROL_PAGE_SIZE as usize + || start >= tail + || start.saturating_add(CONTROL_PAGE_SIZE) <= head + || !ready_pages.insert(page_id) + { return Err(TryRuntimeError::Other( - "canonical queue page lies outside its live physical range", + "canonical Ready page has invalid width or range", )); } - for (slot, entry) in page.into_iter().enumerate() { - let position = page_start + for (slot, cell) in page.iter(/* deos-bypass: bounded-iter -- fixed C32. */).enumerate() { + let Some(cell) = cell else { continue }; + let ticket = start .checked_add(slot as u64) - .ok_or(TryRuntimeError::Other("canonical queue position overflows"))?; - if position < head { - continue; - } - if position >= tail || entry.ticket >= next_ticket { - return Err(TryRuntimeError::Other( - "canonical queue entry lies beyond its physical or global ticket range", - )); - } - physical_occupancy = physical_occupancy - .checked_add(1) - .ok_or(TryRuntimeError::Other( - "canonical queue occupancy overflows", - ))?; - if physical_tickets - .insert(entry.ticket, entry.actor_id) - .is_some() + .ok_or(TryRuntimeError::Other("canonical Ready ticket overflows"))?; + let location = ActorControlLocation::Ready { ticket }; + if ticket < head + || ticket >= tail + || ActorControlLocators::::get(cell.actor_id) != Some(location) + || Self::project_control_cell(cell, location).is_none() { return Err(TryRuntimeError::Other( - "canonical queue contains a duplicate global ticket", + "canonical Ready primary disagrees with its ticket or locator", )); } + ready_live = ready_live.checked_add(1).ok_or(TryRuntimeError::Other( + "canonical Ready live count overflows", + ))?; } } - if physical_occupancy != queue_occupancy { + let expected_ready_pages = if head == tail { + 0 + } else { + (tail - 1) / CONTROL_PAGE_SIZE - head / CONTROL_PAGE_SIZE + 1 + }; + if ready_pages.len() as u64 != expected_ready_pages || ready_live != queue_occupancy { return Err(TryRuntimeError::Other( - "canonical queue occupancy disagrees with physical entries", + "canonical Ready pages or live occupancy disagree", )); } - let mut live_queue_tickets = alloc::collections::BTreeSet::new(); - for actor_id in ActorHot::::iter_keys() { - let hot = ActorHot::::get(actor_id) - .ok_or(TryRuntimeError::Other("live-ticket hot key has no value"))?; - let Some(ticket) = hot.queue_ticket else { - continue; - }; - if ticket >= next_ticket || !live_queue_tickets.insert(ticket) { - return Err(TryRuntimeError::Other( - "ActorHot owns an invalid or duplicate global queue ticket", - )); - } - if !ActorIdentities::::contains_key(actor_id) { - return Err(TryRuntimeError::Other( - "ActorHot live ticket has no ActorIdentity", - )); - } - if physical_tickets.get(&ticket) != Some(&actor_id) { - return Err(TryRuntimeError::Other( - "ActorHot live ticket does not resolve to its canonical queue entry", - )); - } - } + // Empty slots inside the bounded [head, tail) span are FIFO tombstones, not live occupancy. if T::WakeupPageSize::get() == 0 { return Err(TryRuntimeError::Other("WakeupPageSize must be non-zero")); } - let mut wakeup_live_by_key = alloc::collections::BTreeMap::new(); - let mut live_wakeup_memberships = 0u32; - for (block, page_id) in WakeupPages::::iter_keys() { - let page = WakeupPages::::get((block, page_id)) - .ok_or(TryRuntimeError::Other("wakeup page key has no value"))?; - if page.entries.is_empty() || page.entries.len() > T::WakeupPageSize::get() as usize { + let mut waiting_pages = alloc::collections::BTreeMap::new(); + let mut waiting_live = alloc::collections::BTreeMap::new(); + let mut reference_count = 0u64; + for ((key, page_id), page) in ActorWaitingFrameChunks::::iter(/* deos-bypass: bounded-iter -- TryRuntime-only topology audit. */) + { + if page.entries.len() != CONTROL_PAGE_SIZE as usize + || page.scan_slot >= CONTROL_PAGE_SIZE as u32 + || page + .entries + .iter(/* deos-bypass: bounded-iter -- fixed C32. */) + .take(page.scan_slot as usize) + .any(Option::is_some) + { return Err(TryRuntimeError::Other( - "WakeupPages entry has invalid length", + "canonical Waiting page has invalid width or scan cursor", )); } - if page.scan_slot as usize > page.entries.len() { + let actual_live = page.entries.iter(/* deos-bypass: bounded-iter -- fixed C32. */).filter(|entry| entry.is_some()).count() + as u32; + if actual_live == 0 || actual_live != page.live_entries { return Err(TryRuntimeError::Other( - "WakeupPage scan cursor exceeds page length", + "canonical Waiting page live count disagrees with slots", )); } - let mut live_entries = 0u32; - for entry in page.entries.as_slice() { - if entry.is_some() { - live_entries = live_entries - .checked_add(1) - .ok_or(TryRuntimeError::Other("wakeup page live count overflows"))?; + let count = waiting_live.entry(key).or_insert(0u32); + *count = count + .checked_add(actual_live) + .ok_or(TryRuntimeError::Other( + "canonical Waiting per-key count overflows", + ))?; + for (slot, entry) in + page.entries.iter(/* deos-bypass: bounded-iter -- fixed C32. */).enumerate() + { + let Some(entry) = entry else { continue }; + let pointer = WakeupPointer { + block: key, + page_id, + slot: slot as u32, + }; + let (actor_id, primary) = match entry { + ActorWaitingEntry::Primary(cell) => { + let location = ActorControlLocation::Waiting { + key, + page: page_id, + slot: slot as u8, + }; + if ActorControlLocators::::get(cell.actor_id) != Some(location) + || Self::project_control_cell(cell, location).is_none() + { + return Err(TryRuntimeError::Other( + "canonical Waiting primary disagrees with its locator", + )); + } + (cell.actor_id, cell.clone()) + } + ActorWaitingEntry::Reference(reference) => { + let (_, primary) = + Self::load_primary_control_cell(reference.actor_id).map_err(|_| { + TryRuntimeError::Other("canonical Waiting reference has no primary") + })?; + if primary.admission.admission_identity != reference.admission_identity { + return Err(TryRuntimeError::Other( + "canonical Waiting reference admission identity disagrees", + )); + } + reference_count = reference_count + .checked_add(1) + .ok_or(TryRuntimeError::Other( + "canonical Waiting reference count overflows", + ))?; + (reference.actor_id, primary) + } + }; + let authoritative = match key { + WakeupKey::Block(_) => primary.hot.wakeup_pointer, + WakeupKey::Tick(_) => primary + .hot + .trigger_wakeup_pointer + .map(|trigger| WakeupPointer { + block: WakeupKey::Tick(trigger.tick), + page_id: trigger.page_id, + slot: trigger.slot, + }), + }; + if primary.actor_id != actor_id || authoritative != Some(pointer) { + return Err(TryRuntimeError::Other( + "WakeupPage slot addresses an actor with a different clock-domain pointer", + )); } } - if live_entries == 0 || page.live_entries != live_entries { - return Err(TryRuntimeError::Other( - "WakeupPage live-entry count disagrees with slots", - )); - } - let Some(bucket) = WakeupBuckets::::get(block) else { - return Err(TryRuntimeError::Other( - "WakeupPage has no matching bucket metadata", - )); - }; - if let Some(previous_page) = page.previous_page - && WakeupPages::::get((block, previous_page)).and_then(|previous| previous.next_page) - != Some(page_id) - { - return Err(TryRuntimeError::Other( - "WakeupPage previous link is not reciprocal", - )); + if waiting_pages.insert((key, page_id), page).is_some() { + return Err(TryRuntimeError::Other("duplicate canonical Waiting page")); } - if let Some(next_page) = page.next_page - && WakeupPages::::get((block, next_page)).and_then(|next| next.previous_page) - != Some(page_id) + } + if u64::from(active_count) + .checked_add(reference_count) + .is_none_or(|count| count > u64::from(active_count) * 3) + { + return Err(TryRuntimeError::Other( + "canonical primary plus reference cardinality exceeds 3N", + )); + } + let mut directory_keys = alloc::collections::BTreeSet::new(); + let mut visited_pages = alloc::collections::BTreeSet::new(); + for (key, occupancy) in ActorWaitingOccupancies::::iter(/* deos-bypass: bounded-iter -- TryRuntime-only topology audit. */) + { + if occupancy == 0 + || occupancy > T::MaxActiveActors::get() + || waiting_live.get(&key).copied() != Some(occupancy) + || !ActorWaitingHeads::::contains_key(key) + || !ActorWaitingTails::::contains_key(key) + || !directory_keys.insert(key) { return Err(TryRuntimeError::Other( - "WakeupPage next link is not reciprocal", - )); - } - if page_id == bucket.head_page && page.previous_page.is_some() { - return Err(TryRuntimeError::Other( - "WakeupBucket head page has a predecessor", + "canonical Waiting directory occupancy disagrees with pages", )); } - if page_id == bucket.tail_page && page.next_page.is_some() { + let head = ActorWaitingHeads::::get(key); + let tail = ActorWaitingTails::::get(key); + if head >= tail { return Err(TryRuntimeError::Other( - "WakeupBucket tail page has a successor", + "canonical Waiting directory has invalid range", )); } - let key_live = wakeup_live_by_key.entry(block).or_insert(0u32); - *key_live = key_live - .checked_add(live_entries) - .ok_or(TryRuntimeError::Other("wakeup bucket live count overflows"))?; - // 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; - }; - let expected = WakeupPointer { - block, - page_id, - slot: slot as WakeupSlot, - }; - 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 - .checked_add(1) - .ok_or(TryRuntimeError::Other( - "live wakeup membership count overflows", - ))?; - } - Some(_) => { + let first = head / CONTROL_PAGE_SIZE; + let last = (tail - 1) / CONTROL_PAGE_SIZE; + let mut page_id = first; + let mut previous = None; + loop { + if !visited_pages.insert((key, page_id)) { + return Err(TryRuntimeError::Other("canonical Waiting page links cycle")); + } + let page = waiting_pages + .get(&(key, page_id)) + .ok_or(TryRuntimeError::Other( + "canonical Waiting linked page is missing", + ))?; + if page.previous_page != previous + || page_id > last + || (page_id == first && head % CONTROL_PAGE_SIZE != u64::from(page.scan_slot)) + || (page_id != first && page.scan_slot != 0) + { + return Err(TryRuntimeError::Other( + "canonical Waiting page links or head cursor disagree", + )); + } + if page_id == last { + let tail_slot = ((tail - 1) % CONTROL_PAGE_SIZE + 1) as usize; + if page.next_page.is_some() + || page.entries.iter(/* deos-bypass: bounded-iter -- fixed C32. */).skip(tail_slot).any(Option::is_some) + { return Err(TryRuntimeError::Other( - "WakeupPage slot addresses an actor with a different clock-domain pointer", + "canonical Waiting tail page exceeds directory range", )); } - None => {} + break; + } + let next = page.next_page.ok_or(TryRuntimeError::Other( + "canonical Waiting linked chain ends before tail", + ))?; + if next <= page_id { + return Err(TryRuntimeError::Other( + "canonical Waiting links are not strictly ordered", + )); } + previous = Some(page_id); + page_id = next; + } + let index = ActorWaitingCursorIndices::::get(key).ok_or(TryRuntimeError::Other( + "canonical Waiting key has no shared cursor index", + ))?; + if index >= WakeupCursorLen::::get(key.clock()) + || Self::wakeup_cursor_get(key.clock(), index) != Some(key) + { + return Err(TryRuntimeError::Other( + "canonical Waiting cursor reverse index does not resolve", + )); } } - // 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) { + if visited_pages.len() != waiting_pages.len() || directory_keys.len() != waiting_live.len() { return Err(TryRuntimeError::Other( - "live wakeup memberships exceed dual active-actor capacity", + "canonical Waiting pages are disconnected or have no directory", )); } - for block in WakeupBuckets::::iter_keys() { - let bucket = WakeupBuckets::::get(block) - .ok_or(TryRuntimeError::Other("wakeup bucket key has no value"))?; - if wakeup_live_by_key.get(&block).copied() != Some(bucket.live_entries) { - return Err(TryRuntimeError::Other( - "WakeupBucket live-entry count disagrees with pages", - )); - } - if !WakeupPages::::contains_key((block, bucket.head_page)) - || !WakeupPages::::contains_key((block, bucket.tail_page)) - { + for key in ActorWaitingHeads::::iter_keys() + .chain(ActorWaitingTails::::iter_keys()) + .chain(ActorWaitingCursorIndices::::iter_keys()) + { + if !directory_keys.contains(&key) { return Err(TryRuntimeError::Other( - "WakeupBucket head or tail page is missing", + "canonical Waiting directory contains an orphan key", )); } - let cursor_len = WakeupCursorLen::::get(block.clock()); - if let Some(index) = bucket.cursor_index - && (index >= cursor_len || Self::wakeup_cursor_get(block.clock(), index) != Some(block)) + } + for clock in [WakeupClock::Block, WakeupClock::Tick] { + if directory_keys + .iter(/* deos-bypass: bounded-iter -- TryRuntime-only topology audit. */) + .filter(|key| key.clock() == clock) + .count() as u64 + != u64::from(WakeupCursorLen::::get(clock)) { return Err(TryRuntimeError::Other( - "WakeupBucket cursor reverse index does not resolve", + "canonical Waiting directory and heap cardinality disagree", )); } } @@ -7802,9 +9533,9 @@ pub mod pallet { "WakeupCursor contains a duplicate or wrong-clock key", )); } - if WakeupBuckets::::get(key).and_then(|bucket| bucket.cursor_index) != Some(index) { + if Self::wakeup_cursor_owner_index(key) != Some(index) { return Err(TryRuntimeError::Other( - "WakeupCursor key has no matching bucket reverse index", + "WakeupCursor key has no matching owner reverse index", )); } if index > 0 { @@ -7821,10 +9552,10 @@ pub mod pallet { } } let mut live_wakeup_pointers = alloc::collections::BTreeSet::new(); - for actor_id in ActorHot::::iter_keys() { - let hot = ActorHot::::get(actor_id).ok_or(TryRuntimeError::Other( - "wakeup-pointer hot key has no value", - ))?; + let wakeup_control_entries = Self::control_hot_entries_for_try_state().ok_or( + TryRuntimeError::Other("ActorControl frame topology is corrupt"), + )?; + for (actor_id, hot) in wakeup_control_entries { if let Some(pointer) = hot.wakeup_pointer { if !live_wakeup_pointers.insert((pointer.block, pointer.page_id, pointer.slot)) { return Err(TryRuntimeError::Other( @@ -7833,14 +9564,14 @@ pub mod pallet { } if !Self::wakeup_page_entry_matches(pointer, actor_id) { return Err(TryRuntimeError::Other( - "ActorHot Pipeline wakeup pointer does not resolve to its actor", + "ActorControl 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", + "ActorControl Pipeline wakeup pointer exceeds its terminal membership", )); } } @@ -7868,7 +9599,7 @@ pub mod pallet { } if !Self::wakeup_page_entry_matches(pointer, actor_id) { return Err(TryRuntimeError::Other( - "ActorHot Trigger wakeup pointer does not resolve to its actor", + "ActorControl Trigger wakeup pointer does not resolve to its actor", )); } } @@ -7881,33 +9612,8 @@ pub mod pallet { )); } } - let mut system_sovereign_count = 0u32; - for sovereign_id in SystemSovereigns::::iter_keys() { - let state = SystemSovereigns::::get(sovereign_id) - .ok_or(TryRuntimeError::Other("System sovereign key has no value"))?; - system_sovereign_count = system_sovereign_count - .checked_add(1) - .ok_or(TryRuntimeError::Other("System sovereign count overflow"))?; - if let SystemSovereignState::Occupied(actor_id) = state { - let class = Self::active_actor_view(actor_id) - .map(|actor| actor.actor_class) - .or_else(|| ActorIdentities::::get(actor_id).map(|identity| identity.actor_class)); - if class != Some(ActorClass::System { sovereign_id }) { - return Err(TryRuntimeError::Other( - "occupied System sovereign locator has no matching actor identity", - )); - } - } - } - if system_sovereign_count != SystemSovereignCount::::get() - || system_sovereign_count > T::MaxSystemSovereigns::get() - { - return Err(TryRuntimeError::Other( - "SystemSovereignCount disagrees with bounded locator registry", - )); - } let mut owner_hold_totals = alloc::collections::BTreeMap::new(); - for (actor_id, identity) in ActorIdentities::::iter() { + for (actor_id, identity) in &semantic_identities { match identity.actor_class.actor_type() { ActorType::System => { if ActorStateHolds::::contains_key(actor_id) { @@ -7917,7 +9623,25 @@ pub mod pallet { } } ActorType::User => { - let expected = Self::derive_actor_state_hold(actor_id, &identity).map_err(|_| { + let expected = if ActorControlLocators::::contains_key(actor_id) { + let (_, frame_identity, hot, admission) = + Self::load_frame_control_authority(*actor_id).ok_or(TryRuntimeError::Other( + "User Actor frame authority cannot be loaded for state-hold derivation", + ))?; + if frame_identity != *identity { + return Err(TryRuntimeError::Other( + "User Actor frame identity disagrees with semantic state-hold identity", + )); + } + Self::derive_actor_state_hold_with_authority( + *actor_id, + identity, + Some((&hot, &admission)), + ) + } else { + Self::derive_actor_state_hold_with_authority(*actor_id, identity, None) + } + .map_err(|_| { TryRuntimeError::Other("User Actor-state hold geometry cannot be rederived") })?; let record = ActorStateHolds::::get(actor_id).ok_or(TryRuntimeError::Other( @@ -7944,9 +9668,9 @@ pub mod pallet { } for (actor_id, _) in ActorStateHolds::::iter() { // deos-bypass: bounded-iter -- MaxActorIdentities bounds per-Actor hold records. - if !ActorIdentities::::contains_key(actor_id) { + if !semantic_identities.contains_key(&actor_id) { return Err(TryRuntimeError::Other( - "Actor-state hold record has no Actor identity", + "Actor-state hold record has no semantic Actor identity", )); } } diff --git a/template/pallets/actors/src/mock.rs b/template/pallets/actors/src/mock.rs index 300384bf..1d703d31 100644 --- a/template/pallets/actors/src/mock.rs +++ b/template/pallets/actors/src/mock.rs @@ -186,6 +186,9 @@ thread_local! { static BENCHMARK_INGRESS: RefCell> = RefCell::new(None); #[cfg(feature = "runtime-benchmarks")] static BENCHMARK_ASSET_OPS_INGRESS: RefCell = RefCell::new(false); + #[cfg(feature = "runtime-benchmarks")] + static BENCHMARK_OBSERVATION_REVISIONS: RefCell> = + RefCell::new(alloc::collections::BTreeMap::new()); } pub fn set_asset_minimum_balance(amount: Balance) { @@ -227,6 +230,8 @@ pub fn register_lp_pair(lp_asset: TestAsset, asset_a: TestAsset, asset_b: TestAs } pub fn reset_mock_adapters() { + #[cfg(feature = "runtime-benchmarks")] + BENCHMARK_OBSERVATION_REVISIONS.with(|revisions| revisions.borrow_mut().clear()); ASSET_BALANCES.with(|b| b.borrow_mut().clear()); BURNED.with(|b| b.borrow_mut().clear()); MINTED.with(|b| b.borrow_mut().clear()); @@ -275,7 +280,12 @@ impl crate::ObservationProvider for MockObservationProvider { fn current(feed: &u32) -> crate::CanonicalObservationState { match Self::observe(feed, 0, u32::MAX) { crate::ScalarObservationState::Fresh { value, .. } => { - crate::CanonicalObservationState::Available { value, revision: 1 } + #[cfg(feature = "runtime-benchmarks")] + let revision = BENCHMARK_OBSERVATION_REVISIONS + .with(|revisions| revisions.borrow().get(feed).copied().unwrap_or(1)); + #[cfg(not(feature = "runtime-benchmarks"))] + let revision = 1; + crate::CanonicalObservationState::Available { value, revision } } crate::ScalarObservationState::Uninitialized => { crate::CanonicalObservationState::Uninitialized @@ -312,6 +322,17 @@ impl FeeCollector for MockFeeCollector { pub struct MockAssetOps; +#[cfg(feature = "runtime-benchmarks")] +impl MockAssetOps { + fn frozen_account_key(who: &AccountId, asset: TestAsset) -> alloc::vec::Vec { + (b"mock-asset-account-freeze", who, asset).encode() + } + + fn account_is_frozen(who: &AccountId, asset: TestAsset) -> bool { + polkadot_sdk::sp_io::storage::exists(&Self::frozen_account_key(who, asset)) + } +} + impl AssetOps for MockAssetOps { fn transfer( from: &AccountId, @@ -319,6 +340,10 @@ impl AssetOps for MockAssetOps { asset: TestAsset, amount: Balance, ) -> Result<(), TaskFailure> { + #[cfg(feature = "runtime-benchmarks")] + if amount > 0 && Self::account_is_frozen(from, asset) { + return Err(DispatchError::Token(polkadot_sdk::sp_runtime::TokenError::Frozen).into()); + } if FAIL_TRANSFER_TO.with(|target| *target.borrow() == Some(*to)) { return Err(DispatchError::Other("MockTransferTargetFailed").into()); } @@ -359,6 +384,10 @@ impl AssetOps for MockAssetOps { } fn burn(who: &AccountId, asset: TestAsset, amount: Balance) -> Result<(), TaskFailure> { + #[cfg(feature = "runtime-benchmarks")] + if amount > 0 && Self::account_is_frozen(who, asset) { + return Err(DispatchError::Token(polkadot_sdk::sp_runtime::TokenError::Frozen).into()); + } match asset { TestAsset::Native => { use polkadot_sdk::frame_support::traits::Currency; @@ -418,6 +447,10 @@ impl AssetOps for MockAssetOps { } fn balance(who: &AccountId, asset: TestAsset) -> Balance { + #[cfg(feature = "runtime-benchmarks")] + if Self::account_is_frozen(who, asset) { + return 0; + } match asset { TestAsset::Native => { use polkadot_sdk::frame_support::traits::{ @@ -447,6 +480,10 @@ impl AssetOps for MockAssetOps { if amount == 0 { return Ok(()); } + #[cfg(feature = "runtime-benchmarks")] + if Self::account_is_frozen(from, asset) { + return Err(DispatchError::Token(polkadot_sdk::sp_runtime::TokenError::Frozen).into()); + } if asset == TestAsset::Native { use polkadot_sdk::frame_support::traits::{ fungible::Inspect as NativeInspect, tokens::Provenance, @@ -728,7 +765,9 @@ impl LiquidityOps for MockLiquidityOps { } let lp_minted = integer_sqrt(amount_a.saturating_mul(amount_b)); if lp_minted < min_lp_out { - return Err(DispatchError::Other("MinimumLpOutputNotMet").into()); + return Err(TaskFailure::temporary(DispatchError::Other( + "MinimumLpOutputNotMet", + ))); } Ok((amount_a, amount_b, lp_minted)) } @@ -802,6 +841,74 @@ pub struct MockBenchmarkHelper; #[cfg(feature = "runtime-benchmarks")] impl crate::BenchmarkHelper for MockBenchmarkHelper { + fn publish_observation(feed: u32, value: u128) -> DispatchResult { + let crate::CanonicalObservationState::Available { + value: previous, + revision, + } = >::current(&feed) + else { + return Err(DispatchError::Other("BenchmarkObservationUnavailable")); + }; + if previous != value { + let revision = revision + .checked_add(1) + .ok_or(DispatchError::Other("BenchmarkRevisionOverflow"))?; + Actors::note_observation_transition_with_provenance( + feed, + crate::ObservationTransition { + revision, + previous: Some(previous), + current: value, + }, + crate::TriggerCauseProvenance::ExternalPhase, + )?; + BENCHMARK_OBSERVATION_REVISIONS + .with(|revisions| revisions.borrow_mut().insert(feed, revision)); + } + set_observation( + feed, + crate::ScalarObservationState::Fresh { + value, + observed_at: System::block_number(), + }, + ); + Ok(()) + } + fn deactivate_observation_feed(feed: u32) -> DispatchResult { + set_observation(feed, crate::ScalarObservationState::Unavailable); + Ok(()) + } + fn advance_to_scheduler_tick(tick: u64) -> DispatchResult { + assert!(tick > System::block_number()); + System::set_block_number(tick); + Ok(()) + } + fn finalize_scheduler_clock() -> DispatchResult { + Ok(()) + } + fn set_asset_account_frozen( + _owner: &AccountId, + who: &AccountId, + asset: TestAsset, + frozen: bool, + ) -> DispatchResult { + if asset == TestAsset::Native { + return Err(DispatchError::Other("BenchmarkAssetFreezeUnsupported")); + } + if !ASSET_BALANCES.with(|balances| balances.borrow().contains_key(&(*who, asset))) { + return Err(DispatchError::Other("BenchmarkAssetAccountMissing")); + } + // Externalities own the flag so FRAME storage wipes reset it between repetitions. + // The runtime fixture, not this mock ledger, proves host authorization. + let key = MockAssetOps::frozen_account_key(who, asset); + if frozen { + polkadot_sdk::sp_io::storage::set(&key, &[1]); + } else { + polkadot_sdk::sp_io::storage::clear(&key); + } + Ok(()) + } + fn setup_add_liquidity( owner: &AccountId, ) -> Result<(TestAsset, TestAsset, Balance, Balance), DispatchError> { @@ -827,6 +934,7 @@ impl crate::BenchmarkHelper for MockBenchmar fn setup_stake(owner: &AccountId) -> Result<(TestAsset, Balance), DispatchError> { let asset = TestAsset::Local(1); let amount = 1_000_000; + set_staking_share_asset_available(true); MockAssetOps::mint(owner, asset, amount).map_err(|failure| failure.error)?; Ok((asset, amount)) } @@ -838,6 +946,30 @@ impl crate::BenchmarkHelper for MockBenchmar Ok((asset, shares)) } + fn setup_max_encoded_staking_positions( + owner: &AccountId, + max: u32, + ) -> Result, DispatchError> { + set_staking_share_asset_available(true); + (1..=max) + .map(|index| { + let asset = TestAsset::Local(index); + let shares = 1_000_000; + MockAssetOps::mint(owner, asset, shares).map_err(|failure| failure.error)?; + Ok((asset, MockStakingOps::share_balance(owner, asset))) + }) + .collect() + } + + fn remove_empty_staking_receipt(_owner: &AccountId, asset: TestAsset) -> DispatchResult { + if MockStakingOps::share_asset(asset).is_none() { + return Err(DispatchError::Other("BenchmarkReceiptMissing")); + } + // The mock models mapping availability; the runtime fixture proves receipt-class destruction. + set_staking_share_asset_available(false); + Ok(()) + } + fn setup_swap_exact_in( owner: &AccountId, ) -> Result<(TestAsset, TestAsset, Balance), DispatchError> { @@ -882,6 +1014,14 @@ impl crate::BenchmarkHelper for MockBenchmar Ok(Self::funding_assets(max)) } + fn setup_max_encoded_predicate_assets( + owner: &AccountId, + max: u32, + ) -> Result, DispatchError> { + // Mock balances have fixed-width scalar values without account-reason variants. + Self::setup_predicate_assets(owner, max) + } + fn setup_observation_feeds(max: u32) -> Result, DispatchError> { for feed in 1..=max { set_observation( @@ -895,10 +1035,62 @@ impl crate::BenchmarkHelper for MockBenchmar Ok((1..=max).collect()) } + fn setup_max_encoded_observation_feeds(max: u32) -> Result, DispatchError> { + // The mock has fixed-width scalar fields and no configurable feed geometry. + Self::setup_observation_feeds(max) + } + + fn setup_observation_read_state( + max: u32, + state: crate::BenchmarkObservationReadState, + _max_age_blocks: u32, + ) -> Result, DispatchError> { + use crate::BenchmarkObservationReadState; + let feeds = Self::setup_max_encoded_observation_feeds(max)?; + for feed in &feeds { + let value = match state { + BenchmarkObservationReadState::Missing | BenchmarkObservationReadState::Deactivated => { + crate::ScalarObservationState::Unavailable + } + BenchmarkObservationReadState::Uninitialized => { + crate::ScalarObservationState::Uninitialized + } + BenchmarkObservationReadState::Stale => crate::ScalarObservationState::Stale, + BenchmarkObservationReadState::Fresh => crate::ScalarObservationState::Fresh { + value: 1, + observed_at: System::block_number(), + }, + }; + set_observation(*feed, value); + } + Ok(feeds) + } + fn enable_asset_ops_ingress() { BENCHMARK_ASSET_OPS_INGRESS.with(|enabled| *enabled.borrow_mut() = true); } + fn transfer_signed( + source: &AccountId, + recipient: &AccountId, + asset: TestAsset, + amount: Balance, + ) -> DispatchResult { + let event = crate::AddressEvent { + destination: *recipient, + source: Some(*source), + asset, + amount, + provenance: Some(crate::FundingProvenance::Signed), + }; + crate::Pallet::::preflight_ingress(&event).map_err(|failure| failure.error)?; + let previous = BENCHMARK_ASSET_OPS_INGRESS.with(|enabled| enabled.replace(false)); + let movement = MockAssetOps::transfer(source, recipient, asset, amount); + BENCHMARK_ASSET_OPS_INGRESS.with(|enabled| enabled.replace(previous)); + movement.map_err(|failure| failure.error)?; + crate::Pallet::::notify_ingress(&event).map_err(|failure| failure.error) + } + fn setup_address_event_ingress( recipient: &AccountId, source: &AccountId, @@ -1272,10 +1464,10 @@ impl pallet_deos_actors::Config for Test { type PalletId = ActorsPalletId; type SystemOrigin = EnsureRoot; type GlobalBreakerOrigin = EnsureRoot; - type MaxContractSteps = ConstU32<8>; + type MaxContractSteps = ConstU32<12>; type MaxFundingTrackedAssets = ConstU32<10>; - type MaxOpeningSnapshotEntries = ConstU32<16>; - type MaxOpeningPredicateResults = ConstU32<32>; + type MaxOpeningSnapshotEntries = ConstU32<24>; + type MaxOpeningPredicateResults = ConstU32<48>; type MaxPreconditionClauses = ConstU32<4>; type MaxPredicatesPerClause = ConstU32<4>; type MaxPredicatesPerStep = ConstU32<4>; @@ -1285,14 +1477,29 @@ impl pallet_deos_actors::Config for Test { type QueuePageSize = ConstU32<32>; type WakeupPageSize = ConstU32<32>; type ObservationPageSize = ConstU32<16>; + #[cfg(not(feature = "runtime-benchmarks"))] type CrossingPageSize = ConstU32<16>; + #[cfg(feature = "runtime-benchmarks")] + type CrossingPageSize = ConstU32<128>; type MaxCrossingTransitionsPerFeed = ConstU32<4>; type MaxCrossingMembersPerFeed = ConstU32<10_000>; + #[cfg(not(feature = "runtime-benchmarks"))] type MaxUserCrossingMembersPerFeed = ConstU32<8>; + #[cfg(feature = "runtime-benchmarks")] + type MaxUserCrossingMembersPerFeed = ConstU32<128>; type MaxCrossingTransitionsPerBlock = ConstU32<4>; + #[cfg(not(feature = "runtime-benchmarks"))] type MaxCrossingLeavesPerBlock = ConstU32<8>; + #[cfg(feature = "runtime-benchmarks")] + type MaxCrossingLeavesPerBlock = ConstU32<64>; + #[cfg(not(feature = "runtime-benchmarks"))] type MaxCrossingPagesPerBlock = ConstU32<8>; + #[cfg(feature = "runtime-benchmarks")] + type MaxCrossingPagesPerBlock = ConstU32<64>; + #[cfg(not(feature = "runtime-benchmarks"))] type MaxCrossingActorsPerBlock = ConstU32<16>; + #[cfg(feature = "runtime-benchmarks")] + type MaxCrossingActorsPerBlock = ConstU32<128>; type CrossingWorkerWeightLimit = TestCrossingWorkerWeightLimit; type MaxQueueEntriesScannedPerBlock = ConstU32<1024>; type MaxObservationFanoutPagesPerBlock = ConstU32<64>; diff --git a/template/pallets/actors/src/reactions.rs b/template/pallets/actors/src/reactions.rs index 1b2104ce..f09a6ada 100644 --- a/template/pallets/actors/src/reactions.rs +++ b/template/pallets/actors/src/reactions.rs @@ -347,7 +347,7 @@ impl Pallet { let queue = maybe.get_or_insert_default(); if let Some(last) = queue.last() { ensure!( - transition.revision == last.revision.saturating_add(1) + last.revision.checked_add(1) == Some(transition.revision) && transition.previous == Some(last.current), Error::::CrossingTransitionInvariant ); diff --git a/template/pallets/actors/src/scheduler.rs b/template/pallets/actors/src/scheduler.rs index a67e49ac..cf3663ac 100644 --- a/template/pallets/actors/src/scheduler.rs +++ b/template/pallets/actors/src/scheduler.rs @@ -1,11 +1,13 @@ use super::pallet::*; use super::{ AddressEvent, AssetOps, BlockResourceDomain, BlockResourceLimits, BlockResourceState, - FundingAuthority, IngressFailure, StepControlExecution, StepControlOutcome, StepControlPhase, - StepControlPlacement, StepControlWeightProvider as _, TaskEffectWeightProvider as _, - weights::WeightInfo, + CanonicalObservationState, FundingAuthority, IngressFailure, ObservationProvider, + StepControlExecution, StepControlOutcome, StepControlPhase, StepControlPlacement, + StepControlWeightProvider as _, TaskEffectWeightProvider as _, weights::WeightInfo, }; -use alloc::{vec, vec::Vec}; +#[cfg(test)] +use alloc::vec; +use alloc::vec::Vec; use frame::prelude::*; use polkadot_sdk::frame_support::storage::transactional::with_transaction_opaque_err; use polkadot_sdk::sp_runtime::traits::{One, Zero}; @@ -23,28 +25,79 @@ struct QueueTopology { occupancy: u32, } +enum ReadyHeadOwner { + #[cfg(any(test, feature = "runtime-benchmarks"))] + DiscoverCanonical, + Loaded { + actor_id: ActorId, + ticket: QueueTicket, + hot: ActorHotStateOf, + }, + ClosedTombstone, +} + pub(crate) struct QueueAppendPlan { - actors: Vec<(ActorId, ActorHotStateOf)>, - pages: Vec<(QueuePageId, QueuePageOf)>, - next_ticket: QueueTicket, + publications: Vec>, next_tail: QueueTicket, next_occupancy: u32, } +struct PreparedReadyPublication { + ticket: QueueTicket, + cell: ActorControlCellOf, +} + +#[derive(Clone, Copy)] +enum TerminalCleanupReservation { + Included, + NotIncluded, +} + +impl TerminalCleanupReservation { + fn is_included(self) -> bool { + matches!(self, Self::Included) + } +} + +#[derive(Clone, Copy)] +enum ServiceCutoff { + Open, + Snapshotted, +} + +#[derive(Clone, Copy)] +pub enum WakeupBucketDisposition { + Retain, + Remove, +} + +impl ServiceCutoff { + fn is_snapshotted(self) -> bool { + matches!(self, Self::Snapshotted) + } +} + enum AdmissionDecision { Admit { weight: Weight, - terminal_cleanup_reserved: bool, + terminal_cleanup: TerminalCleanupReservation, + opening_observation: Option, }, Close { reason: CloseReason, weight: Weight, }, + Refuse(CompleteEntryOwner), Defer, Skip, Invariant, } +#[derive(Clone, Copy)] +enum CompleteEntryOwner { + CrossingUnavailable, +} + /// Closed outcome of one canonical FIFO placement attempt. Queue capacity /// exhaustion may preserve readiness through an exact later wakeup; monotonic /// ticket/page namespace exhaustion and corruption are not retryable and fail @@ -76,15 +129,19 @@ pub(crate) enum ObservationActivationOutcome { TerminalDeferred, } +#[derive(Clone, Copy)] +enum ObservationTerminalHandling { + Execute, + Defer, +} + pub(crate) struct ObservationQueueCandidate { state: ObservationActivationState, hot: ActorHotStateOf, } pub(crate) struct ObservationWakeupCandidate { - actor_id: ActorId, - actor_type: ActorType, - sovereign_account: T::AccountId, + state: ObservationActivationState, hot: ActorHotStateOf, wakeup_key: WakeupKey>, } @@ -126,12 +183,17 @@ pub(crate) enum PrimeSchedulePlan { pub(crate) enum ActivationAction { Close(CloseReason), CoalesceLive, + EnqueueReady(Result, EnqueueOutcome>), EnqueueTemporal(Result, EnqueueOutcome>), PrimeSchedule(Result>, EnqueueOutcome>), } +/// Synchronous transition authority: commit before any intervening Actor mutation. pub(crate) struct ActivationPlan { pub actor_id: ActorId, + + pub frame_admission: ActorAdmissionCertificateOf, + pub frame_source_state: ActiveActorStateOf, pub already_pending: bool, pub prospective_hot: ActorHotStateOf, pub instance: ActiveActorViewOf, @@ -160,17 +222,46 @@ enum BlockKind { NonWeight, } -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) enum AttemptTransactionError { FeeCollection, StateHold, Invariant, } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum ActorControlTransitionError { + Invariant, + IndexExhausted, +} + +impl From for ActorControlTransitionError { + fn from(_: polkadot_sdk::sp_runtime::DispatchError) -> Self { + Self::Invariant + } +} + +#[derive(Clone, Copy)] +pub(crate) enum ActorWaitingAuthority { + Trigger, + Service, +} + pub(crate) struct StepCommitEvidence { pub(crate) closed_for_exhaustion: bool, pub(crate) actual_control_weight: Weight, pub(crate) actual_effect_weight: Weight, + pub(crate) attempt: ActorAttemptEvidence, +} + +pub(crate) struct ActorAttemptEvidence { + status: AttemptDisposition, + cycle_nonce: u64, + start_cursor: u32, + run_cursor: Option, + unsuccessful_attempts_at_cursor: Option, + cumulative_outcomes: OutcomeTotals, + step: Option, } impl From for AttemptTransactionError { @@ -202,22 +293,98 @@ impl CyclePass { enum FifoStepResult { NoWork, - Progress { executed: bool }, + Progress { + executed: bool, + attempt: Option, + }, Blocked(BlockKind), } +#[derive(Clone, Copy)] +enum ActorServiceSource { + FifoHead, + SelectedReady, +} + +#[derive(Clone, Copy)] +struct ProvisionalEntryAccounting { + cycle_before_entry: Weight, + control_before_entry: Option, +} + +impl ProvisionalEntryAccounting { + fn replacement_meter(self, meter: &WeightMeter) -> WeightMeter { + WeightMeter::with_consumed_and_limit(self.cycle_before_entry, meter.limit()) + } + + fn can_settle_complete( + self, + cycle_meter: &WeightMeter, + control_meter: Option<&WeightMeter>, + complete_weight: Weight, + ) -> bool { + let cycle_fits = self + .cycle_before_entry + .checked_add(&complete_weight) + .is_some_and(|consumed| consumed.all_lte(cycle_meter.limit())); + let control_fits = match (self.control_before_entry, control_meter) { + (None, None) => true, + (Some(before), Some(meter)) => before + .checked_add(&complete_weight) + .is_some_and(|consumed| consumed.all_lte(meter.limit())), + _ => false, + }; + cycle_fits && control_fits + } + + fn settle_complete( + self, + cycle_meter: &mut WeightMeter, + control_meter: Option<&mut WeightMeter>, + complete_weight: Weight, + ) { + *cycle_meter = + WeightMeter::with_consumed_and_limit(self.cycle_before_entry, cycle_meter.limit()); + cycle_meter.consume(complete_weight); + if let (Some(before), Some(meter)) = (self.control_before_entry, control_meter) { + *meter = WeightMeter::with_consumed_and_limit(before, meter.limit()); + meter.consume(complete_weight); + } + } +} + enum HeadDiscovery { Empty, - Head(QueueTicket, QueueEntry), + Head( + QueueTicket, + QueueEntry, + ProvisionalEntryAccounting, + ), WeightStall, PassExhausted, InvariantStall, } impl Pallet { + pub fn next_queue_ticket() -> QueueTicket { + ActorReadyTail::::get() + } + + pub fn queue_head() -> QueueTicket { + ActorReadyHead::::get() + } + + pub fn queue_tail() -> QueueTicket { + ActorReadyTail::::get() + } + + pub fn queue_occupancy() -> u32 { + ActorReadyOccupancy::::get() + } + #[cfg(any(test, feature = "runtime-benchmarks"))] pub(crate) fn execute_cycle(remaining_weight: Weight) -> CyclePass { - Self::execute_cycle_to_cutoff(remaining_weight, NextQueueTicket::::get()) + Self::execute_cycle_to_cutoff(remaining_weight, ActorReadyTail::::get()) } pub(crate) fn execute_cycle_to_cutoff( @@ -278,7 +445,7 @@ impl Pallet { None => None, }; let mut cycle_meter = WeightMeter::with_limit(remaining_weight); - let mut scan_control_meter = resources + let mut control_meter = resources .as_ref() .map(|(_, _, _, control_maximum)| WeightMeter::with_limit(*control_maximum)); let now = frame_system::Pallet::::block_number(); @@ -294,7 +461,7 @@ impl Pallet { let head = Self::live_queue_head( cutoff, &mut cycle_meter, - scan_control_meter.as_mut(), + control_meter.as_mut(), &mut scanned, max_scanned, ); @@ -305,19 +472,23 @@ impl Pallet { break; } HeadDiscovery::PassExhausted => break, - HeadDiscovery::Head(position, entry) => { + HeadDiscovery::Head(position, entry, accounting) => { match Self::service_live_queue_entry( (position, entry), now, &mut cycle_meter, + control_meter.as_mut(), &mut effect_consumed, &mut effect_reconciliation_uncertain, resources .as_mut() .map(|(state, limits, domain, _)| (&mut **state, *limits, *domain)), + ActorServiceSource::FifoHead, + accounting, ) { FifoStepResult::Progress { executed: did_execute, + .. } => executed = executed.saturating_add(u32::from(did_execute)), FifoStepResult::NoWork => continue, FifoStepResult::Blocked(_kind) => { @@ -353,11 +524,17 @@ impl Pallet { pass } + /// Sole cheap readiness-presence seam for block hooks and FIFO classification. frame replaces the + /// backing counters; callers must not inspect scalar head/tail independently. + pub(crate) fn ready_work_exists() -> bool { + ActorReadyHead::::get() < ActorReadyTail::::get() + } + fn classify_current_queue(cutoff: QueueTicket) -> HeadDiscovery> { if Self::queue_topology_preflight(QueueMutation::Head).is_err() { return HeadDiscovery::InvariantStall; } - if QueueHead::::get() >= QueueTail::::get() { + if !Self::ready_work_exists() { return HeadDiscovery::Empty; } match Self::paged_head_entry() { @@ -370,7 +547,7 @@ impl Pallet { /// be admitted. This path performs no unmetered actor-partition reads; the next funded pass /// decides whether the entry is live, stale, or corrupt. fn head_blocked_by_weight(cutoff: QueueTicket) -> bool { - QueueHead::::get() < QueueTail::::get() + Self::ready_work_exists() && Self::paged_head_entry().is_some_and(|(_, entry)| entry.ticket < cutoff) } @@ -397,11 +574,15 @@ impl Pallet { HeadDiscovery::Empty }; } + let accounting = ProvisionalEntryAccounting { + cycle_before_entry: cycle_meter.consumed(), + control_before_entry: scan_control_meter.as_ref().map(|meter| meter.consumed()), + }; cycle_meter.consume(scan_weight); if let Some(meter) = scan_control_meter.as_deref_mut() { meter.consume(scan_weight); } - let before = QueueHead::::get(); + let before = ActorReadyHead::::get(); let stats = match Self::paged_drain_tombstones(cutoff, 1) { Ok(stats) => stats, Err(_) => return HeadDiscovery::InvariantStall, @@ -410,11 +591,13 @@ impl Pallet { return Self::classify_current_queue(cutoff); } *scanned = scanned.saturating_add(stats.entries_scanned); - if QueueHead::::get() != before { + if ActorReadyHead::::get() != before { continue; } return match Self::paged_head_entry() { - Some((position, entry)) if entry.ticket < cutoff => HeadDiscovery::Head(position, entry), + Some((position, entry)) if entry.ticket < cutoff => { + HeadDiscovery::Head(position, entry, accounting) + } Some(_) => HeadDiscovery::Empty, None => Self::classify_current_queue(cutoff), }; @@ -434,14 +617,14 @@ impl Pallet { 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), + HeadDiscovery::Head(_, entry, _) => (1, Some(entry), scanned), HeadDiscovery::WeightStall => (2, None, scanned), HeadDiscovery::InvariantStall => (3, None, scanned), HeadDiscovery::PassExhausted => (4, None, scanned), } } - fn charge_pipeline_opening( + pub(crate) fn charge_pipeline_opening( actor_id: ActorId, instance: &ActiveActorViewOf, ) -> Result<(), AttemptTransactionError> { @@ -460,186 +643,2017 @@ impl Pallet { Ok(()) } - pub(crate) fn execute_zero_step_opening_and_place( + pub(crate) fn deposit_action_fee_receipt( actor_id: ActorId, - mut instance: ActiveActorViewOf, + cycle_nonce: u64, + step_index: u32, + actual_effect_weight: Weight, + fee: T::Balance, + ) { + Self::deposit_event(Event::ActionFeeCharged { + actor_id, + cycle_nonce, + step_index, + actual_effect_weight, + fee, + }); + } + + #[cfg(feature = "runtime-benchmarks")] + pub(crate) fn execute_zero_step_from_consumed_fixture( + actor_id: ActorId, + state: ActiveActorStateOf, + admission: &ActorAdmissionCertificateOf, now: BlockNumberFor, ) -> Result<(), AttemptTransactionError> { - if !instance.steps.is_empty() - || instance.cycle_state != CycleState::Idle - || ActorRunStateStore::::contains_key(actor_id) - { + Self::execute_zero_step_from_consumed_frame(actor_id, state, admission, now, None).map(|_| ()) + } + + fn prepare_cadenced_rearm_hot( + actor_id: ActorId, + instance: &ActiveActorViewOf, + admission: &ActorAdmissionCertificateOf, + mut hot: ActorHotStateOf, + ) -> Result, AttemptTransactionError> { + let Trigger::Cadenced { every_ticks } = &instance.trigger else { 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(_) + }; + let anchor_tick = instance + .temporal_anchor_tick + .ok_or(AttemptTransactionError::Invariant)?; + if let Some(pointer) = hot.trigger_wakeup_pointer { + Self::invalidate_wakeup_reference( + actor_id, + WakeupPointer { + block: WakeupKey::Tick(pointer.tick), + page_id: pointer.page_id, + slot: pointer.slot, + }, + admission.admission_identity, ) + .map_err(|_| AttemptTransactionError::Invariant)?; + hot.trigger_wakeup_pointer = None; + } + let now_tick = + Self::current_scheduler_tick().map_err(|_| AttemptTransactionError::Invariant)?; + let due_tick = next_cadence_due_tick(anchor_tick, *every_ticks, now_tick) + .ok_or(AttemptTransactionError::Invariant)?; + let (page_id, slot) = Self::schedule_fresh_wakeup_reference( + actor_id, + WakeupKey::Tick(due_tick), + admission.admission_identity, + ) + .map_err(|_| AttemptTransactionError::Invariant)?; + Ok(Self::with_wakeup_pointer( + hot, + WakeupKey::Tick(due_tick), + page_id, + slot, + )) + } + + fn prepare_opening_rearm_hot( + actor_id: ActorId, + opening: &ActiveActorViewOf, + admission: &ActorAdmissionCertificateOf, + mut hot: ActorHotStateOf, + opening_observation: Option, + ) -> Result, AttemptTransactionError> { + match &opening.trigger { + Trigger::AtTime { .. } => { + let TriggerRuntimeState::AtTime { consumed, .. } = &mut hot.trigger_runtime_state else { + return Err(AttemptTransactionError::Invariant); + }; + // Replacement may preserve a paid latch from a different Trigger family. + *consumed = true; + Ok(hot) + } + Trigger::Cadenced { .. } => { + Self::prepare_cadenced_rearm_hot(actor_id, opening, admission, hot) + } + Trigger::ObservationCrossing { .. } => { + Self::prepare_crossing_rearm_hot(actor_id, opening, admission, opening_observation) + .map_err(|_| AttemptTransactionError::Invariant) + .map(|replacement| match replacement { + Some(mut replacement) => { + // Detector rearming precedes the common Opening core's latch consumption. + replacement.pending_signal = hot.pending_signal; + replacement + } + None => hot, + }) + } + Trigger::ObservationChange { .. } => { + IndexedTriggerDetectionDisabled::::remove(actor_id); + Ok(hot) + } + Trigger::Manual | Trigger::AddressEvent { .. } => Ok(hot), + } + } + + fn execute_zero_step_from_consumed_frame( + actor_id: ActorId, + mut state: ActiveActorStateOf, + admission: &ActorAdmissionCertificateOf, + now: BlockNumberFor, + opening_observation: Option, + ) -> Result { + if !matches!( + state.contract.trigger, + Trigger::Manual + | Trigger::AddressEvent { .. } + | Trigger::ObservationChange { .. } + | Trigger::ObservationCrossing { .. } + | Trigger::AtTime { .. } + | Trigger::Cadenced { .. } + ) || !state.contract.steps.is_empty() + || state.hot.cycle_state != CycleState::Idle + || state.run_state.is_some() + || ActorControlLocators::::contains_key(actor_id) { 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 + state.hot.queue_ticket = None; + let opening = Self::derive_active_actor_view( + state.identity.clone(), + state.hot.clone(), + state.contract.clone(), + ); + state.hot = Self::prepare_opening_rearm_hot( + actor_id, + &opening, + admission, + state.hot, + opening_observation, + )?; + Self::charge_pipeline_opening(actor_id, &opening)?; + let cycle_nonce = state + .identity .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(()) + state.identity.cycle_nonce = cycle_nonce; + state.hot.pending_signal = false; + state.hot.last_cycle_block = Some(now); + state.hot.unsuccessful_attempt_streak = 0; + state.funding.funding_accumulated.clear(); + ActorFunding::::insert(actor_id, &state.funding); + Self::deposit_event(Event::CycleStarted { + actor_id, + cycle_nonce, + }); + Self::deposit_event(Event::CycleSummary { + actor_id, + cycle_nonce, + result: CycleResult::Completed, + outcomes: OutcomeTotals::default(), + }); + if state + .contract + .auto_close_at_cycle_nonce + .is_some_and(|target| cycle_nonce >= target) + { + Self::finalize_actor_from_consumed_state( + actor_id, + state, + admission, + CloseReason::AutoCloseNonceReached, + ) + .map_err(|_| AttemptTransactionError::Invariant)?; + return Ok(Self::step_simulation_evidence( + cycle_nonce, + 0, + AttemptDisposition::Closed(CloseReason::AutoCloseNonceReached), + OutcomeTotals::default(), + None, + None, + )); + } + let resources = ActorStepResourceEnvelope { + control: T::WeightInfo::scheduler_inner_zero_step_complete(), + effect: Weight::zero(), + }; + let instance = Self::derive_active_actor_view( + state.identity.clone(), + state.hot.clone(), + state.contract.clone(), + ); + let unsignaled_hot = state.hot.clone(); + match Self::schedule_next_work_with_authority( + actor_id, + &instance, + state.hot.clone(), + &state.identity, + None, + admission, + resources, + now, + ServiceCutoff::Snapshotted, + ) { + Ok(StepControlPlacement::None) => Self::restore_unsignaled_from_authority( + actor_id, + unsignaled_hot, + &state.identity, + None, + admission, + resources, + ) + .map_err(|_| AttemptTransactionError::Invariant)?, + Ok(_) => {} + Err(_) => { + Self::finalize_actor_from_consumed_state( + actor_id, + state, + admission, + CloseReason::SchedulerIndexExhausted, + ) + .map_err(|_| AttemptTransactionError::Invariant)?; + return Ok(Self::step_simulation_evidence( + cycle_nonce, + 0, + AttemptDisposition::Closed(CloseReason::SchedulerIndexExhausted), + OutcomeTotals::default(), + None, + None, + )); + } + } + Self::reconcile_actor_state_hold_with_authority(actor_id) + .map_err(|_| AttemptTransactionError::StateHold)?; + Ok(Self::step_simulation_evidence( + cycle_nonce, + 0, + AttemptDisposition::Completed, + OutcomeTotals::default(), + None, + None, + )) } - pub(crate) fn execute_current_step_and_place( + fn execute_stop_cycle_from_consumed_frame( actor_id: ActorId, - execution_instance: ActiveActorViewOf, + mut state: ActiveActorStateOf, commit_plan: CurrentStepPlanOf, + admission: &ActorAdmissionCertificateOf, 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 step = commit_plan.loaded_step.step.clone(); + if state.hot.cycle_state != CycleState::Idle + || state.run_state.is_some() + || commit_plan.run.is_some() + || ActorControlLocators::::contains_key(actor_id) + || commit_plan.ticket.actor_id != actor_id + || commit_plan.ticket.cursor != 0 + || commit_plan.loaded_step.cursor != 0 + || commit_plan.ticket.cycle_nonce + != state + .identity + .cycle_nonce + .checked_add(1) + .ok_or(AttemptTransactionError::Invariant)? + || !matches!(step.task, super::types::Task::StopCycle) + || state.contract.steps.first() != Some(&step) + || commit_plan.identity != state.identity + || commit_plan.hot != state.hot + || commit_plan.funding != state.funding + || commit_plan.admission != *admission + { + return Err(AttemptTransactionError::Invariant); + } + state.hot.queue_ticket = None; + let instance = Self::derive_active_actor_view( + state.identity.clone(), + state.hot.clone(), + state.contract.clone(), + ); + let control_context = + Self::execution_step_control_weight_context(&instance, None, &commit_plan.loaded_step) + .ok_or(AttemptTransactionError::Invariant)?; 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) + if T::StepControlWeight::maximum_control_weight(control_context, &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) { + let expected_fee = Self::maximum_current_action_fee( + instance.actor_class.actor_type(), + &step, + commit_plan.loaded_step.resources, + ) + .map_err(|_| AttemptTransactionError::Invariant)?; + if commit_plan.maximum_fee != expected_fee { return Err(AttemptTransactionError::Invariant); } + state.hot = Self::prepare_opening_rearm_hot(actor_id, &instance, admission, state.hot, None)?; + Self::charge_pipeline_opening(actor_id, &instance)?; + let opening_predicate_results = Self::capture_opening_predicate_results( + &instance.sovereign_account, + &instance.steps, + commit_plan.maximum_fee.total_fee, + ); + let mut opening_predicate_index = 0usize; + let predicate_matches = Self::evaluate_step_precondition( + step.precondition.as_ref(), + &instance.sovereign_account, + commit_plan.maximum_fee.total_fee, + &opening_predicate_results, + &mut opening_predicate_index, + ) + .map_err(|_| AttemptTransactionError::Invariant)?; + let cycle_nonce = commit_plan.ticket.cycle_nonce; + state.identity.cycle_nonce = cycle_nonce; + state.hot.cycle_state = CycleState::Idle; + state.hot.pending_signal = false; + state.hot.last_cycle_block = Some(now); + state.hot.unsuccessful_attempt_streak = 0; + state.funding.funding_accumulated.clear(); + ActorFunding::::insert(actor_id, &state.funding); + Self::deposit_event(Event::CycleStarted { + actor_id, + cycle_nonce, + }); + let (effect_execution, outcomes) = if predicate_matches { + Self::record_stop_cycle_event(actor_id, cycle_nonce, 0); + ( + super::TaskEffectExecution::Invoked, + OutcomeTotals { + executed_steps: 1, + ..Default::default() + }, + ) + } else { + Self::deposit_event(Event::StepSkipped { + actor_id, + cycle_nonce, + step_index: 0, + reason: StepSkippedReason::PreconditionFalse, + }); + ( + super::TaskEffectExecution::NotInvoked, + OutcomeTotals { + precondition_skips: 1, + ..Default::default() + }, + ) + }; + Self::deposit_event(Event::CycleSummary { + actor_id, + cycle_nonce, + result: CycleResult::Completed, + outcomes, + }); + 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, + let mut attempt_status = AttemptDisposition::Completed; + let placement = if state + .contract + .auto_close_at_cycle_nonce + .is_some_and(|target| cycle_nonce >= target) + { + Self::finalize_actor_from_consumed_state( + actor_id, + state.clone(), + admission, + CloseReason::AutoCloseNonceReached, + ) + .map_err(|_| AttemptTransactionError::Invariant)?; + attempt_status = AttemptDisposition::Closed(CloseReason::AutoCloseNonceReached); + StepControlPlacement::None + } else { + let placement_instance = Self::derive_active_actor_view( + state.identity.clone(), + state.hot.clone(), + state.contract.clone(), + ); + let unsignaled_hot = state.hot.clone(); + match Self::schedule_next_work_with_authority( + actor_id, + &placement_instance, + state.hot.clone(), + &state.identity, + None, + admission, + commit_plan.loaded_step.resources, + now, + ServiceCutoff::Snapshotted, + ) { + Ok(placement) => { + if placement == StepControlPlacement::None { + Self::restore_unsignaled_from_authority( + actor_id, + unsignaled_hot, + &state.identity, + None, + admission, + commit_plan.loaded_step.resources, + ) + .map_err(|_| AttemptTransactionError::Invariant)?; + } + placement + } Err(error) => { - if !Self::scheduler_index_is_exhausted(error) - || Self::close_for_scheduler_index_exhaustion(actor_id).is_err() - { + if !Self::scheduler_index_is_exhausted(error) { return Err(AttemptTransactionError::Invariant); } + Self::finalize_actor_from_consumed_state( + actor_id, + state.clone(), + admission, + CloseReason::SchedulerIndexExhausted, + ) + .map_err(|_| AttemptTransactionError::Invariant)?; closed_for_exhaustion = true; + attempt_status = AttemptDisposition::Closed(CloseReason::SchedulerIndexExhausted); 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_effect_weight = + T::TaskEffectWeight::actual_effect_weight(&step.task, effect_execution) + .ok_or(AttemptTransactionError::Invariant)?; + if !actual_effect_weight.all_lte(reserved_effect_weight) { + return Err(AttemptTransactionError::Invariant); + } let actual_control_weight = T::StepControlWeight::actual_control_weight( control_context, - &control_step, + &step, reserved_control_weight, - control_execution, + StepControlExecution { + phase: StepControlPhase::Opening, + outcome: StepControlOutcome::Completed, + placement, + task_effect: effect_execution, + action_fee_collected: false, + }, ) .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, + instance.actor_class.actor_type(), + &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) + if instance.actor_class.actor_type() == ActorType::User && !actual_fee.total_fee.is_zero() { + Self::collect_user_step_fee(&instance.sovereign_account, 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, - }); + if ActorControlLocators::::contains_key(actor_id) { + Self::reconcile_actor_state_hold_with_authority(actor_id) + .map_err(|_| AttemptTransactionError::StateHold)?; } - Self::reconcile_actor_state_hold(actor_id).map_err(|_| AttemptTransactionError::StateHold)?; Ok(StepCommitEvidence { closed_for_exhaustion, actual_control_weight, actual_effect_weight, + attempt: Self::step_simulation_evidence( + cycle_nonce, + 0, + attempt_status, + outcomes, + None, + Some(if predicate_matches { + StepOutcome::Stopped + } else { + StepOutcome::Skipped(StepSkippedReason::PreconditionFalse) + }), + ), + }) + } + + fn execute_running_stop_cycle_from_consumed_frame( + actor_id: ActorId, + mut state: ActiveActorStateOf, + commit_plan: CurrentStepPlanOf, + admission: &ActorAdmissionCertificateOf, + now: BlockNumberFor, + ) -> Result { + let step = commit_plan.loaded_step.step.clone(); + let mut run = commit_plan + .run + .clone() + .ok_or(AttemptTransactionError::Invariant)?; + if state.hot.cycle_state != CycleState::Running + || state.run_state.is_none() + || !run.running_is_coherent() + || ActorControlLocators::::contains_key(actor_id) + || commit_plan.ticket.actor_id != actor_id + || commit_plan.ticket.cursor != run.cursor + || commit_plan.ticket.cycle_nonce != run.cycle_nonce + || commit_plan.loaded_step.cursor != run.cursor + || !matches!(step.task, super::types::Task::StopCycle) + || commit_plan.identity != state.identity + || commit_plan.hot != state.hot + || commit_plan.funding != state.funding + || commit_plan.admission != *admission + { + return Err(AttemptTransactionError::Invariant); + } + let instance = Self::derive_active_actor_view( + state.identity.clone(), + state.hot.clone(), + state.contract.clone(), + ); + let control_context = + Self::execution_step_control_weight_context(&instance, Some(&run), &commit_plan.loaded_step) + .ok_or(AttemptTransactionError::Invariant)?; + let reserved_control_weight = commit_plan.loaded_step.resources.control; + let reserved_effect_weight = commit_plan.loaded_step.resources.effect; + let expected_fee = Self::maximum_current_action_fee( + instance.actor_class.actor_type(), + &step, + commit_plan.loaded_step.resources, + ) + .map_err(|_| AttemptTransactionError::Invariant)?; + if commit_plan.maximum_fee != expected_fee { + return Err(AttemptTransactionError::Invariant); + } + let mut predicate_index = run.opening_predicate_cursor as usize; + let predicate_matches = Self::evaluate_step_precondition( + step.precondition.as_ref(), + &instance.sovereign_account, + commit_plan.maximum_fee.total_fee, + &run.opening_predicate_results, + &mut predicate_index, + ) + .map_err(|_| AttemptTransactionError::Invariant)?; + let cycle_nonce = run.cycle_nonce; + let cursor = run.cursor; + let (effect_execution, outcomes) = if predicate_matches { + run.cumulative_outcomes.executed_steps = run + .cumulative_outcomes + .executed_steps + .checked_add(1) + .ok_or(AttemptTransactionError::Invariant)?; + Self::record_stop_cycle_event(actor_id, cycle_nonce, cursor); + (super::TaskEffectExecution::Invoked, run.cumulative_outcomes) + } else { + run.cumulative_outcomes.precondition_skips = run + .cumulative_outcomes + .precondition_skips + .checked_add(1) + .ok_or(AttemptTransactionError::Invariant)?; + Self::deposit_event(Event::StepSkipped { + actor_id, + cycle_nonce, + step_index: cursor, + reason: StepSkippedReason::PreconditionFalse, + }); + ( + super::TaskEffectExecution::NotInvoked, + run.cumulative_outcomes, + ) + }; + ActorRunStateStore::::remove(actor_id); + state.run_state = None; + state.identity.cycle_nonce = cycle_nonce; + state.hot.cycle_state = CycleState::Idle; + state.hot.queue_ticket = None; + state.hot.last_cycle_block = Some(now); + state.hot.unsuccessful_attempt_streak = 0; + Self::deposit_event(Event::CycleSummary { + actor_id, + cycle_nonce, + result: CycleResult::Completed, + outcomes, + }); + + let next_resources = Self::load_current_step_with_admission(actor_id, 0, admission) + .map(|loaded| loaded.resources) + .ok_or(AttemptTransactionError::Invariant)?; + let mut closed_for_exhaustion = false; + let mut attempt_status = AttemptDisposition::Completed; + let placement = if state + .contract + .auto_close_at_cycle_nonce + .is_some_and(|target| cycle_nonce >= target) + { + Self::finalize_actor_from_consumed_state( + actor_id, + state.clone(), + admission, + CloseReason::AutoCloseNonceReached, + ) + .map_err(|_| AttemptTransactionError::Invariant)?; + attempt_status = AttemptDisposition::Closed(CloseReason::AutoCloseNonceReached); + StepControlPlacement::None + } else { + let placement_instance = Self::derive_active_actor_view( + state.identity.clone(), + state.hot.clone(), + state.contract.clone(), + ); + let unsignaled_hot = state.hot.clone(); + match Self::schedule_next_work_with_authority( + actor_id, + &placement_instance, + state.hot.clone(), + &state.identity, + None, + admission, + next_resources, + now, + ServiceCutoff::Snapshotted, + ) { + Ok(placement) => { + if placement == StepControlPlacement::None { + Self::restore_unsignaled_from_authority( + actor_id, + unsignaled_hot, + &state.identity, + None, + admission, + next_resources, + ) + .map_err(|_| AttemptTransactionError::Invariant)?; + } + placement + } + Err(error) => { + if !Self::scheduler_index_is_exhausted(error) { + return Err(AttemptTransactionError::Invariant); + } + Self::finalize_actor_from_consumed_state( + actor_id, + state.clone(), + admission, + CloseReason::SchedulerIndexExhausted, + ) + .map_err(|_| AttemptTransactionError::Invariant)?; + closed_for_exhaustion = true; + attempt_status = AttemptDisposition::Closed(CloseReason::SchedulerIndexExhausted); + StepControlPlacement::None + } + } + }; + let actual_effect_weight = + T::TaskEffectWeight::actual_effect_weight(&step.task, effect_execution) + .ok_or(AttemptTransactionError::Invariant)?; + if !actual_effect_weight.all_lte(reserved_effect_weight) { + return Err(AttemptTransactionError::Invariant); + } + let actual_control_weight = T::StepControlWeight::actual_control_weight( + control_context, + &step, + reserved_control_weight, + StepControlExecution { + phase: StepControlPhase::Running, + outcome: StepControlOutcome::Completed, + placement, + task_effect: effect_execution, + action_fee_collected: false, + }, + ) + .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( + instance.actor_class.actor_type(), + &step, + ActorStepResourceEnvelope { + control: Weight::zero(), + effect: actual_effect_weight, + }, + ) + .map_err(|_| AttemptTransactionError::Invariant)?; + if instance.actor_class.actor_type() == ActorType::User && !actual_fee.total_fee.is_zero() { + Self::collect_user_step_fee(&instance.sovereign_account, actual_fee.total_fee) + .map_err(|_| AttemptTransactionError::FeeCollection)?; + } + if ActorControlLocators::::contains_key(actor_id) { + Self::reconcile_actor_state_hold_with_authority(actor_id) + .map_err(|_| AttemptTransactionError::StateHold)?; + } + Ok(StepCommitEvidence { + closed_for_exhaustion, + actual_control_weight, + actual_effect_weight, + attempt: Self::step_simulation_evidence( + cycle_nonce, + cursor, + attempt_status, + outcomes, + None, + Some(if predicate_matches { + StepOutcome::Stopped + } else { + StepOutcome::Skipped(StepSkippedReason::PreconditionFalse) + }), + ), + }) + } + + fn execute_effectful_step_from_consumed_frame( + actor_id: ActorId, + mut state: ActiveActorStateOf, + mut plan: CurrentStepPlanOf, + admission: &ActorAdmissionCertificateOf, + now: BlockNumberFor, + ) -> Result { + let header = + ActorContractHeads::::get(actor_id).ok_or(AttemptTransactionError::Invariant)?; + if header.header.admission_identity != admission.admission_identity { + return Err(AttemptTransactionError::Invariant); + } + let step_count = header.header.step_count; + if state.hot.cycle_state == CycleState::Idle { + state.contract = Self::load_contract_geometry_with_admission(actor_id, admission) + .ok_or(AttemptTransactionError::Invariant)?; + } + let step = plan.loaded_step.step.clone(); + let direct_task_policy = matches!( + step.on_error, + StepErrorPolicy::ContinueNextStep + | StepErrorPolicy::AbortCycle + | StepErrorPolicy::RetryLater { .. } + ); + if ActorControlLocators::::contains_key(actor_id) + || plan.identity != state.identity + || plan.hot != state.hot + || plan.funding != state.funding + || plan.admission != *admission + || !direct_task_policy + { + return Err(AttemptTransactionError::Invariant); + } + let execution_instance = Self::derive_active_actor_view( + state.identity.clone(), + state.hot.clone(), + state.contract.clone(), + ); + let control_context = Self::execution_step_control_weight_context( + &execution_instance, + plan.run.as_ref(), + &plan.loaded_step, + ) + .ok_or(AttemptTransactionError::Invariant)?; + let reserved_control_weight = plan.loaded_step.resources.control; + let reserved_effect_weight = plan.loaded_step.resources.effect; + let retry_attempt_limit_reached = if let Some(max_attempts) = step.on_error.retry_max_attempts() + { + plan + .run + .as_ref() + .map_or(0, |run| run.unsuccessful_attempts_at_cursor) + .checked_add(1) + .ok_or(AttemptTransactionError::Invariant)? + >= max_attempts + } else { + false + }; + if execution_instance.cycle_state == CycleState::Idle { + plan.hot.queue_ticket = None; + plan.hot = + Self::prepare_opening_rearm_hot(actor_id, &execution_instance, admission, plan.hot, None)?; + } + Self::charge_pipeline_opening(actor_id, &execution_instance)?; + let (mut plan, effect_execution, disposition, outcomes, eligible_at) = + Self::execute_loaded_single_step_core(actor_id, &execution_instance, plan, now, step_count)?; + let actual_effect_weight = + T::TaskEffectWeight::actual_effect_weight(&step.task, effect_execution) + .ok_or(AttemptTransactionError::Invariant)?; + if !actual_effect_weight.all_lte(reserved_effect_weight) { + return Err(AttemptTransactionError::Invariant); + } + let placement_run = plan.run.take(); + let mut attempt = Self::step_simulation_evidence( + plan.ticket.cycle_nonce, + plan.loaded_step.cursor, + disposition, + outcomes, + placement_run.as_ref(), + Some( + plan + .last_step_outcome + .take() + .ok_or(AttemptTransactionError::Invariant)?, + ), + ); + let placement_cursor = placement_run.as_ref().map_or(0, |run| run.cursor); + let placement_resources = + Self::load_current_step_with_admission(actor_id, placement_cursor, admission) + .map(|loaded| loaded.resources) + .ok_or(AttemptTransactionError::Invariant)?; + let placement_instance = Self::derive_active_actor_view( + plan.identity.clone(), + plan.hot.clone(), + state.contract.clone(), + ); + let unsignaled_hot = plan.hot.clone(); + let mut closed_for_exhaustion = false; + let failure_close_reason = if disposition != AttemptDisposition::Failed { + None + } else if retry_attempt_limit_reached { + Some(CloseReason::RetryAttemptsExhausted) + } else if Self::failure_limit_reached(plan.hot.unsuccessful_attempt_streak) { + Some(CloseReason::ConsecutiveFailures) + } else { + None + }; + let successful_close_reason = if disposition != AttemptDisposition::Completed { + None + } else if state.contract.completion == CompletionPolicy::CloseAfterProductiveCycle + && outcomes.committed_effectful_tasks > 0 + { + Some(CloseReason::ProductiveCycleCompleted) + } else { + state + .contract + .auto_close_at_cycle_nonce + .filter(|target_nonce| plan.identity.cycle_nonce >= *target_nonce) + .map(|_| CloseReason::AutoCloseNonceReached) + }; + let placement = if let Some(close_reason) = failure_close_reason.or(successful_close_reason) { + state.identity = plan.identity.clone(); + state.hot = plan.hot.clone(); + state.run_state = placement_run; + state.funding = plan.funding.clone(); + Self::finalize_actor_from_consumed_state(actor_id, state, admission, close_reason) + .map_err(|_| AttemptTransactionError::Invariant)?; + attempt.status = AttemptDisposition::Closed(close_reason); + attempt.run_cursor = None; + attempt.unsuccessful_attempts_at_cursor = None; + StepControlPlacement::None + } else { + match Self::schedule_next_work_with_authority( + actor_id, + &placement_instance, + plan.hot.clone(), + &plan.identity, + placement_run.as_ref(), + admission, + placement_resources, + now, + ServiceCutoff::Snapshotted, + ) { + Ok(placement) => { + if placement == StepControlPlacement::None { + Self::restore_unsignaled_from_authority( + actor_id, + unsignaled_hot, + &plan.identity, + placement_run.as_ref(), + admission, + placement_resources, + ) + .map_err(|_| AttemptTransactionError::Invariant)?; + } + placement + } + Err(error) => { + if !Self::scheduler_index_is_exhausted(error) { + return Err(AttemptTransactionError::Invariant); + } + state.identity = plan.identity.clone(); + state.hot = plan.hot.clone(); + state.run_state = placement_run; + state.funding = plan.funding.clone(); + Self::finalize_actor_from_consumed_state( + actor_id, + state, + admission, + CloseReason::SchedulerIndexExhausted, + ) + .map_err(|_| AttemptTransactionError::Invariant)?; + closed_for_exhaustion = true; + attempt.status = AttemptDisposition::Closed(CloseReason::SchedulerIndexExhausted); + attempt.run_cursor = None; + attempt.unsuccessful_attempts_at_cursor = None; + StepControlPlacement::None + } + } + }; + let control_outcome = match disposition { + AttemptDisposition::Completed => StepControlOutcome::Completed, + AttemptDisposition::Continued => StepControlOutcome::Continued, + AttemptDisposition::Suspended => StepControlOutcome::Suspended, + AttemptDisposition::Failed => StepControlOutcome::Failed, + _ => return Err(AttemptTransactionError::Invariant), + }; + // Completion may leave a paid deferred activation or terminal deadline even + // though the completed Run no longer has its own service eligibility. + if eligible_at.is_some() && placement == StepControlPlacement::None && !closed_for_exhaustion { + return Err(AttemptTransactionError::Invariant); + } + let actual_fee = Self::maximum_current_action_fee( + execution_instance.actor_class.actor_type(), + &step, + ActorStepResourceEnvelope { + control: Weight::zero(), + effect: actual_effect_weight, + }, + ) + .map_err(|_| AttemptTransactionError::Invariant)?; + let action_fee_collected = execution_instance.actor_class.actor_type() == ActorType::User + && !actual_fee.total_fee.is_zero(); + let actual_control_weight = T::StepControlWeight::actual_control_weight( + control_context, + &step, + reserved_control_weight, + StepControlExecution { + phase: match execution_instance.cycle_state { + CycleState::Idle => StepControlPhase::Opening, + CycleState::Running => StepControlPhase::Running, + CycleState::Suspended => StepControlPhase::Suspended, + }, + outcome: control_outcome, + placement, + task_effect: effect_execution, + action_fee_collected, + }, + ) + .ok_or(AttemptTransactionError::Invariant)?; + if !actual_control_weight.all_lte(reserved_control_weight) { + return Err(AttemptTransactionError::Invariant); + } + if action_fee_collected { + Self::collect_user_step_fee(&execution_instance.sovereign_account, actual_fee.total_fee) + .map_err(|_| AttemptTransactionError::FeeCollection)?; + } + if matches!(effect_execution, super::TaskEffectExecution::Invoked) { + Self::deposit_action_fee_receipt( + actor_id, + plan.ticket.cycle_nonce, + plan.loaded_step.cursor, + actual_effect_weight, + actual_fee.effect_fee, + ); + } + if ActorControlLocators::::contains_key(actor_id) { + Self::reconcile_actor_state_hold_with_authority(actor_id) + .map_err(|_| AttemptTransactionError::StateHold)?; + } + Ok(StepCommitEvidence { + closed_for_exhaustion, + actual_control_weight, + actual_effect_weight, + attempt, + }) + } + + pub(crate) fn execute_current_step_and_place( + actor_id: ActorId, + state: &ActiveActorStateOf, + commit_plan: CurrentStepPlanOf, + admission: &ActorAdmissionCertificateOf, + now: BlockNumberFor, + ) -> Result { + if matches!( + commit_plan.loaded_step.step.task, + super::types::Task::StopCycle + ) && commit_plan.loaded_step.step.precondition.is_none() + { + match state.hot.cycle_state { + CycleState::Idle => Self::execute_stop_cycle_from_consumed_frame( + actor_id, + state.clone(), + commit_plan, + admission, + now, + ), + CycleState::Running => Self::execute_running_stop_cycle_from_consumed_frame( + actor_id, + state.clone(), + commit_plan, + admission, + now, + ), + CycleState::Suspended => Err(AttemptTransactionError::Invariant), + } + } else { + Self::execute_effectful_step_from_consumed_frame( + actor_id, + state.clone(), + commit_plan, + admission, + now, + ) + } + } + + fn control_blank_control_chunk() -> Result, ActorControlTransitionError> { + BoundedVec::try_from(alloc::vec![None; 32]).map_err(|_| ActorControlTransitionError::Invariant) + } + + fn append_waiting_entry( + key: WakeupKey>, + entry: impl FnOnce(WakeupPageId, WakeupSlot) -> ActorWaitingEntry>, + ) -> Result<(WakeupPageId, WakeupSlot), ActorControlTransitionError> { + let occupancy = ActorWaitingOccupancies::::get(key); + let tail = ActorWaitingTails::::get(key); + Self::validate_waiting_directory(key, occupancy)?; + let next_tail = tail + .checked_add(1) + .ok_or(ActorControlTransitionError::IndexExhausted)?; + let next_occupancy = occupancy + .checked_add(1) + .filter(|value| *value <= T::MaxActiveActors::get()) + .ok_or(ActorControlTransitionError::IndexExhausted)?; + let page_id = tail / 32; + let slot = (tail % 32) as u32; + let mut page = if slot == 0 { + if ActorWaitingFrameChunks::::contains_key((key, page_id)) { + return Err(ActorControlTransitionError::Invariant); + } + let previous_page = if occupancy == 0 { + None + } else { + tail.checked_sub(1).map(|tail| tail / 32) + }; + if let Some(previous_id) = previous_page { + let mut previous = ActorWaitingFrameChunks::::get((key, previous_id)) + .ok_or(ActorControlTransitionError::Invariant)?; + if previous.next_page.is_some() || previous.live_entries == 0 { + return Err(ActorControlTransitionError::Invariant); + } + previous.next_page = Some(page_id); + ActorWaitingFrameChunks::::insert((key, previous_id), previous); + } + ActorWaitingPageOf:: { + entries: BoundedVec::try_from(alloc::vec![None; 32]) + .map_err(|_| ActorControlTransitionError::Invariant)?, + live_entries: 0, + scan_slot: 0, + previous_page, + next_page: None, + } + } else { + ActorWaitingFrameChunks::::get((key, page_id)) + .ok_or(ActorControlTransitionError::Invariant)? + }; + if page.next_page.is_some() || page.entries.get(slot as usize).is_none_or(Option::is_some) { + return Err(ActorControlTransitionError::Invariant); + } + page.entries[slot as usize] = Some(entry(page_id, slot)); + page.live_entries = page + .live_entries + .checked_add(1) + .ok_or(ActorControlTransitionError::Invariant)?; + ActorWaitingFrameChunks::::insert((key, page_id), page); + if occupancy == 0 { + ActorWaitingHeads::::insert(key, tail); + } + ActorWaitingTails::::insert(key, next_tail); + ActorWaitingOccupancies::::insert(key, next_occupancy); + if occupancy == 0 && !Self::wakeup_cursor_insert_inner(key) { + return Err(ActorControlTransitionError::Invariant); + } + Ok((page_id, slot)) + } + + fn remove_waiting_entry( + pointer: WakeupPointer>, + ) -> Result>, ActorControlTransitionError> { + let key = pointer.block; + Self::validate_waiting_directory(key, ActorWaitingOccupancies::::get(key))?; + let mut page = ActorWaitingFrameChunks::::get((key, pointer.page_id)) + .ok_or(ActorControlTransitionError::Invariant)?; + // ActorWaitingChunkOf is type-bounded to 32 slots, including malformed short pages. + if page.entries.iter(/* deos-bypass: bounded-iter */).filter(|entry| entry.is_some()).count() + != page.live_entries as usize + { + return Err(ActorControlTransitionError::Invariant); + } + let entry = page + .entries + .get_mut(pointer.slot as usize) + .and_then(Option::take) + .ok_or(ActorControlTransitionError::Invariant)?; + page.live_entries = page + .live_entries + .checked_sub(1) + .ok_or(ActorControlTransitionError::Invariant)?; + let occupancy = ActorWaitingOccupancies::::get(key) + .checked_sub(1) + .ok_or(ActorControlTransitionError::Invariant)?; + if (occupancy == 0) + != (page.live_entries == 0 && page.previous_page.is_none() && page.next_page.is_none()) + || occupancy < page.live_entries + { + return Err(ActorControlTransitionError::Invariant); + } + if page.live_entries > 0 { + ActorWaitingFrameChunks::::insert((key, pointer.page_id), page); + ActorWaitingOccupancies::::insert(key, occupancy); + return Ok(entry); + } + if let Some(previous_id) = page.previous_page { + let mut previous = ActorWaitingFrameChunks::::get((key, previous_id)) + .ok_or(ActorControlTransitionError::Invariant)?; + if previous.next_page != Some(pointer.page_id) { + return Err(ActorControlTransitionError::Invariant); + } + previous.next_page = page.next_page; + ActorWaitingFrameChunks::::insert((key, previous_id), previous); + } + if let Some(next_id) = page.next_page { + let mut next = ActorWaitingFrameChunks::::get((key, next_id)) + .ok_or(ActorControlTransitionError::Invariant)?; + if next.previous_page != Some(pointer.page_id) { + return Err(ActorControlTransitionError::Invariant); + } + next.previous_page = page.previous_page; + if page.previous_page.is_none() { + ActorWaitingHeads::::insert( + key, + next_id + .checked_mul(32) + .and_then(|start| start.checked_add(u64::from(next.scan_slot))) + .ok_or(ActorControlTransitionError::IndexExhausted)?, + ); + } + ActorWaitingFrameChunks::::insert((key, next_id), next); + } else if let Some(previous_id) = page.previous_page { + ActorWaitingTails::::insert( + key, + previous_id + .checked_add(1) + .and_then(|next| next.checked_mul(32)) + .ok_or(ActorControlTransitionError::IndexExhausted)?, + ); + } + ActorWaitingFrameChunks::::remove((key, pointer.page_id)); + if occupancy == 0 { + if page.previous_page.is_some() + || page.next_page.is_some() + || !Self::control_wakeup_cursor_release(key) + { + return Err(ActorControlTransitionError::Invariant); + } + ActorWaitingHeads::::remove(key); + ActorWaitingTails::::remove(key); + ActorWaitingOccupancies::::remove(key); + } else { + ActorWaitingOccupancies::::insert(key, occupancy); + } + Ok(entry) + } + + fn validate_waiting_directory( + key: WakeupKey>, + occupancy: u32, + ) -> Result<(), ActorControlTransitionError> { + // The draining owner advances head before consuming its last live slot, so + // head == tail is a valid in-transaction intermediate, not a missing directory. + let head_present = ActorWaitingHeads::::contains_key(key); + let tail_present = ActorWaitingTails::::contains_key(key); + if occupancy == 0 { + if head_present || tail_present { + return Err(ActorControlTransitionError::Invariant); + } + } else if !head_present + || !tail_present + || ActorWaitingHeads::::get(key) > ActorWaitingTails::::get(key) + || ActorWaitingCursorIndices::::get(key).is_none() + { + return Err(ActorControlTransitionError::Invariant); + } + Ok(()) + } + + pub(crate) fn control_append_waiting( + mut cell: ActorControlCellOf, + key: WakeupKey>, + authority: ActorWaitingAuthority, + ) -> Result>, ActorControlTransitionError> { + let actor_id = cell.actor_id; + if ActorControlLocators::::contains_key(actor_id) + || ActorUnsignaledControlCells::::contains_key(actor_id) + { + return Err(ActorControlTransitionError::Invariant); + } + let pointer = match authority { + ActorWaitingAuthority::Trigger => { + if !matches!(key, WakeupKey::Tick(_)) + || cell.hot.cycle_state != CycleState::Idle + || cell.hot.pending_signal + || cell.eligible_at.is_some() + || cell.hot.wakeup_pointer.is_some() + { + return Err(ActorControlTransitionError::Invariant); + } + cell + .hot + .trigger_wakeup_pointer + .map(|pointer| WakeupPointer { + block: WakeupKey::Tick(pointer.tick), + page_id: pointer.page_id, + slot: pointer.slot, + }) + } + ActorWaitingAuthority::Service => { + let terminal_idle = cell.hot.cycle_state == CycleState::Idle + && !cell.hot.pending_signal + && cell + .hot + .terminal_at + .is_some_and(|terminal| cell.eligible_at.is_some_and(|at| at >= terminal)); + if !matches!(key, WakeupKey::Block(_)) + || cell.eligible_at.is_none() + || !(matches!( + cell.hot.cycle_state, + CycleState::Running | CycleState::Suspended + ) || (cell.hot.cycle_state == CycleState::Idle && cell.hot.pending_signal) + || terminal_idle) + { + return Err(ActorControlTransitionError::Invariant); + } + cell.hot.wakeup_pointer + } + }; + if let Some(pointer) = pointer { + if pointer.block != key { + return Err(ActorControlTransitionError::Invariant); + } + if let Some(mut page) = ActorWaitingFrameChunks::::get((key, pointer.page_id)) { + if let Some(Some(ActorWaitingEntry::Reference(reference))) = + page.entries.get(pointer.slot as usize) + { + if reference.actor_id != actor_id + || reference.admission_identity != cell.admission.admission_identity + { + return Err(ActorControlTransitionError::Invariant); + } + let slot = + u8::try_from(pointer.slot).map_err(|_| ActorControlTransitionError::Invariant)?; + page.entries[pointer.slot as usize] = Some(ActorWaitingEntry::Primary(cell)); + let location = ActorControlLocation::Waiting { + key, + page: pointer.page_id, + slot, + }; + ActorWaitingFrameChunks::::insert((key, pointer.page_id), page); + ActorControlLocators::::insert(actor_id, location); + return Ok(location); + } + if page + .entries + .get(pointer.slot as usize) + .is_none_or(Option::is_some) + { + return Err(ActorControlTransitionError::Invariant); + } + } + } + let (page, slot) = Self::append_waiting_entry(key, |page_id, slot| { + match key { + WakeupKey::Block(_) => { + cell.hot.wakeup_pointer = Some(WakeupPointer { + block: key, + page_id, + slot, + }) + } + WakeupKey::Tick(tick) => { + cell.hot.trigger_wakeup_pointer = Some(TriggerWakeupPointer { + tick, + page_id, + slot, + }) + } + } + ActorWaitingEntry::Primary(cell) + })?; + let slot = u8::try_from(slot).map_err(|_| ActorControlTransitionError::Invariant)?; + let location = ActorControlLocation::Waiting { key, page, slot }; + ActorControlLocators::::insert(actor_id, location); + Ok(location) + } + + pub(crate) fn control_append_ready( + cell: ActorControlCellOf, + ) -> Result<(ActorId, QueueTicket), ActorControlTransitionError> { + const CHUNK_SIZE: QueueTicket = 32; + if cell.eligible_at.is_none() { + return Err(ActorControlTransitionError::Invariant); + } + let actor_id = cell.actor_id; + let tail = ActorReadyTail::::get(); + let occupancy = ActorReadyOccupancy::::get(); + if tail + .checked_sub(ActorReadyHead::::get()) + .is_none_or(|span| span >= u64::from(T::MaxQueueLength::get())) + || ActorControlLocators::::contains_key(actor_id) + { + return Err(ActorControlTransitionError::Invariant); + } + let next_tail = tail + .checked_add(1) + .ok_or(ActorControlTransitionError::IndexExhausted)?; + let next_occupancy = occupancy + .checked_add(1) + .filter(|value| *value <= T::MaxActiveActors::get()) + .ok_or(ActorControlTransitionError::IndexExhausted)?; + if Self::project_control_cell(&cell, ActorControlLocation::Ready { ticket: tail }).is_none() { + return Err(ActorControlTransitionError::Invariant); + } + let page = tail / CHUNK_SIZE; + let slot = (tail % CHUNK_SIZE) as usize; + let mut chunk = match ActorReadyFrameChunks::::get(page) { + Some(chunk) => chunk, + None => Self::control_blank_control_chunk()?, + }; + let Some(target) = chunk.get_mut(slot) else { + return Err(ActorControlTransitionError::Invariant); + }; + if target.replace(cell).is_some() { + return Err(ActorControlTransitionError::Invariant); + } + ActorReadyFrameChunks::::insert(page, chunk); + ActorReadyTail::::put(next_tail); + ActorReadyOccupancy::::put(next_occupancy); + ActorControlLocators::::insert(actor_id, ActorControlLocation::Ready { ticket: tail }); + Ok((actor_id, tail)) + } + + #[cfg(all(test, feature = "runtime-benchmarks"))] + pub(crate) fn control_remove_ready_primary( + actor_id: ActorId, + ) -> Result { + polkadot_sdk::frame_support::storage::with_transaction(|| { + let remove = || { + let ActorControlLocation::Ready { ticket } = + ActorControlLocators::::get(actor_id).ok_or(ActorControlTransitionError::Invariant)? + else { + return Err(ActorControlTransitionError::Invariant); + }; + let page = ticket / 32; + let slot = (ticket % 32) as usize; + let mut chunk = + ActorReadyFrameChunks::::get(page).ok_or(ActorControlTransitionError::Invariant)?; + let stored = chunk + .get_mut(slot) + .ok_or(ActorControlTransitionError::Invariant)?; + if stored.as_ref().map(|cell| cell.actor_id) != Some(actor_id) { + return Err(ActorControlTransitionError::Invariant); + } + *stored = None; + let occupancy = ActorReadyOccupancy::::get() + .checked_sub(1) + .ok_or(ActorControlTransitionError::Invariant)?; + ActorReadyFrameChunks::::insert(page, chunk); + ActorReadyOccupancy::::put(occupancy); + ActorControlLocators::::remove(actor_id); + Ok(ticket) + }; + match remove() { + Ok(ticket) => polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(ticket)), + Err(error) => { + polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)) + } + } + }) + } + + #[cfg(all(test, feature = "runtime-benchmarks"))] + pub(crate) fn control_normalize_ready_head( + cutoff: QueueTicket, + max_scans: u32, + ) -> Result<(u32, Option), ActorControlTransitionError> { + const CHUNK_SIZE: QueueTicket = 32; + polkadot_sdk::frame_support::storage::with_transaction(|| { + let normalize = || { + let initial_head = ActorReadyHead::::get(); + let tail = ActorReadyTail::::get(); + let limit = cutoff.min(tail); + let mut head = initial_head; + let mut scans = 0u32; + while head < limit && scans < max_scans { + let page = head / CHUNK_SIZE; + let slot = (head % CHUNK_SIZE) as usize; + let chunk = + ActorReadyFrameChunks::::get(page).ok_or(ActorControlTransitionError::Invariant)?; + let cell = chunk + .get(slot) + .ok_or(ActorControlTransitionError::Invariant)?; + if cell.is_some() { + break; + } + head = head + .checked_add(1) + .ok_or(ActorControlTransitionError::IndexExhausted)?; + scans = scans.saturating_add(1); + if head.is_multiple_of(CHUNK_SIZE) || head == tail { + if chunk + .iter(/* deos-bypass: bounded-iter */) + .any(Option::is_some) + { + return Err(ActorControlTransitionError::Invariant); + } + ActorReadyFrameChunks::::remove(page); + } + } + if head != initial_head { + ActorReadyHead::::put(head); + } + let next_live = if head < limit { + let page = head / CHUNK_SIZE; + let slot = (head % CHUNK_SIZE) as usize; + ActorReadyFrameChunks::::get(page) + .and_then(|chunk| chunk.get(slot).cloned().flatten()) + .map(|_| head) + } else { + None + }; + Ok((scans, next_live)) + }; + match normalize() { + Ok(result) => polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(result)), + Err(error) => { + polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)) + } + } + }) + } + + #[cfg(feature = "runtime-benchmarks")] + pub(crate) fn control_stage_unsignaled_temporal( + actor_id: ActorId, + due_tick: SchedulerTick, + ) -> Result>, ActorControlTransitionError> { + polkadot_sdk::frame_support::storage::with_transaction(|| { + let transition = || { + if ActorControlLocators::::get(actor_id) != Some(ActorControlLocation::Unsignaled) { + return Err(ActorControlTransitionError::Invariant); + } + let cell = ActorUnsignaledControlCells::::get(actor_id) + .ok_or(ActorControlTransitionError::Invariant)?; + if Self::project_control_cell(&cell, ActorControlLocation::Unsignaled).is_none() { + return Err(ActorControlTransitionError::Invariant); + } + Self::remove_primary_control_cell_inner(actor_id) + .map_err(|_| ActorControlTransitionError::Invariant)?; + let location = Self::control_append_waiting( + cell, + WakeupKey::Tick(due_tick), + ActorWaitingAuthority::Trigger, + )?; + Ok(location) + }; + match transition() { + Ok(location) => { + polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(location)) + } + Err(error) => { + polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)) + } + } + }) + } + + #[cfg(feature = "runtime-benchmarks")] + pub(crate) fn control_latch_temporal_waiting_page( + due_tick: SchedulerTick, + page: u64, + now: BlockNumberFor, + now_tick: SchedulerTick, + ) -> Result, ActorControlTransitionError> { + polkadot_sdk::frame_support::storage::with_transaction(|| { + let transition = || { + const CHUNK_SIZE: u64 = 32; + let source_key = WakeupKey::Tick(due_tick); + if due_tick > now_tick + || Self::wakeup_cursor_peek_key(WakeupClock::Tick) != Some(source_key) + || ActorWaitingHeads::::get(source_key) / CHUNK_SIZE != page + { + return Err(ActorControlTransitionError::Invariant); + } + let mut chunk = ActorWaitingFrameChunks::::get((source_key, page)) + .ok_or(ActorControlTransitionError::Invariant)?; + let eligible_at = now + .checked_add(&One::one()) + .ok_or(ActorControlTransitionError::IndexExhausted)?; + let destination_key = WakeupKey::Block(eligible_at); + let mut moved = Vec::new(); + for (slot, maybe_cell) in chunk + .entries + .iter_mut(/* deos-bypass: bounded-iter */) + .enumerate() + { + let Some(entry) = maybe_cell.take() else { + continue; + }; + let mut cell = entry + .into_primary() + .ok_or(ActorControlTransitionError::Invariant)?; + let slot = u8::try_from(slot).map_err(|_| ActorControlTransitionError::Invariant)?; + let location = ActorControlLocation::Waiting { + key: source_key, + page, + slot, + }; + if Self::project_control_cell(&cell, location).is_none() + || cell.hot.wakeup_pointer.is_some() + { + return Err(ActorControlTransitionError::Invariant); + } + cell.hot.trigger_wakeup_pointer = None; + cell.hot.pending_signal = true; + cell.eligible_at = Some(eligible_at); + let actor_id = cell.actor_id; + Self::remove_primary_control_cell_inner(actor_id)?; + Self::control_append_waiting(cell, destination_key, ActorWaitingAuthority::Service)?; + moved.push(actor_id); + } + Ok(moved) + }; + match transition() { + Ok(moved) => polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(moved)), + Err(error) => { + polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)) + } + } + }) + } + + #[cfg(feature = "runtime-benchmarks")] + pub(crate) fn control_promote_due_waiting_page( + eligible_at: BlockNumberFor, + page: u64, + now: BlockNumberFor, + ) -> Result, ActorControlTransitionError> { + polkadot_sdk::frame_support::storage::with_transaction(|| { + let transition = || { + const CHUNK_SIZE: u64 = 32; + if eligible_at > now { + return Err(ActorControlTransitionError::Invariant); + } + let source_key = WakeupKey::Block(eligible_at); + if Self::wakeup_cursor_peek_key(WakeupClock::Block) != Some(source_key) + || ActorWaitingHeads::::get(source_key) / CHUNK_SIZE != page + { + return Err(ActorControlTransitionError::Invariant); + } + let mut chunk = ActorWaitingFrameChunks::::get((source_key, page)) + .ok_or(ActorControlTransitionError::Invariant)?; + let mut moved = Vec::new(); + for (slot, maybe_cell) in chunk + .entries + .iter_mut(/* deos-bypass: bounded-iter */) + .enumerate() + { + let Some(entry) = maybe_cell.take() else { + continue; + }; + let mut cell = entry + .into_primary() + .ok_or(ActorControlTransitionError::Invariant)?; + let slot = u8::try_from(slot).map_err(|_| ActorControlTransitionError::Invariant)?; + let location = ActorControlLocation::Waiting { + key: source_key, + page, + slot, + }; + if Self::project_control_cell(&cell, location).is_none() + || cell.eligible_at.is_none_or(|value| value > now) + || cell.hot.wakeup_pointer.is_none() + { + return Err(ActorControlTransitionError::Invariant); + } + cell.hot.wakeup_pointer = None; + Self::remove_primary_control_cell_inner(cell.actor_id)?; + moved.push(Self::control_append_ready(cell)?); + } + Ok(moved) + }; + match transition() { + Ok(moved) => polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(moved)), + Err(error) => { + polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)) + } + } }) } + #[cfg(all(test, feature = "runtime-benchmarks"))] + pub(crate) fn control_service_next_due_waiting_unit( + now: BlockNumberFor, + now_tick: SchedulerTick, + ) -> Result, ActorControlTransitionError> { + let preferred = NextWakeupClock::::get(); + let peer = match preferred { + WakeupClock::Block => WakeupClock::Tick, + WakeupClock::Tick => WakeupClock::Block, + }; + let due_key = |clock| { + Self::wakeup_cursor_peek_key(clock).filter(|key| match key { + WakeupKey::Block(block) => *block <= now, + WakeupKey::Tick(tick) => *tick <= now_tick, + }) + }; + let selected = due_key(preferred) + .map(|key| (preferred, key)) + .or_else(|| due_key(peer).map(|key| (peer, key))); + let Some((clock, key)) = selected else { + return Ok(None); + }; + let page = ActorWaitingHeads::::get(key) / 32; + let moved = match key { + WakeupKey::Tick(due_tick) => u32::try_from( + Self::control_latch_temporal_waiting_page(due_tick, page, now, now_tick)?.len(), + ) + .map_err(|_| ActorControlTransitionError::Invariant)?, + WakeupKey::Block(eligible_at) => { + u32::try_from(Self::control_promote_due_waiting_page(eligible_at, page, now)?.len()) + .map_err(|_| ActorControlTransitionError::Invariant)? + } + }; + if moved == 0 { + return Err(ActorControlTransitionError::Invariant); + } + NextWakeupClock::::put(peer); + Ok(Some((clock, moved))) + } + + fn step_simulation_evidence( + cycle_nonce: u64, + start_cursor: u32, + status: AttemptDisposition, + cumulative_outcomes: OutcomeTotals, + run: Option<&ActorRunStateOf>, + outcome: Option, + ) -> ActorAttemptEvidence { + ActorAttemptEvidence { + status, + cycle_nonce, + start_cursor, + run_cursor: run.map(|run| run.cursor), + unsuccessful_attempts_at_cursor: run.map(|run| run.unsuccessful_attempts_at_cursor), + cumulative_outcomes, + step: outcome.map(|outcome| SimulationStepRecord { + step_index: start_cursor, + outcome, + }), + } + } + + fn simulation_placement_error(error: EnqueueOutcome) -> SimulationError { + match error { + EnqueueOutcome::CapacityUnavailable | EnqueueOutcome::WakeupCapacityExhausted => { + SimulationError::ResourceDeferred + } + EnqueueOutcome::TicketExhausted + | EnqueueOutcome::SchedulerIndexExhausted + | EnqueueOutcome::WakeupIndexExhausted => { + SimulationError::Classification(ActorClassificationError::ComputationOverflow) + } + _ => SimulationError::Classification(ActorClassificationError::ActorInvariant), + } + } + + /// Called only within the simulation's outer rollback. Selects one real primary without + /// advancing the FIFO Head or servicing any other Actor. + pub(crate) fn simulate_actor_service( + actor_id: ActorId, + budget: SimulationBudget, + ) -> Result { + let limits = budget + .checked_limits() + .map_err(|_| SimulationError::InvalidBudget)?; + let now = frame_system::Pallet::::block_number(); + let mut resources = BlockResourceState::new(now); + resources + .begin_prepass() + .map_err(|_| SimulationError::InvalidBudget)?; + resources + .open_external_phase() + .map_err(|_| SimulationError::InvalidBudget)?; + resources + .begin_drain() + .map_err(|_| SimulationError::InvalidBudget)?; + let mut reservation = resources + .reserve( + limits, + BlockResourceDomain::ActorControl, + budget.actor_control, + ) + .map_err(|_| SimulationError::ResourceDeferred)?; + let mut control_meter = WeightMeter::with_limit(budget.actor_control); + let mut cycle_meter = WeightMeter::with_limit( + budget + .actor_control + .checked_add(&budget.shared_economic) + .ok_or(SimulationError::InvalidBudget)?, + ); + let (location, cell) = Self::load_primary_control_cell(actor_id) + .map_err(|_| SimulationError::Classification(ActorClassificationError::ActorInvariant))?; + let (terminal_state, terminal_admission, _) = Self::load_frame_actor_service_state(actor_id) + .ok_or(SimulationError::Classification( + ActorClassificationError::ActorInvariant, + ))?; + let terminal_instance = Self::derive_active_actor_view( + terminal_state.identity.clone(), + terminal_state.hot.clone(), + terminal_state.contract.clone(), + ); + let terminal_classification = + Self::classify_actor_loaded(&terminal_instance, terminal_state.run_state.as_ref()) + .map_err(SimulationError::Classification)?; + if terminal_classification.terminal_reason.is_some() { + let accounting = ProvisionalEntryAccounting { + cycle_before_entry: cycle_meter.consumed(), + control_before_entry: Some(control_meter.consumed()), + }; + let probe = Self::scheduler_actor_state_probe_weight_upper(); + if !control_meter.can_consume(probe) || !cycle_meter.can_consume(probe) { + return Err(SimulationError::ResourceDeferred); + } + control_meter.consume(probe); + cycle_meter.consume(probe); + let decision = Self::apply_admission_loaded( + actor_id, + &terminal_instance, + terminal_state.run_state.as_ref(), + None, + None, + &cycle_meter, + Some(&accounting.replacement_meter(&cycle_meter)), + ); + let AdmissionDecision::Close { reason, weight } = decision else { + return Err(match decision { + AdmissionDecision::Defer => SimulationError::ResourceDeferred, + _ => SimulationError::Classification(ActorClassificationError::ActorInvariant), + }); + }; + let source = match location { + ActorControlLocation::Ready { ticket } => { + Some((ActorServiceSource::SelectedReady, ticket, ticket)) + } + _ => None, + }; + let result = Self::service_admitted_close( + actor_id, + terminal_state, + terminal_admission, + reason, + weight, + &mut cycle_meter, + Some(&mut control_meter), + source, + accounting, + ); + resources + .settle(&mut reservation, cycle_meter.consumed()) + .map_err(|_| SimulationError::Classification(ActorClassificationError::ActorInvariant))?; + return Self::simulation_service_result(result); + } + if let ActorControlLocation::Waiting { key, page, slot } = location { + let WakeupKey::Block(due) = key else { + return Err(SimulationError::NotReady); + }; + if due > now + || cell.hot.wakeup_pointer + != Some(WakeupPointer { + block: key, + page_id: page, + slot: u32::from(slot), + }) + { + return Err(SimulationError::Classification( + ActorClassificationError::ActorInvariant, + )); + } + let preparation = Self::wakeup_cursor_drain_unit_weight_for( + WakeupBucketDisposition::Retain, + WakeupClock::Block, + ) + .max(Self::wakeup_cursor_drain_unit_weight_for( + WakeupBucketDisposition::Remove, + WakeupClock::Block, + )); + if !control_meter.can_consume(preparation) || !cycle_meter.can_consume(preparation) { + return Err(SimulationError::ResourceDeferred); + } + control_meter.consume(preparation); + cycle_meter.consume(preparation); + let (mut state, admission, loaded_step) = Self::load_frame_actor_service_state(actor_id) + .ok_or(SimulationError::Classification( + ActorClassificationError::ActorInvariant, + ))?; + Self::remove_primary_control_cell_inner(actor_id) + .map_err(|_| SimulationError::Classification(ActorClassificationError::ActorInvariant))?; + state.hot.wakeup_pointer = None; + let step_resources = if state.contract.steps.is_empty() { + ActorStepResourceEnvelope { + control: T::WeightInfo::scheduler_inner_zero_step_complete(), + effect: Weight::zero(), + } + } else { + loaded_step + .ok_or(SimulationError::Classification( + ActorClassificationError::ActorInvariant, + ))? + .resources + }; + let plan = Self::preflight_paged_enqueue_authority( + actor_id, + state.hot, + &state.identity, + state.run_state.as_ref(), + &admission, + step_resources, + ) + .map_err(Self::simulation_placement_error)?; + Self::commit_paged_enqueue(plan).map_err(Self::simulation_placement_error)?; + } + let (location, cell) = Self::load_primary_control_cell(actor_id) + .map_err(|_| SimulationError::Classification(ActorClassificationError::ActorInvariant))?; + let ActorControlLocation::Ready { ticket } = location else { + return Err(SimulationError::NotReady); + }; + let eligible_at = cell.eligible_at.ok_or(SimulationError::Classification( + ActorClassificationError::ActorInvariant, + ))?; + if eligible_at > now { + return Err(SimulationError::NotReady); + } + let entry = ActorStepTicket { + actor_id, + cycle_nonce: cell.identity.cycle_nonce.saturating_add(1), + cursor: cell.cursor, + ticket, + eligible_at, + contract_commitment: ActorContractCommitment { + semantic_contract_id: cell.admission.semantic_contract_id, + body_commitment: cell.admission.body_commitment, + }, + }; + let mut effect_consumed = Weight::zero(); + let mut uncertain = false; + let accounting = ProvisionalEntryAccounting { + cycle_before_entry: cycle_meter.consumed(), + control_before_entry: Some(control_meter.consumed()), + }; + let result = Self::service_live_queue_entry( + (ticket, entry), + now, + &mut cycle_meter, + Some(&mut control_meter), + &mut effect_consumed, + &mut uncertain, + Some(( + &mut resources, + limits, + BlockResourceDomain::ActorDrainEffect, + )), + ActorServiceSource::SelectedReady, + accounting, + ); + let actual_control = if uncertain { + budget.actor_control + } else { + cycle_meter.consumed().checked_sub(&effect_consumed).ok_or( + SimulationError::Classification(ActorClassificationError::ActorInvariant), + )? + }; + resources + .settle(&mut reservation, actual_control) + .map_err(|_| SimulationError::Classification(ActorClassificationError::ActorInvariant))?; + Self::simulation_service_result(result) + } + + fn simulation_service_result( + result: FifoStepResult, + ) -> Result { + match result { + FifoStepResult::Progress { + attempt: Some(attempt), + .. + } => Ok(SimulationResult { + status: attempt.status, + cycle_nonce: attempt.cycle_nonce, + start_cursor: attempt.start_cursor, + run_cursor: attempt.run_cursor, + unsuccessful_attempts_at_cursor: attempt.unsuccessful_attempts_at_cursor, + cumulative_outcomes: attempt.cumulative_outcomes, + steps: BoundedVec::truncate_from(attempt.step.into_iter().collect()), + }), + FifoStepResult::Blocked(BlockKind::Weight) => Err(SimulationError::ResourceDeferred), + FifoStepResult::Blocked(BlockKind::FeeCollection) => { + Err(SimulationError::FeeCollectionFailed) + } + FifoStepResult::Blocked(BlockKind::NonWeight) => Err(SimulationError::Classification( + ActorClassificationError::ActorInvariant, + )), + _ => Err(SimulationError::NotReady), + } + } + + fn consume_actor_service_source( + source: ActorServiceSource, + position: QueueTicket, + actor_id: ActorId, + ticket: QueueTicket, + hot: ActorHotStateOf, + ) -> Result<(), EnqueueOutcome> { + match source { + ActorServiceSource::FifoHead => { + Self::paged_consume_loaded_head_at(position, actor_id, ticket, hot) + } + ActorServiceSource::SelectedReady => { + if hot.queue_ticket != Some(ticket) { + return Err(EnqueueOutcome::CorruptedTopology); + } + Self::queue_topology_preflight(QueueMutation::Head)?; + Self::consume_ready_primary(actor_id, ticket) + } + } + } + + fn service_admitted_close( + actor_id: ActorId, + state: ActiveActorStateOf, + admission: ActorAdmissionCertificateOf, + reason: CloseReason, + weight: Weight, + cycle_meter: &mut WeightMeter, + mut control_meter: Option<&mut WeightMeter>, + source: Option<(ActorServiceSource, QueueTicket, QueueTicket)>, + accounting: ProvisionalEntryAccounting, + ) -> FifoStepResult { + if !accounting.can_settle_complete(cycle_meter, control_meter.as_deref(), weight) { + return FifoStepResult::Blocked(BlockKind::Weight); + } + let outcome = polkadot_sdk::frame_support::storage::with_transaction(|| { + if matches!(source, Some((ActorServiceSource::SelectedReady, _, _))) + && Self::queue_topology_preflight(QueueMutation::Head).is_err() + { + return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err( + polkadot_sdk::sp_runtime::DispatchError::Other("scheduler queue topology corrupted"), + )); + } + let close_result = + Self::finalize_actor_from_retained_state(actor_id, state.clone(), &admission, reason); + if let Err(error) = close_result { + return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)); + } + Self::apply_test_close_queue_corruption(); + let source_closed = match source { + None => { + if ActorControlLocators::::contains_key(actor_id) { + Err(EnqueueOutcome::CorruptedTopology) + } else { + Ok(()) + } + } + Some((ActorServiceSource::FifoHead, position, _)) => { + Self::paged_consume_closed_head_at(position) + } + Some((ActorServiceSource::SelectedReady, _, ticket)) => { + if Self::queue_topology_preflight(QueueMutation::Head).is_err() + || ActorControlLocators::::contains_key(actor_id) + || ActorReadyFrameChunks::::get(ticket / 32) + .and_then(|chunk| chunk.get((ticket % 32) as usize).cloned().flatten()) + .is_some() + { + Err(EnqueueOutcome::CorruptedTopology) + } else { + Ok(()) + } + } + }; + if source_closed.is_err() { + return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err( + polkadot_sdk::sp_runtime::DispatchError::Other("scheduler queue head changed"), + )); + } + polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(())) + }); + accounting.settle_complete(cycle_meter, control_meter.as_deref_mut(), weight); + match outcome { + Ok(()) => FifoStepResult::Progress { + executed: false, + attempt: Some(Self::step_simulation_evidence( + state + .run_state + .as_ref() + .map_or(state.identity.cycle_nonce, |run| run.cycle_nonce), + state.run_state.as_ref().map_or(0, |run| run.cursor), + AttemptDisposition::Closed(reason), + state + .run_state + .as_ref() + .map_or(OutcomeTotals::default(), |run| run.cumulative_outcomes), + None, + None, + )), + }, + Err(_) => FifoStepResult::Blocked(BlockKind::NonWeight), + } + } + fn service_live_queue_entry( (position, entry): (QueueTicket, QueueEntry>), now: BlockNumberFor, cycle_meter: &mut WeightMeter, + mut control_meter: Option<&mut WeightMeter>, effect_consumed: &mut Weight, effect_reconciliation_uncertain: &mut bool, mut resources: Option<( @@ -647,22 +2661,140 @@ impl Pallet { BlockResourceLimits, BlockResourceDomain, )>, + source: ActorServiceSource, + accounting: ProvisionalEntryAccounting, ) -> FifoStepResult { let consume_weight = T::WeightInfo::scheduler_paged_consume_preserve_page() .max(T::WeightInfo::scheduler_paged_consume_delete_page()); let state_probe_weight = Self::scheduler_actor_state_probe_weight_upper(); - if !cycle_meter.can_consume(state_probe_weight.saturating_add(consume_weight)) { + if !cycle_meter.can_consume(state_probe_weight) + || control_meter + .as_ref() + .is_some_and(|meter| !meter.can_consume(state_probe_weight)) + { return FifoStepResult::Blocked(BlockKind::Weight); } - let Some((state, admission, loaded_step)) = Self::load_actor_service_state(entry.actor_id) - else { - cycle_meter.consume(state_probe_weight); + cycle_meter.consume(state_probe_weight); + if let Some(meter) = control_meter.as_deref_mut() { + meter.consume(state_probe_weight); + } + let Ok((location, cell)) = Self::load_primary_control_cell(entry.actor_id) else { return FifoStepResult::Blocked(BlockKind::NonWeight); }; - cycle_meter.consume(state_probe_weight); - if state.hot.queue_ticket != Some(entry.ticket) { + let Some((identity, hot, admission)) = Self::project_control_cell(&cell, location) else { + return FifoStepResult::Blocked(BlockKind::NonWeight); + }; + if hot.queue_ticket != Some(entry.ticket) { return FifoStepResult::NoWork; } + if cell.eligible_at != Some(entry.eligible_at) { + return FifoStepResult::Blocked(BlockKind::NonWeight); + } + let inline_terminal_due = hot.terminal_at.is_some_and(|terminal| terminal <= now) + || identity.cycle_nonce == u64::MAX + || Self::failure_limit_reached(hot.unsuccessful_attempt_streak); + if entry.eligible_at > now + && !inline_terminal_due + && !hot.lifecycle.is_paused() + && !GlobalCircuitBreaker::::get() + { + return FifoStepResult::Blocked(BlockKind::NonWeight); + } + let opening_head = if hot.cycle_state == CycleState::Idle && hot.pending_signal { + let Some(head) = ActorContractHeads::::get(entry.actor_id) else { + return FifoStepResult::Blocked(BlockKind::NonWeight); + }; + Some(head) + } else { + None + }; + let ordinary_opening = opening_head.as_ref().is_some_and(|head| { + head.header.step_count > 0 + && head + .header + .auto_close_at_cycle_nonce + .is_none_or(|target| identity.cycle_nonce < target) + }); + let mut pipeline_capacity = None; + if (matches!(hot.cycle_state, CycleState::Running | CycleState::Suspended) || ordinary_opening) + && !inline_terminal_due + && !hot.lifecycle.is_paused() + && let Some(required_control) = consume_weight + .checked_add(&cell.resources.control) + .and_then(|weight| weight.checked_add(&Self::close_cleanup_weight_upper())) + && let Some(required_attempt) = required_control.checked_add(&cell.resources.effect) + && (!cycle_meter.can_consume(required_attempt) + || control_meter + .as_ref() + .is_some_and(|meter| !meter.can_consume(required_control)) + || resources.as_ref().is_some_and(|(state, limits, domain)| { + state.capacity_exceeded(*limits, *domain, cell.resources.effect) + })) + && !GlobalCircuitBreaker::::get() + { + if hot.cycle_state == CycleState::Running { + return FifoStepResult::Blocked(BlockKind::Weight); + } + if hot.cycle_state == CycleState::Suspended { + // Retry policy and User Action liability precede resource deferral; neither needs + // Opening payloads or funding state. Exceptional paths retain full classification. + let solvent_retry = (|| { + let run = ActorRunHeads::::get(entry.actor_id)?; + if !run.suspension_is_coherent() + || run.cycle_nonce != identity.cycle_nonce.checked_add(1)? + || run.last_attempt_block == now + || run.last_committed_step_block == Some(now) + || !run.has_contract_authority( + admission.semantic_contract_id, + admission.body_commitment, + admission.admission_identity, + ) + { + return None; + } + let loaded = + Self::load_current_step_with_admission(entry.actor_id, run.cursor, &admission)?; + let retry_limit = loaded.step.on_error.retry_max_attempts()?; + if run.unsuccessful_attempts_at_cursor == 0 + || run.unsuccessful_attempts_at_cursor >= retry_limit + { + return None; + } + Self::action_capacity_sufficient( + identity.actor_class.actor_type(), + &identity.sovereign_account, + &loaded.step, + loaded.resources, + ) + .ok() + })(); + if solvent_retry == Some(true) { + return FifoStepResult::Blocked(BlockKind::Weight); + } + } + pipeline_capacity = opening_head.as_ref().map(|head| { + Self::pipeline_capacity_sufficient_with_envelope( + identity.actor_class.actor_type(), + &identity.sovereign_account, + head.header.pipeline_machine_envelope, + ) + }); + if pipeline_capacity + .as_ref() + .is_some_and(|capacity| matches!(capacity, Ok(true))) + { + return FifoStepResult::Blocked(BlockKind::Weight); + } + } + let service_state = match opening_head { + Some(head) => { + Self::load_actor_service_state_with_head(entry.actor_id, identity, hot, admission, head) + } + None => Self::load_actor_service_state_with_control(entry.actor_id, identity, hot, admission), + }; + let Some((state, admission, loaded_step)) = service_state else { + return FifoStepResult::Blocked(BlockKind::NonWeight); + }; let current_ticket = loaded_step.as_ref().and_then(|_| { Self::build_actor_step_ticket( entry.actor_id, @@ -694,6 +2826,13 @@ impl Pallet { maximum_fee, ) }); + if state + .run_state + .as_ref() + .is_some_and(|run_state| run_state.last_committed_step_block == Some(now)) + { + return FifoStepResult::Blocked(BlockKind::NonWeight); + } if state.hot.cycle_state == CycleState::Suspended { if state .run_state @@ -706,51 +2845,37 @@ impl Pallet { return FifoStepResult::Blocked(BlockKind::NonWeight); } let queue_owner_hot = state.hot.clone(); + // A coherent pause atom clears the ticket and demotes Ready authority to Unsignaled before + // commit. A live paused Ready owner can therefore arise only from malformed frame state; do + // not invent a source/restoration Weight owner for an unreachable production transition. if state.hot.lifecycle.is_paused() && state .hot .terminal_at .is_none_or(|terminal_at| terminal_at > now) { - 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("paused queue consume failed"), - )); - } - ActorHot::::mutate(entry.actor_id, |maybe| { - if let Some(current) = maybe { - current.queue_ticket = None; - } - }); - 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 }; + return FifoStepResult::Blocked(BlockKind::NonWeight); } let actor_id = entry.actor_id; let loaded_run_state = state.run_state.clone(); - let instance = Self::derive_active_actor_view(state.identity, state.hot, state.contract); + let instance = Self::derive_active_actor_view( + state.identity.clone(), + state.hot.clone(), + state.contract.clone(), + ); match Self::apply_admission_loaded( actor_id, &instance, loaded_run_state.as_ref(), current_plan.as_ref(), + pipeline_capacity, cycle_meter, + Some(&accounting.replacement_meter(cycle_meter)), ) { AdmissionDecision::Admit { weight, - terminal_cleanup_reserved, + terminal_cleanup, + opening_observation, } => { if loaded_step.is_none() { if !instance.steps.is_empty() @@ -760,11 +2885,16 @@ impl Pallet { return FifoStepResult::Blocked(BlockKind::NonWeight); } let attempt_weight = consume_weight.saturating_add(weight); - if !cycle_meter.can_consume(attempt_weight) { + if !cycle_meter.can_consume(attempt_weight) + || control_meter + .as_ref() + .is_some_and(|meter| !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( + if Self::consume_actor_service_source( + source, position, entry.actor_id, entry.ticket, @@ -776,16 +2906,31 @@ impl Pallet { AttemptTransactionError::Invariant, )); } - match Self::execute_zero_step_opening_and_place(actor_id, instance, now) { - Ok(()) => polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(())), + let execution = Self::execute_zero_step_from_consumed_frame( + actor_id, + state.clone(), + &admission, + now, + opening_observation, + ); + match execution { + Ok(attempt) => { + polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(attempt)) + } Err(error) => { polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)) } } }); cycle_meter.consume(attempt_weight); + if let Some(meter) = control_meter.as_deref_mut() { + meter.consume(attempt_weight); + } return match outcome { - Ok(()) => FifoStepResult::Progress { executed: true }, + Ok(attempt) => FifoStepResult::Progress { + executed: true, + attempt: Some(attempt), + }, Err(AttemptTransactionError::FeeCollection) => { FifoStepResult::Blocked(BlockKind::FeeCollection) } @@ -795,79 +2940,32 @@ impl Pallet { }; } 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 }; + return FifoStepResult::Blocked(BlockKind::NonWeight); } - 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 { + let exhaustion_close_weight = if terminal_cleanup.is_included() { Weight::zero() } else { Self::close_cleanup_weight_upper() }; - 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 Some(reserved_attempt_control) = attempt_weight.checked_sub(&reserved_effect_weight) + else { + return FifoStepResult::Blocked(BlockKind::NonWeight); + }; + let reserved_control_with_cleanup = + reserved_attempt_control.saturating_add(exhaustion_close_weight); + if !cycle_meter.can_consume(attempt_weight.saturating_add(exhaustion_close_weight)) + || control_meter + .as_ref() + .is_some_and(|meter| !meter.can_consume(reserved_control_with_cleanup)) + { + return FifoStepResult::Blocked(BlockKind::Weight); + } let mut resource_reservation = match resources.as_mut() { Some((state, limits, effect_domain)) => { match state.reserve(*limits, *effect_domain, reserved_effect_weight) { @@ -878,7 +2976,8 @@ impl Pallet { None => None, }; let outcome = polkadot_sdk::frame_support::storage::with_transaction(|| { - if Self::paged_consume_loaded_head_at( + if Self::consume_actor_service_source( + source, position, entry.actor_id, entry.ticket, @@ -890,8 +2989,9 @@ impl Pallet { AttemptTransactionError::Invariant, )); } - match Self::execute_current_step_and_place(actor_id, execution_instance, commit_plan, now) - { + let execution = + Self::execute_current_step_and_place(actor_id, &state, commit_plan, &admission, now); + match execution { Ok(evidence) => { if let (Some((state, _, _)), Some(reservation)) = (resources.as_mut(), resource_reservation.as_mut()) @@ -926,110 +3026,87 @@ impl Pallet { closed_for_exhaustion, actual_control_weight, actual_effect_weight, + attempt, }) => { 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); + if let Some(meter) = control_meter.as_deref_mut() { + meter.consume(reserved_attempt_control); + } 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); + let actual_attempt_control = actual_attempt_weight + .checked_sub(&actual_effect_weight) + .unwrap_or(reserved_attempt_control); + if let Some(meter) = control_meter.as_deref_mut() { + meter.consume(actual_attempt_control); + } effect_consumed.saturating_accrue(actual_effect_weight); if closed_for_exhaustion { cycle_meter.consume(exhaustion_close_weight); + if let Some(meter) = control_meter.as_deref_mut() { + meter.consume(exhaustion_close_weight); + } + } + FifoStepResult::Progress { + executed: true, + attempt: Some(attempt), } - FifoStepResult::Progress { executed: true } } Err(AttemptTransactionError::FeeCollection) => { *effect_reconciliation_uncertain |= !reserved_effect_weight.is_zero(); cycle_meter.consume(attempt_weight); + if let Some(meter) = control_meter.as_deref_mut() { + meter.consume(reserved_attempt_control); + } FifoStepResult::Blocked(BlockKind::FeeCollection) } Err(AttemptTransactionError::StateHold | AttemptTransactionError::Invariant) => { *effect_reconciliation_uncertain |= !reserved_effect_weight.is_zero(); cycle_meter.consume(attempt_weight); + if let Some(meter) = control_meter.as_deref_mut() { + meter.consume(reserved_attempt_control); + } FifoStepResult::Blocked(BlockKind::NonWeight) } } } - AdmissionDecision::Close { reason, weight } => { - let atomic_weight = consume_weight.saturating_add(weight); - if !cycle_meter.can_consume(atomic_weight) { + AdmissionDecision::Close { reason, weight } => Self::service_admitted_close( + actor_id, + state, + admission, + reason, + weight, + cycle_meter, + control_meter, + Some((source, position, entry.ticket)), + accounting, + ), + AdmissionDecision::Refuse(owner) => { + let weight = Self::complete_entry_owner_weight(owner); + if !accounting.can_settle_complete(cycle_meter, control_meter.as_deref(), weight) { return FifoStepResult::Blocked(BlockKind::Weight); } - let outcome = polkadot_sdk::frame_support::storage::with_transaction(|| { - if let Err(error) = Self::finalize_actor(actor_id, &instance, reason) { - return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)); - } - Self::apply_test_close_queue_corruption(); - if Self::paged_consume_closed_head_at(position).is_err() { - return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err( - polkadot_sdk::sp_runtime::DispatchError::Other("scheduler queue head changed"), - )); - } - polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(())) - }); - cycle_meter.consume(atomic_weight); - match outcome { - Ok(()) => FifoStepResult::Progress { executed: false }, - Err(_) => FifoStepResult::Blocked(BlockKind::NonWeight), - } + accounting.settle_complete(cycle_meter, control_meter.as_deref_mut(), weight); + FifoStepResult::Blocked(BlockKind::NonWeight) } AdmissionDecision::Defer => FifoStepResult::Blocked(BlockKind::Weight), AdmissionDecision::Invariant => FifoStepResult::Blocked(BlockKind::NonWeight), - AdmissionDecision::Skip => { - let exhaustion_close_weight = Self::close_cleanup_weight_upper(); - if !cycle_meter.can_consume(consume_weight.saturating_add(exhaustion_close_weight)) { - return FifoStepResult::Blocked(BlockKind::Weight); - } - let outcome = polkadot_sdk::frame_support::storage::with_transaction(|| { - if Self::paged_consume_loaded_head_at(position, actor_id, entry.ticket, queue_owner_hot) - .is_err() - { - return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err( - polkadot_sdk::sp_runtime::DispatchError::Other("scheduler queue topology changed"), - )); - } - 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( - polkadot_sdk::sp_runtime::DispatchError::Other("post-skip placement failed"), - )); - } - return polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(true)); - } - } - Ok(None) => {} - Err(_) => { - return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err( - polkadot_sdk::sp_runtime::DispatchError::Other("post-skip actor state corrupt"), - )); - } - } - polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(false)) - }); - cycle_meter.consume(consume_weight); - match outcome { - Ok(closed_for_exhaustion) => { - if closed_for_exhaustion { - cycle_meter.consume(exhaustion_close_weight); - } - FifoStepResult::Progress { executed: false } - } - Err(_) => FifoStepResult::Blocked(BlockKind::NonWeight), - } - } + // Global-breaker classification is a state-preserving refusal. Keep the exact Ready owner + // and FIFO position rather than consuming and republishing the same authority under an + // unmeasured destination branch. + AdmissionDecision::Skip => FifoStepResult::Blocked(BlockKind::NonWeight), } } + #[cfg(test)] pub(crate) fn enqueue(actor_id: ActorId) -> Result<(), EnqueueOutcome> { match Self::try_paged_enqueue(actor_id) { Ok(()) => Ok(()), @@ -1041,33 +3118,97 @@ impl Pallet { let next_block = frame_system::Pallet::::block_number() .checked_add(&One::one()) .ok_or(EnqueueOutcome::SchedulerIndexExhausted)?; - Self::defer_wakeup(actor_id, next_block) + Self::defer_retained_wakeup(actor_id, next_block) } Err(other) => Err(other), } } - 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(())) + fn enqueue_authority_loaded( + actor_id: ActorId, + hot: ActorHotStateOf, + identity: &ActorIdentityOf, + run_state: Option<&ActorRunStateOf>, + admission: &ActorAdmissionCertificateOf, + resources: ActorStepResourceEnvelope, + ) -> Result<(), EnqueueOutcome> { + let result = with_transaction_opaque_err(|| { + match Self::preflight_paged_enqueue_authority( + actor_id, hot, identity, run_state, admission, resources, + ) { + Ok(plan) => match Self::commit_paged_enqueue(plan) { + Ok(()) => polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(())), + Err(error) => { + polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)) } + }, + Err(error) => { + polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)) + } + } + }) + .map_err(|_| EnqueueOutcome::CorruptedTopology)?; + match result { + Ok(()) | Err(EnqueueOutcome::AlreadyLive) => Ok(()), + Err(other) => Err(other), + } + } + + fn enqueue_actor_state_loaded( + actor_id: ActorId, + state: &ActiveActorStateOf, + admission: &ActorAdmissionCertificateOf, + loaded_step: Option<&LoadedActorStepOf>, + ) -> Result<(), EnqueueOutcome> { + let result = with_transaction_opaque_err(|| { + match Self::preflight_paged_enqueue_actor_state(actor_id, state, admission, loaded_step) { + Ok(plan) => match Self::commit_paged_enqueue(plan) { + Ok(()) => polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(())), Err(error) => { polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)) } }, - ) - .map_err(|_| EnqueueOutcome::CorruptedTopology)?; + 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) + let instance = Self::derive_active_actor_view( + state.identity.clone(), + state.hot.clone(), + state.contract.clone(), + ); + { + let resources = if state.contract.steps.is_empty() { + ActorStepResourceEnvelope { + control: T::WeightInfo::scheduler_inner_zero_step_complete(), + effect: Weight::zero(), + } + } else { + let cursor = state.run_state.as_ref().map_or(0, |run| run.cursor); + loaded_step + .filter(|loaded| loaded.cursor == cursor) + .map(|loaded| loaded.resources) + .ok_or(EnqueueOutcome::CorruptedTopology)? + }; + Self::defer_wakeup_with_authority( + actor_id, + next_block, + &instance, + state.hot.clone(), + &state.identity, + state.run_state.as_ref(), + admission, + resources, + ) + } } Err(other) => Err(other), } @@ -1082,7 +3223,9 @@ impl Pallet { fn apply_test_close_queue_corruption() { CORRUPT_QUEUE_BEFORE_CLOSE_CONSUME.with(|flag| { if flag.replace(false) { - QueueTail::::mutate(|tail| *tail = tail.saturating_add(1)); + ActorReadyTail::::put( + ActorReadyHead::::get().saturating_add(u64::from(T::MaxQueueLength::get()) + 1), + ); } }); } @@ -1090,69 +3233,38 @@ impl Pallet { #[cfg(not(test))] fn apply_test_close_queue_corruption() {} - fn queue_page_size() -> u64 { - u64::from(T::QueuePageSize::get()) - } - - fn queue_page_and_slot(position: QueueTicket) -> (QueuePageId, usize) { - let page_size = Self::queue_page_size(); - ((position / page_size), (position % page_size) as usize) - } - - fn queue_topology_preflight(mutation: QueueMutation) -> Result { - let head = QueueHead::::get(); - let tail = QueueTail::::get(); - let occupancy = QueueOccupancy::::get(); + fn queue_topology_preflight(_mutation: QueueMutation) -> Result { + let head = ActorReadyHead::::get(); + let tail = ActorReadyTail::::get(); + let occupancy = ActorReadyOccupancy::::get(); let span = tail .checked_sub(head) .ok_or(EnqueueOutcome::CorruptedTopology)?; - if span != u64::from(occupancy) || occupancy > T::MaxQueueLength::get() { + if span > u64::from(T::MaxQueueLength::get()) + || u64::from(occupancy) > span + || occupancy > T::MaxActiveActors::get() + { return Err(EnqueueOutcome::CorruptedTopology); } - let page_size = Self::queue_page_size(); - let page_size_usize = page_size as usize; - if occupancy == 0 { - if !head.is_multiple_of(page_size) { + if head < tail { + let chunk = + ActorReadyFrameChunks::::get(head / 32).ok_or(EnqueueOutcome::CorruptedTopology)?; + // ActorControlChunkOf has at most 32 slots; inspect only the consumed head prefix. + if chunk.len() != 32 + || chunk.iter(/* deos-bypass: bounded-iter */).take((head % 32) as usize).any(Option::is_some) + { return Err(EnqueueOutcome::CorruptedTopology); } - if matches!(mutation, QueueMutation::Enqueue) { - let (tail_page_id, tail_slot) = Self::queue_page_and_slot(tail); - if tail_slot != 0 || QueuePages::::contains_key(tail_page_id) { - return Err(EnqueueOutcome::CorruptedTopology); - } - } - return Ok(QueueTopology { - head, - tail, - occupancy, - }); } - - let (head_page_id, head_slot) = Self::queue_page_and_slot(head); - let head_page = QueuePages::::get(head_page_id).ok_or(EnqueueOutcome::CorruptedTopology)?; - if head_page.is_empty() || head_page.len() > page_size_usize || head_slot >= head_page.len() { - return Err(EnqueueOutcome::CorruptedTopology); - } - let (tail_page_id, tail_slot) = Self::queue_page_and_slot(tail); - if head_page_id == tail_page_id { - if tail_slot == 0 || head_page.len() != tail_slot { - return Err(EnqueueOutcome::CorruptedTopology); - } - } else { - if head_page.len() != page_size_usize { + if let Some(chunk) = ActorReadyFrameChunks::::get(tail / 32) { + // ActorControlChunkOf has at most 32 slots; inspect only the unused tail suffix. + if chunk.len() != 32 + || chunk.iter(/* deos-bypass: bounded-iter */).skip((tail % 32) as usize).any(Option::is_some) + { return Err(EnqueueOutcome::CorruptedTopology); } - if tail_slot == 0 { - if QueuePages::::contains_key(tail_page_id) { - return Err(EnqueueOutcome::CorruptedTopology); - } - } else { - let tail_page = - QueuePages::::get(tail_page_id).ok_or(EnqueueOutcome::CorruptedTopology)?; - if tail_page.len() != tail_slot || tail_page.len() > page_size_usize { - return Err(EnqueueOutcome::CorruptedTopology); - } - } + } else if head < tail && !tail.is_multiple_of(32) { + return Err(EnqueueOutcome::CorruptedTopology); } Ok(QueueTopology { head, @@ -1162,9 +3274,10 @@ impl Pallet { } pub fn combined_queue_occupancy() -> u64 { - u64::from(QueueOccupancy::::get()) + u64::from(ActorReadyOccupancy::::get()) } + #[cfg(any(test, feature = "runtime-benchmarks"))] /// Appends one actor to the canonical FIFO using the global ticket allocator. pub fn paged_enqueue(actor_id: ActorId) -> bool { matches!( @@ -1173,209 +3286,268 @@ impl Pallet { ) } - pub(crate) fn preflight_paged_enqueue_cohort( + pub(crate) fn preflight_paged_enqueue_cohort_with_authority( actors: Vec<(ActorId, ActorHotStateOf)>, ) -> Result, EnqueueOutcome> { if actors.is_empty() || actors.len() > T::MaxCrossingActorsPerBlock::get() as usize { return Err(EnqueueOutcome::CapacityUnavailable); } - 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, - }; + let mut plan = Self::new_queue_append_plan()?; for (actor_id, hot) in actors.into_iter(/* deos-bypass: bounded-iter */) { - Self::reserve_following_paged_enqueue(&mut plan, actor_id, hot)?; + let (state, admission, loaded_step) = + Self::load_frame_actor_service_state(actor_id).ok_or(EnqueueOutcome::CorruptedTopology)?; + let cursor = state.run_state.as_ref().map_or(0, |run| run.cursor); + let resources = if state.contract.steps.is_empty() { + ActorStepResourceEnvelope { + control: T::WeightInfo::scheduler_inner_zero_step_complete(), + effect: Weight::zero(), + } + } else { + loaded_step + .filter(|loaded| loaded.cursor == cursor) + .map(|loaded| loaded.resources) + .ok_or(EnqueueOutcome::CorruptedTopology)? + }; + Self::reserve_following_paged_enqueue_with_authority( + &mut plan, + actor_id, + hot, + &state.identity, + state.run_state.as_ref(), + &admission, + resources, + )?; } Ok(plan) } - fn preflight_paged_enqueue_loaded( + fn preflight_paged_enqueue_actor_state( + actor_id: ActorId, + state: &ActiveActorStateOf, + admission: &ActorAdmissionCertificateOf, + loaded_step: Option<&LoadedActorStepOf>, + ) -> Result, EnqueueOutcome> { + let cursor = state.run_state.as_ref().map_or(0, |run| run.cursor); + let resources = if state.contract.steps.is_empty() { + ActorStepResourceEnvelope { + control: T::WeightInfo::scheduler_inner_zero_step_complete(), + effect: Weight::zero(), + } + } else { + loaded_step + .filter(|loaded| loaded.cursor == cursor) + .map(|loaded| loaded.resources) + .ok_or(EnqueueOutcome::CorruptedTopology)? + }; + Self::preflight_paged_enqueue_authority( + actor_id, + state.hot.clone(), + &state.identity, + state.run_state.as_ref(), + admission, + resources, + ) + } + + fn new_queue_append_plan() -> Result, EnqueueOutcome> { + let topology = Self::queue_topology_preflight(QueueMutation::Enqueue)?; + Ok(QueueAppendPlan { + publications: Vec::new(), + next_tail: topology.tail, + next_occupancy: topology.occupancy, + }) + } + + pub(crate) fn preflight_paged_enqueue_authority( actor_id: ActorId, hot: ActorHotStateOf, + identity: &ActorIdentityOf, + run_state: Option<&ActorRunStateOf>, + admission: &ActorAdmissionCertificateOf, + resources: ActorStepResourceEnvelope, ) -> Result, EnqueueOutcome> { - Self::preflight_paged_enqueue_cohort(vec![(actor_id, hot)]) + let mut plan = Self::new_queue_append_plan()?; + Self::reserve_following_paged_enqueue_with_authority( + &mut plan, actor_id, hot, identity, run_state, admission, resources, + )?; + Ok(plan) + } + + fn prepare_observation_ready_cell( + state: &ObservationActivationState, + hot: &ActorHotStateOf, + ) -> Result, EnqueueOutcome> { + let (location, mut cell) = Self::load_primary_control_cell(state.actor_id) + .map_err(|_| EnqueueOutcome::CorruptedTopology)?; + let expected_identity = Self::control_identity_from_scalar(state.identity.clone()) + .ok_or(EnqueueOutcome::CorruptedTopology)?; + if matches!(location, ActorControlLocation::Ready { .. }) + || cell.identity != expected_identity + || cell.hot != Self::control_hot_from_scalar(state.hot.clone()) + { + return Err(EnqueueOutcome::CorruptedTopology); + } + cell.hot = Self::control_hot_from_scalar(hot.clone()); + cell.cursor = state.run_head.as_ref().map_or(0, |run| run.cursor); + cell.eligible_at = Some( + state + .run_head + .as_ref() + .map_or_else(frame_system::Pallet::::block_number, |run| { + run.eligible_at + }), + ); + Ok(cell) } 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)?; + ) -> Result, EnqueueOutcome> { + let cell = Self::prepare_observation_ready_cell(state, &hot)?; + let mut plan = Self::new_queue_append_plan()?; + let mut planned_actors = alloc::collections::BTreeSet::new(); + Self::reserve_following_paged_enqueue_observation( + &mut plan, + &mut planned_actors, + state, + hot, + cell, + )?; Ok(plan) } fn reserve_following_paged_enqueue_observation( plan: &mut QueueAppendPlan, + planned_actors: &mut alloc::collections::BTreeSet, state: &ObservationActivationState, - mut hot: ActorHotStateOf, + hot: ActorHotStateOf, + cell: ActorControlCellOf, ) -> Result<(), EnqueueOutcome> { - let actor_id = state.actor_id; - if hot.queue_ticket.is_some() || plan.next_occupancy >= T::MaxQueueLength::get() { + if hot.queue_ticket.is_some() { 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) => ( + Self::preflight_ready_publication_capacity(plan)?; + 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) + .ok_or(EnqueueOutcome::SchedulerIndexExhausted)?; } + (CycleState::Running | CycleState::Suspended, Some(_)) => {} _ => 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() { + } + if !planned_actors.insert(state.actor_id) { return Err(EnqueueOutcome::CorruptedTopology); } - plan.actors.push((actor_id, hot)); - plan.next_ticket = next_ticket; + Self::reserve_ready_publication(plan, cell) + } + + fn preflight_ready_publication_capacity(plan: &QueueAppendPlan) -> Result<(), EnqueueOutcome> { + if plan.next_tail.saturating_sub(ActorReadyHead::::get()) + >= u64::from(T::MaxQueueLength::get()) + || plan.next_occupancy >= T::MaxActiveActors::get() + { + return Err(EnqueueOutcome::CapacityUnavailable); + } + plan + .next_tail + .checked_add(1) + .ok_or(EnqueueOutcome::TicketExhausted)?; + Ok(()) + } + + fn reserve_ready_publication( + plan: &mut QueueAppendPlan, + cell: ActorControlCellOf, + ) -> Result<(), EnqueueOutcome> { + let ticket = plan.next_tail; + let next_tail = ticket + .checked_add(1) + .ok_or(EnqueueOutcome::TicketExhausted)?; + let next_occupancy = plan + .next_occupancy + .checked_add(1) + .ok_or(EnqueueOutcome::SchedulerIndexExhausted)?; + plan + .publications + .push(PreparedReadyPublication { ticket, cell }); plan.next_tail = next_tail; plan.next_occupancy = next_occupancy; Ok(()) } - pub(crate) fn reserve_following_paged_enqueue( + fn reserve_following_paged_enqueue_with_authority( plan: &mut QueueAppendPlan, actor_id: ActorId, mut hot: ActorHotStateOf, + identity: &ActorIdentityOf, + run_state: Option<&ActorRunStateOf>, + admission: &ActorAdmissionCertificateOf, + resources: ActorStepResourceEnvelope, ) -> Result<(), EnqueueOutcome> { - if plan.actors.len() >= T::MaxCrossingActorsPerBlock::get() as usize { + if plan.publications.len() >= T::MaxCrossingActorsPerBlock::get() as usize { return Err(EnqueueOutcome::CapacityUnavailable); } if hot.queue_ticket.is_some() - || plan - .actors - .iter(/* deos-bypass: bounded-iter */) - .any(|(planned_actor, _)| *planned_actor == actor_id) + || plan.publications.iter(/* deos-bypass: bounded-iter */) + .any(|publication| publication.cell.actor_id == actor_id) { return Err(EnqueueOutcome::AlreadyLive); } - if 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)?; + Self::preflight_ready_publication_capacity(plan)?; + let ticket = plan.next_tail; 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 now = frame_system::Pallet::::block_number(); + let eligible_at = match run_state { + Some(run) => run.eligible_at, + None if hot.last_cycle_block == Some(now) => now + .checked_add(&One::one()) + .ok_or(EnqueueOutcome::SchedulerIndexExhausted)?, + None => now, + }; let step_ticket = Self::build_actor_step_ticket( actor_id, ticket, eligible_at, - &identity, + identity, &hot, - run_state.as_ref(), - &admission, + run_state, + admission, ) .ok_or(EnqueueOutcome::CorruptedTopology)?; - 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 + let cell = ActorControlCell { + actor_id, + identity: Self::control_identity_from_scalar(identity.clone()) + .ok_or(EnqueueOutcome::CorruptedTopology)?, + hot: Self::control_hot_from_scalar(hot), + cursor: step_ticket.cursor, + eligible_at: Some(step_ticket.eligible_at), + admission: admission.clone(), + resources, }; - 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(()) + Self::reserve_ready_publication(plan, cell) } - fn preflight_paged_enqueue(actor_id: ActorId) -> Result, EnqueueOutcome> { - let Some((state, _, _)) = Self::load_actor_service_state(actor_id) else { - return if ActorHot::::contains_key(actor_id) { + #[cfg(any(test, feature = "runtime-benchmarks"))] + fn preflight_retained_paged_enqueue( + actor_id: ActorId, + ) -> Result, EnqueueOutcome> { + let Some((state, admission, loaded_step)) = Self::load_frame_actor_service_state(actor_id) + else { + return if Self::control_hot_exists(actor_id) { Err(EnqueueOutcome::CorruptedTopology) } else { Err(EnqueueOutcome::CapacityUnavailable) }; }; - Self::preflight_paged_enqueue_loaded(actor_id, state.hot) + // Preserve the single-member cohort admission boundary before queue topology checks. + if T::MaxCrossingActorsPerBlock::get() == 0 { + return Err(EnqueueOutcome::CapacityUnavailable); + } + Self::preflight_paged_enqueue_actor_state(actor_id, &state, &admission, loaded_step.as_ref()) } #[cfg(test)] @@ -1391,16 +3563,12 @@ impl Pallet { LoadedActorStateOf::Active(state) => state.hot, _ => return Err(EnqueueOutcome::CorruptedTopology), }; - let plan = - Self::preflight_paged_enqueue_cohort(vec![(first, first_hot), (second, second_hot)])?; - let first_ticket = plan.actors[0] - .1 - .queue_ticket - .ok_or(EnqueueOutcome::CorruptedTopology)?; - let second_ticket = plan.actors[1] - .1 - .queue_ticket - .ok_or(EnqueueOutcome::CorruptedTopology)?; + let plan = Self::preflight_paged_enqueue_cohort_with_authority(vec![ + (first, first_hot), + (second, second_hot), + ])?; + let first_ticket = plan.publications[0].ticket; + let second_ticket = plan.publications[1].ticket; Ok([first_ticket, second_ticket]) } @@ -1420,14 +3588,14 @@ impl Pallet { }; cohort.push((*actor_id, hot)); } - let plan = Self::preflight_paged_enqueue_cohort(cohort)?; + let plan = Self::preflight_paged_enqueue_cohort_with_authority(cohort)?; let mut tickets = [0; 4]; - for (index, (_, hot)) in plan - .actors + for (index, publication) in plan + .publications .iter(/* deos-bypass: bounded-iter */) .enumerate() { - tickets[index] = hot.queue_ticket.ok_or(EnqueueOutcome::CorruptedTopology)?; + tickets[index] = publication.ticket; } Ok(tickets) } @@ -1443,9 +3611,8 @@ impl Pallet { hot.pending_signal = true; cohort.push((actor_id, hot)); } - let plan = Self::preflight_paged_enqueue_cohort(cohort)?; - Self::commit_paged_enqueue(plan); - Ok(()) + let plan = Self::preflight_paged_enqueue_cohort_with_authority(cohort)?; + Self::commit_paged_enqueue(plan) } #[cfg(test)] @@ -1458,7 +3625,7 @@ impl Pallet { }; cohort.push((actor_id, hot)); } - Self::preflight_paged_enqueue_cohort(cohort).map(|_| ()) + Self::preflight_paged_enqueue_cohort_with_authority(cohort).map(|_| ()) } #[cfg(test)] @@ -1505,26 +3672,201 @@ impl Pallet { QUEUE_APPEND_COMMITS.with(core::cell::Cell::get) } - pub(crate) fn commit_paged_enqueue(plan: QueueAppendPlan) { + pub(crate) fn update_existing_frame_control_identity( + actor_id: ActorId, + identity: &ActorIdentityOf, + ) -> Result<(), EnqueueOutcome> { + let (location, mut cell) = + Self::load_primary_control_cell(actor_id).map_err(|_| EnqueueOutcome::CorruptedTopology)?; + cell.identity = Self::control_identity_from_scalar(identity.clone()) + .ok_or(EnqueueOutcome::CorruptedTopology)?; + Self::store_primary_control_cell(location, cell).map_err(|_| EnqueueOutcome::CorruptedTopology) + } + + pub(crate) fn update_existing_frame_control_hot( + actor_id: ActorId, + hot: &ActorHotStateOf, + ) -> Result<(), EnqueueOutcome> { + let (location, mut cell) = + Self::load_primary_control_cell(actor_id).map_err(|_| EnqueueOutcome::CorruptedTopology)?; + cell.hot = Self::control_hot_from_scalar(hot.clone()); + Self::store_primary_control_cell(location, cell).map_err(|_| EnqueueOutcome::CorruptedTopology) + } + + pub(crate) fn restore_unsignaled_from_authority( + actor_id: ActorId, + hot: ActorHotStateOf, + identity: &ActorIdentityOf, + run_state: Option<&ActorRunStateOf>, + admission: &ActorAdmissionCertificateOf, + resources: ActorStepResourceEnvelope, + ) -> Result<(), EnqueueOutcome> { + if ActorControlLocators::::contains_key(actor_id) { + Self::remove_primary_control_cell_inner(actor_id) + .map_err(|_| EnqueueOutcome::CorruptedTopology)?; + } + let cell = ActorControlCell { + actor_id, + identity: Self::control_identity_from_scalar(identity.clone()) + .ok_or(EnqueueOutcome::CorruptedTopology)?, + hot: Self::control_hot_from_scalar(hot), + cursor: run_state.map_or(0, |run| run.cursor), + eligible_at: None, + admission: admission.clone(), + resources, + }; + ActorUnsignaledControlCells::::insert(actor_id, cell); + ActorControlLocators::::insert(actor_id, ActorControlLocation::Unsignaled); + Ok(()) + } + + fn consume_ready_primary(actor_id: ActorId, ticket: QueueTicket) -> Result<(), EnqueueOutcome> { + if ActorControlLocators::::get(actor_id) != Some(ActorControlLocation::Ready { ticket }) { + return Err(EnqueueOutcome::CorruptedTopology); + } + Self::remove_primary_control_cell_inner(actor_id) + .map(|_| ()) + .map_err(|_| EnqueueOutcome::CorruptedTopology) + } + + pub(crate) fn detach_primary_for_successor( + actor_id: ActorId, + successor_hot: &ActorHotStateOf, + ) -> Result<(), EnqueueOutcome> { + let (location, cell) = + Self::load_primary_control_cell(actor_id).map_err(|_| EnqueueOutcome::CorruptedTopology)?; + if let ActorControlLocation::Waiting { key, page, slot } = location { + let pointer = WakeupPointer { + block: key, + page_id: page, + slot: u32::from(slot), + }; + if Self::wakeup_pointer_for_clock(successor_hot, key.clock()) == Some(pointer) { + let mut stored = ActorWaitingFrameChunks::::get((key, page)) + .ok_or(EnqueueOutcome::CorruptedTopology)?; + let target = stored + .entries + .get_mut(slot as usize) + .ok_or(EnqueueOutcome::CorruptedTopology)?; + if target.as_ref().and_then(ActorWaitingEntry::primary) != Some(&cell) { + return Err(EnqueueOutcome::CorruptedTopology); + } + *target = Some(ActorWaitingEntry::Reference(ActorWakeupReference { + actor_id, + admission_identity: cell.admission.admission_identity, + })); + ActorWaitingFrameChunks::::insert((key, page), stored); + ActorControlLocators::::remove(actor_id); + return Ok(()); + } + } + Self::remove_primary_control_cell_inner(actor_id) + .map(|_| ()) + .map_err(|_| EnqueueOutcome::CorruptedTopology) + } + + fn consume_waiting_from_supplied_authority( + actor_id: ActorId, + key: WakeupKey>, + hot: &ActorHotStateOf, + ) -> Result<(), EnqueueOutcome> { + let Some(location) = ActorControlLocators::::get(actor_id) else { + return Ok(()); + }; + if let ActorControlLocation::Waiting { + key: waiting_key, .. + } = location + && waiting_key == key + { + let mut cell = Self::remove_primary_control_cell_inner(actor_id) + .map_err(|_| EnqueueOutcome::CorruptedTopology)?; + // The caller validates the exact physical pointer before consuming its primary. + let consumed_trigger_source = matches!(key, WakeupKey::Tick(tick) + if cell.hot.trigger_wakeup_pointer.is_some_and(|pointer| pointer.tick == tick)); + if consumed_trigger_source { + if hot.cycle_state != CycleState::Idle + || hot.pending_signal + || hot.queue_ticket.is_some() + || hot.trigger_wakeup_pointer.is_some() + { + return Err(EnqueueOutcome::CorruptedTopology); + } + cell.hot = Self::control_hot_from_scalar(hot.clone()); + cell.cursor = 0; + cell.eligible_at = None; + ActorUnsignaledControlCells::::insert(actor_id, cell); + ActorControlLocators::::insert(actor_id, ActorControlLocation::Unsignaled); + } + return Ok(()); + } + let (_, mut cell) = + Self::load_primary_control_cell(actor_id).map_err(|_| EnqueueOutcome::CorruptedTopology)?; + cell.hot = Self::control_hot_from_scalar(hot.clone()); + Self::store_primary_control_cell(location, cell).map_err(|_| EnqueueOutcome::CorruptedTopology) + } + + fn commit_paged_enqueue_transactional(plan: QueueAppendPlan) -> Result<(), EnqueueOutcome> { + with_transaction_opaque_err(|| match Self::commit_paged_enqueue(plan) { + Ok(()) => polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(())), + Err(error) => polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)), + }) + .map_err(|_| EnqueueOutcome::CorruptedTopology)? + } + + pub(crate) fn commit_paged_enqueue(plan: QueueAppendPlan) -> Result<(), EnqueueOutcome> { #[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); + for PreparedReadyPublication { ticket, cell } in plan.publications { + let actor_id = cell.actor_id; + let hot = Self::control_hot_to_scalar(&cell.hot, Some(ticket)); + if let Some(location) = ActorControlLocators::::get(actor_id) { + let (_, source) = Self::load_primary_control_cell(actor_id) + .map_err(|_| EnqueueOutcome::CorruptedTopology)?; + let source_hot_matches = if source.hot == cell.hot { + true + } else { + let mut pre_activation_hot = cell.hot.clone(); + pre_activation_hot.pending_signal = false; + source.hot == pre_activation_hot + }; + if matches!(location, ActorControlLocation::Ready { .. }) + || source.actor_id != cell.actor_id + || source.identity != cell.identity + || !source_hot_matches + || source.cursor != cell.cursor + || source.admission != cell.admission + || source.resources != cell.resources + { + return Err(EnqueueOutcome::CorruptedTopology); + } + Self::detach_primary_for_successor(actor_id, &hot)?; + } + if ActorReadyTail::::get() != ticket || cell.eligible_at.is_none() { + return Err(EnqueueOutcome::CorruptedTopology); + } + let (stored_actor, stored_ticket) = + Self::control_append_ready(cell).map_err(|_| EnqueueOutcome::CorruptedTopology)?; + if (stored_actor, stored_ticket) != (actor_id, ticket) { + return Err(EnqueueOutcome::CorruptedTopology); + } } - 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); + if ActorReadyTail::::get() != plan.next_tail + || ActorReadyOccupancy::::get() != plan.next_occupancy + { + return Err(EnqueueOutcome::CorruptedTopology); } + Ok(()) } - 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(())) - } + #[cfg(any(test, feature = "runtime-benchmarks"))] + pub(crate) fn try_paged_enqueue(actor_id: ActorId) -> Result<(), EnqueueOutcome> { + with_transaction_opaque_err(|| match Self::preflight_retained_paged_enqueue(actor_id) { + Ok(plan) => match Self::commit_paged_enqueue(plan) { + Ok(()) => polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(())), + Err(error) => { + polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)) + } + }, Err(error) => polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)), }) .map_err(|_| EnqueueOutcome::CorruptedTopology)? @@ -1539,20 +3881,14 @@ impl Pallet { ) } - 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 { + let activate = || Self::request_activation_inner(actor_id); if polkadot_sdk::frame_support::storage::transactional::is_transactional() { - return Self::request_activation_inner(actor_id); + return activate(); } - polkadot_sdk::frame_support::storage::with_transaction(|| match Self::request_activation_inner( - actor_id, - ) { + 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)), }) @@ -1580,7 +3916,7 @@ impl Pallet { let activate = || match Self::request_observation_activation_compact_inner( actor_id, feed, - true, + ObservationTerminalHandling::Execute, cause_provenance, cause_block, )? { @@ -1607,7 +3943,7 @@ impl Pallet { Self::request_observation_activation_compact_inner( actor_id, feed, - false, + ObservationTerminalHandling::Defer, cause_provenance, cause_block, ) @@ -1686,9 +4022,7 @@ impl Pallet { 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(), + state, hot, wakeup_key: WakeupKey::Block(wakeup_at), }, @@ -1756,22 +4090,35 @@ impl Pallet { } }; let mut plan = QueueAppendPlan { - actors: Vec::new(), - pages: Vec::new(), - next_ticket: NextQueueTicket::::get(), + publications: Vec::new(), next_tail: topology.tail, next_occupancy: topology.occupancy, }; + // Identity-only membership is bounded by this observation page, not actor population. + let mut planned_actors = alloc::collections::BTreeSet::new(); for candidate in admitted.into_iter(/* deos-bypass: bounded-iter */) { + let prepared_frame_cell = + match Self::prepare_observation_ready_cell(&candidate.state, &candidate.hot) { + Ok(cell) => cell, + Err(error) => { + return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err( + error, + )); + } + }; if let Err(error) = Self::reserve_following_paged_enqueue_observation( &mut plan, + &mut planned_actors, &candidate.state, candidate.hot, + prepared_frame_cell, ) { return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)); } } - Self::commit_paged_enqueue(plan); + if let Err(error) = Self::commit_paged_enqueue(plan) { + return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)); + } for (actor_id, breakdown, _) in occurrences { IndexedTriggerDetectionDisabled::::insert(actor_id, ()); Self::deposit_event(Event::TriggerOccurrenceProcessed { @@ -1800,14 +4147,16 @@ impl Pallet { } let result = with_transaction_opaque_err(|| { for candidate in candidates { + let actor_id = candidate.state.actor_id; + let actor_type = candidate.state.identity.actor_class.actor_type(); let breakdown = Self::trigger_fee_for_weight( - candidate.actor_type, + 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, + actor_type, + &candidate.state.identity.sovereign_account, breakdown, ) { Ok(charged) => charged, @@ -1820,22 +4169,16 @@ impl Pallet { 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); - } + match Self::place_observation_wakeup(&candidate.state, candidate.hot, candidate.wakeup_key) + { + Ok(()) | Err(EnqueueOutcome::AlreadyLive) => {} Err(error) => { return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)); } } - IndexedTriggerDetectionDisabled::::insert(candidate.actor_id, ()); + IndexedTriggerDetectionDisabled::::insert(actor_id, ()); Self::deposit_event(Event::TriggerOccurrenceProcessed { - actor_id: candidate.actor_id, + actor_id, trigger_family: breakdown.trigger_family, fee: breakdown.trigger_fee, }); @@ -1850,15 +4193,45 @@ impl Pallet { result } + fn place_observation_wakeup( + state: &ObservationActivationState, + hot: ActorHotStateOf, + wakeup_key: WakeupKey>, + ) -> Result<(), EnqueueOutcome> { + let admission = state + .admission + .as_ref() + .ok_or(EnqueueOutcome::CorruptedTopology)?; + let cursor = state.run_head.as_ref().map_or(0, |run| run.cursor); + let resources = state.loaded_step.as_ref().map_or( + ActorStepResourceEnvelope { + control: T::WeightInfo::scheduler_inner_zero_step_complete(), + effect: Weight::zero(), + }, + |loaded| loaded.resources, + ); + Self::try_store_control_hot_with_authority(state.actor_id, hot.clone()) + .map_err(|_| EnqueueOutcome::CorruptedTopology)?; + Self::try_wakeup_substrate_schedule_transition_with_authority( + state.actor_id, + wakeup_key, + hot, + &state.identity, + cursor, + admission, + resources, + ) + } + fn request_observation_activation_compact_inner( actor_id: ActorId, feed: T::ObservationFeedId, - execute_terminal: bool, + terminal_handling: ObservationTerminalHandling, _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) { + return if Self::control_hot_exists(actor_id) { Err(ActivationFailure::Permanent( Error::::ActorInvariant.into(), )) @@ -1873,7 +4246,7 @@ impl Pallet { ActivationFailure::Permanent(Self::classification_dispatch_error(error).into()) })?; if classification.terminal_reason.is_some() { - return if execute_terminal { + return if matches!(terminal_handling, ObservationTerminalHandling::Execute) { Self::request_activation_inner(actor_id).map(ObservationActivationOutcome::Ordinary) } else { Ok(ObservationActivationOutcome::TerminalDeferred) @@ -1885,7 +4258,8 @@ impl Pallet { hot.pending_signal = true; if hot.queue_ticket.is_some() { if !already_pending { - ActorHot::::insert(actor_id, hot); + Self::try_store_control_hot_with_authority(actor_id, hot) + .map_err(|_| ActivationFailure::Permanent(Error::::ActorInvariant.into()))?; } return Ok(ObservationActivationOutcome::Ordinary(if already_pending { ActivationOutcome::Coalesced @@ -1950,15 +4324,17 @@ impl Pallet { }; let placement_result = match placement { - CompactPlacement::None => { - ActorHot::::insert(actor_id, hot); - Ok(()) - } + CompactPlacement::None => Self::try_store_control_hot_with_authority(actor_id, hot), CompactPlacement::Queue => { - match Self::preflight_paged_enqueue_observation_loaded(&state, hot) { - Ok(plan) => { - Self::commit_paged_enqueue(plan); - Ok(()) + match Self::preflight_paged_enqueue_observation_loaded(&state, hot.clone()) { + Ok(plan) => Self::commit_paged_enqueue(plan), + Err(EnqueueOutcome::CapacityUnavailable) => { + let retry_at = frame_system::Pallet::::block_number() + .checked_add(&One::one()) + .ok_or(ActivationFailure::Permanent( + Error::::SchedulerIndexExhausted.into(), + ))?; + Self::place_observation_wakeup(&state, hot, WakeupKey::Block(retry_at)) } Err(_) => { return Self::request_activation_inner(actor_id) @@ -1967,12 +4343,7 @@ impl Pallet { } } CompactPlacement::Wakeup(block) => { - ActorHot::::insert(actor_id, hot.clone()); - Self::try_wakeup_substrate_schedule_transition_loaded( - actor_id, - WakeupKey::Block(block), - hot, - ) + Self::place_observation_wakeup(&state, hot, WakeupKey::Block(block)) } }; match placement_result { @@ -1992,15 +4363,58 @@ impl Pallet { } } + fn preflight_activation_enqueue( + actor_id: ActorId, + state: &ActiveActorStateOf, + admission: &ActorAdmissionCertificateOf, + ) -> Result, EnqueueOutcome> { + let cursor = state.run_state.as_ref().map_or(0, |run| run.cursor); + let loaded_step = if state.contract.steps.is_empty() { + None + } else { + Some( + Self::load_current_step_with_admission(actor_id, cursor, admission) + .ok_or(EnqueueOutcome::CorruptedTopology)?, + ) + }; + Self::preflight_paged_enqueue_actor_state(actor_id, state, admission, loaded_step.as_ref()) + } + pub(crate) fn preflight_activation_loaded( actor_id: ActorId, state: ActiveActorStateOf, ) -> Result, ActivationFailure> { + let frame_admission = { + let (_, identity, _, admission) = Self::load_frame_control_authority(actor_id).ok_or( + ActivationFailure::Permanent(Error::::ActorInvariant.into()), + )?; + if identity != state.identity { + return Err(ActivationFailure::Permanent( + Error::::ActorInvariant.into(), + )); + } + admission + }; + Self::preflight_activation_from_authority(actor_id, state, frame_admission) + } + + fn preflight_activation_from_authority( + actor_id: ActorId, + state: ActiveActorStateOf, + frame_admission: ActorAdmissionCertificateOf, + ) -> Result, ActivationFailure> { + let frame_source_state = state.clone(); let already_pending = state.hot.pending_signal; - let run_state = state.run_state; - let mut hot = state.hot; + let mut queue_state = state; + let run_state = queue_state.run_state.clone(); + let mut hot = queue_state.hot.clone(); hot.pending_signal = true; - let instance = Self::derive_active_actor_view(state.identity, hot.clone(), state.contract); + queue_state.hot = hot.clone(); + let instance = Self::derive_active_actor_view( + queue_state.identity.clone(), + hot.clone(), + queue_state.contract.clone(), + ); let classification = Self::classify_actor_loaded(&instance, run_state.as_ref()).map_err(|error| { ActivationFailure::Permanent(Self::classification_dispatch_error(error).into()) @@ -2012,21 +4426,36 @@ impl Pallet { ActivationAction::Close(classification.terminal_reason.ok_or( ActivationFailure::Permanent(Error::::ActorInvariant.into()), )?) - } else if instance.queue_ticket.is_some() { + } else if instance.queue_ticket.is_some() + || (ActorControlLocators::::contains_key(actor_id) + && matches!( + instance.cycle_state, + CycleState::Running | CycleState::Suspended + )) + { ActivationAction::CoalesceLive } 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, - run_state.as_ref(), + ActivationAction::EnqueueTemporal(Self::preflight_activation_enqueue( + actor_id, + &queue_state, + &frame_admission, )) + } else { + match Self::preflight_prime_schedule_loaded(&instance, run_state.as_ref()) { + Ok(PrimeSchedulePlan::Enqueue) => ActivationAction::EnqueueReady( + Self::preflight_activation_enqueue(actor_id, &queue_state, &frame_admission), + ), + other => ActivationAction::PrimeSchedule(other), + } }; Ok(ActivationPlan { actor_id, + + frame_admission, + frame_source_state, already_pending, prospective_hot: hot, instance, @@ -2037,7 +4466,12 @@ impl Pallet { #[cfg(test)] pub(crate) fn test_activation_plan_kind(actor_id: ActorId) -> Result { - let LoadedActorStateOf::Active(state) = Self::load_actor_state(actor_id) else { + let loaded = if cfg!(feature = "runtime-benchmarks") { + Self::load_actor_state(actor_id) + } else { + Self::load_frame_actor_state(actor_id) + }; + let LoadedActorStateOf::Active(state) = loaded else { return Err(ActivationFailure::Permanent( Error::::ActorInvariant.into(), )); @@ -2048,29 +4482,70 @@ impl Pallet { ActivationAction::CoalesceLive => 1, ActivationAction::EnqueueTemporal(_) => 2, ActivationAction::PrimeSchedule(Ok(PrimeSchedulePlan::None)) => 3, + ActivationAction::EnqueueReady(_) => 4, ActivationAction::PrimeSchedule(Ok(PrimeSchedulePlan::Enqueue)) => 4, ActivationAction::PrimeSchedule(Ok(PrimeSchedulePlan::BlockWakeup(_))) => 5, ActivationAction::PrimeSchedule(Err(_)) => 6, }) } - pub(crate) fn commit_activation_plan( + pub(crate) fn commit_activation_plan( + plan: ActivationPlan, + ) -> Result { + if polkadot_sdk::frame_support::storage::transactional::is_transactional() { + return Self::commit_activation_plan_inner(plan); + } + polkadot_sdk::frame_support::storage::with_transaction(|| { + match Self::commit_activation_plan_inner(plan) { + Ok(outcome) => { + polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(outcome)) + } + Err(error) => { + polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)) + } + } + }) + } + + fn commit_activation_plan_inner( plan: ActivationPlan, ) -> Result { let ActivationPlan { actor_id, + + frame_admission, + frame_source_state, already_pending, prospective_hot, instance, terminal_reason: _, action, } = plan; + let placement_hot = prospective_hot.clone(); if !already_pending { - ActorHot::::insert(actor_id, prospective_hot); + // A live process latches the deferred occurrence by mutating its canonical primary in place. + // Placement-changing activations instead publish prospective authority in the destination + // cell; writing it into Unsignaled first would create a forbidden transient owner state. + if matches!( + &action, + ActivationAction::CoalesceLive + | ActivationAction::PrimeSchedule(Ok(PrimeSchedulePlan::None)) + ) { + Self::try_store_control_hot_with_authority(actor_id, prospective_hot) + .map_err(|_| ActivationFailure::Permanent(Error::::ActorInvariant.into()))?; + } } match action { ActivationAction::Close(reason) => { - Self::finalize_actor(actor_id, &instance, reason).map_err(ActivationFailure::Permanent)?; + { + Self::finalize_actor_from_retained_state( + actor_id, + frame_source_state, + &frame_admission, + reason, + ) + .map_err(ActivationFailure::Permanent)?; + } return Ok(ActivationOutcome::Closed); } ActivationAction::CoalesceLive => { @@ -2080,22 +4555,51 @@ impl Pallet { ActivationOutcome::Latched }); } - ActivationAction::EnqueueTemporal(_) | ActivationAction::PrimeSchedule(_) => {} + ActivationAction::EnqueueReady(_) + | ActivationAction::EnqueueTemporal(_) + | ActivationAction::PrimeSchedule(_) => {} } let placement = match action { - ActivationAction::EnqueueTemporal(Ok(queue_plan)) => { - Self::commit_paged_enqueue(queue_plan); - Ok(()) + ActivationAction::EnqueueReady(Ok(queue_plan)) + | ActivationAction::EnqueueTemporal(Ok(queue_plan)) => { + Self::commit_paged_enqueue_transactional(queue_plan) + } + ActivationAction::EnqueueReady(Err(EnqueueOutcome::CapacityUnavailable)) + | ActivationAction::EnqueueTemporal(Err(EnqueueOutcome::CapacityUnavailable)) => { + match frame_system::Pallet::::block_number().checked_add(&One::one()) { + Some(next_block) => Self::defer_activation_wakeup( + actor_id, + next_block, + &instance, + placement_hot.clone(), + &frame_source_state, + &frame_admission, + ), + None => Err(EnqueueOutcome::SchedulerIndexExhausted), + } } - ActivationAction::EnqueueTemporal(Err(EnqueueOutcome::CapacityUnavailable)) => { - Self::enqueue(actor_id) + ActivationAction::EnqueueReady(Err(error)) + | ActivationAction::EnqueueTemporal(Err(error)) => Err(error), + ActivationAction::PrimeSchedule(Ok(PrimeSchedulePlan::None)) => { + if Self::load_frame_control_authority(actor_id).is_some() { + Ok(()) + } else { + Err(EnqueueOutcome::CorruptedTopology) + } + } + ActivationAction::PrimeSchedule(Ok(PrimeSchedulePlan::Enqueue)) => { + Err(EnqueueOutcome::CorruptedTopology) } - 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) + Self::defer_activation_wakeup( + actor_id, + block, + &instance, + placement_hot, + &frame_source_state, + &frame_admission, + ) } ActivationAction::PrimeSchedule(Err(error)) => Err(error), ActivationAction::Close(_) | ActivationAction::CoalesceLive => { @@ -2114,13 +4618,17 @@ impl Pallet { ActivationFailure::Temporary(Error::::QueueCapacityUnavailable.into()), ), Err( - outcome @ (EnqueueOutcome::TicketExhausted + EnqueueOutcome::TicketExhausted | EnqueueOutcome::SchedulerIndexExhausted - | EnqueueOutcome::WakeupIndexExhausted), + | EnqueueOutcome::WakeupIndexExhausted, ) => { - let _ = outcome; - Self::close_for_scheduler_index_exhaustion(actor_id) - .map_err(ActivationFailure::Permanent)?; + Self::finalize_actor_from_retained_state( + actor_id, + frame_source_state, + &frame_admission, + CloseReason::SchedulerIndexExhausted, + ) + .map_err(ActivationFailure::Permanent)?; Ok(ActivationOutcome::Closed) } Err(EnqueueOutcome::CorruptedTopology) => Err(ActivationFailure::Permanent( @@ -2130,7 +4638,8 @@ impl Pallet { } fn request_activation_inner(actor_id: ActorId) -> Result { - let state = match Self::load_actor_state(actor_id) { + let loaded = Self::load_actor_state_with_authority(actor_id); + let state = match loaded { LoadedActorStateOf::NotRegistered | LoadedActorStateOf::Dormant(_) => { return Ok(ActivationOutcome::IgnoredStale); } @@ -2161,454 +4670,1542 @@ impl Pallet { Err(EnqueueOutcome::SchedulerIndexExhausted) => { Err(Error::::SchedulerIndexExhausted.into()) } - Err(EnqueueOutcome::WakeupCapacityExhausted) => { - Err(Error::::QueueCapacityUnavailable.into()) + Err(EnqueueOutcome::WakeupCapacityExhausted) => { + Err(Error::::QueueCapacityUnavailable.into()) + } + Err(EnqueueOutcome::WakeupIndexExhausted) => Err(Error::::SchedulerIndexExhausted.into()), + Err(EnqueueOutcome::CorruptedTopology) => Err(Error::::SchedulerIndexExhausted.into()), + } + } + + /// Extracts the public error from a failed placement outcome for `map_err` sites. + pub fn placement_error(outcome: EnqueueOutcome) -> DispatchError { + match Self::enqueue_outcome_error(Err(outcome)) { + // Placement owners normally normalize AlreadyLive to success before `map_err`. + // A missed normalization fails closed instead of panicking in consensus execution. + Ok(()) => Error::::QueueCapacityUnavailable.into(), + Err(error) => error, + } + } + + #[cfg(any(test, feature = "runtime-benchmarks"))] + pub fn paged_invalidate(actor_id: ActorId) -> Option { + Self::try_invalidate_ready_to_unsignaled(actor_id) + .ok() + .flatten() + } + + fn invalidate_ready_to_unsignaled_inner( + actor_id: ActorId, + ) -> Result, EnqueueOutcome> { + let Some((state, admission, loaded_step)) = Self::load_frame_actor_service_state(actor_id) + else { + return if Self::control_hot_exists(actor_id) { + Err(EnqueueOutcome::CorruptedTopology) + } else { + Ok(None) + }; + }; + let ticket = state.hot.queue_ticket; + if ticket.is_some() { + let resources = if state.contract.steps.is_empty() { + ActorStepResourceEnvelope { + control: T::WeightInfo::scheduler_inner_zero_step_complete(), + effect: Weight::zero(), + } + } else { + let cursor = state.run_state.as_ref().map_or(0, |run| run.cursor); + loaded_step + .filter(|loaded| loaded.cursor == cursor) + .map(|loaded| loaded.resources) + .ok_or(EnqueueOutcome::CorruptedTopology)? + }; + let mut hot = state.hot; + hot.queue_ticket = None; + Self::remove_primary_control_cell_inner(actor_id) + .map_err(|_| EnqueueOutcome::CorruptedTopology)?; + Self::restore_unsignaled_from_authority( + actor_id, + hot, + &state.identity, + state.run_state.as_ref(), + &admission, + resources, + )?; + } + Ok(ticket) + } + + pub(crate) fn try_invalidate_ready_to_unsignaled( + actor_id: ActorId, + ) -> Result, EnqueueOutcome> { + with_transaction_opaque_err( + || match Self::invalidate_ready_to_unsignaled_inner(actor_id) { + Ok(ticket) => polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(ticket)), + Err(error) => { + polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)) + } + }, + ) + .map_err(|_| EnqueueOutcome::CorruptedTopology)? + } + + #[cfg(test)] + pub(crate) fn try_paged_invalidate( + actor_id: ActorId, + ) -> Result, EnqueueOutcome> { + Self::try_invalidate_ready_to_unsignaled(actor_id) + } + + pub(crate) fn invalidate_ready_to_unsignaled_with_authority( + actor_id: ActorId, + ) -> Result<(), EnqueueOutcome> { + Self::try_invalidate_ready_to_unsignaled(actor_id)? + .map(|_| ()) + .ok_or(EnqueueOutcome::CorruptedTopology) + } + + pub fn paged_head_entry() -> Option<(QueueTicket, QueueEntry>)> { + let head = ActorReadyHead::::get(); + if head >= ActorReadyTail::::get() { + return None; + } + let chunk = ActorReadyFrameChunks::::get(head / 32)?; + let cell = chunk.get((head % 32) as usize)?.as_ref()?; + let entry = ActorStepTicket { + actor_id: cell.actor_id, + // Discovery must retain an exhausted head for mandatory terminal cleanup. + // Opening still requires a checked next nonce in the execution ticket builder. + cycle_nonce: cell.identity.cycle_nonce.saturating_add(1), + cursor: cell.cursor, + ticket: head, + eligible_at: cell.eligible_at?, + contract_commitment: ActorContractCommitment { + semantic_contract_id: cell.admission.semantic_contract_id, + body_commitment: cell.admission.body_commitment, + }, + }; + Some((head, entry)) + } + + #[cfg(any(test, feature = "runtime-benchmarks"))] + pub(crate) fn paged_consume_head_at(position: QueueTicket) -> Result<(), EnqueueOutcome> { + Self::paged_consume_head_at_inner(position, ReadyHeadOwner::DiscoverCanonical) + } + + pub(crate) fn paged_consume_loaded_head_at( + position: QueueTicket, + actor_id: ActorId, + ticket: QueueTicket, + hot: ActorHotStateOf, + ) -> Result<(), EnqueueOutcome> { + Self::paged_consume_head_at_inner( + position, + ReadyHeadOwner::Loaded { + actor_id, + ticket, + hot, + }, + ) + } + + fn paged_consume_closed_head_at(position: QueueTicket) -> Result<(), EnqueueOutcome> { + Self::paged_consume_head_at_inner(position, ReadyHeadOwner::ClosedTombstone) + } + + fn paged_consume_head_at_inner( + position: QueueTicket, + owner: ReadyHeadOwner, + ) -> Result<(), EnqueueOutcome> { + with_transaction_opaque_err(|| { + let transition = || -> Result<(), EnqueueOutcome> { + let topology = Self::queue_topology_preflight(QueueMutation::Head)?; + if position != topology.head || position >= topology.tail { + return Err(EnqueueOutcome::CorruptedTopology); + } + let entry = Self::paged_head_entry().map(|(_, entry)| entry); + let loaded = match owner { + ReadyHeadOwner::Loaded { + actor_id, + ticket, + hot, + } => { + let entry = entry.ok_or(EnqueueOutcome::CorruptedTopology)?; + if entry.actor_id != actor_id + || entry.ticket != ticket + || hot.queue_ticket != Some(ticket) + { + return Err(EnqueueOutcome::CorruptedTopology); + } + Some(entry) + } + ReadyHeadOwner::ClosedTombstone => { + if entry.is_some() { + return Err(EnqueueOutcome::CorruptedTopology); + } + None + } + #[cfg(any(test, feature = "runtime-benchmarks"))] + ReadyHeadOwner::DiscoverCanonical => { + let entry = entry.ok_or(EnqueueOutcome::CorruptedTopology)?; + let (state, _, _) = Self::load_frame_actor_service_state(entry.actor_id) + .ok_or(EnqueueOutcome::CorruptedTopology)?; + if state.hot.queue_ticket != Some(entry.ticket) { + return Err(EnqueueOutcome::CorruptedTopology); + } + Some(entry) + } + }; + if let Some(entry) = loaded { + Self::consume_ready_primary(entry.actor_id, entry.ticket)?; + } + let next_head = position + .checked_add(1) + .ok_or(EnqueueOutcome::SchedulerIndexExhausted)?; + ActorReadyHead::::put(next_head); + if next_head.is_multiple_of(32) || next_head == topology.tail { + ActorReadyFrameChunks::::remove(position / 32); + } + Ok(()) + }; + match transition() { + Ok(()) => polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(())), + Err(error) => { + polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)) + } + } + }) + .map_err(|_| EnqueueOutcome::CorruptedTopology)? + } + + #[cfg(any(test, feature = "runtime-benchmarks"))] + pub fn paged_consume_head(ticket: QueueTicket) -> bool { + let Some((position, entry)) = Self::paged_head_entry() else { + return false; + }; + entry.ticket == ticket && Self::paged_consume_head_at(position).is_ok() + } + + pub fn paged_drain_tombstones( + cutoff: QueueTicket, + scan_limit: u32, + ) -> Result { + with_transaction_opaque_err(|| { + let transition = || -> Result { + let topology = Self::queue_topology_preflight(QueueMutation::Head)?; + let mut stats = QueueDrainStats::default(); + let mut head = topology.head; + let limit = scan_limit.min(T::MaxQueueEntriesScannedPerBlock::get()); + while head < topology.tail && head < cutoff && stats.entries_scanned < limit { + let page_id = head / 32; + let chunk = + ActorReadyFrameChunks::::get(page_id).ok_or(EnqueueOutcome::CorruptedTopology)?; + if chunk.len() != 32 { + return Err(EnqueueOutcome::CorruptedTopology); + } + stats.pages_touched = stats.pages_touched.saturating_add(1); + while head < topology.tail + && head < cutoff + && head / 32 == page_id + && stats.entries_scanned < limit + { + stats.entries_scanned = stats.entries_scanned.saturating_add(1); + if let Some(cell) = &chunk[(head % 32) as usize] { + if ActorControlLocators::::get(cell.actor_id) + != Some(ActorControlLocation::Ready { ticket: head }) + || Self::project_control_cell(cell, ActorControlLocation::Ready { ticket: head }) + .is_none() + { + return Err(EnqueueOutcome::CorruptedTopology); + } + if head != topology.head { + ActorReadyHead::::put(head); + } + return Ok(stats); + } + stats.tombstones_skipped = stats.tombstones_skipped.saturating_add(1); + head = head + .checked_add(1) + .ok_or(EnqueueOutcome::SchedulerIndexExhausted)?; + } + if head.is_multiple_of(32) || head == topology.tail { + ActorReadyFrameChunks::::remove(page_id); + stats.pages_deleted = stats.pages_deleted.saturating_add(1); + } + } + if head != topology.head { + ActorReadyHead::::put(head); + } + Ok(stats) + }; + match transition() { + Ok(stats) => polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(stats)), + Err(error) => { + polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)) + } + } + }) + .map_err(|_| EnqueueOutcome::CorruptedTopology)? + } + + pub(crate) fn wakeup_page_entry_matches( + pointer: WakeupPointer>, + actor_id: ActorId, + ) -> bool { + ActorWaitingFrameChunks::::get((pointer.block, pointer.page_id)) + .and_then(|page| page.entries.get(pointer.slot as usize).cloned().flatten()) + .is_some_and(|entry| match entry { + ActorWaitingEntry::Primary(cell) => cell.actor_id == actor_id, + ActorWaitingEntry::Reference(reference) => reference.actor_id == actor_id, + }) + } + + 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, + } + } + + pub(crate) fn wakeup_substrate_invalidate_loaded( + actor_id: ActorId, + state: ActiveActorStateOf, + admission: &ActorAdmissionCertificateOf, + ) -> Result>>, EnqueueOutcome> { + Self::wakeup_substrate_invalidate_clock_loaded(actor_id, state, admission, WakeupClock::Block) + } + + pub(crate) fn trigger_wakeup_substrate_invalidate_loaded( + actor_id: ActorId, + state: ActiveActorStateOf, + admission: &ActorAdmissionCertificateOf, + ) -> Result>>, EnqueueOutcome> { + Self::wakeup_substrate_invalidate_clock_loaded(actor_id, state, admission, WakeupClock::Tick) + } + + #[cfg(any(test, feature = "runtime-benchmarks"))] + fn wakeup_substrate_invalidate_clock_inner( + actor_id: ActorId, + clock: WakeupClock, + ) -> Result>>, EnqueueOutcome> { + let Some((state, admission, _)) = Self::load_frame_actor_service_state(actor_id) else { + return Err(EnqueueOutcome::CorruptedTopology); + }; + Self::wakeup_substrate_invalidate_clock_loaded(actor_id, state, &admission, clock) + } + + fn wakeup_substrate_invalidate_clock_loaded( + actor_id: ActorId, + mut state: ActiveActorStateOf, + admission: &ActorAdmissionCertificateOf, + clock: WakeupClock, + ) -> Result>>, EnqueueOutcome> { + let Some(pointer) = Self::wakeup_pointer_for_clock(&state.hot, clock) else { + return Ok(None); + }; + Self::invalidate_wakeup_reference(actor_id, pointer, admission.admission_identity)?; + Self::clear_wakeup_pointer_for_clock(&mut state.hot, clock); + { + Self::consume_waiting_from_supplied_authority(actor_id, pointer.block, &state.hot)?; + } + Ok(Some(pointer)) + } + + pub(crate) fn invalidate_wakeup_reference( + actor_id: ActorId, + pointer: WakeupPointer>, + admission_identity: [u8; 32], + ) -> Result<(), EnqueueOutcome> { + let page = ActorWaitingFrameChunks::::get((pointer.block, pointer.page_id)) + .ok_or(EnqueueOutcome::CorruptedTopology)?; + match page + .entries + .get(pointer.slot as usize) + .and_then(Option::as_ref) + { + Some(ActorWaitingEntry::Primary(cell)) + if cell.actor_id == actor_id && cell.admission.admission_identity == admission_identity => + { + // The caller transfers primary authority immediately after clearing this pointer. + // Only that transfer may remove the primary slot and its locator. + Ok(()) + } + Some(ActorWaitingEntry::Reference(reference)) + if reference.actor_id == actor_id && reference.admission_identity == admission_identity => + { + Self::remove_waiting_entry(pointer) + .map(|_| ()) + .map_err(|_| EnqueueOutcome::CorruptedTopology) } - Err(EnqueueOutcome::WakeupIndexExhausted) => Err(Error::::SchedulerIndexExhausted.into()), - Err(EnqueueOutcome::CorruptedTopology) => Err(Error::::SchedulerIndexExhausted.into()), + _ => Err(EnqueueOutcome::CorruptedTopology), } } - /// Extracts the public error from a failed placement outcome for `map_err` sites. - pub fn placement_error(outcome: EnqueueOutcome) -> DispatchError { - match Self::enqueue_outcome_error(Err(outcome)) { - // Placement owners normally normalize AlreadyLive to success before `map_err`. - // A missed normalization fails closed instead of panicking in consensus execution. - Ok(()) => Error::::QueueCapacityUnavailable.into(), - Err(error) => error, + #[cfg(all(test, feature = "runtime-benchmarks"))] + pub(crate) fn control_invalidate_wakeup_reference( + mut cell: ActorControlCellOf, + clock: WakeupClock, + ) -> Result, ActorControlTransitionError> { + let pointer = match clock { + WakeupClock::Block => cell.hot.wakeup_pointer, + WakeupClock::Tick => cell + .hot + .trigger_wakeup_pointer + .map(|pointer| WakeupPointer { + block: WakeupKey::Tick(pointer.tick), + page_id: pointer.page_id, + slot: pointer.slot, + }), + } + .ok_or(ActorControlTransitionError::Invariant)?; + Self::invalidate_wakeup_reference(cell.actor_id, pointer, cell.admission.admission_identity) + .map_err(|_| ActorControlTransitionError::Invariant)?; + match clock { + WakeupClock::Block => cell.hot.wakeup_pointer = None, + WakeupClock::Tick => cell.hot.trigger_wakeup_pointer = None, } + Ok(cell) } - fn wakeup_page_physical_live(page: &WakeupPageOf) -> usize { - let mut live = 0usize; - for entry in page.entries.as_slice() { - if entry.is_some() { - live = live.saturating_add(1); - } + #[cfg(all(test, feature = "runtime-benchmarks"))] + pub(crate) fn control_due_wakeup_reference( + key: WakeupKey>, + now: BlockNumberFor, + now_tick: SchedulerTick, + ) -> Result<(ActorId, WakeupPointer>), ActorControlTransitionError> { + let due = match key { + WakeupKey::Block(block) => block <= now, + WakeupKey::Tick(tick) => tick <= now_tick, + }; + if !due || Self::wakeup_cursor_peek_key(key.clock()) != Some(key) { + return Err(ActorControlTransitionError::Invariant); + } + let cursor_index = + ActorWaitingCursorIndices::::get(key).ok_or(ActorControlTransitionError::Invariant)?; + if ActorWaitingOccupancies::::get(key) == 0 + || Self::wakeup_cursor_get(key.clock(), cursor_index) != Some(key) + { + return Err(ActorControlTransitionError::Invariant); + } + let page_id = ActorWaitingHeads::::get(key) / 32; + let page = ActorWaitingFrameChunks::::get((key, page_id)) + .ok_or(ActorControlTransitionError::Invariant)?; + if page.previous_page.is_some() + || page.entries.iter(/* deos-bypass: bounded-iter */).filter(|entry| entry.is_some()).count() + != page.live_entries as usize + || page.live_entries == 0 + { + return Err(ActorControlTransitionError::Invariant); + } + let scan_slot = page.scan_slot as usize; + if page + .entries + .iter(/* deos-bypass: bounded-iter */) + .take(scan_slot) + .any(Option::is_some) + { + return Err(ActorControlTransitionError::Invariant); } - live + let (slot, entry) = page + .entries + .iter(/* deos-bypass: bounded-iter */) + .enumerate() + .skip(scan_slot) + .find_map(|(slot, entry)| entry.as_ref().map(|entry| (slot, entry))) + .ok_or(ActorControlTransitionError::Invariant)?; + let slot = WakeupSlot::try_from(slot).map_err(|_| ActorControlTransitionError::Invariant)?; + Ok(( + match entry { + ActorWaitingEntry::Primary(cell) => cell.actor_id, + ActorWaitingEntry::Reference(reference) => reference.actor_id, + }, + WakeupPointer { + block: key, + page_id, + slot, + }, + )) } - pub fn paged_invalidate(actor_id: ActorId) -> Option { - Self::try_paged_invalidate(actor_id).ok().flatten() + #[cfg(all(test, feature = "runtime-benchmarks"))] + pub(crate) fn control_due_wakeup_primary( + key: WakeupKey>, + now: BlockNumberFor, + now_tick: SchedulerTick, + ) -> Result< + ( + ActorControlLocation>, + ActorControlCellOf, + ), + ActorControlTransitionError, + > { + let (actor_id, pointer) = Self::control_due_wakeup_reference(key, now, now_tick)?; + let location = + ActorControlLocators::::get(actor_id).ok_or(ActorControlTransitionError::Invariant)?; + let cell = match location { + ActorControlLocation::Unsignaled => ActorUnsignaledControlCells::::get(actor_id), + ActorControlLocation::Ready { ticket } => ActorReadyFrameChunks::::get(ticket / 32) + .and_then(|chunk| chunk.get((ticket % 32) as usize).cloned().flatten()), + ActorControlLocation::Waiting { key, page, slot } => { + ActorWaitingFrameChunks::::get((key, page)) + .and_then(|page| page.entries.get(slot as usize).cloned().flatten()) + .and_then(ActorWaitingEntry::into_primary) + } + } + .ok_or(ActorControlTransitionError::Invariant)?; + if cell.actor_id != actor_id { + return Err(ActorControlTransitionError::Invariant); + } + let reference_page = ActorWaitingFrameChunks::::get((key, pointer.page_id)) + .ok_or(ActorControlTransitionError::Invariant)?; + let entry = reference_page + .entries + .get(pointer.slot as usize) + .and_then(Option::as_ref) + .ok_or(ActorControlTransitionError::Invariant)?; + let admission_identity = match entry { + ActorWaitingEntry::Primary(primary) => primary.admission.admission_identity, + ActorWaitingEntry::Reference(reference) => reference.admission_identity, + }; + if admission_identity != cell.admission.admission_identity { + return Err(ActorControlTransitionError::Invariant); + } + let pointer_matches = match key.clock() { + WakeupClock::Block => cell.hot.wakeup_pointer == Some(pointer), + WakeupClock::Tick => match key { + WakeupKey::Tick(tick) => cell.hot.trigger_wakeup_pointer.is_some_and(|candidate| { + candidate.tick == tick + && candidate.page_id == pointer.page_id + && candidate.slot == pointer.slot + }), + WakeupKey::Block(_) => false, + }, + }; + if !pointer_matches { + return Err(ActorControlTransitionError::Invariant); + } + Ok((location, cell)) } - pub(crate) fn try_paged_invalidate( - actor_id: ActorId, - ) -> Result, EnqueueOutcome> { - 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); + #[cfg(all(test, feature = "runtime-benchmarks"))] + pub(crate) fn control_consume_due_wakeup_reference( + cell: ActorControlCellOf, + key: WakeupKey>, + now: BlockNumberFor, + now_tick: SchedulerTick, + ) -> Result, ActorControlTransitionError> { + let due = match key { + WakeupKey::Block(block) => block <= now, + WakeupKey::Tick(tick) => tick <= now_tick, + }; + if !due || Self::wakeup_cursor_peek_key(key.clock()) != Some(key) { + return Err(ActorControlTransitionError::Invariant); } - if ActorHot::::contains_key(actor_id) { - Err(EnqueueOutcome::CorruptedTopology) - } else { - Ok(None) + let pointer_matches = match key { + WakeupKey::Block(_) => cell + .hot + .wakeup_pointer + .is_some_and(|pointer| pointer.block == key), + WakeupKey::Tick(tick) => cell + .hot + .trigger_wakeup_pointer + .is_some_and(|pointer| pointer.tick == tick), + }; + if !pointer_matches { + return Err(ActorControlTransitionError::Invariant); } + Self::control_invalidate_wakeup_reference(cell, key.clock()) } - pub fn paged_head_entry() -> Option<(QueueTicket, QueueEntry>)> { - let head = QueueHead::::get(); - if head >= QueueTail::::get() { + #[cfg(any(feature = "try-runtime", all(test, feature = "runtime-benchmarks")))] + pub(crate) fn frame_control_entries() -> Option< + Vec<( + ActorId, + ActorControlLocation>, + ActorControlCellOf, + )>, + > { + let mut entries = alloc::collections::BTreeMap::< + ActorId, + ( + ActorControlLocation>, + ActorControlCellOf, + ), + >::new(); + let mut ready_count = 0u32; + for (actor_id, cell) in ActorUnsignaledControlCells::::iter(/* deos-bypass: bounded-iter */) + { + if cell.actor_id != actor_id + || entries + .insert(actor_id, (ActorControlLocation::Unsignaled, cell)) + .is_some() + { + return None; + } + } + for (page, chunk) in ActorReadyFrameChunks::::iter(/* deos-bypass: bounded-iter */) { + for (slot, cell) in chunk.into_iter().enumerate() { + let Some(cell) = cell else { + continue; + }; + let ticket = page.checked_mul(32)?.checked_add(slot as u64)?; + if entries + .insert( + cell.actor_id, + (ActorControlLocation::Ready { ticket }, cell), + ) + .is_some() + { + return None; + } + ready_count = ready_count.checked_add(1)?; + } + } + for ((key, page), chunk) in ActorWaitingFrameChunks::::iter(/* deos-bypass: bounded-iter */) + { + for (slot, entry) in chunk.entries.into_iter().enumerate() { + let Some(ActorWaitingEntry::Primary(cell)) = entry else { + continue; + }; + let slot = u8::try_from(slot).ok()?; + if entries + .insert( + cell.actor_id, + (ActorControlLocation::Waiting { key, page, slot }, cell), + ) + .is_some() + { + return None; + } + } + } + let locators = + ActorControlLocators::::iter().collect::>(); + if locators.len() != entries.len() + || entries + .iter(/* deos-bypass: bounded-iter */) + .any(|(actor_id, (location, _))| locators.get(actor_id) != Some(location)) + || ActorReadyOccupancy::::get() != ready_count + { return None; } - let (page_id, slot) = Self::queue_page_and_slot(head); - QueuePages::::get(page_id) - .and_then(|page| page.get(slot).copied()) - .map(|entry| (head, entry)) + Some( + entries + .into_iter() + .map(|(actor_id, (location, cell))| (actor_id, location, cell)) + .collect(), + ) } - pub(crate) fn paged_consume_head_at(position: QueueTicket) -> Result<(), EnqueueOutcome> { - Self::paged_consume_head_at_inner(position, false, None) + pub(crate) fn load_primary_control_cell( + actor_id: ActorId, + ) -> Result< + ( + ActorControlLocation>, + ActorControlCellOf, + ), + ActorControlTransitionError, + > { + let location = + ActorControlLocators::::get(actor_id).ok_or(ActorControlTransitionError::Invariant)?; + let cell = match location { + ActorControlLocation::Unsignaled => ActorUnsignaledControlCells::::get(actor_id), + ActorControlLocation::Ready { ticket } => ActorReadyFrameChunks::::get(ticket / 32) + .and_then(|chunk| chunk.get((ticket % 32) as usize).cloned().flatten()), + ActorControlLocation::Waiting { key, page, slot } => { + ActorWaitingFrameChunks::::get((key, page)) + .and_then(|page| page.entries.get(slot as usize).cloned().flatten()) + .and_then(ActorWaitingEntry::into_primary) + } + } + .ok_or(ActorControlTransitionError::Invariant)?; + if cell.actor_id != actor_id { + return Err(ActorControlTransitionError::Invariant); + } + Ok((location, cell)) + } + + pub(crate) fn store_primary_control_cell( + location: ActorControlLocation>, + cell: ActorControlCellOf, + ) -> Result<(), ActorControlTransitionError> { + let actor_id = cell.actor_id; + if ActorControlLocators::::get(actor_id) != Some(location) { + return Err(ActorControlTransitionError::Invariant); + } + match location { + ActorControlLocation::Unsignaled => { + if ActorUnsignaledControlCells::::get(actor_id) + .is_none_or(|stored| stored.actor_id != actor_id) + { + return Err(ActorControlTransitionError::Invariant); + } + ActorUnsignaledControlCells::::insert(actor_id, cell); + } + ActorControlLocation::Ready { ticket } => { + let page = ticket / 32; + let slot = (ticket % 32) as usize; + let mut chunk = + ActorReadyFrameChunks::::get(page).ok_or(ActorControlTransitionError::Invariant)?; + let stored = chunk + .get_mut(slot) + .ok_or(ActorControlTransitionError::Invariant)?; + if stored.as_ref().map(|stored| stored.actor_id) != Some(actor_id) { + return Err(ActorControlTransitionError::Invariant); + } + *stored = Some(cell); + ActorReadyFrameChunks::::insert(page, chunk); + } + ActorControlLocation::Waiting { key, page, slot } => { + let mut chunk = ActorWaitingFrameChunks::::get((key, page)) + .ok_or(ActorControlTransitionError::Invariant)?; + let stored = chunk + .entries + .get_mut(slot as usize) + .ok_or(ActorControlTransitionError::Invariant)?; + if stored + .as_ref() + .and_then(ActorWaitingEntry::primary) + .map(|stored| stored.actor_id) + != Some(actor_id) + { + return Err(ActorControlTransitionError::Invariant); + } + *stored = Some(ActorWaitingEntry::Primary(cell)); + ActorWaitingFrameChunks::::insert((key, page), chunk); + } + } + Ok(()) } - fn paged_consume_loaded_head_at( - position: QueueTicket, + pub(crate) fn remove_primary_control_cell_inner( actor_id: ActorId, - ticket: QueueTicket, - hot: ActorHotStateOf, - ) -> Result<(), EnqueueOutcome> { - Self::paged_consume_head_at_inner(position, false, Some((actor_id, ticket, hot))) + ) -> Result, ActorControlTransitionError> { + let (location, cell) = Self::load_primary_control_cell(actor_id)?; + match location { + ActorControlLocation::Unsignaled => { + ActorUnsignaledControlCells::::remove(actor_id); + } + ActorControlLocation::Ready { ticket } => { + let page = ticket / 32; + let slot = (ticket % 32) as usize; + let mut chunk = + ActorReadyFrameChunks::::get(page).ok_or(ActorControlTransitionError::Invariant)?; + let stored = chunk + .get_mut(slot) + .ok_or(ActorControlTransitionError::Invariant)?; + if stored.as_ref().map(|stored| stored.actor_id) != Some(actor_id) { + return Err(ActorControlTransitionError::Invariant); + } + *stored = None; + ActorReadyFrameChunks::::insert(page, chunk); + ActorReadyOccupancy::::try_mutate(|occupancy| { + *occupancy = occupancy + .checked_sub(1) + .ok_or(ActorControlTransitionError::Invariant)?; + Ok::<(), ActorControlTransitionError>(()) + })?; + } + ActorControlLocation::Waiting { key, page, slot } => { + let removed = Self::remove_waiting_entry(WakeupPointer { + block: key, + page_id: page, + slot: u32::from(slot), + })?; + if removed.primary().map(|stored| stored.actor_id) != Some(actor_id) { + return Err(ActorControlTransitionError::Invariant); + } + } + } + ActorControlLocators::::remove(actor_id); + Ok(cell) } - fn paged_consume_closed_head_at(position: QueueTicket) -> Result<(), EnqueueOutcome> { - Self::paged_consume_head_at_inner(position, true, None) + #[cfg(all(test, feature = "runtime-benchmarks"))] + fn control_finalize_underfunded_at_time( + actor_id: ActorId, + location: ActorControlLocation>, + cell: ActorControlCellOf, + identity: ActorIdentityOf, + contract: &ActorContractOf, + ) -> Result<(), ActorControlTransitionError> { + let ActorClass::User { owner_slot } = identity.actor_class else { + return Err(ActorControlTransitionError::Invariant); + }; + if location != ActorControlLocation::Unsignaled + || cell.actor_id != actor_id + || cell.hot.cycle_state != CycleState::Idle + || cell.hot.pending_signal + || cell.eligible_at.is_some() + || cell.hot.wakeup_pointer.is_some() + || cell.hot.trigger_wakeup_pointer.is_some() + || ActorIdentities::::contains_key(actor_id) + || !ActorFunding::::contains_key(actor_id) + || ActiveActorCount::::get() == 0 + || ActorIdentityCount::::get() == 0 + || SovereignIndex::::get(&identity.sovereign_account) != Some(actor_id) + || !Self::owner_slot_is_set(&OwnerSlotBitmaps::::get(&identity.owner), owner_slot) + { + return Err(ActorControlTransitionError::Invariant); + } + ActorUnsignaledControlCells::::remove(actor_id); + ActorControlLocators::::remove(actor_id); + if !Self::control_remove_frame_owned_contract_geometry(actor_id, contract) { + return Err(ActorControlTransitionError::Invariant); + } + ActorRunStateStore::::remove(actor_id); + ActorFunding::::remove(actor_id); + let active_count = ActiveActorCount::::get() + .checked_sub(1) + .ok_or(ActorControlTransitionError::Invariant)?; + let identity_count = ActorIdentityCount::::get() + .checked_sub(1) + .ok_or(ActorControlTransitionError::Invariant)?; + ActiveActorCount::::put(active_count); + ActorIdentityCount::::put(identity_count); + Self::remove_owner_slot_binding(&identity.owner, owner_slot, &identity.sovereign_account); + Self::reconcile_actor_state_hold_with_authority(actor_id) + .map_err(|_| ActorControlTransitionError::Invariant)?; + Self::deposit_event(Event::ActorClosed { + actor_id, + reason: CloseReason::TriggerAdmissionInsufficient, + }); + Ok(()) } - fn paged_consume_head_at_inner( - position: QueueTicket, - owner_was_closed: bool, - loaded_owner: Option<(ActorId, QueueTicket, ActorHotStateOf)>, - ) -> Result<(), EnqueueOutcome> { - with_transaction_opaque_err(|| { - let transition = || -> Result<(), EnqueueOutcome> { - let topology = Self::queue_topology_preflight(QueueMutation::Head)?; - if position != topology.head || topology.head >= topology.tail { - return Err(EnqueueOutcome::CorruptedTopology); + #[cfg(all(test, feature = "runtime-benchmarks"))] + pub(crate) fn control_latch_manual_occurrence( + actor_id: ActorId, + now: BlockNumberFor, + ) -> Result>>, ActorControlTransitionError> { + polkadot_sdk::frame_support::storage::with_transaction(|| { + let transition = || { + let (location, mut cell) = Self::load_primary_control_cell(actor_id)?; + if cell.hot.pending_signal { + return Ok(None); } - let (page_id, slot) = Self::queue_page_and_slot(topology.head); - let page = QueuePages::::get(page_id).ok_or(EnqueueOutcome::CorruptedTopology)?; - let entry = page - .get(slot) - .copied() - .ok_or(EnqueueOutcome::CorruptedTopology)?; - let actor_hot = if let Some((actor_id, ticket, hot)) = loaded_owner { - if owner_was_closed - || entry.actor_id != actor_id - || entry.ticket != ticket - || hot.queue_ticket != Some(ticket) - { - return Err(EnqueueOutcome::CorruptedTopology); - } - Some(hot) + if cell.hot.trigger_wakeup_pointer.is_some() { + return Err(ActorControlTransitionError::Invariant); + } + let idle_activation = location == ActorControlLocation::Unsignaled + && cell.hot.cycle_state == CycleState::Idle + && cell.eligible_at.is_none() + && cell.hot.wakeup_pointer.is_none(); + let busy_deferred = matches!( + cell.hot.cycle_state, + CycleState::Running | CycleState::Suspended + ) && cell.eligible_at.is_some() + && matches!( + location, + ActorControlLocation::Ready { .. } | ActorControlLocation::Waiting { .. } + ); + if !idle_activation && !busy_deferred { + return Err(ActorControlTransitionError::Invariant); + } + let (identity, _, admission) = Self::project_control_cell(&cell, location) + .ok_or(ActorControlTransitionError::Invariant)?; + let (contract, _, _) = Self::control_load_current_step_contract(actor_id, &admission, 0) + .ok_or(ActorControlTransitionError::Invariant)?; + if !matches!(contract.trigger, Trigger::Manual) + || !matches!( + cell.hot.trigger_runtime_state, + TriggerRuntimeState::Stateless + ) + { + return Err(ActorControlTransitionError::Invariant); + } + let actor_type = identity.actor_class.actor_type(); + let breakdown = Self::trigger_fee_for_weight( + actor_type, + TriggerFamily::Manual, + T::WeightInfo::manual_trigger(), + ); + if !Self::trigger_occurrence_capacity_sufficient( + actor_type, + &identity.sovereign_account, + breakdown, + ) + .map_err(|_| ActorControlTransitionError::Invariant)? + { + return Err(ActorControlTransitionError::Invariant); + } + if !Self::try_charge_prechecked_automatic_trigger_occurrence( + actor_type, + &identity.sovereign_account, + breakdown, + ) + .map_err(|_| ActorControlTransitionError::Invariant)? + { + return Err(ActorControlTransitionError::Invariant); + } + cell.hot.pending_signal = true; + let destination = if busy_deferred { + Self::store_primary_control_cell(location, cell)?; + location } else { - 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); - } - Some(state.hot) - } + let eligible_at = now + .checked_add(&One::one()) + .ok_or(ActorControlTransitionError::IndexExhausted)?; + cell.eligible_at = Some(eligible_at); + Self::remove_primary_control_cell_inner(actor_id) + .map_err(|_| ActorControlTransitionError::Invariant)?; + let destination = Self::control_append_waiting( + cell, + WakeupKey::Block(eligible_at), + ActorWaitingAuthority::Service, + )?; + destination }; - let next_head = topology - .head - .checked_add(1) - .ok_or(EnqueueOutcome::SchedulerIndexExhausted)?; - let next_occupancy = topology - .occupancy - .checked_sub(1) - .ok_or(EnqueueOutcome::CorruptedTopology)?; - let page_size = Self::queue_page_size(); - if next_head == topology.tail { - let remainder = next_head % page_size; - let aligned = if remainder == 0 { - next_head + Self::deposit_event(Event::TriggerOccurrenceProcessed { + actor_id, + trigger_family: breakdown.trigger_family, + fee: breakdown.trigger_fee, + }); + Self::deposit_event(Event::ManualTriggerSet { actor_id }); + Ok(Some(destination)) + }; + match transition() { + Ok(output) => polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(output)), + Err(error) => { + polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)) + } + } + }) + } + + #[cfg(all(test, feature = "runtime-benchmarks"))] + pub(crate) fn control_apply_address_event( + actor_id: ActorId, + asset: T::AssetId, + amount: T::Balance, + source: Option<&T::AccountId>, + provenance: Option<&FundingProvenance>, + now: BlockNumberFor, + ) -> Result>>, ActorControlTransitionError> { + polkadot_sdk::frame_support::storage::with_transaction(|| { + let transition = || { + if amount.is_zero() { + return Ok(None); + } + let (location, mut cell) = Self::load_primary_control_cell(actor_id)?; + let (identity, _, admission) = Self::project_control_cell(&cell, location) + .ok_or(ActorControlTransitionError::Invariant)?; + let (contract, _, contract_head) = + Self::control_load_current_step_contract(actor_id, &admission, 0) + .ok_or(ActorControlTransitionError::Invariant)?; + let mut funding = + ActorFunding::::get(actor_id).ok_or(ActorControlTransitionError::Invariant)?; + let funding_authorized = Self::funding_event_authorized( + actor_id, + &identity.owner, + &contract.funding, + source, + provenance, + ); + if funding_authorized && funding.funding_tracked_assets.contains(&asset) { + let accumulated = if let Some(accumulated) = funding.funding_accumulated.get_mut(&asset) { + *accumulated = accumulated + .checked_add(&amount) + .ok_or(ActorControlTransitionError::Invariant)?; + *accumulated } else { - let distance = page_size - .checked_sub(remainder) - .ok_or(EnqueueOutcome::CorruptedTopology)?; - next_head - .checked_add(distance) - .ok_or(EnqueueOutcome::SchedulerIndexExhausted)? + funding + .funding_accumulated + .try_insert(asset, amount) + .map_err(|_| ActorControlTransitionError::Invariant)?; + amount }; - QueuePages::::remove(page_id); - QueueHead::::put(aligned); - QueueTail::::put(aligned); + Self::ensure_funding_state_hold_capacity(actor_id, &identity, &funding) + .map_err(|_| ActorControlTransitionError::Invariant)?; + ActorFunding::::insert(actor_id, &funding); + Self::control_reconcile_single_step_state_hold(actor_id, &cell, &contract_head, &funding) + .map_err(|_| ActorControlTransitionError::Invariant)?; + Self::deposit_event(Event::FundingAccumulated { + actor_id, + asset, + added: amount, + accumulated, + }); + } + let signal_matched = if !cell.hot.pending_signal + && let Trigger::AddressEvent { + source_filter, + asset_filter, + } = &contract.trigger + { + Self::source_matches_filter(source_filter, &identity.owner, source) + && Self::asset_matches_filter(asset_filter, asset) + } else { + false + }; + if !signal_matched { + return Ok(None); + } + if cell.hot.trigger_wakeup_pointer.is_some() { + return Err(ActorControlTransitionError::Invariant); + } + let idle_activation = location == ActorControlLocation::Unsignaled + && cell.hot.cycle_state == CycleState::Idle + && cell.eligible_at.is_none() + && cell.hot.wakeup_pointer.is_none(); + let busy_deferred = matches!( + cell.hot.cycle_state, + CycleState::Running | CycleState::Suspended + ) && cell.eligible_at.is_some() + && matches!( + location, + ActorControlLocation::Ready { .. } | ActorControlLocation::Waiting { .. } + ); + if !idle_activation && !busy_deferred { + return Err(ActorControlTransitionError::Invariant); + } + let actor_type = identity.actor_class.actor_type(); + let breakdown = Self::trigger_fee_for_weight( + actor_type, + TriggerFamily::AddressEvent, + T::WeightInfo::address_event_trigger_occurrence(), + ); + if !Self::try_charge_automatic_trigger_occurrence( + actor_type, + &identity.sovereign_account, + breakdown, + ) + .map_err(|_| ActorControlTransitionError::Invariant)? + { + return Ok(None); + } + cell.hot.pending_signal = true; + let destination = if busy_deferred { + Self::store_primary_control_cell(location, cell)?; + location + } else { + let eligible_at = now + .checked_add(&One::one()) + .ok_or(ActorControlTransitionError::IndexExhausted)?; + cell.eligible_at = Some(eligible_at); + Self::remove_primary_control_cell_inner(actor_id) + .map_err(|_| ActorControlTransitionError::Invariant)?; + let destination = Self::control_append_waiting( + cell, + WakeupKey::Block(eligible_at), + ActorWaitingAuthority::Service, + )?; + destination + }; + Self::deposit_event(Event::TriggerOccurrenceProcessed { + actor_id, + trigger_family: breakdown.trigger_family, + fee: breakdown.trigger_fee, + }); + Ok(Some(destination)) + }; + match transition() { + Ok(output) => polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(output)), + Err(error) => { + polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)) + } + } + }) + } + + #[cfg(all(test, feature = "runtime-benchmarks"))] + pub(crate) fn control_latch_observation_change_occurrence( + actor_id: ActorId, + feed: T::ObservationFeedId, + now: BlockNumberFor, + ) -> Result>>, ActorControlTransitionError> { + polkadot_sdk::frame_support::storage::with_transaction(|| { + let transition = || { + let (location, mut cell) = Self::load_primary_control_cell(actor_id)?; + let detector_disabled = IndexedTriggerDetectionDisabled::::contains_key(actor_id); + if cell.hot.pending_signal || detector_disabled { + if cell.hot.pending_signal != detector_disabled { + return Err(ActorControlTransitionError::Invariant); + } + return Ok(None); + } + if cell.hot.trigger_wakeup_pointer.is_some() { + return Err(ActorControlTransitionError::Invariant); + } + let idle_activation = location == ActorControlLocation::Unsignaled + && cell.hot.cycle_state == CycleState::Idle + && cell.eligible_at.is_none() + && cell.hot.wakeup_pointer.is_none(); + let busy_deferred = matches!( + cell.hot.cycle_state, + CycleState::Running | CycleState::Suspended + ) && cell.eligible_at.is_some() + && matches!( + location, + ActorControlLocation::Ready { .. } | ActorControlLocation::Waiting { .. } + ); + if !idle_activation && !busy_deferred { + return Err(ActorControlTransitionError::Invariant); + } + let (identity, _, admission) = Self::project_control_cell(&cell, location) + .ok_or(ActorControlTransitionError::Invariant)?; + let (contract, _, _) = Self::control_load_current_step_contract(actor_id, &admission, 0) + .ok_or(ActorControlTransitionError::Invariant)?; + if !matches!( + contract.trigger, + Trigger::ObservationChange { feed: contract_feed } if contract_feed == feed + ) || !matches!( + cell.hot.trigger_runtime_state, + TriggerRuntimeState::Stateless + ) || ActorObservationFeeds::::get(actor_id) + .is_none_or(|feeds| feeds.as_slice() != [feed]) + || !ObservationSubscriptionSlot::::contains_key(actor_id) + { + return Err(ActorControlTransitionError::Invariant); + } + let actor_type = identity.actor_class.actor_type(); + let breakdown = Self::trigger_fee_for_weight( + actor_type, + TriggerFamily::ObservationChange, + T::WeightInfo::observation_change_trigger_occurrence(), + ); + if !Self::try_charge_automatic_trigger_occurrence( + actor_type, + &identity.sovereign_account, + breakdown, + ) + .map_err(|_| ActorControlTransitionError::Invariant)? + { + return Ok(None); + } + cell.hot.pending_signal = true; + let destination = if busy_deferred { + Self::store_primary_control_cell(location, cell)?; + location } else { - QueueHead::::put(next_head); - if next_head.is_multiple_of(page_size) { - QueuePages::::remove(page_id); + let eligible_at = now + .checked_add(&One::one()) + .ok_or(ActorControlTransitionError::IndexExhausted)?; + cell.eligible_at = Some(eligible_at); + Self::remove_primary_control_cell_inner(actor_id) + .map_err(|_| ActorControlTransitionError::Invariant)?; + let destination = Self::control_append_waiting( + cell, + WakeupKey::Block(eligible_at), + ActorWaitingAuthority::Service, + )?; + destination + }; + IndexedTriggerDetectionDisabled::::insert(actor_id, ()); + Self::deposit_event(Event::TriggerOccurrenceProcessed { + actor_id, + trigger_family: breakdown.trigger_family, + fee: breakdown.trigger_fee, + }); + Ok(Some(destination)) + }; + match transition() { + Ok(output) => polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(output)), + Err(error) => { + polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)) + } + } + }) + } + + #[cfg(all(test, feature = "runtime-benchmarks"))] + pub(crate) fn control_latch_observation_crossing_fire( + actor_id: ActorId, + transition: crate::ObservationTransition, + now: BlockNumberFor, + ) -> Result>>, ActorControlTransitionError> { + polkadot_sdk::frame_support::storage::with_transaction(|| { + let transition_result = || { + let (location, mut cell) = Self::load_primary_control_cell(actor_id)?; + let detector_disabled = IndexedTriggerDetectionDisabled::::contains_key(actor_id); + if cell.hot.pending_signal || detector_disabled { + if cell.hot.pending_signal != detector_disabled { + return Err(ActorControlTransitionError::Invariant); } + return Ok(None); + } + if cell.hot.trigger_wakeup_pointer.is_some() { + return Err(ActorControlTransitionError::Invariant); + } + let idle_activation = location == ActorControlLocation::Unsignaled + && cell.hot.cycle_state == CycleState::Idle + && cell.eligible_at.is_none() + && cell.hot.wakeup_pointer.is_none(); + let busy_deferred = matches!( + cell.hot.cycle_state, + CycleState::Running | CycleState::Suspended + ) && cell.eligible_at.is_some() + && matches!( + location, + ActorControlLocation::Ready { .. } | ActorControlLocation::Waiting { .. } + ); + if !idle_activation && !busy_deferred { + return Err(ActorControlTransitionError::Invariant); + } + let (identity, _, admission) = Self::project_control_cell(&cell, location) + .ok_or(ActorControlTransitionError::Invariant)?; + let (contract, _, _) = Self::control_load_current_step_contract(actor_id, &admission, 0) + .ok_or(ActorControlTransitionError::Invariant)?; + let crossing = Self::crossing_from_trigger(&contract.trigger) + .ok_or(ActorControlTransitionError::Invariant)?; + let TriggerRuntimeState::ObservationCrossing { + phase, + installed_at_revision, + } = cell.hot.trigger_runtime_state + else { + return Err(ActorControlTransitionError::Invariant); + }; + let previous = transition + .previous + .ok_or(ActorControlTransitionError::Invariant)?; + if installed_at_revision >= transition.revision + || crossing.transition(phase, previous, transition.current) + != crate::CrossingTransition::Fire + { + return Err(ActorControlTransitionError::Invariant); } - QueueOccupancy::::put(next_occupancy); - if let Some(mut hot) = actor_hot { - hot.queue_ticket = None; - ActorHot::::insert(entry.actor_id, hot); + let locator = + CrossingMemberships::::get(actor_id).ok_or(ActorControlTransitionError::Invariant)?; + Self::control_move_crossing_membership_without_hot( + actor_id, + crossing, + CrossingPhase::WaitingForRearm, + locator, + identity.actor_class.actor_type(), + ) + .map_err(|_| ActorControlTransitionError::Invariant)?; + cell.hot.trigger_runtime_state = TriggerRuntimeState::ObservationCrossing { + phase: CrossingPhase::WaitingForRearm, + installed_at_revision, + }; + 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(), + ); + if !Self::try_charge_automatic_trigger_occurrence( + actor_type, + &identity.sovereign_account, + breakdown, + ) + .map_err(|_| ActorControlTransitionError::Invariant)? + { + Self::store_primary_control_cell(location, cell)?; + return Ok(None); } - Ok(()) + cell.hot.pending_signal = true; + let destination = if busy_deferred { + Self::store_primary_control_cell(location, cell)?; + location + } else { + let eligible_at = now + .checked_add(&One::one()) + .ok_or(ActorControlTransitionError::IndexExhausted)?; + cell.eligible_at = Some(eligible_at); + Self::remove_primary_control_cell_inner(actor_id) + .map_err(|_| ActorControlTransitionError::Invariant)?; + let destination = Self::control_append_waiting( + cell, + WakeupKey::Block(eligible_at), + ActorWaitingAuthority::Service, + )?; + destination + }; + IndexedTriggerDetectionDisabled::::insert(actor_id, ()); + Self::deposit_event(Event::TriggerOccurrenceProcessed { + actor_id, + trigger_family: breakdown.trigger_family, + fee: breakdown.trigger_fee, + }); + Ok(Some(destination)) }; - match transition() { - Ok(()) => polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(())), + match transition_result() { + Ok(output) => polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(output)), Err(error) => { polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)) } } }) - .map_err(|_| EnqueueOutcome::CorruptedTopology)? } - pub fn paged_consume_head(ticket: QueueTicket) -> bool { - let Some((position, entry)) = Self::paged_head_entry() else { - return false; - }; - entry.ticket == ticket && Self::paged_consume_head_at(position).is_ok() + #[cfg(all(test, feature = "runtime-benchmarks"))] + pub(crate) fn control_apply_observation_crossing_rearm( + actor_id: ActorId, + transition: crate::ObservationTransition, + ) -> Result<(), ActorControlTransitionError> { + polkadot_sdk::frame_support::storage::with_transaction(|| { + let transition_result = || { + let (location, mut cell) = Self::load_primary_control_cell(actor_id)?; + if cell.hot.pending_signal + || IndexedTriggerDetectionDisabled::::contains_key(actor_id) + || cell.hot.trigger_wakeup_pointer.is_some() + { + return Err(ActorControlTransitionError::Invariant); + } + let (identity, _, admission) = Self::project_control_cell(&cell, location) + .ok_or(ActorControlTransitionError::Invariant)?; + let (contract, _, _) = Self::control_load_current_step_contract(actor_id, &admission, 0) + .ok_or(ActorControlTransitionError::Invariant)?; + let crossing = Self::crossing_from_trigger(&contract.trigger) + .ok_or(ActorControlTransitionError::Invariant)?; + let TriggerRuntimeState::ObservationCrossing { + phase, + installed_at_revision, + } = cell.hot.trigger_runtime_state + else { + return Err(ActorControlTransitionError::Invariant); + }; + let previous = transition + .previous + .ok_or(ActorControlTransitionError::Invariant)?; + if installed_at_revision >= transition.revision + || crossing.transition(phase, previous, transition.current) + != crate::CrossingTransition::Rearm + { + return Err(ActorControlTransitionError::Invariant); + } + let locator = + CrossingMemberships::::get(actor_id).ok_or(ActorControlTransitionError::Invariant)?; + Self::control_move_crossing_membership_without_hot( + actor_id, + crossing, + CrossingPhase::Armed, + locator, + identity.actor_class.actor_type(), + ) + .map_err(|_| ActorControlTransitionError::Invariant)?; + cell.hot.trigger_runtime_state = TriggerRuntimeState::ObservationCrossing { + phase: CrossingPhase::Armed, + installed_at_revision, + }; + Self::store_primary_control_cell(location, cell) + }; + match transition_result() { + Ok(()) => polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(())), + Err(error) => { + polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)) + } + } + }) } - pub fn paged_drain_tombstones( - cutoff: QueueTicket, - scan_limit: u32, - ) -> Result { - let outcome = with_transaction_opaque_err(|| { - let corrupt = || { - polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err( - EnqueueOutcome::CorruptedTopology, - )) + #[cfg(all(test, feature = "runtime-benchmarks"))] + pub(crate) fn control_latch_observation_change_page( + feed: T::ObservationFeedId, + page: u32, + now: BlockNumberFor, + ) -> Result< + Vec<(ActorId, Option>>)>, + ActorControlTransitionError, + > { + polkadot_sdk::frame_support::storage::with_transaction(|| { + let transition = || { + let list = ObservationSubscriberPageLists::::get(feed) + .ok_or(ActorControlTransitionError::Invariant)?; + let subscriber_page = ObservationSubscriberPages::::get(feed, page) + .ok_or(ActorControlTransitionError::Invariant)?; + if list.count == 0 + || (subscriber_page.previous.is_none() && list.head != page) + || (subscriber_page.next.is_none() && list.tail != page) + { + return Err(ActorControlTransitionError::Invariant); + } + let mut outcomes = Vec::new(); + for maybe_actor_id in subscriber_page.entries.iter(/* deos-bypass: bounded-iter */) { + let Some(actor_id) = maybe_actor_id else { + continue; + }; + let outcome = Self::control_latch_observation_change_occurrence(*actor_id, feed, now)?; + outcomes.push((*actor_id, outcome)); + } + Ok(outcomes) }; - let mut stats = QueueDrainStats::default(); - if scan_limit == 0 { - return polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(stats)); + match transition() { + Ok(output) => polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(output)), + Err(error) => { + polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)) + } } - let Ok(topology) = Self::queue_topology_preflight(QueueMutation::Head) else { - return corrupt(); - }; - let original_head = topology.head; - let tail = topology.tail; - let occupancy = topology.occupancy; - let next_ticket = NextQueueTicket::::get(); - let page_size = Self::queue_page_size(); - let page_size_usize = page_size as usize; - let mut head = original_head; - let mut last_ticket = None; - let mut pages_to_delete: Vec = Vec::new(); - - 'pages: while head < tail && stats.entries_scanned < scan_limit { - let (page_id, mut slot) = Self::queue_page_and_slot(head); - let Some(page) = QueuePages::::get(page_id) else { - return corrupt(); - }; - if page.is_empty() || page.len() > page_size_usize || slot >= page.len() { - return corrupt(); + }) + } + + #[cfg(all(test, feature = "runtime-benchmarks"))] + pub(crate) fn control_latch_due_temporal_reference( + key: WakeupKey>, + now: BlockNumberFor, + now_tick: SchedulerTick, + ) -> Result<(ActorId, ActorControlLocation>), ActorControlTransitionError> { + polkadot_sdk::frame_support::storage::with_transaction(|| { + let transition = || { + if !matches!(key, WakeupKey::Tick(_)) { + return Err(ActorControlTransitionError::Invariant); + } + let (location, mut cell) = Self::control_due_wakeup_primary(key, now, now_tick)?; + let idle_activation = location == ActorControlLocation::Unsignaled + && cell.hot.cycle_state == CycleState::Idle + && !cell.hot.pending_signal + && cell.eligible_at.is_none() + && cell.hot.wakeup_pointer.is_none(); + let busy_deferred = matches!( + cell.hot.cycle_state, + CycleState::Running | CycleState::Suspended + ) && !cell.hot.pending_signal + && cell.eligible_at.is_some() + && matches!( + location, + ActorControlLocation::Ready { .. } | ActorControlLocation::Waiting { .. } + ); + if !idle_activation && !busy_deferred { + return Err(ActorControlTransitionError::Invariant); } - let Some(pages_touched) = stats.pages_touched.checked_add(1) else { - return corrupt(); + let actor_id = cell.actor_id; + let (identity, _, admission) = Self::project_control_cell(&cell, location) + .ok_or(ActorControlTransitionError::Invariant)?; + let (contract, _) = Self::control_load_frame_contract(actor_id, &admission) + .ok_or(ActorControlTransitionError::Invariant)?; + let (trigger_family, occurrence_weight) = match &contract.trigger { + Trigger::AtTime { .. } => ( + TriggerFamily::AtTime, + T::WeightInfo::at_time_trigger_occurrence(), + ), + Trigger::Cadenced { .. } => ( + TriggerFamily::Cadenced, + T::WeightInfo::cadenced_trigger_occurrence(), + ), + _ => return Err(ActorControlTransitionError::Invariant), + }; + cell.hot.trigger_runtime_state = match (trigger_family, cell.hot.trigger_runtime_state) { + ( + TriggerFamily::AtTime, + TriggerRuntimeState::AtTime { + anchor_tick: Some(anchor_tick), + consumed: false, + }, + ) if idle_activation => TriggerRuntimeState::AtTime { + anchor_tick: Some(anchor_tick), + consumed: true, + }, + ( + TriggerFamily::Cadenced, + state @ TriggerRuntimeState::Cadenced { + anchor_tick: Some(_), + }, + ) => state, + _ => return Err(ActorControlTransitionError::Invariant), }; - stats.pages_touched = pages_touched; - while head < tail && stats.entries_scanned < scan_limit && slot < page.len() { - let entry = page[slot]; - if entry.ticket >= next_ticket - || last_ticket.is_some_and(|previous| entry.ticket <= previous) + let actor_type = identity.actor_class.actor_type(); + let breakdown = Self::trigger_fee_for_weight(actor_type, trigger_family, occurrence_weight); + let charged = if trigger_family == TriggerFamily::AtTime { + if !Self::trigger_occurrence_capacity_sufficient( + actor_type, + &identity.sovereign_account, + breakdown, + ) + .map_err(|_| ActorControlTransitionError::Invariant)? { - return corrupt(); - } - last_ticket = Some(entry.ticket); - if entry.ticket >= cutoff { - break 'pages; - } - let Some(entries_scanned) = stats.entries_scanned.checked_add(1) else { - return corrupt(); - }; - stats.entries_scanned = entries_scanned; - 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 cell = Self::control_consume_due_wakeup_reference(cell, key, now, now_tick)?; + Self::control_finalize_underfunded_at_time( + actor_id, location, cell, identity, &contract, + )?; + return Ok((actor_id, location)); } - let Some(tombstones_skipped) = stats.tombstones_skipped.checked_add(1) else { - return corrupt(); - }; - let Some(next_head) = head.checked_add(1) else { - return corrupt(); - }; - let Some(next_slot) = slot.checked_add(1) else { - return corrupt(); - }; - stats.tombstones_skipped = tombstones_skipped; - head = next_head; - slot = next_slot; + Self::try_charge_prechecked_automatic_trigger_occurrence( + actor_type, + &identity.sovereign_account, + breakdown, + ) + } else { + Self::try_charge_automatic_trigger_occurrence( + actor_type, + &identity.sovereign_account, + breakdown, + ) } - if slot == page.len() { - if head < tail && (page.len() != page_size_usize || !head.is_multiple_of(page_size)) { - return corrupt(); + .map_err(|_| ActorControlTransitionError::Invariant)?; + if !charged { + if trigger_family != TriggerFamily::Cadenced { + return Err(ActorControlTransitionError::Invariant); } - pages_to_delete.push(page_id); - let Some(pages_deleted) = stats.pages_deleted.checked_add(1) else { - return corrupt(); + let Trigger::Cadenced { every_ticks } = contract.trigger else { + return Err(ActorControlTransitionError::Invariant); }; - stats.pages_deleted = pages_deleted; - } else { - break; + let anchor_tick = cell + .hot + .trigger_runtime_state + .temporal_anchor_tick() + .ok_or(ActorControlTransitionError::Invariant)?; + let next_due_tick = next_cadence_due_tick(anchor_tick, every_ticks, now_tick) + .ok_or(ActorControlTransitionError::Invariant)?; + let cell = Self::control_consume_due_wakeup_reference(cell, key, now, now_tick)?; + let cell = + Self::control_schedule_fresh_wakeup_reference(cell, WakeupKey::Tick(next_due_tick))?; + Self::store_primary_control_cell(location, cell)?; + return Ok((actor_id, location)); } - } - - if head == original_head { - return polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(stats)); - } - let Some(next_occupancy) = occupancy.checked_sub(stats.tombstones_skipped) else { - return corrupt(); - }; - let (next_head, next_tail) = if head == tail { - let remainder = tail % page_size; - let Some(aligned) = (if remainder == 0 { - Some(tail) + let mut cell = Self::control_consume_due_wakeup_reference(cell, key, now, now_tick)?; + cell.hot.pending_signal = true; + let destination = if busy_deferred { + Self::store_primary_control_cell(location, cell)?; + location } else { - tail.checked_add(page_size - remainder) - }) else { - return corrupt(); + let eligible_at = now + .checked_add(&One::one()) + .ok_or(ActorControlTransitionError::IndexExhausted)?; + cell.eligible_at = Some(eligible_at); + Self::remove_primary_control_cell_inner(actor_id) + .map_err(|_| ActorControlTransitionError::Invariant)?; + let destination = Self::control_append_waiting( + cell, + WakeupKey::Block(eligible_at), + ActorWaitingAuthority::Service, + )?; + destination }; - (aligned, aligned) - } else { - (head, tail) - }; - for page_id in pages_to_delete { - QueuePages::::remove(page_id); - } - QueueHead::::put(next_head); - QueueTail::::put(next_tail); - QueueOccupancy::::put(next_occupancy); - polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(stats)) - }); - outcome - .map_err(|_| EnqueueOutcome::CorruptedTopology)? - .map_err(|_| EnqueueOutcome::CorruptedTopology) - } - - pub(crate) fn wakeup_page_entry_matches( - pointer: WakeupPointer>, - actor_id: ActorId, - ) -> bool { - WakeupPages::::get((pointer.block, pointer.page_id)) - .and_then(|page| page.entries.get(pointer.slot as usize).copied().flatten()) - .is_some_and(|entry| entry.actor_id == actor_id) - } - - 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 Some((mut state, _, _)) = Self::load_actor_service_state(actor_id) else { - return Err(EnqueueOutcome::CorruptedTopology); - }; - let Some(pointer) = Self::wakeup_pointer_for_clock(&state.hot, clock) else { - return Ok(None); - }; - let key = (pointer.block, pointer.page_id); - let Some(mut page) = WakeupPages::::get(key) else { - return Err(EnqueueOutcome::CorruptedTopology); - }; - let Some(slot) = page.entries.get(pointer.slot as usize) else { - return Err(EnqueueOutcome::CorruptedTopology); - }; - if !slot.is_some_and(|entry| entry.actor_id == actor_id) { - return Err(EnqueueOutcome::CorruptedTopology); - } - let physical_live = Self::wakeup_page_physical_live(&page); - if physical_live != page.live_entries as usize { - return Err(EnqueueOutcome::CorruptedTopology); - } - let Some(next_page_live) = page.live_entries.checked_sub(1) else { - return Err(EnqueueOutcome::CorruptedTopology); - }; - let Some(mut bucket) = WakeupBuckets::::get(pointer.block) else { - return Err(EnqueueOutcome::CorruptedTopology); - }; - let Some(cursor_index) = bucket.cursor_index else { - return Err(EnqueueOutcome::CorruptedTopology); - }; - if Self::wakeup_cursor_get(pointer.block.clock(), cursor_index) != Some(pointer.block) { - return Err(EnqueueOutcome::CorruptedTopology); - } - let Some(next_bucket_live) = bucket.live_entries.checked_sub(1) else { - return Err(EnqueueOutcome::CorruptedTopology); - }; - if page.previous_page.is_none() != (bucket.head_page == pointer.page_id) - || page.next_page.is_none() != (bucket.tail_page == pointer.page_id) - { - return Err(EnqueueOutcome::CorruptedTopology); - } - let previous = if let Some(previous_page) = page.previous_page { - let Some(previous) = WakeupPages::::get((pointer.block, previous_page)) else { - return Err(EnqueueOutcome::CorruptedTopology); - }; - if previous.next_page != Some(pointer.page_id) { - return Err(EnqueueOutcome::CorruptedTopology); - } - Some((previous_page, previous)) - } else { - None - }; - let next = if let Some(next_page) = page.next_page { - let Some(next) = WakeupPages::::get((pointer.block, next_page)) else { - return Err(EnqueueOutcome::CorruptedTopology); + Self::deposit_event(Event::TriggerOccurrenceProcessed { + actor_id, + trigger_family: breakdown.trigger_family, + fee: breakdown.trigger_fee, + }); + Ok((actor_id, destination)) }; - if next.previous_page != Some(pointer.page_id) { - return Err(EnqueueOutcome::CorruptedTopology); - } - Some((next_page, next)) - } else { - None - }; - if next_page_live == 0 && ((next_bucket_live == 0) != (previous.is_none() && next.is_none())) { - return Err(EnqueueOutcome::CorruptedTopology); - } - - page.entries[pointer.slot as usize] = None; - page.live_entries = next_page_live; - bucket.live_entries = next_bucket_live; - 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); - WakeupBuckets::::insert(pointer.block, bucket); - return Ok(Some(pointer)); - } - - if let Some((previous_page, mut previous)) = previous { - previous.next_page = page.next_page; - WakeupPages::::insert((pointer.block, previous_page), previous); - } else if let Some(next_page) = page.next_page { - bucket.head_page = next_page; - } - if let Some((next_page, mut next)) = next { - next.previous_page = page.previous_page; - WakeupPages::::insert((pointer.block, next_page), next); - } else if let Some(previous_page) = page.previous_page { - bucket.tail_page = previous_page; - } - WakeupPages::::remove(key); - if bucket.live_entries == 0 { - if page.previous_page.is_some() || page.next_page.is_some() { - return Err(EnqueueOutcome::CorruptedTopology); - } - if !Self::wakeup_cursor_remove_inner(pointer.block) { - return Err(EnqueueOutcome::CorruptedTopology); + match transition() { + Ok(output) => polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(output)), + Err(error) => { + polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)) + } } - WakeupBuckets::::remove(pointer.block); - } else { - WakeupBuckets::::insert(pointer.block, bucket); - } - 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) + }) } + #[cfg(any(test, feature = "runtime-benchmarks"))] pub(crate) fn trigger_wakeup_substrate_invalidate_inner( actor_id: ActorId, ) -> Result>>, EnqueueOutcome> { Self::wakeup_substrate_invalidate_clock_inner(actor_id, WakeupClock::Tick) } + #[cfg(any(test, feature = "runtime-benchmarks"))] pub fn wakeup_substrate_invalidate( actor_id: ActorId, ) -> Option>> { let result: Result>, DispatchError> = polkadot_sdk::frame_support::storage::with_transaction(|| { - match Self::wakeup_substrate_invalidate_inner(actor_id) { + match Self::wakeup_substrate_invalidate_clock_inner(actor_id, WakeupClock::Block) { Ok(Some(pointer)) => { polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(pointer)) } @@ -2620,6 +6217,7 @@ impl Pallet { result.ok() } + #[cfg(any(test, feature = "runtime-benchmarks"))] fn wakeup_substrate_schedule_inner( actor_id: ActorId, wakeup_key: WakeupKey>, @@ -2638,12 +6236,13 @@ impl Pallet { Self::try_wakeup_substrate_schedule_key_inner(actor_id, WakeupKey::Block(wakeup_block)) } + #[cfg(any(test, feature = "runtime-benchmarks"))] fn try_wakeup_substrate_schedule_key_inner( actor_id: ActorId, wakeup_key: WakeupKey>, ) -> Result<(), EnqueueOutcome> { with_transaction_opaque_err(|| { - match Self::try_wakeup_substrate_schedule_transition(actor_id, wakeup_key) { + match Self::schedule_retained_wakeup_transition(actor_id, wakeup_key) { Ok(()) => polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(())), Err(error) => { polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)) @@ -2653,151 +6252,227 @@ impl Pallet { .map_err(|_| EnqueueOutcome::CorruptedTopology)? } - fn try_wakeup_substrate_schedule_transition( + #[cfg(any(test, feature = "runtime-benchmarks"))] + fn schedule_retained_wakeup_transition( actor_id: ActorId, wakeup_block: WakeupKey>, ) -> Result<(), EnqueueOutcome> { - let Some((state, _, _)) = Self::load_actor_service_state(actor_id) else { + let Some((state, admission, loaded_step)) = Self::load_frame_actor_service_state(actor_id) + else { return Err(EnqueueOutcome::CorruptedTopology); }; - Self::try_wakeup_substrate_schedule_transition_loaded(actor_id, wakeup_block, state.hot) + let resources = if state.contract.steps.is_empty() { + ActorStepResourceEnvelope { + control: T::WeightInfo::scheduler_inner_zero_step_complete(), + effect: Weight::zero(), + } + } else { + loaded_step + .ok_or(EnqueueOutcome::CorruptedTopology)? + .resources + }; + Self::try_wakeup_substrate_schedule_transition_with_authority( + actor_id, + wakeup_block, + state.hot, + &state.identity, + state.run_state.as_ref().map_or(0, |run| run.cursor), + &admission, + resources, + ) + } + + fn publish_waiting_from_authority( + actor_id: ActorId, + wakeup_key: WakeupKey>, + hot: ActorHotStateOf, + identity: &ActorIdentityOf, + cursor: u32, + admission: &ActorAdmissionCertificateOf, + resources: ActorStepResourceEnvelope, + ) -> Result<(), EnqueueOutcome> { + if ActorControlLocators::::contains_key(actor_id) { + return Err(EnqueueOutcome::AlreadyLive); + } + let (authority, eligible_at) = match wakeup_key { + WakeupKey::Block(block) => (ActorWaitingAuthority::Service, Some(block)), + WakeupKey::Tick(_) => (ActorWaitingAuthority::Trigger, None), + }; + let cell = ActorControlCell { + actor_id, + identity: Self::control_identity_from_scalar(identity.clone()) + .ok_or(EnqueueOutcome::CorruptedTopology)?, + hot: Self::control_hot_from_scalar(hot), + cursor, + eligible_at, + admission: admission.clone(), + resources, + }; + Self::control_append_waiting(cell, wakeup_key, authority) + .map(|_| ()) + .map_err(|_| EnqueueOutcome::CorruptedTopology) } - fn try_wakeup_substrate_schedule_transition_loaded( + pub(crate) fn try_wakeup_substrate_schedule_transition_with_authority( actor_id: ActorId, - wakeup_block: WakeupKey>, + wakeup_key: WakeupKey>, + hot: ActorHotStateOf, + identity: &ActorIdentityOf, + cursor: u32, + admission: &ActorAdmissionCertificateOf, + resources: ActorStepResourceEnvelope, + ) -> Result<(), EnqueueOutcome> { + with_transaction_opaque_err(|| { + match Self::schedule_wakeup_transition_with_authority_inner( + actor_id, wakeup_key, hot, identity, cursor, admission, resources, + ) { + Ok(()) => polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(())), + Err(error) => { + polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)) + } + } + }) + .map_err(|_| EnqueueOutcome::CorruptedTopology)? + } + + fn schedule_wakeup_transition_with_authority_inner( + actor_id: ActorId, + wakeup_key: WakeupKey>, mut hot: ActorHotStateOf, + identity: &ActorIdentityOf, + cursor: u32, + admission: &ActorAdmissionCertificateOf, + resources: ActorStepResourceEnvelope, ) -> Result<(), EnqueueOutcome> { - #[cfg(test)] - if FAIL_WAKEUP_PLACEMENT_WITH_CAPACITY.with(|flag| flag.replace(false)) { - return Err(EnqueueOutcome::WakeupCapacityExhausted); - } - let clock = wakeup_block.clock(); + let clock = wakeup_key.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); + if pointer.block == wakeup_key && Self::wakeup_page_entry_matches(pointer, actor_id) { + if let Some(location) = ActorControlLocators::::get(actor_id) { + let (_, mut source) = Self::load_primary_control_cell(actor_id) + .map_err(|_| EnqueueOutcome::CorruptedTopology)?; + if source.admission != *admission { + return Err(EnqueueOutcome::CorruptedTopology); + } + source.identity = Self::control_identity_from_scalar(identity.clone()) + .ok_or(EnqueueOutcome::CorruptedTopology)?; + source.hot = Self::control_hot_from_scalar(hot); + source.cursor = cursor; + source.resources = resources; + return Self::store_primary_control_cell(location, source) + .map_err(|_| EnqueueOutcome::CorruptedTopology); + } + return Self::publish_waiting_from_authority( + actor_id, wakeup_key, hot, identity, cursor, admission, resources, + ); } - Self::wakeup_substrate_invalidate_clock_inner(actor_id, clock)?; + Self::invalidate_wakeup_reference(actor_id, pointer, admission.admission_identity)?; Self::clear_wakeup_pointer_for_clock(&mut hot, clock); } - - let (page_id, slot) = if let Some(mut bucket) = WakeupBuckets::::get(wakeup_block) { - let Some(cursor_index) = bucket.cursor_index else { - return Err(EnqueueOutcome::CorruptedTopology); - }; - if Self::wakeup_cursor_get(wakeup_block.clock(), cursor_index) != Some(wakeup_block) { + let mut retained_primary = None; + if let Some(location) = ActorControlLocators::::get(actor_id) { + let (_, source) = + Self::load_primary_control_cell(actor_id).map_err(|_| EnqueueOutcome::CorruptedTopology)?; + let mut expected_identity = Self::control_identity_from_scalar(identity.clone()) + .ok_or(EnqueueOutcome::CorruptedTopology)?; + expected_identity.cycle_nonce = source.identity.cycle_nonce; + if source.identity != expected_identity || source.admission != *admission { return Err(EnqueueOutcome::CorruptedTopology); } - let tail_key = (wakeup_block, bucket.tail_page); - let Some(mut tail_page) = WakeupPages::::get(tail_key) else { - return Err(EnqueueOutcome::CorruptedTopology); - }; - if tail_page.next_page.is_some() - || Self::wakeup_page_physical_live(&tail_page) != tail_page.live_entries as usize + if location == ActorControlLocation::Unsignaled + || matches!(location, ActorControlLocation::Waiting { key, .. } if key.clock() == clock) { - return Err(EnqueueOutcome::CorruptedTopology); + Self::remove_primary_control_cell_inner(actor_id) + .map_err(|_| EnqueueOutcome::CorruptedTopology)?; + } else { + retained_primary = Some((location, source)); + } + } + let (page_id, slot) = + Self::schedule_fresh_wakeup_reference(actor_id, wakeup_key, admission.admission_identity)?; + hot = Self::with_wakeup_pointer(hot, wakeup_key, page_id, slot); + if let Some((location, mut source)) = retained_primary { + source.identity = Self::control_identity_from_scalar(identity.clone()) + .ok_or(EnqueueOutcome::CorruptedTopology)?; + source.hot = Self::control_hot_from_scalar(hot); + source.cursor = cursor; + source.resources = resources; + return Self::store_primary_control_cell(location, source) + .map_err(|_| EnqueueOutcome::CorruptedTopology); + } + Self::publish_waiting_from_authority( + actor_id, wakeup_key, hot, identity, cursor, admission, resources, + ) + } + + fn schedule_fresh_wakeup_reference( + actor_id: ActorId, + wakeup_key: WakeupKey>, + admission_identity: [u8; 32], + ) -> Result<(WakeupPageId, WakeupSlot), EnqueueOutcome> { + #[cfg(test)] + if FAIL_WAKEUP_PLACEMENT_WITH_CAPACITY.with(|flag| flag.replace(false)) { + return Err(EnqueueOutcome::WakeupCapacityExhausted); + } + Self::append_waiting_entry(wakeup_key, |_, _| { + ActorWaitingEntry::Reference(ActorWakeupReference { + actor_id, + admission_identity, + }) + }) + .map_err(|error| match error { + ActorControlTransitionError::IndexExhausted => EnqueueOutcome::WakeupIndexExhausted, + _ => EnqueueOutcome::CorruptedTopology, + }) + } + + #[cfg(all(test, feature = "runtime-benchmarks"))] + pub(crate) fn control_schedule_fresh_wakeup_reference( + mut cell: ActorControlCellOf, + wakeup_key: WakeupKey>, + ) -> Result, ActorControlTransitionError> { + let pointer_exists = match wakeup_key { + WakeupKey::Block(_) => cell.hot.wakeup_pointer.is_some(), + WakeupKey::Tick(_) => cell.hot.trigger_wakeup_pointer.is_some(), + }; + if pointer_exists { + return Err(ActorControlTransitionError::Invariant); + } + let (page_id, slot) = Self::schedule_fresh_wakeup_reference( + cell.actor_id, + wakeup_key, + cell.admission.admission_identity, + ) + .map_err(|error| match error { + EnqueueOutcome::WakeupIndexExhausted | EnqueueOutcome::SchedulerIndexExhausted => { + ActorControlTransitionError::IndexExhausted } - let Some(next_bucket_live) = bucket.live_entries.checked_add(1) else { - return Err(EnqueueOutcome::WakeupIndexExhausted); - }; - let mut reusable_slot = None; - for slot in tail_page.scan_slot as usize..tail_page.entries.len() { - if tail_page.entries[slot].is_none() { - reusable_slot = Some(slot); - break; - } + _ => ActorControlTransitionError::Invariant, + })?; + match wakeup_key { + WakeupKey::Block(_) => { + cell.hot.wakeup_pointer = Some(WakeupPointer { + block: wakeup_key, + page_id, + slot, + }); } - let slot = if let Some(slot) = reusable_slot { - tail_page.entries[slot] = Some(WakeupEntry { actor_id }); - slot - } else if tail_page.entries.len() < T::WakeupPageSize::get() as usize { - let slot = tail_page.entries.len(); - if tail_page - .entries - .try_push(Some(WakeupEntry { actor_id })) - .is_err() - { - return Err(EnqueueOutcome::WakeupCapacityExhausted); - } - slot - } else { - let page_id = bucket.next_page_id; - let Some(next_page_id) = page_id.checked_add(1) else { - return Err(EnqueueOutcome::WakeupIndexExhausted); - }; - let mut entries = WakeupPageEntriesOf::::default(); - if entries.try_push(Some(WakeupEntry { actor_id })).is_err() { - return Err(EnqueueOutcome::WakeupCapacityExhausted); - } - tail_page.next_page = Some(page_id); - WakeupPages::::insert(tail_key, tail_page); - WakeupPages::::insert( - (wakeup_block, page_id), - WakeupPage { - entries, - live_entries: 1, - scan_slot: 0, - previous_page: Some(bucket.tail_page), - next_page: None, - }, - ); - bucket.tail_page = page_id; - bucket.next_page_id = next_page_id; - bucket.live_entries = next_bucket_live; - WakeupBuckets::::insert(wakeup_block, bucket); - Self::set_wakeup_pointer_loaded(actor_id, hot, wakeup_block, page_id, 0); - return Ok(()); - }; - tail_page.live_entries = tail_page - .live_entries - .checked_add(1) - .ok_or(EnqueueOutcome::WakeupIndexExhausted)?; - let page_id = bucket.tail_page; - WakeupPages::::insert(tail_key, tail_page); - bucket.live_entries = next_bucket_live; - WakeupBuckets::::insert(wakeup_block, bucket); - (page_id, slot as WakeupSlot) - } else { - let mut entries = WakeupPageEntriesOf::::default(); - if entries.try_push(Some(WakeupEntry { actor_id })).is_err() { - return Err(EnqueueOutcome::WakeupCapacityExhausted); - } - WakeupPages::::insert( - (wakeup_block, 0), - WakeupPage { - entries, - live_entries: 1, - scan_slot: 0, - previous_page: None, - next_page: None, - }, - ); - WakeupBuckets::::insert( - wakeup_block, - WakeupBucketState { - head_page: 0, - tail_page: 0, - next_page_id: 1, - live_entries: 1, - cursor_index: None, - }, - ); - if !Self::wakeup_cursor_insert_inner(wakeup_block) { - return Err(EnqueueOutcome::WakeupIndexExhausted); + WakeupKey::Tick(tick) => { + cell.hot.trigger_wakeup_pointer = Some(TriggerWakeupPointer { + tick, + page_id, + slot, + }); } - (0, 0) - }; - Self::set_wakeup_pointer_loaded(actor_id, hot, wakeup_block, page_id, slot); - Ok(()) + } + Ok(cell) } - fn set_wakeup_pointer_loaded( - actor_id: ActorId, + fn with_wakeup_pointer( mut hot: ActorHotStateOf, block: WakeupKey>, page_id: WakeupPageId, slot: WakeupSlot, - ) { + ) -> ActorHotStateOf { match block { WakeupKey::Block(_) => { hot.wakeup_pointer = Some(WakeupPointer { @@ -2814,9 +6489,10 @@ impl Pallet { }); } } - ActorHot::::insert(actor_id, hot); + hot } + #[cfg(any(test, feature = "runtime-benchmarks"))] pub fn wakeup_substrate_schedule(actor_id: ActorId, wakeup_block: BlockNumberFor) -> bool { let result: DispatchResult = polkadot_sdk::frame_support::storage::with_transaction(|| { if Self::wakeup_substrate_schedule_inner(actor_id, WakeupKey::Block(wakeup_block)) { @@ -2831,122 +6507,143 @@ impl Pallet { } fn wakeup_substrate_drain_block_inner( - wakeup_block: WakeupKey>, + wakeup_key: WakeupKey>, max_entries_scanned: u32, - ) -> Option<(BoundedVec, WakeupDrainStats)> { - let mut ready = BoundedVec::::default(); + ) -> Option<( + BoundedVec< + ( + ActorId, + ActiveActorStateOf, + ActorAdmissionCertificateOf, + Option>, + ), + T::MaxWakeupsPerBlock, + >, + WakeupDrainStats, + )> { + let mut ready = BoundedVec::< + ( + ActorId, + ActiveActorStateOf, + ActorAdmissionCertificateOf, + Option>, + ), + T::MaxWakeupsPerBlock, + >::default(); let mut stats = WakeupDrainStats::default(); - let scan_limit = max_entries_scanned.min(T::MaxWakeupsPerBlock::get()); - if scan_limit == 0 { - return Some((ready, stats)); - } - let Some(mut bucket) = WakeupBuckets::::get(wakeup_block) else { - return Some((ready, stats)); - }; - let Some(cursor_index) = bucket.cursor_index else { - return None; - }; - if Self::wakeup_cursor_get(wakeup_block.clock(), cursor_index) != Some(wakeup_block) { - return None; - } - let mut page_id = bucket.head_page; - - while stats.entries_scanned < scan_limit { - let key = (wakeup_block, page_id); - let Some(mut page) = WakeupPages::::get(key) else { + let limit = max_entries_scanned.min(T::MaxWakeupsPerBlock::get()); + let mut last_page = None; + while stats.entries_scanned < limit && ActorWaitingOccupancies::::get(wakeup_key) > 0 { + let cursor_index = ActorWaitingCursorIndices::::get(wakeup_key)?; + if Self::wakeup_cursor_get(wakeup_key.clock(), cursor_index) != Some(wakeup_key) { return None; - }; - if Self::wakeup_page_physical_live(&page) != page.live_entries as usize { + } + let head = ActorWaitingHeads::::get(wakeup_key); + let page_id = head / 32; + let mut page = ActorWaitingFrameChunks::::get((wakeup_key, page_id))?; + if page.previous_page.is_some() + || page.entries.len() != 32 + || page.live_entries == 0 + || page.entries.iter(/* deos-bypass: bounded-iter */).filter(|entry| entry.is_some()).count() + != page.live_entries as usize + { return None; } - stats.pages_touched = stats.pages_touched.saturating_add(1); - let mut slot = page.scan_slot as usize; - while slot < page.entries.len() && stats.entries_scanned < scan_limit { - let entry = page.entries[slot].take(); - let Some(next_scan_slot) = (slot as WakeupSlot).checked_add(1) else { - return None; - }; - let Some(next_slot) = slot.checked_add(1) else { - return None; - }; - page.scan_slot = next_scan_slot; - stats.entries_scanned = stats.entries_scanned.saturating_add(1); - slot = next_slot; - let Some(entry) = entry else { - continue; - }; - page.live_entries = page.live_entries.checked_sub(1)?; - bucket.live_entries = bucket.live_entries.checked_sub(1)?; - let pointer_slot = slot.checked_sub(1)?; - let pointer = WakeupPointer { - block: wakeup_block, - page_id, - slot: pointer_slot as WakeupSlot, + if last_page != Some(page_id) { + stats.pages_touched = stats.pages_touched.saturating_add(1); + last_page = Some(page_id); + } + let slot = page.scan_slot; + if slot >= 32 || head % 32 != u64::from(slot) { + return None; + } + let entry = page.entries.get(slot as usize)?.clone(); + let pointer = WakeupPointer { + block: wakeup_key, + page_id, + slot, + }; + let frozen = entry.as_ref().and_then(|entry| { + let actor_id = match entry { + ActorWaitingEntry::Primary(cell) => cell.actor_id, + ActorWaitingEntry::Reference(reference) => reference.actor_id, }; - 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 - } - Some((state, _, _)) if Self::wakeup_pointer_for_clock(&state.hot, clock).is_none() => { - stats.stale_entries = stats.stale_entries.saturating_add(1); - continue; + Self::load_frame_actor_service_state(actor_id) + }); + page.scan_slot = slot.checked_add(1)?; + stats.entries_scanned = stats.entries_scanned.saturating_add(1); + ActorWaitingFrameChunks::::insert((wakeup_key, page_id), page); + ActorWaitingHeads::::insert(wakeup_key, head.checked_add(1)?); + let Some(entry) = entry else { + continue; + }; + let (actor_id, expected_admission, is_primary) = match entry { + ActorWaitingEntry::Primary(cell) => { + (cell.actor_id, cell.admission.admission_identity, true) + } + ActorWaitingEntry::Reference(reference) => { + (reference.actor_id, reference.admission_identity, false) + } + }; + let (mut state, admission, loaded_step) = match frozen { + Some((state, admission, loaded_step)) + if admission.admission_identity == expected_admission + && Self::wakeup_pointer_for_clock(&state.hot, wakeup_key.clock()) == Some(pointer) => + { + (state, admission, loaded_step) + } + None if !is_primary && !ActorControlLocators::::contains_key(actor_id) => { + Self::remove_waiting_entry(pointer).ok()?; + stats.stale_entries = stats.stale_entries.saturating_add(1); + if !ActorWaitingFrameChunks::::contains_key((wakeup_key, page_id)) { + stats.pages_deleted = stats.pages_deleted.saturating_add(1); } - None if !ActorHot::::contains_key(entry.actor_id) => { - stats.stale_entries = stats.stale_entries.saturating_add(1); - continue; + continue; + } + Some((state, _, _)) + if !is_primary + && Self::wakeup_pointer_for_clock(&state.hot, wakeup_key.clock()).is_none() => + { + Self::remove_waiting_entry(pointer).ok()?; + stats.stale_entries = stats.stale_entries.saturating_add(1); + if !ActorWaitingFrameChunks::::contains_key((wakeup_key, page_id)) { + stats.pages_deleted = stats.pages_deleted.saturating_add(1); } - Some(_) | None => return None, - }; - if ready.try_push(entry.actor_id).is_err() { - page.entries[pointer_slot] = Some(entry); - page.live_entries = page.live_entries.checked_add(1)?; - bucket.live_entries = bucket.live_entries.checked_add(1)?; - page.scan_slot = pointer_slot as WakeupSlot; - stats.entries_scanned = stats.entries_scanned.saturating_sub(1); - WakeupPages::::insert(key, page); - WakeupBuckets::::insert(wakeup_block, bucket); - return Some((ready, stats)); - } - 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); - } - - if page.live_entries > 0 { - WakeupPages::::insert(key, page); - WakeupBuckets::::insert(wakeup_block, bucket); - return Some((ready, stats)); - } - - let next_page = page.next_page; - WakeupPages::::remove(key); - stats.pages_deleted = stats.pages_deleted.saturating_add(1); - let Some(next_page) = next_page else { - if !Self::wakeup_cursor_remove_inner(wakeup_block) { - return None; + continue; } - WakeupBuckets::::remove(wakeup_block); - return Some((ready, stats)); + _ => return None, }; - WakeupPages::::mutate((wakeup_block, next_page), |maybe_next| { - if let Some(next) = maybe_next { - next.previous_page = None; - } - }); - bucket.head_page = next_page; - WakeupBuckets::::insert(wakeup_block, bucket); - page_id = next_page; + Self::clear_wakeup_pointer_for_clock(&mut state.hot, wakeup_key.clock()); + if !is_primary { + Self::remove_waiting_entry(pointer).ok()?; + } + Self::consume_waiting_from_supplied_authority(actor_id, wakeup_key, &state.hot).ok()?; + ready + .try_push((actor_id, state, admission, loaded_step)) + .ok()?; + stats.ready_entries = stats.ready_entries.saturating_add(1); + if !ActorWaitingFrameChunks::::contains_key((wakeup_key, page_id)) { + stats.pages_deleted = stats.pages_deleted.saturating_add(1); + } } Some((ready, stats)) } - fn wakeup_substrate_drain_key( + pub(crate) fn wakeup_substrate_drain_key( wakeup_key: WakeupKey>, max_entries_scanned: u32, - ) -> (BoundedVec, WakeupDrainStats) { + ) -> ( + BoundedVec< + ( + ActorId, + ActiveActorStateOf, + ActorAdmissionCertificateOf, + Option>, + ), + T::MaxWakeupsPerBlock, + >, + WakeupDrainStats, + ) { let result: Result<_, DispatchError> = polkadot_sdk::frame_support::storage::with_transaction(|| { match Self::wakeup_substrate_drain_block_inner(wakeup_key, max_entries_scanned) { @@ -2965,7 +6662,15 @@ impl Pallet { wakeup_block: BlockNumberFor, max_entries_scanned: u32, ) -> (BoundedVec, WakeupDrainStats) { - Self::wakeup_substrate_drain_key(WakeupKey::Block(wakeup_block), max_entries_scanned) + let (loaded, stats) = + Self::wakeup_substrate_drain_key(WakeupKey::Block(wakeup_block), max_entries_scanned); + let ready = BoundedVec::truncate_from( + loaded + .into_iter() + .map(|(actor_id, _, _, _)| actor_id) + .collect(), + ); + (ready, stats) } fn wakeup_cursor_page_and_slot(index: WakeupCursorIndex) -> (WakeupPageId, usize) { @@ -3021,6 +6726,33 @@ impl Pallet { true } + pub(crate) fn wakeup_cursor_owner_index( + key: WakeupKey>, + ) -> Option { + ActorWaitingCursorIndices::::get(key) + } + + fn wakeup_cursor_has_owner(key: WakeupKey>) -> bool { + ActorWaitingOccupancies::::get(key) > 0 || ActorWaitingCursorIndices::::contains_key(key) + } + + fn wakeup_cursor_write_owner_index( + key: WakeupKey>, + index: Option, + ) -> bool { + let mut wrote = false; + if ActorWaitingOccupancies::::get(key) > 0 + || ActorWaitingCursorIndices::::contains_key(key) + { + match index { + Some(index) => ActorWaitingCursorIndices::::insert(key, index), + None => ActorWaitingCursorIndices::::remove(key), + } + wrote = true; + } + wrote + } + fn wakeup_cursor_swap( clock: WakeupClock, left: WakeupCursorIndex, @@ -3032,13 +6764,9 @@ impl Pallet { let Some(right_block) = Self::wakeup_cursor_get(clock, right) else { return false; }; - let Some(mut left_bucket) = WakeupBuckets::::get(left_block) else { - return false; - }; - let Some(mut right_bucket) = WakeupBuckets::::get(right_block) else { - return false; - }; - if left_bucket.cursor_index != Some(left) || right_bucket.cursor_index != Some(right) { + if Self::wakeup_cursor_owner_index(left_block) != Some(left) + || Self::wakeup_cursor_owner_index(right_block) != Some(right) + { return false; } if !Self::wakeup_cursor_set(clock, left, right_block) @@ -3046,11 +6774,8 @@ impl Pallet { { return false; } - right_bucket.cursor_index = Some(left); - left_bucket.cursor_index = Some(right); - WakeupBuckets::::insert(right_block, right_bucket); - WakeupBuckets::::insert(left_block, left_bucket); - true + Self::wakeup_cursor_write_owner_index(right_block, Some(left)) + && Self::wakeup_cursor_write_owner_index(left_block, Some(right)) } fn wakeup_cursor_height_bound() -> u32 { @@ -3059,11 +6784,12 @@ impl Pallet { fn wakeup_cursor_insert_inner(block: WakeupKey>) -> bool { let clock = block.clock(); - let Some(mut bucket) = WakeupBuckets::::get(block) else { + if !Self::wakeup_cursor_has_owner(block) { return false; - }; - if let Some(index) = bucket.cursor_index { - return Self::wakeup_cursor_get(clock, index) == Some(block); + } + if let Some(index) = Self::wakeup_cursor_owner_index(block) { + return Self::wakeup_cursor_get(clock, index) == Some(block) + && Self::wakeup_cursor_write_owner_index(block, Some(index)); } let len = WakeupCursorLen::::get(clock); let Some(next_len) = len.checked_add(1) else { @@ -3072,8 +6798,9 @@ impl Pallet { if len >= T::MaxActiveActors::get() || !Self::wakeup_cursor_set(clock, len, block) { return false; } - bucket.cursor_index = Some(len); - WakeupBuckets::::insert(block, bucket); + if !Self::wakeup_cursor_write_owner_index(block, Some(len)) { + return false; + } WakeupCursorLen::::insert(clock, next_len); let mut current = len; for _ in 0..Self::wakeup_cursor_height_bound() { @@ -3126,10 +6853,7 @@ impl Pallet { fn wakeup_cursor_remove_inner(block: WakeupKey>) -> bool { let clock = block.clock(); - let Some(mut target_bucket) = WakeupBuckets::::get(block) else { - return false; - }; - let Some(index) = target_bucket.cursor_index else { + let Some(index) = Self::wakeup_cursor_owner_index(block) else { return false; }; let len = WakeupCursorLen::::get(clock); @@ -3142,30 +6866,21 @@ impl Pallet { let Some(last_block) = Self::wakeup_cursor_get(clock, last_index) else { return false; }; - let mut last_bucket = if last_block == block { - target_bucket - } else { - let Some(last_bucket) = WakeupBuckets::::get(last_block) else { - return false; - }; - last_bucket - }; - if last_bucket.cursor_index != Some(last_index) + if Self::wakeup_cursor_owner_index(last_block) != Some(last_index) || !Self::wakeup_cursor_remove_tail(clock, last_index) + || !Self::wakeup_cursor_write_owner_index(block, None) { return false; } - target_bucket.cursor_index = None; - WakeupBuckets::::insert(block, target_bucket); WakeupCursorLen::::insert(clock, last_index); if index == last_index { return true; } - if !Self::wakeup_cursor_set(clock, index, last_block) { + if !Self::wakeup_cursor_set(clock, index, last_block) + || !Self::wakeup_cursor_write_owner_index(last_block, Some(index)) + { return false; } - last_bucket.cursor_index = Some(index); - WakeupBuckets::::insert(last_block, last_bucket); let mut current = index; for _ in 0..Self::wakeup_cursor_height_bound() { @@ -3226,9 +6941,25 @@ impl Pallet { true } + pub(crate) fn control_wakeup_cursor_release(key: WakeupKey>) -> bool { + let Some(index) = ActorWaitingCursorIndices::::get(key) else { + return false; + }; + if Self::wakeup_cursor_get(key.clock(), index) != Some(key) { + return false; + } + Self::wakeup_cursor_remove_inner(key) + } + pub fn wakeup_cursor_remove(block: BlockNumberFor) -> bool { let result: DispatchResult = polkadot_sdk::frame_support::storage::with_transaction(|| { - if Self::wakeup_cursor_remove_inner(WakeupKey::Block(block)) { + let key = WakeupKey::Block(block); + if ActorWaitingOccupancies::::get(key) > 0 { + return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err( + Error::::ActorNotFound.into(), + )); + } + if Self::wakeup_cursor_remove_inner(key) { polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(())) } else { polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err( @@ -3241,6 +6972,9 @@ impl Pallet { fn wakeup_cursor_pop_min_inner(clock: WakeupClock) -> Option>> { let min_block = Self::wakeup_cursor_get(clock, 0)?; + if ActorWaitingOccupancies::::get(min_block) > 0 { + return None; + } Self::wakeup_cursor_remove_inner(min_block).then_some(min_block) } @@ -3261,36 +6995,222 @@ impl Pallet { result.ok() } - fn loaded_active_view_for_placement( - actor_id: ActorId, - ) -> Result>, EnqueueOutcome> { - 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, - ))); - } - 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) + pub(crate) fn prime_initial_actor_schedule(actor_id: ActorId) -> Result<(), EnqueueOutcome> { + let Some((mut state, admission, loaded_step)) = Self::load_frame_actor_service_state(actor_id) + else { + return match ( + Self::control_identity_exists(actor_id), + Self::control_hot_exists(actor_id), + ActorContractHeads::::contains_key(actor_id), + Self::control_admission_exists(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(()) + } + _ => Err(EnqueueOutcome::CorruptedTopology), + }; + }; + let mut instance = Self::derive_active_actor_view( + state.identity.clone(), + state.hot.clone(), + state.contract.clone(), + ); + let resources = if state.contract.steps.is_empty() { + ActorStepResourceEnvelope { + control: T::WeightInfo::scheduler_inner_zero_step_complete(), + effect: Weight::zero(), } - _ => Err(EnqueueOutcome::CorruptedTopology), + } else { + loaded_step + .ok_or(EnqueueOutcome::CorruptedTopology)? + .resources + }; + let now = frame_system::Pallet::::block_number(); + if !instance.lifecycle.is_paused() && instance.trigger_wakeup_pointer.is_none() { + if let Some(due_tick) = Self::initial_trigger_wakeup_tick(&instance)? { + #[cfg(test)] + if FAIL_WAKEUP_PLACEMENT_WITH_CAPACITY.with(|flag| flag.replace(false)) { + return Err(EnqueueOutcome::WakeupCapacityExhausted); + } + match Self::try_wakeup_substrate_schedule_transition_with_authority( + actor_id, + WakeupKey::Tick(due_tick), + state.hot.clone(), + &state.identity, + state.run_state.as_ref().map_or(0, |run| run.cursor), + &admission, + resources, + ) { + Ok(()) | Err(EnqueueOutcome::AlreadyLive) => {} + Err(error) => return Err(error), + } + let (_, _, hot, _) = + Self::load_frame_control_authority(actor_id).ok_or(EnqueueOutcome::CorruptedTopology)?; + state.hot = hot; + instance = Self::derive_active_actor_view( + state.identity.clone(), + state.hot.clone(), + state.contract.clone(), + ); + } + } + let placement = Self::schedule_next_work_with_authority( + actor_id, + &instance, + state.hot, + &state.identity, + state.run_state.as_ref(), + &admission, + resources, + now, + ServiceCutoff::Open, + )?; + if placement == StepControlPlacement::None + && Self::load_frame_control_authority(actor_id).is_none() + { + return Err(EnqueueOutcome::CorruptedTopology); + } + Ok(()) + } + + pub(crate) fn demote_ready_frame_to_unsignaled(actor_id: ActorId) -> Result<(), EnqueueOutcome> { + let (location, mut cell) = + Self::load_primary_control_cell(actor_id).map_err(|_| EnqueueOutcome::CorruptedTopology)?; + if !matches!(location, ActorControlLocation::Ready { .. }) + || cell.hot.cycle_state != CycleState::Idle + { + return Ok(()); } + Self::remove_primary_control_cell_inner(actor_id) + .map_err(|_| EnqueueOutcome::CorruptedTopology)?; + cell.eligible_at = None; + ActorUnsignaledControlCells::::insert(actor_id, cell); + ActorControlLocators::::insert(actor_id, ActorControlLocation::Unsignaled); + Ok(()) } - pub(crate) fn prime_actor_schedule(actor_id: ActorId) -> Result<(), EnqueueOutcome> { - let Some(instance) = Self::loaded_active_view_for_placement(actor_id)? else { + pub(crate) fn prime_frame_actor_schedule(actor_id: ActorId) -> Result<(), EnqueueOutcome> { + let Some((state, admission, loaded_step)) = Self::load_frame_actor_service_state(actor_id) + else { + return Err(EnqueueOutcome::CorruptedTopology); + }; + let instance = Self::derive_active_actor_view( + state.identity.clone(), + state.hot.clone(), + state.contract.clone(), + ); + if instance.queue_ticket.is_some() { return Ok(()); + } + let prime = Self::preflight_prime_schedule_loaded(&instance, state.run_state.as_ref()); + match prime? { + PrimeSchedulePlan::Enqueue => { + let plan = Self::preflight_activation_enqueue(actor_id, &state, &admission)?; + Self::commit_paged_enqueue_transactional(plan) + } + PrimeSchedulePlan::BlockWakeup(block) => { + let resources = if state.contract.steps.is_empty() { + ActorStepResourceEnvelope { + control: T::WeightInfo::scheduler_inner_zero_step_complete(), + effect: Weight::zero(), + } + } else { + loaded_step + .as_ref() + .map(|loaded| loaded.resources) + .ok_or(EnqueueOutcome::CorruptedTopology)? + }; + Self::defer_wakeup_with_authority( + actor_id, + block, + &instance, + state.hot, + &state.identity, + state.run_state.as_ref(), + &admission, + resources, + ) + } + PrimeSchedulePlan::None => { + if state.hot.trigger_wakeup_pointer.is_some() { + return Ok(()); + } + let due_tick = Self::initial_trigger_wakeup_tick(&instance)?; + if let Some(due_tick) = due_tick { + let resources = if state.contract.steps.is_empty() { + ActorStepResourceEnvelope { + control: T::WeightInfo::scheduler_inner_zero_step_complete(), + effect: Weight::zero(), + } + } else { + loaded_step + .as_ref() + .map(|loaded| loaded.resources) + .ok_or(EnqueueOutcome::CorruptedTopology)? + }; + Self::try_wakeup_substrate_schedule_transition_with_authority( + actor_id, + WakeupKey::Tick(due_tick), + state.hot, + &state.identity, + state.run_state.as_ref().map_or(0, |run| run.cursor), + &admission, + resources, + ) + } else { + Ok(()) + } + } + } + } + + pub(crate) fn publish_resumed_frame( + actor_id: ActorId, + state: ActiveActorStateOf, + admission: ActorAdmissionCertificateOf, + loaded_step: Option>, + ) -> Result<(), EnqueueOutcome> { + let resources = if state.contract.steps.is_empty() { + ActorStepResourceEnvelope { + control: T::WeightInfo::scheduler_inner_zero_step_complete(), + effect: Weight::zero(), + } + } else { + loaded_step + .as_ref() + .map(|loaded| loaded.resources) + .ok_or(EnqueueOutcome::CorruptedTopology)? }; - Self::prime_actor_schedule_loaded(actor_id, &instance) + let instance = Self::derive_active_actor_view( + state.identity.clone(), + state.hot.clone(), + state.contract.clone(), + ); + let placement = Self::schedule_next_work_with_authority( + actor_id, + &instance, + state.hot.clone(), + &state.identity, + state.run_state.as_ref(), + &admission, + resources, + frame_system::Pallet::::block_number(), + ServiceCutoff::Open, + )?; + if placement == StepControlPlacement::None { + Self::restore_unsignaled_from_authority( + actor_id, + state.hot, + &state.identity, + state.run_state.as_ref(), + &admission, + resources, + )?; + Self::prime_frame_actor_schedule(actor_id)?; + } + Ok(()) } fn preflight_prime_schedule_loaded( @@ -3330,45 +7250,33 @@ impl Pallet { }) } - fn prime_actor_schedule_loaded( - actor_id: ActorId, + fn initial_trigger_wakeup_tick( instance: &ActiveActorViewOf, - ) -> Result<(), EnqueueOutcome> { - let now = frame_system::Pallet::::block_number(); - if instance.lifecycle.is_paused() { - return Self::schedule_window_expiry(actor_id, &instance); - } - 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) + ) -> Result, EnqueueOutcome> { + Ok(match instance.trigger { + Trigger::AtTime { after_ticks } if !instance.temporal_occurrence_consumed => { + Some(instance.temporal_anchor_tick.map_or(Ok(0), |anchor_tick| { + anchor_tick + .checked_add(after_ticks) + .ok_or(EnqueueOutcome::SchedulerIndexExhausted) + })?) + } + 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) - })?; - 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).map(|_| ()) + }) + .transpose()? + .unwrap_or(0), + ), + Trigger::Manual + | Trigger::AddressEvent { .. } + | Trigger::ObservationChange { .. } + | Trigger::ObservationCrossing { .. } + | Trigger::AtTime { .. } => None, + }) } fn window_expiry_wakeup(instance: &ActiveActorViewOf) -> Option> { @@ -3377,15 +7285,31 @@ impl Pallet { .map(|window| Self::window_terminal_at(&window)) } - fn schedule_window_expiry( + fn schedule_window_expiry_with_authority( actor_id: ActorId, instance: &ActiveActorViewOf, + loaded_authority: ( + &ActorHotStateOf, + &ActorIdentityOf, + Option<&ActorRunStateOf>, + &ActorAdmissionCertificateOf, + ActorStepResourceEnvelope, + ), ) -> Result<(), EnqueueOutcome> { - if let Some(expiry) = Self::window_expiry_wakeup(instance) { - Self::defer_wakeup_loaded(actor_id, expiry, instance) - } else { - Ok(()) - } + let Some(expiry) = Self::window_expiry_wakeup(instance) else { + return Ok(()); + }; + let (hot, identity, run_state, admission, resources) = loaded_authority; + Self::defer_wakeup_with_authority( + actor_id, + expiry, + instance, + hot.clone(), + identity, + run_state, + admission, + resources, + ) } #[cfg(test)] @@ -3393,43 +7317,87 @@ impl Pallet { FAIL_WAKEUP_PLACEMENT_WITH_CAPACITY.with(|flag| flag.set(true)); } - fn defer_tick_wakeup( - actor_id: ActorId, - wakeup_tick: SchedulerTick, - ) -> Result<(), EnqueueOutcome> { - #[cfg(test)] - if FAIL_WAKEUP_PLACEMENT_WITH_CAPACITY.with(|flag| flag.replace(false)) { - return Err(EnqueueOutcome::WakeupCapacityExhausted); - } - match Self::try_wakeup_substrate_schedule_key_inner(actor_id, WakeupKey::Tick(wakeup_tick)) { - Ok(()) | Err(EnqueueOutcome::AlreadyLive) => Ok(()), - Err(other) => Err(other), - } - } - - fn defer_wakeup( + #[cfg(test)] + fn defer_retained_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) + let (state, admission, loaded_step) = + Self::load_frame_actor_service_state(actor_id).ok_or(EnqueueOutcome::CorruptedTopology)?; + let instance = Self::derive_active_actor_view( + state.identity.clone(), + state.hot.clone(), + state.contract.clone(), + ); + let resources = if state.contract.steps.is_empty() { + ActorStepResourceEnvelope { + control: T::WeightInfo::scheduler_inner_zero_step_complete(), + effect: Weight::zero(), + } + } else { + loaded_step + .ok_or(EnqueueOutcome::CorruptedTopology)? + .resources + }; + Self::defer_wakeup_with_authority( + actor_id, + wakeup_block, + &instance, + state.hot, + &state.identity, + state.run_state.as_ref(), + &admission, + resources, + ) } - fn defer_wakeup_loaded( + fn defer_wakeup_with_authority( actor_id: ActorId, wakeup_block: BlockNumberFor, instance: &ActiveActorViewOf, + hot: ActorHotStateOf, + identity: &ActorIdentityOf, + run_state: Option<&ActorRunStateOf>, + admission: &ActorAdmissionCertificateOf, + resources: ActorStepResourceEnvelope, ) -> Result<(), EnqueueOutcome> { - let hot = ActorHot::::get(actor_id).ok_or(EnqueueOutcome::CorruptedTopology)?; - Self::defer_wakeup_with_hot(actor_id, wakeup_block, instance, hot) + #[cfg(test)] + if FAIL_WAKEUP_PLACEMENT_WITH_CAPACITY.with(|flag| flag.replace(false)) { + return Err(EnqueueOutcome::WakeupCapacityExhausted); + } + let target = Self::window_expiry_wakeup(instance) + .map(|expiry| wakeup_block.min(expiry)) + .unwrap_or(wakeup_block); + match with_transaction_opaque_err(|| { + match Self::try_wakeup_substrate_schedule_transition_with_authority( + actor_id, + WakeupKey::Block(target), + hot, + identity, + run_state.map_or(0, |run| run.cursor), + admission, + resources, + ) { + 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), + } } - fn defer_wakeup_with_hot( + fn defer_activation_wakeup( actor_id: ActorId, wakeup_block: BlockNumberFor, instance: &ActiveActorViewOf, hot: ActorHotStateOf, + source: &ActiveActorStateOf, + admission: &ActorAdmissionCertificateOf, ) -> Result<(), EnqueueOutcome> { #[cfg(test)] if FAIL_WAKEUP_PLACEMENT_WITH_CAPACITY.with(|flag| flag.replace(false)) { @@ -3438,18 +7406,36 @@ impl Pallet { let target = Self::window_expiry_wakeup(instance) .map(|expiry| wakeup_block.min(expiry)) .unwrap_or(wakeup_block); - match with_transaction_opaque_err( - || match Self::try_wakeup_substrate_schedule_transition_loaded( - actor_id, - WakeupKey::Block(target), - hot, - ) { + match with_transaction_opaque_err(|| { + let transition = || { + let cursor = source.run_state.as_ref().map_or(0, |run| run.cursor); + let resources = if source.contract.steps.is_empty() { + ActorStepResourceEnvelope { + control: T::WeightInfo::scheduler_inner_zero_step_complete(), + effect: Weight::zero(), + } + } else { + Self::load_current_step_with_admission(actor_id, cursor, admission) + .ok_or(EnqueueOutcome::CorruptedTopology)? + .resources + }; + Self::try_wakeup_substrate_schedule_transition_with_authority( + actor_id, + WakeupKey::Block(target), + hot, + &source.identity, + cursor, + admission, + resources, + ) + }; + match transition() { 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(()), @@ -3475,8 +7461,8 @@ impl Pallet { .saturating_add(Self::scheduler_actor_state_probe_weight_upper()) } - /// Conservatively prices pure actor-local terminal deletion from the measured User close path. - /// Shared queue and wakeup records become lazy tombstones. + /// Conservatively prices terminal deletion from the measured User close path. + /// Ready slots become tombstones; Waiting release unlinks empty pages and repairs its directory. pub fn close_cleanup_weight_upper() -> Weight { T::WeightInfo::close_actor() } @@ -3500,11 +7486,21 @@ impl Pallet { actor_id: ActorId, wakeup_tick: SchedulerTick, ) -> Result<(), EnqueueOutcome> { - Self::defer_tick_wakeup(actor_id, wakeup_tick) + #[cfg(test)] + if FAIL_WAKEUP_PLACEMENT_WITH_CAPACITY.with(|flag| flag.replace(false)) { + return Err(EnqueueOutcome::WakeupCapacityExhausted); + } + match Self::try_wakeup_substrate_schedule_key_inner(actor_id, WakeupKey::Tick(wakeup_tick)) { + Ok(()) | Err(EnqueueOutcome::AlreadyLive) => Ok(()), + Err(other) => Err(other), + } } - fn wakeup_cursor_drain_branch_weight(removes_bucket: bool, clock: WakeupClock) -> Weight { - let physical = if removes_bucket { + fn wakeup_cursor_drain_branch_weight( + bucket: WakeupBucketDisposition, + clock: WakeupClock, + ) -> Weight { + let physical = if matches!(bucket, WakeupBucketDisposition::Remove) { T::WeightInfo::scheduler_wakeup_cursor_worker_remove() } else { T::WeightInfo::scheduler_wakeup_cursor_worker_partial() @@ -3522,56 +7518,44 @@ impl Pallet { } } - fn wakeup_cursor_drain_unit_weight_for(removes_bucket: bool, clock: WakeupClock) -> Weight { - Self::wakeup_cursor_drain_branch_weight(removes_bucket, clock) + fn wakeup_cursor_drain_unit_weight_for( + bucket: WakeupBucketDisposition, + clock: WakeupClock, + ) -> Weight { + Self::wakeup_cursor_drain_branch_weight(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) + pub fn wakeup_cursor_drain_unit_weight_upper(bucket: WakeupBucketDisposition) -> Weight { + Self::wakeup_cursor_drain_unit_weight_for(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 block_wakeup_cursor_drain_unit_weight_upper( + bucket: WakeupBucketDisposition, + ) -> Weight { + Self::wakeup_cursor_drain_unit_weight_for(bucket, WakeupClock::Block) } - pub(crate) fn process_due_temporal_occurrence( + pub(crate) fn process_due_temporal_occurrence_loaded( actor_id: ActorId, + mut state: ActiveActorStateOf, + admission: ActorAdmissionCertificateOf, + loaded_step: Option>, 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")); + let resources = if state.contract.steps.is_empty() { + ActorStepResourceEnvelope { + control: T::WeightInfo::scheduler_inner_zero_step_complete(), + effect: Weight::zero(), + } + } else { + let cursor = state.run_state.as_ref().map_or(0, |run| run.cursor); + loaded_step + .filter(|loaded| loaded.cursor == cursor) + .map(|loaded| loaded.resources) + .ok_or(DispatchError::Other("temporal loaded Step is corrupt"))? }; if state.hot.pending_signal { return Ok(false); @@ -3618,11 +7602,27 @@ impl Pallet { }, _ => 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() - { + Self::try_store_control_hot_with_authority(actor_id, state.hot.clone()) + .map_err(|_| DispatchError::Other("genesis temporal authority update failed"))?; + let placement = { + Self::try_wakeup_substrate_schedule_transition_with_authority( + actor_id, + WakeupKey::Tick(due_tick), + state.hot.clone(), + &state.identity, + state.run_state.as_ref().map_or(0, |run| run.cursor), + &admission, + resources, + ) + }; + if let Err(error) = placement { + let close_result = Self::finalize_actor_from_retained_state( + actor_id, + state.clone(), + &admission, + CloseReason::SchedulerIndexExhausted, + ); + if !Self::scheduler_index_is_exhausted(error) || close_result.is_err() { return Err(DispatchError::Other("genesis temporal placement failed")); } return Ok(true); @@ -3645,15 +7645,31 @@ impl Pallet { anchor_tick, consumed: true, }; - ActorHot::::insert(actor_id, state.hot); + Self::try_store_control_hot_with_authority(actor_id, state.hot) + .map_err(|_| DispatchError::Other("AtTime progression commit failed"))?; } 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() - { + let placement = { + Self::try_wakeup_substrate_schedule_transition_with_authority( + actor_id, + WakeupKey::Tick(next_due_tick), + state.hot.clone(), + &state.identity, + state.run_state.as_ref().map_or(0, |run| run.cursor), + &admission, + resources, + ) + }; + if let Err(error) = placement { + let close_result = Self::finalize_actor_from_retained_state( + actor_id, + state.clone(), + &admission, + CloseReason::SchedulerIndexExhausted, + ); + if !Self::scheduler_index_is_exhausted(error) || close_result.is_err() { return Err(DispatchError::Other("cadence rearm failed")); } return Ok(true); @@ -3667,15 +7683,17 @@ impl Pallet { )); } } - Self::reconcile_actor_state_hold(actor_id) + Self::reconcile_actor_state_hold_with_authority(actor_id) .map_err(|_| DispatchError::Other("temporal state hold reconciliation failed"))?; - let Some((state, _, _)) = Self::load_actor_service_state(actor_id) else { + let loaded_state = Self::load_actor_service_state_with_authority(actor_id); + let Some((state, admission, _)) = loaded_state 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 activation = + Self::preflight_activation_from_authority(actor_id, state.clone(), admission.clone()) + .map_err(|_| DispatchError::Other("temporal activation preflight failed"))?; let closes_without_occurrence = activation.terminal_reason.is_some() || matches!( activation.action, @@ -3687,7 +7705,8 @@ impl Pallet { )) ); if closes_without_occurrence { - return match Self::request_activation(actor_id) { + let activation = Self::commit_activation_plan(activation); + return match activation { Ok(ActivationOutcome::Closed) => Ok(true), Ok(ActivationOutcome::Latched | ActivationOutcome::Coalesced) => Ok(false), _ => Err(DispatchError::Other( @@ -3705,12 +7724,13 @@ impl Pallet { ) .map_err(|_| DispatchError::Other("temporal capacity calculation failed"))?; if !temporal_capacity { - Self::finalize_actor_loaded( + let close_result = Self::finalize_actor_from_retained_state( actor_id, - &Self::derive_active_actor_view(state.identity, state.hot, state.contract), + state, + &admission, CloseReason::TriggerAdmissionInsufficient, - ) - .map_err(|_| DispatchError::Other("underfunded temporal apoptosis failed"))?; + ); + close_result.map_err(|_| DispatchError::Other("underfunded temporal apoptosis failed"))?; return Ok(true); } if !Self::try_charge_prechecked_automatic_trigger_occurrence( @@ -3731,12 +7751,28 @@ impl Pallet { { return Ok(false); } - match Self::request_activation(actor_id) { + if trigger_family == TriggerFamily::Cadenced { + let (state, _, _) = Self::load_frame_actor_service_state(actor_id) + .ok_or(DispatchError::Other("cadence latch authority disappeared"))?; + if state.hot.trigger_wakeup_pointer.is_some() { + Self::trigger_wakeup_substrate_invalidate_loaded(actor_id, state, &admission) + .map_err(|_| DispatchError::Other("cadence latch disable failed"))?; + } + } + let activation = Self::request_activation(actor_id); + match activation { 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"))?; + let paused_hot = + Self::load_control_authority_with_authority(actor_id).map(|(_, hot, _)| hot); + if let Some(hot) = paused_hot + && hot.lifecycle.is_paused() + && hot.queue_ticket.is_some() + { + Self::invalidate_ready_to_unsignaled_with_authority(actor_id) + .map_err(|_| DispatchError::Other("paused temporal queue invalidation failed"))?; } + Self::reconcile_actor_state_hold_with_authority(actor_id) + .map_err(|_| DispatchError::Other("temporal latch hold reconciliation failed"))?; Self::deposit_event(Event::TriggerOccurrenceProcessed { actor_id, trigger_family: breakdown.trigger_family, @@ -3805,25 +7841,35 @@ impl Pallet { let Some((wakeup_key, next_clock_after_success)) = selected else { break; }; - let base_weight = Self::wakeup_cursor_drain_branch_weight(false, wakeup_key.clock()); + let base_weight = Self::wakeup_cursor_drain_branch_weight( + WakeupBucketDisposition::Retain, + wakeup_key.clock(), + ); if Self::combined_queue_occupancy() >= u64::from(T::MaxActiveActors::get()) || !meter.can_consume(base_weight) { break; } - let Some(bucket) = WakeupBuckets::::get(wakeup_key) else { + let occupancy = ActorWaitingOccupancies::::get(wakeup_key); + if occupancy == 0 { meter.consume(base_weight); break; + } + let bucket_disposition = if occupancy <= 1 { + WakeupBucketDisposition::Remove + } else { + WakeupBucketDisposition::Retain }; - let removes_bucket = bucket.live_entries <= 1; - let unit_weight = Self::wakeup_cursor_drain_branch_weight(removes_bucket, wakeup_key.clock()); + let unit_weight = + Self::wakeup_cursor_drain_branch_weight(bucket_disposition, wakeup_key.clock()); let admission_weight = - Self::wakeup_cursor_drain_unit_weight_for(removes_bucket, wakeup_key.clock()); + Self::wakeup_cursor_drain_unit_weight_for(bucket_disposition, wakeup_key.clock()); if !meter.can_consume(admission_weight) { meter.consume(base_weight); break; } meter.consume(unit_weight); + let source_page = ActorWaitingHeads::::get(wakeup_key) / 32; let outcome = polkadot_sdk::frame_support::storage::with_transaction(|| { let (ready, stats) = Self::wakeup_substrate_drain_key(wakeup_key, 1); if stats.entries_scanned == 0 { @@ -3831,11 +7877,17 @@ impl Pallet { stats, false, ))); } - let mut closed_for_exhaustion = false; - for actor_id in ready { + let mut closed_with_reserved_cleanup = false; + for (actor_id, post_source_state, admission, loaded_step) in ready { if matches!(wakeup_key, WakeupKey::Tick(_)) { - match Self::process_due_temporal_occurrence(actor_id, now_tick) { - Ok(closed) => closed_for_exhaustion |= closed, + match Self::process_due_temporal_occurrence_loaded( + actor_id, + post_source_state, + admission, + loaded_step, + now_tick, + ) { + Ok(closed) => closed_with_reserved_cleanup |= closed, Err(error) => { return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err( error, @@ -3844,30 +7896,70 @@ impl Pallet { } continue; } - if let Err(error) = Self::enqueue(actor_id) { - if !Self::scheduler_index_is_exhausted(error) - || Self::close_for_scheduler_index_exhaustion(actor_id).is_err() - { + let close_instance = Self::derive_active_actor_view( + post_source_state.identity.clone(), + post_source_state.hot.clone(), + post_source_state.contract.clone(), + ); + match Self::expiry_substitution_due_loaded( + &close_instance, + post_source_state.run_state.as_ref(), + ) { + Ok(true) => { + let close_result = Self::finalize_actor_from_consumed_state( + actor_id, + post_source_state.clone(), + &admission, + CloseReason::WindowExpired, + ); + if let Err(error) = close_result { + return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err( + error, + )); + } + closed_with_reserved_cleanup = true; + continue; + } + Ok(false) => {} + Err(error) => { + return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err( + error.into(), + )); + } + } + if let Err(error) = Self::enqueue_actor_state_loaded( + actor_id, + &post_source_state, + &admission, + loaded_step.as_ref(), + ) { + let close_result = Self::finalize_actor_from_consumed_state( + actor_id, + post_source_state.clone(), + &admission, + CloseReason::SchedulerIndexExhausted, + ); + if !Self::scheduler_index_is_exhausted(error) || close_result.is_err() { return polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err( polkadot_sdk::sp_runtime::DispatchError::Other("wakeup materialization failed"), )); } - closed_for_exhaustion = true; + closed_with_reserved_cleanup = true; } } polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(( stats, - closed_for_exhaustion, + closed_with_reserved_cleanup, ))) }); - let (stats, closed_for_exhaustion) = match outcome { + let (stats, closed_with_reserved_cleanup) = match outcome { Ok(outcome) => outcome, Err(_) => { let recorded = Self::record_wakeup_worker_fault( meter, WakeupWorkerFault { key: wakeup_key, - page: bucket.head_page, + page: source_page, class: CrossingWorkerFaultClass::Invariant, }, ); @@ -3875,7 +7967,7 @@ impl Pallet { break; } }; - if closed_for_exhaustion { + if closed_with_reserved_cleanup { meter.consume(Self::close_cleanup_weight_upper()); } if stats.entries_scanned == 0 { @@ -3954,6 +8046,7 @@ impl Pallet { .min(MAX_RETRY_BACKOFF_BLOCKS) } + #[cfg(test)] pub(crate) fn retry_eligible_at( actor_id: ActorId, instance: &ActiveActorViewOf, @@ -3998,41 +8091,55 @@ impl Pallet { Ok(run_state.eligible_at) } - fn schedule_next_work_local( + fn schedule_next_work_loaded( actor_id: ActorId, instance: &ActiveActorViewOf, - hot: Option>, + loaded_authority: ( + &ActorHotStateOf, + &ActorIdentityOf, + Option<&ActorRunStateOf>, + &ActorAdmissionCertificateOf, + ActorStepResourceEnvelope, + ), now: BlockNumberFor, - cutoff_snapshotted: bool, - requeues: &mut Vec, + cutoff: ServiceCutoff, ) -> Result { if instance.lifecycle.is_paused() { - return Self::schedule_window_expiry(actor_id, instance).map(|()| { - if instance.window.is_some() { - StepControlPlacement::Wakeup - } else { - StepControlPlacement::None - } - }); + return Self::schedule_window_expiry_with_authority(actor_id, instance, loaded_authority) + .map(|()| { + if instance.window.is_some() { + StepControlPlacement::Wakeup + } else { + StepControlPlacement::None + } + }); } - 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); + let eligible_at = if matches!( + instance.cycle_state, + CycleState::Running | CycleState::Suspended + ) { + let run = loaded_authority + .2 + .ok_or(EnqueueOutcome::CorruptedTopology)?; + if instance.cycle_state == CycleState::Running { + if !run.running_is_coherent() { + return Err(EnqueueOutcome::CorruptedTopology); + } + run.eligible_at + } else { + Self::retry_eligible_at_loaded(instance, run)? } - 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).map(|()| { - if instance.window.is_some() { - StepControlPlacement::Wakeup - } else { - StepControlPlacement::None - } - }); + return Self::schedule_window_expiry_with_authority(actor_id, instance, loaded_authority) + .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)) @@ -4040,60 +8147,117 @@ impl Pallet { let exact_next_block = now .checked_add(&One::one()) .ok_or(EnqueueOutcome::SchedulerIndexExhausted)?; - if wakeup_at < exact_next_block || wakeup_at == exact_next_block && cutoff_snapshotted { - requeues.push(actor_id); + if wakeup_at < exact_next_block || wakeup_at == exact_next_block && cutoff.is_snapshotted() { Ok(StepControlPlacement::Queue) } else { - 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), - } + let (hot, identity, run_state, admission, resources) = loaded_authority; + Self::defer_wakeup_with_authority( + actor_id, + wakeup_at, + instance, + hot.clone(), + identity, + run_state, + admission, + resources, + ) .map(|()| StepControlPlacement::Wakeup) } } - fn schedule_next_work( + #[cfg(test)] + pub(crate) fn test_schedule_next_work_source( actor_id: ActorId, - instance: &ActiveActorViewOf, + state: &ActiveActorStateOf, + admission: &ActorAdmissionCertificateOf, + resources: ActorStepResourceEnvelope, + supplied_run: Option>>, now: BlockNumberFor, - cutoff_snapshotted: bool, - ) -> Result { - let mut requeues = Vec::new(); - let placement = Self::schedule_next_work_local( + ) -> Result<(StepControlPlacement, Vec), EnqueueOutcome> { + let retained; + let (state, admission, resources, run) = match supplied_run { + Some(run) => (state, admission, resources, run), + None => { + retained = Self::load_frame_actor_service_state(actor_id) + .ok_or(EnqueueOutcome::CorruptedTopology)?; + let (state, admission, loaded_step) = &retained; + let resources = if state.contract.steps.is_empty() { + ActorStepResourceEnvelope { + control: T::WeightInfo::scheduler_inner_zero_step_complete(), + effect: Weight::zero(), + } + } else { + loaded_step + .as_ref() + .ok_or(EnqueueOutcome::CorruptedTopology)? + .resources + }; + (state, admission, resources, state.run_state.as_ref()) + } + }; + let instance = Self::derive_active_actor_view( + state.identity.clone(), + state.hot.clone(), + state.contract.clone(), + ); + let authority = (&state.hot, &state.identity, run, admission, resources); + let placement = Self::schedule_next_work_loaded( actor_id, - instance, - None, + &instance, + authority, now, - cutoff_snapshotted, - &mut requeues, + ServiceCutoff::Snapshotted, )?; - for actor_id in requeues { - Self::enqueue(actor_id)?; - } - Ok(placement) + let requeues = if placement == StepControlPlacement::Queue { + vec![actor_id] + } else { + Vec::new() + }; + Ok((placement, requeues)) } - fn schedule_next_work_loaded( + fn schedule_next_work_with_authority( actor_id: ActorId, instance: &ActiveActorViewOf, hot: ActorHotStateOf, + identity: &ActorIdentityOf, + run_state: Option<&ActorRunStateOf>, + admission: &ActorAdmissionCertificateOf, + resources: ActorStepResourceEnvelope, now: BlockNumberFor, - cutoff_snapshotted: bool, + cutoff: ServiceCutoff, ) -> 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); + let loaded_authority = (&hot, identity, run_state, admission, resources); + let placement = + Self::schedule_next_work_loaded(actor_id, instance, loaded_authority, now, cutoff)?; + if placement == StepControlPlacement::Queue { + match Self::enqueue_authority_loaded( + actor_id, + hot.clone(), + identity, + run_state, + admission, + resources, + ) { + Ok(()) => {} + Err(EnqueueOutcome::CapacityUnavailable) => { + let next_block = frame_system::Pallet::::block_number() + .checked_add(&One::one()) + .ok_or(EnqueueOutcome::SchedulerIndexExhausted)?; + Self::defer_wakeup_with_authority( + actor_id, + next_block, + instance, + hot.clone(), + identity, + run_state, + admission, + resources, + )?; + return Ok(StepControlPlacement::Wakeup); + } + Err(error) => return Err(error), } - Self::enqueue_loaded(queued_actor, hot.clone())?; } Ok(placement) } @@ -4237,6 +8401,14 @@ impl Pallet { run_state: Option<&ActorRunStateOf>, ) -> Result>, ActorClassificationError> { let cursor = run_state.as_ref().map_or(0, |state| state.cursor as usize); + Self::classify_actor_at_current_step(instance, run_state, instance.steps.get(cursor)) + } + + fn classify_actor_at_current_step( + instance: &ActiveActorViewOf, + run_state: Option<&ActorRunStateOf>, + current_step: Option<&StepOf>, + ) -> Result>, ActorClassificationError> { match (instance.cycle_state, run_state) { (CycleState::Idle, None) => {} (CycleState::Running | CycleState::Suspended, Some(state)) => { @@ -4244,7 +8416,7 @@ impl Pallet { .cycle_nonce .checked_add(1) .ok_or(ActorClassificationError::RunInvariant)?; - if cursor >= instance.steps.len() || state.cycle_nonce != expected_cycle_nonce { + if current_step.is_none() || state.cycle_nonce != expected_cycle_nonce { return Err(ActorClassificationError::RunInvariant); } if instance.cycle_state == CycleState::Running { @@ -4253,9 +8425,8 @@ impl Pallet { } } else if state.unsuccessful_attempts_at_cursor == 0 || !state.suspension_is_coherent() - || instance.steps[cursor] - .on_error - .retry_max_attempts() + || current_step + .and_then(|step| step.on_error.retry_max_attempts()) .is_none() { return Err(ActorClassificationError::RunInvariant); @@ -4269,9 +8440,8 @@ impl Pallet { } else if instance.cycle_state == CycleState::Idle && instance.cycle_nonce == u64::MAX { Some(CloseReason::CycleNonceExhausted) } else if run_state.as_ref().is_some_and(|state| { - instance.steps[state.cursor as usize] - .on_error - .retry_max_attempts() + current_step + .and_then(|step| step.on_error.retry_max_attempts()) .is_some_and(|max_attempts| state.unsuccessful_attempts_at_cursor >= max_attempts) }) { Some(CloseReason::RetryAttemptsExhausted) @@ -4349,18 +8519,22 @@ impl Pallet { }) } - fn close_admission_decision( - instance: &ActiveActorViewOf, - reason: CloseReason, - meter: &WeightMeter, - ) -> AdmissionDecision { + fn complete_entry_owner_weight(owner: CompleteEntryOwner) -> Weight { + match owner { + CompleteEntryOwner::CrossingUnavailable => { + T::WeightInfo::scheduler_paged_zero_step_user_crossing_unavailable() + } + } + } + + fn close_admission_decision(reason: CloseReason, meter: &WeightMeter) -> AdmissionDecision { let weight = if matches!( reason, CloseReason::CycleAdmissionInsufficient | CloseReason::TriggerAdmissionInsufficient ) { T::WeightInfo::pipeline_admission_apoptosis() } else { - Self::close_cycle_weight_upper_bound(instance) + Self::close_cleanup_weight_upper() }; if !meter.can_consume(weight) { return AdmissionDecision::Defer; @@ -4388,11 +8562,12 @@ impl Pallet { }) } - fn cycle_may_auto_close_on_success(instance: &ActiveActorViewOf) -> bool { - instance - .auto_close_at_cycle_nonce - .map(|target| instance.cycle_nonce.saturating_add(1) >= target) - .unwrap_or(false) + fn cycle_may_close_on_success(instance: &ActiveActorViewOf) -> bool { + instance.completion == CompletionPolicy::CloseAfterProductiveCycle + || instance + .auto_close_at_cycle_nonce + .map(|target| instance.cycle_nonce.saturating_add(1) >= target) + .unwrap_or(false) } fn cycle_requires_terminal_cleanup_budget( @@ -4404,7 +8579,7 @@ impl Pallet { instance, start_cursor, prior_unsuccessful_attempts_at_cursor, - ) || Self::cycle_may_auto_close_on_success(instance) + ) || Self::cycle_may_close_on_success(instance) } fn apply_admission_loaded( @@ -4412,40 +8587,43 @@ impl Pallet { instance: &ActiveActorViewOf, run_state: Option<&ActorRunStateOf>, step_plan: Option<&CurrentStepPlanOf>, + pipeline_capacity: Option>>, meter: &WeightMeter, + complete_owner_meter: Option<&WeightMeter>, ) -> AdmissionDecision { - let Ok(classification) = Self::classify_actor_loaded(instance, run_state) else { + let current_step = step_plan.map(|plan| &plan.loaded_step.step).or_else(|| { + instance + .steps + .get(run_state.map_or(0, |run| run.cursor as usize)) + }); + let Ok(classification) = + Self::classify_actor_at_current_step(instance, run_state, current_step) + 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 let Some(reason) = classification.terminal_reason { + return Self::close_admission_decision(reason, complete_owner_meter.unwrap_or(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, - ) { + match pipeline_capacity.unwrap_or_else(|| { + Self::pipeline_capacity_sufficient(actor_id, ActorType::User, &instance.sovereign_account) + }) { Ok(true) => {} Ok(false) => { return Self::close_admission_decision( - instance, CloseReason::CycleAdmissionInsufficient, - meter, + complete_owner_meter.unwrap_or(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; } @@ -4464,6 +8642,25 @@ impl Pallet { if instance.cycle_state != CycleState::Idle || run_state.is_some() || step_plan.is_some() { return AdmissionDecision::Invariant; } + let opening_observation = if let Trigger::ObservationCrossing { feed, .. } = &instance.trigger + && IndexedTriggerDetectionDisabled::::contains_key(actor_id) + { + let current = T::ObservationProvider::current(feed); + match current { + CanonicalObservationState::Unavailable => { + let owner = CompleteEntryOwner::CrossingUnavailable; + let complete_meter = complete_owner_meter.unwrap_or(meter); + if !complete_meter.can_consume(Self::complete_entry_owner_weight(owner)) { + return AdmissionDecision::Defer; + } + return AdmissionDecision::Refuse(owner); + } + CanonicalObservationState::Uninitialized => return AdmissionDecision::Invariant, + CanonicalObservationState::Available { .. } => Some(current), + } + } else { + None + }; let zero_step_weight = Self::contract_steps_admission_weight_upper( instance.actor_class.actor_type(), &instance.steps, @@ -4473,36 +8670,35 @@ impl Pallet { } return AdmissionDecision::Admit { weight: zero_step_weight, - terminal_cleanup_reserved: true, + terminal_cleanup: TerminalCleanupReservation::Included, + opening_observation, }; } 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( + let terminal_cleanup = if Self::cycle_requires_terminal_cleanup_budget( instance, start_cursor, prior_unsuccessful_attempts_at_cursor, - ); + ) { + TerminalCleanupReservation::Included + } else { + TerminalCleanupReservation::NotIncluded + }; 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) - }); + .map(|plan| plan.loaded_step.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 { + let Some(step) = current_step else { return AdmissionDecision::Invariant; }; match Self::action_capacity_sufficient( @@ -4514,25 +8710,24 @@ impl Pallet { Ok(true) => {} Ok(false) => { return Self::close_admission_decision( - instance, CloseReason::CycleAdmissionInsufficient, - meter, + complete_owner_meter.unwrap_or(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 terminal_cleanup.is_included() { + current_step_weight = current_step_weight.saturating_add(Self::close_cleanup_weight_upper()); } if !meter.can_consume(current_step_weight) { return AdmissionDecision::Defer; } AdmissionDecision::Admit { weight: current_step_weight, - terminal_cleanup_reserved, + terminal_cleanup, + opening_observation: None, } } @@ -4541,7 +8736,7 @@ impl Pallet { actor_id: ActorId, ) -> Result>, ActorClassificationError> { - let state = match Self::load_actor_state(actor_id) { + let state = match Self::load_actor_state_for_frame_control(actor_id) { LoadedActorStateOf::NotRegistered => return Ok(ActorEligibility::NotRegistered), LoadedActorStateOf::Dormant(_) => return Ok(ActorEligibility::Dormant), LoadedActorStateOf::Active(state) => state, @@ -4750,7 +8945,7 @@ impl Pallet { source: Option<&T::AccountId>, provenance: Option<&FundingProvenance>, ) -> DispatchResult { - let state = match Self::load_actor_state(actor_id) { + let state = match Self::load_actor_state_for_frame_control(actor_id) { LoadedActorStateOf::NotRegistered | LoadedActorStateOf::Dormant(_) => return Ok(()), LoadedActorStateOf::Active(state) => state, LoadedActorStateOf::Corrupt => return Err(Error::::ActorInvariant.into()), @@ -4890,8 +9085,6 @@ impl Pallet { source, provenance, cause_provenance, - true, - true, ) }) } @@ -4903,10 +9096,8 @@ impl Pallet { source: Option<&T::AccountId>, provenance: Option<&FundingProvenance>, cause_provenance: TriggerCauseProvenance, - apply_trigger: bool, - apply_funding: bool, ) -> DispatchResult { - let state = match Self::load_actor_state(actor_id) { + let state = match Self::load_actor_state_for_frame_control(actor_id) { LoadedActorStateOf::NotRegistered | LoadedActorStateOf::Dormant(_) => return Ok(()), LoadedActorStateOf::Active(state) => state, LoadedActorStateOf::Corrupt => return Err(Error::::ActorInvariant.into()), @@ -4926,8 +9117,7 @@ impl Pallet { 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 signal_matched = if !instance.pending_signal && let Trigger::AddressEvent { source_filter, asset_filter, @@ -4938,7 +9128,7 @@ impl Pallet { } else { false }; - if apply_funding && amount > Zero::zero() { + if amount > Zero::zero() { if funding_authorized && funding.funding_tracked_assets.contains(&asset) { let accumulated = if let Some(accumulated) = funding.funding_accumulated.get_mut(&asset) { *accumulated = accumulated @@ -4953,7 +9143,7 @@ impl Pallet { amount }; ActorFunding::::insert(actor_id, funding); - Self::reconcile_actor_state_hold(actor_id)?; + Self::reconcile_actor_state_hold_with_authority(actor_id)?; Self::deposit_event(Event::FundingAccumulated { actor_id, asset, @@ -4969,7 +9159,7 @@ impl Pallet { TriggerFamily::AddressEvent, T::WeightInfo::address_event_trigger_occurrence(), ); - let _ = Self::try_commit_automatic_trigger_occurrence( + let _ = Self::try_commit_frame_automatic_trigger_occurrence( actor_id, actor_type, &instance.sovereign_account, @@ -4981,7 +9171,7 @@ impl Pallet { } pub(crate) fn evaluate_actor_liveness(actor_id: ActorId) -> DispatchResult { - let state = match Self::load_actor_state(actor_id) { + let state = match Self::load_actor_state_for_frame_control(actor_id) { LoadedActorStateOf::Active(state) => state, LoadedActorStateOf::NotRegistered | LoadedActorStateOf::Dormant(_) => { return Err(Error::::ActorNotFound.into()); diff --git a/template/pallets/actors/src/subscriptions.rs b/template/pallets/actors/src/subscriptions.rs index ad782cca..0bc5b5fd 100644 --- a/template/pallets/actors/src/subscriptions.rs +++ b/template/pallets/actors/src/subscriptions.rs @@ -509,7 +509,8 @@ impl Pallet { let slot = ObservationSubscriptionSlot::::get(actor_id).ok_or(TryRuntimeError::Other( "observation feed owner has no subscription slot", ))?; - if !owned_slots.insert(slot) + if slot >= next_slot + || !owned_slots.insert(slot) || ObservationSubscriptionSlotOwner::::get(slot) != Some(actor_id) { return Err(TryRuntimeError::Other( @@ -563,6 +564,12 @@ impl Pallet { let page = ObservationFreeSlotPages::::get(page_id).ok_or(TryRuntimeError::Other( "observation free-slot page is missing", ))?; + let expected_len = page_size.min(free_len.saturating_sub(page_id.saturating_mul(page_size))); + if page.len() as u32 != expected_len { + return Err(TryRuntimeError::Other( + "observation free-slot page length disagrees with its stack position", + )); + } for slot in page { if slot >= next_slot || !free_slots.insert(slot) @@ -664,6 +671,22 @@ impl Pallet { "observation occupied-page list bounds disagree", )); } + let mut current = Some(list.head); + for _ in 0..list.count { + let page_id = current.ok_or(TryRuntimeError::Other( + "observation occupied-page chain omits stored pages", + ))?; + current = ObservationSubscriberPages::::get(feed, page_id) + .ok_or(TryRuntimeError::Other( + "observation occupied-page chain has a missing page", + ))? + .next; + } + if current.is_some() { + return Err(TryRuntimeError::Other( + "observation occupied-page chain does not terminate at its declared count", + )); + } page_list_count = page_list_count .checked_add(1) .ok_or(TryRuntimeError::Other( @@ -690,15 +713,14 @@ impl Pallet { )); } 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 + let LoadedActorStateOf::Active(state) = Self::load_frame_actor_state(actor_id) else { + return Err(TryRuntimeError::Other( + "disabled indexed detector has no active Actor", + )); + }; + if !state.hot.pending_signal || !matches!( - contract.trigger, + state.contract.trigger, Trigger::ObservationChange { .. } | Trigger::ObservationCrossing { .. } ) { diff --git a/template/pallets/actors/src/tests.rs b/template/pallets/actors/src/tests.rs index 1e4090e6..55a9a72e 100644 --- a/template/pallets/actors/src/tests.rs +++ b/template/pallets/actors/src/tests.rs @@ -1,21 +1,21 @@ use crate::{ - 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, + ActiveLifecycle, ActorActivationPlacement, ActorClass, ActorClassification, + ActorClassificationError, ActorContract, ActorControlLocators, ActorEligibility, + ActorExecutionPhase, ActorFunding, ActorId, ActorIdentities, ActorReadyHead, ActorReadyOccupancy, + ActorReadyTail, 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, 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, + TimedPredicate, Trigger, TriggerFamily, TriggerRuntimeState, WakeupClock, WakeupKey, WakeupPage, + WakeupPointer, adapters::AssetOps, compose_attempt_fee_envelope, fee_native_protected_minimum, + mock::*, settle_attempt_fee_step, }; use alloc::collections::BTreeSet; @@ -167,8 +167,10 @@ type MockBlockNumber = polkadot_sdk::frame_system::pallet_prelude::BlockNumberFo 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"); + let admission = Actors::actor_control_cell(actor_id) + .expect("Actor admission certificate exists") + .1 + .admission; ActorRunAuthority { semantic_contract_id: admission.semantic_contract_id, body_commitment: admission.body_commitment, @@ -176,6 +178,66 @@ fn run_contract_authority(actor_id: ActorId) -> ActorRunAuthority<[u8; 32]> { } } +fn restore_structural_queue_tombstone(actor_id: ActorId) { + assert_eq!( + ActorControlLocators::::get(actor_id), + Some(crate::ActorControlLocation::Unsignaled) + ); + mutate_primary_control_cell(actor_id, |cell| cell.hot.pending_signal = false); + assert!( + Actors::actor_hot(actor_id) + .expect("invalidated Actor exists") + .queue_ticket + .is_none() + ); +} + +fn schedule_latched_service_wakeup(actor_id: ActorId, wakeup_block: MockBlockNumber) -> bool { + try_schedule_latched_service_wakeup(actor_id, wakeup_block).is_ok() +} + +fn try_schedule_latched_service_wakeup( + actor_id: ActorId, + wakeup_block: MockBlockNumber, +) -> Result<(), crate::scheduler::EnqueueOutcome> { + let (location, cell) = Actors::actor_control_cell(actor_id) + .ok_or(crate::scheduler::EnqueueOutcome::CorruptedTopology)?; + let (identity, mut hot, admission) = Actors::project_control_cell(&cell, location) + .ok_or(crate::scheduler::EnqueueOutcome::CorruptedTopology)?; + hot.pending_signal = true; + Actors::try_wakeup_substrate_schedule_transition_with_authority( + actor_id, + WakeupKey::Block(wakeup_block), + hot, + &identity, + cell.cursor, + &admission, + cell.resources, + ) +} + +fn enqueue_latched_actor(actor_id: ActorId) -> bool { + let Some(mut hot) = Actors::actor_hot(actor_id) else { + return false; + }; + if hot.queue_ticket.is_some() { + return Actors::paged_enqueue(actor_id); + } + hot.pending_signal = true; + let result: Result<(), DispatchError> = + polkadot_sdk::frame_support::storage::with_transaction(|| { + let result = Actors::preflight_paged_enqueue_cohort_with_authority(vec![(actor_id, hot)]) + .and_then(Actors::commit_paged_enqueue) + .map_err(|_| Error::::ActorInvariant.into()); + if result.is_ok() { + polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(result) + } else { + polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(result) + } + }); + result.is_ok() +} + fn scheduled_wakeup_block(actor_id: crate::ActorId) -> Option { Actors::actor_hot(actor_id).and_then(|hot| { hot @@ -188,43 +250,30 @@ fn scheduled_wakeup_block(actor_id: crate::ActorId) -> Option { }) } -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 canonical_scheduled_wakeup_block(actor_id: crate::ActorId) -> Option { + Actors::active_actor_view(actor_id).and_then(|actor| { + actor + .wakeup_pointer + .map(|pointer| match pointer.block { + WakeupKey::Block(block) => block, + WakeupKey::Tick(tick) => tick, + }) + .or_else(|| actor.trigger_wakeup_pointer.map(|pointer| pointer.tick)) + }) } fn seed_saturated_tombstone_queue() { - let page_size: u32 = ::QueuePageSize::get(); let capacity: u32 = ::MaxQueueLength::get(); - for page_id in 0..capacity.div_ceil(page_size) { - let first = page_id * page_size; - let len = page_size.min(capacity - first); - let entries = (0..len) - .map(|offset| { - queue_entry( - u64::from(first + offset), - 10_000_000 + u64::from(first + offset), - ) - }) - .collect::>(); - QueuePages::::insert( + for page_id in 0..capacity.div_ceil(32) { + crate::ActorReadyFrameChunks::::insert( u64::from(page_id), - BoundedVec::try_from(entries).expect("saturated queue page fits"), + crate::ActorControlChunkOf::::try_from(vec![None; 32]) + .expect("canonical tombstone page fits"), ); } - QueueHead::::put(0); - QueueTail::::put(u64::from(capacity)); - QueueOccupancy::::put(capacity); - crate::NextQueueTicket::::put(u64::from(capacity)); + crate::ActorReadyHead::::put(0); + crate::ActorReadyTail::::put(u64::from(capacity)); + crate::ActorReadyOccupancy::::put(0); } fn assert_plain_storage_type(entry: &StorageEntryMetadataIR) { @@ -314,7 +363,7 @@ fn prepare_crossing_pair_after_sparse_open() { } fn crossing_phase(actor_id: ActorId) -> CrossingPhase { - match ActorHot::::get(actor_id) + match Actors::active_actor_view(actor_id) .expect("active Crossing actor") .trigger_runtime_state { @@ -328,7 +377,7 @@ fn crossing_phase(actor_id: ActorId) -> CrossingPhase { } fn drain_crossing_work() -> u32 { - drain_crossing_work_with_limit(128) + drain_crossing_work_with_limit(512) } fn drain_crossing_work_with_limit(limit: u32) -> u32 { @@ -349,7 +398,7 @@ fn assert_on_idle_wakeup_insufficiency_preserves_state(wakeup_budget: Weight) { None, transfer_contract_steps(BOB, 1), ); - assert!(Actors::wakeup_substrate_schedule(actor_id, 10)); + assert!(schedule_latched_service_wakeup(actor_id, 10)); GlobalCircuitBreaker::::put(true); let hot_before = Actors::actor_hot(actor_id).expect("actor before bounded wakeup pass"); let bucket_before = Actors::wakeup_buckets(10).expect("due wakeup bucket"); @@ -611,11 +660,8 @@ fn age_fixture_control_clock(actor_id: ActorId) { frame_system::Pallet::::set_block_number(1); return; } - ActorIdentities::::mutate(actor_id, |maybe| { - maybe - .as_mut() - .expect("fixture actor identity exists") - .last_control_mutation_block = now.saturating_sub(1); + mutate_actor_identity_coherent(actor_id, |identity| { + identity.last_control_mutation_block = now.saturating_sub(1); }); } @@ -664,6 +710,40 @@ fn fund_native(actor_id: u64, amount: Balance) { let _ = >::deposit_creating(&actor_acc, amount); } +fn mutate_actor_hot_coherent( + actor_id: ActorId, + mutate: impl FnOnce(&mut crate::ActorHotStateOf), +) { + assert_ok!(Actors::try_mutate_control_hot( + actor_id, + Error::::ActorNotFound, + |hot| { + mutate(hot); + Ok(()) + } + )); +} + +fn mutate_actor_identity_coherent( + actor_id: ActorId, + mutate: impl FnOnce(&mut crate::ActorIdentityOf), +) { + assert_ok!(Actors::try_mutate_control_identity( + actor_id, + Error::::ActorNotFound, + |identity| { + mutate(identity); + Ok(()) + } + )); +} + +fn set_actor_cycle_nonce_coherent(actor_id: ActorId, cycle_nonce: u64) { + mutate_actor_identity_coherent(actor_id, |identity| { + identity.cycle_nonce = cycle_nonce; + }); +} + fn native_balance(who: &AccountId) -> Balance { >::free_balance(who) } @@ -824,7 +904,10 @@ fn run_idle(weight: 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) { + if !ActorControlLocators::::iter_keys() + .filter_map(Actors::actor_hot) + .any(|hot| hot.cycle_state == CycleState::Running) + { break; } let Some(next) = now.checked_add(1) else { @@ -843,7 +926,7 @@ fn starvation_observation_weight() -> Weight { } /// Proof-limited on_idle budget that admits the wakeup cursor, queue scan, hot/contract probes, and -/// the head consume, but not the actor's full cycle admission. This materializes a live FIFO head +/// the head consume, but not the actor's current Step admission. This materializes a live FIFO head /// blocked by weight with no admitted attempt, the only spec 8.6.3 starvation trigger. fn starvation_blocked_budget(actor_id: u64) -> Weight { let base = starvation_observation_weight(); @@ -852,15 +935,14 @@ fn starvation_blocked_budget(actor_id: u64) -> Weight { let state_probe = Actors::scheduler_actor_state_probe_weight_upper(); let consume = ::scheduler_paged_consume_preserve_page() .max(::scheduler_paged_consume_delete_page()); - let instance = Actors::active_actor_view(actor_id).expect("actor exists"); - let cycle = - Actors::compute_cycle_weight_upper(instance.actor_class.actor_type(), &instance.steps); + let (_, cell) = Actors::actor_control_cell(actor_id).expect("current control owner exists"); + let step = cell.resources.control.saturating_add(cell.resources.effect); let full = base .saturating_add(cursor) .saturating_add(scan) .saturating_add(state_probe) .saturating_add(consume) - .saturating_add(cycle); + .saturating_add(step); Weight::from_parts(u64::MAX, full.proof_size().saturating_sub(1)) } @@ -946,8 +1028,15 @@ fn mixed_materialization_ticket_trace() -> Vec { frame_system::Pallet::::set_block_number(2); run_idle(Weight::MAX); - let mut trace = QueuePages::::iter() - .flat_map(|(_, page)| page.into_iter().map(|entry| (entry.ticket, entry.actor_id))) + let mut trace = crate::ActorReadyFrameChunks::::iter() + .flat_map(|(page_id, page)| { + page + .into_iter() + .enumerate() + .filter_map(move |(slot, cell)| { + cell.map(|cell| (page_id * 32 + slot as u64, cell.actor_id)) + }) + }) .collect::>(); trace.sort_unstable_by_key(|(ticket, _)| *ticket); let actor_trace = trace @@ -1004,22 +1093,15 @@ fn assert_scheduler_close_requires_atomic_budget(reason: CloseReason, shortfall: } CloseReason::CycleNonceExhausted => { fund_native(actor_id, 1_000); - ActorIdentities::::mutate(actor_id, |maybe| { - maybe.as_mut().expect("actor identity exists").cycle_nonce = u64::MAX - 1; - }); + mutate_actor_identity_coherent(actor_id, |identity| identity.cycle_nonce = u64::MAX - 1); } CloseReason::ConsecutiveFailures => { - ActorHot::::mutate(actor_id, |maybe| { - maybe - .as_mut() - .expect("actor hot state exists") - .unsuccessful_attempt_streak = ::MaxConsecutiveFailures::get(); + mutate_actor_hot_coherent(actor_id, |hot| { + hot.unsuccessful_attempt_streak = ::MaxConsecutiveFailures::get(); }); } CloseReason::AutoCloseNonceReached => { - ActorIdentities::::mutate(actor_id, |maybe| { - maybe.as_mut().expect("actor identity exists").cycle_nonce = 1; - }); + mutate_actor_identity_coherent(actor_id, |identity| identity.cycle_nonce = 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)); @@ -1031,9 +1113,7 @@ fn assert_scheduler_close_requires_atomic_budget(reason: CloseReason, shortfall: actor_id )); if reason == CloseReason::CycleNonceExhausted { - ActorIdentities::::mutate(actor_id, |maybe| { - maybe.as_mut().expect("actor identity exists").cycle_nonce = u64::MAX; - }); + mutate_actor_identity_coherent(actor_id, |identity| identity.cycle_nonce = u64::MAX); } if reason == CloseReason::WindowExpired { let mut contract = Actors::load_actor_contract(actor_id).expect("actor contract exists"); @@ -1042,11 +1122,11 @@ fn assert_scheduler_close_requires_atomic_budget(reason: CloseReason, shortfall: } let before = Actors::actor_hot(actor_id) .unwrap_or_else(|| panic!("{reason:?} actor remains active before scheduler admission")); - let queue_head = QueueHead::::get(); + let queue_head = ActorReadyHead::::get(); frame_system::Pallet::::reset_events(); let discovery = ::scheduler_paged_tombstone_drain(1); - // Wakeups drain in the on_idle phase before execute_cycle, so the execute_cycle - // admission budget covers only the queue discovery and actor probes plus the close. + // Discovery and the loaded-state probe are provisional for a complete FIFO close. The + // accepted close owner replaces them and source consumption instead of being added to them. let pre_admission = discovery.saturating_add(Actors::scheduler_actor_state_probe_weight_upper()); let close = if reason == CloseReason::CycleAdmissionInsufficient { @@ -1054,23 +1134,31 @@ fn assert_scheduler_close_requires_atomic_budget(reason: CloseReason, shortfall: } else { Actors::close_cleanup_weight_upper() }; - let consume = ::scheduler_paged_consume_preserve_page() - .max(::scheduler_paged_consume_delete_page()); - let budget = pre_admission - .saturating_add(close) - .saturating_add(consume) - .saturating_sub(shortfall); + let budget = close.saturating_sub(shortfall); let consumed = Actors::execute_cycle(budget).consumed; let after = Actors::actor_hot(actor_id).expect("incomplete atomic close budget preserves actor"); assert_eq!(after.queue_ticket, before.queue_ticket); - assert_eq!(QueueHead::::get(), queue_head); + assert_eq!(ActorReadyHead::::get(), queue_head); assert!(!has_actor_event(|event| matches!( event, Event::ActorClosed { actor_id: id, .. } if *id == actor_id ))); - assert_eq!(consumed, pre_admission); + assert!(consumed.all_lte(pre_admission)); assert!(consumed.all_lte(budget)); + + let consumed = Actors::execute_cycle(Weight::MAX).consumed; + assert_eq!( + consumed, + close.saturating_add(discovery), + "the complete entry owner replaces its provisional prefix; the later empty-queue probe remains a distinct pass operation" + ); + assert!(Actors::active_actor_view(actor_id).is_none()); + assert!(has_actor_event(|event| matches!( + event, + Event::ActorClosed { actor_id: id, reason: closed_reason } + if *id == actor_id && *closed_reason == reason + ))); }); } @@ -1083,11 +1171,10 @@ mod proptest_actor { run_prepass, set_asset_balance, setup_pool, setup_temporary_retry_pool, sovereign_account, }; use crate::{ - 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::*, + ActorControlLocators, ActorFunding, ActorIdentities, ActorReadyOccupancy, ActorRunStateStore, + AmountResolution, AssetFilter, CrossingDirection, CrossingPhase, CrossingTransition, + CycleState, Event, FundingSourcePolicy, Mutability, ObservationCrossing, SourceFilter, + StepErrorPolicy, StepOf, SystemSovereignState, SystemSovereigns, Task, Trigger, mock::*, }; use codec::Encode; use polkadot_sdk::frame_support::{ @@ -1467,14 +1554,15 @@ mod proptest_actor { tracked_accounts: &std::collections::BTreeSet, conserved_total: Balance, ) { - let hot_ids: std::collections::BTreeSet<_> = ActorHot::::iter_keys().collect(); + let hot_ids: std::collections::BTreeSet<_> = + ActorControlLocators::::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 dormant_ids: std::collections::BTreeSet<_> = ActorIdentities::::iter_keys().collect(); + assert!(hot_ids.is_disjoint(&dormant_ids)); let identity_ids: std::collections::BTreeSet<_> = - ActorIdentities::::iter_keys().collect(); - let dormant_ids: std::collections::BTreeSet<_> = - identity_ids.difference(&hot_ids).copied().collect(); + hot_ids.union(&dormant_ids).copied().collect(); let run_ids: std::collections::BTreeSet<_> = ActorRunStateStore::::iter_keys().collect(); assert_eq!(hot_ids, contract_ids); assert_eq!(hot_ids, funding_ids); @@ -1482,12 +1570,45 @@ mod proptest_actor { 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()); + let primary_ids: Vec<_> = crate::ActorUnsignaledControlCells::::iter_values() + .map(|cell| cell.actor_id) + .chain( + crate::ActorReadyFrameChunks::::iter_values() + .flat_map(|page| page.into_iter().flatten().map(|cell| cell.actor_id)), + ) + .chain( + crate::ActorWaitingFrameChunks::::iter_values().flat_map(|page| { + page + .entries + .into_iter() + .flatten() + .filter_map(|entry| entry.into_primary().map(|cell| cell.actor_id)) + }), + ) + .collect(); + assert_eq!( + primary_ids.len(), + hot_ids.len(), + "each active Actor has exactly one primary" + ); + assert_eq!( + primary_ids + .into_iter() + .collect::>(), + hot_ids + ); + let run_payload_ids: std::collections::BTreeSet<_> = + crate::ActorRunPayloads::::iter_keys().collect(); + assert_eq!( + run_ids, run_payload_ids, + "Run head and payload inventories agree" + ); let mut live_tickets = std::collections::BTreeSet::new(); let mut live_wakeups = std::collections::BTreeSet::new(); for actor_id in &hot_ids { - let hot = ActorHot::::get(actor_id).expect("hot key resolves"); - let identity = ActorIdentities::::get(actor_id).expect("identity key resolves"); + let hot = Actors::actor_hot(*actor_id).expect("primary hot state resolves"); + let identity = Actors::actor_identity(*actor_id).expect("primary identity resolves"); assert_eq!( Actors::sovereign_index(&identity.sovereign_account), Some(*actor_id) @@ -1526,11 +1647,9 @@ mod proptest_actor { live_tickets.insert(ticket), "duplicate live queue ticket {ticket}" ); - let resolves = QueuePages::::iter().any(|(_, page)| { - page - .iter() - .any(|entry| entry.ticket == ticket && entry.actor_id == *actor_id) - }); + let resolves = crate::ActorReadyFrameChunks::::get(ticket / 32) + .and_then(|page| page.get((ticket % 32) as usize).cloned().flatten()) + .is_some_and(|cell| cell.actor_id == *actor_id); assert!(resolves, "live ticket resolves inside the canonical FIFO"); } if let Some(pointer) = hot.wakeup_pointer { @@ -1538,13 +1657,28 @@ mod proptest_actor { live_wakeups.insert((pointer.block, pointer.page_id, pointer.slot)), "duplicate live wakeup pointer" ); - let page = WakeupPages::::get((pointer.block, pointer.page_id)) + let page = crate::ActorWaitingFrameChunks::::get((pointer.block, pointer.page_id)) .expect("live wakeup page exists"); assert_eq!( - page.entries.get(pointer.slot as usize), - Some(&Some(crate::WakeupEntry { - actor_id: *actor_id - })) + page + .entries + .get(pointer.slot as usize) + .and_then(Option::as_ref) + .map(|entry| match entry { + crate::ActorWaitingEntry::Primary(cell) => cell.actor_id, + crate::ActorWaitingEntry::Reference(reference) => { + assert_eq!( + reference.admission_identity, + Actors::actor_control_cell(*actor_id) + .expect("primary admission") + .1 + .admission + .admission_identity + ); + reference.actor_id + } + }), + Some(*actor_id) ); } if let Some(pointer) = hot.trigger_wakeup_pointer { @@ -1553,13 +1687,28 @@ mod proptest_actor { live_wakeups.insert((key, pointer.page_id, pointer.slot)), "duplicate live Trigger wakeup pointer" ); - let page = WakeupPages::::get((key, pointer.page_id)) + let page = crate::ActorWaitingFrameChunks::::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 - })) + page + .entries + .get(pointer.slot as usize) + .and_then(Option::as_ref) + .map(|entry| match entry { + crate::ActorWaitingEntry::Primary(cell) => cell.actor_id, + crate::ActorWaitingEntry::Reference(reference) => { + assert_eq!( + reference.admission_identity, + Actors::actor_control_cell(*actor_id) + .expect("primary admission") + .1 + .admission + .admission_identity + ); + reference.actor_id + } + }), + Some(*actor_id) ); } } @@ -1569,7 +1718,7 @@ mod proptest_actor { Actors::sovereign_index(&identity.sovereign_account), Some(*actor_id) ); - assert!(ActorHot::::get(actor_id).is_none()); + assert!(Actors::actor_hot(*actor_id).is_none()); } if let Some(actor_id) = system_id { @@ -1578,7 +1727,7 @@ mod proptest_actor { SystemSovereigns::::get(actor_id), Some(SystemSovereignState::Vacant) ); - assert!(ActorHot::::get(actor_id).is_none()); + assert!(Actors::actor_hot(actor_id).is_none()); assert!(ActorIdentities::::get(actor_id).is_none()); if let Some(sovereign) = system_sovereign { assert_eq!(Actors::sovereign_index(sovereign), None); @@ -1741,13 +1890,13 @@ mod proptest_actor { ); actor_ids.push((actor_id, owner)); } - let after_create = ActorHot::::iter_keys().count(); + let after_create = ActorControlLocators::::iter_keys().count(); let close_count = closes.min(creates); for i in 0..close_count { let (actor_id, owner) = actor_ids[i as usize]; assert_ok!(Actors::close_actor(RuntimeOrigin::signed(owner), actor_id)); } - let after_close = ActorHot::::iter_keys().count(); + let after_close = ActorControlLocators::::iter_keys().count(); (after_create, after_close, (creates - close_count) as usize) }); prop_assert_eq!(active_after_create, creates as usize); @@ -1777,19 +1926,17 @@ mod proptest_actor { let actor_id = create_system_with(ALICE, manual_schedule(), None, inert_contract_steps()); let rejected = match corruption { 0 => { - assert!(Actors::wakeup_substrate_schedule(actor_id, 10)); - WakeupBuckets::::mutate(crate::WakeupKey::Block(10), |maybe_bucket| { - maybe_bucket.as_mut().expect("bucket").cursor_index = None; - }); + assert!(super::schedule_latched_service_wakeup(actor_id, 10)); + crate::ActorWaitingCursorIndices::::remove(crate::WakeupKey::Block(10)); let before = polkadot_sdk::sp_io::storage::root(StateVersion::V1); let rejected = Actors::try_wakeup_substrate_schedule_inner(actor_id, 20).is_err(); prop_assert_eq!(polkadot_sdk::sp_io::storage::root(StateVersion::V1), before); rejected } 1 => { - assert!(Actors::wakeup_substrate_schedule(actor_id, 10)); - ActorHot::::mutate(actor_id, |maybe_hot| { - maybe_hot.as_mut().expect("hot").wakeup_pointer + assert!(super::schedule_latched_service_wakeup(actor_id, 10)); + super::mutate_primary_control_cell(actor_id, |cell| { + cell.hot.wakeup_pointer .as_mut().expect("pointer").slot = 7; }); let before = polkadot_sdk::sp_io::storage::root(StateVersion::V1); @@ -1798,28 +1945,26 @@ mod proptest_actor { rejected } 2 => { - assert!(Actors::wakeup_substrate_schedule(actor_id, 10)); - WakeupBuckets::::mutate(crate::WakeupKey::Block(10), |maybe_bucket| { - maybe_bucket.as_mut().expect("bucket").live_entries = 0; - }); + assert!(super::schedule_latched_service_wakeup(actor_id, 10)); + crate::ActorWaitingOccupancies::::insert(crate::WakeupKey::Block(10), 0); let before = polkadot_sdk::sp_io::storage::root(StateVersion::V1); let rejected = Actors::try_wakeup_substrate_schedule_inner(actor_id, 20).is_err(); prop_assert_eq!(polkadot_sdk::sp_io::storage::root(StateVersion::V1), before); rejected } 3 => { - assert!(Actors::paged_enqueue(actor_id)); + assert!(super::enqueue_latched_actor(actor_id)); assert!(Actors::paged_invalidate(actor_id).is_some()); - QueueOccupancy::::put(0); + ActorReadyOccupancy::::put(2); let before = polkadot_sdk::sp_io::storage::root(StateVersion::V1); let rejected = Actors::paged_drain_tombstones(Actors::next_queue_ticket(), 1).is_err(); prop_assert_eq!(polkadot_sdk::sp_io::storage::root(StateVersion::V1), before); rejected } _ => { - assert!(Actors::paged_enqueue(actor_id)); + assert!(super::enqueue_latched_actor(actor_id)); assert!(Actors::paged_invalidate(actor_id).is_some()); - QueuePages::::remove(0); + crate::ActorReadyFrameChunks::::remove(0); let before = polkadot_sdk::sp_io::storage::root(StateVersion::V1); let rejected = Actors::paged_drain_tombstones(Actors::next_queue_ticket(), 1).is_err(); prop_assert_eq!(polkadot_sdk::sp_io::storage::root(StateVersion::V1), before); @@ -1891,9 +2036,11 @@ mod proptest_actor { for (index, operation) in operations.iter().enumerate() { let block = (index as u64).saturating_add(2); frame_system::Pallet::::set_block_number(block); - let before_hot: std::collections::BTreeMap<_, _> = ActorHot::::iter().collect(); + let before_hot: std::collections::BTreeMap<_, _> = ActorControlLocators::::iter_keys() + .map(|id| (id, Actors::actor_hot(id).expect("active primary"))).collect(); let before_identities: std::collections::BTreeMap<_, _> = - ActorIdentities::::iter().collect(); + ActorIdentities::::iter().chain(ActorControlLocators::::iter_keys() + .map(|id| (id, Actors::actor_identity(id).expect("active identity")))).collect(); let before_continuation = ActorRunStateStore::::get(system_id); let before_funding = ActorFunding::::get(system_id); let before_system_balance = Balances::free_balance(system_sovereign); @@ -1905,21 +2052,21 @@ mod proptest_actor { ModelOp::Activate if !closed && ActorIdentities::::contains_key(system_id) - && !ActorHot::::contains_key(system_id) => { + && !ActorControlLocators::::contains_key(system_id) => { let _ = Actors::activate_actor( RuntimeOrigin::root(), system_id, system_contract(Trigger::manual()).expect("direct Actor Contract"), ); } - ModelOp::Deactivate if !closed && ActorHot::::contains_key(system_id) => { + ModelOp::Deactivate if !closed && ActorControlLocators::::contains_key(system_id) => { let _ = Actors::deactivate_actor(RuntimeOrigin::root(), system_id); } ModelOp::Fund if !closed => { - let recipient = ActorIdentities::::get(system_id) + let recipient = Actors::actor_identity(system_id) .map(|identity| identity.sovereign_account); if let Some(recipient) = recipient { - if ActorHot::::contains_key(system_id) { + if ActorControlLocators::::contains_key(system_id) { let provenance = crate::FundingProvenance::Signed; if Actors::preflight_funding_event( system_id, @@ -1953,7 +2100,7 @@ mod proptest_actor { } } } - ModelOp::Signal if !closed && ActorHot::::contains_key(system_id) => { + ModelOp::Signal if !closed && ActorControlLocators::::contains_key(system_id) => { let schedule = Schedule { trigger: Trigger::address_event( SourceFilter::Any, @@ -1962,7 +2109,7 @@ mod proptest_actor { cooldown_blocks: 0, }; let _ = update_contract_partial!(RuntimeOrigin::root(), system_id, schedule, None); - if let Some(identity) = ActorIdentities::::get(system_id) { + if let Some(identity) = Actors::actor_identity(system_id) { let provenance = crate::FundingProvenance::Signed; if Actors::preflight_funding_event( system_id, @@ -1989,17 +2136,17 @@ mod proptest_actor { } } ModelOp::ManualTrigger | ModelOp::Enqueue - if !closed && ActorHot::::contains_key(system_id) => + if !closed && ActorControlLocators::::contains_key(system_id) => { let _ = Actors::manual_trigger(RuntimeOrigin::root(), system_id); } - ModelOp::Pause if !closed && ActorHot::::contains_key(system_id) => { + ModelOp::Pause if !closed && ActorControlLocators::::contains_key(system_id) => { let _ = Actors::pause_actor(RuntimeOrigin::root(), system_id); } - ModelOp::Resume if !closed && ActorHot::::contains_key(system_id) => { + ModelOp::Resume if !closed && ActorControlLocators::::contains_key(system_id) => { let _ = Actors::resume_actor(RuntimeOrigin::root(), system_id); } - ModelOp::UpdateContract if !closed && ActorHot::::contains_key(system_id) => { + ModelOp::UpdateContract if !closed && ActorControlLocators::::contains_key(system_id) => { let _ = update_contract_partial!( RuntimeOrigin::root(), system_id, @@ -2007,11 +2154,11 @@ mod proptest_actor { crate::CompletionPolicy::Persistent, ); } - ModelOp::Wakeup if !closed && ActorHot::::contains_key(system_id) => { + ModelOp::Wakeup if !closed && ActorControlLocators::::contains_key(system_id) => { let schedule = timer_schedule_pt(2); let _ = update_contract_partial!(RuntimeOrigin::root(), system_id, schedule, None); } - ModelOp::UpdateCrossing if !closed && ActorHot::::contains_key(system_id) => { + ModelOp::UpdateCrossing if !closed && ActorControlLocators::::contains_key(system_id) => { let schedule = Schedule { trigger: Trigger::observation_crossing( 9, @@ -2047,10 +2194,10 @@ mod proptest_actor { let _ = Actors::on_idle(block, Weight::MAX); } ModelOp::Suspend if !closed => { - if let Some(hot) = ActorHot::::get(system_id) + if let Some(hot) = Actors::actor_hot(system_id) && !hot.lifecycle.is_paused() { - let identity = ActorIdentities::::get(system_id) + let identity = Actors::actor_identity(system_id) .expect("active actor identity exists"); let _ = update_contract_partial!( RuntimeOrigin::root(), @@ -2162,7 +2309,7 @@ mod proptest_actor { }), "unexpected control-event delta: {actor_event_delta:?}"); } if !closed - && !ActorHot::::contains_key(system_id) + && !ActorControlLocators::::contains_key(system_id) && !ActorIdentities::::contains_key(system_id) && SystemSovereigns::::get(system_id) == Some(SystemSovereignState::Vacant) { @@ -2188,7 +2335,7 @@ mod proptest_actor { } if matches!(operation, ModelOp::Pause | ModelOp::Resume) && before_continuation.is_some() - && ActorHot::::contains_key(system_id) + && ActorControlLocators::::contains_key(system_id) { assert_eq!( after_continuation.as_ref().map(Encode::encode), @@ -2199,7 +2346,7 @@ mod proptest_actor { before_continuation.as_ref(), after_continuation.as_ref(), before_identities.get(&system_id), - ActorIdentities::::get(system_id), + Actors::actor_identity(system_id), ) && previous_identity.cycle_nonce == current_identity.cycle_nonce { assert!(after.cursor >= before.cursor); @@ -2211,9 +2358,9 @@ mod proptest_actor { for (actor_id, previous) in &before_hot { if let (Some(_hot), Some(previous_identity), Some(identity)) = ( - ActorHot::::get(actor_id), + Actors::actor_hot(*actor_id), before_identities.get(actor_id), - ActorIdentities::::get(actor_id), + Actors::actor_identity(*actor_id), ) { assert!(identity.cycle_nonce <= previous_identity.cycle_nonce.saturating_add(1)); if matches!(operation, ModelOp::Execute) && previous.lifecycle.is_paused() { @@ -2652,6 +2799,14 @@ fn parity_expected_steps(case: StepParityCase) -> Vec { steps } +fn ample_simulation_budget() -> crate::SimulationBudget { + let half = Weight::from_parts(u64::MAX / 2, u64::MAX / 2); + crate::SimulationBudget { + actor_control: half, + shared_economic: half, + } +} + fn observed_attempt_projection( actor_id: ActorId, ) -> (AttemptDisposition, OutcomeTotals, Option, Option) { @@ -2684,7 +2839,14 @@ fn observed_attempt_projection( } if let Some(continuation) = Actors::actor_run_state(actor_id) { return ( - AttemptDisposition::Suspended, + match Actors::actor_hot(actor_id) + .expect("persisted Run has canonical hot state") + .cycle_state + { + CycleState::Running => AttemptDisposition::Continued, + CycleState::Suspended => AttemptDisposition::Suspended, + CycleState::Idle => panic!("Idle cannot own a Run"), + }, continuation.cumulative_outcomes, Some(continuation.cursor), Some(continuation.unsuccessful_attempts_at_cursor), @@ -2712,6 +2874,8 @@ fn active_eligibility(actor_id: ActorId) -> ActorClassification { } } +#[cfg(feature = "runtime-benchmarks")] +mod candidate_geometry; mod core; mod crossing; mod execution; @@ -2721,4 +2885,41 @@ mod market_tasks; mod observations; mod scheduling; mod storage_and_api; +#[cfg(feature = "runtime-benchmarks")] +mod waiting_integrity; mod wakeups; + +/// Change only the already-located canonical primary, including deliberate corruption. +/// This fixture helper neither repairs topology nor relocates authority. +fn mutate_primary_control_cell( + actor_id: ActorId, + mutate: impl FnOnce(&mut crate::ActorControlCellOf), +) { + let (location, mut cell) = + Actors::actor_control_cell(actor_id).expect("canonical primary fixture"); + mutate(&mut cell); + match location { + crate::ActorControlLocation::Unsignaled => { + crate::ActorUnsignaledControlCells::::insert(actor_id, cell); + } + crate::ActorControlLocation::Ready { ticket } => { + crate::ActorReadyFrameChunks::::mutate(ticket / 32, |page| { + *page + .as_mut() + .expect("Ready page fixture") + .get_mut((ticket % 32) as usize) + .expect("Ready slot fixture") = Some(cell); + }); + } + crate::ActorControlLocation::Waiting { key, page, slot } => { + crate::ActorWaitingFrameChunks::::mutate((key, page), |page| { + *page + .as_mut() + .expect("Waiting page fixture") + .entries + .get_mut(usize::from(slot)) + .expect("Waiting slot fixture") = Some(crate::ActorWaitingEntry::Primary(cell)); + }); + } + } +} diff --git a/template/pallets/actors/src/tests/candidate_geometry.rs b/template/pallets/actors/src/tests/candidate_geometry.rs new file mode 100644 index 00000000..57315990 --- /dev/null +++ b/template/pallets/actors/src/tests/candidate_geometry.rs @@ -0,0 +1,5146 @@ +use super::*; +use crate::{ + ActiveActorCount, ActorContractHeads, ActorIdentityCount, ActorStateHolds, QueueTicket, +}; +use codec::{Compact, Encode, MaxEncodedLen}; +use polkadot_sdk::sp_weights::Weight; + +const ACTOR_CONTROL_TRANSITION_ORACLE: &str = + include_str!("fixtures/actor_control_transition_oracle_v1.tsv"); + +fn emit_baseline_oracle(name: &str, value: &T) { + let trace = format!("{value:?}"); + let digest = polkadot_sdk::sp_io::hashing::blake2_256(trace.as_bytes()); + let actual = digest + .iter() + .map(|byte| format!("{byte:02x}")) + .collect::(); + let expected = ACTOR_CONTROL_TRANSITION_ORACLE + .lines() + .filter(|line| !line.starts_with('#')) + .filter_map(|line| line.split_once('|')) + .find_map(|(scenario, digest)| (scenario == name).then_some(digest)) + .unwrap_or_else(|| panic!("missing immutable reference oracle scenario: {name}")); + assert_eq!(actual, expected, "immutable reference oracle drift: {name}"); +} + +#[test] +fn control_baseline_transition_oracle_fixture_is_complete_and_unique() { + let expected = [ + "address_event_collection_failure", + "address_event_success", + "at_time_collection_failure", + "at_time_success", + "cadenced_success", + "direct_funding_unavailable", + "direct_stop_cycle", + "direct_temporary_failure", + "direct_transfer_p0_match_true", + "direct_transfer_p2_match_false", + "direct_transfer_p2_match_true", + "direct_transfer_p4_match_true", + "funding_retry_fails_false", + "funding_retry_fails_true", + "manual_collection_failure", + "manual_success", + "middle_running_step", + "observation_change_collection_failure", + "observation_change_success", + "observation_crossing_collection_failure", + "observation_crossing_success", + "temporary_retry_fails_false", + "temporary_retry_fails_true", + "terminal_running_step", + "zero_step_user_opening", + ]; + let entries = ACTOR_CONTROL_TRANSITION_ORACLE + .lines() + .filter(|line| !line.starts_with('#')) + .map(|line| line.split_once('|').expect("oracle row has one delimiter")) + .collect::>(); + assert_eq!(entries.len(), expected.len()); + assert_eq!( + entries.iter().map(|(name, _)| *name).collect::>(), + expected + ); + for (_, digest) in entries { + assert_eq!(digest.len(), 64); + assert!(digest.bytes().all(|byte| byte.is_ascii_hexdigit())); + } +} + +/// Minimum frame frame-owned strip needed to reject a non-executable FIFO head before loading its +/// Contract, funding, Predicate, run payload, or adapter state. This is an experiment-only encoded +/// geometry model, not accepted runtime storage. +#[derive(Clone, Encode, MaxEncodedLen)] +struct AdmissionCellModel { + actor_id: ActorId, + sovereign_account: [u8; 32], + actor_type: ActorType, + lifecycle: ActiveLifecycle, + cycle_state: CycleState, + cycle_nonce: u64, + cursor: u32, + eligible_at: u32, + contract_commitment: crate::ActorContractCommitment<[u8; 32]>, + resources: crate::ActorStepResourceEnvelope, + terminal_at: Option, + last_cycle_block: Option, + last_attempt_block: Option, + pending_signal: bool, +} + +fn bounded_vec_prefix_bytes(bound: u32) -> usize { + Compact(bound).encoded_size() +} + +#[test] +fn control_frame_admission_strip_chunk_sweep_preserves_target_headroom() { + const A0_QUEUE_PAGE_SIZE: u32 = 64; + const A0_QUEUE_PAGE_MAX_BYTES: usize = 6_170; + const TRIE_PROOF_OVERHEAD_BYTES: usize = 8_645 - A0_QUEUE_PAGE_MAX_BYTES; + const TARGET_PROOF_PER_COMMIT: usize = 6_361; + const TARGET_COMMITS: usize = 100; + + let baseline_cell_bytes = crate::QueueEntry::::max_encoded_len(); + let baseline_modeled_page_bytes = bounded_vec_prefix_bytes(A0_QUEUE_PAGE_SIZE) + .saturating_add(baseline_cell_bytes.saturating_mul(A0_QUEUE_PAGE_SIZE as usize)); + + let frame_cell_bytes = AdmissionCellModel::max_encoded_len(); + let full_cell_bytes = crate::ActorControlCell::< + [u8; 32], + u32, + crate::ActorAdmissionCertificate<()>, + >::max_encoded_len(); + assert!( + frame_cell_bytes <= 256, + "minimum frame admission strip already exceeds its prepared compactness ceiling" + ); + assert!( + full_cell_bytes <= 400, + "full single-owner frame cell exceeds its prototype compactness ceiling" + ); + + for chunk_size in [4usize, 8, 16, 32, 64] { + let chunk_bytes = bounded_vec_prefix_bytes(chunk_size as u32) + .saturating_add(frame_cell_bytes.saturating_mul(chunk_size)); + let estimated_chunk_proof = TRIE_PROOF_OVERHEAD_BYTES.saturating_add(chunk_bytes); + let full_chunk_proof_per_cell = estimated_chunk_proof.div_ceil(chunk_size); + let chunks_for_target = TARGET_COMMITS.div_ceil(chunk_size); + let conservative_target_window_proof = estimated_chunk_proof.saturating_mul(chunks_for_target); + let conservative_target_proof_per_commit = + conservative_target_window_proof.div_ceil(TARGET_COMMITS); + + assert!(full_chunk_proof_per_cell < TARGET_PROOF_PER_COMMIT); + assert!(conservative_target_proof_per_commit < TARGET_PROOF_PER_COMMIT); + println!( + "ACTOR_C1_STATIC_GEOMETRY_V1 chunkSize={chunk_size} a0CellBytes={baseline_cell_bytes} a0ModeledPageBytes={baseline_modeled_page_bytes} a0GeneratedPageMaxBytes={A0_QUEUE_PAGE_MAX_BYTES} c1CellBytes={frame_cell_bytes} fullCellBytes={full_cell_bytes} chunkBytes={chunk_bytes} estimatedChunkProof={estimated_chunk_proof} fullChunkProofPerCell={full_chunk_proof_per_cell} targetWindowChunks={chunks_for_target} targetWindowProofPerCommit={conservative_target_proof_per_commit}" + ); + } +} + +#[test] +fn control_ticket_address_is_total_contiguous_and_order_preserving() { + for chunk_size in [4u64, 8, 16, 32, 64] { + for base_ticket in [0u64, 1, 63, 64, 65, 9_999, u32::MAX as u64] { + for offset in 0..=128u64 { + let ticket = base_ticket.saturating_add(offset); + let chunk = ticket / chunk_size; + let slot = ticket % chunk_size; + assert_eq!( + chunk + .checked_mul(chunk_size) + .and_then(|value| value.checked_add(slot)), + Some(ticket) + ); + assert!(slot < chunk_size); + } + } + } +} + +type C1Cell = crate::ActorControlCellOf; +type C1Location = crate::ActorControlLocation; + +fn frame_cell(actor_id: ActorId, eligible_at: MockBlockNumber) -> C1Cell { + crate::ActorControlCell { + actor_id, + identity: crate::ActorControlIdentity { + owner: actor_id.saturating_add(2_000_000), + actor_class: ActorClass::User { owner_slot: 0 }, + mutability: Mutability::Mutable, + cycle_nonce: 0, + last_control_mutation_block: 0, + }, + hot: crate::ActorControlHotState { + lifecycle: ActiveLifecycle::Active, + cycle_state: CycleState::Idle, + trigger_runtime_state: TriggerRuntimeState::Stateless, + unsuccessful_attempt_streak: 0, + pending_signal: true, + wakeup_pointer: None, + trigger_wakeup_pointer: None, + terminal_at: None, + schedule_anchor: 0, + last_cycle_block: None, + }, + cursor: 0, + eligible_at: Some(eligible_at), + admission: crate::ActorAdmissionCertificate::>::new( + [1u8; 32], + [2u8; 32], + 1, + [3u8; 32], + 1, + [4u8; 32], + Weight::from_parts(1, 1), + ), + resources: crate::ActorStepResourceEnvelope { + control: Weight::from_parts(10, 10), + effect: Weight::zero(), + }, + } +} + +fn frame_install_direct_ready(step: StepOf, count: u32) -> Vec { + frame_system::Pallet::::set_block_number(1); + let mut actor_ids = Vec::with_capacity(count as usize); + for _ in 0..count { + let actor_id = create_user_with( + ALICE, + Mutability::Mutable, + manual_schedule(), + None, + contract_steps_with_step(step.clone()), + ); + let sovereign = Actors::active_actor_view(actor_id) + .expect("direct-prefix User exists") + .sovereign_account; + let _ = ::AssetOps::mint( + &sovereign, + ::FeeNativeAssetId::get(), + u128::from(u64::MAX / 4), + ); + actor_ids.push(actor_id); + } + for actor_id in actor_ids.iter().copied() { + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + let (state, admission, loaded_step) = Actors::load_current_step_service_state(actor_id) + .expect("direct-prefix Ready state is coherent"); + let queue_ticket = state.hot.queue_ticket.expect("direct-prefix ticket exists"); + let ticket = Actors::build_actor_step_ticket( + actor_id, + queue_ticket, + 1, + &state.identity, + &state.hot, + state.run_state.as_ref(), + &admission, + ) + .expect("direct-prefix Step ticket builds"); + let cell = Actors::control_opening_cell_from_scalar( + actor_id, + state.identity, + state.hot, + admission, + &ticket, + &loaded_step, + ) + .expect("direct-prefix Ready cell projects"); + assert_eq!( + Actors::actor_control_cell(actor_id).map(|(_, stored)| stored), + Some(cell) + ); + } + assert_eq!(ActorReadyOccupancy::::get(), count); + actor_ids +} + +fn frame_install_direct_stop_cycle_ready(count: u32) -> Vec { + frame_install_direct_ready(make_step(Task::StopCycle), count) +} + +fn frame_install_direct_zero_step_ready( + auto_close_at_cycle_nonce: Option, +) -> (ActorId, QueueTicket) { + frame_system::Pallet::::set_block_number(1); + let steps = BoundedVec::default(); + prefund_active_user_creation(ALICE, &steps); + let actor_id = Actors::next_actor_id(); + let mut contract = + user_active_contract(manual_schedule(), None, steps).expect("zero-Step User Contract exists"); + contract.auto_close_at_cycle_nonce = auto_close_at_cycle_nonce; + assert_ok!(Actors::create_user_actor( + RuntimeOrigin::signed(ALICE), + Mutability::Mutable, + Some(contract), + )); + age_fixture_control_clock(actor_id); + let sovereign = Actors::active_actor_view(actor_id) + .expect("zero-Step frame User exists") + .sovereign_account; + let _ = ::AssetOps::mint( + &sovereign, + ::FeeNativeAssetId::get(), + u128::from(u64::MAX / 4), + ); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + let (state, admission, loaded_step) = Actors::load_frame_actor_service_state(actor_id) + .expect("zero-Step frame Ready state is coherent"); + assert!(loaded_step.is_none()); + let queue_ticket = state + .hot + .queue_ticket + .expect("zero-Step frame ticket exists"); + let ticket = Actors::build_actor_step_ticket( + actor_id, + queue_ticket, + 1, + &state.identity, + &state.hot, + state.run_state.as_ref(), + &admission, + ) + .expect("zero-Step frame ticket builds"); + let cell = Actors::control_zero_step_opening_cell_from_scalar( + actor_id, + state.identity, + state.hot, + admission, + &ticket, + ) + .expect("zero-Step frame Ready cell projects"); + assert_eq!( + Actors::actor_control_cell(actor_id).map(|(_, stored)| stored), + Some(cell) + ); + assert_eq!(ActorReadyOccupancy::::get(), 1); + (actor_id, queue_ticket) +} + +#[test] +fn control_zero_step_user_opening_completes_without_action_or_scalar_owner() { + new_test_ext().execute_with(|| { + let (actor_id, ticket) = frame_install_direct_zero_step_ready(None); + clear_fee_collections(); + System::reset_events(); + + let head_before_report = Actors::queue_head(); + frame_system::Pallet::::set_block_number(1); + Actors::execute_cycle_to_cutoff( + Weight::MAX, + (ticket.saturating_add(1)).min(head_before_report.saturating_add(1)), + ); + assert_eq!(Actors::queue_head().saturating_sub(head_before_report), 1); + + assert_eq!(fee_collections().len(), 1); + assert!(has_actor_event(|event| matches!( + event, + Event::PipelineFeeCharged { actor_id: id, .. } if *id == actor_id + ))); + assert!(has_actor_event(|event| matches!( + event, + Event::CycleStarted { + actor_id: id, + cycle_nonce: 1, + } if *id == actor_id + ))); + assert!(has_actor_event(|event| matches!( + event, + Event::CycleSummary { + actor_id: id, + cycle_nonce: 1, + result: CycleResult::Completed, + outcomes, + } if *id == actor_id && *outcomes == OutcomeTotals::default() + ))); + assert!(!has_actor_event(|event| matches!( + event, + Event::ActionFeeCharged { actor_id: id, .. } if *id == actor_id + ))); + assert!(!ActorRunStateStore::::contains_key(actor_id)); + assert!( + ActorFunding::::get(actor_id) + .expect("zero-Step funding remains") + .funding_accumulated + .is_empty() + ); + let (location, identity, hot, _) = Actors::load_frame_control_authority(actor_id) + .expect("zero-Step output retains sole frame authority"); + assert_eq!(location, C1Location::Unsignaled); + assert_eq!(identity.cycle_nonce, 1); + assert_eq!(hot.cycle_state, CycleState::Idle); + assert!(!hot.pending_signal); + assert_eq!(hot.queue_ticket, None); + assert_eq!(hot.last_cycle_block, Some(1)); + assert!(!ActorIdentities::::contains_key(actor_id)); + frame_assert_single_owner(); + }); +} + +#[test] +fn control_zero_step_auto_close_removes_frame_authority_and_preserves_cycle_events() { + new_test_ext().execute_with(|| { + let (actor_id, ticket) = frame_install_direct_zero_step_ready(Some(1)); + clear_fee_collections(); + System::reset_events(); + + let head_before_report = Actors::queue_head(); + frame_system::Pallet::::set_block_number(1); + Actors::execute_cycle_to_cutoff( + Weight::MAX, + (ticket.saturating_add(1)).min(head_before_report.saturating_add(1)), + ); + assert_eq!(Actors::queue_head().saturating_sub(head_before_report), 1); + + assert_eq!(fee_collections().len(), 1); + assert!(has_actor_event(|event| matches!( + event, + Event::CycleStarted { + actor_id: id, + cycle_nonce: 1, + } if *id == actor_id + ))); + assert!(has_actor_event(|event| matches!( + event, + Event::CycleSummary { + actor_id: id, + cycle_nonce: 1, + result: CycleResult::Completed, + .. + } if *id == actor_id + ))); + assert!(has_actor_event(|event| matches!( + event, + Event::ActorClosed { + actor_id: id, + reason: CloseReason::AutoCloseNonceReached, + } if *id == actor_id + ))); + assert!(!crate::ActorControlLocators::::contains_key(actor_id)); + assert!(!ActorContractHeads::::contains_key(actor_id)); + assert!(!ActorFunding::::contains_key(actor_id)); + assert!(!ActorRunStateStore::::contains_key(actor_id)); + assert!(!ActorStateHolds::::contains_key(actor_id)); + assert_eq!(ActiveActorCount::::get(), 0); + assert_eq!(ActorIdentityCount::::get(), 0); + assert!(!ActorIdentities::::contains_key(actor_id)); + frame_assert_single_owner(); + }); +} + +#[test] +fn control_immutable_zero_step_at_time_closes_after_frame_owned_temporal_path() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let steps = BoundedVec::default(); + prefund_active_user_creation(ALICE, &steps); + let actor_id = Actors::next_actor_id(); + let mut contract = user_active_contract(at_time_schedule(1), None, steps) + .expect("immutable zero-Step AtTime Contract exists"); + contract.auto_close_at_cycle_nonce = Some(1); + assert_ok!(Actors::create_user_actor( + RuntimeOrigin::signed(ALICE), + Mutability::Immutable, + Some(contract), + )); + age_fixture_control_clock(actor_id); + let sovereign = Actors::active_actor_view(actor_id) + .expect("immutable zero-Step AtTime Actor exists") + .sovereign_account; + let _ = ::AssetOps::mint( + &sovereign, + ::FeeNativeAssetId::get(), + u128::from(u64::MAX / 4), + ); + let residual_asset = TestAsset::Local(9); + set_asset_balance(&sovereign, residual_asset, 919); + Actors::trigger_wakeup_substrate_invalidate_inner(actor_id) + .expect("reference AtTime reference invalidates before frame projection"); + let (state, admission, loaded_step) = Actors::load_frame_actor_service_state(actor_id) + .expect("immutable zero-Step AtTime state remains coherent"); + assert!(loaded_step.is_none()); + let cell = Actors::control_zero_step_unsignaled_cell_from_scalar( + actor_id, + state.identity, + state.hot, + admission, + ) + .expect("immutable zero-Step AtTime projects Unsignaled"); + let cell = Actors::control_schedule_fresh_wakeup_reference(cell, WakeupKey::Tick(2)) + .expect("immutable zero-Step AtTime reference schedules"); + crate::ActorUnsignaledControlCells::::insert(actor_id, cell); + crate::ActorControlLocators::::insert(actor_id, C1Location::Unsignaled); + clear_fee_collections(); + System::reset_events(); + + frame_system::Pallet::::set_block_number(2); + let (_, waiting) = Actors::control_latch_due_temporal_reference(WakeupKey::Tick(2), 2, 2) + .expect("immutable zero-Step AtTime occurrence latches"); + let C1Location::Waiting { + key: WakeupKey::Block(3), + page, + .. + } = waiting + else { + panic!("immutable zero-Step AtTime must enter exact N+1 Waiting"); + }; + frame_system::Pallet::::set_block_number(3); + let promoted = Actors::control_promote_due_waiting_page(3, page, 3) + .expect("immutable zero-Step AtTime promotes at N+1"); + assert_eq!(promoted.len(), 1); + let ticket = promoted[0].1; + let head_before_report = Actors::queue_head(); + frame_system::Pallet::::set_block_number(3); + Actors::execute_cycle_to_cutoff( + Weight::MAX, + (ticket.saturating_add(1)).min(head_before_report.saturating_add(1)), + ); + assert_eq!(Actors::queue_head().saturating_sub(head_before_report), 1); + 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 + ))); + assert_eq!(asset_balance(&sovereign, residual_asset), 919); + assert!(!crate::ActorControlLocators::::contains_key(actor_id)); + assert!(!ActorFunding::::contains_key(actor_id)); + assert!(!ActorStateHolds::::contains_key(actor_id)); + frame_assert_single_owner(); + }); +} + +#[test] +fn control_zero_and_one_step_cadenced_recurrence_rearms_from_frame_authority() { + for step_count in [0, 1] { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let steps = if step_count == 0 { + BoundedVec::default() + } else { + contract_steps_with_step(make_step(Task::StopCycle)) + }; + let opening_fee = pipeline_opening_fee(&steps); + let action_fee = steps.first().map(user_step_fee); + prefund_active_user_creation(ALICE, &steps); + let actor_id = Actors::next_actor_id(); + assert_ok!(Actors::create_user_actor( + RuntimeOrigin::signed(ALICE), + Mutability::Mutable, + user_active_contract(timer_schedule(1), None, steps), + )); + age_fixture_control_clock(actor_id); + let installed_hold = Actors::actor_state_hold(actor_id).expect("Cadenced hold exists"); + assert!(installed_hold.breakdown.detector > 0); + let sovereign = Actors::active_actor_view(actor_id) + .expect("zero-Step Cadenced Actor exists") + .sovereign_account; + let _ = ::AssetOps::mint( + &sovereign, + ::FeeNativeAssetId::get(), + u128::from(u64::MAX / 4), + ); + Actors::trigger_wakeup_substrate_invalidate_inner(actor_id) + .expect("reference Cadenced reference invalidates before frame projection"); + let (state, admission, loaded_step) = Actors::load_frame_actor_service_state(actor_id) + .expect("zero-Step Cadenced state remains coherent"); + let cell = match loaded_step { + None => Actors::control_zero_step_unsignaled_cell_from_scalar( + actor_id, + state.identity, + state.hot, + admission, + ), + Some(step) => Actors::control_unsignaled_cell_from_scalar( + actor_id, + state.identity, + state.hot, + admission, + &step, + ), + } + .expect("Cadenced authority projects Unsignaled"); + let cell = Actors::control_schedule_fresh_wakeup_reference(cell, WakeupKey::Tick(2)) + .expect("zero-Step Cadenced source occurrence schedules"); + crate::ActorUnsignaledControlCells::::insert(actor_id, cell); + crate::ActorControlLocators::::insert(actor_id, C1Location::Unsignaled); + clear_fee_collections(); + System::reset_events(); + + let execute_cycle = |due_tick: u64, due_block: u64, ready_block: u64| { + frame_system::Pallet::::set_block_number(due_block); + let (_, waiting) = Actors::control_latch_due_temporal_reference( + WakeupKey::Tick(due_tick), + due_block, + due_tick, + ) + .expect("zero-Step Cadenced occurrence latches"); + let C1Location::Waiting { + key: WakeupKey::Block(eligible_at), + page, + .. + } = waiting + else { + panic!("zero-Step Cadenced occurrence must enter N+1 Waiting"); + }; + assert_eq!(eligible_at, ready_block); + frame_system::Pallet::::set_block_number(ready_block); + let promoted = Actors::control_promote_due_waiting_page(ready_block, page, ready_block) + .expect("zero-Step Cadenced occurrence promotes at N+1"); + assert_eq!(promoted.len(), 1); + let ticket = promoted[0].1; + let head_before_report = Actors::queue_head(); + frame_system::Pallet::::set_block_number(ready_block); + Actors::execute_cycle_to_cutoff( + Weight::MAX, + (ticket.saturating_add(1)).min(head_before_report.saturating_add(1)), + ); + assert_eq!(Actors::queue_head().saturating_sub(head_before_report), 1); + assert_eq!( + Actors::actor_state_hold(actor_id).expect("Cadenced hold survives recurrence"), + installed_hold + ); + }; + + execute_cycle(2, 2, 3); + let first = crate::ActorUnsignaledControlCells::::get(actor_id) + .expect("zero-Step Cadenced recurrence returns Unsignaled"); + assert_eq!(first.identity.cycle_nonce, 1); + assert!(!first.hot.pending_signal); + assert_eq!( + first.hot.trigger_wakeup_pointer.map(|pointer| pointer.tick), + Some(4) + ); + assert_eq!( + Actors::wakeup_cursor_peek_key(WakeupClock::Tick), + Some(WakeupKey::Tick(4)) + ); + + execute_cycle(4, 4, 5); + let second = crate::ActorUnsignaledControlCells::::get(actor_id) + .expect("second zero-Step Cadenced recurrence returns Unsignaled"); + assert_eq!(second.identity.cycle_nonce, 2); + assert_eq!( + second + .hot + .trigger_wakeup_pointer + .map(|pointer| pointer.tick), + Some(6) + ); + let mut expected_fees = Vec::new(); + for _ in 0..2 { + expected_fees.extend([cadenced_trigger_fee(), opening_fee]); + if let Some(fee) = action_fee.filter(|fee| *fee > 0) { + expected_fees.push(fee); + } + } + assert_eq!(fee_collections(), expected_fees); + assert_eq!( + crate::ActorControlLocators::::get(actor_id), + Some(C1Location::Unsignaled) + ); + assert!(!ActorRunStateStore::::contains_key(actor_id)); + frame_assert_single_owner(); + }); + } +} + +#[test] +fn control_zero_step_user_opening_matches_immutable_oracle_logical_authority_and_fees() { + let execute = || { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let (actor_id, _ticket) = { + let actor_id = create_user_with( + ALICE, + Mutability::Mutable, + manual_schedule(), + None, + BoundedVec::default(), + ); + let sovereign = Actors::active_actor_view(actor_id) + .expect("reference zero-Step User exists") + .sovereign_account; + let _ = ::AssetOps::mint( + &sovereign, + ::FeeNativeAssetId::get(), + u128::from(u64::MAX / 4), + ); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + let ticket = Actors::active_actor_view(actor_id) + .and_then(|view| view.queue_ticket) + .expect("reference zero-Step ticket exists"); + (actor_id, ticket) + }; + let sovereign = { + Actors::active_actor_view(actor_id) + .expect("reference zero-Step Actor exists before execution") + .sovereign_account + }; + let custody_before = native_balance(&sovereign); + clear_fee_collections(); + System::reset_events(); + + { + Actors::on_idle(1, Weight::MAX); + } + + let (identity, hot, admission) = { + let (identity, hot) = + Actors::load_control_head(actor_id).expect("reference zero-Step output authority exists"); + let admission = + Actors::load_control_admission(actor_id).expect("reference zero-Step admission remains"); + (identity, hot, admission) + }; + let event_counts = ( + System::events() + .iter() + .filter(|record| { + matches!( + &record.event, + RuntimeEvent::Actors(Event::PipelineFeeCharged { actor_id: id, .. }) + if *id == actor_id + ) + }) + .count(), + System::events() + .iter() + .filter(|record| { + matches!( + &record.event, + RuntimeEvent::Actors(Event::CycleStarted { actor_id: id, .. }) + if *id == actor_id + ) + }) + .count(), + System::events() + .iter() + .filter(|record| { + matches!( + &record.event, + RuntimeEvent::Actors(Event::CycleSummary { actor_id: id, .. }) + if *id == actor_id + ) + }) + .count(), + System::events() + .iter() + .filter(|record| { + matches!( + &record.event, + RuntimeEvent::Actors(Event::ActionFeeCharged { actor_id: id, .. }) + if *id == actor_id + ) + }) + .count(), + ); + ( + custody_before.saturating_sub(native_balance(&sovereign)), + fee_collections(), + identity.cycle_nonce, + hot.cycle_state, + hot.pending_signal, + hot.queue_ticket, + hot.last_cycle_block, + admission, + ActorFunding::::get(actor_id) + .expect("zero-Step differential funding remains") + .funding_accumulated, + ActorRunStateStore::::contains_key(actor_id), + event_counts, + ) + }) + }; + + let baseline = execute(); + emit_baseline_oracle("zero_step_user_opening", &baseline); +} + +fn control_running_branch_snapshot(step_count: u32) -> Vec> { + new_test_ext().execute_with(|| { + assert!(matches!(step_count, 2 | 3)); + frame_system::Pallet::::set_block_number(0); + let transfer = make_step(Task::Transfer { + to: BOB, + asset: TestAsset::Native, + amount: AmountResolution::Fixed(1), + }); + let mut steps = vec![transfer.clone(), make_step(Task::StopCycle)]; + if step_count == 3 { + steps.insert(1, transfer); + } + let steps = BoundedVec::try_from(steps).expect("Running differential Contract fits"); + let actor_id = create_user_with(ALICE, Mutability::Mutable, manual_schedule(), None, steps); + assert_eq!( + Actors::load_actor_contract(actor_id) + .expect("Running differential Contract exists") + .steps + .len(), + step_count as usize, + ); + let sovereign = Actors::active_actor_view(actor_id) + .expect("Running differential User exists") + .sovereign_account; + let native_asset = ::FeeNativeAssetId::get(); + let _ = + ::AssetOps::mint(&sovereign, native_asset, u128::from(u64::MAX / 4)); + frame_system::Pallet::::set_block_number(1); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + System::reset_events(); + { + Actors::on_idle(1, Weight::MAX); + } + let run = ActorRunStateStore::::get(actor_id).unwrap_or_else(|| { + panic!( + "{} Running state persists after Step 0; view={:?}; events={:?}", + { "reference" }, + Actors::active_actor_view(actor_id), + System::events(), + ) + }); + assert_eq!(run.cursor, 1); + assert_eq!(run.last_committed_step_block, Some(1)); + + frame_system::Pallet::::set_block_number(2); + { + Actors::on_initialize(2); + run_prepass(); + } + { + Actors::on_idle(2, Weight::MAX); + } + + let (identity, hot, admission) = { + ( + Actors::actor_identity(actor_id).expect("canonical Running identity exists"), + Actors::actor_hot(actor_id).expect("reference Running hot state exists"), + Actors::load_control_admission(actor_id).expect("canonical Running admission exists"), + ) + }; + vec![ + identity.encode(), + hot.encode(), + admission.encode(), + ActorFunding::::get(actor_id).encode(), + ActorRunStateStore::::get(actor_id).encode(), + crate::ActorStateHolds::::get(actor_id) + .map(|hold| hold.owner) + .encode(), + ::AssetOps::balance(&sovereign, native_asset).encode(), + ::AssetOps::balance(&BOB, native_asset).encode(), + ::AssetOps::balance( + &::FeeSink::get(), + native_asset, + ) + .encode(), + System::events().encode(), + ] + }) +} + +#[test] +fn control_running_continuation_and_terminal_step_matches_immutable_oracle() { + let baseline = control_running_branch_snapshot(2); + emit_baseline_oracle("terminal_running_step", &baseline); +} + +#[test] +fn control_running_middle_step_continuation_matches_immutable_oracle() { + let baseline = control_running_branch_snapshot(3); + emit_baseline_oracle("middle_running_step", &baseline); +} + +#[test] +fn control_stable_scalar_seams_update_an_existing_frame_primary_in_benchmark_builds() { + 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(make_step(Task::StopCycle)), + ); + let (state, admission, loaded_step) = Actors::load_current_step_service_state(actor_id) + .expect("scalar benchmark Actor remains coherent"); + let cell = Actors::control_unsignaled_cell_from_scalar( + actor_id, + state.identity, + state.hot, + admission, + &loaded_step, + ) + .expect("scalar authority projects to one Unsignaled primary"); + crate::ActorUnsignaledControlCells::::insert(actor_id, cell); + crate::ActorControlLocators::::insert(actor_id, C1Location::Unsignaled); + + mutate_actor_hot_coherent(actor_id, |hot| hot.unsuccessful_attempt_streak = 1); + mutate_actor_identity_coherent(actor_id, |identity| { + identity.last_control_mutation_block = 7; + }); + + let (location, identity, hot, _) = Actors::load_frame_control_authority(actor_id) + .expect("stable scalar seams retain the frame primary"); + assert_eq!(location, C1Location::Unsignaled); + assert_eq!(identity.last_control_mutation_block, 7); + assert_eq!(hot.unsuccessful_attempt_streak, 1); + }); +} + +fn frame_install_temporal_system_unsignaled(count: u32) -> Vec { + frame_system::Pallet::::set_block_number(1); + let mut actor_ids = Vec::with_capacity(count as usize); + for _ in 0..count { + let actor_id = create_system_with( + ALICE, + timer_schedule(5), + None, + contract_steps_with_step(make_step(Task::StopCycle)), + ); + Actors::trigger_wakeup_substrate_invalidate_inner(actor_id) + .expect("reference Trigger wakeup invalidates before candidate projection"); + let (state, admission, loaded_step) = Actors::load_current_step_service_state(actor_id) + .expect("temporal System Actor remains coherent"); + assert!(state.hot.trigger_wakeup_pointer.is_none()); + let unsignaled = Actors::control_unsignaled_cell_from_scalar( + actor_id, + state.identity, + state.hot, + admission, + &loaded_step, + ) + .expect("temporal System authority projects Unsignaled"); + crate::ActorUnsignaledControlCells::::insert(actor_id, &unsignaled); + crate::ActorControlLocators::::insert(actor_id, C1Location::Unsignaled); + actor_ids.push(actor_id); + } + actor_ids +} + +#[test] +fn control_fresh_trigger_reference_round_trip_uses_no_scalar_hot_owner() { + new_test_ext().execute_with(|| { + let actor_id = frame_install_temporal_system_unsignaled(1)[0]; + let cell = crate::ActorUnsignaledControlCells::::get(actor_id) + .expect("candidate Unsignaled cell exists"); + let key = WakeupKey::Tick(9); + let cell = Actors::control_schedule_fresh_wakeup_reference(cell, key) + .expect("fresh Trigger reference schedules"); + let pointer = cell + .hot + .trigger_wakeup_pointer + .expect("frame-owned hot state receives Trigger pointer"); + assert_eq!(pointer.tick, 9); + assert!( + crate::ActorWaitingFrameChunks::::get((key, pointer.page_id)) + .and_then(|chunk| chunk.entries.get(pointer.slot as usize).cloned().flatten()) + .is_some_and( + |entry| matches!(entry, crate::ActorWaitingEntry::Reference(reference) + if reference.actor_id == actor_id + && reference.admission_identity == cell.admission.admission_identity) + ) + ); + assert!(matches!( + Actors::control_schedule_fresh_wakeup_reference(cell.clone(), key), + Err(crate::scheduler::ActorControlTransitionError::Invariant) + )); + assert_eq!(crate::ActorWaitingOccupancies::::get(key), 1); + crate::ActorUnsignaledControlCells::::insert(actor_id, cell); + frame_assert_single_owner(); + assert_eq!( + crate::ActorControlLocators::::get(actor_id), + Some(C1Location::Unsignaled) + ); + frame_assert_single_owner(); + + let cell = crate::ActorUnsignaledControlCells::::get(actor_id) + .expect("scheduled candidate cell remains primary"); + assert!(matches!( + Actors::control_due_wakeup_reference(key, 1, 8), + Err(crate::scheduler::ActorControlTransitionError::Invariant) + )); + assert!(matches!( + Actors::control_consume_due_wakeup_reference(cell.clone(), key, 1, 8), + Err(crate::scheduler::ActorControlTransitionError::Invariant) + )); + assert!(crate::ActorWaitingFrameChunks::::contains_key(( + key, + pointer.page_id + ))); + assert_eq!(Actors::wakeup_cursor_peek_key(WakeupClock::Tick), Some(key)); + frame_assert_single_owner(); + let (due_actor, due_pointer) = Actors::control_due_wakeup_reference(key, 1, 9) + .expect("due candidate Trigger reference is discoverable"); + assert_eq!(due_actor, actor_id); + assert_eq!(due_pointer.block, key); + assert_eq!(due_pointer.page_id, pointer.page_id); + assert_eq!(due_pointer.slot, pointer.slot); + crate::ActorControlLocators::::insert(actor_id, C1Location::Ready { ticket: 999 }); + assert!(matches!( + Actors::control_due_wakeup_primary(key, 1, 9), + Err(crate::scheduler::ActorControlTransitionError::Invariant) + )); + assert!(crate::ActorWaitingFrameChunks::::contains_key(( + key, + pointer.page_id + ))); + assert_eq!(Actors::wakeup_cursor_peek_key(WakeupClock::Tick), Some(key)); + crate::ActorControlLocators::::insert(actor_id, C1Location::Unsignaled); + let (due_location, due_cell) = Actors::control_due_wakeup_primary(key, 1, 9) + .expect("due reference resolves the primary frame cell"); + assert_eq!(due_location, C1Location::Unsignaled); + assert_eq!(due_cell, cell); + frame_assert_single_owner(); + let cell = Actors::control_consume_due_wakeup_reference(cell, key, 1, 9) + .expect("due candidate Trigger reference consumes"); + assert!(cell.hot.trigger_wakeup_pointer.is_none()); + crate::ActorUnsignaledControlCells::::insert(actor_id, cell); + assert!(!crate::ActorWaitingFrameChunks::::contains_key(( + key, + pointer.page_id + ))); + assert!(!crate::ActorWaitingOccupancies::::contains_key(key)); + assert_eq!(Actors::wakeup_cursor_peek_key(WakeupClock::Tick), None); + frame_assert_single_owner(); + }); +} + +#[test] +fn control_due_temporal_reference_latches_into_n_plus_one_process_waiting_atomically() { + new_test_ext().execute_with(|| { + let actor_id = frame_install_temporal_system_unsignaled(1)[0]; + let source_key = WakeupKey::Tick(9); + let cell = crate::ActorUnsignaledControlCells::::get(actor_id) + .expect("candidate temporal Unsignaled cell exists"); + let cell = Actors::control_schedule_fresh_wakeup_reference(cell, source_key) + .expect("candidate temporal Trigger reference schedules"); + crate::ActorUnsignaledControlCells::::insert(actor_id, cell); + System::reset_events(); + + assert!(matches!( + Actors::control_latch_due_temporal_reference(source_key, 1, 8), + Err(crate::scheduler::ActorControlTransitionError::Invariant) + )); + assert_eq!( + crate::ActorControlLocators::::get(actor_id), + Some(C1Location::Unsignaled) + ); + assert!(crate::ActorUnsignaledControlCells::::contains_key( + actor_id + )); + assert_eq!( + Actors::wakeup_cursor_peek_key(WakeupClock::Tick), + Some(source_key) + ); + + let source_cell = crate::ActorUnsignaledControlCells::::get(actor_id) + .expect("source cell survives early rejection"); + let mut occupied_destination = + BoundedVec::try_from(vec![None; 32]).expect("collision C32 fits"); + occupied_destination[0] = Some(crate::ActorWaitingEntry::Primary(source_cell.clone())); + crate::ActorWaitingFrameChunks::::insert( + (WakeupKey::Block(2), 0), + crate::ActorWaitingPageOf:: { + entries: occupied_destination, + live_entries: 1, + scan_slot: 0, + previous_page: None, + next_page: None, + }, + ); + assert!(matches!( + Actors::control_latch_due_temporal_reference(source_key, 1, 9), + Err(crate::scheduler::ActorControlTransitionError::Invariant) + )); + assert_eq!( + crate::ActorUnsignaledControlCells::::get(actor_id), + Some(source_cell) + ); + assert_eq!( + crate::ActorControlLocators::::get(actor_id), + Some(C1Location::Unsignaled) + ); + assert_eq!( + Actors::wakeup_cursor_peek_key(WakeupClock::Tick), + Some(source_key) + ); + assert_eq!(Actors::wakeup_cursor_peek_key(WakeupClock::Block), None); + assert!(!has_actor_event(|event| matches!( + event, + Event::TriggerOccurrenceProcessed { actor_id: id, .. } if *id == actor_id + ))); + crate::ActorWaitingFrameChunks::::remove((WakeupKey::Block(2), 0)); + + let (latched_actor, destination) = + Actors::control_latch_due_temporal_reference(source_key, 1, 9) + .expect("due temporal reference latches"); + assert_eq!(latched_actor, actor_id); + let C1Location::Waiting { key, page, slot } = destination else { + panic!("latched temporal reference must enter process Waiting"); + }; + assert_eq!(key, WakeupKey::Block(2)); + let cell = crate::ActorWaitingFrameChunks::::get((key, page)) + .and_then(|chunk| chunk.entries.get(slot as usize).cloned().flatten()) + .and_then(crate::ActorWaitingEntry::into_primary) + .expect("latched process-primary cell exists"); + assert_eq!(cell.actor_id, actor_id); + assert!(cell.hot.pending_signal); + assert_eq!(cell.hot.cycle_state, CycleState::Idle); + assert_eq!(cell.eligible_at, Some(2)); + assert!(cell.hot.trigger_wakeup_pointer.is_none()); + assert_eq!( + cell.hot.wakeup_pointer.map(|pointer| pointer.block), + Some(key) + ); + assert!(!crate::ActorUnsignaledControlCells::::contains_key( + actor_id + )); + assert_eq!( + crate::ActorControlLocators::::get(actor_id), + Some(destination) + ); + assert_eq!(Actors::wakeup_cursor_peek_key(WakeupClock::Tick), None); + assert_eq!( + Actors::wakeup_cursor_peek_key(WakeupClock::Block), + Some(key) + ); + frame_assert_single_owner(); + assert!(has_actor_event(|event| matches!( + event, + Event::TriggerOccurrenceProcessed { + actor_id: id, + trigger_family: TriggerFamily::Cadenced, + fee, + } if *id == actor_id && *fee == 0 + ))); + frame_assert_single_owner(); + + frame_system::Pallet::::set_block_number(2); + let promoted = Actors::control_promote_due_waiting_page(2, page, 2) + .expect("N+1 process Waiting promotes to Ready"); + assert_eq!(promoted, vec![(actor_id, 0)]); + let head_before_report = Actors::queue_head(); + frame_system::Pallet::::set_block_number(2); + Actors::execute_cycle_to_cutoff(Weight::MAX, (1).min(head_before_report.saturating_add(1))); + assert_eq!(Actors::queue_head().saturating_sub(head_before_report), 1); + + let recurrent = crate::ActorUnsignaledControlCells::::get(actor_id) + .expect("completed Cadenced Actor returns to Unsignaled"); + assert_eq!( + crate::ActorControlLocators::::get(actor_id), + Some(C1Location::Unsignaled) + ); + let recurrent_pointer = recurrent + .hot + .trigger_wakeup_pointer + .expect("completed Cadenced Actor rearms Trigger reference"); + assert_eq!(recurrent_pointer.tick, 6); + let recurrent_key = WakeupKey::Tick(recurrent_pointer.tick); + assert!( + crate::ActorWaitingFrameChunks::::get((recurrent_key, recurrent_pointer.page_id)) + .and_then(|chunk| chunk + .entries + .get(recurrent_pointer.slot as usize) + .cloned() + .flatten()) + .is_some_and( + |entry| matches!(entry, crate::ActorWaitingEntry::Reference(reference) + if reference.actor_id == actor_id + && reference.admission_identity == recurrent.admission.admission_identity) + ) + ); + assert_eq!(Actors::wakeup_cursor_peek_key(WakeupClock::Block), None); + assert_eq!( + Actors::wakeup_cursor_peek_key(WakeupClock::Tick), + Some(recurrent_key) + ); + frame_assert_single_owner(); + }); +} + +fn frame_install_temporal_system_unsignaled_with_steps( + steps: crate::ContractSteps, + fund: bool, +) -> ActorId { + frame_system::Pallet::::set_block_number(1); + let actor_id = create_system_with(ALICE, timer_schedule(5), None, steps); + if fund { + let sovereign = Actors::active_actor_view(actor_id) + .expect("temporal System Actor exists before candidate projection") + .sovereign_account; + let _ = ::AssetOps::mint( + &sovereign, + ::FeeNativeAssetId::get(), + u128::from(u64::MAX / 4), + ); + } + Actors::trigger_wakeup_substrate_invalidate_inner(actor_id) + .expect("reference System Trigger wakeup invalidates before candidate projection"); + let (state, admission, loaded_step) = Actors::load_current_step_service_state(actor_id) + .expect("temporal System Actor remains coherent"); + let unsignaled = Actors::control_unsignaled_cell_from_scalar( + actor_id, + state.identity, + state.hot, + admission, + &loaded_step, + ) + .expect("temporal System authority projects Unsignaled"); + crate::ActorUnsignaledControlCells::::insert(actor_id, unsignaled); + crate::ActorControlLocators::::insert(actor_id, C1Location::Unsignaled); + actor_id +} + +fn frame_promote_temporal_actor_to_ready(actor_id: ActorId, source_tick: u64) -> u64 { + let source_key = WakeupKey::Tick(source_tick); + let cell = crate::ActorUnsignaledControlCells::::get(actor_id) + .expect("candidate temporal Unsignaled cell exists"); + let cell = Actors::control_schedule_fresh_wakeup_reference(cell, source_key) + .expect("candidate temporal Trigger reference schedules"); + crate::ActorUnsignaledControlCells::::insert(actor_id, cell); + let (_, destination) = + Actors::control_latch_due_temporal_reference(source_key, source_tick, source_tick) + .expect("candidate temporal occurrence latches"); + let C1Location::Waiting { page, .. } = destination else { + panic!("latched temporal occurrence enters process Waiting"); + }; + let opening_block = source_tick.saturating_add(1); + frame_system::Pallet::::set_block_number(opening_block); + assert_eq!( + Actors::control_promote_due_waiting_page(opening_block, page, opening_block) + .expect("N+1 process Waiting promotes"), + vec![(actor_id, 0)] + ); + opening_block +} + +#[test] +fn control_cadenced_running_primary_retains_lightweight_trigger_rearm() { + new_test_ext().execute_with(|| { + let steps = BoundedVec::try_from(vec![ + make_step(Task::Transfer { + to: BOB, + asset: TestAsset::Native, + amount: AmountResolution::Fixed(1), + }), + make_step(Task::StopCycle), + ]) + .expect("two-Step temporal Contract fits"); + let actor_id = frame_install_temporal_system_unsignaled_with_steps(steps, true); + let opening_block = frame_promote_temporal_actor_to_ready(actor_id, 6); + + let head_before_report = Actors::queue_head(); + frame_system::Pallet::::set_block_number(opening_block); + Actors::execute_cycle_to_cutoff(Weight::MAX, (1).min(head_before_report.saturating_add(1))); + assert_eq!(Actors::queue_head().saturating_sub(head_before_report), 1); + let location = crate::ActorControlLocators::::get(actor_id) + .expect("Running Cadenced Actor has a primary locator"); + assert_eq!(location, C1Location::Ready { ticket: 1 }); + let cell = crate::ActorReadyFrameChunks::::get(0) + .and_then(|chunk| chunk.get(1).cloned().flatten()) + .expect("Running Cadenced primary cell exists"); + assert_eq!(cell.hot.cycle_state, CycleState::Running); + assert_eq!(cell.cursor, 1); + assert_eq!(cell.eligible_at, Some(8)); + let trigger_pointer = cell + .hot + .trigger_wakeup_pointer + .expect("Running primary retains cadence re-arm pointer"); + assert_eq!(trigger_pointer.tick, 11); + let trigger_key = WakeupKey::Tick(trigger_pointer.tick); + assert!( + crate::ActorWaitingFrameChunks::::get((trigger_key, trigger_pointer.page_id)) + .and_then(|chunk| chunk + .entries + .get(trigger_pointer.slot as usize) + .cloned() + .flatten()) + .is_some_and( + |entry| matches!(entry, crate::ActorWaitingEntry::Reference(reference) + if reference.actor_id == actor_id + && reference.admission_identity == cell.admission.admission_identity) + ) + ); + assert_eq!( + Actors::wakeup_cursor_peek_key(WakeupClock::Tick), + Some(trigger_key) + ); + assert_eq!(Actors::wakeup_cursor_peek_key(WakeupClock::Block), None); + System::reset_events(); + let (deferred_actor, deferred_location) = + Actors::control_latch_due_temporal_reference(trigger_key, opening_block, 11) + .expect("due cadence latches while Running"); + assert_eq!(deferred_actor, actor_id); + assert_eq!(deferred_location, location); + let deferred = crate::ActorReadyFrameChunks::::get(0) + .and_then(|chunk| chunk.get(1).cloned().flatten()) + .expect("Running primary survives deferred latch"); + assert_eq!(deferred.hot.cycle_state, CycleState::Running); + assert!(deferred.hot.pending_signal); + assert_eq!(deferred.eligible_at, Some(8)); + assert!(deferred.hot.trigger_wakeup_pointer.is_none()); + assert_eq!(Actors::wakeup_cursor_peek_key(WakeupClock::Tick), None); + assert!(has_actor_event(|event| matches!( + event, + Event::TriggerOccurrenceProcessed { + actor_id: id, + trigger_family: TriggerFamily::Cadenced, + fee, + } if *id == actor_id && *fee == 0 + ))); + + frame_system::Pallet::::set_block_number(8); + let head_before_completion = Actors::queue_head(); + frame_system::Pallet::::set_block_number(8); + Actors::execute_cycle_to_cutoff( + Weight::MAX, + (2).min(head_before_completion.saturating_add(1)), + ); + assert_eq!( + Actors::queue_head().saturating_sub(head_before_completion), + 1 + ); + assert_eq!( + crate::ActorControlLocators::::get(actor_id), + Some(C1Location::Ready { ticket: 2 }) + ); + let deferred_opening = crate::ActorReadyFrameChunks::::get(0) + .and_then(|chunk| chunk.get(2).cloned().flatten()) + .expect("completed current cycle queues deferred Opening"); + assert_eq!(deferred_opening.hot.cycle_state, CycleState::Idle); + assert!(deferred_opening.hot.pending_signal); + assert_eq!(deferred_opening.cursor, 0); + assert_eq!(deferred_opening.eligible_at, Some(9)); + assert!(deferred_opening.hot.trigger_wakeup_pointer.is_none()); + assert!(!ActorRunStateStore::::contains_key(actor_id)); + + frame_system::Pallet::::set_block_number(9); + let head_before_next_cycle = Actors::queue_head(); + frame_system::Pallet::::set_block_number(9); + Actors::execute_cycle_to_cutoff( + Weight::MAX, + (3).min(head_before_next_cycle.saturating_add(1)), + ); + assert_eq!( + Actors::queue_head().saturating_sub(head_before_next_cycle), + 1 + ); + assert_eq!( + crate::ActorControlLocators::::get(actor_id), + Some(C1Location::Ready { ticket: 3 }) + ); + let reopened = crate::ActorReadyFrameChunks::::get(0) + .and_then(|chunk| chunk.get(3).cloned().flatten()) + .expect("deferred next Cycle opens at exact N+1"); + assert_eq!(reopened.hot.cycle_state, CycleState::Running); + assert!(!reopened.hot.pending_signal); + assert_eq!(reopened.cursor, 1); + assert_eq!(reopened.eligible_at, Some(10)); + assert_eq!( + reopened + .hot + .trigger_wakeup_pointer + .map(|pointer| pointer.tick), + Some(11) + ); + let reopened_run = ActorRunStateStore::::get(actor_id) + .expect("deferred next Cycle owns a new Running state"); + assert_eq!(reopened_run.cycle_nonce, 2); + frame_assert_single_owner(); + }); +} + +#[test] +fn control_cadenced_suspended_primary_retains_lightweight_trigger_rearm() { + new_test_ext().execute_with(|| { + let step = StepOf:: { + precondition: None, + task: Task::Transfer { + to: BOB, + asset: TestAsset::Native, + amount: AmountResolution::Fixed(u128::from(u64::MAX)), + }, + on_error: StepErrorPolicy::RetryLater { max_attempts: 3 }, + }; + let actor_id = + frame_install_temporal_system_unsignaled_with_steps(contract_steps_with_step(step), true); + let opening_block = frame_promote_temporal_actor_to_ready(actor_id, 6); + + let head_before_report = Actors::queue_head(); + frame_system::Pallet::::set_block_number(opening_block); + Actors::execute_cycle_to_cutoff(Weight::MAX, (1).min(head_before_report.saturating_add(1))); + assert_eq!(Actors::queue_head().saturating_sub(head_before_report), 1); + let location = crate::ActorControlLocators::::get(actor_id) + .expect("Suspended Cadenced Actor has a primary locator"); + assert_eq!(location, C1Location::Ready { ticket: 1 }); + let cell = crate::ActorReadyFrameChunks::::get(0) + .and_then(|chunk| chunk.get(1).cloned().flatten()) + .expect("Suspended Cadenced primary cell exists"); + assert_eq!(cell.hot.cycle_state, CycleState::Suspended); + assert_eq!(cell.cursor, 0); + assert_eq!(cell.eligible_at, Some(8)); + let trigger_pointer = cell + .hot + .trigger_wakeup_pointer + .expect("Suspended primary retains cadence re-arm pointer"); + assert_eq!(trigger_pointer.tick, 11); + let trigger_key = WakeupKey::Tick(trigger_pointer.tick); + assert!( + crate::ActorWaitingFrameChunks::::get((trigger_key, trigger_pointer.page_id)) + .and_then(|chunk| chunk + .entries + .get(trigger_pointer.slot as usize) + .cloned() + .flatten()) + .is_some_and( + |entry| matches!(entry, crate::ActorWaitingEntry::Reference(reference) + if reference.actor_id == actor_id + && reference.admission_identity == cell.admission.admission_identity) + ) + ); + let run = + ActorRunStateStore::::get(actor_id).expect("Suspended Cadenced run state persists"); + assert_eq!(run.cursor, 0); + assert_eq!( + Actors::wakeup_cursor_peek_key(WakeupClock::Tick), + Some(trigger_key) + ); + assert_eq!(Actors::wakeup_cursor_peek_key(WakeupClock::Block), None); + System::reset_events(); + let (deferred_actor, deferred_location) = + Actors::control_latch_due_temporal_reference(trigger_key, opening_block, 11) + .expect("due cadence latches while Suspended"); + assert_eq!(deferred_actor, actor_id); + assert_eq!(deferred_location, location); + let deferred = crate::ActorReadyFrameChunks::::get(0) + .and_then(|chunk| chunk.get(1).cloned().flatten()) + .expect("Suspended primary survives deferred latch"); + assert_eq!(deferred.hot.cycle_state, CycleState::Suspended); + assert!(deferred.hot.pending_signal); + assert_eq!(deferred.eligible_at, Some(8)); + assert!(deferred.hot.trigger_wakeup_pointer.is_none()); + assert_eq!(Actors::wakeup_cursor_peek_key(WakeupClock::Tick), None); + assert!(has_actor_event(|event| matches!( + event, + Event::TriggerOccurrenceProcessed { + actor_id: id, + trigger_family: TriggerFamily::Cadenced, + fee, + } if *id == actor_id && *fee == 0 + ))); + + frame_system::Pallet::::set_block_number(8); + let head_before_retry = Actors::queue_head(); + frame_system::Pallet::::set_block_number(8); + Actors::execute_cycle_to_cutoff(Weight::MAX, (2).min(head_before_retry.saturating_add(1))); + assert_eq!(Actors::queue_head().saturating_sub(head_before_retry), 1); + let resuspension_key = WakeupKey::Block(10); + assert_eq!( + crate::ActorControlLocators::::get(actor_id), + Some(C1Location::Waiting { + key: resuspension_key, + page: 0, + slot: 0, + }) + ); + let resuspended = crate::ActorWaitingFrameChunks::::get((resuspension_key, 0)) + .and_then(|chunk| chunk.entries.first().cloned().flatten()) + .and_then(crate::ActorWaitingEntry::into_primary) + .expect("Cadenced retry remains one Suspended primary"); + assert_eq!(resuspended.hot.cycle_state, CycleState::Suspended); + assert!(resuspended.hot.pending_signal); + assert_eq!(resuspended.cursor, 0); + assert_eq!(resuspended.eligible_at, Some(10)); + assert!(resuspended.hot.trigger_wakeup_pointer.is_none()); + assert_eq!(Actors::wakeup_cursor_peek_key(WakeupClock::Tick), None); + assert_eq!( + Actors::wakeup_cursor_peek_key(WakeupClock::Block), + Some(resuspension_key) + ); + frame_assert_single_owner(); + }); +} + +#[test] +fn control_underfunded_at_time_closes_from_frame_authority_without_custody_movement() { + 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 = Actors::active_actor_view(actor_id) + .expect("AtTime User Actor exists before candidate projection") + .sovereign_account; + let balance = native_balance(&sovereign); + deplete_user_sovereign(actor_id, balance - TestMinUserBalance::get()); + let custody_before = native_balance(&sovereign); + Actors::trigger_wakeup_substrate_invalidate_inner(actor_id) + .expect("reference AtTime wakeup invalidates before candidate projection"); + let (state, admission, loaded_step) = Actors::load_current_step_service_state(actor_id) + .expect("underfunded AtTime Actor remains structurally coherent"); + let cell = Actors::control_unsignaled_cell_from_scalar( + actor_id, + state.identity, + state.hot, + admission, + &loaded_step, + ) + .expect("underfunded AtTime authority projects Unsignaled"); + crate::ActorUnsignaledControlCells::::insert(actor_id, cell.clone()); + crate::ActorControlLocators::::insert(actor_id, C1Location::Unsignaled); + let source_key = WakeupKey::Tick(2); + let cell = Actors::control_schedule_fresh_wakeup_reference(cell, source_key) + .expect("candidate AtTime reference schedules"); + crate::ActorUnsignaledControlCells::::insert(actor_id, cell); + clear_fee_collections(); + System::reset_events(); + + let (closed_actor, closed_location) = + Actors::control_latch_due_temporal_reference(source_key, 2, 2) + .expect("underfunded AtTime selects minimal apoptosis"); + assert_eq!(closed_actor, actor_id); + assert_eq!(closed_location, C1Location::Unsignaled); + assert!(fee_collections().is_empty()); + assert_eq!(native_balance(&sovereign), custody_before); + assert!(!crate::ActorUnsignaledControlCells::::contains_key( + actor_id + )); + assert!(!crate::ActorControlLocators::::contains_key(actor_id)); + assert!(!ActorFunding::::contains_key(actor_id)); + assert!(!crate::ActorContractHeads::::contains_key(actor_id)); + assert!(!crate::ActorStateHolds::::contains_key(actor_id)); + assert!(!ActorIdentities::::contains_key(actor_id)); + frame_assert_single_owner(); + assert_eq!(Actors::wakeup_cursor_peek_key(WakeupClock::Tick), None); + assert!(has_actor_event(|event| matches!( + event, + Event::ActorClosed { + actor_id: id, + reason: CloseReason::TriggerAdmissionInsufficient, + } if *id == actor_id + ))); + assert!(!has_actor_event(|event| matches!( + event, + Event::TriggerOccurrenceProcessed { actor_id: id, .. } if *id == actor_id + ))); + frame_assert_single_owner(); + }); +} + +#[test] +fn control_funded_at_time_latch_rolls_back_collection_failure_and_completes_once() { + 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, + contract_steps_with_step(make_step(Task::StopCycle)), + ); + let sovereign = Actors::active_actor_view(actor_id) + .expect("funded AtTime User exists before candidate projection") + .sovereign_account; + Actors::trigger_wakeup_substrate_invalidate_inner(actor_id) + .expect("reference AtTime wakeup invalidates before candidate projection"); + let (state, admission, loaded_step) = Actors::load_current_step_service_state(actor_id) + .expect("funded AtTime Actor remains coherent"); + let cell = Actors::control_unsignaled_cell_from_scalar( + actor_id, + state.identity, + state.hot, + admission, + &loaded_step, + ) + .expect("funded AtTime authority projects Unsignaled"); + crate::ActorUnsignaledControlCells::::insert(actor_id, cell.clone()); + crate::ActorControlLocators::::insert(actor_id, C1Location::Unsignaled); + let source_key = WakeupKey::Tick(2); + let cell = Actors::control_schedule_fresh_wakeup_reference(cell, source_key) + .expect("candidate funded AtTime reference schedules"); + crate::ActorUnsignaledControlCells::::insert(actor_id, cell.clone()); + let custody_before = native_balance(&sovereign); + clear_fee_collections(); + System::reset_events(); + + set_fail_fee_sink_transfer(true); + assert!(matches!( + Actors::control_latch_due_temporal_reference(source_key, 2, 2), + Err(crate::scheduler::ActorControlTransitionError::Invariant) + )); + set_fail_fee_sink_transfer(false); + clear_fee_collections(); + assert_eq!(native_balance(&sovereign), custody_before); + assert_eq!( + crate::ActorUnsignaledControlCells::::get(actor_id), + Some(cell) + ); + assert_eq!( + Actors::wakeup_cursor_peek_key(WakeupClock::Tick), + Some(source_key) + ); + assert!(!has_actor_event(|event| matches!( + event, + Event::TriggerOccurrenceProcessed { actor_id: id, .. } if *id == actor_id + ))); + + let (_, destination) = Actors::control_latch_due_temporal_reference(source_key, 2, 2) + .expect("funded AtTime occurrence latches exactly once"); + assert_eq!(fee_collections(), vec![at_time_trigger_fee()]); + assert!(has_actor_event(|event| matches!( + event, + Event::TriggerOccurrenceProcessed { + actor_id: id, + trigger_family: TriggerFamily::AtTime, + fee, + } if *id == actor_id && *fee == at_time_trigger_fee() + ))); + let C1Location::Waiting { page, .. } = destination else { + panic!("funded AtTime latch enters N+1 process Waiting"); + }; + frame_system::Pallet::::set_block_number(3); + assert_eq!( + Actors::control_promote_due_waiting_page(3, page, 3) + .expect("funded AtTime N+1 process promotes"), + vec![(actor_id, 0)] + ); + let head_before_report = Actors::queue_head(); + frame_system::Pallet::::set_block_number(3); + Actors::execute_cycle_to_cutoff(Weight::MAX, (1).min(head_before_report.saturating_add(1))); + assert_eq!(Actors::queue_head().saturating_sub(head_before_report), 1); + let completed = crate::ActorUnsignaledControlCells::::get(actor_id) + .expect("completed AtTime Actor returns to sole Unsignaled primary"); + assert_eq!(completed.hot.cycle_state, CycleState::Idle); + assert!(!completed.hot.pending_signal); + assert!(completed.eligible_at.is_none()); + assert!(completed.hot.wakeup_pointer.is_none()); + assert!(completed.hot.trigger_wakeup_pointer.is_none()); + assert!(matches!( + completed.hot.trigger_runtime_state, + TriggerRuntimeState::AtTime { consumed: true, .. } + )); + assert_eq!(Actors::wakeup_cursor_peek_key(WakeupClock::Tick), None); + assert_eq!(Actors::wakeup_cursor_peek_key(WakeupClock::Block), None); + frame_assert_single_owner(); + }); +} + +#[test] +fn control_manual_latch_charges_once_rolls_back_collection_failure_and_coalesces() { + 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(make_step(Task::StopCycle)), + ); + let sovereign = Actors::active_actor_view(actor_id) + .expect("Manual User exists before candidate projection") + .sovereign_account; + let (state, admission, loaded_step) = + Actors::load_current_step_service_state(actor_id).expect("Manual User remains coherent"); + let cell = Actors::control_unsignaled_cell_from_scalar( + actor_id, + state.identity, + state.hot, + admission, + &loaded_step, + ) + .expect("Manual authority projects Unsignaled"); + crate::ActorUnsignaledControlCells::::insert(actor_id, cell.clone()); + crate::ActorControlLocators::::insert(actor_id, C1Location::Unsignaled); + let custody_before = native_balance(&sovereign); + clear_fee_collections(); + System::reset_events(); + + set_fail_fee_sink_transfer(true); + assert!(matches!( + Actors::control_latch_manual_occurrence(actor_id, 1), + Err(crate::scheduler::ActorControlTransitionError::Invariant) + )); + set_fail_fee_sink_transfer(false); + clear_fee_collections(); + assert_eq!(native_balance(&sovereign), custody_before); + assert_eq!( + crate::ActorUnsignaledControlCells::::get(actor_id), + Some(cell) + ); + assert_eq!(Actors::wakeup_cursor_peek_key(WakeupClock::Block), None); + assert!(!has_actor_event(|event| matches!( + event, + Event::TriggerOccurrenceProcessed { actor_id: id, .. } if *id == actor_id + ))); + + let destination = Actors::control_latch_manual_occurrence(actor_id, 1) + .expect("funded Manual occurrence commits") + .expect("first Manual occurrence latches"); + assert_eq!(fee_collections(), vec![manual_trigger_fee()]); + assert!(has_actor_event(|event| matches!( + event, + Event::TriggerOccurrenceProcessed { + actor_id: id, + trigger_family: TriggerFamily::Manual, + fee, + } if *id == actor_id && *fee == manual_trigger_fee() + ))); + assert!(has_actor_event(|event| matches!( + event, + Event::ManualTriggerSet { actor_id: id } if *id == actor_id + ))); + assert_eq!( + Actors::control_latch_manual_occurrence(actor_id, 1) + .expect("latched Manual occurrence coalesces"), + None + ); + assert_eq!(fee_collections(), vec![manual_trigger_fee()]); + let processed_count = System::events() + .iter() + .filter(|record| { + matches!( + &record.event, + RuntimeEvent::Actors(Event::TriggerOccurrenceProcessed { + actor_id: id, + trigger_family: TriggerFamily::Manual, + .. + }) if *id == actor_id + ) + }) + .count(); + assert_eq!(processed_count, 1); + + let C1Location::Waiting { page, .. } = destination else { + panic!("Manual latch enters N+1 process Waiting"); + }; + frame_system::Pallet::::set_block_number(2); + assert_eq!( + Actors::control_promote_due_waiting_page(2, page, 2).expect("Manual N+1 process promotes"), + vec![(actor_id, 0)] + ); + let head_before_report = Actors::queue_head(); + frame_system::Pallet::::set_block_number(2); + Actors::execute_cycle_to_cutoff(Weight::MAX, (1).min(head_before_report.saturating_add(1))); + assert_eq!(Actors::queue_head().saturating_sub(head_before_report), 1); + let completed = crate::ActorUnsignaledControlCells::::get(actor_id) + .expect("completed Manual Actor returns to Unsignaled"); + assert_eq!(completed.hot.cycle_state, CycleState::Idle); + assert!(!completed.hot.pending_signal); + assert!(completed.eligible_at.is_none()); + assert_eq!(Actors::wakeup_cursor_peek_key(WakeupClock::Block), None); + frame_assert_single_owner(); + }); +} + +#[test] +fn control_manual_due_while_running_latches_one_deferred_next_cycle() { + 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::StopCycle), + ]) + .expect("two-Step Manual Contract fits"); + let actor_id = create_user_with(ALICE, Mutability::Mutable, manual_schedule(), None, steps); + let sovereign = Actors::active_actor_view(actor_id) + .expect("Manual Running fixture exists before candidate projection") + .sovereign_account; + let _ = ::AssetOps::mint( + &sovereign, + ::FeeNativeAssetId::get(), + u128::from(u64::MAX / 4), + ); + let (state, admission, loaded_step) = Actors::load_current_step_service_state(actor_id) + .expect("Manual Running fixture remains coherent"); + let cell = Actors::control_unsignaled_cell_from_scalar( + actor_id, + state.identity, + state.hot, + admission, + &loaded_step, + ) + .expect("Manual Running authority projects Unsignaled"); + crate::ActorUnsignaledControlCells::::insert(actor_id, cell); + crate::ActorControlLocators::::insert(actor_id, C1Location::Unsignaled); + + let first = Actors::control_latch_manual_occurrence(actor_id, 1) + .expect("first Manual occurrence commits") + .expect("first Manual occurrence latches"); + let C1Location::Waiting { page, .. } = first else { + panic!("first Manual occurrence enters N+1 Waiting"); + }; + frame_system::Pallet::::set_block_number(2); + assert_eq!( + Actors::control_promote_due_waiting_page(2, page, 2) + .expect("first Manual N+1 process promotes"), + vec![(actor_id, 0)] + ); + let head_before_opening = Actors::queue_head(); + frame_system::Pallet::::set_block_number(2); + Actors::execute_cycle_to_cutoff(Weight::MAX, (1).min(head_before_opening.saturating_add(1))); + assert_eq!(Actors::queue_head().saturating_sub(head_before_opening), 1); + let running_location = C1Location::Ready { ticket: 1 }; + assert_eq!( + crate::ActorControlLocators::::get(actor_id), + Some(running_location) + ); + let running = crate::ActorReadyFrameChunks::::get(0) + .and_then(|chunk| chunk.get(1).cloned().flatten()) + .expect("Manual cycle owns one Running primary"); + assert_eq!(running.hot.cycle_state, CycleState::Running); + assert!(!running.hot.pending_signal); + assert_eq!(running.cursor, 1); + assert_eq!(running.eligible_at, Some(3)); + assert!(running.hot.trigger_wakeup_pointer.is_none()); + + clear_fee_collections(); + System::reset_events(); + assert_eq!( + Actors::control_latch_manual_occurrence(actor_id, 2).expect("busy Manual occurrence commits"), + Some(running_location) + ); + assert_eq!(fee_collections(), vec![manual_trigger_fee()]); + let deferred = crate::ActorReadyFrameChunks::::get(0) + .and_then(|chunk| chunk.get(1).cloned().flatten()) + .expect("busy Manual latch preserves Running primary"); + assert_eq!(deferred.hot.cycle_state, CycleState::Running); + assert!(deferred.hot.pending_signal); + assert_eq!(deferred.cursor, 1); + assert_eq!(deferred.eligible_at, Some(3)); + assert_eq!( + Actors::control_latch_manual_occurrence(actor_id, 2) + .expect("second busy Manual occurrence coalesces"), + None + ); + assert_eq!(fee_collections(), vec![manual_trigger_fee()]); + + frame_system::Pallet::::set_block_number(3); + let head_before_completion = Actors::queue_head(); + frame_system::Pallet::::set_block_number(3); + Actors::execute_cycle_to_cutoff( + Weight::MAX, + (2).min(head_before_completion.saturating_add(1)), + ); + assert_eq!( + Actors::queue_head().saturating_sub(head_before_completion), + 1 + ); + let next_opening = crate::ActorReadyFrameChunks::::get(0) + .and_then(|chunk| chunk.get(2).cloned().flatten()) + .expect("current Manual cycle hands deferred latch to N+1"); + assert_eq!(next_opening.hot.cycle_state, CycleState::Idle); + assert!(next_opening.hot.pending_signal); + assert_eq!(next_opening.cursor, 0); + assert_eq!(next_opening.eligible_at, Some(4)); + + frame_system::Pallet::::set_block_number(4); + let head_before_reopened = Actors::queue_head(); + frame_system::Pallet::::set_block_number(4); + Actors::execute_cycle_to_cutoff(Weight::MAX, (3).min(head_before_reopened.saturating_add(1))); + assert_eq!(Actors::queue_head().saturating_sub(head_before_reopened), 1); + let next_running = crate::ActorReadyFrameChunks::::get(0) + .and_then(|chunk| chunk.get(3).cloned().flatten()) + .expect("deferred Manual next Cycle opens exactly once"); + assert_eq!(next_running.hot.cycle_state, CycleState::Running); + assert!(!next_running.hot.pending_signal); + assert_eq!(next_running.cursor, 1); + assert_eq!(next_running.eligible_at, Some(5)); + assert!(next_running.hot.trigger_wakeup_pointer.is_none()); + frame_assert_single_owner(); + }); +} + +#[test] +fn control_manual_due_while_suspended_survives_block_waiting_replacement() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let step = StepOf:: { + precondition: None, + task: Task::Transfer { + to: BOB, + asset: TestAsset::Native, + amount: AmountResolution::Fixed(u128::from(u64::MAX)), + }, + on_error: StepErrorPolicy::RetryLater { max_attempts: 3 }, + }; + let actor_id = create_user_with( + ALICE, + Mutability::Mutable, + manual_schedule(), + None, + contract_steps_with_step(step), + ); + let sovereign = Actors::active_actor_view(actor_id) + .expect("Manual Suspended fixture exists before candidate projection") + .sovereign_account; + let _ = ::AssetOps::mint( + &sovereign, + ::FeeNativeAssetId::get(), + u128::from(u64::MAX / 4), + ); + let (state, admission, loaded_step) = Actors::load_current_step_service_state(actor_id) + .expect("Manual Suspended fixture remains coherent"); + let cell = Actors::control_unsignaled_cell_from_scalar( + actor_id, + state.identity, + state.hot, + admission, + &loaded_step, + ) + .expect("Manual Suspended authority projects Unsignaled"); + crate::ActorUnsignaledControlCells::::insert(actor_id, cell); + crate::ActorControlLocators::::insert(actor_id, C1Location::Unsignaled); + + let first = Actors::control_latch_manual_occurrence(actor_id, 1) + .expect("first Manual occurrence commits") + .expect("first Manual occurrence latches"); + let C1Location::Waiting { page, .. } = first else { + panic!("first Manual occurrence enters N+1 Waiting"); + }; + frame_system::Pallet::::set_block_number(2); + assert_eq!( + Actors::control_promote_due_waiting_page(2, page, 2) + .expect("first Manual N+1 process promotes"), + vec![(actor_id, 0)] + ); + let head_before_opening = Actors::queue_head(); + frame_system::Pallet::::set_block_number(2); + Actors::execute_cycle_to_cutoff(Weight::MAX, (1).min(head_before_opening.saturating_add(1))); + assert_eq!(Actors::queue_head().saturating_sub(head_before_opening), 1); + let suspended_location = C1Location::Ready { ticket: 1 }; + let suspended = crate::ActorReadyFrameChunks::::get(0) + .and_then(|chunk| chunk.get(1).cloned().flatten()) + .expect("Manual cycle owns one Suspended primary"); + assert_eq!(suspended.hot.cycle_state, CycleState::Suspended); + assert!(!suspended.hot.pending_signal); + assert_eq!(suspended.eligible_at, Some(3)); + + clear_fee_collections(); + assert_eq!( + Actors::control_latch_manual_occurrence(actor_id, 2) + .expect("busy Suspended Manual occurrence commits"), + Some(suspended_location) + ); + assert_eq!(fee_collections(), vec![manual_trigger_fee()]); + let deferred = crate::ActorReadyFrameChunks::::get(0) + .and_then(|chunk| chunk.get(1).cloned().flatten()) + .expect("Manual deferred latch preserves Suspended primary"); + assert_eq!(deferred.hot.cycle_state, CycleState::Suspended); + assert!(deferred.hot.pending_signal); + assert_eq!(deferred.eligible_at, Some(3)); + assert_eq!( + Actors::control_latch_manual_occurrence(actor_id, 2) + .expect("repeated Suspended Manual occurrence coalesces"), + None + ); + assert_eq!(fee_collections(), vec![manual_trigger_fee()]); + + frame_system::Pallet::::set_block_number(3); + let head_before_retry = Actors::queue_head(); + frame_system::Pallet::::set_block_number(3); + Actors::execute_cycle_to_cutoff(Weight::MAX, (2).min(head_before_retry.saturating_add(1))); + assert_eq!(Actors::queue_head().saturating_sub(head_before_retry), 1); + let waiting_key = WakeupKey::Block(5); + let waiting_location = C1Location::Waiting { + key: waiting_key, + page: 0, + slot: 0, + }; + assert_eq!( + crate::ActorControlLocators::::get(actor_id), + Some(waiting_location) + ); + let resuspended = crate::ActorWaitingFrameChunks::::get((waiting_key, 0)) + .and_then(|chunk| chunk.entries.first().cloned().flatten()) + .and_then(crate::ActorWaitingEntry::into_primary) + .expect("Manual deferred latch survives Block Waiting replacement"); + assert_eq!(resuspended.hot.cycle_state, CycleState::Suspended); + assert!(resuspended.hot.pending_signal); + assert_eq!(resuspended.cursor, 0); + assert_eq!(resuspended.eligible_at, Some(5)); + assert!(resuspended.hot.trigger_wakeup_pointer.is_none()); + assert_eq!( + Actors::wakeup_cursor_peek_key(WakeupClock::Block), + Some(waiting_key) + ); + assert_eq!(Actors::wakeup_cursor_peek_key(WakeupClock::Tick), None); + frame_assert_single_owner(); + }); +} + +#[test] +fn ordinary_fifo_preserves_ingress_latched_by_an_earlier_step() { + let execute = || { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let recipient = create_system_with( + ALICE, + on_address_event_schedule(SourceFilter::Any, AssetFilter::Any), + None, + BoundedVec::try_from(vec![ + make_step(Task::Transfer { + to: BOB, + asset: TestAsset::Native, + amount: AmountResolution::Fixed(1), + }), + make_step(Task::StopCycle), + ]) + .unwrap(), + ); + let recipient_account = Actors::active_actor_view(recipient) + .unwrap() + .sovereign_account; + assert_ok!(::AssetOps::mint( + &recipient_account, + TestAsset::Native, + 1_000_000, + )); + assert_ok!(Actors::notify_address_event( + recipient, + TestAsset::Native, + 1, + &ALICE + )); + assert_eq!(Actors::actor_hot(recipient).unwrap().queue_ticket, Some(0)); + let sender = create_system_with( + ALICE, + manual_schedule(), + None, + contract_steps_with_step(make_step(Task::Transfer { + to: recipient_account, + asset: TestAsset::Native, + amount: AmountResolution::Fixed(7), + })), + ); + let sender_account = Actors::active_actor_view(sender).unwrap().sovereign_account; + assert_ok!(::AssetOps::mint( + &sender_account, + TestAsset::Native, + 1_000_000, + )); + assert_ok!(Actors::manual_trigger(RuntimeOrigin::signed(ALICE), sender)); + let head_before_opening = Actors::queue_head(); + frame_system::Pallet::::set_block_number(1); + Actors::execute_cycle_to_cutoff( + Weight::MAX, + (Actors::queue_tail()).min(head_before_opening.saturating_add(1)), + ); + assert_eq!(Actors::queue_head().saturating_sub(head_before_opening), 1); + let hot = Actors::actor_hot(recipient).unwrap(); + assert_eq!(hot.cycle_state, CycleState::Running); + assert!(!hot.pending_signal); + assert_eq!(Actors::actor_hot(sender).unwrap().queue_ticket, Some(1)); + assert_eq!(hot.queue_ticket, Some(2)); + >::enable_asset_ops_ingress(); + frame_system::Pallet::::set_block_number(2); + System::reset_events(); + + Actors::on_idle(2, Weight::MAX); + + assert!( + System::events().iter().any(|record| matches!( + record.event, + RuntimeEvent::Actors(crate::Event::TriggerOccurrenceProcessed { + actor_id, trigger_family: TriggerFamily::AddressEvent, .. + }) if actor_id == recipient + )), + "the earlier Transfer must traverse the certified AddressEvent ingress" + ); + let state = Actors::active_actor_view(recipient).unwrap(); + assert_eq!(state.cycle_nonce, 1); + assert_eq!(state.cycle_state, CycleState::Idle); + state.pending_signal + }) + }; + let ordinary = execute(); + assert!( + ordinary, + "ordinary FIFO retains the newly paid deferred latch" + ); +} + +#[test] +fn control_address_event_commits_funding_independently_and_latches_once() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let asset_filter = AssetFilter::Whitelist( + BoundedVec::try_from(vec![TestAsset::Native]).expect("one AddressEvent asset filter fits"), + ); + let actor_id = create_user_with( + ALICE, + Mutability::Mutable, + on_address_event_schedule(SourceFilter::OwnerOnly, asset_filter), + None, + contract_steps_with_step(make_step(Task::Transfer { + to: BOB, + asset: TestAsset::Native, + amount: AmountResolution::PercentageOfLastFunding(Perbill::one()), + })), + ); + let sovereign = Actors::active_actor_view(actor_id) + .expect("AddressEvent User exists before candidate projection") + .sovereign_account; + let _ = ::AssetOps::mint( + &sovereign, + ::FeeNativeAssetId::get(), + u128::from(u64::MAX / 4), + ); + let (state, admission, loaded_step) = Actors::load_current_step_service_state(actor_id) + .expect("AddressEvent User remains coherent"); + let cell = Actors::control_unsignaled_cell_from_scalar( + actor_id, + state.identity, + state.hot, + admission, + &loaded_step, + ) + .expect("AddressEvent authority projects Unsignaled"); + crate::ActorUnsignaledControlCells::::insert(actor_id, cell.clone()); + crate::ActorControlLocators::::insert(actor_id, C1Location::Unsignaled); + let provenance = crate::FundingProvenance::Signed; + clear_fee_collections(); + System::reset_events(); + + assert_eq!( + Actors::control_apply_address_event( + actor_id, + TestAsset::Native, + 10, + Some(&BOB), + Some(&provenance), + 1, + ) + .expect("unmatched AddressEvent is a balance-only consequence"), + None + ); + assert!( + ActorFunding::::get(actor_id) + .expect("unmatched AddressEvent funding state remains") + .funding_accumulated + .is_empty() + ); + assert!(fee_collections().is_empty()); + assert!(!has_actor_event(|event| matches!( + event, + Event::TriggerOccurrenceProcessed { actor_id: id, .. } if *id == actor_id + ))); + + set_fail_fee_sink_transfer(true); + assert_eq!( + Actors::control_apply_address_event( + actor_id, + TestAsset::Native, + 100, + Some(&ALICE), + Some(&provenance), + 1, + ) + .expect("failed Trigger collection retains independent funding consequence"), + None + ); + set_fail_fee_sink_transfer(false); + clear_fee_collections(); + assert_eq!( + ActorFunding::::get(actor_id) + .expect("AddressEvent funding remains") + .funding_accumulated + .get(&TestAsset::Native), + Some(&100) + ); + assert_eq!( + crate::ActorUnsignaledControlCells::::get(actor_id), + Some(cell) + ); + assert!(has_actor_event(|event| matches!( + event, + Event::FundingAccumulated { + actor_id: id, + asset: TestAsset::Native, + added: 100, + accumulated: 100, + } if *id == actor_id + ))); + assert!(!has_actor_event(|event| matches!( + event, + Event::TriggerOccurrenceProcessed { actor_id: id, .. } if *id == actor_id + ))); + + let destination = Actors::control_apply_address_event( + actor_id, + TestAsset::Native, + 50, + Some(&ALICE), + Some(&provenance), + 1, + ) + .expect("funded AddressEvent transition commits") + .expect("matched AddressEvent latches"); + assert_eq!(fee_collections(), vec![address_event_trigger_fee()]); + assert_eq!( + ActorFunding::::get(actor_id) + .expect("AddressEvent funding remains") + .funding_accumulated + .get(&TestAsset::Native), + Some(&150) + ); + assert!(has_actor_event(|event| matches!( + event, + Event::TriggerOccurrenceProcessed { + actor_id: id, + trigger_family: TriggerFamily::AddressEvent, + fee, + } if *id == actor_id && *fee == address_event_trigger_fee() + ))); + + assert_eq!( + Actors::control_apply_address_event( + actor_id, + TestAsset::Native, + 25, + Some(&ALICE), + Some(&provenance), + 1, + ) + .expect("latched AddressEvent still commits funding"), + None + ); + assert_eq!(fee_collections(), vec![address_event_trigger_fee()]); + assert_eq!( + ActorFunding::::get(actor_id) + .expect("coalesced AddressEvent funding remains") + .funding_accumulated + .get(&TestAsset::Native), + Some(&175) + ); + let C1Location::Waiting { page, .. } = destination else { + panic!("matched AddressEvent enters N+1 Waiting"); + }; + frame_system::Pallet::::set_block_number(2); + assert_eq!( + Actors::control_promote_due_waiting_page(2, page, 2) + .expect("AddressEvent N+1 process promotes"), + vec![(actor_id, 0)] + ); + let head_before_report = Actors::queue_head(); + frame_system::Pallet::::set_block_number(2); + Actors::execute_cycle_to_cutoff(Weight::MAX, (1).min(head_before_report.saturating_add(1))); + assert_eq!(Actors::queue_head().saturating_sub(head_before_report), 1); + let completed = crate::ActorUnsignaledControlCells::::get(actor_id) + .expect("AddressEvent one-Step cycle completes"); + assert_eq!(completed.hot.cycle_state, CycleState::Idle); + assert!(!completed.hot.pending_signal); + assert!(completed.hot.trigger_wakeup_pointer.is_none()); + frame_assert_single_owner(); + }); +} + +#[test] +fn control_address_event_due_while_running_accumulates_and_defers_in_place() { + 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::PercentageOfLastFunding(Perbill::one()), + }), + make_step(Task::StopCycle), + ]) + .expect("two-Step AddressEvent Contract fits"); + let actor_id = create_user_with( + ALICE, + Mutability::Mutable, + percentage_trigger_schedule(), + None, + steps, + ); + let sovereign = Actors::active_actor_view(actor_id) + .expect("AddressEvent Running fixture exists before candidate projection") + .sovereign_account; + let _ = ::AssetOps::mint( + &sovereign, + ::FeeNativeAssetId::get(), + u128::from(u64::MAX / 4), + ); + let (state, admission, loaded_step) = Actors::load_current_step_service_state(actor_id) + .expect("AddressEvent Running fixture remains coherent"); + let cell = Actors::control_unsignaled_cell_from_scalar( + actor_id, + state.identity, + state.hot, + admission, + &loaded_step, + ) + .expect("AddressEvent Running authority projects Unsignaled"); + crate::ActorUnsignaledControlCells::::insert(actor_id, cell); + crate::ActorControlLocators::::insert(actor_id, C1Location::Unsignaled); + let provenance = crate::FundingProvenance::Signed; + + let first = Actors::control_apply_address_event( + actor_id, + TestAsset::Native, + 100, + Some(&ALICE), + Some(&provenance), + 1, + ) + .expect("first AddressEvent transition commits") + .expect("first AddressEvent latches"); + let C1Location::Waiting { page, .. } = first else { + panic!("first AddressEvent enters N+1 Waiting"); + }; + frame_system::Pallet::::set_block_number(2); + assert_eq!( + Actors::control_promote_due_waiting_page(2, page, 2) + .expect("first AddressEvent N+1 process promotes"), + vec![(actor_id, 0)] + ); + let head_before_opening = Actors::queue_head(); + frame_system::Pallet::::set_block_number(2); + Actors::execute_cycle_to_cutoff(Weight::MAX, (1).min(head_before_opening.saturating_add(1))); + assert_eq!(Actors::queue_head().saturating_sub(head_before_opening), 1); + let running_location = C1Location::Ready { ticket: 1 }; + let running = crate::ActorReadyFrameChunks::::get(0) + .and_then(|chunk| chunk.get(1).cloned().flatten()) + .expect("AddressEvent cycle owns one Running primary"); + assert_eq!(running.hot.cycle_state, CycleState::Running); + assert!(!running.hot.pending_signal); + assert_eq!(running.cursor, 1); + assert_eq!(running.eligible_at, Some(3)); + assert!( + ActorFunding::::get(actor_id) + .expect("Opening funding state remains") + .funding_accumulated + .is_empty() + ); + + clear_fee_collections(); + assert_eq!( + Actors::control_apply_address_event( + actor_id, + TestAsset::Native, + 25, + Some(&ALICE), + Some(&provenance), + 2, + ) + .expect("busy AddressEvent transition commits"), + Some(running_location) + ); + assert_eq!(fee_collections(), vec![address_event_trigger_fee()]); + let deferred = crate::ActorReadyFrameChunks::::get(0) + .and_then(|chunk| chunk.get(1).cloned().flatten()) + .expect("busy AddressEvent preserves Running primary"); + assert_eq!(deferred.hot.cycle_state, CycleState::Running); + assert!(deferred.hot.pending_signal); + assert_eq!(deferred.cursor, 1); + assert_eq!(deferred.eligible_at, Some(3)); + assert_eq!( + Actors::control_apply_address_event( + actor_id, + TestAsset::Native, + 5, + Some(&ALICE), + Some(&provenance), + 2, + ) + .expect("latched AddressEvent still commits funding"), + None + ); + assert_eq!(fee_collections(), vec![address_event_trigger_fee()]); + assert_eq!( + ActorFunding::::get(actor_id) + .expect("busy AddressEvent funding remains") + .funding_accumulated + .get(&TestAsset::Native), + Some(&30) + ); + + frame_system::Pallet::::set_block_number(3); + let head_before_completion = Actors::queue_head(); + frame_system::Pallet::::set_block_number(3); + Actors::execute_cycle_to_cutoff( + Weight::MAX, + (2).min(head_before_completion.saturating_add(1)), + ); + assert_eq!( + Actors::queue_head().saturating_sub(head_before_completion), + 1 + ); + let next_opening = crate::ActorReadyFrameChunks::::get(0) + .and_then(|chunk| chunk.get(2).cloned().flatten()) + .expect("AddressEvent deferred latch enters exact N+1"); + assert_eq!(next_opening.hot.cycle_state, CycleState::Idle); + assert!(next_opening.hot.pending_signal); + assert_eq!(next_opening.cursor, 0); + assert_eq!(next_opening.eligible_at, Some(4)); + assert!(next_opening.hot.trigger_wakeup_pointer.is_none()); + frame_assert_single_owner(); + }); +} + +#[test] +fn control_observation_change_disables_on_latch_and_rearms_at_opening() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let feed = 7; + let actor_id = create_user_with( + ALICE, + Mutability::Mutable, + observation_schedule(vec![feed]), + None, + contract_steps_with_step(make_step(Task::StopCycle)), + ); + let sovereign = Actors::active_actor_view(actor_id) + .expect("ObservationChange User exists before candidate projection") + .sovereign_account; + let _ = ::AssetOps::mint( + &sovereign, + ::FeeNativeAssetId::get(), + u128::from(u64::MAX / 4), + ); + let subscription = crate::ObservationSubscriptionSlot::::get(actor_id) + .expect("ObservationChange subscription exists"); + let (state, admission, loaded_step) = Actors::load_current_step_service_state(actor_id) + .expect("ObservationChange User remains coherent"); + let cell = Actors::control_unsignaled_cell_from_scalar( + actor_id, + state.identity, + state.hot, + admission, + &loaded_step, + ) + .expect("ObservationChange authority projects Unsignaled"); + crate::ActorUnsignaledControlCells::::insert(actor_id, cell.clone()); + crate::ActorControlLocators::::insert(actor_id, C1Location::Unsignaled); + clear_fee_collections(); + System::reset_events(); + + set_fail_fee_sink_transfer(true); + assert_eq!( + Actors::control_latch_observation_change_occurrence(actor_id, feed, 1) + .expect("failed ObservationChange collection is nonfatal"), + None + ); + set_fail_fee_sink_transfer(false); + clear_fee_collections(); + assert_eq!( + crate::ActorUnsignaledControlCells::::get(actor_id), + Some(cell) + ); + assert!(!crate::IndexedTriggerDetectionDisabled::::contains_key(actor_id)); + assert!(!has_actor_event(|event| matches!( + event, + Event::TriggerOccurrenceProcessed { actor_id: id, .. } if *id == actor_id + ))); + + let destination = Actors::control_latch_observation_change_occurrence(actor_id, feed, 1) + .expect("funded ObservationChange occurrence commits") + .expect("ObservationChange occurrence latches"); + assert_eq!(fee_collections(), vec![observation_change_trigger_fee()]); + assert!(crate::IndexedTriggerDetectionDisabled::::contains_key(actor_id)); + assert!(has_actor_event(|event| matches!( + event, + Event::TriggerOccurrenceProcessed { + actor_id: id, + trigger_family: TriggerFamily::ObservationChange, + fee, + } if *id == actor_id && *fee == observation_change_trigger_fee() + ))); + assert_eq!( + Actors::control_latch_observation_change_occurrence(actor_id, feed, 1) + .expect("disabled ObservationChange occurrence coalesces"), + None + ); + assert_eq!(fee_collections(), vec![observation_change_trigger_fee()]); + + let C1Location::Waiting { page, .. } = destination else { + panic!("ObservationChange latch enters N+1 Waiting"); + }; + frame_system::Pallet::::set_block_number(2); + assert_eq!( + Actors::control_promote_due_waiting_page(2, page, 2) + .expect("ObservationChange N+1 process promotes"), + vec![(actor_id, 0)] + ); + let head_before_report = Actors::queue_head(); + frame_system::Pallet::::set_block_number(2); + Actors::execute_cycle_to_cutoff(Weight::MAX, (1).min(head_before_report.saturating_add(1))); + assert_eq!(Actors::queue_head().saturating_sub(head_before_report), 1); + let completed = crate::ActorUnsignaledControlCells::::get(actor_id) + .expect("ObservationChange one-Step cycle completes"); + assert_eq!(completed.hot.cycle_state, CycleState::Idle); + assert!(!completed.hot.pending_signal); + assert!(!crate::IndexedTriggerDetectionDisabled::::contains_key(actor_id)); + assert_eq!( + crate::ObservationSubscriptionSlot::::get(actor_id), + Some(subscription) + ); + assert!(completed.hot.trigger_wakeup_pointer.is_none()); + frame_assert_single_owner(); + }); +} + +#[test] +fn control_observation_change_fanout_consumes_one_bounded_subscription_page() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let feed = 7; + let mut actor_ids = Vec::new(); + let mut page_id = None; + for owner in [ALICE, BOB] { + let actor_id = create_user_with( + owner, + Mutability::Mutable, + observation_schedule(vec![feed]), + None, + contract_steps_with_step(make_step(Task::StopCycle)), + ); + let sovereign = Actors::active_actor_view(actor_id) + .expect("ObservationChange cohort member exists before projection") + .sovereign_account; + let _ = ::AssetOps::mint( + &sovereign, + ::FeeNativeAssetId::get(), + u128::from(u64::MAX / 4), + ); + let slot = crate::ObservationSubscriptionSlot::::get(actor_id) + .expect("ObservationChange cohort member owns one subscription slot"); + let page_size: u32 = ::ObservationPageSize::get(); + let actor_page = slot / page_size; + if let Some(expected) = page_id { + assert_eq!(actor_page, expected); + } else { + page_id = Some(actor_page); + } + let (state, admission, loaded_step) = Actors::load_current_step_service_state(actor_id) + .expect("ObservationChange cohort member remains coherent"); + let cell = Actors::control_unsignaled_cell_from_scalar( + actor_id, + state.identity, + state.hot, + admission, + &loaded_step, + ) + .expect("ObservationChange cohort authority projects Unsignaled"); + crate::ActorUnsignaledControlCells::::insert(actor_id, cell); + crate::ActorControlLocators::::insert(actor_id, C1Location::Unsignaled); + actor_ids.push(actor_id); + } + let page_id = page_id.expect("ObservationChange cohort page exists"); + let page_before = crate::ObservationSubscriberPages::::get(feed, page_id) + .expect("ObservationChange canonical subscriber page exists"); + clear_fee_collections(); + System::reset_events(); + + let outcomes = Actors::control_latch_observation_change_page(feed, page_id, 1) + .expect("one bounded ObservationChange page fanout commits"); + assert_eq!(outcomes.len(), actor_ids.len()); + assert!(outcomes.iter().all(|(_, outcome)| outcome.is_some())); + assert_eq!( + fee_collections(), + vec![ + observation_change_trigger_fee(), + observation_change_trigger_fee(), + ] + ); + assert_eq!( + crate::ObservationSubscriberPages::::get(feed, page_id), + Some(page_before) + ); + for actor_id in actor_ids { + assert!(crate::IndexedTriggerDetectionDisabled::::contains_key(actor_id)); + assert!(matches!( + crate::ActorControlLocators::::get(actor_id), + Some(C1Location::Waiting { + key: WakeupKey::Block(2), + .. + }) + )); + frame_assert_single_owner(); + } + let processed = System::events() + .iter() + .filter(|record| { + matches!( + &record.event, + RuntimeEvent::Actors(Event::TriggerOccurrenceProcessed { + trigger_family: TriggerFamily::ObservationChange, + .. + }) + ) + }) + .count(); + assert_eq!(processed, 2); + frame_assert_single_owner(); + }); +} + +#[test] +fn control_observation_crossing_preserves_phase_on_fee_failure_and_rearms_at_opening() { + 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, + contract_steps_with_step(make_step(Task::StopCycle)), + ); + let sovereign = Actors::active_actor_view(actor_id) + .expect("ObservationCrossing User exists before candidate projection") + .sovereign_account; + let _ = ::AssetOps::mint( + &sovereign, + ::FeeNativeAssetId::get(), + u128::from(u64::MAX / 4), + ); + let initial_membership = crate::CrossingMemberships::::get(actor_id) + .expect("ObservationCrossing Armed membership exists"); + assert_eq!(initial_membership.key.threshold, 100); + let (state, admission, loaded_step) = Actors::load_current_step_service_state(actor_id) + .expect("ObservationCrossing User remains coherent"); + let cell = Actors::control_unsignaled_cell_from_scalar( + actor_id, + state.identity, + state.hot, + admission, + &loaded_step, + ) + .expect("ObservationCrossing authority projects Unsignaled"); + crate::ActorUnsignaledControlCells::::insert(actor_id, cell); + crate::ActorControlLocators::::insert(actor_id, C1Location::Unsignaled); + clear_fee_collections(); + System::reset_events(); + + set_fail_fee_sink_transfer(true); + assert_eq!( + Actors::control_latch_observation_crossing_fire( + actor_id, + crate::ObservationTransition { + revision: 2, + previous: Some(50), + current: 150, + }, + 1, + ) + .expect("failed Crossing fee still commits hysteresis phase"), + None + ); + set_fail_fee_sink_transfer(false); + clear_fee_collections(); + let after_failed_fire = crate::ActorUnsignaledControlCells::::get(actor_id) + .expect("failed Crossing fee leaves sole Unsignaled primary"); + assert!(matches!( + after_failed_fire.hot.trigger_runtime_state, + TriggerRuntimeState::ObservationCrossing { + phase: CrossingPhase::WaitingForRearm, + .. + } + )); + assert_eq!( + crate::CrossingMemberships::::get(actor_id) + .expect("failed Crossing fee retains rearm membership") + .key + .threshold, + 80 + ); + assert!(!after_failed_fire.hot.pending_signal); + assert!(!crate::IndexedTriggerDetectionDisabled::::contains_key(actor_id)); + assert!(!has_actor_event(|event| matches!( + event, + Event::TriggerOccurrenceProcessed { actor_id: id, .. } if *id == actor_id + ))); + + Actors::control_apply_observation_crossing_rearm( + actor_id, + crate::ObservationTransition { + revision: 3, + previous: Some(150), + current: 70, + }, + ) + .expect("Crossing hysteresis rearm commits without Trigger occurrence"); + let rearmed = crate::ActorUnsignaledControlCells::::get(actor_id) + .expect("rearmed Crossing primary remains Unsignaled"); + assert!(matches!( + rearmed.hot.trigger_runtime_state, + TriggerRuntimeState::ObservationCrossing { + phase: CrossingPhase::Armed, + .. + } + )); + assert_eq!( + crate::CrossingMemberships::::get(actor_id) + .expect("Crossing Fire membership is restored") + .key + .threshold, + 100 + ); + + let destination = Actors::control_latch_observation_crossing_fire( + actor_id, + crate::ObservationTransition { + revision: 4, + previous: Some(70), + current: 150, + }, + 1, + ) + .expect("funded Crossing Fire commits") + .expect("funded Crossing Fire latches"); + assert_eq!(fee_collections(), vec![observation_crossing_trigger_fee()]); + assert!(crate::IndexedTriggerDetectionDisabled::::contains_key(actor_id)); + assert!(has_actor_event(|event| matches!( + event, + Event::TriggerOccurrenceProcessed { + actor_id: id, + trigger_family: TriggerFamily::ObservationCrossing, + fee, + } if *id == actor_id && *fee == observation_crossing_trigger_fee() + ))); + assert_eq!( + Actors::control_latch_observation_crossing_fire( + actor_id, + crate::ObservationTransition { + revision: 5, + previous: Some(150), + current: 200, + }, + 1, + ) + .expect("disabled Crossing detector coalesces redundant work"), + None + ); + assert_eq!(fee_collections(), vec![observation_crossing_trigger_fee()]); + + set_observation( + 7, + crate::ScalarObservationState::Fresh { + value: 70, + observed_at: 4, + }, + ); + let C1Location::Waiting { page, .. } = destination else { + panic!("Crossing Fire enters N+1 Waiting"); + }; + frame_system::Pallet::::set_block_number(2); + assert_eq!( + Actors::control_promote_due_waiting_page(2, page, 2).expect("Crossing N+1 process promotes"), + vec![(actor_id, 0)] + ); + let head_before_report = Actors::queue_head(); + frame_system::Pallet::::set_block_number(2); + Actors::execute_cycle_to_cutoff(Weight::MAX, (1).min(head_before_report.saturating_add(1))); + assert_eq!(Actors::queue_head().saturating_sub(head_before_report), 1); + let completed = crate::ActorUnsignaledControlCells::::get(actor_id) + .expect("Crossing one-Step cycle completes"); + assert!(matches!( + completed.hot.trigger_runtime_state, + TriggerRuntimeState::ObservationCrossing { + phase: CrossingPhase::Armed, + installed_at_revision: 1, + } + )); + assert_eq!( + crate::CrossingMemberships::::get(actor_id) + .expect("Opening-time Crossing rearm membership exists") + .key + .threshold, + 100 + ); + assert!(!crate::IndexedTriggerDetectionDisabled::::contains_key(actor_id)); + assert!(!completed.hot.pending_signal); + frame_assert_single_owner(); + }); +} + +#[test] +fn control_manual_trigger_boundary_matches_immutable_oracle_logical_fee_and_event_authority() { + let execute = || { + 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(make_step(Task::StopCycle)), + ); + let sovereign = Actors::active_actor_view(actor_id) + .expect("Manual differential Actor exists") + .sovereign_account; + let _ = ::AssetOps::mint( + &sovereign, + ::FeeNativeAssetId::get(), + u128::from(u64::MAX / 4), + ); + let custody_before = native_balance(&sovereign); + clear_fee_collections(); + System::reset_events(); + + let hot = { + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + Actors::actor_hot(actor_id).expect("reference Manual differential hot state exists") + }; + let trigger_events = System::events() + .iter() + .filter(|record| { + matches!( + &record.event, + RuntimeEvent::Actors(Event::TriggerOccurrenceProcessed { + actor_id: id, + trigger_family: TriggerFamily::Manual, + .. + }) if *id == actor_id + ) + }) + .count(); + let manual_events = System::events() + .iter() + .filter(|record| { + matches!( + &record.event, + RuntimeEvent::Actors(Event::ManualTriggerSet { actor_id: id }) if *id == actor_id + ) + }) + .count(); + ( + custody_before.saturating_sub(native_balance(&sovereign)), + fee_collections(), + hot.pending_signal, + hot.cycle_state, + hot.trigger_runtime_state, + trigger_events, + manual_events, + ) + }) + }; + + let baseline = execute(); + emit_baseline_oracle("manual_success", &baseline); +} + +#[test] +fn control_at_time_trigger_boundary_matches_immutable_oracle_logical_fee_and_event_authority() { + let execute = || { + 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, + contract_steps_with_step(make_step(Task::StopCycle)), + ); + let sovereign = Actors::active_actor_view(actor_id) + .expect("AtTime differential Actor exists") + .sovereign_account; + let _ = ::AssetOps::mint( + &sovereign, + ::FeeNativeAssetId::get(), + u128::from(u64::MAX / 4), + ); + let custody_before = native_balance(&sovereign); + clear_fee_collections(); + System::reset_events(); + frame_system::Pallet::::set_block_number(2); + + let hot = { + let mut meter = WeightMeter::with_limit(Weight::MAX); + Actors::drain_overdue_wakeups_cursor(2, &mut meter); + Actors::actor_hot(actor_id).expect("reference AtTime differential hot state exists") + }; + let trigger_events = System::events() + .iter() + .filter(|record| { + matches!( + &record.event, + RuntimeEvent::Actors(Event::TriggerOccurrenceProcessed { + actor_id: id, + trigger_family: TriggerFamily::AtTime, + .. + }) if *id == actor_id + ) + }) + .count(); + ( + custody_before.saturating_sub(native_balance(&sovereign)), + fee_collections(), + hot.pending_signal, + hot.cycle_state, + hot.trigger_runtime_state, + trigger_events, + ) + }) + }; + + let baseline = execute(); + emit_baseline_oracle("at_time_success", &baseline); +} + +#[test] +fn control_address_event_boundary_matches_immutable_oracle_logical_fee_funding_and_event_authority() +{ + let execute = || { + 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, + contract_steps_with_step(make_step(Task::Transfer { + to: BOB, + asset: TestAsset::Native, + amount: AmountResolution::PercentageOfLastFunding(Perbill::one()), + })), + ); + let sovereign = Actors::active_actor_view(actor_id) + .expect("AddressEvent differential Actor exists") + .sovereign_account; + let _ = ::AssetOps::mint( + &sovereign, + ::FeeNativeAssetId::get(), + u128::from(u64::MAX / 4), + ); + let custody_before = native_balance(&sovereign); + clear_fee_collections(); + System::reset_events(); + + let hot = { + assert_ok!(Actors::notify_address_event( + actor_id, + TestAsset::Native, + 100, + &ALICE, + )); + Actors::actor_hot(actor_id).expect("reference AddressEvent differential hot state exists") + }; + let funding = ActorFunding::::get(actor_id) + .expect("AddressEvent differential funding exists") + .funding_accumulated + .get(&TestAsset::Native) + .copied(); + let funding_events = System::events() + .iter() + .filter(|record| { + matches!( + &record.event, + RuntimeEvent::Actors(Event::FundingAccumulated { + actor_id: id, + asset: TestAsset::Native, + added: 100, + accumulated: 100, + }) if *id == actor_id + ) + }) + .count(); + let trigger_events = System::events() + .iter() + .filter(|record| { + matches!( + &record.event, + RuntimeEvent::Actors(Event::TriggerOccurrenceProcessed { + actor_id: id, + trigger_family: TriggerFamily::AddressEvent, + .. + }) if *id == actor_id + ) + }) + .count(); + ( + custody_before.saturating_sub(native_balance(&sovereign)), + fee_collections(), + funding, + hot.pending_signal, + hot.cycle_state, + hot.trigger_runtime_state, + funding_events, + trigger_events, + ) + }) + }; + + let baseline = execute(); + emit_baseline_oracle("address_event_success", &baseline); +} + +#[test] +fn control_observation_change_boundary_matches_immutable_oracle_logical_fee_and_detector_authority() +{ + let execute = || { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let feed = 33; + let actor_id = create_user_with( + ALICE, + Mutability::Mutable, + observation_schedule(vec![feed]), + None, + contract_steps_with_step(make_step(Task::StopCycle)), + ); + let sovereign = Actors::active_actor_view(actor_id) + .expect("ObservationChange differential Actor exists") + .sovereign_account; + let _ = ::AssetOps::mint( + &sovereign, + ::FeeNativeAssetId::get(), + u128::from(u64::MAX / 4), + ); + let subscription = crate::ObservationSubscriptionSlot::::get(actor_id) + .expect("ObservationChange differential subscription exists"); + let custody_before = native_balance(&sovereign); + clear_fee_collections(); + System::reset_events(); + + let hot = { + assert_ok!(Actors::note_observation_changed(feed, 1)); + assert_eq!(Actors::do_fanout_dirty_observation_page(), Ok(false)); + Actors::actor_hot(actor_id) + .expect("reference ObservationChange differential hot state exists") + }; + let trigger_events = System::events() + .iter() + .filter(|record| { + matches!( + &record.event, + RuntimeEvent::Actors(Event::TriggerOccurrenceProcessed { + actor_id: id, + trigger_family: TriggerFamily::ObservationChange, + .. + }) if *id == actor_id + ) + }) + .count(); + ( + custody_before.saturating_sub(native_balance(&sovereign)), + fee_collections(), + hot.pending_signal, + hot.cycle_state, + hot.trigger_runtime_state, + crate::IndexedTriggerDetectionDisabled::::contains_key(actor_id), + crate::ObservationSubscriptionSlot::::get(actor_id), + subscription, + trigger_events, + ) + }) + }; + + let baseline = execute(); + emit_baseline_oracle("observation_change_success", &baseline); +} + +#[test] +fn control_observation_crossing_boundary_matches_immutable_oracle_fee_phase_and_detector_authority() +{ + let execute = || { + 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, + contract_steps_with_step(make_step(Task::StopCycle)), + ); + let sovereign = Actors::active_actor_view(actor_id) + .expect("ObservationCrossing differential Actor exists") + .sovereign_account; + let _ = ::AssetOps::mint( + &sovereign, + ::FeeNativeAssetId::get(), + u128::from(u64::MAX / 4), + ); + let custody_before = native_balance(&sovereign); + clear_fee_collections(); + System::reset_events(); + let transition = crate::ObservationTransition { + revision: 2, + previous: Some(50), + current: 150, + }; + + let hot = { + assert_ok!(Actors::note_observation_transition(7, transition)); + drain_crossing_work(); + Actors::actor_hot(actor_id) + .expect("reference ObservationCrossing differential hot state exists") + }; + let trigger_events = System::events() + .iter() + .filter(|record| { + matches!( + &record.event, + RuntimeEvent::Actors(Event::TriggerOccurrenceProcessed { + actor_id: id, + trigger_family: TriggerFamily::ObservationCrossing, + .. + }) if *id == actor_id + ) + }) + .count(); + ( + custody_before.saturating_sub(native_balance(&sovereign)), + fee_collections(), + hot.pending_signal, + hot.cycle_state, + hot.trigger_runtime_state, + crate::IndexedTriggerDetectionDisabled::::contains_key(actor_id), + crate::CrossingMemberships::::get(actor_id) + .map(|membership| membership.key.threshold), + trigger_events, + ) + }) + }; + + let baseline = execute(); + emit_baseline_oracle("observation_crossing_success", &baseline); +} + +#[test] +fn control_cadenced_trigger_boundary_matches_immutable_oracle_logical_fee_and_event_authority() { + let execute = || { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = create_user_with( + ALICE, + Mutability::Mutable, + timer_schedule(5), + None, + contract_steps_with_step(make_step(Task::StopCycle)), + ); + let sovereign = Actors::active_actor_view(actor_id) + .expect("Cadenced differential Actor exists") + .sovereign_account; + let _ = ::AssetOps::mint( + &sovereign, + ::FeeNativeAssetId::get(), + u128::from(u64::MAX / 4), + ); + let custody_before = native_balance(&sovereign); + clear_fee_collections(); + System::reset_events(); + frame_system::Pallet::::set_block_number(6); + + let hot = { + let mut meter = WeightMeter::with_limit(Weight::MAX); + Actors::drain_overdue_wakeups_cursor(6, &mut meter); + Actors::actor_hot(actor_id).expect("reference Cadenced differential hot state exists") + }; + let trigger_events = System::events() + .iter() + .filter(|record| { + matches!( + &record.event, + RuntimeEvent::Actors(Event::TriggerOccurrenceProcessed { + actor_id: id, + trigger_family: TriggerFamily::Cadenced, + .. + }) if *id == actor_id + ) + }) + .count(); + ( + custody_before.saturating_sub(native_balance(&sovereign)), + fee_collections(), + hot.pending_signal, + hot.cycle_state, + hot.trigger_runtime_state, + hot.trigger_wakeup_pointer.is_some(), + trigger_events, + ) + }) + }; + + let baseline = execute(); + emit_baseline_oracle("cadenced_success", &baseline); +} + +#[test] +fn control_manual_collection_failure_matches_immutable_oracle_retained_logical_authority() { + let execute = || { + 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(make_step(Task::StopCycle)), + ); + let sovereign = Actors::active_actor_view(actor_id) + .expect("Manual failure differential Actor exists") + .sovereign_account; + let _ = ::AssetOps::mint( + &sovereign, + ::FeeNativeAssetId::get(), + u128::from(u64::MAX / 4), + ); + let custody_before = native_balance(&sovereign); + clear_fee_collections(); + System::reset_events(); + set_fail_fee_sink_transfer(true); + { + assert!(Actors::manual_trigger(RuntimeOrigin::signed(ALICE), actor_id).is_err()); + } + set_fail_fee_sink_transfer(false); + let hot = + { Actors::actor_hot(actor_id).expect("reference Manual failure hot state remains") }; + ( + custody_before.saturating_sub(native_balance(&sovereign)), + fee_collections(), + hot.pending_signal, + hot.cycle_state, + hot.trigger_runtime_state, + System::events() + .iter() + .filter(|record| { + matches!( + &record.event, + RuntimeEvent::Actors(Event::TriggerOccurrenceProcessed { + actor_id: id, + trigger_family: TriggerFamily::Manual, + .. + }) if *id == actor_id + ) + }) + .count(), + ) + }) + }; + + let baseline = execute(); + emit_baseline_oracle("manual_collection_failure", &baseline); +} + +#[test] +fn control_at_time_collection_failure_matches_immutable_oracle_retained_due_authority() { + let execute = || { + 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, + contract_steps_with_step(make_step(Task::StopCycle)), + ); + let sovereign = Actors::active_actor_view(actor_id) + .expect("AtTime failure differential Actor exists") + .sovereign_account; + let _ = ::AssetOps::mint( + &sovereign, + ::FeeNativeAssetId::get(), + u128::from(u64::MAX / 4), + ); + let custody_before = native_balance(&sovereign); + clear_fee_collections(); + System::reset_events(); + 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); + let hot = + { Actors::actor_hot(actor_id).expect("reference AtTime failure hot state remains") }; + ( + custody_before.saturating_sub(native_balance(&sovereign)), + fee_collections(), + hot.pending_signal, + hot.cycle_state, + hot.trigger_runtime_state, + hot.trigger_wakeup_pointer.map(|pointer| pointer.tick), + Actors::wakeup_cursor_peek_key(WakeupClock::Tick), + System::events() + .iter() + .filter(|record| { + matches!( + &record.event, + RuntimeEvent::Actors(Event::TriggerOccurrenceProcessed { + actor_id: id, + trigger_family: TriggerFamily::AtTime, + .. + }) if *id == actor_id + ) + }) + .count(), + ) + }) + }; + + let baseline = execute(); + emit_baseline_oracle("at_time_collection_failure", &baseline); +} + +#[test] +fn control_address_event_collection_failure_matches_immutable_oracle_independent_funding_authority() +{ + let execute = || { + 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, + contract_steps_with_step(make_step(Task::Transfer { + to: BOB, + asset: TestAsset::Native, + amount: AmountResolution::PercentageOfLastFunding(Perbill::one()), + })), + ); + let sovereign = Actors::active_actor_view(actor_id) + .expect("AddressEvent failure differential Actor exists") + .sovereign_account; + let _ = ::AssetOps::mint( + &sovereign, + ::FeeNativeAssetId::get(), + u128::from(u64::MAX / 4), + ); + let custody_before = native_balance(&sovereign); + clear_fee_collections(); + System::reset_events(); + set_fail_fee_sink_transfer(true); + { + assert_ok!(Actors::notify_address_event( + actor_id, + TestAsset::Native, + 100, + &ALICE, + )); + } + set_fail_fee_sink_transfer(false); + let hot = + { Actors::actor_hot(actor_id).expect("reference AddressEvent failure hot state remains") }; + let funding = ActorFunding::::get(actor_id) + .expect("AddressEvent failure funding remains") + .funding_accumulated + .get(&TestAsset::Native) + .copied(); + let funding_events = System::events() + .iter() + .filter(|record| { + matches!( + &record.event, + RuntimeEvent::Actors(Event::FundingAccumulated { + actor_id: id, + asset: TestAsset::Native, + added: 100, + accumulated: 100, + }) if *id == actor_id + ) + }) + .count(); + let trigger_events = System::events() + .iter() + .filter(|record| { + matches!( + &record.event, + RuntimeEvent::Actors(Event::TriggerOccurrenceProcessed { + actor_id: id, + trigger_family: TriggerFamily::AddressEvent, + .. + }) if *id == actor_id + ) + }) + .count(); + ( + custody_before.saturating_sub(native_balance(&sovereign)), + fee_collections(), + funding, + hot.pending_signal, + hot.cycle_state, + hot.trigger_runtime_state, + funding_events, + trigger_events, + ) + }) + }; + + let baseline = execute(); + emit_baseline_oracle("address_event_collection_failure", &baseline); +} + +#[test] +fn control_observation_change_collection_failure_matches_immutable_oracle_detector_authority() { + let execute = || { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let feed = 35; + let actor_id = create_user_with( + ALICE, + Mutability::Mutable, + observation_schedule(vec![feed]), + None, + contract_steps_with_step(make_step(Task::StopCycle)), + ); + let sovereign = Actors::active_actor_view(actor_id) + .expect("ObservationChange failure differential Actor exists") + .sovereign_account; + let _ = ::AssetOps::mint( + &sovereign, + ::FeeNativeAssetId::get(), + u128::from(u64::MAX / 4), + ); + let subscription = crate::ObservationSubscriptionSlot::::get(actor_id) + .expect("ObservationChange failure differential subscription exists"); + let custody_before = native_balance(&sovereign); + clear_fee_collections(); + System::reset_events(); + set_fail_fee_sink_transfer(true); + { + assert_ok!(Actors::note_observation_changed(feed, 1)); + assert_eq!(Actors::do_fanout_dirty_observation_page(), Ok(false)); + } + set_fail_fee_sink_transfer(false); + let hot = { + Actors::actor_hot(actor_id).expect("reference ObservationChange failure hot state remains") + }; + ( + custody_before.saturating_sub(native_balance(&sovereign)), + fee_collections(), + hot.pending_signal, + hot.cycle_state, + hot.trigger_runtime_state, + crate::IndexedTriggerDetectionDisabled::::contains_key(actor_id), + crate::ObservationSubscriptionSlot::::get(actor_id), + subscription, + System::events() + .iter() + .filter(|record| { + matches!( + &record.event, + RuntimeEvent::Actors(Event::TriggerOccurrenceProcessed { + actor_id: id, + trigger_family: TriggerFamily::ObservationChange, + .. + }) if *id == actor_id + ) + }) + .count(), + ) + }) + }; + + let baseline = execute(); + emit_baseline_oracle("observation_change_collection_failure", &baseline); +} + +#[test] +fn control_observation_crossing_collection_failure_matches_immutable_oracle_phase_authority() { + let execute = || { + 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, + contract_steps_with_step(make_step(Task::StopCycle)), + ); + let sovereign = Actors::active_actor_view(actor_id) + .expect("ObservationCrossing failure differential Actor exists") + .sovereign_account; + let _ = ::AssetOps::mint( + &sovereign, + ::FeeNativeAssetId::get(), + u128::from(u64::MAX / 4), + ); + let custody_before = native_balance(&sovereign); + clear_fee_collections(); + System::reset_events(); + set_fail_fee_sink_transfer(true); + let transition = crate::ObservationTransition { + revision: 2, + previous: Some(50), + current: 150, + }; + { + assert_ok!(Actors::note_observation_transition(7, transition)); + drain_crossing_work(); + } + set_fail_fee_sink_transfer(false); + let hot = { + Actors::actor_hot(actor_id) + .expect("reference ObservationCrossing failure hot state remains") + }; + ( + custody_before.saturating_sub(native_balance(&sovereign)), + fee_collections(), + hot.pending_signal, + hot.cycle_state, + hot.trigger_runtime_state, + crate::IndexedTriggerDetectionDisabled::::contains_key(actor_id), + crate::CrossingMemberships::::get(actor_id) + .map(|membership| membership.key.threshold), + System::events() + .iter() + .filter(|record| { + matches!( + &record.event, + RuntimeEvent::Actors(Event::TriggerOccurrenceProcessed { + actor_id: id, + trigger_family: TriggerFamily::ObservationCrossing, + .. + }) if *id == actor_id + ) + }) + .count(), + ) + }) + }; + + let baseline = execute(); + emit_baseline_oracle("observation_crossing_collection_failure", &baseline); +} + +#[test] +fn control_canonical_frame_backing_round_trips_owner_seam_authority() { + new_test_ext().execute_with(|| { + let actor_id = frame_install_temporal_user_unsignaled(); + let (location, mut identity, hot, admission) = Actors::load_frame_control_authority(actor_id) + .expect("canonical frame backing resolves sole authority"); + assert_eq!(location, C1Location::Unsignaled); + assert!(!ActorIdentities::::contains_key(actor_id)); + frame_assert_single_owner(); + + identity.last_control_mutation_block = 2; + assert!(Actors::store_frame_control_authority( + actor_id, + location, + identity.clone(), + hot.clone(), + admission.clone(), + )); + let restored = Actors::load_frame_control_authority(actor_id) + .expect("canonical frame backing reloads updated authority"); + assert_eq!( + restored, + (location, identity.clone(), hot.clone(), admission.clone()) + ); + + let mut invalid_hot = hot; + invalid_hot.queue_ticket = Some(9); + assert!(!Actors::store_frame_control_authority( + actor_id, + location, + identity.clone(), + invalid_hot, + admission.clone(), + )); + assert_eq!( + Actors::load_frame_control_authority(actor_id), + Some((location, identity, restored.2, admission)) + ); + frame_assert_single_owner(); + }); +} + +fn frame_install_temporal_user_unsignaled() -> ActorId { + frame_system::Pallet::::set_block_number(1); + let actor_id = create_user_with( + ALICE, + Mutability::Mutable, + timer_schedule(5), + None, + contract_steps_with_step(make_step(Task::StopCycle)), + ); + Actors::trigger_wakeup_substrate_invalidate_inner(actor_id) + .expect("reference User Trigger wakeup invalidates before candidate projection"); + let (state, admission, loaded_step) = Actors::load_current_step_service_state(actor_id) + .expect("temporal User Actor remains coherent"); + let unsignaled = Actors::control_unsignaled_cell_from_scalar( + actor_id, + state.identity, + state.hot, + admission, + &loaded_step, + ) + .expect("temporal User authority projects Unsignaled"); + crate::ActorUnsignaledControlCells::::insert(actor_id, unsignaled); + crate::ActorControlLocators::::insert(actor_id, C1Location::Unsignaled); + actor_id +} + +#[test] +fn control_user_temporal_latch_charges_exact_trigger_fee_and_advances_failed_cadence() { + new_test_ext().execute_with(|| { + let actor_id = frame_install_temporal_user_unsignaled(); + let source_key = WakeupKey::Tick(9); + let cell = crate::ActorUnsignaledControlCells::::get(actor_id) + .expect("candidate temporal User cell exists"); + let cell = Actors::control_schedule_fresh_wakeup_reference(cell, source_key) + .expect("candidate User Trigger reference schedules"); + crate::ActorUnsignaledControlCells::::insert(actor_id, cell.clone()); + clear_fee_collections(); + System::reset_events(); + + set_fail_fee_sink_transfer(true); + assert_eq!( + Actors::control_latch_due_temporal_reference(source_key, 1, 9) + .expect("failed Cadenced collection advances detector deadline"), + (actor_id, C1Location::Unsignaled) + ); + set_fail_fee_sink_transfer(false); + clear_fee_collections(); + let next_key = WakeupKey::Tick(11); + let advanced = crate::ActorUnsignaledControlCells::::get(actor_id) + .expect("failed Cadenced collection retains sole Unsignaled primary"); + assert!(!advanced.hot.pending_signal); + assert_eq!( + advanced + .hot + .trigger_wakeup_pointer + .map(|pointer| pointer.tick), + Some(11) + ); + assert_eq!( + crate::ActorControlLocators::::get(actor_id), + Some(C1Location::Unsignaled) + ); + assert_eq!( + Actors::wakeup_cursor_peek_key(WakeupClock::Tick), + Some(next_key) + ); + assert_eq!(Actors::wakeup_cursor_peek_key(WakeupClock::Block), None); + assert!(!has_actor_event(|event| matches!( + event, + Event::TriggerOccurrenceProcessed { actor_id: id, .. } if *id == actor_id + ))); + + let (_, destination) = Actors::control_latch_due_temporal_reference(next_key, 1, 11) + .expect("funded User temporal occurrence latches"); + assert_eq!(fee_collections(), vec![cadenced_trigger_fee()]); + assert!(has_actor_event(|event| matches!( + event, + Event::TriggerOccurrenceProcessed { + actor_id: id, + trigger_family: TriggerFamily::Cadenced, + fee, + } if *id == actor_id && *fee == cadenced_trigger_fee() + ))); + assert_eq!( + crate::ActorControlLocators::::get(actor_id), + Some(destination) + ); + frame_assert_single_owner(); + }); +} + +fn frame_assert_single_owner() { + Actors::frame_control_entries().expect("canonical frame traversal preserves sole ownership"); +} + +#[test] +fn control_ready_user_opening_projects_exact_scalar_authority_without_event_drift() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = create_user_with( + ALICE, + Mutability::Mutable, + manual_schedule(), + None, + inert_contract_steps(), + ); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + let (state, admission, loaded_step) = + Actors::load_current_step_service_state(actor_id).expect("ready User Opening is coherent"); + let ticket = state.hot.queue_ticket.expect("Manual Opening is ready"); + let (_, queue_entry) = Actors::paged_head_entry().expect("canonical Ready ticket exists"); + let expected_identity = state.identity.clone(); + let expected_hot = state.hot.clone(); + let expected_admission = admission.clone(); + let events = System::events(); + let cell = Actors::control_opening_cell_from_scalar( + actor_id, + state.identity, + state.hot, + admission, + &queue_entry, + &loaded_step, + ) + .expect("ready Opening projects to one frame cell"); + let restored = Actors::project_control_cell(&cell, C1Location::Ready { ticket }) + .expect("ready frame cell restores complete scalar authority"); + assert_eq!(restored.0, expected_identity); + assert_eq!(restored.1, expected_hot); + assert_eq!(restored.2, expected_admission); + assert_eq!(System::events(), events, "pure projection emits no event"); + }); +} + +#[test] +fn control_complete_user_stop_cycle_projects_exact_scalar_authority_without_event_drift() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = create_user_with( + ALICE, + Mutability::Mutable, + manual_schedule(), + None, + inert_contract_steps(), + ); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + run_idle(Weight::MAX); + + let (state, admission, loaded_step) = Actors::load_current_step_service_state(actor_id) + .expect("completed StopCycle re-arms one coherent Manual Opening"); + assert_eq!(state.identity.cycle_nonce, 1); + assert_eq!(state.hot.cycle_state, CycleState::Idle); + assert!(!state.hot.pending_signal); + assert!(state.hot.queue_ticket.is_none()); + assert!(state.run_state.is_none()); + let expected_identity = state.identity.clone(); + let expected_hot = state.hot.clone(); + let expected_admission = admission.clone(); + let events = System::events(); + let cell = Actors::control_unsignaled_cell_from_scalar( + actor_id, + state.identity, + state.hot, + admission, + &loaded_step, + ) + .expect("complete StopCycle authority projects to one unsignaled frame cell"); + + crate::ActorUnsignaledControlCells::::insert(actor_id, cell.clone()); + crate::ActorControlLocators::::insert(actor_id, C1Location::Unsignaled); + + let restored = Actors::project_control_cell(&cell, C1Location::Unsignaled) + .expect("unsignaled frame cell restores complete scalar authority"); + assert_eq!(restored.0, expected_identity); + assert_eq!(restored.1, expected_hot); + assert_eq!(restored.2, expected_admission); + assert_eq!( + System::events(), + events, + "projection emits no semantic event" + ); + assert!(!ActorIdentities::::contains_key(actor_id)); + frame_assert_single_owner(); + assert_eq!( + crate::ActorControlLocators::::get(actor_id), + Some(C1Location::Unsignaled) + ); + frame_assert_single_owner(); + }); +} + +#[test] +fn control_direct_ready_stop_cycle_matches_immutable_oracle() { + let baseline = new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(0); + let actor_id = create_user_with( + ALICE, + Mutability::Mutable, + manual_schedule(), + None, + contract_steps_with_step(make_step(Task::StopCycle)), + ); + frame_system::Pallet::::set_block_number(1); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + let sovereign = Actors::active_actor_view(actor_id) + .expect("reference User exists") + .sovereign_account; + let reserve = u128::from(u64::MAX / 4); + let _ = ::AssetOps::mint( + &sovereign, + ::FeeNativeAssetId::get(), + reserve, + ); + let fee_sink = ::FeeSink::get(); + let native_asset = ::FeeNativeAssetId::get(); + System::reset_events(); + let sovereign_before = ::AssetOps::balance(&sovereign, native_asset); + let sink_before = ::AssetOps::balance(&fee_sink, native_asset); + run_idle(Weight::MAX); + let (state, admission, loaded_step) = Actors::load_current_step_service_state(actor_id) + .expect("reference StopCycle successor is coherent"); + let funding = ActorFunding::::get(actor_id).expect("reference funding survives"); + let hold = crate::ActorStateHolds::::get(actor_id).expect("reference User hold survives"); + ( + state.identity, + state.hot, + admission, + loaded_step, + funding, + System::events(), + sovereign_before.saturating_sub(::AssetOps::balance( + &sovereign, + native_asset, + )), + ::AssetOps::balance(&fee_sink, native_asset) + .saturating_sub(sink_before), + hold.owner, + ) + }); + + emit_baseline_oracle("direct_stop_cycle", &baseline); +} + +#[test] +fn control_direct_transfer_predicate_matrix_matches_immutable_oracle() { + let execute = |predicate_count: u32, predicates_match: bool| { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(0); + let predicate_assets = (0..predicate_count) + .map(|index| TestAsset::Local(10_000 + index)) + .collect::>(); + let precondition = (!predicate_assets.is_empty()).then(|| { + timed_all_conditions( + ObservationTiming::Opening, + predicate_assets + .iter() + .copied() + .map(|asset| Predicate::BalanceAbove { + asset, + threshold: if predicates_match { 50 } else { Balance::MAX }, + }) + .collect(), + ) + .expect("direct Transfer predicates fit") + }); + let step = StepOf:: { + precondition, + task: Task::Transfer { + to: BOB, + asset: TestAsset::Native, + amount: AmountResolution::Fixed(1), + }, + on_error: StepErrorPolicy::AbortCycle, + }; + let actor_id = create_user_with( + ALICE, + Mutability::Mutable, + manual_schedule(), + None, + contract_steps_with_step(step), + ); + let sovereign = Actors::active_actor_view(actor_id) + .expect("direct Transfer User exists") + .sovereign_account; + let reserve = u128::from(u64::MAX / 4); + let _ = ::AssetOps::mint( + &sovereign, + ::FeeNativeAssetId::get(), + reserve, + ); + for asset in predicate_assets { + set_asset_balance(&sovereign, asset, 100); + } + frame_system::Pallet::::set_block_number(1); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + let fee_sink = ::FeeSink::get(); + let native_asset = ::FeeNativeAssetId::get(); + System::reset_events(); + let sovereign_before = ::AssetOps::balance(&sovereign, native_asset); + let recipient_before = ::AssetOps::balance(&BOB, native_asset); + let sink_before = ::AssetOps::balance(&fee_sink, native_asset); + + { + run_idle(Weight::MAX); + let (state, admission, loaded_step) = Actors::load_current_step_service_state(actor_id) + .expect("reference Transfer successor is coherent"); + let funding = ActorFunding::::get(actor_id).expect("reference funding survives"); + let hold = + crate::ActorStateHolds::::get(actor_id).expect("reference User hold survives"); + ( + state.identity, + state.hot, + admission, + loaded_step, + funding, + System::events(), + sovereign_before.saturating_sub(::AssetOps::balance( + &sovereign, + native_asset, + )), + ::AssetOps::balance(&BOB, native_asset) + .saturating_sub(recipient_before), + ::AssetOps::balance(&fee_sink, native_asset) + .saturating_sub(sink_before), + hold.owner, + ) + } + }) + }; + + for (predicate_count, predicates_match) in [(0u32, true), (2, true), (4, true), (2, false)] { + let baseline = execute(predicate_count, predicates_match); + emit_baseline_oracle( + &format!("direct_transfer_p{predicate_count}_match_{predicates_match}"), + &baseline, + ); + } +} + +#[test] +fn control_direct_funding_unavailable_requeues_exact_next_block_matching_immutable_oracle() { + let step = StepOf:: { + precondition: None, + task: Task::Transfer { + to: BOB, + asset: TestAsset::Native, + amount: AmountResolution::Fixed(Balance::MAX), + }, + on_error: StepErrorPolicy::RetryLater { max_attempts: 3 }, + }; + let execute = || { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = { + let actor_id = create_user_with( + ALICE, + Mutability::Mutable, + manual_schedule(), + None, + contract_steps_with_step(step.clone()), + ); + let sovereign = Actors::active_actor_view(actor_id) + .expect("reference FundingUnavailable User exists") + .sovereign_account; + let reserve = u128::from(u64::MAX / 4); + let _ = ::AssetOps::mint( + &sovereign, + ::FeeNativeAssetId::get(), + reserve, + ); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + actor_id + }; + let sovereign = Actors::sovereign_account_id(&ALICE, 0); + let fee_sink = ::FeeSink::get(); + let native_asset = ::FeeNativeAssetId::get(); + System::reset_events(); + let sovereign_before = ::AssetOps::balance(&sovereign, native_asset); + let sink_before = ::AssetOps::balance(&fee_sink, native_asset); + + { + run_idle(Weight::MAX); + let (state, admission, loaded_step) = Actors::load_current_step_service_state(actor_id) + .expect("reference FundingUnavailable suspension is coherent"); + let run = state + .run_state + .expect("reference FundingUnavailable run persists"); + let funding = ActorFunding::::get(actor_id).expect("reference funding survives"); + let hold = + crate::ActorStateHolds::::get(actor_id).expect("reference User hold survives"); + ( + state.identity, + state.hot, + admission, + loaded_step, + run.encode(), + funding, + System::events(), + sovereign_before.saturating_sub(::AssetOps::balance( + &sovereign, + native_asset, + )), + ::AssetOps::balance(&fee_sink, native_asset) + .saturating_sub(sink_before), + hold.owner, + ) + } + }) + }; + + let baseline = execute(); + emit_baseline_oracle("direct_funding_unavailable", &baseline); +} + +#[test] +fn control_direct_temporary_failure_requeues_with_atomic_effect_rollback_matching_immutable_oracle() +{ + let step = temporary_retry_swap_plan()[0].clone(); + let execute = || { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + setup_temporary_retry_pool(); + set_temporary_dex_failure(true); + let actor_id = { + let actor_id = create_user_with( + ALICE, + Mutability::Mutable, + manual_schedule(), + None, + contract_steps_with_step(step.clone()), + ); + let sovereign = Actors::active_actor_view(actor_id) + .expect("reference Temporary User exists") + .sovereign_account; + let reserve = u128::from(u64::MAX / 4); + let _ = ::AssetOps::mint( + &sovereign, + ::FeeNativeAssetId::get(), + reserve, + ); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + actor_id + }; + let sovereign = Actors::sovereign_account_id(&ALICE, 0); + let fee_sink = ::FeeSink::get(); + let native_asset = ::FeeNativeAssetId::get(); + let output_asset = TestAsset::Local(77); + System::reset_events(); + + { + run_idle(Weight::MAX); + let (state, admission, loaded_step) = Actors::load_current_step_service_state(actor_id) + .expect("reference Temporary suspension is coherent"); + let run = state.run_state.expect("reference Temporary run persists"); + let funding = ActorFunding::::get(actor_id).expect("reference funding survives"); + let hold = + crate::ActorStateHolds::::get(actor_id).expect("reference User hold survives"); + ( + state.identity, + state.hot, + admission, + loaded_step, + run.encode(), + funding, + System::events(), + [ + ::AssetOps::balance(&sovereign, native_asset), + ::AssetOps::balance(&sovereign, output_asset), + ::AssetOps::balance(&u64::MAX, native_asset), + ::AssetOps::balance(&u64::MAX, output_asset), + ::AssetOps::balance(&fee_sink, native_asset), + ], + hold.owner, + ) + } + }) + }; + + let baseline = execute(); + emit_baseline_oracle("direct_temporary_failure", &baseline); +} + +#[test] +fn control_suspended_temporary_continuation_matches_immutable_oracle_success_and_backoff() { + let step = temporary_retry_swap_plan()[0].clone(); + let execute = |retry_fails: bool| { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + setup_temporary_retry_pool(); + set_temporary_dex_failure(true); + let actor_id = { + let actor_id = create_user_with( + ALICE, + Mutability::Mutable, + manual_schedule(), + None, + contract_steps_with_step(step.clone()), + ); + let sovereign = Actors::active_actor_view(actor_id) + .expect("reference retry User exists") + .sovereign_account; + let _ = ::AssetOps::mint( + &sovereign, + ::FeeNativeAssetId::get(), + u128::from(u64::MAX / 4), + ); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + actor_id + }; + { + run_idle(Weight::MAX); + } + + set_temporary_dex_failure(retry_fails); + frame_system::Pallet::::set_block_number(2); + System::reset_events(); + { + run_idle(Weight::MAX); + } + + let sovereign = Actors::sovereign_account_id(&ALICE, 0); + let fee_sink = ::FeeSink::get(); + let native_asset = ::FeeNativeAssetId::get(); + let output_asset = TestAsset::Local(77); + { + let (state, admission, loaded_step) = Actors::load_current_step_service_state(actor_id) + .expect("reference retry successor is coherent"); + let funding = ActorFunding::::get(actor_id).expect("reference funding survives"); + let hold = + crate::ActorStateHolds::::get(actor_id).expect("reference User hold survives"); + ( + state.identity, + state.hot, + admission, + loaded_step, + state.run_state.map(|run| run.encode()), + funding, + System::events(), + [ + ::AssetOps::balance(&sovereign, native_asset), + ::AssetOps::balance(&sovereign, output_asset), + ::AssetOps::balance(&u64::MAX, native_asset), + ::AssetOps::balance(&u64::MAX, output_asset), + ::AssetOps::balance(&fee_sink, native_asset), + ], + hold.owner, + ) + } + }) + }; + + for retry_fails in [false, true] { + let baseline = execute(retry_fails); + emit_baseline_oracle(&format!("temporary_retry_fails_{retry_fails}"), &baseline); + } +} + +#[test] +fn control_suspended_funding_continuation_matches_immutable_oracle_success_and_backoff() { + let transfer_amount = u128::from(u64::MAX); + let step = StepOf:: { + precondition: None, + task: Task::Transfer { + to: BOB, + asset: TestAsset::Native, + amount: AmountResolution::Fixed(transfer_amount), + }, + on_error: StepErrorPolicy::RetryLater { max_attempts: 3 }, + }; + let execute = |retry_fails: bool| { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = { + let actor_id = create_user_with( + ALICE, + Mutability::Mutable, + manual_schedule(), + None, + contract_steps_with_step(step.clone()), + ); + let sovereign = Actors::active_actor_view(actor_id) + .expect("reference Funding retry User exists") + .sovereign_account; + let _ = ::AssetOps::mint( + &sovereign, + ::FeeNativeAssetId::get(), + u128::from(u64::MAX / 4), + ); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + actor_id + }; + { + run_idle(Weight::MAX); + } + + let sovereign = Actors::sovereign_account_id(&ALICE, 0); + let native_asset = ::FeeNativeAssetId::get(); + if !retry_fails { + let _ = ::AssetOps::mint(&sovereign, native_asset, transfer_amount); + } + frame_system::Pallet::::set_block_number(2); + System::reset_events(); + { + run_idle(Weight::MAX); + } + + let fee_sink = ::FeeSink::get(); + { + let (state, admission, loaded_step) = Actors::load_current_step_service_state(actor_id) + .expect("reference Funding retry successor is coherent"); + let funding = ActorFunding::::get(actor_id).expect("reference funding survives"); + let hold = + crate::ActorStateHolds::::get(actor_id).expect("reference User hold survives"); + ( + state.identity, + state.hot, + admission, + loaded_step, + state.run_state.map(|run| run.encode()), + funding, + System::events(), + [ + ::AssetOps::balance(&sovereign, native_asset), + ::AssetOps::balance(&BOB, native_asset), + ::AssetOps::balance(&fee_sink, native_asset), + ], + hold.owner, + ) + } + }) + }; + + for retry_fails in [false, true] { + let baseline = execute(retry_fails); + emit_baseline_oracle(&format!("funding_retry_fails_{retry_fails}"), &baseline); + } +} + +#[test] +fn ordinary_fifo_cutoff_preserves_rejected_head_and_untouched_suffix() { + new_test_ext().execute_with(|| { + let actor_ids = frame_install_direct_stop_cycle_ready(3); + System::reset_events(); + let head_before_first = Actors::queue_head(); + frame_system::Pallet::::set_block_number(1); + Actors::execute_cycle_to_cutoff(Weight::MAX, (3).min(head_before_first.saturating_add(1))); + assert_eq!(Actors::queue_head().saturating_sub(head_before_first), 1); + + assert_eq!(crate::ActorReadyHead::::get(), 1); + assert_eq!(crate::ActorReadyOccupancy::::get(), 2); + assert!(crate::ActorUnsignaledControlCells::::contains_key( + actor_ids[0] + )); + assert!(matches!( + crate::ActorControlLocators::::get(actor_ids[1]), + Some(C1Location::Ready { ticket: 1 }) + )); + assert!(matches!( + crate::ActorControlLocators::::get(actor_ids[2]), + Some(C1Location::Ready { ticket: 2 }) + )); + frame_assert_single_owner(); + + let events_before_rejection = System::events(); + let root_before_rejection = + polkadot_sdk::sp_io::storage::root(polkadot_sdk::sp_runtime::StateVersion::V1); + let head_before_rejected = Actors::queue_head(); + frame_system::Pallet::::set_block_number(1); + Actors::execute_cycle_to_cutoff( + Weight::zero(), + (3).min(head_before_rejected.saturating_add(32)), + ); + assert_eq!(Actors::queue_head().saturating_sub(head_before_rejected), 0); + + assert_eq!(System::events(), events_before_rejection); + assert_eq!( + polkadot_sdk::sp_io::storage::root(polkadot_sdk::sp_runtime::StateVersion::V1), + root_before_rejection, + ); + assert_eq!(crate::ActorReadyHead::::get(), 1); + assert_eq!(crate::ActorReadyOccupancy::::get(), 2); + frame_assert_single_owner(); + + let head_before_resumed = Actors::queue_head(); + frame_system::Pallet::::set_block_number(1); + Actors::execute_cycle_to_cutoff(Weight::MAX, (3).min(head_before_resumed.saturating_add(32))); + assert_eq!(Actors::queue_head().saturating_sub(head_before_resumed), 2); + + assert_eq!(crate::ActorReadyHead::::get(), 3); + assert_eq!(crate::ActorReadyOccupancy::::get(), 0); + for actor_id in actor_ids { + let cell = crate::ActorUnsignaledControlCells::::get(actor_id) + .expect("ordinary FIFO successor exists"); + assert_eq!(cell.identity.cycle_nonce, 1); + assert!(!cell.hot.pending_signal); + } + frame_assert_single_owner(); + }); +} + +#[test] +fn control_empty_partial_ready_chunk_retains_ticket_addressability() { + new_test_ext().execute_with(|| { + let actor_ids = frame_install_direct_stop_cycle_ready(3); + Actors::execute_cycle_to_cutoff(Weight::MAX, 3); + assert_eq!(crate::ActorReadyHead::::get(), 3); + assert_eq!(crate::ActorReadyTail::::get(), 3); + assert_eq!(crate::ActorReadyOccupancy::::get(), 0); + assert!(!crate::ActorReadyFrameChunks::::contains_key(0)); + for actor_id in actor_ids { + let cell = crate::ActorUnsignaledControlCells::::get(actor_id) + .expect("completed Actor retains its Unsignaled primary"); + assert_eq!(cell.identity.cycle_nonce, 1); + assert_eq!(cell.hot.cycle_state, CycleState::Idle); + assert!(!cell.hot.pending_signal); + assert!(!ActorRunStateStore::::contains_key(actor_id)); + } + + let actor_id = frame_install_direct_stop_cycle_ready(1)[0]; + assert_eq!(crate::ActorReadyHead::::get(), 3); + assert_eq!(crate::ActorReadyTail::::get(), 4); + assert_eq!(crate::ActorReadyOccupancy::::get(), 1); + let appended = crate::ActorReadyFrameChunks::::get(0) + .expect("new Ready cell appends after retained ticket slots"); + assert_eq!(appended.len(), 32); + assert!(appended[..3].iter().all(Option::is_none)); + assert_eq!( + appended[3].as_ref().map(|cell| cell.actor_id), + Some(actor_id) + ); + assert_eq!( + crate::ActorControlLocators::::get(actor_id), + Some(C1Location::Ready { ticket: 3 }) + ); + frame_assert_single_owner(); + }); +} + +#[test] +fn control_ready_removal_preserves_ticket_addresses_and_normalizes_head_boundedly() { + new_test_ext().execute_with(|| { + let actor_ids = frame_install_direct_stop_cycle_ready(35); + for actor_id in actor_ids.iter().take(32).copied() { + Actors::control_remove_ready_primary(actor_id) + .expect("head-page Ready removal commits atomically"); + } + Actors::control_remove_ready_primary(actor_ids[34]) + .expect("tail Ready removal commits atomically"); + + assert_eq!(crate::ActorReadyHead::::get(), 0); + assert_eq!(crate::ActorReadyTail::::get(), 35); + assert_eq!(crate::ActorReadyOccupancy::::get(), 2); + assert!(crate::ActorReadyFrameChunks::::contains_key(0)); + assert_eq!( + crate::ActorControlLocators::::get(actor_ids[32]), + Some(C1Location::Ready { ticket: 32 }) + ); + assert_eq!( + crate::ActorControlLocators::::get(actor_ids[33]), + Some(C1Location::Ready { ticket: 33 }) + ); + + assert_eq!(Actors::control_normalize_ready_head(35, 16), Ok((16, None))); + assert_eq!(crate::ActorReadyHead::::get(), 16); + assert!(crate::ActorReadyFrameChunks::::contains_key(0)); + assert_eq!( + Actors::control_normalize_ready_head(35, 16), + Ok((16, Some(32))) + ); + assert_eq!(crate::ActorReadyHead::::get(), 32); + assert!(!crate::ActorReadyFrameChunks::::contains_key(0)); + assert_eq!(crate::ActorReadyTail::::get(), 35); + assert_eq!(crate::ActorReadyOccupancy::::get(), 2); + frame_assert_single_owner(); + }); +} + +#[test] +fn ordinary_fifo_corrupt_head_keeps_committed_prefix_and_untouched_suffix() { + new_test_ext().execute_with(|| { + let actor_ids = frame_install_direct_stop_cycle_ready(3); + let failed_head = crate::ActorContractHeads::::take(actor_ids[1]) + .expect("failed-head Contract exists before injection"); + let untouched = [ + Actors::actor_control_cell(actor_ids[1]).expect("corrupt head retains physical primary"), + Actors::actor_control_cell(actor_ids[2]).expect("unreached suffix primary"), + ]; + System::reset_events(); + let head_before_report = Actors::queue_head(); + frame_system::Pallet::::set_block_number(1); + Actors::execute_cycle_to_cutoff(Weight::MAX, (3).min(head_before_report.saturating_add(32))); + assert_eq!(Actors::queue_head().saturating_sub(head_before_report), 1); + + assert_eq!(crate::ActorReadyHead::::get(), 1); + assert_eq!(crate::ActorReadyOccupancy::::get(), 2); + assert!(crate::ActorUnsignaledControlCells::::contains_key( + actor_ids[0] + )); + assert!(!crate::ActorUnsignaledControlCells::::contains_key( + actor_ids[1] + )); + assert!(!crate::ActorUnsignaledControlCells::::contains_key( + actor_ids[2] + )); + let started = System::events() + .into_iter() + .filter_map(|record| match record.event { + RuntimeEvent::Actors(Event::CycleStarted { actor_id, .. }) => Some(actor_id), + _ => None, + }) + .collect::>(); + assert_eq!(started, vec![actor_ids[0]]); + assert_eq!( + Actors::actor_control_cell(actor_ids[1]), + Some(untouched[0].clone()), + ); + assert_eq!( + Actors::actor_control_cell(actor_ids[2]), + Some(untouched[1].clone()), + ); + frame_assert_single_owner(); + + crate::ActorContractHeads::::insert(actor_ids[1], failed_head); + let head_before_resumed = Actors::queue_head(); + frame_system::Pallet::::set_block_number(1); + Actors::execute_cycle_to_cutoff(Weight::MAX, (3).min(head_before_resumed.saturating_add(32))); + assert_eq!(Actors::queue_head().saturating_sub(head_before_resumed), 2); + + assert_eq!(crate::ActorReadyHead::::get(), 3); + assert_eq!(crate::ActorReadyOccupancy::::get(), 0); + frame_assert_single_owner(); + }); +} + +#[test] +fn control_waiting_round_trip_preserves_primary_and_coexisting_wakeup_authority() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = create_user_with( + ALICE, + Mutability::Mutable, + timer_schedule(5), + None, + contract_steps_with_step(make_step(Task::StopCycle)), + ); + let (state, admission, loaded_step) = + Actors::load_current_step_service_state(actor_id).expect("temporal User state is coherent"); + let trigger_pointer = state + .hot + .trigger_wakeup_pointer + .expect("Cadenced User has Trigger wakeup authority"); + assert!(state.hot.wakeup_pointer.is_none()); + assert!(!state.hot.pending_signal); + let trigger_location = C1Location::Waiting { + key: WakeupKey::Tick(trigger_pointer.tick), + page: trigger_pointer.page_id, + slot: u8::try_from(trigger_pointer.slot).expect("Trigger slot fits C32"), + }; + let trigger_cell = Actors::control_cell_from_parts( + actor_id, + state.identity.clone(), + state.hot.clone(), + admission.clone(), + &loaded_step, + None, + ) + .expect("trigger-only Waiting cell projects"); + Actors::remove_primary_control_cell_inner(actor_id) + .expect("diagnostic projection removes the lifecycle-installed source primary"); + let mut trigger_chunk = BoundedVec::try_from(vec![None; trigger_pointer.slot as usize + 1]) + .expect("trigger Waiting slot fits C32"); + trigger_chunk[trigger_pointer.slot as usize] = + Some(crate::ActorWaitingEntry::Primary(trigger_cell.clone())); + crate::ActorWaitingFrameChunks::::insert( + ( + WakeupKey::Tick(trigger_pointer.tick), + trigger_pointer.page_id, + ), + crate::ActorWaitingPageOf:: { + entries: trigger_chunk, + live_entries: 1, + scan_slot: 0, + previous_page: None, + next_page: None, + }, + ); + crate::ActorControlLocators::::insert(actor_id, trigger_location); + let restored_trigger = Actors::project_control_cell(&trigger_cell, trigger_location) + .expect("trigger-only Waiting authority restores"); + assert_eq!(restored_trigger.0, state.identity); + assert_eq!(restored_trigger.1, state.hot); + assert_eq!(restored_trigger.2, admission); + assert!( + Actors::project_control_cell(&trigger_cell, C1Location::Unsignaled).is_none(), + "temporal membership cannot masquerade as Unsignaled" + ); + frame_assert_single_owner(); + + crate::ActorWaitingFrameChunks::::remove(( + WakeupKey::Tick(trigger_pointer.tick), + trigger_pointer.page_id, + )); + let process_pointer = WakeupPointer { + block: WakeupKey::Block(7), + page_id: trigger_pointer.page_id.saturating_add(1), + slot: 2, + }; + let mut coexisting_cell = trigger_cell; + coexisting_cell.hot.cycle_state = CycleState::Suspended; + coexisting_cell.hot.wakeup_pointer = Some(process_pointer); + coexisting_cell.eligible_at = Some(7); + let process_location = C1Location::Waiting { + key: process_pointer.block, + page: process_pointer.page_id, + slot: u8::try_from(process_pointer.slot).expect("process slot fits C32"), + }; + let mut process_chunk = BoundedVec::try_from(vec![None; process_pointer.slot as usize + 1]) + .expect("process Waiting slot fits C32"); + process_chunk[process_pointer.slot as usize] = + Some(crate::ActorWaitingEntry::Primary(coexisting_cell.clone())); + crate::ActorWaitingFrameChunks::::insert( + (process_pointer.block, process_pointer.page_id), + crate::ActorWaitingPageOf:: { + entries: process_chunk, + live_entries: 1, + scan_slot: 0, + previous_page: None, + next_page: None, + }, + ); + crate::ActorControlLocators::::insert(actor_id, process_location); + let restored_coexisting = Actors::project_control_cell(&coexisting_cell, process_location) + .expect("coexisting process/Trigger authority restores"); + let mut expected_hot = state.hot; + expected_hot.cycle_state = CycleState::Suspended; + expected_hot.wakeup_pointer = Some(process_pointer); + assert_eq!(restored_coexisting.0, state.identity); + assert_eq!(restored_coexisting.1, expected_hot); + assert_eq!(restored_coexisting.2, admission); + assert_eq!( + restored_coexisting.1.trigger_wakeup_pointer, + Some(trigger_pointer) + ); + assert!( + Actors::project_control_cell(&coexisting_cell, trigger_location).is_none(), + "process wakeup is the primary physical placement while Trigger membership coexists" + ); + frame_assert_single_owner(); + }); +} + +#[test] +fn control_temporal_transition_preserves_n_plus_one_cutoff_and_pointer_cleanup() { + new_test_ext().execute_with(|| { + let actor_id = frame_install_temporal_system_unsignaled(1)[0]; + + let trigger_location = Actors::control_stage_unsignaled_temporal(actor_id, 10) + .expect("Unsignaled temporal cell stages atomically"); + assert_eq!( + trigger_location, + C1Location::Waiting { + key: WakeupKey::Tick(10), + page: 0, + slot: 0, + } + ); + assert!(!crate::ActorUnsignaledControlCells::::contains_key( + actor_id + )); + assert_eq!( + crate::ActorWaitingHeads::::get(WakeupKey::Tick(10)), + 0 + ); + assert_eq!( + crate::ActorWaitingTails::::get(WakeupKey::Tick(10)), + 1 + ); + assert_eq!( + crate::ActorWaitingOccupancies::::get(WakeupKey::Tick(10)), + 1 + ); + assert_eq!(crate::WakeupCursorLen::::get(WakeupClock::Tick), 1); + assert_eq!( + Actors::wakeup_cursor_peek_key(WakeupClock::Tick), + Some(WakeupKey::Tick(10)) + ); + assert_eq!( + crate::ActorWaitingCursorIndices::::get(WakeupKey::Tick(10)), + Some(0) + ); + assert_eq!( + Actors::control_latch_temporal_waiting_page(10, 0, 1, 9), + Err(crate::scheduler::ActorControlTransitionError::Invariant), + "future Trigger key cannot bypass the shared deadline heap" + ); + + let latched = Actors::control_latch_temporal_waiting_page(10, 0, 1, 10) + .expect("temporal due page latches into N+1 service waiting"); + assert_eq!(latched, vec![actor_id]); + assert!(!crate::ActorWaitingFrameChunks::::contains_key(( + WakeupKey::Tick(10), + 0 + ))); + assert!(!crate::ActorWaitingTails::::contains_key( + WakeupKey::Tick(10) + )); + assert_eq!(crate::WakeupCursorLen::::get(WakeupClock::Tick), 0); + assert_eq!(crate::WakeupCursorLen::::get(WakeupClock::Block), 1); + assert_eq!( + Actors::wakeup_cursor_peek_key(WakeupClock::Block), + Some(WakeupKey::Block(2)) + ); + let service_location = C1Location::Waiting { + key: WakeupKey::Block(2), + page: 0, + slot: 0, + }; + assert_eq!( + crate::ActorControlLocators::::get(actor_id), + Some(service_location) + ); + let service_cell = crate::ActorWaitingFrameChunks::::get((WakeupKey::Block(2), 0)) + .and_then(|chunk| chunk.entries.first().cloned().flatten()) + .and_then(crate::ActorWaitingEntry::into_primary) + .expect("N+1 service cell exists"); + assert!(service_cell.hot.pending_signal); + assert!(service_cell.hot.trigger_wakeup_pointer.is_none()); + assert_eq!(service_cell.eligible_at, Some(2)); + assert_eq!( + service_cell.hot.wakeup_pointer, + Some(WakeupPointer { + block: WakeupKey::Block(2), + page_id: 0, + slot: 0, + }) + ); + assert!(Actors::project_control_cell(&service_cell, service_location).is_some()); + assert_eq!(Actors::wakeup_cursor_pop_min(), None); + assert!(!Actors::wakeup_cursor_remove(2)); + assert_eq!( + Actors::wakeup_cursor_peek_key(WakeupClock::Block), + Some(WakeupKey::Block(2)), + "generic cursor controls cannot detach candidate-owned Waiting authority" + ); + + assert_eq!( + Actors::control_promote_due_waiting_page(2, 0, 1), + Err(crate::scheduler::ActorControlTransitionError::Invariant) + ); + assert_eq!( + crate::ActorControlLocators::::get(actor_id), + Some(service_location), + "premature promotion rolls back source authority" + ); + let cutoff_before_due = crate::ActorReadyTail::::get(); + assert_eq!(cutoff_before_due, 0); + let promoted = Actors::control_promote_due_waiting_page(2, 0, 2) + .expect("N+1 waiting page promotes in physical order"); + assert_eq!(promoted, vec![(actor_id, 0)]); + assert!(!crate::ActorWaitingTails::::contains_key( + WakeupKey::Block(2) + )); + assert_eq!(crate::WakeupCursorLen::::get(WakeupClock::Block), 0); + assert_eq!(crate::ActorReadyHead::::get(), 0); + assert_eq!(crate::ActorReadyTail::::get(), 1); + assert_eq!(crate::ActorReadyOccupancy::::get(), 1); + let ready = crate::ActorReadyFrameChunks::::get(0) + .and_then(|chunk| chunk.first().cloned().flatten()) + .expect("promoted Ready cell exists"); + assert!(ready.hot.pending_signal); + assert!(ready.hot.wakeup_pointer.is_none()); + assert!(ready.hot.trigger_wakeup_pointer.is_none()); + assert_eq!(ready.eligible_at, Some(2)); + assert!(Actors::project_control_cell(&ready, C1Location::Ready { ticket: 0 }).is_some()); + + let head_before_isolated = Actors::queue_head(); + frame_system::Pallet::::set_block_number(2); + Actors::execute_cycle_to_cutoff( + Weight::MAX, + (cutoff_before_due).min(head_before_isolated.saturating_add(1)), + ); + assert_eq!(Actors::queue_head().saturating_sub(head_before_isolated), 0); + + assert_eq!(crate::ActorReadyHead::::get(), 0); + assert_eq!(crate::ActorReadyTail::::get(), 1); + frame_assert_single_owner(); + }); +} + +#[test] +fn control_due_page_moves_are_atomic_and_preserve_causal_ticket_order() { + new_test_ext().execute_with(|| { + let actor_ids = frame_install_temporal_system_unsignaled(3); + for actor_id in actor_ids.iter().copied() { + Actors::control_stage_unsignaled_temporal(actor_id, 10) + .expect("causal temporal cohort stages"); + } + crate::ActorWaitingFrameChunks::::mutate((WakeupKey::Tick(10), 0), |maybe| { + let chunk = maybe.as_mut().expect("Trigger cohort page exists"); + let cell = chunk.entries[1] + .as_mut() + .and_then(crate::ActorWaitingEntry::primary_mut) + .expect("second Trigger cell exists"); + cell + .hot + .trigger_wakeup_pointer + .as_mut() + .expect("second Trigger pointer exists") + .tick = 99; + }); + assert_eq!( + Actors::control_latch_temporal_waiting_page(10, 0, 1, 10), + Err(crate::scheduler::ActorControlTransitionError::Invariant) + ); + assert_eq!( + crate::ActorWaitingOccupancies::::get(WakeupKey::Tick(10)), + 3 + ); + assert!(!crate::ActorWaitingFrameChunks::::contains_key(( + WakeupKey::Block(2), + 0 + ))); + for (ticket, actor_id) in actor_ids.iter().copied().enumerate() { + assert_eq!( + crate::ActorControlLocators::::get(actor_id), + Some(C1Location::Waiting { + key: WakeupKey::Tick(10), + page: 0, + slot: ticket as u8, + }) + ); + } + + crate::ActorWaitingFrameChunks::::mutate((WakeupKey::Tick(10), 0), |maybe| { + maybe + .as_mut() + .expect("Trigger cohort page survives rollback") + .entries[1] + .as_mut() + .and_then(crate::ActorWaitingEntry::primary_mut) + .expect("second Trigger cell survives rollback") + .hot + .trigger_wakeup_pointer + .as_mut() + .expect("second Trigger pointer survives rollback") + .tick = 10; + }); + assert_eq!( + Actors::control_latch_temporal_waiting_page(10, 0, 1, 10) + .expect("repaired Trigger cohort latches"), + actor_ids + ); + assert_eq!( + crate::ActorWaitingOccupancies::::get(WakeupKey::Block(2)), + 3 + ); + + crate::ActorWaitingFrameChunks::::mutate((WakeupKey::Block(2), 0), |maybe| { + maybe.as_mut().expect("N+1 cohort page exists").entries[1] + .as_mut() + .and_then(crate::ActorWaitingEntry::primary_mut) + .expect("second N+1 cell exists") + .eligible_at = Some(3); + }); + assert_eq!( + Actors::control_promote_due_waiting_page(2, 0, 2), + Err(crate::scheduler::ActorControlTransitionError::Invariant) + ); + assert_eq!(crate::ActorReadyTail::::get(), 0); + assert_eq!(crate::ActorReadyOccupancy::::get(), 0); + assert_eq!( + crate::ActorWaitingOccupancies::::get(WakeupKey::Block(2)), + 3 + ); + for (slot, actor_id) in actor_ids.iter().copied().enumerate() { + assert_eq!( + crate::ActorControlLocators::::get(actor_id), + Some(C1Location::Waiting { + key: WakeupKey::Block(2), + page: 0, + slot: slot as u8, + }) + ); + } + + crate::ActorWaitingFrameChunks::::mutate((WakeupKey::Block(2), 0), |maybe| { + maybe + .as_mut() + .expect("N+1 cohort page survives rollback") + .entries[1] + .as_mut() + .and_then(crate::ActorWaitingEntry::primary_mut) + .expect("second N+1 cell survives rollback") + .eligible_at = Some(2); + }); + let promoted = Actors::control_promote_due_waiting_page(2, 0, 2) + .expect("repaired N+1 cohort promotes atomically"); + assert_eq!( + promoted, + actor_ids + .iter() + .copied() + .enumerate() + .map(|(ticket, actor_id)| (actor_id, ticket as u64)) + .collect::>() + ); + assert_eq!(crate::ActorReadyTail::::get(), 3); + assert_eq!(crate::ActorReadyOccupancy::::get(), 3); + assert!(!crate::ActorWaitingTails::::contains_key( + WakeupKey::Block(2) + )); + frame_assert_single_owner(); + }); +} + +#[test] +fn control_crossing_chunk_transition_rolls_back_then_resumes_exactly() { + new_test_ext().execute_with(|| { + let actor_ids = frame_install_temporal_system_unsignaled(34); + for actor_id in actor_ids[..31].iter().copied() { + Actors::control_stage_unsignaled_temporal(actor_id, 10) + .expect("destination prefix cohort stages"); + } + for actor_id in actor_ids[31..].iter().copied() { + Actors::control_stage_unsignaled_temporal(actor_id, 11) + .expect("crossing source cohort stages"); + } + Actors::control_latch_temporal_waiting_page(10, 0, 1, 10) + .expect("31-cell destination prefix latches"); + assert_eq!( + crate::ActorWaitingTails::::get(WakeupKey::Block(2)), + 31 + ); + + crate::ActorWaitingFrameChunks::::mutate((WakeupKey::Tick(11), 0), |maybe| { + maybe.as_mut().expect("crossing source page exists").entries[2] + .as_mut() + .and_then(crate::ActorWaitingEntry::primary_mut) + .expect("third crossing source exists") + .hot + .trigger_wakeup_pointer + .as_mut() + .expect("third crossing pointer exists") + .tick = 99; + }); + assert_eq!( + Actors::control_latch_temporal_waiting_page(11, 0, 1, 11), + Err(crate::scheduler::ActorControlTransitionError::Invariant) + ); + assert_eq!( + crate::ActorWaitingTails::::get(WakeupKey::Block(2)), + 31, + "failed crossing append restores the destination tail" + ); + assert_eq!( + crate::ActorWaitingFrameChunks::::get((WakeupKey::Block(2), 0)) + .expect("destination prefix page survives") + .entries + .len(), + 32 + ); + assert!(!crate::ActorWaitingFrameChunks::::contains_key(( + WakeupKey::Block(2), + 1 + ))); + for (slot, actor_id) in actor_ids[31..].iter().copied().enumerate() { + assert_eq!( + crate::ActorControlLocators::::get(actor_id), + Some(C1Location::Waiting { + key: WakeupKey::Tick(11), + page: 0, + slot: slot as u8, + }) + ); + } + + crate::ActorWaitingFrameChunks::::mutate((WakeupKey::Tick(11), 0), |maybe| { + maybe + .as_mut() + .expect("crossing source survives rollback") + .entries[2] + .as_mut() + .and_then(crate::ActorWaitingEntry::primary_mut) + .expect("third crossing source survives rollback") + .hot + .trigger_wakeup_pointer + .as_mut() + .expect("third crossing pointer survives rollback") + .tick = 11; + }); + assert_eq!( + Actors::control_latch_temporal_waiting_page(11, 0, 1, 11).expect("crossing source resumes"), + actor_ids[31..] + ); + for (actor_id, page, slot) in [ + (actor_ids[31], 0, 31), + (actor_ids[32], 1, 0), + (actor_ids[33], 1, 1), + ] { + assert_eq!( + crate::ActorControlLocators::::get(actor_id), + Some(C1Location::Waiting { + key: WakeupKey::Block(2), + page, + slot, + }) + ); + } + assert_eq!( + Actors::control_promote_due_waiting_page(2, 0, 2) + .expect("full first service page promotes") + .len(), + 32 + ); + assert_eq!( + crate::ActorWaitingHeads::::get(WakeupKey::Block(2)), + 32 + ); + assert_eq!( + crate::ActorWaitingOccupancies::::get(WakeupKey::Block(2)), + 2 + ); + assert_eq!( + Actors::control_promote_due_waiting_page(2, 1, 2) + .expect("partial crossing service page promotes"), + vec![(actor_ids[32], 32), (actor_ids[33], 33)] + ); + assert_eq!(crate::ActorReadyTail::::get(), 34); + assert_eq!(crate::ActorReadyOccupancy::::get(), 34); + assert_eq!(crate::WakeupCursorLen::::get(WakeupClock::Block), 0); + frame_assert_single_owner(); + }); +} + +#[test] +fn control_temporal_actors_share_one_deadline_heap_key_until_last_close() { + for close_first_created in [true, false] { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let first = create_system_with( + BOB, + timer_schedule(5), + None, + contract_steps_with_step(make_step(Task::StopCycle)), + ); + let due_tick = Actors::actor_hot(first) + .expect("first temporal owner exists") + .trigger_wakeup_pointer + .expect("first deadline exists") + .tick; + let key = WakeupKey::Tick(due_tick); + let cursor_len = crate::WakeupCursorLen::::get(WakeupClock::Tick); + let index = crate::ActorWaitingCursorIndices::::get(key) + .expect("canonical Waiting owns its deadline index"); + let second = create_system_with( + CHARLIE, + timer_schedule(5), + None, + contract_steps_with_step(make_step(Task::StopCycle)), + ); + assert_eq!( + crate::WakeupCursorLen::::get(WakeupClock::Tick), + cursor_len + ); + assert_eq!( + crate::ActorWaitingCursorIndices::::get(key), + Some(index) + ); + assert_eq!(crate::ActorWaitingOccupancies::::get(key), 2); + let (first_closed, last_closed) = if close_first_created { + (first, second) + } else { + (second, first) + }; + assert_ok!(Actors::close_actor(RuntimeOrigin::root(), first_closed)); + assert_eq!(crate::ActorWaitingOccupancies::::get(key), 1); + assert_eq!( + crate::ActorWaitingCursorIndices::::get(key), + Some(index) + ); + assert_eq!(Actors::wakeup_cursor_peek_key(WakeupClock::Tick), Some(key)); + assert_ok!(Actors::close_actor(RuntimeOrigin::root(), last_closed)); + assert!(!crate::ActorWaitingCursorIndices::::contains_key(key)); + assert!(!crate::ActorWaitingOccupancies::::contains_key(key)); + assert_eq!( + crate::WakeupCursorLen::::get(WakeupClock::Tick), + cursor_len - 1 + ); + frame_assert_single_owner(); + }); + } +} + +#[test] +fn control_partial_ready_tail_preserves_existing_cells_before_causal_cohort() { + new_test_ext().execute_with(|| { + let existing = frame_install_direct_stop_cycle_ready(31); + let before = crate::ActorReadyFrameChunks::::get(0) + .expect("admitted existing actors occupy a partial Ready page"); + let cohort = frame_install_temporal_system_unsignaled(2); + for actor_id in cohort.iter().copied() { + Actors::control_stage_unsignaled_temporal(actor_id, 10) + .expect("admitted temporal actor stages in its causal cohort"); + } + assert_eq!( + Actors::control_latch_temporal_waiting_page(10, 0, 1, 10) + .expect("temporal cohort latches into next-block Waiting"), + cohort + ); + frame_system::Pallet::::set_block_number(2); + assert_eq!( + Actors::control_promote_due_waiting_page(2, 0, 2) + .expect("due cohort crosses the partial Ready tail"), + vec![(cohort[0], 31), (cohort[1], 32)] + ); + let first = crate::ActorReadyFrameChunks::::get(0).expect("first page remains"); + let second = crate::ActorReadyFrameChunks::::get(1).expect("cohort spills to next page"); + assert_eq!(first.len(), 32); + assert_eq!(second.len(), 32); + for (slot, actor_id) in existing.iter().copied().enumerate() { + assert_eq!(first[slot], before[slot]); + assert_eq!( + crate::ActorControlLocators::::get(actor_id), + Some(C1Location::Ready { + ticket: slot as u64 + }) + ); + } + assert_eq!( + first[31].as_ref().map(|cell| cell.actor_id), + Some(cohort[0]) + ); + assert_eq!( + second[0].as_ref().map(|cell| cell.actor_id), + Some(cohort[1]) + ); + assert!(second.iter().skip(1).all(Option::is_none)); + assert_eq!(Actors::queue_head(), 0); + assert_eq!(Actors::queue_tail(), 33); + assert_eq!(Actors::queue_occupancy(), 33); + assert!(!crate::ActorWaitingTails::::contains_key( + WakeupKey::Block(2) + )); + frame_assert_single_owner(); + }); +} + +#[test] +fn control_mixed_clock_arbitration_flips_only_after_success_and_faults_fail_closed() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let temporal_actor = frame_install_temporal_system_unsignaled(1)[0]; + Actors::control_stage_unsignaled_temporal(temporal_actor, 5) + .expect("Tick primary enters canonical Waiting"); + Actors::control_append_waiting( + frame_cell(900_000, 1), + WakeupKey::Block(1), + crate::scheduler::ActorWaitingAuthority::Service, + ) + .expect("Block primary enters canonical Waiting"); + crate::NextWakeupClock::::put(WakeupClock::Tick); + + assert_eq!( + Actors::control_service_next_due_waiting_unit(1, 5), + Ok(Some((WakeupClock::Tick, 1))) + ); + assert_eq!(crate::NextWakeupClock::::get(), WakeupClock::Block); + assert_eq!( + crate::ActorWaitingOccupancies::::get(WakeupKey::Block(1)), + 1 + ); + assert_eq!( + crate::ActorWaitingOccupancies::::get(WakeupKey::Block(2)), + 1 + ); + + assert_eq!( + Actors::control_service_next_due_waiting_unit(1, 5), + Ok(Some((WakeupClock::Block, 1))) + ); + assert_eq!(crate::NextWakeupClock::::get(), WakeupClock::Tick); + assert_eq!(crate::ActorReadyOccupancy::::get(), 1); + assert_eq!( + Actors::control_service_next_due_waiting_unit(1, 5), + Ok(None) + ); + assert_eq!(crate::NextWakeupClock::::get(), WakeupClock::Tick); + frame_assert_single_owner(); + }); + + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let temporal_actor = frame_install_temporal_system_unsignaled(1)[0]; + Actors::control_stage_unsignaled_temporal(temporal_actor, 5) + .expect("fault fixture Tick primary enters Waiting"); + Actors::control_append_waiting( + frame_cell(900_001, 1), + WakeupKey::Block(1), + crate::scheduler::ActorWaitingAuthority::Service, + ) + .expect("fault fixture Block primary enters Waiting"); + crate::NextWakeupClock::::put(WakeupClock::Tick); + crate::ActorWaitingFrameChunks::::remove((WakeupKey::Tick(5), 0)); + + assert_eq!( + Actors::control_service_next_due_waiting_unit(1, 5), + Err(crate::scheduler::ActorControlTransitionError::Invariant) + ); + assert_eq!(crate::NextWakeupClock::::get(), WakeupClock::Tick); + assert_eq!( + crate::ActorWaitingOccupancies::::get(WakeupKey::Block(1)), + 1 + ); + assert_eq!(crate::ActorReadyOccupancy::::get(), 0); + }); +} diff --git a/template/pallets/actors/src/tests/core.rs b/template/pallets/actors/src/tests/core.rs index b47f9468..65f1e4d5 100644 --- a/template/pallets/actors/src/tests/core.rs +++ b/template/pallets/actors/src/tests/core.rs @@ -199,7 +199,9 @@ fn maximum_contract_admission_uses_one_step_envelope_instead_of_suffix_sum() { let actor_id = crate::NextActorId::::get().saturating_sub(1); assert_eq!( crate::ActorContractTailChunks::::iter_prefix(actor_id).count(), - 2 + <::MaxContractSteps as Get>::get() + .saturating_sub(1) + .div_ceil(crate::MAX_STEPS_PER_TAIL_CHUNK) as usize ); }); } @@ -270,7 +272,6 @@ fn test_weight_fallback_equals_reference_interface_for_all_classes() { record_wakeup_worker_fault, close_actor, fee_collection, - cycle_orchestration, task_transfer, task_burn, task_mint, @@ -306,14 +307,16 @@ fn test_weight_fallback_equals_reference_interface_for_all_classes() { scheduler_paged_consume_preserve_page, scheduler_paged_consume_delete_page, scheduler_inner_zero_step_complete, + scheduler_paged_zero_step_user_crossing_unavailable, scheduler_actor_state_probe, transaction_extension_ingress_base, transaction_extension_ingress_notify, run_progress, run_suspend, - run_retry, run_complete, run_cancel, + opening_predicate_traversal, + opening_snapshot_traversal, update_contract, set_global_circuit_breaker, set_active_actor_limit, @@ -333,17 +336,19 @@ fn test_weight_fallback_equals_reference_interface_for_all_classes() { 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!(opening_snapshot_capture, 1, 8, 16, 24); + same_at!(opening_target_snapshot_capture, 1, 8, 12); + same_at!(opening_share_mixed_capture, 1, 8, 23); + same_at!(opening_predicate_capture, 0, 1, 24, 48); + same_at!(opening_max_encoded_balance_capture, 0, 1, 24, 48); + same_at!(opening_observation_heavy_capture, 1, 24, 47); same_at!(predicate_set_evaluation, 0, 1, 8); same_at!(task_split_transfer, 0, 1, 8); - same_at!(step_orchestration, 0, 1, 8); same_at!(scheduler_paged_tombstone_drain, 0, 1, 10_000); same_at!(scheduler_paged_mixed_scan, 0, 1, 10_000); 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!(run_suffix_admission, 0, 1, 8); same_at!(permissionless_sweep_many, 0, 1, 5); } @@ -412,7 +417,10 @@ fn create_atomicity_checkpoint_failure_rolls_back_all_state() { assert_eq!(SovereignIndex::::get(&expected_sovereign), None); assert_eq!(OwnerSlotBitmaps::::get(ALICE), [0; 32]); assert_eq!(Actors::active_actor_count(), active_before); - assert_eq!(ActorHot::::iter_keys().count() as u32, active_before); + assert_eq!( + crate::ActorControlLocators::::iter_keys().count() as u32, + active_before + ); assert_eq!(native_balance(&ALICE), owner_before); assert_eq!(native_balance(&TestFeeSink::get()), sink_before); assert_eq!(System::events(), events_before); @@ -526,7 +534,7 @@ fn activation_checkpoint_failure_rolls_back_state_and_event() { )); let actor_id = Actors::next_actor_id() - 1; frame_system::Pallet::::set_block_number(2); - let identity_before = ActorIdentities::::get(actor_id); + let identity_before = Actors::actor_identity(actor_id); fund_native_raw( &identity_before .as_ref() @@ -548,7 +556,7 @@ fn activation_checkpoint_failure_rolls_back_state_and_event() { DispatchError::Other("AtomicityCreateCheckpointFailed") ); - assert_eq!(ActorIdentities::::get(actor_id), identity_before); + assert_eq!(Actors::actor_identity(actor_id), identity_before); assert!(Actors::active_actor_view(actor_id).is_none()); assert_eq!(Actors::active_actor_count(), 0); assert_eq!(System::events(), events_before); @@ -739,22 +747,23 @@ fn weight_deferral_is_silent_when_both_dimensions_are_exhausted() { transfer_contract_steps(BOB, 10), ); assert_ok!(Actors::manual_trigger(RuntimeOrigin::signed(ALICE), actor_id)); - let instance = Actors::active_actor_view(actor_id).expect("Actors exists"); + let (_, cell) = Actors::actor_control_cell(actor_id).expect("current control owner exists"); + let step = cell.resources.control.saturating_add(cell.resources.effect); let queue_weight = <::WeightInfo as crate::WeightInfo>::scheduler_paged_tombstone_drain(1) .saturating_add(Actors::scheduler_actor_probe_weight_upper()) .saturating_add( <::WeightInfo as crate::WeightInfo>::scheduler_paged_consume_preserve_page() .max(<::WeightInfo as crate::WeightInfo>::scheduler_paged_consume_delete_page()), ); - // Both dimensions are exhausted one unit below the full cycle envelope. + // Both dimensions are exhausted one unit below the current Step envelope. let limit = Weight::from_parts( queue_weight .ref_time() - .saturating_add(Actors::attempt_weight_upper_bound(&instance, 0).ref_time()) + .saturating_add(step.ref_time()) .saturating_sub(1), queue_weight .proof_size() - .saturating_add(Actors::attempt_weight_upper_bound(&instance, 0).proof_size()) + .saturating_add(step.proof_size()) .saturating_sub(1), ); Actors::execute_cycle(limit); @@ -775,11 +784,11 @@ fn live_head_consume_rolls_back_on_missing_or_malformed_head_page() { 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)); + assert!(enqueue_latched_actor(actor_id)); if malformed { - QueuePages::::insert(0, BoundedVec::default()); + crate::ActorReadyFrameChunks::::insert(0, BoundedVec::default()); } else { - QueuePages::::remove(0); + crate::ActorReadyFrameChunks::::remove(0); } let events_before = System::events(); let root_before = polkadot_sdk::sp_io::storage::root(StateVersion::V1); @@ -795,8 +804,8 @@ fn live_head_consume_rolls_back_on_missing_or_malformed_head_page() { root_before ); assert_eq!( - Actors::actor_hot(actor_id).expect("hot").queue_ticket, - Some(0) + crate::ActorControlLocators::::get(actor_id), + Some(crate::ActorControlLocation::Ready { ticket: 0 }) ); }); } @@ -968,9 +977,7 @@ fn terminal_membership_rolls_back_atomically_with_failed_schedule_replacement() ); // Corrupt the existing wakeup cursor so the replacement placement must roll back; the // terminal membership update and the whole control transaction revert together. - WakeupBuckets::::mutate(WakeupKey::Block(102), |maybe| { - maybe.as_mut().expect("bucket").cursor_index = None; - }); + crate::ActorWaitingCursorIndices::::remove(WakeupKey::Block(102)); let root_before = polkadot_sdk::sp_io::storage::root(StateVersion::V1); assert!( update_contract_partial!( @@ -993,6 +1000,112 @@ fn terminal_membership_rolls_back_atomically_with_failed_schedule_replacement() }); } +#[test] +fn single_step_simulation_respects_both_resource_lanes_and_preserves_non_head_state() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + let steps = transfer_contract_steps(BOB, 10); + let predecessor = create_system_with(ALICE, manual_schedule(), None, steps.clone()); + let actor_id = create_system_with(ALICE, manual_schedule(), None, steps.clone()); + for id in [predecessor, actor_id] { + fund_native(id, 100); + assert_ok!(Actors::manual_trigger(RuntimeOrigin::signed(ALICE), id)); + } + let contract = system_active_contract(manual_schedule(), None, steps).expect("valid Contract"); + let before = polkadot_sdk::sp_io::storage::root(StateVersion::V1); + let host_before = ( + native_balance(&BOB), + native_balance(&sovereign_account(actor_id)), + fee_collections(), + ); + let full = ample_simulation_budget(); + let half = Weight::from_parts(u64::MAX / 2, u64::MAX / 2); + for budget in [ + crate::SimulationBudget { + actor_control: Weight::from_parts(0, half.proof_size()), + shared_economic: half, + }, + crate::SimulationBudget { + actor_control: Weight::from_parts(half.ref_time(), 0), + shared_economic: half, + }, + crate::SimulationBudget { + actor_control: half, + shared_economic: Weight::from_parts(0, half.proof_size()), + }, + crate::SimulationBudget { + actor_control: half, + shared_economic: Weight::from_parts(half.ref_time(), 0), + }, + ] { + assert_eq!( + Actors::simulate_current_contract( + actor_id, + ActorType::System, + Mutability::Mutable, + contract.clone(), + SimulationMode::FreshCurrentPlan, + budget + ), + Err(SimulationError::ResourceDeferred) + ); + assert_eq!(polkadot_sdk::sp_io::storage::root(StateVersion::V1), before); + assert_eq!( + ( + native_balance(&BOB), + native_balance(&sovereign_account(actor_id)), + fee_collections() + ), + host_before + ); + } + assert_eq!( + Actors::simulate_current_contract( + actor_id, + ActorType::System, + Mutability::Mutable, + contract.clone(), + SimulationMode::FreshCurrentPlan, + crate::SimulationBudget { + actor_control: Weight::MAX, + shared_economic: Weight::MAX + } + ), + Err(SimulationError::InvalidBudget) + ); + let result = Actors::simulate_current_contract( + actor_id, + ActorType::System, + Mutability::Mutable, + contract, + SimulationMode::FreshCurrentPlan, + full, + ) + .expect("non-head actor simulates one local Step"); + assert_eq!( + result.steps.as_slice(), + &[SimulationStepRecord { + step_index: 0, + outcome: StepOutcome::Executed + }] + ); + assert_eq!(result.status, AttemptDisposition::Completed); + assert_eq!( + Actors::paged_head_entry().map(|(_, entry)| entry.actor_id), + Some(predecessor) + ); + assert_eq!(polkadot_sdk::sp_io::storage::root(StateVersion::V1), before); + assert_eq!( + ( + native_balance(&BOB), + native_balance(&sovereign_account(actor_id)), + fee_collections() + ), + host_before + ); + }); +} + #[test] fn matching_runtime_simulation_reports_stop_and_rolls_everything_back() { new_test_ext().execute_with(|| { @@ -1022,6 +1135,7 @@ fn matching_runtime_simulation_reports_stop_and_rolls_everything_back() { Mutability::Mutable, contract, SimulationMode::FreshCurrentPlan, + ample_simulation_budget(), ) .expect("ready current plan simulates"); @@ -1060,6 +1174,7 @@ fn zero_step_simulation_completes_without_step_records_or_state_mutation() { Mutability::Mutable, contract, SimulationMode::FreshCurrentPlan, + ample_simulation_budget(), ) .expect("ready zero-Step plan simulates"); @@ -1080,7 +1195,7 @@ fn canonical_head_discovery_distinguishes_empty_head_and_blocked() { assert_eq!(Actors::test_head_discovery(0, 1, 0, scan), (0, None, 0)); let system = create_system_with(ALICE, manual_schedule(), None, inert_contract_steps()); - assert!(Actors::paged_enqueue(system)); + assert_ok!(Actors::manual_trigger(RuntimeOrigin::signed(ALICE), system)); let cutoff = Actors::next_queue_ticket(); let (state, entry, scanned) = Actors::test_head_discovery(cutoff, 1, 0, scan); assert_eq!((state, scanned), (1, 1)); @@ -1096,7 +1211,9 @@ fn canonical_head_discovery_distinguishes_empty_head_and_blocked() { (2, None, 0), "an unadmitted live-head probe is a weight stall" ); - QueueTail::::mutate(|tail| *tail = tail.saturating_add(1)); + crate::ActorReadyTail::::put( + u64::from(<::MaxQueueLength as Get>::get()) + 1, + ); assert_eq!( Actors::test_head_discovery(cutoff, 1, 0, scan), (3, None, 0), @@ -1220,7 +1337,7 @@ fn explicit_cancellation_preserves_committed_effects_and_emits_terminal_summary( 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) + Actors::actor_identity(actor_id) .expect("identity remains live") .cycle_nonce, 0 @@ -1248,6 +1365,11 @@ fn explicit_cancellation_preserves_committed_effects_and_emits_terminal_summary( 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"); + #[cfg(not(feature = "runtime-benchmarks"))] + assert!(matches!( + Actors::load_frame_actor_state(actor_id), + crate::LoadedActorStateOf::Active(_) + )); assert_eq!(actor_state.cycle_state, CycleState::Idle); assert_eq!(actor_state.cycle_nonce, 1); assert_eq!( @@ -1302,6 +1424,43 @@ fn explicit_cancellation_preserves_committed_effects_and_emits_terminal_summary( }); } +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn cancellation_fails_closed_when_scalar_run_outlives_its_primary() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + setup_temporary_retry_pool(); + let actor_id = create_system_with(ALICE, manual_schedule(), None, temporary_retry_swap_plan()); + fund_native(actor_id, 100); + set_temporary_dex_failure(true); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + run_idle(Weight::MAX); + assert!(Actors::actor_run_state(actor_id).is_some()); + Actors::remove_primary_control_cell_inner(actor_id).expect("primary removal succeeds"); + let hot_before = Actors::actor_hot(actor_id); + let run_before = Actors::actor_run_state(actor_id) + .expect("run remains before failed cancellation") + .encode(); + let events_before = System::events(); + + assert_noop!( + Actors::cancel_run(RuntimeOrigin::signed(ALICE), actor_id), + Error::::ActorInvariant + ); + assert_eq!(Actors::actor_hot(actor_id), hot_before); + assert_eq!( + Actors::actor_run_state(actor_id) + .expect("run remains after failed cancellation") + .encode(), + run_before + ); + assert_eq!(System::events(), events_before); + }); +} + #[test] fn cancellation_is_mutable_only() { new_test_ext().execute_with(|| { @@ -1577,7 +1736,7 @@ fn identity_control_clock_tracks_creation_and_survives_deactivation() { )); let actor_id = Actors::next_actor_id().saturating_sub(1); assert_eq!( - Actors::actor_identities(actor_id) + Actors::actor_identity(actor_id) .expect("created identity") .last_control_mutation_block, 7 @@ -1585,7 +1744,7 @@ fn identity_control_clock_tracks_creation_and_survives_deactivation() { frame_system::Pallet::::set_block_number(8); assert_ok!(Actors::deactivate_actor(RuntimeOrigin::root(), actor_id)); assert_eq!( - Actors::actor_identities(actor_id) + Actors::actor_identity(actor_id) .expect("dormant identity") .last_control_mutation_block, 8 @@ -1864,7 +2023,7 @@ fn admission_canonicalizes_dnf_and_equivalent_update_is_exact_noop() { assert!(clauses[0][0].encode() < clauses[0][1].encode()); let event_count = frame_system::Pallet::::event_count(); - let control_block = ActorIdentities::::get(actor_id) + let control_block = Actors::actor_identity(actor_id) .expect("identity exists") .last_control_mutation_block; assert_ok!(Actors::update_contract( @@ -1877,7 +2036,7 @@ fn admission_canonicalizes_dnf_and_equivalent_update_is_exact_noop() { )); assert_eq!(frame_system::Pallet::::event_count(), event_count); assert_eq!( - ActorIdentities::::get(actor_id) + Actors::actor_identity(actor_id) .expect("identity exists") .last_control_mutation_block, control_block @@ -2249,7 +2408,8 @@ fn system_immutable_indefinite_commitment_survives_breaker_mitigation() { )); let actor_id = Actors::next_actor_id().saturating_sub(1); let before = Actors::active_actor_view(actor_id).expect("immutable actor"); - assert!(Actors::attempt_weight_upper_bound(&before, 0).ref_time() > 0); + let (_, cell) = Actors::actor_control_cell(actor_id).expect("current control owner exists"); + assert!(cell.resources.control.ref_time() > 0); assert_eq!( Actors::system_sovereigns(actor_id), Some(SystemSovereignState::Occupied(actor_id)), @@ -2301,6 +2461,7 @@ fn fresh_current_plan_simulation_returns_runtime_trace_and_rolls_back_every_writ Mutability::Mutable, expected_contract, SimulationMode::FreshCurrentPlan, + ample_simulation_budget(), ) .expect("ready current plan simulates"); diff --git a/template/pallets/actors/src/tests/crossing.rs b/template/pallets/actors/src/tests/crossing.rs index 4c37063a..0d2b5508 100644 --- a/template/pallets/actors/src/tests/crossing.rs +++ b/template/pallets/actors/src/tests/crossing.rs @@ -2,6 +2,52 @@ use super::*; use crate::scheduler::ActivationOutcome; use crate::weights::WeightInfo as _; +#[test] +fn crossing_transition_revision_exhaustion_rejects_duplicate_without_mutation() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + set_observation( + 7, + crate::ScalarObservationState::Fresh { + value: 50, + observed_at: 1, + }, + ); + create_system_with( + ALICE, + Schedule { + trigger: RuntimeTrigger::observation_crossing(7, CrossingDirection::Rising, 100, 80), + cooldown_blocks: 0, + }, + None, + inert_contract_steps(), + ); + for (revision, previous, current) in [(u64::MAX - 1, 50, 110), (u64::MAX, 110, 50)] { + assert_ok!(Actors::note_observation_transition( + 7, + crate::ObservationTransition { + revision, + previous: Some(previous), + current, + } + )); + } + assert_noop!( + Actors::note_observation_transition( + 7, + crate::ObservationTransition { + revision: u64::MAX, + previous: Some(50), + current: 110, + } + ), + Error::::CrossingTransitionInvariant + ); + #[cfg(feature = "try-runtime")] + assert_ok!(Actors::do_try_state()); + }); +} + #[test] fn observation_crossing_semantics_are_exact_and_hysteretic() { let rising = ObservationCrossing { @@ -109,6 +155,47 @@ fn observation_crossing_semantics_are_exact_and_hysteretic() { } } +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn crossing_idle_loader_uses_primary_pending_authority() { + 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_system_with( + ALICE, + Schedule { + trigger: RuntimeTrigger::observation_crossing(7, CrossingDirection::Rising, 100, 80), + cooldown_blocks: 0, + }, + None, + inert_contract_steps(), + ); + let loaded = Actors::load_crossing_idle_activation_state(actor_id, 7) + .expect("frame-owned Crossing state loads"); + assert!(!loaded.hot.pending_signal); + assert!(matches!( + loaded.hot.trigger_runtime_state, + TriggerRuntimeState::ObservationCrossing { + phase: CrossingPhase::Armed, + installed_at_revision: 1, + } + )); + assert!(enqueue_latched_actor(actor_id)); + assert!( + Actors::load_crossing_idle_activation_state(actor_id, 7) + .expect("primary latch remains readable") + .hot + .pending_signal + ); + }); +} + #[test] fn observation_crossing_fire_charges_before_readiness() { new_test_ext().execute_with(|| { @@ -168,7 +255,7 @@ fn observation_crossing_fire_charges_before_readiness() { 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"); + let hot = Actors::actor_hot(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); @@ -228,7 +315,7 @@ fn repeated_latched_crossing_fires_charge_only_the_useful_transition() { let fee = observation_crossing_trigger_fee(); assert_eq!(fee_collections(), vec![fee]); - let hot = ActorHot::::get(actor_id).expect("Crossing Actor remains active"); + let hot = Actors::actor_hot(actor_id).expect("Crossing Actor remains active"); assert!(hot.pending_signal); assert!(hot.queue_ticket.is_some() || hot.wakeup_pointer.is_some()); assert_eq!( @@ -298,7 +385,7 @@ fn busy_crossing_fire_charges_and_latches_only_the_future_pipeline() { 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), + Actors::actor_hot(actor_id).map(|hot| hot.cycle_state), Some(CycleState::Running) ); clear_fee_collections(); @@ -314,7 +401,7 @@ fn busy_crossing_fire_charges_and_latches_only_the_future_pipeline() { drain_crossing_work(); assert_eq!(fee_collections(), vec![observation_crossing_trigger_fee()]); - let hot = ActorHot::::get(actor_id).expect("busy Actor remains active"); + let hot = Actors::actor_hot(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"); @@ -361,7 +448,7 @@ fn underfunded_crossing_fire_advances_without_fee_readiness_or_apoptosis() { assert!(fee_collections().is_empty()); assert_eq!(native_balance(&sovereign), TestMinUserBalance::get()); - let hot = ActorHot::::get(actor_id).expect("process remains live"); + let hot = Actors::actor_hot(actor_id).expect("process remains live"); assert!(!hot.pending_signal); assert!(hot.queue_ticket.is_none()); assert!(hot.wakeup_pointer.is_none()); @@ -416,11 +503,11 @@ fn crossing_batch_falls_back_to_scalar_progress_for_an_underfunded_member() { 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"); + let underfunded_hot = Actors::actor_hot(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"); + let funded_hot = Actors::actor_hot(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()]); @@ -464,7 +551,7 @@ fn crossing_fire_collection_failure_advances_without_readiness() { set_fail_fee_sink_transfer(false); assert_eq!(native_balance(&sovereign), before); - let hot = ActorHot::::get(actor_id).expect("process remains live"); + let hot = Actors::actor_hot(actor_id).expect("process remains live"); assert!(!hot.pending_signal); assert!(hot.queue_ticket.is_none()); assert!(hot.wakeup_pointer.is_none()); @@ -477,6 +564,63 @@ fn crossing_fire_collection_failure_advances_without_readiness() { }); } +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn crossing_fire_collection_failure_uses_frame_authority_without_scalar_control() { + 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, _) = + Actors::load_frame_control_authority(actor_id).expect("frame process remains live"); + assert!(!hot.pending_signal); + assert!(hot.queue_ticket.is_none()); + assert!(hot.wakeup_pointer.is_none()); + assert!(matches!( + hot.trigger_runtime_state, + TriggerRuntimeState::ObservationCrossing { + phase: CrossingPhase::WaitingForRearm, + .. + } + )); + assert!(!ActorIdentities::::contains_key(actor_id)); + assert!(Actors::actor_control_cell(actor_id).is_some()); + #[cfg(feature = "try-runtime")] + assert_ok!(crate::Pallet::::do_try_state()); + }); +} + #[test] fn observation_crossing_u128_boundaries_and_adjacent_hysteresis_are_exact() { let rising = ObservationCrossing { @@ -657,12 +801,12 @@ fn dormant_crossing_initializes_only_during_active_installation() { contract_steps_with_step(make_step(Task::StopCycle)), ) .expect("active Crossing contract"); - let identity = Actors::actor_identities(actor_id).expect("dormant identity"); + let identity = Actors::actor_identity(actor_id).expect("dormant identity"); assert_noop!( Actors::activate_actor(RuntimeOrigin::root(), actor_id, contract.clone()), Error::::ObservationUnavailable ); - assert_eq!(Actors::actor_identities(actor_id), Some(identity)); + assert_eq!(Actors::actor_identity(actor_id), Some(identity)); assert!(Actors::actor_hot(actor_id).is_none()); assert!(Actors::crossing_membership(actor_id).is_none()); @@ -679,7 +823,7 @@ fn dormant_crossing_initializes_only_during_active_installation() { contract, )); assert_eq!(crossing_phase(actor_id), CrossingPhase::WaitingForRearm); - let hot = ActorHot::::get(actor_id).expect("active Crossing actor"); + let hot = Actors::actor_hot(actor_id).expect("active Crossing actor"); assert!(!hot.pending_signal); assert!(hot.queue_ticket.is_none()); }); @@ -708,7 +852,7 @@ fn active_crossing_initializes_without_retrofire_and_cleans_exact_index_state() ); let locator = Actors::crossing_membership(actor_id).expect("Crossing membership exists"); assert!(matches!( - ActorHot::::get(actor_id).map(|hot| hot.trigger_runtime_state), + Actors::actor_hot(actor_id).map(|hot| hot.trigger_runtime_state), Some(TriggerRuntimeState::ObservationCrossing { phase: CrossingPhase::WaitingForRearm, .. @@ -718,7 +862,7 @@ fn active_crossing_initializes_without_retrofire_and_cleans_exact_index_state() assert_eq!(locator.key.traversal, crate::CrossingTraversal::Downward); assert_eq!(locator.key.threshold, 80); assert!( - !ActorHot::::get(actor_id) + !Actors::actor_hot(actor_id) .expect("hot state exists") .pending_signal ); @@ -764,7 +908,7 @@ fn exact_crossing_trigger_equality_preserves_canonical_and_physical_state() { ); frame_system::Pallet::::set_block_number(2); let contract = Actors::load_actor_contract(actor_id).expect("active Actor Contract"); - let hot = ActorHot::::get(actor_id).expect("active Actor hot state"); + let hot = Actors::actor_hot(actor_id).expect("active Actor hot state"); let locator = Actors::crossing_membership(actor_id).expect("Crossing membership"); System::reset_events(); assert_ok!(Actors::update_contract( @@ -773,7 +917,7 @@ fn exact_crossing_trigger_equality_preserves_canonical_and_physical_state() { contract.clone(), )); assert_eq!(Actors::load_actor_contract(actor_id), Some(contract)); - assert_eq!(ActorHot::::get(actor_id), Some(hot)); + assert_eq!(Actors::actor_hot(actor_id), Some(hot)); assert_eq!(Actors::crossing_membership(actor_id), Some(locator)); assert!(System::events().is_empty()); }); @@ -1079,13 +1223,23 @@ fn crossing_source_prefix_snapshot_grants_only_contiguous_validated_authority() cause_provenance: crate::TriggerCauseProvenance::Deferred, cause_block: 0, }; - let homogeneous = Actors::preflight_crossing_cohort(&prefix, transition, false, None) - .expect("homogeneous preflight"); + let homogeneous = Actors::preflight_crossing_cohort( + &prefix, + transition, + crate::crossing::CrossingFireClassification::Deferred, + None, + ) + .expect("homogeneous preflight"); assert_eq!(homogeneous.plan, crate::CrossingWorkPlan::FireCohortPending); assert_eq!(homogeneous.admitted_candidates, 2); assert_eq!(homogeneous.placed_immediate_fifo, None); - let placed_homogeneous = Actors::preflight_crossing_cohort(&prefix, transition, true, None) - .expect("homogeneous placed preflight"); + let placed_homogeneous = Actors::preflight_crossing_cohort( + &prefix, + transition, + crate::crossing::CrossingFireClassification::Resolve, + None, + ) + .expect("homogeneous placed preflight"); assert_eq!( placed_homogeneous.plan, crate::CrossingWorkPlan::FireCohortPlaced @@ -1108,15 +1262,23 @@ fn crossing_source_prefix_snapshot_grants_only_contiguous_validated_authority() let maximum_prefix = Actors::snapshot_crossing_source_prefix(locator.key, locator.page, &page, 0, 4) .expect("maximum production prefix"); - let maximum_preflight = - Actors::preflight_crossing_cohort(&maximum_prefix, transition, true, None) - .expect("maximum placed preflight"); + let maximum_preflight = Actors::preflight_crossing_cohort( + &maximum_prefix, + transition, + crate::crossing::CrossingFireClassification::Resolve, + None, + ) + .expect("maximum placed preflight"); assert_eq!(maximum_preflight.admitted_candidates, 4); assert_eq!(maximum_preflight.placed_immediate_fifo, Some(true)); assert_eq!(maximum_preflight.queue_candidates.len(), 4); let maximum_root = polkadot_sdk::sp_io::storage::root(StateVersion::V1); assert_eq!( - Actors::test_placed_cohort_authority_count(&maximum_prefix, transition, false), + Actors::test_placed_cohort_authority_count( + &maximum_prefix, + transition, + crate::crossing::PlacedCohortFixtureFault::None, + ), Ok(4) ); assert_eq!(Actors::test_queue_append_commits(), 1); @@ -1126,7 +1288,11 @@ fn crossing_source_prefix_snapshot_grants_only_contiguous_validated_authority() "maximum cohort commit prototype must roll back membership and queue surfaces" ); assert_eq!( - Actors::test_placed_cohort_authority_count(&maximum_prefix, transition, true), + Actors::test_placed_cohort_authority_count( + &maximum_prefix, + transition, + crate::crossing::PlacedCohortFixtureFault::MalformedLaterLocator, + ), Ok(4) ); assert_eq!( @@ -1134,7 +1300,10 @@ fn crossing_source_prefix_snapshot_grants_only_contiguous_validated_authority() maximum_root, "malformed later locator must roll back every cohort surface" ); - assert!(Actors::preflight_paged_enqueue_cohort(maximum_preflight.queue_candidates).is_ok()); + assert!( + Actors::preflight_paged_enqueue_cohort_with_authority(maximum_preflight.queue_candidates) + .is_ok() + ); assert_eq!( polkadot_sdk::sp_io::storage::root(StateVersion::V1), maximum_root, @@ -1145,13 +1314,17 @@ fn crossing_source_prefix_snapshot_grants_only_contiguous_validated_authority() let mut wakeup_contract = original_contract.clone(); wakeup_contract.cooldown_blocks = 10; assert_ok!(Actors::store_actor_contract(actors[2], wakeup_contract)); - let mut wakeup_hot = ActorHot::::get(actors[2]).expect("second prefix actor"); + let mut wakeup_hot = Actors::actor_hot(actors[2]).expect("second prefix actor"); let original_wakeup_hot = wakeup_hot.clone(); wakeup_hot.last_cycle_block = Some(1); - ActorHot::::insert(actors[2], wakeup_hot); - let placement_split = - Actors::preflight_crossing_cohort(&maximum_prefix, transition, true, None) - .expect("third-candidate placement split fallback"); + mutate_actor_hot_coherent(actors[2], |hot| *hot = wakeup_hot); + let placement_split = Actors::preflight_crossing_cohort( + &maximum_prefix, + transition, + crate::crossing::CrossingFireClassification::Resolve, + None, + ) + .expect("third-candidate placement split fallback"); assert_eq!( placement_split.plan, crate::CrossingWorkPlan::FireCohortPlaced @@ -1169,14 +1342,19 @@ fn crossing_source_prefix_snapshot_grants_only_contiguous_validated_authority() let wakeup_only = Actors::snapshot_crossing_source_prefix(locator.key, locator.page, &page, 2, 1) .expect("wakeup-only prefix"); - let wakeup_preflight = Actors::preflight_crossing_cohort(&wakeup_only, transition, true, None) - .expect("wakeup-only preflight"); + let wakeup_preflight = Actors::preflight_crossing_cohort( + &wakeup_only, + transition, + crate::crossing::CrossingFireClassification::Resolve, + None, + ) + .expect("wakeup-only preflight"); assert_eq!(wakeup_preflight.admitted_candidates, 1); assert_eq!(wakeup_preflight.placed_immediate_fifo, Some(false)); assert_ok!(Actors::store_actor_contract(actors[2], original_contract)); - ActorHot::::insert(actors[2], original_wakeup_hot); + mutate_actor_hot_coherent(actors[2], |hot| *hot = original_wakeup_hot); - let mut heterogeneous_hot = ActorHot::::get(actors[2]).expect("second prefix actor"); + let mut heterogeneous_hot = Actors::actor_hot(actors[2]).expect("second prefix actor"); let original_hot = heterogeneous_hot.clone(); let TriggerRuntimeState::ObservationCrossing { phase, .. } = heterogeneous_hot.trigger_runtime_state @@ -1187,15 +1365,20 @@ fn crossing_source_prefix_snapshot_grants_only_contiguous_validated_authority() phase, installed_at_revision: transition.revision, }; - ActorHot::::insert(actors[2], heterogeneous_hot); - let heterogeneous = Actors::preflight_crossing_cohort(&prefix, transition, false, None) - .expect("heterogeneous fallback"); + mutate_actor_hot_coherent(actors[2], |hot| *hot = heterogeneous_hot); + let heterogeneous = Actors::preflight_crossing_cohort( + &prefix, + transition, + crate::crossing::CrossingFireClassification::Deferred, + None, + ) + .expect("heterogeneous fallback"); assert_eq!( heterogeneous.plan, crate::CrossingWorkPlan::FireCohortPending ); assert_eq!(heterogeneous.admitted_candidates, 1); - ActorHot::::insert(actors[2], original_hot); + mutate_actor_hot_coherent(actors[2], |hot| *hot = original_hot); let remainder = Actors::snapshot_crossing_source_prefix(locator.key, locator.page, &page, 1, u32::MAX) @@ -1261,10 +1444,12 @@ fn crossing_tail_suffix_snapshot_grants_exact_generation_checked_refill_authorit None, contract_steps_with_step(make_step(Task::StopCycle)), ); - assert!(Actors::paged_enqueue(actor_id)); + assert!(enqueue_latched_actor(actor_id)); } assert_eq!(Actors::queue_occupancy(), 31); - let actors = (0..20) + let crossing_page_size = + <::CrossingPageSize as Get>::get() as usize; + let actors = (0..crossing_page_size as u64 + 4) .map(|index| { create_system_with( 100 + index, @@ -1328,7 +1513,11 @@ fn crossing_tail_suffix_snapshot_grants_exact_generation_checked_refill_authorit Ok(4) ); assert_eq!( - Actors::rewrite_non_tail_source(&source_prefix, &suffix, false), + Actors::rewrite_non_tail_source( + &source_prefix, + &suffix, + crate::crossing::CrossingRewriteDisposition::Preview, + ), Ok(true) ); assert_eq!(polkadot_sdk::sp_io::storage::root(StateVersion::V1), root); @@ -1338,15 +1527,20 @@ fn crossing_tail_suffix_snapshot_grants_exact_generation_checked_refill_authorit .iter() .map(|candidate| candidate.member.actor_id) .collect::>(), - actors[16..20] + actors[crossing_page_size..crossing_page_size + 4] ); assert_eq!(polkadot_sdk::sp_io::storage::root(StateVersion::V1), root); let corrupted = polkadot_sdk::frame_support::storage::with_transaction(|| { - CrossingMemberships::::mutate(actors[18], |locator| { + CrossingMemberships::::mutate(actors[crossing_page_size + 2], |locator| { locator.as_mut().expect("tail locator").generation += 1; }); let rejected = Actors::snapshot_crossing_tail_suffix(source.key, source.page, 4).is_err() - && Actors::rewrite_non_tail_source(&source_prefix, &suffix, false).is_err(); + && Actors::rewrite_non_tail_source( + &source_prefix, + &suffix, + crate::crossing::CrossingRewriteDisposition::Preview, + ) + .is_err(); polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Ok::( rejected, )) @@ -1360,7 +1554,12 @@ fn crossing_tail_suffix_snapshot_grants_exact_generation_checked_refill_authorit locator.as_mut().expect("source locator").generation += 1; }, ); - let rejected = Actors::rewrite_non_tail_source(&source_prefix, &suffix, false).is_err(); + let rejected = Actors::rewrite_non_tail_source( + &source_prefix, + &suffix, + crate::crossing::CrossingRewriteDisposition::Preview, + ) + .is_err(); polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Ok::( rejected, )) @@ -1384,7 +1583,11 @@ fn crossing_tail_suffix_snapshot_grants_exact_generation_checked_refill_authorit .expect("trimmed tail suffix"); assert_eq!(trimmed_suffix.start_offset, 2); assert_eq!( - Actors::rewrite_non_tail_source(&source_prefix, &trimmed_suffix, false), + Actors::rewrite_non_tail_source( + &source_prefix, + &trimmed_suffix, + crate::crossing::CrossingRewriteDisposition::Preview, + ), Ok(true) ); assert_eq!( @@ -1649,6 +1852,77 @@ fn crossing_pair_admits_at_exact_remaining_component_capacity() { } } +#[test] +fn crossing_opening_executes_effectful_step_and_rearms_detector() { + 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_system_with( + ALICE, + Schedule { + trigger: RuntimeTrigger::observation_crossing(7, CrossingDirection::Rising, 100, 80), + cooldown_blocks: 0, + }, + None, + BoundedVec::try_from(vec![ + make_step(Task::Transfer { + to: BOB, + asset: TestAsset::Native, + amount: AmountResolution::Fixed(7), + }), + make_step(Task::StopCycle), + ]) + .expect("two-Step Contract fits"), + ); + fund_native(actor_id, 1_000_000); + let recipient_before = MockAssetOps::balance(&BOB, TestAsset::Native); + set_observation( + 7, + crate::ScalarObservationState::Fresh { + value: 150, + observed_at: 1, + }, + ); + assert_ok!(Actors::note_observation_transition( + 7, + crate::ObservationTransition { + revision: 2, + previous: Some(50), + current: 150, + } + )); + drain_crossing_work(); + assert!( + Actors::actor_hot(actor_id) + .expect("latched Actor exists") + .pending_signal + ); + Actors::on_idle(1, Weight::MAX); + let hot = Actors::actor_hot(actor_id).expect("opened Actor remains active"); + assert_eq!(hot.cycle_state, CycleState::Running); + assert!(!hot.pending_signal); + let run = ActorRunStateStore::::get(actor_id).expect("Opening publishes a Run"); + assert_eq!(run.cursor, 1); + assert_eq!(run.last_committed_step_block, Some(1)); + assert_eq!( + MockAssetOps::balance(&BOB, TestAsset::Native), + recipient_before + 7 + ); + let membership = + Actors::crossing_membership(actor_id).expect("Opening rearms Crossing detector"); + assert_eq!(membership.key.traversal, crate::CrossingTraversal::Downward); + assert_eq!(membership.key.threshold, 80); + #[cfg(feature = "try-runtime")] + Actors::do_try_state().expect("real Crossing Opening preserves canonical state"); + }); +} + #[test] fn crossing_worker_fires_then_rearms_in_revision_order_without_retrofire() { new_test_ext().execute_with(|| { @@ -1688,7 +1962,7 @@ fn crossing_worker_fires_then_rearms_in_revision_order_without_retrofire() { assert_eq!(Actors::crossing_pending_feed_list().count, 0); let locator = Actors::crossing_membership(actor_id).expect("Crossing membership remains"); assert!(matches!( - ActorHot::::get(actor_id).map(|hot| hot.trigger_runtime_state), + Actors::actor_hot(actor_id).map(|hot| hot.trigger_runtime_state), Some(TriggerRuntimeState::ObservationCrossing { phase: CrossingPhase::WaitingForRearm, .. @@ -1697,7 +1971,7 @@ fn crossing_worker_fires_then_rearms_in_revision_order_without_retrofire() { assert_eq!(locator.key.traversal, crate::CrossingTraversal::Downward); assert_eq!(locator.key.threshold, 80); assert!( - ActorHot::::get(actor_id) + Actors::actor_hot(actor_id) .expect("hot state exists") .pending_signal ); @@ -1736,9 +2010,7 @@ fn falling_crossing_fires_and_rearms_without_duplicate_activation() { let fired = Actors::crossing_membership(actor_id).expect("membership remains"); assert_eq!(crossing_phase(actor_id), CrossingPhase::WaitingForRearm); assert_eq!(fired.key.traversal, crate::CrossingTraversal::Upward); - let ticket = ActorHot::::get(actor_id) - .expect("hot state") - .queue_ticket; + let ticket = Actors::actor_hot(actor_id).expect("hot state").queue_ticket; assert!(ticket.is_some()); assert_ok!(Actors::note_observation_transition( @@ -1754,9 +2026,7 @@ fn falling_crossing_fires_and_rearms_without_duplicate_activation() { 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") - .queue_ticket, + Actors::actor_hot(actor_id).expect("hot state").queue_ticket, ticket, "rearm changes detection state only and cannot duplicate activation" ); @@ -1804,7 +2074,7 @@ fn paused_and_breaker_crossings_preserve_state_evolution_and_coalescing() { )); drain_crossing_work(); assert_eq!(crossing_phase(actor_id), CrossingPhase::WaitingForRearm); - let first = ActorHot::::get(actor_id).expect("active Crossing actor"); + let first = Actors::active_actor_view(actor_id).expect("active Crossing actor"); assert!(first.pending_signal); assert_eq!( Actors::indexed_trigger_detection_disabled(actor_id), @@ -1822,7 +2092,7 @@ fn paused_and_breaker_crossings_preserve_state_evolution_and_coalescing() { )); drain_crossing_work(); assert_eq!(crossing_phase(actor_id), CrossingPhase::WaitingForRearm); - let rearmed = ActorHot::::get(actor_id).expect("active Crossing actor"); + let rearmed = Actors::active_actor_view(actor_id).expect("active Crossing actor"); assert!(rearmed.pending_signal); assert_eq!((rearmed.queue_ticket, rearmed.wakeup_pointer), placement); @@ -1836,7 +2106,7 @@ fn paused_and_breaker_crossings_preserve_state_evolution_and_coalescing() { )); drain_crossing_work(); assert_eq!(crossing_phase(actor_id), CrossingPhase::WaitingForRearm); - let fired_again = ActorHot::::get(actor_id).expect("active Crossing actor"); + let fired_again = Actors::active_actor_view(actor_id).expect("active Crossing actor"); assert!(fired_again.pending_signal); assert_eq!( (fired_again.queue_ticket, fired_again.wakeup_pointer), @@ -1853,7 +2123,7 @@ fn paused_and_breaker_crossings_preserve_state_evolution_and_coalescing() { )); } run_idle(Weight::MAX); - let resumed = ActorHot::::get(actor_id).expect("Crossing actor survives StopCycle"); + let resumed = Actors::active_actor_view(actor_id).expect("Crossing actor survives StopCycle"); assert_eq!( resumed.trigger_runtime_state, TriggerRuntimeState::ObservationCrossing { @@ -1904,7 +2174,7 @@ fn same_threshold_crossing_herd_spans_pages_without_loss_or_duplicate_ticket() { drain_crossing_work(); let mut tickets = BTreeSet::new(); for actor_id in actors { - let hot = ActorHot::::get(actor_id).expect("hot state"); + let hot = Actors::actor_hot(actor_id).expect("hot state"); assert!(hot.pending_signal); assert!(tickets.insert(hot.queue_ticket.expect("one live ticket"))); assert_eq!(crossing_phase(actor_id), CrossingPhase::WaitingForRearm); @@ -1938,8 +2208,7 @@ fn crossing_compaction_rewinds_cursor_for_an_unprocessed_tail_member() { ) }) .collect::>(); - ActorHot::::mutate(actors[0], |maybe_hot| { - let hot = maybe_hot.as_mut().expect("active Crossing actor"); + mutate_actor_hot_coherent(actors[0], |hot| { let TriggerRuntimeState::ObservationCrossing { phase, .. } = hot.trigger_runtime_state else { panic!("actor owns Crossing runtime state"); }; @@ -1969,7 +2238,7 @@ fn crossing_compaction_rewinds_cursor_for_an_unprocessed_tail_member() { assert_eq!((rewound.page, rewound.offset), (0, 0)); drain_crossing_work(); assert!( - ActorHot::::get(tail_actor) + Actors::actor_hot(tail_actor) .expect("tail actor remains") .pending_signal ); @@ -2018,7 +2287,7 @@ fn large_crossing_jump_visits_each_distinct_occupied_threshold_once() { .iter() .map(|actor_id| { assert_eq!(crossing_phase(*actor_id), CrossingPhase::WaitingForRearm); - ActorHot::::get(*actor_id) + Actors::actor_hot(*actor_id) .expect("hot state") .queue_ticket .expect("one live ticket") @@ -2072,7 +2341,7 @@ fn sparse_crossing_range_preserves_bounded_leaf_suffix_across_service_passes() { let first_fired = actors .iter() .filter(|actor_id| { - ActorHot::::get(**actor_id) + Actors::actor_hot(**actor_id) .expect("actor remains active") .pending_signal }) @@ -2101,7 +2370,7 @@ fn sparse_crossing_range_preserves_bounded_leaf_suffix_across_service_passes() { let tickets = actors .iter() .map(|actor_id| { - ActorHot::::get(*actor_id) + Actors::actor_hot(*actor_id) .expect("actor remains active") .queue_ticket .expect("one live ticket") @@ -2176,7 +2445,7 @@ fn crossing_scale_10k_zero_match_small_cohort_and_maximum_herd() { assert_eq!(zero_match.faults, 0); assert_eq!(Actors::combined_queue_occupancy(), 0); assert!(matched.iter().all(|actor_id| { - !ActorHot::::get(*actor_id) + !Actors::actor_hot(*actor_id) .expect("matched actor remains active") .pending_signal })); @@ -2212,7 +2481,7 @@ fn crossing_scale_10k_zero_match_small_cohort_and_maximum_herd() { let tickets = matched .iter() .map(|actor_id| { - ActorHot::::get(*actor_id) + Actors::actor_hot(*actor_id) .expect("matched actor remains active") .queue_ticket .expect("matched actor owns one ticket") @@ -2261,7 +2530,7 @@ fn crossing_scale_10k_zero_match_small_cohort_and_maximum_herd() { let mut deferred = 0u32; let mut placements = Vec::with_capacity(actor_count as usize); for actor_id in actors.iter().copied() { - let hot = ActorHot::::get(actor_id).expect("herd actor remains active"); + let hot = Actors::actor_hot(actor_id).expect("herd actor remains active"); assert!(hot.pending_signal); placements.push((hot.queue_ticket, hot.wakeup_pointer)); match (hot.queue_ticket, hot.wakeup_pointer) { @@ -2289,7 +2558,7 @@ 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)); for (index, actor_id) in actors.iter().enumerate() { - let hot = ActorHot::::get(*actor_id).expect("herd actor remains active"); + let hot = Actors::actor_hot(*actor_id).expect("herd actor remains active"); assert_eq!( ( crossing_phase(*actor_id), @@ -2319,7 +2588,7 @@ fn crossing_scale_10k_zero_match_small_cohort_and_maximum_herd() { )); let repeated_herd_units = drain_crossing_work_with_limit(actor_count.saturating_mul(2)); assert!(actors.iter().enumerate().all(|(index, actor_id)| { - let hot = ActorHot::::get(*actor_id).expect("herd actor remains active"); + let hot = Actors::actor_hot(*actor_id).expect("herd actor remains active"); crossing_phase(*actor_id) == CrossingPhase::WaitingForRearm && (hot.queue_ticket, hot.wakeup_pointer) == placements[index] })); @@ -2406,8 +2675,8 @@ fn breaker_materializes_maximum_mixed_wakeup_crossing_and_broad_fanout_without_e } assert!(Actors::crossing_transition_queue(7).is_none()); assert!(Actors::dirty_observation_feeds(8).is_none()); - let cadence_identity = ActorIdentities::::get(cadence_actor).expect("cadence identity"); - let cadence_hot = ActorHot::::get(cadence_actor).expect("cadence hot state"); + let cadence_identity = Actors::actor_identity(cadence_actor).expect("cadence identity"); + let cadence_hot = Actors::actor_hot(cadence_actor).expect("cadence hot state"); assert_eq!(cadence_identity.cycle_nonce, 0); assert!(cadence_hot.pending_signal); assert!( @@ -2415,8 +2684,8 @@ fn breaker_materializes_maximum_mixed_wakeup_crossing_and_broad_fanout_without_e "cadence must retain canonical queue or backpressure-retry placement under breaker" ); for actor_id in crossing_actors { - let identity = ActorIdentities::::get(actor_id).expect("Crossing actor identity"); - let hot = ActorHot::::get(actor_id).expect("Crossing actor hot state"); + let identity = Actors::actor_identity(actor_id).expect("Crossing actor identity"); + let hot = Actors::actor_hot(actor_id).expect("Crossing actor hot state"); assert_eq!(identity.cycle_nonce, 0); assert!(hot.pending_signal); assert_eq!( @@ -2428,8 +2697,8 @@ fn breaker_materializes_maximum_mixed_wakeup_crossing_and_broad_fanout_without_e ); } for actor_id in change_actors { - let identity = ActorIdentities::::get(actor_id).expect("Change actor identity"); - let hot = ActorHot::::get(actor_id).expect("Change actor hot state"); + let identity = Actors::actor_identity(actor_id).expect("Change actor identity"); + let hot = Actors::actor_hot(actor_id).expect("Change actor hot state"); assert_eq!(identity.cycle_nonce, 0); assert!(hot.pending_signal); } @@ -2565,9 +2834,9 @@ fn crossing_mixed_dense_sparse_directional_lifecycle_profile() { .into_iter() .chain(dense) .chain(falling) - .all(|actor_id| { ActorHot::::get(actor_id).is_some_and(|hot| hot.pending_signal) }) + .all(|actor_id| { Actors::actor_hot(actor_id).is_some_and(|hot| hot.pending_signal) }) ); - let insolvent_hot = ActorHot::::get(insolvent).expect("insolvent Actor remains active"); + let insolvent_hot = Actors::actor_hot(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); @@ -2611,7 +2880,7 @@ fn crossing_contract_replacement_removes_stale_pending_membership_before_worker_ )); drain_crossing_work(); assert!(Actors::crossing_membership(actor_id).is_none()); - let hot = ActorHot::::get(actor_id).expect("hot state"); + let hot = Actors::actor_hot(actor_id).expect("hot state"); assert!(!hot.pending_signal); assert!(hot.queue_ticket.is_none()); }); @@ -2664,7 +2933,7 @@ fn semantic_crossing_replacement_reinitializes_from_current_observation() { None, )); assert_eq!(crossing_phase(actor_id), CrossingPhase::Armed); - let hot = ActorHot::::get(actor_id).expect("active Crossing actor"); + let hot = Actors::actor_hot(actor_id).expect("active Crossing actor"); assert!(hot.pending_signal); assert!(hot.queue_ticket.is_some()); let locator = Actors::crossing_membership(actor_id).expect("replacement membership"); @@ -2711,7 +2980,7 @@ fn crossing_deactivation_and_close_remove_pending_members_without_stale_activati } drain_crossing_work(); assert!(Actors::crossing_membership(actor_id).is_none()); - assert!(ActorHot::::get(actor_id).is_none()); + assert!(Actors::actor_hot(actor_id).is_none()); assert_eq!(Actors::combined_queue_occupancy(), 0); }); } @@ -2821,7 +3090,7 @@ fn crossing_compact_authority_mismatch_truncates_at_first_middle_and_final_bound cause_provenance: crate::TriggerCauseProvenance::Deferred, cause_block: 1, }, - true, + crate::crossing::CrossingFireClassification::Resolve, Some(crate::CrossingWorkPlan::FireCohortPlaced), ) .expect("mismatch must select an exact scalar fallback or compact prefix"); @@ -2881,21 +3150,21 @@ fn crossing_same_tail_page_places_two_candidates_in_one_admitted_cohort() { }, ); let queue_capacity: u32 = ::MaxQueueLength::get(); - QueueOccupancy::::put(queue_capacity - 1); + crate::ActorReadyTail::::put(u64::from(queue_capacity - 1)); assert_eq!( Actors::classify_crossing_work(), crate::CrossingWorkPlan::FireCohortPlaced, "a pair must downgrade before mutation when only one FIFO position remains" ); - QueueOccupancy::::put(0); - crate::NextQueueTicket::::put(u64::MAX - 1); + crate::ActorReadyTail::::put(0); + crate::ActorReadyTail::::put(u64::MAX - 1); assert_eq!( Actors::classify_crossing_work(), crate::CrossingWorkPlan::FireCohortPlaced, "a pair must downgrade before its second ticket would exhaust the allocator" ); - crate::NextQueueTicket::::put(0); - QueueTail::::put(1); + crate::ActorReadyTail::::put(0); + crate::ActorReadyHead::::put(1); let corrupt_root = polkadot_sdk::sp_io::storage::root(StateVersion::V1); assert_eq!( Actors::classify_crossing_work(), @@ -2907,7 +3176,7 @@ fn crossing_same_tail_page_places_two_candidates_in_one_admitted_cohort() { corrupt_root, "pair classification must retain no partial queue authority" ); - QueueTail::::put(0); + crate::ActorReadyHead::::put(0); assert_eq!( Actors::classify_crossing_work(), crate::CrossingWorkPlan::FireCohortPlacedBatch @@ -2923,7 +3192,7 @@ fn crossing_same_tail_page_places_two_candidates_in_one_admitted_cohort() { assert_eq!(counters.closes, 0); assert_eq!(counters.faults, 0); for actor_id in [first, second] { - let hot = ActorHot::::get(actor_id).expect("cohort actor remains active"); + let hot = Actors::actor_hot(actor_id).expect("cohort actor remains active"); assert!(hot.pending_signal); assert!(hot.queue_ticket.is_some()); assert_eq!(crossing_phase(actor_id), CrossingPhase::WaitingForRearm); @@ -2965,6 +3234,83 @@ fn crossing_same_tail_page_places_two_candidates_in_one_admitted_cohort() { }); } +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn crossing_placed_cohort_commits_from_frame_authority_without_scalar_control() { + 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 steps = contract_steps_with_step(make_step(Task::StopCycle)); + let first = create_system_with(ALICE, schedule.clone(), None, steps.clone()); + let second = create_system_with(BOB, schedule, None, steps); + assert_ok!(Actors::note_observation_transition( + 7, + crate::ObservationTransition { + revision: 2, + previous: Some(50), + current: 150, + }, + )); + crate::CrossingRangeCursors::::insert( + 7, + crate::CrossingRangeCursor { + revision: 2, + traversal: crate::CrossingTraversal::Upward, + search_bound: 150, + current_threshold: Some(100), + page: 0, + offset: 0, + exhausted: false, + }, + ); + Actors::test_reset_queue_append_commits(); + let (_, counters) = Actors::service_crossing_transitions_with_counters(Weight::MAX); + assert_eq!(counters.candidates, 2); + assert_eq!(counters.activations, 2); + assert_eq!(counters.faults, 0); + assert_eq!(Actors::test_queue_append_commits(), 1); + for actor_id in [first, second] { + let (location, _, hot, _) = + Actors::load_frame_control_authority(actor_id).expect("cohort frame authority survives"); + assert!(matches!( + location, + crate::ActorControlLocation::Ready { .. } + )); + assert!(hot.pending_signal); + assert!(matches!( + hot.trigger_runtime_state, + TriggerRuntimeState::ObservationCrossing { + phase: CrossingPhase::WaitingForRearm, + .. + } + )); + assert!(!ActorIdentities::::contains_key(actor_id)); + assert!(Actors::actor_control_cell(actor_id).is_some()); + } + run_idle(Weight::MAX); + for actor_id in [first, second] { + assert!(has_actor_event(|event| matches!( + event, + Event::CycleSummary { actor_id: id, .. } if *id == actor_id + ))); + assert!(!ActorIdentities::::contains_key(actor_id)); + assert!(Actors::actor_control_cell(actor_id).is_some()); + } + #[cfg(feature = "try-runtime")] + assert_ok!(crate::Pallet::::do_try_state()); + }); +} + #[test] fn crossing_maximum_partial_page_batch_preserves_remainder_and_next_feed() { new_test_ext().execute_with(|| { @@ -3029,10 +3375,10 @@ fn crossing_maximum_partial_page_batch_preserves_remainder_and_next_feed() { assert_eq!(Actors::test_queue_append_commits(), 1); assert_eq!(Actors::test_crossing_cursor_commits(), 1); for actor_id in [first, second, third, fourth] { - assert!(ActorHot::::get(actor_id).is_some_and(|hot| hot.pending_signal)); + assert!(Actors::actor_hot(actor_id).is_some_and(|hot| hot.pending_signal)); assert_eq!(crossing_phase(actor_id), CrossingPhase::WaitingForRearm); } - let retained_hot = ActorHot::::get(retained).expect("retained source actor"); + let retained_hot = Actors::actor_hot(retained).expect("retained source actor"); assert!(!retained_hot.pending_signal); assert!(retained_hot.queue_ticket.is_none()); assert_eq!(crossing_phase(retained), CrossingPhase::Armed); @@ -3040,7 +3386,7 @@ fn crossing_maximum_partial_page_batch_preserves_remainder_and_next_feed() { Actors::crossing_membership(retained).map(|locator| locator.offset), Some(0) ); - let isolated_hot = ActorHot::::get(isolated).expect("isolated feed actor"); + let isolated_hot = Actors::actor_hot(isolated).expect("isolated feed actor"); assert!(!isolated_hot.pending_signal); assert!(isolated_hot.queue_ticket.is_none()); assert_eq!(crossing_phase(isolated), CrossingPhase::Armed); @@ -3086,16 +3432,14 @@ fn crossing_pair_partial_cohort_preserves_second_actor_installation_barrier() { exhausted: false, }, ); - ActorHot::::mutate(second, |maybe_hot| { - let hot = maybe_hot.as_mut().expect("second cohort Actor exists"); - let TriggerRuntimeState::ObservationCrossing { - installed_at_revision, - .. - } = &mut hot.trigger_runtime_state - else { + mutate_actor_hot_coherent(second, |hot| { + let TriggerRuntimeState::ObservationCrossing { phase, .. } = hot.trigger_runtime_state else { panic!("second cohort Actor uses Crossing state"); }; - *installed_at_revision = 2; + hot.trigger_runtime_state = TriggerRuntimeState::ObservationCrossing { + phase, + installed_at_revision: 2, + }; }); assert_eq!( @@ -3109,10 +3453,10 @@ fn crossing_pair_partial_cohort_preserves_second_actor_installation_barrier() { assert_eq!(counters.activations, 1); assert_eq!(counters.closes, 0); assert_eq!(counters.faults, 0); - assert!(ActorHot::::get(first).is_some_and(|hot| hot.pending_signal)); + assert!(Actors::actor_hot(first).is_some_and(|hot| hot.pending_signal)); assert_eq!(crossing_phase(first), CrossingPhase::WaitingForRearm); assert!( - ActorHot::::get(second) + Actors::actor_hot(second) .is_some_and(|hot| { !hot.pending_signal && hot.queue_ticket.is_none() }) ); assert_eq!(crossing_phase(second), CrossingPhase::Armed); @@ -3157,8 +3501,8 @@ fn crossing_pair_preflight_fault_cannot_activate_its_valid_first_candidate() { exhausted: false, }, ); - let first_before = ActorHot::::get(first).expect("first cohort Actor exists"); - let second_before = ActorHot::::get(second).expect("second cohort Actor exists"); + let first_before = Actors::actor_hot(first).expect("first cohort Actor exists"); + let second_before = Actors::actor_hot(second).expect("second cohort Actor exists"); crate::CrossingMemberships::::mutate(second, |maybe_locator| { let locator = maybe_locator .as_mut() @@ -3172,9 +3516,9 @@ fn crossing_pair_preflight_fault_cannot_activate_its_valid_first_candidate() { assert_eq!(counters.activations, 0); assert_eq!(counters.closes, 0); assert_eq!(counters.faults, 1); - assert_eq!(ActorHot::::get(first), Some(first_before)); - assert_eq!(ActorHot::::get(second), Some(second_before)); - assert_eq!(crate::QueueOccupancy::::get(), 0); + assert_eq!(Actors::actor_hot(first), Some(first_before)); + assert_eq!(Actors::actor_hot(second), Some(second_before)); + assert_eq!(crate::ActorReadyOccupancy::::get(), 0); let fault = crate::CrossingWorkerFaultState::::get().expect("Crossing fault recorded"); assert_eq!( actor_event_count(|event| matches!( @@ -3273,6 +3617,62 @@ fn crossing_terminal_weight_is_separate_from_ordinary_placement() { }); } +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn frame_only_zero_step_crossing_never_materializes_scalar_hot() { + 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_system_with( + ALICE, + Schedule { + trigger: RuntimeTrigger::observation_crossing(7, CrossingDirection::Rising, 100, 80), + cooldown_blocks: 0, + }, + None, + BoundedVec::default(), + ); + assert_ok!(Actors::note_observation_transition( + 7, + crate::ObservationTransition { + revision: 2, + previous: Some(50), + current: 150, + }, + )); + let (_, counters) = Actors::service_crossing_transitions_with_counters(Weight::MAX); + assert_eq!(counters.activations, 1); + assert!(crate::IndexedTriggerDetectionDisabled::::contains_key(actor_id)); + + Actors::execute_cycle(Weight::MAX); + + assert!(!crate::IndexedTriggerDetectionDisabled::::contains_key(actor_id)); + assert!(crate::CrossingMemberships::::contains_key(actor_id)); + let state = Actors::active_actor_state(actor_id).expect("Crossing successor remains active"); + assert_eq!(state.identity.cycle_nonce, 1); + assert!(matches!( + state.hot.trigger_runtime_state, + TriggerRuntimeState::ObservationCrossing { + phase: CrossingPhase::Armed, + .. + } + )); + assert!(matches!( + crate::ActorControlLocators::::get(actor_id), + Some(crate::ActorControlLocation::Unsignaled) + )); + assert!(!ActorIdentities::::contains_key(actor_id)); + #[cfg(feature = "try-runtime")] + assert_ok!(crate::Pallet::::do_try_state()); + }); +} + #[test] fn crossing_work_plan_is_read_only_and_classifies_search_before_execution() { new_test_ext().execute_with(|| { @@ -3618,7 +4018,7 @@ fn crossing_activation_under_saturated_fifo_latches_and_defers_exactly_once() { }, )); drain_crossing_work(); - let hot = ActorHot::::get(actor_id).expect("hot state"); + let hot = Actors::actor_hot(actor_id).expect("hot state"); assert!(hot.pending_signal); assert!(hot.queue_ticket.is_none()); assert_eq!( @@ -3627,12 +4027,12 @@ fn crossing_activation_under_saturated_fifo_latches_and_defers_exactly_once() { ); frame_system::Pallet::::set_block_number(2); run_idle(Weight::MAX); - let recovered = ActorHot::::get(actor_id).expect("actor survives StopCycle"); + let recovered = Actors::actor_hot(actor_id).expect("actor survives StopCycle"); assert!(!recovered.pending_signal); assert!(recovered.queue_ticket.is_none()); assert!(recovered.wakeup_pointer.is_none()); assert_eq!( - ActorIdentities::::get(actor_id) + Actors::actor_identity(actor_id) .expect("actor identity") .cycle_nonce, 1 @@ -3660,7 +4060,8 @@ fn crossing_ticket_exhaustion_closes_and_cleans_detection_membership() { None, contract_steps_with_step(make_step(Task::StopCycle)), ); - crate::NextQueueTicket::::put(u64::MAX); + crate::ActorReadyHead::::put(u64::MAX); + crate::ActorReadyTail::::put(u64::MAX); assert_ok!(Actors::note_observation_transition( 7, crate::ObservationTransition { @@ -3690,7 +4091,7 @@ fn crossing_ticket_exhaustion_closes_and_cleans_detection_membership() { } #[test] -fn crossing_queue_index_exhaustion_rolls_back_the_actor_unit_exactly() { +fn crossing_opening_rearm_uses_frozen_class_admission_and_frame_hot_authority() { new_test_ext().execute_with(|| { frame_system::Pallet::::set_block_number(1); set_observation( @@ -3709,9 +4110,6 @@ fn crossing_queue_index_exhaustion_rolls_back_the_actor_unit_exactly() { None, contract_steps_with_step(make_step(Task::StopCycle)), ); - crate::QueueTail::::put(u64::MAX); - crate::NextQueueTicket::::put(5); - QueueOccupancy::::put(0); assert_ok!(Actors::note_observation_transition( 7, crate::ObservationTransition { @@ -3720,25 +4118,44 @@ fn crossing_queue_index_exhaustion_rolls_back_the_actor_unit_exactly() { current: 150, }, )); - let root_before = polkadot_sdk::sp_io::storage::root(StateVersion::V1); - assert_noop!( - Actors::crossing_work_unit(), - Error::::SchedulerIndexExhausted + drain_crossing_work(); + assert!(crate::IndexedTriggerDetectionDisabled::::contains_key(actor_id)); + + set_observation( + 7, + crate::ScalarObservationState::Fresh { + value: 70, + observed_at: 3, + }, ); + Actors::execute_cycle(Weight::MAX); + + let crate::LoadedActorStateOf::Active(state) = + Actors::load_actor_state_for_frame_control(actor_id) + else { + panic!("rearmed Crossing frame state is active"); + }; + assert_eq!(state.identity.cycle_nonce, 1); + assert!(!state.hot.pending_signal); + assert!(state.hot.queue_ticket.is_none()); assert_eq!( - polkadot_sdk::sp_io::storage::root(StateVersion::V1), - root_before - ); - let hot = ActorHot::::get(actor_id).expect("actor remains active"); - assert!(matches!( - hot.trigger_runtime_state, + state.hot.trigger_runtime_state, TriggerRuntimeState::ObservationCrossing { phase: CrossingPhase::Armed, - .. + installed_at_revision: 1, } - )); - assert!(!hot.pending_signal); - assert!(hot.queue_ticket.is_none()); + ); + assert_eq!( + CrossingMemberships::::get(actor_id) + .expect("rearmed Crossing membership exists") + .key + .threshold, + 100 + ); + assert!(!crate::IndexedTriggerDetectionDisabled::::contains_key(actor_id)); + assert!(Actors::actor_control_cell(actor_id).is_some()); + #[cfg(feature = "try-runtime")] + assert_ok!(crate::Pallet::::do_try_state()); }); } @@ -3834,16 +4251,20 @@ fn crossing_try_state_rejects_canonical_locator_radix_and_pending_list_corruptio crate::CrossingMemberPages::::insert(locator.key, locator.page, original_page); assert_ok!(crate::Pallet::::do_try_state()); - let identity = ActorIdentities::::get(actor_id).expect("actor identity exists"); - let hot = ActorHot::::get(actor_id).expect("actor hot state exists"); + let identity = Actors::actor_identity(actor_id).expect("actor identity exists"); + let hot = Actors::actor_hot(actor_id).expect("actor hot state 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()); + assert_ok!(crate::Pallet::::do_try_state()); ActorIdentities::::insert(actor_id, identity); - ActorHot::::remove(actor_id); assert!(crate::Pallet::::do_try_state().is_err()); - ActorHot::::insert(actor_id, hot.clone()); + ActorIdentities::::remove(actor_id); + assert_ok!(crate::Pallet::::do_try_state()); + let primary_location = + crate::ActorControlLocators::::take(actor_id).expect("canonical locator exists"); + assert!(crate::Pallet::::do_try_state().is_err()); + crate::ActorControlLocators::::insert(actor_id, primary_location); + assert_ok!(crate::Pallet::::do_try_state()); assert!(Actors::remove_admitted_contract_geometry(actor_id).is_some()); assert!(crate::Pallet::::do_try_state().is_err()); assert_ok!(Actors::store_actor_contract(actor_id, contract)); @@ -3852,36 +4273,24 @@ fn crossing_try_state_rejects_canonical_locator_radix_and_pending_list_corruptio ActorFunding::::insert(actor_id, funding); let runtime_state = hot.trigger_runtime_state; - ActorHot::::mutate(actor_id, |maybe_hot| { - maybe_hot - .as_mut() - .expect("active Crossing actor") - .trigger_runtime_state = TriggerRuntimeState::Stateless; + mutate_actor_hot_coherent(actor_id, |hot| { + hot.trigger_runtime_state = TriggerRuntimeState::Stateless; }); assert!(crate::Pallet::::do_try_state().is_err()); - ActorHot::::mutate(actor_id, |maybe_hot| { - maybe_hot - .as_mut() - .expect("active Crossing actor") - .trigger_runtime_state = runtime_state.clone(); + mutate_actor_hot_coherent(actor_id, |hot| { + hot.trigger_runtime_state = runtime_state.clone(); }); assert_ok!(crate::Pallet::::do_try_state()); - ActorHot::::mutate(actor_id, |maybe_hot| { - maybe_hot - .as_mut() - .expect("active Crossing actor") - .trigger_runtime_state = TriggerRuntimeState::ObservationCrossing { + mutate_actor_hot_coherent(actor_id, |hot| { + hot.trigger_runtime_state = TriggerRuntimeState::ObservationCrossing { phase: CrossingPhase::WaitingForRearm, installed_at_revision: u64::MAX, }; }); assert!(crate::Pallet::::do_try_state().is_err()); - ActorHot::::mutate(actor_id, |maybe_hot| { - maybe_hot - .as_mut() - .expect("active Crossing actor") - .trigger_runtime_state = runtime_state; + mutate_actor_hot_coherent(actor_id, |hot| { + hot.trigger_runtime_state = runtime_state; }); assert_ok!(crate::Pallet::::do_try_state()); @@ -3894,18 +4303,12 @@ fn crossing_try_state_rejects_canonical_locator_radix_and_pending_list_corruptio None, contract_steps_with_step(make_step(Task::StopCycle)), ); - ActorHot::::mutate(cadence_actor, |maybe_hot| { - maybe_hot - .as_mut() - .expect("active cadence actor") - .trigger_runtime_state = TriggerRuntimeState::Stateless; + mutate_actor_hot_coherent(cadence_actor, |hot| { + hot.trigger_runtime_state = TriggerRuntimeState::Stateless; }); assert!(crate::Pallet::::do_try_state().is_err()); - ActorHot::::mutate(cadence_actor, |maybe_hot| { - maybe_hot - .as_mut() - .expect("active cadence actor") - .trigger_runtime_state = TriggerRuntimeState::Cadenced { anchor_tick: None }; + mutate_actor_hot_coherent(cadence_actor, |hot| { + hot.trigger_runtime_state = TriggerRuntimeState::Cadenced { anchor_tick: None }; }); assert_ok!(crate::Pallet::::do_try_state()); @@ -3972,6 +4375,18 @@ fn crossing_try_state_rejects_canonical_locator_radix_and_pending_list_corruptio let list = crate::CrossingPendingFeedListState::::get(); let pending = crate::CrossingPendingFeeds::::get(7).expect("pending feed exists"); let queue = crate::CrossingTransitionQueues::::get(7).expect("transition queue exists"); + for revisions in [[0, 1], [u64::MAX, u64::MAX]] { + let mut corrupt = queue.clone(); + for (transition, revision) in corrupt.iter_mut().zip(revisions) { + transition.revision = revision; + } + crate::CrossingTransitionQueues::::insert(7, corrupt); + assert!( + Actors::do_try_state().is_err(), + "invalid Crossing revisions {revisions:?} were accepted" + ); + } + crate::CrossingTransitionQueues::::insert(7, queue.clone()); crate::CrossingPendingFeedListState::::mutate(|stored| stored.count += 1); assert!(crate::Pallet::::do_try_state().is_err()); diff --git a/template/pallets/actors/src/tests/execution.rs b/template/pallets/actors/src/tests/execution.rs index 877e5fca..994f7a61 100644 --- a/template/pallets/actors/src/tests/execution.rs +++ b/template/pallets/actors/src/tests/execution.rs @@ -1,5 +1,499 @@ use super::*; +#[test] +fn control_invocation_receipts_are_independent_of_actor_fee_exemption() { + for actor_type in [ActorType::User, ActorType::System] { + for phase in [ + crate::StepControlPhase::Opening, + crate::StepControlPhase::Running, + crate::StepControlPhase::Suspended, + ] { + for result in [0, 1, 2] { + new_test_ext().execute_with(|| { + System::set_block_number(1); + let step = StepOf:: { + precondition: None, + task: Task::Transfer { + to: BOB, + asset: TestAsset::Local(1), + amount: AmountResolution::Fixed(1), + }, + on_error: RETRY_LATER, + }; + let mut steps = Vec::new(); + if phase == crate::StepControlPhase::Running { + steps.push(make_step(Task::Transfer { + to: BOB, + asset: TestAsset::Local(1), + amount: AmountResolution::PercentageOfCurrent(Perbill::one()), + })); + } + steps.push(step.clone()); + let steps = + crate::ContractSteps::::try_from(steps).expect("small phase witness fits"); + let pipeline_fee = pipeline_opening_fee(&steps); + let actor_id = if actor_type == ActorType::User { + create_user_with(ALICE, Mutability::Mutable, manual_schedule(), None, steps) + } else { + create_system_with(ALICE, manual_schedule(), None, steps) + }; + fund_native(actor_id, 1_000_000_000_000_000_000); + let sovereign = Actors::actor_identity(actor_id) + .expect("admitted identity") + .sovereign_account; + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + if phase != crate::StepControlPhase::Opening { + Actors::execute_cycle(Weight::MAX); + let run = + Actors::actor_run_state(actor_id).expect("real prefix or failure retains Run"); + assert_eq!( + run.cursor, + u32::from(phase == crate::StepControlPhase::Running) + ); + System::set_block_number(run.eligible_at); + let mut meter = polkadot_sdk::sp_weights::WeightMeter::with_limit(Weight::MAX); + Actors::drain_overdue_wakeups_cursor(run.eligible_at, &mut meter); + } + if result != 0 { + set_asset_balance(&sovereign, TestAsset::Local(1), 100); + } + set_fail_transfer_to((result == 2).then_some(BOB)); + System::reset_events(); + let sink = TestFeeSink::get(); + let sink_before = native_balance(&sink); + let recipient_before = asset_balance(&BOB, TestAsset::Local(1)); + Actors::execute_cycle(Weight::MAX); + let execution = + last_step_control_execution().expect("committed Step reports Control evidence"); + assert_eq!(execution.phase, phase); + assert_eq!( + execution.task_effect, + if result == 0 { + crate::TaskEffectExecution::NotInvoked + } else { + crate::TaskEffectExecution::Invoked + } + ); + assert_eq!( + execution.action_fee_collected, + actor_type == ActorType::User && result != 0 + ); + let receipts = System::events() + .into_iter() + .filter_map(|record| match record.event { + RuntimeEvent::Actors(Event::ActionFeeCharged { + actor_id: id, fee, .. + }) if id == actor_id => Some(fee), + _ => None, + }) + .collect::>(); + let action_fee = if actor_type == ActorType::User && result != 0 { + user_step_fee(&step) + } else { + 0 + }; + assert_eq!( + receipts, + if result == 0 { + Vec::new() + } else { + vec![action_fee] + } + ); + let opening_fee = + if actor_type == ActorType::User && phase == crate::StepControlPhase::Opening { + pipeline_fee + } else { + 0 + }; + assert_eq!( + native_balance(&sink) - sink_before, + opening_fee + action_fee + ); + assert_eq!( + asset_balance(&BOB, TestAsset::Local(1)) - recipient_before, + u128::from(result == 1) + ); + #[cfg(feature = "try-runtime")] + assert_ok!(Actors::do_try_state()); + }); + } + } + } +} + +#[test] +fn loaded_step_returns_exact_persisted_successor_run() { + use crate::AttemptDisposition; + + // Source states are reached through ordinary Opening/Q1 service. Only the final bounded + // core atom is inspected before successor publication, inside a rolled-back transaction. + for (name, count, initial_funding, advance, refill, retry, expected, cursor) in [ + ( + "opening progress", + 3, + 10, + false, + 0, + false, + AttemptDisposition::Continued, + Some(1), + ), + ( + "running progress", + 3, + 10, + true, + 0, + false, + AttemptDisposition::Continued, + Some(2), + ), + ( + "first retry", + 2, + 0, + false, + 0, + true, + AttemptDisposition::Suspended, + Some(0), + ), + ( + "repeat retry", + 2, + 0, + true, + 0, + true, + AttemptDisposition::Suspended, + Some(0), + ), + ( + "suspended progress", + 2, + 0, + true, + 10, + true, + AttemptDisposition::Continued, + Some(1), + ), + ( + "opening complete", + 1, + 10, + false, + 0, + false, + AttemptDisposition::Completed, + None, + ), + ( + "running complete", + 2, + 10, + true, + 0, + false, + AttemptDisposition::Completed, + None, + ), + ( + "suspended complete", + 1, + 0, + true, + 10, + true, + AttemptDisposition::Completed, + None, + ), + ( + "opening failed", + 1, + 10, + false, + 0, + false, + AttemptDisposition::Failed, + None, + ), + ( + "running failed", + 2, + 10, + true, + 0, + false, + AttemptDisposition::Failed, + None, + ), + ] { + new_test_ext().execute_with(|| { + System::set_block_number(1); + let mut step = make_step(Task::Transfer { + to: BOB, + asset: TestAsset::Local(1), + amount: AmountResolution::Fixed(1), + }); + if retry { + step.on_error = StepErrorPolicy::RetryLater { max_attempts: 3 }; + } + let steps = BoundedVec::try_from(vec![step; count]).expect("bounded test Contract"); + let actor_id = create_system_with(ALICE, manual_schedule(), None, steps); + let sovereign = Actors::actor_identity(actor_id) + .expect("admitted identity") + .sovereign_account; + if initial_funding > 0 { + set_asset_balance(&sovereign, TestAsset::Local(1), initial_funding); + } + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + if advance { + Actors::on_initialize(1); + run_prepass(); + Actors::on_idle(1, Weight::MAX); + let run = Actors::actor_run_state(actor_id).expect("first ordinary attempt retains Run"); + System::set_block_number(run.eligible_at); + } + if refill > 0 { + set_asset_balance(&sovereign, TestAsset::Local(1), refill); + } + if expected == AttemptDisposition::Failed { + set_fail_transfer_to(Some(BOB)); + } + let now = System::block_number(); + let (state, admission, loaded_step) = Actors::load_current_step_service_state(actor_id) + .expect("real source has complete canonical authority"); + let instance = Actors::derive_active_actor_view( + state.identity.clone(), + state.hot.clone(), + state.contract.clone(), + ); + let ticket = Actors::build_actor_step_ticket( + actor_id, + state.hot.queue_ticket.expect("ordinary successor is Ready"), + now, + &state.identity, + &state.hot, + state.run_state.as_ref(), + &admission, + ) + .expect("real source ticket"); + let fee = Actors::maximum_current_action_fee( + ActorType::System, + &loaded_step.step, + loaded_step.resources, + ) + .expect("System fee envelope"); + let plan = Actors::build_current_step_plan( + actor_id, + state.identity, + state.hot, + state.run_state, + state.funding, + admission, + ticket, + loaded_step, + fee, + ) + .expect("source plan is coherent"); + let corruption_plan = plan.clone(); + let mut successor_parts = None; + let before = polkadot_sdk::sp_io::storage::root(polkadot_sdk::sp_runtime::StateVersion::V1); + assert_ok!(polkadot_sdk::frame_support::storage::with_transaction( + || { + assert_ok!(Actors::paged_consume_head_at(ticket.ticket)); + let (returned, _, disposition, _, _) = + Actors::execute_loaded_single_step_core(actor_id, &instance, plan, now, count as u32) + .unwrap_or_else(|error| panic!("{name}: core failed: {error:?}")); + assert_eq!(disposition, expected, "{name}"); + assert_eq!( + returned.run.as_ref().map(|run| run.cursor), + cursor, + "{name}" + ); + assert_eq!( + returned.run.as_ref().map(Encode::encode), + Actors::actor_run_state(actor_id) + .as_ref() + .map(Encode::encode), + "{name}: returned authority must be the persisted successor, including terminal None", + ); + if name == "opening progress" { + successor_parts = Some(( + crate::ActorRunHeads::::get(actor_id).expect("real successor head"), + crate::ActorRunPayloads::::get(actor_id).expect("real successor payload"), + )); + } + polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback( + Ok::<(), DispatchError>(()), + ) + } + )); + assert_eq!( + polkadot_sdk::sp_io::storage::root(polkadot_sdk::sp_runtime::StateVersion::V1), + before + ); + if let Some((head, payload)) = successor_parts { + for head_only in [true, false] { + if head_only { + crate::ActorRunHeads::::insert(actor_id, head.clone()); + } else { + crate::ActorRunPayloads::::insert(actor_id, &payload); + } + assert!(Actors::actor_run_state(actor_id).is_none()); + let corrupt_root = + polkadot_sdk::sp_io::storage::root(polkadot_sdk::sp_runtime::StateVersion::V1); + let custody = ( + asset_balance(&sovereign, TestAsset::Local(1)), + asset_balance(&BOB, TestAsset::Local(1)), + ); + assert_ok!(polkadot_sdk::frame_support::storage::with_transaction( + || { + assert_ok!(Actors::paged_consume_head_at(ticket.ticket)); + let events = System::events(); + let before_core = + polkadot_sdk::sp_io::storage::root(polkadot_sdk::sp_runtime::StateVersion::V1); + let result = Actors::execute_loaded_single_step_core( + actor_id, + &instance, + corruption_plan.clone(), + now, + count as u32, + ); + assert!( + matches!( + result, + Err(crate::scheduler::AttemptTransactionError::Invariant) + ), + "Idle orphan partition must reject before effects; head_only={head_only}" + ); + assert_eq!( + polkadot_sdk::sp_io::storage::root(polkadot_sdk::sp_runtime::StateVersion::V1), + before_core + ); + assert_eq!(System::events(), events); + assert_eq!( + ( + asset_balance(&sovereign, TestAsset::Local(1)), + asset_balance(&BOB, TestAsset::Local(1)) + ), + custody + ); + polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Ok::< + (), + DispatchError, + >(())) + } + )); + assert_eq!( + polkadot_sdk::sp_io::storage::root(polkadot_sdk::sp_runtime::StateVersion::V1), + corrupt_root + ); + crate::ActorRunHeads::::remove(actor_id); + crate::ActorRunPayloads::::remove(actor_id); + } + } + #[cfg(feature = "try-runtime")] + assert_ok!(Actors::do_try_state()); + }); + } +} + +#[test] +fn loaded_cancellation_missing_run_is_idle_only_and_never_resurrects_backing() { + new_test_ext().execute_with(|| { + let actor_id = create_suspended_system_retry(1); + let mut state = Actors::active_actor_state(actor_id).expect("real suspended authority"); + let (_, cell) = Actors::actor_control_cell(actor_id).expect("canonical primary"); + let identity = state.identity.clone(); + let orphan_run = state.run_state.take().expect("Run remains persisted"); + let before = polkadot_sdk::sp_io::storage::root(polkadot_sdk::sp_runtime::StateVersion::V1); + assert_eq!(Actors::cancel_run_internal_loaded(actor_id, &identity, CancellationReason::Explicit, None, + crate::execution::LoadedCancellationContext::RetainedFrame { admission: cell.admission, state }), Err(Error::::ActorRunInvariant.into())); + assert_eq!(polkadot_sdk::sp_io::storage::root(polkadot_sdk::sp_runtime::StateVersion::V1), before); + + let idle_id = create_system_with(ALICE, manual_schedule(), None, BoundedVec::default()); + let idle = Actors::active_actor_state(idle_id).expect("real Idle authority"); + let (_, idle_cell) = Actors::actor_control_cell(idle_id).expect("Idle primary"); + assert!(idle.run_state.is_none()); + crate::ActorRunStateStore::::insert(idle_id, orphan_run); + System::reset_events(); + let before = polkadot_sdk::sp_io::storage::root(polkadot_sdk::sp_runtime::StateVersion::V1); + assert_eq!(Actors::cancel_run_internal_loaded(idle_id, &idle.identity, CancellationReason::Explicit, None, + crate::execution::LoadedCancellationContext::RetainedFrame { admission: idle_cell.admission.clone(), state: idle.clone() }), Ok(false)); + assert_eq!(polkadot_sdk::sp_io::storage::root(polkadot_sdk::sp_runtime::StateVersion::V1), before); + assert_ok!(Actors::finalize_actor_from_retained_state(idle_id, idle, &idle_cell.admission, CloseReason::OwnerInitiated)); + assert!(!crate::ActorRunStateStore::::contains_key(idle_id)); + assert!(!has_actor_event(|event| matches!(event, Event::CycleCancelled { actor_id: id, .. } | Event::CycleSummary { actor_id: id, .. } if *id == idle_id))); + #[cfg(feature = "try-runtime")] + assert_ok!(Actors::do_try_state()); + }); +} + +#[test] +fn loaded_cancellation_uses_supplied_run_despite_missing_or_stale_backing() { + for (consumed, waiting) in [(false, false), (true, false), (false, true)] { + for missing in [false, true] { + new_test_ext().execute_with(|| { + let actor_id = create_suspended_system_retry(1); + if waiting { + let eligible_at = Actors::actor_run_state(actor_id).expect("first retry").eligible_at; + System::set_block_number(eligible_at); + Actors::on_initialize(eligible_at); + run_prepass(); + Actors::on_idle(eligible_at, Weight::MAX); + } + let state = Actors::active_actor_state(actor_id).expect("real suspended authority"); + let (_, cell) = Actors::actor_control_cell(actor_id).expect("canonical primary"); + let identity = state.identity.clone(); + let supplied_run = state.run_state.clone().expect("supplied Run"); + assert_eq!(state.hot.cycle_state, CycleState::Suspended); + assert_eq!(state.hot.wakeup_pointer.is_some(), waiting); + if consumed { + assert_ok!(Actors::remove_primary_control_cell_inner(actor_id)); + } + if missing { + crate::ActorRunStateStore::::remove(actor_id); + } else { + let mut stale = supplied_run.clone(); + stale.cycle_nonce = stale.cycle_nonce.checked_add(7).unwrap(); + crate::ActorRunStateStore::::insert(actor_id, stale); + } + let context = if consumed { + crate::execution::LoadedCancellationContext::ConsumedFrame { admission: cell.admission, state } + } else { + crate::execution::LoadedCancellationContext::RetainedFrame { admission: cell.admission, state } + }; + System::reset_events(); + assert_eq!(Actors::cancel_run_internal_loaded(actor_id, &identity, CancellationReason::Explicit, None, context), Ok(true)); + assert!(Actors::actor_run_state(actor_id).is_none()); + let successor = Actors::active_actor_state(actor_id).expect("Idle successor is canonical"); + assert_eq!(successor.identity.cycle_nonce, supplied_run.cycle_nonce); + assert_eq!(successor.hot.cycle_state, CycleState::Idle); + assert!(successor.hot.wakeup_pointer.is_none()); + assert!(has_actor_event(|event| matches!(event, Event::CycleCancelled { actor_id: id, cycle_nonce, reason: CancellationReason::Explicit } if *id == actor_id && *cycle_nonce == supplied_run.cycle_nonce))); + assert!(has_actor_event(|event| matches!(event, Event::CycleSummary { actor_id: id, cycle_nonce, result: CycleResult::Cancelled, outcomes } if *id == actor_id && *cycle_nonce == supplied_run.cycle_nonce && *outcomes == supplied_run.cumulative_outcomes))); + #[cfg(feature = "try-runtime")] + assert_ok!(Actors::do_try_state()); + }); + } + } +} + +fn current_step_execution_weight(actor_id: ActorId) -> Weight { + let (_, _, step) = Actors::load_current_step_service_state(actor_id) + .expect("current Step has canonical resource authority"); + step.resources.control.saturating_add(step.resources.effect) +} + #[test] fn frozen_cycle_snapshot_dependency_is_closed_over_amounts_tasks_and_preconditions() { assert!(!AmountResolution::Fixed(1u64).requires_frozen_cycle_snapshot()); @@ -212,12 +706,9 @@ fn actor_run_schema_round_trips_retry_position_and_typed_snapshot_surfaces() { Some(SuspensionReason::FundingUnavailable) ); - ActorRunStateStore::::insert(actor_id, run_state); - ActorHot::::mutate(actor_id, |maybe| { - maybe.as_mut().expect("active actor").cycle_state = CycleState::Suspended; - }); + assert_ok!(Actors::write_run_state(actor_id, Some(run_state))); assert_eq!( - ActorIdentities::::get(actor_id) + Actors::actor_identity(actor_id) .expect("actor identity") .cycle_nonce, 0 @@ -308,7 +799,7 @@ fn normal_running_progress_persists_one_causal_successor_and_rejects_stale_autho 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), + Actors::actor_hot(actor_id).map(|hot| hot.cycle_state), Some(CycleState::Running) ); assert!(matches!( @@ -375,6 +866,29 @@ fn running_state_carries_the_exact_opening_predicate_cursor() { run.opening_predicate_results.as_slice(), [Ok(false), Ok(true)] ); + #[cfg(feature = "try-runtime")] + { + assert_ok!(Actors::do_try_state()); + for shifted_cursor in [0, 2] { + ActorRunStateStore::::mutate(actor_id, |maybe| { + maybe + .as_mut() + .expect("Running authority") + .opening_predicate_cursor = shifted_cursor; + }); + assert!( + Actors::do_try_state().is_err(), + "shifted Opening cursor {shifted_cursor} was accepted" + ); + } + ActorRunStateStore::::mutate(actor_id, |maybe| { + maybe + .as_mut() + .expect("Running authority") + .opening_predicate_cursor = 1; + }); + assert_ok!(Actors::do_try_state()); + } frame_system::Pallet::::set_block_number(2); Actors::on_initialize(2); @@ -403,7 +917,7 @@ fn signal_during_running_is_retained_for_a_later_cycle() { )); Actors::on_idle(1, Weight::MAX); assert_eq!( - ActorHot::::get(actor_id).map(|hot| hot.cycle_state), + Actors::actor_hot(actor_id).map(|hot| hot.cycle_state), Some(CycleState::Running) ); assert!(!Actors::pending_signal(actor_id)); @@ -415,7 +929,7 @@ fn signal_during_running_is_retained_for_a_later_cycle() { assert!(Actors::pending_signal(actor_id)); Actors::on_idle(1, Weight::MAX); assert_eq!( - ActorHot::::get(actor_id).map(|hot| hot.cycle_state), + Actors::actor_hot(actor_id).map(|hot| hot.cycle_state), Some(CycleState::Running), "the successor cannot execute in the commit block" ); @@ -451,40 +965,30 @@ fn signal_during_running_is_retained_for_a_later_cycle() { #[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); - ActorRunStateStore::::insert( - actor_id, - 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), - }, + let actor_id = create_suspended_system_retry(1); + let due = Actors::actor_run_state(actor_id) + .expect("coherent Actor run exists") + .eligible_at; + frame_system::Pallet::::set_block_number(due); + let mut meter = WeightMeter::with_limit(Weight::MAX); + Actors::drain_overdue_wakeups_cursor(due, &mut meter); + assert_eq!( + Actors::paged_head_entry().map(|(_, entry)| entry.actor_id), + Some(actor_id) ); - ActorHot::::mutate(actor_id, |maybe| { - maybe.as_mut().expect("actor exists").cycle_state = CycleState::Suspended; - }); - 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(); + let root_before = polkadot_sdk::sp_io::storage::root(StateVersion::V1); - let _ = Actors::execute_single_cycle(actor_id, instance, 1); + Actors::execute_cycle_to_cutoff(Weight::MAX, Actors::queue_tail()); assert_eq!(Actors::actor_hot(actor_id), Some(hot_before)); assert!(System::events().is_empty()); + assert_eq!( + polkadot_sdk::sp_io::storage::root(StateVersion::V1), + root_before + ); }); } @@ -515,9 +1019,8 @@ fn actor_run_try_state_rejects_marker_and_cursor_drift() { }, ); assert!(crate::Pallet::::do_try_state().is_err()); - ActorHot::::mutate(actor_id, |maybe| { - maybe.as_mut().expect("actor exists").cycle_state = CycleState::Suspended; - }); + let run = ActorRunStateStore::::take(actor_id).expect("orphan Run is retained"); + assert_ok!(Actors::write_run_state(actor_id, Some(run))); assert_ok!(crate::Pallet::::do_try_state()); ActorRunStateStore::::mutate(actor_id, |maybe| { maybe.as_mut().expect("Actor run exists").suspension = Some(SuspensionReason::Temporary); @@ -585,37 +1088,11 @@ fn user_dormant_activation_installs_process_without_service_prefunding() { }); } -#[test] -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 = ::run_suspend(); assert_eq!(suspend, Weight::from_parts(28_668_868, 4_178)); - assert_eq!( - ::run_retry(), - Weight::from_parts(22_070_000, 4_266) - ); assert_eq!( ::run_complete(), Weight::from_parts(18_019_000, 4_030) @@ -624,17 +1101,11 @@ fn generated_run_weights_cover_distinct_storage_paths() { ::run_cancel(), Weight::from_parts(56_782_000, 8_120) ); - 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()); - assert_eq!(suffix_min.proof_size(), suffix_max.proof_size()); }); } #[test] -fn generated_predicate_weight_scales_and_chunks_opening_plus_step_visits() { +fn generated_predicate_weight_scales_with_bounded_step_visits() { use crate::WeightInfo; let zero = ::WeightInfo::predicate_set_evaluation(0); @@ -644,10 +1115,6 @@ fn generated_predicate_weight_scales_and_chunks_opening_plus_step_visits() { assert!(one.ref_time() > 0 && one.proof_size() > 0); assert!(maximum.ref_time() >= one.ref_time()); assert!(maximum.proof_size() >= one.proof_size()); - let doubled = Actors::predicate_evaluation_weight(8); - assert_eq!(doubled, maximum.saturating_mul(2)); - assert!(doubled.ref_time() > maximum.ref_time()); - assert!(doubled.proof_size() > maximum.proof_size()); assert!(maximum.ref_time() >= 41_068_000); assert!(maximum.proof_size() >= 9_045); } @@ -764,69 +1231,257 @@ fn authored_abort_preserves_earlier_provisional_task_commit() { }); } +#[cfg(not(feature = "runtime-benchmarks"))] #[test] -fn run_control_placement_failures_roll_back_exactly() { +fn frame_only_abort_cycle_removes_transaction_local_scalar_bridges() { new_test_ext().execute_with(|| { - let actor_id = create_suspended_system_retry(1); + frame_system::Pallet::::set_block_number(1); + setup_temporary_retry_pool(); + let plan = contract_steps_with_step(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: StepErrorPolicy::AbortCycle, + }); + let actor_id = create_system_with(ALICE, manual_schedule(), None, plan); + fund_native(actor_id, 100); + set_temporary_dex_failure(true); + assert_ok!(Actors::manual_trigger( RuntimeOrigin::signed(ALICE), actor_id )); - crate::NextQueueTicket::::put(u64::MAX); - let root_before = polkadot_sdk::sp_io::storage::root(StateVersion::V1); - let events_before = System::events(); + run_idle(Weight::MAX); - assert_noop!( - update_contract_partial!( - RuntimeOrigin::root(), - actor_id, - FundingSourcePolicy::AnyVerifiedIngress, - ), - Error::::QueueTicketExhausted - ); - 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), - root_before - ); + assert!(Actors::actor_run_state(actor_id).is_none()); + assert!(has_actor_event(|event| matches!( + event, + Event::CycleSummary { + actor_id: id, + result: CycleResult::Failed, + .. + } if *id == actor_id + ))); + assert!(!ActorIdentities::::contains_key(actor_id)); + assert!(Actors::load_frame_control_authority(actor_id).is_some()); + #[cfg(feature = "try-runtime")] + assert_ok!(crate::Pallet::::do_try_state()); }); +} +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn frame_only_abort_cycle_closes_at_failure_threshold_without_scalar_hot() { new_test_ext().execute_with(|| { - let actor_id = create_suspended_system_retry(1); + frame_system::Pallet::::set_block_number(1); + setup_temporary_retry_pool(); + let plan = contract_steps_with_step(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: StepErrorPolicy::AbortCycle, + }); + let actor_id = create_system_with(ALICE, manual_schedule(), None, plan); + fund_native(actor_id, 100); + let threshold = ::MaxConsecutiveFailures::get(); + mutate_actor_hot_coherent(actor_id, |hot| { + hot.unsuccessful_attempt_streak = threshold.saturating_sub(1); + }); assert_ok!(Actors::manual_trigger( RuntimeOrigin::signed(ALICE), actor_id )); - crate::NextQueueTicket::::put(u64::MAX); - let root_before = polkadot_sdk::sp_io::storage::root(StateVersion::V1); - let events_before = System::events(); + set_temporary_dex_failure(true); - assert_noop!( - update_contract_partial!( - RuntimeOrigin::root(), - actor_id, - inert_contract_steps(), - crate::CompletionPolicy::Persistent, - ), - Error::::QueueTicketExhausted - ); - 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), - root_before - ); + Actors::on_idle(1, Weight::MAX); + + assert!(Actors::actor_run_state(actor_id).is_none()); + assert!(!crate::ActorControlLocators::::contains_key(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::ConsecutiveFailures, + } if *id == actor_id + ))); + #[cfg(feature = "try-runtime")] + assert_ok!(crate::Pallet::::do_try_state()); }); +} +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn frame_only_running_abort_cycle_failure_never_materializes_scalar_hot() { new_test_ext().execute_with(|| { - let actor_id = create_suspended_system_retry(1); - assert_ok!(Actors::manual_trigger( - RuntimeOrigin::signed(ALICE), - actor_id - )); - crate::NextQueueTicket::::put(u64::MAX); - let root_before = polkadot_sdk::sp_io::storage::root(StateVersion::V1); + frame_system::Pallet::::set_block_number(1); + setup_temporary_retry_pool(); + let steps = BoundedVec::try_from(vec![ + make_step(Task::Transfer { + to: BOB, + asset: TestAsset::Native, + amount: AmountResolution::Fixed(10), + }), + 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: StepErrorPolicy::AbortCycle, + }, + make_step(Task::StopCycle), + ]) + .expect("three Steps fit"); + let actor_id = create_system_with(ALICE, manual_schedule(), None, steps); + fund_native(actor_id, 100); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + + Actors::on_idle(1, Weight::MAX); + assert_eq!( + Actors::actor_run_state(actor_id) + .expect("Running prefix") + .cursor, + 1 + ); + set_temporary_dex_failure(true); + frame_system::Pallet::::set_block_number(2); + Actors::on_initialize(2); + run_prepass(); + Actors::on_idle(2, Weight::MAX); + + assert!(Actors::actor_run_state(actor_id).is_none()); + let authority = Actors::load_frame_control_authority(actor_id).expect("Idle authority"); + assert_eq!(authority.2.cycle_state, CycleState::Idle); + assert_eq!(authority.2.unsuccessful_attempt_streak, 1); + assert!(matches!( + crate::ActorControlLocators::::get(actor_id), + Some(crate::ActorControlLocation::Unsignaled) + )); + assert!(!ActorIdentities::::contains_key(actor_id)); + assert!(has_actor_event(|event| matches!( + event, + Event::CycleSummary { + actor_id: id, + result: CycleResult::Failed, + outcomes: OutcomeTotals { executed_steps: 1, failed_steps: 1, .. }, + .. + } if *id == actor_id + ))); + assert!(!has_actor_event(|event| matches!( + event, + Event::CycleStopped { actor_id: id, .. } if *id == actor_id + ))); + #[cfg(feature = "try-runtime")] + assert_ok!(crate::Pallet::::do_try_state()); + }); +} + +#[test] +fn run_control_placement_failures_roll_back_exactly() { + fn exhaust_ticket_namespace(actor_id: ActorId) { + let (location, cell) = Actors::actor_control_cell(actor_id).expect("suspended primary"); + let crate::ActorControlLocation::Ready { ticket } = location else { + panic!("retry fixture retains its Ready ticket"); + }; + assert_eq!(Actors::queue_occupancy(), 1); + let old_page = + crate::ActorReadyFrameChunks::::take(ticket / 32).expect("sole Ready page"); + assert_eq!(old_page.iter().flatten().count(), 1); + let final_ticket = u64::MAX - 1; + let mut final_page = old_page; + *final_page + .get_mut((ticket % 32) as usize) + .expect("old slot") = None; + *final_page + .get_mut((final_ticket % 32) as usize) + .expect("final slot") = Some(cell); + crate::ActorReadyFrameChunks::::insert(final_ticket / 32, final_page); + crate::ActorControlLocators::::insert( + actor_id, + crate::ActorControlLocation::Ready { + ticket: final_ticket, + }, + ); + crate::ActorReadyHead::::put(final_ticket); + crate::ActorReadyTail::::put(u64::MAX); + assert!(Actors::active_actor_view(actor_id).is_some()); + } + + new_test_ext().execute_with(|| { + let actor_id = create_suspended_system_retry(1); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + exhaust_ticket_namespace(actor_id); + let root_before = polkadot_sdk::sp_io::storage::root(StateVersion::V1); + let events_before = System::events(); + + assert_noop!( + update_contract_partial!( + RuntimeOrigin::root(), + actor_id, + FundingSourcePolicy::AnyVerifiedIngress, + ), + Error::::QueueTicketExhausted + ); + 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), + root_before + ); + }); + + new_test_ext().execute_with(|| { + let actor_id = create_suspended_system_retry(1); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + exhaust_ticket_namespace(actor_id); + let root_before = polkadot_sdk::sp_io::storage::root(StateVersion::V1); + let events_before = System::events(); + + assert_noop!( + update_contract_partial!( + RuntimeOrigin::root(), + actor_id, + inert_contract_steps(), + crate::CompletionPolicy::Persistent, + ), + Error::::QueueTicketExhausted + ); + 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), + root_before + ); + }); + + new_test_ext().execute_with(|| { + let actor_id = create_suspended_system_retry(1); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + exhaust_ticket_namespace(actor_id); + let root_before = polkadot_sdk::sp_io::storage::root(StateVersion::V1); let events_before = System::events(); assert_noop!( @@ -884,12 +1539,7 @@ fn completed_failed_and_suspended_attempts_update_failure_streak_once() { 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()); - ActorHot::::mutate(actor_id, |maybe| { - maybe - .as_mut() - .expect("active actor") - .unsuccessful_attempt_streak = 1; - }); + mutate_actor_hot_coherent(actor_id, |hot| hot.unsuccessful_attempt_streak = 1); assert_ok!(Actors::manual_trigger( RuntimeOrigin::signed(ALICE), actor_id @@ -1274,125 +1924,508 @@ fn retry_later_aborts_permanent_failure_without_executing_suffix() { }); } +#[cfg(not(feature = "runtime-benchmarks"))] #[test] -fn retry_later_resumes_same_cursor_without_replaying_committed_prefix() { +fn frame_only_running_swap_retry_and_resume_never_materialize_scalar_hot() { 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 retry_step = 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, - }; - let contract_steps = BoundedVec::try_from(vec![ + let steps = BoundedVec::try_from(vec![ make_step(Task::Transfer { to: BOB, asset: TestAsset::Native, amount: AmountResolution::Fixed(10), }), - retry_step, - make_step(Task::Transfer { - to: CHARLIE, - asset: TestAsset::Native, - amount: AmountResolution::Fixed(5), - }), + 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, + }, + make_step(Task::StopCycle), ]) - .expect("three steps fit"); - let actor_id = create_system_with(ALICE, manual_schedule(), None, contract_steps); + .expect("three Steps fit"); + let actor_id = create_system_with(ALICE, manual_schedule(), None, steps); fund_native(actor_id, 100); - let bob_before = native_balance(&BOB); - let charlie_before = native_balance(&CHARLIE); - set_temporary_dex_failure(true); - assert_ok!(Actors::manual_trigger( RuntimeOrigin::signed(ALICE), actor_id )); - run_idle(Weight::MAX); - let first = Actors::active_actor_view(actor_id).expect("suspended actor remains"); - 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, 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, - .. - })) - )); + Actors::on_idle(1, Weight::MAX); assert_eq!( - first_continuation.suspension, - Some(SuspensionReason::Temporary) + Actors::actor_run_state(actor_id) + .expect("Running prefix") + .cursor, + 1 ); - 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); - assert_eq!(native_balance(&CHARLIE), charlie_before); - assert_eq!(native_balance(&first.sovereign_account), 90); - #[cfg(feature = "try-runtime")] - assert_ok!(crate::Pallet::::do_try_state()); - assert!(!has_actor_event(|event| { - matches!(event, Event::CycleSummary { actor_id: id, .. } if *id == actor_id) - })); - - frame_system::Pallet::::set_block_number(first_continuation.eligible_at); - run_idle(Weight::MAX); - 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(true); + frame_system::Pallet::::set_block_number(2); + Actors::on_initialize(2); + run_prepass(); + Actors::on_idle(2, Weight::MAX); + let retry = Actors::actor_run_state(actor_id).expect("Running failure suspends"); + assert_eq!(retry.cursor, 1); + assert_eq!(retry.eligible_at, 3); set_temporary_dex_failure(false); - frame_system::Pallet::::set_block_number(second_continuation.eligible_at); - Actors::on_initialize(second_continuation.eligible_at); + frame_system::Pallet::::set_block_number(3); + Actors::on_initialize(3); run_prepass(); - run_idle(Weight::MAX); - run_next_idle(Weight::MAX); + Actors::on_idle(3, Weight::MAX); + assert_eq!( + Actors::actor_run_state(actor_id) + .expect("resumed continuation") + .cursor, + 2 + ); + frame_system::Pallet::::set_block_number(4); + Actors::on_initialize(4); + run_prepass(); + Actors::on_idle(4, 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::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() - .iter() - .filter(|record| { - matches!( - record.event, - RuntimeEvent::Actors(Event::CycleStarted { actor_id: id, .. }) if id == actor_id - ) - }) - .count(); - assert_eq!(starts, 1); - assert!(has_actor_event(|event| { - matches!( - event, - Event::CycleSummary { - actor_id: id, - cycle_nonce: 1, - outcomes: OutcomeTotals { executed_steps: 3, failed_steps: 2, .. }, + assert!(matches!( + crate::ActorControlLocators::::get(actor_id), + Some(crate::ActorControlLocation::Unsignaled) + )); + assert!(!ActorIdentities::::contains_key(actor_id)); + assert!(has_actor_event(|event| matches!( + event, + Event::CycleStopped { + actor_id: id, + cycle_nonce: 1, + step_index: 2, + } if *id == actor_id + ))); + #[cfg(feature = "try-runtime")] + assert_ok!(crate::Pallet::::do_try_state()); + }); +} + +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn frame_only_opening_continue_next_step_failure_never_materializes_scalar_hot() { + 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 steps = 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: StepErrorPolicy::ContinueNextStep, + }, + make_step(Task::StopCycle), + ]) + .expect("two Steps fit"); + let actor_id = create_system_with(ALICE, manual_schedule(), None, steps); + fund_native(actor_id, 100); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + set_temporary_dex_failure(true); + + Actors::on_idle(1, Weight::MAX); + let continuation = Actors::actor_run_state(actor_id).expect("failure advances cursor"); + assert_eq!(continuation.cursor, 1); + assert!(continuation.suspension.is_none()); + assert_eq!(continuation.cumulative_outcomes.failed_steps, 1); + + frame_system::Pallet::::set_block_number(2); + Actors::on_initialize(2); + run_prepass(); + Actors::on_idle(2, Weight::MAX); + + assert!(Actors::actor_run_state(actor_id).is_none()); + assert!(matches!( + crate::ActorControlLocators::::get(actor_id), + Some(crate::ActorControlLocation::Unsignaled) + )); + assert!(!ActorIdentities::::contains_key(actor_id)); + assert!(has_actor_event(|event| matches!( + event, + Event::CycleSummary { + actor_id: id, + outcomes: OutcomeTotals { failed_steps: 1, .. }, + .. + } if *id == actor_id + ))); + #[cfg(feature = "try-runtime")] + assert_ok!(crate::Pallet::::do_try_state()); + }); +} + +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn frame_only_stake_continue_next_step_rolls_back_without_scalar_hot() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let asset = TestAsset::Native; + let steps = BoundedVec::try_from(vec![ + StepOf:: { + precondition: None, + task: Task::Stake { + asset, + amount: AmountResolution::Fixed(40), + }, + on_error: StepErrorPolicy::ContinueNextStep, + }, + make_step(Task::StopCycle), + ]) + .expect("two Steps fit"); + let actor_id = create_system_with(ALICE, manual_schedule(), None, steps); + let actor = sovereign_account(actor_id); + fund_native(actor_id, 120); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + set_fail_staking_after_burn(true); + + Actors::on_idle(1, Weight::MAX); + let continuation = Actors::actor_run_state(actor_id).expect("failure advances cursor"); + assert_eq!(continuation.cursor, 1); + assert_eq!(continuation.cumulative_outcomes.failed_steps, 1); + assert_eq!(native_balance(&actor), 120); + assert_eq!(staked_balance(actor, asset), 0); + + frame_system::Pallet::::set_block_number(2); + Actors::on_initialize(2); + run_prepass(); + Actors::on_idle(2, Weight::MAX); + + assert!(Actors::actor_run_state(actor_id).is_none()); + assert!(!ActorIdentities::::contains_key(actor_id)); + assert!(!has_actor_event(|event| matches!( + event, + Event::StakeExecuted { actor_id: id, .. } if *id == actor_id + ))); + assert!(has_actor_event(|event| matches!( + event, + Event::CycleSummary { + actor_id: id, + outcomes: OutcomeTotals { failed_steps: 1, .. }, + .. + } if *id == actor_id + ))); + #[cfg(feature = "try-runtime")] + assert_ok!(crate::Pallet::::do_try_state()); + }); +} + +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn frame_only_stake_retry_later_permanent_failure_terminates_without_scalar_hot() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let asset = TestAsset::Native; + let plan = contract_steps_with_step(StepOf:: { + precondition: None, + task: Task::Stake { + asset, + amount: AmountResolution::Fixed(40), + }, + on_error: RETRY_LATER, + }); + let actor_id = create_system_with(ALICE, manual_schedule(), None, plan); + let actor = sovereign_account(actor_id); + fund_native(actor_id, 120); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + set_fail_staking_after_burn(true); + + Actors::on_idle(1, Weight::MAX); + + assert!(Actors::actor_run_state(actor_id).is_none()); + assert_eq!(native_balance(&actor), 120); + assert_eq!(staked_balance(actor, asset), 0); + let authority = Actors::load_frame_control_authority(actor_id).expect("Idle authority"); + assert_eq!(authority.2.cycle_state, CycleState::Idle); + assert_eq!(authority.2.unsuccessful_attempt_streak, 1); + assert!(has_actor_event(|event| matches!( + event, + Event::CycleSummary { + actor_id: id, + result: CycleResult::Failed, + outcomes: OutcomeTotals { failed_steps: 1, .. }, + .. + } if *id == actor_id + ))); + assert!(!ActorIdentities::::contains_key(actor_id)); + #[cfg(feature = "try-runtime")] + assert_ok!(crate::Pallet::::do_try_state()); + }); +} + +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn frame_only_retry_attempt_exhaustion_closes_from_consumed_authority() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + setup_temporary_retry_pool(); + let plan = contract_steps_with_step(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: StepErrorPolicy::RetryLater { max_attempts: 2 }, + }); + let actor_id = create_system_with(ALICE, manual_schedule(), None, plan); + fund_native(actor_id, 100); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + set_temporary_dex_failure(true); + + Actors::on_idle(1, Weight::MAX); + assert!(matches!( + Actors::active_actor_view(actor_id).map(|instance| instance.cycle_state), + Some(CycleState::Suspended) + )); + frame_system::Pallet::::set_block_number(2); + Actors::on_initialize(2); + run_prepass(); + Actors::on_idle(2, Weight::MAX); + + assert!(Actors::actor_run_state(actor_id).is_none()); + assert!(!crate::ActorControlLocators::::contains_key(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::RetryAttemptsExhausted, + } if *id == actor_id + ))); + #[cfg(feature = "try-runtime")] + assert_ok!(crate::Pallet::::do_try_state()); + }); +} + +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn frame_only_running_continue_next_step_failure_never_materializes_scalar_hot() { + 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 steps = BoundedVec::try_from(vec![ + make_step(Task::Transfer { + to: BOB, + asset: TestAsset::Native, + amount: AmountResolution::Fixed(10), + }), + 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: StepErrorPolicy::ContinueNextStep, + }, + make_step(Task::StopCycle), + ]) + .expect("three Steps fit"); + let actor_id = create_system_with(ALICE, manual_schedule(), None, steps); + fund_native(actor_id, 100); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + + Actors::on_idle(1, Weight::MAX); + assert_eq!( + Actors::actor_run_state(actor_id) + .expect("Running prefix") + .cursor, + 1 + ); + set_temporary_dex_failure(true); + frame_system::Pallet::::set_block_number(2); + Actors::on_initialize(2); + run_prepass(); + Actors::on_idle(2, Weight::MAX); + let continuation = Actors::actor_run_state(actor_id).expect("failure advances cursor"); + assert_eq!(continuation.cursor, 2); + assert!(continuation.suspension.is_none()); + assert_eq!(continuation.cumulative_outcomes.failed_steps, 1); + + frame_system::Pallet::::set_block_number(3); + Actors::on_initialize(3); + run_prepass(); + Actors::on_idle(3, Weight::MAX); + + assert!(Actors::actor_run_state(actor_id).is_none()); + assert!(matches!( + crate::ActorControlLocators::::get(actor_id), + Some(crate::ActorControlLocation::Unsignaled) + )); + assert!(!ActorIdentities::::contains_key(actor_id)); + assert!(has_actor_event(|event| matches!( + event, + Event::CycleSummary { + actor_id: id, + outcomes: OutcomeTotals { failed_steps: 1, .. }, + .. + } if *id == actor_id + ))); + #[cfg(feature = "try-runtime")] + assert_ok!(crate::Pallet::::do_try_state()); + }); +} + +#[test] +fn retry_later_resumes_same_cursor_without_replaying_committed_prefix() { + 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 retry_step = 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, + }; + let contract_steps = BoundedVec::try_from(vec![ + make_step(Task::Transfer { + to: BOB, + asset: TestAsset::Native, + amount: AmountResolution::Fixed(10), + }), + retry_step, + make_step(Task::Transfer { + to: CHARLIE, + asset: TestAsset::Native, + amount: AmountResolution::Fixed(5), + }), + ]) + .expect("three steps fit"); + let actor_id = create_system_with(ALICE, manual_schedule(), None, contract_steps); + fund_native(actor_id, 100); + let bob_before = native_balance(&BOB); + let charlie_before = native_balance(&CHARLIE); + set_temporary_dex_failure(true); + + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + run_idle(Weight::MAX); + + let first = Actors::active_actor_view(actor_id).expect("suspended actor remains"); + 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, 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!(crate::ActorRunHeads::::contains_key(actor_id)); + assert!(crate::ActorRunPayloads::::contains_key(actor_id)); + assert!(first.queue_ticket.is_some()); + assert!(first.wakeup_pointer.is_none()); + assert_eq!(native_balance(&BOB), bob_before + 10); + assert_eq!(native_balance(&CHARLIE), charlie_before); + assert_eq!(native_balance(&first.sovereign_account), 90); + #[cfg(feature = "try-runtime")] + assert_ok!(crate::Pallet::::do_try_state()); + assert!(!has_actor_event(|event| { + matches!(event, Event::CycleSummary { actor_id: id, .. } if *id == actor_id) + })); + + frame_system::Pallet::::set_block_number(first_continuation.eligible_at); + run_idle(Weight::MAX); + 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); + let second = Actors::active_actor_view(actor_id).expect("long retry Actor remains"); + assert!(crate::ActorRunHeads::::contains_key(actor_id)); + assert!(crate::ActorRunPayloads::::contains_key(actor_id)); + assert!(second.queue_ticket.is_none()); + assert!(second.wakeup_pointer.is_some()); + 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(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::actor_run_state(actor_id).is_none()); + assert!(!crate::ActorRunHeads::::contains_key(actor_id)); + assert!(!crate::ActorRunPayloads::::contains_key(actor_id)); + assert!(completed.queue_ticket.is_none()); + assert!(completed.wakeup_pointer.is_none()); + assert_eq!(native_balance(&BOB), bob_before + 10); + assert_eq!(native_balance(&CHARLIE), charlie_before + 5); + let starts = frame_system::Pallet::::events() + .iter() + .filter(|record| { + matches!( + record.event, + RuntimeEvent::Actors(Event::CycleStarted { actor_id: id, .. }) if id == actor_id + ) + }) + .count(); + assert_eq!(starts, 1); + assert!(has_actor_event(|event| { + matches!( + event, + Event::CycleSummary { + actor_id: id, + cycle_nonce: 1, + outcomes: OutcomeTotals { executed_steps: 3, failed_steps: 2, .. }, .. } if *id == actor_id ) @@ -1581,6 +2614,81 @@ fn close_after_productive_cycle_ignores_false_cycles_then_closes_immutable_actor }); } +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn frame_only_productive_completion_closes_without_scalar_hot() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = Actors::next_actor_id(); + assert_ok!(Actors::create_system_actor( + RuntimeOrigin::root(), + ALICE, + Mutability::Mutable, + system_active_contract_with_completion( + manual_schedule(), + None, + contract_steps_with_step(make_step(Task::Transfer { + to: BOB, + asset: TestAsset::Native, + amount: AmountResolution::Fixed(10), + })), + crate::CompletionPolicy::CloseAfterProductiveCycle, + ), + )); + fund_native(actor_id, 100); + assert_ok!(Actors::manual_trigger(RuntimeOrigin::root(), actor_id)); + + Actors::on_idle(1, Weight::MAX); + + assert!(Actors::active_actor_view(actor_id).is_none()); + assert!(!crate::ActorControlLocators::::contains_key(actor_id)); + assert!(has_actor_event(|event| matches!( + event, + Event::ActorClosed { + actor_id: id, + reason: CloseReason::ProductiveCycleCompleted, + } if *id == actor_id + ))); + #[cfg(feature = "try-runtime")] + assert_ok!(crate::Pallet::::do_try_state()); + }); +} + +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn frame_only_effectful_auto_close_uses_terminal_nonce_without_scalar_hot() { + 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(make_step(Task::Transfer { + to: BOB, + asset: TestAsset::Native, + amount: AmountResolution::Fixed(10), + })), + ); + fund_native(actor_id, 100); + assert_ok!(replace_auto_close(RuntimeOrigin::root(), actor_id, Some(1),)); + assert_ok!(Actors::manual_trigger(RuntimeOrigin::root(), actor_id)); + + Actors::on_idle(1, Weight::MAX); + + assert!(Actors::active_actor_view(actor_id).is_none()); + assert!(!crate::ActorControlLocators::::contains_key(actor_id)); + assert!(has_actor_event(|event| matches!( + event, + Event::ActorClosed { + actor_id: id, + reason: CloseReason::AutoCloseNonceReached, + } if *id == actor_id + ))); + #[cfg(feature = "try-runtime")] + assert_ok!(crate::Pallet::::do_try_state()); + }); +} + #[test] fn close_after_productive_cycle_does_not_treat_stop_cycle_as_effectful() { new_test_ext().execute_with(|| { @@ -1728,14 +2836,18 @@ fn close_after_productive_cycle_keeps_retry_exhaustion_as_failure_terminal() { #[test] fn stop_cycle_runs_normal_auto_close_after_the_summary() { - new_test_ext().execute_with(|| { + for step_count in [1usize, 5, 9, 12] { + 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(make_step(Task::StopCycle)), + BoundedVec::try_from(vec![make_step(Task::StopCycle); step_count]) + .expect("Contract fits the twelve-Step host bound"), ); + let tail_count = (step_count - 1).div_ceil(4); + assert_eq!(crate::ActorContractTailChunks::::iter_prefix(actor_id).count(), tail_count); assert_ok!(replace_auto_close( RuntimeOrigin::root(), actor_id, @@ -1743,10 +2855,12 @@ fn stop_cycle_runs_normal_auto_close_after_the_summary() { )); assert_ok!(Actors::manual_trigger(RuntimeOrigin::signed(ALICE), actor_id)); frame_system::Pallet::::reset_events(); - run_idle(Weight::MAX); assert!(Actors::active_actor_view(actor_id).is_none()); + assert!(!crate::ActorControlLocators::::contains_key(actor_id)); + assert!(!crate::ActorContractHeads::::contains_key(actor_id)); + assert_eq!(crate::ActorContractTailChunks::::iter_prefix(actor_id).count(), 0); let events: Vec<_> = System::events() .into_iter() .filter_map(|record| match record.event { @@ -1772,7 +2886,10 @@ fn stop_cycle_runs_normal_auto_close_after_the_summary() { Some(crate::StepControlOutcome::Completed), "auto-close retains completed control ownership while cleanup is metered separately", ); - }); + #[cfg(feature = "try-runtime")] + assert_ok!(Actors::do_try_state()); + }); + } } #[test] @@ -1829,10 +2946,24 @@ fn continuation_can_complete_at_stop_cycle_without_replaying_prefix() { frame_system::Pallet::::set_block_number(eligible_at); Actors::on_initialize(eligible_at); run_prepass(); - run_idle(Weight::MAX); + Actors::on_idle(eligible_at, Weight::MAX); + #[cfg(not(feature = "runtime-benchmarks"))] + { + let locator = crate::ActorControlLocators::::get(actor_id); + assert!( + matches!(locator, Some(crate::ActorControlLocation::Ready { .. })), + "successor locator: {locator:?}" + ); + } run_next_idle(Weight::MAX); - let actor = Actors::active_actor_view(actor_id).expect("actor remains active"); + let crate::LoadedActorStateOf::Active(state) = + Actors::load_actor_state_for_frame_control(actor_id) + else { + panic!("frame-owned actor remains active"); + }; + let actor = Actors::derive_active_actor_view(state.identity, state.hot, state.contract); + #[cfg(not(feature = "runtime-benchmarks"))] assert_eq!(actor.cycle_state, CycleState::Idle); assert_eq!(actor.unsuccessful_attempt_streak, 0); assert!(Actors::actor_run_state(actor_id).is_none()); @@ -2122,11 +3253,8 @@ fn reached_global_failure_cutoff_closes_before_another_attempt() { let actor_id = create_system_with(ALICE, manual_schedule(), None, temporary_retry_swap_plan()); fund_native(actor_id, 100); let threshold = ::MaxConsecutiveFailures::get(); - ActorHot::::mutate(actor_id, |maybe| { - maybe - .as_mut() - .expect("active actor") - .unsuccessful_attempt_streak = threshold; + mutate_actor_hot_coherent(actor_id, |hot| { + hot.unsuccessful_attempt_streak = threshold; }); assert_ok!(Actors::manual_trigger( RuntimeOrigin::signed(ALICE), @@ -2148,10 +3276,10 @@ fn reached_global_failure_cutoff_closes_before_another_attempt() { } #[test] -fn global_failure_limit_can_close_before_local_retry_limit() { +fn retry_local_limit_precedes_simultaneous_global_failure_limit() { new_test_ext().execute_with(|| { frame_system::Pallet::::set_block_number(1); - set_max_consecutive_failures(1); + set_max_consecutive_failures(2); setup_temporary_retry_pool(); let plan = contract_steps_with_step(StepOf:: { precondition: None, @@ -2173,12 +3301,19 @@ fn global_failure_limit_can_close_before_local_retry_limit() { )); run_idle(Weight::MAX); + let run = Actors::actor_run_state(actor_id).expect("first admitted failure suspends"); + assert_eq!(run.unsuccessful_attempts_at_cursor, 1); + frame_system::Pallet::::set_block_number(run.eligible_at); + Actors::on_initialize(run.eligible_at); + run_prepass(); + 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::ConsecutiveFailures, + reason: CloseReason::RetryAttemptsExhausted, } if *id == actor_id ))); }); @@ -2283,7 +3418,6 @@ fn run_weight_deferral_does_not_admit_attempt() { .queue_ticket; frame_system::Pallet::::set_block_number(2); - let instance = Actors::active_actor_view(actor_id).expect("suspended actor"); let queue_weight = <::WeightInfo as crate::WeightInfo>::scheduler_paged_tombstone_drain(1) .saturating_add(Actors::scheduler_actor_probe_weight_upper()) .saturating_add( @@ -2292,7 +3426,7 @@ fn run_weight_deferral_does_not_admit_attempt() { ); let proof_limit = queue_weight .proof_size() - .saturating_add(Actors::attempt_weight_upper_bound(&instance, 1).proof_size()) + .saturating_add(current_step_execution_weight(actor_id).proof_size()) .saturating_sub(1); Actors::execute_cycle(Weight::from_parts(u64::MAX, proof_limit)); @@ -2309,16 +3443,228 @@ fn run_weight_deferral_does_not_admit_attempt() { 1 ); assert_eq!( - Actors::actor_hot(actor_id) - .expect("suspended actor") - .queue_ticket, - ticket_before + Actors::actor_hot(actor_id) + .expect("suspended actor") + .queue_ticket, + ticket_before + ); + assert!(!has_actor_event(|event| matches!( + event, + Event::CycleContinued { actor_id: id, .. } | Event::CycleSummary { actor_id: id, .. } + if *id == actor_id + ))); + }); +} + +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn frame_only_exact_next_retry_completes_without_scalar_rehydration() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + setup_temporary_retry_pool(); + let actor_id = create_system_with( + ALICE, + Schedule { + trigger: Trigger::manual(), + cooldown_blocks: 1, + }, + None, + temporary_retry_swap_plan(), + ); + fund_native(actor_id, 100); + set_temporary_dex_failure(true); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + + Actors::on_idle(1, Weight::MAX); + let suspended = Actors::actor_run_state(actor_id).expect("suspended Run survives"); + assert_eq!(suspended.cursor, 0); + assert_eq!(suspended.eligible_at, 2); + assert_eq!(canonical_scheduled_wakeup_block(actor_id), None); + assert!(matches!( + crate::ActorControlLocators::::get(actor_id), + Some(crate::ActorControlLocation::Ready { .. }) + )); + assert!(!ActorIdentities::::contains_key(actor_id)); + + set_temporary_dex_failure(false); + frame_system::Pallet::::set_block_number(2); + Actors::on_initialize(2); + run_prepass(); + Actors::on_idle(2, Weight::MAX); + assert!(Actors::actor_run_state(actor_id).is_none()); + assert!(matches!( + crate::ActorControlLocators::::get(actor_id), + Some(crate::ActorControlLocation::Unsignaled) + )); + assert!(!ActorIdentities::::contains_key(actor_id)); + assert!(has_actor_event(|event| matches!( + event, + Event::CycleSummary { + actor_id: id, + result: CycleResult::Completed, + .. + } if *id == actor_id + ))); + #[cfg(feature = "try-runtime")] + assert_ok!(crate::Pallet::::do_try_state()); + }); +} + +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn frame_only_user_retry_preserves_installed_hold_without_scalar_rehydration() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + setup_temporary_retry_pool(); + let actor_id = create_user_with( + ALICE, + Mutability::Mutable, + Schedule { + trigger: Trigger::manual(), + cooldown_blocks: 1, + }, + None, + temporary_retry_swap_plan(), + ); + fund_native(actor_id, 1_000_000_000_000_000_000); + let installed_hold = crate::ActorStateHolds::::get(actor_id) + .expect("active User owns its installed-lifetime hold"); + set_temporary_dex_failure(true); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + + Actors::on_idle(1, Weight::MAX); + let suspended = Actors::actor_run_state(actor_id).expect("User retry suspends"); + assert_eq!(suspended.eligible_at, 2); + assert!(matches!( + crate::ActorControlLocators::::get(actor_id), + Some(crate::ActorControlLocation::Ready { .. }) + )); + assert_eq!( + crate::ActorStateHolds::::get(actor_id), + Some(installed_hold.clone()) + ); + assert!(!ActorIdentities::::contains_key(actor_id)); + + set_temporary_dex_failure(false); + frame_system::Pallet::::set_block_number(2); + Actors::on_initialize(2); + run_prepass(); + Actors::on_idle(2, Weight::MAX); + assert!(Actors::actor_run_state(actor_id).is_none()); + assert!(matches!( + crate::ActorControlLocators::::get(actor_id), + Some(crate::ActorControlLocation::Unsignaled) + )); + assert_eq!( + crate::ActorStateHolds::::get(actor_id), + Some(installed_hold) + ); + assert!(!ActorIdentities::::contains_key(actor_id)); + #[cfg(feature = "try-runtime")] + assert_ok!(crate::Pallet::::do_try_state()); + }); +} + +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn frame_only_user_later_retry_preserves_hold_through_block_waiting() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + setup_temporary_retry_pool(); + let actor_id = create_user_with( + ALICE, + Mutability::Mutable, + Schedule { + trigger: Trigger::manual(), + cooldown_blocks: 1, + }, + None, + temporary_retry_swap_plan(), + ); + fund_native(actor_id, 1_000_000_000_000_000_000); + let installed_hold = crate::ActorStateHolds::::get(actor_id) + .expect("active User owns its installed-lifetime hold"); + set_temporary_dex_failure(true); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + + Actors::on_idle(1, Weight::MAX); + let first_retry = Actors::actor_run_state(actor_id).expect("first retry survives"); + assert_eq!(first_retry.unsuccessful_attempts_at_cursor, 1); + assert_eq!(first_retry.eligible_at, 2); + assert_eq!(canonical_scheduled_wakeup_block(actor_id), None); + + frame_system::Pallet::::set_block_number(2); + Actors::on_initialize(2); + run_prepass(); + Actors::on_idle(2, Weight::MAX); + let second_retry = Actors::actor_run_state(actor_id).expect("second retry survives"); + assert_eq!(second_retry.unsuccessful_attempts_at_cursor, 2); + assert_eq!(second_retry.eligible_at, 4); + assert_eq!(canonical_scheduled_wakeup_block(actor_id), Some(4)); + assert!(matches!( + crate::ActorControlLocators::::get(actor_id), + Some(crate::ActorControlLocation::Waiting { + key: WakeupKey::Block(4), + .. + }) + )); + assert!(!ActorIdentities::::contains_key(actor_id)); + assert_eq!( + crate::ActorStateHolds::::get(actor_id), + Some(installed_hold.clone()) + ); + + frame_system::Pallet::::set_block_number(4); + Actors::on_initialize(4); + run_prepass(); + Actors::on_idle(4, Weight::MAX); + assert!(matches!( + crate::ActorControlLocators::::get(actor_id), + Some(crate::ActorControlLocation::Ready { .. }) + )); + assert_eq!(canonical_scheduled_wakeup_block(actor_id), None); + assert!(Actors::actor_run_state(actor_id).is_some()); + assert!(!ActorIdentities::::contains_key(actor_id)); + assert_eq!( + crate::ActorStateHolds::::get(actor_id), + Some(installed_hold.clone()) + ); + assert!(!crate::WakeupWorkerFaultState::::exists()); + + set_temporary_dex_failure(false); + frame_system::Pallet::::set_block_number(5); + Actors::on_initialize(5); + run_prepass(); + Actors::on_idle(5, Weight::MAX); + assert!(Actors::actor_run_state(actor_id).is_none()); + assert!(matches!( + crate::ActorControlLocators::::get(actor_id), + Some(crate::ActorControlLocation::Unsignaled) + )); + assert!(!ActorIdentities::::contains_key(actor_id)); + assert_eq!( + crate::ActorStateHolds::::get(actor_id), + Some(installed_hold) ); - assert!(!has_actor_event(|event| matches!( + assert!(has_actor_event(|event| matches!( event, - Event::CycleContinued { actor_id: id, .. } | Event::CycleSummary { actor_id: id, .. } - if *id == actor_id + Event::CycleSummary { + actor_id: id, + result: CycleResult::Completed, + .. + } if *id == actor_id ))); + #[cfg(feature = "try-runtime")] + assert_ok!(crate::Pallet::::do_try_state()); }); } @@ -2344,11 +3690,41 @@ fn suspended_retry_wakes_for_window_expiry_before_cooldown() { actor_id )); run_idle(Weight::MAX); - assert_eq!(scheduled_wakeup_block(actor_id), Some(102)); + assert_eq!(canonical_scheduled_wakeup_block(actor_id), Some(102)); + #[cfg(not(feature = "runtime-benchmarks"))] + { + assert!(!ActorIdentities::::contains_key(actor_id)); + let locator = crate::ActorControlLocators::::get(actor_id); + assert!( + matches!( + locator, + Some(crate::ActorControlLocation::Waiting { + key: WakeupKey::Block(102), + .. + }) + ), + "window-expiry locator: {locator:?}; hot={:?}", + Actors::actor_hot(actor_id) + ); + assert!(Actors::load_frame_control_authority(actor_id).is_some()); + } + let ticket_frontier_before_expiry = crate::ActorReadyTail::::get(); frame_system::Pallet::::set_block_number(102); run_idle(Weight::MAX); - assert!(Actors::active_actor_view(actor_id).is_none()); + assert_eq!( + crate::ActorReadyTail::::get(), + ticket_frontier_before_expiry, + "exact window expiry must not publish a transient Ready ticket", + ); + assert!( + Actors::active_actor_view(actor_id).is_none(), + "actor={:?}; locator={:?}; hot={:?}; wakeup_fault={:?}", + Actors::active_actor_view(actor_id), + crate::ActorControlLocators::::get(actor_id), + Actors::actor_hot(actor_id), + crate::WakeupWorkerFaultState::::get(), + ); assert!(Actors::actor_run_state(actor_id).is_none()); assert!(has_actor_event(|event| { matches!( @@ -2362,6 +3738,62 @@ fn suspended_retry_wakes_for_window_expiry_before_cooldown() { }); } +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn frame_only_user_window_expiry_releases_hold_from_consumed_waiting_authority() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + setup_temporary_retry_pool(); + let actor_id = create_user_with( + ALICE, + Mutability::Mutable, + Schedule { + trigger: Trigger::manual(), + cooldown_blocks: 200, + }, + Some(ScheduleWindow { start: 1, end: 101 }), + temporary_retry_swap_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 + )); + + Actors::on_idle(1, Weight::MAX); + assert_eq!(canonical_scheduled_wakeup_block(actor_id), Some(102)); + assert!(crate::ActorStateHolds::::contains_key(actor_id)); + assert!(!ActorIdentities::::contains_key(actor_id)); + + let ticket_frontier_before_expiry = crate::ActorReadyTail::::get(); + frame_system::Pallet::::set_block_number(102); + Actors::on_initialize(102); + run_prepass(); + Actors::on_idle(102, Weight::MAX); + assert_eq!( + crate::ActorReadyTail::::get(), + ticket_frontier_before_expiry, + "User window expiry must not publish a transient Ready ticket", + ); + assert!(!Actors::active_actor_exists(actor_id)); + assert!(!crate::ActorControlLocators::::contains_key(actor_id)); + assert!(!crate::ActorStateHolds::::contains_key(actor_id)); + assert!(!crate::ActorFunding::::contains_key(actor_id)); + assert!(Actors::actor_run_state(actor_id).is_none()); + assert!(!ActorIdentities::::contains_key(actor_id)); + assert!(has_actor_event(|event| matches!( + event, + Event::ActorClosed { + actor_id: id, + reason: CloseReason::WindowExpired, + } if *id == actor_id + ))); + #[cfg(feature = "try-runtime")] + assert_ok!(crate::Pallet::::do_try_state()); + }); +} + #[test] fn user_retry_admits_and_charges_only_each_current_step() { new_test_ext().execute_with(|| { @@ -2411,15 +3843,11 @@ fn user_retry_admits_and_charges_only_each_current_step() { 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::actor_run_state(actor_id).expect("Actor run").cursor, 1 ); - let retry_weight = Actors::attempt_weight_upper_bound(&suspended, 1); - 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()); + let retry_weight = current_step_execution_weight(actor_id); let first_eligible_at = Actors::actor_run_state(actor_id) .expect("first retry remains") @@ -2471,43 +3899,174 @@ fn user_retry_admits_and_charges_only_each_current_step() { } #[test] -fn underfunded_retry_liability_selects_fee_free_minimal_apoptosis() { +fn user_retry_insolvency_closes_before_effect_capacity_deferral() { + for solvent in [false, true] { + for effect_capacity in [ + Weight::from_parts(0, u64::MAX / 2), + Weight::from_parts(u64::MAX / 2, 0), + ] { + new_test_ext().execute_with(|| { + System::set_block_number(1); + setup_temporary_retry_pool(); + let contract = user_active_contract( + Schedule { + trigger: Trigger::manual(), + cooldown_blocks: 1, + }, + None, + temporary_retry_swap_plan(), + ) + .expect("active retry Contract"); + prefund_active_user_creation(ALICE, &contract.steps); + let actor_id = Actors::next_actor_id(); + assert_ok!(Actors::create_user_actor( + RuntimeOrigin::signed(ALICE), + Mutability::Mutable, + Some(contract) + )); + fund_native(actor_id, 1_000_000_000_000_000_000); + set_temporary_dex_failure(true); + System::set_block_number(2); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + Actors::on_idle(2, Weight::MAX); + let run = Actors::actor_run_state(actor_id).expect("temporary failure suspends"); + assert_eq!(run.unsuccessful_attempts_at_cursor, 1); + assert_eq!(run.eligible_at, 3); + assert!(matches!( + crate::ActorControlLocators::::get(actor_id), + Some(crate::ActorControlLocation::Ready { .. }) + )); + let sovereign = sovereign_account(actor_id); + if !solvent { + let excess = native_balance(&sovereign) + .checked_sub(TestMinUserBalance::get()) + .expect("funded Actor covers the protected floor"); + deplete_user_sovereign(actor_id, excess); + } + let custody_before = native_balance(&sovereign); + let actor_before = Actors::active_actor_view(actor_id).expect("Suspended User"); + clear_fee_collections(); + System::reset_events(); + System::set_block_number(run.eligible_at); + let limits = crate::SimulationBudget { + actor_control: Weight::from_parts(u64::MAX / 2, u64::MAX / 2), + shared_economic: effect_capacity, + } + .checked_limits() + .expect("independent component limits fit"); + let mut resources = crate::BlockResourceState::new(run.eligible_at); + assert_ok!(resources.begin_prepass()); + assert_ok!(resources.open_external_phase()); + assert_ok!(resources.begin_drain()); + let pass = Actors::execute_cycle_to_cutoff_with_resources( + Weight::MAX, + Actors::queue_tail(), + &mut resources, + limits, + crate::BlockResourceDomain::ActorDrainEffect, + limits.actor_control(), + ); + assert_eq!(resources.outstanding_reservations(), 0); + assert_eq!(resources.usage().actor_effect_used(), Weight::zero()); + assert_eq!( + pass.reconciled_domains(), + Some((pass.consumed, Weight::zero())) + ); + assert_eq!(native_balance(&sovereign), custody_before); + assert!(fee_collections().is_empty()); + assert!(!has_actor_event(|event| matches!( + event, + Event::StepFailed { actor_id: id, .. } + | Event::ActionFeeCharged { actor_id: id, .. } + | Event::CycleStarted { actor_id: id, .. } if *id == actor_id + ))); + if solvent { + assert_eq!(Actors::active_actor_view(actor_id), Some(actor_before)); + assert_eq!( + Actors::actor_run_state(actor_id) + .expect("deferred Run remains") + .encode(), + run.encode() + ); + assert_eq!( + Actors::paged_head_entry().map(|(_, entry)| entry.actor_id), + Some(actor_id) + ); + Actors::execute_cycle_to_cutoff(Weight::MAX, Actors::queue_tail()); + let retried = Actors::actor_run_state(actor_id).expect("funded retry suspends again"); + assert_eq!(retried.cycle_nonce, run.cycle_nonce); + assert_eq!(retried.cursor, run.cursor); + assert_eq!(retried.unsuccessful_attempts_at_cursor, 2); + assert!(!fee_collections().is_empty()); + } else { + assert!(!Actors::active_actor_exists(actor_id)); + assert!(Actors::actor_run_state(actor_id).is_none()); + assert!(!crate::ActorControlLocators::::contains_key(actor_id)); + assert!(!crate::ActorStateHolds::::contains_key(actor_id)); + assert!(has_actor_event(|event| matches!( + event, + Event::ActorClosed { + actor_id: id, + reason: CloseReason::CycleAdmissionInsufficient, + } if *id == actor_id + ))); + } + #[cfg(feature = "try-runtime")] + assert_ok!(Actors::do_try_state()); + }); + } + } +} + +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn frame_only_underfunded_retry_apoptosis_closes_from_retained_ready_authority() { 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(), + let actor_id = create_user_with( + ALICE, + Mutability::Mutable, + Schedule { + trigger: Trigger::manual(), + cooldown_blocks: 1, }, - on_error: RETRY_LATER, - }]) - .expect("one retry Step fits"); - let actor_id = create_user_with(ALICE, Mutability::Mutable, manual_schedule(), None, plan); + None, + temporary_retry_swap_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); + + Actors::on_idle(1, Weight::MAX); let run = Actors::actor_run_state(actor_id).expect("temporary failure suspends"); + assert_eq!(run.eligible_at, 2); + assert!(matches!( + crate::ActorControlLocators::::get(actor_id), + Some(crate::ActorControlLocation::Ready { .. }) + )); 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); + frame_system::Pallet::::set_block_number(2); + Actors::on_initialize(2); run_prepass(); - run_idle(Weight::MAX); - run_next_idle(Weight::MAX); + Actors::on_idle(2, Weight::MAX); assert!(!Actors::active_actor_exists(actor_id)); + assert!(Actors::actor_run_state(actor_id).is_none()); + assert!(!crate::ActorControlLocators::::contains_key(actor_id)); + assert!(!crate::ActorStateHolds::::contains_key(actor_id)); + assert!(!ActorIdentities::::contains_key(actor_id)); assert_eq!(native_balance(&sovereign), custody_before); assert!(fee_collections().is_empty()); assert!(has_actor_event(|event| matches!( @@ -2517,6 +4076,38 @@ fn underfunded_retry_liability_selects_fee_free_minimal_apoptosis() { reason: CloseReason::CycleAdmissionInsufficient, } if *id == actor_id ))); + #[cfg(feature = "try-runtime")] + assert_ok!(crate::Pallet::::do_try_state()); + }); +} + +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn consumed_frame_finalizer_rejects_a_retained_primary() { + 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 admission = Actors::actor_control_cell(actor_id) + .expect("creation admits the Contract") + .1 + .admission; + let state = Actors::active_actor_state(actor_id) + .expect("capture state while primary remains authoritative"); + + assert_noop!( + Actors::finalize_actor_from_consumed_state( + actor_id, + state, + &admission, + CloseReason::SchedulerIndexExhausted, + ), + Error::::ActorInvariant + ); + assert!(Actors::active_actor_exists(actor_id)); + assert!(crate::ActorControlLocators::::contains_key(actor_id)); + assert!(!ActorIdentities::::contains_key(actor_id)); + #[cfg(feature = "try-runtime")] + assert_ok!(crate::Pallet::::do_try_state()); }); } @@ -2565,11 +4156,6 @@ fn run_opening_snapshot_is_complete_frozen_and_capacity_checked_live() { let continuation = Actors::actor_run_state(actor_id).expect("suspended"); assert_eq!(continuation.cursor, 1); 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_eq!( continuation .opening_snapshot @@ -2630,7 +4216,8 @@ 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(); - for index in 0..8u32 { + let max_steps = <::MaxContractSteps as Get>::get(); + for index in 0..max_steps { steps.push(StepOf:: { precondition: None, task: Task::AddLiquidity { @@ -2650,7 +4237,7 @@ fn maximal_run_opening_snapshot_stays_bounded_to_full_contract_surfaces() { let plan = BoundedVec::try_from(steps).expect("maximal System plan fits"); let actor_id = create_system_with(ALICE, percentage_trigger_schedule(), None, plan); let actor = sovereign_account(actor_id); - for index in 0..16u32 { + for index in 0..max_steps.saturating_mul(2) { set_asset_balance(&actor, TestAsset::Local(100 + index), 100); } set_temporary_add_liquidity_failure(true); @@ -3003,9 +4590,7 @@ fn cycle_success_predicate_drives_failure_reset_auto_close_and_event_order() { .expect("two steps fit"), ); fund_native(continue_id, 100); - ActorHot::::mutate(continue_id, |maybe| { - maybe.as_mut().expect("actor hot state exists").unsuccessful_attempt_streak = 2; - }); + mutate_actor_hot_coherent(continue_id, |hot| hot.unsuccessful_attempt_streak = 2); assert_ok!(replace_auto_close( RuntimeOrigin::root(), continue_id, @@ -3256,9 +4841,9 @@ fn contract_steps_bound_is_single_and_encoded_for_both_classes() { new_test_ext().execute_with(|| { assert_eq!( <::MaxContractSteps as Get>::get(), - 8 + 12 ); - let steps: Vec<_> = (0..9) + let steps: Vec<_> = (0..13) .map(|i| { make_step(Task::Transfer { to: BOB, @@ -3269,7 +4854,7 @@ fn contract_steps_bound_is_single_and_encoded_for_both_classes() { .collect(); assert!(crate::ContractSteps::::try_from(steps).is_err()); let maximum = BoundedVec::try_from( - (0..8) + (0..12) .map(|i| { make_step(Task::Transfer { to: BOB, @@ -3279,7 +4864,7 @@ fn contract_steps_bound_is_single_and_encoded_for_both_classes() { }) .collect::>(), ) - .expect("eight steps fit the shared encoded bound"); + .expect("twelve steps fit the shared encoded bound"); prefund_active_user_creation(ALICE, &maximum); assert_ok!(Actors::create_user_actor( RuntimeOrigin::signed(ALICE), @@ -3307,9 +4892,7 @@ fn cycle_nonce_max_value_is_the_last_executable_cycle() { ); fund_native(actor_id, 100); let bob_before = native_balance(&BOB); - ActorIdentities::::mutate(actor_id, |maybe| { - maybe.as_mut().expect("system Actors identity exists").cycle_nonce = u64::MAX - 1; - }); + set_actor_cycle_nonce_coherent(actor_id, u64::MAX - 1); assert_ok!(Actors::manual_trigger(RuntimeOrigin::signed(ALICE), actor_id)); run_idle(Weight::MAX); let instance = Actors::active_actor_view(actor_id).expect("system Actors remains"); @@ -3337,12 +4920,7 @@ fn cycle_nonce_exhaustion_closes_user_actor() { ); fund_native(actor_id, 1_000); let bob_before = native_balance(&BOB); - ActorIdentities::::mutate(actor_id, |maybe| { - maybe - .as_mut() - .expect("user Actors identity exists") - .cycle_nonce = u64::MAX; - }); + set_actor_cycle_nonce_coherent(actor_id, u64::MAX); let expected_contract = Actors::load_actor_contract(actor_id).expect("contract exists"); let simulation = Actors::simulate_current_contract( actor_id, @@ -3350,6 +4928,7 @@ fn cycle_nonce_exhaustion_closes_user_actor() { Mutability::Mutable, expected_contract, SimulationMode::FreshCurrentPlan, + ample_simulation_budget(), ) .expect("nonce exhaustion is a terminal projection, not arithmetic failure"); assert_eq!( @@ -3393,12 +4972,7 @@ fn cycle_nonce_exhaustion_closes_system_actor() { transfer_contract_steps(BOB, 1), ); let bob_before = native_balance(&BOB); - ActorIdentities::::mutate(actor_id, |maybe| { - maybe - .as_mut() - .expect("system Actors identity exists") - .cycle_nonce = u64::MAX; - }); + set_actor_cycle_nonce_coherent(actor_id, u64::MAX); assert_ok!(Actors::manual_trigger( RuntimeOrigin::signed(ALICE), actor_id @@ -3996,6 +5570,11 @@ fn auto_close_threshold_reached_closes_actor_after_successful_cycle() { )); run_idle(Weight::MAX); assert!(Actors::active_actor_view(actor_id).is_some()); + #[cfg(not(feature = "runtime-benchmarks"))] + assert!(matches!( + Actors::load_frame_actor_state(actor_id), + crate::LoadedActorStateOf::Active(_) + )); frame_system::Pallet::::set_block_number(2); assert_ok!(Actors::manual_trigger( RuntimeOrigin::signed(ALICE), @@ -4003,6 +5582,10 @@ fn auto_close_threshold_reached_closes_actor_after_successful_cycle() { )); run_idle(Weight::MAX); assert!(Actors::active_actor_view(actor_id).is_none()); + #[cfg(not(feature = "runtime-benchmarks"))] + assert!(Actors::load_frame_control_authority(actor_id).is_none()); + #[cfg(not(feature = "runtime-benchmarks"))] + assert!(!crate::ActorControlLocators::::contains_key(actor_id)); assert!(has_actor_event(|event| { matches!( event, @@ -4082,6 +5665,8 @@ fn pure_close_does_not_start_normal_cycle_or_execute_tasks() { #[test] fn canonical_step_transition_matrix_has_production_simulation_parity() { let mut covered_rows = BTreeSet::new(); + let mut fresh_rows = BTreeSet::new(); + let mut current_rows = BTreeSet::new(); let mut covered_policies = BTreeSet::new(); let mut covered_mutability = BTreeSet::new(); let mut covered_actor_types = BTreeSet::new(); @@ -4102,254 +5687,300 @@ fn canonical_step_transition_matrix_has_production_simulation_parity() { .first() .expect("StepOutcome encoding carries a variant index"), ); - new_test_ext().execute_with(|| { - frame_system::Pallet::::set_block_number(10); - set_max_consecutive_failures(if case.bound == StepParityBound::GlobalReached { - 1 - } else { - 10 - }); - set_observation( - 1, - crate::ScalarObservationState::Fresh { - value: 50, - observed_at: u64::MAX, - }, - ); - setup_temporary_retry_pool(); - set_temporary_dex_failure(case.stimulus == StepParityStimulus::TemporaryFailure); - set_fail_dex_after_input_transfer(case.stimulus == StepParityStimulus::PermanentFailure); - let steps = parity_contract_steps(*case); - let schedule = if case.stimulus == StepParityStimulus::PredicateError { - observation_schedule(vec![1]) - } else if case.actor_type == ActorType::System && case.mutability == Mutability::Immutable { - timer_schedule(1) + for current_run in [false, true] { + if !current_run && case.bound == StepParityBound::LocalReached { + continue; + } + if current_run { + current_rows.insert(case.row); } else { - manual_schedule() - }; - let expected_contract = match case.actor_type { - ActorType::User => user_active_contract(schedule.clone(), None, steps.clone()), - ActorType::System => system_active_contract(schedule.clone(), None, steps.clone()), + fresh_rows.insert(case.row); } - .expect("direct Actor Contract"); - let actor_id = match case.actor_type { - ActorType::User => create_user_with(ALICE, case.mutability, schedule, None, steps), - ActorType::System => { - create_system_with_mutability(ALICE, case.mutability, schedule, None, steps) - } - }; - fund_native( - actor_id, - if case.actor_type == ActorType::User { - 1_000 - } else if case.stimulus == StepParityStimulus::FundingUnavailable { - 8 + let target_index = u32::from(current_run); + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(10); + set_max_consecutive_failures(if case.bound == StepParityBound::GlobalReached { + 1 } else { - 100 - }, - ); - if case.actor_type == ActorType::System && case.mutability == Mutability::Immutable { - frame_system::Pallet::::set_block_number(11); - ActorHot::::mutate(actor_id, |maybe| { - maybe - .as_mut() - .expect("parity actor hot state exists") - .pending_signal = true; - }); - Actors::enqueue(actor_id).expect("parity actor enqueues"); - } else if case.stimulus == StepParityStimulus::PredicateError { - ActorHot::::mutate(actor_id, |maybe| { - maybe - .as_mut() - .expect("parity actor hot state exists") - .pending_signal = true; + 10 }); - Actors::enqueue(actor_id).expect("parity actor enqueues"); - } else { - assert_ok!(Actors::manual_trigger( - RuntimeOrigin::signed(ALICE), - actor_id - )); - } - if case.bound == StepParityBound::LocalReached { - 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; + set_observation( + 1, + crate::ScalarObservationState::Fresh { + value: 50, + observed_at: u64::MAX, + }, + ); + setup_temporary_retry_pool(); + set_temporary_dex_failure(case.stimulus == StepParityStimulus::TemporaryFailure); + set_fail_dex_after_input_transfer(case.stimulus == StepParityStimulus::PermanentFailure); + let steps = if current_run { + parity_contract_steps(*case) + } else { + BoundedVec::try_from( + parity_contract_steps(*case) + .into_iter() + .skip(1) + .collect::>(), + ) + .expect("Fresh target and suffix fit") + }; + let schedule = if case.stimulus == StepParityStimulus::PredicateError { + observation_schedule(vec![1]) + } else if case.actor_type == ActorType::System && case.mutability == Mutability::Immutable { + timer_schedule(1) + } else { + manual_schedule() + }; + let expected_contract = match case.actor_type { + ActorType::User => user_active_contract(schedule.clone(), None, steps.clone()), + ActorType::System => system_active_contract(schedule.clone(), None, steps.clone()), + } + .expect("direct Actor Contract"); + let actor_id = match case.actor_type { + ActorType::User => create_user_with(ALICE, case.mutability, schedule, None, steps), + ActorType::System => { + create_system_with_mutability(ALICE, case.mutability, schedule, None, steps) + } + }; + fund_native( + actor_id, + if case.actor_type == ActorType::User { + 1_000 + } else if case.stimulus == StepParityStimulus::FundingUnavailable { + if current_run { 8 } else { 6 } + } else { + 100 + }, + ); + if case.actor_type == ActorType::System && case.mutability == Mutability::Immutable { + frame_system::Pallet::::set_block_number(11); + assert!(enqueue_latched_actor(actor_id)); + } else if case.stimulus == StepParityStimulus::PredicateError { + assert!(enqueue_latched_actor(actor_id)); + } else { + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + } + if current_run { + // Commit the real prefix before comparing one target Step, never a whole-cycle runner. + Actors::execute_cycle_to_cutoff(Weight::MAX, Actors::queue_tail()); + let prefix_run = Actors::actor_run_state(actor_id).expect("matrix prefix opens a Run"); + assert_eq!(prefix_run.cursor, 1, "{} prefix cursor", case.name); + frame_system::Pallet::::set_block_number(prefix_run.eligible_at); + if case.bound == StepParityBound::LocalReached { + Actors::execute_cycle_to_cutoff(Weight::MAX, Actors::queue_tail()); + assert_eq!( + Actors::actor_hot(actor_id).map(|hot| hot.cycle_state), + Some(CycleState::Suspended) + ); + assert_eq!( + Actors::actor_run_state(actor_id).map(|state| state.cursor), + Some(1) + ); + let eligible_at = Actors::actor_run_state(actor_id) + .expect("matrix suspension persists") + .eligible_at; + frame_system::Pallet::::set_block_number(eligible_at); + let mut meter = WeightMeter::with_limit(Weight::MAX); + Actors::drain_overdue_wakeups_cursor(eligible_at, &mut meter); } - 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); } + let mode = if current_run { + SimulationMode::CurrentRun + } else { + SimulationMode::FreshCurrentPlan + }; assert_eq!( - Actors::actor_run_state(actor_id).map(|state| state.cursor), - Some(1) + Actors::paged_head_entry().map(|(_, entry)| entry.actor_id), + Some(actor_id), + "{} target is the real production FIFO head", + case.name + ); + System::reset_events(); + let actor = sovereign_account(actor_id); + let streak_before = Actors::active_actor_view(actor_id) + .expect("matrix actor remains active before production") + .unsuccessful_attempt_streak; + let before = ( + Actors::active_actor_view(actor_id).map(|view| view.encode()), + Actors::actor_run_state(actor_id).map(|state| state.encode()), + native_balance(&actor), + native_balance(&BOB), + native_balance(&CHARLIE), + native_balance(&TestFeeSink::get()), + native_balance(&u64::MAX), + asset_balance(&actor, TestAsset::Local(77)), + asset_balance(&u64::MAX, TestAsset::Local(77)), + System::events(), + ); + let simulation_root = polkadot_sdk::sp_io::storage::root(StateVersion::V1); + let simulation = Actors::simulate_current_contract( + actor_id, + case.actor_type, + case.mutability, + expected_contract, + mode, + ample_simulation_budget(), + ) + .unwrap_or_else(|error| panic!("{} simulation failed: {error:?}", case.name)); + assert_eq!( + polkadot_sdk::sp_io::storage::root(StateVersion::V1), + simulation_root, + "{} simulation changed whole storage", + case.name + ); + let after_simulation = ( + Actors::active_actor_view(actor_id).map(|view| view.encode()), + Actors::actor_run_state(actor_id).map(|state| state.encode()), + native_balance(&actor), + native_balance(&BOB), + native_balance(&CHARLIE), + native_balance(&TestFeeSink::get()), + native_balance(&u64::MAX), + asset_balance(&actor, TestAsset::Local(77)), + asset_balance(&u64::MAX, TestAsset::Local(77)), + System::events(), + ); + assert_eq!( + after_simulation, before, + "{} simulation persisted state or custody", + case.name + ); + assert_eq!( + simulation.steps.as_slice(), + vec![SimulationStepRecord { + step_index: target_index, + outcome: target_outcome.clone() + }], + "{} trace", + case.name ); - 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::CurrentRun - } else { - SimulationMode::FreshCurrentPlan - }; - System::reset_events(); - let actor = sovereign_account(actor_id); - let streak_before = Actors::active_actor_view(actor_id) - .expect("matrix actor remains active before production") - .unsuccessful_attempt_streak; - let before = ( - Actors::active_actor_view(actor_id).map(|view| view.encode()), - Actors::actor_run_state(actor_id).map(|state| state.encode()), - native_balance(&actor), - native_balance(&BOB), - native_balance(&CHARLIE), - native_balance(&TestFeeSink::get()), - native_balance(&u64::MAX), - asset_balance(&actor, TestAsset::Local(77)), - asset_balance(&u64::MAX, TestAsset::Local(77)), - System::events(), - ); - let simulation = Actors::simulate_current_contract( - actor_id, - case.actor_type, - case.mutability, - expected_contract, - mode, - ) - .unwrap_or_else(|error| panic!("{} simulation failed: {error:?}", case.name)); - let after_simulation = ( - Actors::active_actor_view(actor_id).map(|view| view.encode()), - Actors::actor_run_state(actor_id).map(|state| state.encode()), - native_balance(&actor), - native_balance(&BOB), - native_balance(&CHARLIE), - native_balance(&TestFeeSink::get()), - native_balance(&u64::MAX), - asset_balance(&actor, TestAsset::Local(77)), - asset_balance(&u64::MAX, TestAsset::Local(77)), - System::events(), - ); - assert_eq!( - after_simulation, before, - "{} simulation persisted state or custody", - case.name - ); - assert_eq!( - simulation.steps.as_slice(), - parity_expected_steps(*case), - "{} trace", - case.name - ); - clear_fee_collections(); - System::reset_events(); - let bob_before = native_balance(&BOB); - let charlie_before = native_balance(&CHARLIE); - let actor_before = native_balance(&actor); - let sink_before = native_balance(&TestFeeSink::get()); - let pool_native_before = native_balance(&u64::MAX); - run_idle(Weight::MAX); - let (status, outcomes, cursor, attempts) = observed_attempt_projection(actor_id); - assert_eq!(status, simulation.status, "{} disposition", case.name); - assert_eq!( - outcomes, simulation.cumulative_outcomes, - "{} outcomes", - 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 | AttemptDisposition::Continued => assert_eq!( - Actors::active_actor_view(actor_id) - .expect("completed persistent matrix actor remains active") - .unsuccessful_attempt_streak, - 0, - "{} completed attempt resets the failure streak", - case.name, - ), - AttemptDisposition::Failed | AttemptDisposition::Suspended => assert_eq!( - Actors::active_actor_view(actor_id) - .expect("failed or suspended matrix actor remains active") - .unsuccessful_attempt_streak, - streak_before - .checked_add(1) - .expect("matrix streak remains bounded"), - "{} unsuccessful attempt increments the failure streak once", - case.name, - ), - AttemptDisposition::Closed(_) => assert!( - Actors::active_actor_view(actor_id).is_none(), - "{} closed disposition removes active state", - case.name, - ), - } + clear_fee_collections(); + System::reset_events(); + let bob_before = native_balance(&BOB); + let charlie_before = native_balance(&CHARLIE); + let actor_before = native_balance(&actor); + let sink_before = native_balance(&TestFeeSink::get()); + let pool_native_before = native_balance(&u64::MAX); + Actors::execute_cycle_to_cutoff(Weight::MAX, Actors::queue_tail()); + let (status, outcomes, cursor, attempts) = observed_attempt_projection(actor_id); + assert_eq!(status, simulation.status, "{} disposition", case.name); + assert_eq!( + outcomes, simulation.cumulative_outcomes, + "{} outcomes", + 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 | AttemptDisposition::Continued => assert_eq!( + Actors::active_actor_view(actor_id) + .expect("completed persistent matrix actor remains active") + .unsuccessful_attempt_streak, + 0, + "{} completed attempt resets the failure streak", + case.name, + ), + AttemptDisposition::Failed | AttemptDisposition::Suspended => assert_eq!( + Actors::active_actor_view(actor_id) + .expect("failed or suspended matrix actor remains active") + .unsuccessful_attempt_streak, + streak_before + .checked_add(1) + .expect("matrix streak remains bounded"), + "{} unsuccessful attempt increments the failure streak once", + case.name, + ), + AttemptDisposition::Closed(_) => assert!( + Actors::active_actor_view(actor_id).is_none(), + "{} closed disposition removes active state", + case.name, + ), + } - let advances = parity_advances(*case); - let prefix_runs = case.bound != StepParityBound::LocalReached; - let target_transfer = case.stimulus == StepParityStimulus::SuccessfulTask; - let expected_bob: Balance = - (if prefix_runs { 2 } else { 0 }) + (if target_transfer { 5 } else { 0 }); - let expected_charlie: Balance = if advances { 3 } else { 0 }; - assert_eq!( - native_balance(&BOB), - bob_before + expected_bob, - "{} committed prefix/target", - case.name - ); - assert_eq!( - native_balance(&CHARLIE), - charlie_before + expected_charlie, - "{} suffix boundary", - case.name - ); - if matches!( - case.stimulus, - StepParityStimulus::TemporaryFailure | StepParityStimulus::PermanentFailure - ) { + let target_transfer = case.stimulus == StepParityStimulus::SuccessfulTask; + let expected_bob: Balance = if target_transfer { 5 } else { 0 }; + let expected_charlie: Balance = 0; assert_eq!( - native_balance(&u64::MAX), - pool_native_before, - "{} failed task custody rollback", + native_balance(&BOB), + bob_before + expected_bob, + "{} committed prefix/target", case.name ); - } - let fee_delta = native_balance(&TestFeeSink::get()).saturating_sub(sink_before); - match case.actor_type { - ActorType::User => { - assert!( - fee_delta > 0, - "{} must charge its committed attempt", - case.name - ); + assert_eq!( + native_balance(&CHARLIE), + charlie_before + expected_charlie, + "{} suffix boundary", + case.name + ); + if matches!( + case.stimulus, + StepParityStimulus::TemporaryFailure | StepParityStimulus::PermanentFailure + ) { assert_eq!( - fee_collections().iter().copied().sum::(), - fee_delta, - "{} fee accounting", + native_balance(&u64::MAX), + pool_native_before, + "{} failed task custody rollback", case.name ); } - ActorType::System => assert_eq!(fee_delta, 0, "{} System fee exemption", case.name), - } - assert_eq!( - actor_before.saturating_sub(native_balance(&actor)), - expected_bob + expected_charlie + fee_delta, - "{} reservation, fee, and custody conservation", - case.name, - ); - set_temporary_dex_failure(false); - set_fail_dex_after_input_transfer(false); - }); + let fee_delta = native_balance(&TestFeeSink::get()).saturating_sub(sink_before); + match case.actor_type { + ActorType::User => { + // Fresh Opening pays Pipeline admission; a persisted Run pays only an + // invoked target Action, since skipped/StopCycle control is prepaid. + if !current_run + || matches!( + case.stimulus, + StepParityStimulus::SuccessfulTask + | StepParityStimulus::TemporaryFailure + | StepParityStimulus::PermanentFailure + ) + { + assert!( + fee_delta > 0, + "{} invoked target Action is charged", + case.name + ); + } else { + assert_eq!(fee_delta, 0, "{} target invokes no paid Action", case.name); + } + assert_eq!( + fee_collections().iter().copied().sum::(), + fee_delta, + "{} fee accounting", + case.name + ); + } + ActorType::System => assert_eq!(fee_delta, 0, "{} System fee exemption", case.name), + } + assert_eq!( + actor_before.saturating_sub(native_balance(&actor)), + expected_bob + expected_charlie + fee_delta, + "{} reservation, fee, and custody conservation", + case.name, + ); + set_temporary_dex_failure(false); + set_fail_dex_after_input_transfer(false); + }); + } } + assert_eq!( + fresh_rows, covered_rows, + "every canonical row has a Fresh Opening witness" + ); + assert_eq!( + current_rows, covered_rows, + "every canonical row has a persisted Run witness" + ); assert_eq!( covered_rows, BTreeSet::from([ @@ -4380,6 +6011,286 @@ fn canonical_step_transition_matrix_has_production_simulation_parity() { ); } +#[test] +fn simulation_zero_step_user_auto_close_preserves_budget_and_rollback_boundaries() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + let actor_id = create_user_with( + ALICE, + Mutability::Mutable, + manual_schedule(), + None, + BoundedVec::default(), + ); + fund_native(actor_id, 100_000); + assert_ok!(replace_auto_close( + RuntimeOrigin::signed(ALICE), + actor_id, + Some(1) + )); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + let contract = Actors::load_actor_contract(actor_id).expect("zero-Step Contract exists"); + let root = polkadot_sdk::sp_io::storage::root(StateVersion::V1); + let custody = native_balance(&sovereign_account(actor_id)); + let mut budget = ample_simulation_budget(); + budget.actor_control = Weight::zero(); + assert_eq!( + Actors::simulate_current_contract( + actor_id, + ActorType::User, + Mutability::Mutable, + contract.clone(), + SimulationMode::FreshCurrentPlan, + budget + ), + Err(SimulationError::ResourceDeferred) + ); + assert_eq!(polkadot_sdk::sp_io::storage::root(StateVersion::V1), root); + let simulated = Actors::simulate_current_contract( + actor_id, + ActorType::User, + Mutability::Mutable, + contract, + SimulationMode::FreshCurrentPlan, + ample_simulation_budget(), + ) + .expect("zero-Step terminal Opening simulates"); + assert_eq!( + simulated.status, + AttemptDisposition::Closed(CloseReason::AutoCloseNonceReached) + ); + assert_eq!(simulated.cycle_nonce, 1); + assert!(simulated.steps.is_empty()); + assert_eq!(simulated.cumulative_outcomes, OutcomeTotals::default()); + assert_eq!(polkadot_sdk::sp_io::storage::root(StateVersion::V1), root); + assert_eq!(native_balance(&sovereign_account(actor_id)), custody); + System::reset_events(); + Actors::execute_cycle_to_cutoff(Weight::MAX, Actors::queue_tail()); + assert!(Actors::actor_identity(actor_id).is_none()); + assert!(has_actor_event( + |event| matches!(event, Event::ActorClosed { actor_id: id, + reason: CloseReason::AutoCloseNonceReached } if *id == actor_id) + )); + assert!(has_actor_event( + |event| matches!(event, Event::CycleSummary { actor_id: id, + outcomes, .. } if *id == actor_id && *outcomes == OutcomeTotals::default()) + )); + }); +} + +#[test] +fn simulation_terminal_ready_rejects_corrupt_fifo_span_without_cleanup() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + let actor_id = create_system_with( + ALICE, + manual_schedule(), + Some(ScheduleWindow { start: 1, end: 101 }), + inert_contract_steps(), + ); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + let contract = Actors::load_actor_contract(actor_id).expect("admitted windowed Contract"); + assert!(matches!( + Actors::actor_control_cell(actor_id).map(|(location, _)| location), + Some(crate::ActorControlLocation::Ready { .. }) + )); + System::set_block_number(102); + assert_eq!( + active_eligibility(actor_id).terminal_reason, + Some(CloseReason::WindowExpired) + ); + let head = Actors::queue_head(); + crate::ActorReadyTail::::put( + head + u64::from(<::MaxQueueLength as Get>::get()) + 1, + ); + System::reset_events(); + let root = polkadot_sdk::sp_io::storage::root(StateVersion::V1); + let account = sovereign_account(actor_id); + let custody = native_balance(&account); + assert_eq!( + Actors::simulate_current_contract( + actor_id, + ActorType::System, + Mutability::Mutable, + contract, + SimulationMode::FreshCurrentPlan, + ample_simulation_budget() + ), + Err(SimulationError::Classification( + ActorClassificationError::ActorInvariant + )) + ); + assert_eq!(polkadot_sdk::sp_io::storage::root(StateVersion::V1), root); + Actors::execute_cycle_to_cutoff(Weight::MAX, Actors::queue_tail()); + assert_eq!(polkadot_sdk::sp_io::storage::root(StateVersion::V1), root); + assert_eq!(native_balance(&account), custody); + assert!(System::events().is_empty()); + }); +} + +#[test] +fn simulation_non_ready_terminal_cleanup_is_budgeted_and_custody_neutral() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + let actor_id = create_system_with( + ALICE, + manual_schedule(), + Some(ScheduleWindow { start: 1, end: 101 }), + inert_contract_steps(), + ); + fund_native(actor_id, 100); + let contract = Actors::load_actor_contract(actor_id).expect("windowed Contract exists"); + System::set_block_number(102); + assert!( + Actors::actor_hot(actor_id) + .expect("active terminal actor") + .queue_ticket + .is_none() + ); + let account = sovereign_account(actor_id); + let custody = native_balance(&account); + let root = polkadot_sdk::sp_io::storage::root(StateVersion::V1); + let mut budget = ample_simulation_budget(); + budget.actor_control = Weight::zero(); + assert_eq!( + Actors::simulate_current_contract( + actor_id, + ActorType::System, + Mutability::Mutable, + contract.clone(), + SimulationMode::FreshCurrentPlan, + budget + ), + Err(SimulationError::ResourceDeferred) + ); + assert_eq!(polkadot_sdk::sp_io::storage::root(StateVersion::V1), root); + let simulated = Actors::simulate_current_contract( + actor_id, + ActorType::System, + Mutability::Mutable, + contract, + SimulationMode::FreshCurrentPlan, + ample_simulation_budget(), + ) + .expect("non-Ready terminal cleanup simulates"); + assert_eq!( + simulated.status, + AttemptDisposition::Closed(CloseReason::WindowExpired) + ); + assert!(simulated.steps.is_empty()); + assert_eq!(polkadot_sdk::sp_io::storage::root(StateVersion::V1), root); + assert_eq!(native_balance(&account), custody); + assert_ok!(Actors::permissionless_sweep( + RuntimeOrigin::signed(BOB), + actor_id + )); + assert!(Actors::actor_identity(actor_id).is_none()); + assert_eq!(native_balance(&account), custody); + assert!(has_actor_event( + |event| matches!(event, Event::ActorClosed { actor_id: id, + reason: CloseReason::WindowExpired } if *id == actor_id) + )); + }); +} + +#[test] +fn running_simulation_obeys_q1_and_returns_only_the_current_step() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + let steps = BoundedVec::try_from(vec![ + make_step(Task::Transfer { + to: BOB, + asset: TestAsset::Native, + amount: AmountResolution::Fixed(2), + }), + make_step(Task::Transfer { + to: BOB, + asset: TestAsset::Native, + amount: AmountResolution::Fixed(5), + }), + make_step(Task::Transfer { + to: CHARLIE, + asset: TestAsset::Native, + amount: AmountResolution::Fixed(3), + }), + ]) + .expect("three Steps fit"); + let contract = + system_active_contract(manual_schedule(), None, steps.clone()).expect("valid Contract"); + let actor_id = create_system_with(ALICE, manual_schedule(), None, steps); + fund_native(actor_id, 100); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + Actors::execute_cycle_to_cutoff(Weight::MAX, Actors::queue_tail()); + let run = Actors::actor_run_state(actor_id).expect("Opening commits only the prefix"); + assert_eq!(run.cursor, 1); + assert!(run.eligible_at > System::block_number()); + let root_before = polkadot_sdk::sp_io::storage::root(StateVersion::V1); + assert_eq!( + Actors::simulate_current_contract( + actor_id, + ActorType::System, + Mutability::Mutable, + contract.clone(), + SimulationMode::CurrentRun, + ample_simulation_budget() + ), + Err(SimulationError::NotReady) + ); + assert_eq!( + polkadot_sdk::sp_io::storage::root(StateVersion::V1), + root_before + ); + System::set_block_number(run.eligible_at); + let root_before = polkadot_sdk::sp_io::storage::root(StateVersion::V1); + let balances = ( + native_balance(&BOB), + native_balance(&CHARLIE), + native_balance(&sovereign_account(actor_id)), + ); + let result = Actors::simulate_current_contract( + actor_id, + ActorType::System, + Mutability::Mutable, + contract, + SimulationMode::CurrentRun, + ample_simulation_budget(), + ) + .expect("eligible Running Step simulates"); + assert_eq!(result.status, AttemptDisposition::Continued); + assert_eq!(result.start_cursor, 1); + assert_eq!(result.run_cursor, Some(2)); + assert_eq!( + result.steps.as_slice(), + &[SimulationStepRecord { + step_index: 1, + outcome: StepOutcome::Executed + }] + ); + assert_eq!(result.cumulative_outcomes.executed_steps, 2); + assert_eq!( + polkadot_sdk::sp_io::storage::root(StateVersion::V1), + root_before + ); + assert_eq!( + ( + native_balance(&BOB), + native_balance(&CHARLIE), + native_balance(&sovereign_account(actor_id)) + ), + balances + ); + }); +} + #[test] fn run_simulation_preserves_retry_position_and_committed_state() { new_test_ext().execute_with(|| { @@ -4390,7 +6301,25 @@ fn run_simulation_preserves_retry_position_and_committed_state() { 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); + assert!(continuation_before.eligible_at > System::block_number()); + let before_due_root = polkadot_sdk::sp_io::storage::root(StateVersion::V1); + assert_eq!( + Actors::simulate_current_contract( + actor_id, + ActorType::System, + Mutability::Mutable, + expected_contract.clone(), + SimulationMode::CurrentRun, + ample_simulation_budget() + ), + Err(SimulationError::NotReady) + ); + assert_eq!( + polkadot_sdk::sp_io::storage::root(StateVersion::V1), + before_due_root + ); + frame_system::Pallet::::set_block_number(continuation_before.eligible_at); + let root_before = polkadot_sdk::sp_io::storage::root(StateVersion::V1); let result = Actors::simulate_current_contract( actor_id, @@ -4398,6 +6327,7 @@ fn run_simulation_preserves_retry_position_and_committed_state() { Mutability::Mutable, expected_contract, SimulationMode::CurrentRun, + ample_simulation_budget(), ) .expect("eligible Actor run simulates"); @@ -4429,6 +6359,10 @@ fn run_simulation_preserves_retry_position_and_committed_state() { ); assert_eq!(Actors::active_actor_view(actor_id), Some(actor_before)); assert_eq!(frame_system::Pallet::::event_count(), events_before); + assert_eq!( + polkadot_sdk::sp_io::storage::root(StateVersion::V1), + root_before + ); }); } @@ -4465,6 +6399,7 @@ fn simulation_projects_first_retry_suspension_and_rolls_back_actor_mutation() { Mutability::Mutable, expected_contract, SimulationMode::FreshCurrentPlan, + ample_simulation_budget(), ) .expect("retry exhaustion simulates"); @@ -4476,6 +6411,32 @@ fn simulation_projects_first_retry_suspension_and_rolls_back_actor_mutation() { }); } +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn simulation_classification_uses_frame_authority_without_scalar_control_partitions() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let contract_steps = transfer_contract_steps(BOB, 10); + let actor_id = create_system_with(ALICE, manual_schedule(), None, contract_steps); + let expected_contract = Actors::load_actor_contract(actor_id).expect("Actor Contract exists"); + + assert_eq!( + Actors::simulate_current_contract( + actor_id, + ActorType::System, + Mutability::Mutable, + expected_contract, + SimulationMode::FreshCurrentPlan, + ample_simulation_budget(), + ) + .err(), + Some(SimulationError::NotReady) + ); + #[cfg(feature = "try-runtime")] + assert_ok!(crate::Pallet::::do_try_state()); + }); +} + #[test] fn simulation_rejects_contract_and_mode_mismatch_without_execution() { new_test_ext().execute_with(|| { @@ -4490,6 +6451,7 @@ fn simulation_rejects_contract_and_mode_mismatch_without_execution() { system_active_contract(manual_schedule(), None, contract_steps.clone()) .expect("direct Actor Contract"), SimulationMode::FreshCurrentPlan, + ample_simulation_budget(), ) .err(), Some(SimulationError::NotReady) @@ -4512,6 +6474,7 @@ fn simulation_rejects_contract_and_mode_mismatch_without_execution() { .expect("direct Actor Contract") }, SimulationMode::FreshCurrentPlan, + ample_simulation_budget(), ) .err(), Some(SimulationError::InvalidContract) @@ -4528,6 +6491,7 @@ fn simulation_rejects_contract_and_mode_mismatch_without_execution() { .expect("direct Actor Contract") }, SimulationMode::FreshCurrentPlan, + ample_simulation_budget(), ) .err(), Some(SimulationError::ContractMismatch) @@ -4540,6 +6504,7 @@ fn simulation_rejects_contract_and_mode_mismatch_without_execution() { system_active_contract(manual_schedule(), None, contract_steps) .expect("direct Actor Contract"), SimulationMode::CurrentRun, + ample_simulation_budget(), ) .err(), Some(SimulationError::ModeCycleStateMismatch) @@ -4557,76 +6522,97 @@ fn simulation_rejects_contract_and_mode_mismatch_without_execution() { #[test] 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 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), - }; - - ActorRunStateStore::::insert(actor_id, run_state.clone()); + let actor_id = create_suspended_system_retry(1); + let run_state = Actors::actor_run_state(actor_id).expect("suspended run exists"); + let (location, cell) = Actors::actor_control_cell(actor_id).expect("primary exists"); assert!(matches!( Actors::load_actor_state(actor_id), - LoadedActorStateOf::Corrupt + LoadedActorStateOf::Active(_) )); - ActorHot::::mutate(actor_id, |maybe| { - maybe.as_mut().expect("active actor").cycle_state = CycleState::Suspended; - }); + ActorRunStateStore::::remove(actor_id); assert!(matches!( Actors::load_actor_state(actor_id), - LoadedActorStateOf::Active(_) + LoadedActorStateOf::Corrupt )); - ActorRunStateStore::::remove(actor_id); + ActorRunStateStore::::insert(actor_id, run_state); + mutate_primary_control_cell(actor_id, |stored| stored.hot.cycle_state = CycleState::Idle); assert!(matches!( Actors::load_actor_state(actor_id), LoadedActorStateOf::Corrupt )); + mutate_primary_control_cell(actor_id, |stored| *stored = cell.clone()); + assert!(matches!( + Actors::load_actor_state(actor_id), + LoadedActorStateOf::Active(_) + )); - ActorRunStateStore::::insert(actor_id, run_state); + let contract = Actors::load_actor_contract(actor_id).expect("Contract exists"); + let admission = cell.admission.clone(); for remove_partition in 0u8..4 { - 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()); - } - if remove_partition != 1 { - ActorHot::::insert(actor_id, hot.clone()); - } - if remove_partition != 2 { - assert_ok!(Actors::store_actor_contract(actor_id, contract.clone())); - } - if remove_partition != 3 { - ActorFunding::::insert(actor_id, funding.clone()); + match remove_partition { + 0 => { + crate::ActorControlLocators::::remove(actor_id); + assert!(matches!( + Actors::load_actor_state(actor_id), + LoadedActorStateOf::Corrupt + )); + crate::ActorControlLocators::::insert(actor_id, location); + } + 1 => { + mutate_primary_control_cell(actor_id, |stored| stored.actor_id = u64::MAX); + assert!(matches!( + Actors::load_actor_state(actor_id), + LoadedActorStateOf::Corrupt + )); + match location { + crate::ActorControlLocation::Unsignaled => { + crate::ActorUnsignaledControlCells::::insert(actor_id, cell.clone()); + } + crate::ActorControlLocation::Ready { ticket } => { + crate::ActorReadyFrameChunks::::mutate(ticket / 32, |stored| { + *stored + .as_mut() + .expect("Ready page") + .get_mut((ticket % 32) as usize) + .expect("Ready slot") = Some(cell.clone()); + }); + } + crate::ActorControlLocation::Waiting { key, page, slot } => { + crate::ActorWaitingFrameChunks::::mutate((key, page), |stored| { + *stored + .as_mut() + .expect("Waiting page") + .entries + .get_mut(usize::from(slot)) + .expect("Waiting slot") = Some(crate::ActorWaitingEntry::Primary(cell.clone())); + }); + } + } + } + 2 => { + assert!(Actors::remove_admitted_contract_geometry(actor_id).is_some()); + assert!(matches!( + Actors::load_actor_state(actor_id), + LoadedActorStateOf::Corrupt + )); + assert!(Actors::insert_admitted_contract_geometry( + actor_id, &contract, &admission + )); + } + 3 => { + let funding = ActorFunding::::take(actor_id).expect("funding exists"); + assert!(matches!( + Actors::load_actor_state(actor_id), + LoadedActorStateOf::Corrupt + )); + ActorFunding::::insert(actor_id, funding); + } + _ => unreachable!(), } assert!(matches!( Actors::load_actor_state(actor_id), - LoadedActorStateOf::Corrupt + LoadedActorStateOf::Active(_) )); - ActorIdentities::::remove(actor_id); - ActorHot::::remove(actor_id); - let _ = Actors::remove_admitted_contract_geometry(actor_id); - ActorFunding::::remove(actor_id); - 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 24e25b4d..fceb831e 100644 --- a/template/pallets/actors/src/tests/fees_and_funding.rs +++ b/template/pallets/actors/src/tests/fees_and_funding.rs @@ -1,4 +1,5 @@ use super::*; +use crate::FeeAssetClass; #[test] fn actor_state_hold_prices_exact_contract_geometry_and_releases_on_close() { @@ -229,38 +230,80 @@ fn actor_run_state_hold_is_reserved_for_the_active_installed_lifetime() { 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); + let installed_hold = Actors::actor_state_hold(actor_id).expect("idle hold exists"); + assert!(installed_hold.breakdown.run > 0); fund_native(actor_id, 1_000_000_000_000_000_000); assert_ok!(Actors::manual_trigger( RuntimeOrigin::signed(ALICE), actor_id )); + System::reset_events(); 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 + Actors::actor_state_hold(actor_id).expect("Running hold exists"), + installed_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 - ); + Actors::actor_state_hold(actor_id).expect("active installed hold remains"), + installed_hold + ); + assert!(!System::events().iter().any(|record| matches!( + record.event, + RuntimeEvent::Balances( + polkadot_sdk::pallet_balances::Event::Held { .. } + | polkadot_sdk::pallet_balances::Event::Released { .. } + ) + ))); + }); +} + +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn frame_only_cancellation_preserves_active_user_state_hold() { + 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); + 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()); + let hold_before = Actors::actor_state_hold(actor_id).expect("active User hold exists"); + assert!(Actors::actor_control_cell(actor_id).is_some()); + 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), + Some(hold_before), + "active frame authority retains the complete installed-lifetime hold" + ); + assert!(ActorIdentities::::get(actor_id).is_none()); + assert!(Actors::actor_hot(actor_id).is_some()); + assert!(Actors::actor_control_cell(actor_id).is_some()); + #[cfg(feature = "try-runtime")] + assert_ok!(crate::Pallet::::do_try_state()); }); } @@ -505,7 +548,7 @@ fn zero_step_user_opening_charges_pipeline_but_no_action_fee() { } if *id == actor_id && *fee == pipeline_fee ))); assert_eq!( - Actors::actor_identities(actor_id) + Actors::actor_identity(actor_id) .expect("persistent zero-Step User remains") .cycle_nonce, 1 @@ -514,6 +557,68 @@ fn zero_step_user_opening_charges_pipeline_but_no_action_fee() { }); } +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn manual_trigger_uses_canonical_pending_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()); + assert!( + !Actors::actor_hot(actor_id) + .expect("canonical hot") + .pending_signal + ); + frame_system::Pallet::::reset_events(); + + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + let (_, _, frame_hot, _) = + Actors::load_frame_control_authority(actor_id).expect("frame authority remains live"); + let projected_hot = Actors::actor_hot(actor_id).expect("canonical projection remains live"); + assert!(frame_hot.pending_signal); + assert_eq!(projected_hot, frame_hot); + assert!(has_actor_event(|event| matches!( + event, + Event::ManualTriggerSet { actor_id: id } if *id == actor_id + ))); + assert!(has_actor_event(|event| matches!( + event, + Event::TriggerOccurrenceProcessed { + actor_id: id, + trigger_family: TriggerFamily::Manual, + .. + } if *id == actor_id + ))); + }); +} + +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn manual_trigger_fails_closed_without_primary_before_fee_or_event() { + 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()); + fund_native(actor_id, 100); + let sovereign = sovereign_account(actor_id); + Actors::remove_primary_control_cell_inner(actor_id).expect("primary removal succeeds"); + let sovereign_before = native_balance(&sovereign); + let sink_before = native_balance(&TestFeeSink::get()); + let hot_before = Actors::actor_hot(actor_id); + let events_before = System::events(); + + assert_noop!( + Actors::manual_trigger(RuntimeOrigin::signed(ALICE), actor_id), + Error::::ActorInvariant + ); + assert_eq!(native_balance(&sovereign), sovereign_before); + assert_eq!(native_balance(&TestFeeSink::get()), sink_before); + assert_eq!(Actors::actor_hot(actor_id), hot_before); + assert_eq!(System::events(), events_before); + }); +} + #[test] fn manual_trigger_charges_occurrence_before_pipeline_opening() { new_test_ext().execute_with(|| { @@ -685,6 +790,19 @@ fn busy_manual_occurrence_charges_and_latches_only_the_future_pipeline() { let hot = Actors::actor_hot(actor_id).expect("Actor hot state"); assert_eq!(hot.cycle_state, CycleState::Running); assert!(hot.pending_signal); + #[cfg(not(feature = "runtime-benchmarks"))] + { + let (location, _, frame_hot, _) = Actors::load_frame_control_authority(actor_id) + .expect("busy Trigger keeps one frame-owned primary"); + assert!(matches!( + location, + crate::ActorControlLocation::Ready { .. } + )); + assert_eq!( + frame_hot, hot, + "busy Trigger mutates the existing primary in place" + ); + } 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); @@ -758,6 +876,112 @@ fn manual_trigger_collection_failure_rolls_back_readiness_and_fee_movement() { }); } +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn address_event_uses_canonical_pending_authority() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = create_system_with( + ALICE, + percentage_trigger_schedule(), + None, + transfer_contract_steps(BOB, 1), + ); + assert!( + !Actors::actor_hot(actor_id) + .expect("canonical hot") + .pending_signal + ); + frame_system::Pallet::::reset_events(); + + assert_ok!(Actors::notify_address_event( + actor_id, + TestAsset::Native, + 1, + &ALICE, + )); + let (_, _, frame_hot, _) = + Actors::load_frame_control_authority(actor_id).expect("frame authority remains live"); + let projected_hot = Actors::actor_hot(actor_id).expect("canonical projection remains live"); + assert!(frame_hot.pending_signal); + assert_eq!(projected_hot, frame_hot); + assert!(has_actor_event(|event| matches!( + event, + Event::TriggerOccurrenceProcessed { + actor_id: id, + trigger_family: TriggerFamily::AddressEvent, + .. + } if *id == actor_id + ))); + }); +} + +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn address_event_execution_preserves_canonical_control() { + for steps in [inert_contract_steps(), BoundedVec::default()] { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = create_system_with(ALICE, percentage_trigger_schedule(), None, steps); + assert!(Actors::actor_control_cell(actor_id).is_some()); + + assert_ok!(Actors::notify_address_event( + actor_id, + TestAsset::Native, + 1, + &ALICE, + )); + run_idle(Weight::MAX); + + assert!(has_actor_event(|event| matches!( + event, + Event::CycleSummary { actor_id: id, .. } if *id == actor_id + ))); + assert!(!ActorIdentities::::contains_key(actor_id)); + assert!(Actors::actor_hot(actor_id).is_some()); + assert!(Actors::actor_control_cell(actor_id).is_some()); + #[cfg(feature = "try-runtime")] + assert_ok!(crate::Pallet::::do_try_state()); + }); + } +} + +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn address_event_preflight_and_commit_fail_closed_without_primary_authority() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = create_system_with( + ALICE, + percentage_trigger_schedule(), + None, + transfer_contract_steps(BOB, 1), + ); + Actors::remove_primary_control_cell_inner(actor_id).expect("primary removal succeeds"); + let funding_before = Actors::actor_funding(actor_id); + let hot_before = Actors::actor_hot(actor_id); + let events_before = System::events(); + + assert_noop!( + Actors::preflight_funding_event( + actor_id, + TestAsset::Native, + 1, + Some(&ALICE), + Some(&crate::FundingProvenance::Signed), + ), + Error::::ActorInvariant + ); + assert_noop!( + Actors::notify_address_event(actor_id, TestAsset::Native, 1, &ALICE), + Error::::ActorInvariant + ); + assert_eq!(Actors::actor_funding(actor_id), funding_before); + assert_eq!(Actors::actor_hot(actor_id), hot_before); + assert_eq!(System::events(), events_before); + }); +} + #[test] fn address_event_charges_occurrence_before_pipeline_opening() { new_test_ext().execute_with(|| { @@ -1141,32 +1365,42 @@ fn fee_envelope_settlement_releases_reservation_and_prices_attempts() { #[test] fn fee_native_protected_minimum_uses_the_configured_user_fee_native_floor() { assert_eq!( - fee_native_protected_minimum(ActorType::User, true, 1u128, 50), + fee_native_protected_minimum(ActorType::User, FeeAssetClass::FeeNative, 1u128, 50), 50 ); assert_eq!( - fee_native_protected_minimum(ActorType::User, true, 100u128, 50), + fee_native_protected_minimum(ActorType::User, FeeAssetClass::FeeNative, 100u128, 50), 50 ); assert_eq!( - fee_native_protected_minimum(ActorType::User, false, 1u128, 50), + fee_native_protected_minimum(ActorType::User, FeeAssetClass::Other, 1u128, 50), 1 ); assert_eq!( - fee_native_protected_minimum(ActorType::System, true, 100u128, 50), + fee_native_protected_minimum(ActorType::System, FeeAssetClass::FeeNative, 100u128, 50), 100 ); } #[test] -fn user_cycle_weight_includes_one_fee_collection_per_step() { +fn contract_admission_uses_class_neutral_canonical_step_resources() { new_test_ext().execute_with(|| { let contract_steps = inert_contract_steps(); - let system_weight = Actors::compute_cycle_weight_upper(ActorType::System, &contract_steps); - let user_weight = Actors::compute_cycle_weight_upper(ActorType::User, &contract_steps); + let system_weight = + Actors::contract_steps_admission_weight_upper(ActorType::System, &contract_steps); + let user_weight = + Actors::contract_steps_admission_weight_upper(ActorType::User, &contract_steps); + assert_eq!(user_weight, system_weight); + let contract = + user_active_contract(manual_schedule(), None, contract_steps).expect("active Contract"); + let resources = + Actors::derive_step_resource_envelopes(&contract).expect("canonical Step resources")[0]; assert_eq!( - user_weight, - system_weight.saturating_add(::fee_collection()) + system_weight, + Actors::scheduler_admission_overhead() + .saturating_add(resources.control) + .saturating_add(resources.effect) + .saturating_add(Actors::close_cleanup_weight_upper()) ); }); } @@ -1235,7 +1469,7 @@ fn system_sovereign_reattachment_accepts_dormant_contract_input() { Mutability::Mutable, None, )); - let identity = Actors::actor_identities(fresh_id).expect("dormant identity exists"); + let identity = Actors::actor_identity(fresh_id).expect("dormant identity exists"); assert_eq!(identity.sovereign_account, sovereign); assert_eq!( identity.actor_class, @@ -1805,77 +2039,82 @@ fn missing_actual_control_weight_rolls_back_without_fee_collection() { #[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 actor_id = create_system_with( - ALICE, - manual_schedule(), - None, - transfer_contract_steps(BOB, 10), - ); - fund_native(actor_id, 100); - assert_ok!(Actors::manual_trigger( - RuntimeOrigin::signed(ALICE), - 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())); - }); + for excess in [Weight::from_parts(1, 0), Weight::from_parts(0, 1)] { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = create_system_with( + ALICE, + manual_schedule(), + None, + transfer_contract_steps(BOB, 10), + ); + fund_native(actor_id, 100); + let reserved = Actors::load_current_step_from_storage(actor_id, 0) + .expect("current Step resources exist") + .resources + .effect; + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + set_task_effect_actual_weight_override(Some(reserved.saturating_add(excess))); + 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 greater_than_reserved_actual_control_weight_rolls_back_the_complete_attempt() { - new_test_ext().execute_with(|| { - frame_system::Pallet::::set_block_number(1); - let actor_id = create_system_with( - ALICE, - manual_schedule(), - None, - transfer_contract_steps(BOB, 10), - ); - fund_native(actor_id, 100); - assert_ok!(Actors::manual_trigger( - RuntimeOrigin::signed(ALICE), - actor_id - )); - 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())); - }); + for excess in [Weight::from_parts(1, 0), Weight::from_parts(0, 1)] { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = create_system_with( + ALICE, + manual_schedule(), + None, + transfer_contract_steps(BOB, 10), + ); + fund_native(actor_id, 100); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + 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(reserved.saturating_add(excess))); + 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] @@ -2051,6 +2290,18 @@ fn window_expired_takes_precedence_over_balance_exhausted() { Some(window), transfer_contract_steps(BOB, 1), ); + #[cfg(not(feature = "runtime-benchmarks"))] + { + let (_, _, frame_hot, _) = Actors::load_frame_control_authority(actor_id) + .expect("windowed Actor frame authority exists"); + assert_eq!( + frame_hot.wakeup_pointer, + Actors::actor_hot(actor_id) + .expect("canonical projection exists") + .wakeup_pointer + ); + assert!(frame_hot.wakeup_pointer.is_some()); + } frame_system::Pallet::::set_block_number(102); assert_ok!(Actors::permissionless_sweep( RuntimeOrigin::signed(ALICE), @@ -2171,6 +2422,7 @@ fn simulation_projects_fee_collection_failure_as_interface_error_and_rolls_back( Mutability::Mutable, contract, SimulationMode::FreshCurrentPlan, + ample_simulation_budget(), ), Err(SimulationError::FeeCollectionFailed) ); @@ -3340,6 +3592,53 @@ fn one_ingress_matching_the_single_source_mutates_funding_once() { }); } +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn frame_only_address_funding_reconciles_hold_with_canonical_control() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = create_user_with( + ALICE, + Mutability::Mutable, + on_address_event_schedule(SourceFilter::Any, AssetFilter::Any), + None, + contract_steps_with_step(make_step(Task::Transfer { + to: BOB, + asset: TestAsset::Native, + amount: AmountResolution::PercentageOfLastFunding(Perbill::one()), + })), + ); + fund_native(actor_id, 1_000_000_000_000_000_000); + let hold_before = + crate::ActorStateHolds::::get(actor_id).expect("User state hold is installed"); + assert!(Actors::actor_control_cell(actor_id).is_some()); + + assert_ok!(Actors::notify_address_event( + actor_id, + TestAsset::Native, + 100, + &ALICE, + )); + + assert_eq!( + actor_funding(actor_id) + .funding_accumulated + .get(&TestAsset::Native), + Some(&100) + ); + assert!(Actors::active_actor_view(actor_id).is_some_and(|view| view.pending_signal)); + assert!( + crate::ActorStateHolds::::get(actor_id) + .is_some_and(|hold| hold.breakdown.funding > hold_before.breakdown.funding) + ); + assert!(!ActorIdentities::::contains_key(actor_id)); + assert!(Actors::actor_hot(actor_id).is_some()); + assert!(Actors::actor_control_cell(actor_id).is_some()); + #[cfg(feature = "try-runtime")] + assert_ok!(crate::Pallet::::do_try_state()); + }); +} + #[test] fn identical_authoritative_transfers_remain_distinct_funding_events() { new_test_ext().execute_with(|| { @@ -5143,7 +5442,7 @@ fn try_state_reconciles_system_sovereign_and_reverse_index_ownership() { 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("System identity fixture"); + let identity = Actors::actor_identity(actor_id).expect("System identity fixture"); let ActorClass::System { sovereign_id } = identity.actor_class else { panic!("fixture must create a System actor"); }; diff --git a/template/pallets/actors/src/tests/fixtures/actor_control_transition_oracle_v1.tsv b/template/pallets/actors/src/tests/fixtures/actor_control_transition_oracle_v1.tsv new file mode 100644 index 00000000..194f2569 --- /dev/null +++ b/template/pallets/actors/src/tests/fixtures/actor_control_transition_oracle_v1.tsv @@ -0,0 +1,33 @@ +# Actor control transition oracle v1 +# Frozen source: scalar identity/hot/admission control geometry before frame-owner substitution +# Baseline commit: 7f767924e780a4ab032e2cc6799d8148f9abb9a2 +# Baseline tree: b3c0873df8fa56d01d63de1db038488c252160e9 +# Six-file integration manifest: 2ada4b518b05d2937b1f76298449ee7694aa79a8ae2bcf28731d9a3eff53aec0 +# Candidate test module before oracle instrumentation: 8fd921435238e216115dae42792f6a5c5fbcd2ca0ae27c56882610a6872e6c63 +# Value: Blake2-256 of the Rust Debug representation of the complete logical A0 snapshot tuple. +# Regeneration is forbidden after scalar A0 deletion; create a new explicitly reviewed version instead. +address_event_collection_failure|3c72f63e5f2ec3adbcc712519ed4fdba75f6eab9f75a9db411ad221b070d8a69 +address_event_success|57ffdfa609b3cce54f50f7b48cc8b446f1693634dd293271a5c30b905d5ce6a7 +at_time_collection_failure|753460b2b8199763cc5736020f7d236c6cbf7e1441ce180cfc458d2341f24435 +at_time_success|a0886442114cfb2102120278481ab3633552c09e76dddad16e152a32df9ef74c +cadenced_success|b6280d8648c08e5a77e6eb9a09a12e9de52ee92216845450c65ac823b32557ca +direct_funding_unavailable|32f7e10dbe418e76a37773afee5c9fbb4f1985d1bfa699c20eb0321cc6091b65 +direct_stop_cycle|e72f6ca231ee765cfbdacf4e1f4ac3f389be206c778d9b198f35c5ff61ef1bb5 +direct_temporary_failure|42e4cc872b35f1c0aa5b59e2502f64e892b807f4bbadc46f241659d8d3c20082 +direct_transfer_p0_match_true|f7236b21f094b6e136a0df8a36ff9c6a9b9d21d8fa489b8a47abf688e3bfca19 +direct_transfer_p2_match_false|c914ccc10a45614955af3aca01eddafd054a9eb5a077731660141eb9900c82e8 +direct_transfer_p2_match_true|bbcebfcba42dc1c92d83a3c86b989d159de0b0ed48097b6d55734cf9a709948d +direct_transfer_p4_match_true|8e7d41cfe533599693eb158d5387f29e8b9293700959758b53c5412e8a23e2c2 +funding_retry_fails_false|62d2b8d698a00c7aeade5bca25414f8465227ef874704a8378557e04684546fe +funding_retry_fails_true|c9f9d308cd610dd2c3e936dd34a734afc59fcf22e414ec1e991b82650b001b17 +manual_collection_failure|fb5fb13e9047bb8584f1aa17447391025e66c9c7940f644dbfeb5527af958002 +manual_success|2eb4a32e8d64533fdf4f377cabc5779d71cec1849a9682ed34699f682a368d88 +middle_running_step|3da803977c039f32284259378ebfbe287e1b710156ba4860b454496134d8b4a6 +observation_change_collection_failure|8e454988c12cf0baf96ca3628011bd962d1ecd251d7a130e7bea87b6f0023478 +observation_change_success|3ca092ae151977ad9136cba0ff8b094ef3679b6e0596e77043fa772132e41c7b +observation_crossing_collection_failure|f1a564b00f759d9b4bb696a2c069bebf64d999e87bddfd8f3a970efd81beb505 +observation_crossing_success|0ae0d1fa01c53e0b481fe8ffa4dfe199f16771d2c056a1e1ec669e4e9c431979 +temporary_retry_fails_false|ef52414b82832dee432aa4ef67e12114b67fec13515b9eb4843cc05ba9498d3a +temporary_retry_fails_true|b97cccb7f09d443236c6cc786ede7bb4ee0b1721b6b41425e4a8e127bafa369e +terminal_running_step|de9db4d350ffb2200c4ab7d58af903941b7de083009daaa6862cb4af1622f0e8 +zero_step_user_opening|d4bd84e695d8d1d3f129c88f1c16bb5d1d521a865c6c418d4d0725d9353eef43 diff --git a/template/pallets/actors/src/tests/lifecycle.rs b/template/pallets/actors/src/tests/lifecycle.rs index e9b13695..94356b99 100644 --- a/template/pallets/actors/src/tests/lifecycle.rs +++ b/template/pallets/actors/src/tests/lifecycle.rs @@ -1,5 +1,8 @@ use super::*; -use crate::{ActorContractHeads, ActorContractTailChunks, ActorRunHeads, ActorRunPayloads}; +use crate::{ + ActorContractHeads, ActorContractTailChunks, ActorControlLocation, ActorControlLocators, + ActorRunHeads, ActorRunPayloads, ActorUnsignaledControlCells, +}; #[test] fn trigger_grammar_is_single_source_and_non_nested() { @@ -53,6 +56,50 @@ fn trigger_grammar_is_single_source_and_non_nested() { assert!(!non_canonical_filter.has_canonical_filters()); } +#[test] +fn latched_contract_replacement_preserves_temporal_trigger_authority() { + for zero_step in [false, true] { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = create_system_with( + ALICE, + manual_schedule(), + None, + if zero_step { + Default::default() + } else { + contract_steps_with_step(make_step(Task::StopCycle)) + }, + ); + assert_ok!(Actors::manual_trigger(RuntimeOrigin::root(), actor_id)); + frame_system::Pallet::::set_block_number(2); + let mut replacement = Actors::actor_contract(actor_id).expect("admitted Contract"); + replacement.trigger = RuntimeTrigger::at_time(10); + assert_ok!(Actors::update_contract( + RuntimeOrigin::root(), + actor_id, + replacement + )); + assert!(Actors::actor_hot(actor_id).unwrap().pending_signal); + Actors::execute_cycle(Weight::MAX); + let hot = Actors::actor_hot(actor_id).expect("persistent Actor survives Opening"); + assert!( + matches!( + hot.trigger_runtime_state, + TriggerRuntimeState::AtTime { consumed: true, .. } + ), + "Opening consumes AtTime even when replacement preserves another family's latch: {hot:?}", + ); + assert!(!hot.pending_signal); + assert!(hot.trigger_wakeup_pointer.is_none()); + assert_eq!(hot.cycle_state, CycleState::Idle); + assert_eq!(Actors::active_actor_view(actor_id).unwrap().cycle_nonce, 1); + #[cfg(feature = "try-runtime")] + Actors::do_try_state().expect("ordinary replacement and Opening preserve temporal authority"); + }); + } +} + #[test] fn active_trigger_replacement_matrix_preserves_one_canonical_family() { new_test_ext().execute_with(|| { @@ -101,7 +148,7 @@ fn active_trigger_replacement_matrix_preserves_one_canonical_family() { "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"); + let hot = Actors::actor_hot(current_id).expect("active Actor hot state"); assert_eq!(&contract.trigger, new_trigger); assert!( hot @@ -162,7 +209,7 @@ fn late_trigger_transition_failure_rolls_back_canonical_and_derived_state() { root_before ); let contract = Actors::load_actor_contract(actor_id).expect("active Actor Contract"); - let hot = ActorHot::::get(actor_id).expect("active Actor hot state"); + let hot = Actors::actor_hot(actor_id).expect("active Actor hot state"); assert!(matches!( contract.trigger, Trigger::ObservationCrossing { .. } @@ -260,7 +307,7 @@ fn system_creation_accepts_absent_contract() { Mutability::Mutable, None, )); - let identity = Actors::actor_identities(0).expect("dormant identity exists"); + let identity = Actors::actor_identity(0).expect("dormant identity exists"); assert_eq!(identity.actor_class, ActorClass::System { sovereign_id: 0 }); assert!(Actors::active_actor_view(0).is_none()); assert_eq!(Actors::actor_identity_count(), 1); @@ -279,7 +326,7 @@ fn exact_slot_user_creation_accepts_absent_contract() { Mutability::Mutable, None, )); - let identity = Actors::actor_identities(0).expect("dormant identity exists"); + let identity = Actors::actor_identity(0).expect("dormant identity exists"); assert_eq!(identity.actor_class, ActorClass::User { owner_slot }); assert!(Actors::active_actor_view(0).is_none()); assert_eq!(Actors::actor_identity_count(), 1); @@ -303,9 +350,14 @@ fn deactivation_removes_active_epoch_and_all_contract_fragments_without_orphans( 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!(Actors::actor_hot(actor_id).is_some()); + assert_eq!( + ActorControlLocators::::get(actor_id), + Some(ActorControlLocation::Unsignaled) + ); + assert!(ActorUnsignaledControlCells::::contains_key(actor_id)); assert!(ActorContractHeads::::contains_key(actor_id)); - assert!(ActorAdmissionCertificates::::contains_key(actor_id)); + assert!(Actors::actor_control_cell(actor_id).is_some()); assert!(ActorContractTailChunks::::contains_key(actor_id, 0)); assert!(ActorContractTailChunks::::contains_key(actor_id, 1)); } @@ -318,11 +370,13 @@ fn deactivation_removes_active_epoch_and_all_contract_fragments_without_orphans( 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::actor_identity(actor_id).is_some()); assert!(Actors::active_actor_view(actor_id).is_none()); - assert!(!ActorHot::::contains_key(actor_id)); + assert!(!Actors::actor_hot(actor_id).is_some()); + assert!(!ActorControlLocators::::contains_key(actor_id)); + assert!(!ActorUnsignaledControlCells::::contains_key(actor_id)); assert!(!ActorContractHeads::::contains_key(actor_id)); - assert!(!ActorAdmissionCertificates::::contains_key(actor_id)); + assert!(!Actors::actor_control_cell(actor_id).is_some()); assert!(!ActorContractTailChunks::::contains_key(actor_id, 0)); assert!(!ActorContractTailChunks::::contains_key(actor_id, 1)); assert!(!ActorRunHeads::::contains_key(actor_id)); @@ -375,8 +429,10 @@ fn deactivate_activate_preserves_nonce_but_resets_active_epoch_state_for_both_cl )); assert_ok!(Actors::deactivate_actor(RuntimeOrigin::root(), system_id)); for actor_id in [user_id, system_id] { - let dormant = Actors::actor_identities(actor_id).expect("durable identity"); + let dormant = Actors::actor_identity(actor_id).expect("durable identity"); assert_eq!(dormant.cycle_nonce, 1); + assert_eq!(ActorIdentities::::get(actor_id), Some(dormant)); + assert!(!ActorControlLocators::::contains_key(actor_id)); assert!(Actors::actor_funding(actor_id).is_none()); assert!(Actors::actor_run_state(actor_id).is_none()); } @@ -397,6 +453,12 @@ fn deactivate_activate_preserves_nonce_but_resets_active_epoch_state_for_both_cl for actor_id in [user_id, system_id] { let active = Actors::active_actor_view(actor_id).expect("reactivated actor"); assert_eq!(active.cycle_nonce, 1); + assert!(!ActorIdentities::::contains_key(actor_id)); + assert_eq!( + ActorControlLocators::::get(actor_id), + Some(ActorControlLocation::Unsignaled) + ); + assert!(ActorUnsignaledControlCells::::contains_key(actor_id)); assert_eq!(active.unsuccessful_attempt_streak, 0); assert!(!active.pending_signal); assert!(actor_funding(actor_id).funding_accumulated.is_empty()); @@ -422,6 +484,20 @@ fn deactivate_activate_preserves_nonce_but_resets_active_epoch_state_for_both_cl .cycle_nonce, 2 ); + frame_system::Pallet::::set_block_number(5); + for actor_id in [user_id, system_id] { + let latest = Actors::actor_identity(actor_id).expect("latest active identity"); + assert_ok!(Actors::deactivate_actor( + RuntimeOrigin::signed(ALICE), + actor_id + )); + let dormant = ActorIdentities::::get(actor_id).expect("dormant registry restored"); + assert_eq!(dormant.cycle_nonce, latest.cycle_nonce); + assert_eq!(dormant.sovereign_account, latest.sovereign_account); + assert_eq!(dormant.owner, latest.owner); + assert_eq!(dormant.actor_class, latest.actor_class); + assert!(!ActorControlLocators::::contains_key(actor_id)); + } }); } @@ -871,7 +947,8 @@ fn control_permanent_placement_exhaustion_closes_through_the_unified_sink() { None, transfer_contract_steps(BOB, 1), ); - crate::NextQueueTicket::::put(u64::MAX); + crate::ActorReadyHead::::put(u64::MAX); + crate::ActorReadyTail::::put(u64::MAX); assert_ok!(Actors::manual_trigger( RuntimeOrigin::signed(ALICE), actor_id @@ -904,7 +981,8 @@ fn control_permanent_placement_exhaustion_closes_through_the_unified_sink() { Some(trigger_deadline) ); frame_system::Pallet::::set_block_number(2); - crate::NextQueueTicket::::put(u64::MAX); + crate::ActorReadyHead::::put(u64::MAX); + crate::ActorReadyTail::::put(u64::MAX); assert_ok!(Actors::resume_actor(RuntimeOrigin::signed(ALICE), actor_id)); assert_eq!( Actors::actor_hot(actor_id).and_then(|hot| hot.trigger_wakeup_pointer), @@ -921,7 +999,13 @@ fn control_permanent_placement_exhaustion_closes_through_the_unified_sink() { Some(ScheduleWindow { start: 1, end: 101 }), transfer_contract_steps(BOB, 1), ); + let (_, mut consumed) = Actors::actor_control_cell(actor_id).expect("terminal primary"); assert!(Actors::wakeup_substrate_invalidate(actor_id).is_some()); + assert!(!ActorControlLocators::::contains_key(actor_id)); + consumed.hot.wakeup_pointer = None; + consumed.eligible_at = None; + ActorUnsignaledControlCells::::insert(actor_id, consumed); + ActorControlLocators::::insert(actor_id, ActorControlLocation::Unsignaled); crate::WakeupCursorLen::::insert( WakeupClock::Block, <::MaxActiveActors as Get>::get(), @@ -955,7 +1039,8 @@ fn control_permanent_placement_exhaustion_closes_through_the_unified_sink() { None, transfer_contract_steps(BOB, 1), ); - crate::NextQueueTicket::::put(u64::MAX); + crate::ActorReadyHead::::put(u64::MAX); + crate::ActorReadyTail::::put(u64::MAX); assert_ok!(update_contract_partial!( RuntimeOrigin::signed(ALICE), actor_id, @@ -1023,6 +1108,528 @@ fn on_address_event_without_source_is_ignored_for_owner_filter() { }); } +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn stable_hot_mutation_and_store_seams_update_primary_and_fail_closed_on_topology_fault() { + 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_eq!( + Actors::mutate_control_hot_or(actor_id, 99, |hot| { + hot.unsuccessful_attempt_streak = 7; + 7 + }), + 7 + ); + let (_, _, frame_hot, _) = + Actors::load_frame_control_authority(actor_id).expect("frame authority exists"); + let projected_hot = Actors::actor_hot(actor_id).expect("canonical projection exists"); + assert_eq!(frame_hot, projected_hot); + assert_eq!(frame_hot.unsuccessful_attempt_streak, 7); + + let mut replacement = projected_hot; + replacement.unsuccessful_attempt_streak = 6; + assert_eq!( + Actors::try_store_control_hot_with_authority(actor_id, replacement.clone(),), + Ok(()) + ); + let (_, _, frame_hot, _) = + Actors::load_frame_control_authority(actor_id).expect("stored frame authority exists"); + assert_eq!(frame_hot, replacement); + assert_eq!(Actors::actor_hot(actor_id), Some(replacement.clone())); + + ActorUnsignaledControlCells::::remove(actor_id); + let root_before = polkadot_sdk::sp_io::storage::root(StateVersion::V1); + assert!(Actors::actor_hot(actor_id).is_none()); + assert_eq!( + Actors::mutate_control_hot_or(actor_id, 99, |hot| { + hot.unsuccessful_attempt_streak = 8; + 8 + }), + 99 + ); + assert!(Actors::actor_hot(actor_id).is_none()); + let mut rejected = replacement; + rejected.unsuccessful_attempt_streak = 8; + assert_eq!( + Actors::try_store_control_hot_with_authority(actor_id, rejected,), + Err(crate::scheduler::EnqueueOutcome::CorruptedTopology) + ); + assert!(Actors::actor_hot(actor_id).is_none()); + assert_eq!( + polkadot_sdk::sp_io::storage::root(StateVersion::V1), + root_before + ); + }); +} + +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn lifecycle_mutations_keep_frame_identity_and_hot_authority_in_lockstep() { + 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_ok!(Actors::pause_actor(RuntimeOrigin::signed(ALICE), actor_id)); + let (_, frame_identity, frame_hot, frame_admission) = + Actors::load_frame_control_authority(actor_id).expect("paused frame authority exists"); + assert_eq!( + frame_identity, + Actors::actor_identity(actor_id).expect("canonical identity") + ); + assert_eq!( + frame_hot, + Actors::actor_hot(actor_id).expect("canonical hot state") + ); + assert_eq!( + frame_admission, + Actors::actor_control_cell(actor_id) + .map(|(_, cell)| cell.admission) + .expect("canonical admission") + ); + assert!(frame_hot.lifecycle.is_paused()); + + frame_system::Pallet::::set_block_number(2); + assert_ok!(Actors::resume_actor(RuntimeOrigin::signed(ALICE), actor_id)); + let (_, frame_identity, frame_hot, _) = + Actors::load_frame_control_authority(actor_id).expect("resumed frame authority exists"); + assert_eq!( + frame_identity, + Actors::actor_identity(actor_id).expect("canonical identity") + ); + assert_eq!( + frame_hot, + Actors::actor_hot(actor_id).expect("canonical hot state") + ); + assert_eq!(frame_hot.lifecycle, ActiveLifecycle::Active); + }); +} + +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn policy_only_contract_replacement_uses_frame_authority_with_canonical_control() { + new_test_ext().execute_with(|| { + let actor_id = create_suspended_system_retry(1); + let crate::LoadedActorStateOf::Active(state) = Actors::load_frame_actor_state(actor_id) else { + panic!("active frame state"); + }; + let mut replacement = state.contract; + replacement.completion = crate::CompletionPolicy::CloseAfterProductiveCycle; + + assert_ok!(Actors::update_contract( + RuntimeOrigin::root(), + actor_id, + replacement.clone(), + )); + let (_, _, hot, admission) = + Actors::load_frame_control_authority(actor_id).expect("updated frame authority exists"); + assert_eq!(hot.lifecycle, ActiveLifecycle::Active); + let crate::LoadedActorStateOf::Active(updated) = Actors::load_frame_actor_state(actor_id) + else { + panic!("updated frame state"); + }; + assert_eq!(updated.contract, replacement); + assert!(admission.has_valid_identity()); + assert!(ActorIdentities::::get(actor_id).is_none()); + assert!(Actors::actor_hot(actor_id).is_some()); + assert!(Actors::actor_control_cell(actor_id).is_some()); + }); +} + +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn user_contract_replacement_reconciles_state_hold_from_frame_authority() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = create_user_with( + ALICE, + Mutability::Mutable, + manual_schedule(), + None, + inert_contract_steps(), + ); + let hold_before = crate::ActorStateHolds::::get(actor_id).expect("User hold exists"); + let crate::LoadedActorStateOf::Active(state) = Actors::load_frame_actor_state(actor_id) else { + panic!("active User frame state"); + }; + let mut replacement = state.contract; + replacement.completion = crate::CompletionPolicy::CloseAfterProductiveCycle; + frame_system::Pallet::::set_block_number(2); + + assert_ok!(Actors::update_contract( + RuntimeOrigin::signed(ALICE), + actor_id, + replacement, + )); + let hold_after = crate::ActorStateHolds::::get(actor_id).expect("User hold survives"); + assert_eq!(hold_after.owner, hold_before.owner); + assert_eq!(hold_after.breakdown, hold_before.breakdown); + assert!(ActorIdentities::::get(actor_id).is_none()); + assert!(Actors::actor_hot(actor_id).is_some()); + assert!(Actors::actor_control_cell(actor_id).is_some()); + #[cfg(feature = "try-runtime")] + assert_ok!(crate::Pallet::::do_try_state()); + }); +} + +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn user_crossing_install_uses_frozen_class_with_canonical_control() { + 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, + manual_schedule(), + None, + inert_contract_steps(), + ); + let crate::LoadedActorStateOf::Active(state) = Actors::load_frame_actor_state(actor_id) else { + panic!("active User frame state"); + }; + let mut replacement = state.contract; + replacement.trigger = + RuntimeTrigger::observation_crossing(7, CrossingDirection::Rising, 100, 80); + frame_system::Pallet::::set_block_number(2); + + assert_ok!(Actors::update_contract( + RuntimeOrigin::signed(ALICE), + actor_id, + replacement, + )); + let locator = + crate::CrossingMemberships::::get(actor_id).expect("User membership exists"); + assert_eq!(crate::CrossingUserFeedMembershipCount::::get(7), 1); + let (_, _, hot, admission) = + Actors::load_frame_control_authority(actor_id).expect("User Crossing frame authority exists"); + let page = crate::CrossingMemberPages::::get(locator.key, locator.page) + .expect("User Crossing member page exists"); + let member = page + .entries + .get(locator.offset as usize) + .expect("User Crossing member exists"); + assert_eq!(member.admission_identity, admission.admission_identity); + assert_ne!(member.admission_identity, [0; 32]); + assert!(matches!( + hot.trigger_runtime_state, + TriggerRuntimeState::ObservationCrossing { + phase: CrossingPhase::Armed, + .. + } + )); + assert!(ActorIdentities::::get(actor_id).is_none()); + assert!(Actors::actor_hot(actor_id).is_some()); + assert!(Actors::actor_control_cell(actor_id).is_some()); + #[cfg(feature = "try-runtime")] + assert_ok!(crate::Pallet::::do_try_state()); + }); +} + +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn crossing_schedule_replacement_preserves_frame_phase_with_canonical_control() { + 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_system_with( + ALICE, + Schedule { + trigger: RuntimeTrigger::observation_crossing(7, CrossingDirection::Rising, 100, 80), + cooldown_blocks: 0, + }, + None, + inert_contract_steps(), + ); + let membership_before = + crate::CrossingMemberships::::get(actor_id).expect("Crossing membership exists"); + let crate::LoadedActorStateOf::Active(state) = Actors::load_frame_actor_state(actor_id) else { + panic!("active Crossing frame state"); + }; + let mut replacement = state.contract; + replacement.cooldown_blocks = 1; + frame_system::Pallet::::set_block_number(2); + + assert_ok!(Actors::update_contract( + RuntimeOrigin::root(), + actor_id, + replacement, + )); + assert_eq!( + crate::CrossingMemberships::::get(actor_id), + Some(membership_before) + ); + let (_, _, hot, _) = + Actors::load_frame_control_authority(actor_id).expect("Crossing frame authority survives"); + assert!(matches!( + hot.trigger_runtime_state, + TriggerRuntimeState::ObservationCrossing { + phase: CrossingPhase::Armed, + .. + } + )); + assert!(ActorIdentities::::get(actor_id).is_none()); + assert!(Actors::actor_hot(actor_id).is_some()); + assert!(Actors::actor_control_cell(actor_id).is_some()); + #[cfg(feature = "try-runtime")] + assert_ok!(crate::Pallet::::do_try_state()); + }); +} + +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn temporal_schedule_replacement_invalidates_loaded_frame_reference_with_canonical_control() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = create_system_with(ALICE, timer_schedule(5), None, inert_contract_steps()); + let crate::LoadedActorStateOf::Active(state) = Actors::load_frame_actor_state(actor_id) else { + panic!("active temporal frame state"); + }; + assert!(state.hot.trigger_wakeup_pointer.is_some()); + let mut replacement = state.contract; + replacement.trigger = Trigger::manual(); + frame_system::Pallet::::set_block_number(2); + + assert_ok!(Actors::update_contract( + RuntimeOrigin::root(), + actor_id, + replacement, + )); + let (location, _, hot, _) = + Actors::load_frame_control_authority(actor_id).expect("updated frame authority exists"); + assert_eq!(location, crate::ActorControlLocation::Unsignaled); + assert!(hot.trigger_wakeup_pointer.is_none()); + assert!(ActorIdentities::::get(actor_id).is_none()); + assert!(Actors::actor_hot(actor_id).is_some()); + assert!(Actors::actor_control_cell(actor_id).is_some()); + }); +} + +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn pause_and_resume_use_frame_authority_with_canonical_control() { + 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_ok!(Actors::pause_actor(RuntimeOrigin::signed(ALICE), actor_id)); + let (_, _, paused_hot, _) = + Actors::load_frame_control_authority(actor_id).expect("paused frame authority exists"); + assert!(paused_hot.lifecycle.is_paused()); + assert!(ActorIdentities::::get(actor_id).is_none()); + assert!(Actors::actor_hot(actor_id).is_some()); + assert!(Actors::actor_control_cell(actor_id).is_some()); + + frame_system::Pallet::::set_block_number(2); + assert_ok!(Actors::resume_actor(RuntimeOrigin::signed(ALICE), actor_id)); + let (_, _, resumed_hot, _) = + Actors::load_frame_control_authority(actor_id).expect("resumed frame authority exists"); + assert_eq!(resumed_hot.lifecycle, ActiveLifecycle::Active); + assert!(ActorIdentities::::get(actor_id).is_none()); + assert!(Actors::actor_hot(actor_id).is_some()); + assert!(Actors::actor_control_cell(actor_id).is_some()); + }); +} + +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn pause_and_resume_fail_closed_without_primary_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()); + Actors::remove_primary_control_cell_inner(actor_id).expect("primary removal succeeds"); + let hot_before = Actors::actor_hot(actor_id); + let events_before = System::events(); + assert_noop!( + Actors::pause_actor(RuntimeOrigin::signed(ALICE), actor_id), + Error::::ActorInvariant + ); + assert_eq!(Actors::actor_hot(actor_id), hot_before); + assert_eq!(System::events(), events_before); + }); + + 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_ok!(Actors::pause_actor(RuntimeOrigin::signed(ALICE), actor_id)); + frame_system::Pallet::::set_block_number(2); + Actors::remove_primary_control_cell_inner(actor_id).expect("paused primary removal succeeds"); + let hot_before = Actors::actor_hot(actor_id); + let events_before = System::events(); + assert_noop!( + Actors::resume_actor(RuntimeOrigin::signed(ALICE), actor_id), + Error::::ActorInvariant + ); + assert_eq!(Actors::actor_hot(actor_id), hot_before); + assert_eq!(System::events(), events_before); + }); +} + +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn permissionless_sweep_fails_closed_without_primary_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()); + Actors::remove_primary_control_cell_inner(actor_id).expect("primary removal succeeds"); + let identity_before = Actors::actor_identity(actor_id); + let hot_before = Actors::actor_hot(actor_id); + let events_before = System::events(); + assert_noop!( + Actors::permissionless_sweep(RuntimeOrigin::signed(CHARLIE), actor_id), + Error::::ActorInvariant + ); + let sweep_ids: BoundedVec::MaxSweepBatch> = + BoundedVec::try_from(vec![actor_id]).expect("batch fits"); + assert_noop!( + Actors::permissionless_sweep_many(RuntimeOrigin::signed(CHARLIE), sweep_ids), + Error::::ActorInvariant + ); + assert_eq!(Actors::actor_identity(actor_id), identity_before); + assert_eq!(Actors::actor_hot(actor_id), hot_before); + assert_eq!(System::events(), events_before); + }); +} + +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn permissionless_sweep_many_rolls_back_an_earlier_close_on_missing_primary() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let terminal = create_system_with(ALICE, manual_schedule(), None, inert_contract_steps()); + set_actor_cycle_nonce_coherent(terminal, u64::MAX); + let corrupt = create_system_with(ALICE, manual_schedule(), None, inert_contract_steps()); + Actors::remove_primary_control_cell_inner(corrupt).expect("primary removal succeeds"); + let corrupt_contract_before = ActorContractHeads::::get(corrupt); + let corrupt_funding_before = ActorFunding::::get(corrupt); + let events_before = System::events(); + let sweep_ids: BoundedVec::MaxSweepBatch> = + BoundedVec::try_from(vec![terminal, corrupt]).expect("batch fits"); + + assert_noop!( + Actors::permissionless_sweep_many(RuntimeOrigin::signed(CHARLIE), sweep_ids), + Error::::ActorInvariant + ); + assert!(Actors::active_actor_state(terminal).is_some()); + assert_eq!( + Actors::actor_identity(terminal) + .expect("terminal identity rolls back") + .cycle_nonce, + u64::MAX + ); + assert!(Actors::actor_identity(corrupt).is_none()); + assert!(Actors::actor_hot(corrupt).is_none()); + assert!(!ActorControlLocators::::contains_key(corrupt)); + assert!(!crate::ActorUnsignaledControlCells::::contains_key( + corrupt + )); + assert_eq!( + ActorContractHeads::::get(corrupt), + corrupt_contract_before + ); + assert_eq!(ActorFunding::::get(corrupt), corrupt_funding_before); + assert_eq!(System::events(), events_before); + }); +} + +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn close_and_deactivate_fail_closed_without_primary_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()); + Actors::remove_primary_control_cell_inner(actor_id).expect("primary removal succeeds"); + let identity_before = Actors::actor_identity(actor_id); + let hot_before = Actors::actor_hot(actor_id); + let events_before = System::events(); + assert_noop!( + Actors::close_actor(RuntimeOrigin::signed(ALICE), actor_id), + Error::::ActorInvariant + ); + assert_eq!(Actors::actor_identity(actor_id), identity_before); + assert_eq!(Actors::actor_hot(actor_id), hot_before); + assert_eq!(System::events(), events_before); + }); + + 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()); + Actors::remove_primary_control_cell_inner(actor_id).expect("primary removal succeeds"); + let identity_before = Actors::actor_identity(actor_id); + let hot_before = Actors::actor_hot(actor_id); + let events_before = System::events(); + assert_noop!( + Actors::deactivate_actor(RuntimeOrigin::signed(ALICE), actor_id), + Error::::ActorInvariant + ); + assert_eq!(Actors::actor_identity(actor_id), identity_before); + assert_eq!(Actors::actor_hot(actor_id), hot_before); + assert_eq!(System::events(), events_before); + }); +} + +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn active_reactivation_fails_closed_without_primary_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 contract = Actors::load_actor_contract(actor_id).expect("active Contract exists"); + Actors::remove_primary_control_cell_inner(actor_id).expect("primary removal succeeds"); + let identity_before = Actors::actor_identity(actor_id); + let hot_before = Actors::actor_hot(actor_id); + let events_before = System::events(); + + assert_noop!( + Actors::activate_actor(RuntimeOrigin::signed(ALICE), actor_id, contract), + Error::::ActorInvariant + ); + assert_eq!(Actors::actor_identity(actor_id), identity_before); + assert_eq!(Actors::actor_hot(actor_id), hot_before); + assert_eq!(System::events(), events_before); + }); +} + +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn contract_update_fails_closed_without_primary_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 mut replacement = Actors::load_actor_contract(actor_id).expect("active Contract exists"); + replacement.steps = transfer_contract_steps(BOB, 1); + replacement.completion = crate::CompletionPolicy::Persistent; + Actors::remove_primary_control_cell_inner(actor_id).expect("primary removal succeeds"); + let contract_before = Actors::load_actor_contract(actor_id); + let admission_before = Actors::actor_control_cell(actor_id).map(|(_, cell)| cell.admission); + let hot_before = Actors::actor_hot(actor_id); + let events_before = System::events(); + + assert_noop!( + Actors::update_contract(RuntimeOrigin::signed(ALICE), actor_id, replacement,), + Error::::ActorInvariant + ); + assert_eq!(Actors::load_actor_contract(actor_id), contract_before); + assert_eq!( + Actors::actor_control_cell(actor_id).map(|(_, cell)| cell.admission), + admission_before + ); + assert_eq!(Actors::actor_hot(actor_id), hot_before); + assert_eq!(System::events(), events_before); + }); +} + #[test] fn manual_trigger_persists_across_pause_resume() { new_test_ext().execute_with(|| { @@ -1074,7 +1681,7 @@ fn system_pause_resume_churn_is_limited_for_governance_and_owner_origins() { Error::::ControlMutationRateLimited ); assert_eq!( - Actors::actor_identities(actor_id) + Actors::actor_identity(actor_id) .expect("paused System actor identity") .last_control_mutation_block, 1 @@ -1087,7 +1694,7 @@ fn system_pause_resume_churn_is_limited_for_governance_and_owner_origins() { Error::::ControlMutationRateLimited ); assert_eq!( - Actors::actor_identities(actor_id) + Actors::actor_identity(actor_id) .expect("active System actor identity") .last_control_mutation_block, 2 @@ -1116,7 +1723,7 @@ fn paused_head_uses_complete_loaded_state_admission() { let paused = Actors::actor_hot(actor_id).expect("paused actor"); assert!(paused.pending_signal); - assert_eq!(Actors::actor_identities(actor_id).expect("identity").cycle_nonce, 0); + assert_eq!(Actors::actor_identity(actor_id).expect("identity").cycle_nonce, 0); assert!(paused.queue_ticket.is_none()); }); } @@ -1196,6 +1803,14 @@ fn manual_trigger_waits_for_schedule_window_without_second_signal() { assert_eq!(scheduled_wakeup_block(actor_id), Some(10)); frame_system::Pallet::::set_block_number(10); run_idle(Weight::MAX); + #[cfg(not(feature = "runtime-benchmarks"))] + { + let locator = crate::ActorControlLocators::::get(actor_id); + assert!( + matches!(locator, Some(crate::ActorControlLocation::Waiting { .. })), + "post-cycle locator: {locator:?}" + ); + } assert_eq!( Actors::active_actor_view(actor_id) .expect("Actors exists") @@ -1246,7 +1861,8 @@ fn manual_trigger_is_preserved_on_proof_size_defer() { contract_steps_with_step(make_step(task)), ); assert_ok!(Actors::manual_trigger(RuntimeOrigin::signed(ALICE), actor_id)); - let instance = Actors::active_actor_view(actor_id).expect("Actors exists"); + let (_, cell) = Actors::actor_control_cell(actor_id).expect("current control owner exists"); + let step = cell.resources.control.saturating_add(cell.resources.effect); let queue_weight = <::WeightInfo as crate::WeightInfo>::scheduler_paged_tombstone_drain(1) .saturating_add(Actors::scheduler_actor_probe_weight_upper()) .saturating_add( @@ -1255,7 +1871,7 @@ fn manual_trigger_is_preserved_on_proof_size_defer() { ); let proof_limit = queue_weight .proof_size() - .saturating_add(Actors::attempt_weight_upper_bound(&instance, 0).proof_size()) + .saturating_add(step.proof_size()) .saturating_sub(1); Actors::execute_cycle(Weight::from_parts(u64::MAX, proof_limit)); let instance = Actors::active_actor_view(actor_id).expect("Actors exists"); @@ -1287,7 +1903,8 @@ fn typed_ingress_preflight_and_notify_close_permanent_exhaustion() { assert_ok!(Actors::preflight_ingress(&event)); // Monotonic ticket namespace at the ceiling closes through the canonical // SchedulerIndexExhausted owner (spec 5.3, 6.2). - crate::NextQueueTicket::::put(u64::MAX); + crate::ActorReadyHead::::put(u64::MAX); + crate::ActorReadyTail::::put(u64::MAX); let actor_before = native_balance(&sovereign); assert_ok!(Actors::notify_ingress(&event)); assert_eq!( @@ -1356,7 +1973,7 @@ fn exact_update_noops_preserve_all_actor_state_and_emit_nothing() { new_test_ext().execute_with(|| { let encoded_actor_state = |actor_id| { ( - ActorHot::::get(actor_id).encode(), + Actors::actor_hot(actor_id).encode(), Actors::load_actor_contract(actor_id).encode(), crate::ActorFunding::::get(actor_id).encode(), ActorRunStateStore::::get(actor_id).encode(), @@ -1441,6 +2058,203 @@ fn exact_update_noops_preserve_all_actor_state_and_emit_nothing() { }); } +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn authored_cancellation_restores_frame_authority_with_canonical_control() { + new_test_ext().execute_with(|| { + let actor_id = create_suspended_system_retry(9); + frame_system::Pallet::::set_block_number(10); + run_idle(Weight::MAX); + assert!(matches!( + ActorControlLocators::::get(actor_id), + Some(crate::ActorControlLocation::Waiting { .. }) + )); + + assert_ok!(Actors::cancel_run(RuntimeOrigin::signed(ALICE), actor_id,)); + assert!(ActorRunStateStore::::get(actor_id).is_none()); + assert!(ActorIdentities::::get(actor_id).is_none()); + assert!(Actors::actor_control_cell(actor_id).is_some()); + assert!(Actors::actor_hot(actor_id).is_some()); + assert_eq!( + ActorControlLocators::::get(actor_id), + Some(crate::ActorControlLocation::Unsignaled) + ); + let (_, _, hot, _) = + Actors::load_frame_control_authority(actor_id).expect("frame authority is restored"); + assert_eq!(hot.cycle_state, CycleState::Idle); + assert!(hot.wakeup_pointer.is_none()); + assert!(hot.queue_ticket.is_none()); + }); +} + +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn deactivation_uses_frame_authority_with_canonical_control() { + new_test_ext().execute_with(|| { + let actor_id = create_suspended_system_retry(9); + frame_system::Pallet::::set_block_number(10); + run_idle(Weight::MAX); + + assert_ok!(Actors::deactivate_actor(RuntimeOrigin::root(), actor_id)); + assert!(ActorControlLocators::::get(actor_id).is_none()); + assert!(Actors::actor_hot(actor_id).is_none()); + assert!( + Actors::actor_control_cell(actor_id) + .map(|(_, cell)| cell.admission) + .is_none() + ); + assert!(matches!( + Actors::load_actor_state(actor_id), + crate::LoadedActorStateOf::Dormant(_) + )); + }); +} + +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn authored_ready_close_removes_frame_authority_with_canonical_control() { + 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(make_step(Task::StopCycle)), + ); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + assert!(matches!( + ActorControlLocators::::get(actor_id), + Some(ActorControlLocation::Ready { .. }) + )); + frame_system::Pallet::::set_block_number(2); + + assert_ok!(Actors::close_actor(RuntimeOrigin::signed(ALICE), actor_id)); + + assert!(ActorControlLocators::::get(actor_id).is_none()); + assert!(ActorIdentities::::get(actor_id).is_none()); + assert!(Actors::actor_hot(actor_id).is_none()); + assert!( + Actors::actor_control_cell(actor_id) + .map(|(_, cell)| cell.admission) + .is_none() + ); + assert!(matches!( + Actors::load_actor_state(actor_id), + crate::LoadedActorStateOf::NotRegistered + )); + #[cfg(feature = "try-runtime")] + assert_ok!(crate::Pallet::::do_try_state()); + }); +} + +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn cadenced_waiting_deactivation_invalidates_frame_wakeup_with_canonical_control() { + 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()); + assert_eq!(scheduled_wakeup_block(actor_id), Some(2)); + assert!(matches!( + ActorControlLocators::::get(actor_id), + Some(ActorControlLocation::Waiting { .. }) + )); + frame_system::Pallet::::set_block_number(2); + + assert_ok!(Actors::deactivate_actor(RuntimeOrigin::root(), actor_id)); + + assert!(ActorControlLocators::::get(actor_id).is_none()); + assert_eq!(scheduled_wakeup_block(actor_id), None); + assert!(matches!( + Actors::load_actor_state(actor_id), + crate::LoadedActorStateOf::Dormant(_) + )); + #[cfg(feature = "try-runtime")] + assert_ok!(crate::Pallet::::do_try_state()); + }); +} + +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn ready_deactivation_consumes_frame_ticket_with_canonical_control() { + 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(make_step(Task::StopCycle)), + ); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + assert!(matches!( + ActorControlLocators::::get(actor_id), + Some(ActorControlLocation::Ready { .. }) + )); + frame_system::Pallet::::set_block_number(2); + + assert_ok!(Actors::deactivate_actor(RuntimeOrigin::root(), actor_id)); + + assert!(ActorControlLocators::::get(actor_id).is_none()); + assert!(Actors::actor_hot(actor_id).is_none()); + assert!( + Actors::actor_control_cell(actor_id) + .map(|(_, cell)| cell.admission) + .is_none() + ); + assert!(matches!( + Actors::load_actor_state(actor_id), + crate::LoadedActorStateOf::Dormant(_) + )); + #[cfg(feature = "try-runtime")] + assert_ok!(crate::Pallet::::do_try_state()); + }); +} + +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn crossing_deactivation_removes_membership_with_canonical_control() { + 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_system_with( + ALICE, + Schedule { + trigger: RuntimeTrigger::observation_crossing(7, CrossingDirection::Rising, 100, 80), + cooldown_blocks: 0, + }, + None, + inert_contract_steps(), + ); + assert!(crate::CrossingMemberships::::contains_key(actor_id)); + + assert_ok!(Actors::deactivate_actor(RuntimeOrigin::root(), actor_id)); + assert!(!crate::CrossingMemberships::::contains_key(actor_id)); + assert!(ActorControlLocators::::get(actor_id).is_none()); + assert!(Actors::actor_hot(actor_id).is_none()); + assert!( + Actors::actor_control_cell(actor_id) + .map(|(_, cell)| cell.admission) + .is_none() + ); + assert!(matches!( + Actors::load_actor_state(actor_id), + crate::LoadedActorStateOf::Dormant(_) + )); + }); +} + #[test] fn contract_policy_schedule_deactivation_and_close_cancel_with_typed_reasons() { new_test_ext().execute_with(|| { @@ -1711,12 +2525,8 @@ fn permissionless_sweep_many_closes_multiple_and_reports_counts() { transfer_contract_steps(ALICE, 1), ); 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; - }); + set_actor_cycle_nonce_coherent(user_a, u64::MAX); + set_actor_cycle_nonce_coherent(user_b, u64::MAX); let system_alive = create_system_with( ALICE, manual_schedule(), @@ -1767,12 +2577,8 @@ fn permissionless_sweep_many_rolls_back_prior_closes_on_late_failure() { transfer_contract_steps(ALICE, 1), ); 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; - }); + set_actor_cycle_nonce_coherent(user_a, u64::MAX); + set_actor_cycle_nonce_coherent(user_b, u64::MAX); OwnerSlotBitmaps::::remove(BOB); let sweep_ids: BoundedVec::MaxSweepBatch> = BoundedVec::try_from(vec![user_a, user_b]).expect("batch fits"); @@ -1848,8 +2654,7 @@ fn notify_address_event_updates_accumulator_without_resuming_paused_system_actor let actor_id = create_system_with(ALICE, manual_schedule(), None, contract_steps); let actor = sovereign_account(actor_id); fund_native(actor_id, 500); - ActorHot::::mutate(actor_id, |maybe| { - let hot = maybe.as_mut().expect("Actors hot state exists"); + mutate_actor_hot_coherent(actor_id, |hot| { hot.lifecycle = ActiveLifecycle::Paused; }); assert_ok!(Actors::notify_address_event( @@ -1898,6 +2703,366 @@ fn actor_id_overflow_at_max() { }); } +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn actor_id_collision_check_uses_frame_authority_with_canonical_control() { + 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::active_actor_exists(actor_id)); + + let next_actor_id = NextActorId::::get(); + NextActorId::::put(actor_id); + assert_noop!( + Actors::create_system_actor( + RuntimeOrigin::root(), + ALICE, + Mutability::Mutable, + system_active_contract(manual_schedule(), None, inert_contract_steps()), + ), + Error::::ActorIdOccupied + ); + NextActorId::::put(next_actor_id); + assert!(Actors::active_actor_exists(actor_id)); + #[cfg(feature = "try-runtime")] + assert_ok!(crate::Pallet::::do_try_state()); + }); +} + +#[test] +fn actor_id_collision_rejects_each_orphan_canonical_partition_without_writes() { + for partition in 0..5 { + for actor_type in [ActorType::User, ActorType::System] { + new_test_ext().execute_with(|| { + let source = create_suspended_system_retry(1); + let target = NextActorId::::get(); + match partition { + 0 => { + let (_, mut cell) = Actors::actor_control_cell(source).expect("source primary"); + cell.actor_id = target; + ActorUnsignaledControlCells::::insert(target, cell); + } + 1 => ActorContractHeads::::insert( + target, + ActorContractHeads::::get(source).expect("source Contract head"), + ), + 2 => ActorFunding::::insert( + target, + ActorFunding::::get(source).expect("source funding"), + ), + 3 => ActorRunHeads::::insert( + target, + ActorRunHeads::::get(source).expect("source Run head"), + ), + 4 => ActorRunPayloads::::insert( + target, + ActorRunPayloads::::get(source).expect("source Run payload"), + ), + _ => unreachable!(), + } + assert!(!ActorControlLocators::::contains_key(target)); + assert!(!ActorIdentities::::contains_key(target)); + assert_noop!( + match actor_type { + ActorType::User => + Actors::create_user_actor(RuntimeOrigin::signed(ALICE), Mutability::Mutable, None), + ActorType::System => + Actors::create_system_actor(RuntimeOrigin::root(), ALICE, Mutability::Mutable, None), + }, + Error::::ActorIdOccupied + ); + }); + } + } +} + +#[cfg(feature = "try-runtime")] +#[test] +fn dormant_immutable_system_audit_requires_genesis_authority() { + new_test_ext().execute_with(|| { + let actor_id = Actors::next_actor_id(); + assert_ok!(Actors::create_system_actor( + RuntimeOrigin::root(), + ALICE, + Mutability::Mutable, + None + )); + assert_ok!(Actors::do_try_state()); + ActorIdentities::::mutate(actor_id, |identity| { + identity + .as_mut() + .expect("public Dormant identity exists") + .mutability = Mutability::Immutable; + }); + let before = polkadot_sdk::sp_io::storage::root(StateVersion::V1); + assert_eq!( + Actors::do_try_state(), + Err(polkadot_sdk::sp_runtime::TryRuntimeError::Other( + "Immutable Dormant System Actor is not declared by genesis", + )), + ); + assert_eq!(polkadot_sdk::sp_io::storage::root(StateVersion::V1), before); + }); +} + +#[test] +fn duplicate_dormant_and_active_identity_rejects_eligibility_and_close() { + new_test_ext().execute_with(|| { + let actor_id = create_system_with(ALICE, manual_schedule(), None, inert_contract_steps()); + let identity = Actors::actor_identity(actor_id).expect("active identity"); + ActorIdentities::::insert(actor_id, identity); + let before = polkadot_sdk::sp_io::storage::root(StateVersion::V1); + assert_eq!( + Actors::actor_eligibility(actor_id), + Err(ActorClassificationError::ActorInvariant) + ); + assert!(Actors::actor_identity(actor_id).is_none()); + assert_noop!( + Actors::close_actor(RuntimeOrigin::root(), actor_id), + Error::::ActorInvariant + ); + assert_noop!( + Actors::resume_actor(RuntimeOrigin::root(), actor_id), + Error::::ActorInvariant + ); + assert_eq!(polkadot_sdk::sp_io::storage::root(StateVersion::V1), before); + }); +} + +#[test] +fn eligibility_rejects_partial_or_mismatched_run_tiers_in_nonrunning_states() { + for identity_state in 0..3 { + for run_shape in 0..3 { + new_test_ext().execute_with(|| { + let source = create_suspended_system_retry(1); + let target = NextActorId::::get(); + match identity_state { + 0 => assert_eq!(eligibility(target), ActorEligibility::NotRegistered), + 1 => { + assert_ok!(Actors::create_system_actor( + RuntimeOrigin::root(), + BOB, + Mutability::Mutable, + None + )); + assert_eq!(eligibility(target), ActorEligibility::Dormant); + } + 2 => { + assert_eq!( + create_system_with(BOB, manual_schedule(), None, inert_contract_steps()), + target + ); + assert!(matches!(eligibility(target), ActorEligibility::Active(_))); + } + _ => unreachable!(), + } + if run_shape != 0 { + let mut head = ActorRunHeads::::get(source).expect("source Run head"); + if run_shape == 2 { + head.payload_commitment[0] ^= 1; + } + ActorRunHeads::::insert(target, head); + } + if run_shape != 1 { + ActorRunPayloads::::insert( + target, + ActorRunPayloads::::get(source).expect("source Run payload"), + ); + } + assert!(ActorRunStateStore::::get(target).is_none()); + let before = polkadot_sdk::sp_io::storage::root(StateVersion::V1); + assert_eq!( + Actors::actor_eligibility(target), + Err(ActorClassificationError::ActorInvariant) + ); + assert_noop!( + Actors::resume_actor(RuntimeOrigin::root(), target), + Error::::ActorInvariant + ); + assert_eq!(polkadot_sdk::sp_io::storage::root(StateVersion::V1), before); + }); + } + } +} + +#[test] +fn idle_close_removes_terminal_and_temporal_waiting_owners() { + for cadenced in [false, true] { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = create_system_with( + ALICE, + if cadenced { + timer_schedule(20) + } else { + manual_schedule() + }, + (!cadenced).then_some(ScheduleWindow { start: 1, end: 101 }), + inert_contract_steps(), + ); + let (location, cell) = Actors::actor_control_cell(actor_id).expect("idle primary"); + assert_eq!(cell.hot.cycle_state, CycleState::Idle); + let ActorControlLocation::Waiting { key, page, slot } = location else { + panic!("deadline owns the Idle primary"); + }; + if cadenced { + let pointer = cell.hot.trigger_wakeup_pointer.expect("temporal pointer"); + assert_eq!( + (key, page, u32::from(slot)), + (WakeupKey::Tick(pointer.tick), pointer.page_id, pointer.slot) + ); + assert!(cell.hot.wakeup_pointer.is_none()); + } else { + let pointer = cell.hot.wakeup_pointer.expect("terminal pointer"); + assert_eq!(pointer.block, WakeupKey::Block(102)); + assert_eq!( + (key, page, u32::from(slot)), + (pointer.block, pointer.page_id, pointer.slot) + ); + } + let page = crate::ActorWaitingFrameChunks::::get((key, page)).expect("Waiting page"); + assert!(matches!( + page.entries[slot as usize], + Some(crate::ActorWaitingEntry::Primary(_)) + )); + assert_eq!(cell.hot.trigger_wakeup_pointer.is_some(), cadenced); + assert_ok!(Actors::close_actor(RuntimeOrigin::root(), actor_id)); + assert_eq!(Actors::active_actor_count(), 0); + assert_eq!(Actors::actor_identity_count(), 0); + assert_eq!(ActorControlLocators::::iter_keys().count(), 0); + assert_eq!( + crate::ActorWaitingFrameChunks::::iter_keys().count(), + 0 + ); + assert_eq!(crate::ActorWaitingHeads::::iter_keys().count(), 0); + assert_eq!(crate::ActorWaitingTails::::iter_keys().count(), 0); + assert_eq!( + crate::ActorWaitingOccupancies::::iter_keys().count(), + 0 + ); + assert_eq!( + crate::ActorWaitingCursorIndices::::iter_keys().count(), + 0 + ); + for clock in [WakeupClock::Block, WakeupClock::Tick] { + assert_eq!(crate::WakeupCursorLen::::get(clock), 0); + } + assert_eq!(crate::WakeupCursorPages::::iter_keys().count(), 0); + }); + } +} + +#[test] +fn idle_contract_replacement_rebinds_retained_waiting_admission_and_executes_due_work() { + for cadenced in [false, true] { + for policy_only in [false, true] { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = create_system_with( + ALICE, + if cadenced { + timer_schedule(20) + } else { + manual_schedule() + }, + (!cadenced).then_some(ScheduleWindow { start: 1, end: 101 }), + inert_contract_steps(), + ); + let (_, before) = Actors::actor_control_cell(actor_id).expect("idle primary"); + let mut contract = Actors::load_actor_contract(actor_id).expect("Contract"); + if policy_only { + contract.completion = crate::CompletionPolicy::CloseAfterProductiveCycle; + } else { + contract + .steps + .try_push(make_step(Task::StopCycle)) + .expect("extra Step fits"); + } + frame_system::Pallet::::set_block_number(2); + assert_ok!(Actors::update_contract( + RuntimeOrigin::root(), + actor_id, + contract + )); + let (_, after) = Actors::actor_control_cell(actor_id).expect("updated primary"); + assert_ne!( + after.admission.admission_identity, + before.admission.admission_identity + ); + assert_eq!(after.hot.wakeup_pointer, before.hot.wakeup_pointer); + assert_eq!( + after.hot.trigger_wakeup_pointer, + before.hot.trigger_wakeup_pointer + ); + let pointers = [ + after.hot.wakeup_pointer, + after + .hot + .trigger_wakeup_pointer + .map(|pointer| WakeupPointer { + block: WakeupKey::Tick(pointer.tick), + page_id: pointer.page_id, + slot: pointer.slot, + }), + ]; + for pointer in pointers.into_iter().flatten() { + let page = crate::ActorWaitingFrameChunks::::get((pointer.block, pointer.page_id)) + .expect("retained Waiting page"); + let (stored_actor, admission) = match page.entries[pointer.slot as usize] + .as_ref() + .expect("retained Waiting entry") + { + crate::ActorWaitingEntry::Primary(cell) => { + (cell.actor_id, cell.admission.admission_identity) + } + crate::ActorWaitingEntry::Reference(reference) => { + (reference.actor_id, reference.admission_identity) + } + }; + assert_eq!(stored_actor, actor_id); + assert_eq!(admission, after.admission.admission_identity); + } + if !cadenced { + assert_ok!(Actors::manual_trigger(RuntimeOrigin::root(), actor_id)); + } + for block in 3..=25 { + frame_system::Pallet::::set_block_number(block); + run_idle(Weight::MAX); + if Actors::actor_identity(actor_id).is_some_and(|identity| identity.cycle_nonce == 1) { + break; + } + } + assert_eq!( + Actors::actor_identity(actor_id) + .expect("first cycle completes") + .cycle_nonce, + 1 + ); + if cadenced { + assert_ok!(Actors::close_actor(RuntimeOrigin::root(), actor_id)); + } else { + frame_system::Pallet::::set_block_number(102); + Actors::on_initialize(102); + run_prepass(); + run_idle(Weight::MAX); + } + assert!( + Actors::actor_identity(actor_id).is_none(), + "cadenced={cadenced} policy_only={policy_only}: locator={:?}, hot={:?}, fault={:?}", + ActorControlLocators::::get(actor_id), + Actors::actor_hot(actor_id), + Actors::wakeup_worker_fault(), + ); + assert_eq!( + crate::ActorWaitingFrameChunks::::iter_keys().count(), + 0 + ); + assert!(Actors::wakeup_worker_fault().is_none()); + }); + } + } +} + #[test] fn control_authority_matrix_is_class_mutability_and_breaker_complete() { new_test_ext().execute_with(|| { @@ -2043,12 +3208,7 @@ fn governance_can_manage_system_actor_control_surface() { updated_view.cooldown_blocks, updated_schedule.cooldown_blocks ); - ActorHot::::mutate(actor_id, |maybe| { - maybe - .as_mut() - .expect("system actor hot state") - .unsuccessful_attempt_streak = 2; - }); + mutate_actor_hot_coherent(actor_id, |hot| hot.unsuccessful_attempt_streak = 2); frame_system::Pallet::::set_block_number(4); let updated_plan = transfer_contract_steps(BOB, 1); assert_ok!(update_contract_partial!( @@ -2497,14 +3657,14 @@ fn active_actors_set_maintains_integrity() { system_active_contract(schedule.clone(), None, inert_plan.clone()), )); } - assert_eq!(ActorHot::::iter_keys().count(), 3); + assert_eq!(ActorControlLocators::::iter_keys().count(), 3); assert!(Actors::active_actor_view(0).is_some()); assert!(Actors::active_actor_view(1).is_some()); assert!(Actors::active_actor_view(2).is_some()); let inst = Actors::active_actor_view(1).unwrap(); let _ = Balances::deposit_creating(&inst.sovereign_account, 1_000_000); assert_ok!(Actors::close_actor(RuntimeOrigin::root(), 1)); - assert_eq!(ActorHot::::iter_keys().count(), 2); + assert_eq!(ActorControlLocators::::iter_keys().count(), 2); assert!(Actors::active_actor_view(0).is_some()); assert!(Actors::active_actor_view(2).is_some()); assert!(Actors::active_actor_view(1).is_none()); @@ -2609,6 +3769,7 @@ fn system_immutable_creation_rejects_manual_but_allows_internal_window_close() { Error::::ImmutableActor ); assert!(Actors::active_actor_view(actor_id).is_some()); + assert!(Actors::actor_control_cell(actor_id).is_some()); frame_system::Pallet::::set_block_number(102); run_idle(Weight::MAX); assert!(Actors::active_actor_view(actor_id).is_none()); @@ -2680,6 +3841,7 @@ fn classifier_projections_agree_on_breaker_terminal_and_paused_products() { Mutability::Mutable, expected_contract.clone(), SimulationMode::FreshCurrentPlan, + ample_simulation_budget(), ), Err(SimulationError::GlobalCircuitBreaker) ); @@ -2699,6 +3861,7 @@ fn classifier_projections_agree_on_breaker_terminal_and_paused_products() { Mutability::Mutable, expected_contract, SimulationMode::FreshCurrentPlan, + ample_simulation_budget(), ) .expect("terminal simulation projects") .status, @@ -2742,6 +3905,7 @@ fn classifier_projections_agree_on_breaker_terminal_and_paused_products() { Mutability::Mutable, expected_contract, SimulationMode::FreshCurrentPlan, + ample_simulation_budget(), ), Err(SimulationError::Paused) ); @@ -2895,11 +4059,8 @@ fn eligibility_projection_reports_failure_limit_auto_close_and_nonce_exhaustion( 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()); - ActorHot::::mutate(actor_id, |maybe| { - maybe - .as_mut() - .expect("active actor") - .unsuccessful_attempt_streak = ::MaxConsecutiveFailures::get(); + mutate_actor_hot_coherent(actor_id, |hot| { + hot.unsuccessful_attempt_streak = ::MaxConsecutiveFailures::get(); }); assert_eq!( active_eligibility(actor_id).terminal_reason, @@ -2935,9 +4096,7 @@ fn eligibility_projection_reports_failure_limit_auto_close_and_nonce_exhaustion( 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()); - ActorIdentities::::mutate(actor_id, |maybe| { - maybe.as_mut().expect("identity").cycle_nonce = u64::MAX; - }); + set_actor_cycle_nonce_coherent(actor_id, u64::MAX); assert_eq!( active_eligibility(actor_id).terminal_reason, Some(CloseReason::CycleNonceExhausted) diff --git a/template/pallets/actors/src/tests/market_tasks.rs b/template/pallets/actors/src/tests/market_tasks.rs index 65b2cbe2..b9dd5b7d 100644 --- a/template/pallets/actors/src/tests/market_tasks.rs +++ b/template/pallets/actors/src/tests/market_tasks.rs @@ -763,18 +763,20 @@ fn create_rejects_zero_liquidity_output_bounds() { fn liquidity_tasks_fail_before_effects_when_output_minima_are_unmet() { new_test_ext().execute_with(|| { frame_system::Pallet::::set_block_number(1); + let mut add_step = make_step(Task::AddLiquidity { + asset_a: TestAsset::Local(1), + asset_b: TestAsset::Local(2), + amount_a: AmountResolution::Fixed(4), + amount_b: AmountResolution::Fixed(4), + min_lp_out: 5, + }); + add_step.on_error = StepErrorPolicy::RetryLater { max_attempts: 2 }; let add_id = create_user_with( ALICE, Mutability::Mutable, manual_schedule(), None, - contract_steps_with_step(make_step(Task::AddLiquidity { - asset_a: TestAsset::Local(1), - asset_b: TestAsset::Local(2), - amount_a: AmountResolution::Fixed(4), - amount_b: AmountResolution::Fixed(4), - min_lp_out: 5, - })), + contract_steps_with_step(add_step), ); fund_native(add_id, 1_000); let add_actor = sovereign_account(add_id); @@ -786,6 +788,18 @@ fn liquidity_tasks_fail_before_effects_when_output_minima_are_unmet() { event, Event::LiquidityAdded { actor_id, .. } if *actor_id == add_id ))); + assert_eq!(asset_balance(&add_actor, TestAsset::Local(1)), 10); + assert_eq!(asset_balance(&add_actor, TestAsset::Local(2)), 10); + let run = Actors::actor_run_state(add_id).expect("dynamic LP output rejection suspends"); + assert_eq!(run.cursor, 0); + assert_eq!(run.unsuccessful_attempts_at_cursor, 1); + assert!(matches!( + run.last_step_outcome, + Some(StepOutcome::Failed(TaskFailure { + retry: RetryClass::Temporary, + .. + })) + )); let remove_id = create_user_with( BOB, @@ -1028,8 +1042,7 @@ fn ordinary_transfer_updates_accumulator_without_resuming_paused_system_actor() amount: AmountResolution::PercentageOfLastFunding(Perbill::from_percent(100)), })); let actor_id = create_system_with(ALICE, manual_schedule(), None, contract_steps); - ActorHot::::mutate(actor_id, |maybe| { - let hot = maybe.as_mut().expect("Actors hot state exists"); + mutate_actor_hot_coherent(actor_id, |hot| { hot.lifecycle = ActiveLifecycle::Paused; }); assert_ok!(ordinary_transfer_to_actor( diff --git a/template/pallets/actors/src/tests/observations.rs b/template/pallets/actors/src/tests/observations.rs index 0c1d1017..07757954 100644 --- a/template/pallets/actors/src/tests/observations.rs +++ b/template/pallets/actors/src/tests/observations.rs @@ -26,9 +26,17 @@ fn observation_activation_placement_snapshot( Actors::request_activation(actor_id) } }; - let mut outcome = activate().expect("activation placement succeeds"); + let mut outcome = activate().unwrap_or_else(|error| { + panic!( + "activation placement succeeds: compact={compact}, window={window:?}, block={activation_block}, error={error:?}" + ) + }); if repeat { - outcome = activate().expect("repeated activation placement succeeds"); + outcome = activate().unwrap_or_else(|error| { + panic!( + "repeated activation placement succeeds: compact={compact}, window={window:?}, block={activation_block}, error={error:?}" + ) + }); } ( outcome, @@ -54,6 +62,111 @@ fn assert_compact_observation_classification_parity( compact_classification } +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn observation_activation_uses_primary_pending_authority() { + 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, + }, + None, + inert_contract_steps(), + ); + let loaded = Actors::load_observation_activation_state(actor_id, 55) + .expect("frame-owned observation state loads"); + assert!(!loaded.hot.pending_signal); + assert_eq!( + Actors::request_observation_activation_compact(actor_id, 55), + Ok(ActivationOutcome::Latched) + ); + let (_, _, frame_hot, _) = + Actors::load_frame_control_authority(actor_id).expect("frame authority remains live"); + assert!(frame_hot.pending_signal); + assert_eq!( + Actors::actor_hot(actor_id).expect("canonical primary remains live"), + frame_hot + ); + }); +} + +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn observation_change_execution_preserves_absent_scalar_control() { + for steps in [inert_contract_steps(), BoundedVec::default()] { + 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, + }, + None, + steps, + ); + + assert_eq!( + Actors::request_observation_activation_compact(actor_id, 55), + Ok(ActivationOutcome::Latched) + ); + run_idle(Weight::MAX); + + assert!(has_actor_event(|event| matches!( + event, + Event::CycleSummary { actor_id: id, .. } if *id == actor_id + ))); + assert!(!ActorIdentities::::contains_key(actor_id)); + #[cfg(feature = "try-runtime")] + assert_ok!(crate::Pallet::::do_try_state()); + }); + } +} + +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn observation_future_activation_moves_primary_to_waiting_without_scalar_bridge() { + 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, + }, + Some(crate::ScheduleWindow { + start: 10, + end: 200, + }), + inert_contract_steps(), + ); + + assert_eq!( + Actors::request_observation_activation_compact(actor_id, 55), + Ok(ActivationOutcome::Latched) + ); + assert!(matches!( + crate::ActorControlLocators::::get(actor_id), + Some(crate::ActorControlLocation::Waiting { + key: crate::WakeupKey::Block(10), + .. + }) + )); + let state = Actors::active_actor_state(actor_id).expect("Waiting primary remains active"); + assert!(state.hot.pending_signal); + assert_eq!( + state.hot.wakeup_pointer.map(|pointer| pointer.block), + Some(crate::WakeupKey::Block(10)) + ); + assert!(!ActorIdentities::::contains_key(actor_id)); + #[cfg(feature = "try-runtime")] + assert_ok!(crate::Pallet::::do_try_state()); + }); +} + #[test] fn observation_only_sources_admit_non_trigger_amount_resolutions() { new_test_ext().execute_with(|| { @@ -148,9 +261,7 @@ fn compact_observation_classification_matches_terminal_precedence() { None, inert_contract_steps(), ); - ActorIdentities::::mutate(nonce_actor, |maybe| { - maybe.as_mut().expect("identity exists").cycle_nonce = u64::MAX; - }); + set_actor_cycle_nonce_coherent(nonce_actor, u64::MAX); assert_eq!( assert_compact_observation_classification_parity(nonce_actor, 9).terminal_reason, Some(CloseReason::CycleNonceExhausted) @@ -243,11 +354,8 @@ fn compact_observation_classification_matches_failure_auto_close_pause_and_break None, inert_contract_steps(), ); - ActorHot::::mutate(failure_actor, |maybe| { - maybe - .as_mut() - .expect("active Actor exists") - .unsuccessful_attempt_streak = ::MaxConsecutiveFailures::get(); + mutate_actor_hot_coherent(failure_actor, |hot| { + hot.unsuccessful_attempt_streak = ::MaxConsecutiveFailures::get(); }); assert_eq!( assert_compact_observation_classification_parity(failure_actor, 12).terminal_reason, @@ -334,7 +442,6 @@ fn compact_observation_activation_loads_only_current_authority_tiers() { 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) @@ -380,7 +487,8 @@ fn observation_subscriptions_follow_schedule_lifecycle_exactly() { 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) + let certificate = Actors::actor_control_cell(actor_id) + .map(|(_, cell)| cell.admission) .expect("ObservationChange admission certificate"); assert_eq!(authority.feed, 1); assert_eq!( @@ -1062,9 +1170,7 @@ fn fanout_fault_captures_exact_page_position_actor_c6_authority_and_branch() { 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); - }); + mutate_actor_hot_coherent(second, |hot| hot.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)); @@ -1075,16 +1181,7 @@ fn fanout_fault_captures_exact_page_position_actor_c6_authority_and_branch() { 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, - }, - ); + crate::ActorWaitingOccupancies::::insert(WakeupKey::Block(101), 1); Actors::fanout_dirty_observations(Weight::MAX); assert_eq!( @@ -1196,7 +1293,7 @@ fn observation_change_charges_occurrence_before_pipeline_opening() { assert!(hot.pending_signal); assert!(hot.queue_ticket.is_some()); assert_eq!( - Actors::actor_identities(actor_id) + Actors::actor_identity(actor_id) .expect("identity remains") .cycle_nonce, 0 @@ -1453,9 +1550,7 @@ fn contiguous_observation_wakeup_run_commits_once() { .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); - }); + mutate_actor_hot_coherent(*actor_id, |hot| hot.last_cycle_block = Some(1)); } assert_ok!(Actors::note_observation_changed(24, 1)); Actors::test_reset_observation_wakeup_cohort_commits(); @@ -1475,6 +1570,42 @@ fn contiguous_observation_wakeup_run_commits_once() { }); } +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn observation_wakeup_cohort_preserves_absent_scalar_control() { + 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 { + mutate_actor_hot_coherent(*actor_id, |hot| hot.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 state = Actors::active_actor_state(actor_id).expect("Waiting primary remains active"); + assert!(state.hot.pending_signal); + assert!(state.hot.queue_ticket.is_none()); + assert_eq!( + state.hot.wakeup_pointer.map(|pointer| pointer.block), + Some(WakeupKey::Block(101)) + ); + assert!(!ActorIdentities::::contains_key(actor_id)); + } + #[cfg(feature = "try-runtime")] + assert_ok!(crate::Pallet::::do_try_state()); + }); +} + #[test] fn saturated_queue_materializes_fanout_through_the_canonical_deferred_wakeup() { new_test_ext().execute_with(|| { @@ -1485,28 +1616,7 @@ fn saturated_queue_materializes_fanout_through_the_canonical_deferred_wakeup() { None, inert_contract_steps(), ); - let page_size: u32 = ::QueuePageSize::get(); - let capacity: u32 = ::MaxQueueLength::get(); - for page_id in 0..capacity.div_ceil(page_size) { - let first = page_id.saturating_mul(page_size); - let len = page_size.min(capacity.saturating_sub(first)); - let entries = (0..len) - .map(|offset| { - queue_entry( - u64::from(first.saturating_add(offset)), - 20_000_000u64.saturating_add(u64::from(first.saturating_add(offset))), - ) - }) - .collect::>(); - QueuePages::::insert( - u64::from(page_id), - BoundedVec::try_from(entries).expect("saturated queue page fits"), - ); - } - QueueHead::::put(0); - QueueTail::::put(u64::from(capacity)); - QueueOccupancy::::put(capacity); - crate::NextQueueTicket::::put(u64::from(capacity)); + seed_saturated_tombstone_queue(); assert_ok!(Actors::note_observation_changed(16, 1)); let base = <::WeightInfo as crate::WeightInfo>::observation_fanout_base(); @@ -1519,13 +1629,27 @@ fn saturated_queue_materializes_fanout_through_the_canonical_deferred_wakeup() { assert!(Actors::dirty_observation_feeds(16).is_none()); assert!(Actors::pending_signal(actor_id)); assert!( - Actors::actor_hot(actor_id) + Actors::active_actor_state(actor_id) .expect("actor") + .hot .queue_ticket .is_none() ); - - assert_eq!(scheduled_wakeup_block(actor_id), Some(2)); + assert_eq!( + Actors::active_actor_state(actor_id) + .and_then(|state| state.hot.wakeup_pointer) + .and_then(|pointer| match pointer.block { + WakeupKey::Block(block) => Some(block), + WakeupKey::Tick(_) => None, + }), + Some(2) + ); + #[cfg(not(feature = "runtime-benchmarks"))] + { + assert!(!ActorIdentities::::contains_key(actor_id)); + } + #[cfg(feature = "try-runtime")] + assert_ok!(crate::Pallet::::do_try_state()); }); } @@ -1543,9 +1667,7 @@ fn fanout_position_cursor_does_not_replay_a_committed_page_prefix() { 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); - }); + mutate_actor_hot_coherent(second, |hot| hot.last_cycle_block = Some(1)); Actors::test_fail_wakeup_placement_with_capacity(); assert_ok!(Actors::note_observation_changed(18, 1)); @@ -1620,7 +1742,7 @@ fn on_idle_fanout_feeds_the_existing_scheduler_without_direct_execution() { ); assert_ok!(Actors::note_observation_changed(14, 1)); assert_eq!( - Actors::actor_identities(actor_id) + Actors::actor_identity(actor_id) .expect("identity") .cycle_nonce, 0 @@ -1632,7 +1754,7 @@ fn on_idle_fanout_feeds_the_existing_scheduler_without_direct_execution() { assert!(Actors::dirty_observation_feeds(14).is_none()); let after = Actors::actor_hot(actor_id).expect("actor survives productive cycle"); assert_eq!( - Actors::actor_identities(actor_id) + Actors::actor_identity(actor_id) .expect("identity") .cycle_nonce, 1 @@ -1668,9 +1790,6 @@ fn newer_revision_during_fanout_restarts_from_the_first_subscriber_page() { assert_eq!(in_progress.next_subscriber_page, Some(1)); let first = actors[0]; let first_ticket = Actors::actor_hot(first).expect("first actor").queue_ticket; - crate::ActorHot::::mutate(first, |maybe| { - maybe.as_mut().expect("first actor").pending_signal = false; - }); assert_ok!(Actors::note_observation_changed(12, 2)); Actors::fanout_dirty_observations(base.saturating_add(unit).saturating_add(fault)); @@ -1678,10 +1797,10 @@ fn newer_revision_during_fanout_restarts_from_the_first_subscriber_page() { 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)); + assert!(Actors::pending_signal(first)); Actors::fanout_dirty_observations(base.saturating_add(unit).saturating_add(fault)); - assert!(!Actors::pending_signal(first)); + assert!(Actors::pending_signal(first)); assert_eq!( Actors::actor_hot(first).expect("first actor").queue_ticket, first_ticket @@ -1750,12 +1869,6 @@ fn latest_revision_fanout_model_converges_across_seeded_races() { latest_revision += 1; assert_ok!(Actors::note_observation_changed(15, latest_revision)); } - if seed.is_multiple_of(5) { - let index = (seed as usize) % actors.len(); - crate::ActorHot::::mutate(actors[index], |maybe| { - maybe.as_mut().expect("model actor").pending_signal = false; - }); - } process_one(&mut delivered); } for _ in 0..256 { @@ -1994,6 +2107,159 @@ fn try_state_rejects_dirty_observation_list_drift() { }); } +#[cfg(feature = "try-runtime")] +#[test] +fn try_state_rejects_coherent_subscription_beyond_slot_frontier() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = create_system_with( + ALICE, + observation_schedule(vec![7]), + None, + inert_contract_steps(), + ); + assert_ok!(Actors::do_try_state()); + assert_eq!( + crate::ObservationSubscriptionSlot::::get(actor_id), + Some(0) + ); + assert_eq!(crate::NextObservationSubscriptionSlot::::get(), 1); + crate::ObservationSubscriptionSlot::::insert(actor_id, 1); + crate::ObservationSubscriptionSlotOwner::::remove(0); + crate::ObservationSubscriptionSlotOwner::::insert(1, actor_id); + crate::ObservationSubscriberPages::::mutate(7, 0, |page| { + let page = page.as_mut().expect("admitted subscriber page"); + page.entries[0] = None; + page + .entries + .try_push(Some(actor_id)) + .expect("second slot fits"); + }); + assert!( + Actors::do_try_state().is_err(), + "subscription beyond allocation frontier was accepted" + ); + crate::NextObservationSubscriptionSlot::::put(2); + crate::ObservationFreeSlotLen::::put(1); + crate::ObservationFreeSlotPages::::insert( + 0, + BoundedVec::try_from(vec![0]).expect("one free slot fits"), + ); + assert_ok!(Actors::do_try_state()); + }); +} + +#[cfg(feature = "try-runtime")] +#[test] +fn try_state_rejects_mispartitioned_observation_free_pages() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let page_size = ::ObservationPageSize::get(); + let actors: Vec<_> = (0..=page_size) + .map(|_| { + create_system_with( + ALICE, + observation_schedule(vec![7]), + None, + inert_contract_steps(), + ) + }) + .collect(); + for actor_id in actors { + assert_ok!(Actors::close_actor(RuntimeOrigin::signed(ALICE), actor_id)); + } + assert_ok!(Actors::do_try_state()); + let first = crate::ObservationFreeSlotPages::::get(0).expect("full free page"); + let last = crate::ObservationFreeSlotPages::::get(1).expect("partial free page"); + let mut short = first.clone(); + let mut long = last.clone(); + long + .try_push(short.pop().expect("full page has a slot")) + .expect("shifted slot fits"); + crate::ObservationFreeSlotPages::::insert(0, short); + crate::ObservationFreeSlotPages::::insert(1, long); + assert!( + Actors::do_try_state().is_err(), + "incorrect free-page lengths preserved aggregate accounting" + ); + crate::ObservationFreeSlotPages::::insert(0, first); + crate::ObservationFreeSlotPages::::insert(1, last); + assert_ok!(Actors::do_try_state()); + }); +} + +#[cfg(feature = "try-runtime")] +#[test] +fn try_state_binds_complete_subscription_membership_to_the_trigger() { + for (source_feed, replacement_feed) in [(None, Some(7)), (Some(7), None), (Some(7), Some(8))] { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let schedule = + source_feed.map_or_else(manual_schedule, |feed| observation_schedule(vec![feed])); + let actor_id = create_system_with(ALICE, schedule, None, inert_contract_steps()); + assert_ok!(Actors::do_try_state()); + let replacement = BoundedVec::try_from(replacement_feed.into_iter().collect::>()) + .expect("at most one feed"); + assert_ok!(Actors::commit_observation_subscription_replace( + actor_id, + replacement + )); + assert!( + Actors::do_try_state().is_err(), + "Trigger {source_feed:?} accepted membership {replacement_feed:?}" + ); + let original = BoundedVec::try_from(source_feed.into_iter().collect::>()) + .expect("at most one feed"); + assert_ok!(Actors::commit_observation_subscription_replace( + actor_id, original + )); + assert_ok!(Actors::do_try_state()); + }); + } +} + +#[cfg(feature = "try-runtime")] +#[test] +fn try_state_rejects_disconnected_reciprocal_subscription_pages() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let page_size: u32 = ::ObservationPageSize::get(); + for _ in 0..=page_size * 2 { + create_system_with( + ALICE, + observation_schedule(vec![7]), + None, + inert_contract_steps(), + ); + } + assert_ok!(Actors::do_try_state()); + let middle = crate::ObservationSubscriberPages::::get(7, 1).expect("middle page"); + crate::ObservationSubscriberPages::::mutate(7, 0, |page| { + page.as_mut().expect("head page").next = Some(2); + }); + crate::ObservationSubscriberPages::::mutate(7, 2, |page| { + page.as_mut().expect("tail page").previous = Some(0); + }); + crate::ObservationSubscriberPages::::mutate(7, 1, |page| { + let page = page.as_mut().expect("detached page"); + page.previous = Some(1); + page.next = Some(1); + }); + assert!( + Actors::do_try_state().is_err(), + "disconnected reciprocal page was accepted" + ); + crate::ObservationSubscriberPages::::mutate(7, 0, |page| { + page.as_mut().expect("head page").next = Some(1); + }); + crate::ObservationSubscriberPages::::mutate(7, 2, |page| { + page.as_mut().expect("tail page").previous = Some(1); + }); + crate::ObservationSubscriberPages::::insert(7, 1, middle); + assert_ok!(Actors::do_try_state()); + }); +} + #[cfg(feature = "try-runtime")] #[test] fn try_state_rejects_subscription_reverse_index_drift() { diff --git a/template/pallets/actors/src/tests/scheduling.rs b/template/pallets/actors/src/tests/scheduling.rs index bc30a22d..e55ae7a7 100644 --- a/template/pallets/actors/src/tests/scheduling.rs +++ b/template/pallets/actors/src/tests/scheduling.rs @@ -1,6 +1,1104 @@ use super::*; use crate::{FundingProvenance, TriggerCauseProvenance}; +#[test] +fn cancelled_run_auto_close_precedes_insufficient_next_pipeline_fee() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + setup_temporary_retry_pool(); + let mut contract = user_active_contract(manual_schedule(), None, temporary_retry_swap_plan()) + .expect("active retry Contract"); + contract.auto_close_at_cycle_nonce = Some(1); + prefund_active_user_creation(ALICE, &contract.steps); + let actor_id = Actors::next_actor_id(); + assert_ok!(Actors::create_user_actor( + RuntimeOrigin::signed(ALICE), + Mutability::Mutable, + Some(contract) + )); + fund_native(actor_id, 1_000_000_000_000_000); + set_temporary_dex_failure(true); + System::set_block_number(2); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + run_idle(Weight::MAX); + assert!(Actors::actor_run_state(actor_id).is_some()); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + assert_ok!(Actors::cancel_run(RuntimeOrigin::signed(ALICE), actor_id)); + let cancelled = + Actors::active_actor_view(actor_id).expect("deferred readiness survives cancel"); + assert_eq!(cancelled.cycle_nonce, 1); + assert!(cancelled.pending_signal); + assert!(Actors::actor_run_state(actor_id).is_none()); + assert_eq!( + Actors::actor_control_cell(actor_id).and_then(|(_, cell)| cell.eligible_at), + Some(3) + ); + Actors::execute_cycle_to_cutoff(Weight::MAX, Actors::queue_tail()); + assert_eq!(Actors::active_actor_view(actor_id), Some(cancelled)); + let sovereign = sovereign_account(actor_id); + let excess = native_balance(&sovereign) + .checked_sub(TestMinUserBalance::get()) + .expect("funded Actor covers the protected floor"); + deplete_user_sovereign(actor_id, excess); + System::set_block_number(3); + System::reset_events(); + run_idle(Weight::MAX); + assert!(Actors::active_actor_view(actor_id).is_none()); + assert_eq!(native_balance(&sovereign), TestMinUserBalance::get()); + assert!(has_actor_event(|event| matches!( + event, + Event::ActorClosed { actor_id: id, reason: CloseReason::AutoCloseNonceReached } + if *id == actor_id + ))); + assert!(!has_actor_event(|event| matches!( + event, Event::CycleStarted { actor_id: id, .. } if *id == actor_id + ))); + #[cfg(feature = "try-runtime")] + assert_ok!(Actors::do_try_state()); + }); +} + +#[test] +fn user_pipeline_insolvency_closes_before_effect_capacity_deferral() { + for solvent in [false, true] { + for effect_capacity in [ + Weight::from_parts(0, u64::MAX / 2), + Weight::from_parts(u64::MAX / 2, 0), + ] { + new_test_ext().execute_with(|| { + System::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_000_000_000_000); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + let sovereign = sovereign_account(actor_id); + if !solvent { + let excess = native_balance(&sovereign) + .checked_sub(TestMinUserBalance::get()) + .expect("funded Actor covers the protected floor"); + deplete_user_sovereign(actor_id, excess); + } + let balance_before = native_balance(&sovereign); + let recipient_before = native_balance(&BOB); + let actor_before = Actors::active_actor_view(actor_id).expect("paid readiness"); + let limits = crate::SimulationBudget { + actor_control: Weight::from_parts(u64::MAX / 2, u64::MAX / 2), + shared_economic: effect_capacity, + } + .checked_limits() + .expect("independent component limits fit"); + let mut resources = crate::BlockResourceState::new(1); + assert_ok!(resources.begin_prepass()); + assert_ok!(resources.open_external_phase()); + assert_ok!(resources.begin_drain()); + let pass = Actors::execute_cycle_to_cutoff_with_resources( + Weight::MAX, + Actors::queue_tail(), + &mut resources, + limits, + crate::BlockResourceDomain::ActorDrainEffect, + limits.actor_control(), + ); + assert_eq!(resources.outstanding_reservations(), 0); + assert_eq!(resources.usage().actor_effect_used(), Weight::zero()); + assert_eq!( + pass.reconciled_domains(), + Some((pass.consumed, Weight::zero())) + ); + assert_eq!(native_balance(&BOB), recipient_before); + assert_eq!(native_balance(&sovereign), balance_before); + assert!(!has_actor_event(|event| matches!( + event, Event::CycleStarted { actor_id: id, .. } if *id == actor_id + ))); + if solvent { + assert_eq!(Actors::active_actor_view(actor_id), Some(actor_before)); + assert_eq!( + Actors::paged_head_entry().map(|(_, entry)| entry.actor_id), + Some(actor_id) + ); + Actors::execute_cycle_to_cutoff(Weight::MAX, Actors::queue_tail()); + assert_eq!( + Actors::active_actor_view(actor_id).map(|actor| actor.cycle_nonce), + Some(1) + ); + assert_eq!(native_balance(&BOB), recipient_before.saturating_add(10)); + } else { + assert!(Actors::active_actor_view(actor_id).is_none()); + assert!(has_actor_event(|event| matches!( + event, + Event::ActorClosed { actor_id: id, reason: CloseReason::CycleAdmissionInsufficient } + if *id == actor_id + ))); + } + #[cfg(feature = "try-runtime")] + assert_ok!(Actors::do_try_state()); + }); + } + } +} + +#[test] +fn idle_weight_refusal_reads_only_header_and_user_fee_prerequisite() { + for actor_type in [ActorType::System, ActorType::User] { + let mut ext = new_test_ext(); + let (actor_id, cutoff, minimum_probe, header_key, native_key, payload_keys) = + ext.execute_with(|| { + System::set_block_number(1); + let actor_id = match actor_type { + ActorType::System => create_system_with( + ALICE, + manual_schedule(), + None, + transfer_contract_steps(BOB, 10), + ), + ActorType::User => create_user_with( + ALICE, + Mutability::Mutable, + manual_schedule(), + None, + transfer_contract_steps(BOB, 10), + ), + }; + fund_native(actor_id, 1_000_000_000_000_000); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + let minimum_probe = + ::scheduler_paged_tombstone_drain(1) + .saturating_add(Actors::scheduler_actor_state_probe_weight_upper()) + .saturating_add( + ::scheduler_paged_consume_preserve_page() + .max(::scheduler_paged_consume_delete_page()), + ); + ( + actor_id, + Actors::queue_tail(), + minimum_probe, + crate::ActorContractHeads::::hashed_key_for(actor_id), + polkadot_sdk::frame_system::Account::::hashed_key_for(sovereign_account(actor_id)), + [ + crate::ActorFunding::::hashed_key_for(actor_id), + crate::ActorRunHeads::::hashed_key_for(actor_id), + crate::ActorRunPayloads::::hashed_key_for(actor_id), + ], + ) + }); + ext.commit_all().expect("commit fixture before recording"); + let before = ext.execute_with(|| polkadot_sdk::sp_io::storage::root(StateVersion::V1)); + ext.commit_all().expect("commit root calculation"); + for scarce in [ + Weight::from_parts(minimum_probe.ref_time(), u64::MAX), + Weight::from_parts(u64::MAX, minimum_probe.proof_size()), + ] { + for domain in [ + None, + Some(crate::BlockResourceDomain::ActorControl), + Some(crate::BlockResourceDomain::ActorDrainEffect), + ] { + let recorder = polkadot_sdk::sp_trie::recorder::Recorder::< + polkadot_sdk::sp_core::Blake2Hasher, + >::default(); + ext.execute_with_recorder(recorder.clone(), || { + if let Some(domain) = domain { + let ample = Weight::from_parts(u64::MAX / 2, u64::MAX / 2); + let shared_economic = if domain == crate::BlockResourceDomain::ActorDrainEffect { + if scarce.ref_time() == u64::MAX { + Weight::from_parts(ample.ref_time(), 0) + } else { + Weight::from_parts(0, ample.proof_size()) + } + } else { + ample + }; + let limits = crate::SimulationBudget { + actor_control: ample, + shared_economic, + } + .checked_limits() + .expect("independent resource lanes fit"); + let mut resources = crate::BlockResourceState::new(1); + assert_ok!(resources.begin_prepass()); + assert_ok!(resources.open_external_phase()); + assert_ok!(resources.begin_drain()); + let pass = Actors::execute_cycle_to_cutoff_with_resources( + Weight::MAX, + cutoff, + &mut resources, + limits, + crate::BlockResourceDomain::ActorDrainEffect, + if domain == crate::BlockResourceDomain::ActorControl { + scarce.min(ample) + } else { + ample + }, + ); + assert_eq!(resources.outstanding_reservations(), 0); + assert_eq!(resources.usage().actor_effect_used(), Weight::zero()); + assert_eq!(resources.usage().actor_control_used(), pass.consumed); + } else { + Actors::execute_cycle_to_cutoff(scarce, cutoff); + } + }); + let recorded = recorder.recorded_keys(); + let was_read = |key: &[u8]| { + recorded + .values() + .any(|keys| keys.keys().any(|read| read.as_ref() == key)) + }; + assert!( + was_read(&header_key), + "terminal policy requires the Contract header" + ); + assert_eq!( + was_read(&native_key), + actor_type == ActorType::User, + "only User admission requires a native-fee balance read" + ); + for key in &payload_keys { + assert!( + !was_read(key), + "Weight-rejected Idle {actor_type:?} read payload {key:?}" + ); + } + ext.execute_with(|| { + assert_eq!(polkadot_sdk::sp_io::storage::root(StateVersion::V1), before); + assert_eq!( + Actors::paged_head_entry().map(|(_, entry)| entry.actor_id), + Some(actor_id) + ); + }); + ext.commit_all().expect("commit unchanged rejection state"); + } + } + ext.execute_with(|| { + let recipient = native_balance(&BOB); + Actors::execute_cycle_to_cutoff(Weight::MAX, cutoff); + assert_eq!(native_balance(&BOB), recipient + 10); + assert_eq!( + Actors::active_actor_view(actor_id).map(|actor| actor.cycle_nonce), + Some(1) + ); + #[cfg(feature = "try-runtime")] + assert_ok!(Actors::do_try_state()); + }); + } +} + +#[test] +fn suspended_weight_refusal_avoids_run_payload_and_funding_reads() { + for funding_unavailable in [false, true] { + for cursor in [0, 1] { + for actor_type in [ActorType::System, ActorType::User] { + let mut ext = new_test_ext(); + let (actor_id, cutoff, probe, native_key, cold_keys) = ext.execute_with(|| { + System::set_block_number(1); + setup_temporary_retry_pool(); + let mut steps = if cursor == 0 { + Vec::new() + } else { + transfer_contract_steps(BOB, 1).into_inner() + }; + let mut retry = temporary_retry_swap_plan().into_inner(); + retry[0].on_error = StepErrorPolicy::RetryLater { max_attempts: 2 }; + if funding_unavailable { + retry[0].task = Task::SwapIn { + asset_in: TestAsset::Local(77), + asset_out: TestAsset::Native, + amount_in: AmountResolution::Fixed(10), + slippage_tolerance: Perbill::one(), + }; + } + steps.extend(retry); + let steps = BoundedVec::try_from(steps).expect("head or tail retry fits"); + let actor_id = match actor_type { + ActorType::System => create_system_with(ALICE, manual_schedule(), None, steps), + ActorType::User => { + create_user_with(ALICE, Mutability::Mutable, manual_schedule(), None, steps) + } + }; + fund_native(actor_id, 1_000_000_000_000_000); + set_temporary_dex_failure(!funding_unavailable); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + run_idle(Weight::MAX); + let run = + Actors::actor_run_state(actor_id).expect("recoverable failure suspends the cycle"); + assert_eq!( + run.suspension, + Some(if funding_unavailable { + crate::SuspensionReason::FundingUnavailable + } else { + crate::SuspensionReason::Temporary + }) + ); + assert_eq!(run.cursor, cursor); + let due = run.eligible_at; + System::set_block_number(due); + Actors::on_initialize(due); + Actors::on_idle(due, starvation_blocked_budget(actor_id)); + assert_eq!( + Actors::actor_run_state(actor_id) + .expect("retry retained") + .encode(), + run.encode() + ); + assert_eq!( + Actors::paged_head_entry().map(|(_, entry)| entry.actor_id), + Some(actor_id) + ); + let probe = ::scheduler_paged_tombstone_drain(1) + .saturating_add(Actors::scheduler_actor_state_probe_weight_upper()) + .saturating_add( + ::scheduler_paged_consume_preserve_page() + .max(::scheduler_paged_consume_delete_page()), + ); + ( + actor_id, + Actors::queue_tail(), + probe, + polkadot_sdk::frame_system::Account::::hashed_key_for(sovereign_account( + actor_id, + )), + [ + crate::ActorFunding::::hashed_key_for(actor_id), + crate::ActorRunPayloads::::hashed_key_for(actor_id), + ], + ) + }); + ext.commit_all().expect("commit retry fixture"); + let before = ext.execute_with(|| polkadot_sdk::sp_io::storage::root(StateVersion::V1)); + ext.commit_all().expect("commit root calculation"); + for scarce in [ + Weight::from_parts(probe.ref_time(), u64::MAX), + Weight::from_parts(u64::MAX, probe.proof_size()), + ] { + for domain in [ + None, + Some(crate::BlockResourceDomain::ActorControl), + Some(crate::BlockResourceDomain::ActorDrainEffect), + ] { + let recorder = polkadot_sdk::sp_trie::recorder::Recorder::< + polkadot_sdk::sp_core::Blake2Hasher, + >::default(); + ext.execute_with_recorder(recorder.clone(), || { + if let Some(domain) = domain { + let ample = Weight::from_parts(u64::MAX / 2, u64::MAX / 2); + let limits = crate::SimulationBudget { + actor_control: ample, + shared_economic: if domain == crate::BlockResourceDomain::ActorDrainEffect { + if scarce.ref_time() == u64::MAX { + Weight::from_parts(ample.ref_time(), 0) + } else { + Weight::from_parts(0, ample.proof_size()) + } + } else { + ample + }, + } + .checked_limits() + .expect("independent resource lanes fit"); + let mut resources = crate::BlockResourceState::new(System::block_number()); + assert_ok!(resources.begin_prepass()); + assert_ok!(resources.open_external_phase()); + assert_ok!(resources.begin_drain()); + let pass = Actors::execute_cycle_to_cutoff_with_resources( + Weight::MAX, + cutoff, + &mut resources, + limits, + crate::BlockResourceDomain::ActorDrainEffect, + if domain == crate::BlockResourceDomain::ActorControl { + scarce.min(ample) + } else { + ample + }, + ); + assert_eq!(resources.outstanding_reservations(), 0); + assert_eq!(resources.usage().actor_effect_used(), Weight::zero()); + assert_eq!(resources.usage().actor_control_used(), pass.consumed); + } else { + Actors::execute_cycle_to_cutoff(scarce, cutoff); + } + }); + let recorded = recorder.recorded_keys(); + assert_eq!( + recorded.values().any(|keys| keys + .keys() + .any(|read| read.as_ref() == native_key.as_slice())), + actor_type == ActorType::User, + "User retry liability requires its native balance" + ); + for key in &cold_keys { + assert!( + !recorded + .values() + .any(|keys| keys.keys().any(|read| read.as_ref() == key.as_slice())), + "Weight-rejected Suspended {actor_type:?} read cold payload {key:?}" + ); + } + ext.execute_with(|| { + assert_eq!(polkadot_sdk::sp_io::storage::root(StateVersion::V1), before); + assert_eq!( + Actors::paged_head_entry().map(|(_, entry)| entry.actor_id), + Some(actor_id) + ); + }); + ext.commit_all().expect("commit unchanged refusal"); + } + } + ext.execute_with(|| { + set_temporary_dex_failure(false); + if funding_unavailable { + set_asset_balance(&sovereign_account(actor_id), TestAsset::Local(77), 100); + } + Actors::execute_cycle_to_cutoff(Weight::MAX, cutoff); + assert!(Actors::actor_run_state(actor_id).is_none()); + #[cfg(feature = "try-runtime")] + assert_ok!(Actors::do_try_state()); + }); + } + } + } +} + +#[test] +fn suspended_expiry_and_breaker_precede_liability_and_effect_deferral() { + for actor_type in [ActorType::System, ActorType::User] { + for paused in [false, true] { + for breaker in [false, true] { + for effect_capacity in [ + Weight::from_parts(0, u64::MAX / 2), + Weight::from_parts(u64::MAX / 2, 0), + ] { + new_test_ext().execute_with(|| { + System::set_block_number(1); + setup_temporary_retry_pool(); + let schedule = Schedule { trigger: Trigger::manual(), cooldown_blocks: 1 }; + let window = Some(ScheduleWindow { start: 1, end: 101 }); + let actor_id = match actor_type { + ActorType::System => create_system_with(ALICE, schedule, window, temporary_retry_swap_plan()), + ActorType::User => create_user_with(ALICE, Mutability::Mutable, schedule, window, temporary_retry_swap_plan()), + }; + fund_native(actor_id, 1_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("ordinary retry"); + assert_eq!(run.eligible_at, 2); + System::set_block_number(2); + if paused { assert_ok!(Actors::pause_actor(RuntimeOrigin::signed(ALICE), actor_id)); } + if breaker { assert_ok!(Actors::set_global_circuit_breaker(RuntimeOrigin::root(), true)); } + let sovereign = sovereign_account(actor_id); + if actor_type == ActorType::User { + let excess = native_balance(&sovereign).checked_sub(TestMinUserBalance::get()).expect("funded fee floor"); + deplete_user_sovereign(actor_id, excess); + } + assert_eq!(Actors::paged_head_entry().map(|(_, entry)| entry.actor_id), Some(actor_id)); + let custody = native_balance(&sovereign); + clear_fee_collections(); + System::reset_events(); + System::set_block_number(102); + let limits = crate::SimulationBudget { + actor_control: Weight::from_parts(u64::MAX / 2, u64::MAX / 2), + shared_economic: effect_capacity, + }.checked_limits().expect("independent lanes fit"); + let mut resources = crate::BlockResourceState::new(102); + assert_ok!(resources.begin_prepass()); + assert_ok!(resources.open_external_phase()); + assert_ok!(resources.begin_drain()); + let pass = Actors::execute_cycle_to_cutoff_with_resources(Weight::MAX, Actors::queue_tail(), &mut resources, limits, + crate::BlockResourceDomain::ActorDrainEffect, limits.actor_control()); + assert_eq!(resources.outstanding_reservations(), 0); + assert_eq!(resources.usage().actor_effect_used(), Weight::zero()); + assert_eq!(resources.usage().actor_control_used(), pass.consumed); + assert_eq!(native_balance(&sovereign), custody); + assert!(fee_collections().is_empty()); + if breaker { + assert!(Actors::active_actor_view(actor_id).is_some()); + assert_eq!(Actors::actor_run_state(actor_id).expect("breaker preserves retry").encode(), run.encode()); + assert!(!has_actor_event(|event| matches!(event, Event::ActorClosed { actor_id: id, .. } if *id == actor_id))); + } else { + assert!(Actors::active_actor_view(actor_id).is_none()); + assert!(Actors::actor_run_state(actor_id).is_none()); + assert!(has_actor_event(|event| matches!(event, Event::ActorClosed { actor_id: id, reason: CloseReason::WindowExpired } if *id == actor_id))); + } + #[cfg(feature = "try-runtime")] + assert_ok!(Actors::do_try_state()); + }); + } + } + } + } +} + +#[test] +fn running_fifo_head_avoids_cold_reads_before_eligibility_and_weight_admission() { + let mut ext = new_test_ext(); + let (actor_id, due, cutoff, cold_keys) = ext.execute_with(|| { + System::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: BOB, + asset: TestAsset::Native, + amount: AmountResolution::Fixed(2), + }), + ]) + .expect("two Steps fit"); + let actor_id = create_system_with(ALICE, manual_schedule(), None, steps); + fund_native(actor_id, 100); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + Actors::execute_cycle_to_cutoff(Weight::MAX, Actors::queue_tail()); + let run = Actors::actor_run_state(actor_id).expect("real Opening persists its successor"); + assert_eq!(run.cursor, 1); + assert!(run.eligible_at > System::block_number()); + assert_eq!( + Actors::paged_head_entry().map(|(_, entry)| entry.actor_id), + Some(actor_id) + ); + let cold_keys = [ + crate::ActorContractHeads::::hashed_key_for(actor_id), + crate::ActorContractTailChunks::::hashed_key_for(actor_id, 0), + crate::ActorFunding::::hashed_key_for(actor_id), + crate::ActorRunPayloads::::hashed_key_for(actor_id), + ]; + (actor_id, run.eligible_at, Actors::queue_tail(), cold_keys) + }); + ext + .commit_all() + .expect("fixture overlay commits before read recording"); + let before = ext.execute_with(|| polkadot_sdk::sp_io::storage::root(StateVersion::V1)); + ext + .commit_all() + .expect("root calculation leaves no overlay authority"); + let recorder = + polkadot_sdk::sp_trie::recorder::Recorder::::default(); + ext.execute_with_recorder(recorder.clone(), || { + Actors::execute_cycle_to_cutoff(Weight::MAX, cutoff); + }); + let recorded = recorder.recorded_keys(); + for key in &cold_keys { + assert!( + !recorded + .values() + .any(|keys| keys.keys().any(|read| read.as_ref() == key.as_slice())), + "future head read cold key {key:?}" + ); + } + ext.execute_with(|| { + assert_eq!(polkadot_sdk::sp_io::storage::root(StateVersion::V1), before); + assert_eq!( + Actors::actor_run_state(actor_id).map(|run| run.cursor), + Some(1) + ); + System::set_block_number(due); + }); + ext + .commit_all() + .expect("eligible fixture overlay commits before positive control"); + let (minimum_probe, eligible_root, step_resources) = ext.execute_with(|| { + let probe = ::scheduler_paged_tombstone_drain(1) + .saturating_add(Actors::scheduler_actor_state_probe_weight_upper()) + .saturating_add( + ::scheduler_paged_consume_preserve_page() + .max(::scheduler_paged_consume_delete_page()), + ); + let resources = Actors::actor_control_cell(actor_id) + .expect("canonical Running primary") + .1 + .resources; + ( + probe, + polkadot_sdk::sp_io::storage::root(StateVersion::V1), + resources, + ) + }); + ext + .commit_all() + .expect("eligible root commits before recording"); + for scarce in [ + Weight::from_parts(minimum_probe.ref_time(), u64::MAX), + Weight::from_parts(u64::MAX, minimum_probe.proof_size()), + ] { + for (domain, cleanup_only) in [ + (None, false), + (Some(crate::BlockResourceDomain::ActorControl), false), + (Some(crate::BlockResourceDomain::ActorBaseEffect), false), + (Some(crate::BlockResourceDomain::ActorDrainEffect), false), + (None, true), + (Some(crate::BlockResourceDomain::ActorControl), true), + ] { + let scarce = if cleanup_only { + let step = if domain.is_none() { + step_resources.control.saturating_add(step_resources.effect) + } else { + step_resources.control + }; + if scarce.ref_time() == u64::MAX { + Weight::from_parts( + u64::MAX, + scarce.proof_size().saturating_add(step.proof_size()), + ) + } else { + Weight::from_parts(scarce.ref_time().saturating_add(step.ref_time()), u64::MAX) + } + } else { + scarce + }; + let recorder = + polkadot_sdk::sp_trie::recorder::Recorder::::default(); + ext.execute_with_recorder(recorder.clone(), || { + if let Some(domain) = domain { + let limits = crate::SimulationBudget { + actor_control: Weight::from_parts(u64::MAX / 2, u64::MAX / 2), + shared_economic: Weight::from_parts(u64::MAX / 2, u64::MAX / 2), + } + .checked_limits() + .expect("two ample lanes fit"); + let mut resources = crate::BlockResourceState::new(due); + assert_ok!(resources.begin_prepass()); + let effect_domain = if domain == crate::BlockResourceDomain::ActorBaseEffect { + let saturated = if scarce.ref_time() == u64::MAX { + Weight::from_parts(0, limits.actor_base_turn().proof_size()) + } else { + Weight::from_parts(limits.actor_base_turn().ref_time(), 0) + }; + assert!( + saturated + .saturating_add(step_resources.effect) + .all_lte(limits.shared_economic()) + ); + let mut prior = resources + .reserve(limits, domain, saturated) + .expect("prior Actor work fits its base turn exactly"); + assert_ok!(resources.settle(&mut prior, saturated)); + domain + } else { + assert_ok!(resources.open_external_phase()); + crate::BlockResourceDomain::ActorDrainEffect + }; + if domain == crate::BlockResourceDomain::ActorDrainEffect { + let saturated = if scarce.ref_time() == u64::MAX { + Weight::from_parts(0, limits.shared_economic().proof_size()) + } else { + Weight::from_parts(limits.shared_economic().ref_time(), 0) + }; + let mut user = resources + .reserve(limits, crate::BlockResourceDomain::UserDispatch, saturated) + .expect("prior user work fits exactly"); + assert_ok!(resources.settle(&mut user, saturated)); + } + if effect_domain == crate::BlockResourceDomain::ActorDrainEffect { + assert_ok!(resources.begin_drain()); + } + let prior_usage = resources.usage(); + let pass = Actors::execute_cycle_to_cutoff_with_resources( + Weight::MAX, + cutoff, + &mut resources, + limits, + effect_domain, + if domain == crate::BlockResourceDomain::ActorControl { + scarce.min(limits.actor_control()) + } else { + limits.actor_control() + }, + ); + assert_eq!(resources.outstanding_reservations(), 0); + assert_eq!( + resources.usage().actor_effect_used(), + prior_usage.actor_effect_used() + ); + assert_eq!(resources.usage().actor_control_used(), pass.consumed); + assert_eq!( + resources.usage().user_dispatch_used(), + prior_usage.user_dispatch_used() + ); + } else { + Actors::execute_cycle_to_cutoff(scarce, cutoff); + } + }); + let recorded = recorder.recorded_keys(); + for key in &cold_keys { + assert!( + !recorded + .values() + .any(|keys| keys.keys().any(|read| read.as_ref() == key.as_slice())), + "Weight-rejected Running head read cold key {key:?}" + ); + } + ext.execute_with(|| { + assert_eq!( + polkadot_sdk::sp_io::storage::root(StateVersion::V1), + eligible_root + ); + }); + ext + .commit_all() + .expect("refusal assertions stay outside the next recording"); + } + } + let recorder = + polkadot_sdk::sp_trie::recorder::Recorder::::default(); + ext.execute_with_recorder(recorder.clone(), || { + Actors::execute_cycle_to_cutoff(Weight::MAX, cutoff); + }); + let recorded = recorder.recorded_keys(); + for key in &cold_keys { + assert!( + recorded + .values() + .any(|keys| keys.keys().any(|read| read.as_ref() == key.as_slice())), + "eligible positive control did not record cold key {key:?}" + ); + } + ext.execute_with(|| { + assert!(Actors::actor_run_state(actor_id).is_none()); + assert_eq!( + Actors::actor_identity(actor_id).map(|identity| identity.cycle_nonce), + Some(1) + ); + }); +} + +#[test] +fn initial_placement_preserves_creation_and_reactivation_authority() { + for schedule in [manual_schedule(), at_time_schedule(10), timer_schedule(10)] { + for empty in [false, true] { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let window = matches!(schedule.trigger, Trigger::Manual).then_some(ScheduleWindow { + start: 10, + end: 10 + <::MinWindowLength as Get>::get(), + }); + let steps = if empty { BoundedVec::default() } else { inert_contract_steps() }; + let contract = system_active_contract(schedule.clone(), window, steps); + let actor_id = NextActorId::::get(); + for reactivation in [false, true] { + if reactivation { + frame_system::Pallet::::set_block_number(2); + assert_ok!(Actors::deactivate_actor(RuntimeOrigin::root(), actor_id)); + frame_system::Pallet::::set_block_number(3); + } + let install = || { + if reactivation { + Actors::activate_actor(RuntimeOrigin::root(), actor_id, contract.clone().expect("active Contract")) + } else { + Actors::create_system_actor(RuntimeOrigin::root(), ALICE, Mutability::Mutable, contract.clone()) + } + }; + let before = polkadot_sdk::sp_io::storage::root(polkadot_sdk::sp_runtime::StateVersion::V1); + Actors::test_fail_wakeup_placement_with_capacity(); + assert_noop!(install(), Error::::QueueCapacityUnavailable); + assert_eq!(polkadot_sdk::sp_io::storage::root(polkadot_sdk::sp_runtime::StateVersion::V1), before); + assert_ok!(install()); + let (state, admission, step) = Actors::load_frame_actor_service_state(actor_id) + .expect("complete installed authority"); + assert_eq!(step.is_some(), !empty); + assert_eq!(state.hot.cycle_state, CycleState::Idle); + assert!(!state.hot.pending_signal); + assert!(state.hot.queue_ticket.is_none()); + assert!(state.run_state.is_none()); + let expected_key = if let Some(window) = window { + assert!(state.hot.trigger_wakeup_pointer.is_none()); + let pointer = state.hot.wakeup_pointer.expect("terminal Block pointer"); + assert_eq!(pointer.block, WakeupKey::Block(window.end + 1)); + pointer.block + } else { + assert!(state.hot.wakeup_pointer.is_none()); + let pointer = state.hot.trigger_wakeup_pointer.expect("initial Tick pointer"); + assert_eq!(pointer.tick, if reactivation { 13 } else { 11 }); + WakeupKey::Tick(pointer.tick) + }; + let (location, cell) = Actors::actor_control_cell(actor_id).expect("installed primary"); + assert!(matches!(location, crate::ActorControlLocation::Waiting { key, .. } if key == expected_key)); + assert_eq!(cell.admission, admission); + #[cfg(feature = "try-runtime")] + assert_ok!(Actors::do_try_state()); + } + }); + } + } +} + +#[test] +fn temporal_replacement_publishes_exact_primary_and_preserves_failed_source() { + for schedule in [at_time_schedule(10), timer_schedule(10)] { + for empty in [false, true] { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let steps = if empty { + BoundedVec::default() + } else { + inert_contract_steps() + }; + let actor_id = create_system_with(ALICE, manual_schedule(), None, steps); + let mut replacement = Actors::load_actor_contract(actor_id).expect("admitted Contract"); + replacement.trigger = schedule.trigger.clone(); + frame_system::Pallet::::set_block_number(2); + let before = polkadot_sdk::sp_io::storage::root(polkadot_sdk::sp_runtime::StateVersion::V1); + Actors::test_fail_wakeup_placement_with_capacity(); + assert_noop!( + Actors::update_contract(RuntimeOrigin::root(), actor_id, replacement.clone()), + Error::::QueueCapacityUnavailable + ); + assert_eq!( + polkadot_sdk::sp_io::storage::root(polkadot_sdk::sp_runtime::StateVersion::V1), + before + ); + assert_ok!(Actors::update_contract( + RuntimeOrigin::root(), + actor_id, + replacement + )); + let (state, admission, loaded_step) = + Actors::load_frame_actor_service_state(actor_id).expect("complete temporal authority"); + assert_eq!(loaded_step.is_some(), !empty); + let anchor = match state.hot.trigger_runtime_state { + TriggerRuntimeState::AtTime { anchor_tick, .. } + | TriggerRuntimeState::Cadenced { anchor_tick } => { + anchor_tick.expect("anchored replacement") + } + _ => panic!("temporal Trigger"), + }; + let pointer = state.hot.trigger_wakeup_pointer.expect("Tick pointer"); + assert_eq!(pointer.tick, anchor + 10); + let (location, cell) = Actors::actor_control_cell(actor_id).expect("sole primary"); + let crate::ActorControlLocation::Waiting { key, page, slot } = location else { + panic!("Tick primary"); + }; + assert_eq!(key, WakeupKey::Tick(pointer.tick)); + assert_eq!(page, pointer.page_id); + assert_eq!(u32::from(slot), pointer.slot); + assert_eq!(cell.admission, admission); + assert_eq!(cell.cursor, 0); + assert_eq!( + cell.resources, + loaded_step.map_or( + crate::ActorStepResourceEnvelope { + control: <::WeightInfo as crate::weights::WeightInfo>::scheduler_inner_zero_step_complete(), + effect: Weight::zero(), + }, + |loaded| loaded.resources + ) + ); + let published = + polkadot_sdk::sp_io::storage::root(polkadot_sdk::sp_runtime::StateVersion::V1); + assert_ok!(Actors::prime_frame_actor_schedule(actor_id)); + assert_eq!( + polkadot_sdk::sp_io::storage::root(polkadot_sdk::sp_runtime::StateVersion::V1), + published + ); + #[cfg(feature = "try-runtime")] + assert_ok!(Actors::do_try_state()); + }); + } + } +} + +#[test] +fn retained_ingress_rejects_incomplete_authority_without_writes() { + for enqueue in [false, true] { + new_test_ext().execute_with(|| { + let actor_id = create_suspended_system_retry(1); + let state = Actors::active_actor_state(actor_id).expect("real suspended Actor"); + let invoke = || { + if enqueue { + Actors::try_paged_enqueue(actor_id) + } else { + Actors::try_wakeup_substrate_schedule_inner(actor_id, 10) + } + }; + let funding = crate::ActorFunding::::take(actor_id).expect("funding authority"); + let corrupt = polkadot_sdk::sp_io::storage::root(polkadot_sdk::sp_runtime::StateVersion::V1); + assert_eq!( + invoke(), + Err(crate::scheduler::EnqueueOutcome::CorruptedTopology) + ); + assert_eq!( + polkadot_sdk::sp_io::storage::root(polkadot_sdk::sp_runtime::StateVersion::V1), + corrupt + ); + crate::ActorFunding::::insert(actor_id, funding); + let payload = crate::ActorRunPayloads::::take(actor_id).expect("Run payload authority"); + let corrupt = polkadot_sdk::sp_io::storage::root(polkadot_sdk::sp_runtime::StateVersion::V1); + assert_eq!( + invoke(), + Err(crate::scheduler::EnqueueOutcome::CorruptedTopology) + ); + assert_eq!( + polkadot_sdk::sp_io::storage::root(polkadot_sdk::sp_runtime::StateVersion::V1), + corrupt + ); + crate::ActorRunPayloads::::insert(actor_id, payload); + let expected = if enqueue && state.hot.queue_ticket.is_some() { + Err(crate::scheduler::EnqueueOutcome::AlreadyLive) + } else { + Ok(()) + }; + assert_eq!(invoke(), expected, "retained ingress enqueue={enqueue}"); + assert_eq!( + Actors::actor_run_state(actor_id).encode(), + state.run_state.encode() + ); + assert!(Actors::active_actor_state(actor_id).is_some()); + #[cfg(feature = "try-runtime")] + assert_ok!(Actors::do_try_state()); + }); + } +} + +#[test] +fn retained_wakeup_deferral_preserves_capacity_rollback_and_rejects_corruption() { + new_test_ext().execute_with(|| { + let actor_id = create_suspended_system_retry(1); + let state = Actors::active_actor_state(actor_id).expect("real suspended Actor"); + let (_, cell) = Actors::actor_control_cell(actor_id).expect("canonical primary"); + let invoke = || { + Actors::test_schedule_next_work_source( + actor_id, + &state, + &cell.admission, + cell.resources, + None, + 0, + ) + }; + let before = polkadot_sdk::sp_io::storage::root(polkadot_sdk::sp_runtime::StateVersion::V1); + Actors::test_fail_wakeup_placement_with_capacity(); + assert_eq!( + invoke(), + Err(crate::scheduler::EnqueueOutcome::WakeupCapacityExhausted) + ); + assert_eq!( + polkadot_sdk::sp_io::storage::root(polkadot_sdk::sp_runtime::StateVersion::V1), + before + ); + crate::ActorFunding::::remove(actor_id); + let corrupt = polkadot_sdk::sp_io::storage::root(polkadot_sdk::sp_runtime::StateVersion::V1); + assert_eq!( + invoke(), + Err(crate::scheduler::EnqueueOutcome::CorruptedTopology) + ); + assert_eq!( + polkadot_sdk::sp_io::storage::root(polkadot_sdk::sp_runtime::StateVersion::V1), + corrupt + ); + crate::ActorFunding::::insert(actor_id, state.funding.clone()); + assert_eq!(invoke(), Ok((crate::StepControlPlacement::Wakeup, vec![]))); + assert_eq!( + Actors::actor_run_state(actor_id).encode(), + state.run_state.encode() + ); + #[cfg(feature = "try-runtime")] + assert_ok!(Actors::do_try_state()); + }); +} + +#[test] +fn supplied_run_is_the_only_consumed_scheduling_authority() { + for suspended in [false, true] { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = if suspended { + create_suspended_system_retry(1) + } else { + let steps = BoundedVec::try_from(vec![ + make_step(Task::Transfer { + to: BOB, + asset: TestAsset::Native, + amount: AmountResolution::Fixed(1), + }), + make_step(Task::StopCycle), + ]) + .expect("two steps fit"); + let actor_id = create_system_with(ALICE, manual_schedule(), None, steps); + fund_native(actor_id, 10); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + Actors::on_idle(1, Weight::MAX); + actor_id + }; + let state = Actors::active_actor_state(actor_id).expect("real active Run"); + assert_eq!( + state.hot.cycle_state, + if suspended { + CycleState::Suspended + } else { + CycleState::Running + } + ); + let run = state + .run_state + .as_ref() + .expect("real lifecycle Run") + .clone(); + let (_, cell) = Actors::actor_control_cell(actor_id).expect("canonical primary"); + let invoke = |supplied| { + Actors::test_schedule_next_work_source( + actor_id, + &state, + &cell.admission, + cell.resources, + supplied, + run.eligible_at, + ) + }; + let assert_read_only = |supplied, expected| { + let before = polkadot_sdk::sp_io::storage::root(polkadot_sdk::sp_runtime::StateVersion::V1); + assert_eq!(invoke(supplied), expected); + assert_eq!( + polkadot_sdk::sp_io::storage::root(polkadot_sdk::sp_runtime::StateVersion::V1), + before + ); + }; + let queued = Ok((crate::StepControlPlacement::Queue, vec![actor_id])); + let corrupt = Err(crate::scheduler::EnqueueOutcome::CorruptedTopology); + assert_read_only(None, queued.clone()); + assert_read_only(Some(None), corrupt.clone()); + let mut stale = run.clone(); + if suspended { + stale.eligible_at += 1; + } else { + stale.suspension = Some(crate::SuspensionReason::Temporary); + } + assert_read_only(Some(Some(&stale)), corrupt.clone()); + crate::ActorRunStateStore::::insert(actor_id, stale.clone()); + assert_read_only(Some(Some(&run)), queued.clone()); + assert_read_only(None, corrupt.clone()); + crate::ActorRunStateStore::::remove(actor_id); + assert_read_only(Some(Some(&run)), queued); + assert_read_only(None, corrupt); + }); + } +} + #[test] fn empty_on_idle_settles_housekeeping_into_actor_control() { new_test_ext().execute_with(|| { @@ -164,7 +1262,7 @@ fn exhausted_actor_control_prevents_on_idle_housekeeping_mutation() { 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::ActorReadyTail::::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"); @@ -181,7 +1279,7 @@ fn block_initialize_freezes_the_pre_external_ticket_frontier() { assert_eq!(state.usage().actor_control_used(), empty_prepass); assert!(!state.optional_actor_work_halted()); - crate::NextQueueTicket::::put(9); + crate::ActorReadyTail::::put(9); assert_noop!( Actors::actor_prepass(RuntimeOrigin::none()), crate::Error::::PrepassDuplicateOrStale @@ -313,6 +1411,119 @@ fn activation_preflight_selects_prime_schedule_without_mutating_state() { }); } +#[test] +fn ready_activation_plan_commits_its_frozen_destination_from_canonical_authority() { + 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(make_step(Task::StopCycle)), + ); + let loaded = if cfg!(feature = "runtime-benchmarks") { + Actors::load_actor_state(actor_id) + } else { + Actors::load_frame_actor_state(actor_id) + }; + let crate::LoadedActorStateOf::Active(state) = loaded else { + panic!("active Actor state"); + }; + let plan = Actors::preflight_activation_loaded(actor_id, state) + .expect("ready activation preflight succeeds"); + assert!(matches!( + &plan.action, + crate::scheduler::ActivationAction::EnqueueReady(Ok(_)) + )); + + assert_eq!( + Actors::commit_activation_plan(plan), + Ok(crate::scheduler::ActivationOutcome::Latched) + ); + let hot = Actors::actor_hot(actor_id).expect("committed hot authority"); + assert!(hot.pending_signal); + assert!(hot.queue_ticket.is_some()); + assert_eq!(hot.unsuccessful_attempt_streak, 0); + let (location, _, frame_hot, _) = Actors::load_frame_control_authority(actor_id) + .expect("ready destination owns frame authority"); + assert!(matches!( + location, + crate::ActorControlLocation::Ready { .. } + )); + assert_eq!(frame_hot, hot); + }); +} + +#[test] +fn deferred_activation_preserves_source_on_failure_and_publishes_exact_waiting() { + for saturated in [false, true] { + for empty in [false, true] { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let window = (!saturated).then_some(ScheduleWindow { + start: 10, + end: 10 + <::MinWindowLength as Get>::get(), + }); + let steps = if empty { + BoundedVec::default() + } else { + inert_contract_steps() + }; + let actor_id = create_system_with(ALICE, manual_schedule(), window, steps); + if saturated { + seed_saturated_tombstone_queue(); + } + let state = Actors::active_actor_state(actor_id).expect("canonical activation source"); + let plan = Actors::preflight_activation_loaded(actor_id, state).expect("valid preflight"); + if saturated { + assert!(matches!( + plan.action, + crate::scheduler::ActivationAction::EnqueueReady(Err( + crate::EnqueueOutcome::CapacityUnavailable + )) + )); + } else { + assert!(matches!( + plan.action, + crate::scheduler::ActivationAction::PrimeSchedule(Ok( + crate::scheduler::PrimeSchedulePlan::BlockWakeup(10) + )) + )); + } + let before = polkadot_sdk::sp_io::storage::root(polkadot_sdk::sp_runtime::StateVersion::V1); + Actors::test_fail_wakeup_placement_with_capacity(); + assert_noop!( + Actors::manual_trigger(RuntimeOrigin::signed(ALICE), actor_id), + Error::::QueueCapacityUnavailable + ); + assert_eq!( + polkadot_sdk::sp_io::storage::root(polkadot_sdk::sp_runtime::StateVersion::V1), + before + ); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + let hot = Actors::actor_hot(actor_id).expect("published activation"); + assert!(hot.pending_signal); + assert!(hot.queue_ticket.is_none()); + assert_eq!( + hot.wakeup_pointer.expect("Waiting pointer").block, + WakeupKey::Block(if saturated { 2 } else { 10 }) + ); + assert!(matches!( + Actors::actor_control_cell(actor_id) + .expect("canonical primary") + .0, + crate::ActorControlLocation::Waiting { .. } + )); + #[cfg(feature = "try-runtime")] + assert_ok!(Actors::do_try_state()); + }); + } + } +} + #[test] fn activation_preflight_selects_exact_block_wakeup_without_mutating_state() { new_test_ext().execute_with(|| { @@ -326,9 +1537,7 @@ fn activation_preflight_selects_exact_block_wakeup_without_mutating_state() { None, contract_steps_with_step(make_step(Task::StopCycle)), ); - ActorHot::::mutate(actor_id, |hot| { - hot.as_mut().expect("active actor").last_cycle_block = Some(1); - }); + mutate_actor_hot_coherent(actor_id, |hot| hot.last_cycle_block = Some(1)); let root_before = polkadot_sdk::sp_io::storage::root(polkadot_sdk::sp_runtime::StateVersion::V1); @@ -467,9 +1676,16 @@ fn queue_quartet_commit_applies_one_aggregate_plan_exactly_once() { assert_eq!(Actors::next_queue_ticket(), 4); assert_eq!(Actors::queue_occupancy(), 4); assert_eq!(Actors::queue_tail(), 4); - assert_eq!(Actors::queue_pages(0).expect("first queue page").len(), 4); + assert_eq!( + crate::ActorReadyFrameChunks::::get(0) + .expect("first queue page") + .iter() + .filter(|cell| cell.is_some()) + .count(), + 4 + ); for (index, actor_id) in actors.into_iter().enumerate() { - let hot = ActorHot::::get(actor_id).expect("queued actor"); + let hot = Actors::actor_hot(actor_id).expect("queued actor"); assert!(hot.pending_signal); assert_eq!(hot.queue_ticket, Some(index as u64)); } @@ -480,10 +1696,11 @@ fn queue_quartet_commit_applies_one_aggregate_plan_exactly_once() { fn queue_cohort_preflight_rejects_more_than_runtime_maximum_without_mutation() { new_test_ext().execute_with(|| { frame_system::Pallet::::set_block_number(1); - let actors = (20_000..20_017) - .map(|owner| { + let cohort_cap = <::MaxCrossingActorsPerBlock as Get>::get(); + let actors = (0..=cohort_cap) + .map(|offset| { create_system_with( - owner, + 20_000 + u64::from(offset), manual_schedule(), None, contract_steps_with_step(make_step(Task::StopCycle)), @@ -520,7 +1737,7 @@ fn queue_pair_preflight_crosses_page_boundary_without_mutating_state() { )); } for actor_id in actors.iter(/* deos-bypass: bounded-iter */).take(31) { - assert!(Actors::paged_enqueue(*actor_id)); + assert!(enqueue_latched_actor(*actor_id)); } let next_ticket = Actors::next_queue_ticket(); let root_before = @@ -536,7 +1753,7 @@ fn queue_pair_preflight_crosses_page_boundary_without_mutating_state() { ); assert_eq!(Actors::next_queue_ticket(), next_ticket); assert_eq!(Actors::queue_occupancy(), 31); - assert!(Actors::queue_pages(1).is_none()); + assert!(crate::ActorReadyFrameChunks::::get(1).is_none()); }); } @@ -551,7 +1768,7 @@ fn zero_step_opening_completes_without_step_or_run_state() { )); assert_eq!(Actors::queue_occupancy(), 1); let (state, _, loaded_step) = - Actors::load_actor_service_state(actor_id).expect("zero-Step service state loads"); + Actors::load_frame_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!( @@ -575,44 +1792,148 @@ fn zero_step_opening_completes_without_step_or_run_state() { 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 (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_identity(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(), + }, + ] + ); + }); +} + +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn frame_only_manual_zero_step_uses_only_canonical_control() { + 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(), + ); + fund_native(actor_id, 1_000_000_000_000_000_000); + let installed_hold = + crate::ActorStateHolds::::get(actor_id).expect("User state hold is installed"); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + + Actors::on_idle(1, Weight::MAX); + + let state = Actors::active_actor_state(actor_id).expect("frame successor remains active"); + assert_eq!(state.identity.cycle_nonce, 1); + assert_eq!(state.hot.cycle_state, CycleState::Idle); + assert!(!state.hot.pending_signal); + assert!(state.hot.queue_ticket.is_none()); + assert!(state.run_state.is_none()); + assert_eq!( + crate::ActorStateHolds::::get(actor_id), + Some(installed_hold) + ); + assert!(matches!( + crate::ActorControlLocators::::get(actor_id), + Some(crate::ActorControlLocation::Unsignaled) + )); + assert!(!ActorIdentities::::contains_key(actor_id)); + assert!(Actors::actor_hot(actor_id).is_some()); + assert!(Actors::actor_control_cell(actor_id).is_some()); + assert!(has_actor_event(|event| matches!( + event, + Event::CycleSummary { + actor_id: id, + cycle_nonce: 1, + result: CycleResult::Completed, + .. + } if *id == actor_id + ))); + #[cfg(feature = "try-runtime")] + assert_ok!(crate::Pallet::::do_try_state()); + }); +} + +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn frame_only_opening_stop_cycle_uses_only_canonical_control() { + 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(make_step(Task::StopCycle)), + ); + fund_native(actor_id, 1_000_000_000_000_000_000); + let installed_hold = + crate::ActorStateHolds::::get(actor_id).expect("User state hold is installed"); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); - 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::>(); + Actors::on_idle(1, Weight::MAX); + + let state = Actors::active_actor_state(actor_id).expect("StopCycle successor remains active"); + assert_eq!(state.identity.cycle_nonce, 1); + assert_eq!(state.hot.cycle_state, CycleState::Idle); + assert!(!state.hot.pending_signal); + assert!(state.run_state.is_none()); 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(), - }, - ] + crate::ActorStateHolds::::get(actor_id), + Some(installed_hold) ); + assert!(has_actor_event(|event| matches!( + event, + Event::CycleStopped { + actor_id: id, + cycle_nonce: 1, + step_index: 0, + } if *id == actor_id + ))); + assert!(matches!( + crate::ActorControlLocators::::get(actor_id), + Some(crate::ActorControlLocation::Unsignaled) + )); + assert!(!ActorIdentities::::contains_key(actor_id)); + assert!(Actors::actor_hot(actor_id).is_some()); + assert!(Actors::actor_control_cell(actor_id).is_some()); + #[cfg(feature = "try-runtime")] + assert_ok!(crate::Pallet::::do_try_state()); }); } @@ -635,10 +1956,12 @@ fn zero_step_auto_close_one_is_atomic_and_has_no_run_state() { actor_id )); System::reset_events(); + #[cfg(not(feature = "runtime-benchmarks"))] + {} run_idle(Weight::MAX); - assert!(Actors::actor_identities(actor_id).is_none()); + assert!(Actors::actor_identity(actor_id).is_none()); assert!(ActorRunStateStore::::get(actor_id).is_none()); assert!(System::events().iter().any(|record| matches!( record.event, @@ -662,7 +1985,7 @@ fn dormant_identity_owns_no_scheduler_state_and_round_trips_activation() { None, )); let actor_id = 0; - let identity = Actors::actor_identities(actor_id).expect("dormant identity exists"); + let identity = Actors::actor_identity(actor_id).expect("dormant identity exists"); assert_eq!(Actors::actor_identity_count(), 1); assert_eq!(Actors::active_actor_count(), 0); assert!(Actors::active_actor_view(actor_id).is_none()); @@ -698,7 +2021,7 @@ fn dormant_identity_owns_no_scheduler_state_and_round_trips_activation() { ), Error::::MintNotAllowedForUserActor ); - assert!(Actors::actor_identities(actor_id).is_some()); + assert!(Actors::actor_identity(actor_id).is_some()); assert!(Actors::active_actor_view(actor_id).is_none()); assert_eq!(Actors::active_actor_count(), 0); assert_ok!(Actors::activate_actor( @@ -710,7 +2033,7 @@ fn dormant_identity_owns_no_scheduler_state_and_round_trips_activation() { .expect("direct Actor Contract") }, )); - assert!(Actors::actor_identities(actor_id).is_some()); + assert!(Actors::actor_identity(actor_id).is_some()); let _activated = Actors::active_actor_view(actor_id).expect("active Actor Contract exists"); assert_eq!( Actors::load_actor_contract(actor_id) @@ -727,12 +2050,12 @@ fn dormant_identity_owns_no_scheduler_state_and_round_trips_activation() { )); assert!(Actors::active_actor_view(actor_id).is_none()); assert!(Actors::actor_funding(actor_id).is_none()); - assert!(Actors::actor_identities(actor_id).is_some()); + assert!(Actors::actor_identity(actor_id).is_some()); assert_eq!(Actors::actor_identity_count(), 1); assert_eq!(Actors::active_actor_count(), 0); assert_eq!(native_balance(&identity.sovereign_account), preserved); assert_ok!(Actors::close_actor(RuntimeOrigin::signed(ALICE), actor_id)); - assert!(Actors::actor_identities(actor_id).is_none()); + assert!(Actors::actor_identity(actor_id).is_none()); assert_eq!(Actors::actor_identity_count(), 0); assert_eq!(Actors::owner_slot_bitmap(ALICE), [0; 32]); assert_eq!(native_balance(&identity.sovereign_account), preserved); @@ -740,93 +2063,392 @@ fn dormant_identity_owns_no_scheduler_state_and_round_trips_activation() { } #[test] -fn on_idle_never_consumes_above_the_runtime_reserve() { +fn on_idle_never_consumes_above_the_runtime_reserve() { + 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_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + let reserve = ::scheduler_on_idle_base(); + set_guaranteed_on_idle_weight(reserve); + + let used = Actors::on_idle(1, Weight::MAX); + + assert!(used.all_lte(reserve)); + assert_eq!( + Actors::actor_identity(actor_id) + .expect("actor identity remains") + .cycle_nonce, + 0, + ); + }); +} + +#[test] +fn create_rejects_zero_cadence() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let schedule = Schedule { + trigger: Trigger::Cadenced { every_ticks: 0 }, + cooldown_blocks: 0, + }; + assert_noop!( + Actors::create_user_actor( + RuntimeOrigin::signed(ALICE), + Mutability::Mutable, + user_active_contract(schedule, None, transfer_contract_steps(BOB, 1)), + ), + Error::::InvalidTriggerConfiguration + ); + }); +} + +#[test] +fn user_pause_resume_churn_is_limited_to_one_queue_mutation_per_block() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = create_user_with( + ALICE, + Mutability::Mutable, + manual_schedule(), + None, + inert_contract_steps(), + ); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + assert_eq!(Actors::queue_tail(), 1); + assert_ok!(Actors::pause_actor(RuntimeOrigin::signed(ALICE), actor_id)); + assert!( + Actors::actor_hot(actor_id) + .expect("paused actor") + .queue_ticket + .is_none() + ); + assert_noop!( + Actors::resume_actor(RuntimeOrigin::signed(ALICE), actor_id), + Error::::ControlMutationRateLimited + ); + assert_eq!( + Actors::queue_tail(), + 1, + "rate-limited resume must not append" + ); + + frame_system::Pallet::::set_block_number(2); + assert_ok!(Actors::resume_actor(RuntimeOrigin::signed(ALICE), actor_id)); + assert_eq!(Actors::queue_tail(), 2); + assert_noop!( + Actors::pause_actor(RuntimeOrigin::signed(ALICE), actor_id), + Error::::ControlMutationRateLimited + ); + assert_eq!( + Actors::queue_tail(), + 2, + "rate-limited pause must not create a tombstone" + ); + }); +} + +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn successful_manual_execution_preserves_canonical_control() { + for steps in [inert_contract_steps(), BoundedVec::default()] { + let predicate_stop = !steps.is_empty(); + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = create_system_with(ALICE, manual_schedule(), None, steps); + + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + run_idle(Weight::MAX); + + if predicate_stop { + assert!(has_actor_event(|event| matches!( + event, + Event::StepSkipped { + actor_id: id, + step_index: 0, + reason: StepSkippedReason::PreconditionFalse, + .. + } if *id == actor_id + ))); + assert!(!has_actor_event(|event| matches!( + event, + Event::CycleStopped { actor_id: id, .. } if *id == actor_id + ))); + } + assert!(has_actor_event(|event| matches!( + event, + Event::CycleSummary { actor_id: id, .. } if *id == actor_id + ))); + assert!(!ActorIdentities::::contains_key(actor_id)); + assert!(Actors::actor_hot(actor_id).is_some()); + assert!(Actors::actor_control_cell(actor_id).is_some()); + #[cfg(feature = "try-runtime")] + assert_ok!(crate::Pallet::::do_try_state()); + }); + } +} + +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn canonical_execution_preserves_running_successor_and_q1() { + 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::StopCycle), + ]) + .expect("two Steps fit"); + let actor_id = create_system_with(ALICE, manual_schedule(), None, steps); + fund_native(actor_id, 10); + + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + Actors::on_idle(1, Weight::MAX); + assert_eq!( + Actors::actor_run_state(actor_id) + .unwrap_or_else(|| { + panic!( + "middle-Step Run survives with canonical authority; events={:?}", + System::events(), + ) + }) + .cursor, + 1, + ); + + assert!(!ActorIdentities::::contains_key(actor_id)); + assert!(Actors::actor_hot(actor_id).is_some()); + assert!(Actors::actor_control_cell(actor_id).is_some()); + Actors::on_idle(1, Weight::MAX); + assert_eq!( + Actors::actor_run_state(actor_id) + .expect("same-block retry retains the Running successor") + .cursor, + 1, + ); + assert!(!has_actor_event(|event| matches!( + event, + Event::CycleStopped { + actor_id: id, + step_index: 1, + .. + } if *id == actor_id + ))); + assert!(!ActorIdentities::::contains_key(actor_id)); + assert!(Actors::actor_hot(actor_id).is_some()); + assert!(Actors::actor_control_cell(actor_id).is_some()); + + frame_system::Pallet::::set_block_number(2); + Actors::on_initialize(2); + run_prepass(); + Actors::on_idle(2, Weight::MAX); + assert!(Actors::actor_run_state(actor_id).is_none()); + assert!(!ActorIdentities::::contains_key(actor_id)); + assert!(Actors::actor_hot(actor_id).is_some()); + assert!(Actors::actor_control_cell(actor_id).is_some()); + #[cfg(feature = "try-runtime")] + assert_ok!(crate::Pallet::::do_try_state()); + }); +} + +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn frame_only_expired_manual_activation_closes_from_retained_unsignaled_authority() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = create_user_with( + ALICE, + Mutability::Mutable, + manual_schedule(), + Some(ScheduleWindow { start: 1, end: 101 }), + BoundedVec::default(), + ); + fund_native(actor_id, 1_000_000_000_000_000_000); + assert!(crate::ActorStateHolds::::contains_key(actor_id)); + + frame_system::Pallet::::set_block_number(102); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + assert!(!Actors::active_actor_exists(actor_id)); + assert!(!crate::ActorControlLocators::::contains_key(actor_id)); + assert!(!crate::ActorStateHolds::::contains_key(actor_id)); + assert!(!ActorIdentities::::contains_key(actor_id)); + assert!(!Actors::actor_hot(actor_id).is_some()); + assert!(Actors::actor_control_cell(actor_id).is_none()); + assert!(has_actor_event(|event| matches!( + event, + Event::ActorClosed { + actor_id: id, + reason: CloseReason::WindowExpired, + } if *id == actor_id + ))); + #[cfg(feature = "try-runtime")] + assert_ok!(crate::Pallet::::do_try_state()); + }); +} + +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn manual_middle_step_preserves_canonical_control() { + 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::StopCycle), + ]) + .expect("two Steps fit"); + let actor_id = create_system_with(ALICE, manual_schedule(), None, steps); + fund_native(actor_id, 10); + + 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("middle-Step Run survives"); + assert_eq!(run.cursor, 1); + assert!(!ActorIdentities::::contains_key(actor_id)); + assert!(Actors::actor_hot(actor_id).is_some()); + assert!(Actors::actor_control_cell(actor_id).is_some()); + + frame_system::Pallet::::set_block_number(2); + run_idle(Weight::MAX); + assert!(Actors::actor_run_state(actor_id).is_none()); + assert!(has_actor_event(|event| matches!( + event, + Event::CycleSummary { actor_id: id, .. } if *id == actor_id + ))); + assert!(!ActorIdentities::::contains_key(actor_id)); + assert!(Actors::actor_hot(actor_id).is_some()); + assert!(Actors::actor_control_cell(actor_id).is_some()); + #[cfg(feature = "try-runtime")] + assert_ok!(crate::Pallet::::do_try_state()); + }); +} + +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn frame_only_paused_ready_pop_uses_only_canonical_control() { 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 actor_id = create_user_with( + ALICE, + Mutability::Mutable, + manual_schedule(), + None, + transfer_contract_steps(BOB, 10), + ); + fund_native(actor_id, 1_000_000_000_000_000_000); + let installed_hold = + crate::ActorStateHolds::::get(actor_id).expect("User state hold is installed"); assert_ok!(Actors::manual_trigger( RuntimeOrigin::signed(ALICE), actor_id )); - let reserve = ::scheduler_on_idle_base(); - set_guaranteed_on_idle_weight(reserve); + assert_ok!(Actors::pause_actor(RuntimeOrigin::signed(ALICE), actor_id)); + let paused_before = + Actors::actor_control_cell(actor_id).expect("pause atom retains canonical authority"); + assert_eq!(paused_before.0, crate::ActorControlLocation::Unsignaled); + assert!(paused_before.1.hot.lifecycle.is_paused()); + System::reset_events(); - let used = Actors::on_idle(1, Weight::MAX); + Actors::on_idle(1, Weight::MAX); - assert!(used.all_lte(reserve)); + let state = Actors::active_actor_state(actor_id).expect("paused authority remains active"); + assert!(state.hot.lifecycle.is_paused()); + assert!(state.hot.pending_signal); + assert!(state.hot.queue_ticket.is_none()); assert_eq!( - Actors::actor_identities(actor_id) - .expect("actor identity remains") - .cycle_nonce, - 0, - ); - }); -} - -#[test] -fn create_rejects_zero_cadence() { - new_test_ext().execute_with(|| { - frame_system::Pallet::::set_block_number(1); - let schedule = Schedule { - trigger: Trigger::Cadenced { every_ticks: 0 }, - cooldown_blocks: 0, - }; - assert_noop!( - Actors::create_user_actor( - RuntimeOrigin::signed(ALICE), - Mutability::Mutable, - user_active_contract(schedule, None, transfer_contract_steps(BOB, 1)), - ), - Error::::InvalidTriggerConfiguration + crate::ActorStateHolds::::get(actor_id), + Some(installed_hold) ); + assert!(matches!( + crate::ActorControlLocators::::get(actor_id), + Some(crate::ActorControlLocation::Unsignaled) + )); + assert!(!ActorIdentities::::contains_key(actor_id)); + assert!(Actors::actor_hot(actor_id).is_some()); + assert!(Actors::actor_control_cell(actor_id).is_some()); + assert!(!has_actor_event(|event| matches!( + event, + Event::CycleStarted { actor_id: id, .. } if *id == actor_id + ))); + #[cfg(feature = "try-runtime")] + assert_ok!(crate::Pallet::::do_try_state()); }); } +#[cfg(not(feature = "runtime-benchmarks"))] #[test] -fn user_pause_resume_churn_is_limited_to_one_queue_mutation_per_block() { +fn frame_only_circuit_breaker_skip_uses_only_canonical_control() { new_test_ext().execute_with(|| { frame_system::Pallet::::set_block_number(1); - let actor_id = create_user_with( - ALICE, - Mutability::Mutable, - manual_schedule(), - None, - inert_contract_steps(), - ); + let actor_id = create_system_with(ALICE, manual_schedule(), None, inert_contract_steps()); assert_ok!(Actors::manual_trigger( RuntimeOrigin::signed(ALICE), actor_id )); - assert_eq!(Actors::queue_tail(), 1); - assert_ok!(Actors::pause_actor(RuntimeOrigin::signed(ALICE), actor_id)); - assert!( - Actors::actor_hot(actor_id) - .expect("paused actor") - .queue_ticket - .is_none() - ); - assert_noop!( - Actors::resume_actor(RuntimeOrigin::signed(ALICE), actor_id), - Error::::ControlMutationRateLimited - ); + assert_ok!(Actors::set_global_circuit_breaker( + RuntimeOrigin::root(), + true + )); + let before = Actors::actor_control_cell(actor_id).expect("breaker Ready owner exists"); + let queue_before = ( + crate::ActorReadyHead::::get(), + crate::ActorReadyTail::::get(), + crate::ActorReadyOccupancy::::get(), + ); + + let consumed = Actors::execute_cycle(Weight::MAX).consumed; + let expected = ::scheduler_paged_tombstone_drain(1) + .saturating_add(::scheduler_actor_state_probe()); + assert_eq!(consumed, expected); + assert_eq!(Actors::actor_control_cell(actor_id), Some(before)); assert_eq!( - Actors::queue_tail(), - 1, - "rate-limited resume must not append" + ( + crate::ActorReadyHead::::get(), + crate::ActorReadyTail::::get(), + crate::ActorReadyOccupancy::::get(), + ), + queue_before ); - frame_system::Pallet::::set_block_number(2); - assert_ok!(Actors::resume_actor(RuntimeOrigin::signed(ALICE), actor_id)); - assert_eq!(Actors::queue_tail(), 2); - assert_noop!( - Actors::pause_actor(RuntimeOrigin::signed(ALICE), actor_id), - Error::::ControlMutationRateLimited - ); - assert_eq!( - Actors::queue_tail(), - 2, - "rate-limited pause must not create a tombstone" - ); + let state = Actors::active_actor_state(actor_id).expect("breaker-skipped authority remains"); + assert!(state.hot.pending_signal); + assert!(state.hot.queue_ticket.is_some()); + assert!(matches!( + crate::ActorControlLocators::::get(actor_id), + Some(crate::ActorControlLocation::Ready { .. }) + )); + assert!(!ActorIdentities::::contains_key(actor_id)); + assert!(Actors::actor_hot(actor_id).is_some()); + assert!(Actors::actor_control_cell(actor_id).is_some()); + assert!(!has_actor_event(|event| matches!( + event, + Event::CycleStarted { actor_id: id, .. } if *id == actor_id + ))); + #[cfg(feature = "try-runtime")] + assert_ok!(crate::Pallet::::do_try_state()); }); } @@ -980,7 +2602,7 @@ fn global_fifo_services_system_actor_when_it_is_the_only_ready_work() { fn paged_enqueue_coalesces_without_a_per_block_insertion_cap() { new_test_ext().execute_with(|| { frame_system::Pallet::::set_block_number(1); - let total = <::QueuePageSize as Get>::get() + 7; + let total = 32u32 + 7; for _ in 0..total { let id = create_system_with(ALICE, manual_schedule(), None, inert_contract_steps()); assert_ok!(Actors::manual_trigger(RuntimeOrigin::signed(ALICE), id)); @@ -994,13 +2616,14 @@ fn paged_enqueue_coalesces_without_a_per_block_insertion_cap() { } #[test] -fn tombstone_drain_rolls_back_on_occupancy_underflow() { +fn tombstone_drain_rolls_back_on_live_occupancy_exceeding_span() { 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)); + assert!(enqueue_latched_actor(actor_id)); assert_eq!(Actors::paged_invalidate(actor_id), Some(0)); - QueueOccupancy::::put(0); + restore_structural_queue_tombstone(actor_id); + crate::ActorReadyOccupancy::::put(2); let before = polkadot_sdk::sp_io::storage::root(StateVersion::V1); assert_eq!( @@ -1016,13 +2639,14 @@ fn tombstone_drain_rolls_back_on_occupancy_underflow() { fn tombstone_drain_rolls_back_on_cross_page_corruption() { new_test_ext().execute_with(|| { frame_system::Pallet::::set_block_number(1); - let page_size = ::QueuePageSize::get(); + let page_size = 32u32; for _ in 0..=page_size { let actor_id = create_system_with(ALICE, manual_schedule(), None, inert_contract_steps()); - assert!(Actors::paged_enqueue(actor_id)); + assert!(enqueue_latched_actor(actor_id)); assert!(Actors::paged_invalidate(actor_id).is_some()); + restore_structural_queue_tombstone(actor_id); } - QueuePages::::remove(1); + crate::ActorReadyFrameChunks::::remove(1); let before = polkadot_sdk::sp_io::storage::root(StateVersion::V1); assert_eq!( @@ -1032,7 +2656,7 @@ fn tombstone_drain_rolls_back_on_cross_page_corruption() { assert_eq!(polkadot_sdk::sp_io::storage::root(StateVersion::V1), before); assert!( - Actors::queue_pages(0).is_some(), + crate::ActorReadyFrameChunks::::get(0).is_some(), "first page deletion rolls back" ); }); @@ -1043,9 +2667,10 @@ fn tombstone_drain_missing_current_page_is_blocked_without_mutation() { 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)); + assert!(enqueue_latched_actor(actor_id)); assert!(Actors::paged_invalidate(actor_id).is_some()); - QueuePages::::remove(0); + restore_structural_queue_tombstone(actor_id); + crate::ActorReadyFrameChunks::::remove(0); let cutoff = Actors::queue_tail(); let before = polkadot_sdk::sp_io::storage::root(StateVersion::V1); @@ -1061,8 +2686,8 @@ fn enqueue_rolls_back_on_span_occupancy_mismatch() { 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()); - QueueTail::::put(1); - QueueOccupancy::::put(0); + crate::ActorReadyTail::::put(1); + crate::ActorReadyOccupancy::::put(0); let events_before = System::events(); let root_before = polkadot_sdk::sp_io::storage::root(StateVersion::V1); @@ -1087,20 +2712,16 @@ fn enqueue_rolls_back_on_missing_or_malformed_tail_page() { let mut actors = Vec::new(); for _ in 0..33 { let actor_id = create_system_with(ALICE, manual_schedule(), None, inert_contract_steps()); - assert!(Actors::paged_enqueue(actor_id)); + assert!(enqueue_latched_actor(actor_id)); actors.push(actor_id); } let candidate = create_system_with(BOB, manual_schedule(), None, inert_contract_steps()); if malformed { - QueuePages::::mutate(1, |maybe_page| { - maybe_page - .as_mut() - .expect("tail page") - .try_push(queue_entry(99_999, candidate)) - .expect("malformed extra slot fits"); + crate::ActorReadyFrameChunks::::mutate(1, |maybe_page| { + maybe_page.as_mut().expect("tail page").truncate(1); }); } else { - QueuePages::::remove(1); + crate::ActorReadyFrameChunks::::remove(1); } let events_before = System::events(); let root_before = polkadot_sdk::sp_io::storage::root(StateVersion::V1); @@ -1122,25 +2743,35 @@ fn enqueue_rolls_back_on_missing_or_malformed_tail_page() { #[cfg(feature = "try-runtime")] #[test] -fn try_state_rejects_every_live_queue_ticket_mismatch() { +fn try_state_rejects_frame_and_physical_queue_mismatch() { for corruption in 0u8..3 { 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 other = create_system_with(BOB, manual_schedule(), None, inert_contract_steps()); - assert!(Actors::paged_enqueue(actor_id)); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); match corruption { - 0 => ActorHot::::mutate(actor_id, |maybe| { - maybe.as_mut().expect("active actor").queue_ticket = Some(1); - }), - 1 => QueuePages::::mutate(0, |maybe| { - maybe.as_mut().expect("queue page")[0].actor_id = other; + 0 => crate::ActorControlLocators::::insert( + actor_id, + crate::ActorControlLocation::Ready { ticket: 1 }, + ), + 1 => crate::ActorReadyFrameChunks::::mutate(0, |maybe| { + maybe.as_mut().expect("queue page")[0] + .as_mut() + .expect("live cell") + .actor_id = other; }), 2 => { - QueuePages::::mutate(0, |maybe| { - maybe.as_mut().expect("queue page")[0].ticket = 1; + crate::ActorReadyFrameChunks::::mutate(0, |maybe| { + let page = maybe.as_mut().expect("queue page"); + let first = page.get_mut(0).expect("first queue slot").take(); + let second = ::core::mem::replace(page.get_mut(1).expect("second queue slot"), first); + *page.get_mut(0).expect("first queue slot") = second; }); - crate::NextQueueTicket::::put(2); + crate::ActorReadyTail::::put(2); } _ => unreachable!(), } @@ -1153,33 +2784,37 @@ fn try_state_rejects_every_live_queue_ticket_mismatch() { } #[test] -fn physical_occupancy_counts_tombstones_until_drained() { +fn live_occupancy_and_tombstone_span_are_independently_bounded() { new_test_ext().execute_with(|| { frame_system::Pallet::::set_block_number(1); let mut actors = Vec::new(); for _ in 0..5 { let actor_id = create_system_with(ALICE, manual_schedule(), None, inert_contract_steps()); - assert!(Actors::paged_enqueue(actor_id)); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); actors.push(actor_id); } assert_eq!(Actors::combined_queue_occupancy(), 5); - // Invalidate the first four: their physical entries remain as tombstones and still count as - // occupied capacity until drained, so tail gaps never weaken the namespace bound. + // Tombstones retain physical span but no longer contribute to live occupancy. for actor_id in &actors[0..4] { assert!(Actors::paged_invalidate(*actor_id).is_some()); + restore_structural_queue_tombstone(*actor_id); } assert_eq!( Actors::combined_queue_occupancy(), - 5, - "tombstones stay inside exact physical occupancy" + 1, + "invalidation removes exactly four live cells" ); + assert_eq!(Actors::queue_tail() - Actors::queue_head(), 5); let cutoff = Actors::next_queue_ticket(); let drained = Actors::paged_drain_tombstones(cutoff, 10).expect("valid queue topology"); assert_eq!(drained.tombstones_skipped, 4); assert_eq!( Actors::combined_queue_occupancy(), 1, - "draining tombstones releases exactly their occupancy" + "draining tombstones does not decrement live occupancy" ); #[cfg(feature = "try-runtime")] assert_ok!(crate::Pallet::::do_try_state()); @@ -1192,7 +2827,7 @@ fn paged_queue_uses_one_live_actor_ticket_and_lazy_invalidation() { 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)); + assert!(enqueue_latched_actor(actor_id)); assert!(Actors::paged_enqueue(actor_id)); assert_eq!(Actors::queue_head(), 0); assert_eq!(Actors::queue_tail(), 1); @@ -1200,40 +2835,61 @@ fn paged_queue_uses_one_live_actor_ticket_and_lazy_invalidation() { Actors::actor_hot(actor_id).expect("hot state").queue_ticket, Some(0) ); - assert_eq!(Actors::queue_pages(0).expect("head page").len(), 1); + assert_eq!( + crate::ActorReadyFrameChunks::::get(0) + .expect("head page") + .iter() + .filter(|cell| cell.is_some()) + .count(), + 1 + ); assert_eq!(Actors::paged_invalidate(actor_id), Some(0)); + restore_structural_queue_tombstone(actor_id); assert_eq!( Actors::actor_hot(actor_id).expect("hot state").queue_ticket, None ); - let (position, entry) = Actors::paged_head_entry().expect("queue head exists"); - assert_eq!((position, entry.ticket, entry.actor_id), (0, 0, actor_id)); + assert!(crate::ActorReadyFrameChunks::::get(0).expect("tombstone page")[0].is_none()); let drained = Actors::paged_drain_tombstones(Actors::next_queue_ticket(), 1) .expect("invalidated head drains as a tombstone"); assert_eq!(drained.tombstones_skipped, 1); - assert_eq!(Actors::queue_head(), 32); - assert_eq!(Actors::queue_tail(), 32); - assert!(Actors::queue_pages(0).is_none()); + assert_eq!(Actors::queue_head(), 1); + assert_eq!(Actors::queue_tail(), 1); + assert!(crate::ActorReadyFrameChunks::::get(0).is_none()); }); } #[cfg(feature = "try-runtime")] #[test] -fn canonical_queue_try_state_rejects_an_internal_physical_gap() { +fn canonical_queue_try_state_rejects_a_malformed_page_width() { new_test_ext().execute_with(|| { - QueuePages::::insert( + crate::ActorReadyFrameChunks::::insert( 0, - BoundedVec::try_from(vec![queue_entry(0, 99_999)]).expect("one queue entry fits"), + BoundedVec::try_from(vec![None]).expect("malformed short page fits"), ); - QueueHead::::put(0); - QueueTail::::put(2); - QueueOccupancy::::put(1); - crate::NextQueueTicket::::put(2); + crate::ActorReadyHead::::put(0); + crate::ActorReadyTail::::put(2); + crate::ActorReadyOccupancy::::put(0); assert!(crate::Pallet::::do_try_state().is_err()); }); } +fn consume_structural_ready_head(ticket: u64) { + let (_, entry) = Actors::paged_head_entry().expect("live structural head"); + let (_, mut cell) = Actors::actor_control_cell(entry.actor_id).expect("source primary"); + assert_eq!(cell.hot.cycle_state, CycleState::Idle); + assert!(Actors::paged_consume_head(ticket)); + assert!(Actors::actor_control_cell(entry.actor_id).is_none()); + cell.hot.pending_signal = false; + cell.eligible_at = None; + crate::ActorUnsignaledControlCells::::insert(entry.actor_id, cell); + crate::ActorControlLocators::::insert( + entry.actor_id, + crate::ActorControlLocation::Unsignaled, + ); +} + #[test] fn paged_queue_crosses_and_reclaims_page_boundaries_without_prefix_rewrites() { new_test_ext().execute_with(|| { @@ -1241,13 +2897,24 @@ fn paged_queue_crosses_and_reclaims_page_boundaries_without_prefix_rewrites() { let mut actors = Vec::new(); for _ in 0..33 { let actor_id = create_system_with(ALICE, manual_schedule(), None, inert_contract_steps()); - assert!(Actors::paged_enqueue(actor_id)); + assert!(enqueue_latched_actor(actor_id)); actors.push(actor_id); } assert_eq!(Actors::queue_tail(), 33); - assert_eq!(Actors::queue_pages(0).expect("full first page").len(), 32); assert_eq!( - Actors::queue_pages(1).expect("partial second page").len(), + crate::ActorReadyFrameChunks::::get(0) + .expect("full first page") + .iter() + .filter(|cell| cell.is_some()) + .count(), + 32 + ); + assert_eq!( + crate::ActorReadyFrameChunks::::get(1) + .expect("partial second page") + .iter() + .filter(|cell| cell.is_some()) + .count(), 1 ); @@ -1257,71 +2924,39 @@ fn paged_queue_crosses_and_reclaims_page_boundaries_without_prefix_rewrites() { (position, entry.ticket, entry.actor_id), (ticket as u64, ticket as u64, actor_id) ); - assert!(Actors::paged_consume_head(ticket as u64)); + consume_structural_ready_head(ticket as u64); } assert_eq!(Actors::queue_head(), 32); - assert!(Actors::queue_pages(0).is_none()); + assert!(crate::ActorReadyFrameChunks::::get(0).is_none()); assert_eq!( - Actors::queue_pages(1).expect("remaining head page").len(), + crate::ActorReadyFrameChunks::::get(1) + .expect("remaining head page") + .iter() + .filter(|cell| cell.is_some()) + .count(), 1 ); - assert!(Actors::paged_consume_head(32)); - assert_eq!(Actors::queue_head(), 64); - assert_eq!(Actors::queue_tail(), 64); - assert!(Actors::queue_pages(1).is_none()); + consume_structural_ready_head(32); + assert_eq!(Actors::queue_head(), 33); + assert_eq!(Actors::queue_tail(), 33); + assert!(crate::ActorReadyFrameChunks::::get(1).is_none()); #[cfg(feature = "try-runtime")] assert_ok!(crate::Pallet::::do_try_state()); }); } -#[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(|| { frame_system::Pallet::::set_block_number(1); let actor_a = create_system_with(ALICE, manual_schedule(), None, inert_contract_steps()); let actor_b = create_system_with(ALICE, manual_schedule(), None, inert_contract_steps()); - assert!(Actors::paged_enqueue(actor_a)); + assert!(enqueue_latched_actor(actor_a)); assert_eq!(Actors::paged_invalidate(actor_a), Some(0)); - assert!(Actors::paged_enqueue(actor_b)); - assert!(Actors::paged_enqueue(actor_a)); + restore_structural_queue_tombstone(actor_a); + assert!(enqueue_latched_actor(actor_b)); + assert!(enqueue_latched_actor(actor_a)); assert_eq!( Actors::actor_hot(actor_a) @@ -1335,8 +2970,9 @@ fn paged_queue_replacement_ticket_leaves_old_entry_as_tombstone() { .queue_ticket, Some(1) ); - let (position, entry) = Actors::paged_head_entry().expect("queue head exists"); - assert_eq!((position, entry.ticket, entry.actor_id), (0, 0, actor_a)); + assert_eq!(Actors::queue_head(), 0); + assert!(crate::ActorReadyFrameChunks::::get(0).expect("tombstone page")[0].is_none()); + assert!(Actors::paged_head_entry().is_none()); let drained = Actors::paged_drain_tombstones(Actors::next_queue_ticket(), 1) .expect("replacement head drains as a tombstone"); assert_eq!(drained.tombstones_skipped, 1); @@ -1360,11 +2996,12 @@ fn paged_tombstone_drain_is_scan_bounded_and_reclaims_multiple_pages() { let mut actors = Vec::new(); for _ in 0..65 { let actor_id = create_system_with(ALICE, manual_schedule(), None, inert_contract_steps()); - assert!(Actors::paged_enqueue(actor_id)); + assert!(enqueue_latched_actor(actor_id)); actors.push(actor_id); } for actor_id in actors { assert!(Actors::paged_invalidate(actor_id).is_some()); + restore_structural_queue_tombstone(actor_id); } let cutoff = Actors::queue_tail(); @@ -1380,11 +3017,11 @@ fn paged_tombstone_drain_is_scan_bounded_and_reclaims_multiple_pages() { assert_eq!(rest.tombstones_skipped, 55); assert_eq!(rest.pages_touched, 3); assert_eq!(rest.pages_deleted, 3); - assert_eq!(Actors::queue_head(), 96); - assert_eq!(Actors::queue_tail(), 96); - assert!(Actors::queue_pages(0).is_none()); - assert!(Actors::queue_pages(1).is_none()); - assert!(Actors::queue_pages(2).is_none()); + assert_eq!(Actors::queue_head(), 65); + assert_eq!(Actors::queue_tail(), 65); + assert!(crate::ActorReadyFrameChunks::::get(0).is_none()); + assert!(crate::ActorReadyFrameChunks::::get(1).is_none()); + assert!(crate::ActorReadyFrameChunks::::get(2).is_none()); #[cfg(feature = "try-runtime")] assert_ok!(crate::Pallet::::do_try_state()); }); @@ -1394,10 +3031,24 @@ fn paged_tombstone_drain_is_scan_bounded_and_reclaims_multiple_pages() { fn mandatory_prepass_reclaims_saturated_fifo_without_domain_spill() { 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()); seed_saturated_tombstone_queue(); + let cutoff = Actors::queue_tail(); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + assert_eq!(scheduled_wakeup_block(actor_id), Some(2)); + frame_system::Pallet::::set_block_number(2); run_prepass(); assert!(Actors::queue_head() > 0); + let actor = Actors::active_actor_view(actor_id).expect("deferred Actor is materialized"); + assert_eq!(actor.queue_ticket, Some(cutoff)); + assert!(actor.pending_signal); + assert_eq!(actor.cycle_nonce, 0); + assert_eq!(Actors::prepass_execution_cutoff(), Some((2, cutoff))); let state = Actors::block_resource_state().expect("prepass resource state exists"); + assert_eq!(state.usage().actor_effect_used(), Weight::zero()); assert_eq!(state.outstanding_reservations(), 0); assert_eq!(state.phase(), crate::BlockResourcePhase::ExternalPhase); }); @@ -1408,30 +3059,16 @@ fn saturated_tombstone_queue_reclaims_head_before_ingress_and_recovers_deferred_ 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 page_size = <::QueuePageSize as Get>::get(); let capacity = <::MaxQueueLength as Get>::get(); - for page_id in 0..capacity.div_ceil(page_size) { - 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| { - 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( + for page_id in 0..capacity.div_ceil(32) { + crate::ActorReadyFrameChunks::::insert( u64::from(page_id), - BoundedVec::try_from(entries).expect("saturated queue page fits"), + BoundedVec::try_from(vec![None; 32]).expect("canonical tombstone page"), ); } - QueueHead::::put(0); - QueueTail::::put(u64::from(capacity)); - QueueOccupancy::::put(capacity); - crate::pallet::NextQueueTicket::::put(u64::from(capacity)); + crate::ActorReadyHead::::put(0); + crate::ActorReadyTail::::put(u64::from(capacity)); + crate::ActorReadyOccupancy::::put(0); assert_ok!(Actors::manual_trigger( RuntimeOrigin::signed(ALICE), @@ -1494,7 +3131,8 @@ fn queue_ticket_exhaustion_closes_through_the_unified_sink() { let sovereign = sovereign_account(actor_id); // Monotonic ticket namespace at the ceiling closes through the single // scheduler-exhaustion terminal owner. - crate::NextQueueTicket::::put(u64::MAX); + crate::ActorReadyHead::::put(u64::MAX); + crate::ActorReadyTail::::put(u64::MAX); let actor_before = native_balance(&sovereign); assert_ok!(Actors::notify_address_event( actor_id, @@ -1508,7 +3146,7 @@ fn queue_ticket_exhaustion_closes_through_the_unified_sink() { } #[test] -fn scheduler_index_exhaustion_fails_closed_when_tail_cannot_advance() { +fn queue_cohort_preflight_ticket_exhaustion_is_read_only() { new_test_ext().execute_with(|| { frame_system::Pallet::::set_block_number(1); let actor_id = create_system_with( @@ -1518,17 +3156,22 @@ fn scheduler_index_exhaustion_fails_closed_when_tail_cannot_advance() { transfer_contract_steps(BOB, 10), ); fund_native(actor_id, 1_000); - // Tail at the u64 ceiling: the next placement cannot advance the monotonic - // index and must fail closed with SchedulerIndexExhausted. The ticket stays - // well below its ceiling so the tail checked-add is the failing surface. - crate::QueueTail::::put(u64::MAX); - crate::NextQueueTicket::::put(5); - QueueOccupancy::::put(0); + // The canonical tail is the sole non-resetting ticket allocator. + crate::ActorReadyHead::::put(u64::MAX); + crate::ActorReadyTail::::put(u64::MAX); + crate::ActorReadyOccupancy::::put(0); let actor_before = native_balance(&sovereign_account(actor_id)); - assert_noop!( - Actors::notify_address_event(actor_id, TestAsset::Native, 100, &ALICE), - Error::::SchedulerIndexExhausted + let root_before = polkadot_sdk::sp_io::storage::root(StateVersion::V1); + let mut hot = Actors::actor_hot(actor_id).expect("valid source authority"); + hot.pending_signal = true; + assert_eq!( + Actors::preflight_paged_enqueue_cohort_with_authority(vec![(actor_id, hot)]).map(|_| ()), + Err(crate::EnqueueOutcome::TicketExhausted) + ); + assert_eq!( + polkadot_sdk::sp_io::storage::root(StateVersion::V1), + root_before ); assert_eq!(native_balance(&sovereign_account(actor_id)), actor_before); }); @@ -1610,12 +3253,17 @@ fn paged_tombstone_drain_stops_at_live_head_and_honors_cutoff() { let live = create_system_with(ALICE, manual_schedule(), None, inert_contract_steps()); let appended_after_cutoff = create_system_with(ALICE, manual_schedule(), None, inert_contract_steps()); - assert!(Actors::paged_enqueue(stale)); - assert!(Actors::paged_enqueue(live)); + assert_ok!(Actors::manual_trigger(RuntimeOrigin::signed(ALICE), stale)); + assert_ok!(Actors::manual_trigger(RuntimeOrigin::signed(ALICE), live)); let cutoff = Actors::queue_tail(); - assert!(Actors::paged_enqueue(appended_after_cutoff)); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + appended_after_cutoff + )); assert_eq!(Actors::paged_invalidate(stale), Some(0)); + restore_structural_queue_tombstone(stale); assert_eq!(Actors::paged_invalidate(appended_after_cutoff), Some(2)); + restore_structural_queue_tombstone(appended_after_cutoff); let drained = Actors::paged_drain_tombstones(cutoff, 100).expect("valid cutoff drain"); assert_eq!(drained.entries_scanned, 2); @@ -1628,7 +3276,13 @@ fn paged_tombstone_drain_stops_at_live_head_and_honors_cutoff() { Some(1) ); + let (_, mut live_cell) = Actors::actor_control_cell(live).expect("live primary before consume"); assert!(Actors::paged_consume_head(1)); + assert!(Actors::actor_control_cell(live).is_none()); + live_cell.hot.pending_signal = false; + live_cell.eligible_at = None; + crate::ActorUnsignaledControlCells::::insert(live, live_cell); + crate::ActorControlLocators::::insert(live, crate::ActorControlLocation::Unsignaled); let after_live = Actors::paged_drain_tombstones(cutoff, 100).expect("valid post-live drain"); assert_eq!( after_live.entries_scanned, 0, @@ -1766,6 +3420,7 @@ fn simulation_and_scheduler_reject_the_same_protected_fee_floor_boundary() { Mutability::Mutable, contract, SimulationMode::FreshCurrentPlan, + ample_simulation_budget(), ) .expect("terminal viability projects as a closed simulation"); assert_eq!( @@ -1928,10 +3583,11 @@ fn canonical_fifo_uses_one_physical_ticket_sequence() { } assert_eq!(Actors::next_queue_ticket(), 4); - let tickets: Vec<_> = Actors::queue_pages(0) + let tickets: Vec<_> = crate::ActorReadyFrameChunks::::get(0) .expect("canonical queue page") .into_iter() - .map(|entry| entry.ticket) + .enumerate() + .filter_map(|(slot, cell)| cell.map(|_| slot as u64)) .collect(); assert_eq!(tickets, vec![0, 1, 2, 3]); assert_eq!(Actors::queue_occupancy(), 4); @@ -1950,20 +3606,31 @@ fn canonical_tombstones_cannot_bypass_the_oldest_live_head() { None, inert_contract_steps(), ); - assert!(Actors::paged_enqueue(old_user)); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + old_user + )); for _ in 0..3 { let tombstone = create_system_with(ALICE, manual_schedule(), None, inert_contract_steps()); - assert!(Actors::paged_enqueue(tombstone)); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + tombstone + )); assert!(Actors::paged_invalidate(tombstone).is_some()); + restore_structural_queue_tombstone(tombstone); } let later_system = create_system_with(ALICE, manual_schedule(), None, inert_contract_steps()); - assert!(Actors::paged_enqueue(later_system)); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + later_system + )); let cutoff = Actors::next_queue_ticket(); let (state, entry, _) = Actors::test_head_discovery(cutoff, 1, 0, scan); assert_eq!(state, 1); assert_eq!(entry.map(|entry| entry.actor_id), Some(old_user)); assert_eq!(Actors::paged_invalidate(old_user), Some(0)); + restore_structural_queue_tombstone(old_user); let (state, entry, scanned) = Actors::test_head_discovery(cutoff, 5, 0, scan.saturating_mul(5)); assert_eq!((state, scanned), (1, 5)); assert_eq!(entry.map(|entry| entry.actor_id), Some(later_system)); @@ -2141,8 +3808,15 @@ fn cancellation_requeues_a_signal_latched_for_the_next_logical_run() { assert!(cancelled.pending_signal); assert!(cancelled.queue_ticket.is_some()); - frame_system::Pallet::::set_block_number(2); - run_idle(Weight::MAX); + for block in 2..=4 { + frame_system::Pallet::::set_block_number(block); + Actors::on_initialize(block); + run_prepass(); + Actors::on_idle(block, Weight::MAX); + if Actors::active_actor_view(actor_id).is_some_and(|actor| actor.cycle_nonce == 2) { + break; + } + } let completed = Actors::active_actor_view(actor_id).expect("next logical cycle completes"); assert_eq!(completed.cycle_nonce, 2); assert!(!completed.pending_signal); @@ -2295,14 +3969,33 @@ fn shared_materialization_remainder_follows_rotated_first_family_after_reserving .saturating_sub(minima[1]) .saturating_sub(minima[2]); assert!(lendable.ref_time() > 0 && lendable.proof_size() > 0); + assert_eq!( + Actors::materialization_family_budget( + 0, + 0, + lendable, + crate::MaterializationMinimumReservation::Unavailable, + ), + lendable + ); for cursor in 0u8..3 { - let first = Actors::materialization_family_budget(cursor, 0, shared, true); + let first = Actors::materialization_family_budget( + cursor, + 0, + shared, + crate::MaterializationMinimumReservation::ReserveAllFamilies, + ); assert_eq!(first, minima[usize::from(cursor)].saturating_add(lendable)); let next = cursor.saturating_add(1) % 3; let after_first = shared.saturating_sub(first); assert_eq!( - Actors::materialization_family_budget(cursor, 1, after_first, true), + Actors::materialization_family_budget( + cursor, + 1, + after_first, + crate::MaterializationMinimumReservation::ReserveAllFamilies, + ), minima[usize::from(next)] ); } @@ -2697,6 +4390,96 @@ fn breaker_defers_scheduler_owned_window_expiry_close() { }); } +#[test] +fn breaker_preserves_lower_level_unlatched_terminal_ready_authority() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + let actor_id = create_user_with( + ALICE, + Mutability::Mutable, + manual_schedule(), + Some(ScheduleWindow { start: 2, end: 102 }), + transfer_contract_steps(BOB, 1), + ); + fund_native(actor_id, 1_000); + assert_eq!(scheduled_wakeup_block(actor_id), Some(103)); + assert!( + !Actors::actor_hot(actor_id) + .expect("untriggered Actor") + .pending_signal + ); + + System::set_block_number(103); + // Exercise the canonical lower-level consume/publication contract. The high-level + // due worker intentionally substitutes WindowExpired close before enqueue instead. + assert_ok!(polkadot_sdk::frame_support::storage::with_transaction( + || { + let (due, stats) = Actors::wakeup_substrate_drain_key(WakeupKey::Block(103), 1); + assert_eq!(stats.entries_scanned, 1); + assert_eq!(due.len(), 1); + let (due_actor, state, admission, loaded_step) = + due.into_iter().next().expect("due source"); + assert_eq!(due_actor, actor_id); + let plan = Actors::preflight_paged_enqueue_authority( + actor_id, + state.hot, + &state.identity, + state.run_state.as_ref(), + &admission, + loaded_step.expect("authored current Step").resources, + ) + .expect("complete consumed terminal authority admits Ready"); + assert_ok!(Actors::commit_paged_enqueue(plan)); + polkadot_sdk::frame_support::storage::TransactionOutcome::Commit( + Ok::<(), DispatchError>(()), + ) + } + )); + let (source_location, source_cell) = Actors::actor_control_cell(actor_id) + .expect("lower-level due publication retains terminal primary"); + let crate::ActorControlLocation::Ready { ticket } = source_location else { + panic!("canonical queue publication must materialize Ready"); + }; + assert_eq!(source_cell.hot.cycle_state, CycleState::Idle); + assert!(!source_cell.hot.pending_signal); + assert_eq!(source_cell.hot.terminal_at, Some(103)); + assert_eq!(crate::ActorReadyOccupancy::::get(), 1); + assert_eq!(Actors::queue_head(), ticket); + assert_eq!(Actors::queue_tail(), ticket + 1); + assert_ok!(Actors::set_global_circuit_breaker( + RuntimeOrigin::root(), + true + )); + System::reset_events(); + let custody = (native_balance(&ALICE), native_balance(&BOB)); + + Actors::execute_cycle(Weight::MAX); + + let (location, cell) = Actors::actor_control_cell(actor_id) + .expect("breaker must retain exactly one valid terminal primary"); + assert_eq!(location, crate::ActorControlLocation::Ready { ticket }); + assert_eq!(cell, source_cell); + assert!(Actors::actor_run_state(actor_id).is_none()); + assert_eq!( + crate::ActorWaitingOccupancies::::get(WakeupKey::Block(103)), + 0 + ); + assert_eq!(crate::ActorReadyOccupancy::::get(), 1); + assert_eq!(Actors::queue_head(), ticket); + assert_eq!(Actors::queue_tail(), ticket + 1); + assert!(!crate::ActorUnsignaledControlCells::::contains_key( + actor_id + )); + assert_eq!((native_balance(&ALICE), native_balance(&BOB)), custody); + assert!(!has_actor_event(|event| matches!(event, + Event::ActorClosed { actor_id: id, .. } | Event::CycleStarted { actor_id: id, .. } + if *id == actor_id + ))); + #[cfg(feature = "try-runtime")] + assert_ok!(Actors::do_try_state()); + }); +} + #[test] fn at_time_occurrence_charges_once_consumes_deadline_and_latches_readiness() { new_test_ext().execute_with(|| { @@ -2740,7 +4523,7 @@ fn at_time_occurrence_charges_once_consumes_deadline_and_latches_readiness() { } #[test] -fn busy_at_time_occurrence_charges_and_preserves_independent_run_service() { +fn latched_at_time_replacement_does_not_retrigger_running_pipeline() { new_test_ext().execute_with(|| { frame_system::Pallet::::set_block_number(1); let steps = BoundedVec::try_from(vec![ @@ -2756,24 +4539,32 @@ fn busy_at_time_occurrence_charges_and_preserves_independent_run_service() { }), ]) .expect("two-Step Contract fits"); - let actor_id = create_user_with(ALICE, Mutability::Mutable, at_time_schedule(2), None, steps); + let actor_id = create_user_with(ALICE, Mutability::Mutable, manual_schedule(), None, steps); fund_native(actor_id, 1_000_000); - assert_eq!( - Actors::request_activation(actor_id), - Ok(crate::scheduler::ActivationOutcome::Latched) - ); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + frame_system::Pallet::::set_block_number(2); + let mut replacement = Actors::actor_contract(actor_id).expect("admitted Contract"); + replacement.trigger = RuntimeTrigger::at_time(2); + assert_ok!(Actors::update_contract( + RuntimeOrigin::signed(ALICE), + actor_id, + replacement + )); 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); + frame_system::Pallet::::set_block_number(4); let mut meter = WeightMeter::with_limit(Weight::MAX); - Actors::drain_overdue_wakeups_cursor(3, &mut meter); + Actors::drain_overdue_wakeups_cursor(4, &mut meter); - assert_eq!(fee_collections(), vec![at_time_trigger_fee()]); + assert!(fee_collections().is_empty()); 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.pending_signal); assert!(hot.trigger_wakeup_pointer.is_none()); assert!(matches!( hot.trigger_runtime_state, @@ -2782,6 +4573,8 @@ fn busy_at_time_occurrence_charges_and_preserves_independent_run_service() { 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); + #[cfg(feature = "try-runtime")] + assert_ok!(Actors::do_try_state()); }); } @@ -2819,6 +4612,78 @@ fn underfunded_at_time_occurrence_selects_prepaid_custody_neutral_apoptosis() { }); } +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn frame_only_underfunded_at_time_closes_from_consumed_wakeup_authority() { + 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!(!crate::ActorControlLocators::::contains_key(actor_id)); + assert!(!crate::ActorStateHolds::::contains_key(actor_id)); + assert_eq!(native_balance(&sovereign), custody_before); + assert!(!ActorIdentities::::contains_key(actor_id)); + assert!(!Actors::actor_hot(actor_id).is_some()); + assert!(Actors::actor_control_cell(actor_id).is_none()); + assert!(has_actor_event(|event| matches!( + event, + Event::ActorClosed { + actor_id: id, + reason: CloseReason::TriggerAdmissionInsufficient, + } if *id == actor_id + ))); + #[cfg(feature = "try-runtime")] + assert_ok!(crate::Pallet::::do_try_state()); + }); +} + +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn frame_only_zero_step_at_time_uses_only_canonical_control() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = create_system_with(ALICE, at_time_schedule(1), None, BoundedVec::default()); + + 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 state = Actors::active_actor_state(actor_id).expect("AtTime successor remains active"); + assert_eq!(state.identity.cycle_nonce, 1); + assert!(matches!( + state.hot.trigger_runtime_state, + TriggerRuntimeState::AtTime { consumed: true, .. } + )); + assert!(matches!( + crate::ActorControlLocators::::get(actor_id), + Some(crate::ActorControlLocation::Unsignaled) + )); + assert!(!ActorIdentities::::contains_key(actor_id)); + assert!(Actors::actor_hot(actor_id).is_some()); + assert!(Actors::actor_control_cell(actor_id).is_some()); + #[cfg(feature = "try-runtime")] + assert_ok!(crate::Pallet::::do_try_state()); + }); +} + #[test] fn at_time_collection_failure_rolls_back_consumption_and_retains_wakeup() { new_test_ext().execute_with(|| { @@ -2927,17 +4792,51 @@ fn immutable_zero_step_at_time_closes_at_authored_cycle_nonce() { }); } +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn uninitialized_genesis_cadence_reanchors_from_frame_authority() { + 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()); + assert_eq!(scheduled_wakeup_block(actor_id), Some(2)); + mutate_actor_hot_coherent(actor_id, |hot| { + hot.trigger_runtime_state = TriggerRuntimeState::Cadenced { anchor_tick: None }; + }); + + frame_system::Pallet::::set_block_number(100); + Actors::on_idle(100, Weight::MAX); + + let (_, identity, hot, _) = Actors::load_frame_control_authority(actor_id) + .expect("reanchored Cadenced frame authority exists"); + assert_eq!(identity.cycle_nonce, 0); + assert!(!hot.pending_signal); + assert!(matches!( + hot.trigger_runtime_state, + TriggerRuntimeState::Cadenced { + anchor_tick: Some(100) + } + )); + assert_eq!( + hot.trigger_wakeup_pointer.map(|pointer| pointer.tick), + Some(101) + ); + assert!(ActorIdentities::::get(actor_id).is_none()); + assert!(Actors::actor_hot(actor_id).is_some()); + assert!(Actors::actor_control_cell(actor_id).is_some()); + #[cfg(feature = "try-runtime")] + assert_ok!(crate::Pallet::::do_try_state()); + }); +} + +#[cfg(feature = "runtime-benchmarks")] #[test] -fn uninitialized_genesis_cadence_reanchors_without_first_service_execution() { +fn uninitialized_genesis_cadence_reanchors_in_benchmark_fixture() { 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()); assert_eq!(scheduled_wakeup_block(actor_id), Some(2)); - crate::ActorHot::::mutate(actor_id, |maybe_hot| { - maybe_hot - .as_mut() - .expect("active cadence actor") - .trigger_runtime_state = TriggerRuntimeState::Cadenced { anchor_tick: None }; + mutate_actor_hot_coherent(actor_id, |hot| { + hot.trigger_runtime_state = TriggerRuntimeState::Cadenced { anchor_tick: None }; }); frame_system::Pallet::::set_block_number(100); @@ -2994,6 +4893,183 @@ fn cadenced_latch_disables_detection_until_pipeline_opening() { }); } +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn at_time_occurrence_uses_primary_pending_authority() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = create_system_with(ALICE, at_time_schedule(1), None, inert_contract_steps()); + assert!(!Actors::pending_signal(actor_id)); + + frame_system::Pallet::::set_block_number(2); + let mut meter = WeightMeter::with_limit(Weight::MAX); + Actors::drain_overdue_wakeups_cursor(2, &mut meter); + + let (location, _, frame_hot, _) = Actors::load_frame_control_authority(actor_id) + .expect("AtTime occurrence retains canonical primary"); + assert!(matches!( + location, + crate::ActorControlLocation::Ready { .. } + )); + assert!(frame_hot.pending_signal); + assert!( + matches!( + frame_hot.trigger_runtime_state, + TriggerRuntimeState::AtTime { consumed: true, .. } + ), + "{:?}", + frame_hot.trigger_runtime_state + ); + let projected_hot = Actors::actor_hot(actor_id).expect("canonical hot projection exists"); + assert!(projected_hot.pending_signal); + assert_eq!( + projected_hot.trigger_runtime_state, + frame_hot.trigger_runtime_state + ); + assert!(has_actor_event(|event| matches!( + event, + Event::TriggerOccurrenceProcessed { + actor_id: id, + trigger_family: TriggerFamily::AtTime, + .. + } if *id == actor_id + ))); + #[cfg(feature = "try-runtime")] + assert_ok!(crate::Pallet::::do_try_state()); + }); +} + +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn cadenced_occurrence_uses_primary_pending_authority() { + 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()); + assert!(!Actors::pending_signal(actor_id)); + + frame_system::Pallet::::set_block_number(2); + let mut meter = WeightMeter::with_limit(Weight::MAX); + Actors::drain_overdue_wakeups_cursor(2, &mut meter); + + let (location, _, frame_hot, _) = Actors::load_frame_control_authority(actor_id) + .expect("Cadenced occurrence retains canonical primary"); + assert!(matches!( + location, + crate::ActorControlLocation::Ready { .. } + )); + assert!(frame_hot.pending_signal); + assert!(matches!( + frame_hot.trigger_runtime_state, + TriggerRuntimeState::Cadenced { + anchor_tick: Some(1) + } + )); + assert!(frame_hot.trigger_wakeup_pointer.is_none()); + let projected_hot = Actors::actor_hot(actor_id).expect("canonical hot projection exists"); + assert_eq!(projected_hot.pending_signal, frame_hot.pending_signal); + assert_eq!( + projected_hot.trigger_runtime_state, + frame_hot.trigger_runtime_state + ); + assert_eq!( + projected_hot.trigger_wakeup_pointer, + frame_hot.trigger_wakeup_pointer + ); + assert!(has_actor_event(|event| matches!( + event, + Event::TriggerOccurrenceProcessed { + actor_id: id, + trigger_family: TriggerFamily::Cadenced, + .. + } if *id == actor_id + ))); + #[cfg(feature = "try-runtime")] + assert_ok!(crate::Pallet::::do_try_state()); + }); +} + +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn paused_temporal_occurrence_restores_unsignaled_frame_with_canonical_control() { + 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_idle(Weight::MAX); + assert_ok!(Actors::pause_actor(RuntimeOrigin::signed(ALICE), actor_id)); + frame_system::Pallet::::set_block_number(6); + + crate::NextWakeupClock::::put(WakeupClock::Tick); + let mut meter = WeightMeter::with_limit(Weight::MAX); + let stats = Actors::drain_overdue_wakeups_cursor(6, &mut meter); + assert_eq!(stats.entries_scanned, 1); + assert_eq!(stats.ready_entries, 1); + assert!(!crate::WakeupWorkerFaultState::::exists()); + + assert_eq!( + crate::ActorControlLocators::::get(actor_id), + Some(crate::ActorControlLocation::Unsignaled) + ); + let (_, _, hot, _) = Actors::load_frame_control_authority(actor_id) + .expect("paused temporal Unsignaled frame authority exists"); + assert!(hot.lifecycle.is_paused()); + assert!(hot.pending_signal); + assert_eq!(hot.queue_ticket, None); + assert!(ActorIdentities::::get(actor_id).is_none()); + assert!(Actors::actor_hot(actor_id).is_some()); + assert!(Actors::actor_control_cell(actor_id).is_some()); + #[cfg(feature = "try-runtime")] + assert_ok!(crate::Pallet::::do_try_state()); + }); +} + +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn cadenced_rearm_uses_frozen_opening_authority() { + 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()); + frame_system::Pallet::::set_block_number(2); + let mut meter = WeightMeter::with_limit(Weight::MAX); + Actors::drain_overdue_wakeups_cursor(2, &mut meter); + assert!(Actors::pending_signal(actor_id)); + Actors::execute_cycle(Weight::MAX); + + let crate::LoadedActorStateOf::Active(state) = + Actors::load_actor_state_for_frame_control(actor_id) + else { + panic!("rearmed Cadenced frame state is active"); + }; + assert_eq!(state.identity.cycle_nonce, 1); + assert!(!state.hot.pending_signal); + assert!(state.hot.queue_ticket.is_none()); + assert!(matches!( + state.hot.trigger_runtime_state, + TriggerRuntimeState::Cadenced { + anchor_tick: Some(1) + } + )); + assert_eq!( + state.hot.trigger_wakeup_pointer.map(|pointer| pointer.tick), + Some(3) + ); + let projected_hot = Actors::actor_hot(actor_id).expect("canonical hot projection exists"); + assert_eq!( + projected_hot.trigger_runtime_state, + state.hot.trigger_runtime_state + ); + assert_eq!( + projected_hot.trigger_wakeup_pointer, + state.hot.trigger_wakeup_pointer + ); + assert_eq!( + crate::ActorWaitingOccupancies::::get(WakeupKey::Tick(3)), + 1 + ); + #[cfg(feature = "try-runtime")] + assert_ok!(crate::Pallet::::do_try_state()); + }); +} + #[test] fn busy_cadenced_occurrence_charges_and_preserves_independent_run_service() { new_test_ext().execute_with(|| { @@ -3035,6 +5111,57 @@ fn busy_cadenced_occurrence_charges_and_preserves_independent_run_service() { }); } +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn busy_cadenced_occurrence_preserves_frame_service_with_canonical_control() { + 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); + let stats = Actors::drain_overdue_wakeups_cursor(3, &mut meter); + + assert_eq!(stats.entries_scanned, 1); + assert_eq!(stats.ready_entries, 1); + assert!(!crate::WakeupWorkerFaultState::::exists()); + assert_eq!(fee_collections(), vec![cadenced_trigger_fee()]); + let (_, _, hot, _) = Actors::load_frame_control_authority(actor_id) + .expect("busy Cadenced frame authority 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); + assert!(ActorIdentities::::get(actor_id).is_none()); + assert!(Actors::actor_hot(actor_id).is_some()); + assert!(Actors::actor_control_cell(actor_id).is_some()); + #[cfg(feature = "try-runtime")] + assert_ok!(crate::Pallet::::do_try_state()); + }); +} + #[test] fn underfunded_cadenced_occurrence_advances_without_fee_readiness_or_apoptosis() { new_test_ext().execute_with(|| { @@ -3521,6 +5648,8 @@ fn scheduler_fails_closed_without_consuming_a_corrupt_live_head() { let events_before = frame_system::Pallet::::events(); ActorFunding::::remove(actor_id); + let corrupt_root = + polkadot_sdk::sp_io::storage::root(polkadot_sdk::sp_runtime::StateVersion::V1); assert_eq!( Actors::try_paged_enqueue(actor_id), Err(crate::EnqueueOutcome::CorruptedTopology) @@ -3529,12 +5658,27 @@ fn scheduler_fails_closed_without_consuming_a_corrupt_live_head() { Actors::try_paged_invalidate(actor_id), Err(crate::EnqueueOutcome::CorruptedTopology) ); + assert_eq!( + polkadot_sdk::sp_io::storage::root(polkadot_sdk::sp_runtime::StateVersion::V1), + corrupt_root + ); Actors::execute_cycle(Weight::MAX); assert_eq!(Actors::queue_head(), head_before); assert_eq!(Actors::actor_hot(actor_id), Some(hot_before)); assert_eq!(frame_system::Pallet::::events(), events_before); - assert_eq!(Actors::actor_identities(actor_id).unwrap().cycle_nonce, 0); + assert!( + Actors::actor_identity(actor_id).is_none(), + "full classification rejects corruption" + ); + assert_eq!( + Actors::actor_control_cell(actor_id) + .expect("retained primary") + .1 + .identity + .cycle_nonce, + 0 + ); }); } @@ -3555,7 +5699,7 @@ fn mixed_fifo_stops_at_corrupt_actor_without_touching_valid_suffix() { let sovereign_accounts = actors .iter() .map(|actor_id| { - Actors::actor_identities(*actor_id) + Actors::actor_identity(*actor_id) .expect("identity") .sovereign_account }) @@ -3575,8 +5719,12 @@ fn mixed_fifo_stops_at_corrupt_actor_without_touching_valid_suffix() { assert_eq!(Actors::queue_head(), corrupt_index as u64); for (index, actor_id) in actors.iter().enumerate() { - let identity = Actors::actor_identities(*actor_id).expect("identity remains"); - assert_eq!(identity.cycle_nonce, u64::from(index < corrupt_index)); + let (_, cell) = Actors::actor_control_cell(*actor_id).expect("primary remains"); + assert_eq!(cell.identity.cycle_nonce, u64::from(index < corrupt_index)); + assert_eq!( + Actors::actor_identity(*actor_id).is_none(), + index == corrupt_index + ); } for (offset, actor_id) in actors[corrupt_index..].iter().enumerate() { assert_eq!( @@ -3599,6 +5747,25 @@ fn mixed_fifo_stops_at_corrupt_actor_without_touching_valid_suffix() { } } +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn eligibility_projection_uses_primary_authority_and_rejects_missing_primary() { + 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_eq!( + active_eligibility(actor_id).execution_phase, + ActorExecutionPhase::WaitingSignal + ); + + Actors::remove_primary_control_cell_inner(actor_id).expect("primary removal succeeds"); + assert_eq!( + Actors::actor_eligibility(actor_id), + Err(crate::ActorClassificationError::ActorInvariant) + ); + }); +} + #[test] fn eligibility_projection_reports_exact_at_time_gate_and_consumption() { new_test_ext().execute_with(|| { @@ -3660,6 +5827,7 @@ fn eligibility_projection_reports_exact_cadence_gate_without_actor_phase() { system_active_contract(timer_schedule(cadence), None, inert_contract_steps()) .expect("expected Actor Contract"), SimulationMode::FreshCurrentPlan, + ample_simulation_budget(), ), Err(SimulationError::NotReady), "simulation must not synthesize readiness before scheduler materialization" diff --git a/template/pallets/actors/src/tests/storage_and_api.rs b/template/pallets/actors/src/tests/storage_and_api.rs index f96ed947..6d6cc4b3 100644 --- a/template/pallets/actors/src/tests/storage_and_api.rs +++ b/template/pallets/actors/src/tests/storage_and_api.rs @@ -1,7 +1,6 @@ use super::*; use crate::{ - ActorAdmissionCertificates, ActorContractHeads, ActorContractTailChunks, ActorCostQuoteError, - PipelineMachineFeeStrategy, + ActorContractHeads, ActorContractTailChunks, ActorCostQuoteError, PipelineMachineFeeStrategy, }; use frame::traits::ConstU32; @@ -26,6 +25,33 @@ fn geometry_certificate( ) } +#[test] +fn actor_identity_resolves_lifecycle_owner_and_fails_closed_on_missing_primary() { + new_test_ext().execute_with(|| { + let actor_id = create_user_with( + ALICE, + Mutability::Mutable, + manual_schedule(), + None, + transfer_contract_steps(BOB, 2), + ); + let identity = Actors::actor_identity(actor_id).expect("active identity exists"); + let primary = crate::ActorUnsignaledControlCells::::take(actor_id) + .expect("Manual Actor starts unsignaled"); + assert!(Actors::actor_identity(actor_id).is_none()); + crate::ActorUnsignaledControlCells::::insert(actor_id, primary); + assert_eq!(Actors::actor_identity(actor_id), Some(identity.clone())); + assert_ok!(Actors::deactivate_actor( + RuntimeOrigin::signed(ALICE), + actor_id + )); + let dormant = Actors::actor_identity(actor_id).expect("dormant identity exists"); + assert_eq!(dormant.sovereign_account, identity.sovereign_account); + assert_eq!(dormant.owner, identity.owner); + assert!(Actors::actor_identity(actor_id + 1).is_none()); + }); +} + #[test] fn public_api_error_signatures_use_shared_typed_cores() { let _: fn(ActorId) -> Result, ActorClassificationError> = @@ -38,7 +64,8 @@ fn public_api_error_signatures_use_shared_typed_cores() { Mutability, RuntimeActorContract, SimulationMode, - ) -> Result, SimulationError> = Actors::simulate_current_contract; + crate::SimulationBudget, + ) -> Result = Actors::simulate_current_contract; let classification_cases = [ ( @@ -293,11 +320,22 @@ fn current_step_service_state_does_not_load_unreached_tail_chunks() { )); 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!( + assert!(Actors::actor_hot(actor_id).is_some_and(|hot| !hot.pending_signal)); + assert!(Actors::actor_run_state(actor_id).is_none()); + assert!(has_actor_event(|event| matches!( event, Event::CycleStarted { actor_id: id, .. } if *id == actor_id ))); + assert!(has_actor_event(|event| matches!( + event, + Event::CycleSummary { actor_id: id, result: CycleResult::Completed, outcomes, .. } + if *id == actor_id && outcomes.executed_steps == 1 && outcomes.committed_effectful_tasks == 0 + ))); + assert!(!ActorContractTailChunks::::contains_key(actor_id, 0)); + assert!(matches!( + Actors::load_actor_state(actor_id), + crate::LoadedActorStateOf::Corrupt + )); }); } @@ -395,7 +433,7 @@ fn running_execution_and_post_placement_ignore_unreached_tail_chunks() { )); 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())); + assert!(Actors::actor_hot(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); @@ -408,7 +446,7 @@ fn running_execution_and_post_placement_ignore_unreached_tail_chunks() { 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| { + assert!(Actors::actor_hot(actor_id).is_some_and(|hot| { hot.cycle_state == CycleState::Running && (hot.queue_ticket.is_some() || hot.wakeup_pointer.is_some()) })); @@ -420,12 +458,18 @@ 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()); + crate::ActorReadyHead::::put(9); + crate::ActorReadyTail::::put(9); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + let identity = Actors::actor_identity(actor_id).expect("identity exists"); + let hot = Actors::actor_hot(actor_id).expect("hot state exists"); + assert_eq!(hot.queue_ticket, Some(9)); let funding = ActorFunding::::get(actor_id).expect("funding exists"); - let admission = ActorAdmissionCertificates::::get(actor_id) + let admission = Actors::actor_control_cell(actor_id) + .map(|(_, cell)| cell.admission) .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) @@ -841,6 +885,8 @@ fn unconfigured_resource_weight_ports_fail_closed() { &step, Weight::from_parts(1, 1), crate::StepControlExecution { + task_effect: crate::TaskEffectExecution::NotInvoked, + action_fee_collected: false, phase: crate::StepControlPhase::Opening, outcome: crate::StepControlOutcome::Completed, placement: crate::StepControlPlacement::None, @@ -882,14 +928,14 @@ fn step_control_weight_context_matches_c6_head_and_tail_geometry() { }) ); assert_eq!( - Actors::step_control_weight_context(8, 0, 7, 8, 9, 10) - .expect("eight-Step head context exists") + Actors::step_control_weight_context(12, 0, 7, 8, 9, 10) + .expect("twelve-Step head context exists") .opening_tail_chunks, - 2, + 3, ); - for (cursor, steps_in_fragment) in [(1, 4), (4, 4), (5, 3), (7, 3)] { + for (cursor, steps_in_fragment) in [(1, 4), (4, 4), (5, 4), (8, 4), (9, 3), (11, 3)] { assert_eq!( - Actors::step_control_weight_context(8, cursor, 7, 8, 9, 10), + Actors::step_control_weight_context(12, cursor, 7, 8, 9, 10), Some(crate::StepControlWeightContext { cursor, steps_in_fragment, @@ -902,8 +948,11 @@ fn step_control_weight_context_matches_c6_head_and_tail_geometry() { ); } 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); + assert_eq!( + Actors::step_control_weight_context(12, 12, 0, 0, 0, 0), + None + ); + assert_eq!(Actors::step_control_weight_context(13, 0, 0, 0, 0, 0), None); } #[test] @@ -1044,6 +1093,161 @@ fn pipeline_machine_envelope_absorbs_stop_cycle_control_effect() { }); } +#[cfg(feature = "try-runtime")] +#[test] +fn try_state_rejects_funding_sources_outside_the_authored_contract() { + for corruption in 0..3 { + new_test_ext().execute_with(|| { + let mut steps = transfer_contract_steps(BOB, 1); + steps[0].task = Task::Transfer { + to: BOB, + asset: TestAsset::Native, + amount: AmountResolution::PercentageOfLastFunding(Perbill::one()), + }; + let actor_id = create_system_with(ALICE, manual_schedule(), None, steps); + let funding = crate::ActorFunding::::get(actor_id).expect("funding exists"); + assert_eq!(funding.funding_tracked_assets.len(), 1); + assert!(funding.funding_tracked_assets.contains(&TestAsset::Native)); + assert_ok!(Actors::do_try_state()); + crate::ActorFunding::::mutate(actor_id, |maybe| { + let tracked = &mut maybe + .as_mut() + .expect("funding exists") + .funding_tracked_assets; + if corruption != 1 { + tracked.remove(&TestAsset::Native); + } + if corruption != 0 { + tracked + .try_insert(TestAsset::Local(1)) + .expect("one extra source fits storage"); + } + }); + let result = Actors::do_try_state(); + assert!( + matches!( + result, + Err(polkadot_sdk::sp_runtime::TryRuntimeError::Other( + "ActorFunding tracked assets disagree with the Actor Contract" + )) + ), + "funding source corruption was not diagnosed: {result:?}" + ); + crate::ActorFunding::::insert(actor_id, funding); + assert_ok!(Actors::do_try_state()); + }); + } +} + +#[cfg(feature = "try-runtime")] +#[test] +fn try_state_qualifies_funding_sources_by_admitted_staking_positions() { + new_test_ext().execute_with(|| { + let mut steps = transfer_contract_steps(BOB, 1); + steps.try_push(steps[0].clone()).expect("second Step fits"); + steps.try_push(steps[0].clone()).expect("third Step fits"); + steps[0].task = Task::Transfer { + to: BOB, + asset: TestAsset::Native, + amount: AmountResolution::PercentageOfLastFunding(Perbill::one()), + }; + for step in steps.iter_mut().skip(1) { + step.task = Task::Unstake { + asset: TestAsset::Local(1), + shares: AmountResolution::PercentageOfLastFunding(Perbill::one()), + }; + } + let actor_id = create_system_with(ALICE, manual_schedule(), None, steps); + let funding = crate::ActorFunding::::get(actor_id).expect("funding exists"); + assert_eq!(funding.funding_tracked_assets.len(), 2); + assert_ok!(Actors::do_try_state()); + set_staking_share_asset_available(false); + assert_ok!(Actors::do_try_state()); + for corruption in 0..3 { + crate::ActorFunding::::mutate(actor_id, |maybe| { + let tracked = &mut maybe + .as_mut() + .expect("funding exists") + .funding_tracked_assets; + match corruption { + 0 => { + tracked.remove(&TestAsset::Native); + } + 1 => { + tracked + .try_insert(TestAsset::Local(2)) + .expect("source fits storage"); + } + _ => { + tracked.clear(); + } + } + }); + assert!(matches!( + Actors::do_try_state(), + Err(polkadot_sdk::sp_runtime::TryRuntimeError::Other( + "ActorFunding tracked assets disagree with the Actor Contract" + )) + )); + crate::ActorFunding::::insert(actor_id, funding.clone()); + assert_ok!(Actors::do_try_state()); + } + }); +} + +#[cfg(feature = "try-runtime")] +#[test] +fn try_state_rejects_zero_funding_snapshot_in_a_coherent_run() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let mut steps = transfer_contract_steps(BOB, 1); + steps.try_push(steps[0].clone()).expect("second Step fits"); + steps[0].task = Task::Transfer { + to: BOB, + asset: TestAsset::Native, + amount: AmountResolution::PercentageOfLastFunding(Perbill::from_percent(50)), + }; + let actor_id = create_system_with(ALICE, manual_schedule(), None, steps); + assert_ok!(ordinary_transfer_to_actor( + RuntimeOrigin::signed(ALICE), + actor_id, + TestAsset::Native, + 100 + )); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + Actors::execute_cycle(Weight::MAX); + let run = ActorRunStateStore::::get(actor_id).expect("first Step leaves a live Run"); + assert_eq!(run.cursor, 1); + assert_eq!(run.funding_snapshot.get(&TestAsset::Native), Some(&100)); + assert_ok!(Actors::do_try_state()); + let mut corrupt = run.clone(); + corrupt + .funding_snapshot + .try_insert(TestAsset::Native, 0) + .expect("existing key fits"); + ActorRunStateStore::::insert(actor_id, corrupt); + assert!( + ActorRunStateStore::::get(actor_id).is_some(), + "payload commitment remains coherent" + ); + let result = Actors::do_try_state(); + assert!( + matches!( + result, + Err(polkadot_sdk::sp_runtime::TryRuntimeError::Other( + "ActorRunState funding snapshot contains an untracked asset or zero amount" + )) + ), + "unreachable snapshot was not diagnosed: {result:?}" + ); + ActorRunStateStore::::insert(actor_id, run); + assert_ok!(Actors::do_try_state()); + }); +} + #[cfg(feature = "try-runtime")] #[test] fn try_state_rejects_pipeline_machine_envelope_drift() { @@ -1266,10 +1470,106 @@ fn contract_geometry_decomposition_is_gap_free_and_head_only_for_one_step() { }); } +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn contract_replacement_updates_frame_admission_and_current_step_resources_in_place() { + 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(make_step(Task::StopCycle)), + ); + let (_, before) = Actors::load_primary_control_cell(actor_id).expect("initial primary exists"); + let replacement = + system_active_contract(manual_schedule(), None, transfer_contract_steps(BOB, 7)) + .expect("replacement Contract"); + + assert_ok!(Actors::store_actor_contract(actor_id, replacement)); + + let (_, after) = + Actors::load_primary_control_cell(actor_id).expect("replacement primary exists"); + let projected_admission = Actors::actor_control_cell(actor_id) + .map(|(_, cell)| cell.admission) + .expect("canonical admission projection exists"); + let loaded_step = Actors::load_current_step_from_storage(actor_id, after.cursor) + .expect("replacement current Step loads"); + assert_ne!( + after.admission.admission_identity, + before.admission.admission_identity + ); + assert_eq!(after.admission, projected_admission); + assert_eq!(after.resources, loaded_step.resources); + }); +} + +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn dormant_activation_and_trigger_replacements_preserve_strict_frame_loading() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + assert_ok!(Actors::create_system_actor( + RuntimeOrigin::root(), + ALICE, + Mutability::Mutable, + None, + )); + let actor_id = 0; + frame_system::Pallet::::set_block_number(2); + assert_ok!(Actors::activate_actor( + RuntimeOrigin::root(), + actor_id, + system_active_contract(manual_schedule(), None, inert_contract_steps()) + .expect("Manual Contract"), + )); + assert!(matches!( + Actors::load_frame_actor_state(actor_id), + crate::LoadedActorStateOf::Active(_) + )); + + frame_system::Pallet::::set_block_number(3); + assert_ok!(update_contract_partial!( + RuntimeOrigin::root(), + actor_id, + timer_schedule(2), + None, + )); + assert!(matches!( + Actors::load_frame_actor_state(actor_id), + crate::LoadedActorStateOf::Active(_) + )); + + frame_system::Pallet::::set_block_number(4); + assert_ok!(Actors::pause_actor(RuntimeOrigin::root(), actor_id)); + assert!(matches!( + Actors::load_frame_actor_state(actor_id), + crate::LoadedActorStateOf::Active(_) + )); + + frame_system::Pallet::::set_block_number(5); + let address_schedule = Schedule { + trigger: Trigger::address_event(SourceFilter::Any, AssetFilter::Any), + cooldown_blocks: 0, + }; + assert_ok!(update_contract_partial!( + RuntimeOrigin::root(), + actor_id, + address_schedule, + None, + )); + assert!(matches!( + Actors::load_frame_actor_state(actor_id), + crate::LoadedActorStateOf::Active(_) + )); + assert_eq!(Actors::test_activation_plan_kind(actor_id), Ok(3)); + }); +} + #[test] fn admitted_contract_storage_loads_one_current_fragment_and_replaces_exact_tail() { new_test_ext().execute_with(|| { - let actor_id = 77; + frame_system::Pallet::::set_block_number(1); let contract = system_active_contract( manual_schedule(), None, @@ -1281,7 +1581,15 @@ fn admitted_contract_storage_loads_one_current_fragment_and_replaces_exact_tail( .expect("eight Steps fit"), ) .expect("active Contract"); - let certificate = geometry_certificate(&contract); + let actor_id = create_system_with(ALICE, manual_schedule(), None, contract.steps.clone()); + let certificate = Actors::actor_control_cell(actor_id) + .expect("live primary owns admission") + .1 + .admission; + assert_eq!( + Actors::remove_admitted_contract_geometry(actor_id), + Some(contract.clone()) + ); assert!(Actors::insert_admitted_contract_geometry( actor_id, &contract, @@ -1317,7 +1625,8 @@ fn admitted_contract_storage_loads_one_current_fragment_and_replaces_exact_tail( BoundedVec::try_from(vec![make_step(Task::StopCycle)]).expect("one Step fits"), ) .expect("replacement Contract"); - let replacement_certificate = geometry_certificate(&replacement); + let replacement_certificate = + Actors::build_admission_certificate(&replacement).expect("replacement host admission"); assert!(Actors::replace_admitted_contract_geometry( actor_id, &replacement, @@ -1326,12 +1635,17 @@ fn admitted_contract_storage_loads_one_current_fragment_and_replaces_exact_tail( 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()); + let primary_before = Actors::actor_control_cell(actor_id).expect("replacement primary"); assert_eq!( Actors::remove_admitted_contract_geometry(actor_id), Some(replacement) ); assert!(!ActorContractHeads::::contains_key(actor_id)); - assert!(!ActorAdmissionCertificates::::contains_key(actor_id)); + assert_eq!(Actors::actor_control_cell(actor_id), Some(primary_before)); + assert!(matches!( + Actors::load_actor_state(actor_id), + LoadedActorStateOf::Corrupt + )); assert!(Actors::remove_admitted_contract_geometry(actor_id).is_none()); }); } @@ -1354,7 +1668,7 @@ fn admitted_contract_storage_fails_closed_before_mutation_on_stale_authority() { &stale_certificate, )); assert!(!ActorContractHeads::::contains_key(actor_id)); - assert!(!ActorAdmissionCertificates::::contains_key(actor_id)); + assert!(!Actors::actor_control_cell(actor_id).is_some()); }); } @@ -1642,11 +1956,13 @@ fn public_reachability_inventory_is_closed_and_canonical() { "TypeMismatch", "MutabilityMismatch", "InvalidContract", + "InvalidBudget", "ContractMismatch", "ModeCycleStateMismatch", "GlobalCircuitBreaker", "Paused", "NotReady", + "ResourceDeferred", "FeeCollectionFailed", ]); } @@ -1659,77 +1975,6 @@ fn actor_storage_schema_is_explicit() { .iter() .all(|entry| entry.pallet_name == b"Actors") ); - let actual: alloc::vec::Vec<_> = storage_info - .iter() - .map(|entry| ::core::str::from_utf8(&entry.storage_name).expect("storage name is UTF-8")) - .collect(); - assert_eq!( - actual, - [ - "NextActorId", - "ActorHot", - "ActorContractHead", - "ActorActivationAuthority", - "ActorAdmissionCertificate", - "ActorContractTailChunk", - "ActorFunding", - "ActorRunHead", - "ActorRunPayload", - "ActorIdentities", - "ActorIdentityCount", - "ActorStateHolds", - "ActiveActorCount", - "SystemSovereigns", - "SystemSovereignCount", - "NextQueueTicket", - "PrepassExecutionCutoff", - "CurrentBlockResourceState", - "FinalizedBlockResourceTelemetry", - "QueueHead", - "QueueTail", - "QueueOccupancy", - "QueuePages", - "WakeupPages", - "WakeupBuckets", - "WakeupCursorPages", - "WakeupCursorLen", - "NextWakeupClock", - "WakeupWorkerFaultState", - "OwnerSlotBitmaps", - "SovereignIndex", - "ActiveActorLimit", - "IndexedTriggerDetectionDisabled", - "ActorObservationFeeds", - "ObservationSubscriptionSlot", - "ObservationSubscriptionSlotOwner", - "NextObservationSubscriptionSlot", - "ObservationFreeSlotLen", - "ObservationFreeSlotPages", - "ObservationSubscriberPages", - "ObservationSubscriberPageLists", - "ObservationSubscriberCount", - "ObservationSubscriptionCount", - "ObservationIngressRevisions", - "DirtyObservationFeeds", - "DirtyObservationListState", - "ObservationFanoutWorkerFaultState", - "CrossingMemberships", - "CrossingMemberPages", - "CrossingLeafStates", - "CrossingRadixNodes", - "CrossingFeedMembershipCount", - "CrossingUserFeedMembershipCount", - "CrossingTransitionQueues", - "CrossingPendingFeeds", - "CrossingPendingFeedListState", - "CrossingRangeCursors", - "CrossingWorkerFaultState", - "MaterializationFamilyCursor", - "GlobalCircuitBreaker", - "IdleStarvationState", - ] - ); - let metadata = Actors::storage_metadata(); assert_eq!(metadata.prefix, "Actors"); let actual_shapes: alloc::vec::Vec<_> = metadata @@ -1755,10 +2000,8 @@ fn actor_storage_schema_is_explicit() { actual_shapes, [ ("NextActorId", false, false), - ("ActorHot", true, true), ("ActorContractHead", true, true), ("ActorActivationAuthority", true, true), - ("ActorAdmissionCertificate", true, true), ("ActorContractTailChunk", true, true), ("ActorFunding", true, true), ("ActorRunHead", true, true), @@ -1769,16 +2012,20 @@ fn actor_storage_schema_is_explicit() { ("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), - ("QueuePages", true, true), - ("WakeupPages", true, true), - ("WakeupBuckets", true, true), + ("ActorUnsignaledControlCells", true, true), + ("ActorReadyFrameChunks", true, true), + ("ActorWaitingFrameChunks", true, true), + ("ActorControlLocators", true, true), + ("ActorReadyHead", false, false), + ("ActorReadyTail", false, false), + ("ActorReadyOccupancy", false, false), + ("ActorWaitingHeads", false, true), + ("ActorWaitingTails", false, true), + ("ActorWaitingOccupancies", false, true), + ("ActorWaitingCursorIndices", true, true), ("WakeupCursorPages", true, true), ("WakeupCursorLen", false, true), ("NextWakeupClock", false, false), @@ -1818,6 +2065,18 @@ fn actor_storage_schema_is_explicit() { ] ); + assert_eq!( + storage_info + .iter() + .map(|entry| ::core::str::from_utf8(&entry.storage_name).expect("storage name is UTF-8")) + .collect::>(), + actual_shapes + .iter() + .map(|(name, _, _)| *name) + .collect::>(), + "storage info and complete canonical metadata must agree without exclusions" + ); + let entry = |name: &str| { metadata .entries @@ -1826,38 +2085,43 @@ fn actor_storage_schema_is_explicit() { .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]); + + assert_plain_storage_type::(entry("NextActorId")); + assert_map_storage_types::>(entry("ActorContractHead")); + assert_map_storage_types::>(entry("ActorFunding")); + + assert_map_storage_types::>(entry( + "ActorActivationAuthority", + )); + assert_map_storage_types::>(entry( + "ActorUnsignaledControlCells", + )); + assert_map_storage_types::>(entry("ActorReadyFrameChunks")); + assert_map_storage_types::<(WakeupKey, u64), crate::ActorWaitingPageOf>( + entry("ActorWaitingFrameChunks"), + ); + assert_map_storage_types::>(entry( + "ActorControlLocators", + )); + assert_plain_storage_type::(entry("ActorReadyHead")); + assert_plain_storage_type::(entry("ActorReadyTail")); + assert_plain_storage_type::(entry("ActorReadyOccupancy")); + assert_map_storage_types::, u64>(entry("ActorWaitingHeads")); + assert_map_storage_types::, u64>(entry("ActorWaitingTails")); + assert_map_storage_types::, u32>(entry("ActorWaitingOccupancies")); + assert_map_storage_types::, u32>(entry("ActorWaitingCursorIndices")); + assert_plain_storage_type::<(MockBlockNumber, u64)>(entry("PrepassExecutionCutoff")); + assert_plain_storage_type::>(entry( + "CurrentBlockResourceState", + )); + assert_plain_storage_type::>(entry( + "FinalizedBlockResourceTelemetry", + )); let mut registry = scale_info::Registry::new(); let contract_type = @@ -1885,7 +2149,7 @@ fn actor_storage_schema_is_explicit() { "auto_close_at_cycle_nonce" ] ); - assert_map_storage_types::>(&entries[4]); + assert_map_storage_types::>(entry("ActorRunHead")); assert_map_storage_types::>(entry("ActorRunPayload")); let run_type = registry.register_type(&scale_info::meta_type::()); let (_, run_state) = registry @@ -1918,66 +2182,81 @@ fn actor_storage_schema_is_explicit() { "suspension" ] ); - assert_map_storage_types::>(&entries[5]); - assert_plain_storage_type::(&entries[6]); - assert_plain_storage_type::(&entries[7]); - assert_map_storage_types::(&entries[8]); - assert_plain_storage_type::(&entries[9]); - assert_plain_storage_type::(&entries[10]); - assert_plain_storage_type::(&entries[11]); - assert_plain_storage_type::(&entries[12]); - assert_plain_storage_type::(&entries[13]); - assert_map_storage_types::>(&entries[14]); - assert_map_storage_types::<(WakeupKey, u64), crate::WakeupPageOf>( - &entries[15], - ); - assert_map_storage_types::, WakeupBucketState>(&entries[16]); - assert_map_storage_types::<(WakeupClock, u64), crate::WakeupCursorPageOf>(&entries[17]); - assert_map_storage_types::(&entries[18]); - assert_plain_storage_type::(&entries[19]); - assert_plain_storage_type::>(&entries[20]); - assert_map_storage_types::(&entries[21]); - assert_map_storage_types::(&entries[22]); - assert_plain_storage_type::(&entries[23]); - assert_map_storage_types::>(&entries[24]); - assert_map_storage_types::(&entries[25]); - assert_map_storage_types::(&entries[26]); - assert_plain_storage_type::(&entries[27]); - assert_plain_storage_type::(&entries[28]); - assert_map_storage_types::>(&entries[29]); - assert_map_storage_types::<(u32, u32), crate::ObservationSubscriberPageOf>(&entries[30]); - assert_map_storage_types::(&entries[31]); - assert_map_storage_types::(&entries[32]); - assert_plain_storage_type::(&entries[33]); - assert_map_storage_types::(&entries[34]); + assert_map_storage_types::>(entry("ActorIdentities")); + assert_plain_storage_type::(entry("ActorIdentityCount")); + assert_plain_storage_type::(entry("ActiveActorCount")); + assert_map_storage_types::(entry("SystemSovereigns")); + assert_plain_storage_type::(entry("SystemSovereignCount")); + assert_map_storage_types::<(WakeupClock, u64), crate::WakeupCursorPageOf>(entry( + "WakeupCursorPages", + )); + assert_map_storage_types::(entry("WakeupCursorLen")); + assert_plain_storage_type::(entry("NextWakeupClock")); + assert_plain_storage_type::>(entry( + "WakeupWorkerFaultState", + )); + assert_map_storage_types::(entry("OwnerSlotBitmaps")); + assert_map_storage_types::(entry("SovereignIndex")); + assert_plain_storage_type::(entry("ActiveActorLimit")); + assert_map_storage_types::>(entry( + "ActorObservationFeeds", + )); + assert_map_storage_types::(entry("ObservationSubscriptionSlot")); + assert_map_storage_types::(entry("ObservationSubscriptionSlotOwner")); + assert_plain_storage_type::(entry("NextObservationSubscriptionSlot")); + assert_plain_storage_type::(entry("ObservationFreeSlotLen")); + assert_map_storage_types::>(entry( + "ObservationFreeSlotPages", + )); + assert_map_storage_types::<(u32, u32), crate::ObservationSubscriberPageOf>(entry( + "ObservationSubscriberPages", + )); + assert_map_storage_types::(entry( + "ObservationSubscriberPageLists", + )); + assert_map_storage_types::(entry("ObservationSubscriberCount")); + assert_plain_storage_type::(entry("ObservationSubscriptionCount")); + assert_map_storage_types::(entry("ObservationIngressRevisions")); assert_map_storage_types::< u32, crate::types::DirtyObservationState< u32, polkadot_sdk::frame_system::pallet_prelude::BlockNumberFor, >, - >(&entries[35]); - assert_plain_storage_type::>(&entries[36]); - assert_plain_storage_type::>(&entries[37]); - assert_map_storage_types::>(&entries[38]); + >(entry("DirtyObservationFeeds")); + assert_plain_storage_type::>(entry( + "DirtyObservationListState", + )); + assert_plain_storage_type::>(entry( + "ObservationFanoutWorkerFaultState", + )); + assert_map_storage_types::>(entry( + "CrossingMemberships", + )); assert_map_storage_types::< (crate::CrossingLeafKeyOf, u32), crate::CrossingMemberPageOf, - >(&entries[39]); - assert_map_storage_types::, crate::CrossingLeafState>( - &entries[40], - ); - assert_map_storage_types::, u16>(&entries[41]); - assert_map_storage_types::(&entries[42]); - assert_map_storage_types::(&entries[43]); - assert_map_storage_types::>(&entries[44]); - assert_map_storage_types::>(&entries[45]); - assert_plain_storage_type::>(&entries[46]); - assert_map_storage_types::(&entries[47]); - assert_plain_storage_type::>(&entries[48]); - assert_plain_storage_type::(&entries[49]); - assert_plain_storage_type::(&entries[50]); - assert_plain_storage_type::(&entries[51]); + >(entry("CrossingMemberPages")); + assert_map_storage_types::, crate::CrossingLeafState>(entry( + "CrossingLeafStates", + )); + assert_map_storage_types::, u16>(entry("CrossingRadixNodes")); + assert_map_storage_types::(entry("CrossingFeedMembershipCount")); + assert_map_storage_types::(entry("CrossingUserFeedMembershipCount")); + assert_map_storage_types::>(entry( + "CrossingTransitionQueues", + )); + assert_map_storage_types::>(entry( + "CrossingPendingFeeds", + )); + assert_plain_storage_type::>(entry( + "CrossingPendingFeedListState", + )); + assert_map_storage_types::(entry("CrossingRangeCursors")); + assert_plain_storage_type::>(entry("CrossingWorkerFaultState")); + assert_plain_storage_type::(entry("MaterializationFamilyCursor")); + assert_plain_storage_type::(entry("GlobalCircuitBreaker")); + assert_plain_storage_type::(entry("IdleStarvationState")); } #[test] @@ -2038,13 +2317,14 @@ fn owner_slot_bitmap_try_state_rejects_invalid_and_orphaned_bits() { #[test] fn live_head_consume_rolls_back_on_occupancy_or_span_corruption() { - for (tail, occupancy) in [(1u64, 0u32), (2u64, 1u32)] { + let excessive_span = u64::from(<::MaxQueueLength as Get>::get()) + 1; + for (tail, occupancy) in [(1u64, 0u32), (excessive_span, 1u32)] { 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)); - QueueTail::::put(tail); - QueueOccupancy::::put(occupancy); + assert!(enqueue_latched_actor(actor_id)); + crate::ActorReadyTail::::put(tail); + crate::ActorReadyOccupancy::::put(occupancy); let events_before = System::events(); let root_before = polkadot_sdk::sp_io::storage::root(StateVersion::V1); @@ -2078,7 +2358,7 @@ fn reverse_index_corruption_matrix_fails_closed_for_system_close() { assert_ok!(Actors::deactivate_actor(RuntimeOrigin::root(), actor_id)); frame_system::Pallet::::set_block_number(2); } - let identity = Actors::actor_identities(actor_id).expect("system identity"); + let identity = Actors::actor_identity(actor_id).expect("system identity"); let sovereign_id = match identity.actor_class { ActorClass::System { sovereign_id } => sovereign_id, _ => unreachable!(), @@ -2133,7 +2413,7 @@ fn reverse_index_corruption_matrix_fails_closed_for_user_close() { None, inert_contract_steps(), ); - let identity = Actors::actor_identities(actor_id).expect("user identity"); + let identity = Actors::actor_identity(actor_id).expect("user identity"); match corruption { 0 => SovereignIndex::::remove(&identity.sovereign_account), 1 => SovereignIndex::::insert(&identity.sovereign_account, other), @@ -2194,23 +2474,14 @@ fn temporal_membership_try_state_rejects_terminal_at_drift() { assert_ok!(crate::Pallet::::do_try_state()); // Terminal membership is derived from the schedule window: any `terminal_at` that is not // the exact window terminal (or absent without a window) must fail try_state. - ActorHot::::mutate(actor_id, |maybe| { - maybe.as_mut().expect("hot").terminal_at = Some(999); - }); + mutate_primary_control_cell(actor_id, |cell| cell.hot.terminal_at = Some(999)); assert_eq!( crate::Pallet::::do_try_state().map_err(|error| format!("{error:?}")), - Err( - "Other(\"ActorHot terminal_at disagrees with schedule window terminal membership\")" - .into() - ) + Err("Other(\"ActorControl primary cannot restore its semantic identity\")".into()) ); - ActorHot::::mutate(actor_id, |maybe| { - maybe.as_mut().expect("hot").terminal_at = None; - }); + mutate_primary_control_cell(actor_id, |cell| cell.hot.terminal_at = None); assert!(crate::Pallet::::do_try_state().is_err()); - ActorHot::::mutate(actor_id, |maybe| { - maybe.as_mut().expect("hot").terminal_at = Some(102); - }); + mutate_primary_control_cell(actor_id, |cell| cell.hot.terminal_at = Some(102)); assert_ok!(crate::Pallet::::do_try_state()); } }); @@ -2237,21 +2508,23 @@ fn temporal_membership_try_state_rejects_page_slot_pointing_at_different_actor() frame_system::Pallet::::set_block_number(1); let actor_id = create_system_with(ALICE, manual_schedule(), None, inert_contract_steps()); 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)); + assert!(schedule_latched_service_wakeup(actor_id, 10)); + assert!(schedule_latched_service_wakeup(other, 10)); // 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| { + crate::ActorWaitingFrameChunks::::mutate((WakeupKey::Block(10), 0), |maybe| { let page = maybe.as_mut().expect("wakeup page"); - page.entries[0] = Some(crate::WakeupEntry { actor_id: other }); + let crate::ActorWaitingEntry::Primary(cell) = + page.entries[0].as_mut().expect("occupied temporal slot") + else { + panic!("service wakeup owns the primary"); + }; + cell.actor_id = other; }); #[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 clock-domain pointer\")" - .into() - ) + Err("Other(\"ActorControl frame topology is corrupt\")".into()) ); }); } @@ -2313,6 +2586,68 @@ fn missing_frozen_snapshot_is_a_permanent_invariant_failure() { }); } +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn canonical_loader_rejects_contract_admission_disagreement() { + 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 crate::LoadedActorStateOf::Active(frame_state) = + Actors::load_actor_state_for_frame_control(actor_id) + else { + panic!("canonical frame state must remain active"); + }; + assert!(!frame_state.hot.pending_signal); + assert_eq!(frame_state.hot.unsuccessful_attempt_streak, 0); + assert!(matches!( + frame_state.hot.trigger_runtime_state, + TriggerRuntimeState::Stateless + )); + assert_eq!(frame_state.identity.cycle_nonce, 0); + let projected = Actors::active_actor_state(actor_id).expect("frame-owned projection is active"); + assert!(!projected.hot.pending_signal); + assert_eq!(projected.hot.unsuccessful_attempt_streak, 0); + assert_eq!(projected.identity.cycle_nonce, 0); + + let (service_state, service_admission, loaded_step) = + Actors::load_frame_actor_service_state(actor_id).expect("frame service state remains live"); + assert_eq!(service_state.identity.cycle_nonce, 0); + assert!(!service_state.hot.pending_signal); + assert!(service_admission.has_valid_identity()); + assert_eq!(loaded_step.expect("current Step remains loaded").cursor, 0); + #[cfg(feature = "try-runtime")] + assert_ok!(crate::Pallet::::do_try_state()); + + ActorContractHeads::::mutate(actor_id, |maybe| { + maybe + .as_mut() + .expect("Contract head remains live") + .header + .body_commitment[0] ^= 1; + }); + assert!(matches!( + Actors::load_frame_actor_state(actor_id), + crate::LoadedActorStateOf::Corrupt + )); + assert!(Actors::active_actor_state(actor_id).is_none()); + }); +} + +#[cfg(all(feature = "try-runtime", not(feature = "runtime-benchmarks")))] +#[test] +fn try_state_rejects_orphan_primary_without_locator() { + 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()); + crate::ActorControlLocators::::remove(actor_id); + assert!(crate::ActorUnsignaledControlCells::::contains_key( + actor_id + )); + assert!(!crate::ActorControlLocators::::contains_key(actor_id)); + assert!(crate::Pallet::::do_try_state().is_err()); + }); +} + #[test] fn canonical_loader_distinguishes_absence_dormancy_active_and_corruption() { new_test_ext().execute_with(|| { @@ -2339,10 +2674,7 @@ fn canonical_loader_distinguishes_absence_dormancy_active_and_corruption() { Actors::load_actor_state(actor_id), LoadedActorStateOf::Active(_) )); - ActorHot::::mutate(actor_id, |maybe| { - maybe.as_mut().expect("active actor").pending_signal = true; - }); - assert!(Actors::pending_signal(actor_id)); + assert!(!Actors::pending_signal(actor_id)); ActorFunding::::remove(actor_id); assert!(matches!( Actors::load_actor_state(actor_id), @@ -2358,7 +2690,7 @@ fn canonical_loader_distinguishes_absence_dormancy_active_and_corruption() { Error::::ActorInvariant ); - ActorIdentities::::remove(actor_id); + crate::ActorControlLocators::::remove(actor_id); assert!(matches!( Actors::load_actor_state(actor_id), LoadedActorStateOf::Corrupt @@ -2367,63 +2699,37 @@ fn canonical_loader_distinguishes_absence_dormancy_active_and_corruption() { } #[test] -fn canonical_loader_classifies_every_four_partition_presence_mask() { +fn canonical_loader_classifies_every_primary_partition_presence_mask() { for mask in 0u8..16 { 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::::take(actor_id).expect("identity fixture"); - let hot = ActorHot::::take(actor_id).expect("hot fixture"); - let contract = Actors::load_actor_contract(actor_id).expect("contract fixture"); - assert!(Actors::remove_admitted_contract_geometry(actor_id).is_some()); + let cell = + crate::ActorUnsignaledControlCells::::take(actor_id).expect("primary fixture"); + let locator = crate::ActorControlLocators::::take(actor_id).expect("locator fixture"); + let head = ActorContractHeads::::take(actor_id).expect("Contract head fixture"); let funding = ActorFunding::::take(actor_id).expect("funding fixture"); - ActorRunStateStore::::remove(actor_id); if mask & 0b0001 != 0 { - ActorIdentities::::insert(actor_id, identity); + crate::ActorUnsignaledControlCells::::insert(actor_id, cell); } if mask & 0b0010 != 0 { - ActorHot::::insert(actor_id, hot); + crate::ActorControlLocators::::insert(actor_id, locator); } if mask & 0b0100 != 0 { - assert_ok!(Actors::store_actor_contract(actor_id, contract)); + ActorContractHeads::::insert(actor_id, head); } if mask & 0b1000 != 0 { ActorFunding::::insert(actor_id, funding); } + let loaded = Actors::load_actor_state(actor_id); match mask { - 0 => assert!(matches!( - Actors::load_actor_state(actor_id), - LoadedActorStateOf::NotRegistered - )), - 1 => assert!(matches!( - Actors::load_actor_state(actor_id), - LoadedActorStateOf::Dormant(_) - )), - 15 => assert!(matches!( - Actors::load_actor_state(actor_id), - LoadedActorStateOf::Active(_) - )), - _ => { - assert!(matches!( - Actors::load_actor_state(actor_id), - LoadedActorStateOf::Corrupt - )); - let before = polkadot_sdk::sp_io::storage::root(StateVersion::V1); - assert_eq!( - Actors::actor_eligibility(actor_id), - Err(ActorClassificationError::ActorInvariant) - ); - assert_noop!( - Actors::pause_actor(RuntimeOrigin::root(), actor_id), - Error::::ActorInvariant - ); - assert_noop!( - Actors::preflight_funding_event(actor_id, TestAsset::Native, 1, None, None), - Error::::ActorInvariant - ); - assert_eq!(polkadot_sdk::sp_io::storage::root(StateVersion::V1), before); - } + 0 => assert!(matches!(loaded, LoadedActorStateOf::NotRegistered)), + 15 => assert!(matches!(loaded, LoadedActorStateOf::Active(_))), + _ => assert!( + matches!(loaded, LoadedActorStateOf::Corrupt), + "mask {mask:04b}" + ), } }); } @@ -2431,30 +2737,90 @@ fn canonical_loader_classifies_every_four_partition_presence_mask() { #[cfg(feature = "try-runtime")] #[test] -fn try_state_rejects_every_corrupt_four_partition_presence_mask() { - for mask in 2u8..15 { +fn try_state_rejects_contract_tail_keys_outside_the_admitted_geometry() { + for orphan in [false, true] { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let step = transfer_contract_steps(BOB, 1)[0].clone(); + let actor_id = create_system_with( + ALICE, + manual_schedule(), + None, + BoundedVec::try_from(vec![step.clone(), step]).expect("two Steps fit"), + ); + assert_ok!(Actors::do_try_state()); + let chunk = ActorContractTailChunks::::get(actor_id, 0).expect("admitted tail"); + let key = if orphan { + (actor_id + 100, 0) + } else { + (actor_id, 1) + }; + ActorContractTailChunks::::insert(key.0, key.1, chunk); + assert!( + Actors::do_try_state().is_err(), + "unowned tail {key:?} was accepted" + ); + ActorContractTailChunks::::remove(key.0, key.1); + assert_ok!(Actors::do_try_state()); + }); + } +} + +#[cfg(feature = "try-runtime")] +#[test] +fn try_state_rejects_orphan_activation_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 head = ActorContractHeads::::get(actor_id).expect("admitted Contract"); + assert_ok!(Actors::do_try_state()); + let orphan_id = actor_id + 100; + crate::ActorActivationAuthorities::::insert( + orphan_id, + crate::ActorActivationAuthority { + feed: 1, + cooldown_blocks: head.header.cooldown_blocks, + window: head.header.window, + auto_close_at_cycle_nonce: head.header.auto_close_at_cycle_nonce, + semantic_contract_id: head.header.semantic_contract_id, + body_commitment: head.header.body_commitment, + admission_identity: head.header.admission_identity, + }, + ); + assert!( + Actors::do_try_state().is_err(), + "orphan activation authority was accepted" + ); + crate::ActorActivationAuthorities::::remove(orphan_id); + assert_ok!(Actors::do_try_state()); + }); +} + +#[cfg(feature = "try-runtime")] +#[test] +fn try_state_rejects_every_incomplete_primary_partition_presence_mask() { + for mask in 0u8..15 { 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::::take(actor_id).expect("identity fixture"); - let hot = ActorHot::::take(actor_id).expect("hot fixture"); - let contract = Actors::load_actor_contract(actor_id).expect("contract fixture"); - assert!(Actors::remove_admitted_contract_geometry(actor_id).is_some()); + let cell = + crate::ActorUnsignaledControlCells::::take(actor_id).expect("primary fixture"); + let locator = crate::ActorControlLocators::::take(actor_id).expect("locator fixture"); + let head = ActorContractHeads::::take(actor_id).expect("Contract head fixture"); let funding = ActorFunding::::take(actor_id).expect("funding fixture"); if mask & 0b0001 != 0 { - ActorIdentities::::insert(actor_id, identity); + crate::ActorUnsignaledControlCells::::insert(actor_id, cell); } if mask & 0b0010 != 0 { - ActorHot::::insert(actor_id, hot); + crate::ActorControlLocators::::insert(actor_id, locator); } if mask & 0b0100 != 0 { - assert_ok!(Actors::store_actor_contract(actor_id, contract)); + ActorContractHeads::::insert(actor_id, head); } if mask & 0b1000 != 0 { ActorFunding::::insert(actor_id, funding); } - crate::ActorIdentityCount::::put(u32::from(mask & 0b0001 != 0)); - crate::ActiveActorCount::::put(u32::from(mask & 0b0010 != 0)); + assert!( crate::Pallet::::do_try_state().is_err(), "mask {mask:04b}" @@ -2465,20 +2831,20 @@ fn try_state_rejects_every_corrupt_four_partition_presence_mask() { #[cfg(feature = "try-runtime")] #[test] -fn try_state_rejects_system_identity_with_noncanonical_derived_account() { +fn try_state_rejects_system_reverse_index_outside_derived_account() { 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 original = ActorIdentities::::get(actor_id).expect("System identity fixture"); + let original = Actors::actor_identity(actor_id).expect("System identity fixture"); let replacement_account = 999_998; crate::SovereignIndex::::remove(original.sovereign_account); - ActorIdentities::::mutate(actor_id, |maybe| { - maybe - .as_mut() - .expect("System identity fixture") - .sovereign_account = replacement_account; - }); crate::SovereignIndex::::insert(replacement_account, actor_id); + assert_eq!( + Actors::actor_identity(actor_id) + .expect("identity derives its account") + .sovereign_account, + original.sovereign_account + ); assert!(crate::Pallet::::do_try_state().is_err()); }); } @@ -2503,6 +2869,7 @@ fn eligibility_projection_rejects_partial_active_partitions() { Mutability::Mutable, expected_contract, SimulationMode::FreshCurrentPlan, + ample_simulation_budget(), ), Err(SimulationError::Classification( ActorClassificationError::ActorInvariant diff --git a/template/pallets/actors/src/tests/waiting_integrity.rs b/template/pallets/actors/src/tests/waiting_integrity.rs new file mode 100644 index 00000000..43c06d54 --- /dev/null +++ b/template/pallets/actors/src/tests/waiting_integrity.rs @@ -0,0 +1,443 @@ +//! Focused corruption and ownership witnesses for the canonical Waiting container. + +use super::*; +use crate::{ + ActorControlCellOf, ActorControlLocation, ActorControlLocators, ActorUnsignaledControlCells, + ActorWaitingEntry, ActorWaitingFrameChunks, ActorWaitingOccupancies, + scheduler::{ActorControlTransitionError, ActorWaitingAuthority}, +}; +use polkadot_sdk::sp_runtime::StateVersion; + +#[test] +fn mixed_waiting_pages_reclaim_both_entry_kinds_through_real_close() { + for primary_first in [true, false] { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let primaries = (0..33) + .map(|_| create_system_with(ALICE, timer_schedule(200), None, inert_contract_steps())) + .collect::>(); + let step = inert_contract_steps()[0].clone(); + let steps = BoundedVec::try_from(vec![step.clone(), step]).expect("two Steps fit"); + let references = (0..33) + .map(|_| create_system_with(ALICE, timer_schedule(100), None, steps.clone())) + .collect::>(); + for block in 101..=111 { + frame_system::Pallet::::set_block_number(block); + Actors::on_idle(block, Weight::MAX); + } + let key = WakeupKey::Tick(201); + for (index, actor) in references.iter().enumerate() { + let run = Actors::actor_run_state(*actor).expect("real Opening retains a suffix"); + assert_eq!(run.cursor, 1); + assert_eq!(run.last_committed_step_block, Some(101 + index as u64 / 3)); + let pointer = Actors::actor_hot(*actor) + .expect("Running Actor") + .trigger_wakeup_pointer + .expect("Running cadence is rearmed"); + assert_eq!(pointer.tick, 201); + let page = + ActorWaitingFrameChunks::::get((key, pointer.page_id)).expect("rearmed page"); + assert!(matches!(&page.entries[pointer.slot as usize], + Some(ActorWaitingEntry::Reference(reference)) if reference.actor_id == *actor)); + } + // Append after real rearming has settled: lifecycle churn need not compact earlier holes. + let later_primaries = (0..33) + .map(|_| create_system_with(ALICE, timer_schedule(90), None, inert_contract_steps())) + .collect::>(); + let middle = ActorWaitingFrameChunks::::get((key, 3)).expect("mixed middle page"); + assert!(matches!(&middle.entries[0], + Some(ActorWaitingEntry::Reference(reference)) if reference.actor_id == references[32])); + for (entry, actor) in middle.entries.iter().skip(1).zip(&later_primaries[..31]) { + assert!(matches!(entry, Some(ActorWaitingEntry::Primary(cell)) if cell.actor_id == *actor)); + } + let assert_directory = |pages: &[(u64, u32)], total, tail| { + assert_eq!(ActorWaitingOccupancies::::get(key), total); + assert_eq!(crate::ActorWaitingTails::::get(key), tail); + for (index, (page_id, live)) in pages.iter().enumerate() { + let page = + ActorWaitingFrameChunks::::get((key, *page_id)).expect("linked live page"); + assert_eq!(page.live_entries, *live); + assert_eq!( + page.entries.iter().filter(|entry| entry.is_some()).count(), + *live as usize + ); + assert_eq!(page.scan_slot, 0); + assert_eq!( + page.previous_page, + index.checked_sub(1).map(|previous| pages[previous].0) + ); + assert_eq!(page.next_page, pages.get(index + 1).map(|next| next.0)); + } + assert_eq!(crate::ActorWaitingHeads::::get(key), 0); + assert!(crate::ActorWaitingCursorIndices::::contains_key(key)); + assert_eq!(Actors::wakeup_cursor_peek_key(WakeupClock::Tick), Some(key)); + #[cfg(feature = "try-runtime")] + { + let before = polkadot_sdk::sp_io::storage::root(StateVersion::V1); + assert_ok!(Actors::do_try_state()); + assert_eq!(polkadot_sdk::sp_io::storage::root(StateVersion::V1), before); + } + }; + assert_directory(&[(0, 32), (1, 1), (2, 32), (3, 32), (4, 2)], 99, 130); + let mut middle_removals = later_primaries[..31].to_vec(); + if primary_first { + middle_removals.push(references[32]); + } else { + middle_removals.insert(0, references[32]); + } + for (index, actor) in middle_removals.into_iter().enumerate() { + assert_ok!(Actors::close_actor(RuntimeOrigin::root(), actor)); + let remaining = 31 - index as u32; + if remaining > 0 { + assert_directory( + &[(0, 32), (1, 1), (2, 32), (3, remaining), (4, 2)], + 67 + remaining, + 130, + ); + } + } + assert!(!ActorWaitingFrameChunks::::contains_key((key, 3))); + assert_directory(&[(0, 32), (1, 1), (2, 32), (4, 2)], 67, 130); + assert_ok!(Actors::close_actor( + RuntimeOrigin::root(), + later_primaries[31] + )); + assert_directory(&[(0, 32), (1, 1), (2, 32), (4, 1)], 66, 130); + assert_ok!(Actors::close_actor( + RuntimeOrigin::root(), + later_primaries[32] + )); + assert!(!ActorWaitingFrameChunks::::contains_key((key, 4))); + assert_directory(&[(0, 32), (1, 1), (2, 32)], 65, 96); + for (index, actor) in references[..32].iter().enumerate() { + assert_ok!(Actors::close_actor(RuntimeOrigin::root(), *actor)); + if index < 31 { + assert_directory( + &[(0, 32), (1, 1), (2, 31 - index as u32)], + 64 - index as u32, + 96, + ); + } + } + assert!(!ActorWaitingFrameChunks::::contains_key((key, 2))); + assert_directory(&[(0, 32), (1, 1)], 33, 64); + assert_ok!(Actors::close_actor(RuntimeOrigin::root(), primaries[32])); + assert!(!ActorWaitingFrameChunks::::contains_key((key, 1))); + assert_directory(&[(0, 32)], 32, 32); + for (index, actor) in primaries[..32].iter().enumerate() { + assert_ok!(Actors::close_actor(RuntimeOrigin::root(), *actor)); + if index < 31 { + assert_directory(&[(0, 31 - index as u32)], 31 - index as u32, 32); + } + } + assert!(!ActorWaitingFrameChunks::::contains_key((key, 0))); + assert!(!ActorWaitingOccupancies::::contains_key(key)); + assert!(!crate::ActorWaitingHeads::::contains_key(key)); + assert!(!crate::ActorWaitingTails::::contains_key(key)); + assert!(!crate::ActorWaitingCursorIndices::::contains_key(key)); + assert_eq!(Actors::wakeup_cursor_peek_key(WakeupClock::Tick), None); + #[cfg(feature = "try-runtime")] + assert_ok!(Actors::do_try_state()); + }); + } +} + +#[cfg(feature = "try-runtime")] +#[test] +fn primary_inline_authority_matches_canonical_cursor_and_resources() { + for zero_step in [false, true] { + for corruption in 0..3 { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let steps = if zero_step { + BoundedVec::default() + } else { + inert_contract_steps() + }; + let actor_id = create_system_with(ALICE, manual_schedule(), None, steps); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + assert_ok!(Actors::do_try_state()); + mutate_primary_control_cell(actor_id, |cell| match corruption { + 0 => cell.cursor += 1, + 1 => { + cell.resources.control = cell + .resources + .control + .saturating_add(Weight::from_parts(1, 0)) + } + _ => { + cell.resources.effect = cell + .resources + .effect + .saturating_add(Weight::from_parts(0, 1)) + } + }); + let before = polkadot_sdk::sp_io::storage::root(StateVersion::V1); + let expected = if corruption == 0 { + "ActorControl primary cursor disagrees with canonical Run authority" + } else { + "ActorControl primary resources disagree with canonical Step authority" + }; + assert_eq!( + Actors::do_try_state(), + Err(polkadot_sdk::sp_runtime::TryRuntimeError::Other(expected)) + ); + assert_eq!(polkadot_sdk::sp_io::storage::root(StateVersion::V1), before); + }); + } + } +} + +fn temporal_primary() -> ( + ActorId, + ActorControlCellOf, + WakeupKey, +) { + let actor_id = create_system_with(ALICE, timer_schedule(5), None, inert_contract_steps()); + let (location, cell) = Actors::actor_control_cell(actor_id) + .expect("ordinary temporal creation installs canonical control authority"); + let ActorControlLocation::Waiting { key, .. } = location else { + panic!("an unlatched temporal Actor must own a Waiting primary"); + }; + assert!(matches!(key, WakeupKey::Tick(_))); + (actor_id, cell, key) +} + +fn temporal_reference() -> ( + ActorId, + ActorControlCellOf, + WakeupKey, +) { + let (actor_id, _, key) = temporal_primary(); + let mut cell = Actors::remove_primary_control_cell_inner(actor_id) + .expect("fixture moves primary ownership before installing a separate reference"); + cell.hot.trigger_wakeup_pointer = None; + let cell = Actors::control_schedule_fresh_wakeup_reference(cell, key) + .expect("one fresh reference retains the same temporal obligation"); + ActorUnsignaledControlCells::::insert(actor_id, &cell); + ActorControlLocators::::insert(actor_id, ActorControlLocation::Unsignaled); + assert!(!ActorIdentities::::contains_key(actor_id)); + (actor_id, cell, key) +} + +#[test] +fn stale_ready_plan_rolls_back_waiting_primary_demotion() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let (actor_id, cell, key) = temporal_primary(); + let location = ActorControlLocators::::get(actor_id).unwrap(); + let pointer = cell.hot.trigger_wakeup_pointer.unwrap(); + let mut hot = Actors::actor_hot(actor_id).unwrap(); + hot.pending_signal = true; + let plan = Actors::preflight_paged_enqueue_cohort_with_authority(vec![(actor_id, hot)]) + .expect("retained temporal primary admits a prepared Ready publication"); + let old_tail = Actors::queue_tail(); + let other = create_system_with(ALICE, manual_schedule(), None, inert_contract_steps()); + assert_ok!(Actors::manual_trigger(RuntimeOrigin::signed(ALICE), other)); + assert_eq!(Actors::queue_tail(), old_tail + 1); + let page_before = ActorWaitingFrameChunks::::get((key, pointer.page_id)).unwrap(); + let before = polkadot_sdk::sp_io::storage::root(StateVersion::V1); + let result = + polkadot_sdk::frame_support::storage::transactional::with_transaction_opaque_err(|| { + let result = Actors::commit_paged_enqueue(plan); + assert_eq!( + result, + Err(crate::scheduler::EnqueueOutcome::CorruptedTopology) + ); + // The stale-tail guard follows source detachment: prove this rollback exercises demotion. + assert!(!ActorControlLocators::::contains_key(actor_id)); + let page = ActorWaitingFrameChunks::::get((key, pointer.page_id)).unwrap(); + let Some(ActorWaitingEntry::Reference(reference)) = &page.entries[pointer.slot as usize] + else { + panic!("retained trigger obligation must survive transient primary detachment"); + }; + assert_eq!(reference.actor_id, actor_id); + assert_eq!( + reference.admission_identity, + cell.admission.admission_identity + ); + polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(result) + }) + .expect("transaction nesting is available"); + assert_eq!( + result, + Err(crate::scheduler::EnqueueOutcome::CorruptedTopology) + ); + assert_eq!(polkadot_sdk::sp_io::storage::root(StateVersion::V1), before); + assert_eq!(ActorControlLocators::::get(actor_id), Some(location)); + assert_eq!(Actors::actor_control_cell(actor_id), Some((location, cell))); + assert_eq!( + ActorWaitingFrameChunks::::get((key, pointer.page_id)), + Some(page_before) + ); + }); +} + +#[test] +fn waiting_append_rejects_an_existing_primary_without_mutation() { + for retain_locator in [true, false] { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let (actor_id, cell, key) = temporal_primary(); + if !retain_locator { + // Even a missing locator must not permit overwriting an occupied primary slot. + ActorControlLocators::::remove(actor_id); + } + let before = polkadot_sdk::sp_io::storage::root(StateVersion::V1); + assert_eq!( + Actors::control_append_waiting(cell, key, ActorWaitingAuthority::Trigger), + Err(ActorControlTransitionError::Invariant) + ); + assert_eq!(polkadot_sdk::sp_io::storage::root(StateVersion::V1), before); + }); + } +} + +#[test] +fn waiting_reference_cannot_collapse_while_another_primary_is_live() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let (actor_id, cell, key) = temporal_reference(); + let pointer = cell + .hot + .trigger_wakeup_pointer + .expect("reference pointer exists"); + assert!(matches!( + ActorWaitingFrameChunks::::get((key, pointer.page_id)).and_then(|page| page + .entries + .get(pointer.slot as usize) + .cloned() + .flatten()), + Some(ActorWaitingEntry::Reference(_)) + )); + let before = polkadot_sdk::sp_io::storage::root(StateVersion::V1); + assert_eq!( + Actors::control_append_waiting(cell, key, ActorWaitingAuthority::Trigger), + Err(ActorControlTransitionError::Invariant) + ); + assert_eq!(polkadot_sdk::sp_io::storage::root(StateVersion::V1), before); + assert_eq!( + ActorControlLocators::::get(actor_id), + Some(ActorControlLocation::Unsignaled) + ); + }); +} + +#[test] +fn waiting_removal_rejects_page_and_global_occupancy_disagreement_without_mutation() { + for (live_entries, corrupt_occupancy) in [(1, 2), (2, 1)] { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let (actor_id, _, key) = temporal_primary(); + if live_entries == 2 { + let (_, _, second_key) = temporal_primary(); + assert_eq!(second_key, key); + } + assert_eq!(ActorWaitingOccupancies::::get(key), live_entries); + ActorWaitingOccupancies::::insert(key, corrupt_occupancy); + let before = polkadot_sdk::sp_io::storage::root(StateVersion::V1); + assert_eq!( + Actors::remove_primary_control_cell_inner(actor_id), + Err(ActorControlTransitionError::Invariant) + ); + assert_eq!(polkadot_sdk::sp_io::storage::root(StateVersion::V1), before); + }); + } +} + +#[test] +fn waiting_reference_with_wrong_admission_cannot_resolve_or_latch() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let (actor_id, cell, key) = temporal_reference(); + let pointer = cell + .hot + .trigger_wakeup_pointer + .expect("reference pointer exists"); + assert!(matches!( + Actors::load_actor_state(actor_id), + crate::LoadedActorStateOf::Active(_) + )); + ActorWaitingFrameChunks::::mutate((key, pointer.page_id), |page| { + let entry = page + .as_mut() + .and_then(|page| page.entries.get_mut(pointer.slot as usize)) + .and_then(Option::as_mut) + .expect("reference slot exists"); + let ActorWaitingEntry::Reference(reference) = entry else { + panic!("fixture owns a separate reference"); + }; + assert_eq!(reference.actor_id, actor_id); + assert_eq!( + reference.admission_identity, + cell.admission.admission_identity + ); + reference.admission_identity[0] ^= 1; + }); + let before = polkadot_sdk::sp_io::storage::root(StateVersion::V1); + assert!(matches!( + Actors::load_actor_state(actor_id), + crate::LoadedActorStateOf::Corrupt + )); + assert_eq!( + Actors::control_due_wakeup_primary(key, 1, pointer.tick), + Err(ActorControlTransitionError::Invariant) + ); + assert_eq!( + Actors::control_latch_due_temporal_reference(key, 1, pointer.tick), + Err(ActorControlTransitionError::Invariant) + ); + assert_eq!(polkadot_sdk::sp_io::storage::root(StateVersion::V1), before); + assert!( + !ActorUnsignaledControlCells::::get(actor_id) + .expect("original primary remains") + .hot + .pending_signal + ); + }); +} + +#[test] +fn waiting_primary_pointer_must_address_its_own_slot() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let (actor_id, cell, key) = temporal_primary(); + assert!(matches!( + Actors::load_actor_state(actor_id), + crate::LoadedActorStateOf::Active(_) + )); + let pointer = cell + .hot + .trigger_wakeup_pointer + .expect("Tick primary pointer exists"); + let reference_slot = pointer.slot + 1; + ActorWaitingFrameChunks::::mutate((key, pointer.page_id), |page| { + let page = page.as_mut().expect("primary page exists"); + assert!(page.entries[reference_slot as usize].is_none()); + page.entries[reference_slot as usize] = + Some(ActorWaitingEntry::Reference(crate::ActorWakeupReference { + actor_id, + admission_identity: cell.admission.admission_identity, + })); + page.live_entries += 1; + }); + ActorWaitingOccupancies::::mutate(key, |count| *count += 1); + mutate_primary_control_cell(actor_id, |primary| { + primary + .hot + .trigger_wakeup_pointer + .as_mut() + .expect("primary pointer exists") + .slot = reference_slot; + }); + let before = polkadot_sdk::sp_io::storage::root(StateVersion::V1); + assert!(matches!( + Actors::load_actor_state(actor_id), + crate::LoadedActorStateOf::Corrupt + )); + assert_eq!(polkadot_sdk::sp_io::storage::root(StateVersion::V1), before); + }); +} diff --git a/template/pallets/actors/src/tests/wakeups.rs b/template/pallets/actors/src/tests/wakeups.rs index 03fedb01..31d40c53 100644 --- a/template/pallets/actors/src/tests/wakeups.rs +++ b/template/pallets/actors/src/tests/wakeups.rs @@ -11,12 +11,15 @@ fn paged_wakeup_primitives_encode_exact_pointer_and_bounded_page_ownership() { assert_eq!(pointer.page_id, 7); assert_eq!(pointer.slot, 3); - let entries = - BoundedVec::, ::WakeupPageSize>::try_from(vec![ - Some(WakeupEntry { actor_id: 9 }), - None, - ]) - .expect("wakeup page entries fit"); + let reference = crate::ActorWakeupReference { + actor_id: 9, + admission_identity: [1; 32], + }; + let entries = crate::ActorWaitingChunkOf::::try_from(vec![ + Some(crate::ActorWaitingEntry::Reference(reference.clone())), + None, + ]) + .expect("wakeup page entries fit"); let page = WakeupPage { entries, live_entries: 1, @@ -24,12 +27,15 @@ fn paged_wakeup_primitives_encode_exact_pointer_and_bounded_page_ownership() { previous_page: Some(6), next_page: Some(8), }; - assert_eq!(page.entries[0], Some(WakeupEntry { actor_id: 9 })); + assert_eq!( + page.entries[0], + Some(crate::ActorWaitingEntry::Reference(reference)) + ); assert_eq!(page.entries[1], None); assert_eq!(page.live_entries, 1); assert_eq!((page.previous_page, page.next_page), (Some(6), Some(8))); - let bucket = WakeupBucketState { + let bucket = crate::WakeupBucketState { head_page: 6, tail_page: 8, next_page_id: 9, @@ -52,8 +58,7 @@ fn paged_wakeup_substrate_replaces_and_invalidates_exact_slots() { None, transfer_contract_steps(BOB, 1), ); - - assert!(Actors::wakeup_substrate_schedule(actor_id, 10)); + assert!(schedule_latched_service_wakeup(actor_id, 10)); let first = Actors::actor_hot(actor_id) .expect("hot state") .wakeup_pointer @@ -87,10 +92,17 @@ fn paged_wakeup_substrate_replaces_and_invalidates_exact_slots() { #[cfg(feature = "try-runtime")] assert_ok!(crate::Pallet::::do_try_state()); + let (_, mut consumed) = Actors::actor_control_cell(actor_id).expect("Waiting authority"); assert_eq!( Actors::wakeup_substrate_invalidate(actor_id), Some(replacement) ); + assert!(!crate::ActorControlLocators::::contains_key(actor_id)); + consumed.hot.pending_signal = false; + consumed.hot.wakeup_pointer = None; + consumed.eligible_at = None; + crate::ActorUnsignaledControlCells::::insert(actor_id, consumed); + crate::ActorControlLocators::::insert(actor_id, crate::ActorControlLocation::Unsignaled); assert!( Actors::actor_hot(actor_id) .expect("hot state") @@ -109,10 +121,8 @@ fn wakeup_replacement_rolls_back_when_existing_cursor_is_corrupt() { 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::wakeup_substrate_schedule(actor_id, 10)); - WakeupBuckets::::mutate(WakeupKey::Block(10), |maybe_bucket| { - maybe_bucket.as_mut().expect("bucket").cursor_index = Some(1); - }); + assert!(schedule_latched_service_wakeup(actor_id, 10)); + crate::ActorWaitingCursorIndices::::insert(WakeupKey::Block(10), 1); let before = polkadot_sdk::sp_io::storage::root(StateVersion::V1); assert_eq!( @@ -131,51 +141,41 @@ fn wakeup_pointer_corruption_matrix_fails_closed_and_is_detected() { frame_system::Pallet::::set_block_number(1); let actor_id = create_system_with(ALICE, manual_schedule(), None, inert_contract_steps()); let other = create_system_with(BOB, manual_schedule(), None, inert_contract_steps()); - assert!(Actors::wakeup_substrate_schedule(actor_id, 10)); + assert!(schedule_latched_service_wakeup(actor_id, 10)); match corruption { - 0 => ActorHot::::mutate(actor_id, |maybe| { - maybe - .as_mut() - .expect("active actor") + 0 => mutate_primary_control_cell(actor_id, |cell| { + cell + .hot .wakeup_pointer .as_mut() .expect("wakeup pointer") .block = WakeupKey::Block(11); }), - 1 => ActorHot::::mutate(actor_id, |maybe| { - maybe - .as_mut() - .expect("active actor") + 1 => mutate_primary_control_cell(actor_id, |cell| { + cell + .hot .wakeup_pointer .as_mut() .expect("wakeup pointer") .page_id = 1; }), - 2 => ActorHot::::mutate(actor_id, |maybe| { - maybe - .as_mut() - .expect("active actor") + 2 => mutate_primary_control_cell(actor_id, |cell| { + cell + .hot .wakeup_pointer .as_mut() .expect("wakeup pointer") .slot = 7; }), - 3 => WakeupPages::::mutate((WakeupKey::Block(10), 0), |maybe| { - maybe.as_mut().expect("wakeup page").entries[0] = - Some(crate::WakeupEntry { actor_id: other }); - }), - 4 => WakeupPages::::remove((WakeupKey::Block(10), 0)), - 5 => WakeupBuckets::::remove(WakeupKey::Block(10)), - 6 => WakeupBuckets::::mutate(WakeupKey::Block(10), |maybe| { - maybe.as_mut().expect("wakeup bucket").cursor_index = None; - }), - 7 => WakeupBuckets::::mutate(WakeupKey::Block(10), |maybe| { - maybe.as_mut().expect("wakeup bucket").live_entries = 0; - }), + 3 => mutate_primary_control_cell(actor_id, |cell| cell.actor_id = other), + 4 => crate::ActorWaitingFrameChunks::::remove((WakeupKey::Block(10), 0)), + 5 => crate::ActorWaitingTails::::remove(WakeupKey::Block(10)), + 6 => crate::ActorWaitingCursorIndices::::remove(WakeupKey::Block(10)), + 7 => crate::ActorWaitingOccupancies::::insert(WakeupKey::Block(10), 0), _ => unreachable!(), } let events_before = System::events(); - let identity_before = Actors::actor_identities(actor_id).expect("identity"); + let identity_before = Actors::actor_identity(actor_id); let root_before = polkadot_sdk::sp_io::storage::root(StateVersion::V1); assert_eq!( @@ -185,7 +185,7 @@ fn wakeup_pointer_corruption_matrix_fails_closed_and_is_detected() { ); assert_eq!(Actors::wakeup_substrate_invalidate(actor_id), None); assert_eq!(System::events(), events_before); - assert_eq!(Actors::actor_identities(actor_id), Some(identity_before)); + assert_eq!(Actors::actor_identity(actor_id), identity_before); assert_eq!( polkadot_sdk::sp_io::storage::root(StateVersion::V1), root_before, @@ -200,13 +200,43 @@ fn wakeup_pointer_corruption_matrix_fails_closed_and_is_detected() { } } +#[cfg(all(feature = "try-runtime", not(feature = "runtime-benchmarks")))] +#[test] +fn try_state_reads_frame_wakeup_authority_without_scalar_hot() { + 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()); + let Some(crate::ActorControlLocation::Waiting { key, page, slot }) = + crate::ActorControlLocators::::get(actor_id) + else { + panic!("Cadenced actor owns one Waiting frame cell"); + }; + crate::ActorWaitingFrameChunks::::mutate((key, page), |maybe_chunk| { + let cell = maybe_chunk + .as_mut() + .and_then(|page| page.entries.get_mut(slot as usize)) + .and_then(Option::as_mut) + .and_then(crate::ActorWaitingEntry::primary_mut) + .expect("Waiting frame cell exists"); + cell + .hot + .trigger_wakeup_pointer + .as_mut() + .expect("Cadenced pointer exists") + .slot = u32::from(slot).saturating_add(1); + }); + + assert!(crate::Pallet::::do_try_state().is_err()); + }); +} + #[test] fn wakeup_replacement_rolls_back_when_existing_page_or_slot_is_missing() { 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::wakeup_substrate_schedule(actor_id, 10)); - WakeupPages::::remove((WakeupKey::Block(10), 0)); + assert!(schedule_latched_service_wakeup(actor_id, 10)); + crate::ActorWaitingFrameChunks::::remove((WakeupKey::Block(10), 0)); let before = polkadot_sdk::sp_io::storage::root(StateVersion::V1); assert_eq!( Actors::try_wakeup_substrate_schedule_inner(actor_id, 20), @@ -218,15 +248,9 @@ fn wakeup_replacement_rolls_back_when_existing_page_or_slot_is_missing() { 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::wakeup_substrate_schedule(actor_id, 10)); - ActorHot::::mutate(actor_id, |maybe_hot| { - maybe_hot - .as_mut() - .expect("hot") - .wakeup_pointer - .as_mut() - .expect("pointer") - .slot = 7; + assert!(schedule_latched_service_wakeup(actor_id, 10)); + mutate_primary_control_cell(actor_id, |cell| { + cell.hot.wakeup_pointer.as_mut().expect("pointer").slot = 7; }); let before = polkadot_sdk::sp_io::storage::root(StateVersion::V1); assert_eq!( @@ -242,10 +266,8 @@ fn wakeup_replacement_rolls_back_on_live_count_underflow() { 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::wakeup_substrate_schedule(actor_id, 10)); - WakeupBuckets::::mutate(WakeupKey::Block(10), |maybe_bucket| { - maybe_bucket.as_mut().expect("bucket").live_entries = 0; - }); + assert!(schedule_latched_service_wakeup(actor_id, 10)); + crate::ActorWaitingOccupancies::::insert(WakeupKey::Block(10), 0); let before = polkadot_sdk::sp_io::storage::root(StateVersion::V1); assert_eq!( @@ -267,7 +289,7 @@ fn wakeup_cursor_capacity_overflow_fails_closed_and_preserves_existing_path() { None, transfer_contract_steps(BOB, 1), ); - assert!(Actors::wakeup_substrate_schedule(actor_id, 10)); + assert!(schedule_latched_service_wakeup(actor_id, 10)); let pointer = Actors::actor_hot(actor_id) .expect("hot state") .wakeup_pointer @@ -322,14 +344,14 @@ fn wakeup_page_index_overflow_fails_closed_as_namespace_exhaustion() { None, transfer_contract_steps(BOB, 1), ); - // Seed a bucket whose next_page_id is at the u64 ceiling; appending a new page - // cannot advance the monotonic page index and must fail closed as + // Seed a Waiting bucket whose tail position is at the u64 ceiling; appending + // cannot advance the monotonic position and must fail closed as // WakeupIndexExhausted (mapped to the SchedulerIndexExhausted public error). let block = 10u64; - assert!(Actors::wakeup_substrate_schedule(actor_id, block)); - // Fill the tail page to force the page-append branch, then set the next page id - // at the u64 ceiling so the monotonic index cannot advance. - let page_size = <::WakeupPageSize as Get>::get(); + assert!(schedule_latched_service_wakeup(actor_id, block)); + // Fill the tail page, then set the next Waiting position at the u64 ceiling + // so the monotonic address cannot advance. + let page_size = 32u32; for _ in 0..page_size.saturating_sub(1) { let extra = create_system_with( BOB, @@ -337,17 +359,14 @@ fn wakeup_page_index_overflow_fails_closed_as_namespace_exhaustion() { None, transfer_contract_steps(CHARLIE, 1), ); - assert!(Actors::wakeup_substrate_schedule(extra, block)); + assert!(schedule_latched_service_wakeup(extra, block)); } - crate::WakeupBuckets::::mutate(WakeupKey::Block(block), |bucket| { - let bucket = bucket.as_mut().expect("bucket exists"); - bucket.next_page_id = u64::MAX; - }); + crate::ActorWaitingTails::::insert(WakeupKey::Block(block), u64::MAX); let pointer_before = Actors::actor_hot(actor_id) .expect("hot") .wakeup_pointer .expect("existing pointer"); - // A different actor scheduling into the full bucket with the page index at the + // A different actor scheduling into the full bucket with the tail position at the // u64 ceiling cannot advance the monotonic index and fails closed. let new_actor = create_system_with( BOB, @@ -356,7 +375,7 @@ fn wakeup_page_index_overflow_fails_closed_as_namespace_exhaustion() { transfer_contract_steps(CHARLIE, 1), ); assert!(matches!( - crate::Pallet::::try_wakeup_substrate_schedule_inner(new_actor, block), + try_schedule_latched_service_wakeup(new_actor, block), Err(crate::EnqueueOutcome::WakeupIndexExhausted) )); // The existing pointer is never cleared before the replacement path fits. @@ -378,21 +397,12 @@ fn wakeup_bucket_corruption_fails_closed_as_corrupted_topology() { transfer_contract_steps(BOB, 1), ); let block = 10u64; - assert!(Actors::wakeup_substrate_schedule(actor_id, block)); + assert!(schedule_latched_service_wakeup(actor_id, block)); // Corrupt the TARGET bucket for a different actor: a bucket exists at block + 1 but // its cursor_index is missing, so the schedule path must fail closed as corrupted // topology instead of retrying as queue-full. let target = block + 1; - crate::WakeupBuckets::::insert( - WakeupKey::Block(target), - crate::WakeupBucketState { - head_page: 0, - tail_page: 0, - next_page_id: 1, - live_entries: 1, - cursor_index: None, - }, - ); + crate::ActorWaitingOccupancies::::insert(WakeupKey::Block(target), 1); let pointer_before = Actors::actor_hot(actor_id) .expect("hot") .wakeup_pointer @@ -404,7 +414,7 @@ fn wakeup_bucket_corruption_fails_closed_as_corrupted_topology() { transfer_contract_steps(CHARLIE, 1), ); assert!(matches!( - crate::Pallet::::try_wakeup_substrate_schedule_inner(new_actor, target), + try_schedule_latched_service_wakeup(new_actor, target), Err(crate::EnqueueOutcome::CorruptedTopology) )); assert_eq!( @@ -429,7 +439,10 @@ fn saturated_enqueue_falls_back_to_exact_next_block_wakeup_not_silent_loss() { // Placement must not silently lose readiness: the fallback is an exact // next-block wakeup, so the actor owns a wakeup path even though the FIFO // rejected the ticket (spec 8.1.4). - assert_ok!(Actors::enqueue(actor_id)); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); let hot = Actors::actor_hot(actor_id).expect("hot"); assert!( hot.queue_ticket.is_none(), @@ -457,22 +470,14 @@ fn saturated_enqueue_fails_closed_when_wakeup_fallback_also_fails() { // Saturate the FIFO so enqueue falls back to the next-block wakeup, then // corrupt the target wakeup bucket so that fallback placement also fails. seed_saturated_tombstone_queue(); - crate::WakeupBuckets::::insert( - WakeupKey::Block(2), - crate::WakeupBucketState { - head_page: 0, - tail_page: 0, - next_page_id: 1, - live_entries: 0, - cursor_index: None, - }, - ); + crate::ActorWaitingOccupancies::::insert(WakeupKey::Block(2), 1); // The placement must fail closed: the caller learns the actor owns no path // instead of believing enqueue succeeded while readiness was lost. - assert!(matches!( - Actors::enqueue(actor_id), - Err(crate::EnqueueOutcome::CorruptedTopology) - )); + // The public placement boundary maps CorruptedTopology to SchedulerIndexExhausted. + assert_noop!( + Actors::manual_trigger(RuntimeOrigin::signed(ALICE), actor_id), + Error::::SchedulerIndexExhausted + ); let hot = Actors::actor_hot(actor_id).expect("hot"); assert!(hot.queue_ticket.is_none()); assert!(hot.wakeup_pointer.is_none(), "no phantom wakeup on failure"); @@ -493,16 +498,7 @@ fn manual_trigger_fails_closed_when_wakeup_fallback_cannot_preserve_readiness() // Saturate the FIFO so any ticket placement falls back to the next-block // wakeup, then corrupt that target bucket so the fallback also fails. seed_saturated_tombstone_queue(); - crate::WakeupBuckets::::insert( - WakeupKey::Block(2), - crate::WakeupBucketState { - head_page: 0, - tail_page: 0, - next_page_id: 1, - live_entries: 0, - cursor_index: None, - }, - ); + crate::ActorWaitingOccupancies::::insert(WakeupKey::Block(2), 1); // The extrinsic must fail closed (namespace/corruption is not retryable // queue-full), leaving the actor with its pre-trigger state: no ticket and // no wakeup, and the pending-signal mutation rolled back. @@ -547,14 +543,12 @@ fn paged_wakeup_substrate_invalidation_rolls_back_on_cursor_mismatch() { None, transfer_contract_steps(BOB, 1), ); - assert!(Actors::wakeup_substrate_schedule(actor_id, 10)); + assert!(schedule_latched_service_wakeup(actor_id, 10)); let pointer = Actors::actor_hot(actor_id) .expect("hot state") .wakeup_pointer .expect("wakeup pointer"); - crate::pallet::WakeupBuckets::::mutate(WakeupKey::Block(10), |maybe_bucket| { - maybe_bucket.as_mut().expect("wakeup bucket").cursor_index = None; - }); + crate::ActorWaitingCursorIndices::::remove(WakeupKey::Block(10)); assert_eq!(Actors::wakeup_substrate_invalidate(actor_id), None); assert_eq!( @@ -579,7 +573,7 @@ fn paged_wakeup_substrate_invalidation_rolls_back_on_cursor_mismatch() { fn paged_wakeup_substrate_links_and_unlinks_middle_pages() { new_test_ext().execute_with(|| { frame_system::Pallet::::set_block_number(1); - let page_size: u32 = ::WakeupPageSize::get(); + let page_size = 32u32; let count = page_size.saturating_mul(2).saturating_add(1); let mut actors = Vec::new(); for _ in 0..count { @@ -589,7 +583,7 @@ fn paged_wakeup_substrate_links_and_unlinks_middle_pages() { None, transfer_contract_steps(BOB, 1), ); - assert!(Actors::wakeup_substrate_schedule(actor_id, 10)); + assert!(schedule_latched_service_wakeup(actor_id, 10)); actors.push(actor_id); } @@ -617,8 +611,21 @@ fn paged_wakeup_substrate_links_and_unlinks_middle_pages() { ); let page_size = page_size as usize; + let invalidate_service = |actor_id| { + let (_, mut consumed) = Actors::actor_control_cell(actor_id).expect("Waiting authority"); + assert!(Actors::wakeup_substrate_invalidate(actor_id).is_some()); + assert!(!crate::ActorControlLocators::::contains_key(actor_id)); + consumed.hot.pending_signal = false; + consumed.hot.wakeup_pointer = None; + consumed.eligible_at = None; + crate::ActorUnsignaledControlCells::::insert(actor_id, consumed); + crate::ActorControlLocators::::insert( + actor_id, + crate::ActorControlLocation::Unsignaled, + ); + }; for actor_id in &actors[page_size..page_size * 2] { - assert!(Actors::wakeup_substrate_invalidate(*actor_id).is_some()); + invalidate_service(*actor_id); } let bucket = Actors::wakeup_buckets(10).expect("bucket after middle unlink"); assert_eq!(bucket.live_entries, count.saturating_sub(page_size as u32)); @@ -636,8 +643,10 @@ fn paged_wakeup_substrate_links_and_unlinks_middle_pages() { #[cfg(feature = "try-runtime")] assert_ok!(crate::Pallet::::do_try_state()); - for actor_id in actors { - let _ = Actors::wakeup_substrate_invalidate(actor_id); + for (index, actor_id) in actors.into_iter().enumerate() { + if !(page_size..page_size * 2).contains(&index) { + invalidate_service(actor_id); + } } assert!(Actors::wakeup_buckets(10).is_none()); assert!(Actors::wakeup_pages((10, 0)).is_none()); @@ -649,7 +658,7 @@ fn paged_wakeup_substrate_links_and_unlinks_middle_pages() { fn paged_wakeup_drain_preserves_partial_progress_and_crosses_page_boundaries() { new_test_ext().execute_with(|| { frame_system::Pallet::::set_block_number(1); - let page_size: u32 = ::WakeupPageSize::get(); + let page_size = 32u32; let count = page_size.saturating_mul(2).saturating_add(1); let mut actors = Vec::new(); for _ in 0..count { @@ -659,13 +668,17 @@ fn paged_wakeup_drain_preserves_partial_progress_and_crosses_page_boundaries() { None, transfer_contract_steps(BOB, 1), ); - assert!(Actors::wakeup_substrate_schedule(actor_id, 10)); + assert!(schedule_latched_service_wakeup(actor_id, 10)); actors.push(actor_id); } let first_limit = page_size / 2; - let (first, first_stats) = Actors::wakeup_substrate_drain_block(10, first_limit); - assert_eq!(first.as_slice(), &actors[..first_limit as usize]); + let (first, first_stats) = + Actors::wakeup_substrate_drain_key(WakeupKey::Block(10), first_limit); + assert_eq!( + first.iter().map(|entry| entry.0).collect::>(), + actors[..first_limit as usize] + ); assert_eq!(first_stats.entries_scanned, first_limit); assert_eq!(first_stats.ready_entries, first_limit); assert_eq!(first_stats.pages_touched, 1); @@ -674,9 +687,13 @@ fn paged_wakeup_drain_preserves_partial_progress_and_crosses_page_boundaries() { assert_eq!(head.scan_slot, first_limit); assert_eq!(head.live_entries, page_size - first_limit); - let (second, second_stats) = Actors::wakeup_substrate_drain_block(10, page_size); + let (second, second_stats) = + Actors::wakeup_substrate_drain_key(WakeupKey::Block(10), page_size); let second_end = first_limit.saturating_add(page_size) as usize; - assert_eq!(second.as_slice(), &actors[first_limit as usize..second_end]); + assert_eq!( + second.iter().map(|entry| entry.0).collect::>(), + actors[first_limit as usize..second_end] + ); assert_eq!(second_stats.entries_scanned, page_size); assert_eq!(second_stats.ready_entries, page_size); assert_eq!(second_stats.pages_touched, 2); @@ -687,11 +704,30 @@ fn paged_wakeup_drain_preserves_partial_progress_and_crosses_page_boundaries() { assert_eq!(head.previous_page, None); assert_eq!(head.scan_slot, first_limit); - let (final_ready, final_stats) = Actors::wakeup_substrate_drain_block(10, u32::MAX); - assert_eq!(final_ready.as_slice(), &actors[second_end..]); + let (final_ready, final_stats) = + Actors::wakeup_substrate_drain_key(WakeupKey::Block(10), u32::MAX); + assert_eq!( + final_ready.iter().map(|entry| entry.0).collect::>(), + actors[second_end..] + ); assert_eq!(final_stats.ready_entries, count - first_limit - page_size); assert_eq!(final_stats.pages_touched, 2); assert_eq!(final_stats.pages_deleted, 2); + for (actor_id, state, admission, loaded_step) in + first.into_iter().chain(second).chain(final_ready) + { + let loaded_step = loaded_step.expect("nonempty service Step"); + let cell = crate::ActorControlCellOf:: { + actor_id, + identity: Actors::control_identity_from_scalar(state.identity).expect("canonical identity"), + hot: Actors::control_hot_from_scalar(state.hot), + admission, + cursor: loaded_step.cursor, + resources: loaded_step.resources, + eligible_at: Some(10), + }; + assert_ok!(Actors::control_append_ready(cell)); + } assert!(Actors::wakeup_buckets(10).is_none()); assert!(Actors::wakeup_pages((10, 1)).is_none()); assert!(Actors::wakeup_pages((10, 2)).is_none()); @@ -709,10 +745,11 @@ fn paged_wakeup_drain_preserves_partial_progress_and_crosses_page_boundaries() { } #[test] -fn paged_wakeup_drain_discards_stale_only_bucket() { +fn paged_wakeup_drain_discards_explicit_orphan_references() { new_test_ext().execute_with(|| { frame_system::Pallet::::set_block_number(1); let mut actors = Vec::new(); + let mut stale_cells = Vec::new(); for _ in 0..3 { let actor_id = create_system_with( ALICE, @@ -720,10 +757,25 @@ fn paged_wakeup_drain_discards_stale_only_bucket() { None, transfer_contract_steps(BOB, 1), ); - assert!(Actors::wakeup_substrate_schedule(actor_id, 10)); - ActorHot::::mutate(actor_id, |maybe_hot| { - maybe_hot.as_mut().expect("hot state").wakeup_pointer = None; + assert!(schedule_latched_service_wakeup(actor_id, 10)); + let (location, mut cell) = Actors::actor_control_cell(actor_id).expect("scheduled primary"); + // Deliberate orphan corruption: real lifecycle removal deletes the entry as well. + let crate::ActorControlLocation::Waiting { key, page, slot } = location else { + panic!("scheduled primary occupies Waiting"); + }; + crate::ActorWaitingFrameChunks::::mutate((key, page), |stored| { + stored.as_mut().expect("Waiting page").entries[slot as usize] = Some( + crate::ActorWaitingEntry::Reference(crate::ActorWakeupReference { + actor_id, + admission_identity: cell.admission.admission_identity, + }), + ); }); + crate::ActorControlLocators::::remove(actor_id); + cell.hot.pending_signal = false; + cell.hot.wakeup_pointer = None; + cell.eligible_at = None; + stale_cells.push((actor_id, cell)); actors.push(actor_id); } @@ -736,12 +788,18 @@ fn paged_wakeup_drain_discards_stale_only_bucket() { assert_eq!(stats.pages_deleted, 1); assert!(Actors::wakeup_buckets(10).is_none()); assert!(Actors::wakeup_pages((10, 0)).is_none()); - assert!(actors.iter().all(|actor_id| { - Actors::actor_hot(*actor_id) - .expect("hot state") - .wakeup_pointer - .is_none() - })); + assert!( + actors + .iter() + .all(|actor_id| Actors::actor_hot(*actor_id).is_none()) + ); + for (actor_id, cell) in stale_cells { + crate::ActorUnsignaledControlCells::::insert(actor_id, cell); + crate::ActorControlLocators::::insert( + actor_id, + crate::ActorControlLocation::Unsignaled, + ); + } #[cfg(feature = "try-runtime")] assert_ok!(crate::Pallet::::do_try_state()); }); @@ -763,8 +821,8 @@ fn cursor_wakeup_drain_recovers_sparse_overdue_blocks_without_scanning_gaps() { None, transfer_contract_steps(BOB, 1), ); - assert!(Actors::wakeup_substrate_schedule(due, 10)); - assert!(Actors::wakeup_substrate_schedule(future, 1_000_000)); + assert!(schedule_latched_service_wakeup(due, 10)); + assert!(schedule_latched_service_wakeup(future, 1_000_000)); let mut halted = WeightMeter::with_limit(Weight::zero()); assert_eq!( @@ -811,12 +869,14 @@ fn cursor_wakeup_drain_halts_and_resumes_between_slot_units() { None, transfer_contract_steps(BOB, 1), ); - assert!(Actors::wakeup_substrate_schedule(actor_id, 10)); + assert!(schedule_latched_service_wakeup(actor_id, 10)); actors.push(actor_id); } let limit = <::WeightInfo as crate::WeightInfo>::scheduler_wakeup_cursor_worker_future() - .saturating_add(Actors::block_wakeup_cursor_drain_unit_weight_upper(false)); + .saturating_add(Actors::block_wakeup_cursor_drain_unit_weight_upper( + crate::scheduler::WakeupBucketDisposition::Retain, + )); 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); @@ -854,10 +914,12 @@ fn cursor_wakeup_drain_stops_independently_on_reftime_and_proof_size() { None, transfer_contract_steps(BOB, 1), ); - assert!(Actors::wakeup_substrate_schedule(actor_id, 10)); + assert!(schedule_latched_service_wakeup(actor_id, 10)); let required = <::WeightInfo as crate::WeightInfo>::scheduler_wakeup_cursor_worker_future() - .saturating_add(Actors::block_wakeup_cursor_drain_unit_weight_upper(true)); + .saturating_add(Actors::block_wakeup_cursor_drain_unit_weight_upper( + crate::scheduler::WakeupBucketDisposition::Remove, + )); let mut reftime_short = WeightMeter::with_limit(Weight::from_parts( required.ref_time().saturating_sub(1), @@ -891,7 +953,9 @@ 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::block_wakeup_cursor_drain_unit_weight_upper(true)); + .saturating_add(Actors::block_wakeup_cursor_drain_unit_weight_upper( + crate::scheduler::WakeupBucketDisposition::Remove, + )); assert_on_idle_wakeup_insufficiency_preserves_state(Weight::from_parts( required.ref_time().saturating_sub(1), u64::MAX, @@ -938,8 +1002,9 @@ fn wakeup_materialization_closes_when_queue_ticket_is_exhausted() { None, transfer_contract_steps(BOB, 1), ); - assert!(Actors::wakeup_substrate_schedule(actor_id, 10)); - crate::NextQueueTicket::::put(u64::MAX); + assert!(schedule_latched_service_wakeup(actor_id, 10)); + crate::ActorReadyHead::::put(u64::MAX); + crate::ActorReadyTail::::put(u64::MAX); System::reset_events(); let mut meter = WeightMeter::with_limit(Weight::MAX); @@ -965,9 +1030,9 @@ fn wakeup_materialization_faults_and_requires_repair_on_fifo_topology_corruption 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::wakeup_substrate_schedule(actor_id, 10)); - QueueTail::::put(1); - QueueOccupancy::::put(0); + assert!(schedule_latched_service_wakeup(actor_id, 10)); + ActorReadyTail::::put(1); + ActorReadyOccupancy::::put(0); 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); @@ -1019,7 +1084,7 @@ fn wakeup_materialization_faults_and_requires_repair_on_fifo_topology_corruption Actors::clear_wakeup_worker_fault(RuntimeOrigin::signed(ALICE)), DispatchError::BadOrigin ); - QueueTail::::put(0); + ActorReadyTail::::put(0); assert_ok!(Actors::clear_wakeup_worker_fault(RuntimeOrigin::root())); let actor_events: Vec<_> = System::events() .into_iter() @@ -1096,6 +1161,86 @@ fn wakeup_fault_recording_admits_both_weight_dimensions_and_is_idempotent() { }); } +#[test] +fn waiting_removal_repairs_upward_and_reclaims_heap_tail_page() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + let size: u32 = ::WakeupPageSize::get(); + assert_eq!( + size, 32, + "fixture straddles the reference heap-page boundary" + ); + let count = size * 2 + 1; + let mut actors = Vec::new(); + let mut blocks = Vec::new(); + for index in 0..count { + let mut branch = index; + while branch > 2 { + branch = (branch - 1) / 2; + } + let block = if index == 0 { + 10_000 + } else if branch == 1 { + 100_000 + } else { + 1_000_000 + } + u64::from(index); + let actor_id = create_system_with( + ALICE, + manual_schedule(), + None, + transfer_contract_steps(BOB, 1), + ); + assert!(schedule_latched_service_wakeup(actor_id, block)); + assert_eq!( + crate::ActorWaitingCursorIndices::::get(WakeupKey::Block(block)), + Some(index) + ); + actors.push(actor_id); + blocks.push(block); + } + #[cfg(feature = "try-runtime")] + assert_ok!(Actors::do_try_state()); + + // The final key belongs to the low left subtree. Replacing a deep right-subtree key + // forces four upward swaps across a page boundary, then deletes the one-entry tail page. + assert_ok!(Actors::deactivate_actor(RuntimeOrigin::root(), actors[62])); + assert!(!Actors::active_actor_exists(actors[62])); + assert_eq!(Actors::wakeup_cursor_len(), 64); + assert!(Actors::wakeup_cursor_pages(2).is_none()); + assert!(!crate::ActorWaitingCursorIndices::::contains_key( + WakeupKey::Block(blocks[62]) + )); + assert_eq!( + crate::ActorWaitingCursorIndices::::get(WakeupKey::Block(blocks[64])), + Some(2) + ); + for (from, to) in [(2, 6), (6, 14), (14, 30), (30, 62)] { + assert_eq!( + crate::ActorWaitingCursorIndices::::get(WakeupKey::Block(blocks[from])), + Some(to) + ); + } + #[cfg(feature = "try-runtime")] + assert_ok!(Actors::do_try_state()); + + // Removing the current last key needs no repair; it truncates the retained page. + assert_ok!(Actors::deactivate_actor(RuntimeOrigin::root(), actors[63])); + assert!(!Actors::active_actor_exists(actors[63])); + assert_eq!(Actors::wakeup_cursor_len(), 63); + assert_eq!( + Actors::wakeup_cursor_pages(1).map(|page| page.len()), + Some(31) + ); + assert!(!crate::ActorWaitingCursorIndices::::contains_key( + WakeupKey::Block(blocks[63]) + )); + assert_eq!(Actors::wakeup_cursor_peek(), Some(blocks[0])); + #[cfg(feature = "try-runtime")] + assert_ok!(Actors::do_try_state()); + }); +} + #[test] fn paged_wakeup_cursor_orders_sparse_blocks_across_page_boundaries() { new_test_ext().execute_with(|| { @@ -1114,7 +1259,7 @@ fn paged_wakeup_cursor_orders_sparse_blocks_across_page_boundaries() { None, transfer_contract_steps(BOB, 1), ); - assert!(Actors::wakeup_substrate_schedule(actor_id, block)); + assert!(schedule_latched_service_wakeup(actor_id, block)); assert!(Actors::wakeup_cursor_insert(block)); actors.push(actor_id); } @@ -1132,14 +1277,26 @@ fn paged_wakeup_cursor_orders_sparse_blocks_across_page_boundaries() { assert!(Actors::wakeup_cursor_insert(blocks[0])); assert_eq!(Actors::wakeup_cursor_len(), count); + let mut consumed = Vec::new(); + for actor_id in actors { + let (location, cell) = Actors::actor_control_cell(actor_id).expect("live Waiting owner"); + let crate::ActorControlLocation::Waiting { key, page, .. } = location else { + panic!("service primary is Waiting"); + }; + // Model the atomic boundary after each sole entry is consumed, before heap repair. + crate::ActorWaitingFrameChunks::::remove((key, page)); + crate::ActorWaitingHeads::::remove(key); + crate::ActorWaitingTails::::remove(key); + crate::ActorWaitingOccupancies::::remove(key); + crate::ActorControlLocators::::remove(actor_id); + consumed.push(cell); + } let removed = blocks[(count / 2) as usize]; assert!(Actors::wakeup_cursor_remove(removed)); assert!(!Actors::wakeup_cursor_remove(removed)); assert_eq!(Actors::wakeup_cursor_len(), count.saturating_sub(1)); assert_eq!( - Actors::wakeup_buckets(removed) - .expect("removed cursor bucket") - .cursor_index, + crate::ActorWaitingCursorIndices::::get(WakeupKey::Block(removed)), None ); let expected: Vec<_> = blocks @@ -1156,14 +1313,18 @@ fn paged_wakeup_cursor_orders_sparse_blocks_across_page_boundaries() { assert!(Actors::wakeup_cursor_pages(0).is_none()); assert!(Actors::wakeup_cursor_pages(1).is_none()); assert!(blocks.iter().all(|block| { - Actors::wakeup_buckets(*block) - .expect("wakeup bucket") - .cursor_index - .is_none() + !crate::ActorWaitingCursorIndices::::contains_key(WakeupKey::Block(*block)) })); - for actor_id in actors { - let _ = Actors::wakeup_substrate_invalidate(actor_id); + for mut cell in consumed { + cell.hot.pending_signal = false; + cell.hot.wakeup_pointer = None; + cell.eligible_at = None; + crate::ActorControlLocators::::insert( + cell.actor_id, + crate::ActorControlLocation::Unsignaled, + ); + crate::ActorUnsignaledControlCells::::insert(cell.actor_id, cell); } #[cfg(feature = "try-runtime")] assert_ok!(crate::Pallet::::do_try_state()); @@ -1174,7 +1335,8 @@ fn paged_wakeup_cursor_orders_sparse_blocks_across_page_boundaries() { fn creation_and_activation_before_cutoff_use_exact_next_block_wakeup() { new_test_ext().execute_with(|| { frame_system::Pallet::::set_block_number(1); - crate::NextQueueTicket::::put(u64::MAX); + crate::ActorReadyHead::::put(u64::MAX); + crate::ActorReadyTail::::put(u64::MAX); prefund_active_user_creation(ALICE, &transfer_contract_steps(BOB, 1)); assert_ok!(Actors::create_user_actor( RuntimeOrigin::signed(ALICE), @@ -1199,7 +1361,8 @@ fn creation_and_activation_before_cutoff_use_exact_next_block_wakeup() { None, )); let actor_id = Actors::next_actor_id() - 1; - crate::NextQueueTicket::::put(u64::MAX); + crate::ActorReadyHead::::put(u64::MAX); + crate::ActorReadyTail::::put(u64::MAX); prefund_user_sovereign(ALICE, 0, &transfer_contract_steps(BOB, 1)); frame_system::Pallet::::set_block_number(2); assert_ok!(Actors::activate_actor( @@ -1224,18 +1387,21 @@ fn wakeup_materialization_index_exhaustion_closes_without_an_attempt() { ); fund_native(actor_id, 100); frame_system::Pallet::::set_block_number(2); - crate::NextQueueTicket::::put(u64::MAX); + crate::ActorReadyHead::::put(u64::MAX); + crate::ActorReadyTail::::put(u64::MAX); let bob_before = native_balance(&BOB); System::reset_events(); run_idle(Weight::MAX); assert_eq!(native_balance(&BOB), bob_before); - assert!(Actors::actor_identities(actor_id).is_none()); + assert!(Actors::actor_identity(actor_id).is_none()); assert!(Actors::actor_hot(actor_id).is_none()); assert!(Actors::actor_funding(actor_id).is_none()); assert_eq!(Actors::combined_queue_occupancy(), 0); - assert!(!WakeupBuckets::::contains_key(WakeupKey::Block(2))); + assert!(!crate::ActorWaitingOccupancies::::contains_key( + WakeupKey::Block(2) + )); assert!(has_actor_event(|event| matches!( event, Event::ActorClosed { @@ -1257,16 +1423,11 @@ fn trigger_rearm_index_exhaustion_closes_before_the_due_attempt() { transfer_contract_steps(BOB, 10), ); fund_native(actor_id, 100); - let page_size = ::WakeupPageSize::get(); + let page_size = 32u32; for _ in 0..page_size { create_system_with(ALICE, timer_schedule(2), None, inert_contract_steps()); } - WakeupBuckets::::mutate(WakeupKey::Tick(3), |maybe| { - maybe - .as_mut() - .expect("saturated future bucket") - .next_page_id = u64::MAX; - }); + crate::ActorWaitingTails::::insert(WakeupKey::Tick(3), u64::MAX); frame_system::Pallet::::set_block_number(2); let bob_before = native_balance(&BOB); System::reset_events(); @@ -1274,7 +1435,7 @@ fn trigger_rearm_index_exhaustion_closes_before_the_due_attempt() { run_idle(Weight::MAX); assert_eq!(native_balance(&BOB), bob_before); - assert!(Actors::actor_identities(actor_id).is_none()); + assert!(Actors::actor_identity(actor_id).is_none()); assert!(Actors::actor_hot(actor_id).is_none()); assert_eq!(Actors::combined_queue_occupancy(), 0); assert!(has_actor_event(|event| matches!( @@ -1334,7 +1495,7 @@ fn wakeup_drain_respects_max_wakeups_per_block() { ids.push(id); } for actor_id in ids { - assert!(Actors::wakeup_substrate_schedule(actor_id, 1)); + assert!(schedule_latched_service_wakeup(actor_id, 1)); } run_idle(Weight::MAX); let remaining = Actors::wakeup_buckets(1) @@ -1349,11 +1510,13 @@ fn wakeup_worker_stops_at_its_own_weight_envelope_without_lending() { 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::wakeup_substrate_schedule(actor_id, 1)); + assert!(schedule_latched_service_wakeup(actor_id, 1)); // 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::block_wakeup_cursor_drain_unit_weight_upper(true); + let worker_unit = Actors::block_wakeup_cursor_drain_unit_weight_upper( + crate::scheduler::WakeupBucketDisposition::Remove, + ); 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)), @@ -1380,7 +1543,7 @@ fn wakeup_drain_preserves_bucket_when_proof_budget_cannot_admit_it() { 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::wakeup_substrate_schedule(actor_id, 1)); + assert!(schedule_latched_service_wakeup(actor_id, 1)); run_idle(Weight::from_parts(u64::MAX, 300)); assert_eq!( Actors::wakeup_buckets(1) @@ -1398,7 +1561,7 @@ fn wakeup_drain_stops_at_the_sparse_future_minimum() { new_test_ext().execute_with(|| { frame_system::Pallet::::set_block_number(10_000); let actor_id = create_system_with(ALICE, manual_schedule(), None, inert_contract_steps()); - assert!(Actors::wakeup_substrate_schedule(actor_id, 1_000_000)); + assert!(schedule_latched_service_wakeup(actor_id, 1_000_000)); run_idle(Weight::MAX); assert_eq!(Actors::wakeup_cursor_peek(), Some(1_000_000)); assert_eq!(scheduled_wakeup_block(actor_id), Some(1_000_000)); @@ -1508,7 +1671,9 @@ fn cadence_update_replaces_live_future_wakeup_instead_of_accumulating() { )); let rescheduled_block = scheduled_wakeup_block(actor_id).expect("replacement wakeup"); assert_ne!(rescheduled_block, initial_block); - assert!(WakeupBuckets::::get(WakeupKey::Tick(initial_block)).is_none()); + assert!(!crate::ActorWaitingOccupancies::::contains_key( + WakeupKey::Tick(initial_block) + )); assert_eq!(crate::WakeupCursorLen::::get(WakeupClock::Tick), 1); }); } @@ -1519,9 +1684,7 @@ fn cadence_update_rolls_back_exactly_when_existing_wakeup_cursor_is_corrupt() { frame_system::Pallet::::set_block_number(1); let actor_id = create_system_with(ALICE, timer_schedule(20), None, inert_contract_steps()); let initial_block = scheduled_wakeup_block(actor_id).expect("initial wakeup"); - WakeupBuckets::::mutate(WakeupKey::Tick(initial_block), |maybe_bucket| { - maybe_bucket.as_mut().expect("bucket").cursor_index = None; - }); + crate::ActorWaitingCursorIndices::::remove(WakeupKey::Tick(initial_block)); frame_system::Pallet::::set_block_number(2); let events_before = System::events(); let root_before = polkadot_sdk::sp_io::storage::root(StateVersion::V1); @@ -1607,19 +1770,17 @@ fn temporal_membership_try_state_rejects_wakeup_pointer_beyond_terminal() { assert_ok!(crate::Pallet::::do_try_state()); // Drift the window and terminal membership together to a shorter terminal, leaving the // existing wakeup beyond it: the earlier-due service-point contract must fail try_state. - ActorHot::::mutate(actor_id, |maybe| { - maybe.as_mut().expect("hot").terminal_at = Some(50); - }); 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)); + mutate_primary_control_cell(actor_id, |cell| cell.hot.terminal_at = Some(50)); assert_eq!( crate::Pallet::::do_try_state().map_err(|error| format!("{error:?}")), - Err("Other(\"ActorHot Pipeline wakeup pointer exceeds its terminal membership\")".into()) + Err( + "Other(\"ActorControl Pipeline wakeup pointer exceeds its terminal membership\")".into() + ) ); - ActorHot::::mutate(actor_id, |maybe| { - maybe.as_mut().expect("hot").terminal_at = Some(102); - }); + mutate_primary_control_cell(actor_id, |cell| cell.hot.terminal_at = Some(102)); 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)); @@ -1629,26 +1790,27 @@ fn temporal_membership_try_state_rejects_wakeup_pointer_beyond_terminal() { } #[test] -fn temporal_membership_try_state_accepts_lazy_wakeup_tombstones() { +fn temporal_membership_try_state_accepts_exact_close_cleanup() { new_test_ext().execute_with(|| { frame_system::Pallet::::set_block_number(1); let actor_id = create_system_with(ALICE, timer_schedule(20), None, inert_contract_steps()); let scheduled_block = scheduled_wakeup_block(actor_id).expect("timer wakeup scheduled"); assert_ok!(Actors::close_actor(RuntimeOrigin::signed(ALICE), actor_id)); - // Lazy terminal cleanup leaves a stale physical wakeup entry behind; the entry carries no - // membership authority and must not fail try_state (spec 5.1 stale-entry semantics). - assert!(WakeupBuckets::::contains_key(WakeupKey::Tick( - scheduled_block - ))); - assert!(crate::WakeupCursorLen::::get(WakeupClock::Tick) > 0); + assert!(!crate::ActorWaitingOccupancies::::contains_key( + WakeupKey::Tick(scheduled_block) + )); + assert_eq!(crate::WakeupCursorLen::::get(WakeupClock::Tick), 0); + assert_eq!( + crate::ActorWaitingFrameChunks::::iter_keys().count(), + 0 + ); #[cfg(feature = "try-runtime")] assert_ok!(crate::Pallet::::do_try_state()); - // The bounded drain converges the tombstone at its due tick. frame_system::Pallet::::set_block_number(scheduled_block); run_idle(Weight::MAX); - assert!(!WakeupBuckets::::contains_key(WakeupKey::Tick( - scheduled_block - ))); + assert!(!crate::ActorWaitingOccupancies::::contains_key( + WakeupKey::Tick(scheduled_block) + )); #[cfg(feature = "try-runtime")] assert_ok!(crate::Pallet::::do_try_state()); }); @@ -1687,7 +1849,7 @@ fn paged_wakeup_recovery_is_independent_of_sparse_actor_ids() { } #[test] -fn close_before_future_wakeup_leaves_harmless_lazy_tombstone() { +fn close_before_future_wakeup_removes_exact_waiting_membership() { new_test_ext().execute_with(|| { frame_system::Pallet::::set_block_number(1); let actor_id = create_system_with(ALICE, timer_schedule(20), None, inert_contract_steps()); @@ -1696,10 +1858,11 @@ fn close_before_future_wakeup_leaves_harmless_lazy_tombstone() { assert_ok!(Actors::close_actor(RuntimeOrigin::signed(ALICE), actor_id)); assert!(Actors::active_actor_view(actor_id).is_none()); assert!(scheduled_wakeup_block(actor_id).is_none()); - assert!(WakeupBuckets::::contains_key(WakeupKey::Tick( - scheduled_block - ))); - assert_eq!(crate::WakeupCursorLen::::get(WakeupClock::Tick), 1); + assert!(!crate::ActorWaitingOccupancies::::contains_key( + WakeupKey::Tick(scheduled_block) + )); + assert_eq!(crate::WakeupCursorLen::::get(WakeupClock::Tick), 0); + assert_eq!(crate::ActorWaitingFrameChunks::::iter().count(), 0); frame_system::Pallet::::set_block_number(scheduled_block); frame_system::Pallet::::reset_events(); run_idle(Weight::MAX); @@ -1711,8 +1874,98 @@ fn close_before_future_wakeup_leaves_harmless_lazy_tombstone() { }); } +#[cfg(not(feature = "runtime-benchmarks"))] #[test] -fn repeated_timer_close_churn_converges_lazy_wakeup_tombstones() { +fn temporal_service_preserves_exact_primary_wakeup_address() { + for schedule in [timer_schedule(1), at_time_schedule(1)] { + let recurrent = matches!(schedule.trigger, Trigger::Cadenced { .. }); + for steps in [inert_contract_steps(), BoundedVec::default()] { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let tombstone = create_system_with(ALICE, schedule.clone(), None, inert_contract_steps()); + assert_ok!(Actors::close_actor(RuntimeOrigin::signed(ALICE), tombstone)); + let actor_id = create_system_with(BOB, schedule.clone(), None, steps); + let pointer = Actors::actor_hot(actor_id) + .and_then(|hot| hot.trigger_wakeup_pointer) + .expect("temporal physical wakeup pointer exists"); + let Some(crate::ActorControlLocation::Waiting { page, slot, .. }) = + crate::ActorControlLocators::::get(actor_id) + else { + panic!("temporal frame Waiting address exists"); + }; + assert_eq!( + (pointer.page_id, pointer.slot), + (page, u32::from(slot)), + "the temporal pointer must identify its single Waiting primary", + ); + + frame_system::Pallet::::set_block_number(2); + run_idle(Weight::MAX); + + assert!(has_actor_event(|event| matches!( + event, + Event::CycleSummary { actor_id: id, .. } if *id == actor_id + ))); + assert!(crate::WakeupWorkerFaultState::::get().is_none()); + assert!(!ActorIdentities::::contains_key(actor_id)); + + frame_system::Pallet::::set_block_number(3); + run_idle(Weight::MAX); + let summaries = System::events() + .iter() + .filter(|record| { + matches!( + record.event, + RuntimeEvent::Actors(Event::CycleSummary { actor_id: id, .. }) if id == actor_id + ) + }) + .count(); + assert_eq!(summaries, if recurrent { 2 } else { 1 }); + assert!(crate::WakeupWorkerFaultState::::get().is_none()); + assert!(!ActorIdentities::::contains_key(actor_id)); + #[cfg(feature = "try-runtime")] + assert_ok!(crate::Pallet::::do_try_state()); + }); + } + } +} + +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn canonical_close_removes_future_tick_and_releases_state_hold() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = create_user_with( + ALICE, + Mutability::Mutable, + timer_schedule(20), + None, + inert_contract_steps(), + ); + let scheduled_tick = scheduled_wakeup_block(actor_id).expect("Tick wakeup is scheduled"); + assert!(Actors::actor_state_hold(actor_id).is_some()); + + assert_ok!(Actors::close_actor(RuntimeOrigin::signed(ALICE), actor_id)); + + assert!(crate::ActorControlLocators::::get(actor_id).is_none()); + assert!(Actors::active_actor_view(actor_id).is_none()); + assert!(Actors::actor_state_hold(actor_id).is_none()); + assert!(!crate::ActorWaitingOccupancies::::contains_key( + WakeupKey::Tick(scheduled_tick) + )); + assert_eq!(crate::WakeupCursorLen::::get(WakeupClock::Tick), 0); + assert_eq!(crate::ActorWaitingFrameChunks::::iter().count(), 0); + frame_system::Pallet::::set_block_number(scheduled_tick); + run_idle(Weight::MAX); + assert!(Actors::wakeup_buckets(scheduled_tick).is_none()); + assert_eq!(Actors::queue_head(), Actors::queue_tail()); + #[cfg(feature = "try-runtime")] + assert_ok!(crate::Pallet::::do_try_state()); + }); +} + +#[test] +fn repeated_timer_close_churn_leaves_no_wakeup_tombstones() { new_test_ext().execute_with(|| { frame_system::Pallet::::set_block_number(1); let total = <::MaxWakeupsPerBlock as Get>::get() + 2; @@ -1729,7 +1982,8 @@ fn repeated_timer_close_churn_converges_lazy_wakeup_tombstones() { assert_ok!(Actors::close_actor(RuntimeOrigin::signed(ALICE), actor_id)); assert!(scheduled_wakeup_block(actor_id).is_none()); } - assert!(crate::WakeupCursorLen::::get(WakeupClock::Tick) > 0); + assert_eq!(crate::WakeupCursorLen::::get(WakeupClock::Tick), 0); + assert_eq!(crate::ActorWaitingFrameChunks::::iter().count(), 0); frame_system::Pallet::::reset_events(); for offset in 0..10 { frame_system::Pallet::::set_block_number( @@ -1805,33 +2059,12 @@ fn terminal_window_wakeup_survives_queue_saturation_and_continuation() { let existing_ticket = Actors::actor_hot(actor_id) .and_then(|hot| hot.queue_ticket) .expect("Continuation retains its live queue ticket"); - let page_size: u32 = ::QueuePageSize::get(); - let capacity: u32 = ::MaxQueueLength::get(); - for page_id in 0..capacity.div_ceil(page_size) { - let first = page_id.saturating_mul(page_size); - let len = page_size.min(capacity.saturating_sub(first)); - let entries = (0..len) - .map(|offset| { - let ticket = u64::from(first.saturating_add(offset)); - queue_entry( - ticket, - if ticket == existing_ticket { - actor_id - } else { - 30_000_000u64.saturating_add(ticket) - }, - ) - }) - .collect::>(); - QueuePages::::insert( - u64::from(page_id), - BoundedVec::try_from(entries).expect("saturated queue page fits"), - ); - } - QueueHead::::put(0); - QueueTail::::put(u64::from(capacity)); - QueueOccupancy::::put(capacity); - crate::NextQueueTicket::::put(u64::from(capacity)); + let (_, cell) = Actors::actor_control_cell(actor_id).expect("Continuation primary"); + seed_saturated_tombstone_queue(); + crate::ActorReadyFrameChunks::::mutate(existing_ticket / 32, |page| { + page.as_mut().expect("saturated Ready page")[(existing_ticket % 32) as usize] = Some(cell); + }); + crate::ActorReadyOccupancy::::put(1); frame_system::Pallet::::set_block_number(102); frame_system::Pallet::::reset_events(); run_idle(Weight::MAX); @@ -1892,16 +2125,7 @@ fn continuation_attempt_rolls_back_when_retry_wakeup_topology_is_corrupt() { let due = 2u64; frame_system::Pallet::::set_block_number(due); let next_retry = due.saturating_add(2); - crate::WakeupBuckets::::insert( - WakeupKey::Block(next_retry), - crate::WakeupBucketState { - head_page: 0, - tail_page: 0, - next_page_id: 1, - live_entries: 0, - cursor_index: None, - }, - ); + crate::ActorWaitingOccupancies::::insert(WakeupKey::Block(next_retry), 1); let actor_before = Actors::active_actor_view(actor_id).expect("queued continuation"); let continuation_before = Actors::actor_run_state(actor_id) .expect("continuation before corrupt retry placement") @@ -1975,6 +2199,47 @@ fn cancelled_continuation_exactly_invalidates_its_wakeup_before_reprime() { }); } +#[test] +fn saturated_enqueue_defers_latched_actor_and_preserves_failed_placement() { + 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!(schedule_latched_service_wakeup(actor_id, 10)); + seed_saturated_tombstone_queue(); + assert!( + Actors::load_current_step_service_state(actor_id).is_some(), + "canonical latched source" + ); + assert_eq!( + Actors::try_paged_enqueue(actor_id), + Err(crate::EnqueueOutcome::CapacityUnavailable) + ); + let before = polkadot_sdk::sp_io::storage::root(StateVersion::V1); + Actors::test_fail_wakeup_placement_with_capacity(); + assert_eq!( + Actors::enqueue(actor_id), + Err(crate::EnqueueOutcome::WakeupCapacityExhausted) + ); + assert_eq!(polkadot_sdk::sp_io::storage::root(StateVersion::V1), before); + assert_ok!(Actors::enqueue(actor_id)); + let hot = Actors::actor_hot(actor_id).expect("deferred actor"); + assert!(hot.pending_signal); + assert!(hot.queue_ticket.is_none()); + assert_eq!( + hot.wakeup_pointer.expect("exact fallback").block, + WakeupKey::Block(2) + ); + assert!(matches!( + Actors::actor_control_cell(actor_id) + .expect("sole primary") + .0, + crate::ActorControlLocation::Waiting { .. } + )); + #[cfg(feature = "try-runtime")] + assert_ok!(Actors::do_try_state()); + }); +} + #[test] fn queue_saturation_at_block_max_cannot_create_same_block_wakeup() { new_test_ext().execute_with(|| { @@ -2002,25 +2267,107 @@ fn queue_saturation_at_block_max_cannot_create_same_block_wakeup() { 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 step = inert_contract_steps()[0].clone(); + let steps = BoundedVec::try_from(vec![step.clone(), step]).expect("two Steps fit"); + let actor_id = create_system_with(ALICE, timer_schedule(100), None, steps); + let first_tick = Actors::actor_hot(actor_id) + .and_then(|hot| hot.trigger_wakeup_pointer) + .expect("initial Cadenced pointer") + .tick; + frame_system::Pallet::::set_block_number(first_tick); + Actors::on_idle(first_tick, Weight::MAX); + let run = Actors::actor_run_state(actor_id).expect("first Step leaves a Running suffix"); + assert_eq!(run.cursor, 1); + let service_at = run.eligible_at; + let (location, _) = Actors::actor_control_cell(actor_id).expect("Running primary"); + if matches!(location, crate::ActorControlLocation::Ready { .. }) { + let cell = + Actors::remove_primary_control_cell_inner(actor_id).expect("consume Ready placement"); + assert_ok!(Actors::control_append_waiting( + cell, + WakeupKey::Block(service_at), + crate::scheduler::ActorWaitingAuthority::Service, + )); + } 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)); + .expect("Running cadence is rearmed"); let hot = Actors::actor_hot(actor_id).expect("Actor owns both temporal memberships"); + assert_eq!(hot.cycle_state, CycleState::Running); + assert!(!hot.pending_signal); assert!(hot.wakeup_pointer.is_some()); assert_eq!(hot.trigger_wakeup_pointer, Some(trigger_pointer)); + assert!(matches!( + crate::ActorWaitingFrameChunks::::get(( + WakeupKey::Tick(trigger_pointer.tick), + trigger_pointer.page_id + )) + .expect("independent Trigger page") + .entries[trigger_pointer.slot as usize], + Some(crate::ActorWaitingEntry::Reference(_)) + )); #[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]); + let (mut ready, stats) = Actors::wakeup_substrate_drain_key(WakeupKey::Block(service_at), 1); + assert_eq!( + ready.iter().map(|entry| entry.0).collect::>(), + vec![actor_id] + ); assert_eq!(stats.ready_entries, 1); + let (id, state, admission, loaded_step) = ready.pop().expect("consumed service authority"); + let loaded_step = loaded_step.expect("Running Step"); + let cell = crate::ActorControlCellOf:: { + actor_id: id, + identity: Actors::control_identity_from_scalar(state.identity).expect("canonical identity"), + hot: Actors::control_hot_from_scalar(state.hot), + admission, + cursor: loaded_step.cursor, + resources: loaded_step.resources, + eligible_at: Some(service_at), + }; + assert_ok!(Actors::control_append_ready(cell)); 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()); + { + assert_ok!(crate::Pallet::::do_try_state()); + } + }); +} + +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn canonical_waiting_publication_shares_and_releases_the_temporal_cursor_atomically() { + new_test_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let actor_id = create_system_with(ALICE, timer_schedule(20), None, inert_contract_steps()); + let key = WakeupKey::Tick(21); + let waiting_index = crate::ActorWaitingCursorIndices::::get(key) + .expect("canonical temporal primary owns the shared key"); + assert!(matches!( + crate::ActorControlLocators::::get(actor_id), + Some(crate::ActorControlLocation::Waiting { key: stored, .. }) if stored == key + )); + assert_eq!(crate::ActorWaitingOccupancies::::get(key), 1); + assert_eq!( + crate::ActorWaitingCursorIndices::::get(key), + Some(waiting_index) + ); + assert_eq!(Actors::wakeup_cursor_peek_key(WakeupClock::Tick), Some(key)); + + frame_system::Pallet::::set_block_number(21); + run_idle(Weight::MAX); + assert_eq!(crate::ActorWaitingOccupancies::::get(key), 0); + assert!(!crate::ActorWaitingCursorIndices::::contains_key(key)); + assert!(!crate::ActorWaitingOccupancies::::contains_key(key)); + assert_eq!(scheduled_wakeup_block(actor_id), Some(41)); + assert_eq!( + crate::ActorControlLocators::::get(actor_id), + Some(crate::ActorControlLocation::Unsignaled), + "completed cadence retains one stable primary while the next trigger is lightweight" + ); }); } @@ -2043,7 +2390,7 @@ fn wakeup_ownership_fails_closed_for_corrupt_actor_partitions() { 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::wakeup_substrate_schedule(actor_id, 10)); + assert!(schedule_latched_service_wakeup(actor_id, 10)); let pointer = Actors::actor_hot(actor_id) .and_then(|hot| hot.wakeup_pointer) .expect("scheduled actor owns a wakeup pointer"); @@ -2079,8 +2426,14 @@ fn mixed_wakeup_bucket_rolls_back_valid_neighbors_around_corruption() { .map(|_| create_system_with(ALICE, manual_schedule(), None, inert_contract_steps())) .collect::>(); for actor_id in &actors { - assert!(Actors::wakeup_substrate_schedule(*actor_id, 10)); + assert!(schedule_latched_service_wakeup(*actor_id, 10)); } + let primaries = actors + .iter() + .map(|actor_id| { + Actors::actor_control_cell(*actor_id).expect("canonical primary before corruption") + }) + .collect::>(); ActorFunding::::remove(actors[corrupt_index]); let pointers = actors .iter() @@ -2108,7 +2461,21 @@ fn mixed_wakeup_bucket_rolls_back_valid_neighbors_around_corruption() { Actors::actor_hot(*actor_id).and_then(|hot| hot.wakeup_pointer), Some(pointers[index]) ); - assert_eq!(Actors::actor_identities(*actor_id).unwrap().cycle_nonce, 0); + assert_eq!( + Actors::actor_control_cell(*actor_id), + Some(primaries[index].clone()) + ); + assert_eq!(primaries[index].1.identity.cycle_nonce, 0); + if index == corrupt_index { + assert!(Actors::actor_identity(*actor_id).is_none()); + } else { + assert_eq!( + Actors::actor_identity(*actor_id) + .expect("healthy identity") + .cycle_nonce, + 0 + ); + } } #[cfg(feature = "try-runtime")] assert!(crate::Pallet::::do_try_state().is_err()); diff --git a/template/pallets/actors/src/types/lifecycle.rs b/template/pallets/actors/src/types/lifecycle.rs index 5bc9baa4..65975a84 100644 --- a/template/pallets/actors/src/types/lifecycle.rs +++ b/template/pallets/actors/src/types/lifecycle.rs @@ -241,33 +241,27 @@ pub enum SimulationError { TypeMismatch, MutabilityMismatch, InvalidContract, + InvalidBudget, ContractMismatch, ModeCycleStateMismatch, GlobalCircuitBreaker, Paused, NotReady, + ResourceDeferred, FeeCollectionFailed, } #[derive( - polkadot_sdk::frame_support::CloneNoBound, - polkadot_sdk::frame_support::DebugNoBound, - polkadot_sdk::frame_support::PartialEqNoBound, - polkadot_sdk::frame_support::EqNoBound, - Decode, - DecodeWithMemTracking, - Encode, - TypeInfo, + Clone, Debug, Decode, DecodeWithMemTracking, Encode, Eq, PartialEq, TypeInfo, MaxEncodedLen, )] -#[scale_info(skip_type_params(MaxContractSteps))] -pub struct SimulationResult> { +pub struct SimulationResult { pub status: AttemptDisposition, pub cycle_nonce: u64, pub start_cursor: u32, pub run_cursor: Option, pub unsuccessful_attempts_at_cursor: Option, pub cumulative_outcomes: OutcomeTotals, - pub steps: BoundedVec, + pub steps: BoundedVec>, } /// Internal execution-phase output of the canonical actor classifier. @@ -916,6 +910,7 @@ pub(crate) struct ActiveActorView { pub window: Option>, pub steps: Steps, pub completion: CompletionPolicy, + pub trigger_runtime_state: TriggerRuntimeState, pub cycle_nonce: u64, pub auto_close_at_cycle_nonce: Option, pub unsuccessful_attempt_streak: u32, diff --git a/template/pallets/actors/src/types/plan.rs b/template/pallets/actors/src/types/plan.rs index 2b834d11..3a299422 100644 --- a/template/pallets/actors/src/types/plan.rs +++ b/template/pallets/actors/src/types/plan.rs @@ -8,4 +8,6 @@ pub struct StepExecutionPlan, } diff --git a/template/pallets/actors/src/types/resource.rs b/template/pallets/actors/src/types/resource.rs index cbe5c86a..7569ef2f 100644 --- a/template/pallets/actors/src/types/resource.rs +++ b/template/pallets/actors/src/types/resource.rs @@ -1,6 +1,31 @@ use frame::prelude::*; use polkadot_sdk::frame_support::weights::Weight; +/// Explicit independent resource ceilings for one rollback-only Actor preview. +#[derive( + Clone, Copy, Debug, Decode, DecodeWithMemTracking, Encode, Eq, MaxEncodedLen, PartialEq, TypeInfo, +)] +pub struct SimulationBudget { + pub actor_control: Weight, + pub shared_economic: Weight, +} + +impl SimulationBudget { + pub fn checked_limits(self) -> Result { + let actor_base = weight_floor_div(self.shared_economic, 2); + let user_base = self + .shared_economic + .checked_sub(&actor_base) + .ok_or(BlockResourceError::ArithmeticOverflow)?; + BlockResourceLimits::new( + self.actor_control, + self.shared_economic, + actor_base, + user_base, + ) + } +} + /// One authoritative block-resource domain. #[derive( Clone, Copy, Debug, Decode, DecodeWithMemTracking, Encode, Eq, MaxEncodedLen, PartialEq, TypeInfo, @@ -369,6 +394,20 @@ impl BlockResourceState { Ok(reservation) } + /// Negative capacity probe only; real reservation retains phase and overflow handling. + pub(crate) fn capacity_exceeded( + &self, + limits: BlockResourceLimits, + domain: BlockResourceDomain, + maximum: Weight, + ) -> bool { + let mut probe = *self; + matches!( + probe.reserve(limits, domain, maximum), + Err(BlockResourceError::LimitExceeded) + ) + } + pub fn reserve_actor_step( &mut self, limits: BlockResourceLimits, @@ -786,6 +825,49 @@ impl BlockResourceUsage { mod tests { use super::*; + #[test] + fn simulation_budget_preserves_domain_ceilings_and_checked_sum() -> Result<(), BlockResourceError> + { + for (actor_control, shared_economic) in [ + (Weight::zero(), Weight::zero()), + (Weight::MAX, Weight::zero()), + (Weight::from_parts(7, 11), Weight::from_parts(13, 17)), + ( + Weight::from_parts(u64::MAX / 2, u64::MAX / 2), + Weight::from_parts(u64::MAX / 2, u64::MAX / 2), + ), + ] { + let limits = SimulationBudget { + actor_control, + shared_economic, + } + .checked_limits()?; + assert_eq!(limits.actor_control(), actor_control); + assert_eq!(limits.shared_economic(), shared_economic); + assert_eq!( + limits.actor_base_turn(), + weight_floor_div(shared_economic, 2) + ); + assert_eq!( + limits + .actor_base_turn() + .checked_add(&limits.user_base_turn()), + Some(shared_economic), + ); + } + for shared_economic in [Weight::from_parts(1, 0), Weight::from_parts(0, 1)] { + assert_eq!( + SimulationBudget { + actor_control: Weight::MAX, + shared_economic + } + .checked_limits(), + Err(BlockResourceError::ArithmeticOverflow), + ); + } + Ok(()) + } + fn weight(ref_time: u64, proof_size: u64) -> Weight { Weight::from_parts(ref_time, proof_size) } @@ -1421,6 +1503,34 @@ mod tests { ); } + #[test] + fn capacity_probe_is_read_only_and_distinguishes_owner_errors() { + let domain = BlockResourceDomain::ActorDrainEffect; + let initial = BlockResourceState::new(1u64); + let mut active = initial; + active.phase = BlockResourcePhase::FreshDrain; + let mut halted = active; + halted.halt_optional_actor_work(); + let mut overflow = active; + overflow.usage.actor_effect = Weight::MAX; + let mut reservation_overflow = active; + reservation_overflow.outstanding_reservations = u32::MAX; + for (state, maximum, exceeded) in [ + (initial, Weight::MAX, false), + (active, Weight::zero(), false), + (active, weight(80, 80), false), + (active, weight(81, 0), true), + (active, weight(0, 81), true), + (halted, Weight::MAX, false), + (overflow, weight(1, 0), false), + (reservation_overflow, weight(1, 0), false), + ] { + let before = state; + assert_eq!(state.capacity_exceeded(limits(), domain, maximum), exceeded); + assert_eq!(state, before); + } + } + #[test] fn fixed_components_are_complete_checked_and_feed_the_budget() { let components = FixedBlockWeightComponents::new( diff --git a/template/pallets/actors/src/weights.rs b/template/pallets/actors/src/weights.rs index 70082061..ba985b09 100644 --- a/template/pallets/actors/src/weights.rs +++ b/template/pallets/actors/src/weights.rs @@ -93,9 +93,10 @@ pub trait WeightInfo { fn pipeline_admission_apoptosis() -> Weight; fn close_actor() -> Weight; fn fee_collection() -> Weight; + fn action_invocation_receipt() -> Weight; fn predicate_set_evaluation(predicates: u32) -> Weight; - fn cycle_orchestration() -> Weight; - fn step_orchestration(steps: u32) -> Weight; + fn predicate_asset_evaluation(predicates: u32) -> Weight; + fn predicate_observation_heavy_evaluation(observations: u32) -> Weight; fn task_transfer() -> Weight; fn task_burn() -> Weight; fn task_mint() -> Weight; @@ -118,7 +119,13 @@ pub trait WeightInfo { 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_target_snapshot_capture(entries: u32) -> Weight; + fn opening_share_mixed_capture(entries: u32) -> Weight; + fn opening_snapshot_traversal() -> Weight; fn opening_predicate_capture(predicates: u32) -> Weight; + fn opening_max_encoded_balance_capture(predicates: u32) -> Weight; + fn opening_observation_heavy_capture(observations: u32) -> Weight; + fn opening_predicate_traversal() -> Weight; fn scheduler_on_initialize_cutoff() -> Weight; fn scheduler_on_idle_base() -> Weight; fn materialization_coordinator_base() -> Weight; @@ -143,15 +150,27 @@ pub trait WeightInfo { fn scheduler_paged_tombstone_drain(entries: u32) -> Weight; fn scheduler_paged_mixed_scan(entries: u32) -> Weight; fn scheduler_inner_zero_step_complete() -> Weight; + /// Complete state-preserving FIFO refusal for a paid zero-Step User Crossing whose current + /// observation is unavailable. The conservative fallback exists only until the host regenerates + /// this benchmark-owned method with its complete production Weight artifact. + fn scheduler_paged_zero_step_user_crossing_unavailable() -> Weight { + Self::scheduler_paged_execute_opening_max() + } 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; + /// Complete zero-tail User Opening with the maximum admitted funding-policy header. + fn scheduler_inner_opening_user_complete_header_max() -> Weight { + Self::scheduler_inner_opening_complete_max(0) + } + /// Complete positive-tail User Opening with the maximum admitted funding-policy header. + fn scheduler_inner_opening_user_complete_header_max_tail(tail_chunks: u32) -> Weight { + Self::scheduler_inner_opening_complete_max(tail_chunks) + } 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; @@ -160,37 +179,22 @@ pub trait WeightInfo { 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, + tail_opening_amount_entries: u32, + current_predicates: u32, ) -> Weight; + fn scheduler_inner_suspended_head_complete(current_predicates: u32) -> Weight; fn scheduler_inner_suspended_head_progress( - opening_snapshot_entries: u32, - opening_predicate_results: u32, - funding_snapshot_entries: u32, - predicates: u32, + tail_opening_amount_entries: u32, + current_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, + tail_opening_amount_entries: u32, + current_predicates: u32, ) -> Weight; + fn scheduler_inner_suspended_head_opening_complete(current_predicates: u32) -> Weight; fn scheduler_inner_suspended_head_opening_progress( - opening_snapshot_entries: u32, - opening_predicate_results: u32, - funding_snapshot_entries: u32, + tail_opening_amount_entries: u32, + current_predicates: u32, ) -> Weight; fn scheduler_paged_execute_cheap(executions: u32) -> Weight; fn scheduler_paged_execute_cheap_mixed(executions: u32) -> Weight; @@ -200,10 +204,8 @@ pub trait WeightInfo { fn funding_snapshot_open(assets: 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; @@ -373,6 +375,10 @@ impl WeightInfo for Subst .saturating_add(T::DbWeight::get().writes(1)) } + fn action_invocation_receipt() -> Weight { + Weight::from_parts(10_000_000, 0) + } + fn predicate_set_evaluation(predicates: u32) -> Weight { if predicates == 0 { return Weight::zero(); @@ -383,14 +389,12 @@ impl WeightInfo for Subst .saturating_add(T::DbWeight::get().reads(1u64.saturating_add(2u64.saturating_mul(bounded)))) } - fn cycle_orchestration() -> Weight { - Weight::from_parts(44_699_000, 9667).saturating_add(T::DbWeight::get().reads_writes(3, 2)) + fn predicate_asset_evaluation(predicates: u32) -> Weight { + Self::predicate_set_evaluation(predicates) } - fn step_orchestration(steps: u32) -> Weight { - Weight::from_parts(44_555_323, 9667) - .saturating_add(Weight::from_parts(215_321, 0).saturating_mul(steps.into())) - .saturating_add(T::DbWeight::get().reads_writes(3, 2)) + fn predicate_observation_heavy_evaluation(observations: u32) -> Weight { + Self::predicate_set_evaluation(observations.saturating_add(1)) } fn task_transfer() -> Weight { @@ -525,12 +529,36 @@ impl WeightInfo for Subst .saturating_add(T::DbWeight::get().reads(u64::from(entries))) } + fn opening_target_snapshot_capture(entries: u32) -> Weight { + Self::opening_snapshot_capture(entries) + } + + fn opening_share_mixed_capture(entries: u32) -> Weight { + Self::opening_snapshot_capture(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 opening_max_encoded_balance_capture(predicates: u32) -> Weight { + Self::opening_predicate_capture(predicates) + } + + fn opening_observation_heavy_capture(observations: u32) -> Weight { + Self::opening_predicate_capture(observations.saturating_add(1)) + } + + fn opening_predicate_traversal() -> Weight { + Weight::from_parts(25_000_000, 0) + } + + fn opening_snapshot_traversal() -> Weight { + Weight::from_parts(25_000_000, 0) + } + fn scheduler_on_initialize_cutoff() -> Weight { Weight::from_parts(7_543_000, 1_493) .saturating_add(T::DbWeight::get().reads(1)) @@ -647,13 +675,6 @@ impl WeightInfo for Subst 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) @@ -707,27 +728,27 @@ impl WeightInfo for Subst Weight::from_parts(20_000_000_000, 600_000) } - fn scheduler_inner_suspended_head_retry(_: u32, _: u32, _: u32, _: u32) -> Weight { + fn scheduler_inner_suspended_head_retry(_: u32, _: u32) -> Weight { Weight::from_parts(20_000_000_000, 600_000) } - fn scheduler_inner_suspended_head_complete(_: u32, _: u32, _: u32, _: u32) -> Weight { + fn scheduler_inner_suspended_head_complete(_: u32) -> Weight { Weight::from_parts(20_000_000_000, 600_000) } - fn scheduler_inner_suspended_head_progress(_: u32, _: u32, _: u32, _: u32) -> Weight { + fn scheduler_inner_suspended_head_progress(_: u32, _: u32) -> Weight { Weight::from_parts(20_000_000_000, 600_000) } - fn scheduler_inner_suspended_head_opening_retry(_: u32, _: u32, _: u32) -> Weight { + fn scheduler_inner_suspended_head_opening_retry(_: u32, _: u32) -> Weight { Weight::from_parts(20_000_000_000, 600_000) } - fn scheduler_inner_suspended_head_opening_complete(_: u32, _: u32, _: u32) -> Weight { + fn scheduler_inner_suspended_head_opening_complete(_: u32) -> Weight { Weight::from_parts(20_000_000_000, 600_000) } - fn scheduler_inner_suspended_head_opening_progress(_: u32, _: u32, _: u32) -> Weight { + fn scheduler_inner_suspended_head_opening_progress(_: u32, _: u32) -> Weight { Weight::from_parts(20_000_000_000, 600_000) } @@ -770,19 +791,12 @@ impl WeightInfo for Subst Weight::from_parts(28_668_868, 4_178) .saturating_add(T::DbWeight::get().reads_writes(2, 2)) } - fn run_retry() -> Weight { - Weight::from_parts(22_070_000, 4_266).saturating_add(T::DbWeight::get().reads_writes(1, 1)) - } fn run_complete() -> Weight { Weight::from_parts(18_019_000, 4_030).saturating_add(T::DbWeight::get().reads_writes(1, 2)) } fn run_cancel() -> Weight { Weight::from_parts(56_782_000, 8_120).saturating_add(T::DbWeight::get().reads_writes(6, 4)) } - 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())) - } fn update_contract() -> Weight { Weight::from_parts(162_733_000, 10_181) @@ -842,7 +856,9 @@ impl WeightInfo for Subst Weight::MAX } fn block_resource_finalize() -> Weight { - Weight::MAX + Weight::from_parts(10_000_000, 1_560) + .saturating_add(T::DbWeight::get().reads(1)) + .saturating_add(T::DbWeight::get().writes(2)) } } @@ -881,6 +897,7 @@ impl WeightInfo for TestWeightInfo { 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 action_invocation_receipt() -> Weight { Weight::from_parts(10_000_000, 0) } fn predicate_set_evaluation(predicates: u32) -> Weight { if predicates == 0 { return Weight::zero(); @@ -890,10 +907,11 @@ impl WeightInfo for TestWeightInfo { .saturating_add(Weight::from_parts(9_778_566, 2_561).saturating_mul(bounded)) } - fn cycle_orchestration() -> Weight { Weight::from_parts(44_699_000, 9667) } - fn step_orchestration(steps: u32) -> Weight { - Weight::from_parts(44_555_323, 9667) - .saturating_add(Weight::from_parts(215_321, 0).saturating_mul(steps.into())) + fn predicate_asset_evaluation(predicates: u32) -> Weight { + Self::predicate_set_evaluation(predicates) + } + fn predicate_observation_heavy_evaluation(observations: u32) -> Weight { + Self::predicate_set_evaluation(observations.saturating_add(1)) } fn task_transfer() -> Weight { Weight::from_parts(159_800_000, 8_120) } fn task_burn() -> Weight { Weight::from_parts(23_397_000, 3_593) } @@ -938,10 +956,24 @@ impl WeightInfo for TestWeightInfo { Weight::from_parts(25_000_000, 4_000) .saturating_add(Weight::from_parts(15_000_000, 3_000).saturating_mul(entries.into())) } + fn opening_target_snapshot_capture(entries: u32) -> Weight { + Self::opening_snapshot_capture(entries) + } + fn opening_share_mixed_capture(entries: u32) -> Weight { + Self::opening_snapshot_capture(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())) } + fn opening_max_encoded_balance_capture(predicates: u32) -> Weight { + Self::opening_predicate_capture(predicates) + } + fn opening_observation_heavy_capture(observations: u32) -> Weight { + Self::opening_predicate_capture(observations.saturating_add(1)) + } + fn opening_predicate_traversal() -> Weight { Weight::from_parts(25_000_000, 0) } + fn opening_snapshot_traversal() -> Weight { Weight::from_parts(25_000_000, 0) } 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) } @@ -977,12 +1009,6 @@ impl WeightInfo for TestWeightInfo { 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) } @@ -1022,22 +1048,22 @@ impl WeightInfo for TestWeightInfo { 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 { + fn scheduler_inner_suspended_head_retry(_: u32, _: u32) -> Weight { Weight::from_parts(20_000_000_000, 600_000) } - fn scheduler_inner_suspended_head_complete(_: u32, _: u32, _: u32, _: u32) -> Weight { + fn scheduler_inner_suspended_head_complete(_: u32) -> Weight { Weight::from_parts(20_000_000_000, 600_000) } - fn scheduler_inner_suspended_head_progress(_: u32, _: u32, _: u32, _: u32) -> Weight { + fn scheduler_inner_suspended_head_progress(_: u32, _: u32) -> Weight { Weight::from_parts(20_000_000_000, 600_000) } - fn scheduler_inner_suspended_head_opening_retry(_: u32, _: u32, _: u32) -> Weight { + fn scheduler_inner_suspended_head_opening_retry(_: u32, _: u32) -> Weight { Weight::from_parts(20_000_000_000, 600_000) } - fn scheduler_inner_suspended_head_opening_complete(_: u32, _: u32, _: u32) -> Weight { + fn scheduler_inner_suspended_head_opening_complete(_: u32) -> Weight { Weight::from_parts(20_000_000_000, 600_000) } - fn scheduler_inner_suspended_head_opening_progress(_: u32, _: u32, _: u32) -> Weight { + fn scheduler_inner_suspended_head_opening_progress(_: u32, _: u32) -> Weight { Weight::from_parts(20_000_000_000, 600_000) } fn scheduler_paged_execute_cheap(executions: u32) -> Weight { @@ -1057,13 +1083,8 @@ impl WeightInfo for TestWeightInfo { } 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())) - } fn update_contract() -> Weight { Weight::from_parts(162_733_000, 10_181) } fn set_global_circuit_breaker() -> Weight { Weight::from_parts(8_000_000, 600) } fn clear_crossing_worker_fault() -> Weight { Weight::from_parts(16_000_000, 1_529) } diff --git a/template/pallets/governance/docs/architecture.en.md b/template/pallets/governance/docs/architecture.en.md index b25d47c8..b4877a44 100644 --- a/template/pallets/governance/docs/architecture.en.md +++ b/template/pallets/governance/docs/architecture.en.md @@ -153,7 +153,7 @@ Public read helpers now expose these governance-observability layers, and the sa - Runtime-declared power-profile identity for the live track family behind that vote kind - Current runtime returns `DecliningDirectStake` for ordinary `Aye / Nay` - Current runtime returns `DecliningVetoAsset` for protocol / network protection-track `Veto / Pass` - - Current runtime returns `DecliningNativeStake` for `$BLDR`-domain protection-track `Veto / Pass`, backed by locked `$NTVE`, locked `stNTVE`, and locked `NTVE/stNTVE` LP-derived `NativeVotePower` + - Current runtime returns `DecliningNativeStake` for `$BLDR`-domain protection-track `Veto / Pass`, backed by locked `$NTVE`, locked `stNTVE`, and locked `$NTVE/stNTVE` LP-derived `NativeVotePower` 5. `proposal_resolution_state(domain, item_id)` - `VotingWindowOpen { current_epoch, maturity_epoch }` @@ -642,7 +642,7 @@ For the current launch line, the runtime policy is now intentionally frozen to t Normal runtime builds apply the shipped piecewise `7x -> 1x` curve to ordinary and protection-track ballots. Ordinary `Aye / Nay` derive their base from same-domain `Staking::stake_value(domain, account)`; protection-track `Veto / Pass` use the runtime-declared protection surface for that domain. -For `$BLDR`, the native protection surface adds locked `$NTVE`, locked `stNTVE` converted through the staking exchange rate, and account-level locked `NTVE/stNTVE` LP converted into conservative native-equivalent `NativeVotePower`. +For `$BLDR`, the native protection surface adds locked `$NTVE`, locked `stNTVE` converted through the staking exchange rate, and account-level locked `$NTVE/stNTVE` LP converted into conservative native-equivalent `NativeVotePower`. **2. Domain-scoped hierarchy.** @@ -650,7 +650,7 @@ Protocol / network governance runs as `$NTVE` primary + `$VETO` protection, whil **3. Protection-track cancellation.** -Domain-specific backing enables the first live protection slice. Protocol governance uses the well-known `$VETO` asset class, created at genesis with deterministic metadata and an Asset Registry-owned admin surface. `$BLDR` governance uses locked `$NTVE` / `stNTVE` / `NTVE/stNTVE` LP-derived native `NativeVotePower`. +Domain-specific backing enables the first live protection slice. Protocol governance uses the well-known `$VETO` asset class, created at genesis with deterministic metadata and an Asset Registry-owned admin surface. `$BLDR` governance uses locked `$NTVE` / `stNTVE` / `$NTVE/stNTVE` LP-derived native `NativeVotePower`. One account may vote once in each track on the same item. Later protection replacements use the later ballot epoch, and protection ballots remain admissible until the configured close. @@ -682,7 +682,7 @@ Instead: - `unlock_vote_power(lock_id)` transactionally returns the full aggregate source position after its horizon and removes the position; early and unknown releases fail explicitly - `cast_vote(...)` now already enforces the generic rule that ordinary ballots cannot enter before `primary_open`, while protection-track ballots remain admissible during any configured lead-in - `proposal_resolution_state(...)` first checks whether frozen raw protection-majority triggers the immediate threshold. After maturity, raw `Veto` turnout must clear the `1%` dust floor before the stored-weight `Veto` versus `Pass` gate applies; only then does the evaluator derive primary state from the family-aware tally. -- Binary families use weighted `Aye / Nay`. Invoice families use `weighted_positive` versus `weighted_nay` with deterministic lowest-scalar tie-breaking across `Amplify / Approve / Reduce`. The launch-line runtime still reports only `Binary`, so invoice resolution remains kernel-ready rather than live reference-line policy. +- Binary families use weighted `Aye / Nay`. Invoice families use `weighted_positive` versus `weighted_nay` with deterministic lowest-scalar tie-breaking across `Amplify / Approve / Reduce`. The current runtime reports `Invoice` for tactical-domain `L2TreasurySpend` in the canonical `$BLDR` domain and `Binary` for every other enabled combination. - Vote-derived finalization paths reuse the same logic rather than carrying a second hidden policy engine This keeps the pallet simpler and more honest, but it means tally/resolution cost scales with the bounded ballot-set size rather than O(1) cached counters. @@ -881,7 +881,7 @@ The pallet intentionally retains only recent finalized outcomes. **1. Launch policy is intentionally narrow and frozen.** -Ordinary `Aye / Nay` applies ballot-time Declining Power to same-domain `Staking::stake_value(...)`. Protocol / network protection-track `Veto / Pass` applies the same curve to the `$VETO` asset. `$BLDR` protection applies it to locked `$NTVE` / `stNTVE` / `NTVE/stNTVE` LP-derived native `NativeVotePower`. +Ordinary `Aye / Nay` applies ballot-time Declining Power to same-domain `Staking::stake_value(...)`. Protocol / network protection-track `Veto / Pass` applies the same curve to the `$VETO` asset. `$BLDR` protection applies it to locked `$NTVE` / `stNTVE` / `$NTVE/stNTVE` LP-derived native `NativeVotePower`. The policy includes a raw-supply immediate-cancellation gate and a raw `1%` veto dust floor before final protection can activate. It admits protection ballots until the configured close, retains bounded recent outcomes, and exposes a deliberately narrow admin recovery surface. Broader models remain future opt-ins, not hidden implementation debt. @@ -917,6 +917,10 @@ TMCTOL treats the live proposal set as protected, so the separate dual-mode prot The pallet ships only bounded GovXP input counters. Richer identity layers, any later bounded multiplier policy, delegation semantics, and soulbound reputation policy remain future work. +**10. Builder invoice settlement has not converged to the current contract.** + +The runtime still selects the fixed `BldrTreasury` enum, carries no bounded validated CIDv1 invoice identity or explicit validated treasury account, transfers only the full scalar target, and collapses insufficient capacity into generic dispatch failure. Runtime types, tests, benchmarks, Weight, metadata, and clients must implement `BaseFloorCapped` settlement, the new payload fields, and typed below-floor `InsufficientTreasuryCapacity` evidence before this architecture can claim the complete Builder invoice contract as shipped. + ## Conclusion `pallet-governance` has evolved from a narrow reward-memory helper into a bounded governance kernel with a real proposal lifecycle. diff --git a/template/pallets/governance/docs/specification.en.md b/template/pallets/governance/docs/specification.en.md index 7696ebd3..2935717e 100644 --- a/template/pallets/governance/docs/specification.en.md +++ b/template/pallets/governance/docs/specification.en.md @@ -542,17 +542,26 @@ At minimum that payload MUST declare: - `beneficiary` — the account that receives the payout - `payout asset` — the asset in which the invoice is paid - `base amount` — the pre-scalar amount approved for evaluation -- `funding source` — the treasury or other governance-declared controlled account from which payment is drawn +- `treasury account` — the governance-declared controlled account from which payment is drawn +- `invoice CID` — a required bounded content-addressed IPFS identifier for the canonical reason and supporting evidence + +The shared queryable `MaxCidBytes` bound applies; the reference bound is `96` bytes. The bytes MUST encode one canonical CIDv1 string in a runtime-supported multibase form. A mutable web URL is not a conforming canonical invoice identity, though the content-addressed document may contain ordinary external links. + +The canonical invoice settlement policy is `BaseFloorCapped`: approval defines a scalar target, while only the premium above the submitted base amount may be clipped by treasury capacity. Execution rules: -1. The discrete winning scalar from Section 6.1 applies to the declared `base amount` -2. `final payout = base amount * winning scalar` -3. The funding source MUST be one of the domain's governance-declared executable treasury sources; payout MUST NOT be implicitly drawn from arbitrary protocol balances. On the current reference line, the tactical `$BLDR` domain declares exactly one executable treasury source: `BldrTreasury` -4. The beneficiary is the explicit payload field, not automatically the proposer unless those identities are intentionally the same -5. Invoice execution MUST be transactional: either the full payout executes, or no payout executes -6. If the payload exceeds declared domain/payload caps or available spendable capacity at enactment, execution MUST fail explicitly rather than partially paying -7. A successful invoice enactment MUST produce a bounded on-chain observability record that includes beneficiary, payout asset, base amount, winning scalar, final payout amount, and funding source +1. The discrete winning scalar from Section 6.1 applies to the declared `base amount` and derives one checked `target amount` +2. The runtime MUST validate the treasury account against the proposal's governance domain before admission and enactment; a payload MUST NOT select an arbitrary account or a treasury outside that domain's declared authority +3. At enactment, the runtime MUST derive the treasury's `spendable amount` after host-ledger minimums and explicit holds +4. The minimum payable amount is `min(base amount, target amount)`; this equals `base amount` for an above-base target and equals the complete target for a target at or below base +5. If `spendable amount < minimum payable amount`, execution MUST record a typed `InsufficientTreasuryCapacity` failure and transfer nothing +6. Otherwise `actual amount = min(target amount, spendable amount)`; `actual amount < target amount` is conforming only when the target exceeds base and actual remains at least the complete base amount +7. Multiple live invoices create no implicit balance reservation or pro-rata entitlement; canonical execution order may reduce an above-base premium or leave a later approved invoice below its minimum payable capacity, in which case that invoice fails and requires a fresh proposal after treasury replenishment +8. If the computed actual amount fails the host ledger's transfer admissibility rules, execution MUST fail explicitly with no payout +9. The beneficiary is the explicit payload field, not automatically the proposer unless those identities are intentionally the same +10. Invoice execution MUST be transactional: the complete computed `actual amount` transfers or no transfer occurs +11. A successful invoice enactment MUST produce a bounded on-chain observability record containing beneficiary, payout asset, treasury account, invoice CID, base amount, winning scalar, target amount, actual amount, and capacity-limited status The protection lane MUST remain constitutionally separate from primary-track pricing: @@ -771,7 +780,7 @@ At minimum, the event contract MUST cover: 5. `Enactment scheduled` — domain, item identity, finalized epoch, enactment epoch 6. `Enacted successfully` — domain, item identity, payload identity, execution authority, success marker 7. `Execution failed` — domain, item identity, payload identity, execution authority, bounded failure reason -8. `Invoice executed` when an `L2TreasurySpend` payload pays out — beneficiary, payout asset, base amount, scalar, final payout amount, funding source +8. `Invoice executed` when an `L2TreasurySpend` payload pays out — beneficiary, payout asset, treasury account, invoice CID, base amount, scalar, target amount, actual amount, and capacity-limited status 9. `Runtime upgrade executed` when an `L1RootAction` runtime-upgrade payload succeeds — code-update observability sufficient for operators and indexers to identify the upgrade event The event contract MAY be finer-grained than this minimum, but it MUST NOT be poorer than this minimum. @@ -896,7 +905,7 @@ A conforming governance subsystem SHOULD realize the contract above through boun - Typed payload or preimage-backed payload handling for governance actions - Burned proposal-opening-fee handling for payload kinds that use it - Governance-driven runtime upgrade execution, including `set_code` -- Invoice payout execution from declared funding sources +- Base-floor-capped invoice payout execution from declared domain treasury accounts with typed below-floor capacity failure - Treasury actions, LP accounting, and streaming-state transitions - GovXP counter updates at declared lifecycle boundaries - The event/observability contract from Section 10.1 diff --git a/template/primitives/src/ecosystem.rs b/template/primitives/src/ecosystem.rs index 6eae6e1e..00dea94f 100644 --- a/template/primitives/src/ecosystem.rs +++ b/template/primitives/src/ecosystem.rs @@ -121,13 +121,13 @@ pub mod actor_ids { /// SS58: `5D7ZRz4hMphgVdq9UYBA9Gtk1q2cBjKTgoDCqpBETQi6Ziq4` pub const BLDR_LIQUIDITY_ACTOR_ID: u64 = 11; - /// BLDR Bucket A (Anchor) — permanent LP accumulator for NTVE-BLDR pair + /// BLDR Anchor — permanent LP accumulator for the NTVE-BLDR pair /// Created at genesis (`actor_id = 12`), Noop by default /// /// Sovereign account (ActorPalletId = `*b"actors00"`, SS58 prefix 42): /// hex: `0x791ec3fe30f34d005232cdf3bb5abdc0ae14e51fe3caeb62914d35f7c81ae544` /// SS58: `5EoWnoVuB925BHs9UwHUfLkcm5rSbmqzrHgFZRzY5nA4M5B6` - pub const BLDR_BUCKET_A_ACTORS_ID: u64 = 12; + pub const BLDR_ANCHOR_ACTORS_ID: u64 = 12; /// BLDR Treasury — receives 50% of minted $BLDR from Splitter /// Created at genesis (`actor_id = 13`), Noop by default diff --git a/template/primitives/src/tmctol.rs b/template/primitives/src/tmctol.rs index f45911a6..a1661f70 100644 --- a/template/primitives/src/tmctol.rs +++ b/template/primitives/src/tmctol.rs @@ -44,9 +44,9 @@ pub enum GuaranteeStatus { Deserialize, )] pub enum AnchorDomain { - /// Native TMCTOL anchor bucket (`actor_id = 3`). + /// Native TMCTOL Bucket A anchor (`actor_id = 3`). Tol, - /// BLDR TMCTOL anchor bucket (`actor_id = 12`). + /// BLDR Anchor (`actor_id = 12`). Bldr, } @@ -96,16 +96,16 @@ pub enum TmctolConformanceStatus { Violated, } -/// Bounded read projection for a deterministic custody-only Bucket A anchor. +/// Bounded read projection for a sealed Immutable System Actor Anchor. #[derive(Clone, Debug, Decode, DecodeWithMemTracking, Encode, Eq, PartialEq, TypeInfo)] -pub struct AnchorBucketState { +pub struct AnchorState { pub domain: AnchorDomain, pub actor_id: u64, pub status: GuaranteeStatus, pub sovereign_account: AccountId, - pub is_custody_only: bool, + pub is_immutable_system_actor: bool, pub actor_identity_exists: bool, - pub scheduler_state_exists: bool, + pub active_state_exists: bool, } /// Bounded read projection for the current anchor pool behind a domain. @@ -179,8 +179,8 @@ pub struct ReportedFloorInputs { /// Complete bounded TMCTOL guarantee-state projection for the reference runtime. #[derive(Clone, Debug, Decode, DecodeWithMemTracking, Encode, Eq, PartialEq, TypeInfo)] pub struct TmctolGuaranteeState { - pub tol_anchor: AnchorBucketState, - pub bldr_anchor: AnchorBucketState, + pub tol_anchor: AnchorState, + pub bldr_anchor: AnchorState, pub tol_pool: PoolProjection, pub bldr_pool: PoolProjection, pub native_floor_inputs: ReportedFloorInputs, diff --git a/template/runtime/Cargo.toml b/template/runtime/Cargo.toml index 91f896b8..76f587c6 100644 --- a/template/runtime/Cargo.toml +++ b/template/runtime/Cargo.toml @@ -21,7 +21,9 @@ substrate-wasm-builder = { optional = true, workspace = true, default-features = [dev-dependencies] cumulus-primitives-parachain-inherent = "0.26.0" +cumulus-primitives-proof-size-hostfunction = "=0.20.0" cumulus-test-relay-sproof-builder = "0.27.0" +sc-executor = "=0.50.0" [dependencies] codec = { workspace = true } diff --git a/template/runtime/examples/actor_cost_vectors.rs b/template/runtime/examples/actor_cost_vectors.rs index 02835743..be20546d 100644 --- a/template/runtime/examples/actor_cost_vectors.rs +++ b/template/runtime/examples/actor_cost_vectors.rs @@ -149,7 +149,7 @@ fn prefund_user_sovereign(owner: &AccountId, steps: &RuntimeSteps) { } fn create_user_vector( - name: &'static str, + name: &str, owner_byte: u8, trigger: RuntimeTrigger, steps: RuntimeSteps, @@ -295,10 +295,16 @@ fn manifest() -> Value { transfer_steps(8, &destination), ), create_user_vector( - "user-manual-32", + &format!( + "user-manual-{}", + ::MaxContractSteps::get() + ), 5, Trigger::Manual, - transfer_steps(32, &destination), + transfer_steps( + ::MaxContractSteps::get() as usize, + &destination, + ), ), create_user_vector( "user-address-event-1", diff --git a/template/runtime/examples/export_metadata.rs b/template/runtime/examples/export_metadata.rs index 84c08f8a..049473f4 100644 --- a/template/runtime/examples/export_metadata.rs +++ b/template/runtime/examples/export_metadata.rs @@ -1,7 +1,25 @@ -use deos_runtime::Runtime; -use polkadot_sdk::{sp_core::storage::Storage, sp_io::TestExternalities}; +use deos_runtime::{PARACHAIN_ID, ParachainInfoConfig, Runtime, RuntimeGenesisConfig}; +use polkadot_sdk::{ + cumulus_primitives_core::ParaId, sp_core::storage::Storage, sp_io::TestExternalities, + sp_runtime::BuildStorage, +}; use std::{env, path::PathBuf, process}; +fn canonical_metadata_storage() -> Result { + // Metadata constants such as ParachainSystem::SelfParaId and + // PolkadotXcm::UniversalLocation are storage-backed. Export under the same + // canonical parachain identity used by every reference preset rather than + // silently encoding the empty-externalities default (ParaId 100). + RuntimeGenesisConfig { + parachain_info: ParachainInfoConfig { + parachain_id: ParaId::from(PARACHAIN_ID), + ..Default::default() + }, + ..Default::default() + } + .build_storage() +} + fn usage() { eprintln!("Usage: export_metadata [metadata-version]"); } @@ -23,7 +41,11 @@ fn main() { }) }) .unwrap_or(16); - let mut ext = TestExternalities::new(Storage::default()); + let storage = canonical_metadata_storage().unwrap_or_else(|error| { + eprintln!("Failed to build canonical metadata externalities: {error}"); + process::exit(1); + }); + let mut ext = TestExternalities::new(storage); let metadata = ext.execute_with(|| { Runtime::metadata_at_version(metadata_version).unwrap_or_else(|| { eprintln!("Runtime metadata version {metadata_version} is unavailable"); @@ -51,3 +73,21 @@ fn main() { output_path.display() ); } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn metadata_externalities_use_the_reference_parachain_identity() { + let mut ext = TestExternalities::new( + canonical_metadata_storage().expect("canonical metadata storage builds"), + ); + ext.execute_with(|| { + assert_eq!( + deos_runtime::ParachainInfo::parachain_id(), + ParaId::from(PARACHAIN_ID) + ); + }); + } +} diff --git a/template/runtime/src/apis.rs b/template/runtime/src/apis.rs index d2766eaf..71c1ece6 100644 --- a/template/runtime/src/apis.rs +++ b/template/runtime/src/apis.rs @@ -198,7 +198,7 @@ impl_runtime_apis! { impl pallet_deos_actors::ActorSimulationApi< Block, pallet_deos_actors::ActorContractOf, - pallet_deos_actors::SimulationResultOf, + pallet_deos_actors::SimulationResult, > for Runtime { fn simulate_current_contract( actor_id: pallet_deos_actors::ActorId, @@ -206,13 +206,15 @@ impl_runtime_apis! { expected_mutability: pallet_deos_actors::Mutability, expected_contract: pallet_deos_actors::ActorContractOf, mode: pallet_deos_actors::SimulationMode, - ) -> Result, pallet_deos_actors::SimulationError> { + budget: pallet_deos_actors::SimulationBudget, + ) -> Result { Actors::simulate_current_contract( actor_id, expected_type, expected_mutability, expected_contract, mode, + budget, ) } } diff --git a/template/runtime/src/configs/actor_config.rs b/template/runtime/src/configs/actor_config.rs index 5f1c4829..6bc52bbc 100644 --- a/template/runtime/src/configs/actor_config.rs +++ b/template/runtime/src/configs/actor_config.rs @@ -43,10 +43,13 @@ parameter_types! { // --- Execution-plan and task bounds --- - pub const ActorMaxContractSteps: u32 = 32; + /// Independent structural ceilings, not a Cartesian admission promise. Creation derives the + /// authored Contract geometry and admits it only when its complete per-Contract resource + /// envelope fits the guaranteed Actor service floor. + pub const ActorMaxContractSteps: u32 = 12; pub const ActorMaxFundingTrackedAssets: u32 = 40; - pub const ActorMaxOpeningSnapshotEntries: u32 = 64; - pub const ActorMaxOpeningPredicateResults: u32 = 128; + pub const ActorMaxOpeningSnapshotEntries: u32 = 24; + pub const ActorMaxOpeningPredicateResults: u32 = 48; pub const ActorMaxPreconditionClauses: u32 = 4; pub const ActorMaxPredicatesPerClause: u32 = 4; pub const ActorMaxPredicatesPerStep: u32 = 4; @@ -71,7 +74,7 @@ parameter_types! { pub const ActorWakeupPageSize: u32 = 32; /// Broad ObservationChange subscriber/fanout page granularity. pub const ActorObservationPageSize: u32 = 64; - /// ObservationCrossing membership page granularity selected by EXP-0008/EXP-0009. + /// ObservationCrossing membership page granularity selected by bounded observation fanout requirements. pub const ActorCrossingPageSize: u32 = 128; pub const ActorMaxCrossingTransitionsPerFeed: u32 = 64; pub const ActorMaxCrossingMembersPerFeed: u32 = 10_000; @@ -165,6 +168,12 @@ impl Get for ActorFeeRecipient { pub struct RuntimeStepControlWeight; +/// Whether a generated control envelope already covers invocation-receipt deposition. +enum InvocationReceiptCoverage { + Separate, + Included, +} + impl RuntimeStepControlWeight { fn component_max(left: Weight, right: Weight) -> Weight { Weight::from_parts( @@ -180,6 +189,73 @@ impl RuntimeStepControlWeight { weight(value) } } + + fn current_predicate_weight(units: u32) -> Weight { + type ControlWeights = crate::weights::pallet_deos_actors::SubstrateWeight; + let count = units.min(ActorMaxPredicatesPerStep::get()); + if count == 0 { + return Weight::zero(); + } + let weight = Self::component_max( + ControlWeights::predicate_set_evaluation(count), + ControlWeights::predicate_asset_evaluation(count), + ); + if count >= 2 { + Self::component_max( + weight, + ControlWeights::predicate_observation_heavy_evaluation(count - 1), + ) + } else { + weight + } + } + + fn opening_snapshot_models(entries: u32) -> [Weight; 3] { + type ControlWeights = crate::weights::pallet_deos_actors::SubstrateWeight; + let single = ActorMaxOpeningSnapshotEntries::get().min(ActorMaxContractSteps::get()); + let share_mixed = ActorMaxOpeningSnapshotEntries::get().min( + ActorMaxContractSteps::get() + .saturating_mul(2) + .saturating_sub(1), + ); + [ + ControlWeights::opening_snapshot_capture(entries), + ControlWeights::opening_target_snapshot_capture(entries.min(single)), + ControlWeights::opening_share_mixed_capture(entries.min(share_mixed)), + ] + } + + fn opening_snapshot_weight(entries: u32) -> Weight { + type ControlWeights = crate::weights::pallet_deos_actors::SubstrateWeight; + if entries == 0 { + ControlWeights::opening_snapshot_traversal() + } else { + Self::opening_snapshot_models(entries) + .into_iter() + .fold(Weight::zero(), Self::component_max) + } + } + + fn opening_predicate_models(results: u32) -> [Option; 3] { + type ControlWeights = crate::weights::pallet_deos_actors::SubstrateWeight; + [ + Some(ControlWeights::opening_predicate_capture(results)), + Some(ControlWeights::opening_max_encoded_balance_capture(results)), + (results >= 2).then(|| ControlWeights::opening_observation_heavy_capture(results - 1)), + ] + } + + fn opening_predicate_weight(results: u32) -> Weight { + type ControlWeights = crate::weights::pallet_deos_actors::SubstrateWeight; + if results == 0 { + ControlWeights::opening_predicate_traversal() + } else { + Self::opening_predicate_models(results) + .into_iter() + .flatten() + .fold(Weight::zero(), Self::component_max) + } + } } impl StepControlWeightProvider> for RuntimeStepControlWeight { @@ -258,172 +334,48 @@ impl StepControlWeightProvider> for RuntimeS }) }) .collect::>(); + let maximum_tail_opening_amount_entries = ActorMaxContractSteps::get() + .saturating_sub(1) + .saturating_mul(2) + .min(ActorMaxOpeningSnapshotEntries::get()); + let maximum_current_predicates = ActorMaxPredicatesPerStep::get(); + let maximum_opening_head_current_predicates = maximum_current_predicates.saturating_sub(1); let suspended_head_retry = [ - ControlWeights::scheduler_inner_suspended_head_retry(0, 0, 0, 0), + ControlWeights::scheduler_inner_suspended_head_retry(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(), + maximum_tail_opening_amount_entries, + maximum_current_predicates, ), ]; 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(), - ), + ControlWeights::scheduler_inner_suspended_head_complete(0), + ControlWeights::scheduler_inner_suspended_head_complete(maximum_current_predicates), ]; let suspended_head_progress = [ - ControlWeights::scheduler_inner_suspended_head_progress(0, 0, 0, 0), + ControlWeights::scheduler_inner_suspended_head_progress(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(), + maximum_tail_opening_amount_entries, + maximum_current_predicates, ), ]; 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(0, 0), ControlWeights::scheduler_inner_suspended_head_opening_retry( - ActorMaxOpeningSnapshotEntries::get(), - ActorMaxOpeningPredicateResults::get(), - ActorMaxFundingTrackedAssets::get(), + maximum_tail_opening_amount_entries, + maximum_opening_head_current_predicates, ), ]; let suspended_head_opening_complete = [ - ControlWeights::scheduler_inner_suspended_head_opening_complete(0, 1, 0), + ControlWeights::scheduler_inner_suspended_head_opening_complete(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(), + maximum_opening_head_current_predicates, ), ]; 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, 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(), + maximum_tail_opening_amount_entries, + maximum_opening_head_current_predicates, ), ]; let opening_tail_chunks = (0..=ActorMaxContractSteps::get() @@ -431,22 +383,38 @@ impl StepControlWeightProvider> for RuntimeS .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) + let opening_snapshot = (0..=ActorMaxOpeningSnapshotEntries::get()) + .map(|count| { + ( + Self::opening_snapshot_weight(count), + (count > 0).then(|| Self::opening_snapshot_models(count)), + ) + }) .collect::>(); - let opening_predicates = (1..=ActorMaxOpeningPredicateResults::get()) - .map(ControlWeights::opening_predicate_capture) + let opening_predicates = (0..=ActorMaxOpeningPredicateResults::get()) + .map(|count| { + ( + Self::opening_predicate_weight(count), + (count > 0).then(|| Self::opening_predicate_models(count)), + ) + }) .collect::>(); let funding = (1..=ActorMaxFundingTrackedAssets::get()) .map(ControlWeights::funding_snapshot_open) .collect::>(); let current_predicates = (1..=ActorMaxPredicatesPerStep::get()) - .map(ControlWeights::predicate_set_evaluation) + .map(|count| { + ( + ControlWeights::predicate_set_evaluation(count), + ControlWeights::predicate_asset_evaluation(count), + (count >= 2).then(|| ControlWeights::predicate_observation_heavy_evaluation(count - 1)), + ) + }) .collect::>(); Some( ( *b"DEOS_ACTOR_STEP_CONTROL_WEIGHT", - *b"DEOS_ACTOR_CONTROL_ACTUAL_V2", + *b"DEOS_ACTOR_CONTROL_ACTUAL_V10", ( ControlWeights::scheduler_paged_tombstone_drain(1), ControlWeights::scheduler_actor_state_probe(), @@ -489,6 +457,8 @@ impl StepControlWeightProvider> for RuntimeS ( ControlWeights::scheduler_wakeup_append_new_page(), ControlWeights::close_actor(), + ControlWeights::fee_collection(), + ControlWeights::action_invocation_receipt(), ), ) .using_encoded(polkadot_sdk::sp_io::hashing::blake2_256), @@ -496,6 +466,103 @@ impl StepControlWeightProvider> for RuntimeS } fn maximum_control_weight( + context: pallet_deos_actors::StepControlWeightContext, + step: &pallet_deos_actors::StepOf, + ) -> Option { + Self::maximum_control_without_collection(context, step)? + .checked_add(&Self::action_collection_allowance(step)) + } + + fn actual_control_weight( + context: pallet_deos_actors::StepControlWeightContext, + step: &pallet_deos_actors::StepOf, + maximum: Weight, + execution: StepControlExecution, + ) -> Option { + let allowance = Self::action_collection_allowance(step); + let receipt = Self::action_receipt_allowance(step); + let invoked = execution.task_effect == pallet_deos_actors::TaskEffectExecution::Invoked; + if execution.action_fee_collected && (allowance == Weight::zero() || !invoked) { + return None; + } + if execution.phase == StepControlPhase::Opening + && Self::maximum_control_weight(context, step)? != maximum + { + return None; + } + let (base, receipt_coverage) = + Self::base_actual_control_weight(context, step, maximum.checked_sub(&allowance)?, execution)?; + let receipt = match receipt_coverage { + InvocationReceiptCoverage::Separate if invoked => receipt, + InvocationReceiptCoverage::Separate | InvocationReceiptCoverage::Included => Weight::zero(), + }; + let actual = base + .checked_add(&receipt)? + .checked_add(&if execution.action_fee_collected { + allowance + } else { + Weight::zero() + })?; + actual.all_lte(maximum).then_some(actual) + } +} + +impl RuntimeStepControlWeight { + fn action_receipt_allowance(step: &pallet_deos_actors::StepOf) -> Weight { + if matches!(step.task, Task::StopCycle) && step.precondition.is_none() { + Weight::zero() + } else { + crate::weights::pallet_deos_actors::SubstrateWeight::::action_invocation_receipt() + } + } + + fn action_collection_allowance(step: &pallet_deos_actors::StepOf) -> Weight { + if matches!(step.task, Task::StopCycle) { + Weight::zero() + } else { + crate::weights::pallet_deos_actors::SubstrateWeight::::fee_collection() + } + } + + fn maximal_opening_completion(context: pallet_deos_actors::StepControlWeightContext) -> bool { + let steps = 1u32 + .saturating_add( + context + .opening_tail_chunks + .saturating_mul(pallet_deos_actors::MAX_STEPS_PER_TAIL_CHUNK), + ) + .min(ActorMaxContractSteps::get()); + context.cursor == 0 + && context.predicate_evaluation_units == ActorMaxPredicatesPerStep::get().saturating_mul(2) + && context.opening_snapshot_entries + == steps + .saturating_sub(1) + .saturating_mul(2) + .min(ActorMaxOpeningSnapshotEntries::get()) + && context.opening_predicate_results + == steps + .saturating_mul(ActorMaxPredicatesPerStep::get()) + .min(ActorMaxOpeningPredicateResults::get()) + && context.funding_snapshot_entries == ActorMaxFundingTrackedAssets::get() + } + + fn maximum_control_without_collection( + context: pallet_deos_actors::StepControlWeightContext, + step: &pallet_deos_actors::StepOf, + ) -> Option { + let separate = Self::base_maximum_control_weight(context, step)? + .checked_add(&Self::action_receipt_allowance(step))?; + // This direct profile executes predicated StopCycle and already deposits its receipt. + // Compare complete envelopes; never subtract an isolated measurement from an aggregate. + Some(if Self::maximal_opening_completion(context) { + let complete = crate::weights::pallet_deos_actors::SubstrateWeight::::scheduler_inner_opening_complete_max(context.opening_tail_chunks); + Self::component_max(separate, complete) + } else { + separate + }) + } + + fn base_maximum_control_weight( context: pallet_deos_actors::StepControlWeightContext, _: &pallet_deos_actors::StepOf, ) -> Option { @@ -520,12 +587,6 @@ impl StepControlWeightProvider> for RuntimeS && 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; @@ -546,27 +607,21 @@ impl StepControlWeightProvider> for RuntimeS 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 opening_snapshot = if context.cursor > 0 && context.opening_snapshot_entries == 0 { + Weight::zero() + } else { + Self::opening_snapshot_weight(context.opening_snapshot_entries) + }; + let opening_predicates = if context.cursor > 0 && context.opening_predicate_results == 0 { + Weight::zero() + } else { + Self::opening_predicate_weight(context.opening_predicate_results) + }; 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 current_predicates = Self::current_predicate_weight(context.predicate_evaluation_units); let commit = Self::component_max( Self::component_max( ControlWeights::run_progress(), @@ -609,24 +664,23 @@ impl StepControlWeightProvider> for RuntimeS .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 maximal_opening_completion = Self::maximal_opening_completion(context); + let user_completion = if context.opening_tail_chunks == 0 { + ControlWeights::scheduler_inner_opening_user_complete_header_max() + } else { + ControlWeights::scheduler_inner_opening_user_complete_header_max_tail( + context.opening_tail_chunks, + ) + }; 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), + Self::component_max( + ControlWeights::scheduler_inner_opening_complete_min(context.opening_tail_chunks), + user_completion, + ), ), ); let direct = if context.opening_tail_chunks == 0 { @@ -641,17 +695,21 @@ impl StepControlWeightProvider> for RuntimeS } else if maximal_opening { Self::component_max( composed, - ControlWeights::scheduler_inner_opening_progress_max(context.opening_tail_chunks), + Self::component_max( + ControlWeights::scheduler_inner_opening_progress_max(context.opening_tail_chunks), + if maximum_opening { + ControlWeights::scheduler_paged_execute_opening_max() + } else { + Weight::zero() + }, + ), ) } 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), - ), + ControlWeights::scheduler_inner_opening_retry_max(context.opening_tail_chunks), ), ) } else { @@ -659,23 +717,20 @@ impl StepControlWeightProvider> for RuntimeS }; if context.cursor == 0 && context.predicate_evaluation_units <= ActorMaxPredicatesPerStep::get() { + let maximum_tail_opening_amount_entries = ActorMaxContractSteps::get() + .saturating_sub(1) + .saturating_mul(2) + .min(ActorMaxOpeningSnapshotEntries::get()); + let maximum_current_predicates = 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, + maximum_tail_opening_amount_entries, + maximum_current_predicates, ); + let complete = + ControlWeights::scheduler_inner_suspended_head_complete(maximum_current_predicates); let progress = ControlWeights::scheduler_inner_suspended_head_progress( - context.opening_snapshot_entries, - context.opening_predicate_results, - context.funding_snapshot_entries, - context.predicate_evaluation_units, + maximum_tail_opening_amount_entries, + maximum_current_predicates, ); return Some(Self::component_max( composed, @@ -686,20 +741,20 @@ impl StepControlWeightProvider> for RuntimeS && context.predicate_evaluation_units > ActorMaxPredicatesPerStep::get() && context.predicate_evaluation_units <= maximum_evaluation_units { + let maximum_tail_opening_amount_entries = ActorMaxContractSteps::get() + .saturating_sub(1) + .saturating_mul(2) + .min(ActorMaxOpeningSnapshotEntries::get()); + let maximum_current_predicates = ActorMaxPredicatesPerStep::get().saturating_sub(1); 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, + maximum_tail_opening_amount_entries, + maximum_current_predicates, ); + let opening_complete = + ControlWeights::scheduler_inner_suspended_head_opening_complete(maximum_current_predicates); let opening_progress = ControlWeights::scheduler_inner_suspended_head_opening_progress( - context.opening_snapshot_entries, - context.opening_predicate_results, - context.funding_snapshot_entries, + maximum_tail_opening_amount_entries, + maximum_current_predicates, ); return Some(Self::component_max( composed, @@ -756,17 +811,16 @@ impl StepControlWeightProvider> for RuntimeS Some(composed) } - fn actual_control_weight( + fn base_actual_control_weight( context: pallet_deos_actors::StepControlWeightContext, step: &pallet_deos_actors::StepOf, maximum: Weight, execution: StepControlExecution, - ) -> Option { + ) -> Option<(Weight, InvocationReceiptCoverage)> { 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; - } + Self::base_maximum_control_weight(context, step)?; + let separate = |weight| Some((weight, InvocationReceiptCoverage::Separate)); + let included = |weight| Some((weight, InvocationReceiptCoverage::Included)); let maximum_evaluation_units = ActorMaxPredicatesPerStep::get().checked_mul(2)?; let maximum_opening_tail_chunks = ActorMaxContractSteps::get() .saturating_sub(1) @@ -786,7 +840,7 @@ impl StepControlWeightProvider> for RuntimeS && context.opening_snapshot_entries == 0 && context.opening_predicate_results == 0 { - return Some(ControlWeights::scheduler_inner_opening_failed_min( + return separate(ControlWeights::scheduler_inner_opening_failed_min( context.opening_tail_chunks, )); } @@ -798,7 +852,7 @@ impl StepControlWeightProvider> for RuntimeS && context.opening_snapshot_entries == 0 && context.opening_predicate_results == 0 { - return Some(ControlWeights::scheduler_inner_opening_retry_min( + return separate(ControlWeights::scheduler_inner_opening_retry_min( context.opening_tail_chunks, )); } @@ -810,9 +864,13 @@ impl StepControlWeightProvider> for RuntimeS && context.opening_snapshot_entries == 0 && context.opening_predicate_results == 0 { - return Some(ControlWeights::scheduler_inner_opening_complete_min( - context.opening_tail_chunks, - )); + return separate(if context.opening_tail_chunks == 0 { + ControlWeights::scheduler_inner_opening_user_complete_header_max() + } else { + ControlWeights::scheduler_inner_opening_user_complete_header_max_tail( + context.opening_tail_chunks, + ) + }); } if execution.phase == StepControlPhase::Opening && execution.outcome == StepControlOutcome::Continued @@ -831,7 +889,7 @@ impl StepControlWeightProvider> for RuntimeS && context.opening_snapshot_entries == 0 && context.opening_predicate_results == 0 { - return Some(ControlWeights::scheduler_inner_opening_progress_min( + return separate(ControlWeights::scheduler_inner_opening_progress_min( context.opening_tail_chunks, )); } @@ -846,9 +904,9 @@ impl StepControlWeightProvider> for RuntimeS .min(ActorMaxOpeningPredicateResults::get()) && context.funding_snapshot_entries == ActorMaxFundingTrackedAssets::get() { - return Some(ControlWeights::scheduler_inner_opening_progress_max( - context.opening_tail_chunks, - )); + // The direct profile does not bound independent amount/predicate sources. + // Retain the admitted composed envelope until complete-path coverage is regenerated. + return included(maximum); } } if execution.phase == StepControlPhase::Opening @@ -875,7 +933,7 @@ impl StepControlWeightProvider> for RuntimeS .min(ActorMaxOpeningPredicateResults::get()) && context.funding_snapshot_entries == ActorMaxFundingTrackedAssets::get() { - return Some(ControlWeights::scheduler_inner_opening_failed_max( + return separate(ControlWeights::scheduler_inner_opening_failed_max( context.opening_tail_chunks, )); } @@ -890,166 +948,64 @@ impl StepControlWeightProvider> for RuntimeS .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, - )); + .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 separate(ControlWeights::scheduler_inner_opening_retry_max( + context.opening_tail_chunks, + )); + } } - if execution.phase == StepControlPhase::Suspended + if execution.phase == StepControlPhase::Opening && execution.outcome == StepControlOutcome::Completed && execution.placement == StepControlPlacement::None - && context.cursor == 0 - && context.opening_tail_chunks == 0 - && context.predicate_evaluation_units <= ActorMaxPredicatesPerStep::get() + && Self::maximal_opening_completion(context) { - return Some(ControlWeights::scheduler_inner_suspended_head_complete( - context.opening_snapshot_entries, - context.opening_predicate_results, - context.funding_snapshot_entries, - context.predicate_evaluation_units, + return included(ControlWeights::scheduler_inner_opening_complete_max( + context.opening_tail_chunks, )); } - if execution.phase == StepControlPhase::Suspended - && execution.outcome == StepControlOutcome::Suspended - && execution.placement == StepControlPlacement::Wakeup - && context.cursor == 0 + if maximum_opening && execution.phase == StepControlPhase::Opening { + return included(maximum); + } + if execution.phase == StepControlPhase::Running + && 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.outcome == StepControlOutcome::Completed + && execution.placement == StepControlPlacement::None + { + return separate(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 separate(ControlWeights::scheduler_inner_running_progress( + context.steps_in_fragment, + context.predicate_evaluation_units, + )); + } + } + if execution.phase == StepControlPhase::Suspended && context.cursor == 0 { + // The generated head profiles deliberately model coupled retained-source fixtures rather + // than independent Opening/result/funding axes. Without Actor type and exact profile + // identity in this context, refund only to the admitted complete envelope. + return included(maximum); } if execution.phase == StepControlPhase::Suspended && execution.outcome == StepControlOutcome::Completed @@ -1057,7 +1013,7 @@ impl StepControlWeightProvider> for RuntimeS && context.cursor > 0 && context.predicate_evaluation_units <= ActorMaxPredicatesPerStep::get() { - return Some(ControlWeights::scheduler_inner_suspended_tail_complete( + return separate(ControlWeights::scheduler_inner_suspended_tail_complete( context.steps_in_fragment, context.predicate_evaluation_units, )); @@ -1069,7 +1025,7 @@ impl StepControlWeightProvider> for RuntimeS && context.steps_in_fragment >= 2 && context.predicate_evaluation_units <= ActorMaxPredicatesPerStep::get() { - return Some(ControlWeights::scheduler_inner_suspended_tail_progress( + return separate(ControlWeights::scheduler_inner_suspended_tail_progress( context.steps_in_fragment, context.predicate_evaluation_units, )); @@ -1080,7 +1036,7 @@ impl StepControlWeightProvider> for RuntimeS && context.cursor > 0 && context.predicate_evaluation_units <= ActorMaxPredicatesPerStep::get() { - return Some(ControlWeights::scheduler_inner_suspended_tail_retry( + return separate(ControlWeights::scheduler_inner_suspended_tail_retry( context.steps_in_fragment, context.predicate_evaluation_units, )); @@ -1105,18 +1061,12 @@ impl StepControlWeightProvider> for RuntimeS .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, - ) + Self::opening_snapshot_weight(context.opening_snapshot_entries) } else { Weight::zero() }; let opening_predicates = if execution.phase == StepControlPhase::Opening { - Self::nonzero_parameterized( - context.opening_predicate_results, - ControlWeights::opening_predicate_capture, - ) + Self::opening_predicate_weight(context.opening_predicate_results) } else { Weight::zero() }; @@ -1128,12 +1078,7 @@ impl StepControlWeightProvider> for RuntimeS } else { Weight::zero() }; - let current_predicates = Self::nonzero_parameterized( - context - .predicate_evaluation_units - .min(ActorMaxPredicatesPerStep::get()), - ControlWeights::predicate_set_evaluation, - ); + let current_predicates = Self::current_predicate_weight(context.predicate_evaluation_units); let commit = match execution.outcome { StepControlOutcome::Continued => ControlWeights::run_progress(), StepControlOutcome::Suspended => ControlWeights::run_suspend(), @@ -1144,7 +1089,7 @@ impl StepControlWeightProvider> for RuntimeS StepControlPlacement::Queue => ControlWeights::scheduler_paged_append_new_page(), StepControlPlacement::Wakeup => ControlWeights::scheduler_wakeup_append_new_page(), }; - Some( + separate( plan .saturating_add(opening_tail) .saturating_add(opening_snapshot) @@ -2018,7 +1963,7 @@ const SYSTEM_ACTOR_TOPOLOGY_IDS: [pallet_deos_actors::ActorId; 15] = [ ecosystem::actor_ids::TREASURY_D_ACTORS_ID, ecosystem::actor_ids::BLDR_SPLITTER_ACTORS_ID, ecosystem::actor_ids::BLDR_LIQUIDITY_ACTOR_ID, - ecosystem::actor_ids::BLDR_BUCKET_A_ACTORS_ID, + ecosystem::actor_ids::BLDR_ANCHOR_ACTORS_ID, ecosystem::actor_ids::BLDR_TREASURY_ACTORS_ID, ecosystem::actor_ids::NATIVE_STAKING_LIQUIDITY_ACTOR_ID, ]; @@ -2026,7 +1971,7 @@ const SYSTEM_ACTOR_TOPOLOGY_IDS: [pallet_deos_actors::ActorId; 15] = [ const SYSTEM_ACTIVATION_MANIFEST_EDGES: [( pallet_deos_actors::ActorId, pallet_deos_actors::ActorId, -); 12] = [ +); 11] = [ ( ecosystem::actor_ids::FEE_SINK_ACTORS_ID, ecosystem::actor_ids::NATIVE_STAKING_LIQUIDITY_ACTOR_ID, @@ -2069,11 +2014,7 @@ const SYSTEM_ACTIVATION_MANIFEST_EDGES: [( ), ( ecosystem::actor_ids::BLDR_LIQUIDITY_ACTOR_ID, - ecosystem::actor_ids::BLDR_BUCKET_A_ACTORS_ID, - ), - ( - ecosystem::actor_ids::BLDR_BUCKET_A_ACTORS_ID, - ecosystem::actor_ids::BLDR_TREASURY_ACTORS_ID, + ecosystem::actor_ids::BLDR_ANCHOR_ACTORS_ID, ), ]; @@ -2520,30 +2461,66 @@ impl ] } - fn system_custody_accounts() -> alloc::vec::Vec { - alloc::vec![ - ecosystem::actor_ids::TOL_BUCKET_A_ACTORS_ID, - ecosystem::actor_ids::BLDR_BUCKET_A_ACTORS_ID, - ] - } - - fn dormant_system_actors() -> alloc::vec::Vec<(pallet_deos_actors::ActorId, AccountId)> { + fn dormant_system_actors() -> alloc::vec::Vec<( + pallet_deos_actors::ActorId, + AccountId, + pallet_deos_actors::Mutability, + )> { + use pallet_deos_actors::Mutability; use polkadot_sdk::sp_runtime::traits::AccountIdConversion; let governance: AccountId = ActorsPalletId::get().into_account_truncating(); alloc::vec![ - ecosystem::actor_ids::LIQUIDITY_ACTOR_ACTORS_ID, - ecosystem::actor_ids::TOL_BUCKET_B_ACTORS_ID, - ecosystem::actor_ids::TOL_BUCKET_C_ACTORS_ID, - ecosystem::actor_ids::TOL_BUCKET_D_ACTORS_ID, - ecosystem::actor_ids::TREASURY_B_ACTORS_ID, - ecosystem::actor_ids::TREASURY_C_ACTORS_ID, - ecosystem::actor_ids::TREASURY_D_ACTORS_ID, - ecosystem::actor_ids::BLDR_LIQUIDITY_ACTOR_ID, - ecosystem::actor_ids::BLDR_TREASURY_ACTORS_ID, - ecosystem::actor_ids::NATIVE_STAKING_LIQUIDITY_ACTOR_ID, + ( + ecosystem::actor_ids::TOL_BUCKET_A_ACTORS_ID, + Mutability::Immutable, + ), + ( + ecosystem::actor_ids::LIQUIDITY_ACTOR_ACTORS_ID, + Mutability::Mutable, + ), + ( + ecosystem::actor_ids::TOL_BUCKET_B_ACTORS_ID, + Mutability::Mutable, + ), + ( + ecosystem::actor_ids::TOL_BUCKET_C_ACTORS_ID, + Mutability::Mutable, + ), + ( + ecosystem::actor_ids::TOL_BUCKET_D_ACTORS_ID, + Mutability::Mutable, + ), + ( + ecosystem::actor_ids::TREASURY_B_ACTORS_ID, + Mutability::Mutable, + ), + ( + ecosystem::actor_ids::TREASURY_C_ACTORS_ID, + Mutability::Mutable, + ), + ( + ecosystem::actor_ids::TREASURY_D_ACTORS_ID, + Mutability::Mutable, + ), + ( + ecosystem::actor_ids::BLDR_LIQUIDITY_ACTOR_ID, + Mutability::Mutable, + ), + ( + ecosystem::actor_ids::BLDR_ANCHOR_ACTORS_ID, + Mutability::Immutable, + ), + ( + ecosystem::actor_ids::BLDR_TREASURY_ACTORS_ID, + Mutability::Mutable, + ), + ( + ecosystem::actor_ids::NATIVE_STAKING_LIQUIDITY_ACTOR_ID, + Mutability::Mutable, + ), ] .into_iter() - .map(|actor_id| (actor_id, governance.clone())) + .map(|(actor_id, mutability)| (actor_id, governance.clone(), mutability)) .collect() } @@ -2566,7 +2543,7 @@ impl "executable System Actor owner must be the non-signable Actors pallet account", ); } - for (_, owner) in Self::dormant_system_actors() { + for (_, owner, _) in Self::dormant_system_actors() { assert_eq!( owner, system_control_account, "dormant System Actor owner must be the non-signable Actors pallet account", @@ -2606,7 +2583,9 @@ impl assert!( Self::dormant_system_actors() .iter() - .any(|(actor_id, _)| *actor_id == ecosystem::actor_ids::NATIVE_STAKING_LIQUIDITY_ACTOR_ID), + .any(|(actor_id, _, _)| { + *actor_id == ecosystem::actor_ids::NATIVE_STAKING_LIQUIDITY_ACTOR_ID + }), "Fee Sink liquidity ingress must retain its System sovereign locator", ); let staking_pool = crate::Staking::pool_account_for(0); @@ -3061,7 +3040,7 @@ impl TmctolGenesisSystemActors { /// /// Contract steps: /// 1. AddLiquidity(NTVE, BLDR) — opportunistic at current pool ratio - /// 2. SplitTransfer(LP → BLDR Bucket A, 100%) + /// 2. Transfer(LP → BLDR Anchor, 100%) pub fn build_bldr_liquidity_contract_steps( bldr_asset: AssetKind, lp_asset: AssetKind, @@ -3086,8 +3065,8 @@ impl TmctolGenesisSystemActors { }, ]) }; - let bldr_bucket_a = pallet_deos_actors::Pallet::::sovereign_account_id_system( - ecosystem::actor_ids::BLDR_BUCKET_A_ACTORS_ID, + let bldr_anchor = pallet_deos_actors::Pallet::::sovereign_account_id_system( + ecosystem::actor_ids::BLDR_ANCHOR_ACTORS_ID, ); let steps: alloc::vec::Vec> = alloc::vec![ Step { @@ -3104,7 +3083,7 @@ impl TmctolGenesisSystemActors { Step { precondition: dust_guard(lp_asset), task: Task::Transfer { - to: bldr_bucket_a, + to: bldr_anchor, asset: lp_asset, amount: AmountResolution::AllAvailable, }, @@ -3488,6 +3467,49 @@ pub struct RuntimeActorsBenchmarkHelper; #[cfg(feature = "runtime-benchmarks")] impl RuntimeActorsBenchmarkHelper { + fn prepare_max_encoded_asset_accounts(owner: &AccountId, assets: &[AssetKind]) -> DispatchResult { + let depositor: AccountId = + polkadot_sdk::frame_benchmarking::account("predicate_depositor", 0, 0); + assert_ne!(&depositor, owner); + let deposit = ::AssetAccountDeposit::get(); + let funding = deposit + .checked_mul(assets.len() as Balance) + .and_then(|amount| amount.checked_add(EXISTENTIAL_DEPOSIT)) + .ok_or(DispatchError::Other("BenchmarkPredicateDepositOverflow"))?; + let _ = >::deposit_creating(&depositor, funding); + for asset in assets { + if let AssetKind::Local(id) = asset { + crate::Assets::touch_other( + RuntimeOrigin::signed(depositor.clone()), + *id, + polkadot_sdk::sp_runtime::MultiAddress::Id(owner.clone()), + )?; + } + } + Self::check_max_encoded_asset_accounts(owner, assets) + } + + fn check_max_encoded_asset_accounts(owner: &AccountId, assets: &[AssetKind]) -> DispatchResult { + use codec::MaxEncodedLen; + for asset in assets { + if let AssetKind::Local(id) = asset { + let details = pallet_assets::Asset::::get(id) + .ok_or(DispatchError::Other("BenchmarkPredicateAssetMissing"))?; + let account = pallet_assets::Account::::get(id, owner) + .ok_or(DispatchError::Other("BenchmarkPredicateAccountMissing"))?; + assert_eq!( + details.encoded_size(), + pallet_assets::AssetDetails::::max_encoded_len() + ); + assert_eq!( + account.encoded_size(), + pallet_assets::AssetAccount::::max_encoded_len() + ); + } + } + Ok(()) + } + fn ensure_local_asset(asset_id: u32, owner: &AccountId) -> Result<(), DispatchError> { if ! as FungiblesInspect>::asset_exists(asset_id) { pallet_assets::Pallet::::force_create( @@ -3792,6 +3814,125 @@ impl pallet_deos_actors::BenchmarkHelper Result, DispatchError> { + use pallet_deos_actors::adapters::StakingOps as _; + let assets: alloc::vec::Vec<_> = Self::setup_predicate_assets(owner, max + 1)? + .into_iter() + .filter(|asset| *asset != AssetKind::Native) + .take(max as usize) + .collect(); + assert_eq!(assets.len() as u32, max); + let amount = 1_000_000; + let mut receipts = alloc::vec::Vec::with_capacity(assets.len()); + for asset in &assets { + let AssetKind::Local(id) = asset else { + return Err(DispatchError::Other("BenchmarkLocalStakingAssetRequired")); + }; + as FungiblesMutate>::mint_into( + *id, + owner, + amount + 1, + )?; + crate::Staking::register_staking_asset(RuntimeOrigin::root(), *id)?; + receipts.push( + TmctolStakingOps::share_asset(*asset) + .ok_or(DispatchError::Other("BenchmarkStakingReceiptMissing"))?, + ); + } + Self::prepare_max_encoded_asset_accounts(owner, &receipts)?; + let positions = assets + .into_iter() + .map(|asset| { + TmctolStakingOps::stake(owner, asset, amount).map_err(|failure| failure.error)?; + let shares = TmctolStakingOps::share_balance(owner, asset); + if shares == 0 { + return Err(DispatchError::Other("BenchmarkStakingSharesMissing")); + } + Ok((asset, shares)) + }) + .collect::>()?; + Self::check_max_encoded_asset_accounts(owner, &receipts)?; + Ok(positions) + } + + fn set_asset_account_frozen( + owner: &AccountId, + who: &AccountId, + asset: AssetKind, + frozen: bool, + ) -> DispatchResult { + let AssetKind::Local(id) = asset else { + return Err(DispatchError::Other("UnsupportedAccountFreezeAsset")); + }; + let balance = crate::Assets::balance(id, who); + let issuance = + as FungiblesInspect>::total_issuance(id); + polkadot_sdk::frame_support::storage::with_transaction(|| { + let result = (|| -> DispatchResult { + if frozen { + crate::Assets::freeze(RuntimeOrigin::signed(owner.clone()), id, who.clone().into())?; + } else { + crate::Assets::thaw(RuntimeOrigin::signed(owner.clone()), id, who.clone().into())?; + } + if crate::Assets::balance(id, who) != balance + || as FungiblesInspect>::total_issuance(id) + != issuance + { + return Err(DispatchError::Other("AccountFreezeChangedCustody")); + } + if frozen && TmctolAssetOps::balance(who, asset) != 0 { + return Err(DispatchError::Other("FrozenAssetStillSpendable")); + } + Ok(()) + })(); + match result { + Ok(()) => polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(())), + Err(error) => { + polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)) + } + } + }) + } + + fn remove_empty_staking_receipt(owner: &AccountId, asset: AssetKind) -> DispatchResult { + if !matches!(asset, AssetKind::Native | AssetKind::Local(_)) { + return Err(DispatchError::Other("UnsupportedStakingReceiptAsset")); + } + let share_asset = || { + >::share_asset(asset) + }; + let Some(AssetKind::Local(receipt)) = share_asset() else { + return Err(DispatchError::Other("LiveStakingReceiptMissing")); + }; + if as FungiblesInspect>::total_issuance(receipt) != 0 + { + return Err(DispatchError::Other("StakingReceiptNotEmpty")); + } + polkadot_sdk::frame_support::storage::with_transaction(|| { + let result = (|| -> DispatchResult { + crate::Assets::start_destroy(RuntimeOrigin::root(), receipt)?; + crate::Assets::finish_destroy(RuntimeOrigin::signed(owner.clone()), receipt)?; + if share_asset().is_some() { + return Err(DispatchError::Other("StakingReceiptMappingRemains")); + } + Ok(()) + })(); + match result { + Ok(()) => polkadot_sdk::frame_support::storage::TransactionOutcome::Commit(Ok(())), + Err(error) => { + polkadot_sdk::frame_support::storage::TransactionOutcome::Rollback(Err(error)) + } + } + }) + } + fn setup_swap_exact_in( owner: &AccountId, ) -> Result<(AssetKind, AssetKind, Balance), DispatchError> { @@ -3836,6 +3977,54 @@ impl pallet_deos_actors::BenchmarkHelper DispatchResult { + use crate::configs::address_event_ingress::AddressEventIngressExtension; + use polkadot_sdk::frame_support::{dispatch::GetDispatchInfo, storage::TransactionOutcome}; + use polkadot_sdk::sp_runtime::traits::{Dispatchable, TransactionExtension}; + + let call = match asset { + AssetKind::Native => { + crate::RuntimeCall::Balances(pallet_balances::Call::transfer_allow_death { + dest: crate::Address::Id(recipient.clone()), + value: amount, + }) + } + AssetKind::Local(id) | AssetKind::Foreign(id) => { + crate::RuntimeCall::Assets(pallet_assets::Call::transfer { + id, + target: crate::Address::Id(recipient.clone()), + amount, + }) + } + }; + polkadot_sdk::frame_support::storage::with_transaction(|| { + let result = (|| { + let origin = RuntimeOrigin::signed(source.clone()); + let info = call.get_dispatch_info(); + let pre = AddressEventIngressExtension + .prepare((), &origin, &call, &info, 0) + .map_err(|_| DispatchError::Other("BenchmarkSignedTransferPreflight"))?; + let (post_info, result) = match call.dispatch(origin) { + Ok(post_info) => (post_info, Ok(())), + Err(error) => (error.post_info, Err(error.error)), + }; + AddressEventIngressExtension::post_dispatch_details(pre, &info, &post_info, 0, &result) + .map_err(|_| DispatchError::Other("BenchmarkSignedTransferConsequence"))?; + result + })(); + if result.is_ok() { + TransactionOutcome::Commit(result) + } else { + TransactionOutcome::Rollback(result) + } + }) + } + fn setup_predicate_assets( owner: &AccountId, max: u32, @@ -3849,6 +4038,15 @@ impl pallet_deos_actors::BenchmarkHelper Result, DispatchError> { + let assets = Self::setup_predicate_assets(owner, max)?; + Self::prepare_max_encoded_asset_accounts(owner, &assets)?; + Ok(assets) + } + fn setup_observation_feeds( max: u32, ) -> Result, DispatchError> { @@ -3865,6 +4063,147 @@ impl pallet_deos_actors::BenchmarkHelper Result, DispatchError> { + use codec::MaxEncodedLen; + let producer = crate::DeosRouter::account_id(); + let mut feeds = alloc::vec::Vec::with_capacity(max as usize); + for index in 1..=max { + let asset_in = AssetKind::Local(0x3000_0000u32.saturating_add(index)); + let asset_out = AssetKind::Local(0x3001_0000u32.saturating_add(index)); + crate::configs::oracle_config::ensure_deos_router_pool_feeds(asset_in, asset_out)?; + let feed = crate::configs::oracle_config::deos_router_pool_feed(asset_in, asset_out); + crate::Oracle::publish(RuntimeOrigin::signed(producer.clone()), feed, 1)?; + let config = pallet_oracle::Feeds::::get(feed) + .ok_or(DispatchError::Other("BenchmarkCaptureFeedMissing"))?; + let observation = pallet_oracle::Observations::::get(feed) + .ok_or(DispatchError::Other("BenchmarkCaptureObservationMissing"))?; + assert_eq!( + feed.encoded_size(), + primitives::OracleFeedId::max_encoded_len() + ); + assert_eq!( + config.encoded_size(), + pallet_oracle::FeedConfigOf::::max_encoded_len() + ); + assert_eq!( + observation.encoded_size(), + pallet_oracle::ObservationOf::::max_encoded_len() + ); + feeds.push(feed); + } + Ok(feeds) + } + + fn setup_observation_read_state( + max: u32, + state: pallet_deos_actors::BenchmarkObservationReadState, + max_age_blocks: u32, + ) -> Result, DispatchError> { + use codec::MaxEncodedLen; + use pallet_deos_actors::BenchmarkObservationReadState; + let mut feeds = Self::setup_max_encoded_observation_feeds(max)?; + match state { + BenchmarkObservationReadState::Missing => { + feeds = (1..=max) + .map(|index| { + crate::configs::oracle_config::deos_router_pool_feed( + AssetKind::Local(0x3002_0000u32.saturating_add(index)), + AssetKind::Local(0x3003_0000u32.saturating_add(index)), + ) + }) + .collect(); + for feed in &feeds { + assert_eq!( + feed.encoded_size(), + primitives::OracleFeedId::max_encoded_len() + ); + assert!(!pallet_oracle::Feeds::::contains_key(feed)); + } + } + BenchmarkObservationReadState::Deactivated => { + for feed in &feeds { + Self::deactivate_observation_feed(*feed)?; + } + } + BenchmarkObservationReadState::Uninitialized => { + for feed in &mut feeds { + *feed = feed.reverse(); + let config = pallet_oracle::Feeds::::get(*feed) + .ok_or(DispatchError::Other("BenchmarkCaptureReverseFeedMissing"))?; + assert_eq!( + config.encoded_size(), + pallet_oracle::FeedConfigOf::::max_encoded_len() + ); + assert!(!pallet_oracle::Observations::::contains_key(*feed)); + } + } + BenchmarkObservationReadState::Stale => { + let block = crate::System::block_number() + .checked_add(max_age_blocks) + .and_then(|block| block.checked_add(1)) + .ok_or(DispatchError::Other("BenchmarkObservationAgeOverflow"))?; + crate::System::set_block_number(block); + } + BenchmarkObservationReadState::Fresh => {} + } + Ok(feeds) + } + + fn contract_head_observation_feed() -> Result { + Ok(primitives::OracleFeedId::directional_local_pool_price( + AssetKind::Local(1), + AssetKind::Local(2), + primitives::LocalPoolObservationMethod::PreExecutionSpot, + primitives::OracleAggregationId::Ema { + half_life_blocks: 1, + }, + 8, + )) + } + + fn advance_to_scheduler_tick(tick: u64) -> DispatchResult { + let moment = tick + .checked_mul(ActorCadenceTickMillis::get()) + .ok_or(DispatchError::Other("BenchmarkClockOverflow"))?; + let slot = moment / crate::Aura::slot_duration(); + let block = crate::System::block_number() + .checked_add(1) + .ok_or(DispatchError::Other("BenchmarkBlockOverflow"))?; + let digest = polkadot_sdk::sp_runtime::Digest { + logs: alloc::vec![polkadot_sdk::sp_runtime::DigestItem::PreRuntime( + polkadot_sdk::sp_consensus_aura::AURA_ENGINE_ID, + slot.encode(), + )], + }; + // Initialize the block-header input; Aura's hook remains the CurrentSlot owner. + crate::System::initialize(&block, &crate::System::parent_hash(), &digest); + let _ = >::on_initialize( + crate::System::block_number(), + ); + crate::Timestamp::set(polkadot_sdk::frame_system::RawOrigin::None.into(), moment) + } + + fn finalize_scheduler_clock() -> DispatchResult { + >::on_finalize( + crate::System::block_number(), + ); + Ok(()) + } + + fn publish_observation(feed: primitives::OracleFeedId, sample: u128) -> DispatchResult { + crate::Oracle::publish( + RuntimeOrigin::signed(crate::DeosRouter::account_id()), + feed, + sample, + ) + } + + fn deactivate_observation_feed(feed: primitives::OracleFeedId) -> DispatchResult { + crate::Oracle::deactivate_feed(RuntimeOrigin::root(), feed) + } + fn setup_address_event_ingress( recipient: &AccountId, source: &AccountId, diff --git a/template/runtime/src/configs/assets_config.rs b/template/runtime/src/configs/assets_config.rs index 8cd49f6f..922a56ed 100644 --- a/template/runtime/src/configs/assets_config.rs +++ b/template/runtime/src/configs/assets_config.rs @@ -41,6 +41,32 @@ impl polkadot_sdk::frame_support::traits::EnsureOriginWithArg for AnchorLpFreezer { + fn frozen_balance(asset: AssetId, who: &AccountId) -> Option { + let is_lp = (asset & primitives::assets::MASK_TYPE) == primitives::assets::TYPE_LP; + let is_anchor = [ + primitives::ecosystem::actor_ids::TOL_BUCKET_A_ACTORS_ID, + primitives::ecosystem::actor_ids::BLDR_ANCHOR_ACTORS_ID, + ] + .into_iter() + .map(pallet_deos_actors::Pallet::::sovereign_account_id_system) + .any(|anchor| anchor == *who); + (is_lp && is_anchor).then_some(Balance::MAX) + } + + fn died(_: AssetId, _: &AccountId) {} + + fn contains_freezes(asset: AssetId) -> bool { + (asset & primitives::assets::MASK_TYPE) == primitives::assets::TYPE_LP + } +} + /// Converter to distinguish between native and asset tokens pub struct NativeOrAssetIdConverter; @@ -140,7 +166,7 @@ impl pallet_assets::Config for Runtime { type Extra = (); type ReserveData = (); type ForceOrigin = AssetsForceOrigin; - type Freezer = (); + type Freezer = AnchorLpFreezer; type Holder = (); type MetadataDepositBase = MetadataDepositBase; type MetadataDepositPerByte = MetadataDepositPerByte; diff --git a/template/runtime/src/tests/actors_integration_tests.rs b/template/runtime/src/tests/actors_integration_tests.rs index 7c51cc31..cac774ff 100644 --- a/template/runtime/src/tests/actors_integration_tests.rs +++ b/template/runtime/src/tests/actors_integration_tests.rs @@ -1,6 +1,7 @@ use super::common::{ ALICE, ASSET_A, BOB, CHARLIE, add_liquidity, create_pool, create_test_asset, deos_router_account, - mint_tokens, seeded_test_ext, set_consensus_timestamp, update_actor_contract_partial, + mint_tokens, seeded_synthetic_actor_test_ext, seeded_test_ext, set_consensus_timestamp, + synthetic_actor_test_ext, update_actor_contract_partial, }; macro_rules! update_actor_contract_partial { ($origin:expr, $actor:expr, $value:expr $(,)?) => { @@ -63,7 +64,10 @@ use polkadot_sdk::frame_support::{ ReservableCurrency, StorageVersion, fungible::InspectHold, fungibles::{Inspect as FungiblesInspect, Mutate as FungiblesMutate}, - tokens::imbalance::{ImbalanceAccounting, UnsafeConstructorDestructor, UnsafeManualAccounting}, + tokens::{ + Fortitude, Precision, Preservation, + imbalance::{ImbalanceAccounting, UnsafeConstructorDestructor, UnsafeManualAccounting}, + }, }, weights::Weight, }; @@ -100,25 +104,31 @@ fn task_effect_weight_provider_owns_all_task_families() { ); assert!(RuntimeAdmissionCertificateAuthority::current().is_some()); let (_, actor_base_turn) = conservative_actor_resource_limits(); + let assert_settlement = |task: &TaskOf| { + let maximum = TmctolTaskEffectWeight::maximum_effect_weight(task) + .expect("every runtime Task shape has a production effect reservation"); + assert_eq!( + TmctolTaskEffectWeight::actual_effect_weight(task, TaskEffectExecution::NotInvoked), + Some(Weight::zero()), + "NotInvoked must release the complete effect reservation", + ); + assert_eq!( + TmctolTaskEffectWeight::actual_effect_weight(task, TaskEffectExecution::Invoked), + Some(maximum), + "Invoked must settle to the matching Task-family reservation", + ); + assert!(maximum.all_lte(actor_base_turn)); + maximum + }; 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), - ); + let swap_weight = assert_settlement(&swap); 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, @@ -129,26 +139,22 @@ fn task_effect_weight_provider_owns_all_task_families() { shares: AmountResolution::Fixed(1), }, ] { - let weight = TmctolTaskEffectWeight::maximum_effect_weight(&task) - .expect("staking Task has a production envelope"); + let weight = assert_settlement(&task); 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()) + assert_settlement(&TaskOf::::StopCycle), + 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"); + let transfer_weight = assert_settlement(&transfer); 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 { @@ -205,11 +211,340 @@ fn task_effect_weight_provider_owns_all_task_families() { }, ]; for task in remaining { - let weight = TmctolTaskEffectWeight::maximum_effect_weight(&task) - .expect("every runtime Task family has a production envelope"); + let weight = assert_settlement(&task); assert!(weight.ref_time() > 0); assert!(weight.proof_size() > 0); - assert!(weight.all_lte(actor_base_turn)); + } + for leg_count in 0..=::MaxSplitTransferLegs::get() { + let legs = SplitTransferLegsOf::::try_from( + (0..leg_count) + .map(|_| SplitLeg { + to: BOB, + share: Perbill::zero(), + }) + .collect::>(), + ) + .expect("every configured SplitTransfer cardinality fits"); + let task = TaskOf::::SplitTransfer { + asset: AssetKind::Native, + amount: AmountResolution::Fixed(1), + legs, + }; + let weight = assert_settlement(&task); + assert_eq!( + weight, + crate::weights::pallet_deos_actors::SubstrateWeight::::task_split_transfer( + leg_count, + ) + ); + } +} + +#[test] +fn encoded_control_resource_boundary() { + type W = crate::weights::pallet_deos_actors::SubstrateWeight; + let service = Actors::guaranteed_actor_service_weight().expect("reference service"); + let capacity = service + .checked_sub(&Actors::scheduler_admission_overhead()) + .and_then(|remaining| remaining.checked_sub(&Actors::close_cleanup_weight_upper())) + .expect("reference per-Step capacity"); + let mut effects = vec![ + ("transfer", W::task_transfer()), + ("swap-in", W::task_dex_exact_in()), + ("swap-out", W::task_dex_exact_out()), + ("add-liquidity", W::task_add_liquidity()), + ("remove-liquidity", W::task_remove_liquidity()), + ("donate-liquidity", W::task_donate_liquidity()), + ("burn", W::task_burn()), + ("mint", W::task_mint()), + ("stake", W::task_stake()), + ("unstake", W::task_unstake()), + ]; + for legs in 1..=::MaxSplitTransferLegs::get() { + effects.push(("split-transfer", W::task_split_transfer(legs))); + } + let step = make_step(Task::Transfer { + asset: AssetKind::Native, + to: BOB, + amount: AmountResolution::Fixed(1), + }); + let mut widest = 0; + let mut witness = None; + let mut contexts = 0; + let mut inspect = |context: StepControlWeightContext| { + let control = RuntimeStepControlWeight::maximum_control_weight(context, &step) + .expect("declared source context fits typed bounds"); + contexts += 1; + for (effect_owner, effect) in &effects { + if control.saturating_add(*effect).all_lte(capacity) { + let width = control.encoded_size() + effect.encoded_size(); + if width > widest { + widest = width; + witness = Some((context, control, *effect_owner, *effect)); + } + } + } + }; + // An overapproximation: these contexts need not be jointly authorable. Every admitted + // non-StopCycle resource cell must nevertheless occur within this finite source domain. + let tails = ::MaxContractSteps::get() + .saturating_sub(1) + .div_ceil(pallet_deos_actors::MAX_STEPS_PER_TAIL_CHUNK); + let units = ::MaxPredicatesPerStep::get() * 2; + for opening_tail_chunks in (0..=tails).rev() { + for predicate_evaluation_units in 0..=units { + for opening_snapshot_entries in + 0..=::MaxOpeningSnapshotEntries::get() + { + for opening_predicate_results in + 0..=::MaxOpeningPredicateResults::get() + { + inspect(StepControlWeightContext { + cursor: 0, + steps_in_fragment: 1, + opening_tail_chunks, + predicate_evaluation_units, + opening_snapshot_entries, + opening_predicate_results, + funding_snapshot_entries: + ::MaxFundingTrackedAssets::get(), + }); + } + } + } + } + for steps_in_fragment in 1..=pallet_deos_actors::MAX_STEPS_PER_TAIL_CHUNK { + for predicate_evaluation_units in 0..=units { + inspect(StepControlWeightContext { + cursor: 1, + steps_in_fragment, + opening_tail_chunks: 0, + predicate_evaluation_units, + opening_snapshot_entries: 0, + opening_predicate_results: 0, + funding_snapshot_entries: 0, + }); + } + } + assert_eq!( + widest, 20, + "requalify the stored resource encoding bound when production Weight changes" + ); + assert!(witness.is_some()); + println!( + "Encoded resource domain: contexts={contexts}, capacity={capacity:?}, widest={widest}, source candidate={witness:?}" + ); + for effect in effects { + println!("Effect encoding domain: {effect:?}"); + } +} + +#[test] +fn action_collection_control_is_reserved_and_settled_once_in_each_phase() { + let step = make_step(Task::Transfer { + asset: AssetKind::Native, + to: BOB, + amount: AmountResolution::Fixed(1), + }); + let stop = make_step(Task::StopCycle); + let collection = crate::weights::pallet_deos_actors::SubstrateWeight::::fee_collection(); + let receipt = + crate::weights::pallet_deos_actors::SubstrateWeight::::action_invocation_receipt(); + for phase in [ + StepControlPhase::Opening, + StepControlPhase::Running, + StepControlPhase::Suspended, + ] { + let context = StepControlWeightContext { + cursor: u32::from(phase == StepControlPhase::Running), + 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 maximum = RuntimeStepControlWeight::maximum_control_weight(context, &step).unwrap(); + let stop_maximum = RuntimeStepControlWeight::maximum_control_weight(context, &stop).unwrap(); + assert_eq!( + maximum, + stop_maximum + .saturating_add(collection) + .saturating_add(receipt) + ); + let execution = StepControlExecution { + phase, + outcome: StepControlOutcome::Completed, + placement: StepControlPlacement::None, + task_effect: TaskEffectExecution::NotInvoked, + action_fee_collected: false, + }; + let without = + RuntimeStepControlWeight::actual_control_weight(context, &step, maximum, execution).unwrap(); + let system_invoked = StepControlExecution { + task_effect: TaskEffectExecution::Invoked, + ..execution + }; + let system = + RuntimeStepControlWeight::actual_control_weight(context, &step, maximum, system_invoked) + .unwrap(); + if phase == StepControlPhase::Suspended { + assert_eq!( + system, without, + "the coupled Suspended-head envelope conservatively includes receipt coverage" + ); + } else { + assert_eq!(system, without.saturating_add(receipt)); + } + // The specialized unpredicated StopCycle reports Invoked without emitting a receipt. + let stop_without = + RuntimeStepControlWeight::actual_control_weight(context, &stop, stop_maximum, execution) + .unwrap(); + assert_eq!(RuntimeStepControlWeight::actual_control_weight( + context, &stop, stop_maximum, system_invoked, + ).unwrap(), stop_without); + // A predicated StopCycle uses the general execution path and emits the zero-fee receipt. + let predicated_stop = RuntimeStep { + precondition: all_preconditions_at( + transfer_matrix_predicates(2), + pallet_deos_actors::ObservationTiming::Opening, + ), + ..stop.clone() + }; + let predicated_maximum = + RuntimeStepControlWeight::maximum_control_weight(context, &predicated_stop).unwrap(); + assert_eq!(predicated_maximum, stop_maximum.saturating_add(receipt)); + let predicated_without = RuntimeStepControlWeight::actual_control_weight( + context, + &predicated_stop, + predicated_maximum, + execution, + ) + .unwrap(); + let predicated_invoked = RuntimeStepControlWeight::actual_control_weight( + context, + &predicated_stop, + predicated_maximum, + system_invoked, + ) + .unwrap(); + if phase == StepControlPhase::Suspended { + assert_eq!(predicated_invoked, predicated_without); + } else { + assert_eq!( + predicated_invoked, + predicated_without.saturating_add(receipt) + ); + } + + let collected = StepControlExecution { + action_fee_collected: true, + ..system_invoked + }; + let with = + RuntimeStepControlWeight::actual_control_weight(context, &step, maximum, collected).unwrap(); + assert_eq!(with, system.saturating_add(collection)); + assert!(with.all_lte(maximum)); + assert!( + RuntimeStepControlWeight::actual_control_weight( + context, + &step, + maximum, + StepControlExecution { + action_fee_collected: true, + ..execution + }, + ) + .is_none() + ); + assert!( + RuntimeStepControlWeight::actual_control_weight(context, &stop, stop_maximum, collected) + .is_none() + ); + assert!( + RuntimeStepControlWeight::actual_control_weight(context, &step, Weight::zero(), collected) + .is_none() + ); + if phase == StepControlPhase::Opening { + assert!( + RuntimeStepControlWeight::actual_control_weight( + context, + &step, + maximum.saturating_add(Weight::from_parts(1, 0)), + collected + ) + .is_none() + ); + } + } +} + +#[test] +fn opening_completion_direct_weight_already_owns_invocation_receipt() { + use crate::configs::actor_config::{ + ActorMaxContractSteps, ActorMaxFundingTrackedAssets, ActorMaxOpeningPredicateResults, + ActorMaxOpeningSnapshotEntries, ActorMaxPredicatesPerStep, + }; + type W = crate::weights::pallet_deos_actors::SubstrateWeight; + let step = RuntimeStep { + precondition: all_preconditions_at( + transfer_matrix_predicates(4), + pallet_deos_actors::ObservationTiming::Opening, + ), + ..make_step(Task::StopCycle) + }; + for tails in 0..=ActorMaxContractSteps::get() + .saturating_sub(1) + .div_ceil(pallet_deos_actors::MAX_STEPS_PER_TAIL_CHUNK) + { + let count = + (1 + tails * pallet_deos_actors::MAX_STEPS_PER_TAIL_CHUNK).min(ActorMaxContractSteps::get()); + let context = StepControlWeightContext { + cursor: 0, + steps_in_fragment: 1, + opening_tail_chunks: tails, + predicate_evaluation_units: ActorMaxPredicatesPerStep::get() * 2, + opening_snapshot_entries: ((count - 1) * 2).min(ActorMaxOpeningSnapshotEntries::get()), + opening_predicate_results: (count * ActorMaxPredicatesPerStep::get()) + .min(ActorMaxOpeningPredicateResults::get()), + funding_snapshot_entries: ActorMaxFundingTrackedAssets::get(), + }; + let maximum = RuntimeStepControlWeight::maximum_control_weight(context, &step).unwrap(); + let execution = StepControlExecution { + phase: StepControlPhase::Opening, + outcome: StepControlOutcome::Completed, + placement: StepControlPlacement::None, + task_effect: TaskEffectExecution::Invoked, + action_fee_collected: false, + }; + let direct = ::scheduler_inner_opening_complete_max(tails); + assert_eq!( + RuntimeStepControlWeight::actual_control_weight(context, &step, maximum, execution), + Some(direct) + ); + assert!(direct.all_lte(maximum)); + let transfer = RuntimeStep { + task: Task::Transfer { + asset: AssetKind::Native, + to: BOB, + amount: AmountResolution::Fixed(1), + }, + ..step.clone() + }; + let transfer_maximum = + RuntimeStepControlWeight::maximum_control_weight(context, &transfer).unwrap(); + assert_eq!( + transfer_maximum, + maximum.saturating_add(::fee_collection()) + ); + let user = StepControlExecution { + action_fee_collected: true, + ..execution + }; + assert_eq!( + RuntimeStepControlWeight::actual_control_weight(context, &transfer, transfer_maximum, user), + Some(direct.saturating_add(::fee_collection())) + ); } } @@ -237,6 +572,8 @@ fn runtime_step_control_weight_identity_commits_every_staged_production_branch() &step, simple, StepControlExecution { + task_effect: TaskEffectExecution::NotInvoked, + action_fee_collected: false, phase: StepControlPhase::Opening, outcome: StepControlOutcome::Completed, placement: StepControlPlacement::None, @@ -250,18 +587,15 @@ fn runtime_step_control_weight_identity_commits_every_staged_production_branch() &step, simple, StepControlExecution { + task_effect: TaskEffectExecution::NotInvoked, + action_fee_collected: false, 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_eq!(suspended_head_retry, simple); assert!(suspended_head_retry.all_lte(simple)); let opening_heavy_retry_context = StepControlWeightContext { predicate_evaluation_units: 5, @@ -276,36 +610,30 @@ fn runtime_step_control_weight_identity_commits_every_staged_production_branch() &step, opening_heavy_retry_maximum, StepControlExecution { + task_effect: TaskEffectExecution::NotInvoked, + action_fee_collected: false, 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_eq!(opening_heavy_retry, opening_heavy_retry_maximum); 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 { + task_effect: TaskEffectExecution::NotInvoked, + action_fee_collected: false, 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_eq!(opening_heavy_complete, opening_heavy_retry_maximum); assert!(opening_heavy_complete.all_lte(opening_heavy_retry_maximum)); let opening_heavy_progress_context = StepControlWeightContext { opening_tail_chunks: 1, @@ -319,36 +647,30 @@ fn runtime_step_control_weight_identity_commits_every_staged_production_branch() &step, opening_heavy_progress_maximum, StepControlExecution { + task_effect: TaskEffectExecution::NotInvoked, + action_fee_collected: false, 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_eq!(opening_heavy_progress, opening_heavy_progress_maximum); assert!(opening_heavy_progress.all_lte(opening_heavy_progress_maximum)); let suspended_head_complete = RuntimeStepControlWeight::actual_control_weight( simple_context, &step, simple, StepControlExecution { + task_effect: TaskEffectExecution::NotInvoked, + action_fee_collected: false, 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_eq!(suspended_head_complete, simple); assert!(suspended_head_complete.all_lte(simple)); let suspended_head_progress_context = StepControlWeightContext { opening_tail_chunks: 1, @@ -362,18 +684,15 @@ fn runtime_step_control_weight_identity_commits_every_staged_production_branch() &step, suspended_head_progress_maximum, StepControlExecution { + task_effect: TaskEffectExecution::NotInvoked, + action_fee_collected: false, 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_eq!(suspended_head_progress, suspended_head_progress_maximum); assert!(suspended_head_progress.all_lte(suspended_head_progress_maximum)); let two_tail_chunks = RuntimeStepControlWeight::maximum_control_weight( StepControlWeightContext { @@ -419,8 +738,7 @@ fn runtime_step_control_weight_identity_commits_every_staged_production_branch() ); let conservative_opening = crate::weights::pallet_deos_actors::SubstrateWeight::::scheduler_paged_execute_opening_max(); - assert_eq!( - authored, + assert!( Weight::from_parts( conservative_opening .ref_time() @@ -428,7 +746,8 @@ fn runtime_step_control_weight_identity_commits_every_staged_production_branch() conservative_opening .proof_size() .max(opening_progress.proof_size()), - ), + ) + .all_lte(authored), ); let authored_progress_actual = RuntimeStepControlWeight::actual_control_weight( StepControlWeightContext { @@ -447,13 +766,21 @@ fn runtime_step_control_weight_identity_commits_every_staged_production_branch() &step, authored, StepControlExecution { + task_effect: TaskEffectExecution::NotInvoked, + action_fee_collected: false, phase: StepControlPhase::Opening, outcome: StepControlOutcome::Continued, placement: StepControlPlacement::Queue, }, ) .expect("maximum Opening progress actual evidence exists"); - assert_eq!(authored_progress_actual, opening_progress); + assert_eq!(authored_progress_actual, authored); + // Independent sources in the measured 12-Step Opening point expose the old direct shortcut. + // This is a counterexample floor, not a generated model for the full parameter domain. + let opening_point = Weight::from_parts(706_981_633, 196_777) + .saturating_add(::DbWeight::get().reads(155)) + .saturating_add(::DbWeight::get().writes(7)); + assert!(opening_point.all_lte(authored_progress_actual)); assert!(authored_progress_actual.all_lte(authored)); let minimal_opening_context = StepControlWeightContext { cursor: 0, @@ -473,6 +800,8 @@ fn runtime_step_control_weight_identity_commits_every_staged_production_branch() &step, minimal_opening_maximum, StepControlExecution { + task_effect: TaskEffectExecution::NotInvoked, + action_fee_collected: false, phase: StepControlPhase::Opening, outcome: StepControlOutcome::Continued, placement: StepControlPlacement::Queue, @@ -491,6 +820,8 @@ fn runtime_step_control_weight_identity_commits_every_staged_production_branch() &step, minimal_opening_maximum, StepControlExecution { + task_effect: TaskEffectExecution::NotInvoked, + action_fee_collected: false, phase: StepControlPhase::Opening, outcome: StepControlOutcome::Failed, placement: StepControlPlacement::None, @@ -509,6 +840,8 @@ fn runtime_step_control_weight_identity_commits_every_staged_production_branch() &step, minimal_opening_maximum, StepControlExecution { + task_effect: TaskEffectExecution::NotInvoked, + action_fee_collected: false, phase: StepControlPhase::Opening, outcome: StepControlOutcome::Suspended, placement: StepControlPlacement::Wakeup, @@ -527,6 +860,8 @@ fn runtime_step_control_weight_identity_commits_every_staged_production_branch() &step, minimal_opening_maximum, StepControlExecution { + task_effect: TaskEffectExecution::NotInvoked, + action_fee_collected: false, phase: StepControlPhase::Opening, outcome: StepControlOutcome::Completed, placement: StepControlPlacement::None, @@ -535,11 +870,35 @@ fn runtime_step_control_weight_identity_commits_every_staged_production_branch() .expect("minimal Opening completion actual evidence exists"); assert_eq!( minimal_completion_actual, - crate::weights::pallet_deos_actors::SubstrateWeight::::scheduler_inner_opening_complete_min( + crate::weights::pallet_deos_actors::SubstrateWeight::::scheduler_inner_opening_user_complete_header_max_tail( maximum_opening_tail_chunks, ), ); assert!(minimal_completion_actual.all_lte(minimal_opening_maximum)); + let zero_tail_context = StepControlWeightContext { + opening_tail_chunks: 0, + ..minimal_opening_context + }; + let zero_tail_maximum = + RuntimeStepControlWeight::maximum_control_weight(zero_tail_context, &step).unwrap(); + let zero_tail_completion = RuntimeStepControlWeight::actual_control_weight( + zero_tail_context, + &step, + zero_tail_maximum, + StepControlExecution { + task_effect: TaskEffectExecution::NotInvoked, + action_fee_collected: false, + phase: StepControlPhase::Opening, + outcome: StepControlOutcome::Completed, + placement: StepControlPlacement::None, + }, + ) + .unwrap(); + assert_eq!( + zero_tail_completion, + crate::weights::pallet_deos_actors::SubstrateWeight::::scheduler_inner_opening_user_complete_header_max(), + ); + assert!(zero_tail_completion.all_lte(zero_tail_maximum)); let maximal_completion_context = StepControlWeightContext { cursor: 0, steps_in_fragment: 1, @@ -563,6 +922,8 @@ fn runtime_step_control_weight_identity_commits_every_staged_production_branch() &step, maximal_completion_maximum, StepControlExecution { + task_effect: TaskEffectExecution::NotInvoked, + action_fee_collected: false, phase: StepControlPhase::Opening, outcome: StepControlOutcome::Failed, placement: StepControlPlacement::None, @@ -581,6 +942,8 @@ fn runtime_step_control_weight_identity_commits_every_staged_production_branch() &step, maximal_completion_maximum, StepControlExecution { + task_effect: TaskEffectExecution::NotInvoked, + action_fee_collected: false, phase: StepControlPhase::Opening, outcome: StepControlOutcome::Suspended, placement: StepControlPlacement::Wakeup, @@ -599,6 +962,8 @@ fn runtime_step_control_weight_identity_commits_every_staged_production_branch() &step, maximal_completion_maximum, StepControlExecution { + task_effect: TaskEffectExecution::NotInvoked, + action_fee_collected: false, phase: StepControlPhase::Opening, outcome: StepControlOutcome::Completed, placement: StepControlPlacement::None, @@ -650,6 +1015,8 @@ fn runtime_step_control_weight_identity_commits_every_staged_production_branch() &step, tail_maximum, StepControlExecution { + task_effect: TaskEffectExecution::NotInvoked, + action_fee_collected: false, phase: StepControlPhase::Running, outcome: StepControlOutcome::Completed, placement: StepControlPlacement::None, @@ -668,6 +1035,8 @@ fn runtime_step_control_weight_identity_commits_every_staged_production_branch() &step, tail_maximum, StepControlExecution { + task_effect: TaskEffectExecution::NotInvoked, + action_fee_collected: false, phase: StepControlPhase::Running, outcome: StepControlOutcome::Continued, placement: StepControlPlacement::Queue, @@ -686,6 +1055,8 @@ fn runtime_step_control_weight_identity_commits_every_staged_production_branch() &step, tail_maximum, StepControlExecution { + task_effect: TaskEffectExecution::NotInvoked, + action_fee_collected: false, phase: StepControlPhase::Suspended, outcome: StepControlOutcome::Completed, placement: StepControlPlacement::None, @@ -704,6 +1075,8 @@ fn runtime_step_control_weight_identity_commits_every_staged_production_branch() &step, tail_maximum, StepControlExecution { + task_effect: TaskEffectExecution::NotInvoked, + action_fee_collected: false, phase: StepControlPhase::Suspended, outcome: StepControlOutcome::Continued, placement: StepControlPlacement::Queue, @@ -722,6 +1095,8 @@ fn runtime_step_control_weight_identity_commits_every_staged_production_branch() &step, tail_maximum, StepControlExecution { + task_effect: TaskEffectExecution::NotInvoked, + action_fee_collected: false, phase: StepControlPhase::Suspended, outcome: StepControlOutcome::Suspended, placement: StepControlPlacement::Wakeup, @@ -738,6 +1113,159 @@ fn runtime_step_control_weight_identity_commits_every_staged_production_branch() assert!(RuntimeAdmissionCertificateAuthority::current().is_some()); } +#[test] +fn composed_current_weight_covers_host_models_without_opening_double_charge() { + let step = StepOf:: { + precondition: None, + task: TaskOf::::StopCycle, + on_error: StepErrorPolicy::AbortCycle, + }; + let context = StepControlWeightContext { + cursor: 0, + steps_in_fragment: 1, + opening_tail_chunks: 1, + predicate_evaluation_units: 0, + opening_snapshot_entries: 1, + opening_predicate_results: 0, + funding_snapshot_entries: 0, + }; + let actual = |units, phase, cursor| { + let context = StepControlWeightContext { + predicate_evaluation_units: units, + cursor, + opening_tail_chunks: u32::from(cursor == 0), + ..context + }; + let maximum = RuntimeStepControlWeight::maximum_control_weight(context, &step).unwrap(); + let actual = RuntimeStepControlWeight::actual_control_weight( + context, + &step, + maximum, + StepControlExecution { + task_effect: TaskEffectExecution::NotInvoked, + action_fee_collected: false, + phase, + outcome: StepControlOutcome::Failed, + placement: StepControlPlacement::None, + }, + ) + .unwrap(); + assert!(actual.all_lte(maximum)); + actual + }; + // Generated host models: total counts 1..4; the Oracle-heavy axis counts only observations. + let expected = [ + Weight::from_parts(68_093_955, 5_579), + Weight::from_parts(130_596_880, 7_319), + Weight::from_parts(190_854_004, 9_858), + Weight::from_parts(251_656_545, 12_696), + ]; + for (phase, cursor) in [ + (StepControlPhase::Opening, 0), + (StepControlPhase::Running, 1), + (StepControlPhase::Suspended, 0), + (StepControlPhase::Suspended, 1), + ] { + let baseline = actual(0, phase, cursor); + for units in 1..=8 { + assert_eq!( + actual(units, phase, cursor).checked_sub(&baseline), + Some(expected[(units.min(4) - 1) as usize]) + ); + } + } +} + +#[test] +fn composed_opening_capture_models_replace_traversal_without_double_charge() { + use crate::configs::actor_config::{ + ActorMaxContractSteps, ActorMaxOpeningPredicateResults, ActorMaxOpeningSnapshotEntries, + }; + type ControlWeights = crate::weights::pallet_deos_actors::SubstrateWeight; + let step = StepOf:: { + precondition: all_preconditions(vec![pallet_deos_actors::Predicate::BlockNumberAbove { + threshold: u32::MAX, + }]), + task: TaskOf::::StopCycle, + on_error: StepErrorPolicy::AbortCycle, + }; + let actual = |amounts, results| { + let context = StepControlWeightContext { + cursor: 0, + steps_in_fragment: 1, + opening_tail_chunks: 1, + predicate_evaluation_units: 1, + opening_snapshot_entries: if amounts { results } else { 1 }, + opening_predicate_results: if amounts { 1 } else { results }, + funding_snapshot_entries: 0, + }; + let maximum = RuntimeStepControlWeight::maximum_control_weight(context, &step).unwrap(); + let actual = RuntimeStepControlWeight::actual_control_weight( + context, + &step, + maximum, + StepControlExecution { + task_effect: TaskEffectExecution::NotInvoked, + action_fee_collected: false, + phase: StepControlPhase::Opening, + outcome: StepControlOutcome::Continued, + placement: StepControlPlacement::Queue, + }, + ) + .unwrap(); + assert!(actual.all_lte(maximum)); + actual + }; + for amounts in [false, true] { + let maximum = if amounts { + ActorMaxOpeningSnapshotEntries::get() + } else { + ActorMaxOpeningPredicateResults::get() + }; + for count in 1..=maximum { + let (traversal, capture) = if amounts { + let models = [ + ControlWeights::opening_snapshot_capture(count), + ControlWeights::opening_target_snapshot_capture(count.min(ActorMaxContractSteps::get())), + ControlWeights::opening_share_mixed_capture( + count.min(2 * ActorMaxContractSteps::get() - 1), + ), + ]; + ( + ControlWeights::opening_snapshot_traversal(), + Weight::from_parts( + models.iter().map(Weight::ref_time).max().unwrap(), + models.iter().map(Weight::proof_size).max().unwrap(), + ), + ) + } else { + let models = [ + Some(ControlWeights::opening_predicate_capture(count)), + Some(ControlWeights::opening_max_encoded_balance_capture(count)), + (count >= 2).then(|| ControlWeights::opening_observation_heavy_capture(count - 1)), + ]; + ( + ControlWeights::opening_predicate_traversal(), + Weight::from_parts( + models.iter().flatten().map(Weight::ref_time).max().unwrap(), + models + .iter() + .flatten() + .map(Weight::proof_size) + .max() + .unwrap(), + ), + ) + }; + assert!(traversal.ref_time() > 0); + assert_eq!( + actual(amounts, count).checked_sub(&actual(amounts, 0)), + capture.checked_sub(&traversal), + ); + } + } +} + fn system_transfer_steps(target_actor_id: ActorId) -> pallet_deos_actors::ContractSteps { alloc::vec![StepOf:: { precondition: None, @@ -753,29 +1281,300 @@ fn system_transfer_steps(target_actor_id: ActorId) -> pallet_deos_actors::Contra } #[test] -fn host_system_activation_manifest_is_ranked_and_rejects_undeclared_cycle_edges_before_commit() { +fn deos_reference_actor_contract_bounds_are_structural_and_genesis_is_resource_admitted() { + use crate::configs::actor_config::{ + ActorMaxContractSteps, ActorMaxOpeningPredicateResults, ActorMaxOpeningSnapshotEntries, + }; + + assert_eq!(ActorMaxContractSteps::get(), 12); + assert_eq!(ActorMaxOpeningSnapshotEntries::get(), 24); + assert_eq!(ActorMaxOpeningPredicateResults::get(), 48); seeded_test_ext().execute_with(|| { - use crate::configs::actor_config::{ - DeosSystemActorContractValidator, SystemActivationEdge, SystemActivationEffect, - }; - use primitives::ecosystem::actor_ids; + let service = Actors::guaranteed_actor_service_weight().expect("reference service exists"); + let mut system_contracts = 0u32; + for actor_id in pallet_deos_actors::ActorControlLocators::::iter_keys() { + let identity = Actors::actor_identity(actor_id).expect("genesis active identity exists"); + if identity.actor_class.actor_type() != ActorType::System { + continue; + } + let contract = Actors::actor_contract(actor_id).expect("genesis System Contract exists"); + assert!(contract.steps.len() <= ActorMaxContractSteps::get() as usize); + let required = + Actors::contract_steps_admission_weight_upper(ActorType::System, &contract.steps); + assert!( + required.all_lte(service), + "genesis actor_id={actor_id}, required={required:?}, service={service:?}", + ); + system_contracts = system_contracts.saturating_add(1); + } + assert!(system_contracts > 0); + }); +} - let topology = DeosSystemActorContractValidator::manifest().expect("DAG manifest"); - let projection = DeosSystemActorContractValidator::projection().expect("derived topology"); - assert!( - projection - .edges - .iter() - .all(|edge| topology.edges.contains(edge)) - ); - assert!(projection.nodes.iter().all(|node| { - topology - .nodes - .iter() - .any(|manifest_node| manifest_node.actor_id == node.actor_id) - })); - assert!(topology.edges.contains(&SystemActivationEdge { - source: actor_ids::FEE_SINK_ACTORS_ID, +#[test] +fn twelve_twenty_four_forty_eight_product_is_selected_by_complete_contract_weight() { + use crate::configs::actor_config::{ + ActorMaxContractSteps, ActorMaxOpeningPredicateResults, ActorMaxOpeningSnapshotEntries, + ActorMaxPredicatesPerStep, + }; + + let steps = ActorMaxContractSteps::get(); + let sparse: RuntimeContractSteps = (0..steps) + .map(|_| { + make_step(Task::Mint { + asset: AssetKind::Native, + amount: AmountResolution::Fixed(1), + }) + }) + .collect::>() + .try_into() + .expect("structural Step ceiling fits"); + let dense: RuntimeContractSteps = (0..steps) + .map(|index| { + let asset_a = AssetKind::Local(40_000 + index * 2); + let asset_b = AssetKind::Local(40_001 + index * 2); + RuntimeStep { + precondition: all_preconditions_at( + (0..ActorMaxPredicatesPerStep::get()) + .map(|predicate| pallet_deos_actors::Predicate::BalanceAbove { + asset: if predicate % 2 == 0 { asset_a } else { asset_b }, + threshold: u128::from(predicate + 1), + }) + .collect(), + pallet_deos_actors::ObservationTiming::Opening, + ), + task: Task::AddLiquidity { + asset_a, + asset_b, + amount_a: AmountResolution::PercentageAtOpening(Perbill::one()), + amount_b: AmountResolution::PercentageAtOpening(Perbill::one()), + min_lp_out: 1, + }, + on_error: StepErrorPolicy::AbortCycle, + } + }) + .collect::>() + .try_into() + .expect("structural 12/24/48 product fits"); + let service = Actors::guaranteed_actor_service_weight().expect("reference service exists"); + let sparse_required = Actors::contract_steps_admission_weight_upper(ActorType::System, &sparse); + let dense_required = Actors::contract_steps_admission_weight_upper(ActorType::System, &dense); + + assert_eq!(dense.len() as u32, ActorMaxContractSteps::get()); + assert_eq!( + dense.len() as u32 * 2, + ActorMaxOpeningSnapshotEntries::get() + ); + assert_eq!( + dense.len() as u32 * ActorMaxPredicatesPerStep::get(), + ActorMaxOpeningPredicateResults::get(), + ); + assert!(sparse_required.all_lte(service)); + assert!(dense_required.ref_time() <= service.ref_time()); + assert!(dense_required.proof_size() > service.proof_size()); + assert!(!dense_required.all_lte(service)); + println!( + "ACTOR_PRODUCT_ADMISSION_V1 sparse={sparse_required:?} dense={dense_required:?} service={service:?}" + ); +} + +#[test] +fn pre_g5_waiting_selector_is_finite_contained_and_genesis_fit() { + use crate::configs::actor_config::ActorWakeupWeightLimit; + use pallet_deos_actors::WakeupBucketDisposition; + type W = crate::weights::pallet_deos_actors::SubstrateWeight; + + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + enum TransitionCell { + OpeningDirectWaiting, + RunningDirectWaiting, + SuspendedDirectWaiting, + TemporalDirectWaiting, + UnmatchedWakeup, + DuePartial, + DueRemove, + PopulatedTerminal, + QueueOrUnsignaled, + } + + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + enum WaitingOwner { + InclusiveDirect, + Publication, + DueWorker, + Removal, + None, + } + + let owner = |cell| match cell { + TransitionCell::OpeningDirectWaiting + | TransitionCell::RunningDirectWaiting + | TransitionCell::SuspendedDirectWaiting + | TransitionCell::TemporalDirectWaiting => WaitingOwner::InclusiveDirect, + TransitionCell::UnmatchedWakeup => WaitingOwner::Publication, + TransitionCell::DuePartial | TransitionCell::DueRemove => WaitingOwner::DueWorker, + TransitionCell::PopulatedTerminal => WaitingOwner::Removal, + TransitionCell::QueueOrUnsignaled => WaitingOwner::None, + }; + let matrix = [ + ( + TransitionCell::OpeningDirectWaiting, + WaitingOwner::InclusiveDirect, + ), + ( + TransitionCell::RunningDirectWaiting, + WaitingOwner::InclusiveDirect, + ), + ( + TransitionCell::SuspendedDirectWaiting, + WaitingOwner::InclusiveDirect, + ), + ( + TransitionCell::TemporalDirectWaiting, + WaitingOwner::InclusiveDirect, + ), + (TransitionCell::UnmatchedWakeup, WaitingOwner::Publication), + (TransitionCell::DuePartial, WaitingOwner::DueWorker), + (TransitionCell::DueRemove, WaitingOwner::DueWorker), + (TransitionCell::PopulatedTerminal, WaitingOwner::Removal), + (TransitionCell::QueueOrUnsignaled, WaitingOwner::None), + ]; + for (cell, expected) in matrix { + assert_eq!(owner(cell), expected, "selector mismatch for {cell:?}"); + } + + let component_max = |left: Weight, right: Weight| { + Weight::from_parts( + left.ref_time().max(right.ref_time()), + left.proof_size().max(right.proof_size()), + ) + }; + let publication = Weight::from_parts(5_131_223_000, 76_618); + for actual in [ + Weight::from_parts(5_083_590_000, 76_246), + Weight::from_parts(5_119_839_000, 76_618), + Weight::from_parts(5_123_540_000, 74_057), + Weight::from_parts(5_131_223_000, 74_993), + ] { + assert!(actual.all_lte(publication)); + } + let removal = Weight::from_parts(8_078_069_000, 96_741); + for actual in [ + Weight::from_parts(2_620_804_000, 18_665), + Weight::from_parts(2_891_826_000, 33_564), + Weight::from_parts(8_078_069_000, 96_741), + Weight::from_parts(6_846_035_000, 76_112), + Weight::from_parts(3_311_590_000, 21_030), + ] { + assert!(actual.all_lte(removal)); + } + + let occurrence = component_max( + W::at_time_trigger_occurrence(), + W::cadenced_trigger_occurrence(), + ); + for (bucket, physical) in [ + ( + WakeupBucketDisposition::Retain, + W::scheduler_wakeup_cursor_worker_partial(), + ), + ( + WakeupBucketDisposition::Remove, + W::scheduler_wakeup_cursor_worker_remove(), + ), + ] { + let actual = physical.saturating_add(occurrence); + let maximum = Actors::wakeup_cursor_drain_unit_weight_upper(bucket); + assert!(actual.all_lte(maximum)); + assert!(maximum.all_lte(ActorWakeupWeightLimit::get())); + } + + seeded_test_ext().execute_with(|| { + let service = Actors::guaranteed_actor_service_weight().expect("reference service exists"); + let source = component_max( + W::scheduler_paged_consume_preserve_page(), + W::scheduler_paged_consume_delete_page(), + ); + let queue = component_max( + W::scheduler_paged_append_existing_page(), + W::scheduler_paged_append_new_page(), + ); + let prefix = W::scheduler_on_idle_base() + .saturating_add(W::scheduler_paged_tombstone_drain(1).saturating_mul(2)) + .saturating_add(source) + .saturating_add(W::scheduler_wakeup_cursor_worker_future().saturating_mul(2)) + .saturating_add(Actors::scheduler_actor_probe_weight_upper()); + let cleanup = Actors::close_dispatch_weight_upper(); + let mut current_genesis_maximum = Weight::zero(); + let mut selected_genesis_maximum = Weight::zero(); + let mut system_contracts = 0u32; + for actor_id in pallet_deos_actors::ActorControlLocators::::iter_keys() { + let identity = Actors::actor_identity(actor_id).expect("genesis active identity exists"); + if identity.actor_class.actor_type() != ActorType::System { + continue; + } + let contract = Actors::actor_contract(actor_id).expect("genesis System Contract exists"); + assert_eq!(contract.steps.len(), 1, "requalify changed genesis geometry"); + let (_, cell) = Actors::actor_control_cell(actor_id).expect("genesis control cell exists"); + let step = cell.resources.control.saturating_add(cell.resources.effect); + let direct = prefix + .saturating_add(queue) + .saturating_add(step) + .saturating_add(cleanup); + assert_eq!( + direct, + Actors::contract_steps_admission_weight_upper(ActorType::System, &contract.steps), + ); + let unmatched_publication = prefix + .saturating_add(publication) + .saturating_add(step) + .saturating_add(cleanup); + let selected = component_max(component_max(direct, unmatched_publication), removal); + current_genesis_maximum = component_max(current_genesis_maximum, direct); + selected_genesis_maximum = component_max(selected_genesis_maximum, selected); + assert!(selected.all_lte(service), "actor_id={actor_id}, selected={selected:?}"); + system_contracts = system_contracts.saturating_add(1); + } + assert!(system_contracts > 0); + assert_eq!( + current_genesis_maximum, + Weight::from_parts(18_490_261_174, 223_822), + ); + assert_eq!( + selected_genesis_maximum, + Weight::from_parts(22_961_831_174, 283_994), + ); + assert!(selected_genesis_maximum.all_lte(service)); + println!( + "ACTOR_PRE_G5_WAITING_SELECTOR_V1 current={current_genesis_maximum:?} selected={selected_genesis_maximum:?} service={service:?}" + ); + }); +} + +#[test] +fn host_system_activation_manifest_is_ranked_and_rejects_undeclared_cycle_edges_before_commit() { + seeded_test_ext().execute_with(|| { + use crate::configs::actor_config::{ + DeosSystemActorContractValidator, SystemActivationEdge, SystemActivationEffect, + }; + use primitives::ecosystem::actor_ids; + + let topology = DeosSystemActorContractValidator::manifest().expect("DAG manifest"); + let projection = DeosSystemActorContractValidator::projection().expect("derived topology"); + assert!( + projection + .edges + .iter() + .all(|edge| topology.edges.contains(edge)) + ); + assert!(projection.nodes.iter().all(|node| { + topology + .nodes + .iter() + .any(|manifest_node| manifest_node.actor_id == node.actor_id) + })); + assert!(topology.edges.contains(&SystemActivationEdge { + source: actor_ids::FEE_SINK_ACTORS_ID, target: actor_ids::NATIVE_STAKING_LIQUIDITY_ACTOR_ID, effect: SystemActivationEffect::CertifiedActorTransfer, })); @@ -816,14 +1615,14 @@ fn host_system_activation_manifest_is_ranked_and_rejects_undeclared_cycle_edges_ } #[test] -#[ignore = "fixed T+1 runtime length-32 stress profile; run by exact test name"] +#[ignore = "fixed T+1 runtime length-12 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 lengths = [0_u32, 1, 4, 8, 12]; let actors = (0..10) .map(|index| { let steps = BoundedVec::try_from( @@ -901,7 +1700,7 @@ fn run_next_block_runtime_herd(requested_population: Option) { } let population = requested_population.unwrap_or(maximum_population); assert!(population <= maximum_population); - let lengths = [0_u32, 1, 4, 8, 32]; + let lengths = [0_u32, 1, 4, 8, 12]; let actors = (0..population) .map(|index| { let steps = BoundedVec::try_from( @@ -1317,7 +2116,7 @@ fn paid_user_long_cycle_preserves_fifo_under_queue_pressure_and_closes_insolvent } #[derive(Clone)] -struct RuntimeSchedule { +pub(super) struct RuntimeSchedule { trigger: pallet_deos_actors::TriggerOf, cooldown_blocks: u32, } @@ -1691,7 +2490,7 @@ fn ensure_current_resource_state() { } } -fn signed_extrinsic( +pub(super) fn signed_extrinsic( signer: &sr25519::Pair, nonce: crate::Nonce, call: RuntimeCall, @@ -1730,16 +2529,17 @@ fn make_step(task: RuntimeTask) -> RuntimeStep { } } -fn all_preconditions( +fn all_preconditions_at( predicates: Vec>, + timing: pallet_deos_actors::ObservationTiming, ) -> Option> { + if predicates.is_empty() { + return None; + } let clause = BoundedVec::try_from( predicates .into_iter() - .map(|predicate| pallet_deos_actors::TimedPredicate { - timing: pallet_deos_actors::ObservationTiming::Current, - predicate, - }) + .map(|predicate| pallet_deos_actors::TimedPredicate { timing, predicate }) .collect::>(), ) .expect("runtime predicates fit"); @@ -1748,6 +2548,12 @@ fn all_preconditions( }) } +fn all_preconditions( + predicates: Vec>, +) -> Option> { + all_preconditions_at(predicates, pallet_deos_actors::ObservationTiming::Current) +} + fn any_preconditions( predicates: Vec>, ) -> Option> { @@ -1770,7 +2576,7 @@ fn inert_task() -> RuntimeTask { Task::StopCycle } -fn exp_0002_false_steps(step_count: u32) -> RuntimeContractSteps { +fn contract_geometry_false_steps(step_count: u32) -> RuntimeContractSteps { let step = RuntimeStep { precondition: all_preconditions(vec![pallet_deos_actors::Predicate::BlockNumberBelow { threshold: 0, @@ -1781,13 +2587,39 @@ fn exp_0002_false_steps(step_count: u32) -> RuntimeContractSteps { BoundedVec::try_from(vec![step; step_count as usize]).expect("profile Contract Steps fit") } -fn manual_schedule() -> RuntimeSchedule { +pub(super) fn manual_schedule() -> RuntimeSchedule { RuntimeSchedule { trigger: Trigger::manual(), cooldown_blocks: 0, } } +pub(super) fn cadenced_schedule(every_ticks: u64) -> RuntimeSchedule { + RuntimeSchedule { + trigger: Trigger::cadenced(every_ticks), + cooldown_blocks: 0, + } +} + +pub(super) fn at_time_schedule(after_ticks: u64) -> RuntimeSchedule { + RuntimeSchedule { + trigger: Trigger::at_time(after_ticks), + cooldown_blocks: 0, + } +} + +pub(super) fn observation_crossing_schedule( + feed: primitives::OracleFeedId, + direction: CrossingDirection, + threshold: u128, + rearm_threshold: u128, +) -> RuntimeSchedule { + RuntimeSchedule { + trigger: Trigger::observation_crossing(feed, direction, threshold, rearm_threshold), + cooldown_blocks: 0, + } +} + fn observation_schedule(feed: primitives::OracleFeedId) -> RuntimeSchedule { RuntimeSchedule { trigger: Trigger::observation_change(feed), @@ -1805,7 +2637,7 @@ fn on_address_event_schedule( } } -fn transfer_contract_steps( +pub(super) fn transfer_contract_steps( to: crate::AccountId, asset: AssetKind, amount: u128, @@ -1818,6 +2650,70 @@ fn transfer_contract_steps( .expect("steps fits") } +pub(super) fn transfer_contract_steps_with_opening_predicates( + to: crate::AccountId, + asset: AssetKind, + amount: u128, + step_count: u32, + predicates_per_step: u32, +) -> RuntimeContractSteps { + assert!(step_count > 0, "W3 Contract must contain a Step"); + let steps = (0..step_count) + .map(|step_index| RuntimeStep { + precondition: all_preconditions_at( + (0..predicates_per_step) + .map( + |predicate_index| pallet_deos_actors::Predicate::BalanceNotEquals { + asset, + threshold: 10_000u128 + .saturating_mul(crate::EXISTENTIAL_DEPOSIT) + .saturating_add(u128::from( + step_index + .saturating_mul(predicates_per_step) + .saturating_add(predicate_index) + .saturating_add(1), + )), + }, + ) + .collect(), + pallet_deos_actors::ObservationTiming::Opening, + ), + task: Task::Transfer { + to: to.clone(), + asset, + amount: AmountResolution::Fixed(amount), + }, + on_error: StepErrorPolicy::AbortCycle, + }) + .collect::>(); + BoundedVec::try_from(steps).expect("W3 Contract fits runtime Step bounds") +} + +pub(super) fn one_step_fifo_attempt_maxima(steps: &RuntimeContractSteps) -> (Weight, Weight) { + assert_eq!(steps.len(), 1, "one-Step profile has one Step"); + let step = steps.first().expect("one-Step profile has one Step"); + let effect = TmctolTaskEffectWeight::maximum_effect_weight(&step.task) + .expect("one-Step profile Task has a production effect envelope"); + let 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: ::MaxFundingTrackedAssets::get( + ), + }; + let step_control = RuntimeStepControlWeight::maximum_control_weight(context, step) + .expect("one-Step profile has a production control envelope"); + let consume = as WeightInfo>::scheduler_paged_consume_preserve_page() + .max( as WeightInfo>::scheduler_paged_consume_delete_page()); + let control = consume + .saturating_add(step_control) + .saturating_add(Actors::close_dispatch_weight_upper()); + (control, effect) +} + fn user_active_contract( schedule: RuntimeSchedule, window: Option>, @@ -1850,7 +2746,7 @@ fn system_active_contract( }) } -fn create_user( +pub(super) fn create_user( who: crate::AccountId, schedule: RuntimeSchedule, schedule_window: Option>, @@ -1867,18 +2763,37 @@ fn create_user( id } -fn create_system( +pub(super) fn create_system( + owner: crate::AccountId, + schedule: RuntimeSchedule, + schedule_window: Option>, + steps: RuntimeContractSteps, +) -> ActorId { + create_system_with_completion( + owner, + schedule, + schedule_window, + steps, + pallet_deos_actors::CompletionPolicy::Persistent, + ) +} + +pub(super) fn create_system_with_completion( owner: crate::AccountId, schedule: RuntimeSchedule, schedule_window: Option>, steps: RuntimeContractSteps, + completion: pallet_deos_actors::CompletionPolicy, ) -> ActorId { let id = Actors::next_actor_id(); + let mut contract = system_active_contract(schedule, schedule_window, steps) + .expect("active System Contract exists"); + contract.completion = completion; assert_ok!(Actors::create_system_actor( RuntimeOrigin::root(), owner, Mutability::Mutable, - system_active_contract(schedule, schedule_window, steps), + Some(contract), )); age_fixture_control_clock(id); id @@ -1890,12 +2805,43 @@ fn age_fixture_control_clock(actor_id: ActorId) { System::set_block_number(1); return; } - pallet_deos_actors::ActorIdentities::::mutate(actor_id, |maybe| { - maybe - .as_mut() - .expect("fixture actor identity exists") - .last_control_mutation_block = now.saturating_sub(1); - }); + let aged_at = now.saturating_sub(1); + match pallet_deos_actors::ActorControlLocators::::get(actor_id) + .expect("fixture actor frame locator exists") + { + pallet_deos_actors::ActorControlLocation::Unsignaled => { + pallet_deos_actors::ActorUnsignaledControlCells::::mutate(actor_id, |maybe| { + maybe + .as_mut() + .expect("fixture Unsignaled frame cell exists") + .identity + .last_control_mutation_block = aged_at; + }); + } + pallet_deos_actors::ActorControlLocation::Ready { ticket } => { + pallet_deos_actors::ActorReadyFrameChunks::::mutate(ticket / 32, |maybe| { + maybe + .as_mut() + .and_then(|chunk| chunk.get_mut((ticket % 32) as usize)) + .and_then(Option::as_mut) + .expect("fixture Ready frame cell exists") + .identity + .last_control_mutation_block = aged_at; + }); + } + pallet_deos_actors::ActorControlLocation::Waiting { key, page, slot } => { + pallet_deos_actors::ActorWaitingFrameChunks::::mutate((key, page), |maybe| { + maybe + .as_mut() + .and_then(|page| page.entries.get_mut(slot as usize)) + .and_then(Option::as_mut) + .and_then(pallet_deos_actors::ActorWaitingEntry::primary_mut) + .expect("fixture Waiting frame cell exists") + .identity + .last_control_mutation_block = aged_at; + }); + } + } } fn actor_funding(actor_id: ActorId) -> pallet_deos_actors::ActorFundingStateOf { @@ -1908,7 +2854,7 @@ fn actor_account(actor_id: ActorId) -> crate::AccountId { .expect("Actors must exist") } -fn fund_native(actor_id: ActorId, amount: u128) { +pub(super) fn fund_native(actor_id: ActorId, amount: u128) { let actor_acc = actor_account(actor_id); let _ = >::deposit_creating(&actor_acc, amount); } @@ -2057,17 +3003,33 @@ fn deos_runtime_executes_unconditional_and_dnf_with_fixed_successors() { } #[test] -fn genesis_anchor_buckets_are_custody_only_accounts() { +fn genesis_anchors_are_sealed_immutable_system_actors() { seeded_test_ext().execute_with(|| { for actor_id in [ primitives::ecosystem::actor_ids::TOL_BUCKET_A_ACTORS_ID, - primitives::ecosystem::actor_ids::BLDR_BUCKET_A_ACTORS_ID, + primitives::ecosystem::actor_ids::BLDR_ANCHOR_ACTORS_ID, ] { let sovereign = Actors::sovereign_account_id_system(actor_id); + let identity = Actors::actor_identity(actor_id).expect("Anchor identity must exist"); + assert_eq!(identity.actor_class.actor_type(), ActorType::System); + assert_eq!(identity.mutability, Mutability::Immutable); assert!(Actors::active_actor_state(actor_id).is_none()); - assert!(Actors::actor_identities(actor_id).is_none()); - assert!(Actors::sovereign_index(sovereign).is_none()); + assert_eq!(Actors::sovereign_index(sovereign), Some(actor_id)); + let plan = transfer_contract_steps(BOB, AssetKind::Native, 1); + let contract = ActorContract { + trigger: Trigger::address_event(SourceFilter::Any, AssetFilter::Any), + cooldown_blocks: 0, + window: None, + steps: plan.clone(), + funding: FundingSourcePolicy::RuntimePolicy, + completion: CompletionPolicy::Persistent, + auto_close_at_cycle_nonce: None, + }; + assert_noop!( + Actors::activate_actor(RuntimeOrigin::root(), actor_id, contract), + Error::::ImmutableActor + ); assert_noop!( update_actor_contract_partial!( RuntimeOrigin::root(), @@ -2080,15 +3042,92 @@ fn genesis_anchor_buckets_are_custody_only_accounts() { Actors::pause_actor(RuntimeOrigin::root(), actor_id), Error::::ActorNotFound ); + assert_noop!( + Actors::deactivate_actor(RuntimeOrigin::root(), actor_id), + Error::::ActorDormant + ); assert_noop!( Actors::manual_trigger(RuntimeOrigin::root(), actor_id), Error::::ActorNotFound ); assert_noop!( Actors::close_actor(RuntimeOrigin::root(), actor_id), - Error::::ActorNotFound + Error::::ImmutableActor + ); + } + }); +} + +#[test] +fn immutable_anchor_lp_freezer_blocks_every_debit_and_asset_destruction() { + seeded_test_ext().execute_with(|| { + let lp_asset = primitives::assets::TYPE_LP | 777; + assert_ok!(create_test_asset(lp_asset, &ALICE)); + assert_ok!(mint_tokens(lp_asset, &ALICE, &ALICE, 2_000)); + + for actor_id in [ + primitives::ecosystem::actor_ids::TOL_BUCKET_A_ACTORS_ID, + primitives::ecosystem::actor_ids::BLDR_ANCHOR_ACTORS_ID, + ] { + let anchor = Actors::sovereign_account_id_system(actor_id); + assert_ok!(Assets::transfer( + RuntimeOrigin::signed(ALICE), + lp_asset, + anchor.clone().into(), + 1_000, + )); + let before = Assets::balance(lp_asset, &anchor); + assert_eq!(before, 1_000); + + assert_noop!( + Assets::transfer( + RuntimeOrigin::signed(anchor.clone()), + lp_asset, + BOB.into(), + 1, + ), + polkadot_sdk::pallet_assets::Error::::BalanceLow + ); + assert_noop!( + Assets::force_transfer( + RuntimeOrigin::signed(ALICE), + lp_asset, + anchor.clone().into(), + BOB.into(), + 1, + ), + polkadot_sdk::pallet_assets::Error::::BalanceLow + ); + assert!( + >::transfer( + lp_asset, + &anchor, + &BOB, + 1, + Preservation::Expendable, + ) + .is_err(), + "runtime-internal transfer must observe the Anchor LP freeze", + ); + assert!( + >::burn_from( + lp_asset, + &anchor, + 1, + Preservation::Expendable, + Precision::Exact, + Fortitude::Force, + ) + .is_err(), + "runtime-internal forced burn must observe the Anchor LP freeze", ); + assert_eq!(Assets::balance(lp_asset, &anchor), before); } + + assert_noop!( + Assets::start_destroy(RuntimeOrigin::signed(ALICE), lp_asset), + polkadot_sdk::pallet_assets::Error::::ContainsFreezes + ); }); } @@ -2208,7 +3247,13 @@ fn run_next_idle(weight: Weight) { .expect("test block advances"); System::set_block_number(now); set_consensus_timestamp( - u64::from(now).saturating_mul(primitives::ecosystem::params::ACTOR_CADENCE_TICK_MILLIS), + u64::from(now) + .saturating_mul(primitives::ecosystem::params::ACTOR_CADENCE_TICK_MILLIS) + .max( + crate::Timestamp::get() + .checked_add(primitives::ecosystem::params::ACTOR_CADENCE_TICK_MILLIS) + .expect("test timestamp advances"), + ), ); Actors::on_initialize(now); run_idle(weight); @@ -2224,7 +3269,8 @@ fn run_idle(weight: 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() + if !pallet_deos_actors::ActorControlLocators::::iter_keys() + .filter_map(Actors::actor_hot) .any(|hot| hot.cycle_state == pallet_deos_actors::CycleState::Running) { break; @@ -2232,7 +3278,13 @@ fn run_idle(weight: Weight) { 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), + u64::from(now) + .saturating_mul(primitives::ecosystem::params::ACTOR_CADENCE_TICK_MILLIS) + .max( + crate::Timestamp::get() + .checked_add(primitives::ecosystem::params::ACTOR_CADENCE_TICK_MILLIS) + .expect("test timestamp advances"), + ), ); Actors::on_initialize(now); ensure_current_resource_state(); @@ -2240,12 +3292,48 @@ fn run_idle(weight: Weight) { } } +#[test] +fn idle_fixture_keeps_distant_consensus_time_monotonic_through_q1() { + seeded_synthetic_actor_test_ext().execute_with(|| { + System::set_block_number(1); + let actor_id = create_system( + ALICE, + manual_schedule(), + None, + contract_geometry_false_steps(2), + ); + let distant_time = 1_000_000; + set_consensus_timestamp(distant_time); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + run_next_idle(Weight::MAX); + assert_eq!( + Actors::active_actor_state(actor_id) + .expect("completed Actor") + .identity + .cycle_nonce, + 1 + ); + assert!( + System::block_number() >= 3, + "two Steps require distinct blocks" + ); + let tick = primitives::ecosystem::params::ACTOR_CADENCE_TICK_MILLIS; + assert!(crate::Timestamp::get() >= distant_time + u64::from(System::block_number() - 1) * tick); + let before = crate::Timestamp::get(); + run_next_idle(Weight::MAX); + assert!(crate::Timestamp::get() >= before + tick); + }); +} + fn starvation_observation_weight() -> Weight { <::WeightInfo as WeightInfo>::scheduler_on_idle_base() } /// Proof-limited on_idle budget that admits the wakeup cursor, queue scan, loaded-state probe, and -/// the head consume, but not the actor's full cycle admission. Materializes the only spec 8.6.3 +/// the head consume, but not the actor's current Step admission. Materializes the only spec 8.6.3 /// starvation trigger: a live FIFO head blocked by weight with no admitted attempt. fn starvation_blocked_budget(actor_id: ActorId) -> Weight { let base = starvation_observation_weight(); @@ -2255,17 +3343,14 @@ fn starvation_blocked_budget(actor_id: ActorId) -> Weight { let state_probe = Actors::scheduler_actor_state_probe_weight_upper(); let consume = <::WeightInfo as WeightInfo>::scheduler_paged_consume_preserve_page() .max(<::WeightInfo as WeightInfo>::scheduler_paged_consume_delete_page()); - let instance = Actors::active_actor_state(actor_id).expect("actor exists"); - let cycle = Actors::compute_cycle_weight_upper( - instance.identity.actor_class.actor_type(), - &instance.contract.steps, - ); + let (_, cell) = Actors::actor_control_cell(actor_id).expect("current control owner exists"); + let step = cell.resources.control.saturating_add(cell.resources.effect); let full = base .saturating_add(cursor) .saturating_add(scan) .saturating_add(state_probe) .saturating_add(consume) - .saturating_add(cycle); + .saturating_add(step); Weight::from_parts(u64::MAX, full.proof_size().saturating_sub(1)) } @@ -2339,7 +3424,7 @@ fn manual_trigger_executes_transfer_contract_steps() { } #[test] -fn exp_0002_ceiling_semantics_profile_commits_exactly_one_step_per_block() { +fn contract_geometry_ceiling_semantics_profile_commits_exactly_one_step_per_block() { seeded_test_ext().execute_with(|| { System::set_block_number(1); let step_count = ::MaxContractSteps::get(); @@ -2347,7 +3432,7 @@ fn exp_0002_ceiling_semantics_profile_commits_exactly_one_step_per_block() { ALICE, manual_schedule(), None, - exp_0002_false_steps(step_count), + contract_geometry_false_steps(step_count), ); let expected_tail_chunks = step_count .saturating_sub(1) @@ -2418,8 +3503,8 @@ fn exp_0002_ceiling_semantics_profile_commits_exactly_one_step_per_block() { } #[test] -#[ignore = "deterministic EXP-0002 profile output; run explicitly with --nocapture"] -fn profile_exp_0002_state_footprint() { +#[ignore = "deterministic Contract storage profile; run explicitly with --nocapture"] +fn profile_contract_storage_footprint() { seeded_test_ext().execute_with(|| { System::set_block_number(1); let step_count = ::MaxContractSteps::get(); @@ -2427,19 +3512,20 @@ fn profile_exp_0002_state_footprint() { ALICE, manual_schedule(), None, - exp_0002_false_steps(1), + contract_geometry_false_steps(1), ); let max_actor = create_system( BOB, manual_schedule(), None, - exp_0002_false_steps(step_count), + contract_geometry_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 certificate = pallet_deos_actors::ActorUnsignaledControlCells::::get(actor_id) + .expect("manual profile control cell exists") + .admission; let tails = pallet_deos_actors::ActorContractTailChunks::::iter_prefix(actor_id) .collect::>(); let tail_bytes = tails @@ -2463,7 +3549,7 @@ fn profile_exp_0002_state_footprint() { .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={}", + "CONTRACT_GEOMETRY_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(), @@ -2514,6 +3600,10 @@ fn productive_run_completion_closes_runtime_actor_after_committed_transfer() { Mutability::Mutable, contract, SimulationMode::FreshCurrentPlan, + pallet_deos_actors::SimulationBudget { + actor_control: Weight::from_parts(u64::MAX / 2, u64::MAX / 2), + shared_economic: Weight::from_parts(u64::MAX / 2, u64::MAX / 2), + }, ) .expect("ready productive contract simulates"); assert_eq!( @@ -2882,6 +3972,26 @@ fn system_actor_executes_native_staking_lp_donation_task() { actor_id )); run_idle(Weight::MAX); + let usage = Actors::block_resource_state() + .expect("liquidity service resource usage exists") + .usage(); + assert_eq!( + ( + usage.actor_control_used(), + usage.actor_effect_used(), + usage.user_dispatch_used() + ), + ( + Weight::from_parts(14_152_748_935, 155_278), + Weight::from_parts(1_206_242_000, 14_035), + Weight::zero() + ), + "the successful donation keeps Control and liquidity effect ownership separate" + ); + let limits = BlockResourceBudgetValue::get().limits(); + assert!(usage.actor_control_used().all_lte(limits.actor_control())); + assert!(usage.actor_effect_used().all_lte(limits.shared_economic())); + assert!(usage.actor_effect_used().all_lte(limits.actor_base_turn())); let lp_supply_after = ::PoolAssets::total_issuance( pool.lp_token, @@ -2905,15 +4015,114 @@ fn system_actor_executes_native_staking_lp_donation_task() { } if *id == actor_id && *asset_b == AssetKind::Local(staked_asset_id) ) })); - }); -} -#[test] -fn create_user_charges_creation_fee_to_fee_sink() { - seeded_test_ext().execute_with(|| { - System::set_block_number(1); - let fee = ::ActorCreationFee::get(); - let fee_sink = ::FeeSink::get(); + let cohort_steps = TmctolGenesisSystemActors::build_native_staking_liquidity_contract_steps(1); + let cohort_maxima = one_step_fifo_attempt_maxima(&cohort_steps); + assert_eq!( + cohort_maxima, + ( + Weight::from_parts(12_924_472_858, 127_063), + Weight::from_parts(1_206_242_000, 14_035) + ) + ); + let cohort = (0..100) + .map(|_| { + let id = create_system(ALICE, manual_schedule(), None, cohort_steps.clone()); + assert_ok!(mint_tokens(0, &ALICE, &actor_account(id), 81)); + assert_ok!(Actors::manual_trigger(RuntimeOrigin::signed(ALICE), id)); + id + }) + .collect::>(); + let (_, cohort_cell) = + Actors::actor_control_cell(cohort[0]).expect("homogeneous donation control owner exists"); + assert_eq!( + cohort_cell.resources, + pallet_deos_actors::ActorStepResourceEnvelope { + control: Weight::from_parts(3_271_396_813, 45_638), + effect: Weight::from_parts(1_206_242_000, 14_035), + } + ); + let cutoff = pallet_deos_actors::ActorReadyTail::::get(); + System::reset_events(); + System::set_block_number(2); + ensure_actor_prepass_context(); + assert_ok!(Actors::actor_prepass(RuntimeOrigin::none())); + let progressed = System::events() + .iter() + .filter_map(|record| match &record.event { + RuntimeEvent::Actors(Event::LiquidityDonated { actor_id, .. }) + if cohort.contains(actor_id) => + { + Some(*actor_id) + } + _ => None, + }) + .collect::>(); + let frontier_usage = Actors::block_resource_state() + .expect("liquidity cohort usage exists") + .usage(); + assert_eq!( + progressed, + cohort[..12], + "the first actual admission stop must preserve the homogeneous FIFO prefix" + ); + assert_eq!( + ( + frontier_usage.actor_control_used(), + frontier_usage.actor_effect_used() + ), + ( + Weight::from_parts(33_660_842_618, 577_863), + Weight::from_parts(14_474_904_000, 168_420) + ) + ); + assert!(cohort[..12].iter().all(|id| { + Actors::active_actor_state(*id).is_some_and(|state| !state.hot.pending_signal) + })); + assert!(cohort[12..].iter().all(|id| { + Actors::active_actor_state(*id) + .is_some_and(|state| state.hot.pending_signal && state.hot.queue_ticket.is_some()) + })); + let next_ticket = Actors::actor_hot(cohort[12]) + .and_then(|hot| hot.queue_ticket) + .expect("first deferred Actor retains its ticket"); + assert_eq!(Actors::prepass_execution_cutoff(), Some((2, cutoff))); + assert!(next_ticket < cutoff); + assert_eq!( + pallet_deos_actors::ActorReadyHead::::get(), + next_ticket + ); + let limits = BlockResourceBudgetValue::get().limits(); + assert!( + frontier_usage + .actor_control_used() + .all_lte(limits.actor_control()) + ); + assert!( + frontier_usage + .actor_effect_used() + .all_lte(limits.shared_economic()) + ); + let control_remaining = limits + .actor_control() + .checked_sub(&frontier_usage.actor_control_used()) + .expect("actual Control remains contained"); + assert!(cohort_maxima.0.ref_time() <= control_remaining.ref_time()); + assert!(cohort_maxima.0.proof_size() > control_remaining.proof_size()); + let shared_remaining = limits + .shared_economic() + .checked_sub(&frontier_usage.actor_effect_used()) + .expect("actual Shared Economic remains contained"); + assert!(cohort_maxima.1.all_lte(shared_remaining)); + }); +} + +#[test] +fn create_user_charges_creation_fee_to_fee_sink() { + seeded_test_ext().execute_with(|| { + System::set_block_number(1); + let fee = ::ActorCreationFee::get(); + let fee_sink = ::FeeSink::get(); let sink_before = native_balance(&fee_sink); let alice_before = native_balance(&ALICE); let actor_id = create_user( @@ -2982,10 +4191,10 @@ fn actor_fee_collector_ignores_malformed_actor_and_scheduler_state() { let amount = crate::EXISTENTIAL_DEPOSIT; let payer_before = native_balance(&payer); let sink_before = native_balance(&fee_sink); - let hot_before = Actors::actor_hot(fee_sink_id).expect("Fee Sink hot state"); + let control_before = Actors::actor_control_cell(fee_sink_id).expect("Fee Sink control cell"); pallet_deos_actors::ActorFunding::::remove(fee_sink_id); - pallet_deos_actors::QueueTail::::put(1); - pallet_deos_actors::QueueOccupancy::::put(0); + pallet_deos_actors::ActorReadyTail::::put(1); + pallet_deos_actors::ActorReadyOccupancy::::put(0); let wakeup_len = ::MaxActiveActors::get(); pallet_deos_actors::WakeupCursorLen::::insert( pallet_deos_actors::WakeupClock::Block, @@ -3003,9 +4212,12 @@ fn actor_fee_collector_ignores_malformed_actor_and_scheduler_state() { assert_eq!(native_balance(&payer), payer_before - amount); assert_eq!(native_balance(&fee_sink), sink_before + amount); assert_eq!(Actors::actor_funding(fee_sink_id), None); - assert_eq!(Actors::actor_hot(fee_sink_id), Some(hot_before)); - assert_eq!(pallet_deos_actors::QueueTail::::get(), 1); - assert_eq!(pallet_deos_actors::QueueOccupancy::::get(), 0); + assert_eq!( + Actors::actor_control_cell(fee_sink_id), + Some(control_before) + ); + assert_eq!(pallet_deos_actors::ActorReadyTail::::get(), 1); + assert_eq!(pallet_deos_actors::ActorReadyOccupancy::::get(), 0); assert_eq!( pallet_deos_actors::WakeupCursorLen::::get(pallet_deos_actors::WakeupClock::Block), wakeup_len @@ -3046,7 +4258,7 @@ fn actor_fee_collector_rolls_back_completely_on_ledger_failure() { } #[test] -fn fee_sink_processes_ten_percent_and_splits_it_fifty_fifty() { +fn fee_sink_allocation_respects_security_phase_and_epoch_readiness() { seeded_test_ext().execute_with(|| { System::set_block_number(1); let fee_sink_id = primitives::ecosystem::actor_ids::FEE_SINK_ACTORS_ID; @@ -3062,6 +4274,8 @@ fn fee_sink_processes_ten_percent_and_splits_it_fifty_fifty() { ); let pool_before = native_balance(&staking_pool); let liquidity_before = native_balance(&staking_liquidity_actor); + let security_recipient = Staking::native_security_reward_account(); + let security_before = native_balance(&security_recipient); set_consensus_timestamp(1_000); System::set_block_number(2); @@ -3082,7 +4296,22 @@ fn fee_sink_processes_ten_percent_and_splits_it_fifty_fifty() { run_idle(Weight::MAX); run_next_idle(Weight::MAX); - // Each cycle processes 10% of the spendable buffer and splits that amount 50/50. + // The benchmark runtime selects LP-backed security without opening a reward epoch here. + if Staking::native_security_mode() == pallet_staking::NativeSecurityMode::LpBackedSelection { + assert!(has_actor_event(|event| matches!(event, + Event::StepFailed { actor_id, error, .. } + if *actor_id == fee_sink_id && *error == pallet_staking::Error::::NativeSecurityEpochNotOpen.into() + ))); + assert_eq!(native_balance(&staking_pool), pool_before); + assert_eq!(native_balance(&staking_liquidity_actor), liquidity_before); + assert_eq!(native_balance(&fee_sink), total); + assert_eq!(native_balance(&security_recipient), security_before); + assert!(has_actor_event(|event| matches!(event, + Event::CycleSummary { actor_id, result: CycleResult::Failed, .. } if *actor_id == fee_sink_id + ))); + return; + } + // Trusted-set cycles process 10% of the spendable buffer and split that amount 50/50. let pool_delta = native_balance(&staking_pool).saturating_sub(pool_before); let liquidity_delta = native_balance(&staking_liquidity_actor).saturating_sub(liquidity_before); let distributed = pool_delta.saturating_add(liquidity_delta); @@ -3294,6 +4523,208 @@ fn active_trigger_lifecycle_reconciles_only_the_dedicated_state_hold() { }); } +#[test] +fn observation_change_single_user_binds_fanout_and_scheduled_service() { + seeded_test_ext().execute_with(|| { + System::set_block_number(1); + let producer = deos_router_account(); + let feed = crate::configs::oracle_config::deos_router_pool_feed( + AssetKind::Native, + AssetKind::Local(8_089), + ); + assert_ok!(Oracle::register_feed( + RuntimeOrigin::root(), + feed, + producer.clone(), + feed.meaning(), + primitives::OracleProvenance::DeosRouterPreExecutionReserves, + feed.scale, + pallet_oracle::Aggregation::Ema { + half_life_blocks: 100, + }, + pallet_oracle::ZeroPolicy::Reject, + false, + )); + assert_ok!(Oracle::publish( + RuntimeOrigin::signed(producer.clone()), + feed, + 1_000_000_000_000, + )); + let actor_id = create_user( + ALICE, + RuntimeSchedule { + trigger: Trigger::observation_change(feed), + cooldown_blocks: 0, + }, + None, + transfer_contract_steps(BOB, AssetKind::Native, 1), + ); + fund_native(actor_id, 1_000_000_000_000_000); + type W = crate::weights::pallet_deos_actors::SubstrateWeight; + use pallet_deos_actors::WeightInfo as _; + assert_eq!( + Actors::observation_change_ingress_weight(), + W::observation_change_ingress(), + "Oracle ingress and Actors dirty-state ownership share one generated boundary" + ); + assert_ok!(Oracle::publish( + RuntimeOrigin::signed(producer), + feed, + 2_000_000_000_000, + )); + + let fanout = Actors::fanout_dirty_observations(Weight::MAX); + assert_eq!( + fanout, + W::observation_fanout_base() + .saturating_add(W::observation_fanout_branch_probe()) + .saturating_add(Actors::observation_fanout_ordinary_weight_upper()), + "one ordinary page owns base, branch selection and exactly one admitted branch unit" + ); + assert_eq!(fanout, Weight::from_parts(174_670_577_000, 309_950)); + let state = Actors::active_actor_state(actor_id).expect("ObservationChange Actor remains"); + assert!(state.hot.pending_signal); + assert!(state.hot.queue_ticket.is_some()); + + let recipient_before = Balances::free_balance(BOB); + System::set_block_number(2); + ensure_actor_prepass_context(); + assert_ok!(Actors::actor_prepass(RuntimeOrigin::none())); + let usage = Actors::block_resource_state() + .expect("ObservationChange service usage exists") + .usage(); + assert_eq!( + ( + usage.actor_control_used(), + usage.actor_effect_used(), + usage.user_dispatch_used() + ), + ( + Weight::from_parts(12_954_769_748, 141_918), + Weight::from_parts(2_293_433_000, 29_222), + Weight::zero() + ) + ); + assert_eq!( + System::events() + .iter() + .filter(|record| matches!( + &record.event, + RuntimeEvent::Actors(Event::TriggerOccurrenceProcessed { + actor_id: observed, + trigger_family: TriggerFamily::ObservationChange, + .. + }) if *observed == actor_id + )) + .count(), + 1 + ); + assert_eq!(Balances::free_balance(BOB), recipient_before + 1); + assert!( + Actors::active_actor_state(actor_id) + .is_some_and(|state| { !state.hot.pending_signal && state.hot.queue_ticket.is_none() }) + ); + }); +} + +#[test] +fn crossing_single_user_fire_binds_executed_detector_latch_control() { + seeded_test_ext().execute_with(|| { + System::set_block_number(1); + let producer = deos_router_account(); + let feed = crate::configs::oracle_config::deos_router_pool_feed( + AssetKind::Native, + AssetKind::Local(8_098), + ); + assert_ok!(Oracle::register_feed( + RuntimeOrigin::root(), + feed, + producer.clone(), + feed.meaning(), + primitives::OracleProvenance::DeosRouterPreExecutionReserves, + feed.scale, + pallet_oracle::Aggregation::Ema { + half_life_blocks: 100, + }, + pallet_oracle::ZeroPolicy::Reject, + false, + )); + assert_ok!(Oracle::publish( + RuntimeOrigin::signed(producer.clone()), + feed, + 1_000_000_000_000, + )); + let actor_id = create_user( + ALICE, + RuntimeSchedule { + trigger: Trigger::observation_crossing( + feed, + CrossingDirection::Rising, + 1_500_000_000_000, + 800_000_000_000, + ), + cooldown_blocks: 0, + }, + None, + transfer_contract_steps(BOB, AssetKind::Native, 1), + ); + fund_native(actor_id, 1_000_000_000_000_000); + assert_ok!(Oracle::publish( + RuntimeOrigin::signed(producer), + feed, + 100_000_000_000_000, + )); + + let consumed = Actors::service_crossing_transitions(Weight::MAX); + assert_eq!( + consumed, + Weight::from_parts(14_876_034_000, 386_687), + "the generated Crossing worker owners must settle the complete one-Actor detector/latch path" + ); + assert_eq!( + System::events() + .iter() + .filter(|record| matches!( + &record.event, + RuntimeEvent::Actors(Event::TriggerOccurrenceProcessed { + actor_id: observed, + trigger_family: TriggerFamily::ObservationCrossing, + .. + }) if *observed == actor_id + )) + .count(), + 1, + "one Crossing occurrence owns one latch transition" + ); + let state = Actors::active_actor_state(actor_id).expect("Crossing Actor remains active"); + assert!(state.hot.pending_signal); + assert!(state.hot.queue_ticket.is_some()); + + let recipient_before = Balances::free_balance(BOB); + System::set_block_number(2); + ensure_actor_prepass_context(); + assert_ok!(Actors::actor_prepass(RuntimeOrigin::none())); + let usage = Actors::block_resource_state() + .expect("scheduled service records resource usage") + .usage(); + assert_eq!( + usage.actor_control_used(), + Weight::from_parts(12_954_769_748, 141_918), + "the next-block prepass and one funded User service own the complete Control charge" + ); + assert_eq!( + usage.actor_effect_used(), + Weight::from_parts(2_293_433_000, 29_222), + "the Transfer effect remains separate from detector and service Control" + ); + assert_eq!(Balances::free_balance(BOB), recipient_before + 1); + assert!( + Actors::active_actor_state(actor_id) + .is_some_and(|state| { !state.hot.pending_signal && state.hot.queue_ticket.is_none() }) + ); + }); +} + #[test] fn crossing_capacity_policy_is_bound_to_measured_minimum_progress_and_explicit_horizons() { use pallet_deos_actors::WeightInfo as _; @@ -3351,6 +4782,14 @@ fn crossing_capacity_policy_is_bound_to_measured_minimum_progress_and_explicit_h let user_blocks = u64::from(user_cap).div_ceil(candidates_per_block); let total_blocks = u64::from(total_cap).div_ceil(candidates_per_block); assert_eq!((user_cap, total_cap), (9_000, 10_000)); + const REQUIRED_REACTIVE_USER_TARGET: u64 = 10_000; + const REQUIRED_ELIGIBLE_BLOCKS: u64 = 100; + assert!(u64::from(user_cap) < REQUIRED_REACTIVE_USER_TARGET); + assert_eq!(candidates_per_block * REQUIRED_ELIGIBLE_BLOCKS, 400); + assert!( + candidates_per_block * REQUIRED_ELIGIBLE_BLOCKS < REQUIRED_REACTIVE_USER_TARGET, + "current production Crossing admission cannot satisfy the governing User target" + ); assert_eq!((user_blocks, total_blocks), (2_250, 2_500)); assert_eq!( total_blocks * 6, @@ -3404,10 +4843,13 @@ fn reactive_delivery_envelopes_follow_production_weights_and_topology_bounds() { .min(available.ref_time() / admitted_unit.ref_time()) .min(available.proof_size() / admitted_unit.proof_size()); - 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)); + println!( + "ACTOR_REACTIVE_ENVELOPES_V1 base={base:?} branch_probe={branch_probe:?} unit={unit:?} fault={fault:?} units_per_block={units_per_block}" + ); + assert_eq!(base, Weight::from_parts(56_775_000, 1_629)); + assert_eq!(branch_probe, Weight::from_parts(64_247_000, 3_587)); + assert_eq!(unit, Weight::from_parts(174_549_555_000, 304_734)); + assert_eq!(fault, Weight::from_parts(209_851_000, 4_106)); assert_eq!(limit, Weight::from_parts(400_000_000_000, 1_000_000)); assert_eq!( units_per_block, 2, @@ -3441,7 +4883,9 @@ fn sched_workers_static_envelope_leaves_one_actor_unit_inside_guaranteed_budget( // Maximum wakeup worker envelope: cursor probe plus one worst-case complete wakeup unit per // `MaxWakeupsPerBlock` slot, capped by the dedicated two-dimensional `WakeupWeightLimit`. let cursor_probe = W::scheduler_wakeup_cursor_worker_future(); - let wakeup_unit = crate::Actors::wakeup_cursor_drain_unit_weight_upper(true); + let wakeup_unit = crate::Actors::wakeup_cursor_drain_unit_weight_upper( + pallet_deos_actors::WakeupBucketDisposition::Remove, + ); let wakeup_ceiling = ::WakeupWeightLimit::get(); let wakeup_per_block = u64::from(::MaxWakeupsPerBlock::get()); @@ -4696,6 +6140,19 @@ 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 first_usage = Actors::block_resource_state() + .expect("first retry usage exists") + .usage(); + assert_eq!( + ( + first_usage.actor_control_used(), + first_usage.actor_effect_used() + ), + ( + Weight::from_parts(15_253_398_980, 165_779), + Weight::from_parts(3_443_678_000, 19_253) + ) + ); let continuation = Actors::actor_run_state(actor_id).expect("deviation suspends"); assert_eq!(continuation.unsuccessful_attempts_at_cursor, 1); assert_eq!(continuation.cursor, 0); @@ -4706,6 +6163,20 @@ fn excessive_system_reference_deviation_suspends_without_fill_and_backs_off() { System::set_block_number(2); run_idle(Weight::MAX); + let second_usage = Actors::block_resource_state() + .expect("long-retry placement usage exists") + .usage(); + assert_eq!( + ( + second_usage.actor_control_used(), + second_usage.actor_effect_used() + ), + ( + Weight::from_parts(6_526_601_980, 90_386), + Weight::from_parts(3_443_678_000, 19_253) + ), + "the second failure owns long-Wakeup placement rather than another Ready ticket" + ); let continuation = Actors::actor_run_state(actor_id).expect("deviation resuspends"); assert_eq!(continuation.unsuccessful_attempts_at_cursor, 2); assert_eq!(continuation.cursor, 0); @@ -4724,6 +6195,40 @@ fn excessive_system_reference_deviation_suspends_without_fill_and_backs_off() { .. } if *id == actor_id ))); + + System::set_block_number(4); + run_idle(Weight::MAX); + let return_usage = Actors::block_resource_state() + .expect("wakeup return usage exists") + .usage(); + assert_eq!( + ( + return_usage.actor_control_used(), + return_usage.actor_effect_used() + ), + (Weight::from_parts(8_506_170_748, 95_611), Weight::zero()), + "Wakeup return is Control-only and cannot execute the retry in the same block" + ); + System::set_block_number(5); + run_idle(Weight::MAX); + let resumed_usage = Actors::block_resource_state() + .expect("returned retry service usage exists") + .usage(); + assert_eq!( + ( + resumed_usage.actor_control_used(), + resumed_usage.actor_effect_used() + ), + ( + Weight::from_parts(15_650_484_980, 172_272), + Weight::from_parts(3_443_678_000, 19_253) + ), + "returned service owns the third failed effect separately from Wakeup return" + ); + assert_eq!(native_balance(&actor), before); + assert!(Actors::actor_run_state(actor_id).is_none()); + assert!(Actors::active_actor_state(actor_id).is_none()); + assert!(Actors::actor_hot(actor_id).is_none()); }); } @@ -5117,30 +6622,387 @@ fn staking_adapter_supports_liquid_native_stake_without_operator_context() { } #[test] -fn actor_unstake_percentage_current_resolves_live_staking_shares() { +fn actor_liquidity_retry_skips_frozen_current_balance_without_moving_custody() { seeded_test_ext().execute_with(|| { - System::set_block_number(1); - assert_ok!(create_test_asset(0, &ALICE)); - assert_ok!(crate::Staking::register_staking_asset( - RuntimeOrigin::root(), - 0 + const FREEZABLE_ASSET: u32 = 901_003; + let liquidity = 1_000_000_000_000_000u128; + let local = AssetKind::Local(FREEZABLE_ASSET); + assert_ok!(create_test_asset(FREEZABLE_ASSET, &ALICE)); + assert_ok!(mint_tokens(FREEZABLE_ASSET, &ALICE, &ALICE, liquidity * 2)); + assert_ok!(create_pool( + RuntimeOrigin::signed(ALICE), + AssetKind::Native, + local )); - assert_ok!(Assets::set_team( + assert_ok!(add_liquidity( RuntimeOrigin::signed(ALICE), - 0, - ALICE.into(), - ALICE.into(), - ALICE.into(), + AssetKind::Native, + local, + liquidity, + liquidity, + 1, + 1, + &ALICE, )); - let steps = BoundedVec::try_from(vec![make_step(Task::Unstake { - asset: AssetKind::Native, - shares: AmountResolution::PercentageOfCurrent(Perbill::from_percent(50)), - })]) - .expect("steps fits"); - let actor_id = create_user(BOB, manual_schedule(), None, steps); - let actor = actor_account(actor_id); - let stake_amount = crate::EXISTENTIAL_DEPOSIT.saturating_mul(10); - assert_ok!(mint_tokens( + let steps = BoundedVec::try_from(vec![ + make_step(Task::Transfer { + to: BOB, + asset: AssetKind::Local(ASSET_A), + amount: AmountResolution::PercentageOfCurrent(Perbill::from_percent(50)), + }), + StepOf:: { + precondition: None, + task: Task::AddLiquidity { + asset_a: AssetKind::Native, + asset_b: local, + amount_a: AmountResolution::PercentageOfCurrent(Perbill::from_percent(50)), + amount_b: AmountResolution::PercentageOfCurrent(Perbill::from_percent(50)), + min_lp_out: Balance::MAX, + }, + on_error: StepErrorPolicy::RetryLater { max_attempts: 3 }, + }, + ]) + .expect("prefix and retry Step fit"); + let mut contract = + system_active_contract(manual_schedule(), None, steps).expect("active Contract exists"); + contract.cooldown_blocks = 10; + let actor_id = Actors::next_actor_id(); + assert_ok!(Actors::create_system_actor( + RuntimeOrigin::root(), + ALICE, + Mutability::Mutable, + Some(contract), + )); + let actor = actor_account(actor_id); + let amount = liquidity / 10; + let _ = >::deposit_creating(&actor, amount); + assert_ok!(mint_tokens(FREEZABLE_ASSET, &ALICE, &actor, amount)); + assert_eq!( + TmctolAssetOps::balance(&actor, AssetKind::Local(ASSET_A)), + 0 + ); + let custody = ( + native_balance(&actor), + Assets::balance(FREEZABLE_ASSET, &actor), + ); + let reserves = crate::AssetConversion::get_reserves(AssetKind::Native, local) + .expect("seeded pool has reserves"); + System::set_block_number(2); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + run_idle(Weight::MAX); + let run = Actors::actor_run_state(actor_id).expect("real Temporary failure suspends"); + assert_eq!(run.cursor, 1); + assert_eq!(run.last_committed_step_block, Some(2)); + assert!( + run.last_attempt_block > 2, + "tail attempt follows the real Q1 prefix" + ); + assert_eq!(run.unsuccessful_attempts_at_cursor, 1); + // SCALE retains the Other variant, not its static diagnostic string. The liquidity adapter's + // Temporary branch is the minimum-LP rejection; compare its persisted representation. + assert!( + matches!(run.last_step_outcome, + Some(StepOutcome::Failed(ref failure)) if failure.retry == RetryClass::Temporary + && failure.error.encode() == DispatchError::Other("MinimumLpOutputNotMet").encode()), + "expected the persisted Temporary LP-output rejection, got {:?}", + run.last_step_outcome + ); + assert_eq!( + ( + native_balance(&actor), + Assets::balance(FREEZABLE_ASSET, &actor) + ), + custody + ); + assert_eq!( + crate::AssetConversion::get_reserves(AssetKind::Native, local).unwrap(), + reserves + ); + let available = TmctolAssetOps::balance(&actor, local); + assert!(available > 0); + #[cfg(feature = "runtime-benchmarks")] + assert_ok!( + >::set_asset_account_frozen(&ALICE, &actor, local, true) + ); + #[cfg(not(feature = "runtime-benchmarks"))] + assert_ok!(Assets::freeze( + RuntimeOrigin::signed(ALICE), + FREEZABLE_ASSET, + actor.clone().into() + )); + assert_eq!(TmctolAssetOps::balance(&actor, local), 0); + assert_eq!( + ( + native_balance(&actor), + Assets::balance(FREEZABLE_ASSET, &actor) + ), + custody + ); + assert_eq!( + Actors::actor_run_state(actor_id) + .expect("freeze does not mutate Run") + .encode(), + run.encode() + ); + + // Due materialization and FIFO service may occupy different ordinary block passes. + // Keep the frozen custody across the waiting interval and two subsequent service blocks. + let first_waiting_block = System::block_number() + .checked_add(1) + .expect("next block fits"); + let last_service_block = run + .eligible_at + .checked_add(2) + .expect("service horizon fits"); + let mut events = Vec::new(); + for block in first_waiting_block..=last_service_block { + 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), + ); + Actors::on_initialize(block); + ensure_current_resource_state(); + Actors::on_idle(block, Weight::MAX); + events = actor_events(); + if block < run.eligible_at { + assert_eq!( + Actors::actor_run_state(actor_id) + .expect("retry is not due") + .encode(), + run.encode() + ); + assert!(!events.iter().any(|event| matches!(event, + Event::StepSkipped { actor_id: id, .. } | Event::StepFailed { actor_id: id, .. } + | Event::LiquidityAdded { actor_id: id, .. } if *id == actor_id + ))); + } + if Actors::actor_run_state(actor_id).is_none() { + assert!( + block >= run.eligible_at, + "retry cannot execute before eligibility" + ); + break; + } + } + assert_eq!( + events + .iter() + .filter(|event| matches!(event, + Event::StepSkipped { actor_id: id, cycle_nonce: 1, step_index: 1, + reason: StepSkippedReason::ResolutionSkipped } if *id == actor_id + )) + .count(), + 1, + "due retry must resolve-skip; hot={:?}, run={:?}, events={:?}", + Actors::actor_hot(actor_id), + Actors::actor_run_state(actor_id).map(|run| ( + run.cursor, + run.eligible_at, + run.last_step_outcome + )), + events + ); + assert!(!events.iter().any(|event| matches!(event, + Event::LiquidityAdded { actor_id: id, .. } | Event::StepFailed { actor_id: id, .. } + if *id == actor_id + ))); + let state = Actors::active_actor_state(actor_id).expect("retry completion retains authority"); + assert_eq!(state.identity.cycle_nonce, 1); + assert_eq!(state.hot.cycle_state, pallet_deos_actors::CycleState::Idle); + assert!(state.run_state.is_none()); + assert_eq!( + ( + native_balance(&actor), + Assets::balance(FREEZABLE_ASSET, &actor) + ), + custody + ); + assert_eq!( + crate::AssetConversion::get_reserves(AssetKind::Native, local).unwrap(), + reserves + ); + let telemetry = Actors::finalized_block_resource_telemetry().expect("retry usage recorded"); + assert_eq!(telemetry.usage().actor_effect_used(), Weight::zero()); + #[cfg(feature = "runtime-benchmarks")] + assert_ok!( + >::set_asset_account_frozen(&ALICE, &actor, local, false) + ); + #[cfg(not(feature = "runtime-benchmarks"))] + assert_ok!(Assets::thaw( + RuntimeOrigin::signed(ALICE), + FREEZABLE_ASSET, + actor.clone().into() + )); + assert_eq!(TmctolAssetOps::balance(&actor, local), available); + assert_eq!( + ( + native_balance(&actor), + Assets::balance(FREEZABLE_ASSET, &actor) + ), + custody + ); + #[cfg(feature = "try-runtime")] + assert_ok!(>::try_state( + System::block_number() + )); + }); +} + +#[test] +fn actor_unstake_last_funding_fails_before_effect_after_empty_receipt_destruction() { + seeded_test_ext().execute_with(|| { + System::set_block_number(1); + assert_ok!(create_test_asset(0, &ALICE)); + assert_ok!(Staking::register_staking_asset(RuntimeOrigin::root(), 0)); + let receipt = Staking::staked_asset_id_for_queries(0).expect("live empty receipt class"); + assert_eq!(Assets::total_supply(receipt), 0); + let steps = BoundedVec::try_from(vec![make_step(Task::Unstake { + asset: AssetKind::Native, + shares: AmountResolution::PercentageOfLastFunding(Perbill::from_percent(50)), + })]) + .expect("one admitted Step fits"); + let mut contract = + system_active_contract(manual_schedule(), None, steps).expect("active Contract exists"); + contract.funding = FundingSourcePolicy::OwnerOnly; + let actor_id = Actors::next_actor_id(); + assert_ok!(Actors::create_system_actor( + RuntimeOrigin::root(), + ALICE, + Mutability::Mutable, + Some(contract), + )); + let actor = actor_account(actor_id); + assert!( + actor_funding(actor_id) + .funding_tracked_assets + .contains(&AssetKind::Local(receipt)) + ); + assert!(actor_funding(actor_id).funding_accumulated.is_empty()); + + // Destroy the empty class through its authorized lifecycle, not Actor storage mutation. + #[cfg(feature = "runtime-benchmarks")] + assert_ok!( + >::remove_empty_staking_receipt(&ALICE, AssetKind::Native) + ); + #[cfg(not(feature = "runtime-benchmarks"))] + { + assert_ok!(Assets::start_destroy(RuntimeOrigin::root(), receipt)); + assert_ok!(Assets::finish_destroy( + RuntimeOrigin::signed(ALICE), + receipt + )); + } + assert_eq!(Staking::staked_asset_id_for_queries(0), None); + let pool = Staking::pool_account_for(0); + let custody = ( + native_balance(&actor), + Assets::balance(0, &actor), + Assets::balance(0, &pool), + ); + System::set_block_number(2); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + System::reset_events(); + run_idle(Weight::MAX); + + let events: Vec<_> = System::events() + .into_iter() + .map(|record| record.event) + .collect(); + assert_eq!( + events + .iter() + .filter(|event| matches!(event, + RuntimeEvent::Actors(Event::StepFailed { actor_id: id, cycle_nonce: 1, step_index: 0, + retry_class: RetryClass::Permanent, error }) + if *id == actor_id && *error == Error::::InvalidAmountResolution.into() + )) + .count(), + 1 + ); + assert!(!events.iter().any(|event| matches!(event, + RuntimeEvent::Actors(Event::UnstakeExecuted { actor_id: id, .. }) if *id == actor_id + ))); + assert!(events.iter().any(|event| matches!(event, + RuntimeEvent::Actors(Event::CycleSummary { actor_id: id, cycle_nonce: 1, + result: CycleResult::Failed, outcomes }) + if *id == actor_id && outcomes.failed_steps == 1 && outcomes.executed_steps == 0 + ))); + assert_eq!( + ( + native_balance(&actor), + Assets::balance(0, &actor), + Assets::balance(0, &pool) + ), + custody + ); + let state = + Actors::active_actor_state(actor_id).expect("failed cycle retains canonical authority"); + assert_eq!(state.identity.cycle_nonce, 1); + assert_eq!(state.hot.cycle_state, pallet_deos_actors::CycleState::Idle); + assert!(!state.hot.pending_signal); + assert!(state.run_state.is_none()); + let telemetry = + Actors::finalized_block_resource_telemetry().expect("ordinary FIFO records resource usage"); + assert_eq!( + telemetry.usage().actor_effect_used(), + Weight::zero(), + "preparation failed before Task invocation" + ); + #[cfg(feature = "try-runtime")] + assert_ok!(>::try_state( + System::block_number() + )); + }); +} + +#[test] +fn actor_unstake_percentage_current_resolves_live_staking_shares() { + seeded_test_ext().execute_with(|| { + System::set_block_number(1); + assert_ok!(create_test_asset(0, &ALICE)); + assert_ok!(crate::Staking::register_staking_asset( + RuntimeOrigin::root(), + 0 + )); + assert_ok!(Assets::set_team( + RuntimeOrigin::signed(ALICE), + 0, + ALICE.into(), + ALICE.into(), + ALICE.into(), + )); + let steps = BoundedVec::try_from(vec![make_step(Task::Unstake { + asset: AssetKind::Native, + shares: AmountResolution::PercentageOfCurrent(Perbill::from_percent(50)), + })]) + .expect("steps fits"); + let actor_id = create_user(BOB, manual_schedule(), None, steps); + let actor = actor_account(actor_id); + let stake_amount = crate::EXISTENTIAL_DEPOSIT.saturating_mul(10); + assert_ok!(mint_tokens( 0, &ALICE, &actor, @@ -6265,7 +8127,7 @@ fn genesis_system_locator_is_recoverable_after_close_through_reattachment() { let original_sovereign_balance_before = Balances::free_balance(&sovereign); let preserved = crate::EXISTENTIAL_DEPOSIT.saturating_mul(777); let _ = >::deposit_creating(&sovereign, preserved); - let original_identity = Actors::actor_identities(fee_sink_id).expect("genesis identity"); + let original_identity = Actors::actor_identity(fee_sink_id).expect("genesis identity"); let _original_nonce = original_identity.cycle_nonce; assert_ok!(Actors::close_actor(RuntimeOrigin::root(), fee_sink_id)); @@ -6618,7 +8480,9 @@ fn xcm_source_less_scheduler_exhaustion_closes_actor_and_preserves_deposit() { ); let sovereign = actor_account(actor_id); let recipient = account_location(sovereign.clone()); - pallet_deos_actors::NextQueueTicket::::put(u64::MAX); + assert_eq!(pallet_deos_actors::ActorReadyOccupancy::::get(), 0); + pallet_deos_actors::ActorReadyHead::::put(u64::MAX); + pallet_deos_actors::ActorReadyTail::::put(u64::MAX); let before = native_balance(&sovereign); let asset = native_xcm_asset(5_000); assert_ok!( @@ -7113,14 +8977,14 @@ fn wakeup_drain_admission_uses_generated_runtime_weights() { at_time.proof_size().max(cadence.proof_size()), ); assert_eq!( - Actors::wakeup_cursor_drain_unit_weight_upper(false), + Actors::wakeup_cursor_drain_unit_weight_upper(pallet_deos_actors::WakeupBucketDisposition::Retain), <::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), + Actors::wakeup_cursor_drain_unit_weight_upper(pallet_deos_actors::WakeupBucketDisposition::Remove), <::WeightInfo as WeightInfo>::scheduler_wakeup_cursor_worker_remove() .saturating_add(temporal) .saturating_add(close) @@ -7233,7 +9097,9 @@ fn certified_extension_scheduler_exhaustion_closes_actor_without_rejecting_value let sovereign = actor_account(actor_id); // Monotonic ticket namespace at the ceiling closes the destination Actor // through the unified sink without rejecting the certified movement. - pallet_deos_actors::NextQueueTicket::::put(u64::MAX); + assert_eq!(pallet_deos_actors::ActorReadyOccupancy::::get(), 0); + pallet_deos_actors::ActorReadyHead::::put(u64::MAX); + pallet_deos_actors::ActorReadyTail::::put(u64::MAX); let sovereign_before = native_balance(&sovereign); let signer_before = native_balance(&signer); let transfer_amount = 25_000_000_000_000u128; @@ -7269,7 +9135,9 @@ fn asset_ops_transfer_preserves_value_while_closing_scheduler_exhaustion() { let _ = >::deposit_creating(&ALICE, 100_000_000_000_000); // Monotonic ticket exhaustion closes the destination Actor while preserving // the already-certified movement (spec 6.1). - pallet_deos_actors::NextQueueTicket::::put(u64::MAX); + assert_eq!(pallet_deos_actors::ActorReadyOccupancy::::get(), 0); + pallet_deos_actors::ActorReadyHead::::put(u64::MAX); + pallet_deos_actors::ActorReadyTail::::put(u64::MAX); let actor_before = native_balance(&sovereign); assert_ok!(TmctolAssetOps::transfer( &ALICE, @@ -7307,7 +9175,9 @@ fn asset_ops_source_less_mint_closes_scheduler_exhaustion_after_movement() { BoundedVec::try_from(vec![make_step(inert_task())]).expect("execution plan fits"), ); let sovereign = actor_account(actor_id); - pallet_deos_actors::NextQueueTicket::::put(u64::MAX); + assert_eq!(pallet_deos_actors::ActorReadyOccupancy::::get(), 0); + pallet_deos_actors::ActorReadyHead::::put(u64::MAX); + pallet_deos_actors::ActorReadyTail::::put(u64::MAX); let before = native_balance(&sovereign); assert_ok!(TmctolAssetOps::mint(&sovereign, AssetKind::Native, 5_000)); assert_eq!(native_balance(&sovereign), before.saturating_add(5_000)); @@ -7917,6 +9787,105 @@ fn maximum_user_creation_and_replacement_pass_real_transaction_extensions() { }); } +#[test] +fn running_inner_fixture_admission_frontier_is_class_neutral() { + seeded_test_ext().execute_with(|| { + let maximum = ::MaxContractSteps::get(); + let predicates = ::MaxPredicatesPerStep::get(); + let chunk = pallet_deos_actors::MAX_STEPS_PER_TAIL_CHUNK; + let funding = ::MaxFundingTrackedAssets::get(); + let service = Actors::guaranteed_actor_service_weight().expect("reference service exists"); + let overhead = Actors::scheduler_admission_overhead(); + let cleanup = Actors::close_dispatch_weight_upper(); + for complete in [true, false] { + for fragment in (if complete { 1 } else { 2 })..=chunk { + let count = if !complete && fragment == chunk { + maximum + } else { + 1 + fragment + ((maximum - 1 - fragment) / chunk) * chunk + }; + let target = if complete { + count - 1 + } else if fragment == chunk { + 1 + } else { + count - fragment + }; + for current_predicates in [0, predicates] { + let mut steps: ContractSteps = (0..count).map(|index| { + let a = if index == 0 { AssetKind::Native } else { AssetKind::Local(10_000 + index * 2) }; + let b = AssetKind::Local(10_001 + index * 2); + RuntimeStep { + precondition: all_preconditions_at((0..predicates).map(|p| { + let asset = if p % 2 == 0 { a } else { b }; + pallet_deos_actors::Predicate::BalanceAbove { + asset, + threshold: if asset == AssetKind::Native { u128::MAX - u128::from(p + 1) } else { u128::from(p + 1) }, + } + }).collect(), pallet_deos_actors::ObservationTiming::Opening), + task: Task::AddLiquidity { + asset_a: a, asset_b: b, + amount_a: AmountResolution::PercentageAtOpening(Perbill::one()), + amount_b: AmountResolution::PercentageAtOpening(Perbill::one()), + min_lp_out: 1, + }, + on_error: StepErrorPolicy::AbortCycle, + } + }).collect::>().try_into().expect("reference Contract bound fits"); + let full_geometry = Actors::contract_steps_admission_weight_upper(ActorType::System, &steps); + steps[target as usize] = RuntimeStep { + precondition: all_preconditions((0..current_predicates).map(|p| { + pallet_deos_actors::Predicate::BalanceAbove { + asset: AssetKind::Local(20_000 + p), threshold: 1, + } + }).collect()), + task: if complete { Task::StopCycle } else { + Task::Transfer { to: BOB, asset: AssetKind::Local(30_000), + amount: AmountResolution::PercentageOfCurrent(Perbill::from_percent(50)) } + }, + on_error: StepErrorPolicy::AbortCycle, + }; + let required = Actors::contract_steps_admission_weight_upper(ActorType::System, &steps); + assert_eq!(required, Actors::contract_steps_admission_weight_upper(ActorType::User, &steps)); + let mut maximum_step = Weight::zero(); + let mut opening_control = Weight::zero(); + let mut opening_effect = Weight::zero(); + let mut ref_cursor = 0; + let mut proof_cursor = 0; + for (cursor, step) in steps.iter().enumerate() { + let cursor = cursor as u32; + let context = StepControlWeightContext { + cursor, + steps_in_fragment: if cursor == 0 { 1 } else { + (count - (1 + ((cursor - 1) / chunk) * chunk)).min(chunk) + }, + opening_tail_chunks: if cursor == 0 { (count - 1).div_ceil(chunk) } else { 0 }, + predicate_evaluation_units: step.precondition.as_ref().map_or(0, |p| p.evaluation_units()), + opening_snapshot_entries: if cursor == 0 { 2 * (count - 1) } else { 0 }, + opening_predicate_results: if cursor == 0 { predicates * (count - 1) } else { 0 }, + funding_snapshot_entries: if cursor == 0 { funding } else { 0 }, + }; + let control = RuntimeStepControlWeight::maximum_control_weight(context, step) + .expect("authored context has a control owner"); + let effect = TmctolTaskEffectWeight::maximum_effect_weight(&step.task) + .expect("authored Task has an effect owner"); + if cursor == 0 { + opening_control = control; + opening_effect = effect; + } + let total = control.checked_add(&effect).expect("bounded Step sum fits"); + if total.ref_time() > maximum_step.ref_time() { ref_cursor = cursor; } + if total.proof_size() > maximum_step.proof_size() { proof_cursor = cursor; } + maximum_step = Weight::from_parts(maximum_step.ref_time().max(total.ref_time()), maximum_step.proof_size().max(total.proof_size())); + } + assert_eq!(required, overhead.checked_add(&maximum_step).and_then(|w| w.checked_add(&cleanup)).expect("admission sum fits")); + println!("ACTOR_RUNNING_ADMISSION_V1 complete={complete} fragment={fragment} predicates={current_predicates} steps={count} target={target} opening_entries={} opening_results={} funding_bound={funding} required={required:?} service={service:?} fits={} overhead={overhead:?} cleanup={cleanup:?} maximum_step={maximum_step:?} ref_cursor={ref_cursor} proof_cursor={proof_cursor} opening_control={opening_control:?} opening_effect={opening_effect:?} full_geometry={full_geometry:?}", 2 * (count - 1), predicates * (count - 1), required.all_lte(service)); + } + } + } + }); +} + #[test] fn actor_resource_runtime_api_backing_projects_budget_current_state_and_finalized_telemetry() { seeded_test_ext().execute_with(|| { @@ -8660,7 +10629,7 @@ fn configured_on_idle_reserve_admits_every_genesis_actor_with_pure_cleanup() { let mut actor_count = 0u32; let mut max_ref_time = (0u64, 0u64); let mut max_proof_size = (0u64, 0u64); - for actor_id in pallet_deos_actors::ActorHot::::iter_keys() { + for actor_id in pallet_deos_actors::ActorControlLocators::::iter_keys() { let instance = Actors::active_actor_state(actor_id).expect("split active actor exists"); let required = Actors::contract_steps_admission_weight_upper( instance.identity.actor_class.actor_type(), @@ -9232,13 +11201,11 @@ fn run_blocks_with_queue_diagnostics( _ => {} } } - let queue_occupancy = Actors::queue_tail() - .saturating_sub(Actors::queue_head()) - .min(u64::from(u32::MAX)) as u32; + let queue_occupancy = Actors::queue_occupancy(); let mut wakeup_backlog = 0u32; let mut wakeup_buckets = 0u32; - for (_, bucket) in pallet_deos_actors::WakeupBuckets::::iter() { - wakeup_backlog = wakeup_backlog.saturating_add(bucket.live_entries); + for (_, live_entries) in pallet_deos_actors::ActorWaitingOccupancies::::iter() { + wakeup_backlog = wakeup_backlog.saturating_add(live_entries); wakeup_buckets = wakeup_buckets.saturating_add(1); } queue_diag.max_queue_occupancy = queue_diag.max_queue_occupancy.max(queue_occupancy); @@ -9271,7 +11238,7 @@ fn assert_core_stability(actor_ids: &[u64], diag: &StressDiagnostics) { /// Under-capacity: 45 chain actors plus active genesis work remain inside the /// configurable execution ceiling and receive recurring service through the measured reserve. -/// Dormant and custody-only genesis addresses never compete for scheduler capacity. +/// Dormant genesis identities, including sealed immutable Anchors, never compete for scheduler capacity. /// /// Asserts exact balance conservation, complete traversal, bounded fairness, /// zero failures, and zero unsuccessful-attempt streak. @@ -9340,7 +11307,7 @@ fn diagnose_over_capacity_first_blocks() { let initial_balance: u128 = 1_000_000 * crate::EXISTENTIAL_DEPOSIT; let (_actor_ids, _sovereign_accounts) = setup_circular_chain(chain_len, initial_balance); println!("\n=== Initial state ==="); - let active_count = pallet_deos_actors::ActorHot::::iter_keys().count(); + let active_count = pallet_deos_actors::ActorControlLocators::::iter_keys().count(); println!("Active instances len: {}", active_count); for block in 2..=6 { System::set_block_number(block); @@ -9398,7 +11365,7 @@ fn diagnose_over_capacity_first_blocks() { println!( "Actors {} present: {}", id, - pallet_deos_actors::ActorHot::::contains_key(id) + pallet_deos_actors::ActorControlLocators::::contains_key(id) ); } }); @@ -9456,7 +11423,7 @@ fn circular_chain_respects_execution_ceiling_and_remains_fair() { zero_actors={:?}, active_actors_len={})", min_count, &zero_actors[..zero_actors.len().min(10)], - pallet_deos_actors::ActorHot::::iter_keys().count(), + pallet_deos_actors::ActorControlLocators::::iter_keys().count(), ); // Fairness: examine cycle_nonce spread across chain actors. // With identical periodic actors, the queue scheduler should keep nonce spread minimal (≤ 2). @@ -9485,59 +11452,34 @@ fn circular_chain_respects_execution_ceiling_and_remains_fair() { }); } -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::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); - } - let identities: alloc::vec::Vec<_> = - pallet_deos_actors::ActorIdentities::::iter().collect(); - for (actor_id, identity) in identities { - pallet_deos_actors::ActorIdentities::::remove(actor_id); - pallet_deos_actors::SovereignIndex::::remove(&identity.sovereign_account); - } - // Isolate the synthetic active-capacity profile from retained genesis locators. - // Production close preserves those locators for deterministic reattachment. - let _ = pallet_deos_actors::SystemSovereigns::::clear(u32::MAX, None); - pallet_deos_actors::SystemSovereignCount::::put(0); - let _ = pallet_deos_actors::WakeupPages::::clear(u32::MAX, None); - let _ = pallet_deos_actors::WakeupBuckets::::clear(u32::MAX, None); - let _ = pallet_deos_actors::WakeupCursorPages::::clear(u32::MAX, None); - let _ = pallet_deos_actors::WakeupCursorLen::::clear(u32::MAX, None); - let _ = pallet_deos_actors::QueuePages::::clear(u32::MAX, None); - pallet_deos_actors::QueueHead::::put(0); - pallet_deos_actors::QueueTail::::put(0); - pallet_deos_actors::QueueOccupancy::::put(0); - pallet_deos_actors::NextQueueTicket::::put(0); - pallet_deos_actors::ActiveActorCount::::put(0); - pallet_deos_actors::ActorIdentityCount::::put(0); -} - -fn close_genesis_system_actors() { - clear_genesis_system_actors_for_stress_fixture(); +fn assert_synthetic_actor_genesis() { + assert_eq!(Actors::active_actor_count(), 0); + assert_eq!(Actors::actor_identity_count(), 0); + assert_eq!(Actors::queue_occupancy(), 0); + assert!( + pallet_deos_actors::ActorControlLocators::::iter_keys() + .next() + .is_none() + ); + assert!( + pallet_deos_actors::ActorWaitingOccupancies::::iter() + .next() + .is_none() + ); } fn run_fairness_matrix_case(total_actors: u64, num_blocks: u32) -> StressDiagnostics { System::set_block_number(1); - close_genesis_system_actors(); + assert_synthetic_actor_genesis(); assert_eq!( - pallet_deos_actors::ActorHot::::iter_keys().count(), + pallet_deos_actors::ActorControlLocators::::iter_keys().count(), 0, - "Genesis actors must be removed for isolated fairness matrix", + "Synthetic genesis must contain no active actors before the fairness matrix", ); let initial_balance = 10_000u128; let actor_ids = setup_inert_actors(total_actors, initial_balance); - let active_count = pallet_deos_actors::ActorHot::::iter_keys().count() as u64; + let active_count = + pallet_deos_actors::ActorControlLocators::::iter_keys().count() as u64; assert_eq!( active_count, total_actors, "Scenario must start with exact actor count (expected={}, got={})", @@ -9591,18 +11533,17 @@ fn run_fairness_matrix_case(total_actors: u64, num_blocks: u32) -> StressDiagnos #[test] fn scheduler_fast_fifo_dense_vs_sparse_topology_smoke() { - use super::common::new_test_ext; let scenarios: [(u64, u32, u64); 2] = [(64, 96, 8), (256, 128, 16)]; for (actors, blocks, stride) in scenarios { - let dense_diag = new_test_ext().execute_with(|| { + let dense_diag = synthetic_actor_test_ext().execute_with(|| { System::set_block_number(1); - close_genesis_system_actors(); + assert_synthetic_actor_genesis(); let actor_ids = setup_inert_actors(actors, 10_000u128); run_blocks_with_diagnostics(&actor_ids, blocks, Weight::MAX) }); - let sparse_diag = new_test_ext().execute_with(|| { + let sparse_diag = synthetic_actor_test_ext().execute_with(|| { System::set_block_number(1); - close_genesis_system_actors(); + assert_synthetic_actor_genesis(); let actor_ids = setup_inert_actors_sparse(actors, 10_000u128, stride); run_blocks_with_diagnostics(&actor_ids, blocks, Weight::MAX) }); @@ -9648,10 +11589,9 @@ fn scheduler_fast_fifo_dense_vs_sparse_topology_smoke() { #[test] fn scheduler_fast_fifo_sparse_topology_liveness_smoke() { - use super::common::new_test_ext; - new_test_ext().execute_with(|| { + synthetic_actor_test_ext().execute_with(|| { System::set_block_number(1); - close_genesis_system_actors(); + assert_synthetic_actor_genesis(); let actors = 256u64; let blocks = 192u32; let stride = 32u64; @@ -9677,10 +11617,9 @@ fn scheduler_fast_fifo_sparse_topology_liveness_smoke() { #[test] fn reference_idle_budget_admits_mixed_tasks_without_starvation() { - use super::common::new_test_ext; - new_test_ext().execute_with(|| { + synthetic_actor_test_ext().execute_with(|| { System::set_block_number(1); - close_genesis_system_actors(); + assert_synthetic_actor_genesis(); let mut actor_ids = setup_inert_actors(32, 10_000u128); let (transfer_ids, _) = setup_circular_chain(32, 10_000u128); actor_ids.extend(transfer_ids); @@ -9704,10 +11643,9 @@ fn reference_idle_budget_admits_mixed_tasks_without_starvation() { #[test] fn reference_idle_budget_converges_paged_wakeup_and_pure_close_pressure() { - use super::common::new_test_ext; - new_test_ext().execute_with(|| { + synthetic_actor_test_ext().execute_with(|| { System::set_block_number(1); - close_genesis_system_actors(); + assert_synthetic_actor_genesis(); let retry_ids = setup_inert_actors( u64::from(::MaxSweepBatch::get()), 10_000u128, @@ -9736,9 +11674,13 @@ fn reference_idle_budget_converges_paged_wakeup_and_pure_close_pressure() { assert_ok!(mint_tokens(asset_id, &ALICE, &close_account, 500)); let budget = <::ActorOnIdleReserve as Get>::get(); - for block in 102..=150 { + let bounded_work = retry_ids.len().saturating_add(expired_ids.len()) as u32; + let convergence_horizon = bounded_work.saturating_mul(8).saturating_add(32); + let convergence_timestamp = 102u64.saturating_mul(6_000); + for offset in 0..convergence_horizon { + let block = 102u32.saturating_add(offset); System::set_block_number(block); - set_consensus_timestamp(u64::from(block).saturating_mul(6_000)); + set_consensus_timestamp(convergence_timestamp); Actors::on_initialize(block); run_idle(budget); let retries_done = retry_ids.iter().all(|id| { @@ -9756,12 +11698,21 @@ fn reference_idle_budget_converges_paged_wakeup_and_pure_close_pressure() { } } + let pending_retries = retry_ids + .iter() + .filter(|id| { + Actors::actor_hot(**id) + .is_none_or(|hot| hot.wakeup_pointer.is_some() || hot.trigger_wakeup_pointer.is_none()) + }) + .copied() + .collect::>(); assert!( - retry_ids.iter().all(|id| { - 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" + pending_retries.is_empty(), + "overdue block wakeups must converge back to cadence ownership: pending={:?}, first_hot={:?}, queue={:?}, resource={:?}", + &pending_retries[..pending_retries.len().min(8)], + pending_retries.first().and_then(|id| Actors::actor_hot(*id)), + (Actors::queue_head(), Actors::queue_tail()), + Actors::block_resource_state(), ); assert!( retry_ids.iter().all(|id| { @@ -9793,10 +11744,9 @@ fn reference_idle_budget_converges_paged_wakeup_and_pure_close_pressure() { #[test] #[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, 1_300)]; + let scenarios: [(u64, u32); 4] = [(48, 96), (100, 150), (1000, 252), (10_000, 2_500)]; for (actors, blocks) in scenarios { - new_test_ext().execute_with(|| { + synthetic_actor_test_ext().execute_with(|| { let _ = run_fairness_matrix_case(actors, blocks); }); } @@ -9805,18 +11755,17 @@ fn scheduler_stress_fifo_over_capacity_fairness_matrix() { #[test] #[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, 700, 32)]; + let scenarios: [(u64, u32, u64); 3] = [(100, 200, 8), (1000, 300, 16), (5000, 1_500, 32)]; for (actors, blocks, stride) in scenarios { - let dense_diag = new_test_ext().execute_with(|| { + let dense_diag = synthetic_actor_test_ext().execute_with(|| { System::set_block_number(1); - close_genesis_system_actors(); + assert_synthetic_actor_genesis(); let actor_ids = setup_inert_actors(actors, 10_000u128); run_blocks_with_diagnostics(&actor_ids, blocks, Weight::MAX) }); - let sparse_diag = new_test_ext().execute_with(|| { + let sparse_diag = synthetic_actor_test_ext().execute_with(|| { System::set_block_number(1); - close_genesis_system_actors(); + assert_synthetic_actor_genesis(); let actor_ids = setup_inert_actors_sparse(actors, 10_000u128, stride); run_blocks_with_diagnostics(&actor_ids, blocks, Weight::MAX) }); @@ -9859,10 +11808,9 @@ fn scheduler_stress_fifo_dense_vs_sparse_topology_matrix() { #[test] #[ignore] // Heavy long-run sparse-liveness check, run in the scheduled nightly FIFO stress job fn scheduler_stress_fifo_sparse_topology_long_run_liveness() { - use super::common::new_test_ext; - new_test_ext().execute_with(|| { + synthetic_actor_test_ext().execute_with(|| { System::set_block_number(1); - close_genesis_system_actors(); + assert_synthetic_actor_genesis(); let actors = 2000u64; let blocks = 1024u32; let stride = 32u64; @@ -9889,23 +11837,37 @@ fn scheduler_stress_fifo_sparse_topology_long_run_liveness() { #[test] #[ignore] // Checkpoint A capacity acceptance; run through scripts/actors-assurance.sh. fn checkpoint_a_s6_dense_10k_wakeups_converge_without_drops() { - use super::common::new_test_ext; - new_test_ext().execute_with(|| { + synthetic_actor_test_ext().execute_with(|| { System::set_block_number(1); - close_genesis_system_actors(); + assert_synthetic_actor_genesis(); let actor_count = 10_000u32; let wakeup_block = 10; - let actor_ids = setup_inert_actors(actor_count.into(), 10_000u128); + let mut actor_ids = alloc::vec::Vec::new(); + for _ in 0..actor_count { + let actor_id = Actors::next_actor_id(); + assert_ok!(Actors::create_system_actor( + RuntimeOrigin::root(), + ALICE, + Mutability::Mutable, + inert_manual_window_contract(wakeup_block, 1_000), + )); + let sovereign = Actors::sovereign_account_id_system(actor_id); + let _ = >::deposit_creating(&sovereign, 10_000u128); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + actor_ids.push(actor_id); + } assert_eq!(Actors::queue_head(), Actors::queue_tail()); assert!(actor_ids.iter().all(|actor_id| { Actors::actor_hot(*actor_id).is_some_and(|hot| hot.queue_ticket.is_none()) })); - for actor_id in &actor_ids { - assert!(Actors::wakeup_substrate_schedule(*actor_id, wakeup_block)); - } - - let bucket = Actors::wakeup_buckets(wakeup_block).expect("dense wakeup bucket"); - assert_eq!(bucket.live_entries, actor_count); + let wakeup_key = pallet_deos_actors::WakeupKey::Block(wakeup_block); + assert_eq!( + pallet_deos_actors::ActorWaitingOccupancies::::get(wakeup_key), + actor_count + ); assert_eq!(Actors::wakeup_cursor_len(), 1); assert_eq!(Actors::wakeup_cursor_peek(), Some(wakeup_block)); @@ -9921,7 +11883,7 @@ fn checkpoint_a_s6_dense_10k_wakeups_converge_without_drops() { } assert_eq!(scanned, actor_count); - assert!(Actors::wakeup_buckets(wakeup_block).is_none()); + assert!(!pallet_deos_actors::ActorWaitingOccupancies::::contains_key(wakeup_key)); assert!(actor_ids.iter().all(|actor_id| { let hot = Actors::actor_hot(*actor_id).expect("active actor"); hot.wakeup_pointer.is_none() && hot.queue_ticket.is_some() @@ -9931,10 +11893,9 @@ fn checkpoint_a_s6_dense_10k_wakeups_converge_without_drops() { #[test] fn scheduler_512_mixed_clocks_survives_delayed_timestamp() { - use super::common::new_test_ext; - new_test_ext().execute_with(|| { + synthetic_actor_test_ext().execute_with(|| { System::set_block_number(1); - close_genesis_system_actors(); + assert_synthetic_actor_genesis(); let tick_millis = primitives::ecosystem::params::ACTOR_CADENCE_TICK_MILLIS; set_consensus_timestamp(tick_millis); @@ -10023,7 +11984,16 @@ fn scheduler_512_mixed_clocks_survives_delayed_timestamp() { } } - assert!(counts.values().all(|count| *count == 1)); + let missing = counts + .iter() + .filter_map(|(actor_id, count)| (*count != 1).then_some(*actor_id)) + .take(16) + .collect::>(); + let wakeup_fault = pallet_deos_actors::WakeupWorkerFaultState::::get(); + assert!( + missing.is_empty(), + "mixed-clock actors did not execute exactly once: {missing:?}; wakeup_fault={wakeup_fault:?}", + ); let tick_first = tick_first.expect("tick clock makes progress"); let block_first = block_first.expect("block clock makes progress"); let tick_last = tick_last.expect("tick clock completes"); @@ -10054,12 +12024,11 @@ fn scheduler_512_mixed_clocks_survives_delayed_timestamp() { #[test] #[ignore] // Queue/wakeup occupancy diagnostics for over-capacity stress scenario fn profile_scheduler_queue_wakeup_occupancy_10k() { - use super::common::new_test_ext; - new_test_ext().execute_with(|| { + synthetic_actor_test_ext().execute_with(|| { System::set_block_number(1); - close_genesis_system_actors(); + assert_synthetic_actor_genesis(); let actors = 10_000u64; - let blocks = 1_300u32; + let blocks = 2_500u32; 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); @@ -10091,11 +12060,10 @@ fn profile_scheduler_queue_wakeup_occupancy_10k() { #[test] #[ignore] 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, 1_300)]; + let scenarios: [(u64, u32); 4] = [(48, 96), (100, 150), (1000, 252), (10_000, 2_500)]; for (actors, blocks) in scenarios { - new_test_ext().execute_with(|| { + synthetic_actor_test_ext().execute_with(|| { let started = Instant::now(); let diag = run_fairness_matrix_case(actors, blocks); let elapsed = started.elapsed(); @@ -10119,30 +12087,32 @@ fn genesis_sparse_id_space_executes_only_active_actors() { new_test_ext().execute_with(|| { System::set_block_number(1); let initial_balance: u128 = 1_000_000 * crate::EXISTENTIAL_DEPOSIT; - // Genesis reserves IDs 0-14 as three active actors, ten dormant identities, - // and two custody-only accounts. The gap after ID 14 stays empty until a + // Genesis reserves IDs 0-14 as three active actors and twelve dormant identities, + // including two sealed immutable Anchors. The gap after ID 14 stays empty until a // new actor is created. // - // Ringless scheduler iterates ActiveActors BTreeSet directly, - // so sparse IDs are handled efficiently — no scanning over empty slots. + // The canonical Ready FIFO addresses live control cells by ticket, + // so service never scans the unused Actor-id space. // // Direct test funding bypasses ingress notification. The three genesis // contracts must therefore remain idle while the explicit timer fixture runs. assert_eq!(Actors::active_actor_count(), 3); - assert_eq!(Actors::actor_identity_count(), 13); + assert_eq!(Actors::actor_identity_count(), 15); let genesis_ids_all: alloc::vec::Vec = alloc::vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,]; for &id in &genesis_ids_all { let sov = Actors::sovereign_account_id_system(id); let _ = >::deposit_creating(&sov, initial_balance); } - // Dormant and custody identities own no executable contract. + // Dormant identities own no executable contract. for id in [2, 4, 5, 6, 7, 8, 9, 11, 13, 14] { - assert!(Actors::actor_identities(id).is_some()); + let identity = Actors::actor_identity(id).expect("Mutable dormant identity exists"); + assert_eq!(identity.mutability, Mutability::Mutable); assert!(Actors::active_actor_state(id).is_none()); } for id in [3, 12] { - assert!(Actors::actor_identities(id).is_none()); + let identity = Actors::actor_identity(id).expect("Immutable Anchor identity exists"); + assert_eq!(identity.mutability, Mutability::Immutable); assert!(Actors::active_actor_state(id).is_none()); } // Create a fresh actor at the current high end to extend the sparse space. @@ -10355,6 +12325,10 @@ fn runtime_simulation_core_rolls_back_deos_adapter_effects() { Mutability::Mutable, expected_contract, SimulationMode::FreshCurrentPlan, + pallet_deos_actors::SimulationBudget { + actor_control: Weight::from_parts(u64::MAX / 2, u64::MAX / 2), + shared_economic: Weight::from_parts(u64::MAX / 2, u64::MAX / 2), + }, ) .expect("ready DEOS actor simulates"); @@ -10375,15 +12349,2249 @@ fn runtime_simulation_core_rolls_back_deos_adapter_effects() { }); } +#[derive(Clone, Copy, Debug)] +struct UserDispatchSaturation { + calls: u32, + next_weight: Weight, +} + +fn saturate_user_base_with_remarks( + signer: &sr25519::Pair, + nonce: &mut u32, + marker: u32, +) -> UserDispatchSaturation { + let user_base = BlockResourceBudgetValue::get().limits().user_base_turn(); + let mut calls = 0u32; + loop { + assert!(calls < 10_000, "remark saturation must remain bounded"); + let call = RuntimeCall::System(polkadot_sdk::frame_system::Call::remark { + remark: marker.to_le_bytes().to_vec(), + }); + let extrinsic = signed_extrinsic(signer, crate::Nonce::from(*nonce), call); + let next_weight = extrinsic.get_dispatch_info().total_weight(); + let state = Actors::block_resource_state().expect("prepass resource state exists"); + let remaining = user_base + .checked_sub(&state.usage().user_dispatch_used()) + .unwrap_or_else(Weight::zero); + if !next_weight.all_lte(remaining) { + assert!(calls > 0, "user base must admit at least one remark"); + return UserDispatchSaturation { calls, next_weight }; + } + let outcome = Executive::apply_extrinsic(extrinsic) + .unwrap_or_else(|error| panic!("valid user remark rejected: {error:?}")); + assert!(outcome.is_ok(), "user remark dispatch failed: {outcome:?}"); + *nonce = nonce.saturating_add(1); + calls = calls.saturating_add(1); + } +} + +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +enum ProductionStopReason { + QueueEmpty, + CausalCutoff, + ActorControl, + SharedEconomic, + SameBlockOrTopology, + AccountingHalt, +} + +#[derive(Debug)] +struct ProductionBlockSample { + block: u32, + base_committed_steps: u32, + drain_committed_steps: u32, + progressed_actor_ids: Vec, + failed_steps: u32, + openings: u32, + completions: u32, + closures: u32, + cadenced_occurrences: u32, + actor_control_used: Weight, + actor_effect_used: Weight, + user_dispatch_used: Weight, + user_dispatch_calls: u32, + next_user_dispatch_weight: Option, + queue_head: u64, + queue_tail: u64, + optional_actor_work_halted: bool, + stop_reason: ProductionStopReason, +} + +#[derive(Debug, Default)] +struct ProductionThroughputDiagnostics { + samples: Vec, + progressed_actor_ids: BTreeSet, + last_progress_block: BTreeMap, + maximum_service_gap_blocks: u32, +} + +#[derive(Debug, Default)] +struct ProductionPhaseSummary { + committed_steps: u32, + failed_steps: u32, + openings: u32, + completions: u32, + closures: u32, + cadenced_occurrences: u32, + actor_ids: Vec, +} + +fn one_step_phase_summary(event_start: usize, event_end: usize) -> ProductionPhaseSummary { + let events = System::events(); + assert!(event_start <= event_end && event_end <= events.len()); + let mut summary = ProductionPhaseSummary::default(); + for record in events.iter().take(event_end).skip(event_start) { + match &record.event { + RuntimeEvent::Actors(Event::CycleStarted { .. }) => { + summary.openings = summary.openings.saturating_add(1); + } + RuntimeEvent::Actors(Event::CycleSummary { + actor_id, outcomes, .. + }) => { + let dispositions = outcomes + .executed_steps + .saturating_add(outcomes.precondition_skips) + .saturating_add(outcomes.skipped_resolution) + .saturating_add(outcomes.skipped_funding_unavailable) + .saturating_add(outcomes.failed_steps); + assert_eq!( + dispositions, 1, + "One-Step production profile emitted a non-unit cumulative disposition for Actor {actor_id}" + ); + summary.committed_steps = summary.committed_steps.saturating_add(dispositions); + summary.failed_steps = summary.failed_steps.saturating_add(outcomes.failed_steps); + summary.completions = summary.completions.saturating_add(1); + summary.actor_ids.push(*actor_id); + } + RuntimeEvent::Actors(Event::ActorClosed { .. }) => { + summary.closures = summary.closures.saturating_add(1); + } + RuntimeEvent::Actors(Event::TriggerOccurrenceProcessed { + trigger_family: pallet_deos_actors::TriggerFamily::Cadenced, + .. + }) => { + summary.cadenced_occurrences = summary.cadenced_occurrences.saturating_add(1); + } + _ => {} + } + } + summary +} + +fn nearest_rank_percentile(values: &[u32], percentile: usize) -> u32 { + if values.is_empty() { + return 0; + } + let mut sorted = values.to_vec(); + sorted.sort_unstable(); + let rank = percentile + .saturating_mul(sorted.len()) + .div_ceil(100) + .saturating_sub(1) + .min(sorted.len().saturating_sub(1)); + sorted[rank] +} + +impl ProductionThroughputDiagnostics { + fn record_one_step_block( + &mut self, + block: u32, + base_event_end: usize, + next_control_maximum: Weight, + next_effect_maximum: Weight, + user_saturation: Option, + ) { + let event_end = System::events().len(); + let mut base = one_step_phase_summary(0, base_event_end); + let drain = one_step_phase_summary(base_event_end, event_end); + base.actor_ids.extend(drain.actor_ids.iter().copied()); + let distinct_actor_ids = base.actor_ids.iter().copied().collect::>(); + let committed_steps = base.committed_steps.saturating_add(drain.committed_steps); + assert_eq!( + committed_steps, + distinct_actor_ids.len() as u32, + "Q1 requires committed Steps to equal distinct progressed Actors in block {block}" + ); + self + .progressed_actor_ids + .extend(base.actor_ids.iter().copied()); + for actor_id in &base.actor_ids { + if let Some(previous) = self.last_progress_block.insert(*actor_id, block) { + self.maximum_service_gap_blocks = self + .maximum_service_gap_blocks + .max(block.saturating_sub(previous)); + } + } + let telemetry = Actors::finalized_block_resource_telemetry() + .filter(|telemetry| telemetry.block_number() == block) + .expect("saturated production block must finalize current resource telemetry"); + let usage = telemetry.usage(); + let optional_actor_work_halted = telemetry.optional_actor_work_halted(); + let limits = BlockResourceBudgetValue::get().limits(); + let control_remaining = limits + .actor_control() + .checked_sub(&usage.actor_control_used()) + .unwrap_or_else(Weight::zero); + let shared_remaining = limits + .shared_economic() + .checked_sub(&usage.shared_used().expect("resource usage addition fits")) + .unwrap_or_else(Weight::zero); + let queue_head = Actors::queue_head(); + let queue_tail = Actors::queue_tail(); + let cutoff = Actors::prepass_execution_cutoff() + .filter(|(cutoff_block, _)| *cutoff_block == block) + .map(|(_, cutoff)| cutoff); + let stop_reason = if optional_actor_work_halted { + ProductionStopReason::AccountingHalt + } else if queue_head >= queue_tail { + ProductionStopReason::QueueEmpty + } else if cutoff.is_some_and(|cutoff| queue_head >= cutoff) { + ProductionStopReason::CausalCutoff + } else if !next_control_maximum.all_lte(control_remaining) { + ProductionStopReason::ActorControl + } else if !next_effect_maximum.all_lte(shared_remaining) { + ProductionStopReason::SharedEconomic + } else { + ProductionStopReason::SameBlockOrTopology + }; + self.samples.push(ProductionBlockSample { + block, + base_committed_steps: base.committed_steps, + drain_committed_steps: drain.committed_steps, + progressed_actor_ids: base.actor_ids, + failed_steps: base.failed_steps.saturating_add(drain.failed_steps), + openings: base.openings.saturating_add(drain.openings), + completions: base.completions.saturating_add(drain.completions), + closures: base.closures.saturating_add(drain.closures), + cadenced_occurrences: base + .cadenced_occurrences + .saturating_add(drain.cadenced_occurrences), + actor_control_used: usage.actor_control_used(), + actor_effect_used: usage.actor_effect_used(), + user_dispatch_used: usage.user_dispatch_used(), + user_dispatch_calls: user_saturation + .map(|saturation| saturation.calls) + .unwrap_or(0), + next_user_dispatch_weight: user_saturation.map(|saturation| saturation.next_weight), + queue_head, + queue_tail, + optional_actor_work_halted, + stop_reason, + }); + } + + fn assert_complete_first_traversal(&self, actor_ids: &[ActorId]) { + assert_eq!(self.progressed_actor_ids.len(), actor_ids.len()); + assert!( + actor_ids + .iter() + .all(|actor_id| self.progressed_actor_ids.contains(actor_id)) + ); + assert_eq!( + self + .samples + .iter() + .map(|sample| sample.failed_steps) + .sum::(), + 0 + ); + assert!( + self + .samples + .iter() + .all(|sample| !sample.optional_actor_work_halted) + ); + } + + fn cadenced_occurrences(&self) -> u32 { + self + .samples + .iter() + .map(|sample| sample.cadenced_occurrences) + .sum() + } + + fn print_summary(&self, label: &str) { + let base_steps = self + .samples + .iter() + .map(|sample| sample.base_committed_steps) + .sum::(); + let drain_steps = self + .samples + .iter() + .map(|sample| sample.drain_committed_steps) + .sum::(); + let total_steps = base_steps.saturating_add(drain_steps); + let per_block_steps = self + .samples + .iter() + .map(|sample| sample.progressed_actor_ids.len() as u32) + .collect::>(); + let min_steps = per_block_steps.iter().copied().min().unwrap_or(0); + let max_steps = per_block_steps.iter().copied().max().unwrap_or(0); + let p50_steps = nearest_rank_percentile(&per_block_steps, 50); + let p95_steps = nearest_rank_percentile(&per_block_steps, 95); + let p99_steps = nearest_rank_percentile(&per_block_steps, 99); + let step_histogram = + per_block_steps + .iter() + .fold(BTreeMap::::new(), |mut histogram, steps| { + histogram + .entry(*steps) + .and_modify(|count| *count = count.saturating_add(1)) + .or_insert(1); + histogram + }); + let step_histogram_json = step_histogram + .iter() + .map(|(steps, blocks)| format!("\"{steps}\":{blocks}")) + .collect::>() + .join(","); + let failed_steps = self + .samples + .iter() + .map(|sample| sample.failed_steps) + .sum::(); + let openings = self + .samples + .iter() + .map(|sample| sample.openings) + .sum::(); + let completions = self + .samples + .iter() + .map(|sample| sample.completions) + .sum::(); + let closures = self + .samples + .iter() + .map(|sample| sample.closures) + .sum::(); + let halted_blocks = self + .samples + .iter() + .filter(|sample| sample.optional_actor_work_halted) + .count(); + let user_base = BlockResourceBudgetValue::get().limits().user_base_turn(); + let user_dispatch_calls = self + .samples + .iter() + .map(|sample| sample.user_dispatch_calls) + .sum::(); + let user_saturated_blocks = self + .samples + .iter() + .filter(|sample| { + sample.next_user_dispatch_weight.is_some_and(|next| { + let remaining = user_base + .checked_sub(&sample.user_dispatch_used) + .unwrap_or_else(Weight::zero); + !next.all_lte(remaining) + }) + }) + .count(); + let stop_counts = self.samples.iter().fold( + BTreeMap::::new(), + |mut counts, sample| { + counts + .entry(sample.stop_reason) + .and_modify(|count| *count = count.saturating_add(1)) + .or_insert(1); + counts + }, + ); + let stop_count = |reason| stop_counts.get(&reason).copied().unwrap_or(0); + let cadenced_occurrences = self.cadenced_occurrences(); + let last = self.samples.last(); + let last_control = last + .map(|sample| sample.actor_control_used) + .unwrap_or_else(Weight::zero); + let last_effect = last + .map(|sample| sample.actor_effect_used) + .unwrap_or_else(Weight::zero); + let last_user = last + .map(|sample| sample.user_dispatch_used) + .unwrap_or_else(Weight::zero); + let last_user_remaining = user_base + .checked_sub(&last_user) + .unwrap_or_else(Weight::zero); + println!( + "ACTOR_THROUGHPUT_REPORT_V1 {{\"profile\":\"{label}\",\"blocks\":{},\"totalSteps\":{total_steps},\"baseSteps\":{base_steps},\"drainSteps\":{drain_steps},\"failedSteps\":{failed_steps},\"openings\":{openings},\"completions\":{completions},\"closures\":{closures},\"meanSteps\":{:.4},\"p50Steps\":{p50_steps},\"p95Steps\":{p95_steps},\"p99Steps\":{p99_steps},\"minSteps\":{min_steps},\"maxSteps\":{max_steps},\"stepHistogram\":{{{step_histogram_json}}},\"maximumServiceGapBlocks\":{},\"haltedBlocks\":{halted_blocks},\"userDispatchCalls\":{user_dispatch_calls},\"userSaturatedBlocks\":{user_saturated_blocks},\"stopQueueEmpty\":{},\"stopCausalCutoff\":{},\"stopActorControl\":{},\"stopSharedEconomic\":{},\"stopSameBlockOrTopology\":{},\"stopAccountingHalt\":{},\"lastBlock\":{},\"lastControlRefTime\":{},\"lastControlProofSize\":{},\"lastEffectRefTime\":{},\"lastEffectProofSize\":{},\"lastUserRefTime\":{},\"lastUserProofSize\":{},\"lastUserRemainingRefTime\":{},\"lastUserRemainingProofSize\":{},\"lastQueueHead\":{},\"lastQueueTail\":{}}}", + self.samples.len(), + total_steps as f64 / self.samples.len().max(1) as f64, + self.maximum_service_gap_blocks, + stop_count(ProductionStopReason::QueueEmpty), + stop_count(ProductionStopReason::CausalCutoff), + stop_count(ProductionStopReason::ActorControl), + stop_count(ProductionStopReason::SharedEconomic), + stop_count(ProductionStopReason::SameBlockOrTopology), + stop_count(ProductionStopReason::AccountingHalt), + last.map(|sample| sample.block).unwrap_or(0), + last_control.ref_time(), + last_control.proof_size(), + last_effect.ref_time(), + last_effect.proof_size(), + last_user.ref_time(), + last_user.proof_size(), + last_user_remaining.ref_time(), + last_user_remaining.proof_size(), + last.map(|sample| sample.queue_head).unwrap_or(0), + last.map(|sample| sample.queue_tail).unwrap_or(0), + ); + println!( + "ACTOR_FREQUENCY_REPORT_V1 {{\"profile\":\"{label}\",\"committedSteps\":{total_steps},\"openings\":{openings},\"completions\":{completions},\"cadencedOccurrences\":{cadenced_occurrences}}}" + ); + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum TransferMatrixPhase { + FreshOpening, + Running, +} + +fn transfer_matrix_predicates( + count: u32, +) -> Vec> { + (0..count) + .map(|offset| pallet_deos_actors::Predicate::BalanceNotEquals { + asset: AssetKind::Native, + threshold: 10_000u128 + .saturating_mul(crate::EXISTENTIAL_DEPOSIT) + .saturating_add(u128::from(offset)) + .saturating_add(1), + }) + .collect() +} + +fn measure_transfer_matrix_cell( + phase: TransferMatrixPhase, + predicate_count: u32, +) -> (Weight, Weight) { + super::common::synthetic_actor_test_ext().execute_with(|| { + System::set_block_number(1); + assert_synthetic_actor_genesis(); + let initial_balance = 1_000u128.saturating_mul(crate::EXISTENTIAL_DEPOSIT); + let timing = match phase { + TransferMatrixPhase::FreshOpening => pallet_deos_actors::ObservationTiming::Opening, + TransferMatrixPhase::Running => pallet_deos_actors::ObservationTiming::Current, + }; + let measured_step = RuntimeStep { + precondition: all_preconditions_at(transfer_matrix_predicates(predicate_count), timing), + task: Task::Transfer { + to: BOB, + asset: AssetKind::Native, + amount: AmountResolution::Fixed(1), + }, + on_error: StepErrorPolicy::AbortCycle, + }; + let steps = match phase { + TransferMatrixPhase::FreshOpening => vec![measured_step], + TransferMatrixPhase::Running => vec![ + make_step(Task::Transfer { + to: BOB, + asset: AssetKind::Native, + amount: AmountResolution::Fixed(1), + }), + measured_step, + ], + }; + let actor_id = create_system( + ALICE, + manual_schedule(), + None, + BoundedVec::try_from(steps).expect("matrix Contract fits"), + ); + fund_native(actor_id, initial_balance); + assert_ok!(Actors::manual_trigger(RuntimeOrigin::root(), actor_id)); + + let final_block = match phase { + TransferMatrixPhase::FreshOpening => 2, + TransferMatrixPhase::Running => 3, + }; + let mut measured = None; + for block in 2..=final_block { + 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); + let telemetry = Actors::finalized_block_resource_telemetry() + .filter(|snapshot| snapshot.block_number() == block) + .expect("matrix block finalizes resource telemetry"); + assert!(!telemetry.optional_actor_work_halted()); + let summaries = System::events() + .iter() + .filter(|record| { + matches!( + record.event, + RuntimeEvent::Actors(Event::CycleSummary { actor_id: id, .. }) if id == actor_id + ) + }) + .count(); + if block == final_block { + assert_eq!(summaries, 1, "measured Step completes one Cycle"); + let usage = telemetry.usage(); + measured = Some((usage.actor_control_used(), usage.actor_effect_used())); + } else { + assert_eq!( + Actors::actor_hot(actor_id) + .expect("matrix Actor stays active") + .cycle_state, + pallet_deos_actors::CycleState::Running, + "first Step preserves Running continuation" + ); + } + Actors::on_finalize(block); + } + measured.expect("matrix measured block exists") + }) +} + +#[test] +fn cadenced_complete_path_isolates_reactive_control_delta() { + super::common::synthetic_actor_test_ext().execute_with(|| { + System::set_block_number(1); + assert_synthetic_actor_genesis(); + let contract_steps = transfer_contract_steps(BOB, AssetKind::Native, 1); + let actor_id = create_system( + ALICE, + RuntimeSchedule { + trigger: Trigger::cadenced(1), + cooldown_blocks: 0, + }, + None, + contract_steps, + ); + fund_native( + actor_id, + 1_000u128.saturating_mul(crate::EXISTENTIAL_DEPOSIT), + ); + let mut measured = Vec::new(); + let mut block_usage = Vec::new(); + for block in 2..=100 { + 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); + let telemetry = Actors::finalized_block_resource_telemetry() + .filter(|snapshot| snapshot.block_number() == block) + .expect("Cadenced matrix block finalizes resource telemetry"); + assert!(!telemetry.optional_actor_work_halted()); + let summary = actor_events().into_iter().find_map(|event| match event { + Event::CycleSummary { + actor_id: id, + outcomes, + .. + } if id == actor_id => Some(outcomes), + _ => None, + }); + let usage = telemetry.usage(); + let completed = summary.is_some(); + if let Some(summary) = summary { + assert_eq!(summary.executed_steps, 1); + assert_eq!(summary.failed_steps, 0); + measured.push((block, usage.actor_control_used(), usage.actor_effect_used())); + } + block_usage.push(( + block, + completed, + usage.actor_control_used(), + usage.actor_effect_used(), + )); + Actors::on_finalize(block); + if measured.len() == 2 { + break; + } + } + assert_eq!(measured.len(), 2, "two Cadenced Cycles must materialize"); + assert_ok!(Actors::pause_actor(RuntimeOrigin::root(), actor_id)); + let mut paused_controls = Vec::new(); + for empty_block in 6..=12 { + System::set_block_number(empty_block); + System::reset_events(); + System::set_block_consumed_resources(Weight::zero(), 0); + set_consensus_timestamp( + u64::from(empty_block) + .saturating_mul(primitives::ecosystem::params::ACTOR_CADENCE_TICK_MILLIS), + ); + assert_eq!(Actors::on_initialize(empty_block), Weight::zero()); + ensure_current_resource_state(); + Actors::on_idle(empty_block, Weight::MAX); + let empty_telemetry = Actors::finalized_block_resource_telemetry() + .filter(|snapshot| snapshot.block_number() == empty_block) + .expect("paused block finalizes resource telemetry"); + let empty_usage = empty_telemetry.usage(); + assert_eq!(empty_usage.actor_effect_used(), Weight::zero()); + paused_controls.push((empty_block, empty_usage.actor_control_used())); + Actors::on_finalize(empty_block); + } + let empty_control = paused_controls + .last() + .map(|(_, control)| *control) + .expect("paused baseline exists"); + assert_eq!( + block_usage, + vec![ + (2, false, Weight::from_parts(11_491_516_748, 110_628), Weight::zero()), + ( + 3, + true, + Weight::from_parts(4_528_011_122, 64_513), + Weight::from_parts(2_293_433_000, 29_222), + ), + (4, false, Weight::from_parts(11_491_516_748, 110_628), Weight::zero()), + ( + 5, + true, + Weight::from_parts(4_528_011_122, 64_513), + Weight::from_parts(2_293_433_000, 29_222), + ), + ] + ); + assert_eq!(empty_control, Weight::from_parts(2_400_465_748, 33_188)); + type W = crate::weights::pallet_deos_actors::SubstrateWeight; + let physical_remove = ::scheduler_wakeup_cursor_worker_remove(); + let at_time = ::at_time_trigger_occurrence(); + let cadenced = ::cadenced_trigger_occurrence(); + let occurrence = Weight::from_parts( + at_time.ref_time().max(cadenced.ref_time()), + at_time.proof_size().max(cadenced.proof_size()), + ); + let materialization_actual = physical_remove.saturating_add(occurrence); + let materialization_increment = block_usage[0].2.saturating_sub(empty_control); + assert_eq!(physical_remove, Weight::from_parts(5_931_400_000, 55_857)); + assert_eq!(occurrence, Weight::from_parts(2_811_041_000, 8_451)); + assert_eq!( + materialization_actual, + Weight::from_parts(8_742_441_000, 64_308) + ); + let rotated_baseline_delta = materialization_increment + .checked_sub(&materialization_actual) + .expect("due occurrence fits measured materialization increment"); + assert_eq!(rotated_baseline_delta, Weight::from_parts(348_610_000, 13_132)); + let materialization_admission = Actors::wakeup_cursor_drain_unit_weight_upper( + pallet_deos_actors::WakeupBucketDisposition::Remove, + ); + assert_eq!( + materialization_admission, + Weight::from_parts(18_001_241_000, 147_697) + ); + assert_eq!( + paused_controls[paused_controls.len() - 2].1, + empty_control, + "paused cleanup must converge to a stable empty baseline" + ); + for (block, completed, control, effect) in &block_usage { + println!( + "ACTOR_CADENCED_BLOCK_V1 block={block} completed={completed} control_ref_time={} control_proof_size={} incremental_control_ref_time={} incremental_control_proof_size={} effect_ref_time={} effect_proof_size={}", + control.ref_time(), + control.proof_size(), + control.saturating_sub(empty_control).ref_time(), + control.saturating_sub(empty_control).proof_size(), + effect.ref_time(), + effect.proof_size(), + ); + } + println!( + "ACTOR_CADENCED_OWNER_V1 physical_ref_time={} physical_proof_size={} occurrence_ref_time={} occurrence_proof_size={} rotated_delta_ref_time={} rotated_delta_proof_size={} actual_ref_time={} actual_proof_size={} incremental_ref_time={} incremental_proof_size={} admission_ref_time={} admission_proof_size={}", + physical_remove.ref_time(), + physical_remove.proof_size(), + occurrence.ref_time(), + occurrence.proof_size(), + rotated_baseline_delta.ref_time(), + rotated_baseline_delta.proof_size(), + materialization_actual.ref_time(), + materialization_actual.proof_size(), + materialization_increment.ref_time(), + materialization_increment.proof_size(), + materialization_admission.ref_time(), + materialization_admission.proof_size(), + ); + for (block, control) in paused_controls { + println!( + "ACTOR_CADENCED_EMPTY_V1 block={block} control_ref_time={} control_proof_size={}", + control.ref_time(), + control.proof_size(), + ); + } + let manual_control = Weight::from_parts(4_528_011_122, 64_513); + let transfer_effect = Weight::from_parts(2_293_433_000, 29_222); + for (index, (block, control, effect)) in measured.into_iter().enumerate() { + assert_eq!(effect, transfer_effect); + assert!(manual_control.all_lte(control)); + let delta = control + .checked_sub(&manual_control) + .expect("Cadenced control contains Manual complete-path control"); + println!( + "ACTOR_CADENCED_DELTA_V1 cycle={} block={block} control_ref_time={} control_proof_size={} delta_ref_time={} delta_proof_size={}", + index + 1, + control.ref_time(), + control.proof_size(), + delta.ref_time(), + delta.proof_size(), + ); + } + }); +} + +fn measure_manual_control_branch(close_after_productive_cycle: bool) -> (Weight, Weight) { + super::common::synthetic_actor_test_ext().execute_with(|| { + System::set_block_number(1); + assert_synthetic_actor_genesis(); + let steps = if close_after_productive_cycle { + transfer_contract_steps(BOB, AssetKind::Native, 1) + } else { + RuntimeContractSteps::default() + }; + let actor_id = Actors::next_actor_id(); + let schedule = manual_schedule(); + let contract = ActorContract { + trigger: schedule.trigger, + cooldown_blocks: schedule.cooldown_blocks, + window: None, + steps, + completion: if close_after_productive_cycle { + CompletionPolicy::CloseAfterProductiveCycle + } else { + CompletionPolicy::Persistent + }, + funding: FundingSourcePolicy::RuntimePolicy, + auto_close_at_cycle_nonce: None, + }; + assert_ok!(Actors::create_system_actor( + RuntimeOrigin::root(), + ALICE, + Mutability::Mutable, + Some(contract), + )); + age_fixture_control_clock(actor_id); + if close_after_productive_cycle { + fund_native( + actor_id, + 1_000u128.saturating_mul(crate::EXISTENTIAL_DEPOSIT), + ); + } + assert_ok!(Actors::manual_trigger(RuntimeOrigin::root(), actor_id)); + let block = 2; + 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); + let telemetry = Actors::finalized_block_resource_telemetry() + .filter(|snapshot| snapshot.block_number() == block) + .expect("control branch finalizes resource telemetry"); + assert!(!telemetry.optional_actor_work_halted()); + if close_after_productive_cycle { + assert!(Actors::active_actor_state(actor_id).is_none()); + assert!(has_actor_event(|event| matches!( + event, + Event::ActorClosed { + actor_id: id, + reason: CloseReason::ProductiveCycleCompleted, + } if *id == actor_id + ))); + } else { + assert!(Actors::active_actor_state(actor_id).is_some()); + } + let usage = telemetry.usage(); + let measured = (usage.actor_control_used(), usage.actor_effect_used()); + Actors::on_finalize(block); + measured + }) +} + +#[test] +fn zero_step_and_productive_cleanup_complete_path_matrix_is_exact() { + let zero_step = measure_manual_control_branch(false); + let productive_cleanup = measure_manual_control_branch(true); + assert_eq!( + zero_step, + (Weight::from_parts(16_116_690_870, 200_381), Weight::zero()) + ); + assert_eq!( + productive_cleanup, + ( + Weight::from_parts(13_651_894_122, 146_399), + Weight::from_parts(2_293_433_000, 29_222), + ) + ); + println!( + "ACTOR_CONTROL_BRANCH_V1 branch=ZeroStep control_ref_time={} control_proof_size={} effect_ref_time={} effect_proof_size={}", + zero_step.0.ref_time(), + zero_step.0.proof_size(), + zero_step.1.ref_time(), + zero_step.1.proof_size(), + ); + println!( + "ACTOR_CONTROL_BRANCH_V1 branch=ProductiveCleanup control_ref_time={} control_proof_size={} effect_ref_time={} effect_proof_size={}", + productive_cleanup.0.ref_time(), + productive_cleanup.0.proof_size(), + productive_cleanup.1.ref_time(), + productive_cleanup.1.proof_size(), + ); +} + +#[test] +fn cleanup_reservation_counterfactual_is_bounded_by_actual_proof_frontier() { + let receipt = + crate::weights::pallet_deos_actors::SubstrateWeight::::action_invocation_receipt(); + let collection = crate::weights::pallet_deos_actors::SubstrateWeight::::fee_collection(); + let steps = transfer_contract_steps(BOB, AssetKind::Native, 1); + let (fifo_attempt_control, _) = one_step_fifo_attempt_maxima(&steps); + let cleanup = Actors::close_dispatch_weight_upper(); + let without_cleanup = fifo_attempt_control + .checked_sub(&cleanup) + .expect("cleanup is part of FIFO attempt reservation"); + assert_eq!( + fifo_attempt_control, + Weight::from_parts(12_619_785_858, 120_867) + .saturating_add(collection) + .saturating_add(receipt) + ); + assert_eq!(cleanup, Weight::from_parts(9_123_883_000, 81_886)); + assert_eq!( + without_cleanup.proof_size(), + 38_981 + collection.proof_size() + ); + + let control_proof = BlockResourceBudgetValue::get() + .limits() + .actor_control() + .proof_size(); + let empty_proof = 39_980u64; + let actual_increment_proof = 24_685u64; + let available_proof = control_proof.saturating_sub(empty_proof); + let actual_frontier = available_proof / actual_increment_proof; + assert_eq!(actual_frontier, 25); + assert_eq!(available_proof / fifo_attempt_control.proof_size(), 5); + assert_eq!(available_proof / without_cleanup.proof_size(), 14); + assert_eq!(actual_frontier.saturating_sub(21), 4); + assert!( + available_proof.saturating_sub(21 * actual_increment_proof) >= without_cleanup.proof_size() + ); + assert!( + available_proof.saturating_sub(actual_frontier * actual_increment_proof) + < actual_increment_proof + ); +} + +#[test] +fn one_step_admission_envelope_identifies_control_fragmentation_owner() { + let receipt = + crate::weights::pallet_deos_actors::SubstrateWeight::::action_invocation_receipt(); + let collection = crate::weights::pallet_deos_actors::SubstrateWeight::::fee_collection(); + let transfer_steps = transfer_contract_steps(BOB, AssetKind::Native, 1); + let stop_steps = + BoundedVec::try_from(vec![make_step(Task::StopCycle)]).expect("one control Step fits"); + let (fifo_attempt_control, transfer_effect) = one_step_fifo_attempt_maxima(&transfer_steps); + let (stop_fifo_attempt_control, stop_effect) = one_step_fifo_attempt_maxima(&stop_steps); + assert_eq!( + fifo_attempt_control, + stop_fifo_attempt_control + .saturating_add(collection) + .saturating_add(receipt) + ); + let lifecycle_control = + Actors::contract_steps_admission_weight_upper(ActorType::System, &transfer_steps) + .checked_sub(&transfer_effect) + .expect("lifecycle envelope contains the effect envelope"); + assert_ne!(transfer_effect, Weight::zero()); + assert_eq!(stop_effect, Weight::zero()); + let control_limit = BlockResourceBudgetValue::get().limits().actor_control(); + let admitted_per_block = (control_limit.ref_time() / fifo_attempt_control.ref_time()) + .min(control_limit.proof_size() / fifo_attempt_control.proof_size()); + assert_eq!( + fifo_attempt_control, + Weight::from_parts(12_619_785_858, 120_867) + .saturating_add(collection) + .saturating_add(receipt) + ); + assert_eq!( + lifecycle_control, + Weight::from_parts(14_983_891_606, 173_333) + .saturating_add(collection) + .saturating_add(receipt) + ); + assert_eq!(admitted_per_block, 5); + type W = crate::weights::pallet_deos_actors::SubstrateWeight; + let queue_scan = ::scheduler_paged_tombstone_drain(1); + let actor_probe = ::scheduler_actor_state_probe(); + let queue_consume = ::scheduler_paged_consume_preserve_page() + .max(::scheduler_paged_consume_delete_page()); + let opening_complete = ::scheduler_inner_opening_complete_min(0); + let actual_increment = queue_scan + .saturating_add(actor_probe) + .saturating_add(queue_consume) + .saturating_add(opening_complete) + .saturating_add(receipt); + assert_eq!(queue_scan, Weight::from_parts(335_663_374, 3_111)); + assert_eq!(actor_probe, Weight::from_parts(289_890_000, 15_106)); + assert_eq!(queue_consume, Weight::from_parts(597_287_000, 5_118)); + assert_eq!(opening_complete, Weight::from_parts(418_359_831, 4_398)); + assert_eq!( + actual_increment, + Weight::from_parts(1_641_200_205, 27_733).saturating_add(receipt) + ); + let empty_control = Weight::from_parts(2_954_966_226, 39_980); + let actual_capacity = control_limit + .checked_sub(&empty_control) + .map(|available| { + (available.ref_time() / actual_increment.ref_time()) + .min(available.proof_size() / actual_increment.proof_size()) + }) + .expect("empty baseline fits Actor Control"); + assert_eq!(actual_capacity, 22); + println!( + "ACTOR_ADMISSION_ENVELOPE_V1 fifo_attempt_control_ref_time={} fifo_attempt_control_proof_size={} lifecycle_control_ref_time={} lifecycle_control_proof_size={} transfer_effect_ref_time={} transfer_effect_proof_size={} admitted_per_block={admitted_per_block}", + fifo_attempt_control.ref_time(), + fifo_attempt_control.proof_size(), + lifecycle_control.ref_time(), + lifecycle_control.proof_size(), + transfer_effect.ref_time(), + transfer_effect.proof_size(), + ); + println!( + "ACTOR_ACTUAL_CONTROL_V1 scan_ref_time={} scan_proof_size={} probe_ref_time={} probe_proof_size={} consume_ref_time={} consume_proof_size={} opening_ref_time={} opening_proof_size={} increment_ref_time={} increment_proof_size={} empty_ref_time={} empty_proof_size={} proof_capacity={actual_capacity}", + queue_scan.ref_time(), + queue_scan.proof_size(), + actor_probe.ref_time(), + actor_probe.proof_size(), + queue_consume.ref_time(), + queue_consume.proof_size(), + opening_complete.ref_time(), + opening_complete.proof_size(), + actual_increment.ref_time(), + actual_increment.proof_size(), + empty_control.ref_time(), + empty_control.proof_size(), + ); +} + +#[test] +fn transfer_complete_path_matrix_reports_opening_running_and_predicate_cost() { + let mut cells = Vec::new(); + for phase in [ + TransferMatrixPhase::FreshOpening, + TransferMatrixPhase::Running, + ] { + for predicate_count in [0, 2, 4] { + let (control, effect) = measure_transfer_matrix_cell(phase, predicate_count); + assert_ne!(control, Weight::zero()); + assert!(control.all_lte(BlockResourceBudgetValue::get().limits().actor_control())); + assert!(effect.all_lte(BlockResourceBudgetValue::get().limits().actor_base_turn())); + cells.push((phase, predicate_count, control, effect)); + } + } + // Four Opening predicates select CompleteMax, whose direct owner includes the receipt. + let expected_control = [ + Weight::from_parts(4_528_011_122, 64_513), + Weight::from_parts(5_369_464_661, 94_485), + Weight::from_parts(4_351_534_677, 66_501), + Weight::from_parts(4_438_966_462, 67_486), + Weight::from_parts(4_580_408_852, 73_314), + Weight::from_parts(4_721_851_242, 79_142), + ]; + for ((phase, predicates, control, effect), expected_control) in + cells.into_iter().zip(expected_control) + { + assert_eq!(control, expected_control); + assert_eq!(effect, Weight::from_parts(2_293_433_000, 29_222)); + println!( + "ACTOR_TRANSFER_MATRIX_V1 phase={phase:?} predicates={predicates} control_ref_time={} control_proof_size={} effect_ref_time={} effect_proof_size={}", + control.ref_time(), + control.proof_size(), + effect.ref_time(), + effect.proof_size(), + ); + } +} + +#[test] +fn running_middle_complete_path_is_exact() { + super::common::synthetic_actor_test_ext().execute_with(|| { + System::set_block_number(1); + assert_synthetic_actor_genesis(); + let step = make_step(Task::Transfer { + to: BOB, + asset: AssetKind::Native, + amount: AmountResolution::Fixed(1), + }); + let actor_id = create_system( + ALICE, + manual_schedule(), + None, + BoundedVec::try_from(vec![step.clone(), step.clone(), step]) + .expect("three-Step Contract fits"), + ); + fund_native( + actor_id, + 1_000u128.saturating_mul(crate::EXISTENTIAL_DEPOSIT), + ); + assert_ok!(Actors::manual_trigger(RuntimeOrigin::root(), actor_id)); + + let mut middle = None; + for block in 2..=4 { + 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); + let telemetry = Actors::finalized_block_resource_telemetry() + .filter(|snapshot| snapshot.block_number() == block) + .expect("middle-Running block finalizes telemetry"); + assert!(!telemetry.optional_actor_work_halted()); + if block == 3 { + let run = Actors::actor_run_state(actor_id).expect("middle Step preserves RunState"); + assert_eq!(run.cursor, 2); + assert!(!has_actor_event(|event| matches!( + event, + Event::CycleSummary { actor_id: id, .. } if *id == actor_id + ))); + let usage = telemetry.usage(); + middle = Some((usage.actor_control_used(), usage.actor_effect_used())); + } + Actors::on_finalize(block); + } + let measured = middle.expect("middle Running measurement exists"); + assert_eq!( + measured, + ( + Weight::from_parts(4_862_289_366, 69_069), + Weight::from_parts(2_293_433_000, 29_222), + ) + ); + println!( + "ACTOR_RUNNING_MIDDLE_V1 control_ref_time={} control_proof_size={} effect_ref_time={} effect_proof_size={}", + measured.0.ref_time(), + measured.0.proof_size(), + measured.1.ref_time(), + measured.1.proof_size(), + ); + }); +} + +fn measure_swapout_success_cell(predicate_count: u32) -> (Weight, Weight) { + seeded_synthetic_actor_test_ext().execute_with(|| { + System::set_block_number(1); + assert_synthetic_actor_genesis(); + assert_ok!(super::common::setup_deos_router_infrastructure()); + System::set_block_number(2); + System::reset_events(); + System::set_block_consumed_resources(Weight::zero(), 0); + set_consensus_timestamp( + 2u64.saturating_mul(primitives::ecosystem::params::ACTOR_CADENCE_TICK_MILLIS), + ); + assert_eq!(Actors::on_initialize(2), Weight::zero()); + ensure_current_resource_state(); + Actors::on_idle(2, Weight::MAX); + Actors::on_finalize(2); + let initial_balance = 100_000_000_000_000u128; + let step = RuntimeStep { + precondition: all_preconditions_at( + transfer_matrix_predicates(predicate_count), + pallet_deos_actors::ObservationTiming::Opening, + ), + task: 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::zero(), + }, + on_error: StepErrorPolicy::AbortCycle, + }; + let actor_id = create_system( + ALICE, + manual_schedule(), + None, + BoundedVec::try_from(vec![step]).expect("one SwapOut Step fits"), + ); + fund_native(actor_id, initial_balance); + assert_ok!(Actors::manual_trigger(RuntimeOrigin::root(), actor_id)); + + let block = 3; + 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); + let telemetry = Actors::finalized_block_resource_telemetry() + .filter(|snapshot| snapshot.block_number() == block) + .expect("SwapOut matrix block finalizes resource telemetry"); + assert!(!telemetry.optional_actor_work_halted()); + assert!(has_actor_event(|event| matches!( + event, + Event::SwapExecuted { actor_id: id, .. } if *id == actor_id + ))); + let summary = actor_events() + .into_iter() + .find_map(|event| match event { + Event::CycleSummary { + actor_id: id, + outcomes, + .. + } if id == actor_id => Some(outcomes), + _ => None, + }) + .expect("SwapOut Cycle summary exists"); + assert_eq!(summary.executed_steps, 1); + assert_eq!(summary.failed_steps, 0); + let usage = telemetry.usage(); + let measured = (usage.actor_control_used(), usage.actor_effect_used()); + Actors::on_finalize(block); + measured + }) +} + +#[test] +fn swapout_success_complete_path_matrix_reports_predicate_cost() { + let mut cells = Vec::new(); + for predicate_count in [0, 2, 4] { + let (control, effect) = measure_swapout_success_cell(predicate_count); + assert_ne!(control, Weight::zero()); + assert_ne!(effect, Weight::zero()); + assert!(control.all_lte(BlockResourceBudgetValue::get().limits().actor_control())); + assert!(effect.all_lte(BlockResourceBudgetValue::get().limits().actor_base_turn())); + cells.push((predicate_count, control, effect)); + } + // Four Opening predicates select CompleteMax, whose direct owner includes the receipt. + let expected_control = [ + Weight::from_parts(4_528_011_122, 64_513), + Weight::from_parts(5_369_464_661, 94_485), + Weight::from_parts(4_351_534_677, 66_501), + ]; + for ((predicates, control, effect), expected_control) in cells.into_iter().zip(expected_control) { + assert_eq!(control, expected_control); + assert_eq!(effect, Weight::from_parts(3_414_697_000, 19_253)); + println!( + "ACTOR_SWAPOUT_MATRIX_V1 outcome=Success predicates={predicates} control_ref_time={} control_proof_size={} effect_ref_time={} effect_proof_size={}", + control.ref_time(), + control.proof_size(), + effect.ref_time(), + effect.proof_size(), + ); + } +} + +fn measure_swapout_retry_cell( + predicate_count: u32, + withdraw_before_retry: bool, + actor_type: ActorType, +) -> ([(Weight, Weight); 2], BTreeSet>, usize) { + let mut ext = seeded_synthetic_actor_test_ext(); + let actor_id = ext.execute_with(|| { + System::set_block_number(1); + assert_synthetic_actor_genesis(); + assert_ok!(super::common::setup_deos_router_infrastructure()); + System::set_block_number(2); + System::reset_events(); + System::set_block_consumed_resources(Weight::zero(), 0); + set_consensus_timestamp( + 2u64.saturating_mul(primitives::ecosystem::params::ACTOR_CADENCE_TICK_MILLIS), + ); + assert_eq!(Actors::on_initialize(2), Weight::zero()); + ensure_current_resource_state(); + Actors::on_idle(2, Weight::MAX); + Actors::on_finalize(2); + + let step = RuntimeStep { + precondition: all_preconditions_at( + transfer_matrix_predicates(predicate_count), + pallet_deos_actors::ObservationTiming::Opening, + ), + task: Task::SwapOut { + asset_out: AssetKind::Local(ASSET_A), + amount_out: AmountResolution::Fixed(crate::EXISTENTIAL_DEPOSIT), + asset_in: AssetKind::Native, + input_limit: InputLimit::Absolute(1), + slippage_tolerance: Perbill::zero(), + }, + on_error: StepErrorPolicy::RetryLater { max_attempts: 3 }, + }; + let actor_id = Actors::next_actor_id(); + let steps = BoundedVec::try_from(vec![step]).expect("one retrying SwapOut Step fits"); + match actor_type { + ActorType::System => { + assert_ok!(Actors::create_system_actor( + RuntimeOrigin::root(), + ALICE, + Mutability::Mutable, + system_active_contract(manual_schedule(), None, steps), + )); + } + ActorType::User => { + prefund_active_user_creation(&ALICE, &steps); + assert_ok!(Actors::create_user_actor( + RuntimeOrigin::signed(ALICE), + Mutability::Mutable, + user_active_contract(manual_schedule(), None, steps), + )); + } + } + fund_native(actor_id, 100_000_000_000_000); + let origin = match actor_type { + ActorType::System => RuntimeOrigin::root(), + ActorType::User => RuntimeOrigin::signed(ALICE), + }; + assert_ok!(Actors::manual_trigger(origin, actor_id)); + actor_id + }); + let mut measured = [(Weight::zero(), Weight::zero()); 2]; + let mut retry_keys = BTreeSet::new(); + let mut retry_proof_bytes = 0; + for block in 3..=4 { + let funding_unavailable = block == 4 && withdraw_before_retry; + let fee_sink = ::FeeSink::get(); + let (prior_run, custody_before, sink_before) = ext.execute_with(|| { + let prior_run = Actors::actor_run_state(actor_id); + if funding_unavailable { + let sovereign = actor_account(actor_id); + let retained = match actor_type { + ActorType::System => crate::EXISTENTIAL_DEPOSIT, + ActorType::User => { + ::MinUserBalance::get() + + Actors::actor_cost_quote(actor_id) + .expect("active retry quote") + .maximum_next_action_fee + .maximum_effect_fee + } + }; + let amount = TmctolAssetOps::balance(&sovereign, AssetKind::Native) - retained; + assert_ok!(>::transfer( + &sovereign, + &BOB, + amount, + ExistenceRequirement::KeepAlive, + )); + assert_eq!( + TmctolAssetOps::balance(&sovereign, AssetKind::Native), + retained + ); + } + let custody_before = + >::total_balance(&actor_account(actor_id)); + let sink_before = >::total_balance(&fee_sink); + 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), + ); + (prior_run, custody_before, sink_before) + }); + ext + .commit_all() + .expect("commit prepared block before recording Actor hooks"); + let recorder = + polkadot_sdk::sp_trie::recorder::Recorder::::default(); + ext.execute_with_recorder(recorder.clone(), || { + assert_eq!(Actors::on_initialize(block), Weight::zero()); + ensure_current_resource_state(); + Actors::on_idle(block, Weight::MAX); + }); + if block == 4 { + retry_keys = recorder + .recorded_keys() + .values() + .flat_map(|keys| keys.keys().map(|key| key.as_ref().to_vec())) + .collect(); + retry_proof_bytes = recorder.drain_storage_proof().encoded_size(); + assert!( + retry_keys + .contains(&pallet_deos_actors::ActorContractHeads::::hashed_key_for(actor_id)) + ); + assert!( + retry_keys + .contains(&pallet_deos_actors::ActorRunPayloads::::hashed_key_for(actor_id)) + ); + } + ext.execute_with(|| { + let telemetry = Actors::finalized_block_resource_telemetry() + .filter(|snapshot| snapshot.block_number() == block) + .expect("retry matrix block finalizes resource telemetry"); + assert!(!telemetry.optional_actor_work_halted()); + let continuation = Actors::actor_run_state(actor_id).expect("Temporary failure suspends"); + assert_eq!(continuation.cursor, 0); + assert!(continuation.funding_snapshot.is_empty()); + assert_eq!(continuation.unsuccessful_attempts_at_cursor, block - 2); + if let Some(prior) = prior_run { + assert_eq!(continuation.cycle_nonce, prior.cycle_nonce); + assert_eq!(continuation.opening_snapshot, prior.opening_snapshot); + assert_eq!( + continuation.opening_predicate_results, + prior.opening_predicate_results + ); + assert_eq!(continuation.funding_snapshot, prior.funding_snapshot); + } + if actor_type == ActorType::System || block == 4 { + let fee = if actor_type == ActorType::User && !funding_unavailable { + ::WeightToFee::weight_to_fee( + &telemetry.usage().actor_effect_used(), + ) + } else { + 0 + }; + if actor_type == ActorType::User && !funding_unavailable { + assert!(fee > 0); + assert!(has_actor_event(|event| matches!(event, + Event::ActionFeeCharged { actor_id: id, fee: charged, .. } + if *id == actor_id && *charged == fee + ))); + } + assert_eq!( + >::total_balance(&actor_account(actor_id)), + custody_before - fee + ); + assert_eq!( + >::total_balance(&fee_sink), + sink_before + fee + ); + } + let reason = if funding_unavailable { + pallet_deos_actors::SuspensionReason::FundingUnavailable + } else { + pallet_deos_actors::SuspensionReason::Temporary + }; + assert!(has_actor_event(|event| matches!( + event, + Event::CycleSuspended { + actor_id: id, + reason: actual_reason, + .. + } if *id == actor_id && *actual_reason == reason + ))); + assert_eq!( + has_actor_event(|event| matches!( + event, Event::StepFailed { actor_id: id, .. } if *id == actor_id + )), + !funding_unavailable + ); + assert_eq!( + has_actor_event(|event| matches!( + event, Event::ActionFeeCharged { actor_id: id, .. } if *id == actor_id + )), + !funding_unavailable + ); + let usage = telemetry.usage(); + measured[(block - 3) as usize] = (usage.actor_control_used(), usage.actor_effect_used()); + Actors::on_finalize(block); + }); + } + (measured, retry_keys, retry_proof_bytes) +} + +#[test] +fn swapout_temporary_failure_and_retry_matrix_reports_predicate_cost() { + let receipt = + crate::weights::pallet_deos_actors::SubstrateWeight::::action_invocation_receipt(); + use polkadot_sdk::pallet_asset_conversion::PoolLocator; + let pool_account = + ::PoolLocator::pool_address( + &AssetKind::Native, + &AssetKind::Local(ASSET_A), + ) + .expect("reference pool has a deterministic account"); + let feed = crate::configs::oracle_config::deos_router_pool_feed( + AssetKind::Native, + AssetKind::Local(ASSET_A), + ); + let quote_keys = BTreeSet::from([ + polkadot_sdk::frame_system::Account::::hashed_key_for(&pool_account), + polkadot_sdk::pallet_assets::Account::::hashed_key_for(ASSET_A, &pool_account), + pallet_deos_router::RouterFee::::hashed_key().to_vec(), + pallet_oracle::Feeds::::hashed_key_for(feed), + pallet_oracle::Observations::::hashed_key_for(feed), + ]); + let mut fee_quote_keys = quote_keys.clone(); + fee_quote_keys.extend([ + polkadot_sdk::frame_system::Account::::hashed_key_for( + ::FeeSink::get(), + ), + polkadot_sdk::pallet_balances::TotalIssuance::::hashed_key().to_vec(), + ]); + let mut cells = Vec::new(); + for predicate_count in [0, 2, 4] { + let ([first, resumed], invoked_keys, invoked_proof) = + measure_swapout_retry_cell(predicate_count, false, ActorType::System); + let ([unfunded_first, unfunded_retry], unfunded_keys, unfunded_proof) = + measure_swapout_retry_cell(predicate_count, true, ActorType::System); + assert_eq!(first, unfunded_first); + assert_eq!( + resumed.0, unfunded_retry.0, + "the coupled Suspended-head envelope conservatively withholds a receipt refund" + ); + assert_eq!(unfunded_retry.1, Weight::zero()); + assert_eq!( + invoked_keys + .difference(&unfunded_keys) + .cloned() + .collect::>(), + quote_keys + ); + assert!(unfunded_keys.difference(&invoked_keys).next().is_none()); + let ([user_first, user_retry], user_keys, user_proof) = + measure_swapout_retry_cell(predicate_count, false, ActorType::User); + let ([user_unfunded_first, user_unfunded_retry], user_unfunded_keys, user_unfunded_proof) = + measure_swapout_retry_cell(predicate_count, true, ActorType::User); + assert_eq!(user_first, user_unfunded_first); + assert_eq!( + user_retry.0, + user_unfunded_retry + .0 + .saturating_add( + crate::weights::pallet_deos_actors::SubstrateWeight::::fee_collection(), + ) + ); + assert_eq!(user_unfunded_retry.1, Weight::zero()); + assert_ne!(user_retry.1, Weight::zero()); + assert_eq!( + user_keys + .difference(&user_unfunded_keys) + .cloned() + .collect::>(), + fee_quote_keys + ); + assert!(user_unfunded_keys.difference(&user_keys).next().is_none()); + println!( + "ACTOR_USER_RETRY_STORAGE_V1 predicates={predicate_count} invoked_keys={} unfunded_keys={} invoked_only={} unfunded_only={} invoked_proof_bytes={user_proof} unfunded_proof_bytes={user_unfunded_proof}", + user_keys.len(), + user_unfunded_keys.len(), + user_keys.difference(&user_unfunded_keys).count(), + user_unfunded_keys.difference(&user_keys).count() + ); + println!( + "ACTOR_RETRY_STORAGE_V1 predicates={predicate_count} invoked_keys={} unfunded_keys={} invoked_only={} unfunded_only={} invoked_proof_bytes={invoked_proof} unfunded_proof_bytes={unfunded_proof}", + invoked_keys.len(), + unfunded_keys.len(), + invoked_keys.difference(&unfunded_keys).count(), + unfunded_keys.difference(&invoked_keys).count() + ); + for (outcome, (control, effect)) in [("Temporary", first), ("Retry", resumed)] { + assert_ne!(control, Weight::zero()); + assert_ne!(effect, Weight::zero()); + assert!(control.all_lte(BlockResourceBudgetValue::get().limits().actor_control())); + assert!(effect.all_lte(BlockResourceBudgetValue::get().limits().actor_base_turn())); + cells.push((outcome, predicate_count, control, effect)); + } + } + let expected_control = [ + Weight::from_parts(6_157_667_980, 88_339), + Weight::from_parts(6_521_921_980, 90_386), + Weight::from_parts(6_533_528_661, 110_565), + Weight::from_parts(6_897_782_661, 112_612), + Weight::from_parts(6_652_093_584, 115_911), + Weight::from_parts(7_016_347_584, 117_958), + ]; + for ((outcome, predicates, control, effect), expected_control) in + cells.into_iter().zip(expected_control) + { + assert_eq!(control, expected_control.saturating_add(receipt)); + assert_eq!(effect, Weight::from_parts(3_414_697_000, 19_253)); + println!( + "ACTOR_SWAPOUT_MATRIX_V1 outcome={outcome} predicates={predicates} control_ref_time={} control_proof_size={} effect_ref_time={} effect_proof_size={}", + control.ref_time(), + control.proof_size(), + effect.ref_time(), + effect.proof_size(), + ); + } +} + +fn measure_funding_unavailable_retry(funding_tail: Option) -> [(Weight, Weight); 2] { + super::common::synthetic_actor_test_ext().execute_with(|| { + System::set_block_number(1); + assert_synthetic_actor_genesis(); + let step = RuntimeStep { + precondition: None, + task: Task::Transfer { + asset: AssetKind::Native, + to: BOB, + amount: AmountResolution::Fixed(100u128.saturating_mul(crate::EXISTENTIAL_DEPOSIT)), + }, + on_error: StepErrorPolicy::RetryLater { max_attempts: 3 }, + }; + let mut steps = vec![step]; + if funding_tail.is_some() { + steps.push(make_step(Task::Transfer { + asset: AssetKind::Native, + to: BOB, + amount: AmountResolution::PercentageOfLastFunding(Perbill::one()), + })); + } + let actor_id = Actors::next_actor_id(); + let mut contract = system_active_contract( + manual_schedule(), + None, + BoundedVec::try_from(steps).expect("retry head and optional funding tail fit"), + ) + .expect("active retry Contract exists"); + contract.funding = pallet_deos_actors::FundingSourcePolicy::OwnerOnly; + assert_ok!(Actors::create_system_actor( + RuntimeOrigin::root(), + ALICE, + Mutability::Mutable, + Some(contract), + )); + let custody = 10u128.saturating_mul(crate::EXISTENTIAL_DEPOSIT); + let ingress = funding_tail.unwrap_or_default(); + assert!(ingress <= custody); + fund_native(actor_id, custody - ingress); + if ingress > 0 { + fund_native_via_call(ALICE, actor_id, ingress); + assert_eq!( + actor_funding(actor_id) + .funding_accumulated + .get(&AssetKind::Native), + Some(&ingress) + ); + } + let sovereign = actor_account(actor_id); + let before = ( + Balances::free_balance(&sovereign), + Balances::free_balance(BOB), + ); + assert_ok!(Actors::manual_trigger(RuntimeOrigin::root(), actor_id)); + + let mut measured = [(Weight::zero(), Weight::zero()); 2]; + for block in 2..=3 { + 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); + let telemetry = Actors::finalized_block_resource_telemetry() + .filter(|snapshot| snapshot.block_number() == block) + .expect("FundingUnavailable block finalizes telemetry"); + assert!(!telemetry.optional_actor_work_halted()); + let continuation = Actors::actor_run_state(actor_id).expect("funding failure suspends"); + assert_eq!(continuation.cursor, 0); + if ingress == 0 { + assert!(continuation.funding_snapshot.is_empty()); + } else { + assert_eq!(continuation.funding_snapshot.len(), 1); + assert_eq!( + continuation.funding_snapshot.get(&AssetKind::Native), + Some(&ingress) + ); + } + assert!(actor_funding(actor_id).funding_accumulated.is_empty()); + assert_eq!( + ( + Balances::free_balance(&sovereign), + Balances::free_balance(BOB) + ), + before + ); + assert_eq!(continuation.unsuccessful_attempts_at_cursor, block - 1); + assert!(has_actor_event(|event| matches!( + event, + Event::CycleSuspended { + actor_id: id, + reason: pallet_deos_actors::SuspensionReason::FundingUnavailable, + .. + } if *id == actor_id + ))); + let usage = telemetry.usage(); + measured[(block - 2) as usize] = (usage.actor_control_used(), usage.actor_effect_used()); + Actors::on_finalize(block); + } + measured + }) +} + +#[test] +fn funding_unavailable_and_retry_complete_paths_are_exact() { + let [first, retry] = measure_funding_unavailable_retry(None); + assert_eq!( + first, + (Weight::from_parts(6_157_667_980, 88_339), Weight::zero()) + ); + assert_eq!( + retry, + (Weight::from_parts(6_526_601_980, 90_386), Weight::zero()) + ); + println!( + "ACTOR_FUNDING_RETRY_V1 phase=Opening control_ref_time={} control_proof_size={} effect_ref_time={} effect_proof_size={}", + first.0.ref_time(), + first.0.proof_size(), + first.1.ref_time(), + first.1.proof_size(), + ); + println!( + "ACTOR_FUNDING_RETRY_V1 phase=Suspended control_ref_time={} control_proof_size={} effect_ref_time={} effect_proof_size={}", + retry.0.ref_time(), + retry.0.proof_size(), + retry.1.ref_time(), + retry.1.proof_size(), + ); + let empty = measure_funding_unavailable_retry(Some(0)); + let funded = measure_funding_unavailable_retry(Some(crate::EXISTENTIAL_DEPOSIT)); + assert_eq!(empty[0], funded[0], "Opening retains its admission bound"); + assert_eq!(empty[1].1, Weight::zero()); + assert_eq!(funded[1].1, Weight::zero()); + assert_eq!( + funded[1].0, empty[1].0, + "coupled Suspended-head profiles do not expose an independent funding-entry refund", + ); +} + +#[test] +fn minimal_pipeline_admission_apoptosis_complete_path_is_exact() { + super::common::synthetic_actor_test_ext().execute_with(|| { + System::set_block_number(1); + assert_synthetic_actor_genesis(); + let steps = transfer_contract_steps(BOB, AssetKind::Native, 10); + let prefunded = user_prefunding_requirement(&steps); + let envelope = Actors::attempt_fee_envelope(ActorType::User, &steps, 0) + .expect("User attempt fee is bounded"); + let min_balance = ::MinUserBalance::get(); + let actor_id = create_user(ALICE, manual_schedule(), None, steps); + deplete_user_sovereign(actor_id, prefunded); + let raw_balance = envelope.total.max(min_balance); + let trigger_fee = ::WeightToFee::weight_to_fee( + &<::WeightInfo as WeightInfo>::manual_trigger(), + ); + fund_native(actor_id, raw_balance.saturating_add(trigger_fee)); + assert!(raw_balance >= envelope.total); + assert!(raw_balance.saturating_sub(min_balance) < envelope.total); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + + let block = 2; + 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); + let telemetry = Actors::finalized_block_resource_telemetry() + .filter(|snapshot| snapshot.block_number() == block) + .expect("apoptosis block finalizes telemetry"); + assert!(!telemetry.optional_actor_work_halted()); + assert!(Actors::active_actor_state(actor_id).is_none()); + assert!(has_actor_event(|event| matches!( + event, + Event::ActorClosed { + actor_id: id, + reason: CloseReason::CycleAdmissionInsufficient, + } if *id == actor_id + ))); + let usage = telemetry.usage(); + let measured = (usage.actor_control_used(), usage.actor_effect_used()); + assert_eq!( + measured, + (Weight::from_parts(4_862_811_748, 48_294), Weight::zero()), + "the complete apoptosis owner replaces provisional FIFO discovery, actor probe, and Ready consumption accounting" + ); + println!( + "ACTOR_MINIMAL_APOPTOSIS_V1 control_ref_time={} control_proof_size={} effect_ref_time={} effect_proof_size={}", + measured.0.ref_time(), + measured.0.proof_size(), + measured.1.ref_time(), + measured.1.proof_size(), + ); + Actors::on_finalize(block); + }); +} + +fn saturate_user_base_with_router_swaps( + signer: &sr25519::Pair, + nonce: &mut u32, + marker: u32, +) -> UserDispatchSaturation { + let signer_account = crate::AccountId::from(signer.public()); + let user_base = BlockResourceBudgetValue::get().limits().user_base_turn(); + let mut calls = 0u32; + loop { + assert!(calls < 1_000, "Router saturation must remain bounded"); + let native_to_local = calls.is_multiple_of(2); + let call = RuntimeCall::DeosRouter(pallet_deos_router::Call::swap { + from: if native_to_local { + AssetKind::Native + } else { + AssetKind::Local(ASSET_A) + }, + to: if native_to_local { + AssetKind::Local(ASSET_A) + } else { + AssetKind::Native + }, + amount_in: super::common::SWAP_AMOUNT, + min_amount_out: 1, + recipient: signer_account.clone(), + deadline: marker.saturating_add(1), + }); + let extrinsic = signed_extrinsic(signer, crate::Nonce::from(*nonce), call); + let next_weight = extrinsic.get_dispatch_info().total_weight(); + let state = Actors::block_resource_state().expect("Prepass resource state exists"); + let remaining = user_base + .checked_sub(&state.usage().user_dispatch_used()) + .unwrap_or_else(Weight::zero); + if !next_weight.all_lte(remaining) { + assert!(calls > 0, "User base must admit at least one Router swap"); + return UserDispatchSaturation { calls, next_weight }; + } + let outcome = Executive::apply_extrinsic(extrinsic) + .unwrap_or_else(|error| panic!("valid Router saturation call rejected: {error:?}")); + assert!( + outcome.is_ok(), + "Router saturation call failed: {outcome:?}" + ); + *nonce = nonce.saturating_add(1); + calls = calls.saturating_add(1); + } +} + +#[test] +fn saturated_prepass_preserves_drain_finalization_reachability() { + synthetic_actor_test_ext().execute_with(|| { + System::set_block_number(1); + assert_synthetic_actor_genesis(); + let actor_ids = setup_inert_actors(128, 1_000 * crate::EXISTENTIAL_DEPOSIT); + let block = 2; + 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 prepass = Actors::block_resource_state().expect("Prepass resource state exists"); + assert!(!prepass.optional_actor_work_halted()); + Actors::on_idle(block, Weight::MAX); + let telemetry = Actors::finalized_block_resource_telemetry() + .filter(|snapshot| snapshot.block_number() == block) + .expect("saturated block finalizes resource telemetry"); + assert!(!telemetry.optional_actor_work_halted()); + assert!(Actors::block_resource_state().is_some_and(|state| state.finalized_snapshot().is_ok())); + assert!( + actor_ids.iter().any(|actor_id| { + Actors::active_actor_state(*actor_id).is_some_and(|state| state.identity.cycle_nonce == 0) + }), + "fixture must leave an eligible FIFO suffix after saturating the Prepass" + ); + Actors::on_finalize(block); + assert!(Actors::block_resource_state().is_none()); + }); +} + +#[test] +fn ref_time_heavy_user_frontier_preserves_actor_progress_and_finalization() { + seeded_synthetic_actor_test_ext().execute_with(|| { + System::set_block_number(1); + assert_synthetic_actor_genesis(); + assert_ok!(super::common::setup_deos_router_infrastructure()); + let contract_steps = transfer_contract_steps(BOB, AssetKind::Native, 1); + let mut actor_ids = Vec::with_capacity(128); + for _ in 0..128 { + let actor_id = create_system(ALICE, manual_schedule(), None, contract_steps.clone()); + fund_native(actor_id, 1_000 * crate::EXISTENTIAL_DEPOSIT); + assert_ok!(Actors::manual_trigger(RuntimeOrigin::root(), actor_id)); + actor_ids.push(actor_id); + } + let signer = sr25519::Pair::from_seed(&[83u8; 32]); + let signer_account = crate::AccountId::from(signer.public()); + assert_ok!(Balances::force_set_balance( + RuntimeOrigin::root(), + polkadot_sdk::sp_runtime::MultiAddress::Id(signer_account.clone()), + u128::MAX / 4, + )); + assert_ok!(Assets::mint( + RuntimeOrigin::signed(ALICE), + ASSET_A, + polkadot_sdk::sp_runtime::MultiAddress::Id(signer_account), + u128::MAX / 4, + )); + + let block = 2; + 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 mut nonce = 0; + let saturation = saturate_user_base_with_router_swaps(&signer, &mut nonce, block); + let before_actor_idle = Actors::block_resource_state() + .expect("User-heavy Prepass state exists") + .usage() + .user_dispatch_used(); + let user_limit = BlockResourceBudgetValue::get().limits().user_base_turn(); + let remaining = user_limit + .checked_sub(&before_actor_idle) + .expect("User usage fits its turn"); + assert!(!saturation.next_weight.all_lte(remaining)); + assert!(saturation.next_weight.ref_time() <= remaining.ref_time()); + assert!(saturation.next_weight.proof_size() > remaining.proof_size()); + + Actors::on_idle(block, Weight::MAX); + let telemetry = Actors::finalized_block_resource_telemetry() + .filter(|snapshot| snapshot.block_number() == block) + .expect("RefTime-heavy block finalizes resource telemetry"); + assert!(!telemetry.optional_actor_work_halted()); + assert!(actor_ids.iter().any(|actor_id| { + Actors::active_actor_state(*actor_id).is_some_and(|state| state.identity.cycle_nonce > 0) + })); + assert!(Actors::block_resource_state().is_some_and(|state| state.finalized_snapshot().is_ok())); + println!( + "ACTOR_USER_FRONTIER_V1 calls={} user_ref_time={} user_proof_size={} remaining_ref_time={} remaining_proof_size={} next_ref_time={} next_proof_size={}", + saturation.calls, + before_actor_idle.ref_time(), + before_actor_idle.proof_size(), + remaining.ref_time(), + remaining.proof_size(), + saturation.next_weight.ref_time(), + saturation.next_weight.proof_size(), + ); + Actors::on_finalize(block); + assert!(Actors::block_resource_state().is_none()); + }); +} + +fn run_transfer_homogeneous_trigger_profile(label: &str, cadenced: bool) { + super::common::synthetic_actor_test_ext().execute_with(|| { + System::set_block_number(1); + assert_synthetic_actor_genesis(); + let actor_count = ::MaxActiveActors::get(); + let initial_balance = 1_000u128.saturating_mul(crate::EXISTENTIAL_DEPOSIT); + let contract_steps = transfer_contract_steps(BOB, AssetKind::Native, 1); + let (next_control_maximum, next_effect_maximum) = one_step_fifo_attempt_maxima(&contract_steps); + let mut actor_ids = Vec::with_capacity(actor_count as usize); + for _ in 0..actor_count { + let actor_id = create_system( + ALICE, + RuntimeSchedule { + trigger: if cadenced { + Trigger::cadenced(1) + } else { + Trigger::manual() + }, + cooldown_blocks: 0, + }, + None, + contract_steps.clone(), + ); + fund_native(actor_id, initial_balance); + if !cadenced { + assert_ok!(Actors::manual_trigger(RuntimeOrigin::root(), actor_id)); + } + actor_ids.push(actor_id); + } + + let mut diagnostics = ProductionThroughputDiagnostics::default(); + for block in 2..=2_501u32 { + 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 base_event_end = System::events().len(); + Actors::on_idle(block, Weight::MAX); + diagnostics.record_one_step_block( + block, + base_event_end, + next_control_maximum, + next_effect_maximum, + None, + ); + Actors::on_finalize(block); + if diagnostics.progressed_actor_ids.len() == actor_ids.len() { + break; + } + } + diagnostics.assert_complete_first_traversal(&actor_ids); + let per_block_steps = diagnostics + .samples + .iter() + .map(|sample| sample.progressed_actor_ids.len() as u32) + .collect::>(); + let total_steps = per_block_steps.iter().sum::(); + let control_stops = diagnostics + .samples + .iter() + .filter(|sample| sample.stop_reason == ProductionStopReason::ActorControl) + .count(); + if cadenced { + assert_eq!(diagnostics.samples.len(), 1_726); + assert_eq!(total_steps, 10_002); + assert_eq!(nearest_rank_percentile(&per_block_steps, 50), 6); + assert_eq!(nearest_rank_percentile(&per_block_steps, 95), 13); + assert_eq!(control_stops, 1_725); + assert_eq!(diagnostics.maximum_service_gap_blocks, 1_723); + assert_eq!(diagnostics.cadenced_occurrences(), 19_992); + } else { + assert_eq!(diagnostics.samples.len(), 477); + assert_eq!(total_steps, 10_000); + assert_eq!(nearest_rank_percentile(&per_block_steps, 50), 21); + assert_eq!(nearest_rank_percentile(&per_block_steps, 95), 21); + assert_eq!(control_stops, 476); + assert_eq!(diagnostics.maximum_service_gap_blocks, 0); + assert_eq!(diagnostics.cadenced_occurrences(), 0); + } + diagnostics.print_summary(label); + }); +} + +#[test] +#[ignore = "10,000 homogeneous Manual/Cadenced Trigger attribution; run through actors-assurance"] +fn transfer_10k_homogeneous_trigger_attribution() { + run_transfer_homogeneous_trigger_profile("ACTOR_THROUGHPUT_MANUAL_ONLY", false); + run_transfer_homogeneous_trigger_profile("ACTOR_THROUGHPUT_CADENCED_ONLY", true); +} + +fn run_transfer_predicate_population_profile(predicate_count: u32) { + super::common::synthetic_actor_test_ext().execute_with(|| { + System::set_block_number(1); + assert_synthetic_actor_genesis(); + let actor_count = ::MaxActiveActors::get(); + let initial_balance = 1_000u128.saturating_mul(crate::EXISTENTIAL_DEPOSIT); + let contract_steps = BoundedVec::try_from(vec![RuntimeStep { + precondition: all_preconditions_at( + transfer_matrix_predicates(predicate_count), + pallet_deos_actors::ObservationTiming::Opening, + ), + task: Task::Transfer { + asset: AssetKind::Native, + to: BOB, + amount: AmountResolution::Fixed(crate::EXISTENTIAL_DEPOSIT), + }, + on_error: StepErrorPolicy::AbortCycle, + }]) + .expect("one predicated Transfer Step fits"); + let (next_control_maximum, next_effect_maximum) = one_step_fifo_attempt_maxima(&contract_steps); + 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.is_multiple_of(2) { + Trigger::manual() + } else { + Trigger::cadenced(1) + }, + cooldown_blocks: 0, + }, + None, + contract_steps.clone(), + ); + fund_native(actor_id, initial_balance); + if index.is_multiple_of(2) { + assert_ok!(Actors::manual_trigger(RuntimeOrigin::root(), actor_id)); + } + actor_ids.push(actor_id); + } + + let mut diagnostics = ProductionThroughputDiagnostics::default(); + for block in 2..=2_501u32 { + 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 base_event_end = System::events().len(); + Actors::on_idle(block, Weight::MAX); + diagnostics.record_one_step_block( + block, + base_event_end, + next_control_maximum, + next_effect_maximum, + None, + ); + Actors::on_finalize(block); + if diagnostics.progressed_actor_ids.len() == actor_ids.len() { + break; + } + } + diagnostics.assert_complete_first_traversal(&actor_ids); + let per_block_steps = diagnostics + .samples + .iter() + .map(|sample| sample.progressed_actor_ids.len() as u32) + .collect::>(); + diagnostics.print_summary(&format!("ACTOR_THROUGHPUT_PREDICATES_{predicate_count}")); + match predicate_count { + 2 => { + assert_eq!(diagnostics.samples.len(), 1_900); + assert_eq!(per_block_steps.iter().sum::(), 10_002); + assert_eq!(nearest_rank_percentile(&per_block_steps, 50), 5); + assert_eq!(nearest_rank_percentile(&per_block_steps, 95), 8); + assert_eq!(diagnostics.maximum_service_gap_blocks, 999); + assert_eq!(diagnostics.cadenced_occurrences(), 9_997); + } + 4 => { + assert_eq!(diagnostics.samples.len(), 1_345); + assert_eq!(per_block_steps.iter().sum::(), 10_002); + assert_eq!(nearest_rank_percentile(&per_block_steps, 50), 7); + assert_eq!(nearest_rank_percentile(&per_block_steps, 95), 15); + assert_eq!(diagnostics.maximum_service_gap_blocks, 731); + assert_eq!(diagnostics.cadenced_occurrences(), 9_996); + } + _ => panic!("unsupported homogeneous Predicate population"), + } + }); +} + +#[test] +#[ignore = "10,000-Actor homogeneous 2/4-Predicate attribution; run through actors-assurance"] +fn transfer_10k_homogeneous_predicate_attribution() { + run_transfer_predicate_population_profile(2); + run_transfer_predicate_population_profile(4); +} + +fn run_control_only_10k_profile(label: &str, proof_saturated_user: bool) { + super::common::synthetic_actor_test_ext().execute_with(|| { + System::set_block_number(1); + assert_synthetic_actor_genesis(); + let actor_count = ::MaxActiveActors::get(); + let contract_steps = + BoundedVec::try_from(vec![make_step(Task::StopCycle)]).expect("one control Step fits"); + let (next_control_maximum, next_effect_maximum) = one_step_fifo_attempt_maxima(&contract_steps); + assert_eq!(next_effect_maximum, Weight::zero()); + 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, + contract_steps.clone(), + ); + 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 user_nonce = 0u32; + let mut diagnostics = ProductionThroughputDiagnostics::default(); + 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 base_event_end = System::events().len(); + let user_saturation = proof_saturated_user + .then(|| saturate_user_base_with_remarks(&signer_pair, &mut user_nonce, block)); + Actors::on_idle(block, Weight::MAX); + diagnostics.record_one_step_block( + block, + base_event_end, + next_control_maximum, + next_effect_maximum, + user_saturation, + ); + Actors::on_finalize(block); + if diagnostics.progressed_actor_ids.len() == actor_ids.len() { + break; + } + } + diagnostics.assert_complete_first_traversal(&actor_ids); + diagnostics.print_summary(label); + }); +} + +#[test] +#[ignore = "10,000 control-only Actor production profile; run through actors-assurance"] +fn control_only_10k_manual_and_reactive_first_traversal() { + run_control_only_10k_profile("ACTOR_THROUGHPUT_CONTROL_ONLY_W1", false); +} + +#[test] +#[ignore = "10,000 control-only Actors with proof-saturated valid user demand; run through actors-assurance"] +fn control_only_10k_first_traversal_with_continuous_user_dispatch() { + run_control_only_10k_profile("ACTOR_THROUGHPUT_CONTROL_ONLY_W2_PROOF_SATURATED", true); +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum ProductionUserLoad { + None, + ProofSaturatedRemarks, + RefTimeHeavyRouter, +} + +fn run_swapout_10k_profile(label: &str, user_load: ProductionUserLoad) { + seeded_synthetic_actor_test_ext().execute_with(|| { + System::set_block_number(1); + assert_synthetic_actor_genesis(); + assert_ok!(super::common::setup_deos_router_infrastructure()); + let actor_count = ::MaxActiveActors::get(); + let initial_balance = 10_000u128.saturating_mul(crate::EXISTENTIAL_DEPOSIT); + let contract_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 (next_control_maximum, next_effect_maximum) = one_step_fifo_attempt_maxima(&contract_steps); + 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, + contract_steps.clone(), + ); + 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(&[60u8; 32]); + let signer = AccountId::from(signer_pair.public()); + let _ = >::deposit_creating(&signer, u128::MAX / 4); + if user_load == ProductionUserLoad::RefTimeHeavyRouter { + assert_ok!(Assets::mint( + RuntimeOrigin::signed(ALICE), + ASSET_A, + polkadot_sdk::sp_runtime::MultiAddress::Id(signer.clone()), + u128::MAX / 4, + )); + } + let mut user_nonce = 0u32; + let mut diagnostics = ProductionThroughputDiagnostics::default(); + 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 base_event_end = System::events().len(); + let user_saturation = match user_load { + ProductionUserLoad::None => None, + ProductionUserLoad::ProofSaturatedRemarks => Some(saturate_user_base_with_remarks( + &signer_pair, + &mut user_nonce, + block, + )), + ProductionUserLoad::RefTimeHeavyRouter => Some(saturate_user_base_with_router_swaps( + &signer_pair, + &mut user_nonce, + block, + )), + }; + Actors::on_idle(block, Weight::MAX); + diagnostics.record_one_step_block( + block, + base_event_end, + next_control_maximum, + next_effect_maximum, + user_saturation, + ); + Actors::on_finalize(block); + if diagnostics.progressed_actor_ids.len() == actor_ids.len() { + break; + } + } + diagnostics.assert_complete_first_traversal(&actor_ids); + diagnostics.print_summary(label); + }); +} + +#[test] +#[ignore = "10,000 Router-backed SwapOut Actor production profile; run through actors-assurance"] +fn swapout_10k_manual_and_reactive_first_traversal() { + run_swapout_10k_profile("ACTOR_THROUGHPUT_SWAPOUT_W1", ProductionUserLoad::None); +} + +#[test] +#[ignore = "10,000 Router-backed SwapOut Actors with proof-saturated valid user demand; run through actors-assurance"] +fn swapout_10k_first_traversal_with_continuous_user_dispatch() { + run_swapout_10k_profile( + "ACTOR_THROUGHPUT_SWAPOUT_W2_PROOF_SATURATED", + ProductionUserLoad::ProofSaturatedRemarks, + ); +} + +#[test] +#[ignore = "10,000 Router-backed SwapOut Actors with continuous RefTime-heavy valid user demand; run through actors-assurance"] +fn swapout_10k_first_traversal_with_ref_time_heavy_user_dispatch() { + run_swapout_10k_profile( + "ACTOR_THROUGHPUT_SWAPOUT_W3_REF_TIME_HEAVY", + ProductionUserLoad::RefTimeHeavyRouter, + ); +} + #[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(|| { + synthetic_actor_test_ext().execute_with(|| { System::set_block_number(1); - close_genesis_system_actors(); + assert_synthetic_actor_genesis(); let actor_count = ::MaxActiveActors::get(); let initial_balance = 1_000u128.saturating_mul(crate::EXISTENTIAL_DEPOSIT); + let contract_steps = transfer_contract_steps(BOB, AssetKind::Native, 1); + let (next_control_maximum, next_effect_maximum) = + one_step_fifo_attempt_maxima(&contract_steps); let mut actor_ids = Vec::with_capacity(actor_count as usize); for index in 0..actor_count { let trigger = if index % 2 == 0 { @@ -10398,7 +14606,7 @@ fn transfer_10k_manual_and_reactive_first_traversal() { cooldown_blocks: 0, }, None, - transfer_contract_steps(BOB, AssetKind::Native, 1), + contract_steps.clone(), ); fund_native(actor_id, initial_balance); if index % 2 == 0 { @@ -10411,7 +14619,7 @@ fn transfer_10k_manual_and_reactive_first_traversal() { actor_count ); - let mut progressed = alloc::collections::BTreeSet::new(); + let mut diagnostics = ProductionThroughputDiagnostics::default(); let mut completion_block = None; for block in 2..=1_301u32 { System::set_block_number(block); @@ -10422,22 +14630,24 @@ fn transfer_10k_manual_and_reactive_first_traversal() { ); assert_eq!(Actors::on_initialize(block), Weight::zero()); ensure_current_resource_state(); + let base_event_end = System::events().len(); 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() { + diagnostics.record_one_step_block( + block, + base_event_end, + next_control_maximum, + next_effect_maximum, + None, + ); + Actors::on_finalize(block); + if diagnostics.progressed_actor_ids.len() == actor_ids.len() { completion_block = Some(block); break; } } - assert_eq!(progressed.len(), actor_ids.len()); + diagnostics.assert_complete_first_traversal(&actor_ids); + diagnostics.print_summary("ACTOR_THROUGHPUT_W1"); + assert_eq!(diagnostics.cadenced_occurrences(), 9_997); let limits = crate::configs::BlockResourceBudgetValue::get().limits(); println!( "CONTROL_SWEEP_W1 control={:?} shared={:?} actor_base={:?} user_base={:?} completion_block={:?}", @@ -10451,14 +14661,15 @@ fn transfer_10k_manual_and_reactive_first_traversal() { } #[test] -#[ignore = "10,000 effectful Actors with continuous user demand; run through actors-assurance"] +#[ignore = "10,000 effectful Actors with proof-saturated valid 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(|| { + synthetic_actor_test_ext().execute_with(|| { System::set_block_number(1); - close_genesis_system_actors(); + assert_synthetic_actor_genesis(); let actor_count = ::MaxActiveActors::get(); let initial_balance = 1_000u128.saturating_mul(crate::EXISTENTIAL_DEPOSIT); + let contract_steps = transfer_contract_steps(BOB, AssetKind::Native, 1); + let (next_control_maximum, next_effect_maximum) = one_step_fifo_attempt_maxima(&contract_steps); let mut actor_ids = Vec::with_capacity(actor_count as usize); for index in 0..actor_count { let actor_id = create_system( @@ -10472,7 +14683,7 @@ fn transfer_10k_first_traversal_with_continuous_user_dispatch() { cooldown_blocks: 0, }, None, - transfer_contract_steps(BOB, AssetKind::Native, 1), + contract_steps.clone(), ); fund_native(actor_id, initial_balance); if index % 2 == 0 { @@ -10484,7 +14695,8 @@ fn transfer_10k_first_traversal_with_continuous_user_dispatch() { 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 user_nonce = 0u32; + let mut diagnostics = ProductionThroughputDiagnostics::default(); let mut completion_block = None; for block in 2..=1_301u32 { System::set_block_number(block); @@ -10495,35 +14707,20 @@ fn transfer_10k_first_traversal_with_continuous_user_dispatch() { ); 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 base_event_end = System::events().len(); + let user_saturation = saturate_user_base_with_remarks(&signer_pair, &mut user_nonce, block); 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() { + diagnostics.record_one_step_block( + block, + base_event_end, + next_control_maximum, + next_effect_maximum, + Some(user_saturation), + ); + Actors::on_finalize(block); + if diagnostics.progressed_actor_ids.len() == actor_ids.len() { completion_block = Some(block); break; } @@ -10531,19 +14728,21 @@ fn transfer_10k_first_traversal_with_continuous_user_dispatch() { let missing_manual = actor_ids .iter() .step_by(2) - .filter(|actor_id| !progressed.contains(actor_id)) + .filter(|actor_id| !diagnostics.progressed_actor_ids.contains(actor_id)) .count(); let missing_reactive = actor_ids .iter() .skip(1) .step_by(2) - .filter(|actor_id| !progressed.contains(actor_id)) + .filter(|actor_id| !diagnostics.progressed_actor_ids.contains(actor_id)) .count(); assert_eq!( - progressed.len(), + diagnostics.progressed_actor_ids.len(), actor_ids.len(), "missing Manual={missing_manual}, reactive={missing_reactive}" ); + diagnostics.assert_complete_first_traversal(&actor_ids); + diagnostics.print_summary("ACTOR_THROUGHPUT_W2_PROOF_SATURATED"); let limits = crate::configs::BlockResourceBudgetValue::get().limits(); println!( "CONTROL_SWEEP control={:?} shared={:?} actor_base={:?} user_base={:?} completion_block={:?}", @@ -10559,22 +14758,13 @@ fn transfer_10k_first_traversal_with_continuous_user_dispatch() { #[test] #[ignore = "mixed 10,000-Actor production throughput profile"] fn mixed_9500_transfer_400_swapout_100_control_first_traversal() { - seeded_test_ext().execute_with(|| { + seeded_synthetic_actor_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_synthetic_actor_genesis(); assert_ok!(super::common::setup_deos_router_infrastructure()); let actor_count = ::MaxActiveActors::get(); let initial_balance = 10_000u128.saturating_mul(crate::EXISTENTIAL_DEPOSIT); + let transfer_steps = transfer_contract_steps(BOB, AssetKind::Native, 1); let swap_steps = BoundedVec::try_from(vec![make_step(Task::SwapOut { asset_out: AssetKind::Local(ASSET_A), amount_out: AmountResolution::Fixed(crate::EXISTENTIAL_DEPOSIT), @@ -10585,10 +14775,19 @@ fn mixed_9500_transfer_400_swapout_100_control_first_traversal() { .expect("one SwapOut Step fits"); let control_steps = BoundedVec::try_from(vec![make_step(Task::StopCycle)]) .expect("one control Step fits"); + let (next_control_maximum, transfer_effect_maximum) = + one_step_fifo_attempt_maxima(&transfer_steps); + let (swap_control_maximum, swap_effect_maximum) = + one_step_fifo_attempt_maxima(&swap_steps); + assert_eq!(next_control_maximum, swap_control_maximum); + let next_effect_maximum = transfer_effect_maximum.max(swap_effect_maximum); let mut actor_ids = Vec::with_capacity(actor_count as usize); + let mut transfer_ids = BTreeSet::new(); + let mut swap_ids = BTreeSet::new(); + let mut control_ids = BTreeSet::new(); for index in 0..actor_count { let steps = if index < 9_500 { - transfer_contract_steps(BOB, AssetKind::Native, 1) + transfer_steps.clone() } else if index < 9_900 { swap_steps.clone() } else { @@ -10611,8 +14810,19 @@ fn mixed_9500_transfer_400_swapout_100_control_first_traversal() { if index % 2 == 0 { assert_ok!(Actors::manual_trigger(RuntimeOrigin::root(), actor_id)); } + if index < 9_500 { + transfer_ids.insert(actor_id); + } else if index < 9_900 { + swap_ids.insert(actor_id); + } else { + control_ids.insert(actor_id); + } actor_ids.push(actor_id); } + assert_eq!( + (transfer_ids.len(), swap_ids.len(), control_ids.len()), + (9_500, 400, 100) + ); let signer_pair = sr25519::Pair::from_seed(&[59u8; 32]); let signer = AccountId::from(signer_pair.public()); @@ -10620,8 +14830,8 @@ fn mixed_9500_transfer_400_swapout_100_control_first_traversal() { &signer, 1_000_000_000_000_000_000, ); - let mut progressed = alloc::collections::BTreeSet::new(); - let mut failed_steps = 0u32; + let mut user_nonce = 0u32; + let mut diagnostics = ProductionThroughputDiagnostics::default(); let mut completion_block = None; for block in 2..=1_301u32 { System::set_block_number(block); @@ -10632,40 +14842,43 @@ fn mixed_9500_transfer_400_swapout_100_control_first_traversal() { ); 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()); + let base_event_end = System::events().len(); + let user_saturation = + saturate_user_base_with_remarks(&signer_pair, &mut user_nonce, block); 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() { + diagnostics.record_one_step_block( + block, + base_event_end, + next_control_maximum, + next_effect_maximum, + Some(user_saturation), + ); + Actors::on_finalize(block); + if diagnostics.progressed_actor_ids.len() == actor_ids.len() { completion_block = Some(block); break; } } - assert_eq!(progressed.len(), actor_ids.len()); + diagnostics.assert_complete_first_traversal(&actor_ids); + diagnostics.print_summary("ACTOR_THROUGHPUT_MIXED_9500_400_100_W2"); + assert_eq!(diagnostics.cadenced_occurrences(), 9_998); 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(); + let progressed = diagnostics + .samples + .iter() + .flat_map(|sample| sample.progressed_actor_ids.iter()) + .copied() + .collect::>(); + let transfer_commits = progressed.intersection(&transfer_ids).count(); + let swap_commits = progressed.intersection(&swap_ids).count(); + let control_commits = progressed.intersection(&control_ids).count(); + assert_eq!((transfer_commits, swap_commits, control_commits), (9_500, 400, 100)); 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, + "ACTOR_MIXED_REPORT_V1 {{\"completionBlock\":{},\"transferCommits\":{transfer_commits},\"swapOutCommits\":{swap_commits},\"controlCommits\":{control_commits},\"failedSteps\":0}}", + completion_block.unwrap_or_default(), ); }); } @@ -10673,30 +14886,20 @@ fn mixed_9500_transfer_400_swapout_100_control_first_traversal() { #[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(|| { + synthetic_actor_test_ext().execute_with(|| { System::set_block_number(1); - let num_blocks = 1_300u32; + let num_blocks = 2_500u32; 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. - // Remove dormant genesis identities so the identity cap does not prevent saturating - // the independently asserted active-actor cap. - let genesis_ids: alloc::vec::Vec = alloc::vec![0, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]; - for &id in &genesis_ids { - let _ = Actors::pause_actor(RuntimeOrigin::root(), id); - } - let dormant: alloc::vec::Vec<_> = 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); + assert_synthetic_actor_genesis(); + // Retain paused synthetic actors to validate mixed ready/non-ready fairness + // at the active cap without removing sealed reference genesis identities. + let paused_ids = setup_inert_actors(3, initial_balance); + for id in paused_ids { + age_fixture_control_clock(id); + assert_ok!(Actors::pause_actor(RuntimeOrigin::root(), id)); } - pallet_deos_actors::ActorIdentityCount::::mutate(|count| { - *count = count.saturating_sub(dormant.len() as u32); - }); - let active_before = pallet_deos_actors::ActorHot::::iter_keys().count() as u64; + let active_before = pallet_deos_actors::ActorControlLocators::::iter_keys().count() as u64; assert!( active_before < max_active, "Test precondition failed: active_before={} must be < max_active={}", @@ -10706,7 +14909,7 @@ fn stress_10k_actors_queue_scheduler() { let actor_count = max_active - active_before; let actor_ids = setup_mixed_inert_actors(actor_count, initial_balance); assert_eq!(actor_ids.len(), actor_count as usize); - let active_after = pallet_deos_actors::ActorHot::::iter_keys().count() as u64; + let active_after = pallet_deos_actors::ActorControlLocators::::iter_keys().count() as u64; assert_eq!( active_after, max_active, "ActiveActors must be saturated to max capacity", @@ -10765,7 +14968,7 @@ fn dust_attack_min_balance_actors_preserve_scheduler_stability() { seeded_test_ext().execute_with(|| { let min_balance = ::MinUserBalance::get(); let actor_count = 96u32; - let baseline_active = pallet_deos_actors::ActorHot::::iter_keys().count(); + let baseline_active = pallet_deos_actors::ActorControlLocators::::iter_keys().count(); let mut actor_ids = Vec::new(); for i in 0..actor_count { let mut owner_bytes = [0u8; 32]; @@ -10793,13 +14996,13 @@ fn dust_attack_min_balance_actors_preserve_scheduler_stability() { ); actor_ids.push(actor_id); } - let initial_active = pallet_deos_actors::ActorHot::::iter_keys().count(); + let initial_active = pallet_deos_actors::ActorControlLocators::::iter_keys().count(); assert_eq!(initial_active, baseline_active + actor_count as usize); for block in 1..=32u32 { System::set_block_number(block); run_idle(Weight::MAX); } - let final_active = pallet_deos_actors::ActorHot::::iter_keys().count(); + let final_active = pallet_deos_actors::ActorControlLocators::::iter_keys().count(); let progressed = actor_ids .iter() .filter(|id| { diff --git a/template/runtime/src/tests/common.rs b/template/runtime/src/tests/common.rs index 88aa3782..4d569902 100644 --- a/template/runtime/src/tests/common.rs +++ b/template/runtime/src/tests/common.rs @@ -11,7 +11,7 @@ use alloc::vec; use polkadot_sdk::frame_support::{ assert_ok, dispatch::DispatchResult, - traits::{Currency, Get, Hooks}, + traits::{Currency, Get, GetStorageVersion, Hooks}, }; use polkadot_sdk::sp_std::boxed::Box; use polkadot_sdk::{ @@ -157,6 +157,21 @@ pub const MIN_LIQUIDITY: Balance = 0; /// Initialize test externalities with a clean state pub fn new_test_ext() -> TestExternalities { + build_test_ext(ActorTestGenesis::Reference) +} + +enum ActorTestGenesis { + Reference, + SyntheticCapacity, +} + +/// Capacity-only genesis excludes reference System Actors while retaining ledger +/// anchors and the reserved Actor-id frontier. It does not model their lifecycle. +pub fn synthetic_actor_test_ext() -> TestExternalities { + build_test_ext(ActorTestGenesis::SyntheticCapacity) +} + +fn build_test_ext(actor_genesis: ActorTestGenesis) -> TestExternalities { let mut t = polkadot_sdk::frame_system::GenesisConfig::::default() .build_storage() .unwrap(); @@ -206,14 +221,35 @@ pub fn new_test_ext() -> TestExternalities { } .assimilate_storage(&mut t) .unwrap(); - pallet_deos_actors::GenesisConfig::::default() - .assimilate_storage(&mut t) - .unwrap(); + if matches!(actor_genesis, ActorTestGenesis::Reference) { + pallet_deos_actors::GenesisConfig::::default() + .assimilate_storage(&mut t) + .unwrap(); + } pallet_governance::GenesisConfig::::default() .assimilate_storage(&mut t) .unwrap(); let mut ext = TestExternalities::new(t); ext.execute_with(|| { + if matches!(actor_genesis, ActorTestGenesis::SyntheticCapacity) { + use pallet_deos_actors::GenesisSystemActors; + type GenesisActors = ::GenesisSystemActors; + crate::Actors::in_code_storage_version().put::(); + let active_limit = ::MaxActiveActors::get() + .min(::MaxQueueLength::get()); + pallet_deos_actors::ActiveActorLimit::::put(active_limit); + let next_id = GenesisActors::system_actors() + .into_iter() + .map(|(id, _, _, _)| id) + .chain( + GenesisActors::dormant_system_actors() + .into_iter() + .map(|(id, _, _)| id), + ) + .max() + .map_or(0, |id| id.checked_add(1).expect("reserved Actor ids fit")); + pallet_deos_actors::NextActorId::::put(next_id); + } System::set_block_number(1); let _ = Staking::on_initialize(1); }); @@ -225,6 +261,10 @@ pub fn seeded_test_ext() -> TestExternalities { setup_basic_test_environment() } +pub fn seeded_synthetic_actor_test_ext() -> TestExternalities { + seed_test_environment(synthetic_actor_test_ext()) +} + /// Mint test assets to multiple accounts pub fn create_test_asset(asset_id: u32, owner: &AccountId) -> DispatchResult { Assets::force_create( @@ -253,7 +293,10 @@ pub fn mint_tokens( /// Setup a basic test environment with common assets and accounts pub fn setup_basic_test_environment() -> TestExternalities { - let mut ext = new_test_ext(); + seed_test_environment(new_test_ext()) +} + +fn seed_test_environment(mut ext: TestExternalities) -> TestExternalities { ext.execute_with(|| { System::set_block_number(1); // Create test assets using standard asset IDs for consistency diff --git a/template/runtime/src/tests/fee_integration_tests.rs b/template/runtime/src/tests/fee_integration_tests.rs index 6cbd8e44..3ca3d9d5 100644 --- a/template/runtime/src/tests/fee_integration_tests.rs +++ b/template/runtime/src/tests/fee_integration_tests.rs @@ -1,25 +1,32 @@ use super::common::{ - ALICE, BOB, INITIAL_BALANCE, actor_fee_sink_account, add_liquidity, create_pool, new_test_ext, - set_consensus_timestamp, + ALICE, INITIAL_BALANCE, actor_fee_sink_account, new_test_ext, set_consensus_timestamp, }; +#[cfg(not(feature = "runtime-benchmarks"))] +use super::common::{BOB, add_liquidity, create_pool}; use crate::{ - Actors, Assets, Balances, Runtime, RuntimeOrigin, Staking, - configs::{ - AssetKind, RuntimeFeeCollector, - actor_config::{TmctolAssetOps, TmctolFeeCollector, TmctolGenesisSystemActors}, - }, + Actors, Balances, RuntimeOrigin, Staking, + configs::{AssetKind, RuntimeFeeCollector, actor_config::TmctolFeeCollector}, +}; +#[cfg(not(feature = "runtime-benchmarks"))] +use crate::{ + Assets, Runtime, + configs::actor_config::{TmctolAssetOps, TmctolGenesisSystemActors}, }; -use pallet_deos_actors::{AssetOps, FeeCollector, TriggerRuntimeState, WakeupKey}; +#[cfg(not(feature = "runtime-benchmarks"))] +use pallet_deos_actors::AssetOps; +use pallet_deos_actors::{FeeCollector, TriggerRuntimeState, WakeupKey}; +#[cfg(not(feature = "runtime-benchmarks"))] +use polkadot_sdk::frame_support::traits::fungibles::Inspect; use polkadot_sdk::frame_support::{ assert_ok, traits::{ Hooks, OnUnbalanced, fungible::Balanced, - fungibles::Inspect, tokens::{Fortitude, Precision, Preservation}, }, weights::Weight, }; +#[cfg(not(feature = "runtime-benchmarks"))] use polkadot_sdk::pallet_asset_conversion::PoolLocator; fn run_at_cadence_tick(key: WakeupKey) { @@ -90,6 +97,7 @@ fn runtime_fee_collector_routes_the_full_credit_to_fee_sink() { } #[test] +#[cfg(not(feature = "runtime-benchmarks"))] fn source_less_runtime_fee_credit_is_processed_by_fee_sink_cadence() { new_test_ext().execute_with(|| { let fee_sink_id = primitives::ecosystem::actor_ids::FEE_SINK_ACTORS_ID; @@ -235,6 +243,7 @@ fn repeated_low_volume_fee_sink_distributions_preserve_anchors_without_failures( } #[test] +#[cfg(not(feature = "runtime-benchmarks"))] fn fee_sink_threshold_admits_exactly_one_ed_per_permissioned_leg() { new_test_ext().execute_with(|| { let fee_sink_id = primitives::ecosystem::actor_ids::FEE_SINK_ACTORS_ID; @@ -261,6 +270,7 @@ fn fee_sink_threshold_admits_exactly_one_ed_per_permissioned_leg() { } #[test] +#[cfg(not(feature = "runtime-benchmarks"))] fn fee_sink_actor_splits_trusted_set_native_flow_to_staking_and_lp_ingress() { new_test_ext().execute_with(|| { let native_asset_id = 0; diff --git a/template/runtime/src/tests/governance_integration_tests.rs b/template/runtime/src/tests/governance_integration_tests.rs index af83ad5c..5db1cd5d 100644 --- a/template/runtime/src/tests/governance_integration_tests.rs +++ b/template/runtime/src/tests/governance_integration_tests.rs @@ -2086,7 +2086,11 @@ fn transferable_staking_receipt_power_is_custodied_reused_and_increased() { .frozen_ordinary_ballot .expect("second primary ballot is frozen") .raw_power, - 7 * 80 + if cfg!(feature = "runtime-benchmarks") { + 1 + } else { + 7 * 80 + } ); System::set_block_number(position.lock_until); diff --git a/template/runtime/src/tests/mod.rs b/template/runtime/src/tests/mod.rs index 98871fd0..a972dfd9 100644 --- a/template/runtime/src/tests/mod.rs +++ b/template/runtime/src/tests/mod.rs @@ -25,8 +25,12 @@ pub mod load_testing; #[cfg(test)] pub mod oracle_integration_tests; #[cfg(test)] +mod production_block_replay; +#[cfg(test)] pub mod staking_integration_tests; #[cfg(test)] pub mod tmctol_integration_tests; #[cfg(test)] pub mod tmctol_invariants; +#[cfg(test)] +mod wasm_replay; diff --git a/template/runtime/src/tests/production_block_replay.rs b/template/runtime/src/tests/production_block_replay.rs new file mode 100644 index 00000000..badb8238 --- /dev/null +++ b/template/runtime/src/tests/production_block_replay.rs @@ -0,0 +1,7226 @@ +//! Full Executive block fixtures for production-Wasm replay. +//! +//! Unlike the Actor-only capacity harness, this includes consensus context, +//! all runtime hooks, inherent order, signed extrinsics, roots and finalization. + +use super::{ + actors_integration_tests, + common::{ALICE, ASSET_A}, +}; +use crate::{ + Actors, Assets, Balances, Block, Executive, Header, InherentDataExt, Oracle, Runtime, + RuntimeCall, RuntimeEvent, RuntimeGenesisConfig, RuntimeOrigin, System, + configs::BlockResourceBudgetValue, +}; +use codec::Encode; +use cumulus_primitives_parachain_inherent::{INHERENT_IDENTIFIER, ParachainInherentData}; +use cumulus_test_relay_sproof_builder::RelayStateSproofBuilder; +use pallet_deos_actors::{ + ActorId, AmountResolution, CloseReason, CompletionPolicy, ContractSteps, CrossingDirection, + CycleResult, Event, InputLimit, ScheduleWindow, StepErrorPolicy, StepOf, Task, Trigger, + TriggerFamily, WakeupKey, WeightInfo, +}; +use polkadot_sdk::frame_support::{ + BoundedVec, assert_ok, + dispatch::GetDispatchInfo, + traits::{StorageInfo, StorageInfoTrait}, + weights::Weight, +}; +use polkadot_sdk::{ + cumulus_primitives_core::PersistedValidationData, + polkadot_parachain_primitives::primitives::HeadData, + sp_core::{ + H256, Pair, sr25519, + storage::{Storage, well_known_keys}, + }, + sp_inherents::InherentData, + sp_io::TestExternalities, + sp_runtime::{ + BuildStorage, Digest, DigestItem, MultiAddress, Perbill, + traits::{BlakeTwo256, Hash, Header as HeaderT}, + }, + sp_trie::{ + StorageProof, + proof_size_extension::{ProofSizeExt, RecordingProofSizeProvider}, + recorder::Recorder, + }, + sp_weights::WeightToFee, +}; +use std::collections::{BTreeMap, BTreeSet}; + +const PREPARED_W1_ACTORS: u32 = 128; +const W1_TARGET_CYCLES: u32 = 10_000; +const W1_TARGET_BLOCKS: u32 = 100; +const W3_REPETITIONS_PER_CELL: u32 = 14; +const W3_BLOCK_LIMIT: u32 = 100; +const W4_TRANSFER_ACTORS: u32 = 9_485; +const W4_SWAP_OUT_ACTORS: u32 = 400; +const W4_STOP_CYCLE_ACTORS: u32 = 100; +const W5_BLOCK_LIMIT: u32 = 10; +const W6_BLOCK_LIMIT: u32 = 16; +const W6_RANDOM_SEED: u64 = 0x0066_c10c_5eed; +const W7_DUE_ACTORS: u32 = 100; +const W7_FUTURE_WORKLOAD_ACTORS: u32 = 4_943; +const W7_UNSIGNALED_WORKLOAD_ACTORS: u32 = 4_942; +const W7_FUTURE_BLOCK: u32 = 100; +const W7_BLOCK_LIMIT: u32 = 10; +const W8_TOMBSTONE_PREFIXES: [u32; 7] = [1, 4, 8, 16, 32, 64, 128]; +const W8_DUE_ACTORS: u32 = 100; +const W9_DUE_ACTORS: u32 = 100; +const CONTROL_ATTRIBUTION_ACTORS: u32 = 100; +const CONTROL_ATTRIBUTION_BLOCKS: u32 = 9; +const REFERENCE_ACTIVE_SYSTEM_ACTORS: u32 = 3; +const REFERENCE_SYSTEM_ACTOR_IDENTITIES: u32 = 15; +const EXP_0095_PRODUCTION_WASM_SHA256: [u8; 32] = [ + 0x25, 0xb9, 0x69, 0x5f, 0xd9, 0xe9, 0x00, 0xf1, 0x7a, 0xe1, 0xf3, 0xfb, 0x0b, 0x81, 0x5a, 0xc1, + 0x40, 0x38, 0x30, 0x26, 0x4d, 0x9c, 0x31, 0xf7, 0xce, 0xe5, 0x4e, 0x29, 0xf4, 0x34, 0xb7, 0x00, +]; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum UserDemand { + ActorOnly, + ContinuousValid, + RefTimeHeavy, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum WorkloadSchedule { + ManualOnly, + CadencedOnly, + MixedManualCadenced, +} + +impl UserDemand { + fn label(self) -> &'static str { + match self { + Self::ActorOnly => "actor-only", + Self::ContinuousValid => "continuous-valid-user-demand", + Self::RefTimeHeavy => "ref-time-heavy-valid-user-demand", + } + } +} + +impl WorkloadSchedule { + fn uses_manual_trigger(self, index: u32) -> bool { + match self { + Self::ManualOnly => true, + Self::CadencedOnly => false, + Self::MixedManualCadenced => index.is_multiple_of(2), + } + } + + fn label(self) -> &'static str { + match self { + Self::ManualOnly => "manual-only", + Self::CadencedOnly => "cadenced-only", + Self::MixedManualCadenced => "mixed-manual-cadenced", + } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum WorkloadTask { + Transfer, + RouterSwapOut, + StopCycle, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct WorkloadActorProfile { + step_count: u32, + opening_predicates_per_step: u32, + task: Option, +} + +struct PreparedActorFixture { + storage: Storage, + actor_ids: Vec, + actor_profiles: BTreeMap, + signer: sr25519::Pair, + next_control_maximum: Weight, + next_effect_maximum: Weight, +} + +struct PreparedW5Fixture { + actors: PreparedActorFixture, + zero_step: ActorId, + running: ActorId, + retry_prefix: ActorId, + productive_cleanup: ActorId, + apoptosis: ActorId, + retry_sovereign: crate::AccountId, + retry_balance_before: u128, +} + +struct PreparedW6Fixture { + actors: PreparedActorFixture, + feed: primitives::OracleFeedId, + dense_manual: Vec, + sparse_block: Vec, + sparse_tick: Vec, + randomized_cadenced: Vec, + randomized_periods: Vec, + paused_actor: ActorId, + closed_actor: ActorId, + closed_wakeup_key: WakeupKey, + crossing_actor: ActorId, + crossing_generation: u64, + crossing_replacement: pallet_deos_actors::ActorContractOf, +} + +struct PreparedW7Fixture { + actors: PreparedActorFixture, + due: Vec, + future: Vec, + unsignaled: Vec, + future_wakeup_key: Option>, + reference_identities: u32, +} + +struct PreparedW8Fixture { + actors: PreparedActorFixture, + due: Vec, + tombstone_prefix: u32, +} + +#[derive(Debug)] +struct W7CampaignResult { + steps_per_block: Vec, + control_ref_time: Vec, + control_proof_size: Vec, + effect_ref_time: Vec, + effect_proof_size: Vec, + execution_storage_proof: Vec, + execution_compact_proof: Vec, + verification_storage_proof: Vec, + verification_compact_proof: Vec, +} + +#[derive(Clone, Debug)] +struct FullExecutiveBlockMetrics { + actor_steps: u32, + distinct_actors: u32, + progressed_steps: Vec<(ActorId, u32)>, + opening_steps: u32, + middle_steps: u32, + final_steps: u32, + transfer_steps: u32, + swap_out_steps: u32, + stop_cycle_steps: u32, + non_successful_steps: u32, + completed_cycles: u32, + completed_cycle_actors: Vec, + failed_cycle_actors: Vec, + suspended_steps: Vec<(ActorId, u32)>, + continued_steps: Vec<(ActorId, u32)>, + closed_actors: Vec<(ActorId, CloseReason)>, + paused_actors: Vec, + resumed_actors: Vec, + trigger_occurrences: Vec<(ActorId, TriggerFamily)>, + user_calls: u32, + next_user_weight: Option, + prepass_steps: u32, + prepass_trigger_occurrences: u32, + prepass_actor_control: Weight, + prepass_actor_effect: Weight, + actor_control: Weight, + actor_effect: Weight, + user_dispatch: Weight, + queue_head: u64, + queue_tail: u64, +} + +#[derive(Clone, Debug)] +struct WasmProofMetrics { + execution_storage_proof_bytes: u64, + execution_compact_proof_bytes: u64, + verification_storage_proof_bytes: u64, + verification_compact_proof_bytes: u64, + execution_recorded_keys: BTreeSet<(H256, Vec)>, + remaining_residual_node_bytes: usize, +} + +struct AuthoredBlock { + pre_state: Storage, + post_state: Storage, + block: Block, + inherent_data: InherentData, + metrics: FullExecutiveBlockMetrics, +} + +#[derive(Debug)] +struct ControlAttributionResult { + steps: Vec, + prepass_steps: Vec, + trigger_occurrences: Vec, + prepass_control: Vec, + final_control: Vec, + wasm_proofs: Vec, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct DatabaseIo { + reads: u64, + writes: u64, +} + +impl DatabaseIo { + const fn new(reads: u64, writes: u64) -> Self { + Self { reads, writes } + } + + const fn saturating_add(self, other: Self) -> Self { + Self { + reads: self.reads.saturating_add(other.reads), + writes: self.writes.saturating_add(other.writes), + } + } + + const fn saturating_mul(self, multiplier: u64) -> Self { + Self { + reads: self.reads.saturating_mul(multiplier), + writes: self.writes.saturating_mul(multiplier), + } + } +} + +fn merge_genesis_patch(target: &mut serde_json::Value, patch: serde_json::Value) { + match (target, patch) { + (serde_json::Value::Object(target), serde_json::Value::Object(patch)) => { + for (key, value) in patch { + merge_genesis_patch(target.entry(key).or_insert(serde_json::Value::Null), value); + } + } + (target, patch) => *target = patch, + } +} + +fn reference_genesis_storage(wasm: &[u8]) -> Storage { + let preset = crate::genesis_config_presets::get_preset( + &polkadot_sdk::sp_genesis_builder::DEV_RUNTIME_PRESET.into(), + ) + .expect("Development is a declared runtime preset"); + let mut config = serde_json::to_value(RuntimeGenesisConfig::default()) + .expect("default runtime genesis serializes"); + merge_genesis_patch( + &mut config, + serde_json::from_slice(&preset).expect("runtime-owned preset is JSON"), + ); + let config: RuntimeGenesisConfig = + serde_json::from_value(config).expect("complete runtime preset deserializes"); + let mut storage = config.build_storage().expect("reference genesis builds"); + storage + .top + .insert(well_known_keys::CODE.to_vec(), wasm.to_vec()); + storage +} + +fn current_top_storage() -> Storage { + let mut storage = Storage::default(); + let mut cursor = Vec::new(); + while let Some(key) = polkadot_sdk::sp_io::storage::next_key(&cursor) { + let value = polkadot_sdk::sp_io::storage::get(&key) + .unwrap_or_else(|| panic!("enumerated storage key disappeared: {key:?}")); + cursor = key.clone(); + storage.top.insert(key, value.to_vec()); + } + storage +} + +fn prepare_actor_fixture( + wasm: &[u8], + actor_count: u32, + workload_schedule: WorkloadSchedule, +) -> PreparedActorFixture { + let steps = actors_integration_tests::transfer_contract_steps( + super::common::BOB, + primitives::AssetKind::Native, + crate::EXISTENTIAL_DEPOSIT, + ); + let (next_control_maximum, _) = actors_integration_tests::one_step_fifo_attempt_maxima(&steps); + let mut ext = TestExternalities::new_with_code_and_state( + wasm, + reference_genesis_storage(wasm), + crate::VERSION.state_version(), + ); + let signer = sr25519::Pair::from_seed(&[66u8; 32]); + let (actor_ids, actor_profiles) = ext.execute_with(|| { + System::set_block_number(1); + let initial_active = pallet_deos_actors::ActiveActorCount::::get(); + let maximum = ::MaxActiveActors::get(); + assert!( + initial_active.saturating_add(actor_count) <= maximum, + "prepared W1 population must fit the reference active bound" + ); + let mut actor_ids = Vec::with_capacity(actor_count as usize); + let mut actor_profiles = BTreeMap::new(); + for index in 0..actor_count { + let manually_triggered = workload_schedule.uses_manual_trigger(index); + let schedule = if manually_triggered { + actors_integration_tests::manual_schedule() + } else { + actors_integration_tests::cadenced_schedule(1) + }; + let actor_id = actors_integration_tests::create_system(ALICE, schedule, None, steps.clone()); + actors_integration_tests::fund_native( + actor_id, + 1_000u128.saturating_mul(crate::EXISTENTIAL_DEPOSIT), + ); + if manually_triggered { + assert_ok!(Actors::manual_trigger(RuntimeOrigin::root(), actor_id)); + } + actor_ids.push(actor_id); + actor_profiles.insert( + actor_id, + WorkloadActorProfile { + step_count: 1, + opening_predicates_per_step: 0, + task: Some(WorkloadTask::Transfer), + }, + ); + } + assert_eq!( + pallet_deos_actors::ActiveActorCount::::get(), + initial_active.saturating_add(actor_count) + ); + let signer_account = crate::AccountId::from(signer.public()); + assert_ok!(Balances::force_set_balance( + RuntimeOrigin::root(), + MultiAddress::Id(signer_account), + u128::MAX / 4, + )); + (actor_ids, actor_profiles) + }); + ext + .commit_all() + .expect("prepared Actor fixture commits before block authoring"); + let storage = ext.execute_with(current_top_storage); + PreparedActorFixture { + storage, + actor_ids, + actor_profiles, + signer, + next_control_maximum, + next_effect_maximum: actors_integration_tests::one_step_fifo_attempt_maxima(&steps).1, + } +} + +fn prepare_w3_fixture(wasm: &[u8]) -> PreparedActorFixture { + let actor_count = W3_REPETITIONS_PER_CELL.saturating_mul(9); + let mut ext = TestExternalities::new_with_code_and_state( + wasm, + reference_genesis_storage(wasm), + crate::VERSION.state_version(), + ); + let signer = sr25519::Pair::from_seed(&[66u8; 32]); + let (actor_ids, actor_profiles, next_control_maximum) = ext.execute_with(|| { + System::set_block_number(1); + let initial_active = pallet_deos_actors::ActiveActorCount::::get(); + assert_eq!(initial_active, REFERENCE_ACTIVE_SYSTEM_ACTORS); + assert!( + initial_active.saturating_add(actor_count) + <= ::MaxActiveActors::get() + ); + let mut actor_ids = Vec::with_capacity(actor_count as usize); + let mut actor_profiles = BTreeMap::new(); + let mut next_control_maximum = Weight::zero(); + for predicates_per_step in [0, 2, 4] { + for step_count in [1, 2, 3] { + let steps = actors_integration_tests::transfer_contract_steps_with_opening_predicates( + super::common::BOB, + primitives::AssetKind::Native, + crate::EXISTENTIAL_DEPOSIT, + step_count, + predicates_per_step, + ); + let (control, _) = if step_count == 1 && predicates_per_step == 0 { + actors_integration_tests::one_step_fifo_attempt_maxima(&steps) + } else { + ( + Actors::contract_steps_admission_weight_upper( + pallet_deos_actors::ActorType::System, + &steps, + ), + Weight::zero(), + ) + }; + next_control_maximum = next_control_maximum.max(control); + for _ in 0..W3_REPETITIONS_PER_CELL { + let actor_id = actors_integration_tests::create_system( + ALICE, + actors_integration_tests::manual_schedule(), + None, + steps.clone(), + ); + actors_integration_tests::fund_native( + actor_id, + 1_000u128.saturating_mul(crate::EXISTENTIAL_DEPOSIT), + ); + assert_ok!(Actors::manual_trigger(RuntimeOrigin::root(), actor_id)); + actor_ids.push(actor_id); + actor_profiles.insert( + actor_id, + WorkloadActorProfile { + step_count, + opening_predicates_per_step: predicates_per_step, + task: Some(WorkloadTask::Transfer), + }, + ); + } + } + } + assert_eq!(actor_ids.len() as u32, actor_count); + assert_eq!(actor_profiles.len() as u32, actor_count); + (actor_ids, actor_profiles, next_control_maximum) + }); + ext + .commit_all() + .expect("prepared W3 fixture commits before block authoring"); + let storage = ext.execute_with(current_top_storage); + PreparedActorFixture { + storage, + actor_ids, + actor_profiles, + signer, + next_control_maximum, + next_effect_maximum: Weight::zero(), + } +} + +fn w4_contract_steps(task: WorkloadTask, initial_balance: u128) -> ContractSteps { + let task = match task { + WorkloadTask::Transfer => Task::Transfer { + to: super::common::BOB, + asset: primitives::AssetKind::Native, + amount: AmountResolution::Fixed(crate::EXISTENTIAL_DEPOSIT), + }, + WorkloadTask::RouterSwapOut => Task::SwapOut { + asset_out: primitives::AssetKind::Local(ASSET_A), + amount_out: AmountResolution::Fixed(crate::EXISTENTIAL_DEPOSIT), + asset_in: primitives::AssetKind::Native, + input_limit: InputLimit::Absolute(initial_balance), + slippage_tolerance: Perbill::from_percent(5), + }, + WorkloadTask::StopCycle => Task::StopCycle, + }; + BoundedVec::try_from(vec![StepOf:: { + precondition: None, + task, + on_error: StepErrorPolicy::AbortCycle, + }]) + .expect("one W4 Step fits") +} + +fn prepare_w4_fixture(wasm: &[u8]) -> PreparedActorFixture { + let actor_count = W4_TRANSFER_ACTORS + .saturating_add(W4_SWAP_OUT_ACTORS) + .saturating_add(W4_STOP_CYCLE_ACTORS); + let initial_balance = 10_000u128.saturating_mul(crate::EXISTENTIAL_DEPOSIT); + let mut ext = TestExternalities::new_with_code_and_state( + wasm, + reference_genesis_storage(wasm), + crate::VERSION.state_version(), + ); + let signer = sr25519::Pair::from_seed(&[66u8; 32]); + let (actor_ids, actor_profiles, next_control_maximum, next_effect_maximum) = + ext.execute_with(|| { + System::set_block_number(1); + assert_eq!( + pallet_deos_actors::ActorIdentityCount::::get(), + REFERENCE_SYSTEM_ACTOR_IDENTITIES + ); + assert_eq!( + actor_count.saturating_add(REFERENCE_SYSTEM_ACTOR_IDENTITIES), + ::MaxActorIdentities::get(), + "W4 fills production identity capacity without replacing reference identities" + ); + + assert_ok!(Balances::force_set_balance( + RuntimeOrigin::root(), + MultiAddress::Id(ALICE), + u128::MAX / 4, + )); + assert_ok!(super::common::create_test_asset(ASSET_A, &ALICE)); + assert_ok!(Assets::set_team( + RuntimeOrigin::signed(ALICE), + ASSET_A, + ALICE.into(), + ALICE.into(), + ALICE.into(), + )); + assert_ok!(super::common::mint_tokens( + ASSET_A, + &ALICE, + &ALICE, + super::common::INITIAL_BALANCE, + )); + super::common::setup_deos_router_infrastructure() + .expect("W4 canonical Router pool infrastructure initializes"); + + let transfer_steps = w4_contract_steps(WorkloadTask::Transfer, initial_balance); + let swap_steps = w4_contract_steps(WorkloadTask::RouterSwapOut, initial_balance); + let stop_steps = w4_contract_steps(WorkloadTask::StopCycle, initial_balance); + let mut next_control_maximum = Weight::zero(); + let mut next_effect_maximum = Weight::zero(); + for steps in [&transfer_steps, &swap_steps, &stop_steps] { + let (control, effect) = actors_integration_tests::one_step_fifo_attempt_maxima(steps); + next_control_maximum = next_control_maximum.max(control); + next_effect_maximum = next_effect_maximum.max(effect); + } + + let mut actor_ids = Vec::with_capacity(actor_count as usize); + let mut actor_profiles = BTreeMap::new(); + let mut task_counts = BTreeMap::<&'static str, u32>::new(); + for index in 0..actor_count { + // Every complete hundred contributes 95/4/1. The final 85 positions + // contribute 80/4/1, yielding 9,485/400/100 after the retained 15 + // production System identities consume the remaining capacity slots. + let task = match index % 100 { + 0..=3 => WorkloadTask::RouterSwapOut, + 4 => WorkloadTask::StopCycle, + _ => WorkloadTask::Transfer, + }; + let steps = match task { + WorkloadTask::Transfer => transfer_steps.clone(), + WorkloadTask::RouterSwapOut => swap_steps.clone(), + WorkloadTask::StopCycle => stop_steps.clone(), + }; + let schedule = if index.is_multiple_of(2) { + actors_integration_tests::manual_schedule() + } else { + actors_integration_tests::cadenced_schedule(1) + }; + let actor_id = actors_integration_tests::create_system(ALICE, schedule, None, steps); + actors_integration_tests::fund_native(actor_id, initial_balance); + if index.is_multiple_of(2) { + assert_ok!(Actors::manual_trigger(RuntimeOrigin::root(), actor_id)); + } + *task_counts + .entry(match task { + WorkloadTask::Transfer => "transfer", + WorkloadTask::RouterSwapOut => "swap-out", + WorkloadTask::StopCycle => "stop-cycle", + }) + .or_default() += 1; + actor_ids.push(actor_id); + actor_profiles.insert( + actor_id, + WorkloadActorProfile { + step_count: 1, + opening_predicates_per_step: 0, + task: Some(task), + }, + ); + } + assert_eq!(task_counts.get("transfer"), Some(&W4_TRANSFER_ACTORS)); + assert_eq!(task_counts.get("swap-out"), Some(&W4_SWAP_OUT_ACTORS)); + assert_eq!(task_counts.get("stop-cycle"), Some(&W4_STOP_CYCLE_ACTORS)); + let signer_account = crate::AccountId::from(signer.public()); + assert_ok!(Balances::force_set_balance( + RuntimeOrigin::root(), + MultiAddress::Id(signer_account), + u128::MAX / 4, + )); + ( + actor_ids, + actor_profiles, + next_control_maximum, + next_effect_maximum, + ) + }); + ext + .commit_all() + .expect("prepared W4 fixture commits before block authoring"); + let storage = ext.execute_with(current_top_storage); + PreparedActorFixture { + storage, + actor_ids, + actor_profiles, + signer, + next_control_maximum, + next_effect_maximum, + } +} + +fn prepare_w5_fixture(wasm: &[u8]) -> PreparedW5Fixture { + let mut ext = TestExternalities::new_with_code_and_state( + wasm, + reference_genesis_storage(wasm), + crate::VERSION.state_version(), + ); + let signer = sr25519::Pair::from_seed(&[66u8; 32]); + let ( + actor_ids, + actor_profiles, + zero_step, + running, + retry_prefix, + productive_cleanup, + apoptosis, + retry_sovereign, + retry_balance_before, + ) = ext.execute_with(|| { + System::set_block_number(1); + assert_ok!(Balances::force_set_balance( + RuntimeOrigin::root(), + MultiAddress::Id(ALICE), + u128::MAX / 4, + )); + assert_ok!(super::common::create_test_asset(ASSET_A, &ALICE)); + assert_ok!(Assets::set_team( + RuntimeOrigin::signed(ALICE), + ASSET_A, + ALICE.into(), + ALICE.into(), + ALICE.into(), + )); + assert_ok!(super::common::mint_tokens( + ASSET_A, + &ALICE, + &ALICE, + super::common::INITIAL_BALANCE, + )); + super::common::setup_deos_router_infrastructure() + .expect("W5 canonical Router pool infrastructure initializes"); + let transfer_step = || StepOf:: { + precondition: None, + task: Task::Transfer { + to: super::common::BOB, + asset: primitives::AssetKind::Native, + amount: AmountResolution::Fixed(crate::EXISTENTIAL_DEPOSIT), + }, + on_error: StepErrorPolicy::AbortCycle, + }; + + let zero_step = actors_integration_tests::create_system( + ALICE, + actors_integration_tests::manual_schedule(), + None, + ContractSteps::::default(), + ); + let running_steps = + BoundedVec::try_from(vec![transfer_step(), transfer_step(), transfer_step()]) + .expect("W5 three-Step Running Contract fits"); + let running = actors_integration_tests::create_system( + ALICE, + actors_integration_tests::manual_schedule(), + None, + running_steps, + ); + actors_integration_tests::fund_native( + running, + 1_000u128.saturating_mul(crate::EXISTENTIAL_DEPOSIT), + ); + + let retry_steps = BoundedVec::try_from(vec![ + transfer_step(), + StepOf:: { + precondition: None, + task: Task::SwapOut { + asset_out: primitives::AssetKind::Local(ASSET_A), + amount_out: AmountResolution::Fixed(crate::EXISTENTIAL_DEPOSIT), + asset_in: primitives::AssetKind::Native, + input_limit: InputLimit::Absolute(1), + slippage_tolerance: Perbill::zero(), + }, + on_error: StepErrorPolicy::RetryLater { max_attempts: 2 }, + }, + transfer_step(), + ]) + .expect("W5 committed-prefix retry Contract fits"); + let retry_prefix = actors_integration_tests::create_system( + ALICE, + actors_integration_tests::manual_schedule(), + None, + retry_steps, + ); + let retry_balance_before = 10_000u128.saturating_mul(crate::EXISTENTIAL_DEPOSIT); + actors_integration_tests::fund_native(retry_prefix, retry_balance_before); + let retry_sovereign = Actors::active_actor_state(retry_prefix) + .expect("W5 retry Actor exists") + .identity + .sovereign_account; + + let cleanup_steps = + BoundedVec::try_from(vec![transfer_step()]).expect("W5 productive-cleanup Contract fits"); + let productive_cleanup = actors_integration_tests::create_system_with_completion( + ALICE, + actors_integration_tests::manual_schedule(), + None, + cleanup_steps, + CompletionPolicy::CloseAfterProductiveCycle, + ); + actors_integration_tests::fund_native( + productive_cleanup, + 1_000u128.saturating_mul(crate::EXISTENTIAL_DEPOSIT), + ); + + let apoptosis_steps = + BoundedVec::try_from(vec![transfer_step()]).expect("W5 apoptosis Contract fits"); + let apoptosis = actors_integration_tests::create_user( + ALICE, + actors_integration_tests::manual_schedule(), + None, + apoptosis_steps, + ); + let apoptosis_account = Actors::active_actor_state(apoptosis) + .expect("W5 apoptosis Actor exists") + .identity + .sovereign_account; + let trigger_fee = ::WeightToFee::weight_to_fee( + &<::WeightInfo as WeightInfo>::manual_trigger(), + ); + assert_ok!(Balances::force_set_balance( + RuntimeOrigin::root(), + MultiAddress::Id(apoptosis_account), + crate::configs::actor_config::ActorMinUserBalance::get().saturating_add(trigger_fee), + )); + + for actor_id in [zero_step, running, retry_prefix, productive_cleanup] { + assert_ok!(Actors::manual_trigger(RuntimeOrigin::root(), actor_id)); + } + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + apoptosis, + )); + + let actor_ids = vec![ + zero_step, + running, + retry_prefix, + productive_cleanup, + apoptosis, + ]; + let actor_profiles = BTreeMap::from([ + ( + zero_step, + WorkloadActorProfile { + step_count: 0, + opening_predicates_per_step: 0, + task: None, + }, + ), + ( + running, + WorkloadActorProfile { + step_count: 3, + opening_predicates_per_step: 0, + task: Some(WorkloadTask::Transfer), + }, + ), + ( + retry_prefix, + WorkloadActorProfile { + step_count: 3, + opening_predicates_per_step: 0, + task: None, + }, + ), + ( + productive_cleanup, + WorkloadActorProfile { + step_count: 1, + opening_predicates_per_step: 0, + task: Some(WorkloadTask::Transfer), + }, + ), + ( + apoptosis, + WorkloadActorProfile { + step_count: 1, + opening_predicates_per_step: 0, + task: Some(WorkloadTask::Transfer), + }, + ), + ]); + ( + actor_ids, + actor_profiles, + zero_step, + running, + retry_prefix, + productive_cleanup, + apoptosis, + retry_sovereign, + retry_balance_before, + ) + }); + ext + .commit_all() + .expect("prepared W5 fixture commits before block authoring"); + let storage = ext.execute_with(current_top_storage); + PreparedW5Fixture { + actors: PreparedActorFixture { + storage, + actor_ids, + actor_profiles, + signer, + next_control_maximum: Weight::zero(), + next_effect_maximum: Weight::zero(), + }, + zero_step, + running, + retry_prefix, + productive_cleanup, + apoptosis, + retry_sovereign, + retry_balance_before, + } +} + +fn prepare_w6_fixture(wasm: &[u8]) -> PreparedW6Fixture { + let mut ext = TestExternalities::new_with_code_and_state( + wasm, + reference_genesis_storage(wasm), + crate::VERSION.state_version(), + ); + let signer = sr25519::Pair::from_seed(&[66u8; 32]); + let owner = crate::AccountId::from(signer.public()); + let ( + actor_ids, + actor_profiles, + feed, + dense_manual, + sparse_block, + sparse_tick, + randomized_cadenced, + randomized_periods, + paused_actor, + closed_actor, + closed_wakeup_key, + crossing_actor, + crossing_generation, + crossing_replacement, + ) = ext.execute_with(|| { + System::set_block_number(1); + assert_ok!(Balances::force_set_balance( + RuntimeOrigin::root(), + MultiAddress::Id(owner.clone()), + u128::MAX / 4, + )); + let feed = crate::configs::oracle_config::deos_router_pool_feed( + primitives::AssetKind::Native, + primitives::AssetKind::Local(66), + ); + assert_ok!(Oracle::register_feed( + RuntimeOrigin::root(), + feed, + owner.clone(), + feed.meaning(), + primitives::OracleProvenance::DeosRouterPreExecutionReserves, + feed.scale, + pallet_oracle::Aggregation::Ema { + half_life_blocks: 100, + }, + pallet_oracle::ZeroPolicy::Reject, + false, + )); + assert_ok!(Oracle::publish( + RuntimeOrigin::signed(owner.clone()), + feed, + 1_000_000_000_000, + )); + + let steps = actors_integration_tests::transfer_contract_steps( + super::common::BOB, + primitives::AssetKind::Native, + crate::EXISTENTIAL_DEPOSIT, + ); + let mut actor_ids = Vec::new(); + let mut actor_profiles = BTreeMap::new(); + let mut add_actor = |schedule, window, trigger_manual| { + let actor_id = + actors_integration_tests::create_user(owner.clone(), schedule, window, steps.clone()); + actors_integration_tests::fund_native( + actor_id, + 1_000u128.saturating_mul(crate::EXISTENTIAL_DEPOSIT), + ); + if trigger_manual { + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(owner.clone()), + actor_id, + )); + } + actor_ids.push(actor_id); + actor_profiles.insert( + actor_id, + WorkloadActorProfile { + step_count: 1, + opening_predicates_per_step: 0, + task: Some(WorkloadTask::Transfer), + }, + ); + actor_id + }; + + let dense_manual = (0..3) + .map(|_| add_actor(actors_integration_tests::manual_schedule(), None, true)) + .collect::>(); + let sparse_block = [3, 6, 9] + .into_iter() + .map(|start| { + add_actor( + actors_integration_tests::manual_schedule(), + Some(ScheduleWindow { start, end: 120 }), + true, + ) + }) + .collect::>(); + let sparse_tick = [1, 4, 7] + .into_iter() + .map(|after_ticks| { + add_actor( + actors_integration_tests::at_time_schedule(after_ticks), + None, + false, + ) + }) + .collect::>(); + let mut random_state = W6_RANDOM_SEED; + let randomized_periods = (0..3) + .map(|_| { + random_state ^= random_state << 13; + random_state ^= random_state >> 7; + random_state ^= random_state << 17; + 1 + random_state % 7 + }) + .collect::>(); + let randomized_cadenced = randomized_periods + .iter() + .map(|period| { + add_actor( + actors_integration_tests::cadenced_schedule(*period), + None, + false, + ) + }) + .collect::>(); + let crossing_actor = add_actor( + actors_integration_tests::observation_crossing_schedule( + feed, + CrossingDirection::Rising, + 1_500_000_000_000, + 800_000_000_000, + ), + None, + false, + ); + let crossing_generation = Actors::crossing_membership(crossing_actor) + .expect("W6 crossing membership exists") + .generation; + let mut crossing_replacement = + Actors::actor_contract(crossing_actor).expect("W6 crossing Contract exists"); + crossing_replacement.trigger = Trigger::observation_crossing( + feed, + CrossingDirection::Rising, + 4_000_000_000_000, + 3_000_000_000_000, + ); + let paused_actor = dense_manual[2]; + let closed_actor = sparse_block[2]; + let closed_wakeup_key = Actors::actor_hot(closed_actor) + .and_then(|hot| hot.wakeup_pointer) + .map(|pointer| pointer.block) + .expect("future-window W6 Actor has a block-clock wakeup"); + assert_eq!(closed_wakeup_key, WakeupKey::Block(9)); + ( + actor_ids, + actor_profiles, + feed, + dense_manual, + sparse_block, + sparse_tick, + randomized_cadenced, + randomized_periods, + paused_actor, + closed_actor, + closed_wakeup_key, + crossing_actor, + crossing_generation, + crossing_replacement, + ) + }); + ext + .commit_all() + .expect("prepared W6 fixture commits before block authoring"); + let storage = ext.execute_with(current_top_storage); + PreparedW6Fixture { + actors: PreparedActorFixture { + storage, + actor_ids, + actor_profiles, + signer, + next_control_maximum: Weight::zero(), + next_effect_maximum: Weight::zero(), + }, + feed, + dense_manual, + sparse_block, + sparse_tick, + randomized_cadenced, + randomized_periods, + paused_actor, + closed_actor, + closed_wakeup_key, + crossing_actor, + crossing_generation, + crossing_replacement, + } +} + +fn prepare_w7_fixture(wasm: &[u8], maximum_population: bool) -> PreparedW7Fixture { + let steps = actors_integration_tests::transfer_contract_steps( + super::common::BOB, + primitives::AssetKind::Native, + crate::EXISTENTIAL_DEPOSIT, + ); + let (next_control_maximum, next_effect_maximum) = + actors_integration_tests::one_step_fifo_attempt_maxima(&steps); + let mut ext = TestExternalities::new_with_code_and_state( + wasm, + reference_genesis_storage(wasm), + crate::VERSION.state_version(), + ); + let signer = sr25519::Pair::from_seed(&[66u8; 32]); + let (actor_ids, actor_profiles, due, future, unsignaled, future_wakeup_key, reference_identities) = + ext.execute_with(|| { + System::set_block_number(1); + let reference_identities = pallet_deos_actors::ActorIdentityCount::::get(); + assert_eq!(reference_identities, REFERENCE_SYSTEM_ACTOR_IDENTITIES); + assert_eq!( + pallet_deos_actors::ActiveActorCount::::get(), + REFERENCE_ACTIVE_SYSTEM_ACTORS + ); + let future_count = if maximum_population { + W7_FUTURE_WORKLOAD_ACTORS + } else { + 0 + }; + let unsignaled_count = if maximum_population { + W7_UNSIGNALED_WORKLOAD_ACTORS + } else { + 0 + }; + let added_count = future_count + .saturating_add(unsignaled_count) + .saturating_add(W7_DUE_ACTORS); + if maximum_population { + assert_eq!( + added_count.saturating_add(reference_identities), + ::MaxActorIdentities::get(), + "W7 fills the exact production identity capacity" + ); + } + + let mut actor_ids = Vec::with_capacity(added_count as usize); + let mut actor_profiles = BTreeMap::new(); + let mut add_actor = |window, trigger_manual| { + let actor_id = actors_integration_tests::create_system( + ALICE, + actors_integration_tests::manual_schedule(), + window, + steps.clone(), + ); + actors_integration_tests::fund_native( + actor_id, + 1_000u128.saturating_mul(crate::EXISTENTIAL_DEPOSIT), + ); + if trigger_manual { + assert_ok!(Actors::manual_trigger(RuntimeOrigin::root(), actor_id)); + } + actor_ids.push(actor_id); + actor_profiles.insert( + actor_id, + WorkloadActorProfile { + step_count: 1, + opening_predicates_per_step: 0, + task: Some(WorkloadTask::Transfer), + }, + ); + actor_id + }; + let future = (0..future_count) + .map(|_| { + add_actor( + Some(ScheduleWindow { + start: W7_FUTURE_BLOCK, + end: W7_FUTURE_BLOCK.saturating_add(100), + }), + true, + ) + }) + .collect::>(); + let unsignaled = (0..unsignaled_count) + .map(|_| add_actor(None, false)) + .collect::>(); + let due = (0..W7_DUE_ACTORS) + .map(|_| add_actor(None, true)) + .collect::>(); + let future_wakeup_key = future.first().map(|actor_id| { + Actors::actor_hot(*actor_id) + .and_then(|hot| hot.wakeup_pointer) + .map(|pointer| pointer.block) + .expect("future W7 Actor has one block-clock wakeup") + }); + if maximum_population { + assert_eq!(future_wakeup_key, Some(WakeupKey::Block(W7_FUTURE_BLOCK))); + assert_eq!( + pallet_deos_actors::ActorWaitingOccupancies::::get( + future_wakeup_key.expect("maximum W7 fixture has a future wakeup key") + ), + future_count + ); + assert_eq!( + reference_identities + .saturating_add(future.len() as u32) + .saturating_add(unsignaled.len() as u32), + 9_900, + "retained references and future/unsignaled workload form 9,900 non-due identities" + ); + } + assert_eq!(due.len() as u32, W7_DUE_ACTORS); + assert_eq!(actor_ids.len() as u32, added_count); + ( + actor_ids, + actor_profiles, + due, + future, + unsignaled, + future_wakeup_key, + reference_identities, + ) + }); + ext + .commit_all() + .expect("prepared W7 fixture commits before block authoring"); + let storage = ext.execute_with(current_top_storage); + PreparedW7Fixture { + actors: PreparedActorFixture { + storage, + actor_ids, + actor_profiles, + signer, + next_control_maximum, + next_effect_maximum, + }, + due, + future, + unsignaled, + future_wakeup_key, + reference_identities, + } +} + +fn prepare_w8_fixture(wasm: &[u8], tombstone_prefix: u32) -> PreparedW8Fixture { + assert!(W8_TOMBSTONE_PREFIXES.contains(&tombstone_prefix)); + let steps = actors_integration_tests::transfer_contract_steps( + super::common::BOB, + primitives::AssetKind::Native, + crate::EXISTENTIAL_DEPOSIT, + ); + let (next_control_maximum, next_effect_maximum) = + actors_integration_tests::one_step_fifo_attempt_maxima(&steps); + let mut ext = TestExternalities::new_with_code_and_state( + wasm, + reference_genesis_storage(wasm), + crate::VERSION.state_version(), + ); + let signer = sr25519::Pair::from_seed(&[66u8; 32]); + let (due, actor_profiles) = ext.execute_with(|| { + System::set_block_number(1); + assert_eq!( + pallet_deos_actors::ActorIdentityCount::::get(), + REFERENCE_SYSTEM_ACTOR_IDENTITIES + ); + assert_eq!( + pallet_deos_actors::ActiveActorCount::::get(), + REFERENCE_ACTIVE_SYSTEM_ACTORS + ); + + let mut tombstones = Vec::with_capacity(tombstone_prefix as usize); + for _ in 0..tombstone_prefix { + let actor_id = actors_integration_tests::create_system( + ALICE, + actors_integration_tests::manual_schedule(), + None, + steps.clone(), + ); + assert_ok!(Actors::manual_trigger(RuntimeOrigin::root(), actor_id)); + tombstones.push(actor_id); + } + for actor_id in &tombstones { + assert_ok!(Actors::close_actor(RuntimeOrigin::root(), *actor_id)); + } + assert_eq!(Actors::queue_head(), 0); + assert_eq!(Actors::queue_tail(), u64::from(tombstone_prefix)); + assert_eq!( + pallet_deos_actors::ActorReadyOccupancy::::get(), + 0, + "closed W8 prefix Actors leave only canonical tombstones" + ); + assert!(tombstones.iter().all(|actor_id| { + pallet_deos_actors::ActorControlLocators::::get(actor_id).is_none() + && Actors::active_actor_state(*actor_id).is_none() + })); + + let mut actor_profiles = BTreeMap::new(); + let due = (0..W8_DUE_ACTORS) + .map(|_| { + let actor_id = actors_integration_tests::create_system( + ALICE, + actors_integration_tests::manual_schedule(), + None, + steps.clone(), + ); + actors_integration_tests::fund_native( + actor_id, + 1_000u128.saturating_mul(crate::EXISTENTIAL_DEPOSIT), + ); + assert_ok!(Actors::manual_trigger(RuntimeOrigin::root(), actor_id)); + actor_profiles.insert( + actor_id, + WorkloadActorProfile { + step_count: 1, + opening_predicates_per_step: 0, + task: Some(WorkloadTask::Transfer), + }, + ); + actor_id + }) + .collect::>(); + assert_eq!(due.len() as u32, W8_DUE_ACTORS); + assert_eq!(Actors::queue_head(), 0); + assert_eq!( + Actors::queue_tail(), + u64::from(tombstone_prefix.saturating_add(W8_DUE_ACTORS)) + ); + assert_eq!( + pallet_deos_actors::ActorReadyOccupancy::::get(), + W8_DUE_ACTORS + ); + assert_eq!( + pallet_deos_actors::ActorIdentityCount::::get(), + REFERENCE_SYSTEM_ACTOR_IDENTITIES.saturating_add(W8_DUE_ACTORS), + "closed prefix identities do not consume retained production capacity" + ); + (due, actor_profiles) + }); + ext + .commit_all() + .expect("prepared W8 fixture commits before block authoring"); + let storage = ext.execute_with(current_top_storage); + PreparedW8Fixture { + actors: PreparedActorFixture { + storage, + actor_ids: due.clone(), + actor_profiles, + signer, + next_control_maximum, + next_effect_maximum, + }, + due, + tombstone_prefix, + } +} + +fn prepare_w9_fixture(wasm: &[u8]) -> PreparedActorFixture { + let mut fixture = prepare_w7_fixture(wasm, false).actors; + assert_eq!(fixture.actor_ids.len() as u32, W9_DUE_ACTORS); + let mut ext = TestExternalities::new_with_code_and_state( + wasm, + fixture.storage.clone(), + crate::VERSION.state_version(), + ); + ext.execute_with(|| { + System::set_block_number(1); + assert_ok!(Balances::force_set_balance( + RuntimeOrigin::root(), + MultiAddress::Id(ALICE), + u128::MAX / 4, + )); + assert_ok!(super::common::create_test_asset(ASSET_A, &ALICE)); + assert_ok!(Assets::set_team( + RuntimeOrigin::signed(ALICE), + ASSET_A, + ALICE.into(), + ALICE.into(), + ALICE.into(), + )); + assert_ok!(super::common::mint_tokens( + ASSET_A, + &ALICE, + &ALICE, + super::common::INITIAL_BALANCE, + )); + super::common::setup_deos_router_infrastructure() + .expect("W9 canonical Router pool infrastructure initializes"); + let signer_account = crate::AccountId::from(fixture.signer.public()); + assert_ok!(Balances::force_set_balance( + RuntimeOrigin::root(), + MultiAddress::Id(signer_account.clone()), + u128::MAX / 4, + )); + assert_ok!(Assets::mint( + RuntimeOrigin::signed(ALICE), + ASSET_A, + MultiAddress::Id(signer_account), + u128::MAX / 4, + )); + }); + ext + .commit_all() + .expect("prepared W9 fixture commits before block authoring"); + fixture.storage = ext.execute_with(current_top_storage); + fixture +} + +fn parent_header_for(storage: Storage, wasm: &[u8], parent_number: u32) -> Header { + let mut ext = + TestExternalities::new_with_code_and_state(wasm, storage, crate::VERSION.state_version()); + let state_root = *ext.as_backend().root(); + Header::new( + parent_number, + BlakeTwo256::ordered_trie_root(Vec::new(), crate::VERSION.extrinsics_root_state_version()), + state_root, + Default::default(), + Digest::default(), + ) +} + +fn inherent_data_for(parent: &Header, block_number: u32) -> InherentData { + let parent_head = HeadData(parent.encode()); + let proof_builder = RelayStateSproofBuilder { + para_id: crate::PARACHAIN_ID.into(), + current_slot: u64::from(block_number).into(), + included_para_head: Some(parent_head.clone()), + ..Default::default() + }; + let (relay_parent_storage_root, relay_chain_state) = proof_builder.into_state_root_and_proof(); + let mut data = InherentData::new(); + data + .put_data( + polkadot_sdk::sp_timestamp::INHERENT_IDENTIFIER, + &u64::from(block_number).saturating_mul(crate::SLOT_DURATION), + ) + .expect("timestamp inherent encodes"); + data + .put_data( + INHERENT_IDENTIFIER, + &ParachainInherentData { + validation_data: PersistedValidationData { + parent_head, + relay_parent_number: block_number, + relay_parent_storage_root, + max_pov_size: 5_000_000, + }, + relay_chain_state, + downward_messages: Default::default(), + horizontal_messages: Default::default(), + relay_parent_descendants: Default::default(), + collator_peer_id: None, + }, + ) + .expect("parachain inherent encodes"); + pallet_deos_actors::provide_actor_prepass_inherent_data(&mut data) + .expect("Actor Prepass inherent encodes"); + data +} + +fn saturate_user_dispatch( + signer: &sr25519::Pair, + block_number: u32, + initial_nonce: crate::Nonce, + extrinsics: &mut Vec, +) -> (u32, Weight) { + let user_limit = BlockResourceBudgetValue::get().limits().user_base_turn(); + let mut nonce = initial_nonce; + let mut calls = 0u32; + loop { + assert!(calls < 10_000, "valid user-demand fixture remains bounded"); + let call = RuntimeCall::System(polkadot_sdk::frame_system::Call::remark { + remark: (block_number, nonce).encode(), + }); + let extrinsic = + actors_integration_tests::signed_extrinsic(signer, crate::Nonce::from(nonce), call); + let next_weight = extrinsic.get_dispatch_info().total_weight(); + let state = Actors::block_resource_state().expect("Actor Prepass opens resource state"); + let remaining = user_limit + .checked_sub(&state.usage().user_dispatch_used()) + .unwrap_or_else(Weight::zero); + if !next_weight.all_lte(remaining) { + assert!( + calls > 0, + "continuous demand must admit at least one valid call" + ); + return (calls, next_weight); + } + Executive::apply_extrinsic(extrinsic.clone()) + .unwrap_or_else(|error| panic!("valid user call rejected: {error:?}")) + .unwrap_or_else(|error| panic!("valid user call failed dispatch: {error:?}")); + extrinsics.push(extrinsic); + nonce = nonce.saturating_add(1); + calls = calls.saturating_add(1); + } +} + +fn saturate_user_dispatch_with_router_swaps( + signer: &sr25519::Pair, + block_number: u32, + initial_nonce: crate::Nonce, + extrinsics: &mut Vec, +) -> (u32, Weight) { + let signer_account = crate::AccountId::from(signer.public()); + let user_limit = BlockResourceBudgetValue::get().limits().user_base_turn(); + let mut nonce = initial_nonce; + let mut calls = 0u32; + loop { + assert!(calls < 1_000, "W9 Router saturation remains bounded"); + let native_to_local = calls.is_multiple_of(2); + let call = RuntimeCall::DeosRouter(pallet_deos_router::Call::swap { + from: if native_to_local { + primitives::AssetKind::Native + } else { + primitives::AssetKind::Local(ASSET_A) + }, + to: if native_to_local { + primitives::AssetKind::Local(ASSET_A) + } else { + primitives::AssetKind::Native + }, + amount_in: super::common::SWAP_AMOUNT, + min_amount_out: 1, + recipient: signer_account.clone(), + deadline: block_number.saturating_add(1), + }); + let extrinsic = + actors_integration_tests::signed_extrinsic(signer, crate::Nonce::from(nonce), call); + let next_weight = extrinsic.get_dispatch_info().total_weight(); + let state = Actors::block_resource_state().expect("Actor Prepass opens resource state"); + let remaining = user_limit + .checked_sub(&state.usage().user_dispatch_used()) + .unwrap_or_else(Weight::zero); + if !next_weight.all_lte(remaining) { + assert!(calls > 0, "W9 must admit at least one valid Router swap"); + return (calls, next_weight); + } + Executive::apply_extrinsic(extrinsic.clone()) + .unwrap_or_else(|error| panic!("valid W9 Router call rejected: {error:?}")) + .unwrap_or_else(|error| panic!("valid W9 Router call failed dispatch: {error:?}")); + extrinsics.push(extrinsic); + nonce = nonce.saturating_add(1); + calls = calls.saturating_add(1); + } +} + +fn authored_metrics( + workload_profiles: &BTreeMap, + user_calls: u32, + next_user_weight: Option, + prepass_steps: u32, + prepass_trigger_occurrences: u32, + prepass_actor_control: Weight, + prepass_actor_effect: Weight, +) -> FullExecutiveBlockMetrics { + let mut progressed_steps = Vec::new(); + let mut opening_steps = 0u32; + let mut middle_steps = 0u32; + let mut final_steps = 0u32; + let mut transfer_steps = 0u32; + let mut swap_out_steps = 0u32; + let mut stop_cycle_steps = 0u32; + let mut non_successful_steps = 0u32; + let mut completed_cycles = 0u32; + let mut completed_cycle_actors = Vec::new(); + let mut failed_cycle_actors = Vec::new(); + let mut suspended_steps = Vec::new(); + let mut continued_steps = Vec::new(); + let mut closed_actors = Vec::new(); + let mut paused_actors = Vec::new(); + let mut resumed_actors = Vec::new(); + let mut trigger_occurrences = Vec::new(); + for record in System::events() { + let (actor_id, step_index, successful, observed_task) = match record.event { + RuntimeEvent::Actors(Event::ActorPaused { actor_id }) + if workload_profiles.contains_key(&actor_id) => + { + paused_actors.push(actor_id); + continue; + } + RuntimeEvent::Actors(Event::ActorResumed { actor_id }) + if workload_profiles.contains_key(&actor_id) => + { + resumed_actors.push(actor_id); + continue; + } + RuntimeEvent::Actors(Event::TriggerOccurrenceProcessed { + actor_id, + trigger_family, + .. + }) if workload_profiles.contains_key(&actor_id) => { + trigger_occurrences.push((actor_id, trigger_family)); + continue; + } + RuntimeEvent::Actors(Event::TransferExecuted { + actor_id, + step_index, + .. + }) if workload_profiles.contains_key(&actor_id) => { + transfer_steps = transfer_steps.saturating_add(1); + (actor_id, step_index, true, Some(WorkloadTask::Transfer)) + } + RuntimeEvent::Actors(Event::SwapExecuted { + actor_id, + step_index, + .. + }) if workload_profiles.contains_key(&actor_id) => { + swap_out_steps = swap_out_steps.saturating_add(1); + ( + actor_id, + step_index, + true, + Some(WorkloadTask::RouterSwapOut), + ) + } + RuntimeEvent::Actors(Event::CycleStopped { + actor_id, + step_index, + .. + }) if workload_profiles.contains_key(&actor_id) => { + stop_cycle_steps = stop_cycle_steps.saturating_add(1); + (actor_id, step_index, true, Some(WorkloadTask::StopCycle)) + } + RuntimeEvent::Actors(Event::StepSkipped { + actor_id, + step_index, + .. + }) if workload_profiles.contains_key(&actor_id) => (actor_id, step_index, false, None), + RuntimeEvent::Actors(Event::StepFailed { + actor_id, + step_index, + retry_class, + ref error, + .. + }) if workload_profiles.contains_key(&actor_id) => { + println!( + "EXP_0066_WORKLOAD_STEP_FAILED actor={actor_id} step={step_index} retry={retry_class:?} error={error:?}" + ); + (actor_id, step_index, false, None) + } + RuntimeEvent::Actors(Event::CycleSummary { + actor_id, result, .. + }) if workload_profiles.contains_key(&actor_id) => { + match result { + CycleResult::Completed => { + completed_cycles = completed_cycles.saturating_add(1); + completed_cycle_actors.push(actor_id); + } + CycleResult::Failed => failed_cycle_actors.push(actor_id), + CycleResult::Cancelled => {} + } + continue; + } + RuntimeEvent::Actors(Event::CycleSuspended { + actor_id, cursor, .. + }) if workload_profiles.contains_key(&actor_id) => { + suspended_steps.push((actor_id, cursor)); + continue; + } + RuntimeEvent::Actors(Event::CycleContinued { + actor_id, cursor, .. + }) if workload_profiles.contains_key(&actor_id) => { + continued_steps.push((actor_id, cursor)); + continue; + } + RuntimeEvent::Actors(Event::ActorClosed { + actor_id, reason, .. + }) if workload_profiles.contains_key(&actor_id) => { + closed_actors.push((actor_id, reason)); + continue; + } + _ => continue, + }; + let profile = workload_profiles + .get(&actor_id) + .expect("matched workload Actor has a profile"); + if let (Some(task), Some(expected)) = (observed_task, profile.task) { + assert_eq!(task, expected, "effect event matches its fixture task"); + } + assert!(step_index < profile.step_count); + if step_index == 0 { + opening_steps = opening_steps.saturating_add(1); + } else if step_index.saturating_add(1) == profile.step_count { + final_steps = final_steps.saturating_add(1); + } else { + middle_steps = middle_steps.saturating_add(1); + } + if !successful { + non_successful_steps = non_successful_steps.saturating_add(1); + } + progressed_steps.push((actor_id, step_index)); + } + let mut actor_ids = progressed_steps + .iter() + .map(|(actor_id, _)| *actor_id) + .collect::>(); + actor_ids.sort_unstable(); + actor_ids.dedup(); + assert_eq!( + progressed_steps.len(), + actor_ids.len(), + "full block must preserve Q1 for the workload population" + ); + let actor_steps = + u32::try_from(progressed_steps.len()).expect("bounded block Step count fits u32"); + assert_eq!( + actor_steps, + opening_steps + .saturating_add(middle_steps) + .saturating_add(final_steps) + ); + assert_eq!( + actor_steps, + transfer_steps + .saturating_add(swap_out_steps) + .saturating_add(stop_cycle_steps) + .saturating_add(non_successful_steps), + "every workload Step has exactly one terminal task outcome" + ); + let telemetry = Actors::finalized_block_resource_telemetry() + .expect("full Executive finalization publishes Actor telemetry"); + let usage = telemetry.usage(); + assert!(!telemetry.optional_actor_work_halted()); + FullExecutiveBlockMetrics { + actor_steps, + distinct_actors: actor_ids.len() as u32, + progressed_steps, + opening_steps, + middle_steps, + final_steps, + transfer_steps, + swap_out_steps, + stop_cycle_steps, + non_successful_steps, + completed_cycles, + completed_cycle_actors, + failed_cycle_actors, + suspended_steps, + continued_steps, + closed_actors, + paused_actors, + resumed_actors, + trigger_occurrences, + user_calls, + next_user_weight, + prepass_steps, + prepass_trigger_occurrences, + prepass_actor_control, + prepass_actor_effect, + actor_control: usage.actor_control_used(), + actor_effect: usage.actor_effect_used(), + user_dispatch: usage.user_dispatch_used(), + queue_head: Actors::queue_head(), + queue_tail: Actors::queue_tail(), + } +} + +fn author_complete_block_after_with_calls( + pre_state: Storage, + wasm: &[u8], + parent: &Header, + block_number: u32, + demand: UserDemand, + signer: &sr25519::Pair, + signer_nonce: crate::Nonce, + workload_profiles: &BTreeMap, + signed_calls: &[RuntimeCall], +) -> AuthoredBlock { + let inherent_data = inherent_data_for(parent, block_number); + let mut ext = TestExternalities::new_with_code_and_state( + wasm, + pre_state.clone(), + crate::VERSION.state_version(), + ); + let recorder = Recorder::::default(); + let proof_size = RecordingProofSizeProvider::new(recorder.clone()); + ext.register_extension(ProofSizeExt::new(proof_size)); + let (block, metrics, post_state) = ext.execute_with_recorder(recorder, || { + let header = Header::new( + block_number, + Default::default(), + Default::default(), + parent.hash(), + Digest { + logs: vec![DigestItem::PreRuntime( + polkadot_sdk::sp_consensus_aura::AURA_ENGINE_ID, + u64::from(block_number).encode(), + )], + }, + ); + Executive::initialize_block(&header); + let mut extrinsics = inherent_data.create_extrinsics(); + assert!( + extrinsics.len() >= 3, + "runtime inherent order must include parachain context, timestamp, and Actor Prepass" + ); + for extrinsic in &extrinsics { + Executive::apply_extrinsic(extrinsic.clone()) + .expect("ordered inherent is valid") + .expect("ordered inherent dispatch succeeds"); + } + let prepass_state = + Actors::block_resource_state().expect("Actor Prepass inherent runs before signed demand"); + let prepass_usage = prepass_state.usage(); + let mut prepass_steps = 0u32; + let mut prepass_trigger_occurrences = 0u32; + for record in System::events() { + match record.event { + RuntimeEvent::Actors( + Event::TransferExecuted { actor_id, .. } + | Event::SwapExecuted { actor_id, .. } + | Event::CycleStopped { actor_id, .. } + | Event::StepSkipped { actor_id, .. } + | Event::StepFailed { actor_id, .. }, + ) if workload_profiles.contains_key(&actor_id) => { + prepass_steps = prepass_steps.saturating_add(1); + } + RuntimeEvent::Actors(Event::TriggerOccurrenceProcessed { actor_id, .. }) + if workload_profiles.contains_key(&actor_id) => + { + prepass_trigger_occurrences = prepass_trigger_occurrences.saturating_add(1); + } + _ => {} + } + } + let signed_call_count = u32::try_from(signed_calls.len()).expect("bounded W6 call count fits"); + let mut next_nonce = signer_nonce; + for (call_index, call) in signed_calls.iter().enumerate() { + let extrinsic = actors_integration_tests::signed_extrinsic(signer, next_nonce, call.clone()); + Executive::apply_extrinsic(extrinsic.clone()) + .unwrap_or_else(|error| panic!("valid scripted call {call_index} rejected: {error:?}")) + .unwrap_or_else(|error| { + panic!("valid scripted call {call_index} failed dispatch: {error:?}") + }); + extrinsics.push(extrinsic); + next_nonce = next_nonce.saturating_add(1); + } + let (demand_calls, next_user_weight) = match demand { + UserDemand::ActorOnly => (0, None), + UserDemand::ContinuousValid => { + let (calls, next) = + saturate_user_dispatch(signer, block_number, next_nonce, &mut extrinsics); + (calls, Some(next)) + } + UserDemand::RefTimeHeavy => { + let (calls, next) = saturate_user_dispatch_with_router_swaps( + signer, + block_number, + next_nonce, + &mut extrinsics, + ); + (calls, Some(next)) + } + }; + let user_calls = signed_call_count.saturating_add(demand_calls); + let block = Block { + header: Executive::finalize_block(), + extrinsics, + }; + let metrics = authored_metrics( + workload_profiles, + user_calls, + next_user_weight, + prepass_steps, + prepass_trigger_occurrences, + prepass_usage.actor_control_used(), + prepass_usage.actor_effect_used(), + ); + let post_state = current_top_storage(); + (block, metrics, post_state) + }); + AuthoredBlock { + pre_state, + post_state, + block, + inherent_data, + metrics, + } +} + +fn author_complete_block_after( + pre_state: Storage, + wasm: &[u8], + parent: &Header, + block_number: u32, + demand: UserDemand, + signer: &sr25519::Pair, + signer_nonce: crate::Nonce, + workload_profiles: &BTreeMap, +) -> AuthoredBlock { + author_complete_block_after_with_calls( + pre_state, + wasm, + parent, + block_number, + demand, + signer, + signer_nonce, + workload_profiles, + &[], + ) +} + +fn author_complete_block( + pre_state: Storage, + wasm: &[u8], + block_number: u32, + demand: UserDemand, + signer: &sr25519::Pair, + workload_profiles: &BTreeMap, +) -> AuthoredBlock { + let parent = parent_header_for(pre_state.clone(), wasm, block_number.saturating_sub(1)); + author_complete_block_after( + pre_state, + wasm, + &parent, + block_number, + demand, + signer, + 0, + workload_profiles, + ) +} + +fn assert_runtime_version_equivalent(actual: &polkadot_sdk::sp_version::RuntimeVersion) { + let expected = crate::VERSION; + assert_eq!(actual.spec_name, expected.spec_name); + assert_eq!(actual.impl_name, expected.impl_name); + assert_eq!(actual.authoring_version, expected.authoring_version); + assert_eq!(actual.spec_version, expected.spec_version); + assert_eq!(actual.impl_version, expected.impl_version); + assert_eq!(actual.transaction_version, expected.transaction_version); + assert_eq!(actual.system_version, expected.system_version); + let mut actual_apis = actual.apis.to_vec(); + let mut expected_apis = expected.apis.to_vec(); + actual_apis.sort_unstable(); + expected_apis.sort_unstable(); + assert_eq!( + actual_apis, expected_apis, + "runtime API declaration order is not semantic" + ); +} + +fn assert_complete_block_replays_natively(authored: &AuthoredBlock, wasm: &[u8]) { + let mut replay = TestExternalities::new_with_code_and_state( + wasm, + authored.pre_state.clone(), + crate::VERSION.state_version(), + ); + let recorder = Recorder::::default(); + let proof_size = RecordingProofSizeProvider::new(recorder.clone()); + replay.register_extension(ProofSizeExt::new(proof_size)); + replay.execute_with_recorder(recorder, || { + crate::apis::validate_context_inherent_geometry(&authored.inherent_data) + .expect("full block context geometry is bounded"); + assert!( + authored + .inherent_data + .check_extrinsics(&authored.block.clone().into()) + .ok() + ); + Executive::execute_block(authored.block.clone().into()); + }); +} + +#[derive(Debug, Eq, PartialEq)] +struct StorageOwnerProof { + key_paths: usize, + node_bytes: usize, + exclusive_node_bytes: usize, + shared_node_bytes: usize, +} + +fn runtime_proof_storage_info() -> Vec { + let mut info = crate::AllPalletsWithSystem::storage_info(); + // The SDK XCMP on_idle path still probes this private migration alias, absent from metadata. + info.push(StorageInfo { + pallet_name: b"XcmpQueue".to_vec(), + storage_name: b"InboundXcmpStatus".to_vec(), + prefix: polkadot_sdk::frame_support::storage::storage_prefix( + b"XcmpQueue", + b"InboundXcmpStatus", + ) + .to_vec(), + max_values: Some(1), + max_size: None, + }); + info +} + +#[test] +fn runtime_proof_storage_info_covers_upstream_idle_probe() { + let key = + polkadot_sdk::frame_support::storage::storage_prefix(b"XcmpQueue", b"InboundXcmpStatus"); + assert_eq!( + format!( + "{:?}", + polkadot_sdk::sp_core::hexdisplay::HexDisplay::from(&key) + ), + "7b3237373ffdfeb1cab4222e3b520d6b345d8e88afa015075c945637c07e8f20", + ); + let info = runtime_proof_storage_info(); + let matching = info + .iter() + .filter(|info| info.prefix == key) + .collect::>(); + assert_eq!(matching.len(), 1); + assert_eq!(matching[0].storage_name, b"InboundXcmpStatus"); + assert!( + !reference_genesis_storage(&[]) + .top + .contains_key(key.as_slice()) + ); +} + +fn storage_proof_owners( + pre_state_root: H256, + paths: &BTreeMap<(H256, Vec), StorageProof>, + storage_info: &[StorageInfo], +) -> (BTreeMap, usize) { + let mut owner_nodes = BTreeMap::>)>::new(); + for ((root, key), proof) in paths { + let owner = if *root != pre_state_root { + "NonTopTrie".to_owned() + } else { + let mut matching = storage_info + .iter() + .filter(|info| key.starts_with(&info.prefix)); + let owner = matching.next().map(|info| { + format!( + "{}.{}", + std::str::from_utf8(&info.pallet_name).expect("pallet name is UTF-8"), + std::str::from_utf8(&info.storage_name).expect("storage name is UTF-8"), + ) + }); + assert!( + matching.next().is_none(), + "storage prefixes must have one owner" + ); + owner.unwrap_or_else(|| { + if key.starts_with(b":") { + "WellKnownTop".to_owned() + } else { + format!( + "UnmappedTop:0x{:?}", + polkadot_sdk::sp_core::hexdisplay::HexDisplay::from(key), + ) + } + }) + }; + let (key_paths, nodes) = owner_nodes.entry(owner).or_default(); + *key_paths += 1; + nodes.extend(proof.iter_nodes()); + } + let mut node_owner_counts = BTreeMap::<&Vec, usize>::new(); + for (_, nodes) in owner_nodes.values() { + for node in nodes { + *node_owner_counts.entry(node).or_default() += 1; + } + } + let cross_owner_shared_node_bytes = node_owner_counts + .iter() + .filter(|(_, count)| **count > 1) + .map(|(node, _)| node.len()) + .sum(); + let owners = owner_nodes + .into_iter() + .map(|(owner, (key_paths, nodes))| { + let node_bytes = nodes.iter().map(|node| node.len()).sum::(); + let exclusive_node_bytes = nodes + .iter() + .filter(|node| node_owner_counts[*node] == 1) + .map(|node| node.len()) + .sum::(); + ( + owner, + StorageOwnerProof { + key_paths, + node_bytes, + exclusive_node_bytes, + shared_node_bytes: node_bytes - exclusive_node_bytes, + }, + ) + }) + .collect(); + (owners, cross_owner_shared_node_bytes) +} + +#[test] +fn storage_proof_owners_preserve_shared_nodes_and_unmapped_roots() { + let root = H256::repeat_byte(1); + let child_root = H256::repeat_byte(2); + let shared = vec![9; 3]; + let paths = [ + ( + (root, vec![1, 0]), + StorageProof::new([vec![1; 4], shared.clone()]), + ), + ( + (root, vec![1, 1]), + StorageProof::new([vec![1; 4], shared.clone()]), + ), + ( + (root, vec![2, 0]), + StorageProof::new([vec![2; 5], shared.clone()]), + ), + ( + (child_root, vec![1, 0]), + StorageProof::new([vec![3; 6], shared]), + ), + ((root, b":key".to_vec()), StorageProof::new([vec![4; 7]])), + ((root, vec![7]), StorageProof::new([vec![5; 8]])), + ] + .into(); + let storage_info = [1, 2].map(|prefix| StorageInfo { + pallet_name: b"Pallet".to_vec(), + storage_name: format!("Item{prefix}").into_bytes(), + prefix: vec![prefix], + max_values: None, + max_size: None, + }); + let (owners, cross_owner_shared) = storage_proof_owners(root, &paths, &storage_info); + assert_eq!(owners.len(), 5); + assert_eq!( + owners["Pallet.Item1"], + StorageOwnerProof { + key_paths: 2, + node_bytes: 7, + exclusive_node_bytes: 4, + shared_node_bytes: 3, + } + ); + assert_eq!(owners["Pallet.Item2"].exclusive_node_bytes, 5); + assert_eq!(owners["NonTopTrie"].exclusive_node_bytes, 6); + assert_eq!(owners["WellKnownTop"].exclusive_node_bytes, 7); + assert_eq!(owners["UnmappedTop:0x07"].exclusive_node_bytes, 8); + assert_eq!(cross_owner_shared, 3); + assert_eq!( + owners + .values() + .map(|owner| owner.exclusive_node_bytes) + .sum::() + + cross_owner_shared, + 33, + ); + assert_eq!( + owners.values().map(|owner| owner.key_paths).sum::(), + paths.len() + ); +} + +fn replay_complete_block_in_wasm( + authored: &AuthoredBlock, + wasm: &[u8], + label: &str, +) -> WasmProofMetrics { + let evidence = super::wasm_replay::replay_block( + authored.pre_state.clone(), + wasm, + &authored.block.encode(), + &authored.inherent_data, + ) + .unwrap_or_else(|error| panic!("{label} production Wasm replay failed: {error}")); + assert_runtime_version_equivalent(&evidence.runtime_version); + assert_eq!(evidence.block_hash, authored.block.header.hash()); + assert_eq!( + evidence.post_state_root, + *authored.block.header.state_root() + ); + assert!(evidence.execute_block_result.is_empty()); + for proof in [&evidence.execution_proof, &evidence.verification_proof] { + assert_eq!( + proof.storage_proof_scale_bytes, + proof.storage_proof.encoded_size() + ); + assert_eq!( + proof.compact_proof_scale_bytes, + proof.compact_proof.encoded_size() + ); + assert_eq!(proof.trie_node_count, proof.storage_proof.len()); + assert!(proof.trie_node_bytes > 0); + } + println!( + "EXP_0066_WASM_BLOCK_PREP_V1 profile={label} code={:?} block={:?} execution_storage_proof_bytes={} execution_compact_proof_bytes={} execution_node_bytes={} execution_nodes={} verification_storage_proof_bytes={} verification_compact_proof_bytes={} post_execute_root_node_bytes={} proof_size_observations={:?}", + evidence.runtime_code_hash, + evidence.block_hash, + evidence.execution_proof.storage_proof_scale_bytes, + evidence.execution_proof.compact_proof_scale_bytes, + evidence.execution_proof.trie_node_bytes, + evidence.execution_proof.trie_node_count, + evidence.verification_proof.storage_proof_scale_bytes, + evidence.verification_proof.compact_proof_scale_bytes, + evidence.post_execute_root_node_bytes, + evidence.proof_size_observations, + ); + let overlap = evidence.execution_key_overlap; + assert_eq!( + overlap.unique_node_bytes + overlap.unattributed_node_bytes, + evidence.execution_proof.trie_node_bytes, + ); + println!( + "EXP_0066_WASM_KEY_PROOF_V1 profile={label} key_paths={} per_key_node_bytes={} unique_node_bytes={} shared_node_bytes={} repeated_node_bytes={} unattributed_node_bytes={}", + evidence.execution_recorded_keys.len(), + overlap.per_key_node_bytes, + overlap.unique_node_bytes, + overlap.shared_node_bytes, + overlap.per_key_node_bytes - overlap.unique_node_bytes, + overlap.unattributed_node_bytes, + ); + assert_eq!( + overlap.paths.keys().cloned().collect::>(), + evidence.execution_recorded_keys, + ); + let key_nodes = overlap + .paths + .values() + .flat_map(StorageProof::iter_nodes) + .collect::>(); + let execution_nodes = evidence + .execution_proof + .storage_proof + .iter_nodes() + .collect::>(); + let root_nodes = evidence + .committed_root_proof + .iter_nodes() + .collect::>(); + let residual_nodes = execution_nodes + .difference(&key_nodes) + .copied() + .collect::>(); + let root_covered_residual_node_bytes = residual_nodes + .intersection(&root_nodes) + .map(|node| node.len()) + .sum::(); + let remaining_residual_node_bytes = residual_nodes + .difference(&root_nodes) + .map(|node| node.len()) + .sum::(); + assert_eq!( + root_covered_residual_node_bytes + remaining_residual_node_bytes, + overlap.unattributed_node_bytes, + ); + println!( + "EXP_0066_WASM_ROOT_DELTA_PROOF_V1 profile={label} committed_root_node_bytes={} root_covered_residual_node_bytes={root_covered_residual_node_bytes} remaining_residual_node_bytes={remaining_residual_node_bytes}", + root_nodes.iter().map(|node| node.len()).sum::(), + ); + let (owners, cross_owner_shared_node_bytes) = storage_proof_owners( + evidence.pre_state_root, + &overlap.paths, + &runtime_proof_storage_info(), + ); + let exclusive_node_bytes = owners + .values() + .map(|owner| owner.exclusive_node_bytes) + .sum::(); + assert_eq!( + exclusive_node_bytes + cross_owner_shared_node_bytes, + overlap.unique_node_bytes + ); + assert_eq!( + owners.values().map(|owner| owner.key_paths).sum::(), + evidence.execution_recorded_keys.len(), + ); + for (owner, metrics) in owners { + println!( + "EXP_0066_WASM_STORAGE_OWNER_V1 profile={label} owner={owner} key_paths={} node_bytes={} exclusive_node_bytes={} shared_node_bytes={}", + metrics.key_paths, + metrics.node_bytes, + metrics.exclusive_node_bytes, + metrics.shared_node_bytes, + ); + } + println!( + "EXP_0066_WASM_STORAGE_PARTITION_V1 profile={label} exclusive_node_bytes={exclusive_node_bytes} cross_owner_shared_node_bytes={cross_owner_shared_node_bytes} unattributed_node_bytes={}", + overlap.unattributed_node_bytes, + ); + WasmProofMetrics { + execution_storage_proof_bytes: evidence.execution_proof.storage_proof_scale_bytes as u64, + execution_compact_proof_bytes: evidence.execution_proof.compact_proof_scale_bytes as u64, + verification_storage_proof_bytes: evidence.verification_proof.storage_proof_scale_bytes as u64, + verification_compact_proof_bytes: evidence.verification_proof.compact_proof_scale_bytes as u64, + execution_recorded_keys: evidence.execution_recorded_keys, + remaining_residual_node_bytes, + } +} + +fn assert_successful_transfer_outcomes(metrics: &FullExecutiveBlockMetrics) { + assert_eq!( + metrics.non_successful_steps, 0, + "Transfer evidence rejects failed or skipped Steps" + ); + assert_eq!( + metrics.actor_steps, metrics.transfer_steps, + "every Step must execute a Transfer" + ); + assert_eq!( + metrics.completed_cycles, metrics.transfer_steps, + "each one-Step Transfer must complete its Cycle" + ); +} + +fn assert_successful_transfer_resource_ledger( + wasm: &[u8], + empty: &AuthoredBlock, + profiles: [(&str, &AuthoredBlock, u64); 3], +) { + type W = crate::weights::pallet_deos_actors::SubstrateWeight; + let components = [ + ( + "discovery", + W::scheduler_paged_tombstone_drain(1), + 35_663_374, + 3_111, + DatabaseIo::new(4, 2), + ), + ( + "state-probe", + W::scheduler_actor_state_probe(), + 114_890_000, + 15_106, + DatabaseIo::new(7, 0), + ), + ( + "consume-envelope", + W::scheduler_paged_consume_preserve_page(), + 72_287_000, + 5_118, + DatabaseIo::new(5, 4), + ), + ( + "opening-complete-inclusive", + W::scheduler_inner_opening_user_complete_header_max(), + 175_025_000, + 7_990, + DatabaseIo::new(9, 5), + ), + ( + "invocation-receipt", + W::action_invocation_receipt(), + 4_680_000, + 0, + DatabaseIo::new(0, 0), + ), + ]; + // Consume settles its component-wise upper, including a physical delete-page path. + assert!( + W::scheduler_paged_consume_delete_page().all_lte(W::scheduler_paged_consume_preserve_page()) + ); + let mut control_per_step = Weight::zero(); + let mut control_io_per_step = DatabaseIo::new(0, 0); + for (_, weight, base, proof, io) in &components { + assert_production_weight_component(*weight, *base, *proof, *io); + control_per_step = control_per_step.saturating_add(*weight); + control_io_per_step = control_io_per_step.saturating_add(*io); + } + let effect_per_step = W::task_transfer(); + let effect_io_per_step = DatabaseIo::new(25, 12); + assert_production_weight_component(effect_per_step, 468_433_000, 29_222, effect_io_per_step); + let pair = |weight: Weight| [weight.ref_time(), weight.proof_size()]; + let reference_events = |authored: &AuthoredBlock| { + let ids = (0..ActorId::from(REFERENCE_SYSTEM_ACTOR_IDENTITIES)).collect::>(); + let snapshot = actor_lifecycle_observation(wasm, &authored.post_state, &ids); + let mut counts = BTreeMap::::new(); + for event in snapshot["events"] + .as_array() + .expect("observation has events") + { + *counts + .entry(event["kind"].as_str().expect("event has a kind").to_owned()) + .or_default() += 1; + } + counts + }; + let baseline_reference_events = reference_events(empty); + assert_eq!(empty.metrics.actor_steps, 0); + assert_eq!(empty.metrics.completed_cycles, 0); + assert_eq!(empty.metrics.actor_effect, Weight::zero()); + let mut rows = Vec::new(); + for (label, authored, extra_probes) in profiles { + assert_successful_transfer_outcomes(&authored.metrics); + assert_eq!(authored.metrics.prepass_steps, authored.metrics.actor_steps); + assert_eq!(reference_events(authored), baseline_reference_events); + let steps = u64::from(authored.metrics.actor_steps); + let step_control = control_per_step.saturating_mul(steps); + let extra_probe_control = W::scheduler_actor_state_probe().saturating_mul(extra_probes); + assert_eq!( + authored.metrics.actor_control, + empty + .metrics + .actor_control + .saturating_add(step_control) + .saturating_add(extra_probe_control), + "successful-Step owners and additional state probes explain the complete Control delta", + ); + assert_eq!( + authored.metrics.prepass_actor_control, + empty + .metrics + .prepass_actor_control + .saturating_add(step_control) + .saturating_add(W::scheduler_actor_state_probe().saturating_mul(extra_probes / 2)), + "one extra state probe belongs to each service phase in the retained-frontier fixtures", + ); + assert_eq!( + authored.metrics.actor_effect, + effect_per_step.saturating_mul(steps) + ); + let control_io = control_io_per_step + .saturating_mul(steps) + .saturating_add(DatabaseIo::new(7, 0).saturating_mul(extra_probes)); + let effect_io = effect_io_per_step.saturating_mul(steps); + rows.push(serde_json::json!({ + "profile": label, "committedSteps": steps, "extraStateProbes": extra_probes, + "stepControl": pair(step_control), "extraProbeControl": pair(extra_probe_control), + "controlDelta": pair(step_control.saturating_add(extra_probe_control)), + "effect": pair(authored.metrics.actor_effect), + "generatedControlIoDelta": [control_io.reads, control_io.writes], + "generatedEffectIo": [effect_io.reads, effect_io.writes], + })); + } + println!( + "ACTOR_TRANSFER_RESOURCE_LEDGER_V1 {}", + serde_json::json!({ + "scope": "W0-W1-first-block-successful-System-Transfer-delta", + "controlPerStep": pair(control_per_step), "effectPerStep": pair(effect_per_step), + "generatedControlIoPerStep": [control_io_per_step.reads, control_io_per_step.writes], + "generatedEffectIoPerStep": [effect_io_per_step.reads, effect_io_per_step.writes], + "baselineControl": pair(empty.metrics.actor_control), + "baselineReferenceEvents": baseline_reference_events, + "baselineWorkloadSteps": 0, "baselineIoAllocatedToSteps": false, + "components": components.map(|(name, weight, _, _, io)| serde_json::json!({ + "name": name, "weight": pair(weight), "reads": io.reads, "writes": io.writes, + })), + "profiles": rows, + }) + ); +} + +fn assert_prepared_w0_w1(wasm: &[u8], replay_wasm: bool) { + let empty = prepare_actor_fixture(wasm, 0, WorkloadSchedule::ManualOnly); + let w0_empty = author_complete_block( + empty.storage, + wasm, + 2, + UserDemand::ActorOnly, + &empty.signer, + &empty.actor_profiles, + ); + assert_eq!(w0_empty.metrics.actor_steps, 0); + assert_complete_block_replays_natively(&w0_empty, wasm); + + let one = prepare_actor_fixture(wasm, 1, WorkloadSchedule::ManualOnly); + let w0_one = author_complete_block( + one.storage, + wasm, + 2, + UserDemand::ActorOnly, + &one.signer, + &one.actor_profiles, + ); + assert_eq!(w0_one.metrics.actor_steps, 1); + assert_eq!(w0_one.metrics.distinct_actors, 1); + assert_successful_transfer_outcomes(&w0_one.metrics); + assert_complete_block_replays_natively(&w0_one, wasm); + + let w1 = prepare_actor_fixture(wasm, PREPARED_W1_ACTORS, WorkloadSchedule::ManualOnly); + let actor_only = author_complete_block( + w1.storage.clone(), + wasm, + 2, + UserDemand::ActorOnly, + &w1.signer, + &w1.actor_profiles, + ); + let user_demand = author_complete_block( + w1.storage, + wasm, + 2, + UserDemand::ContinuousValid, + &w1.signer, + &w1.actor_profiles, + ); + for authored in [&actor_only, &user_demand] { + assert_successful_transfer_outcomes(&authored.metrics); + assert!(authored.metrics.actor_steps > 0); + assert!(authored.metrics.actor_steps <= w1.actor_ids.len() as u32); + assert_eq!( + authored.metrics.actor_steps, + authored.metrics.distinct_actors + ); + assert_complete_block_replays_natively(authored, wasm); + } + assert_eq!(actor_only.metrics.user_calls, 0); + assert_eq!(actor_only.metrics.user_dispatch, Weight::zero()); + assert!(user_demand.metrics.user_calls > 0); + assert_ne!(user_demand.metrics.user_dispatch, Weight::zero()); + let next_user = user_demand + .metrics + .next_user_weight + .expect("continuous demand records the first rejected frontier"); + let remaining = BlockResourceBudgetValue::get() + .limits() + .user_base_turn() + .checked_sub(&user_demand.metrics.user_dispatch) + .unwrap_or_else(Weight::zero); + assert!(!next_user.all_lte(remaining)); + + for (label, authored) in [ + ("W0-empty", &w0_empty), + ("W0-one-step", &w0_one), + (UserDemand::ActorOnly.label(), &actor_only), + (UserDemand::ContinuousValid.label(), &user_demand), + ] { + println!( + "EXP_0066_FULL_EXECUTIVE_PREP_V1 profile={label} block={} inherents_and_calls={} actor_steps={} distinct_actors={} user_calls={} control_ref_time={} control_proof_size={} effect_ref_time={} effect_proof_size={} user_ref_time={} user_proof_size={}", + authored.block.header.number(), + authored.block.extrinsics.len(), + authored.metrics.actor_steps, + authored.metrics.distinct_actors, + authored.metrics.user_calls, + authored.metrics.actor_control.ref_time(), + authored.metrics.actor_control.proof_size(), + authored.metrics.actor_effect.ref_time(), + authored.metrics.actor_effect.proof_size(), + authored.metrics.user_dispatch.ref_time(), + authored.metrics.user_dispatch.proof_size(), + ); + } + + assert_successful_transfer_resource_ledger( + wasm, + &w0_empty, + [ + ("one-step", &w0_one, 0), + ("actor-only", &actor_only, 2), + ("continuous-user", &user_demand, 2), + ], + ); + + if replay_wasm { + for (label, authored) in [ + ("W0-empty", w0_empty), + ("W0-one-step", w0_one), + (UserDemand::ActorOnly.label(), actor_only), + (UserDemand::ContinuousValid.label(), user_demand), + ] { + let _ = replay_complete_block_in_wasm(&authored, wasm, label); + } + } +} + +fn nearest_rank_percentile(values: &[u64], percentile: usize) -> u64 { + if values.is_empty() { + return 0; + } + let mut sorted = values.to_vec(); + sorted.sort_unstable(); + let rank = percentile + .saturating_mul(sorted.len()) + .div_ceil(100) + .saturating_sub(1) + .min(sorted.len().saturating_sub(1)); + sorted[rank] +} + +#[derive(Default)] +struct OneStepActorHistory { + pending_signal_block: Option, + first_signal_block: Option, + first_step_block: Option, + last_step_block: Option, +} + +struct OneStepServiceTrace { + actors: BTreeMap, + signal_to_step_blocks: Vec<(ActorId, u64)>, + inter_step_gaps: Vec<(ActorId, u64)>, +} + +impl OneStepServiceTrace { + fn new(actor_ids: &[ActorId]) -> Self { + let actors = actor_ids + .iter() + .map(|id| (*id, OneStepActorHistory::default())) + .collect::>(); + assert_eq!( + actors.len(), + actor_ids.len(), + "workload Actor identities are unique" + ); + Self { + actors, + signal_to_step_blocks: Vec::new(), + inter_step_gaps: Vec::new(), + } + } + + fn for_schedule(actor_ids: &[ActorId], schedule: WorkloadSchedule) -> Self { + let mut trace = Self::new(actor_ids); + for (index, actor_id) in actor_ids.iter().enumerate() { + if schedule.uses_manual_trigger(index as u32) { + trace.signal(*actor_id, 1); + } + } + trace + } + + fn observe(&mut self, metrics: &FullExecutiveBlockMetrics, block: u32) { + assert_eq!( + metrics.non_successful_steps, 0, + "service evidence rejects failed/skipped Steps" + ); + assert_eq!( + metrics.actor_steps, metrics.completed_cycles, + "one completed Cycle per successful one-Step outcome" + ); + assert_eq!( + metrics.actor_steps, + metrics.transfer_steps + metrics.swap_out_steps + metrics.stop_cycle_steps, + "every service sample has a successful typed outcome" + ); + assert!( + metrics.continued_steps.is_empty() + && metrics.closed_actors.is_empty() + && metrics.suspended_steps.is_empty() + && metrics.paused_actors.is_empty() + && metrics.resumed_actors.is_empty() + && metrics.failed_cycle_actors.is_empty(), + "one-Step service observations exclude lifecycle interruptions" + ); + assert_eq!( + metrics + .progressed_steps + .iter() + .map(|(id, _)| *id) + .collect::>(), + metrics + .completed_cycle_actors + .iter() + .copied() + .collect::>(), + "successful Steps and completed Cycles belong to the same Actors", + ); + for (actor_id, family) in &metrics.trigger_occurrences { + assert_eq!( + *family, + TriggerFamily::Cadenced, + "only automatic cadence signals arrive during these campaigns" + ); + self.signal(*actor_id, block); + } + for (actor_id, _) in &metrics.progressed_steps { + self.step(*actor_id, block); + } + } + + fn assert_terminal_state(&self, wasm: &[u8], storage: Storage) { + let mut ext = + TestExternalities::new_with_code_and_state(wasm, storage, crate::VERSION.state_version()); + ext.execute_with(|| { + for (actor_id, history) in &self.actors { + let hot = Actors::actor_hot(*actor_id).expect("workload Actor remains active"); + assert_eq!( + hot.pending_signal, + history.pending_signal_block.is_some(), + "event-derived latch matches terminal state" + ); + } + }); + } + + fn signal(&mut self, actor_id: ActorId, block: u32) { + let actor = self + .actors + .get_mut(&actor_id) + .expect("signal belongs to the workload"); + assert!( + actor.pending_signal_block.is_none(), + "one useful signal per pending latch" + ); + assert!( + actor + .last_step_block + .is_none_or(|previous| block > previous), + "one-Step cadence rearms after prior service" + ); + actor.pending_signal_block = Some(block); + actor.first_signal_block.get_or_insert(block); + } + + fn step(&mut self, actor_id: ActorId, block: u32) { + let actor = self + .actors + .get_mut(&actor_id) + .expect("Step belongs to the workload"); + let signal = actor + .pending_signal_block + .take() + .expect("a Step must consume known readiness"); + assert!( + block > signal, + "materialized readiness has a next-block causal floor" + ); + self + .signal_to_step_blocks + .push((actor_id, u64::from(block - signal))); + actor.first_step_block.get_or_insert(block); + if let Some(previous) = actor.last_step_block.replace(block) { + assert!(block > previous, "Q1 permits one Step per Actor per block"); + self + .inter_step_gaps + .push((actor_id, u64::from(block - previous))); + } + } + + fn summary(&self, creation_block: u32, last_block: u32) -> serde_json::Value { + self.summary_for(creation_block, last_block, |_| true) + } + + fn summary_for( + &self, + creation_block: u32, + last_block: u32, + include: impl Fn(ActorId) -> bool, + ) -> serde_json::Value { + let actors = self + .actors + .iter() + .filter(|(id, _)| include(**id)) + .map(|(_, actor)| actor) + .collect::>(); + let signal_to_step_blocks = self + .signal_to_step_blocks + .iter() + .filter(|(id, _)| include(*id)) + .map(|(_, blocks)| *blocks) + .collect::>(); + let inter_step_gaps = self + .inter_step_gaps + .iter() + .filter(|(id, _)| include(*id)) + .map(|(_, blocks)| *blocks) + .collect::>(); + let first_step_latencies = actors + .iter() + .filter_map(|actor| actor.first_step_block) + .map(|block| { + u64::from( + block + .checked_sub(creation_block) + .expect("creation precedes service"), + ) + }) + .collect::>(); + let pending_waits = actors + .iter() + .filter_map(|actor| actor.pending_signal_block) + .map(|block| { + u64::from( + last_block + .checked_sub(block) + .expect("signal is inside the observed horizon"), + ) + }) + .collect::>(); + serde_json::json!({ + "workloadActors": actors.len(), + "creationBlock": creation_block, + "lastObservedBlock": last_block, + "everMaterializedActors": actors.iter().filter(|actor| actor.first_signal_block.is_some()).count(), + "neverMaterializedActors": actors.iter().filter(|actor| actor.first_signal_block.is_none()).count(), + "distinctProgressedActors": first_step_latencies.len(), + "firstStepRightCensoring": { + "actors": actors.len() - first_step_latencies.len(), + "atBlocksSinceCreation": last_block.checked_sub(creation_block).expect("ordered horizon"), + }, + "pendingSignals": pending_waits.len(), + "pendingPastCausalFloor": pending_waits.iter().filter(|wait| **wait > 0).count(), + "firstStepFromCreationBlocks": observed_distribution(&first_step_latencies), + "materializedSignalToStepBlocks": observed_distribution(&signal_to_step_blocks), + "pendingSignalRightCensoringBlocks": observed_distribution(&pending_waits), + "interStepGapBlocks": observed_distribution(&inter_step_gaps), + }) + } +} + +fn observed_distribution(values: &[u64]) -> serde_json::Value { + let observed = !values.is_empty(); + serde_json::json!({ + "samples": values.len(), + "min": values.iter().min(), + "mean": observed.then(|| values.iter().sum::() as f64 / values.len() as f64), + "p50": observed.then(|| nearest_rank_percentile(values, 50)), + "p95": observed.then(|| nearest_rank_percentile(values, 95)), + "p99": observed.then(|| nearest_rank_percentile(values, 99)), + "max": values.iter().max(), + }) +} + +#[test] +fn one_step_service_trace_separates_observed_delays_from_censoring() { + let mut trace = OneStepServiceTrace::new(&[15, 16, 17, 18]); + trace.signal(15, 1); + trace.signal(16, 2); + trace.step(15, 3); + trace.signal(15, 5); + trace.step(15, 6); + trace.signal(18, 7); + let summary = trace.summary(1, 7); + assert_eq!(summary["distinctProgressedActors"], 1); + assert_eq!(summary["neverMaterializedActors"], 1); + assert_eq!( + summary["firstStepRightCensoring"], + serde_json::json!({"actors": 3, "atBlocksSinceCreation": 6}) + ); + assert_eq!(summary["pendingSignals"], 2); + assert_eq!(summary["pendingPastCausalFloor"], 1); + assert_eq!(summary["materializedSignalToStepBlocks"]["samples"], 2); + assert_eq!(summary["materializedSignalToStepBlocks"]["mean"], 1.5); + assert_eq!(summary["firstStepFromCreationBlocks"]["max"], 2); + assert_eq!(summary["interStepGapBlocks"]["max"], 3); + assert_eq!(summary["pendingSignalRightCensoringBlocks"]["max"], 5); + let selected = trace.summary_for(1, 7, |id| id == 15 || id == 17); + assert_eq!(selected["workloadActors"], 2); + assert_eq!(selected["neverMaterializedActors"], 1); + assert_eq!(selected["firstStepRightCensoring"]["actors"], 1); + assert_eq!(selected["pendingSignals"], 0); + assert_eq!(selected["materializedSignalToStepBlocks"]["samples"], 2); + assert_eq!(selected["interStepGapBlocks"]["max"], 3); + let pending = trace.summary_for(1, 7, |id| id == 16 || id == 18); + assert_eq!(pending["pendingSignals"], 2); + assert_eq!(pending["pendingSignalRightCensoringBlocks"]["max"], 5); + assert_eq!(pending["materializedSignalToStepBlocks"]["samples"], 0); + assert!(pending["interStepGapBlocks"]["max"].is_null()); +} + +#[test] +fn one_step_service_trace_reports_missing_samples_as_unknown() { + let summary = OneStepServiceTrace::new(&[15]).summary(1, 101); + for field in [ + "firstStepFromCreationBlocks", + "materializedSignalToStepBlocks", + "interStepGapBlocks", + "pendingSignalRightCensoringBlocks", + ] { + assert_eq!(summary[field]["samples"], 0); + assert!(summary[field]["max"].is_null()); + assert!(summary[field]["mean"].is_null()); + } + assert_eq!(summary["firstStepRightCensoring"]["actors"], 1); + assert_eq!( + summary["firstStepRightCensoring"]["atBlocksSinceCreation"], + 100 + ); +} + +#[test] +#[should_panic(expected = "next-block causal floor")] +fn one_step_service_trace_rejects_same_block_service() { + let mut trace = OneStepServiceTrace::new(&[15]); + trace.signal(15, 2); + trace.step(15, 2); +} + +#[test] +fn one_step_service_trace_matches_runtime_latches() { + for schedule in [ + WorkloadSchedule::ManualOnly, + WorkloadSchedule::CadencedOnly, + WorkloadSchedule::MixedManualCadenced, + ] { + let fixture = prepare_actor_fixture(&[], 3, schedule); + let mut trace = OneStepServiceTrace::for_schedule(&fixture.actor_ids, schedule); + let mut pre_state = fixture.storage; + let mut parent = parent_header_for(pre_state.clone(), &[], 1); + for block in 2..=9 { + let authored = author_complete_block_after( + pre_state, + &[], + &parent, + block, + UserDemand::ActorOnly, + &fixture.signer, + 0, + &fixture.actor_profiles, + ); + trace.observe(&authored.metrics, block); + parent = authored.block.header; + pre_state = authored.post_state; + } + trace.assert_terminal_state(&[], pre_state); + let summary = trace.summary(1, 9); + assert_eq!(summary["distinctProgressedActors"], 3); + if schedule == WorkloadSchedule::ManualOnly { + assert_eq!(summary["materializedSignalToStepBlocks"]["samples"], 3); + assert!(summary["interStepGapBlocks"]["max"].is_null()); + } else { + assert!(summary["interStepGapBlocks"]["samples"].as_u64().unwrap() > 0); + } + } +} + +fn run_schedule_campaign( + wasm: &[u8], + workload: &str, + demand: UserDemand, + workload_schedule: WorkloadSchedule, +) { + let maximum_active = ::MaxActiveActors::get(); + let maximum_identities = ::MaxActorIdentities::get(); + let (initial_active, initial_identities) = { + let mut ext = TestExternalities::new_with_code_and_state( + wasm, + reference_genesis_storage(wasm), + crate::VERSION.state_version(), + ); + ext.execute_with(|| { + ( + pallet_deos_actors::ActiveActorCount::::get(), + pallet_deos_actors::ActorIdentityCount::::get(), + ) + }) + }; + assert_eq!( + initial_active, REFERENCE_ACTIVE_SYSTEM_ACTORS, + "{workload} must explicitly account for the reference preset's active System Actors" + ); + assert_eq!( + initial_identities, REFERENCE_SYSTEM_ACTOR_IDENTITIES, + "{workload} must explicitly account for active and dormant reference System Actor identities" + ); + let added_actor_count = maximum_identities + .checked_sub(initial_identities) + .expect("reference identity population fits the production bound"); + let active_actor_count = initial_active.saturating_add(added_actor_count); + assert!(active_actor_count <= maximum_active); + let fixture = prepare_actor_fixture(wasm, added_actor_count, workload_schedule); + assert_eq!(fixture.actor_ids.len() as u32, added_actor_count); + + let mut pre_state = fixture.storage; + let mut parent = parent_header_for(pre_state.clone(), wasm, 1); + let mut signer_nonce = 0; + let mut total_cycles = 0u32; + let mut service = OneStepServiceTrace::for_schedule(&fixture.actor_ids, workload_schedule); + let mut steps = Vec::with_capacity(W1_TARGET_BLOCKS as usize); + let mut control_ref_time = Vec::with_capacity(W1_TARGET_BLOCKS as usize); + let mut control_proof_size = Vec::with_capacity(W1_TARGET_BLOCKS as usize); + let mut effect_ref_time = Vec::with_capacity(W1_TARGET_BLOCKS as usize); + let mut effect_proof_size = Vec::with_capacity(W1_TARGET_BLOCKS as usize); + let mut user_calls = Vec::with_capacity(W1_TARGET_BLOCKS as usize); + let mut user_ref_time = Vec::with_capacity(W1_TARGET_BLOCKS as usize); + let mut user_proof_size = Vec::with_capacity(W1_TARGET_BLOCKS as usize); + let mut user_bound_blocks = 0u32; + let mut control_remaining_ref_time = Vec::with_capacity(W1_TARGET_BLOCKS as usize); + let mut control_remaining_proof_size = Vec::with_capacity(W1_TARGET_BLOCKS as usize); + let mut control_bound_blocks = 0u32; + let actor_control_limit = BlockResourceBudgetValue::get().limits().actor_control(); + let mut execution_storage_proof = Vec::with_capacity(W1_TARGET_BLOCKS as usize); + let mut execution_compact_proof = Vec::with_capacity(W1_TARGET_BLOCKS as usize); + let mut verification_storage_proof = Vec::with_capacity(W1_TARGET_BLOCKS as usize); + let mut verification_compact_proof = Vec::with_capacity(W1_TARGET_BLOCKS as usize); + + for block_number in 2..W1_TARGET_BLOCKS.saturating_add(2) { + let authored = author_complete_block_after( + pre_state, + wasm, + &parent, + block_number, + demand, + &fixture.signer, + signer_nonce, + &fixture.actor_profiles, + ); + match demand { + UserDemand::ActorOnly => { + assert_eq!(authored.metrics.user_calls, 0); + assert_eq!(authored.metrics.user_dispatch, Weight::zero()); + assert!(authored.metrics.next_user_weight.is_none()); + } + UserDemand::ContinuousValid => { + assert!(authored.metrics.user_calls > 0); + let next_user = authored + .metrics + .next_user_weight + .expect("continuous demand records its first inadmissible valid call"); + let remaining = BlockResourceBudgetValue::get() + .limits() + .user_base_turn() + .checked_sub(&authored.metrics.user_dispatch) + .unwrap_or_else(Weight::zero); + assert!( + !next_user.all_lte(remaining), + "continuous demand must reach the User base-turn frontier" + ); + user_bound_blocks = user_bound_blocks.saturating_add(1); + } + UserDemand::RefTimeHeavy => { + unreachable!("the W1/W2 schedule campaign does not prepare Router demand") + } + } + assert_eq!( + authored.metrics.actor_steps, + authored.metrics.distinct_actors + ); + assert!( + authored.metrics.queue_head < authored.metrics.queue_tail, + "the 10,000-Cycle campaign must remain saturated in block {block_number}" + ); + assert_successful_transfer_outcomes(&authored.metrics); + service.observe(&authored.metrics, block_number); + total_cycles = total_cycles.saturating_add(authored.metrics.completed_cycles); + steps.push(u64::from(authored.metrics.actor_steps)); + control_ref_time.push(authored.metrics.actor_control.ref_time()); + control_proof_size.push(authored.metrics.actor_control.proof_size()); + effect_ref_time.push(authored.metrics.actor_effect.ref_time()); + effect_proof_size.push(authored.metrics.actor_effect.proof_size()); + signer_nonce = signer_nonce.saturating_add(authored.metrics.user_calls); + user_calls.push(u64::from(authored.metrics.user_calls)); + user_ref_time.push(authored.metrics.user_dispatch.ref_time()); + user_proof_size.push(authored.metrics.user_dispatch.proof_size()); + let control_remaining = actor_control_limit + .checked_sub(&authored.metrics.actor_control) + .unwrap_or_else(Weight::zero); + control_remaining_ref_time.push(control_remaining.ref_time()); + control_remaining_proof_size.push(control_remaining.proof_size()); + if !fixture.next_control_maximum.all_lte(control_remaining) { + control_bound_blocks = control_bound_blocks.saturating_add(1); + } + let proof = replay_complete_block_in_wasm( + &authored, + wasm, + &format!("{}-campaign-{block_number}", demand.label()), + ); + execution_storage_proof.push(proof.execution_storage_proof_bytes); + execution_compact_proof.push(proof.execution_compact_proof_bytes); + verification_storage_proof.push(proof.verification_storage_proof_bytes); + verification_compact_proof.push(proof.verification_compact_proof_bytes); + parent = authored.block.header.clone(); + pre_state = authored.post_state; + } + + service.assert_terminal_state(wasm, pre_state); + let service_summary = service.summary(1, W1_TARGET_BLOCKS + 1); + let distinct_progressed = service_summary["distinctProgressedActors"] + .as_u64() + .expect("count is numeric"); + println!( + "EXP_0066_SERVICE_WAITS_V1 {}", + serde_json::json!({ + "workload": workload, "demand": demand.label(), "schedule": workload_schedule.label(), + "workloadActorType": "System", "observations": service_summary, + }) + ); + let step_histogram = steps + .iter() + .fold(BTreeMap::::new(), |mut result, value| { + result + .entry(*value) + .and_modify(|count| *count = count.saturating_add(1)) + .or_insert(1); + result + }); + let histogram = step_histogram + .iter() + .map(|(value, count)| format!("\"{value}\":{count}")) + .collect::>() + .join(","); + let minimum_steps = steps.iter().copied().min().unwrap_or(0); + let maximum_steps = steps.iter().copied().max().unwrap_or(0); + let target_met = total_cycles >= W1_TARGET_CYCLES && minimum_steps >= 100; + println!( + "EXP_0066_{workload}_V5 {{\"demand\":\"{}\",\"schedule\":\"{}\",\"referenceActiveActors\":{initial_active},\"referenceSystemActorIdentities\":{initial_identities},\"addedWorkloadActors\":{added_actor_count},\"activeActors\":{active_actor_count},\"actorIdentities\":{maximum_identities},\"targetCycles\":{W1_TARGET_CYCLES},\"eligibleBlocks\":{W1_TARGET_BLOCKS},\"committedCycles\":{total_cycles},\"successfulTransferSteps\":{total_cycles},\"failedOrSkippedSteps\":0,\"distinctProgressedActors\":{},\"meanSteps\":{:.4},\"p50Steps\":{},\"p95Steps\":{},\"p99Steps\":{},\"minSteps\":{minimum_steps},\"maxSteps\":{maximum_steps},\"stepHistogram\":{{{histogram}}},\"workloadActorType\":\"System\",\"targetMet\":{target_met},\"controlBoundBlocks\":{control_bound_blocks},\"userBoundBlocks\":{user_bound_blocks},\"totalUserCalls\":{},\"meanUserCalls\":{:.4},\"p50UserCalls\":{},\"p95UserCalls\":{},\"p99UserCalls\":{},\"minUserCalls\":{},\"maxUserCalls\":{},\"userRefTimeMin\":{},\"userRefTimeP50\":{},\"userRefTimeP95\":{},\"userRefTimeP99\":{},\"userRefTimeMax\":{},\"userProofSizeMin\":{},\"userProofSizeP50\":{},\"userProofSizeP95\":{},\"userProofSizeP99\":{},\"userProofSizeMax\":{},\"nextControlMaximumRefTime\":{},\"nextControlMaximumProofSize\":{},\"controlRemainingRefTimeMin\":{},\"controlRemainingRefTimeP50\":{},\"controlRemainingRefTimeP95\":{},\"controlRemainingRefTimeP99\":{},\"controlRemainingRefTimeMax\":{},\"controlRemainingProofSizeMin\":{},\"controlRemainingProofSizeP50\":{},\"controlRemainingProofSizeP95\":{},\"controlRemainingProofSizeP99\":{},\"controlRemainingProofSizeMax\":{},\"controlRefTimeMin\":{},\"controlRefTimeP50\":{},\"controlRefTimeP95\":{},\"controlRefTimeP99\":{},\"controlRefTimeMax\":{},\"controlProofSizeMin\":{},\"controlProofSizeP50\":{},\"controlProofSizeP95\":{},\"controlProofSizeP99\":{},\"controlProofSizeMax\":{},\"effectRefTimeMin\":{},\"effectRefTimeP50\":{},\"effectRefTimeP95\":{},\"effectRefTimeP99\":{},\"effectRefTimeMax\":{},\"effectProofSizeMin\":{},\"effectProofSizeP50\":{},\"effectProofSizeP95\":{},\"effectProofSizeP99\":{},\"effectProofSizeMax\":{},\"executionStorageProofMin\":{},\"executionStorageProofP50\":{},\"executionStorageProofP95\":{},\"executionStorageProofP99\":{},\"executionStorageProofMax\":{},\"executionCompactProofMin\":{},\"executionCompactProofP50\":{},\"executionCompactProofP95\":{},\"executionCompactProofP99\":{},\"executionCompactProofMax\":{},\"verificationStorageProofMin\":{},\"verificationStorageProofP50\":{},\"verificationStorageProofP95\":{},\"verificationStorageProofP99\":{},\"verificationStorageProofMax\":{},\"verificationCompactProofMin\":{},\"verificationCompactProofP50\":{},\"verificationCompactProofP95\":{},\"verificationCompactProofP99\":{},\"verificationCompactProofMax\":{}}}", + demand.label(), + workload_schedule.label(), + distinct_progressed, + total_cycles as f64 / f64::from(W1_TARGET_BLOCKS), + nearest_rank_percentile(&steps, 50), + nearest_rank_percentile(&steps, 95), + nearest_rank_percentile(&steps, 99), + user_calls.iter().sum::(), + user_calls.iter().sum::() as f64 / f64::from(W1_TARGET_BLOCKS), + nearest_rank_percentile(&user_calls, 50), + nearest_rank_percentile(&user_calls, 95), + nearest_rank_percentile(&user_calls, 99), + user_calls.iter().copied().min().unwrap_or(0), + user_calls.iter().copied().max().unwrap_or(0), + user_ref_time.iter().copied().min().unwrap_or(0), + nearest_rank_percentile(&user_ref_time, 50), + nearest_rank_percentile(&user_ref_time, 95), + nearest_rank_percentile(&user_ref_time, 99), + user_ref_time.iter().copied().max().unwrap_or(0), + user_proof_size.iter().copied().min().unwrap_or(0), + nearest_rank_percentile(&user_proof_size, 50), + nearest_rank_percentile(&user_proof_size, 95), + nearest_rank_percentile(&user_proof_size, 99), + user_proof_size.iter().copied().max().unwrap_or(0), + fixture.next_control_maximum.ref_time(), + fixture.next_control_maximum.proof_size(), + control_remaining_ref_time + .iter() + .copied() + .min() + .unwrap_or(0), + nearest_rank_percentile(&control_remaining_ref_time, 50), + nearest_rank_percentile(&control_remaining_ref_time, 95), + nearest_rank_percentile(&control_remaining_ref_time, 99), + control_remaining_ref_time + .iter() + .copied() + .max() + .unwrap_or(0), + control_remaining_proof_size + .iter() + .copied() + .min() + .unwrap_or(0), + nearest_rank_percentile(&control_remaining_proof_size, 50), + nearest_rank_percentile(&control_remaining_proof_size, 95), + nearest_rank_percentile(&control_remaining_proof_size, 99), + control_remaining_proof_size + .iter() + .copied() + .max() + .unwrap_or(0), + control_ref_time.iter().copied().min().unwrap_or(0), + nearest_rank_percentile(&control_ref_time, 50), + nearest_rank_percentile(&control_ref_time, 95), + nearest_rank_percentile(&control_ref_time, 99), + control_ref_time.iter().copied().max().unwrap_or(0), + control_proof_size.iter().copied().min().unwrap_or(0), + nearest_rank_percentile(&control_proof_size, 50), + nearest_rank_percentile(&control_proof_size, 95), + nearest_rank_percentile(&control_proof_size, 99), + control_proof_size.iter().copied().max().unwrap_or(0), + effect_ref_time.iter().copied().min().unwrap_or(0), + nearest_rank_percentile(&effect_ref_time, 50), + nearest_rank_percentile(&effect_ref_time, 95), + nearest_rank_percentile(&effect_ref_time, 99), + effect_ref_time.iter().copied().max().unwrap_or(0), + effect_proof_size.iter().copied().min().unwrap_or(0), + nearest_rank_percentile(&effect_proof_size, 50), + nearest_rank_percentile(&effect_proof_size, 95), + nearest_rank_percentile(&effect_proof_size, 99), + effect_proof_size.iter().copied().max().unwrap_or(0), + execution_storage_proof.iter().copied().min().unwrap_or(0), + nearest_rank_percentile(&execution_storage_proof, 50), + nearest_rank_percentile(&execution_storage_proof, 95), + nearest_rank_percentile(&execution_storage_proof, 99), + execution_storage_proof.iter().copied().max().unwrap_or(0), + execution_compact_proof.iter().copied().min().unwrap_or(0), + nearest_rank_percentile(&execution_compact_proof, 50), + nearest_rank_percentile(&execution_compact_proof, 95), + nearest_rank_percentile(&execution_compact_proof, 99), + execution_compact_proof.iter().copied().max().unwrap_or(0), + verification_storage_proof + .iter() + .copied() + .min() + .unwrap_or(0), + nearest_rank_percentile(&verification_storage_proof, 50), + nearest_rank_percentile(&verification_storage_proof, 95), + nearest_rank_percentile(&verification_storage_proof, 99), + verification_storage_proof + .iter() + .copied() + .max() + .unwrap_or(0), + verification_compact_proof + .iter() + .copied() + .min() + .unwrap_or(0), + nearest_rank_percentile(&verification_compact_proof, 50), + nearest_rank_percentile(&verification_compact_proof, 95), + nearest_rank_percentile(&verification_compact_proof, 99), + verification_compact_proof + .iter() + .copied() + .max() + .unwrap_or(0), + ); +} + +fn completed_manual_service_timeline( + signal_block: u32, + step_blocks: &[u32], + completion_block: u32, +) -> serde_json::Value { + let first = *step_blocks.first().expect("nonempty completed Contract"); + assert!( + first > signal_block, + "Manual readiness has a next-block causal floor" + ); + assert_eq!( + step_blocks.last(), + Some(&completion_block), + "completion accompanies the final Step" + ); + let gaps = step_blocks + .windows(2) + .map(|pair| { + assert!( + pair[1] > pair[0], + "Running progress preserves Q1 and chronology" + ); + pair[1] - pair[0] + }) + .collect::>(); + serde_json::json!({ + "signalBlock": signal_block, + "stepBlocks": step_blocks, + "completionBlock": completion_block, + "firstStepLatencyBlocks": first - signal_block, + "completionLatencyBlocks": completion_block - signal_block, + "runningServiceGapBlocks": gaps, + }) +} + +#[test] +fn completed_manual_service_timeline_separates_first_service_completion_and_running() { + let single = completed_manual_service_timeline(1, &[4], 4); + assert_eq!(single["firstStepLatencyBlocks"], 3); + assert_eq!(single["completionLatencyBlocks"], 3); + assert_eq!(single["runningServiceGapBlocks"], serde_json::json!([])); + let running = completed_manual_service_timeline(1, &[4, 9, 11], 11); + assert_eq!(running["firstStepLatencyBlocks"], 3); + assert_eq!(running["completionLatencyBlocks"], 10); + assert_eq!( + running["runningServiceGapBlocks"], + serde_json::json!([5, 2]) + ); +} + +#[test] +#[should_panic(expected = "Q1 and chronology")] +fn completed_manual_service_timeline_rejects_duplicate_block_progress() { + completed_manual_service_timeline(1, &[2, 2], 2); +} + +#[test] +#[should_panic(expected = "completion accompanies the final Step")] +fn completed_manual_service_timeline_rejects_unmatched_completion() { + completed_manual_service_timeline(1, &[2, 3], 4); +} + +fn run_w3_opening_predicate_mixed_length_campaign(wasm: &[u8]) { + let fixture = prepare_w3_fixture(wasm); + let expected_steps = fixture + .actor_profiles + .values() + .map(|profile| profile.step_count) + .sum::(); + assert_eq!(fixture.actor_ids.len(), 126); + assert_eq!(expected_steps, 252); + + let mut pre_state = fixture.storage; + let mut parent = parent_header_for(pre_state.clone(), wasm, 1); + let mut committed_steps = 0u32; + let mut completed_cycles = 0u32; + let mut progressed_actors = BTreeSet::new(); + let mut progressed_step_keys = BTreeSet::new(); + let mut service_blocks = BTreeMap::>::new(); + let mut completion_blocks = BTreeMap::new(); + let mut maximum_service_gap = 0u32; + let mut by_predicates = BTreeMap::::new(); + let mut steps_per_block = Vec::new(); + let mut opening_per_block = Vec::new(); + let mut middle_per_block = Vec::new(); + let mut final_per_block = Vec::new(); + let mut control_ref_time = Vec::new(); + let mut control_proof_size = Vec::new(); + let mut effect_ref_time = Vec::new(); + let mut effect_proof_size = Vec::new(); + let mut execution_storage_proof = Vec::new(); + let mut execution_compact_proof = Vec::new(); + let mut verification_storage_proof = Vec::new(); + let mut verification_compact_proof = Vec::new(); + + for block_number in 2..W3_BLOCK_LIMIT.saturating_add(2) { + let authored = author_complete_block_after( + pre_state, + wasm, + &parent, + block_number, + UserDemand::ActorOnly, + &fixture.signer, + 0, + &fixture.actor_profiles, + ); + assert_eq!(authored.metrics.user_calls, 0); + assert_eq!(authored.metrics.user_dispatch, Weight::zero()); + assert_eq!(authored.metrics.non_successful_steps, 0); + assert_eq!( + authored.metrics.actor_steps, + authored.metrics.distinct_actors + ); + assert!( + authored.metrics.actor_steps > 0, + "W3 must make workload progress until its bounded matrix completes" + ); + assert_eq!( + authored.metrics.transfer_steps, + authored.metrics.actor_steps + ); + assert!( + authored.metrics.closed_actors.is_empty() + && authored.metrics.suspended_steps.is_empty() + && authored.metrics.continued_steps.is_empty() + && authored.metrics.paused_actors.is_empty() + && authored.metrics.resumed_actors.is_empty() + && authored.metrics.failed_cycle_actors.is_empty(), + "W3 service gaps exclude lifecycle interruptions and retries" + ); + for actor_id in &authored.metrics.completed_cycle_actors { + assert!( + completion_blocks.insert(*actor_id, block_number).is_none(), + "each Manual matrix Actor completes exactly once" + ); + } + for (actor_id, step_index) in &authored.metrics.progressed_steps { + assert!( + progressed_step_keys.insert((*actor_id, *step_index)), + "Manual-only W3 executes each matrix Step exactly once" + ); + progressed_actors.insert(*actor_id); + let blocks = service_blocks.entry(*actor_id).or_default(); + assert_eq!( + *step_index as usize, + blocks.len(), + "W3 observes every cursor in order" + ); + if let Some(previous) = blocks.last() { + assert!(block_number > *previous, "W3 Running progress preserves Q1"); + maximum_service_gap = maximum_service_gap.max(block_number - *previous); + } + blocks.push(block_number); + let profile = fixture + .actor_profiles + .get(actor_id) + .expect("progressed W3 Actor has a matrix profile"); + let counts = by_predicates + .entry(profile.opening_predicates_per_step) + .or_default(); + counts.0 = counts.0.saturating_add(1); + if *step_index == 0 { + counts.1 = counts.1.saturating_add(1); + } else if step_index.saturating_add(1) == profile.step_count { + counts.3 = counts.3.saturating_add(1); + } else { + counts.2 = counts.2.saturating_add(1); + } + } + committed_steps = committed_steps.saturating_add(authored.metrics.actor_steps); + completed_cycles = completed_cycles.saturating_add(authored.metrics.completed_cycles); + steps_per_block.push(u64::from(authored.metrics.actor_steps)); + opening_per_block.push(u64::from(authored.metrics.opening_steps)); + middle_per_block.push(u64::from(authored.metrics.middle_steps)); + final_per_block.push(u64::from(authored.metrics.final_steps)); + control_ref_time.push(authored.metrics.actor_control.ref_time()); + control_proof_size.push(authored.metrics.actor_control.proof_size()); + effect_ref_time.push(authored.metrics.actor_effect.ref_time()); + effect_proof_size.push(authored.metrics.actor_effect.proof_size()); + let proof = replay_complete_block_in_wasm( + &authored, + wasm, + &format!("W3-opening-predicate-mixed-length-{block_number}"), + ); + execution_storage_proof.push(proof.execution_storage_proof_bytes); + execution_compact_proof.push(proof.execution_compact_proof_bytes); + verification_storage_proof.push(proof.verification_storage_proof_bytes); + verification_compact_proof.push(proof.verification_compact_proof_bytes); + parent = authored.block.header.clone(); + pre_state = authored.post_state; + if committed_steps == expected_steps { + break; + } + assert!( + committed_steps < expected_steps, + "W3 cannot overrun its matrix" + ); + } + + assert_eq!( + committed_steps, expected_steps, + "W3 completes within its block limit" + ); + assert_eq!(completed_cycles, fixture.actor_ids.len() as u32); + assert_eq!(progressed_actors.len(), fixture.actor_ids.len()); + assert_eq!(progressed_step_keys.len() as u32, expected_steps); + assert_eq!(completion_blocks.len(), fixture.actor_ids.len()); + for actor_id in &fixture.actor_ids { + let profile = &fixture.actor_profiles[actor_id]; + let blocks = &service_blocks[actor_id]; + assert_eq!(blocks.len(), profile.step_count as usize); + println!( + "EXP_0066_W3_SERVICE_V1 {}", + serde_json::json!({ + "actorId": actor_id, + "actorType": "System", + "contractSteps": profile.step_count, + "openingPredicatesPerStep": profile.opening_predicates_per_step, + "timeline": completed_manual_service_timeline(1, blocks, completion_blocks[actor_id]), + }) + ); + } + for predicates in [0, 2, 4] { + assert_eq!( + by_predicates.get(&predicates), + Some(&(84, 42, 14, 28)), + "each predicate cohort preserves the same 1/2/3-Step matrix" + ); + } + let opening_steps = opening_per_block.iter().sum::(); + let middle_steps = middle_per_block.iter().sum::(); + let final_steps = final_per_block.iter().sum::(); + assert_eq!((opening_steps, middle_steps, final_steps), (126, 42, 84)); + let measured_blocks = steps_per_block.len(); + let step_histogram = steps_per_block + .iter() + .fold(BTreeMap::::new(), |mut result, value| { + result + .entry(*value) + .and_modify(|count| *count = count.saturating_add(1)) + .or_insert(1); + result + }) + .iter() + .map(|(value, count)| format!("\"{value}\":{count}")) + .collect::>() + .join(","); + println!( + "EXP_0066_W3_V1 {{\"schedule\":\"manual-only\",\"matrixActors\":{},\"repetitionsPerCell\":{W3_REPETITIONS_PER_CELL},\"contractLengths\":[1,2,3],\"openingPredicatesPerStep\":[0,2,4],\"measuredBlocks\":{measured_blocks},\"committedSteps\":{committed_steps},\"completedCycles\":{completed_cycles},\"distinctProgressedActors\":{},\"openingSteps\":{opening_steps},\"middleSteps\":{middle_steps},\"finalSteps\":{final_steps},\"maximumServiceGapBlocks\":{maximum_service_gap},\"meanSteps\":{:.4},\"p50Steps\":{},\"p95Steps\":{},\"p99Steps\":{},\"minSteps\":{},\"maxSteps\":{},\"stepHistogram\":{{{step_histogram}}},\"controlRefTimeMin\":{},\"controlRefTimeP50\":{},\"controlRefTimeP95\":{},\"controlRefTimeMax\":{},\"controlProofSizeMin\":{},\"controlProofSizeP50\":{},\"controlProofSizeP95\":{},\"controlProofSizeMax\":{},\"effectRefTimeMin\":{},\"effectRefTimeP50\":{},\"effectRefTimeP95\":{},\"effectRefTimeMax\":{},\"effectProofSizeMin\":{},\"effectProofSizeP50\":{},\"effectProofSizeP95\":{},\"effectProofSizeMax\":{},\"executionStorageProofMin\":{},\"executionStorageProofP50\":{},\"executionStorageProofP95\":{},\"executionStorageProofMax\":{},\"executionCompactProofMin\":{},\"executionCompactProofP50\":{},\"executionCompactProofP95\":{},\"executionCompactProofMax\":{},\"verificationStorageProofMin\":{},\"verificationStorageProofP50\":{},\"verificationStorageProofP95\":{},\"verificationStorageProofMax\":{},\"verificationCompactProofMin\":{},\"verificationCompactProofP50\":{},\"verificationCompactProofP95\":{},\"verificationCompactProofMax\":{}}}", + fixture.actor_ids.len(), + progressed_actors.len(), + f64::from(committed_steps) / measured_blocks as f64, + nearest_rank_percentile(&steps_per_block, 50), + nearest_rank_percentile(&steps_per_block, 95), + nearest_rank_percentile(&steps_per_block, 99), + steps_per_block.iter().copied().min().unwrap_or(0), + steps_per_block.iter().copied().max().unwrap_or(0), + control_ref_time.iter().copied().min().unwrap_or(0), + nearest_rank_percentile(&control_ref_time, 50), + nearest_rank_percentile(&control_ref_time, 95), + control_ref_time.iter().copied().max().unwrap_or(0), + control_proof_size.iter().copied().min().unwrap_or(0), + nearest_rank_percentile(&control_proof_size, 50), + nearest_rank_percentile(&control_proof_size, 95), + control_proof_size.iter().copied().max().unwrap_or(0), + effect_ref_time.iter().copied().min().unwrap_or(0), + nearest_rank_percentile(&effect_ref_time, 50), + nearest_rank_percentile(&effect_ref_time, 95), + effect_ref_time.iter().copied().max().unwrap_or(0), + effect_proof_size.iter().copied().min().unwrap_or(0), + nearest_rank_percentile(&effect_proof_size, 50), + nearest_rank_percentile(&effect_proof_size, 95), + effect_proof_size.iter().copied().max().unwrap_or(0), + execution_storage_proof.iter().copied().min().unwrap_or(0), + nearest_rank_percentile(&execution_storage_proof, 50), + nearest_rank_percentile(&execution_storage_proof, 95), + execution_storage_proof.iter().copied().max().unwrap_or(0), + execution_compact_proof.iter().copied().min().unwrap_or(0), + nearest_rank_percentile(&execution_compact_proof, 50), + nearest_rank_percentile(&execution_compact_proof, 95), + execution_compact_proof.iter().copied().max().unwrap_or(0), + verification_storage_proof + .iter() + .copied() + .min() + .unwrap_or(0), + nearest_rank_percentile(&verification_storage_proof, 50), + nearest_rank_percentile(&verification_storage_proof, 95), + verification_storage_proof + .iter() + .copied() + .max() + .unwrap_or(0), + verification_compact_proof + .iter() + .copied() + .min() + .unwrap_or(0), + nearest_rank_percentile(&verification_compact_proof, 50), + nearest_rank_percentile(&verification_compact_proof, 95), + verification_compact_proof + .iter() + .copied() + .max() + .unwrap_or(0), + ); +} + +#[test] +fn one_step_service_trace_tracks_heterogeneous_outcomes() { + let fixture = prepare_w4_fixture(&[]); + let mut service = + OneStepServiceTrace::for_schedule(&fixture.actor_ids, WorkloadSchedule::MixedManualCadenced); + let mut pre_state = fixture.storage; + let mut parent = parent_header_for(pre_state.clone(), &[], 1); + for block in 2..=4 { + let authored = author_complete_block_after( + pre_state, + &[], + &parent, + block, + UserDemand::ActorOnly, + &fixture.signer, + 0, + &fixture.actor_profiles, + ); + service.observe(&authored.metrics, block); + parent = authored.block.header; + pre_state = authored.post_state; + } + service.assert_terminal_state(&[], pre_state); + let mut samples = 0; + for task in [ + WorkloadTask::Transfer, + WorkloadTask::RouterSwapOut, + WorkloadTask::StopCycle, + ] { + let summary = service.summary_for(1, 4, |id| fixture.actor_profiles[&id].task == Some(task)); + let count = summary["materializedSignalToStepBlocks"]["samples"] + .as_u64() + .unwrap(); + assert!( + count > 0, + "each W4 Task family contributes real successful service" + ); + assert!(summary["interStepGapBlocks"]["max"].is_null()); + samples += count; + } + assert_eq!( + service.summary(1, 4)["materializedSignalToStepBlocks"]["samples"], + samples + ); +} + +fn run_w4_heterogeneous_effect_campaign(wasm: &[u8], demand: UserDemand) { + let fixture = prepare_w4_fixture(wasm); + let population_counts = fixture.actor_profiles.values().fold( + (0u32, 0u32, 0u32), + |(transfer, swap_out, stop_cycle), profile| match profile.task { + Some(WorkloadTask::Transfer) => (transfer.saturating_add(1), swap_out, stop_cycle), + Some(WorkloadTask::RouterSwapOut) => (transfer, swap_out.saturating_add(1), stop_cycle), + Some(WorkloadTask::StopCycle) => (transfer, swap_out, stop_cycle.saturating_add(1)), + None => panic!("W4 profiles declare one exact task"), + }, + ); + assert_eq!( + population_counts, + (W4_TRANSFER_ACTORS, W4_SWAP_OUT_ACTORS, W4_STOP_CYCLE_ACTORS) + ); + + let mut pre_state = fixture.storage; + let mut parent = parent_header_for(pre_state.clone(), wasm, 1); + let mut signer_nonce = 0; + let mut progressed = BTreeSet::new(); + let mut service = + OneStepServiceTrace::for_schedule(&fixture.actor_ids, WorkloadSchedule::MixedManualCadenced); + let mut total_cycles = 0u32; + let mut transfer_steps = 0u32; + let mut swap_out_steps = 0u32; + let mut stop_cycle_steps = 0u32; + let mut user_bound_blocks = 0u32; + let mut steps = Vec::with_capacity(W1_TARGET_BLOCKS as usize); + let mut control_ref_time = Vec::with_capacity(W1_TARGET_BLOCKS as usize); + let mut control_proof_size = Vec::with_capacity(W1_TARGET_BLOCKS as usize); + let mut effect_ref_time = Vec::with_capacity(W1_TARGET_BLOCKS as usize); + let mut effect_proof_size = Vec::with_capacity(W1_TARGET_BLOCKS as usize); + let mut user_calls = Vec::with_capacity(W1_TARGET_BLOCKS as usize); + let mut execution_storage_proof = Vec::with_capacity(W1_TARGET_BLOCKS as usize); + let mut execution_compact_proof = Vec::with_capacity(W1_TARGET_BLOCKS as usize); + let mut verification_storage_proof = Vec::with_capacity(W1_TARGET_BLOCKS as usize); + let mut verification_compact_proof = Vec::with_capacity(W1_TARGET_BLOCKS as usize); + + for block_number in 2..W1_TARGET_BLOCKS.saturating_add(2) { + let authored = author_complete_block_after( + pre_state, + wasm, + &parent, + block_number, + demand, + &fixture.signer, + signer_nonce, + &fixture.actor_profiles, + ); + assert_eq!(authored.metrics.non_successful_steps, 0); + assert_eq!( + authored.metrics.actor_steps, authored.metrics.distinct_actors, + "W4 preserves Q1 in every full block" + ); + assert!( + authored.metrics.queue_head < authored.metrics.queue_tail, + "W4 retains pending FIFO work in block {block_number}" + ); + match demand { + UserDemand::ActorOnly => { + assert_eq!(authored.metrics.user_calls, 0); + assert_eq!(authored.metrics.user_dispatch, Weight::zero()); + assert!(authored.metrics.next_user_weight.is_none()); + } + UserDemand::ContinuousValid => { + assert!(authored.metrics.user_calls > 0); + let next_user = authored + .metrics + .next_user_weight + .expect("continuous W4 demand records its first inadmissible valid call"); + let remaining = BlockResourceBudgetValue::get() + .limits() + .user_base_turn() + .checked_sub(&authored.metrics.user_dispatch) + .unwrap_or_else(Weight::zero); + assert!( + !next_user.all_lte(remaining), + "continuous W4 demand reaches the User base-turn frontier" + ); + user_bound_blocks = user_bound_blocks.saturating_add(1); + } + UserDemand::RefTimeHeavy => { + unreachable!("the W4 campaign does not prepare signed Router demand") + } + } + + total_cycles = total_cycles.saturating_add(authored.metrics.completed_cycles); + transfer_steps = transfer_steps.saturating_add(authored.metrics.transfer_steps); + swap_out_steps = swap_out_steps.saturating_add(authored.metrics.swap_out_steps); + stop_cycle_steps = stop_cycle_steps.saturating_add(authored.metrics.stop_cycle_steps); + service.observe(&authored.metrics, block_number); + for (actor_id, _) in &authored.metrics.progressed_steps { + progressed.insert(*actor_id); + } + steps.push(u64::from(authored.metrics.actor_steps)); + control_ref_time.push(authored.metrics.actor_control.ref_time()); + control_proof_size.push(authored.metrics.actor_control.proof_size()); + effect_ref_time.push(authored.metrics.actor_effect.ref_time()); + effect_proof_size.push(authored.metrics.actor_effect.proof_size()); + signer_nonce = signer_nonce.saturating_add(authored.metrics.user_calls); + user_calls.push(u64::from(authored.metrics.user_calls)); + let proof = replay_complete_block_in_wasm( + &authored, + wasm, + &format!("W4-heterogeneous-{}-{block_number}", demand.label()), + ); + execution_storage_proof.push(proof.execution_storage_proof_bytes); + execution_compact_proof.push(proof.execution_compact_proof_bytes); + verification_storage_proof.push(proof.verification_storage_proof_bytes); + verification_compact_proof.push(proof.verification_compact_proof_bytes); + parent = authored.block.header.clone(); + pre_state = authored.post_state; + } + + service.assert_terminal_state(wasm, pre_state); + for (task, label) in [ + (WorkloadTask::Transfer, "transfer"), + (WorkloadTask::RouterSwapOut, "swap-out"), + (WorkloadTask::StopCycle, "stop-cycle"), + ] { + println!( + "EXP_0066_W4_SERVICE_V1 {}", + serde_json::json!({ + "demand": demand.label(), "task": label, "actorType": "System", + "observations": service.summary_for(1, W1_TARGET_BLOCKS + 1, |id| fixture.actor_profiles[&id].task == Some(task)), + }) + ); + } + let committed_steps = transfer_steps + .saturating_add(swap_out_steps) + .saturating_add(stop_cycle_steps); + assert_eq!(committed_steps, steps.iter().sum::() as u32); + assert_eq!(total_cycles, committed_steps); + assert!(transfer_steps > 0, "W4 commits Transfer effects"); + assert!(swap_out_steps > 0, "W4 commits Router SwapOut effects"); + assert!( + stop_cycle_steps > 0, + "W4 commits StopCycle control outcomes" + ); + assert_eq!( + user_bound_blocks, + if demand == UserDemand::ContinuousValid { + W1_TARGET_BLOCKS + } else { + 0 + } + ); + let minimum_steps = steps.iter().copied().min().unwrap_or(0); + let target_met = committed_steps >= W1_TARGET_CYCLES && minimum_steps >= 100; + println!( + "EXP_0066_W4_V2 {{\"demand\":\"{}\",\"schedule\":\"mixed-manual-cadenced\",\"referenceSystemActorIdentities\":{REFERENCE_SYSTEM_ACTOR_IDENTITIES},\"nominalTransferSlots\":9500,\"retainedReferenceSlots\":15,\"workloadTransferActors\":{W4_TRANSFER_ACTORS},\"swapOutActors\":{W4_SWAP_OUT_ACTORS},\"stopCycleActors\":{W4_STOP_CYCLE_ACTORS},\"eligibleBlocks\":{W1_TARGET_BLOCKS},\"committedSteps\":{committed_steps},\"completedCycles\":{total_cycles},\"transferSteps\":{transfer_steps},\"swapOutSteps\":{swap_out_steps},\"stopCycleSteps\":{stop_cycle_steps},\"distinctProgressedActors\":{},\"workloadActorType\":\"System\",\"meanSteps\":{:.4},\"p50Steps\":{},\"p95Steps\":{},\"p99Steps\":{},\"minSteps\":{minimum_steps},\"maxSteps\":{},\"targetMet\":{target_met},\"userBoundBlocks\":{user_bound_blocks},\"totalUserCalls\":{},\"nextControlMaximumRefTime\":{},\"nextControlMaximumProofSize\":{},\"nextEffectMaximumRefTime\":{},\"nextEffectMaximumProofSize\":{},\"controlRefTimeMin\":{},\"controlRefTimeP50\":{},\"controlRefTimeP95\":{},\"controlRefTimeMax\":{},\"controlProofSizeMin\":{},\"controlProofSizeP50\":{},\"controlProofSizeP95\":{},\"controlProofSizeMax\":{},\"effectRefTimeMin\":{},\"effectRefTimeP50\":{},\"effectRefTimeP95\":{},\"effectRefTimeMax\":{},\"effectProofSizeMin\":{},\"effectProofSizeP50\":{},\"effectProofSizeP95\":{},\"effectProofSizeMax\":{},\"executionStorageProofMin\":{},\"executionStorageProofP50\":{},\"executionStorageProofP95\":{},\"executionStorageProofMax\":{},\"executionCompactProofMin\":{},\"executionCompactProofP50\":{},\"executionCompactProofP95\":{},\"executionCompactProofMax\":{},\"verificationStorageProofMin\":{},\"verificationStorageProofP50\":{},\"verificationStorageProofP95\":{},\"verificationStorageProofMax\":{},\"verificationCompactProofMin\":{},\"verificationCompactProofP50\":{},\"verificationCompactProofP95\":{},\"verificationCompactProofMax\":{}}}", + demand.label(), + progressed.len(), + f64::from(committed_steps) / f64::from(W1_TARGET_BLOCKS), + nearest_rank_percentile(&steps, 50), + nearest_rank_percentile(&steps, 95), + nearest_rank_percentile(&steps, 99), + steps.iter().copied().max().unwrap_or(0), + user_calls.iter().sum::(), + fixture.next_control_maximum.ref_time(), + fixture.next_control_maximum.proof_size(), + fixture.next_effect_maximum.ref_time(), + fixture.next_effect_maximum.proof_size(), + control_ref_time.iter().copied().min().unwrap_or(0), + nearest_rank_percentile(&control_ref_time, 50), + nearest_rank_percentile(&control_ref_time, 95), + control_ref_time.iter().copied().max().unwrap_or(0), + control_proof_size.iter().copied().min().unwrap_or(0), + nearest_rank_percentile(&control_proof_size, 50), + nearest_rank_percentile(&control_proof_size, 95), + control_proof_size.iter().copied().max().unwrap_or(0), + effect_ref_time.iter().copied().min().unwrap_or(0), + nearest_rank_percentile(&effect_ref_time, 50), + nearest_rank_percentile(&effect_ref_time, 95), + effect_ref_time.iter().copied().max().unwrap_or(0), + effect_proof_size.iter().copied().min().unwrap_or(0), + nearest_rank_percentile(&effect_proof_size, 50), + nearest_rank_percentile(&effect_proof_size, 95), + effect_proof_size.iter().copied().max().unwrap_or(0), + execution_storage_proof.iter().copied().min().unwrap_or(0), + nearest_rank_percentile(&execution_storage_proof, 50), + nearest_rank_percentile(&execution_storage_proof, 95), + execution_storage_proof.iter().copied().max().unwrap_or(0), + execution_compact_proof.iter().copied().min().unwrap_or(0), + nearest_rank_percentile(&execution_compact_proof, 50), + nearest_rank_percentile(&execution_compact_proof, 95), + execution_compact_proof.iter().copied().max().unwrap_or(0), + verification_storage_proof + .iter() + .copied() + .min() + .unwrap_or(0), + nearest_rank_percentile(&verification_storage_proof, 50), + nearest_rank_percentile(&verification_storage_proof, 95), + verification_storage_proof + .iter() + .copied() + .max() + .unwrap_or(0), + verification_compact_proof + .iter() + .copied() + .min() + .unwrap_or(0), + nearest_rank_percentile(&verification_compact_proof, 50), + nearest_rank_percentile(&verification_compact_proof, 95), + verification_compact_proof + .iter() + .copied() + .max() + .unwrap_or(0), + ); +} + +fn actor_lifecycle_event_label(event: &Event) -> Option<(ActorId, &'static str)> { + let (actor_id, kind) = match event { + Event::CycleStarted { actor_id, .. } => (*actor_id, "CycleStarted"), + Event::CycleSummary { actor_id, .. } => (*actor_id, "CycleSummary"), + Event::CycleSuspended { actor_id, .. } => (*actor_id, "CycleSuspended"), + Event::CycleContinued { actor_id, .. } => (*actor_id, "CycleContinued"), + Event::CycleCancelled { actor_id, .. } => (*actor_id, "CycleCancelled"), + Event::CycleStopped { actor_id, .. } => (*actor_id, "CycleStopped"), + Event::StepSkipped { actor_id, .. } => (*actor_id, "StepSkipped"), + Event::StepFailed { actor_id, .. } => (*actor_id, "StepFailed"), + Event::TransferExecuted { actor_id, .. } => (*actor_id, "TransferExecuted"), + Event::SwapExecuted { actor_id, .. } => (*actor_id, "SwapExecuted"), + Event::ActorPaused { actor_id } => (*actor_id, "ActorPaused"), + Event::ActorResumed { actor_id } => (*actor_id, "ActorResumed"), + Event::ActorClosed { actor_id, .. } => (*actor_id, "ActorClosed"), + Event::ContractUpdated { actor_id } => (*actor_id, "ContractUpdated"), + Event::ManualTriggerSet { actor_id } => (*actor_id, "ManualTriggerSet"), + Event::TriggerOccurrenceProcessed { actor_id, .. } => (*actor_id, "TriggerOccurrenceProcessed"), + _ => return None, + }; + Some((actor_id, kind)) +} + +fn actor_lifecycle_observation( + wasm: &[u8], + storage: &Storage, + actor_ids: &[ActorId], +) -> serde_json::Value { + let mut ext = TestExternalities::new_with_code_and_state( + wasm, + storage.clone(), + crate::VERSION.state_version(), + ); + ext.execute_with(|| { + let events = System::events().into_iter().enumerate().filter_map(|(ordinal, record)| { + let RuntimeEvent::Actors(event) = record.event else { return None; }; + let (actor_id, kind) = actor_lifecycle_event_label(&event)?; + actor_ids.contains(&actor_id).then(|| serde_json::json!({ + "ordinal": ordinal, "actorId": actor_id, "kind": kind, + "phase": format!("{:?}", record.phase), "details": format!("{event:?}"), + })) + }).collect::>(); + let actors = actor_ids.iter().map(|actor_id| { + let Some(state) = Actors::active_actor_state(*actor_id) else { + return serde_json::json!({"actorId": actor_id, "active": false}); + }; + serde_json::json!({ + "actorId": actor_id, "active": true, + "actorType": format!("{:?}", state.identity.actor_class.actor_type()), + "trigger": format!("{:?}", state.contract.trigger), + "temporalAnchorTick": state.hot.trigger_runtime_state.temporal_anchor_tick(), + "atTimeConsumed": matches!(&state.contract.trigger, Trigger::AtTime { .. }) + .then(|| state.hot.trigger_runtime_state.temporal_occurrence_consumed()), + "crossingGeneration": matches!(&state.contract.trigger, Trigger::ObservationCrossing { .. }) + .then(|| Actors::crossing_membership(*actor_id).map(|membership| membership.generation)).flatten(), + "window": state.contract.window.map(|w| serde_json::json!({"start": w.start, "end": w.end})), + "lifecycle": format!("{:?}", state.hot.lifecycle), + "cycleState": format!("{:?}", state.hot.cycle_state), + "pendingSignal": state.hot.pending_signal, "queueTicket": state.hot.queue_ticket, + "wakeup": state.hot.wakeup_pointer.map(|p| format!("{:?}", p.block)), + "triggerWakeupTick": state.hot.trigger_wakeup_pointer.map(|p| p.tick), + "run": state.run_state.map(|run| serde_json::json!({ + "cycleNonce": run.cycle_nonce, "cursor": run.cursor, + "eligibleAt": run.eligible_at, "lastAttemptBlock": run.last_attempt_block, + "lastCommittedStepBlock": run.last_committed_step_block, + "unsuccessfulAttemptsAtCursor": run.unsuccessful_attempts_at_cursor, + "suspension": run.suspension.map(|reason| format!("{reason:?}")), + })), + }) + }).collect::>(); + serde_json::json!({ + "block": System::block_number(), "timestampMillis": crate::Timestamp::get(), + "cadenceTickMillis": ::CadenceTickMillis::get(), + "events": events, "actors": actors, + }) + }) +} + +#[test] +fn actor_lifecycle_observation_preserves_event_positions_and_filters_population() { + let mut ext = TestExternalities::new_with_code_and_state( + &[], + reference_genesis_storage(&[]), + crate::VERSION.state_version(), + ); + let storage = ext.execute_with(|| { + System::set_block_number(1); + System::reset_events(); + System::deposit_event(RuntimeEvent::Actors(Event::CycleStarted { + actor_id: 15, + cycle_nonce: 1, + })); + System::deposit_event(RuntimeEvent::Actors(Event::CycleStarted { + actor_id: 16, + cycle_nonce: 1, + })); + System::deposit_event(RuntimeEvent::Actors(Event::ActorClosed { + actor_id: 15, + reason: CloseReason::OwnerInitiated, + })); + current_top_storage() + }); + let observation = actor_lifecycle_observation(&[], &storage, &[15]); + let events = observation["events"].as_array().unwrap(); + assert_eq!(events.len(), 2); + assert_eq!(events[0]["ordinal"], 0); + assert_eq!(events[0]["kind"], "CycleStarted"); + assert_eq!(events[1]["ordinal"], 2); + assert_eq!(events[1]["kind"], "ActorClosed"); + assert_eq!(observation["actors"][0]["active"], false); +} + +#[test] +fn full_executive_funded_pipeline_collection_witness() { + use pallet_deos_actors::ActorType; + use polkadot_sdk::frame_support::traits::Get; + let signer = sr25519::Pair::from_seed(&[66u8; 32]); + let mut rows = Vec::new(); + for actor_type in [ActorType::System, ActorType::User] { + let mut ext = TestExternalities::new_with_code_and_state( + &[], + reference_genesis_storage(&[]), + crate::VERSION.state_version(), + ); + let (actor_id, payer, trigger_fee, payer_after_trigger, sink_after_trigger) = + ext.execute_with(|| { + System::set_block_number(1); + assert_ok!(Balances::force_set_balance( + RuntimeOrigin::root(), + MultiAddress::Id(ALICE), + u128::MAX / 4 + )); + let steps = BoundedVec::try_from(vec![StepOf:: { + precondition: None, + task: Task::StopCycle, + on_error: StepErrorPolicy::AbortCycle, + }]) + .unwrap(); + let actor_id = match actor_type { + ActorType::System => actors_integration_tests::create_system( + ALICE, + actors_integration_tests::manual_schedule(), + None, + steps, + ), + ActorType::User => actors_integration_tests::create_user( + ALICE, + actors_integration_tests::manual_schedule(), + None, + steps, + ), + }; + actors_integration_tests::fund_native(actor_id, 1_000 * crate::EXISTENTIAL_DEPOSIT); + let payer = Actors::active_actor_state(actor_id) + .unwrap() + .identity + .sovereign_account; + let sink = ::FeeSink::get(); + let payer_before = Balances::free_balance(&payer); + let sink_before = Balances::free_balance(&sink); + assert_ok!(Actors::manual_trigger( + match actor_type { + ActorType::System => RuntimeOrigin::root(), + ActorType::User => RuntimeOrigin::signed(ALICE), + }, + actor_id + )); + let fees = System::events() + .into_iter() + .filter_map(|record| match record.event { + RuntimeEvent::Actors(Event::TriggerOccurrenceProcessed { + actor_id: id, fee, .. + }) if id == actor_id => Some(fee), + _ => None, + }) + .collect::>(); + assert_eq!(fees.len(), 1); + let payer_after = Balances::free_balance(&payer); + let sink_after = Balances::free_balance(&sink); + assert_eq!(payer_before - payer_after, fees[0]); + assert_eq!(sink_after - sink_before, fees[0]); + (actor_id, payer, fees[0], payer_after, sink_after) + }); + ext.commit_all().unwrap(); + let storage = ext.execute_with(current_top_storage); + let profiles = BTreeMap::from([( + actor_id, + WorkloadActorProfile { + step_count: 1, + opening_predicates_per_step: 0, + task: Some(WorkloadTask::StopCycle), + }, + )]); + let authored = + author_complete_block(storage, &[], 2, UserDemand::ActorOnly, &signer, &profiles); + assert_eq!(authored.metrics.stop_cycle_steps, 1); + assert_eq!(authored.metrics.completed_cycles, 1); + assert_eq!(authored.metrics.actor_effect, Weight::zero()); + let mut after = TestExternalities::new_with_code_and_state( + &[], + authored.post_state, + crate::VERSION.state_version(), + ); + let pipeline_fee = after.execute_with(|| { + let mut pipeline = Vec::new(); + let mut action_receipts = 0; + for record in System::events() { + match record.event { + RuntimeEvent::Actors(Event::PipelineFeeCharged { actor_id: id, fee }) + if id == actor_id => + { + pipeline.push(fee) + } + RuntimeEvent::Actors(Event::ActionFeeCharged { actor_id: id, .. }) if id == actor_id => { + action_receipts += 1 + } + _ => {} + } + } + assert_eq!(action_receipts, 0, "StopCycle does not invoke an Action"); + match actor_type { + ActorType::System => { + assert!(pipeline.is_empty()); + assert_eq!(trigger_fee, 0); + } + ActorType::User => { + assert_eq!(pipeline.len(), 1); + assert!(pipeline[0] > 0); + assert!(trigger_fee > 0); + } + } + let fee = pipeline.into_iter().sum::(); + assert_eq!(payer_after_trigger - Balances::free_balance(&payer), fee); + assert_eq!( + Balances::free_balance(::FeeSink::get()) + - sink_after_trigger, + fee + ); + fee + }); + rows.push(serde_json::json!({ + "actorType": format!("{actor_type:?}"), "triggerFee": trigger_fee.to_string(), "pipelineFee": pipeline_fee.to_string(), + "control": [authored.metrics.actor_control.ref_time(), authored.metrics.actor_control.proof_size()], + "prepassControl": [authored.metrics.prepass_actor_control.ref_time(), authored.metrics.prepass_actor_control.proof_size()], + "effect": [authored.metrics.actor_effect.ref_time(), authored.metrics.actor_effect.proof_size()], + })); + } + println!( + "ACTOR_PIPELINE_COLLECTION_WITNESS_V1 {}", + serde_json::json!({ + "mode": "native-full-executive-monetary-ledger", "task": "StopCycle", "rows": rows, + }) + ); +} + +fn run_funded_action_collection_witness(wasm: &[u8], replay_wasm: bool) { + use polkadot_sdk::frame_support::traits::Get; + use primitives::AssetKind; + + let recipient = super::common::BOB; + let signer = sr25519::Pair::from_seed(&[66u8; 32]); + let mut ext = TestExternalities::new_with_code_and_state( + wasm, + reference_genesis_storage(wasm), + crate::VERSION.state_version(), + ); + let (actor_id, payer, trigger_fee, payer_after_trigger, sink_after_trigger, recipient_before) = + ext.execute_with(|| { + System::set_block_number(1); + assert_ok!(Balances::force_set_balance( + RuntimeOrigin::root(), + MultiAddress::Id(ALICE), + u128::MAX / 4 + )); + let steps = BoundedVec::try_from(vec![StepOf:: { + precondition: None, + task: Task::Transfer { + to: recipient.clone(), + asset: AssetKind::Native, + amount: AmountResolution::Fixed(crate::EXISTENTIAL_DEPOSIT), + }, + on_error: StepErrorPolicy::AbortCycle, + }]) + .unwrap(); + let actor_id = actors_integration_tests::create_user( + ALICE, + actors_integration_tests::manual_schedule(), + None, + steps, + ); + actors_integration_tests::fund_native(actor_id, 1_000 * crate::EXISTENTIAL_DEPOSIT); + let payer = Actors::active_actor_state(actor_id) + .unwrap() + .identity + .sovereign_account; + let sink = ::FeeSink::get(); + let payer_before = Balances::free_balance(&payer); + let sink_before = Balances::free_balance(&sink); + let recipient_before = Balances::free_balance(&recipient); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + let trigger_fee = System::events() + .into_iter() + .find_map(|record| match record.event { + RuntimeEvent::Actors(Event::TriggerOccurrenceProcessed { + actor_id: id, fee, .. + }) if id == actor_id => Some(fee), + _ => None, + }) + .expect("funded User trigger fee"); + assert!(trigger_fee > 0); + assert_eq!(payer_before - Balances::free_balance(&payer), trigger_fee); + assert_eq!(Balances::free_balance(&sink) - sink_before, trigger_fee); + ( + actor_id, + payer.clone(), + trigger_fee, + Balances::free_balance(&payer), + Balances::free_balance(&sink), + recipient_before, + ) + }); + ext.commit_all().unwrap(); + let storage = ext.execute_with(current_top_storage); + let profiles = BTreeMap::from([( + actor_id, + WorkloadActorProfile { + step_count: 1, + opening_predicates_per_step: 0, + task: Some(WorkloadTask::Transfer), + }, + )]); + let authored = author_complete_block(storage, wasm, 2, UserDemand::ActorOnly, &signer, &profiles); + assert_eq!(authored.metrics.transfer_steps, 1); + assert_eq!(authored.metrics.completed_cycles, 1); + let proof = replay_wasm + .then(|| replay_complete_block_in_wasm(&authored, wasm, "funded-user-action-collection")); + let mut after = TestExternalities::new_with_code_and_state( + wasm, + authored.post_state, + crate::VERSION.state_version(), + ); + let (pipeline_fee, action_fee) = after.execute_with(|| { + let mut pipeline = Vec::new(); + let mut action = Vec::new(); + for record in System::events() { + match record.event { + RuntimeEvent::Actors(Event::PipelineFeeCharged { actor_id: id, fee }) if id == actor_id => { + pipeline.push(fee) + } + RuntimeEvent::Actors(Event::ActionFeeCharged { + actor_id: id, fee, .. + }) if id == actor_id => action.push(fee), + _ => {} + } + } + assert_eq!(pipeline.len(), 1); + assert_eq!(action.len(), 1); + assert!(pipeline[0] > 0 && action[0] > 0); + let total_fees = pipeline[0] + action[0]; + assert_eq!( + payer_after_trigger - Balances::free_balance(&payer), + crate::EXISTENTIAL_DEPOSIT + total_fees + ); + assert_eq!( + Balances::free_balance(::FeeSink::get()) + - sink_after_trigger, + total_fees + ); + assert_eq!( + Balances::free_balance(&recipient) - recipient_before, + crate::EXISTENTIAL_DEPOSIT + ); + (pipeline[0], action[0]) + }); + println!( + "ACTOR_FUNDED_USER_ACTION_COLLECTION_V1 {}", + serde_json::json!({ + "mode": if replay_wasm { "native-author-linked-to-exact-wasm" } else { "native-full-executive-monetary-ledger" }, + "triggerFee": trigger_fee.to_string(), "pipelineFee": pipeline_fee.to_string(), + "actionFee": action_fee.to_string(), + "control": [authored.metrics.actor_control.ref_time(), authored.metrics.actor_control.proof_size()], + "effect": [authored.metrics.actor_effect.ref_time(), authored.metrics.actor_effect.proof_size()], + "executionStorageProofBytes": proof.as_ref().map(|p| p.execution_storage_proof_bytes), + "verificationStorageProofBytes": proof.as_ref().map(|p| p.verification_storage_proof_bytes), + }) + ); +} + +#[test] +fn full_executive_funded_action_collection_witness() { + run_funded_action_collection_witness(&[], false); +} + +#[test] +#[ignore = "requires exact current production Wasm via DEOS_PRODUCTION_WASM"] +fn full_executive_funded_action_collection_replays_exact_production_wasm() { + let path = std::env::var_os("DEOS_PRODUCTION_WASM") + .expect("DEOS_PRODUCTION_WASM must explicitly select the accepted production artifact"); + let wasm = std::fs::read(path).expect("selected production Wasm is readable"); + assert_eq!( + polkadot_sdk::sp_io::hashing::sha2_256(&wasm), + EXP_0095_PRODUCTION_WASM_SHA256, + "funded User Action witness must remain bound to EXP-0095" + ); + run_funded_action_collection_witness(&wasm, true); +} + +#[test] +fn full_executive_user_completion_header_domain_witness() { + use pallet_deos_actors::{ + ActorContract, CompletionPolicy, FundingSourcePolicy, Mutability, StepControlWeightContext, + StepControlWeightProvider, + }; + let signer = sr25519::Pair::from_seed(&[66u8; 32]); + let mut rows = Vec::new(); + let mut geometry = Vec::new(); + for wide in [false, true] { + let mut ext = TestExternalities::new_with_code_and_state( + &[], + reference_genesis_storage(&[]), + crate::VERSION.state_version(), + ); + let (actor_id, head_bytes, funding_bytes, context, resources) = ext.execute_with(|| { + System::set_block_number(0); + assert!( + pallet_deos_actors::OwnerSlotBitmaps::::get(ALICE) + .iter() + .all(|byte| *byte == 0) + ); + assert_ok!(Balances::force_set_balance( + RuntimeOrigin::root(), + MultiAddress::Id(ALICE), + u128::MAX / 4 + )); + let sovereign = Actors::sovereign_account_id(&ALICE, 0); + assert_ok!(Balances::force_set_balance( + RuntimeOrigin::root(), + MultiAddress::Id(sovereign), + 1_000 * crate::EXISTENTIAL_DEPOSIT + )); + let funding = if wide { + let maximum = ::MaxWhitelistSize::get() as usize; + let mut sources = BTreeSet::from([ALICE]); + for byte in 0..=u8::MAX { + if sources.len() == maximum { + break; + } + sources.insert(crate::AccountId::new([byte; 32])); + } + assert_eq!(sources.len(), maximum); + FundingSourcePolicy::SignedAllowlist(sources.try_into().unwrap()) + } else { + FundingSourcePolicy::OwnerOnly + }; + let step = StepOf:: { + precondition: None, + task: Task::StopCycle, + on_error: StepErrorPolicy::AbortCycle, + }; + let actor_id = Actors::next_actor_id(); + assert_ok!(Actors::create_user_actor( + RuntimeOrigin::signed(ALICE), + Mutability::Mutable, + Some(ActorContract { + trigger: Trigger::manual(), + cooldown_blocks: 0, + window: None, + steps: vec![step.clone()].try_into().unwrap(), + funding, + completion: CompletionPolicy::Persistent, + auto_close_at_cycle_nonce: None, + }), + )); + let head = pallet_deos_actors::ActorContractHeads::::get(actor_id).unwrap(); + assert_eq!(head.header.step_count, 1); + assert_eq!(head.first_step, Some(step.clone())); + assert!( + Actors::active_actor_state(actor_id) + .unwrap() + .funding + .funding_tracked_assets + .is_empty() + ); + // Source-derived context: the production constructor uses Step/capture geometry, not funding policy. + let 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: + ::MaxFundingTrackedAssets::get(), + }; + let resources = head.first_step_resources.unwrap(); + assert_eq!( + resources.control, + ::StepControlWeight::maximum_control_weight( + context, &step + ) + .unwrap() + ); + System::set_block_number(1); + assert_ok!(Actors::manual_trigger( + RuntimeOrigin::signed(ALICE), + actor_id + )); + ( + actor_id, + head.encoded_size(), + head.header.funding.encoded_size(), + context, + resources, + ) + }); + ext.commit_all().unwrap(); + let storage = ext.execute_with(current_top_storage); + let profiles = BTreeMap::from([( + actor_id, + WorkloadActorProfile { + step_count: 1, + opening_predicates_per_step: 0, + task: Some(WorkloadTask::StopCycle), + }, + )]); + let authored = + author_complete_block(storage, &[], 2, UserDemand::ActorOnly, &signer, &profiles); + assert_eq!(authored.metrics.stop_cycle_steps, 1); + assert_eq!(authored.metrics.completed_cycles, 1); + assert_eq!(authored.metrics.actor_effect, Weight::zero()); + geometry.push((head_bytes, funding_bytes, context, resources)); + rows.push(serde_json::json!({ + "funding": if wide { "signed-allowlist-max" } else { "owner-only" }, + "headBytes": head_bytes, "fundingBytes": funding_bytes, + "context": [context.cursor, context.steps_in_fragment, context.opening_tail_chunks, + context.predicate_evaluation_units, context.opening_snapshot_entries, + context.opening_predicate_results, context.funding_snapshot_entries], + "storedControl": [resources.control.ref_time(), resources.control.proof_size()], + "blockControl": [authored.metrics.actor_control.ref_time(), authored.metrics.actor_control.proof_size()], + })); + } + assert_eq!(geometry[0].2, geometry[1].2); + assert_eq!(geometry[0].3, geometry[1].3); + assert!(geometry[1].0 > geometry[0].0); + assert_eq!(geometry[1].0 - geometry[0].0, geometry[1].1 - geometry[0].1); + println!( + "ACTOR_USER_COMPLETION_CONTEXT_V1 {}", + serde_json::json!({ + "mode": "native-full-executive-source-derived-context", "rows": rows, + }) + ); +} + +fn assert_w5_resource_ledger( + wasm: &[u8], + authored: &AuthoredBlock, + fixture: &PreparedW5Fixture, + block: u32, +) { + use pallet_deos_actors::{ + ActorType, CycleState, StepControlExecution, StepControlOutcome, StepControlPhase, + StepControlPlacement, StepControlWeightContext, StepControlWeightProvider, TaskEffectExecution, + }; + type W = crate::weights::pallet_deos_actors::SubstrateWeight; + let index = usize::try_from(block - 2).unwrap(); + assert!(index < 3); + let discovery = W::scheduler_paged_tombstone_drain(1); + let probe = W::scheduler_actor_state_probe(); + let consume = + W::scheduler_paged_consume_preserve_page().max(W::scheduler_paged_consume_delete_page()); + let append = W::scheduler_paged_append_new_page(); + assert!(W::scheduler_paged_append_existing_page().all_lte(append)); + let close = Actors::close_cleanup_weight_upper(); + let receipt = W::action_invocation_receipt(); + let opening_progress = W::scheduler_inner_opening_progress_min(1); + let opening_complete = W::scheduler_inner_opening_user_complete_header_max(); + let running_progress = W::scheduler_inner_running_progress(2, 0); + let running_complete = W::scheduler_inner_running_complete(2, 0); + let tail_plan = W::current_step_plan_running_tail(2); + let suspend = W::run_suspend(); + let complete = W::run_complete(); + // Pin generated I/O for the newly selected owners, not physical storage operations. + for (weight, base, proof, reads, writes) in [ + ( + W::pipeline_admission_apoptosis(), + 387_346_000, + 15_106, + 19, + 16, + ), + ( + W::scheduler_inner_zero_step_complete(), + 58_109_000, + 4_388, + 2, + 3, + ), + (opening_progress, 185_578_808 + 14_888_868, 9_320, 12, 7), + (running_progress, 284_303_244, 12_546, 18, 5), + (running_complete, 133_921_853 + 2 * 2_058_487, 10_974, 11, 4), + (tail_plan, 117_787_560 + 2 * 1_082_421, 5_942, 7, 0), + (suspend, 286_075_000, 5_871, 13, 9), + (complete, 201_984_000, 6_237, 8, 6), + (append, 109_653_000, 16_446, 6, 4), + (W::task_dex_exact_out(), 714_697_000, 19_253, 40, 17), + ] { + assert_production_weight_component(weight, base, proof, DatabaseIo::new(reads, writes)); + } + let baseline = W::scheduler_on_initialize_cutoff() + .saturating_add(W::materialization_coordinator_base()) + .saturating_add(W::scheduler_wakeup_cursor_worker_future().saturating_mul(2)) + .saturating_add(W::crossing_worker_base()) + .saturating_add(W::observation_fanout_base()) + .saturating_add(discovery.saturating_mul(2)) + .saturating_add(W::scheduler_on_idle_base()) + .saturating_add(W::block_resource_finalize()); + let startup = W::scheduler_wakeup_cursor_worker_remove() + .saturating_add(W::at_time_trigger_occurrence().max(W::cadenced_trigger_occurrence())) + .saturating_add(W::scheduler_wakeup_cursor_worker_future().saturating_mul(2)); + let zero_envelope = Actors::contract_steps_admission_weight_upper( + ActorType::System, + &ContractSteps::::default(), + ); + assert_eq!( + zero_envelope, + Actors::scheduler_admission_overhead() + .saturating_add(W::scheduler_inner_zero_step_complete()) + .saturating_add(close) + ); + let mut before = TestExternalities::new_with_code_and_state( + wasm, + authored.pre_state.clone(), + crate::VERSION.state_version(), + ); + before.execute_with(|| { + for actor_id in [ + fixture.running, + fixture.retry_prefix, + fixture.productive_cleanup, + ] { + let Some(state) = Actors::active_actor_state(actor_id) else { + continue; + }; + assert_eq!(state.identity.actor_class.actor_type(), ActorType::System); + assert!( + state + .contract + .steps + .iter() + .all(|s| s.precondition.is_none()) + ); + let cursor = state.run_state.as_ref().map_or(0, |run| run.cursor); + let count = state.contract.steps.len() as u32; + assert!(count == 1 || count == 3); + let context = StepControlWeightContext { + cursor, + steps_in_fragment: if cursor == 0 { 1 } else { count - 1 }, + opening_tail_chunks: if cursor == 0 { + (count - 1).div_ceil(pallet_deos_actors::MAX_STEPS_PER_TAIL_CHUNK) + } else { + 0 + }, + predicate_evaluation_units: 0, + opening_snapshot_entries: 0, + opening_predicate_results: 0, + funding_snapshot_entries: if cursor == 0 { + ::MaxFundingTrackedAssets::get() + } else { + 0 + }, + }; + let (outcome, placement, selected) = if block == 2 && count == 3 { + ( + StepControlOutcome::Continued, + StepControlPlacement::Queue, + opening_progress, + ) + } else if block == 2 { + ( + StepControlOutcome::Completed, + StepControlPlacement::None, + opening_complete, + ) + } else if actor_id == fixture.running && block == 3 { + ( + StepControlOutcome::Continued, + StepControlPlacement::Queue, + running_progress, + ) + } else if actor_id == fixture.running { + ( + StepControlOutcome::Completed, + StepControlPlacement::None, + running_complete, + ) + } else if block == 3 { + ( + StepControlOutcome::Suspended, + StepControlPlacement::Queue, + tail_plan.saturating_add(suspend).saturating_add(append), + ) + } else { + assert_eq!( + state + .run_state + .as_ref() + .unwrap() + .unsuccessful_attempts_at_cursor, + 1 + ); + assert!(state.hot.wakeup_pointer.is_none()); + assert!(state.hot.queue_ticket.is_some()); + ( + StepControlOutcome::Failed, + StepControlPlacement::None, + tail_plan.saturating_add(complete), + ) + }; + let phase = match state.hot.cycle_state { + CycleState::Idle => StepControlPhase::Opening, + CycleState::Running => StepControlPhase::Running, + CycleState::Suspended => StepControlPhase::Suspended, + }; + let (_, cell) = Actors::actor_control_cell(actor_id).unwrap(); + assert_eq!( + ::StepControlWeight::actual_control_weight( + context, + &state.contract.steps[cursor as usize], + cell.resources.control, + StepControlExecution { + phase, + outcome, + placement, + task_effect: TaskEffectExecution::Invoked, + action_fee_collected: false + } + ), + Some(selected.saturating_add(receipt)) + ); + } + }); + let terms = [ + ("baseline", baseline, DatabaseIo::new(44, 11), [1, 1, 1]), + ( + "genesis-anchor-extra", + startup, + DatabaseIo::new(104, 51), + [1, 0, 0], + ), + ( + "ordinary-entry-prefix", + discovery.saturating_add(probe).saturating_add(consume), + DatabaseIo::new(16, 6), + [4, 2, 2], + ), + ( + "zero-step-admission-envelope", + zero_envelope, + DatabaseIo::new(113, 82), + [1, 0, 0], + ), + ( + "opening-progress", + opening_progress, + DatabaseIo::new(12, 7), + [2, 0, 0], + ), + ( + "opening-complete", + opening_complete, + DatabaseIo::new(2, 3), + [1, 0, 0], + ), + ( + "running-progress", + running_progress, + DatabaseIo::new(18, 5), + [0, 1, 0], + ), + ( + "running-complete", + running_complete, + DatabaseIo::new(11, 4), + [0, 0, 1], + ), + ("tail-plan", tail_plan, DatabaseIo::new(7, 0), [0, 1, 1]), + ("run-suspend", suspend, DatabaseIo::new(13, 9), [0, 1, 0]), + ( + "run-complete-fallback", + complete, + DatabaseIo::new(8, 6), + [0, 0, 1], + ), + ( + "ready-append-envelope", + append, + DatabaseIo::new(6, 4), + [0, 1, 0], + ), + ("action-receipt", receipt, DatabaseIo::new(0, 0), [3, 2, 2]), + ( + "authored-terminal-cleanup", + close, + DatabaseIo::new(66, 65), + [1, 0, 1], + ), + // Complete apoptosis replaces provisional discovery/probe; it is not an extra close term. + ( + "complete-admission-apoptosis", + W::pipeline_admission_apoptosis(), + DatabaseIo::new(19, 16), + [1, 0, 0], + ), + ]; + let (control, io) = terms.iter().fold( + (Weight::zero(), DatabaseIo::new(0, 0)), + |(weight, io), (_, w, db, n)| { + ( + weight.saturating_add(w.saturating_mul(n[index])), + io.saturating_add(db.saturating_mul(n[index])), + ) + }, + ); + let effects = W::task_transfer() + .saturating_mul([3, 1, 1][index]) + .saturating_add(W::task_dex_exact_out().saturating_mul([0, 1, 1][index])); + assert_eq!(authored.metrics.actor_control, control); + assert_eq!(authored.metrics.actor_effect, effects); + assert_eq!(authored.metrics.actor_steps, [3, 2, 2][index]); + assert_eq!(authored.metrics.non_successful_steps, [0, 1, 1][index]); + let idle = W::scheduler_on_idle_base() + .saturating_add(W::block_resource_finalize()) + .saturating_add(discovery); + assert_eq!( + authored.metrics.prepass_actor_control, + control.checked_sub(&idle).unwrap() + ); + let effect_io = DatabaseIo::new(25, 12) + .saturating_mul([3, 1, 1][index]) + .saturating_add(DatabaseIo::new(40, 17).saturating_mul([0, 1, 1][index])); + println!( + "ACTOR_LIFECYCLE_RESOURCE_LEDGER_V1 {}", + serde_json::json!({ + "mode": "native-source-bound-reconstruction", "block": block, "blockHash": format!("{:?}", authored.block.header.hash()), + "generatedEffectIo": [effect_io.reads, effect_io.writes], + "control": [control.ref_time(),control.proof_size()], "prepassControl": [authored.metrics.prepass_actor_control.ref_time(),authored.metrics.prepass_actor_control.proof_size()], + "generatedControlIo": [io.reads,io.writes], "effect": [effects.ref_time(),effects.proof_size()], + "terms": terms.iter().map(|(name,w,db,n)| serde_json::json!({"owner":name,"frequency":n[index], + "weight":[w.ref_time(),w.proof_size()],"generatedIo":[db.reads,db.writes]})).collect::>(), + }) + ); +} + +fn run_w5_lifecycle_retry_cleanup_campaign(wasm: &[u8], replay_wasm: bool) { + let fixture = prepare_w5_fixture(wasm); + let setup = actor_lifecycle_observation(wasm, &fixture.actors.storage, &fixture.actors.actor_ids); + let mut timeline = Vec::new(); + let mut pre_state = fixture.actors.storage.clone(); + let mut parent = parent_header_for(pre_state.clone(), wasm, 1); + let mut transfer_steps = 0u32; + let mut non_successful_steps = 0u32; + let mut opening_steps = 0u32; + let mut middle_steps = 0u32; + let mut final_steps = 0u32; + let mut completed_cycle_actors = BTreeSet::new(); + let mut failed_cycle_actors = BTreeSet::new(); + let mut suspended_steps = Vec::new(); + let mut continued_steps = Vec::new(); + let mut closed_actors = Vec::new(); + let mut steps_per_block = Vec::new(); + let mut control_ref_time = Vec::new(); + let mut control_proof_size = Vec::new(); + let mut effect_ref_time = Vec::new(); + let mut effect_proof_size = Vec::new(); + let mut execution_storage_proof = Vec::new(); + let mut execution_compact_proof = Vec::new(); + let mut verification_storage_proof = Vec::new(); + let mut verification_compact_proof = Vec::new(); + + for block_number in 2..W5_BLOCK_LIMIT.saturating_add(2) { + let authored = author_complete_block_after( + pre_state, + wasm, + &parent, + block_number, + UserDemand::ActorOnly, + &fixture.actors.signer, + 0, + &fixture.actors.actor_profiles, + ); + assert_eq!(authored.metrics.user_calls, 0); + assert_eq!(authored.metrics.user_dispatch, Weight::zero()); + assert_eq!( + authored.metrics.actor_steps, authored.metrics.distinct_actors, + "W5 preserves Q1 in every full block" + ); + assert_w5_resource_ledger(wasm, &authored, &fixture, block_number); + transfer_steps = transfer_steps.saturating_add(authored.metrics.transfer_steps); + non_successful_steps = + non_successful_steps.saturating_add(authored.metrics.non_successful_steps); + opening_steps = opening_steps.saturating_add(authored.metrics.opening_steps); + middle_steps = middle_steps.saturating_add(authored.metrics.middle_steps); + final_steps = final_steps.saturating_add(authored.metrics.final_steps); + completed_cycle_actors.extend(authored.metrics.completed_cycle_actors.iter().copied()); + failed_cycle_actors.extend(authored.metrics.failed_cycle_actors.iter().copied()); + suspended_steps.extend(authored.metrics.suspended_steps.iter().copied()); + continued_steps.extend(authored.metrics.continued_steps.iter().copied()); + closed_actors.extend(authored.metrics.closed_actors.iter().copied()); + steps_per_block.push(u64::from(authored.metrics.actor_steps)); + control_ref_time.push(authored.metrics.actor_control.ref_time()); + control_proof_size.push(authored.metrics.actor_control.proof_size()); + effect_ref_time.push(authored.metrics.actor_effect.ref_time()); + effect_proof_size.push(authored.metrics.actor_effect.proof_size()); + if replay_wasm { + let proof = replay_complete_block_in_wasm( + &authored, + wasm, + &format!("W5-lifecycle-retry-cleanup-{block_number}"), + ); + execution_storage_proof.push(proof.execution_storage_proof_bytes); + execution_compact_proof.push(proof.execution_compact_proof_bytes); + verification_storage_proof.push(proof.verification_storage_proof_bytes); + verification_compact_proof.push(proof.verification_compact_proof_bytes); + } + parent = authored.block.header.clone(); + pre_state = authored.post_state; + timeline.push(actor_lifecycle_observation( + wasm, + &pre_state, + &fixture.actors.actor_ids, + )); + let terminal_closures = [ + ( + fixture.productive_cleanup, + CloseReason::ProductiveCycleCompleted, + ), + (fixture.apoptosis, CloseReason::CycleAdmissionInsufficient), + (fixture.retry_prefix, CloseReason::RetryAttemptsExhausted), + ]; + if terminal_closures + .iter() + .all(|expected| closed_actors.contains(expected)) + && completed_cycle_actors.contains(&fixture.zero_step) + && completed_cycle_actors.contains(&fixture.running) + { + break; + } + } + + assert_eq!( + steps_per_block.len(), + 3, + "W5 converges in three linked blocks" + ); + assert_eq!( + transfer_steps, 5, + "W5 commits each authored prefix exactly once" + ); + assert_eq!( + non_successful_steps, 2, + "W5 attempts the retrying middle Step twice" + ); + assert_eq!((opening_steps, middle_steps, final_steps), (3, 3, 1)); + assert!(completed_cycle_actors.contains(&fixture.zero_step)); + assert!(completed_cycle_actors.contains(&fixture.running)); + assert!(completed_cycle_actors.contains(&fixture.productive_cleanup)); + assert_eq!( + suspended_steps, + vec![(fixture.retry_prefix, 1)], + "the first temporary failure creates one Actor-owned suspension" + ); + assert_eq!( + continued_steps, + vec![(fixture.retry_prefix, 1)], + "the retry resumes the same cursor exactly once" + ); + for expected in [ + ( + fixture.productive_cleanup, + CloseReason::ProductiveCycleCompleted, + ), + (fixture.apoptosis, CloseReason::CycleAdmissionInsufficient), + (fixture.retry_prefix, CloseReason::RetryAttemptsExhausted), + ] { + assert!( + closed_actors.contains(&expected), + "W5 observes closure {expected:?}" + ); + } + assert!( + !closed_actors + .iter() + .any(|(actor_id, _)| *actor_id == fixture.zero_step) + ); + assert!( + !closed_actors + .iter() + .any(|(actor_id, _)| *actor_id == fixture.running) + ); + + let mut final_ext = + TestExternalities::new_with_code_and_state(wasm, pre_state, crate::VERSION.state_version()); + final_ext.execute_with(|| { + assert!(Actors::active_actor_state(fixture.zero_step).is_some()); + assert!(Actors::active_actor_state(fixture.running).is_some()); + assert!(Actors::active_actor_state(fixture.productive_cleanup).is_none()); + assert!(Actors::active_actor_state(fixture.apoptosis).is_none()); + assert!(Actors::active_actor_state(fixture.retry_prefix).is_none()); + assert_eq!( + Balances::free_balance(&fixture.retry_sovereign), + fixture + .retry_balance_before + .saturating_sub(crate::EXISTENTIAL_DEPOSIT), + "retry exhaustion preserves the committed prefix and performs no compensation" + ); + }); + + println!( + "EXP_0066_W5_TIMELINE_V1 {}", + serde_json::json!({ + "productionWasmReplayed": replay_wasm, + "roles": {"zeroStep": fixture.zero_step, "running": fixture.running, + "retryPrefix": fixture.retry_prefix, "productiveCleanup": fixture.productive_cleanup, + "admissionApoptosis": fixture.apoptosis}, + "fixtureSetup": setup, "finalizedBlocks": timeline, + }) + ); + println!( + "EXP_0066_W5_V1 {{\"schedule\":\"manual-only\",\"workloadActors\":{},\"measuredBlocks\":{},\"stepsPerBlock\":{:?},\"transferSteps\":{transfer_steps},\"nonSuccessfulSteps\":{non_successful_steps},\"openingAttempts\":{opening_steps},\"middleAttempts\":{middle_steps},\"finalAttempts\":{final_steps},\"completedCycles\":{},\"failedCycleSummaries\":{},\"suspensions\":{},\"continuations\":{},\"productiveCleanupClosures\":1,\"admissionApoptosisClosures\":1,\"retryExhaustionClosures\":1,\"committedPrefixAmount\":{},\"controlRefTimeMin\":{},\"controlRefTimeP50\":{},\"controlRefTimeMax\":{},\"controlProofSizeMin\":{},\"controlProofSizeP50\":{},\"controlProofSizeMax\":{},\"effectRefTimeMin\":{},\"effectRefTimeP50\":{},\"effectRefTimeMax\":{},\"effectProofSizeMin\":{},\"effectProofSizeP50\":{},\"effectProofSizeMax\":{},\"executionStorageProofMin\":{},\"executionStorageProofMax\":{},\"executionCompactProofMin\":{},\"executionCompactProofMax\":{},\"verificationStorageProofMin\":{},\"verificationStorageProofMax\":{},\"verificationCompactProofMin\":{},\"verificationCompactProofMax\":{}}}", + fixture.actors.actor_ids.len(), + steps_per_block.len(), + steps_per_block, + completed_cycle_actors.len(), + failed_cycle_actors.len(), + suspended_steps.len(), + continued_steps.len(), + crate::EXISTENTIAL_DEPOSIT, + control_ref_time.iter().copied().min().unwrap_or(0), + nearest_rank_percentile(&control_ref_time, 50), + control_ref_time.iter().copied().max().unwrap_or(0), + control_proof_size.iter().copied().min().unwrap_or(0), + nearest_rank_percentile(&control_proof_size, 50), + control_proof_size.iter().copied().max().unwrap_or(0), + effect_ref_time.iter().copied().min().unwrap_or(0), + nearest_rank_percentile(&effect_ref_time, 50), + effect_ref_time.iter().copied().max().unwrap_or(0), + effect_proof_size.iter().copied().min().unwrap_or(0), + nearest_rank_percentile(&effect_proof_size, 50), + effect_proof_size.iter().copied().max().unwrap_or(0), + execution_storage_proof.iter().copied().min().unwrap_or(0), + execution_storage_proof.iter().copied().max().unwrap_or(0), + execution_compact_proof.iter().copied().min().unwrap_or(0), + execution_compact_proof.iter().copied().max().unwrap_or(0), + verification_storage_proof + .iter() + .copied() + .min() + .unwrap_or(0), + verification_storage_proof + .iter() + .copied() + .max() + .unwrap_or(0), + verification_compact_proof + .iter() + .copied() + .min() + .unwrap_or(0), + verification_compact_proof + .iter() + .copied() + .max() + .unwrap_or(0), + ); +} + +fn run_w6_mixed_arrival_lifecycle_campaign(wasm: &[u8], replay_wasm: bool) { + let fixture = prepare_w6_fixture(wasm); + let setup = actor_lifecycle_observation(wasm, &fixture.actors.storage, &fixture.actors.actor_ids); + let mut timeline = Vec::new(); + let mut pre_state = fixture.actors.storage.clone(); + let mut parent = parent_header_for(pre_state.clone(), wasm, 1); + let mut signer_nonce = 0; + let mut first_progress = BTreeMap::::new(); + let mut progress_counts = BTreeMap::::new(); + let mut paused_events = Vec::new(); + let mut resumed_events = Vec::new(); + let mut closed_events = Vec::new(); + let mut trigger_counts = [0u32; 6]; + let mut crossing_occurrence_blocks = Vec::new(); + let mut steps_per_block = Vec::new(); + let mut control_ref_time = Vec::new(); + let mut control_proof_size = Vec::new(); + let mut effect_ref_time = Vec::new(); + let mut effect_proof_size = Vec::new(); + let mut execution_storage_proof = Vec::new(); + let mut execution_compact_proof = Vec::new(); + let mut verification_storage_proof = Vec::new(); + let mut verification_compact_proof = Vec::new(); + + for block_number in 2..W6_BLOCK_LIMIT.saturating_add(2) { + let calls = match block_number { + 2 => vec![RuntimeCall::Actors(pallet_deos_actors::Call::pause_actor { + actor_id: fixture.paused_actor, + })], + 3 => vec![ + RuntimeCall::Oracle(pallet_oracle::Call::publish { + feed: fixture.feed, + sample: 100_000_000_000_000, + }), + RuntimeCall::Actors(pallet_deos_actors::Call::update_contract { + actor_id: fixture.crossing_actor, + contract: fixture.crossing_replacement.clone(), + }), + ], + 4 => vec![RuntimeCall::Oracle(pallet_oracle::Call::publish { + feed: fixture.feed, + sample: 200_000_000_000_000, + })], + 5 => vec![RuntimeCall::Actors(pallet_deos_actors::Call::close_actor { + actor_id: fixture.closed_actor, + })], + 6 => vec![RuntimeCall::Actors( + pallet_deos_actors::Call::resume_actor { + actor_id: fixture.paused_actor, + }, + )], + _ => Vec::new(), + }; + let authored = author_complete_block_after_with_calls( + pre_state, + wasm, + &parent, + block_number, + UserDemand::ActorOnly, + &fixture.actors.signer, + signer_nonce, + &fixture.actors.actor_profiles, + &calls, + ); + signer_nonce = signer_nonce.saturating_add(authored.metrics.user_calls); + assert_eq!( + authored.metrics.actor_steps, authored.metrics.distinct_actors, + "W6 preserves Q1 in every full block" + ); + assert_eq!( + authored.metrics.non_successful_steps, 0, + "W6 retained arrivals complete without failed Steps" + ); + for (actor_id, _) in &authored.metrics.progressed_steps { + first_progress.entry(*actor_id).or_insert(block_number); + progress_counts + .entry(*actor_id) + .and_modify(|count| *count = count.saturating_add(1)) + .or_insert(1); + } + paused_events.extend(authored.metrics.paused_actors.iter().copied()); + resumed_events.extend(authored.metrics.resumed_actors.iter().copied()); + closed_events.extend(authored.metrics.closed_actors.iter().copied()); + for (actor_id, family) in &authored.metrics.trigger_occurrences { + if *actor_id == fixture.crossing_actor && *family == TriggerFamily::ObservationCrossing { + crossing_occurrence_blocks.push(block_number); + } + let index = match family { + TriggerFamily::Manual => 0, + TriggerFamily::AddressEvent => 1, + TriggerFamily::ObservationChange => 2, + TriggerFamily::ObservationCrossing => 3, + TriggerFamily::AtTime => 4, + TriggerFamily::Cadenced => 5, + }; + trigger_counts[index] = trigger_counts[index].saturating_add(1); + } + steps_per_block.push(u64::from(authored.metrics.actor_steps)); + control_ref_time.push(authored.metrics.actor_control.ref_time()); + control_proof_size.push(authored.metrics.actor_control.proof_size()); + effect_ref_time.push(authored.metrics.actor_effect.ref_time()); + effect_proof_size.push(authored.metrics.actor_effect.proof_size()); + if replay_wasm { + let proof = replay_complete_block_in_wasm( + &authored, + wasm, + &format!("W6-mixed-arrival-lifecycle-{block_number}"), + ); + execution_storage_proof.push(proof.execution_storage_proof_bytes); + execution_compact_proof.push(proof.execution_compact_proof_bytes); + verification_storage_proof.push(proof.verification_storage_proof_bytes); + verification_compact_proof.push(proof.verification_compact_proof_bytes); + } + parent = authored.block.header.clone(); + pre_state = authored.post_state; + timeline.push(actor_lifecycle_observation( + wasm, + &pre_state, + &fixture.actors.actor_ids, + )); + } + + assert_eq!(paused_events, vec![fixture.paused_actor]); + assert_eq!(resumed_events, vec![fixture.paused_actor]); + assert!(closed_events.contains(&(fixture.closed_actor, CloseReason::OwnerInitiated))); + assert!(!first_progress.contains_key(&fixture.closed_actor)); + assert!( + fixture + .dense_manual + .iter() + .all(|actor_id| first_progress.contains_key(actor_id)) + ); + for (index, actor_id) in fixture.sparse_block.iter().enumerate() { + if *actor_id == fixture.closed_actor { + continue; + } + let expected_start = [3, 6, 9][index]; + assert!( + first_progress + .get(actor_id) + .is_some_and(|block| *block >= expected_start), + "block-clock W6 Actor cannot progress before its sparse window" + ); + } + assert!( + fixture + .sparse_tick + .iter() + .all(|actor_id| first_progress.contains_key(actor_id)) + ); + assert!( + fixture + .randomized_cadenced + .iter() + .all(|actor_id| first_progress.contains_key(actor_id)) + ); + assert!( + first_progress + .get(&fixture.paused_actor) + .is_some_and(|block| *block >= 6), + "paused Manual Actor resumes without losing its retained arrival" + ); + assert_eq!( + crossing_occurrence_blocks, + vec![5], + "only generation 2 may own the W6 Crossing detector/latch transition" + ); + assert_eq!( + first_progress.get(&fixture.crossing_actor), + Some(&6), + "generation-2 Crossing service must obey the N -> N+1 causal floor" + ); + assert_eq!( + progress_counts.get(&fixture.crossing_actor), + Some(&1), + "only the post-replacement Crossing transition executes" + ); + assert_eq!(trigger_counts[3], 1); + assert!(trigger_counts[4] >= fixture.sparse_tick.len() as u32); + assert!(trigger_counts[5] >= fixture.randomized_cadenced.len() as u32); + + let mut final_ext = + TestExternalities::new_with_code_and_state(wasm, pre_state, crate::VERSION.state_version()); + final_ext.execute_with(|| { + assert!(Actors::active_actor_state(fixture.closed_actor).is_none()); + assert_eq!( + pallet_deos_actors::ActorWaitingOccupancies::::get(fixture.closed_wakeup_key), + 0, + "the closed block-clock Actor leaves no live tombstone occupancy" + ); + assert!(Actors::crossing_worker_fault().is_none()); + assert!(Actors::wakeup_worker_fault().is_none()); + assert!(Actors::observation_fanout_worker_fault().is_none()); + assert!( + Actors::crossing_membership(fixture.crossing_actor) + .is_some_and(|locator| locator.generation > fixture.crossing_generation), + "normal Contract replacement rotates Crossing generation authority" + ); + }); + + println!( + "EXP_0066_W6_TIMELINE_V1 {}", + serde_json::json!({ + "productionWasmReplayed": replay_wasm, + "roles": {"denseManual": fixture.dense_manual, "windowedManual": fixture.sparse_block, + "atTime": fixture.sparse_tick, "cadenced": fixture.randomized_cadenced, + "cadencedPeriodsTicks": fixture.randomized_periods, "crossing": fixture.crossing_actor, + "paused": fixture.paused_actor, "closed": fixture.closed_actor}, + "fixtureSetup": setup, "finalizedBlocks": timeline, + }) + ); + println!( + "EXP_0066_W6_V2 {{\"seed\":{W6_RANDOM_SEED},\"measuredBlocks\":{},\"workloadActors\":{},\"denseManualActors\":{},\"sparseBlockClockActors\":{},\"atTimeDeadlineActors\":{},\"randomizedCadencedActors\":{},\"randomizedCadencePeriodsTicks\":{:?},\"distinctProgressedActors\":{},\"totalSteps\":{},\"pausedEvents\":{},\"resumedEvents\":{},\"ownerCloseEvents\":{},\"crossingOccurrences\":{},\"atTimeOccurrences\":{},\"cadencedOccurrences\":{},\"controlRefTimeMin\":{},\"controlRefTimeP50\":{},\"controlRefTimeMax\":{},\"controlProofSizeMin\":{},\"controlProofSizeP50\":{},\"controlProofSizeMax\":{},\"effectRefTimeMin\":{},\"effectRefTimeP50\":{},\"effectRefTimeMax\":{},\"effectProofSizeMin\":{},\"effectProofSizeP50\":{},\"effectProofSizeMax\":{},\"executionStorageProofMin\":{},\"executionStorageProofMax\":{},\"executionCompactProofMin\":{},\"executionCompactProofMax\":{},\"verificationStorageProofMin\":{},\"verificationStorageProofMax\":{},\"verificationCompactProofMin\":{},\"verificationCompactProofMax\":{}}}", + steps_per_block.len(), + fixture.actors.actor_ids.len(), + fixture.dense_manual.len(), + fixture.sparse_block.len(), + fixture.sparse_tick.len(), + fixture.randomized_cadenced.len(), + fixture.randomized_periods, + first_progress.len(), + steps_per_block.iter().sum::(), + paused_events.len(), + resumed_events.len(), + closed_events + .iter() + .filter(|(_, reason)| *reason == CloseReason::OwnerInitiated) + .count(), + trigger_counts[3], + trigger_counts[4], + trigger_counts[5], + control_ref_time.iter().copied().min().unwrap_or(0), + nearest_rank_percentile(&control_ref_time, 50), + control_ref_time.iter().copied().max().unwrap_or(0), + control_proof_size.iter().copied().min().unwrap_or(0), + nearest_rank_percentile(&control_proof_size, 50), + control_proof_size.iter().copied().max().unwrap_or(0), + effect_ref_time.iter().copied().min().unwrap_or(0), + nearest_rank_percentile(&effect_ref_time, 50), + effect_ref_time.iter().copied().max().unwrap_or(0), + effect_proof_size.iter().copied().min().unwrap_or(0), + nearest_rank_percentile(&effect_proof_size, 50), + effect_proof_size.iter().copied().max().unwrap_or(0), + execution_storage_proof.iter().copied().min().unwrap_or(0), + execution_storage_proof.iter().copied().max().unwrap_or(0), + execution_compact_proof.iter().copied().min().unwrap_or(0), + execution_compact_proof.iter().copied().max().unwrap_or(0), + verification_storage_proof + .iter() + .copied() + .min() + .unwrap_or(0), + verification_storage_proof + .iter() + .copied() + .max() + .unwrap_or(0), + verification_compact_proof + .iter() + .copied() + .min() + .unwrap_or(0), + verification_compact_proof + .iter() + .copied() + .max() + .unwrap_or(0), + ); +} + +fn run_w7_due_only_active_frontier_campaign( + wasm: &[u8], + maximum_population: bool, + replay_wasm: bool, +) -> W7CampaignResult { + let fixture = prepare_w7_fixture(wasm, maximum_population); + let profile = if maximum_population { + "maximum-population" + } else { + "due-frontier-control" + }; + let mut pre_state = fixture.actors.storage.clone(); + let mut parent = parent_header_for(pre_state.clone(), wasm, 1); + let mut progressed_order = Vec::new(); + let mut progressed = BTreeSet::new(); + let mut pending_frontier_blocks = 0u32; + let mut control_bound_blocks = 0u32; + let actor_control_limit = BlockResourceBudgetValue::get().limits().actor_control(); + let mut control_remaining_ref_time = Vec::new(); + let mut control_remaining_proof_size = Vec::new(); + let mut result = W7CampaignResult { + steps_per_block: Vec::new(), + control_ref_time: Vec::new(), + control_proof_size: Vec::new(), + effect_ref_time: Vec::new(), + effect_proof_size: Vec::new(), + execution_storage_proof: Vec::new(), + execution_compact_proof: Vec::new(), + verification_storage_proof: Vec::new(), + verification_compact_proof: Vec::new(), + }; + + for block_number in 2..W7_BLOCK_LIMIT.saturating_add(2) { + let authored = author_complete_block_after( + pre_state, + wasm, + &parent, + block_number, + UserDemand::ActorOnly, + &fixture.actors.signer, + 0, + &fixture.actors.actor_profiles, + ); + assert_eq!(authored.metrics.user_calls, 0); + assert_eq!(authored.metrics.user_dispatch, Weight::zero()); + assert_eq!(authored.metrics.non_successful_steps, 0); + assert_eq!( + authored.metrics.actor_steps, authored.metrics.distinct_actors, + "W7 preserves Q1 in every full block" + ); + assert!( + authored + .metrics + .progressed_steps + .iter() + .all(|(actor_id, _)| fixture.due.contains(actor_id)), + "only the declared W7 due frontier may progress" + ); + for (actor_id, step_index) in &authored.metrics.progressed_steps { + assert_eq!(*step_index, 0); + assert!(progressed.insert(*actor_id), "W7 due Actor progresses once"); + progressed_order.push(*actor_id); + } + let control_remaining = actor_control_limit + .checked_sub(&authored.metrics.actor_control) + .unwrap_or_else(Weight::zero); + control_remaining_ref_time.push(control_remaining.ref_time()); + control_remaining_proof_size.push(control_remaining.proof_size()); + if authored.metrics.queue_head < authored.metrics.queue_tail { + pending_frontier_blocks = pending_frontier_blocks.saturating_add(1); + assert!( + !fixture + .actors + .next_control_maximum + .all_lte(control_remaining), + "pending W7 due work stops only at the complete-attempt Control frontier" + ); + control_bound_blocks = control_bound_blocks.saturating_add(1); + } + result + .steps_per_block + .push(u64::from(authored.metrics.actor_steps)); + result + .control_ref_time + .push(authored.metrics.actor_control.ref_time()); + result + .control_proof_size + .push(authored.metrics.actor_control.proof_size()); + result + .effect_ref_time + .push(authored.metrics.actor_effect.ref_time()); + result + .effect_proof_size + .push(authored.metrics.actor_effect.proof_size()); + if replay_wasm { + let proof = replay_complete_block_in_wasm( + &authored, + wasm, + &format!("W7-due-only-active-frontier-{profile}-{block_number}"), + ); + result + .execution_storage_proof + .push(proof.execution_storage_proof_bytes); + result + .execution_compact_proof + .push(proof.execution_compact_proof_bytes); + result + .verification_storage_proof + .push(proof.verification_storage_proof_bytes); + result + .verification_compact_proof + .push(proof.verification_compact_proof_bytes); + } + parent = authored.block.header.clone(); + pre_state = authored.post_state; + if progressed.len() == fixture.due.len() { + assert_eq!(authored.metrics.queue_head, authored.metrics.queue_tail); + break; + } + } + + assert_eq!(progressed.len(), fixture.due.len()); + assert_eq!(progressed_order, fixture.due, "W7 due service remains FIFO"); + assert_eq!( + result.steps_per_block.iter().sum::(), + u64::from(W7_DUE_ACTORS) + ); + assert_eq!(control_bound_blocks, pending_frontier_blocks); + let mut final_ext = + TestExternalities::new_with_code_and_state(wasm, pre_state, crate::VERSION.state_version()); + final_ext.execute_with(|| { + assert!(fixture.future.iter().all(|actor_id| { + Actors::active_actor_state(*actor_id).is_some_and(|state| state.identity.cycle_nonce == 0) + })); + assert!(fixture.unsignaled.iter().all(|actor_id| { + Actors::active_actor_state(*actor_id).is_some_and(|state| state.identity.cycle_nonce == 0) + })); + assert!(fixture.unsignaled.iter().all(|actor_id| { + pallet_deos_actors::ActorUnsignaledControlCells::::contains_key(actor_id) + })); + if let Some(key) = fixture.future_wakeup_key { + assert_eq!( + pallet_deos_actors::ActorWaitingOccupancies::::get(key), + fixture.future.len() as u32, + "the future W7 cohort remains outside the due frontier" + ); + } + }); + + let non_due_identities = fixture + .reference_identities + .saturating_add(fixture.future.len() as u32) + .saturating_add(fixture.unsignaled.len() as u32); + println!( + "EXP_0066_W7_V1 {{\"profile\":\"{profile}\",\"actorIdentities\":{},\"referenceIdentities\":{},\"futureWorkloadActors\":{},\"unsignaledWorkloadActors\":{},\"nonDueIdentities\":{non_due_identities},\"dueActors\":{},\"measuredBlocks\":{},\"stepsPerBlock\":{:?},\"totalSteps\":{},\"distinctProgressedActors\":{},\"fifoOrderPreserved\":true,\"pendingFrontierBlocks\":{pending_frontier_blocks},\"controlBoundBlocks\":{control_bound_blocks},\"nextControlMaximumRefTime\":{},\"nextControlMaximumProofSize\":{},\"controlRemainingRefTimeMin\":{},\"controlRemainingRefTimeMax\":{},\"controlRemainingProofSizeMin\":{},\"controlRemainingProofSizeMax\":{},\"controlRefTimeMin\":{},\"controlRefTimeP50\":{},\"controlRefTimeMax\":{},\"controlProofSizeMin\":{},\"controlProofSizeP50\":{},\"controlProofSizeMax\":{},\"effectRefTimeMin\":{},\"effectRefTimeP50\":{},\"effectRefTimeMax\":{},\"effectProofSizeMin\":{},\"effectProofSizeP50\":{},\"effectProofSizeMax\":{},\"executionStorageProofMin\":{},\"executionStorageProofMax\":{},\"executionCompactProofMin\":{},\"executionCompactProofMax\":{},\"verificationStorageProofMin\":{},\"verificationStorageProofMax\":{},\"verificationCompactProofMin\":{},\"verificationCompactProofMax\":{}}}", + fixture.reference_identities as usize + fixture.actors.actor_ids.len(), + fixture.reference_identities, + fixture.future.len(), + fixture.unsignaled.len(), + fixture.due.len(), + result.steps_per_block.len(), + result.steps_per_block, + result.steps_per_block.iter().sum::(), + progressed.len(), + fixture.actors.next_control_maximum.ref_time(), + fixture.actors.next_control_maximum.proof_size(), + control_remaining_ref_time + .iter() + .copied() + .min() + .unwrap_or(0), + control_remaining_ref_time + .iter() + .copied() + .max() + .unwrap_or(0), + control_remaining_proof_size + .iter() + .copied() + .min() + .unwrap_or(0), + control_remaining_proof_size + .iter() + .copied() + .max() + .unwrap_or(0), + result.control_ref_time.iter().copied().min().unwrap_or(0), + nearest_rank_percentile(&result.control_ref_time, 50), + result.control_ref_time.iter().copied().max().unwrap_or(0), + result.control_proof_size.iter().copied().min().unwrap_or(0), + nearest_rank_percentile(&result.control_proof_size, 50), + result.control_proof_size.iter().copied().max().unwrap_or(0), + result.effect_ref_time.iter().copied().min().unwrap_or(0), + nearest_rank_percentile(&result.effect_ref_time, 50), + result.effect_ref_time.iter().copied().max().unwrap_or(0), + result.effect_proof_size.iter().copied().min().unwrap_or(0), + nearest_rank_percentile(&result.effect_proof_size, 50), + result.effect_proof_size.iter().copied().max().unwrap_or(0), + result + .execution_storage_proof + .iter() + .copied() + .min() + .unwrap_or(0), + result + .execution_storage_proof + .iter() + .copied() + .max() + .unwrap_or(0), + result + .execution_compact_proof + .iter() + .copied() + .min() + .unwrap_or(0), + result + .execution_compact_proof + .iter() + .copied() + .max() + .unwrap_or(0), + result + .verification_storage_proof + .iter() + .copied() + .min() + .unwrap_or(0), + result + .verification_storage_proof + .iter() + .copied() + .max() + .unwrap_or(0), + result + .verification_compact_proof + .iter() + .copied() + .min() + .unwrap_or(0), + result + .verification_compact_proof + .iter() + .copied() + .max() + .unwrap_or(0), + ); + result +} + +fn assert_w7_due_only_active_frontier_campaign(wasm: &[u8], replay_wasm: bool) { + let control = run_w7_due_only_active_frontier_campaign(wasm, false, replay_wasm); + let maximum = run_w7_due_only_active_frontier_campaign(wasm, true, replay_wasm); + assert_eq!( + control.steps_per_block, maximum.steps_per_block, + "9,900 non-due identities must not change due-frontier throughput" + ); + let control_ref_time_delta_max = control + .control_ref_time + .iter() + .zip(&maximum.control_ref_time) + .map(|(control, maximum)| control.abs_diff(*maximum)) + .max() + .unwrap_or(0); + let control_proof_size_delta_max = control + .control_proof_size + .iter() + .zip(&maximum.control_proof_size) + .map(|(control, maximum)| control.abs_diff(*maximum)) + .max() + .unwrap_or(0); + println!( + "EXP_0066_W7_COMPARISON_V1 {{\"stepsPerBlockEqual\":true,\"controlRefTimeDeltaMax\":{control_ref_time_delta_max},\"controlProofSizeDeltaMax\":{control_proof_size_delta_max}}}" + ); +} + +fn run_w8_tombstone_prefix_chunk_pressure_campaign(wasm: &[u8], replay_wasm: bool) { + let actor_control_limit = BlockResourceBudgetValue::get().limits().actor_control(); + for tombstone_prefix in W8_TOMBSTONE_PREFIXES { + let fixture = prepare_w8_fixture(wasm, tombstone_prefix); + let initial_tail = u64::from(tombstone_prefix.saturating_add(W8_DUE_ACTORS)); + let parent = parent_header_for(fixture.actors.storage.clone(), wasm, 1); + let authored = author_complete_block_after( + fixture.actors.storage, + wasm, + &parent, + 2, + UserDemand::ActorOnly, + &fixture.actors.signer, + 0, + &fixture.actors.actor_profiles, + ); + assert_eq!(authored.metrics.user_calls, 0); + assert_eq!(authored.metrics.user_dispatch, Weight::zero()); + assert_eq!(authored.metrics.non_successful_steps, 0); + assert_eq!( + authored.metrics.actor_steps, authored.metrics.distinct_actors, + "W8 preserves Q1 under every tombstone-prefix profile" + ); + assert!( + authored.metrics.actor_steps > 0, + "every bounded W8 prefix must leave capacity for a live due Actor" + ); + assert!(authored.metrics.actor_steps < W8_DUE_ACTORS); + assert_eq!(authored.metrics.queue_tail, initial_tail); + let progressed = authored + .metrics + .progressed_steps + .iter() + .map(|(actor_id, step_index)| { + assert_eq!(*step_index, 0); + *actor_id + }) + .collect::>(); + assert_eq!( + progressed, + fixture.due[..progressed.len()], + "W8 service remains the exact live FIFO prefix after tombstone reclamation" + ); + assert_eq!( + authored.metrics.queue_head, + u64::from(fixture.tombstone_prefix).saturating_add(u64::from(authored.metrics.actor_steps)), + "W8 queue advancement accounts for each tombstone and committed live prefix exactly once" + ); + assert!(authored.metrics.queue_head < authored.metrics.queue_tail); + let control_remaining = actor_control_limit + .checked_sub(&authored.metrics.actor_control) + .unwrap_or_else(Weight::zero); + assert!( + !fixture + .actors + .next_control_maximum + .all_lte(control_remaining), + "pending W8 live work stops only at the complete-attempt Control frontier" + ); + + let proof = replay_wasm.then(|| { + replay_complete_block_in_wasm( + &authored, + wasm, + &format!("W8-tombstone-prefix-{tombstone_prefix}"), + ) + }); + let proof = proof.as_ref(); + println!( + "EXP_0066_W8_V1 {{\"tombstonePrefix\":{tombstone_prefix},\"physicalChunkWidth\":32,\"tombstoneChunksTouched\":{},\"fullyReclaimedTombstoneChunks\":{},\"initialQueueHead\":0,\"initialQueueTail\":{initial_tail},\"dueActors\":{},\"dueSteps\":{},\"distinctProgressedActors\":{},\"dueActorsRemaining\":{},\"finalQueueHead\":{},\"finalQueueTail\":{},\"finalHeadChunk\":{},\"finalHeadOffset\":{},\"fifoPrefixPreserved\":true,\"q1Preserved\":true,\"controlBound\":true,\"nextControlMaximumRefTime\":{},\"nextControlMaximumProofSize\":{},\"controlRemainingRefTime\":{},\"controlRemainingProofSize\":{},\"controlRefTime\":{},\"controlProofSize\":{},\"effectRefTime\":{},\"effectProofSize\":{},\"executionStorageProofBytes\":{},\"executionCompactProofBytes\":{},\"verificationStorageProofBytes\":{},\"verificationCompactProofBytes\":{}}}", + tombstone_prefix.div_ceil(32), + tombstone_prefix / 32, + fixture.due.len(), + authored.metrics.actor_steps, + authored.metrics.distinct_actors, + fixture + .due + .len() + .saturating_sub(authored.metrics.actor_steps as usize), + authored.metrics.queue_head, + authored.metrics.queue_tail, + authored.metrics.queue_head / 32, + authored.metrics.queue_head % 32, + fixture.actors.next_control_maximum.ref_time(), + fixture.actors.next_control_maximum.proof_size(), + control_remaining.ref_time(), + control_remaining.proof_size(), + authored.metrics.actor_control.ref_time(), + authored.metrics.actor_control.proof_size(), + authored.metrics.actor_effect.ref_time(), + authored.metrics.actor_effect.proof_size(), + proof.map_or(0, |metrics| metrics.execution_storage_proof_bytes), + proof.map_or(0, |metrics| metrics.execution_compact_proof_bytes), + proof.map_or(0, |metrics| metrics.verification_storage_proof_bytes), + proof.map_or(0, |metrics| metrics.verification_compact_proof_bytes), + ); + } +} + +fn run_w9_resource_independence_campaign(wasm: &[u8], replay_wasm: bool) { + let user_limit = BlockResourceBudgetValue::get().limits().user_base_turn(); + let mut results = Vec::new(); + for demand in [ + UserDemand::ActorOnly, + UserDemand::ContinuousValid, + UserDemand::RefTimeHeavy, + ] { + let fixture = prepare_w9_fixture(wasm); + let parent = parent_header_for(fixture.storage.clone(), wasm, 1); + let authored = author_complete_block_after( + fixture.storage, + wasm, + &parent, + 2, + demand, + &fixture.signer, + 0, + &fixture.actor_profiles, + ); + assert_eq!(authored.metrics.non_successful_steps, 0); + assert_eq!( + authored.metrics.actor_steps, authored.metrics.distinct_actors, + "W9 preserves Q1 in every demand profile" + ); + assert!(authored.metrics.actor_steps < W9_DUE_ACTORS); + assert!(authored.metrics.queue_head < authored.metrics.queue_tail); + let progressed = authored + .metrics + .progressed_steps + .iter() + .map(|(actor_id, step_index)| { + assert_eq!(*step_index, 0); + *actor_id + }) + .collect::>(); + assert_eq!( + progressed, + fixture.actor_ids[..progressed.len()], + "W9 demand profiles preserve the exact live FIFO prefix" + ); + + let (next_user, user_remaining, proof_bound, ref_time_bound) = match demand { + UserDemand::ActorOnly => { + assert_eq!(authored.metrics.user_calls, 0); + assert_eq!(authored.metrics.user_dispatch, Weight::zero()); + assert!(authored.metrics.next_user_weight.is_none()); + (Weight::zero(), user_limit, false, false) + } + UserDemand::ContinuousValid | UserDemand::RefTimeHeavy => { + assert!(authored.metrics.user_calls > 0); + let next_user = authored + .metrics + .next_user_weight + .expect("W9 valid demand records the first inadmissible call"); + let remaining = user_limit + .checked_sub(&authored.metrics.user_dispatch) + .unwrap_or_else(Weight::zero); + assert!(!next_user.all_lte(remaining)); + let proof_bound = next_user.proof_size() > remaining.proof_size(); + let ref_time_bound = next_user.ref_time() > remaining.ref_time(); + assert!( + proof_bound, + "retained W9 valid demand must exhaust User ProofSize" + ); + assert!( + !ref_time_bound, + "W9 fallback must not mislabel a ProofSize frontier as RefTime saturation" + ); + (next_user, remaining, proof_bound, ref_time_bound) + } + }; + let proof = replay_wasm + .then(|| replay_complete_block_in_wasm(&authored, wasm, &format!("W9-{}", demand.label()))); + let proof = proof.as_ref(); + println!( + "EXP_0066_W9_V1 {{\"profile\":\"{}\",\"dueActors\":{},\"dueSteps\":{},\"distinctProgressedActors\":{},\"fifoPrefixPreserved\":true,\"q1Preserved\":true,\"userCalls\":{},\"userRefTime\":{},\"userProofSize\":{},\"userRemainingRefTime\":{},\"userRemainingProofSize\":{},\"nextUserRefTime\":{},\"nextUserProofSize\":{},\"userProofSizeBound\":{proof_bound},\"userRefTimeBound\":{ref_time_bound},\"controlRefTime\":{},\"controlProofSize\":{},\"effectRefTime\":{},\"effectProofSize\":{},\"executionStorageProofBytes\":{},\"executionCompactProofBytes\":{},\"verificationStorageProofBytes\":{},\"verificationCompactProofBytes\":{}}}", + demand.label(), + fixture.actor_ids.len(), + authored.metrics.actor_steps, + authored.metrics.distinct_actors, + authored.metrics.user_calls, + authored.metrics.user_dispatch.ref_time(), + authored.metrics.user_dispatch.proof_size(), + user_remaining.ref_time(), + user_remaining.proof_size(), + next_user.ref_time(), + next_user.proof_size(), + authored.metrics.actor_control.ref_time(), + authored.metrics.actor_control.proof_size(), + authored.metrics.actor_effect.ref_time(), + authored.metrics.actor_effect.proof_size(), + proof.map_or(0, |metrics| metrics.execution_storage_proof_bytes), + proof.map_or(0, |metrics| metrics.execution_compact_proof_bytes), + proof.map_or(0, |metrics| metrics.verification_storage_proof_bytes), + proof.map_or(0, |metrics| metrics.verification_compact_proof_bytes), + ); + results.push((demand, progressed, authored.metrics)); + } + + let actor_only = &results[0]; + let proof_saturated = &results[1]; + let ref_time_heavy = &results[2]; + assert_eq!(actor_only.1, proof_saturated.1); + assert_eq!(actor_only.1, ref_time_heavy.1); + assert_eq!( + actor_only.2.actor_control, proof_saturated.2.actor_control, + "proof-saturated User demand cannot change W9 Actor Control accounting" + ); + assert_eq!( + actor_only.2.actor_control, ref_time_heavy.2.actor_control, + "RefTime-heavy User demand cannot change W9 Actor Control accounting" + ); + assert!( + ref_time_heavy.2.user_dispatch.ref_time() > proof_saturated.2.user_dispatch.ref_time(), + "W9 Router demand must be materially more RefTime-heavy than remarks" + ); + assert_eq!( + actor_only.2.actor_effect, proof_saturated.2.actor_effect, + "proof-saturated User demand cannot change W9 Actor effect accounting" + ); + assert_eq!( + actor_only.2.actor_effect, ref_time_heavy.2.actor_effect, + "RefTime-heavy User demand cannot change W9 Actor effect accounting" + ); + println!( + "EXP_0066_W9_COMPARISON_V1 {{\"actorPrefixesEqual\":true,\"actorControlEqual\":true,\"actorEffectEqual\":true,\"proofSaturatedUserRefTime\":{},\"refTimeHeavyUserRefTime\":{},\"proofSaturatedUserProofSize\":{},\"refTimeHeavyUserProofSize\":{},\"userProofSizeDelta\":{},\"refTimeFrontierFallback\":\"highest-valid-business-call-profile-remains-proof-size-bound\"}}", + proof_saturated.2.user_dispatch.ref_time(), + ref_time_heavy.2.user_dispatch.ref_time(), + proof_saturated.2.user_dispatch.proof_size(), + ref_time_heavy.2.user_dispatch.proof_size(), + proof_saturated + .2 + .user_dispatch + .proof_size() + .abs_diff(ref_time_heavy.2.user_dispatch.proof_size()), + ); +} + +fn run_control_phase_attribution_campaign( + wasm: &[u8], + schedule: WorkloadSchedule, + replay_wasm: bool, +) -> ControlAttributionResult { + let fixture = prepare_actor_fixture(wasm, CONTROL_ATTRIBUTION_ACTORS, schedule); + let mut pre_state = fixture.storage; + let mut parent = parent_header_for(pre_state.clone(), wasm, 1); + let mut result = ControlAttributionResult { + steps: Vec::new(), + prepass_steps: Vec::new(), + trigger_occurrences: Vec::new(), + prepass_control: Vec::new(), + final_control: Vec::new(), + wasm_proofs: Vec::new(), + }; + + for block_number in 2..CONTROL_ATTRIBUTION_BLOCKS.saturating_add(2) { + let authored = author_complete_block_after( + pre_state, + wasm, + &parent, + block_number, + UserDemand::ActorOnly, + &fixture.signer, + 0, + &fixture.actor_profiles, + ); + let trigger_occurrences = authored.metrics.trigger_occurrences.len() as u32; + let progressed_actors = authored + .metrics + .progressed_steps + .iter() + .map(|(actor_id, _)| *actor_id) + .collect::>(); + assert!( + authored + .metrics + .trigger_occurrences + .iter() + .all(|(actor_id, _)| !progressed_actors.contains(actor_id)), + "the captured FIFO cutoff defers every newly materialized Actor to a later block" + ); + assert_eq!( + authored.metrics.non_successful_steps, 0, + "Control attribution requires successful production-valid Transfer demand" + ); + assert_eq!( + authored.metrics.prepass_trigger_occurrences, trigger_occurrences, + "temporal materialization belongs entirely to the mandatory Prepass phase" + ); + assert!(authored.metrics.prepass_steps <= authored.metrics.actor_steps); + assert!( + authored + .metrics + .prepass_actor_control + .all_lte(authored.metrics.actor_control) + ); + assert!( + authored + .metrics + .prepass_actor_effect + .all_lte(authored.metrics.actor_effect) + ); + if replay_wasm { + result.wasm_proofs.push(replay_complete_block_in_wasm( + &authored, + wasm, + &format!("control-attribution-{}-{block_number}", schedule.label()), + )); + } + result.steps.push(authored.metrics.actor_steps); + result.prepass_steps.push(authored.metrics.prepass_steps); + result.trigger_occurrences.push(trigger_occurrences); + result + .prepass_control + .push(authored.metrics.prepass_actor_control); + result.final_control.push(authored.metrics.actor_control); + parent = authored.block.header.clone(); + pre_state = authored.post_state; + } + + println!( + "EXP_0066_CONTROL_ATTRIBUTION_V1 {{\"schedule\":\"{}\",\"actors\":{CONTROL_ATTRIBUTION_ACTORS},\"blocks\":{CONTROL_ATTRIBUTION_BLOCKS},\"steps\":{:?},\"prepassSteps\":{:?},\"triggerOccurrences\":{:?},\"prepassControlRefTime\":{:?},\"prepassControlProofSize\":{:?},\"finalControlRefTime\":{:?},\"finalControlProofSize\":{:?},\"executionRecordedKeys\":{:?},\"executionStorageProofBytes\":{:?},\"executionCompactProofBytes\":{:?}}}", + schedule.label(), + result.steps, + result.prepass_steps, + result.trigger_occurrences, + result + .prepass_control + .iter() + .map(Weight::ref_time) + .collect::>(), + result + .prepass_control + .iter() + .map(Weight::proof_size) + .collect::>(), + result + .final_control + .iter() + .map(Weight::ref_time) + .collect::>(), + result + .final_control + .iter() + .map(Weight::proof_size) + .collect::>(), + result + .wasm_proofs + .iter() + .map(|proof| proof.execution_recorded_keys.len()) + .collect::>(), + result + .wasm_proofs + .iter() + .map(|proof| proof.execution_storage_proof_bytes) + .collect::>(), + result + .wasm_proofs + .iter() + .map(|proof| proof.execution_compact_proof_bytes) + .collect::>(), + ); + result +} + +fn assert_production_weight_component( + actual: Weight, + base_ref_time: u64, + proof_size: u64, + io: DatabaseIo, +) { + let database = ::DbWeight::get(); + let expected = Weight::from_parts(base_ref_time, proof_size) + .saturating_add(database.reads(io.reads)) + .saturating_add(database.writes(io.writes)); + assert_eq!(actual, expected, "production Weight component I/O drifted"); +} + +#[test] +fn full_executive_schedule_ledger_counts_processed_units_and_refusals() { + let (large, schedule, demand) = match std::env::var("DEOS_SCHEDULE_LEDGER").as_deref() { + Err(std::env::VarError::NotPresent) | Ok("cadenced-small") => { + (false, WorkloadSchedule::CadencedOnly, UserDemand::ActorOnly) + } + Ok("cadenced") => (true, WorkloadSchedule::CadencedOnly, UserDemand::ActorOnly), + Ok("manual") => (true, WorkloadSchedule::ManualOnly, UserDemand::ActorOnly), + Ok("mixed") => ( + true, + WorkloadSchedule::MixedManualCadenced, + UserDemand::ActorOnly, + ), + Ok("mixed-user") => ( + true, + WorkloadSchedule::MixedManualCadenced, + UserDemand::ContinuousValid, + ), + _ => { + panic!("DEOS_SCHEDULE_LEDGER must be cadenced-small, cadenced, manual, mixed or mixed-user") + } + }; + let wasm = if large { + std::fs::read( + std::env::var_os("DEOS_PRODUCTION_WASM") + .expect("large ledger requires the accepted Wasm as genesis code, without executing it"), + ) + .expect("accepted genesis code is readable") + } else { + Vec::new() + }; + if large { + assert_eq!( + polkadot_sdk::sp_io::hashing::sha2_256(&wasm), + EXP_0095_PRODUCTION_WASM_SHA256, + "native large ledger must retain the current production genesis code" + ); + } + let actor_count = if large { + ::MaxActorIdentities::get() + .checked_sub(REFERENCE_SYSTEM_ACTOR_IDENTITIES) + .unwrap() + } else { + CONTROL_ATTRIBUTION_ACTORS + }; + let block_count = if large { + W1_TARGET_BLOCKS + } else { + CONTROL_ATTRIBUTION_BLOCKS + }; + type W = crate::weights::pallet_deos_actors::SubstrateWeight; + struct TickPass { + budget: Weight, + used: Weight, + io: DatabaseIo, + processed: u32, + removed: u64, + probes: u64, + refusals: u64, + refusal: Option, + due_after: bool, + } + let due_ticks = |now_tick: u64| { + pallet_deos_actors::ActorWaitingOccupancies::::iter() + .filter_map(|(key, count)| match key { + WakeupKey::Tick(tick) => (count > 0 && tick <= now_tick).then_some(tick), + WakeupKey::Block(_) => panic!("Tick-only fixture has no block-clock membership"), + }) + .collect::>() + }; + let tick_branch = W::at_time_trigger_occurrence().max(W::cadenced_trigger_occurrence()); + let retained = W::scheduler_wakeup_cursor_worker_partial().saturating_add(tick_branch); + let removed = W::scheduler_wakeup_cursor_worker_remove().saturating_add(tick_branch); + let clock_probe = W::scheduler_wakeup_cursor_worker_future(); + let run_tick_pass = |block: u32, now_tick: u64, budget: Weight| { + assert_eq!( + pallet_deos_actors::NextWakeupClock::::get(), + pallet_deos_actors::WakeupClock::Block + ); + let before = due_ticks(now_tick); + let mut meter = polkadot_sdk::sp_weights::WeightMeter::with_limit(budget); + let stats = Actors::drain_overdue_wakeups_cursor(block, &mut meter); + assert!(Actors::wakeup_worker_fault().is_none()); + assert_eq!( + pallet_deos_actors::DirtyObservationListState::::get().count, + 0 + ); + assert_eq!( + pallet_deos_actors::CrossingPendingFeedListState::::get().count, + 0 + ); + assert_eq!(stats.stale_entries, 0); + assert_eq!(stats.entries_scanned, stats.ready_entries); + let after = due_ticks(now_tick); + assert!( + after.is_subset(&before), + "this fixture creates only future temporal deadlines" + ); + let removed_count = before.difference(&after).count() as u64; + let retained_count = u64::from(stats.entries_scanned) + .checked_sub(removed_count) + .unwrap(); + let probes = 2 * (u64::from(stats.entries_scanned) + 1); + let refusals = u64::from(!after.is_empty()); + let expected = retained + .saturating_mul(retained_count + refusals) + .saturating_add(removed.saturating_mul(removed_count)) + .saturating_add(clock_probe.saturating_mul(probes)); + assert_eq!( + meter.consumed(), + expected, + "real worker consumption must match the path ledger" + ); + let refusal = after.first().map(|tick| { + let count = + pallet_deos_actors::ActorWaitingOccupancies::::get(WakeupKey::Tick(*tick)); + let disposition = if count == 1 { + pallet_deos_actors::WakeupBucketDisposition::Remove + } else { + pallet_deos_actors::WakeupBucketDisposition::Retain + }; + let before_refusal_charge = meter.remaining().saturating_add(retained); + let required = Actors::wakeup_cursor_drain_unit_weight_upper(disposition); + assert!(!required.all_lte(before_refusal_charge)); + let deficit = required.saturating_sub(before_refusal_charge); + serde_json::json!({ + "nextTick": tick, "bucketOccupancy": count, + "remainingBeforeRefusalCharge": [before_refusal_charge.ref_time(), before_refusal_charge.proof_size()], + "requiredAdmission": [required.ref_time(), required.proof_size()], + "deficit": [deficit.ref_time(), deficit.proof_size()], + }) + }); + let io = DatabaseIo::new(48, 29) + .saturating_mul(retained_count + refusals) + .saturating_add(DatabaseIo::new(92, 51).saturating_mul(removed_count)) + .saturating_add(DatabaseIo::new(6, 0).saturating_mul(probes)); + TickPass { + budget, + used: meter.consumed(), + io, + processed: stats.entries_scanned, + removed: removed_count, + probes, + refusals, + refusal, + due_after: !after.is_empty(), + } + }; + if large { + eprintln!( + "Schedule ledger ({}): preparing {actor_count} System Actors", + schedule.label() + ); + } + let fixture = prepare_actor_fixture(&wasm, actor_count, schedule); + if large { + eprintln!("Schedule ledger: fixture prepared"); + } + let collect_occurrences = || { + System::events() + .into_iter() + .filter_map(|record| match record.event { + RuntimeEvent::Actors(Event::TriggerOccurrenceProcessed { + actor_id, + trigger_family, + .. + }) => Some((actor_id, trigger_family)), + _ => None, + }) + .collect::>() + }; + let mut pre_state = fixture.storage; + let mut parent = parent_header_for(pre_state.clone(), &wasm, 1); + let limits = BlockResourceBudgetValue::get().limits(); + let coordinator = W::materialization_coordinator_base(); + let minima = [0, 1, 2].map(Actors::materialization_family_minimum); + let available = limits + .actor_control() + .saturating_sub(W::scheduler_on_initialize_cutoff()); + let configured = coordinator.saturating_add(Actors::materialization_weight_limit()); + let family_limit = Weight::from_parts( + available.ref_time().min(configured.ref_time()), + available.proof_size().min(configured.proof_size()), + ) + .saturating_sub(coordinator); + assert!( + minima + .iter() + .fold(Weight::zero(), |sum, w| sum.saturating_add(*w)) + .all_lte(family_limit) + ); + let non_temporal = W::scheduler_on_initialize_cutoff() + .saturating_add(coordinator) + .saturating_add(W::crossing_worker_base()) + .saturating_add(W::observation_fanout_base()) + .saturating_add(W::scheduler_paged_tombstone_drain(1).saturating_mul(2)) + .saturating_add(W::scheduler_on_idle_base()) + .saturating_add(W::block_resource_finalize()); + let step_control = W::scheduler_paged_tombstone_drain(1) + .saturating_add(W::scheduler_actor_state_probe()) + .saturating_add( + W::scheduler_paged_consume_preserve_page().max(W::scheduler_paged_consume_delete_page()), + ) + .saturating_add(W::scheduler_inner_opening_user_complete_header_max()) + .saturating_add(W::action_invocation_receipt()); + let mut rows = Vec::new(); + let mut signer_nonce = 0; + for block in 2..block_count + 2 { + let authored = author_complete_block_after( + pre_state, + &wasm, + &parent, + block, + demand, + &fixture.signer, + signer_nonce, + &fixture.actor_profiles, + ); + assert_successful_transfer_outcomes(&authored.metrics); + assert_eq!(authored.metrics.actor_steps, authored.metrics.prepass_steps); + assert_eq!( + authored.block.extrinsics.len(), + 3 + authored.metrics.user_calls as usize + ); + signer_nonce += authored.metrics.user_calls; + let user_frontier = match demand { + UserDemand::ActorOnly => { + assert_eq!(authored.metrics.user_calls, 0); + assert_eq!(authored.metrics.user_dispatch, Weight::zero()); + assert!(authored.metrics.next_user_weight.is_none()); + None + } + UserDemand::ContinuousValid => { + assert!(authored.metrics.user_calls > 0); + let required = authored + .metrics + .next_user_weight + .expect("first inadmissible valid call"); + let remaining = limits + .user_base_turn() + .checked_sub(&authored.metrics.user_dispatch) + .unwrap(); + assert!(!required.all_lte(remaining)); + let deficit = required.saturating_sub(remaining); + Some(serde_json::json!({ + "required": [required.ref_time(), required.proof_size()], + "remaining": [remaining.ref_time(), remaining.proof_size()], + "deficit": [deficit.ref_time(), deficit.proof_size()], + })) + } + UserDemand::RefTimeHeavy => unreachable!("schedule ledger excludes Router demand"), + }; + let mut prefix = TestExternalities::new_with_code_and_state( + &wasm, + authored.pre_state.clone(), + crate::VERSION.state_version(), + ); + let recorder = Recorder::::default(); + prefix.register_extension(ProofSizeExt::new(RecordingProofSizeProvider::new( + recorder.clone(), + ))); + let (cursor, passes, reference_occurrences) = prefix.execute_with_recorder(recorder, || { + Executive::initialize_block(&authored.block.header); + for extrinsic in &authored.block.extrinsics[..2] { + Executive::apply_extrinsic(extrinsic.clone()) + .expect("prefix valid") + .expect("inherent succeeds"); + } + assert_eq!( + pallet_deos_actors::DirtyObservationListState::::get().count, + 0 + ); + assert_eq!( + pallet_deos_actors::CrossingPendingFeedListState::::get().count, + 0 + ); + assert!(!pallet_deos_actors::ObservationFanoutWorkerFaultState::< + Runtime, + >::exists()); + assert!(!pallet_deos_actors::CrossingWorkerFaultState::::exists()); + let active = pallet_deos_actors::ActiveActorCount::::get(); + assert!(active < ::MaxActiveActors::get()); + assert!( + Actors::combined_queue_occupancy() <= u64::from(active), + "single-ticket population bounds live Ready capacity throughout materialization" + ); + assert!( + Actors::queue_tail() - Actors::queue_head() + < u64::from(::MaxQueueLength::get()), + "this composition excludes saturated Ready cleanup" + ); + let now_tick = + crate::Timestamp::get() / ::CadenceTickMillis::get(); + let cursor = Actors::materialization_family_cursor(); + let mut remaining = family_limit; + let mut passes = Vec::new(); + // Use the published minimum envelopes; the empty peer families spend only their bases. + // Full-block event order and both phase totals below falsify this restricted composition. + for offset in 0u8..3 { + let family = (cursor + offset) % 3; + let reserved = ((offset + 1)..3).fold(Weight::zero(), |sum, later| { + sum.saturating_add(minima[usize::from((cursor + later) % 3)]) + }); + let budget = remaining.checked_sub(&reserved).unwrap(); + let used = if family == 0 { + let pass = run_tick_pass(block, now_tick, budget); + let used = pass.used; + passes.push(pass); + used + } else if family == 1 { + W::crossing_worker_base() + } else { + W::observation_fanout_base() + }; + assert!(used.all_lte(budget)); + remaining = remaining.checked_sub(&used).unwrap(); + } + if cursor == 0 && remaining != Weight::zero() && !due_ticks(now_tick).is_empty() { + passes.push(run_tick_pass(block, now_tick, remaining)); + } + let processed = passes.iter().map(|p| p.processed).sum::(); + assert!( + processed < ::MaxWakeupsPerBlock::get(), + "fresh public-worker counters are equivalent only below the shared scan cap" + ); + assert_eq!( + pallet_deos_actors::ActiveActorCount::::get(), + active + ); + assert!(Actors::combined_queue_occupancy() <= u64::from(active)); + let (occurrences, reference_occurrences): (Vec<_>, Vec<_>) = collect_occurrences() + .into_iter() + .partition(|(id, _)| fixture.actor_profiles.contains_key(id)); + assert!( + reference_occurrences + .iter() + .all(|(id, _)| schedule == WorkloadSchedule::ManualOnly + && *id == primitives::ecosystem::actor_ids::FEE_SINK_ACTORS_ID) + ); + assert_eq!( + occurrences, authored.metrics.trigger_occurrences, + "real worker replay preserves exact useful occurrence order" + ); + assert_eq!( + processed as usize, + occurrences.len() + reference_occurrences.len() + usize::from(block == 2) + ); + (cursor, passes, reference_occurrences) + }); + let mut finalized = TestExternalities::new_with_code_and_state( + &wasm, + authored.post_state.clone(), + crate::VERSION.state_version(), + ); + let (head_probes, frontier) = finalized.execute_with(|| { + assert_eq!(collect_occurrences().into_iter() + .filter(|(id, _)| !fixture.actor_profiles.contains_key(id)).collect::>(), reference_occurrences, + "reference occurrence order also matches the full author"); + let (_, cutoff) = Actors::prepass_execution_cutoff().unwrap(); + let (_, head) = Actors::paged_head_entry().expect("ledger cohort retains a head"); + assert!(head.eligible_at <= block); + (u64::from(head.ticket < cutoff) * 2, serde_json::json!({ + "actorId": head.actor_id, "ticket": head.ticket, "eligibleAt": head.eligible_at, + "cutoff": cutoff, "readyOccupancy": Actors::combined_queue_occupancy(), + "initialTickBucketRemaining": pallet_deos_actors::ActorWaitingOccupancies::::get(WakeupKey::Tick(1)), + })) + }); + let temporal = passes + .iter() + .fold(Weight::zero(), |sum, p| sum.saturating_add(p.used)); + let steps = u64::from(authored.metrics.actor_steps); + let expected = non_temporal + .saturating_add(temporal) + .saturating_add(step_control.saturating_mul(steps)) + .saturating_add(W::scheduler_actor_state_probe().saturating_mul(head_probes)); + assert_eq!(authored.metrics.actor_control, expected); + let idle = W::scheduler_on_idle_base() + .saturating_add(W::block_resource_finalize()) + .saturating_add(W::scheduler_paged_tombstone_drain(1)) + .saturating_add(W::scheduler_actor_state_probe().saturating_mul(head_probes / 2)); + assert_eq!( + authored.metrics.prepass_actor_control, + expected.checked_sub(&idle).unwrap() + ); + let temporal_io = passes + .iter() + .fold(DatabaseIo::new(0, 0), |sum, p| sum.saturating_add(p.io)); + let all_io = DatabaseIo::new(32, 11) + .saturating_add(temporal_io) + .saturating_add(DatabaseIo::new(18, 9).saturating_mul(steps)) + .saturating_add(DatabaseIo::new(7, 0).saturating_mul(head_probes)); + rows.push(serde_json::json!({ + "block": block, "blockHash": format!("{:?}", authored.block.header.hash()), + "familyCursor": cursor, "steps": steps, "headStateProbes": head_probes, "frontier": frontier, + "referenceOccurrences": reference_occurrences.iter().map(|(id, _)| *id).collect::>(), + "userCalls": authored.metrics.user_calls, "userFrontier": user_frontier, + "userDispatch": [authored.metrics.user_dispatch.ref_time(), authored.metrics.user_dispatch.proof_size()], + "effect": [authored.metrics.actor_effect.ref_time(), authored.metrics.actor_effect.proof_size()], + "prepassControl": [authored.metrics.prepass_actor_control.ref_time(), authored.metrics.prepass_actor_control.proof_size()], + "control": [expected.ref_time(), expected.proof_size()], "generatedControlIo": [all_io.reads, all_io.writes], + "passes": passes.iter().map(|p| serde_json::json!({"processed": p.processed, "removedBuckets": p.removed, + "clockProbes": p.probes, "admissionRefusals": p.refusals, "dueAfter": p.due_after, + "budget": [p.budget.ref_time(), p.budget.proof_size()], "refusal": p.refusal.as_ref(), + "charged": [p.used.ref_time(), p.used.proof_size()], "refTimeSelectedIo": [p.io.reads, p.io.writes]})).collect::>(), + })); + if large && (block - 1).is_multiple_of(10) { + eprintln!("Schedule ledger: {} blocks verified", block - 1); + } + parent = authored.block.header.clone(); + pre_state = authored.post_state; + } + let reference = large.then(|| { + let snapshot = actor_lifecycle_observation( + &wasm, + &pre_state, + &[primitives::ecosystem::actor_ids::FEE_SINK_ACTORS_ID], + ); + let actor = &snapshot["actors"][0]; + assert_eq!(actor["active"], true); + assert_eq!(actor["temporalAnchorTick"], 24); + if schedule == WorkloadSchedule::ManualOnly { + assert_eq!(actor["pendingSignal"], true); + assert_eq!(actor["queueTicket"], actor_count); + assert!(actor["triggerWakeupTick"].is_null()); + } else { + assert_eq!( + actor["triggerWakeupTick"], + 24 + primitives::ecosystem::params::FEE_SINK_CADENCE_TICKS + ); + assert_eq!(actor["pendingSignal"], false); + assert!(actor["queueTicket"].is_null()); + } + assert!(actor["run"].is_null()); + snapshot + }); + println!( + "ACTOR_SCHEDULE_PATH_LEDGER_V1 {}", + serde_json::json!({ + "mode": "native-Tick-only-public-worker-replay", "schedule": schedule.label(), "demand": demand.label(), + "workloadActorType": "System", "blocks": rows, + "workloadActors": actor_count, "eligibleBlocks": block_count, "referenceTerminalState": reference, + "genesisCodeSha256": large.then(|| format!("{:?}", H256::from(polkadot_sdk::sp_io::hashing::sha2_256(&wasm)))), + }) + ); +} + +#[test] +fn full_executive_empty_workload_control_baseline_has_explicit_owners() { + type W = crate::weights::pallet_deos_actors::SubstrateWeight; + control_temporal_weight_io_ledger_matches_production_selectors(); + let components = [ + ( + "cutoff", + W::scheduler_on_initialize_cutoff(), + 12_292_000, + 1_560, + DatabaseIo::new(2, 2), + 1, + ), + ( + "coordinator", + W::materialization_coordinator_base(), + 25_842_000, + 5_982, + DatabaseIo::new(10, 1), + 1, + ), + ( + "clock-probe", + W::scheduler_wakeup_cursor_worker_future(), + 24_305_000, + 6_566, + DatabaseIo::new(6, 0), + 2, + ), + ( + "crossing-base", + W::crossing_worker_base(), + 6_984_000, + 1_543, + DatabaseIo::new(2, 0), + 1, + ), + ( + "fanout-base", + W::observation_fanout_base(), + 6_775_000, + 1_629, + DatabaseIo::new(2, 0), + 1, + ), + ( + "empty-discovery", + W::scheduler_paged_tombstone_drain(1), + 35_663_374, + 3_111, + DatabaseIo::new(4, 2), + 2, + ), + ( + "idle-base", + W::scheduler_on_idle_base(), + 19_626_000, + 1_560, + DatabaseIo::new(7, 2), + 1, + ), + ( + "finalize", + W::block_resource_finalize(), + 9_010_000, + 1_560, + DatabaseIo::new(1, 2), + 1, + ), + ]; + let mut fixed = Weight::zero(); + let mut fixed_io = DatabaseIo::new(0, 0); + for (_, weight, base, proof, io, frequency) in &components { + assert_production_weight_component(*weight, *base, *proof, *io); + fixed = fixed.saturating_add(weight.saturating_mul(*frequency)); + fixed_io = fixed_io.saturating_add(io.saturating_mul(*frequency)); + } + assert_eq!(fixed_io, DatabaseIo::new(44, 11)); + let tick_branch = W::scheduler_wakeup_cursor_worker_remove() + .saturating_add(W::at_time_trigger_occurrence().max(W::cadenced_trigger_occurrence())); + let first_block_extra = + tick_branch.saturating_add(W::scheduler_wakeup_cursor_worker_future().saturating_mul(2)); + let first_extra_io = DatabaseIo::new(65, 35) + .saturating_add(DatabaseIo::new(27, 16)) + .saturating_add(DatabaseIo::new(6, 0).saturating_mul(2)); + let fixture = prepare_actor_fixture(&[], 0, WorkloadSchedule::ManualOnly); + let fee_sink = primitives::ecosystem::actor_ids::FEE_SINK_ACTORS_ID; + let before = actor_lifecycle_observation(&[], &fixture.storage, &[fee_sink]); + assert!(before["actors"][0]["temporalAnchorTick"].is_null()); + assert_eq!(before["actors"][0]["triggerWakeupTick"], 0); + let first = author_complete_block( + fixture.storage, + &[], + 2, + UserDemand::ActorOnly, + &fixture.signer, + &fixture.actor_profiles, + ); + let first_snapshot = actor_lifecycle_observation(&[], &first.post_state, &[fee_sink]); + assert_eq!(first_snapshot["actors"][0]["temporalAnchorTick"], 24); + assert_eq!( + first_snapshot["actors"][0]["triggerWakeupTick"], + 24 + primitives::ecosystem::params::FEE_SINK_CADENCE_TICKS + ); + assert_eq!( + first.metrics.actor_control, + fixed.saturating_add(first_block_extra) + ); + let second = author_complete_block_after( + first.post_state, + &[], + &first.block.header, + 3, + UserDemand::ActorOnly, + &fixture.signer, + 0, + &fixture.actor_profiles, + ); + assert_eq!(second.metrics.actor_control, fixed); + let second_snapshot = actor_lifecycle_observation(&[], &second.post_state, &[fee_sink]); + assert_eq!(first_snapshot["actors"], second_snapshot["actors"]); + for snapshot in [&first_snapshot, &second_snapshot] { + assert!(snapshot["events"].as_array().unwrap().is_empty()); + assert_eq!(snapshot["actors"][0]["pendingSignal"], false); + assert!(snapshot["actors"][0]["queueTicket"].is_null()); + assert!(snapshot["actors"][0]["run"].is_null()); + } + let idle = W::scheduler_on_idle_base() + .saturating_add(W::block_resource_finalize()) + .saturating_add(W::scheduler_paged_tombstone_drain(1)); + for metrics in [&first.metrics, &second.metrics] { + assert_eq!( + metrics + .actor_control + .checked_sub(&metrics.prepass_actor_control), + Some(idle) + ); + assert_eq!(metrics.actor_steps, 0); + assert_eq!(metrics.actor_effect, Weight::zero()); + } + println!( + "ACTOR_BASELINE_CONTROL_LEDGER_V1 {}", + serde_json::json!({ + "fixed": [fixed.ref_time(), fixed.proof_size()], + "fixedGeneratedIo": [fixed_io.reads, fixed_io.writes], + "firstExtraRefTimeSelectedIo": [first_extra_io.reads, first_extra_io.writes], + "firstTotalRefTimeSelectedIo": [fixed_io.reads + first_extra_io.reads, fixed_io.writes + first_extra_io.writes], + "feeSinkActor": fee_sink, "initialAnchor": before["actors"][0]["temporalAnchorTick"], + "initializedAnchor": first_snapshot["actors"][0]["temporalAnchorTick"], + "initializedDeadline": first_snapshot["actors"][0]["triggerWakeupTick"], + "components": components.map(|(name, weight, _, _, io, frequency)| serde_json::json!({ + "name": name, "weight": [weight.ref_time(), weight.proof_size()], + "reads": io.reads, "writes": io.writes, "frequency": frequency, + })), + "firstBlockExtra": [first_block_extra.ref_time(), first_block_extra.proof_size()], + "firstPrepass": [first.metrics.prepass_actor_control.ref_time(), first.metrics.prepass_actor_control.proof_size()], + "secondPrepass": [second.metrics.prepass_actor_control.ref_time(), second.metrics.prepass_actor_control.proof_size()], + }) + ); +} + +#[test] +fn control_temporal_weight_io_ledger_matches_production_selectors() { + type ProductionWeight = crate::weights::pallet_deos_actors::SubstrateWeight; + + let coordinator_io = DatabaseIo::new(10, 1); + let future_probe_io = DatabaseIo::new(6, 0); + let partial_worker_io = DatabaseIo::new(21, 13); + let remove_worker_io = DatabaseIo::new(65, 35); + let at_time_occurrence_io = DatabaseIo::new(20, 9); + let cadenced_occurrence_io = DatabaseIo::new(27, 16); + let close_contingency_io = DatabaseIo::new(66, 65); + let fault_contingency_io = DatabaseIo::new(1, 1); + + assert_production_weight_component( + ProductionWeight::materialization_coordinator_base(), + 25_842_000, + 5_982, + coordinator_io, + ); + assert_production_weight_component( + ProductionWeight::scheduler_wakeup_cursor_worker_future(), + 24_305_000, + 6_566, + future_probe_io, + ); + assert_production_weight_component( + ProductionWeight::scheduler_wakeup_cursor_worker_partial(), + 235_997_000, + 7_959, + partial_worker_io, + ); + assert_production_weight_component( + ProductionWeight::scheduler_wakeup_cursor_worker_remove(), + 806_400_000, + 55_857, + remove_worker_io, + ); + assert_production_weight_component( + ProductionWeight::at_time_trigger_occurrence(), + 349_561_000, + 8_451, + at_time_occurrence_io, + ); + assert_production_weight_component( + ProductionWeight::cadenced_trigger_occurrence(), + 536_041_000, + 8_450, + cadenced_occurrence_io, + ); + assert_production_weight_component( + ProductionWeight::close_actor(), + 973_883_000, + 81_886, + close_contingency_io, + ); + assert_production_weight_component( + ProductionWeight::record_wakeup_worker_fault(), + 9_917_000, + 1_503, + fault_contingency_io, + ); + + let no_due_probe_io = future_probe_io.saturating_mul(2); + let retained_actual_io = partial_worker_io.saturating_add(cadenced_occurrence_io); + let removed_actual_io = remove_worker_io.saturating_add(cadenced_occurrence_io); + let retained_admission_io = retained_actual_io + .saturating_add(close_contingency_io) + .saturating_add(fault_contingency_io); + let removed_admission_io = removed_actual_io + .saturating_add(close_contingency_io) + .saturating_add(fault_contingency_io); + let rearm_topology_io = DatabaseIo::new( + cadenced_occurrence_io + .reads + .saturating_sub(at_time_occurrence_io.reads), + cadenced_occurrence_io + .writes + .saturating_sub(at_time_occurrence_io.writes), + ); + + assert_eq!(no_due_probe_io, DatabaseIo::new(12, 0)); + assert_eq!(retained_actual_io, DatabaseIo::new(48, 29)); + assert_eq!(removed_actual_io, DatabaseIo::new(92, 51)); + assert_eq!(retained_admission_io, DatabaseIo::new(115, 95)); + assert_eq!(removed_admission_io, DatabaseIo::new(159, 117)); + assert_eq!(rearm_topology_io, DatabaseIo::new(7, 7)); + + let cadenced = ProductionWeight::cadenced_trigger_occurrence(); + let at_time = ProductionWeight::at_time_trigger_occurrence(); + let temporal_admission = Weight::from_parts( + cadenced.ref_time().max(at_time.ref_time()), + cadenced.proof_size().max(at_time.proof_size()), + ); + let temporal_admission_proof_size = temporal_admission.proof_size(); + assert!( + cadenced.ref_time() > at_time.ref_time(), + "generated branch I/O follows the Cadenced RefTime owner" + ); + let retained_actual = + ProductionWeight::scheduler_wakeup_cursor_worker_partial().saturating_add(temporal_admission); + let removed_actual = + ProductionWeight::scheduler_wakeup_cursor_worker_remove().saturating_add(temporal_admission); + let retained_admission = ProductionWeight::scheduler_wakeup_cursor_worker_partial() + .saturating_add(temporal_admission) + .saturating_add(ProductionWeight::close_actor()) + .saturating_add(ProductionWeight::record_wakeup_worker_fault()); + let removed_admission = ProductionWeight::scheduler_wakeup_cursor_worker_remove() + .saturating_add(temporal_admission) + .saturating_add(ProductionWeight::close_actor()) + .saturating_add(ProductionWeight::record_wakeup_worker_fault()); + assert_eq!( + retained_admission, + Actors::wakeup_cursor_drain_unit_weight_upper( + pallet_deos_actors::WakeupBucketDisposition::Retain, + ), + "retained temporal ledger must match the production selector" + ); + assert_eq!( + removed_admission, + Actors::wakeup_cursor_drain_unit_weight_upper( + pallet_deos_actors::WakeupBucketDisposition::Remove, + ), + "removed temporal ledger must match the production selector" + ); + + assert_eq!( + retained_actual, + retained_admission + .saturating_sub(ProductionWeight::close_actor()) + .saturating_sub(ProductionWeight::record_wakeup_worker_fault()), + "the meter charges the temporal branch maximum, not a Cadenced-only component sum", + ); + assert_eq!( + removed_actual, + removed_admission + .saturating_sub(ProductionWeight::close_actor()) + .saturating_sub(ProductionWeight::record_wakeup_worker_fault()), + ); + + println!( + "EXP_0066_CONTROL_IO_LEDGER_V3 {{\"temporalOccurrenceAdmissionProofSize\":{temporal_admission_proof_size},\"coordinatorReads\":{},\"coordinatorWrites\":{},\"noDueTwoClockProbeReads\":{},\"noDueTwoClockProbeWrites\":{},\"retainedBranchRefTimeReads\":{},\"retainedBranchRefTimeWrites\":{},\"removedBranchRefTimeReads\":{},\"removedBranchRefTimeWrites\":{},\"cadencedRearmTopologyReads\":{},\"cadencedRearmTopologyWrites\":{},\"retainedAdmissionReads\":{},\"retainedAdmissionWrites\":{},\"removedAdmissionReads\":{},\"removedAdmissionWrites\":{},\"retainedActualRefTime\":{},\"retainedActualProofSize\":{},\"retainedAdmissionRefTime\":{},\"retainedAdmissionProofSize\":{},\"removedActualRefTime\":{},\"removedActualProofSize\":{},\"removedAdmissionRefTime\":{},\"removedAdmissionProofSize\":{}}}", + coordinator_io.reads, + coordinator_io.writes, + no_due_probe_io.reads, + no_due_probe_io.writes, + retained_actual_io.reads, + retained_actual_io.writes, + removed_actual_io.reads, + removed_actual_io.writes, + rearm_topology_io.reads, + rearm_topology_io.writes, + retained_admission_io.reads, + retained_admission_io.writes, + removed_admission_io.reads, + removed_admission_io.writes, + retained_actual.ref_time(), + retained_actual.proof_size(), + retained_admission.ref_time(), + retained_admission.proof_size(), + removed_actual.ref_time(), + removed_actual.proof_size(), + removed_admission.ref_time(), + removed_admission.proof_size(), + ); +} + +fn assert_control_phase_attribution_campaign(wasm: &[u8], replay_wasm: bool) { + control_temporal_weight_io_ledger_matches_production_selectors(); + let manual = + run_control_phase_attribution_campaign(wasm, WorkloadSchedule::ManualOnly, replay_wasm); + let cadenced = + run_control_phase_attribution_campaign(wasm, WorkloadSchedule::CadencedOnly, replay_wasm); + if replay_wasm { + for result in [&manual, &cadenced] { + assert_eq!( + result.wasm_proofs.len(), + CONTROL_ATTRIBUTION_BLOCKS as usize + ); + assert!( + result + .wasm_proofs + .iter() + .all(|proof| proof.remaining_residual_node_bytes == 0), + "matched-phase proofs must be covered by recorded key paths plus committed-root reconstruction" + ); + } + } + assert_eq!(manual.steps, vec![14, 17, 17, 17, 17, 17, 1, 0, 0]); + assert_eq!(manual.prepass_steps, manual.steps); + assert_eq!(manual.trigger_occurrences, vec![0; 9]); + assert_eq!(cadenced.steps, vec![0, 0, 4, 0, 0, 4, 2, 13, 5]); + assert_eq!(cadenced.prepass_steps, cadenced.steps); + assert_eq!( + cadenced.trigger_occurrences, + vec![16, 19, 13, 18, 19, 13, 10, 2, 12] + ); + assert!(manual.steps.iter().sum::() > cadenced.steps.iter().sum()); + assert!( + cadenced + .trigger_occurrences + .iter() + .zip(&cadenced.steps) + .any(|(triggers, steps)| *triggers > 0 && *steps == 0) + ); + println!( + "EXP_0066_CONTROL_ATTRIBUTION_COMPARISON_V1 {{\"manualSteps\":{},\"cadencedSteps\":{},\"cadencedTriggerOccurrences\":{},\"materializationPrepassOnly\":true,\"materializedActorsDeferredByCutoff\":true}}", + manual.steps.iter().sum::(), + cadenced.steps.iter().sum::(), + cadenced.trigger_occurrences.iter().sum::(), + ); +} + +/// Author a first block from the complete reference preset, without synthetic +/// timestamp/validation/resource writes or direct Actor-hook invocation. +fn author_reference_first_block(wasm: &[u8]) -> (Storage, Block, InherentData) { + let storage = reference_genesis_storage(wasm); + let parent = parent_header_for(storage.clone(), wasm, 0); + let profiles = BTreeMap::new(); + let authored = author_complete_block( + storage, + wasm, + 1, + UserDemand::ActorOnly, + &sr25519::Pair::from_seed(&[0u8; 32]), + &profiles, + ); + assert_eq!(*authored.block.header.parent_hash(), parent.hash()); + (authored.pre_state, authored.block, authored.inherent_data) +} + +#[test] +fn reference_full_block_fixture_replays_through_executive() { + let (storage, block, data) = author_reference_first_block(&[]); + let authored = AuthoredBlock { + pre_state: storage.clone(), + post_state: storage, + block, + inherent_data: data, + metrics: FullExecutiveBlockMetrics { + actor_steps: 0, + distinct_actors: 0, + progressed_steps: Vec::new(), + opening_steps: 0, + middle_steps: 0, + final_steps: 0, + transfer_steps: 0, + swap_out_steps: 0, + stop_cycle_steps: 0, + non_successful_steps: 0, + completed_cycles: 0, + completed_cycle_actors: Vec::new(), + failed_cycle_actors: Vec::new(), + suspended_steps: Vec::new(), + continued_steps: Vec::new(), + closed_actors: Vec::new(), + paused_actors: Vec::new(), + resumed_actors: Vec::new(), + trigger_occurrences: Vec::new(), + user_calls: 0, + next_user_weight: None, + prepass_steps: 0, + prepass_trigger_occurrences: 0, + prepass_actor_control: Weight::zero(), + prepass_actor_effect: Weight::zero(), + actor_control: Weight::zero(), + actor_effect: Weight::zero(), + user_dispatch: Weight::zero(), + queue_head: 0, + queue_tail: 0, + }, + }; + assert_complete_block_replays_natively(&authored, &[]); +} + +#[test] +fn full_executive_step_observations_preserve_ticket_order_not_actor_id_order() { + let mut ext = TestExternalities::new_with_code_and_state( + &[], + reference_genesis_storage(&[]), + crate::VERSION.state_version(), + ); + let (actor_ids, profiles) = ext.execute_with(|| { + System::set_block_number(1); + let steps = actors_integration_tests::transfer_contract_steps( + super::common::BOB, + primitives::AssetKind::Native, + crate::EXISTENTIAL_DEPOSIT, + ); + let actor_ids = (0..2) + .map(|_| { + let actor_id = actors_integration_tests::create_system( + ALICE, + actors_integration_tests::manual_schedule(), + None, + steps.clone(), + ); + actors_integration_tests::fund_native(actor_id, 1_000 * crate::EXISTENTIAL_DEPOSIT); + actor_id + }) + .collect::>(); + assert!(actor_ids[0] < actor_ids[1]); + for actor_id in actor_ids.iter().rev() { + assert_ok!(Actors::manual_trigger(RuntimeOrigin::root(), *actor_id)); + } + let profiles = actor_ids + .iter() + .map(|actor_id| { + ( + *actor_id, + WorkloadActorProfile { + step_count: 1, + opening_predicates_per_step: 0, + task: Some(WorkloadTask::Transfer), + }, + ) + }) + .collect(); + (actor_ids, profiles) + }); + ext.commit_all().expect("reversed-ticket fixture commits"); + let storage = ext.execute_with(current_top_storage); + let parent = parent_header_for(storage.clone(), &[], 1); + let signer = sr25519::Pair::from_seed(&[66; 32]); + let authored = author_complete_block_after( + storage, + &[], + &parent, + 2, + UserDemand::ActorOnly, + &signer, + 0, + &profiles, + ); + assert_successful_transfer_outcomes(&authored.metrics); + assert_eq!( + authored.metrics.progressed_steps, + vec![(actor_ids[1], 0), (actor_ids[0], 0)], + "Step observations must retain actual FIFO event chronology" + ); +} + +#[test] +fn full_executive_manual_phase_control_stops_use_live_head_and_stage_budget() { + type W = crate::weights::pallet_deos_actors::SubstrateWeight; + let snapshot = || { + let (block, cutoff) = Actors::prepass_execution_cutoff().expect("frozen cutoff exists"); + assert_eq!(block, System::block_number()); + assert!(!pallet_deos_actors::GlobalCircuitBreaker::::get()); + let head = Actors::paged_head_entry().map(|(position, entry)| { + let (location, cell) = + Actors::actor_control_cell(entry.actor_id).expect("live primary exists"); + assert_eq!(position, entry.ticket); + assert_eq!( + location, + pallet_deos_actors::ActorControlLocation::Ready { + ticket: entry.ticket + } + ); + assert_eq!(cell.actor_id, entry.actor_id); + assert_eq!(cell.eligible_at, Some(entry.eligible_at)); + assert_eq!( + cell.identity.actor_class.actor_type(), + pallet_deos_actors::ActorType::System + ); + assert_eq!(cell.identity.cycle_nonce, 0); + assert_eq!(cell.hot.cycle_state, pallet_deos_actors::CycleState::Idle); + assert!(cell.hot.pending_signal); + assert!(!cell.hot.lifecycle.is_paused()); + assert!(cell.hot.terminal_at.is_none()); + assert_eq!(cell.hot.unsuccessful_attempt_streak, 0); + let contract = pallet_deos_actors::ActorContractHeads::::get(entry.actor_id) + .expect("ordinary Opening has its Contract head"); + assert_eq!(contract.header.trigger, Trigger::Manual); + assert_eq!(contract.header.step_count, 1); + assert_eq!(contract.header.completion, CompletionPolicy::Persistent); + assert!(contract.header.window.is_none()); + assert!(contract.header.auto_close_at_cycle_nonce.is_none()); + assert_eq!(contract.first_step_resources, Some(cell.resources)); + assert!(entry.ticket < cutoff && entry.eligible_at <= block); + (entry, cell.resources) + }); + if head.is_none() { + assert_eq!(Actors::queue_head(), Actors::queue_tail()); + } + (cutoff, head) + }; + let fixture = prepare_actor_fixture( + &[], + CONTROL_ATTRIBUTION_ACTORS, + WorkloadSchedule::ManualOnly, + ); + let mut pre_state = fixture.storage; + let mut parent = parent_header_for(pre_state.clone(), &[], 1); + let limits = BlockResourceBudgetValue::get().limits(); + let idle_fixed = W::scheduler_on_idle_base().saturating_add(W::block_resource_finalize()); + let discovery_and_probe = + W::scheduler_paged_tombstone_drain(1).saturating_add(W::scheduler_actor_state_probe()); + let pair = |w: Weight| [w.ref_time(), w.proof_size()]; + let mut completed = 0usize; + let mut rows = Vec::new(); + for block in 2..CONTROL_ATTRIBUTION_BLOCKS + 2 { + let authored = author_complete_block_after( + pre_state, + &[], + &parent, + block, + UserDemand::ActorOnly, + &fixture.signer, + 0, + &fixture.actor_profiles, + ); + assert_successful_transfer_outcomes(&authored.metrics); + assert_eq!(authored.metrics.actor_steps, authored.metrics.prepass_steps); + assert_eq!(authored.block.extrinsics.len(), 3); + completed += authored.metrics.completed_cycles as usize; + + // Replay only the exact inherent prefix on a disposable clone. Observation reads must not + // enter the original block's recorder or influence subsequent extrinsics/idle execution. + let mut prefix = TestExternalities::new_with_code_and_state( + &[], + authored.pre_state.clone(), + crate::VERSION.state_version(), + ); + let recorder = Recorder::::default(); + prefix.register_extension(ProofSizeExt::new(RecordingProofSizeProvider::new( + recorder.clone(), + ))); + let (cutoff, head) = prefix.execute_with_recorder(recorder, || { + Executive::initialize_block(&authored.block.header); + for extrinsic in &authored.block.extrinsics { + Executive::apply_extrinsic(extrinsic.clone()) + .expect("prefix is valid") + .expect("inherent succeeds"); + } + let state = Actors::block_resource_state().expect("Prepass state exists"); + assert_eq!( + state.usage().actor_control_used(), + authored.metrics.prepass_actor_control + ); + assert_eq!( + state.usage().actor_effect_used(), + authored.metrics.prepass_actor_effect + ); + assert_eq!(state.outstanding_reservations(), 0); + snapshot() + }); + let mut finalized = TestExternalities::new_with_code_and_state( + &[], + authored.post_state.clone(), + crate::VERSION.state_version(), + ); + assert_eq!( + finalized.execute_with(snapshot), + (cutoff, head), + "idle preserves the observed head" + ); + if let Some((entry, resources)) = head { + assert_eq!(entry.actor_id, fixture.actor_ids[completed]); + let required = W::scheduler_paged_consume_preserve_page() + .max(W::scheduler_paged_consume_delete_page()) + .saturating_add(resources.control) + .saturating_add(Actors::close_cleanup_weight_upper()); + let prepass_remaining = limits + .actor_control() + .checked_sub(&authored.metrics.prepass_actor_control) + .unwrap() + .checked_sub(&idle_fixed) + .unwrap(); + let idle_remaining = limits + .actor_control() + .checked_sub(&authored.metrics.actor_control) + .unwrap(); + // No charge follows the terminal gate in either pass. Prepass withholds idle/finalization + // authority; idle spends that fixed work before its one discovery and state probe. + assert_eq!( + prepass_remaining.checked_sub(&idle_remaining), + Some(discovery_and_probe) + ); + let prepass_deficit = required.saturating_sub(prepass_remaining); + let idle_deficit = required.saturating_sub(idle_remaining); + for deficit in [prepass_deficit, idle_deficit] { + assert_eq!(deficit.ref_time(), 0); + assert!(deficit.proof_size() > 0); + } + let effect_remaining = limits + .actor_base_turn() + .checked_sub(&authored.metrics.prepass_actor_effect) + .unwrap(); + assert!(resources.effect.all_lte(effect_remaining)); + assert!( + required + .saturating_add(resources.effect) + .all_lte(prepass_remaining.saturating_add(effect_remaining)) + ); + rows.push(serde_json::json!({ + "block": block, "steps": authored.metrics.actor_steps, "actorId": entry.actor_id, + "ticket": entry.ticket, "cutoff": cutoff, "eligibleAt": entry.eligible_at, + "requiredControl": pair(required), "prepassRemaining": pair(prepass_remaining), + "idleRemaining": pair(idle_remaining), "prepassDeficit": pair(prepass_deficit), + "idleDeficit": pair(idle_deficit), "prepassEffectAndCombinedCapacityFit": true, + })); + } else { + assert_eq!(completed, fixture.actor_ids.len()); + assert_eq!( + authored + .metrics + .actor_control + .checked_sub(&authored.metrics.prepass_actor_control), + Some(idle_fixed.saturating_add(W::scheduler_paged_tombstone_drain(1))) + ); + rows.push( + serde_json::json!({"block": block, "steps": authored.metrics.actor_steps, "head": null}), + ); + } + parent = authored.block.header.clone(); + pre_state = authored.post_state; + } + assert_eq!( + rows + .iter() + .filter(|row| row.get("actorId").is_some()) + .count(), + 6 + ); + println!( + "ACTOR_MANUAL_PHASE_STOPS_V1 {}", + serde_json::json!({ + "mode": "native-inherent-prefix-and-final-state", "blocks": rows, + "idleFinalizationReservation": pair(idle_fixed), + }) + ); +} + +#[test] +fn full_executive_control_frontier_separates_capacity_from_service_eligibility() { + use crate::weights::pallet_deos_actors::SubstrateWeight as ProductionWeights; + use pallet_deos_actors::WeightInfo; + + for (count, schedule) in [ + (0, WorkloadSchedule::ManualOnly), + (128, WorkloadSchedule::ManualOnly), + (128, WorkloadSchedule::CadencedOnly), + (128, WorkloadSchedule::MixedManualCadenced), + ] { + let fixture = prepare_actor_fixture(&[], count, schedule); + let authored = author_complete_block( + fixture.storage, + &[], + 2, + UserDemand::ActorOnly, + &fixture.signer, + &fixture.actor_profiles, + ); + assert_successful_transfer_outcomes(&authored.metrics); + let remaining = BlockResourceBudgetValue::get() + .limits() + .actor_control() + .checked_sub(&authored.metrics.actor_control) + .expect("finalized Control usage fits its limit"); + let mut ext = TestExternalities::new_with_code_and_state( + &[], + authored.post_state, + crate::VERSION.state_version(), + ); + ext.execute_with(|| { + let (block, cutoff) = Actors::prepass_execution_cutoff().expect("block freezes its cutoff"); + assert_eq!(block, 2); + let Some((position, entry)) = Actors::paged_head_entry() else { + assert_eq!(count, 0); + assert_eq!(Actors::queue_head(), Actors::queue_tail()); + assert_eq!(authored.metrics.actor_steps, 0); + return; + }; + let (location, cell) = Actors::actor_control_cell(entry.actor_id) + .expect("live head has its canonical control cell"); + assert_eq!(cell.actor_id, entry.actor_id); + assert_eq!( + location, + pallet_deos_actors::ActorControlLocation::Ready { + ticket: entry.ticket + } + ); + assert_eq!(cell.eligible_at, Some(entry.eligible_at)); + // This is the ordinary gate's stored-cell expression, not a replay of its stop point. + // Discovery/state-probe charges precede it; finalized remainder is a later boundary. + let required = ProductionWeights::::scheduler_paged_consume_preserve_page() + .max(ProductionWeights::::scheduler_paged_consume_delete_page()) + .saturating_add(cell.resources.control) + .saturating_add(Actors::close_dispatch_weight_upper()); + assert_eq!(required, fixture.next_control_maximum); + assert_eq!(cell.resources.effect, fixture.next_effect_maximum); + let deficit = required.saturating_sub(remaining); + assert_eq!(deficit.ref_time(), 0); + assert!(deficit.proof_size() > 0); + let before_cutoff = entry.ticket < cutoff; + let due = entry.eligible_at <= block; + assert_eq!(before_cutoff, schedule != WorkloadSchedule::CadencedOnly); + assert!( + due, + "a due timestamp alone does not admit a post-cutoff ticket" + ); + assert_eq!( + authored.metrics.actor_steps > 0, + schedule == WorkloadSchedule::ManualOnly + ); + println!( + "ACTOR_CONTROL_FRONTIER_V1 {}", + serde_json::json!({ + "mode": "native-finalized-snapshot", + "schedule": schedule.label(), "block": block, "actors": count, + "steps": authored.metrics.actor_steps, "headPosition": position, + "headActor": entry.actor_id, "headTicket": entry.ticket, + "cutoff": cutoff, "eligibleAt": entry.eligible_at, + "beforeCutoff": before_cutoff, "due": due, + "storedControl": [cell.resources.control.ref_time(), cell.resources.control.proof_size()], + "ordinaryControlRequirement": [required.ref_time(), required.proof_size()], + "finalizedControlRemaining": [remaining.ref_time(), remaining.proof_size()], + "finalizedControlDeficit": [deficit.ref_time(), deficit.proof_size()], + }) + ); + }); + } +} + +#[test] +fn full_executive_w0_w1_fixture_covers_actor_only_and_valid_user_demand() { + assert_prepared_w0_w1(&[], false); +} + +#[test] +#[ignore = "requires exact current production Wasm via DEOS_PRODUCTION_WASM"] +fn full_executive_w0_w1_replays_exact_production_wasm() { + let path = std::env::var_os("DEOS_PRODUCTION_WASM") + .expect("DEOS_PRODUCTION_WASM must explicitly select the accepted production artifact"); + let wasm = std::fs::read(path).expect("selected production Wasm is readable"); + assert_eq!( + polkadot_sdk::sp_io::hashing::sha2_256(&wasm), + EXP_0095_PRODUCTION_WASM_SHA256, + "EXP-0066 preparation must remain bound to EXP-0095's accepted production Wasm" + ); + assert_prepared_w0_w1(&wasm, true); +} + +#[test] +#[ignore = "requires exact current production Wasm via DEOS_PRODUCTION_WASM"] +fn full_executive_w1_actor_only_100_block_campaign_replays_exact_production_wasm() { + let path = std::env::var_os("DEOS_PRODUCTION_WASM") + .expect("DEOS_PRODUCTION_WASM must explicitly select the accepted production artifact"); + let wasm = std::fs::read(path).expect("selected production Wasm is readable"); + assert_eq!( + polkadot_sdk::sp_io::hashing::sha2_256(&wasm), + EXP_0095_PRODUCTION_WASM_SHA256, + "EXP-0066 W1 must remain bound to EXP-0095's accepted production Wasm" + ); + run_schedule_campaign( + &wasm, + "W1", + UserDemand::ActorOnly, + WorkloadSchedule::MixedManualCadenced, + ); +} + +#[test] +#[ignore = "requires exact current production Wasm via DEOS_PRODUCTION_WASM"] +fn full_executive_w1_continuous_valid_user_100_block_campaign_replays_exact_production_wasm() { + let path = std::env::var_os("DEOS_PRODUCTION_WASM") + .expect("DEOS_PRODUCTION_WASM must explicitly select the accepted production artifact"); + let wasm = std::fs::read(path).expect("selected production Wasm is readable"); + assert_eq!( + polkadot_sdk::sp_io::hashing::sha2_256(&wasm), + EXP_0095_PRODUCTION_WASM_SHA256, + "EXP-0085 signed-demand linkage must use EXP-0095's accepted production Wasm" + ); + run_schedule_campaign( + &wasm, + "W1", + UserDemand::ContinuousValid, + WorkloadSchedule::MixedManualCadenced, + ); +} + +#[test] +#[ignore = "requires exact current production Wasm via DEOS_PRODUCTION_WASM"] +fn full_executive_w2_manual_and_cadenced_only_100_block_campaigns_replay_exact_production_wasm() { + let path = std::env::var_os("DEOS_PRODUCTION_WASM") + .expect("DEOS_PRODUCTION_WASM must explicitly select the accepted production artifact"); + let wasm = std::fs::read(path).expect("selected production Wasm is readable"); + assert_eq!( + polkadot_sdk::sp_io::hashing::sha2_256(&wasm), + EXP_0095_PRODUCTION_WASM_SHA256, + "EXP-0066 W2 must remain bound to EXP-0095's accepted production Wasm" + ); + for schedule in [WorkloadSchedule::ManualOnly, WorkloadSchedule::CadencedOnly] { + run_schedule_campaign(&wasm, "W2", UserDemand::ActorOnly, schedule); + } +} + +#[test] +#[ignore = "requires exact current production Wasm via DEOS_PRODUCTION_WASM"] +fn full_executive_w3_opening_predicate_mixed_length_campaign_replays_exact_production_wasm() { + let path = std::env::var_os("DEOS_PRODUCTION_WASM") + .expect("DEOS_PRODUCTION_WASM must explicitly select the accepted production artifact"); + let wasm = std::fs::read(path).expect("selected production Wasm is readable"); + assert_eq!( + polkadot_sdk::sp_io::hashing::sha2_256(&wasm), + EXP_0095_PRODUCTION_WASM_SHA256, + "EXP-0066 W3 must remain bound to EXP-0095's accepted production Wasm" + ); + run_w3_opening_predicate_mixed_length_campaign(&wasm); +} + +#[test] +#[ignore = "requires exact current production Wasm via DEOS_PRODUCTION_WASM"] +fn full_executive_w4_heterogeneous_effect_campaigns_replay_exact_production_wasm() { + let path = std::env::var_os("DEOS_PRODUCTION_WASM") + .expect("DEOS_PRODUCTION_WASM must explicitly select the accepted production artifact"); + let wasm = std::fs::read(path).expect("selected production Wasm is readable"); + assert_eq!( + polkadot_sdk::sp_io::hashing::sha2_256(&wasm), + EXP_0095_PRODUCTION_WASM_SHA256, + "EXP-0066 W4 must remain bound to EXP-0095's accepted production Wasm" + ); + for demand in [UserDemand::ActorOnly, UserDemand::ContinuousValid] { + run_w4_heterogeneous_effect_campaign(&wasm, demand); + } +} + +#[test] +fn full_executive_w5_lifecycle_retry_cleanup_fixture_preserves_prefixes() { + let wasm = std::env::var_os("DEOS_PRODUCTION_WASM") + .map(|path| { + let code = std::fs::read(path).expect("genesis code is readable"); + assert_eq!( + polkadot_sdk::sp_io::hashing::sha2_256(&code), + EXP_0095_PRODUCTION_WASM_SHA256 + ); + code + }) + .unwrap_or_default(); + run_w5_lifecycle_retry_cleanup_campaign(&wasm, false); +} + +#[test] +#[ignore = "requires exact current production Wasm via DEOS_PRODUCTION_WASM"] +fn full_executive_w5_lifecycle_retry_cleanup_campaign_replays_exact_production_wasm() { + let path = std::env::var_os("DEOS_PRODUCTION_WASM") + .expect("DEOS_PRODUCTION_WASM must explicitly select the accepted production artifact"); + let wasm = std::fs::read(path).expect("selected production Wasm is readable"); + assert_eq!( + polkadot_sdk::sp_io::hashing::sha2_256(&wasm), + EXP_0095_PRODUCTION_WASM_SHA256, + "EXP-0066 W5 must remain bound to EXP-0095's accepted production Wasm" + ); + run_w5_lifecycle_retry_cleanup_campaign(&wasm, true); +} + +#[test] +fn full_executive_w6_mixed_arrival_lifecycle_fixture_handles_stale_generations() { + run_w6_mixed_arrival_lifecycle_campaign(&[], false); +} + +#[test] +#[ignore = "requires exact current production Wasm via DEOS_PRODUCTION_WASM"] +fn full_executive_w6_mixed_arrival_lifecycle_campaign_replays_exact_production_wasm() { + let path = std::env::var_os("DEOS_PRODUCTION_WASM") + .expect("DEOS_PRODUCTION_WASM must explicitly select the accepted production artifact"); + let wasm = std::fs::read(path).expect("selected production Wasm is readable"); + assert_eq!( + polkadot_sdk::sp_io::hashing::sha2_256(&wasm), + EXP_0095_PRODUCTION_WASM_SHA256, + "EXP-0066 W6 must remain bound to EXP-0095's accepted production Wasm" + ); + run_w6_mixed_arrival_lifecycle_campaign(&wasm, true); +} + +#[test] +fn full_executive_w7_due_only_active_frontier_fixture_scales_independently() { + assert_w7_due_only_active_frontier_campaign(&[], false); +} + +#[test] +#[ignore = "requires exact current production Wasm via DEOS_PRODUCTION_WASM"] +fn full_executive_w7_due_only_active_frontier_campaign_replays_exact_production_wasm() { + let path = std::env::var_os("DEOS_PRODUCTION_WASM") + .expect("DEOS_PRODUCTION_WASM must explicitly select the accepted production artifact"); + let wasm = std::fs::read(path).expect("selected production Wasm is readable"); + assert_eq!( + polkadot_sdk::sp_io::hashing::sha2_256(&wasm), + EXP_0095_PRODUCTION_WASM_SHA256, + "EXP-0066 W7 must remain bound to EXP-0095's accepted production Wasm" + ); + assert_w7_due_only_active_frontier_campaign(&wasm, true); +} + +#[test] +fn full_executive_w8_tombstone_prefix_chunk_pressure_fixture_preserves_fifo() { + run_w8_tombstone_prefix_chunk_pressure_campaign(&[], false); +} + +#[test] +#[ignore = "requires exact current production Wasm via DEOS_PRODUCTION_WASM"] +fn full_executive_w8_tombstone_prefix_chunk_pressure_campaign_replays_exact_production_wasm() { + let path = std::env::var_os("DEOS_PRODUCTION_WASM") + .expect("DEOS_PRODUCTION_WASM must explicitly select the accepted production artifact"); + let wasm = std::fs::read(path).expect("selected production Wasm is readable"); + assert_eq!( + polkadot_sdk::sp_io::hashing::sha2_256(&wasm), + EXP_0095_PRODUCTION_WASM_SHA256, + "EXP-0066 W8 must remain bound to EXP-0095's accepted production Wasm" + ); + run_w8_tombstone_prefix_chunk_pressure_campaign(&wasm, true); +} + +#[test] +fn full_executive_w9_resource_independence_fixture_preserves_actor_service() { + run_w9_resource_independence_campaign(&[], false); +} + +#[test] +fn full_executive_control_phase_attribution_fixture_isolates_materialization() { + assert_control_phase_attribution_campaign(&[], false); +} + +#[test] +#[ignore = "requires exact current production Wasm via DEOS_PRODUCTION_WASM"] +fn full_executive_control_phase_attribution_replays_exact_production_wasm() { + let path = std::env::var_os("DEOS_PRODUCTION_WASM") + .expect("DEOS_PRODUCTION_WASM must explicitly select the accepted production artifact"); + let wasm = std::fs::read(path).expect("selected production Wasm is readable"); + assert_eq!( + polkadot_sdk::sp_io::hashing::sha2_256(&wasm), + EXP_0095_PRODUCTION_WASM_SHA256, + "EXP-0066 Control attribution must remain bound to EXP-0095's accepted production Wasm" + ); + assert_control_phase_attribution_campaign(&wasm, true); +} + +#[test] +#[ignore = "requires exact current production Wasm via DEOS_PRODUCTION_WASM"] +fn full_executive_w9_resource_independence_campaign_replays_exact_production_wasm() { + let path = std::env::var_os("DEOS_PRODUCTION_WASM") + .expect("DEOS_PRODUCTION_WASM must explicitly select the accepted production artifact"); + let wasm = std::fs::read(path).expect("selected production Wasm is readable"); + assert_eq!( + polkadot_sdk::sp_io::hashing::sha2_256(&wasm), + EXP_0095_PRODUCTION_WASM_SHA256, + "EXP-0066 W9 must remain bound to EXP-0095's accepted production Wasm" + ); + run_w9_resource_independence_campaign(&wasm, true); +} + +#[test] +#[ignore = "requires exact current production Wasm via DEOS_PRODUCTION_WASM"] +fn reference_full_block_replays_in_production_wasm_with_verified_storage_proof() { + let path = std::env::var_os("DEOS_PRODUCTION_WASM") + .expect("DEOS_PRODUCTION_WASM must explicitly select the current production artifact"); + let wasm = std::fs::read(path).expect("selected production Wasm is readable"); + assert_eq!( + polkadot_sdk::sp_io::hashing::sha2_256(&wasm), + EXP_0095_PRODUCTION_WASM_SHA256, + "reference replay must bind the current EXP-0095 production Wasm" + ); + let (storage, block, data) = author_reference_first_block(&wasm); + let evidence = super::wasm_replay::replay_block(storage, &wasm, &block.encode(), &data) + .expect("production Wasm and independent proof-only execution agree"); + assert_runtime_version_equivalent(&evidence.runtime_version); + assert_eq!(evidence.block_hash, block.header.hash()); + assert_eq!(evidence.post_state_root, *block.header.state_root()); + assert!(evidence.execute_block_result.is_empty()); + for proof in [&evidence.execution_proof, &evidence.verification_proof] { + assert_eq!( + proof.storage_proof_scale_bytes, + proof.storage_proof.encoded_size() + ); + assert_eq!( + proof.compact_proof_scale_bytes, + proof.compact_proof.encoded_size() + ); + assert_eq!(proof.trie_node_count, proof.storage_proof.len()); + assert!(proof.trie_node_bytes > 0); + } + println!( + "WASM_BLOCK_REPLAY_V1 code={:?} block={:?} preRoot={:?} postRoot={:?} executionStorageProofBytes={} executionCompactProofBytes={} executionNodeBytes={} executionNodes={} verificationStorageProofBytes={} verificationCompactProofBytes={} proofSizeObservations={:?}", + evidence.runtime_code_hash, + evidence.block_hash, + evidence.pre_state_root, + evidence.post_state_root, + evidence.execution_proof.storage_proof_scale_bytes, + evidence.execution_proof.compact_proof_scale_bytes, + evidence.execution_proof.trie_node_bytes, + evidence.execution_proof.trie_node_count, + evidence.verification_proof.storage_proof_scale_bytes, + evidence.verification_proof.compact_proof_scale_bytes, + evidence.proof_size_observations, + ); +} diff --git a/template/runtime/src/tests/staking_integration_tests.rs b/template/runtime/src/tests/staking_integration_tests.rs index f7528a05..1cc4faa7 100644 --- a/template/runtime/src/tests/staking_integration_tests.rs +++ b/template/runtime/src/tests/staking_integration_tests.rs @@ -1857,6 +1857,7 @@ fn ntve_stntve_pool_direct_balanced_donation_increases_lp_value_without_minting_ } #[test] +#[cfg(not(feature = "runtime-benchmarks"))] fn trusted_mode_rejects_new_collator_lp_nomination_without_custody_mutation() { let mut ext = seeded_test_ext(); ext.execute_with(|| { @@ -2358,6 +2359,7 @@ fn trusted_security_mode_session_boundary_settles_retained_reward_obligations() } #[test] +#[cfg(not(feature = "runtime-benchmarks"))] fn trusted_session_boundary_finalizes_open_state_and_removes_unactivated_planning() { use polkadot_sdk::pallet_session::SessionManager; @@ -2478,6 +2480,7 @@ fn failed_session_retention_cancels_the_plan_and_recovers_the_oldest_due_epoch() } #[test] +#[cfg(not(feature = "runtime-benchmarks"))] fn trusted_security_mode_ignores_candidates_and_reports_capabilities() { let mut ext = seeded_test_ext(); ext.execute_with(|| { diff --git a/template/runtime/src/tests/tmctol_integration_tests.rs b/template/runtime/src/tests/tmctol_integration_tests.rs index 88aae6f7..b4e23800 100644 --- a/template/runtime/src/tests/tmctol_integration_tests.rs +++ b/template/runtime/src/tests/tmctol_integration_tests.rs @@ -87,7 +87,13 @@ fn tmctol_guarantee_state_reports_anchor_protection_without_pool_initialization( new_test_ext().execute_with(|| { let state = crate::tmctol_read_model::TmctolReadModel::tmctol_guarantee_state(); assert_eq!(state.tol_anchor.status, GuaranteeStatus::Satisfied); + assert!(state.tol_anchor.is_immutable_system_actor); + assert!(state.tol_anchor.actor_identity_exists); + assert!(!state.tol_anchor.active_state_exists); assert_eq!(state.bldr_anchor.status, GuaranteeStatus::Satisfied); + assert!(state.bldr_anchor.is_immutable_system_actor); + assert!(state.bldr_anchor.actor_identity_exists); + assert!(!state.bldr_anchor.active_state_exists); assert_eq!(state.anchor_status, GuaranteeStatus::Satisfied); assert_eq!(state.tol_pool.status, GuaranteeStatus::NotInitialized); assert_eq!(state.bldr_pool.status, GuaranteeStatus::NotInitialized); @@ -130,7 +136,7 @@ fn tmctol_guarantee_state_reports_bldr_anchor_pool_when_initialized() { let AssetKind::Local(lp_asset_id) = lp_asset else { panic!("pool LP asset must be local"); }; - let bldr_anchor = Actors::sovereign_account_id_system(actor_ids::BLDR_BUCKET_A_ACTORS_ID); + let bldr_anchor = Actors::sovereign_account_id_system(actor_ids::BLDR_ANCHOR_ACTORS_ID); assert_ok!( >::mint_into( lp_asset_id, @@ -344,6 +350,43 @@ fn tmctol_guarantee_state_flags_anchor_mutation_as_violation() { }); } +#[test] +fn tmctol_guarantee_state_rejects_orphan_unsignaled_anchor_control() { + new_test_ext().execute_with(|| { + let anchor_id = actor_ids::TOL_BUCKET_A_ACTORS_ID; + let (_, mut orphan) = Actors::actor_control_cell(actor_ids::BURN_ACTOR_ID) + .expect("genesis Burn Actor has canonical control"); + orphan.actor_id = anchor_id; + assert!(!pallet_deos_actors::ActorControlLocators::::contains_key(anchor_id)); + // Deliberately corrupt only the directly keyed process owner: a sealed dormant + // Anchor must not be reported healthy merely because its locator is absent. + pallet_deos_actors::ActorUnsignaledControlCells::::insert(anchor_id, orphan); + + let state = crate::tmctol_read_model::TmctolReadModel::tmctol_guarantee_state(); + assert!(state.tol_anchor.active_state_exists); + assert_eq!(state.tol_anchor.status, GuaranteeStatus::Violated); + assert_eq!(state.anchor_status, GuaranteeStatus::Violated); + assert_eq!(state.conformance_status, TmctolConformanceStatus::Violated); + }); +} + +#[test] +fn tmctol_guarantee_state_rejects_missing_active_burn_control() { + new_test_ext().execute_with(|| { + let actor_id = actor_ids::BURN_ACTOR_ID; + assert!(Actors::active_actor_state(actor_id).is_some()); + pallet_deos_actors::ActorUnsignaledControlCells::::remove(actor_id); + pallet_deos_actors::ActorControlLocators::::insert( + actor_id, + pallet_deos_actors::ActorControlLocation::Unsignaled, + ); + + let state = crate::tmctol_read_model::TmctolReadModel::tmctol_guarantee_state(); + assert_eq!(state.native_burn_liveness.status, GuaranteeStatus::Violated); + assert_eq!(state.conformance_status, TmctolConformanceStatus::Violated); + }); +} + #[test] fn genesis_burn_actor_has_deterministic_sovereign_and_correct_state() { new_test_ext().execute_with(|| { @@ -849,14 +892,6 @@ 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, @@ -1763,10 +1798,10 @@ fn bldr_splitter_distributes_to_liquidity_and_treasury() { }); } -// --- BLDR Full E2E: Router → TMC → Splitter → Liquidity Actor → LP → Bucket A --- +// --- BLDR Full E2E: Router → TMC → Splitter → Liquidity Actor → LP → Anchor --- #[test] -fn bldr_full_e2e_router_tmc_splitter_liquidity_bucket() { +fn bldr_full_e2e_router_tmc_splitter_liquidity_anchor() { use polkadot_sdk::frame_support::traits::fungibles::Inspect as FungiblesInspect; use primitives::ecosystem::{actor_ids, protocol_tokens}; seeded_test_ext().execute_with(|| { @@ -1778,15 +1813,15 @@ fn bldr_full_e2e_router_tmc_splitter_liquidity_bucket() { let precision = primitives::ecosystem::params::PRECISION; let splitter_id = actor_ids::BLDR_SPLITTER_ACTORS_ID; let liquidity_id = actor_ids::BLDR_LIQUIDITY_ACTOR_ID; - let bucket_a_id = actor_ids::BLDR_BUCKET_A_ACTORS_ID; + let anchor_id = actor_ids::BLDR_ANCHOR_ACTORS_ID; let splitter_sov = Actors::sovereign_account_id_system(splitter_id); let liquidity_sov = Actors::sovereign_account_id_system(liquidity_id); let treasury_sov = Actors::sovereign_account_id_system(actor_ids::BLDR_TREASURY_ACTORS_ID); - let bucket_a_sov = Actors::sovereign_account_id_system(bucket_a_id); + let anchor_sov = Actors::sovereign_account_id_system(anchor_id); // 1. Create the NTVE-BLDR pool so the Liquidity Actor can provision it. Seed it against // BLDR buys so the router exercises the TMC mint path rather than XYK. super::common::setup_bldr_pool_with_reserves(900 * precision, 10 * precision); - // 2. Activate the BLDR Liquidity Actor execution plan (AddLiquidity + LP → Bucket A). + // 2. Activate the BLDR Liquidity Actor execution plan (AddLiquidity + LP → BLDR Anchor). let lp_asset = super::common::get_pool_lp_asset(AssetKind::Native, bldr_asset); let liquidity_contract_steps = crate::configs::actor_config::TmctolGenesisSystemActors::build_bldr_liquidity_contract_steps( @@ -1844,7 +1879,7 @@ fn bldr_full_e2e_router_tmc_splitter_liquidity_bucket() { "BLDR Liquidity Actor must hold NTVE collateral" ); assert!(splitter_bldr > 0, "Splitter must hold BLDR zap share"); - // 5. Run blocks to execute the Splitter → Liquidity Actor → Bucket A chain. + // 5. Run blocks to execute the Splitter → Liquidity Actor → BLDR Anchor chain. // The queue scheduler should keep this chain progressing without starvation. System::reset_events(); for block in 2..=40 { @@ -1854,20 +1889,20 @@ fn bldr_full_e2e_router_tmc_splitter_liquidity_bucket() { } // 6. Verify: Splitter distributed BLDR to both Liquidity Actor and Treasury. The Actor may // consume received NTVE immediately into LP, so the durable final proof is - // Bucket A receiving LP plus the Splitter sovereign draining below dust. + // BLDR Anchor receiving LP plus the Splitter sovereign draining below dust. let treasury_bldr = >::balance(bldr_id, &treasury_sov); assert!(treasury_bldr > 0, "Treasury must have received BLDR"); - // 8. Verify: Bucket A received LP tokens provisioned by the Liquidity Actor. + // 8. Verify: BLDR Anchor received LP tokens provisioned by the Liquidity Actor. if let Some(lp_id) = match lp_asset { AssetKind::Local(id) => Some(id), _ => None, } { - let bucket_lp = - >::balance(lp_id, &bucket_a_sov); + let anchor_lp = + >::balance(lp_id, &anchor_sov); assert!( - bucket_lp > 0, - "Bucket A must hold LP tokens from BLDR Liquidity Actor provisioning" + anchor_lp > 0, + "BLDR Anchor must hold LP tokens from BLDR Liquidity Actor provisioning" ); } // 9. Verify: Splitter distributed its complete BLDR input below dust diff --git a/template/runtime/src/tests/tmctol_invariants.rs b/template/runtime/src/tests/tmctol_invariants.rs index 9a248ac7..52b2c252 100644 --- a/template/runtime/src/tests/tmctol_invariants.rs +++ b/template/runtime/src/tests/tmctol_invariants.rs @@ -163,12 +163,12 @@ fn floor_price_proxy_after_tol_accumulation() { }) .expect("BLDR pool must exist"); let lp_id = pool_info.lp_token; - // Seed Bucket A with LP, simulating prior BLDR Liquidity Actor activity. - let bucket_a = crate::Actors::sovereign_account_id_system(actor_ids::BLDR_BUCKET_A_ACTORS_ID); + // Seed BLDR Anchor with LP, simulating prior BLDR Liquidity Actor activity. + let bldr_anchor = crate::Actors::sovereign_account_id_system(actor_ids::BLDR_ANCHOR_ACTORS_ID); assert_ok!( >::mint_into( lp_id, - &bucket_a, + &bldr_anchor, 100 * PRECISION ) ); @@ -416,9 +416,9 @@ fn heavy_use_invariants_preserved() { let splitter = crate::Actors::sovereign_account_id_system(actor_ids::BLDR_SPLITTER_ACTORS_ID); let liquidity = crate::Actors::sovereign_account_id_system(actor_ids::BLDR_LIQUIDITY_ACTOR_ID); let treasury = crate::Actors::sovereign_account_id_system(actor_ids::BLDR_TREASURY_ACTORS_ID); - let bucket_a = crate::Actors::sovereign_account_id_system(actor_ids::BLDR_BUCKET_A_ACTORS_ID); + let anchor = crate::Actors::sovereign_account_id_system(actor_ids::BLDR_ANCHOR_ACTORS_ID); let total_bldr_issued = Assets::total_issuance(bldr_id); - let known_holders = [ALICE, BOB, CHARLIE, splitter, liquidity, treasury, bucket_a]; + let known_holders = [ALICE, BOB, CHARLIE, splitter, liquidity, treasury, anchor]; let sum_known: u128 = known_holders .iter() .map(|a| Assets::balance(bldr_id, a)) diff --git a/template/runtime/src/tests/wasm_replay.rs b/template/runtime/src/tests/wasm_replay.rs new file mode 100644 index 00000000..2b02db51 --- /dev/null +++ b/template/runtime/src/tests/wasm_replay.rs @@ -0,0 +1,663 @@ +//! Dev-only full-block Wasm replay under the SDK import-with-proof-recording profile. +//! +//! The caller owns complete pre-state, correct state-layout lineage, block authoring, and artifact +//! provenance. This module neither authors blocks nor proves chain membership/finality. Its trie +//! proof sizes are not a full parachain PoV or registered FRAME ProofSize measurement. + +use codec::{DecodeAll, Encode}; +use polkadot_sdk::{ + sp_core::{ + Blake2Hasher, H256, + storage::{ChildInfo, StateVersion, Storage, well_known_keys}, + traits::{CallContext, RuntimeCode, WrappedRuntimeCode}, + }, + sp_externalities::Extensions, + sp_inherents::{CheckInherentsResult, InherentData}, + sp_io, + sp_runtime::traits::{Block as BlockT, Header as HeaderT}, + sp_state_machine::{ + Backend, BasicExternalities, OverlayedChanges, StateMachine, StorageChanges, TrieBackend, + TrieBackendBuilder, create_proof_check_backend, + }, + sp_trie::{ + CompactProof, PrefixedMemoryDB, StorageProof, + proof_size_extension::{ProofSizeExt, RecordingProofSizeProvider}, + recorder::Recorder, + }, + sp_version::RuntimeVersion, +}; +use sc_executor::{RuntimeVersionOf, WasmExecutor}; +use std::{ + borrow::Cow, + collections::{BTreeMap, BTreeSet}, + panic::AssertUnwindSafe, +}; + +type ReplayHostFunctions = ( + sp_io::SubstrateHostFunctions, + cumulus_primitives_proof_size_hostfunction::storage_proof_size::HostFunctions, +); +type ReplayExecutor = WasmExecutor; +const EXECUTE_BLOCK: &str = "Core_execute_block"; +const IMPORT_CONTEXT: CallContext = CallContext::Onchain { import: true }; + +#[derive(Debug)] +pub(crate) struct ProofArtifact { + pub storage_proof: StorageProof, + pub compact_proof: CompactProof, + pub storage_proof_scale_bytes: usize, + pub compact_proof_scale_bytes: usize, + pub trie_node_bytes: usize, + pub trie_node_count: usize, +} + +/// Raw trie-node bytes without proof framing. Shared nodes count once; summing independent +/// key paths counts their repeated appearances without implying duplicate runtime execution. +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) struct KeyProofOverlap { + pub paths: BTreeMap<(H256, Vec), StorageProof>, + pub per_key_node_bytes: usize, + pub unique_node_bytes: usize, + pub shared_node_bytes: usize, + pub unattributed_node_bytes: usize, +} + +#[derive(Debug)] +pub(crate) struct VerifiedWasmReplay { + pub runtime_code_hash: H256, + pub runtime_version: RuntimeVersion, + pub block_hash: H256, + pub pre_state_root: H256, + pub post_state_root: H256, + /// Exact SCALE unit returned by Core_execute_block, also returned by independent replay. + pub execute_block_result: Vec, + /// Nodes touched by execution/root calculation; excludes extra code/heap binding reads. + pub execution_proof: ProofArtifact, + /// Execution plus code/heap binding and inherent checks, independently replayed after compaction. + pub verification_proof: ProofArtifact, + /// Root-scoped key paths retained by the SDK recorder after execution and root calculation. + pub execution_recorded_keys: BTreeSet<(H256, Vec)>, + /// Independent key-path reconstruction from execution proof; never a FRAME Weight discount. + pub execution_key_overlap: KeyProofOverlap, + /// Raw nodes first recorded by host root verification after Core_execute_block returned. + pub post_execute_root_node_bytes: usize, + /// Root reconstruction from committed deltas using only execution proof nodes. + pub committed_root_proof: StorageProof, + /// Actual hostfunction observations; independently replayed in exactly the same order. + pub proof_size_observations: Vec, +} + +/// Replays an already authored complete block; any malformed input, trap, missing proof node, +/// invalid block, non-unit result, host observation mismatch, or post-root mismatch fails closed. +pub(crate) fn replay_block( + pre_state: Storage, + wasm: &[u8], + encoded_block: &[u8], + inherent_data: &InherentData, +) -> Result { + std::panic::catch_unwind(AssertUnwindSafe(|| { + replay_block_inner(pre_state, wasm, encoded_block, inherent_data) + })) + .map_err(|_| "Wasm replay or proof backend panicked".to_owned())? +} + +fn replay_block_inner( + pre_state: Storage, + wasm: &[u8], + encoded_block: &[u8], + inherent_data: &InherentData, +) -> Result { + if pre_state.top.get(well_known_keys::CODE).map(Vec::as_slice) != Some(wasm) { + return Err("Supplied Wasm does not equal pre-state :code".into()); + } + let block = crate::Block::decode_all(&mut &encoded_block[..]) + .map_err(|error| format!("Complete block decode: {error}"))?; + if block.encode() != encoded_block { + return Err("Block encoding is not canonical".into()); + } + let heap_pages = decode_heap_pages(pre_state.top.get(well_known_keys::HEAP_PAGES))?; + let code_fetcher = WrappedRuntimeCode(Cow::Borrowed(wasm)); + let runtime_code_hash = H256::from(sp_io::hashing::blake2_256(wasm)); + let runtime_code = RuntimeCode { + code_fetcher: &code_fetcher, + heap_pages, + hash: runtime_code_hash.encode(), + }; + let executor = ReplayExecutor::builder().build(); + // Version discovery is isolated from the measured execution overlay and recorder. + let runtime_version = executor + .runtime_version( + &mut BasicExternalities::new(pre_state.clone()), + &runtime_code, + ) + .map_err(|error| format!("Exact Wasm version: {error}"))?; + let state_version = runtime_version.state_version(); + let expected_post_root = *block.header().state_root(); + let backend: TrieBackend, Blake2Hasher> = + (pre_state, state_version).into(); + let pre_state_root = *backend.root(); + + // Bind code/heap and validate inherent data without charging these verification reads to the + // execution proof-size hostfunction. Actual block execution starts with a separate recorder. + let binding_backend = TrieBackendBuilder::wrap(&backend) + .with_recorder(Recorder::::default()) + .build(); + verify_code_binding(&binding_backend, wasm, heap_pages)?; + let inherent_check = check_inherents( + &binding_backend, + &executor, + &runtime_code, + &block, + inherent_data, + )?; + let binding_proof = binding_backend + .extract_proof() + .ok_or_else(|| "Code binding recorder was absent".to_owned())?; + + let recorder = Recorder::::default(); + let proving_backend = TrieBackendBuilder::wrap(&backend) + .with_recorder(recorder.clone()) + .build(); + let observations = RecordingProofSizeProvider::new(recorder.clone()); + let mut extensions = Extensions::default(); + extensions.register(ProofSizeExt::new(observations.clone())); + let mut overlay = OverlayedChanges::::default(); + let result = StateMachine::new( + &proving_backend, + &mut overlay, + &executor, + EXECUTE_BLOCK, + encoded_block, + &mut extensions, + &runtime_code, + IMPORT_CONTEXT, + ) + .set_parent_hash(*block.header().parent_hash()) + .execute() + .map_err(|error| format!("Full-state Core_execute_block: {error}"))?; + require_unit(&result)?; + let execute_proof = recorder.to_storage_proof(); + let storage_changes = overlay + .drain_storage_changes(&proving_backend, state_version) + .map_err(|error| format!("Full-state post root: {error}"))?; + let post_state_root = storage_changes.transaction_storage_root; + if post_state_root != expected_post_root { + return Err("Full-state post root differs from the authored header".into()); + } + let execution_recorded_keys = recorded_trie_keys(&recorder); + let execution_key_overlap = key_proof_overlap(&recorder)?; + let execution_proof = proving_backend + .extract_proof() + .ok_or_else(|| "Execution recorder was absent".to_owned())?; + let execute_nodes = execute_proof.iter_nodes().collect::>(); + let execution_nodes = execution_proof.iter_nodes().collect::>(); + if !execute_nodes.is_subset(&execution_nodes) { + return Err("Host root verification removed execution proof nodes".into()); + } + let post_execute_root_node_bytes = execution_nodes + .difference(&execute_nodes) + .map(|node| node.len()) + .sum(); + let committed_root_proof = reconstruct_committed_root_proof( + pre_state_root, + &storage_changes, + &execution_proof, + state_version, + )?; + let verification_proof = StorageProof::merge([execution_proof.clone(), binding_proof]); + let verification_proof = proof_artifact(verification_proof, pre_state_root)?; + let (decoded_proof, decoded_root) = verification_proof + .compact_proof + .to_storage_proof::(Some(&pre_state_root)) + .map_err(|error| format!("Compact proof decode: {error:?}"))?; + if decoded_root != pre_state_root { + return Err("Compact proof changed the pre-state root".into()); + } + + // execution_proof_check uses Offchain in SDK 0.53. Use its same proof-only backend constructor + // with the actual Onchain import context and fresh ProofSizeExt instead of changing conditions. + let proof_backend = create_proof_check_backend::(pre_state_root, decoded_proof) + .map_err(|error| format!("Proof backend: {error}"))?; + verify_code_binding(&proof_backend, wasm, heap_pages)?; + let verifier = ReplayExecutor::builder().build(); + if check_inherents( + &proof_backend, + &verifier, + &runtime_code, + &block, + inherent_data, + )? != inherent_check + { + return Err("Proof-only inherent validation differs".into()); + } + let verifier_recorder = Recorder::::default(); + let checking_backend = TrieBackendBuilder::wrap(&proof_backend) + .with_recorder(verifier_recorder.clone()) + .build(); + let verifier_observations = RecordingProofSizeProvider::new(verifier_recorder.clone()); + let mut verifier_extensions = Extensions::default(); + verifier_extensions.register(ProofSizeExt::new(verifier_observations.clone())); + let mut verifier_overlay = OverlayedChanges::::default(); + let checked_result = StateMachine::new( + &checking_backend, + &mut verifier_overlay, + &verifier, + EXECUTE_BLOCK, + encoded_block, + &mut verifier_extensions, + &runtime_code, + IMPORT_CONTEXT, + ) + .set_parent_hash(*block.header().parent_hash()) + .execute() + .map_err(|error| format!("Proof-only Core_execute_block: {error}"))?; + require_unit(&checked_result)?; + let checked_root = verifier_overlay + .drain_storage_changes(&checking_backend, state_version) + .map_err(|error| format!("Proof-only post root: {error}"))? + .transaction_storage_root; + let proof_size_observations = observations.recorded_estimations(); + if checked_result != result + || checked_root != post_state_root + || verifier_observations.recorded_estimations() != proof_size_observations + || recorded_trie_keys(&verifier_recorder) != execution_recorded_keys + { + return Err( + "Proof-only result, post root, proof-size observations, or recorded keys differ".into(), + ); + } + Ok(VerifiedWasmReplay { + runtime_code_hash, + runtime_version, + block_hash: block.header().hash(), + pre_state_root, + post_state_root, + execute_block_result: result, + execution_proof: proof_artifact(execution_proof, pre_state_root)?, + verification_proof, + execution_recorded_keys, + execution_key_overlap, + post_execute_root_node_bytes, + committed_root_proof, + proof_size_observations, + }) +} + +fn reconstruct_committed_root_proof( + pre_state_root: H256, + changes: &StorageChanges, + execution_proof: &StorageProof, + state_version: StateVersion, +) -> Result { + let backend = create_proof_check_backend::(pre_state_root, execution_proof.clone()) + .map_err(|error| format!("Committed-root proof backend: {error}"))?; + let recorder = Recorder::::default(); + let proving_backend = TrieBackendBuilder::wrap(&backend) + .with_recorder(recorder.clone()) + .build(); + let children = changes + .child_storage_changes + .iter() + .map(|(key, delta)| (ChildInfo::new_default(key), delta)) + .collect::>(); + let (root, _) = proving_backend.full_storage_root( + changes + .main_storage_changes + .iter() + .map(|(key, value)| (key.as_slice(), value.as_deref())), + children.iter().map(|(info, delta)| { + ( + info, + delta + .iter() + .map(|(key, value)| (key.as_slice(), value.as_deref())), + ) + }), + state_version, + ); + if root != changes.transaction_storage_root { + return Err("Committed delta proof did not reproduce the post-state root".into()); + } + let proof = recorder.to_storage_proof(); + let execution_nodes = execution_proof.iter_nodes().collect::>(); + if proof + .iter_nodes() + .any(|node| !execution_nodes.contains(node)) + { + return Err("Committed root reconstruction escaped the execution proof".into()); + } + Ok(proof) +} + +fn recorded_trie_keys(recorder: &Recorder) -> BTreeSet<(H256, Vec)> { + recorder + .recorded_keys() + .into_iter() + .flat_map(|(root, keys)| { + keys + .into_keys() + .map(move |key| (root, key.as_ref().to_vec())) + }) + .collect() +} + +fn key_proof_overlap(recorder: &Recorder) -> Result { + let proof = recorder.to_storage_proof(); + let proof_nodes = proof.iter_nodes().collect::>(); + let db = proof.to_memory_db::(); + let mut uses = BTreeMap::, usize>::new(); + let mut per_key_node_bytes = 0; + let mut paths = BTreeMap::new(); + for (root, keys) in recorder.recorded_keys() { + for (key, access) in keys { + let key_recorder = Recorder::::default(); + let backend = TrieBackendBuilder::new(&db, root) + .with_recorder(key_recorder.clone()) + .build(); + backend + .storage_hash(&key) + .map_err(|error| format!("Recorded key hash path: {error}"))?; + let hash_accesses = key_recorder.recorded_keys(); + if hash_accesses.get(&root).and_then(|keys| keys.get(&key)) != Some(&access) { + backend + .storage(&key) + .map_err(|error| format!("Recorded key value path: {error}"))?; + } + let replayed_accesses = key_recorder.recorded_keys(); + if replayed_accesses.get(&root).and_then(|keys| keys.get(&key)) != Some(&access) { + return Err("Reconstructed key path changed the recorded access kind".into()); + } + let key_proof = key_recorder.to_storage_proof(); + for node in key_proof.iter_nodes() { + if !proof_nodes.contains(node) { + return Err("Reconstructed key path contains a node outside execution proof".into()); + } + per_key_node_bytes += node.len(); + *uses.entry(node.clone()).or_default() += 1; + } + paths.insert((root, key.as_ref().to_vec()), key_proof); + } + } + let unique_node_bytes = uses.keys().map(Vec::len).sum::(); + let shared_node_bytes = uses + .iter() + .filter(|(_, count)| **count > 1) + .map(|(node, _)| node.len()) + .sum(); + let execution_node_bytes = proof.iter_nodes().map(Vec::len).sum::(); + Ok(KeyProofOverlap { + paths, + per_key_node_bytes, + unique_node_bytes, + shared_node_bytes, + unattributed_node_bytes: execution_node_bytes + .checked_sub(unique_node_bytes) + .ok_or("Key-path union exceeds execution proof")?, + }) +} + +fn check_inherents>( + backend: &B, + executor: &ReplayExecutor, + runtime_code: &RuntimeCode, + block: &crate::Block, + inherent_data: &InherentData, +) -> Result, String> { + let mut overlay = OverlayedChanges::::default(); + let mut extensions = Extensions::default(); + let result = StateMachine::new( + backend, + &mut overlay, + executor, + "BlockBuilder_check_inherents", + &(block, inherent_data).encode(), + &mut extensions, + runtime_code, + IMPORT_CONTEXT, + ) + .set_parent_hash(*block.header().parent_hash()) + .execute() + .map_err(|error| format!("Wasm inherent validation: {error}"))?; + let checked = CheckInherentsResult::decode_all(&mut &result[..]) + .map_err(|error| format!("Inherent validation result decode: {error}"))?; + if !checked.ok() || !overlay.is_empty() { + return Err("Wasm inherent validation rejected input or mutated state".into()); + } + Ok(result) +} + +fn require_unit(result: &[u8]) -> Result<(), String> { + <()>::decode_all(&mut &result[..]) + .map_err(|error| format!("Core_execute_block returned non-unit bytes: {error}")) +} + +fn decode_heap_pages(bytes: Option<&Vec>) -> Result, String> { + bytes + .map(|bytes| { + u64::decode_all(&mut &bytes[..]).map_err(|error| format!("Invalid :heappages: {error}")) + }) + .transpose() +} + +fn verify_code_binding>( + backend: &B, + wasm: &[u8], + expected_heap_pages: Option, +) -> Result<(), String> { + let stored_code = backend + .storage(well_known_keys::CODE) + .map_err(|_| "Cannot read proof-bound :code".to_owned())?; + let stored_heap_pages = backend + .storage(well_known_keys::HEAP_PAGES) + .map_err(|_| "Cannot read proof-bound :heappages".to_owned())?; + if stored_code.as_deref() != Some(wasm) + || decode_heap_pages(stored_heap_pages.as_ref())? != expected_heap_pages + { + return Err("Proof-bound code or heap differs from the replay executor".into()); + } + Ok(()) +} + +fn proof_artifact(proof: StorageProof, root: H256) -> Result { + let compact_proof = proof + .to_compact_proof::(root) + .map_err(|error| format!("Storage proof compaction: {error:?}"))?; + let trie_node_bytes = proof.iter_nodes().try_fold(0usize, |total, node| { + total + .checked_add(node.len()) + .ok_or("Proof byte count overflow") + })?; + Ok(ProofArtifact { + storage_proof_scale_bytes: proof.encoded_size(), + compact_proof_scale_bytes: compact_proof.encoded_size(), + trie_node_bytes, + trie_node_count: proof.len(), + storage_proof: proof, + compact_proof, + }) +} + +#[test] +fn committed_root_proof_covers_top_and_child_deltas_and_rejects_invalid_evidence() { + let state_version = crate::VERSION.state_version(); + let storage = Storage { + top: [(b"a".to_vec(), vec![1; 64]), (b"b".to_vec(), vec![2; 64])].into(), + ..Default::default() + }; + let backend: TrieBackend, Blake2Hasher> = + (storage, state_version).into(); + let recorder = Recorder::::default(); + let proving_backend = TrieBackendBuilder::wrap(&backend) + .with_recorder(recorder.clone()) + .build(); + let mut overlay = OverlayedChanges::::default(); + overlay.set_storage(b"a".to_vec(), Some(vec![3; 64])); + overlay.set_storage(b"b".to_vec(), None); + overlay.set_storage(b"c".to_vec(), Some(vec![4; 64])); + overlay.set_child_storage( + &ChildInfo::new_default(b"child"), + b"a".to_vec(), + Some(vec![5; 64]), + ); + let mut changes = overlay + .drain_storage_changes(&proving_backend, state_version) + .unwrap(); + assert_eq!(changes.child_storage_changes.len(), 1); + let proof = recorder.to_storage_proof(); + assert_eq!( + reconstruct_committed_root_proof(*backend.root(), &changes, &proof, state_version).unwrap(), + proof, + ); + assert!( + reconstruct_committed_root_proof( + *backend.root(), + &changes, + &StorageProof::empty(), + state_version + ) + .is_err() + ); + changes.transaction_storage_root = H256::zero(); + assert!( + reconstruct_committed_root_proof(*backend.root(), &changes, &proof, state_version).is_err() + ); +} + +#[test] +fn recorded_keys_retain_trie_root_identity() { + let recorder = Recorder::::default(); + let key = b"shared-key".to_vec(); + let mut expected = BTreeSet::new(); + for value in [vec![1; 64], vec![2; 64]] { + let storage = Storage { + top: [(key.clone(), value.clone())].into(), + ..Default::default() + }; + let backend: TrieBackend, Blake2Hasher> = + (storage, crate::VERSION.state_version()).into(); + expected.insert((*backend.root(), key.clone())); + let proving_backend = TrieBackendBuilder::wrap(&backend) + .with_recorder(recorder.clone()) + .build(); + for _ in 0..2 { + assert_eq!(proving_backend.storage(&key).unwrap(), Some(value.clone())); + } + } + assert_eq!(expected.len(), 2, "fixture roots must be distinct"); + assert_eq!(recorded_trie_keys(&recorder), expected); + let overlap = key_proof_overlap(&recorder).expect("both roots must be proof-reconstructible"); + assert_eq!(overlap.unattributed_node_bytes, 0); + assert_eq!(overlap.per_key_node_bytes, overlap.unique_node_bytes); +} + +#[test] +fn key_proof_overlap_preserves_access_kind_and_shared_nodes() { + let shared_value = vec![7; 64]; + let hash_only_value = vec![9; 128]; + let storage = Storage { + top: [ + (b"prefix/a".to_vec(), shared_value.clone()), + (b"prefix/b".to_vec(), shared_value.clone()), + (b"prefix/c".to_vec(), hash_only_value.clone()), + (b"prefix/d".to_vec(), vec![3; 8]), + ] + .into(), + ..Default::default() + }; + let backend: TrieBackend, Blake2Hasher> = + (storage, crate::VERSION.state_version()).into(); + let recorder = Recorder::::default(); + let proving_backend = TrieBackendBuilder::wrap(&backend) + .with_recorder(recorder.clone()) + .build(); + for key in [b"prefix/a", b"prefix/b", b"prefix/d", b"prefix/z"] { + proving_backend.storage(key).unwrap(); + } + proving_backend.storage_hash(b"prefix/c").unwrap(); + let proof = recorder.to_storage_proof(); + assert!(!proof.iter_nodes().any(|node| *node == hash_only_value)); + assert_eq!(recorded_trie_keys(&recorder).len(), 5); + let overlap = key_proof_overlap(&recorder).expect("recorded key paths must reconstruct"); + assert_eq!(overlap.unattributed_node_bytes, 0); + assert_eq!( + overlap.unique_node_bytes, + proof.iter_nodes().map(Vec::len).sum::(), + ); + assert!(overlap.shared_node_bytes >= shared_value.len()); + assert!(overlap.per_key_node_bytes > overlap.unique_node_bytes); + proving_backend.storage(b"prefix/a").unwrap(); + assert_eq!(key_proof_overlap(&recorder).unwrap(), overlap); +} + +#[test] +fn key_proof_overlap_keeps_iterator_nodes_unattributed() { + let storage = Storage { + top: [(b"key".to_vec(), vec![1; 64])].into(), + ..Default::default() + }; + let backend: TrieBackend, Blake2Hasher> = + (storage, crate::VERSION.state_version()).into(); + let recorder = Recorder::::default(); + let proving_backend = TrieBackendBuilder::wrap(&backend) + .with_recorder(recorder.clone()) + .build(); + assert_eq!( + proving_backend.next_storage_key(b"").unwrap(), + Some(b"key".to_vec()) + ); + assert!(recorded_trie_keys(&recorder).is_empty()); + let overlap = key_proof_overlap(&recorder).unwrap(); + assert_eq!(overlap.per_key_node_bytes, 0); + assert_eq!(overlap.unique_node_bytes, 0); + assert_eq!(overlap.shared_node_bytes, 0); + assert!(overlap.unattributed_node_bytes > 0); + assert_eq!( + overlap.unattributed_node_bytes, + recorder + .to_storage_proof() + .iter_nodes() + .map(Vec::len) + .sum::(), + ); +} + +#[test] +fn replay_rejects_unbound_wasm_before_execution() { + let mut storage = Storage::default(); + storage.top.insert(well_known_keys::CODE.to_vec(), vec![1]); + assert_eq!( + replay_block(storage, &[2], &[], &InherentData::new()).expect_err("different code must fail"), + "Supplied Wasm does not equal pre-state :code" + ); +} + +#[test] +fn replay_rejects_malformed_block_before_loading_wasm() { + // Deliberately invalid Wasm proves rejection does not instantiate untrusted code. + let wasm = b"not a Wasm module"; + let mut storage = Storage::default(); + storage + .top + .insert(well_known_keys::CODE.to_vec(), wasm.to_vec()); + let block = crate::Block { + header: crate::Header::new( + 0, + Default::default(), + Default::default(), + Default::default(), + Default::default(), + ), + extrinsics: Vec::new(), + }; + let mut trailing_bytes = block.encode(); + trailing_bytes.push(0); + let mut noncanonical_length = block.encode(); + assert_eq!(noncanonical_length.pop(), Some(0)); + // Empty extrinsics use one-byte Compact(0); a two-byte zero is noncanonical SCALE. + noncanonical_length.extend_from_slice(&[1, 0]); + for bytes in [Vec::new(), trailing_bytes, noncanonical_length] { + let error = replay_block(storage.clone(), wasm, &bytes, &InherentData::new()) + .expect_err("malformed or noncanonical block bytes must fail before Wasm loading"); + assert!(error.starts_with("Complete block decode:"), "{error}"); + } +} diff --git a/template/runtime/src/tmctol_read_model.rs b/template/runtime/src/tmctol_read_model.rs index 0a12f7d8..101b43de 100644 --- a/template/runtime/src/tmctol_read_model.rs +++ b/template/runtime/src/tmctol_read_model.rs @@ -7,12 +7,12 @@ //! historical metrics, dashboards, trend analysis, or alerting; those belong in external indexers, //! operator tooling, or test-only analytical helpers that may consume this projection. -use pallet_deos_actors::{ActorType, Task}; +use pallet_deos_actors::{ActorEligibility, ActorType, Task}; use polkadot_sdk::frame_support::traits::fungibles::Inspect as FungiblesInspect; use polkadot_sdk::pallet_asset_conversion::{self, PoolLocator}; use polkadot_sdk::sp_runtime::traits::Zero; use primitives::{ - AnchorBucketState, AnchorDomain, AssetKind, BurnDomain, BurnLivenessState, GuaranteeStatus, + AnchorDomain, AnchorState, AssetKind, BurnDomain, BurnLivenessState, GuaranteeStatus, PoolProjection, ReportedFloorInputs, TmctolConformanceStatus, TmctolGuaranteeState, ZapPostconditionState, ecosystem, }; @@ -23,13 +23,13 @@ pub struct TmctolReadModel; impl TmctolReadModel { pub fn tmctol_guarantee_state() -> TmctolGuaranteeState { - let tol_anchor = Self::anchor_bucket_state( + let tol_anchor = Self::anchor_state( AnchorDomain::Tol, ecosystem::actor_ids::TOL_BUCKET_A_ACTORS_ID, ); - let bldr_anchor = Self::anchor_bucket_state( + let bldr_anchor = Self::anchor_state( AnchorDomain::Bldr, - ecosystem::actor_ids::BLDR_BUCKET_A_ACTORS_ID, + ecosystem::actor_ids::BLDR_ANCHOR_ACTORS_ID, ); let tol_counterparty = @@ -70,45 +70,54 @@ impl TmctolReadModel { } } - fn anchor_bucket_state(domain: AnchorDomain, actor_id: u64) -> AnchorBucketState { + fn anchor_state(domain: AnchorDomain, actor_id: u64) -> AnchorState { let sovereign_account = crate::Actors::sovereign_account_id_system(actor_id); - let is_custody_only = matches!( - (domain, actor_id), - ( - AnchorDomain::Tol, - ecosystem::actor_ids::TOL_BUCKET_A_ACTORS_ID - ) | ( - AnchorDomain::Bldr, - ecosystem::actor_ids::BLDR_BUCKET_A_ACTORS_ID - ) - ); - let actor_identity_exists = - pallet_deos_actors::ActorIdentities::::contains_key(actor_id); - let scheduler_state_exists = pallet_deos_actors::ActorHot::::get(actor_id) - .is_some_and(|hot| { - hot.pending_signal || hot.queue_ticket.is_some() || hot.wakeup_pointer.is_some() - }); - let status = if is_custody_only && !actor_identity_exists && !scheduler_state_exists { + let identity = crate::Actors::actor_identity(actor_id); + let is_immutable_system_actor = identity.as_ref().is_some_and(|identity| { + identity.sovereign_account == sovereign_account + && identity.mutability == pallet_deos_actors::Mutability::Immutable + && matches!( + identity.actor_class, + pallet_deos_actors::ActorClass::System { sovereign_id } if sovereign_id == actor_id + ) + }); + let actor_identity_exists = identity.is_some(); + let max_tail_chunks = ::MaxContractSteps::get() + .saturating_sub(1) + .div_ceil(pallet_deos_actors::MAX_STEPS_PER_TAIL_CHUNK); + let has_tail_chunk = (0..max_tail_chunks).any(|chunk_index| { + pallet_deos_actors::ActorContractTailChunks::::contains_key(actor_id, chunk_index) + }); + let active_state_exists = + pallet_deos_actors::ActorControlLocators::::contains_key(actor_id) + || pallet_deos_actors::ActorUnsignaledControlCells::::contains_key(actor_id) + || pallet_deos_actors::ActorContractHeads::::contains_key(actor_id) + || pallet_deos_actors::ActorActivationAuthorities::::contains_key(actor_id) + || has_tail_chunk + || pallet_deos_actors::ActorFunding::::contains_key(actor_id) + || pallet_deos_actors::ActorRunHeads::::contains_key(actor_id) + || pallet_deos_actors::ActorRunPayloads::::contains_key(actor_id); + let status = if is_immutable_system_actor && !active_state_exists { GuaranteeStatus::Satisfied } else { GuaranteeStatus::Violated }; - AnchorBucketState { + AnchorState { domain, actor_id, status, sovereign_account, - is_custody_only, + is_immutable_system_actor, actor_identity_exists, - scheduler_state_exists, + active_state_exists, } } fn pool_projection( domain: AnchorDomain, asset_b: Option, - anchor: &AnchorBucketState, + anchor: &AnchorState, ) -> PoolProjection { let asset_a = AssetKind::Native; let Some(asset_b_value) = asset_b else { @@ -249,9 +258,7 @@ impl TmctolReadModel { let sovereign_account = crate::Actors::sovereign_account_id_system(actor_id); let target_balance = Self::asset_balance(target_asset, &sovereign_account); 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(crate::Actors::actor_contract(actor_id)); + let maybe_actor = crate::Actors::active_actor_state(actor_id); let ( actor_exists, is_system, @@ -260,23 +267,26 @@ impl TmctolReadModel { has_required_burn_step, has_required_swap_step, ) = maybe_actor - .map(|((identity, hot), program)| { + .map(|state| { + let program = state.contract; let has_address_event_trigger = program.trigger.address_event_source_enabled(); let has_required_burn_step = program.steps.iter().any(|step| burn_match(&step.task)); let has_required_swap_step = !requires_swap || program.steps.iter().any(|step| swap_match(&step.task)); ( true, - identity.actor_class.actor_type() == ActorType::System, - hot.lifecycle.is_paused(), + state.identity.actor_class.actor_type() == ActorType::System, + state.hot.lifecycle.is_paused(), has_address_event_trigger, has_required_burn_step, has_required_swap_step, ) }) .unwrap_or((false, false, false, false, false, !requires_swap)); - let dormant = pallet_deos_actors::ActorIdentities::::contains_key(actor_id) - && !pallet_deos_actors::ActorHot::::contains_key(actor_id); + let dormant = matches!( + crate::Actors::actor_eligibility(actor_id), + Ok(ActorEligibility::Dormant) + ); let status = if domain == BurnDomain::BldrBuyback && dormant { GuaranteeStatus::NotInitialized } else if !actor_exists || !is_system || is_paused || !has_address_event_trigger { @@ -314,11 +324,11 @@ impl TmctolReadModel { fn zap_postconditions() -> ZapPostconditionState { let actor_id = ecosystem::actor_ids::LIQUIDITY_ACTOR_ACTORS_ID; 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(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) { + let Some(state) = crate::Actors::active_actor_state(actor_id) else { + let status = if matches!( + crate::Actors::actor_eligibility(actor_id), + Ok(ActorEligibility::Dormant) + ) { GuaranteeStatus::NotInitialized } else { GuaranteeStatus::Violated @@ -342,7 +352,8 @@ impl TmctolReadModel { }; }; - let is_system = identity.actor_class.actor_type() == ActorType::System; + let program = state.contract; + let is_system = state.identity.actor_class.actor_type() == ActorType::System; let has_address_event_trigger = program.trigger.address_event_source_enabled(); let mut foreign_from_add: Option = None; let mut foreign_from_swap: Option = None; @@ -433,7 +444,7 @@ impl TmctolReadModel { && split_targets_all_buckets && split_shares_sum_to_one && split_shares_match_policy; - let status = if !is_system || hot.lifecycle.is_paused() || !has_address_event_trigger { + let status = if !is_system || state.hot.lifecycle.is_paused() || !has_address_event_trigger { GuaranteeStatus::Violated } else if structure_satisfied { GuaranteeStatus::Satisfied @@ -447,7 +458,7 @@ impl TmctolReadModel { sovereign_account, actor_exists: true, is_system, - is_paused: hot.lifecycle.is_paused(), + is_paused: state.hot.lifecycle.is_paused(), has_address_event_trigger, configured_foreign_asset, configured_lp_asset, diff --git a/template/runtime/src/weights/pallet_deos_actors.rs b/template/runtime/src/weights/pallet_deos_actors.rs index c8181a93..b9bd96e8 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-31, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2026-09-08, 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 @@ -19,6 +19,70 @@ // --extrinsic // * // --exclude-extrinsics +// pallet_deos_actors::scheduler_inner_zero_step_user_complete +// --exclude-extrinsics +// pallet_deos_actors::scheduler_inner_zero_step_user_max_head +// --exclude-extrinsics +// pallet_deos_actors::scheduler_inner_zero_step_user_cadenced +// --exclude-extrinsics +// pallet_deos_actors::scheduler_inner_zero_step_user_cadenced_close +// --exclude-extrinsics +// pallet_deos_actors::scheduler_inner_zero_step_user_at_time +// --exclude-extrinsics +// pallet_deos_actors::scheduler_inner_zero_step_user_at_time_close +// --exclude-extrinsics +// pallet_deos_actors::scheduler_wakeup_zero_step_user_expiry +// --exclude-extrinsics +// pallet_deos_actors::scheduler_paged_zero_step_user_insolvency +// --exclude-extrinsics +// pallet_deos_actors::scheduler_paged_zero_step_user_crossing_insolvency +// --exclude-extrinsics +// pallet_deos_actors::scheduler_paged_user_crossing_insolvency_max_contract +// --exclude-extrinsics +// pallet_deos_actors::scheduler_paged_user_crossing_insolvency_max_contract_window +// --exclude-extrinsics +// pallet_deos_actors::pipeline_admission_apoptosis_crossing +// --exclude-extrinsics +// pallet_deos_actors::scheduler_inner_zero_step_user_crossing_armed +// --exclude-extrinsics +// pallet_deos_actors::scheduler_inner_zero_step_user_crossing_armed_pages +// --exclude-extrinsics +// pallet_deos_actors::scheduler_inner_zero_step_user_crossing_armed_new_page +// --exclude-extrinsics +// pallet_deos_actors::scheduler_inner_zero_step_user_crossing_armed_pages_close +// --exclude-extrinsics +// pallet_deos_actors::scheduler_inner_zero_step_user_crossing_armed_new_page_close +// --exclude-extrinsics +// pallet_deos_actors::scheduler_inner_zero_step_user_crossing_cursor +// --exclude-extrinsics +// pallet_deos_actors::scheduler_inner_zero_step_user_crossing_cursor_close +// --exclude-extrinsics +// pallet_deos_actors::scheduler_inner_zero_step_user_crossing_waiting +// --exclude-extrinsics +// pallet_deos_actors::scheduler_inner_zero_step_user_crossing_close +// --exclude-extrinsics +// pallet_deos_actors::scheduler_inner_zero_step_user_crossing_page +// --exclude-extrinsics +// pallet_deos_actors::scheduler_inner_zero_step_user_crossing_page_close +// --exclude-extrinsics +// pallet_deos_actors::scheduler_inner_zero_step_user_crossing_page_full +// --exclude-extrinsics +// pallet_deos_actors::scheduler_inner_zero_step_user_crossing_page_full_close +// --exclude-extrinsics +// pallet_deos_actors::scheduler_inner_zero_step_user_observation +// --exclude-extrinsics +// pallet_deos_actors::scheduler_inner_zero_step_user_observation_close +// --exclude-extrinsics +// pallet_deos_actors::scheduler_inner_zero_step_user_observation_page_close +// --exclude-extrinsics +// pallet_deos_actors::scheduler_inner_zero_step_user_observation_unlink +// --exclude-extrinsics +// pallet_deos_actors::scheduler_inner_zero_step_user_observation_feed_close +// --exclude-extrinsics +// pallet_deos_actors::pipeline_zero_step_opening_collection +// --exclude-extrinsics +// pallet_deos_actors::pipeline_large_head_opening_collection +// --exclude-extrinsics // pallet_deos_actors::process_remove_liquidity_indexed // --exclude-extrinsics // pallet_deos_actors::scheduler_on_idle_healthy_empty @@ -29,6 +93,8 @@ // --exclude-extrinsics // pallet_deos_actors::close_actor_system_pure // --exclude-extrinsics +// pallet_deos_actors::close_actor_opening_partition +// --exclude-extrinsics // pallet_deos_actors::close_actor_crossing_page // --exclude-extrinsics // pallet_deos_actors::close_actor_crossing_tail @@ -47,6 +113,10 @@ // --exclude-extrinsics // pallet_deos_actors::precondition_observation // --exclude-extrinsics +// pallet_deos_actors::scheduler_wakeup_cursor_remove_upward_depth +// --exclude-extrinsics +// pallet_deos_actors::scheduler_wakeup_cursor_remove_upward_pages +// --exclude-extrinsics // pallet_deos_actors::benchmark_monolithic_create // --exclude-extrinsics // pallet_deos_actors::benchmark_chunked_create @@ -86,6 +156,8 @@ // 4096 // --output // template/runtime/src/weights/pallet_deos_actors.rs +// --min-duration +// 0 #![cfg_attr(rustfmt, rustfmt_skip)] #![allow(unused_parens)] @@ -108,16 +180,24 @@ impl pallet_deos_actors::WeightInfo for S /// Proof: `Actors::ActorIdentityCount` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `Actors::NextActorId` (r:1 w:1) /// Proof: `Actors::NextActorId` (`max_values`: Some(1), `max_size`: Some(8), added: 503, 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::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorUnsignaledControlCells` (r:1 w:1) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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(1066), added: 3541, 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::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(1491), added: 3966, 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::ActorContractTailChunk` (r:4 w:3) + /// Proof: `Actors::ActorContractTailChunk` (`max_values`: None, `max_size`: Some(4070), added: 6545, 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::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) @@ -132,8 +212,6 @@ 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) @@ -144,27 +222,23 @@ impl pallet_deos_actors::WeightInfo for S /// 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: `2506` + // Measured: `2481` // Estimated: `81886` - // Minimum execution time: 464_243_000 picoseconds. - Weight::from_parts(489_874_000, 0) + // Minimum execution time: 509_780_000 picoseconds. + Weight::from_parts(563_908_000, 0) .saturating_add(Weight::from_parts(0, 81886)) - .saturating_add(T::DbWeight::get().reads(65)) - .saturating_add(T::DbWeight::get().writes(60)) + .saturating_add(T::DbWeight::get().reads(63)) + .saturating_add(T::DbWeight::get().writes(55)) } /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) @@ -176,16 +250,24 @@ impl pallet_deos_actors::WeightInfo for S /// Proof: `Actors::ActorIdentityCount` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `Actors::NextActorId` (r:1 w:1) /// Proof: `Actors::NextActorId` (`max_values`: Some(1), `max_size`: Some(8), added: 503, 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::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorUnsignaledControlCells` (r:1 w:1) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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(1066), added: 3541, 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::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(1491), added: 3966, 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::ActorContractTailChunk` (r:4 w:3) + /// Proof: `Actors::ActorContractTailChunk` (`max_values`: None, `max_size`: Some(4070), added: 6545, 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::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) @@ -200,8 +282,6 @@ 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) @@ -212,27 +292,23 @@ impl pallet_deos_actors::WeightInfo for S /// 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: `2404` + // Measured: `2379` // Estimated: `81886` - // Minimum execution time: 461_868_000 picoseconds. - Weight::from_parts(481_145_000, 0) + // Minimum execution time: 512_923_000 picoseconds. + Weight::from_parts(604_277_000, 0) .saturating_add(Weight::from_parts(0, 81886)) - .saturating_add(T::DbWeight::get().reads(65)) - .saturating_add(T::DbWeight::get().writes(60)) + .saturating_add(T::DbWeight::get().reads(63)) + .saturating_add(T::DbWeight::get().writes(55)) } /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) @@ -244,16 +320,24 @@ impl pallet_deos_actors::WeightInfo for S /// Proof: `Actors::ActorIdentityCount` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `Actors::NextActorId` (r:1 w:1) /// Proof: `Actors::NextActorId` (`max_values`: Some(1), `max_size`: Some(8), added: 503, 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::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorUnsignaledControlCells` (r:1 w:1) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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(1066), added: 3541, 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::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(1491), added: 3966, 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::ActorContractTailChunk` (r:4 w:3) + /// Proof: `Actors::ActorContractTailChunk` (`max_values`: None, `max_size`: Some(4070), added: 6545, 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::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) @@ -268,8 +352,6 @@ 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) @@ -278,25 +360,21 @@ impl pallet_deos_actors::WeightInfo for S /// 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: `2308` + // Measured: `2266` // Estimated: `81886` - // Minimum execution time: 520_396_000 picoseconds. - Weight::from_parts(541_837_000, 0) + // Minimum execution time: 481_354_000 picoseconds. + Weight::from_parts(594_079_000, 0) .saturating_add(Weight::from_parts(0, 81886)) - .saturating_add(T::DbWeight::get().reads(63)) - .saturating_add(T::DbWeight::get().writes(58)) + .saturating_add(T::DbWeight::get().reads(61)) + .saturating_add(T::DbWeight::get().writes(53)) } /// Storage: `Actors::SystemSovereigns` (r:1 w:1) /// Proof: `Actors::SystemSovereigns` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `MaxEncodedLen`) @@ -310,16 +388,24 @@ impl pallet_deos_actors::WeightInfo for S /// Proof: `Actors::ActorIdentityCount` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `Actors::NextActorId` (r:1 w:1) /// Proof: `Actors::NextActorId` (`max_values`: Some(1), `max_size`: Some(8), added: 503, 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::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorUnsignaledControlCells` (r:1 w:1) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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(1066), added: 3541, 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::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(1491), added: 3966, 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::ActorContractTailChunk` (r:4 w:3) + /// Proof: `Actors::ActorContractTailChunk` (`max_values`: None, `max_size`: Some(4070), added: 6545, 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::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) @@ -330,8 +416,6 @@ 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) @@ -340,23 +424,19 @@ impl pallet_deos_actors::WeightInfo for S /// 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: `2372` + // Measured: `2263` // Estimated: `81886` - // Minimum execution time: 509_500_000 picoseconds. - Weight::from_parts(528_846_000, 0) + // Minimum execution time: 498_047_000 picoseconds. + Weight::from_parts(743_612_000, 0) .saturating_add(Weight::from_parts(0, 81886)) - .saturating_add(T::DbWeight::get().reads(61)) - .saturating_add(T::DbWeight::get().writes(56)) + .saturating_add(T::DbWeight::get().reads(59)) + .saturating_add(T::DbWeight::get().writes(51)) } /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) @@ -368,16 +448,24 @@ impl pallet_deos_actors::WeightInfo for S /// Proof: `Actors::ActorIdentityCount` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `Actors::NextActorId` (r:1 w:1) /// Proof: `Actors::NextActorId` (`max_values`: Some(1), `max_size`: Some(8), added: 503, 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::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorUnsignaledControlCells` (r:1 w:1) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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(1066), added: 3541, 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::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(1491), added: 3966, 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::ActorContractTailChunk` (r:4 w:3) + /// Proof: `Actors::ActorContractTailChunk` (`max_values`: None, `max_size`: Some(4070), added: 6545, 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::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) @@ -392,8 +480,6 @@ 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) @@ -402,27 +488,23 @@ impl pallet_deos_actors::WeightInfo for S /// 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(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: `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)) + // Measured: `15413` + // Estimated: `27170` + // Minimum execution time: 632_982_000 picoseconds. + Weight::from_parts(755_066_000, 0) + .saturating_add(Weight::from_parts(0, 27170)) + .saturating_add(T::DbWeight::get().reads(31)) + .saturating_add(T::DbWeight::get().writes(23)) } /// Storage: `Actors::GlobalCircuitBreaker` (r:1 w:0) /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) @@ -430,16 +512,24 @@ impl pallet_deos_actors::WeightInfo for S /// Proof: `Actors::ActorIdentityCount` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `Actors::NextActorId` (r:1 w:1) /// Proof: `Actors::NextActorId` (`max_values`: Some(1), `max_size`: Some(8), added: 503, 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:0) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorControlLocators` (r:1 w:0) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorUnsignaledControlCells` (r:1 w:0) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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(1066), added: 3541, 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::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(1491), added: 3966, 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::ActorContractTailChunk` (r:1 w:0) + /// Proof: `Actors::ActorContractTailChunk` (`max_values`: None, `max_size`: Some(4070), added: 6545, 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::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) @@ -452,24 +542,32 @@ impl pallet_deos_actors::WeightInfo for S /// 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: `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)) + // Measured: `1828` + // Estimated: `7535` + // Minimum execution time: 104_554_000 picoseconds. + Weight::from_parts(139_895_000, 0) + .saturating_add(Weight::from_parts(0, 7535)) + .saturating_add(T::DbWeight::get().reads(17)) .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::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorUnsignaledControlCells` (r:1 w:1) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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(1066), added: 3541, 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::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(1491), added: 3966, 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::ActorContractTailChunk` (r:4 w:3) + /// Proof: `Actors::ActorContractTailChunk` (`max_values`: None, `max_size`: Some(4070), added: 6545, 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::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) @@ -484,8 +582,6 @@ 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) @@ -494,46 +590,54 @@ impl pallet_deos_actors::WeightInfo for S /// 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: `2347` + // Measured: `2305` // Estimated: `81886` - // Minimum execution time: 499_932_000 picoseconds. - Weight::from_parts(520_675_000, 0) + // Minimum execution time: 552_035_000 picoseconds. + Weight::from_parts(878_618_000, 0) .saturating_add(Weight::from_parts(0, 81886)) - .saturating_add(T::DbWeight::get().reads(57)) - .saturating_add(T::DbWeight::get().writes(52)) + .saturating_add(T::DbWeight::get().reads(55)) + .saturating_add(T::DbWeight::get().writes(47)) } + /// Storage: `Actors::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, 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::ActorWaitingFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorWaitingFrameChunks` (`max_values`: None, `max_size`: Some(11708), added: 14183, 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) + /// Storage: `Actors::ActorContractTailChunk` (r:3 w:3) /// 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(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`) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(1491), added: 3966, 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::ActorWaitingOccupancies` (r:1 w:1) + /// Proof: `Actors::ActorWaitingOccupancies` (`max_values`: None, `max_size`: Some(29), added: 2504, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorWaitingHeads` (r:1 w:1) + /// Proof: `Actors::ActorWaitingHeads` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorWaitingTails` (r:1 w:1) + /// Proof: `Actors::ActorWaitingTails` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorWaitingCursorIndices` (r:1 w:1) + /// Proof: `Actors::ActorWaitingCursorIndices` (`max_values`: None, `max_size`: Some(29), added: 2504, 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::WakeupCursorLen` (r:1 w:1) + /// Proof: `Actors::WakeupCursorLen` (`max_values`: None, `max_size`: Some(21), added: 2496, 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) @@ -550,6 +654,8 @@ impl pallet_deos_actors::WeightInfo for S /// Proof: `Actors::CrossingRadixNodes` (`max_values`: None, `max_size`: Some(53), added: 2528, 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::ActorUnsignaledControlCells` (r:0 w:1) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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) @@ -558,146 +664,152 @@ impl pallet_deos_actors::WeightInfo for S /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) fn deactivate_actor() -> Weight { // Proof Size summary in bytes: - // Measured: `8967` + // Measured: `6582` // Estimated: `81886` - // Minimum execution time: 543_094_000 picoseconds. - Weight::from_parts(561_742_000, 0) + // Minimum execution time: 802_210_000 picoseconds. + Weight::from_parts(1_255_487_000, 0) .saturating_add(Weight::from_parts(0, 81886)) - .saturating_add(T::DbWeight::get().reads(57)) - .saturating_add(T::DbWeight::get().writes(55)) + .saturating_add(T::DbWeight::get().reads(58)) + .saturating_add(T::DbWeight::get().writes(57)) } - /// Storage: `Actors::ActorIdentities` (r:1 w:1) + /// Storage: `Actors::ActorControlLocators` (r:1 w:0) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, 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(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorUnsignaledControlCells` (r:1 w:1) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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: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(1491), added: 3966, 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: `1825` + // Measured: `1749` // Estimated: `5736` - // Minimum execution time: 67_537_000 picoseconds. - Weight::from_parts(70_541_000, 0) + // Minimum execution time: 122_644_000 picoseconds. + Weight::from_parts(140_663_000, 0) .saturating_add(Weight::from_parts(0, 5736)) - .saturating_add(T::DbWeight::get().reads(7)) - .saturating_add(T::DbWeight::get().writes(2)) + .saturating_add(T::DbWeight::get().reads(8)) + .saturating_add(T::DbWeight::get().writes(1)) } - /// Storage: `Actors::ActorIdentities` (r:1 w:1) + /// 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(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorUnsignaledControlCells` (r:1 w:1) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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: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(1491), added: 3966, 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: `1825` + // Measured: `1749` // Estimated: `5736` - // Minimum execution time: 67_188_000 picoseconds. - Weight::from_parts(69_214_000, 0) + // Minimum execution time: 114_402_000 picoseconds. + Weight::from_parts(123_482_000, 0) .saturating_add(Weight::from_parts(0, 5736)) - .saturating_add(T::DbWeight::get().reads(7)) + .saturating_add(T::DbWeight::get().reads(8)) .saturating_add(T::DbWeight::get().writes(2)) } + /// Storage: `Actors::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, 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(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorUnsignaledControlCells` (r:1 w:1) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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: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(1491), added: 3966, 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) - /// 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`) + /// Storage: `Actors::ActorReadyHead` (r:1 w:0) + /// Proof: `Actors::ActorReadyHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyTail` (r:1 w:1) + /// Proof: `Actors::ActorReadyTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyOccupancy` (r:1 w:1) + /// Proof: `Actors::ActorReadyOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, mode: `MaxEncodedLen`) fn manual_trigger() -> Weight { // Proof Size summary in bytes: - // 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)) + // Measured: `2452` + // Estimated: `15106` + // Minimum execution time: 208_549_000 picoseconds. + Weight::from_parts(243_890_000, 0) + .saturating_add(Weight::from_parts(0, 15106)) .saturating_add(T::DbWeight::get().reads(14)) .saturating_add(T::DbWeight::get().writes(7)) } + /// Storage: `Actors::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, 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::ActorUnsignaledControlCells` (r:1 w:1) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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::ActorRunPayload` (r:1 w:0) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(1491), added: 3966, 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`) + /// Storage: `Actors::ActorReadyHead` (r:1 w:0) + /// Proof: `Actors::ActorReadyHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::ActorReadyTail` (r:1 w:1) + /// Proof: `Actors::ActorReadyTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::ActorReadyOccupancy` (r:1 w:1) + /// Proof: `Actors::ActorReadyOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `Measured`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, 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)) + // Measured: `2454` + // Estimated: `8394` + // Minimum execution time: 227_406_000 picoseconds. + Weight::from_parts(292_779_000, 0) + .saturating_add(Weight::from_parts(0, 8394)) .saturating_add(T::DbWeight::get().reads(14)) .saturating_add(T::DbWeight::get().writes(7)) } - /// Storage: `Actors::ActorIdentities` (r:1 w:1) + /// Storage: `Actors::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, 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(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, 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::ActorRunPayload` (r:1 w:1) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(1491), added: 3966, 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) @@ -712,42 +824,46 @@ 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:0) /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyOccupancy` (r:1 w:1) + /// Proof: `Actors::ActorReadyOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, 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::ActorUnsignaledControlCells` (r:1 w:0) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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`) /// 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)) + // Measured: `2677` + // Estimated: `15106` + // Minimum execution time: 203_102_000 picoseconds. + Weight::from_parts(387_346_000, 0) + .saturating_add(Weight::from_parts(0, 15106)) + .saturating_add(T::DbWeight::get().reads(19)) + .saturating_add(T::DbWeight::get().writes(16)) } - /// Storage: `Actors::ActorIdentities` (r:1 w:1) + /// Storage: `Actors::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, 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(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorWaitingFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorWaitingFrameChunks` (`max_values`: None, `max_size`: Some(11708), added: 14183, 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) + /// Storage: `Actors::ActorContractTailChunk` (r:3 w:3) /// 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(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`) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(1491), added: 3966, 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) @@ -762,12 +878,18 @@ 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::ActorWaitingOccupancies` (r:1 w:1) + /// Proof: `Actors::ActorWaitingOccupancies` (`max_values`: None, `max_size`: Some(29), added: 2504, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorWaitingHeads` (r:1 w:1) + /// Proof: `Actors::ActorWaitingHeads` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorWaitingTails` (r:1 w:1) + /// Proof: `Actors::ActorWaitingTails` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorWaitingCursorIndices` (r:1 w:1) + /// Proof: `Actors::ActorWaitingCursorIndices` (`max_values`: None, `max_size`: Some(29), added: 2504, 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::WakeupCursorLen` (r:1 w:1) + /// Proof: `Actors::WakeupCursorLen` (`max_values`: None, `max_size`: Some(21), added: 2496, 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) @@ -784,36 +906,46 @@ impl pallet_deos_actors::WeightInfo for S /// Proof: `Actors::CrossingPendingFeedListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, 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::ActorStateHolds` (r:1 w:1) + /// Proof: `Actors::ActorStateHolds` (`max_values`: None, `max_size`: Some(152), added: 2627, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorUnsignaledControlCells` (r:1 w:0) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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::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 close_actor() -> Weight { // Proof Size summary in bytes: - // Measured: `9691` + // Measured: `9705` // Estimated: `81886` - // Minimum execution time: 683_757_000 picoseconds. - Weight::from_parts(708_551_000, 0) + // Minimum execution time: 811_639_000 picoseconds. + Weight::from_parts(973_883_000, 0) .saturating_add(Weight::from_parts(0, 81886)) - .saturating_add(T::DbWeight::get().reads(64)) - .saturating_add(T::DbWeight::get().writes(64)) + .saturating_add(T::DbWeight::get().reads(66)) + .saturating_add(T::DbWeight::get().writes(65)) } - /// Storage: `Actors::ActorIdentities` (r:1 w:1) + /// Storage: `Actors::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, 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(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorWaitingFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorWaitingFrameChunks` (`max_values`: None, `max_size`: Some(11708), added: 14183, 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) + /// Storage: `Actors::ActorContractTailChunk` (r:3 w:3) /// 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(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`) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(1491), added: 3966, 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) @@ -824,14 +956,32 @@ 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::ActorWaitingOccupancies` (r:1 w:1) + /// Proof: `Actors::ActorWaitingOccupancies` (`max_values`: None, `max_size`: Some(29), added: 2504, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorWaitingHeads` (r:1 w:1) + /// Proof: `Actors::ActorWaitingHeads` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorWaitingTails` (r:1 w:1) + /// Proof: `Actors::ActorWaitingTails` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorWaitingCursorIndices` (r:1 w:1) + /// Proof: `Actors::ActorWaitingCursorIndices` (`max_values`: None, `max_size`: Some(29), added: 2504, 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::WakeupCursorLen` (r:1 w:1) + /// Proof: `Actors::WakeupCursorLen` (`max_values`: None, `max_size`: Some(21), added: 2496, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyHead` (r:1 w:0) + /// Proof: `Actors::ActorReadyHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyTail` (r:1 w:1) + /// Proof: `Actors::ActorReadyTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyOccupancy` (r:1 w:1) + /// Proof: `Actors::ActorReadyOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, 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) @@ -844,29 +994,23 @@ impl pallet_deos_actors::WeightInfo for S /// 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) + /// Storage: `Actors::CrossingRadixNodes` (r:64 w:64) /// Proof: `Actors::CrossingRadixNodes` (`max_values`: None, `max_size`: Some(53), added: 2528, 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:1) - /// Proof: `Actors::WakeupPages` (`max_values`: None, `max_size`: Some(348), added: 2823, 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::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: `10450` - // Estimated: `81886` - // 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(67)) - .saturating_add(T::DbWeight::get().writes(63)) + // Measured: `7530` + // Estimated: `162782` + // Minimum execution time: 1_140_317_000 picoseconds. + Weight::from_parts(1_352_289_000, 0) + .saturating_add(Weight::from_parts(0, 162782)) + .saturating_add(T::DbWeight::get().reads(101)) + .saturating_add(T::DbWeight::get().writes(94)) } /// Storage: `Actors::GlobalCircuitBreaker` (r:0 w:1) /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) @@ -874,8 +1018,8 @@ impl pallet_deos_actors::WeightInfo for S // Proof Size summary in bytes: // Measured: `0` // Estimated: `0` - // Minimum execution time: 5_238_000 picoseconds. - Weight::from_parts(6_845_000, 0) + // Minimum execution time: 6_635_000 picoseconds. + Weight::from_parts(7_683_000, 0) .saturating_add(Weight::from_parts(0, 0)) .saturating_add(T::DbWeight::get().writes(1)) } @@ -883,10 +1027,10 @@ impl pallet_deos_actors::WeightInfo for S /// 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` + // Measured: `465` // Estimated: `1529` - // Minimum execution time: 9_499_000 picoseconds. - Weight::from_parts(11_105_000, 0) + // Minimum execution time: 10_965_000 picoseconds. + Weight::from_parts(11_874_000, 0) .saturating_add(Weight::from_parts(0, 1529)) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) @@ -899,10 +1043,10 @@ impl pallet_deos_actors::WeightInfo for S /// 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` + // Measured: `784` // Estimated: `4106` - // Minimum execution time: 18_648_000 picoseconds. - Weight::from_parts(20_464_000, 0) + // Minimum execution time: 23_327_000 picoseconds. + Weight::from_parts(34_851_000, 0) .saturating_add(Weight::from_parts(0, 4106)) .saturating_add(T::DbWeight::get().reads(3)) .saturating_add(T::DbWeight::get().writes(1)) @@ -911,10 +1055,10 @@ impl pallet_deos_actors::WeightInfo for S /// 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` + // Measured: `426` // Estimated: `1503` - // Minimum execution time: 8_451_000 picoseconds. - Weight::from_parts(10_057_000, 0) + // Minimum execution time: 8_661_000 picoseconds. + Weight::from_parts(9_917_000, 0) .saturating_add(Weight::from_parts(0, 1503)) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) @@ -923,10 +1067,10 @@ impl pallet_deos_actors::WeightInfo for S /// 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: `459` + // Measured: `526` // Estimated: `1529` - // Minimum execution time: 11_943_000 picoseconds. - Weight::from_parts(13_759_000, 0) + // Minimum execution time: 13_270_000 picoseconds. + Weight::from_parts(14_597_000, 0) .saturating_add(Weight::from_parts(0, 1529)) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) @@ -935,10 +1079,10 @@ impl pallet_deos_actors::WeightInfo for S /// 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: `454` + // Measured: `487` // Estimated: `1629` - // Minimum execution time: 12_222_000 picoseconds. - Weight::from_parts(13_968_000, 0) + // Minimum execution time: 13_550_000 picoseconds. + Weight::from_parts(15_155_000, 0) .saturating_add(Weight::from_parts(0, 1629)) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) @@ -947,10 +1091,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: `431` + // Measured: `460` // Estimated: `1503` - // Minimum execution time: 10_826_000 picoseconds. - Weight::from_parts(12_501_000, 0) + // Minimum execution time: 11_455_000 picoseconds. + Weight::from_parts(12_851_000, 0) .saturating_add(Weight::from_parts(0, 1503)) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) @@ -961,48 +1105,52 @@ 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: `421` + // Measured: `491` // Estimated: `1489` - // Minimum execution time: 8_032_000 picoseconds. - Weight::from_parts(9_918_000, 0) + // Minimum execution time: 10_825_000 picoseconds. + Weight::from_parts(11_454_000, 0) .saturating_add(Weight::from_parts(0, 1489)) .saturating_add(T::DbWeight::get().reads(2)) } + /// Storage: `Actors::ActorControlLocators` (r:1 w:0) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, 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(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorUnsignaledControlCells` (r:1 w:0) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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: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(1491), added: 3966, 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: `1825` + // Measured: `1749` // Estimated: `5736` - // Minimum execution time: 46_305_000 picoseconds. - Weight::from_parts(48_610_000, 0) + // Minimum execution time: 65_373_000 picoseconds. + Weight::from_parts(89_608_000, 0) .saturating_add(Weight::from_parts(0, 5736)) - .saturating_add(T::DbWeight::get().reads(7)) + .saturating_add(T::DbWeight::get().reads(8)) } - /// Storage: `Actors::ActorIdentities` (r:5 w:5) + /// Storage: `Actors::ActorControlLocators` (r:5 w:5) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorIdentities` (r:5 w:0) /// 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(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorUnsignaledControlCells` (r:5 w:5) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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(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::ActorRunPayload` (r:5 w:5) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(1491), added: 3966, 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) @@ -1025,8 +1173,6 @@ impl pallet_deos_actors::WeightInfo for S /// 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) @@ -1034,80 +1180,94 @@ impl pallet_deos_actors::WeightInfo for S /// The range of component `n` is `[1, 5]`. fn permissionless_sweep_many(n: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `1291 + n * (1222 ±0)` + // Measured: `1320 + n * (1198 ±0)` // Estimated: `1489 + n * (4746 ±0)` - // Minimum execution time: 158_752_000 picoseconds. - Weight::from_parts(26_061_500, 0) + // Minimum execution time: 201_356_000 picoseconds. + Weight::from_parts(86_378_643, 0) .saturating_add(Weight::from_parts(0, 1489)) - // Standard Error: 74_200 - .saturating_add(Weight::from_parts(140_666_863, 0).saturating_mul(n.into())) + // Standard Error: 1_217_682 + .saturating_add(Weight::from_parts(173_665_591, 0).saturating_mul(n.into())) .saturating_add(T::DbWeight::get().reads(3)) - .saturating_add(T::DbWeight::get().reads((14_u64).saturating_mul(n.into()))) + .saturating_add(T::DbWeight::get().reads((15_u64).saturating_mul(n.into()))) .saturating_add(T::DbWeight::get().writes(2)) - .saturating_add(T::DbWeight::get().writes((14_u64).saturating_mul(n.into()))) + .saturating_add(T::DbWeight::get().writes((13_u64).saturating_mul(n.into()))) .saturating_add(Weight::from_parts(0, 4746).saturating_mul(n.into())) } - /// Storage: `System::Account` (r:1 w:1) + /// Storage: `System::Account` (r:2 w:2) /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) fn fee_collection() -> Weight { // Proof Size summary in bytes: - // Measured: `207` - // Estimated: `3593` - // 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)) + // Measured: `381` + // Estimated: `6196` + // Minimum execution time: 47_213_000 picoseconds. + Weight::from_parts(50_007_000, 0) + .saturating_add(Weight::from_parts(0, 6196)) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(2)) + } + fn action_invocation_receipt() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 3_282_000 picoseconds. + Weight::from_parts(4_680_000, 0) + .saturating_add(Weight::from_parts(0, 0)) } /// 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::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, 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(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorUnsignaledControlCells` (r:1 w:1) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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(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(1491), added: 3966, 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: `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) - /// 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::WakeupBuckets` (r:1 w:1) - /// Proof: `Actors::WakeupBuckets` (`max_values`: None, `max_size`: Some(58), added: 2533, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyHead` (r:1 w:0) + /// Proof: `Actors::ActorReadyHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyTail` (r:1 w:0) + /// Proof: `Actors::ActorReadyTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyOccupancy` (r:1 w:0) + /// Proof: `Actors::ActorReadyOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:2 w:0) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorWaitingOccupancies` (r:1 w:1) + /// Proof: `Actors::ActorWaitingOccupancies` (`max_values`: None, `max_size`: Some(29), added: 2504, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorWaitingTails` (r:1 w:1) + /// Proof: `Actors::ActorWaitingTails` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorWaitingHeads` (r:1 w:1) + /// Proof: `Actors::ActorWaitingHeads` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorWaitingFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorWaitingFrameChunks` (`max_values`: None, `max_size`: Some(11708), added: 14183, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorWaitingCursorIndices` (r:1 w:1) + /// Proof: `Actors::ActorWaitingCursorIndices` (`max_values`: None, `max_size`: Some(29), added: 2504, 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:1) - /// Proof: `Actors::WakeupPages` (`max_values`: None, `max_size`: Some(348), added: 2823, mode: `MaxEncodedLen`) fn task_transfer() -> Weight { // Proof Size summary in bytes: - // 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)) + // Measured: `5307` + // Estimated: `29222` + // Minimum execution time: 417_448_000 picoseconds. + Weight::from_parts(468_433_000, 0) + .saturating_add(Weight::from_parts(0, 29222)) + .saturating_add(T::DbWeight::get().reads(25)) + .saturating_add(T::DbWeight::get().writes(12)) } /// Storage: `System::Account` (r:1 w:1) /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) @@ -1115,8 +1275,8 @@ impl pallet_deos_actors::WeightInfo for S // Proof Size summary in bytes: // Measured: `140` // Estimated: `3593` - // Minimum execution time: 17_251_000 picoseconds. - Weight::from_parts(18_997_000, 0) + // Minimum execution time: 18_159_000 picoseconds. + Weight::from_parts(20_045_000, 0) .saturating_add(Weight::from_parts(0, 3593)) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) @@ -1125,130 +1285,143 @@ 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::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, 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(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorUnsignaledControlCells` (r:1 w:1) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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: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(1491), added: 3966, 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) - /// 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::WakeupBuckets` (r:1 w:1) - /// Proof: `Actors::WakeupBuckets` (`max_values`: None, `max_size`: Some(58), added: 2533, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyHead` (r:1 w:0) + /// Proof: `Actors::ActorReadyHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyTail` (r:1 w:0) + /// Proof: `Actors::ActorReadyTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyOccupancy` (r:1 w:0) + /// Proof: `Actors::ActorReadyOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:2 w:0) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorWaitingOccupancies` (r:1 w:1) + /// Proof: `Actors::ActorWaitingOccupancies` (`max_values`: None, `max_size`: Some(29), added: 2504, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorWaitingTails` (r:1 w:1) + /// Proof: `Actors::ActorWaitingTails` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorWaitingHeads` (r:1 w:1) + /// Proof: `Actors::ActorWaitingHeads` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorWaitingFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorWaitingFrameChunks` (`max_values`: None, `max_size`: Some(11708), added: 14183, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorWaitingCursorIndices` (r:1 w:1) + /// Proof: `Actors::ActorWaitingCursorIndices` (`max_values`: None, `max_size`: Some(29), added: 2504, 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:1) - /// Proof: `Actors::WakeupPages` (`max_values`: None, `max_size`: Some(348), added: 2823, mode: `MaxEncodedLen`) fn task_mint() -> Weight { // Proof Size summary in bytes: - // 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)) + // Measured: `4081` + // Estimated: `29222` + // Minimum execution time: 313_383_000 picoseconds. + Weight::from_parts(343_415_000, 0) + .saturating_add(Weight::from_parts(0, 29222)) + .saturating_add(T::DbWeight::get().reads(22)) + .saturating_add(T::DbWeight::get().writes(10)) } - /// Storage: `System::Account` (r:1 w:0) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) /// Storage: `Assets::Asset` (r:3 w:0) - /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) + /// 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: `MaxEncodedLen`) - /// The range of component `c` is `[1, 4]`. - fn predicate_set_evaluation(c: u32, ) -> Weight { + /// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `Measured`) + /// Storage: `Oracle::Feeds` (r:1 w:0) + /// Proof: `Oracle::Feeds` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `Measured`) + /// Storage: `Oracle::Observations` (r:1 w:0) + /// Proof: `Oracle::Observations` (`max_values`: None, `max_size`: Some(61), added: 2536, mode: `Measured`) + /// The range of component `c` is `[1, 4]`. + fn predicate_set_evaluation(c: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `0 + c * (488 ±0)` - // Estimated: `3675 + c * (674 ±18)` - // Minimum execution time: 6_146_000 picoseconds. - Weight::from_parts(7_264_000, 0) - .saturating_add(Weight::from_parts(0, 3675)) - // 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())) + // Measured: `350 + c * (369 ±0)` + // Estimated: `3839 + c * (1740 ±143)` + // Minimum execution time: 15_645_000 picoseconds. + Weight::from_parts(6_906_425, 0) + .saturating_add(Weight::from_parts(0, 3839)) + // Standard Error: 78_944 + .saturating_add(Weight::from_parts(11_187_530, 0).saturating_mul(c.into())) + .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(c.into()))) + .saturating_add(Weight::from_parts(0, 1740).saturating_mul(c.into())) } fn task_stop_cycle() -> Weight { // Proof Size summary in bytes: // Measured: `0` // Estimated: `0` - // Minimum execution time: 2_793_000 picoseconds. - Weight::from_parts(4_260_000, 0) + // Minimum execution time: 5_169_000 picoseconds. + Weight::from_parts(6_705_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::ActorControlLocators` (r:8 w:8) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, 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(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorUnsignaledControlCells` (r:8 w:8) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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(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::ActorRunPayload` (r:8 w:0) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(1491), added: 3966, 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: `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) - /// 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::WakeupBuckets` (r:1 w:1) - /// Proof: `Actors::WakeupBuckets` (`max_values`: None, `max_size`: Some(58), added: 2533, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyHead` (r:1 w:0) + /// Proof: `Actors::ActorReadyHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyTail` (r:1 w:0) + /// Proof: `Actors::ActorReadyTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyOccupancy` (r:1 w:0) + /// Proof: `Actors::ActorReadyOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:2 w:0) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorWaitingOccupancies` (r:1 w:1) + /// Proof: `Actors::ActorWaitingOccupancies` (`max_values`: None, `max_size`: Some(29), added: 2504, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorWaitingTails` (r:1 w:1) + /// Proof: `Actors::ActorWaitingTails` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorWaitingHeads` (r:1 w:1) + /// Proof: `Actors::ActorWaitingHeads` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorWaitingFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorWaitingFrameChunks` (`max_values`: None, `max_size`: Some(11708), added: 14183, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorWaitingCursorIndices` (r:1 w:1) + /// Proof: `Actors::ActorWaitingCursorIndices` (`max_values`: None, `max_size`: Some(29), added: 2504, 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:1) - /// Proof: `Actors::WakeupPages` (`max_values`: None, `max_size`: Some(348), added: 2823, mode: `MaxEncodedLen`) /// The range of component `l` is `[2, 8]`. fn task_split_transfer(l: u32, ) -> Weight { // Proof Size summary in bytes: - // 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()))) + // Measured: `3868 + l * (1455 ±0)` + // Estimated: `29222 + l * (4746 ±0)` + // Minimum execution time: 674_329_000 picoseconds. + Weight::from_parts(163_392_331, 0) + .saturating_add(Weight::from_parts(0, 29222)) + // Standard Error: 1_748_382 + .saturating_add(Weight::from_parts(310_098_141, 0).saturating_mul(l.into())) + .saturating_add(T::DbWeight::get().reads(14)) + .saturating_add(T::DbWeight::get().reads((10_u64).saturating_mul(l.into()))) + .saturating_add(T::DbWeight::get().writes(7)) + .saturating_add(T::DbWeight::get().writes((4_u64).saturating_mul(l.into()))) .saturating_add(Weight::from_parts(0, 4746).saturating_mul(l.into())) } /// Storage: `AssetRegistry::ForeignAssetMapping` (r:1 w:0) @@ -1259,49 +1432,55 @@ impl pallet_deos_actors::WeightInfo for S /// 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::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, 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(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorUnsignaledControlCells` (r:1 w:1) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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: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(1491), added: 3966, 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) - /// 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::WakeupBuckets` (r:1 w:1) - /// Proof: `Actors::WakeupBuckets` (`max_values`: None, `max_size`: Some(58), added: 2533, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyHead` (r:1 w:0) + /// Proof: `Actors::ActorReadyHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyTail` (r:1 w:0) + /// Proof: `Actors::ActorReadyTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyOccupancy` (r:1 w:0) + /// Proof: `Actors::ActorReadyOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:2 w:0) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorWaitingOccupancies` (r:1 w:1) + /// Proof: `Actors::ActorWaitingOccupancies` (`max_values`: None, `max_size`: Some(29), added: 2504, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorWaitingTails` (r:1 w:1) + /// Proof: `Actors::ActorWaitingTails` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorWaitingHeads` (r:1 w:1) + /// Proof: `Actors::ActorWaitingHeads` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorWaitingFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorWaitingFrameChunks` (`max_values`: None, `max_size`: Some(11708), added: 14183, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorWaitingCursorIndices` (r:1 w:1) + /// Proof: `Actors::ActorWaitingCursorIndices` (`max_values`: None, `max_size`: Some(29), added: 2504, 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: `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: `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)) + // Measured: `5296` + // Estimated: `29222` + // Minimum execution time: 364_648_000 picoseconds. + Weight::from_parts(458_445_000, 0) + .saturating_add(Weight::from_parts(0, 29222)) + .saturating_add(T::DbWeight::get().reads(25)) + .saturating_add(T::DbWeight::get().writes(12)) } /// Storage: `System::Account` (r:1 w:1) /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) @@ -1329,8 +1508,8 @@ impl pallet_deos_actors::WeightInfo for S // Proof Size summary in bytes: // Measured: `938` // Estimated: `34255` - // Minimum execution time: 273_503_000 picoseconds. - Weight::from_parts(281_953_000, 0) + // Minimum execution time: 288_170_000 picoseconds. + Weight::from_parts(316_317_000, 0) .saturating_add(Weight::from_parts(0, 34255)) .saturating_add(T::DbWeight::get().reads(16)) .saturating_add(T::DbWeight::get().writes(15)) @@ -1347,8 +1526,8 @@ impl pallet_deos_actors::WeightInfo for S // Proof Size summary in bytes: // Measured: `1559` // Estimated: `14035` - // Minimum execution time: 160_009_000 picoseconds. - Weight::from_parts(164_968_000, 0) + // Minimum execution time: 173_628_000 picoseconds. + Weight::from_parts(181_242_000, 0) .saturating_add(Weight::from_parts(0, 14035)) .saturating_add(T::DbWeight::get().reads(9)) .saturating_add(T::DbWeight::get().writes(8)) @@ -1367,8 +1546,8 @@ impl pallet_deos_actors::WeightInfo for S // Proof Size summary in bytes: // Measured: `1541` // Estimated: `8817` - // Minimum execution time: 147_926_000 picoseconds. - Weight::from_parts(152_675_000, 0) + // Minimum execution time: 160_009_000 picoseconds. + Weight::from_parts(164_758_000, 0) .saturating_add(Weight::from_parts(0, 8817)) .saturating_add(T::DbWeight::get().reads(8)) .saturating_add(T::DbWeight::get().writes(6)) @@ -1385,8 +1564,8 @@ impl pallet_deos_actors::WeightInfo for S // Proof Size summary in bytes: // Measured: `1137` // Estimated: `8817` - // Minimum execution time: 83_532_000 picoseconds. - Weight::from_parts(85_976_000, 0) + // Minimum execution time: 89_468_000 picoseconds. + Weight::from_parts(91_005_000, 0) .saturating_add(Weight::from_parts(0, 8817)) .saturating_add(T::DbWeight::get().reads(7)) .saturating_add(T::DbWeight::get().writes(7)) @@ -1403,8 +1582,8 @@ impl pallet_deos_actors::WeightInfo for S // Proof Size summary in bytes: // Measured: `1409` // Estimated: `8817` - // Minimum execution time: 99_176_000 picoseconds. - Weight::from_parts(101_900_000, 0) + // Minimum execution time: 108_884_000 picoseconds. + Weight::from_parts(112_935_000, 0) .saturating_add(Weight::from_parts(0, 8817)) .saturating_add(T::DbWeight::get().reads(7)) .saturating_add(T::DbWeight::get().writes(7)) @@ -1425,30 +1604,30 @@ impl pallet_deos_actors::WeightInfo for S /// Proof: `Oracle::Observations` (`max_values`: None, `max_size`: Some(61), added: 2536, mode: `MaxEncodedLen`) /// Storage: `Actors::SovereignIndex` (r:1 w:0) /// Proof: `Actors::SovereignIndex` (`max_values`: None, `max_size`: Some(56), added: 2531, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, 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(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorUnsignaledControlCells` (r:1 w:1) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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: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(1491), added: 3966, 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) - /// 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`) + /// Storage: `Actors::ActorReadyHead` (r:1 w:0) + /// Proof: `Actors::ActorReadyHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyTail` (r:1 w:1) + /// Proof: `Actors::ActorReadyTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyOccupancy` (r:1 w:1) + /// Proof: `Actors::ActorReadyOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, mode: `MaxEncodedLen`) /// Storage: `Actors::ObservationSubscriberCount` (r:2 w:0) /// Proof: `Actors::ObservationSubscriberCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) /// Storage: `Actors::ObservationIngressRevisions` (r:2 w:0) @@ -1459,10 +1638,10 @@ impl pallet_deos_actors::WeightInfo for S /// 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: `4765` + // Measured: `4880` // Estimated: `19253` - // Minimum execution time: 513_132_000 picoseconds. - Weight::from_parts(536_949_000, 0) + // Minimum execution time: 635_147_000 picoseconds. + Weight::from_parts(718_678_000, 0) .saturating_add(Weight::from_parts(0, 19253)) .saturating_add(T::DbWeight::get().reads(41)) .saturating_add(T::DbWeight::get().writes(17)) @@ -1481,30 +1660,30 @@ impl pallet_deos_actors::WeightInfo for S /// Proof: `Oracle::Observations` (`max_values`: None, `max_size`: Some(61), added: 2536, mode: `MaxEncodedLen`) /// Storage: `Actors::SovereignIndex` (r:1 w:0) /// Proof: `Actors::SovereignIndex` (`max_values`: None, `max_size`: Some(56), added: 2531, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, 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(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorUnsignaledControlCells` (r:1 w:1) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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: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(1491), added: 3966, 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) - /// 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`) + /// Storage: `Actors::ActorReadyHead` (r:1 w:0) + /// Proof: `Actors::ActorReadyHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyTail` (r:1 w:1) + /// Proof: `Actors::ActorReadyTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyOccupancy` (r:1 w:1) + /// Proof: `Actors::ActorReadyOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, mode: `MaxEncodedLen`) /// Storage: `Actors::ObservationSubscriberCount` (r:2 w:0) /// Proof: `Actors::ObservationSubscriberCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) /// Storage: `Actors::ObservationIngressRevisions` (r:2 w:0) @@ -1515,26 +1694,26 @@ impl pallet_deos_actors::WeightInfo for S /// 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: `4546` + // Measured: `4661` // Estimated: `19253` - // Minimum execution time: 505_310_000 picoseconds. - Weight::from_parts(530_243_000, 0) + // Minimum execution time: 609_864_000 picoseconds. + Weight::from_parts(714_697_000, 0) .saturating_add(Weight::from_parts(0, 19253)) .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::ActorReadyTail` (r:1 w:0) + /// Proof: `Actors::ActorReadyTail` (`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` + // Measured: `508` // Estimated: `1560` - // Minimum execution time: 8_940_000 picoseconds. - Weight::from_parts(10_616_000, 0) + // Minimum execution time: 10_756_000 picoseconds. + Weight::from_parts(12_292_000, 0) .saturating_add(Weight::from_parts(0, 1560)) .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(2)) @@ -1543,22 +1722,22 @@ impl pallet_deos_actors::WeightInfo for S /// 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) - /// 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::ActorReadyHead` (r:1 w:0) + /// Proof: `Actors::ActorReadyHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyTail` (r:1 w:0) + /// Proof: `Actors::ActorReadyTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyOccupancy` (r:1 w:0) + /// Proof: `Actors::ActorReadyOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, 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::IdleStarvationState` (r:1 w:1) /// 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: `485` + // Measured: `845` // Estimated: `1560` - // Minimum execution time: 15_086_000 picoseconds. - Weight::from_parts(16_832_000, 0) + // Minimum execution time: 18_089_000 picoseconds. + Weight::from_parts(19_626_000, 0) .saturating_add(Weight::from_parts(0, 1560)) .saturating_add(T::DbWeight::get().reads(7)) .saturating_add(T::DbWeight::get().writes(2)) @@ -1583,170 +1762,172 @@ impl pallet_deos_actors::WeightInfo for S /// Proof: `Actors::MaterializationFamilyCursor` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`) fn materialization_coordinator_base() -> Weight { // Proof Size summary in bytes: - // Measured: `635` + // Measured: `634` // Estimated: `5982` - // Minimum execution time: 24_724_000 picoseconds. - Weight::from_parts(26_680_000, 0) + // Minimum execution time: 24_864_000 picoseconds. + Weight::from_parts(25_842_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::ActorControlLocators` (r:1 w:0) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `Measured`) + /// Storage: `Actors::ActorUnsignaledControlCells` (r:1 w:0) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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) + /// Storage: `Actors::ActorContractHead` (r:1 w:1) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `Measured`) + /// Storage: `Actors::ActorContractTailChunk` (r:3 w:3) /// Proof: `Actors::ActorContractTailChunk` (`max_values`: None, `max_size`: Some(4070), added: 6545, mode: `Measured`) - /// The range of component `c` is `[0, 8]`. + /// The range of component `c` is `[0, 3]`. 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)) + // Measured: `1030 + c * (11 ±0)` + // Estimated: `4499 + c * (2475 ±1)` + // Minimum execution time: 30_032_000 picoseconds. + Weight::from_parts(35_052_172, 0) + .saturating_add(Weight::from_parts(0, 4499)) + // Standard Error: 177_905 + .saturating_add(Weight::from_parts(6_036_087, 0).saturating_mul(c.into())) + .saturating_add(T::DbWeight::get().reads(4)) .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)) .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::ActorControlLocators` (r:1 w:0) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `Measured`) + /// Storage: `Actors::ActorUnsignaledControlCells` (r:1 w:0) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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) + /// Storage: `Actors::ActorContractTailChunk` (r:3 w:3) /// 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]`. + /// The range of component `c` is `[0, 3]`. 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)) + // Measured: `1266 + c * (218 ±0)` + // Estimated: `4740 + c * (2669 ±2)` + // Minimum execution time: 31_918_000 picoseconds. + Weight::from_parts(34_244_305, 0) + .saturating_add(Weight::from_parts(0, 4740)) + // Standard Error: 103_684 + .saturating_add(Weight::from_parts(8_784_131, 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(3)) + .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, 2670).saturating_mul(c.into())) + .saturating_add(Weight::from_parts(0, 2669).saturating_mul(c.into())) } + /// Storage: `Actors::ActorControlLocators` (r:1 w:0) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `Measured`) + /// Storage: `Actors::ActorUnsignaledControlCells` (r:1 w:0) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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) + /// Storage: `Actors::ActorContractTailChunk` (r:3 w:0) /// Proof: `Actors::ActorContractTailChunk` (`max_values`: None, `max_size`: Some(4070), added: 6545, mode: `Measured`) - /// The range of component `c` is `[0, 8]`. + /// The range of component `c` is `[0, 3]`. 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)) + // Measured: `1266 + c * (218 ±0)` + // Estimated: `4740 + c * (2669 ±2)` + // Minimum execution time: 28_915_000 picoseconds. + Weight::from_parts(31_311_645, 0) + .saturating_add(Weight::from_parts(0, 4740)) + // Standard Error: 64_178 + .saturating_add(Weight::from_parts(6_659_243, 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(Weight::from_parts(0, 2669).saturating_mul(c.into())) } + /// Storage: `Actors::ActorControlLocators` (r:1 w:0) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `Measured`) + /// Storage: `Actors::ActorUnsignaledControlCells` (r:1 w:0) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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`) 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)) + // Measured: `1266` + // Estimated: `4731` + // Minimum execution time: 28_147_000 picoseconds. + Weight::from_parts(30_451_000, 0) + .saturating_add(Weight::from_parts(0, 4731)) + .saturating_add(T::DbWeight::get().reads(3)) } + /// Storage: `Actors::ActorControlLocators` (r:1 w:0) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `Measured`) + /// Storage: `Actors::ActorUnsignaledControlCells` (r:1 w:0) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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: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)) + // Measured: `1448 + s * (14 ±0)` + // Estimated: `4913 + s * (14 ±0)` + // Minimum execution time: 32_407_000 picoseconds. + Weight::from_parts(39_333_653, 0) + .saturating_add(Weight::from_parts(0, 4913)) + .saturating_add(T::DbWeight::get().reads(4)) .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::ActorControlLocators` (r:1 w:0) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `Measured`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:1 w:0) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, 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)) + // Measured: `1503` + // Estimated: `4968` + // Minimum execution time: 75_919_000 picoseconds. + Weight::from_parts(81_297_000, 0) + .saturating_add(Weight::from_parts(0, 4968)) + .saturating_add(T::DbWeight::get().reads(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:0) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `Measured`) + /// Storage: `Actors::ActorControlLocators` (r:1 w:0) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `Measured`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:1 w:0) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, 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`) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(1491), added: 3966, 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)) + // Measured: `2242` + // Estimated: `5707` + // Minimum execution time: 94_218_000 picoseconds. + Weight::from_parts(123_132_000, 0) + .saturating_add(Weight::from_parts(0, 5707)) + .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::ActorControlLocators` (r:1 w:0) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `Measured`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:1 w:0) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, 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`) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(1491), added: 3966, 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) @@ -1754,60 +1935,186 @@ impl pallet_deos_actors::WeightInfo for S /// 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())) + // Measured: `2360 + s * (55 ±0)` + // Estimated: `5796 + s * (73 ±7)` + // Minimum execution time: 95_055_000 picoseconds. + Weight::from_parts(117_787_560, 0) + .saturating_add(Weight::from_parts(0, 5796)) + // Standard Error: 709_653 + .saturating_add(Weight::from_parts(1_082_421, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(7)) + .saturating_add(Weight::from_parts(0, 73).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) + fn opening_snapshot_traversal() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 489_000 picoseconds. + Weight::from_parts(1_397_000, 0) + .saturating_add(Weight::from_parts(0, 0)) + } + /// Storage: `Assets::Asset` (r:24 w:0) /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `Measured`) - /// Storage: `Assets::Account` (r:15 w:0) + /// Storage: `Assets::Account` (r:24 w:0) /// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `Measured`) - /// The range of component `e` is `[1, 16]`. + /// The range of component `e` is `[1, 24]`. 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)) + // Measured: `364 + e * (361 ±0)` + // Estimated: `1366 + e * (2836 ±0)` + // Minimum execution time: 18_928_000 picoseconds. + Weight::from_parts(6_280_406, 0) + .saturating_add(Weight::from_parts(0, 1366)) + // Standard Error: 46_886 + .saturating_add(Weight::from_parts(10_546_366, 0).saturating_mul(e.into())) .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(e.into()))) - .saturating_add(Weight::from_parts(0, 2260).saturating_mul(e.into())) + .saturating_add(Weight::from_parts(0, 2836).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) + /// Storage: `Assets::Asset` (r:12 w:0) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `Measured`) + /// Storage: `Assets::Account` (r:12 w:0) + /// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `Measured`) + /// The range of component `e` is `[1, 12]`. + fn opening_target_snapshot_capture(e: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `367 + e * (361 ±0)` + // Estimated: `1377 + e * (2834 ±0)` + // Minimum execution time: 16_623_000 picoseconds. + Weight::from_parts(11_715_105, 0) + .saturating_add(Weight::from_parts(0, 1377)) + // Standard Error: 62_591 + .saturating_add(Weight::from_parts(8_545_944, 0).saturating_mul(e.into())) + .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(e.into()))) + .saturating_add(Weight::from_parts(0, 2834).saturating_mul(e.into())) + } + /// Storage: `Assets::Asset` (r:23 w:0) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `Measured`) + /// Storage: `Assets::Account` (r:23 w:0) + /// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `Measured`) + /// The range of component `e` is `[1, 23]`. + fn opening_share_mixed_capture(e: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `334 + e * (361 ±0)` + // Estimated: `1308 + e * (2838 ±0)` + // Minimum execution time: 14_667_000 picoseconds. + Weight::from_parts(6_913_373, 0) + .saturating_add(Weight::from_parts(0, 1308)) + // Standard Error: 64_707 + .saturating_add(Weight::from_parts(10_801_886, 0).saturating_mul(e.into())) + .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(e.into()))) + .saturating_add(Weight::from_parts(0, 2838).saturating_mul(e.into())) + } + fn opening_predicate_traversal() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 1_396_000 picoseconds. + Weight::from_parts(1_466_000, 0) + .saturating_add(Weight::from_parts(0, 0)) + } + /// Storage: `Assets::Asset` (r:47 w:0) /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `Measured`) - /// Storage: `Assets::Account` (r:31 w:0) + /// Storage: `Assets::Account` (r:47 w:0) /// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `Measured`) - /// The range of component `p` is `[1, 32]`. + /// Storage: `Oracle::Feeds` (r:1 w:0) + /// Proof: `Oracle::Feeds` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `Measured`) + /// Storage: `Oracle::Observations` (r:1 w:0) + /// Proof: `Oracle::Observations` (`max_values`: None, `max_size`: Some(61), added: 2536, mode: `Measured`) + /// The range of component `p` is `[0, 48]`. 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)) + // Measured: `380 + p * (361 ±0)` + // Estimated: `4184 + p * (2673 ±13)` + // Minimum execution time: 978_000 picoseconds. + Weight::from_parts(5_665_087, 0) + .saturating_add(Weight::from_parts(0, 4184)) + // Standard Error: 69_174 + .saturating_add(Weight::from_parts(9_642_493, 0).saturating_mul(p.into())) .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(p.into()))) - .saturating_add(Weight::from_parts(0, 2524).saturating_mul(p.into())) + .saturating_add(Weight::from_parts(0, 2673).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(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Assets::Asset` (r:4 w:0) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `Measured`) + /// Storage: `Assets::Account` (r:4 w:0) + /// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `Measured`) + /// The range of component `p` is `[1, 4]`. + fn predicate_asset_evaluation(p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `356 + p * (363 ±0)` + // Estimated: `1344 + p * (2838 ±0)` + // Minimum execution time: 15_575_000 picoseconds. + Weight::from_parts(8_967_155, 0) + .saturating_add(Weight::from_parts(0, 1344)) + // Standard Error: 52_072 + .saturating_add(Weight::from_parts(8_774_724, 0).saturating_mul(p.into())) + .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(p.into()))) + .saturating_add(Weight::from_parts(0, 2838).saturating_mul(p.into())) + } + /// Storage: `Assets::Asset` (r:1 w:0) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `Measured`) + /// Storage: `Assets::Account` (r:1 w:0) + /// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `Measured`) + /// Storage: `Oracle::Feeds` (r:3 w:0) + /// Proof: `Oracle::Feeds` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `Measured`) + /// Storage: `Oracle::Observations` (r:3 w:0) + /// Proof: `Oracle::Observations` (`max_values`: None, `max_size`: Some(61), added: 2536, mode: `Measured`) + /// The range of component `o` is `[1, 3]`. + fn predicate_observation_heavy_evaluation(o: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `898 + o * (199 ±0)` + // Estimated: `4364 + o * (2673 ±0)` + // Minimum execution time: 28_077_000 picoseconds. + Weight::from_parts(20_339_756, 0) + .saturating_add(Weight::from_parts(0, 4364)) + // Standard Error: 111_517 + .saturating_add(Weight::from_parts(10_257_124, 0).saturating_mul(o.into())) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(o.into()))) + .saturating_add(Weight::from_parts(0, 2673).saturating_mul(o.into())) + } + /// Storage: `Assets::Asset` (r:48 w:0) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `Measured`) + /// Storage: `Assets::Account` (r:48 w:0) + /// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `Measured`) + /// The range of component `p` is `[0, 48]`. + fn opening_max_encoded_balance_capture(p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `366 + p * (361 ±0)` + // Estimated: `1358 + p * (2836 ±0)` + // Minimum execution time: 908_000 picoseconds. + Weight::from_parts(8_117_762, 0) + .saturating_add(Weight::from_parts(0, 1358)) + // Standard Error: 38_133 + .saturating_add(Weight::from_parts(8_776_187, 0).saturating_mul(p.into())) + .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(p.into()))) + .saturating_add(Weight::from_parts(0, 2836).saturating_mul(p.into())) + } + /// Storage: `Assets::Asset` (r:1 w:0) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `Measured`) + /// Storage: `Assets::Account` (r:1 w:0) + /// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `Measured`) + /// Storage: `Oracle::Feeds` (r:47 w:0) + /// Proof: `Oracle::Feeds` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `Measured`) + /// Storage: `Oracle::Observations` (r:47 w:0) + /// Proof: `Oracle::Observations` (`max_values`: None, `max_size`: Some(61), added: 2536, mode: `Measured`) + /// The range of component `o` is `[1, 47]`. + fn opening_observation_heavy_capture(o: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `860 + o * (192 ±0)` + // Estimated: `4329 + o * (2667 ±0)` + // Minimum execution time: 29_124_000 picoseconds. + Weight::from_parts(12_070_595, 0) + .saturating_add(Weight::from_parts(0, 4329)) + // Standard Error: 84_804 + .saturating_add(Weight::from_parts(10_272_731, 0).saturating_mul(o.into())) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(o.into()))) + .saturating_add(Weight::from_parts(0, 2667).saturating_mul(o.into())) + } + /// Storage: `Actors::ActorControlLocators` (r:1 w:0) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:1 w:0) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, 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) @@ -1815,403 +2122,340 @@ impl pallet_deos_actors::WeightInfo for S /// 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`) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(1491), added: 3966, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorContractTailChunk` (r:1 w:0) + /// Proof: `Actors::ActorContractTailChunk` (`max_values`: None, `max_size`: Some(4070), added: 6545, mode: `MaxEncodedLen`) fn scheduler_actor_state_probe() -> Weight { // Proof Size summary in bytes: - // 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)) + // Measured: `2772` + // Estimated: `15106` + // Minimum execution time: 72_077_000 picoseconds. + Weight::from_parts(114_890_000, 0) + .saturating_add(Weight::from_parts(0, 15106)) .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(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`) - fn cycle_orchestration() -> Weight { - // Proof Size summary in bytes: - // 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(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`) - /// The range of component `n` is `[1, 8]`. - fn step_orchestration(n: u32, ) -> Weight { - // Proof Size summary in bytes: - // 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(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::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::ActorReadyHead` (r:1 w:0) + /// Proof: `Actors::ActorReadyHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::ActorReadyTail` (r:1 w:1) + /// Proof: `Actors::ActorReadyTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::ActorReadyOccupancy` (r:1 w:1) + /// Proof: `Actors::ActorReadyOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `Measured`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, mode: `Measured`) + /// Storage: `Actors::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `Measured`) fn scheduler_paged_append_existing_page() -> Weight { // Proof Size summary in bytes: - // 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)) + // Measured: `10195` + // Estimated: `13660` + // Minimum execution time: 91_004_000 picoseconds. + Weight::from_parts(124_040_000, 0) + .saturating_add(Weight::from_parts(0, 13660)) + .saturating_add(T::DbWeight::get().reads(5)) + .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(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::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:2 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::ActorReadyHead` (r:1 w:0) + /// Proof: `Actors::ActorReadyHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::ActorReadyTail` (r:1 w:1) + /// Proof: `Actors::ActorReadyTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::ActorReadyOccupancy` (r:1 w:1) + /// Proof: `Actors::ActorReadyOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `Measured`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:2 w:1) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, mode: `Measured`) + /// Storage: `Actors::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `Measured`) fn scheduler_paged_append_new_page() -> Weight { // Proof Size summary in bytes: - // 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)) + // Measured: `10506` + // Estimated: `16446` + // Minimum execution time: 65_443_000 picoseconds. + Weight::from_parts(109_653_000, 0) + .saturating_add(Weight::from_parts(0, 16446)) + .saturating_add(T::DbWeight::get().reads(6)) + .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(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::WakeupCursorPages` (r:1 w:0) - /// Proof: `Actors::WakeupCursorPages` (`max_values`: None, `max_size`: Some(314), added: 2789, mode: `Measured`) - /// Storage: `Actors::WakeupPages` (r:1 w:1) - /// Proof: `Actors::WakeupPages` (`max_values`: None, `max_size`: Some(348), added: 2823, mode: `Measured`) + /// Storage: `Actors::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `Measured`) + /// Storage: `Actors::ActorUnsignaledControlCells` (r:1 w:1) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, mode: `Measured`) + /// Storage: `Actors::ActorWaitingOccupancies` (r:1 w:1) + /// Proof: `Actors::ActorWaitingOccupancies` (`max_values`: None, `max_size`: Some(29), added: 2504, mode: `Measured`) + /// Storage: `Actors::ActorWaitingTails` (r:1 w:1) + /// Proof: `Actors::ActorWaitingTails` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `Measured`) + /// Storage: `Actors::ActorWaitingHeads` (r:1 w:0) + /// Proof: `Actors::ActorWaitingHeads` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `Measured`) + /// Storage: `Actors::ActorWaitingCursorIndices` (r:1 w:0) + /// Proof: `Actors::ActorWaitingCursorIndices` (`max_values`: None, `max_size`: Some(29), added: 2504, mode: `Measured`) + /// Storage: `Actors::ActorWaitingFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorWaitingFrameChunks` (`max_values`: None, `max_size`: Some(11708), added: 14183, mode: `Measured`) fn scheduler_wakeup_append_existing_page() -> Weight { // Proof Size summary in bytes: - // 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)) + // Measured: `10901` + // Estimated: `14366` + // Minimum execution time: 125_996_000 picoseconds. + Weight::from_parts(162_035_000, 0) + .saturating_add(Weight::from_parts(0, 14366)) + .saturating_add(T::DbWeight::get().reads(7)) + .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(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::WakeupCursorPages` (r:1 w:0) - /// Proof: `Actors::WakeupCursorPages` (`max_values`: None, `max_size`: Some(314), added: 2789, 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::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `Measured`) + /// Storage: `Actors::ActorUnsignaledControlCells` (r:1 w:1) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, mode: `Measured`) + /// Storage: `Actors::ActorWaitingOccupancies` (r:1 w:1) + /// Proof: `Actors::ActorWaitingOccupancies` (`max_values`: None, `max_size`: Some(29), added: 2504, mode: `Measured`) + /// Storage: `Actors::ActorWaitingTails` (r:1 w:1) + /// Proof: `Actors::ActorWaitingTails` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `Measured`) + /// Storage: `Actors::ActorWaitingHeads` (r:1 w:0) + /// Proof: `Actors::ActorWaitingHeads` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `Measured`) + /// Storage: `Actors::ActorWaitingCursorIndices` (r:1 w:0) + /// Proof: `Actors::ActorWaitingCursorIndices` (`max_values`: None, `max_size`: Some(29), added: 2504, mode: `Measured`) + /// Storage: `Actors::ActorWaitingFrameChunks` (r:2 w:2) + /// Proof: `Actors::ActorWaitingFrameChunks` (`max_values`: None, `max_size`: Some(11708), added: 14183, mode: `Measured`) fn scheduler_wakeup_append_new_page() -> Weight { // Proof Size summary in bytes: - // 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)) + // Measured: `11229` + // Estimated: `17169` + // Minimum execution time: 108_185_000 picoseconds. + Weight::from_parts(138_637_000, 0) + .saturating_add(Weight::from_parts(0, 17169)) + .saturating_add(T::DbWeight::get().reads(8)) + .saturating_add(T::DbWeight::get().writes(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:1) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `Measured`) + /// Storage: `Actors::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `Measured`) + /// Storage: `Actors::ActorWaitingFrameChunks` (r:2 w:2) + /// Proof: `Actors::ActorWaitingFrameChunks` (`max_values`: None, `max_size`: Some(11708), added: 14183, 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::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) - /// Proof: `Actors::WakeupBuckets` (`max_values`: None, `max_size`: Some(58), added: 2533, mode: `Measured`) + /// Storage: `Actors::ActorWaitingOccupancies` (r:2 w:2) + /// Proof: `Actors::ActorWaitingOccupancies` (`max_values`: None, `max_size`: Some(29), added: 2504, mode: `Measured`) + /// Storage: `Actors::ActorWaitingHeads` (r:2 w:2) + /// Proof: `Actors::ActorWaitingHeads` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `Measured`) + /// Storage: `Actors::ActorWaitingTails` (r:2 w:2) + /// Proof: `Actors::ActorWaitingTails` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `Measured`) + /// Storage: `Actors::ActorWaitingCursorIndices` (r:2 w:2) + /// Proof: `Actors::ActorWaitingCursorIndices` (`max_values`: None, `max_size`: Some(29), added: 2504, 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::WakeupCursorLen` (r:1 w:1) /// Proof: `Actors::WakeupCursorLen` (`max_values`: None, `max_size`: Some(21), added: 2496, mode: `Measured`) + /// Storage: `Actors::ActorUnsignaledControlCells` (r:1 w:0) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, mode: `Measured`) fn scheduler_wakeup_replace_exact() -> Weight { // Proof Size summary in bytes: - // 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)) + // Measured: `1880` + // Estimated: `7820` + // Minimum execution time: 182_568_000 picoseconds. + Weight::from_parts(202_054_000, 0) + .saturating_add(Weight::from_parts(0, 7820)) + .saturating_add(T::DbWeight::get().reads(17)) + .saturating_add(T::DbWeight::get().writes(13)) } - /// 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::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `Measured`) + /// Storage: `Actors::ActorWaitingFrameChunks` (r:3 w:3) + /// Proof: `Actors::ActorWaitingFrameChunks` (`max_values`: None, `max_size`: Some(11708), added: 14183, 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::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) - /// Proof: `Actors::WakeupBuckets` (`max_values`: None, `max_size`: Some(58), added: 2533, mode: `Measured`) - /// Storage: `Actors::WakeupCursorPages` (r:1 w:0) - /// Proof: `Actors::WakeupCursorPages` (`max_values`: None, `max_size`: Some(314), added: 2789, mode: `Measured`) + /// Storage: `Actors::ActorWaitingOccupancies` (r:1 w:1) + /// Proof: `Actors::ActorWaitingOccupancies` (`max_values`: None, `max_size`: Some(29), added: 2504, mode: `Measured`) + /// Storage: `Actors::ActorWaitingHeads` (r:1 w:0) + /// Proof: `Actors::ActorWaitingHeads` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `Measured`) + /// Storage: `Actors::ActorWaitingTails` (r:1 w:0) + /// Proof: `Actors::ActorWaitingTails` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `Measured`) + /// Storage: `Actors::ActorWaitingCursorIndices` (r:1 w:0) + /// Proof: `Actors::ActorWaitingCursorIndices` (`max_values`: None, `max_size`: Some(29), added: 2504, mode: `Measured`) fn scheduler_wakeup_invalidate_middle_page() -> Weight { // Proof Size summary in bytes: - // 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)) + // Measured: `13266` + // Estimated: `21681` + // Minimum execution time: 163_850_000 picoseconds. + Weight::from_parts(201_705_000, 0) + .saturating_add(Weight::from_parts(0, 21681)) .saturating_add(T::DbWeight::get().reads(11)) .saturating_add(T::DbWeight::get().writes(5)) } - /// Storage: `Actors::WakeupBuckets` (r:1 w:1) - /// Proof: `Actors::WakeupBuckets` (`max_values`: None, `max_size`: Some(58), added: 2533, mode: `Measured`) + /// Storage: `Actors::ActorWaitingOccupancies` (r:1 w:1) + /// Proof: `Actors::ActorWaitingOccupancies` (`max_values`: None, `max_size`: Some(29), added: 2504, mode: `Measured`) + /// Storage: `Actors::ActorWaitingCursorIndices` (r:1 w:0) + /// Proof: `Actors::ActorWaitingCursorIndices` (`max_values`: None, `max_size`: Some(29), added: 2504, mode: `Measured`) /// Storage: `Actors::WakeupCursorPages` (r:1 w:0) /// Proof: `Actors::WakeupCursorPages` (`max_values`: None, `max_size`: Some(314), added: 2789, mode: `Measured`) - /// Storage: `Actors::WakeupPages` (r:1 w:1) - /// Proof: `Actors::WakeupPages` (`max_values`: None, `max_size`: Some(348), added: 2823, mode: `Measured`) - /// 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(108), added: 2583, mode: `Measured`) + /// Storage: `Actors::ActorWaitingHeads` (r:1 w:1) + /// Proof: `Actors::ActorWaitingHeads` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `Measured`) + /// Storage: `Actors::ActorWaitingFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorWaitingFrameChunks` (`max_values`: None, `max_size`: Some(11708), added: 14183, mode: `Measured`) + /// Storage: `Actors::ActorControlLocators` (r:16 w:16) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, 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(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`) + /// Storage: `Actors::ActorWaitingTails` (r:1 w:0) + /// Proof: `Actors::ActorWaitingTails` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `Measured`) fn scheduler_wakeup_drain_partial_page() -> Weight { // Proof Size summary in bytes: - // 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) - /// Proof: `Actors::WakeupBuckets` (`max_values`: None, `max_size`: Some(58), added: 2533, mode: `Measured`) + // Measured: `15949` + // Estimated: `56539` + // Minimum execution time: 1_926_534_000 picoseconds. + Weight::from_parts(2_162_182_000, 0) + .saturating_add(Weight::from_parts(0, 56539)) + .saturating_add(T::DbWeight::get().reads(70)) + .saturating_add(T::DbWeight::get().writes(19)) + } + /// Storage: `Actors::ActorWaitingOccupancies` (r:1 w:1) + /// Proof: `Actors::ActorWaitingOccupancies` (`max_values`: None, `max_size`: Some(29), added: 2504, mode: `Measured`) + /// Storage: `Actors::ActorWaitingCursorIndices` (r:1 w:1) + /// Proof: `Actors::ActorWaitingCursorIndices` (`max_values`: None, `max_size`: Some(29), added: 2504, 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:1 w:1) - /// Proof: `Actors::WakeupPages` (`max_values`: None, `max_size`: Some(348), added: 2823, mode: `Measured`) - /// 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(108), added: 2583, mode: `Measured`) + /// Storage: `Actors::ActorWaitingHeads` (r:1 w:1) + /// Proof: `Actors::ActorWaitingHeads` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `Measured`) + /// Storage: `Actors::ActorWaitingFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorWaitingFrameChunks` (`max_values`: None, `max_size`: Some(11708), added: 14183, mode: `Measured`) + /// Storage: `Actors::ActorControlLocators` (r:32 w:32) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, 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(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::ActorWaitingTails` (r:1 w:1) + /// Proof: `Actors::ActorWaitingTails` (`max_values`: None, `max_size`: Some(33), added: 2508, 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: `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) - /// Proof: `Actors::WakeupBuckets` (`max_values`: None, `max_size`: Some(58), added: 2533, mode: `Measured`) + // Measured: `19082` + // Estimated: `99272` + // Minimum execution time: 3_325_407_000 picoseconds. + Weight::from_parts(3_699_832_000, 0) + .saturating_add(Weight::from_parts(0, 99272)) + .saturating_add(T::DbWeight::get().reads(135)) + .saturating_add(T::DbWeight::get().writes(39)) + } + /// Storage: `Actors::ActorWaitingOccupancies` (r:1 w:1) + /// Proof: `Actors::ActorWaitingOccupancies` (`max_values`: None, `max_size`: Some(29), added: 2504, mode: `Measured`) + /// Storage: `Actors::ActorWaitingCursorIndices` (r:1 w:1) + /// Proof: `Actors::ActorWaitingCursorIndices` (`max_values`: None, `max_size`: Some(29), added: 2504, 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:2 w:2) - /// Proof: `Actors::WakeupPages` (`max_values`: None, `max_size`: Some(348), added: 2823, mode: `Measured`) - /// 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(108), added: 2583, mode: `Measured`) + /// Storage: `Actors::ActorWaitingHeads` (r:1 w:1) + /// Proof: `Actors::ActorWaitingHeads` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `Measured`) + /// Storage: `Actors::ActorWaitingFrameChunks` (r:2 w:2) + /// Proof: `Actors::ActorWaitingFrameChunks` (`max_values`: None, `max_size`: Some(11708), added: 14183, mode: `Measured`) + /// Storage: `Actors::ActorControlLocators` (r:33 w:33) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, 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(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::ActorWaitingTails` (r:1 w:1) + /// Proof: `Actors::ActorWaitingTails` (`max_values`: None, `max_size`: Some(33), added: 2508, 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: `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) - /// Proof: `Actors::WakeupBuckets` (`max_values`: None, `max_size`: Some(58), added: 2533, mode: `Measured`) + // Measured: `19729` + // Estimated: `102394` + // Minimum execution time: 3_486_045_000 picoseconds. + Weight::from_parts(3_698_296_000, 0) + .saturating_add(Weight::from_parts(0, 102394)) + .saturating_add(T::DbWeight::get().reads(140)) + .saturating_add(T::DbWeight::get().writes(41)) + } + /// Storage: `Actors::ActorWaitingOccupancies` (r:1 w:1) + /// Proof: `Actors::ActorWaitingOccupancies` (`max_values`: None, `max_size`: Some(29), added: 2504, mode: `Measured`) + /// Storage: `Actors::ActorWaitingCursorIndices` (r:1 w:1) + /// Proof: `Actors::ActorWaitingCursorIndices` (`max_values`: None, `max_size`: Some(29), added: 2504, 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:1 w:1) - /// Proof: `Actors::WakeupPages` (`max_values`: None, `max_size`: Some(348), added: 2823, mode: `Measured`) - /// 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(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(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::ActorWaitingHeads` (r:1 w:1) + /// Proof: `Actors::ActorWaitingHeads` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `Measured`) + /// Storage: `Actors::ActorWaitingFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorWaitingFrameChunks` (`max_values`: None, `max_size`: Some(11708), added: 14183, mode: `Measured`) + /// Storage: `Actors::ActorControlLocators` (r:32 w:0) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `Measured`) + /// Storage: `Actors::ActorWaitingTails` (r:1 w:1) + /// Proof: `Actors::ActorWaitingTails` (`max_values`: None, `max_size`: Some(33), added: 2508, 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: `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)) + // Measured: `2410` + // Estimated: `82600` + // Minimum execution time: 1_308_428_000 picoseconds. + Weight::from_parts(1_357_457_000, 0) + .saturating_add(Weight::from_parts(0, 82600)) + .saturating_add(T::DbWeight::get().reads(39)) + .saturating_add(T::DbWeight::get().writes(7)) } - /// Storage: `Actors::WakeupBuckets` (r:14 w:14) - /// Proof: `Actors::WakeupBuckets` (`max_values`: None, `max_size`: Some(58), added: 2533, mode: `Measured`) + /// Storage: `Actors::ActorWaitingOccupancies` (r:14 w:0) + /// Proof: `Actors::ActorWaitingOccupancies` (`max_values`: None, `max_size`: Some(29), added: 2504, mode: `Measured`) + /// Storage: `Actors::ActorWaitingCursorIndices` (r:14 w:14) + /// Proof: `Actors::ActorWaitingCursorIndices` (`max_values`: None, `max_size`: Some(29), added: 2504, 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:10 w:10) /// 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: `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)) + // Measured: `6176` + // Estimated: `41816` + // Minimum execution time: 423_524_000 picoseconds. + Weight::from_parts(442_243_000, 0) + .saturating_add(Weight::from_parts(0, 41816)) + .saturating_add(T::DbWeight::get().reads(39)) .saturating_add(T::DbWeight::get().writes(25)) } /// Storage: `Actors::WakeupCursorPages` (r:18 w:10) /// Proof: `Actors::WakeupCursorPages` (`max_values`: None, `max_size`: Some(314), added: 2789, mode: `Measured`) - /// Storage: `Actors::WakeupBuckets` (r:15 w:15) - /// Proof: `Actors::WakeupBuckets` (`max_values`: None, `max_size`: Some(58), added: 2533, mode: `Measured`) + /// Storage: `Actors::ActorWaitingOccupancies` (r:15 w:0) + /// Proof: `Actors::ActorWaitingOccupancies` (`max_values`: None, `max_size`: Some(29), added: 2504, mode: `Measured`) + /// Storage: `Actors::ActorWaitingCursorIndices` (r:15 w:15) + /// Proof: `Actors::ActorWaitingCursorIndices` (`max_values`: None, `max_size`: Some(29), added: 2504, 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_cursor_pop_min() -> Weight { // Proof Size summary in bytes: - // 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)) + // Measured: `8818` + // Estimated: `54358` + // Minimum execution time: 514_739_000 picoseconds. + Weight::from_parts(571_450_000, 0) + .saturating_add(Weight::from_parts(0, 54358)) + .saturating_add(T::DbWeight::get().reads(49)) .saturating_add(T::DbWeight::get().writes(26)) } - /// Storage: `Actors::WakeupBuckets` (r:14 w:14) - /// Proof: `Actors::WakeupBuckets` (`max_values`: None, `max_size`: Some(58), added: 2533, mode: `Measured`) + /// Storage: `Actors::ActorWaitingOccupancies` (r:14 w:0) + /// Proof: `Actors::ActorWaitingOccupancies` (`max_values`: None, `max_size`: Some(29), added: 2504, mode: `Measured`) + /// Storage: `Actors::ActorWaitingCursorIndices` (r:14 w:14) + /// Proof: `Actors::ActorWaitingCursorIndices` (`max_values`: None, `max_size`: Some(29), added: 2504, 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:18 w:10) /// 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: `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)) + // Measured: `8347` + // Estimated: `53887` + // Minimum execution time: 505_869_000 picoseconds. + Weight::from_parts(539_812_000, 0) + .saturating_add(Weight::from_parts(0, 53887)) + .saturating_add(T::DbWeight::get().reads(47)) .saturating_add(T::DbWeight::get().writes(25)) } /// Storage: `Timestamp::Now` (r:1 w:0) @@ -2224,48 +2468,48 @@ impl pallet_deos_actors::WeightInfo for S /// 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::QueueOccupancy` (r:1 w:0) - /// Proof: `Actors::QueueOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `Measured`) - /// Storage: `Actors::WakeupBuckets` (r:2 w:2) - /// Proof: `Actors::WakeupBuckets` (`max_values`: None, `max_size`: Some(58), added: 2533, 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::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::ActorReadyOccupancy` (r:1 w:0) + /// Proof: `Actors::ActorReadyOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `Measured`) + /// Storage: `Actors::ActorWaitingOccupancies` (r:2 w:2) + /// Proof: `Actors::ActorWaitingOccupancies` (`max_values`: None, `max_size`: Some(29), added: 2504, mode: `Measured`) + /// Storage: `Actors::ActorWaitingHeads` (r:2 w:2) + /// Proof: `Actors::ActorWaitingHeads` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `Measured`) + /// Storage: `Actors::ActorWaitingCursorIndices` (r:2 w:1) + /// Proof: `Actors::ActorWaitingCursorIndices` (`max_values`: None, `max_size`: Some(29), added: 2504, mode: `Measured`) + /// Storage: `Actors::ActorWaitingFrameChunks` (r:2 w:2) + /// Proof: `Actors::ActorWaitingFrameChunks` (`max_values`: None, `max_size`: Some(11708), added: 14183, mode: `Measured`) + /// Storage: `Actors::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, 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::ActorWaitingTails` (r:2 w:1) + /// Proof: `Actors::ActorWaitingTails` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `Measured`) + /// Storage: `Actors::ActorUnsignaledControlCells` (r:0 w:1) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, mode: `Measured`) fn scheduler_wakeup_cursor_worker_partial() -> Weight { // Proof Size summary in bytes: - // 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)) + // Measured: `2019` + // Estimated: `7959` + // Minimum execution time: 199_679_000 picoseconds. + Weight::from_parts(235_997_000, 0) + .saturating_add(Weight::from_parts(0, 7959)) + .saturating_add(T::DbWeight::get().reads(21)) + .saturating_add(T::DbWeight::get().writes(13)) } - /// 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::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `Measured`) + /// Storage: `Actors::ActorUnsignaledControlCells` (r:1 w:1) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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::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) @@ -2276,50 +2520,58 @@ impl pallet_deos_actors::WeightInfo for S /// 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::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::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::ActorReadyHead` (r:1 w:0) + /// Proof: `Actors::ActorReadyHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::ActorReadyTail` (r:1 w:1) + /// Proof: `Actors::ActorReadyTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::ActorReadyOccupancy` (r:1 w:1) + /// Proof: `Actors::ActorReadyOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `Measured`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, 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::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(1491), added: 3966, 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)) + // Measured: `2511` + // Estimated: `8451` + // Minimum execution time: 304_094_000 picoseconds. + Weight::from_parts(349_561_000, 0) + .saturating_add(Weight::from_parts(0, 8451)) .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::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `Measured`) + /// Storage: `Actors::ActorUnsignaledControlCells` (r:1 w:1) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, mode: `Measured`) + /// Storage: `Actors::ActorWaitingOccupancies` (r:1 w:1) + /// Proof: `Actors::ActorWaitingOccupancies` (`max_values`: None, `max_size`: Some(29), added: 2504, mode: `Measured`) + /// Storage: `Actors::ActorWaitingTails` (r:1 w:1) + /// Proof: `Actors::ActorWaitingTails` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `Measured`) + /// Storage: `Actors::ActorWaitingHeads` (r:1 w:1) + /// Proof: `Actors::ActorWaitingHeads` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `Measured`) + /// Storage: `Actors::ActorWaitingFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorWaitingFrameChunks` (`max_values`: None, `max_size`: Some(11708), added: 14183, mode: `Measured`) + /// Storage: `Actors::ActorWaitingCursorIndices` (r:1 w:1) + /// Proof: `Actors::ActorWaitingCursorIndices` (`max_values`: None, `max_size`: Some(29), added: 2504, 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) + /// Storage: `Actors::ActorStateHolds` (r:1 w:1) /// Proof: `Actors::ActorStateHolds` (`max_values`: None, `max_size`: Some(152), added: 2627, 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::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) @@ -2328,31 +2580,35 @@ impl pallet_deos_actors::WeightInfo for S /// 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::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::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::ActorReadyHead` (r:1 w:0) + /// Proof: `Actors::ActorReadyHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::ActorReadyTail` (r:1 w:1) + /// Proof: `Actors::ActorReadyTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::ActorReadyOccupancy` (r:1 w:1) + /// Proof: `Actors::ActorReadyOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `Measured`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, 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`) + /// 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(1491), added: 3966, mode: `Measured`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(121), added: 2596, 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)) + // Measured: `2510` + // Estimated: `8450` + // Minimum execution time: 474_090_000 picoseconds. + Weight::from_parts(536_041_000, 0) + .saturating_add(Weight::from_parts(0, 8450)) + .saturating_add(T::DbWeight::get().reads(27)) + .saturating_add(T::DbWeight::get().writes(16)) } /// Storage: `Timestamp::Now` (r:1 w:0) /// Proof: `Timestamp::Now` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) @@ -2364,41 +2620,43 @@ impl pallet_deos_actors::WeightInfo for S /// Proof: `Actors::WakeupCursorLen` (`max_values`: None, `max_size`: Some(21), added: 2496, mode: `Measured`) /// Storage: `Actors::WakeupCursorPages` (r:18 w:10) /// Proof: `Actors::WakeupCursorPages` (`max_values`: None, `max_size`: Some(314), added: 2789, 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::WakeupBuckets` (r:15 w:15) - /// Proof: `Actors::WakeupBuckets` (`max_values`: None, `max_size`: Some(58), added: 2533, mode: `Measured`) - /// Storage: `Actors::WakeupPages` (r:1 w:1) - /// Proof: `Actors::WakeupPages` (`max_values`: None, `max_size`: Some(348), added: 2823, 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::ActorReadyOccupancy` (r:1 w:1) + /// Proof: `Actors::ActorReadyOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `Measured`) + /// Storage: `Actors::ActorWaitingOccupancies` (r:15 w:1) + /// Proof: `Actors::ActorWaitingOccupancies` (`max_values`: None, `max_size`: Some(29), added: 2504, mode: `Measured`) + /// Storage: `Actors::ActorWaitingHeads` (r:1 w:1) + /// Proof: `Actors::ActorWaitingHeads` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `Measured`) + /// Storage: `Actors::ActorWaitingCursorIndices` (r:15 w:15) + /// Proof: `Actors::ActorWaitingCursorIndices` (`max_values`: None, `max_size`: Some(29), added: 2504, mode: `Measured`) + /// Storage: `Actors::ActorWaitingFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorWaitingFrameChunks` (`max_values`: None, `max_size`: Some(11708), added: 14183, mode: `Measured`) + /// Storage: `Actors::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, 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::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(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::ActorWaitingTails` (r:1 w:1) + /// Proof: `Actors::ActorWaitingTails` (`max_values`: None, `max_size`: Some(33), added: 2508, 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::ActorReadyHead` (r:1 w:0) + /// Proof: `Actors::ActorReadyHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::ActorReadyTail` (r:1 w:1) + /// Proof: `Actors::ActorReadyTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, mode: `Measured`) fn scheduler_wakeup_cursor_worker_remove() -> Weight { // Proof Size summary in bytes: - // 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)) + // Measured: `10317` + // Estimated: `55857` + // Minimum execution time: 728_317_000 picoseconds. + Weight::from_parts(806_400_000, 0) + .saturating_add(Weight::from_parts(0, 55857)) + .saturating_add(T::DbWeight::get().reads(65)) + .saturating_add(T::DbWeight::get().writes(35)) } /// Storage: `Timestamp::Now` (r:1 w:0) /// Proof: `Timestamp::Now` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) @@ -2412,192 +2670,186 @@ 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: `668` - // Estimated: `6608` - // Minimum execution time: 23_397_000 picoseconds. - Weight::from_parts(25_283_000, 0) - .saturating_add(Weight::from_parts(0, 6608)) + // Measured: `626` + // Estimated: `6566` + // Minimum execution time: 22_698_000 picoseconds. + Weight::from_parts(24_305_000, 0) + .saturating_add(Weight::from_parts(0, 6566)) .saturating_add(T::DbWeight::get().reads(6)) } - /// 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: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(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(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::ActorReadyHead` (r:1 w:1) + /// Proof: `Actors::ActorReadyHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::ActorReadyTail` (r:1 w:0) + /// Proof: `Actors::ActorReadyTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::ActorReadyOccupancy` (r:1 w:1) + /// Proof: `Actors::ActorReadyOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `Measured`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, mode: `Measured`) + /// Storage: `Actors::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `Measured`) fn scheduler_paged_consume_preserve_page() -> Weight { // Proof Size summary in bytes: - // 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)) + // Measured: `1653` + // Estimated: `5118` + // Minimum execution time: 65_233_000 picoseconds. + Weight::from_parts(72_287_000, 0) + .saturating_add(Weight::from_parts(0, 5118)) + .saturating_add(T::DbWeight::get().reads(5)) + .saturating_add(T::DbWeight::get().writes(4)) } - /// 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::QueuePages` (r:1 w:1) - /// 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(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::ActorReadyHead` (r:1 w:1) + /// Proof: `Actors::ActorReadyHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::ActorReadyTail` (r:1 w:0) + /// Proof: `Actors::ActorReadyTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::ActorReadyOccupancy` (r:1 w:1) + /// Proof: `Actors::ActorReadyOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `Measured`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, mode: `Measured`) + /// Storage: `Actors::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `Measured`) fn scheduler_paged_consume_delete_page() -> Weight { // Proof Size summary in bytes: - // 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)) + // Measured: `1344` + // Estimated: `4809` + // Minimum execution time: 64_255_000 picoseconds. + Weight::from_parts(67_258_000, 0) + .saturating_add(Weight::from_parts(0, 4809)) + .saturating_add(T::DbWeight::get().reads(5)) + .saturating_add(T::DbWeight::get().writes(4)) } - /// 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:157 w:157) - /// 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(108), added: 2583, mode: `Measured`) + /// Storage: `Actors::ActorReadyHead` (r:1 w:1) + /// Proof: `Actors::ActorReadyHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::ActorReadyTail` (r:1 w:0) + /// Proof: `Actors::ActorReadyTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::ActorReadyOccupancy` (r:1 w:0) + /// Proof: `Actors::ActorReadyOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `Measured`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:313 w:313) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, mode: `Measured`) /// The range of component `n` is `[1, 10000]`. fn scheduler_paged_tombstone_drain(n: u32, ) -> Weight { // Proof Size summary in bytes: - // 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((2_u64).saturating_mul(n.into()))) - .saturating_add(T::DbWeight::get().writes(4)) - .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`) - /// 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:157 w:157) - /// 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(108), added: 2583, mode: `Measured`) + // Measured: `667 + n * (1 ±0)` + // Estimated: `3032 + n * (79 ±0)` + // Minimum execution time: 34_293_000 picoseconds. + Weight::from_parts(35_340_000, 0) + .saturating_add(Weight::from_parts(0, 3032)) + // Standard Error: 744 + .saturating_add(Weight::from_parts(323_374, 0).saturating_mul(n.into())) + .saturating_add(T::DbWeight::get().reads(4)) + .saturating_add(T::DbWeight::get().writes(2)) + .saturating_add(Weight::from_parts(0, 79).saturating_mul(n.into())) + } + /// Storage: `Actors::ActorReadyHead` (r:1 w:1) + /// Proof: `Actors::ActorReadyHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::ActorReadyTail` (r:1 w:0) + /// Proof: `Actors::ActorReadyTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::ActorReadyOccupancy` (r:1 w:1) + /// Proof: `Actors::ActorReadyOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `Measured`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:313 w:313) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, mode: `Measured`) + /// Storage: `Actors::ActorControlLocators` (r:5000 w:5000) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, 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: `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)) + // Measured: `2636 + n * (267 ±0)` + // Estimated: `4675 + n * (1505 ±0)` + // Minimum execution time: 34_851_000 picoseconds. + Weight::from_parts(35_550_000, 0) + .saturating_add(Weight::from_parts(0, 4675)) + // Standard Error: 192_171 + .saturating_add(Weight::from_parts(80_648_201, 0).saturating_mul(n.into())) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(n.into()))) + .saturating_add(T::DbWeight::get().writes(2)) .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(n.into()))) - .saturating_add(Weight::from_parts(0, 2866).saturating_mul(n.into())) + .saturating_add(Weight::from_parts(0, 1505).saturating_mul(n.into())) } - /// 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::ActorFunding` (r:1 w:1) - /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `Measured`) + /// Storage: `Actors::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, 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::ActorUnsignaledControlCells` (r:0 w:1) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, mode: `Measured`) + /// Storage: `Actors::ActorFunding` (r:0 w:1) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `Measured`) fn scheduler_inner_zero_step_complete() -> Weight { // Proof Size summary in bytes: - // 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)) + // Measured: `923` + // Estimated: `4388` + // Minimum execution time: 43_022_000 picoseconds. + Weight::from_parts(58_109_000, 0) + .saturating_add(Weight::from_parts(0, 4388)) + .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(3)) } - /// 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`) - /// 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: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:1 w:1) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `Measured`) + /// Storage: `Actors::ActorReadyTail` (r:1 w:0) + /// Proof: `Actors::ActorReadyTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::ActorReadyHead` (r:1 w:0) + /// Proof: `Actors::ActorReadyHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::ActorReadyOccupancy` (r:1 w:0) + /// Proof: `Actors::ActorReadyOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `Measured`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:1 w:0) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, mode: `Measured`) + /// Storage: `Actors::ActorControlLocators` (r:1 w:0) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, 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::GlobalCircuitBreaker` (r:1 w:0) + /// Proof: `Actors::GlobalCircuitBreaker` (`max_values`: Some(1), `max_size`: Some(1), added: 496, 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::IndexedTriggerDetectionDisabled` (r:1 w:0) + /// Proof: `Actors::IndexedTriggerDetectionDisabled` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `Measured`) + /// Storage: `Oracle::Feeds` (r:1 w:0) + /// Proof: `Oracle::Feeds` (`max_values`: None, `max_size`: Some(86), added: 2561, mode: `Measured`) + fn scheduler_paged_zero_step_user_crossing_unavailable() -> Weight { + // Proof Size summary in bytes: + // Measured: `2662` + // Estimated: `6127` + // Minimum execution time: 129_977_000 picoseconds. + Weight::from_parts(161_755_000, 0) + .saturating_add(Weight::from_parts(0, 6127)) + .saturating_add(T::DbWeight::get().reads(12)) + } + /// Storage: `Actors::ActorReadyTail` (r:1 w:1) + /// Proof: `Actors::ActorReadyTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::ActorReadyHead` (r:1 w:1) + /// Proof: `Actors::ActorReadyHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::ActorReadyOccupancy` (r:1 w:1) + /// Proof: `Actors::ActorReadyOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `Measured`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, mode: `Measured`) + /// Storage: `Actors::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, 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:3 w:3) /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `Measured`) - /// Storage: `Actors::ActorContractTailChunk` (r:8 w:0) + /// Storage: `Actors::ActorContractTailChunk` (r:3 w:0) /// Proof: `Actors::ActorContractTailChunk` (`max_values`: None, `max_size`: Some(4070), added: 6545, mode: `Measured`) + /// Storage: `Actors::ActorRunPayload` (r:1 w:1) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(1491), added: 3966, mode: `Measured`) + /// Storage: `Assets::Asset` (r:1 w:0) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `Measured`) + /// Storage: `Assets::Account` (r:1 w:0) + /// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, 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) @@ -2610,1024 +2862,929 @@ impl pallet_deos_actors::WeightInfo for S /// 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: `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)) + // Measured: `5743` + // Estimated: `14158` + // Minimum execution time: 493_436_000 picoseconds. + Weight::from_parts(593_660_000, 0) + .saturating_add(Weight::from_parts(0, 14158)) + .saturating_add(T::DbWeight::get().reads(24)) + .saturating_add(T::DbWeight::get().writes(13)) } - /// 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) + /// 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:1) - /// Proof: `Actors::ActorAdmissionCertificate` (`max_values`: None, `max_size`: Some(208), added: 2683, mode: `Measured`) - /// Storage: `Actors::ActorContractTailChunk` (r:8 w:8) + /// Storage: `Actors::ActorContractTailChunk` (r:3 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::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, 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::ActorRunPayload` (r:1 w:1) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(1491), added: 3966, mode: `Measured`) + /// Storage: `Assets::Asset` (r:1 w:0) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, 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) + /// Storage: `Actors::ActorUnsignaledControlCells` (r:0 w:1) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, mode: `Measured`) + /// Storage: `Actors::ActorFunding` (r:0 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]`. + /// The range of component `t` is `[0, 3]`. 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())) + // Measured: `1732 + t * (651 ±0)` + // Estimated: `5237 + t * (3000 ±15)` + // Minimum execution time: 86_326_000 picoseconds. + Weight::from_parts(109_517_654, 0) + .saturating_add(Weight::from_parts(0, 5237)) + // Standard Error: 626_531 + .saturating_add(Weight::from_parts(25_013_024, 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())) + .saturating_add(T::DbWeight::get().writes(5)) + .saturating_add(Weight::from_parts(0, 3000).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) + /// Storage: `Actors::ActorContractTailChunk` (r:3 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::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, 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::WakeupBuckets` (r:1 w:1) - /// Proof: `Actors::WakeupBuckets` (`max_values`: None, `max_size`: Some(58), added: 2533, mode: `Measured`) + /// Storage: `Actors::ActorRunPayload` (r:1 w:1) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(1491), added: 3966, mode: `Measured`) + /// Storage: `Assets::Asset` (r:1 w:0) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `Measured`) + /// Storage: `Assets::Account` (r:1 w:0) + /// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `Measured`) + /// Storage: `Actors::ActorWaitingOccupancies` (r:1 w:1) + /// Proof: `Actors::ActorWaitingOccupancies` (`max_values`: None, `max_size`: Some(29), added: 2504, mode: `Measured`) + /// Storage: `Actors::ActorWaitingTails` (r:1 w:1) + /// Proof: `Actors::ActorWaitingTails` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `Measured`) + /// Storage: `Actors::ActorWaitingHeads` (r:1 w:1) + /// Proof: `Actors::ActorWaitingHeads` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `Measured`) + /// Storage: `Actors::ActorWaitingFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorWaitingFrameChunks` (`max_values`: None, `max_size`: Some(11708), added: 14183, mode: `Measured`) + /// Storage: `Actors::ActorWaitingCursorIndices` (r:1 w:1) + /// Proof: `Actors::ActorWaitingCursorIndices` (`max_values`: None, `max_size`: Some(29), added: 2504, 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]`. + /// Storage: `Actors::ActorUnsignaledControlCells` (r:1 w:0) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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::ActorFunding` (r:0 w:1) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `Measured`) + /// The range of component `t` is `[0, 3]`. 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)) + // Measured: `2386 + t * (733 ±0)` + // Estimated: `5946 + t * (2950 ±29)` + // Minimum execution time: 164_060_000 picoseconds. + Weight::from_parts(195_144_203, 0) + .saturating_add(Weight::from_parts(0, 5946)) + // Standard Error: 795_081 + .saturating_add(Weight::from_parts(29_296_426, 0).saturating_mul(t.into())) + .saturating_add(T::DbWeight::get().reads(15)) .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())) + .saturating_add(T::DbWeight::get().writes(11)) + .saturating_add(Weight::from_parts(0, 2950).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) + /// Storage: `Actors::ActorContractTailChunk` (r:3 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) + /// Storage: `Actors::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, 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(1491), added: 3966, mode: `Measured`) + /// Storage: `Assets::Asset` (r:8 w:0) /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `Measured`) - /// Storage: `Assets::Account` (r:63 w:0) + /// Storage: `Assets::Account` (r:7 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]`. + /// Storage: `Actors::ActorUnsignaledControlCells` (r:0 w:1) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, mode: `Measured`) + /// Storage: `Actors::ActorFunding` (r:0 w:1) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `Measured`) + /// The range of component `t` is `[0, 3]`. fn scheduler_inner_opening_failed_max(t: u32, ) -> Weight { // Proof Size summary in bytes: - // 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())) + // Measured: `2370 + t * (1959 ±0)` + // Estimated: `12845 + t * (10106 ±147)` + // Minimum execution time: 111_888_000 picoseconds. + Weight::from_parts(165_770_741, 0) + .saturating_add(Weight::from_parts(0, 12845)) + // Standard Error: 2_150_702 + .saturating_add(Weight::from_parts(94_996_343, 0).saturating_mul(t.into())) + .saturating_add(T::DbWeight::get().reads(12)) + .saturating_add(T::DbWeight::get().reads((8_u64).saturating_mul(t.into()))) + .saturating_add(T::DbWeight::get().writes(5)) + .saturating_add(Weight::from_parts(0, 10106).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) + /// Storage: `Actors::ActorContractTailChunk` (r:3 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) + /// Storage: `Actors::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, 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(1491), added: 3966, mode: `Measured`) + /// Storage: `Assets::Asset` (r:5 w:0) /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `Measured`) - /// Storage: `Assets::Account` (r:63 w:0) + /// Storage: `Assets::Account` (r:5 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::WakeupBuckets` (r:1 w:1) - /// Proof: `Actors::WakeupBuckets` (`max_values`: None, `max_size`: Some(58), added: 2533, mode: `Measured`) + /// Storage: `Actors::ActorWaitingOccupancies` (r:1 w:1) + /// Proof: `Actors::ActorWaitingOccupancies` (`max_values`: None, `max_size`: Some(29), added: 2504, mode: `Measured`) + /// Storage: `Actors::ActorWaitingTails` (r:1 w:1) + /// Proof: `Actors::ActorWaitingTails` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `Measured`) + /// Storage: `Actors::ActorWaitingHeads` (r:1 w:1) + /// Proof: `Actors::ActorWaitingHeads` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `Measured`) + /// Storage: `Actors::ActorWaitingFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorWaitingFrameChunks` (`max_values`: None, `max_size`: Some(11708), added: 14183, mode: `Measured`) + /// Storage: `Actors::ActorWaitingCursorIndices` (r:1 w:1) + /// Proof: `Actors::ActorWaitingCursorIndices` (`max_values`: None, `max_size`: Some(29), added: 2504, 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]`. + /// Storage: `Actors::ActorUnsignaledControlCells` (r:1 w:0) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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::ActorFunding` (r:0 w:1) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `Measured`) + /// The range of component `t` is `[0, 3]`. 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())) + // Measured: `2679 + t * (1183 ±0)` + // Estimated: `8291 + t * (5515 ±78)` + // Minimum execution time: 180_124_000 picoseconds. + Weight::from_parts(228_451_910, 0) + .saturating_add(Weight::from_parts(0, 8291)) + // Standard Error: 1_384_735 + .saturating_add(Weight::from_parts(47_226_607, 0).saturating_mul(t.into())) + .saturating_add(T::DbWeight::get().reads(18)) + .saturating_add(T::DbWeight::get().reads((5_u64).saturating_mul(t.into()))) + .saturating_add(T::DbWeight::get().writes(11)) + .saturating_add(Weight::from_parts(0, 5515).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::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, 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]`. + /// Storage: `Actors::ActorUnsignaledControlCells` (r:0 w:1) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, mode: `Measured`) + /// Storage: `Actors::ActorFunding` (r:0 w:1) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `Measured`) + /// The range of component `t` is `[0, 3]`. 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()))) + // Measured: `933` + // Estimated: `4398` + // Minimum execution time: 50_426_000 picoseconds. + Weight::from_parts(68_359_831, 0) + .saturating_add(Weight::from_parts(0, 4398)) + // Standard Error: 548_468 + .saturating_add(Weight::from_parts(11_019_188, 0).saturating_mul(t.into())) + .saturating_add(T::DbWeight::get().reads(2)) .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::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, 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) + /// Storage: `System::Account` (r:2 w:2) /// 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: `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_min(t: u32, ) -> Weight { + /// 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::ActorUnsignaledControlCells` (r:0 w:1) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, mode: `Measured`) + /// Storage: `Actors::ActorFunding` (r:0 w:1) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `Measured`) + fn scheduler_inner_opening_user_complete_header_max() -> Weight { // Proof Size summary in bytes: - // 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())) + // Measured: `2050` + // Estimated: `7990` + // Minimum execution time: 135_075_000 picoseconds. + Weight::from_parts(175_025_000, 0) + .saturating_add(Weight::from_parts(0, 7990)) + .saturating_add(T::DbWeight::get().reads(9)) + .saturating_add(T::DbWeight::get().writes(5)) } - /// 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) + /// Storage: `Actors::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, 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: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) + /// Storage: `System::Account` (r:3 w:3) /// 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::ActorStateHolds` (r:1 w:1) + /// Proof: `Actors::ActorStateHolds` (`max_values`: None, `max_size`: Some(152), added: 2627, mode: `Measured`) + /// Storage: `Actors::ActorContractTailChunk` (r:3 w:0) + /// Proof: `Actors::ActorContractTailChunk` (`max_values`: None, `max_size`: Some(4070), added: 6545, 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::ActorUnsignaledControlCells` (r:0 w:1) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, mode: `Measured`) + /// Storage: `Actors::ActorFunding` (r:0 w:1) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `Measured`) + /// The range of component `t` is `[1, 3]`. + fn scheduler_inner_opening_user_complete_header_max_tail(t: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `2248 + t * (370 ±0)` + // Estimated: `10677 + t * (2834 ±3)` + // Minimum execution time: 182_219_000 picoseconds. + Weight::from_parts(221_092_770, 0) + .saturating_add(Weight::from_parts(0, 10677)) + // Standard Error: 1_691_800 + .saturating_add(Weight::from_parts(17_027_764, 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, 2834).saturating_mul(t.into())) + } + /// 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:3 w:0) + /// Proof: `Actors::ActorContractTailChunk` (`max_values`: None, `max_size`: Some(4070), added: 6545, mode: `Measured`) + /// Storage: `Actors::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, 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(1491), added: 3966, mode: `Measured`) + /// Storage: `Assets::Asset` (r:1 w:0) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `Measured`) + /// Storage: `Assets::Account` (r:1 w:0) + /// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `Measured`) + /// Storage: `Actors::ActorReadyHead` (r:1 w:0) + /// Proof: `Actors::ActorReadyHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::ActorReadyTail` (r:1 w:1) + /// Proof: `Actors::ActorReadyTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::ActorReadyOccupancy` (r:1 w:1) + /// Proof: `Actors::ActorReadyOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `Measured`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, 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: `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::ActorFunding` (r:0 w:1) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `Measured`) + /// The range of component `t` is `[1, 3]`. + fn scheduler_inner_opening_progress_min(t: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `2872 + t * (502 ±0)` + // Estimated: `6363 + t * (2957 ±6)` + // Minimum execution time: 159_730_000 picoseconds. + Weight::from_parts(185_578_808, 0) + .saturating_add(Weight::from_parts(0, 6363)) + // Standard Error: 1_318_147 + .saturating_add(Weight::from_parts(14_888_868, 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(7)) + .saturating_add(Weight::from_parts(0, 2957).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) + /// Storage: `Actors::ActorContractTailChunk` (r:3 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) + /// Storage: `Actors::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, 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(1491), added: 3966, mode: `Measured`) + /// Storage: `Assets::Asset` (r:9 w:0) /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `Measured`) - /// Storage: `Assets::Account` (r:63 w:0) + /// Storage: `Assets::Account` (r:9 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]`. + /// Storage: `Actors::ActorUnsignaledControlCells` (r:0 w:1) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, mode: `Measured`) + /// Storage: `Actors::ActorFunding` (r:0 w:1) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `Measured`) + /// The range of component `t` is `[0, 3]`. fn scheduler_inner_opening_complete_max(t: u32, ) -> Weight { // Proof Size summary in bytes: - // 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()))) + // Measured: `2290 + t * (1719 ±0)` + // Estimated: `9978 + t * (10416 ±4)` + // Minimum execution time: 107_628_000 picoseconds. + Weight::from_parts(153_228_555, 0) + .saturating_add(Weight::from_parts(0, 9978)) + // Standard Error: 1_910_822 + .saturating_add(Weight::from_parts(90_160_651, 0).saturating_mul(t.into())) + .saturating_add(T::DbWeight::get().reads(11)) + .saturating_add(T::DbWeight::get().reads((8_u64).saturating_mul(t.into()))) .saturating_add(T::DbWeight::get().writes(3)) - .saturating_add(Weight::from_parts(0, 22129).saturating_mul(t.into())) + .saturating_add(Weight::from_parts(0, 10416).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) + /// Storage: `Actors::ActorContractTailChunk` (r:3 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::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, 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(1491), added: 3966, 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) + /// Storage: `Assets::Asset` (r:17 w:0) /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `Measured`) - /// Storage: `Assets::Account` (r:63 w:0) + /// Storage: `Assets::Account` (r:17 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::ActorReadyHead` (r:1 w:0) + /// Proof: `Actors::ActorReadyHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::ActorReadyTail` (r:1 w:1) + /// Proof: `Actors::ActorReadyTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::ActorReadyOccupancy` (r:1 w:1) + /// Proof: `Actors::ActorReadyOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `Measured`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, 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]`. + /// Storage: `Actors::ActorFunding` (r:0 w:1) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `Measured`) + /// The range of component `t` is `[1, 3]`. 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())) + // Measured: `6280 + t * (738 ±0)` + // Estimated: `49359 + t * (3202 ±3)` + // Minimum execution time: 276_367_000 picoseconds. + Weight::from_parts(313_030_635, 0) + .saturating_add(Weight::from_parts(0, 49359)) + // Standard Error: 1_438_626 + .saturating_add(Weight::from_parts(15_870_597, 0).saturating_mul(t.into())) + .saturating_add(T::DbWeight::get().reads(44)) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(t.into()))) + .saturating_add(T::DbWeight::get().writes(7)) + .saturating_add(Weight::from_parts(0, 3202).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::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `Measured`) + /// Storage: `Assets::Asset` (r:4 w:0) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `Measured`) + /// Storage: `Assets::Account` (r:4 w:0) + /// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, 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::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) + /// Storage: `Actors::ActorContractTailChunk` (r:2 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`) + /// 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::ActorUnsignaledControlCells` (r:0 w:1) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, mode: `Measured`) + /// Storage: `Actors::ActorRunHead` (r:0 w:1) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `Measured`) + /// Storage: `Actors::ActorRunPayload` (r:0 w:1) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(1491), added: 3966, 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)) + // Measured: `3156 + p * (403 ±0) + s * (71 ±0)` + // Estimated: `10952 + p * (2914 ±19) + s * (11 ±25)` + // Minimum execution time: 101_202_000 picoseconds. + Weight::from_parts(133_921_853, 0) + .saturating_add(Weight::from_parts(0, 10952)) + // Standard Error: 585_238 + .saturating_add(Weight::from_parts(2_058_487, 0).saturating_mul(s.into())) + // Standard Error: 456_113 + .saturating_add(Weight::from_parts(20_721_195, 0).saturating_mul(p.into())) + .saturating_add(T::DbWeight::get().reads(11)) .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())) + .saturating_add(Weight::from_parts(0, 2914).saturating_mul(p.into())) + .saturating_add(Weight::from_parts(0, 11).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) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `Measured`) + /// Storage: `Actors::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `Measured`) + /// Storage: `Assets::Asset` (r:4 w:0) /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `Measured`) - /// Storage: `Assets::Account` (r:3 w:0) + /// Storage: `Assets::Account` (r:4 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::ActorContractTailChunk` (r:3 w:0) + /// Proof: `Actors::ActorContractTailChunk` (`max_values`: None, `max_size`: Some(4070), added: 6545, mode: `Measured`) + /// Storage: `Actors::ActorReadyHead` (r:1 w:0) + /// Proof: `Actors::ActorReadyHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::ActorReadyTail` (r:1 w:1) + /// Proof: `Actors::ActorReadyTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::ActorReadyOccupancy` (r:1 w:1) + /// Proof: `Actors::ActorReadyOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `Measured`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, 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`) + /// Storage: `Actors::ActorFunding` (r:1 w:0) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, 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`) /// 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)) + fn scheduler_inner_running_progress(_s: u32, p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `3866 + p * (313 ±0) + s * (88 ±0)` + // Estimated: `12546 + p * (2220 ±10)` + // Minimum execution time: 183_546_000 picoseconds. + Weight::from_parts(284_303_244, 0) + .saturating_add(Weight::from_parts(0, 12546)) + // Standard Error: 648_058 + .saturating_add(Weight::from_parts(6_766_806, 0).saturating_mul(p.into())) + .saturating_add(T::DbWeight::get().reads(18)) .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())) + .saturating_add(T::DbWeight::get().writes(5)) + .saturating_add(Weight::from_parts(0, 2220).saturating_mul(p.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: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `Measured`) + /// Storage: `Actors::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, 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) + /// Storage: `Assets::Asset` (r:4 w:0) /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `Measured`) - /// Storage: `Assets::Account` (r:3 w:0) + /// Storage: `Assets::Account` (r:4 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::ActorRunHead` (r:1 w:1) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, 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::ActorWaitingOccupancies` (r:1 w:1) + /// Proof: `Actors::ActorWaitingOccupancies` (`max_values`: None, `max_size`: Some(29), added: 2504, mode: `Measured`) + /// Storage: `Actors::ActorWaitingTails` (r:1 w:1) + /// Proof: `Actors::ActorWaitingTails` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `Measured`) + /// Storage: `Actors::ActorWaitingHeads` (r:1 w:1) + /// Proof: `Actors::ActorWaitingHeads` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `Measured`) + /// Storage: `Actors::ActorWaitingFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorWaitingFrameChunks` (`max_values`: None, `max_size`: Some(11708), added: 14183, mode: `Measured`) + /// Storage: `Actors::ActorWaitingCursorIndices` (r:1 w:1) + /// Proof: `Actors::ActorWaitingCursorIndices` (`max_values`: None, `max_size`: Some(29), added: 2504, 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::ActorUnsignaledControlCells` (r:1 w:0) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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_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)) + // Measured: `3451 + p * (276 ±0) + s * (71 ±0)` + // Estimated: `6937 + p * (2210 ±7) + s * (101 ±6)` + // Minimum execution time: 176_771_000 picoseconds. + Weight::from_parts(224_662_450, 0) + .saturating_add(Weight::from_parts(0, 6937)) + // Standard Error: 956_453 + .saturating_add(Weight::from_parts(3_771_914, 0).saturating_mul(s.into())) + // Standard Error: 745_424 + .saturating_add(Weight::from_parts(15_334_382, 0).saturating_mul(p.into())) + .saturating_add(T::DbWeight::get().reads(15)) .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())) + .saturating_add(T::DbWeight::get().writes(9)) + .saturating_add(Weight::from_parts(0, 2210).saturating_mul(p.into())) + .saturating_add(Weight::from_parts(0, 101).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) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `Measured`) + /// Storage: `Actors::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `Measured`) + /// Storage: `System::Account` (r:2 w:1) /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `Measured`) - /// Storage: `Assets::Asset` (r:3 w:0) + /// Storage: `Assets::Asset` (r:4 w:1) /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `Measured`) - /// Storage: `Assets::Account` (r:3 w:0) + /// Storage: `Assets::Account` (r:5 w:2) /// 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::SovereignIndex` (r:1 w:0) + /// Proof: `Actors::SovereignIndex` (`max_values`: None, `max_size`: Some(56), added: 2531, 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::ActorUnsignaledControlCells` (r:0 w:1) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, mode: `Measured`) + /// Storage: `Actors::ActorRunHead` (r:0 w:1) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `Measured`) + /// Storage: `Actors::ActorRunPayload` (r:0 w:1) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(1491), added: 3966, 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)) + // Measured: `3000 + p * (235 ±0) + s * (16 ±0)` + // Estimated: `8925 + p * (2207 ±74) + s * (204 ±88)` + // Minimum execution time: 153_654_000 picoseconds. + Weight::from_parts(213_795_680, 0) + .saturating_add(Weight::from_parts(0, 8925)) + // Standard Error: 604_860 + .saturating_add(Weight::from_parts(15_584_645, 0).saturating_mul(p.into())) + .saturating_add(T::DbWeight::get().reads(8)) .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())) + .saturating_add(T::DbWeight::get().writes(8)) + .saturating_add(Weight::from_parts(0, 2207).saturating_mul(p.into())) + .saturating_add(Weight::from_parts(0, 204).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) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `Measured`) + /// Storage: `Actors::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `Measured`) + /// Storage: `System::Account` (r:2 w:1) /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `Measured`) - /// Storage: `Assets::Asset` (r:3 w:0) + /// Storage: `Assets::Asset` (r:4 w:1) /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `Measured`) - /// Storage: `Assets::Account` (r:3 w:0) + /// Storage: `Assets::Account` (r:5 w:2) /// 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::SovereignIndex` (r:1 w:0) + /// Proof: `Actors::SovereignIndex` (`max_values`: None, `max_size`: Some(56), added: 2531, 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::ActorContractTailChunk` (r:1 w:0) + /// Proof: `Actors::ActorContractTailChunk` (`max_values`: None, `max_size`: Some(4070), added: 6545, mode: `Measured`) + /// Storage: `Actors::ActorReadyHead` (r:1 w:0) + /// Proof: `Actors::ActorReadyHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::ActorReadyTail` (r:1 w:1) + /// Proof: `Actors::ActorReadyTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::ActorReadyOccupancy` (r:1 w:1) + /// Proof: `Actors::ActorReadyOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `Measured`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, 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)) + // Measured: `3477 + p * (276 ±0) + s * (88 ±0)` + // Estimated: `9954 + p * (2192 ±4) + s * (491 ±7)` + // Minimum execution time: 202_543_000 picoseconds. + Weight::from_parts(244_666_435, 0) + .saturating_add(Weight::from_parts(0, 9954)) + // Standard Error: 1_315_838 + .saturating_add(Weight::from_parts(853_649, 0).saturating_mul(s.into())) + // Standard Error: 734_463 + .saturating_add(Weight::from_parts(21_015_964, 0).saturating_mul(p.into())) + .saturating_add(T::DbWeight::get().reads(14)) .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())) + .saturating_add(T::DbWeight::get().writes(9)) + .saturating_add(Weight::from_parts(0, 2192).saturating_mul(p.into())) + .saturating_add(Weight::from_parts(0, 491).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) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `Measured`) + /// Storage: `Actors::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `Measured`) + /// Storage: `Assets::Asset` (r:5 w:0) /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `Measured`) - /// Storage: `Assets::Account` (r:3 w:0) + /// Storage: `Assets::Account` (r:5 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::ActorRunHead` (r:1 w:1) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `Measured`) + /// Storage: `Actors::ActorWaitingOccupancies` (r:1 w:1) + /// Proof: `Actors::ActorWaitingOccupancies` (`max_values`: None, `max_size`: Some(29), added: 2504, mode: `Measured`) + /// Storage: `Actors::ActorWaitingTails` (r:1 w:1) + /// Proof: `Actors::ActorWaitingTails` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `Measured`) + /// Storage: `Actors::ActorWaitingHeads` (r:1 w:1) + /// Proof: `Actors::ActorWaitingHeads` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `Measured`) + /// Storage: `Actors::ActorWaitingFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorWaitingFrameChunks` (`max_values`: None, `max_size`: Some(11708), added: 14183, mode: `Measured`) + /// Storage: `Actors::ActorWaitingCursorIndices` (r:1 w:1) + /// Proof: `Actors::ActorWaitingCursorIndices` (`max_values`: None, `max_size`: Some(29), added: 2504, 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]`. + /// Storage: `Actors::ActorUnsignaledControlCells` (r:1 w:0) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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, 22]`. /// 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)) + fn scheduler_inner_suspended_head_retry(n: u32, p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `3085 + p * (346 ±0)` + // Estimated: `6848 + p * (2793 ±7)` + // Minimum execution time: 172_581_000 picoseconds. + Weight::from_parts(233_830_626, 0) + .saturating_add(Weight::from_parts(0, 6848)) + // Standard Error: 140_262 + .saturating_add(Weight::from_parts(244_723, 0).saturating_mul(n.into())) + // Standard Error: 691_729 + .saturating_add(Weight::from_parts(9_611_569, 0).saturating_mul(p.into())) + .saturating_add(T::DbWeight::get().reads(14)) .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())) + .saturating_add(T::DbWeight::get().writes(9)) + .saturating_add(Weight::from_parts(0, 2793).saturating_mul(p.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::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `Measured`) + /// Storage: `Assets::Asset` (r:5 w:0) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `Measured`) + /// Storage: `Assets::Account` (r:5 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::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]`. + /// Storage: `Actors::ActorUnsignaledControlCells` (r:0 w:1) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, mode: `Measured`) + /// Storage: `Actors::ActorRunHead` (r:0 w:1) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `Measured`) + /// Storage: `Actors::ActorRunPayload` (r:0 w:1) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(1491), added: 3966, mode: `Measured`) /// 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)) + fn scheduler_inner_suspended_head_complete(p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `2548 + p * (220 ±0)` + // Estimated: `6004 + p * (2701 ±3)` + // Minimum execution time: 100_713_000 picoseconds. + Weight::from_parts(145_300_991, 0) + .saturating_add(Weight::from_parts(0, 6004)) + // Standard Error: 474_659 + .saturating_add(Weight::from_parts(1_898_621, 0).saturating_mul(p.into())) + .saturating_add(T::DbWeight::get().reads(6)) .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())) + .saturating_add(Weight::from_parts(0, 2701).saturating_mul(p.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) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `Measured`) + /// Storage: `Actors::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `Measured`) + /// Storage: `Assets::Asset` (r:5 w:0) /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `Measured`) - /// Storage: `Assets::Account` (r:3 w:0) + /// Storage: `Assets::Account` (r:5 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: `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::ActorContractTailChunk` (r:1 w:0) + /// Proof: `Actors::ActorContractTailChunk` (`max_values`: None, `max_size`: Some(4070), added: 6545, mode: `Measured`) + /// Storage: `Actors::ActorReadyHead` (r:1 w:0) + /// Proof: `Actors::ActorReadyHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::ActorReadyTail` (r:1 w:1) + /// Proof: `Actors::ActorReadyTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::ActorReadyOccupancy` (r:1 w:1) + /// Proof: `Actors::ActorReadyOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `Measured`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, 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 `n` is `[0, 22]`. /// 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())) + fn scheduler_inner_suspended_head_progress(n: u32, p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `4019 + p * (347 ±0)` + // Estimated: `7784 + p * (2793 ±8)` + // Minimum execution time: 168_809_000 picoseconds. + Weight::from_parts(224_981_329, 0) + .saturating_add(Weight::from_parts(0, 7784)) + // Standard Error: 119_468 + .saturating_add(Weight::from_parts(39_366, 0).saturating_mul(n.into())) + // Standard Error: 589_180 + .saturating_add(Weight::from_parts(11_797_163, 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())) + .saturating_add(T::DbWeight::get().writes(5)) + .saturating_add(Weight::from_parts(0, 2793).saturating_mul(p.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) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `Measured`) + /// Storage: `Actors::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `Measured`) + /// Storage: `Assets::Asset` (r:4 w:0) /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `Measured`) - /// Storage: `Assets::Account` (r:3 w:0) + /// Storage: `Assets::Account` (r:4 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::ActorRunHead` (r:1 w:1) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `Measured`) + /// Storage: `Actors::ActorWaitingOccupancies` (r:1 w:1) + /// Proof: `Actors::ActorWaitingOccupancies` (`max_values`: None, `max_size`: Some(29), added: 2504, mode: `Measured`) + /// Storage: `Actors::ActorWaitingTails` (r:1 w:1) + /// Proof: `Actors::ActorWaitingTails` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `Measured`) + /// Storage: `Actors::ActorWaitingHeads` (r:1 w:1) + /// Proof: `Actors::ActorWaitingHeads` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `Measured`) + /// Storage: `Actors::ActorWaitingFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorWaitingFrameChunks` (`max_values`: None, `max_size`: Some(11708), added: 14183, mode: `Measured`) + /// Storage: `Actors::ActorWaitingCursorIndices` (r:1 w:1) + /// Proof: `Actors::ActorWaitingCursorIndices` (`max_values`: None, `max_size`: Some(29), added: 2504, 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::ActorUnsignaledControlCells` (r:1 w:0) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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, 22]`. + /// The range of component `p` is `[0, 3]`. + fn scheduler_inner_suspended_head_opening_retry(_n: u32, p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `3110 + p * (347 ±0)` + // Estimated: `6832 + p * (2786 ±8)` + // Minimum execution time: 179_285_000 picoseconds. + Weight::from_parts(273_071_818, 0) + .saturating_add(Weight::from_parts(0, 6832)) + // Standard Error: 865_877 + .saturating_add(Weight::from_parts(6_709_813, 0).saturating_mul(p.into())) + .saturating_add(T::DbWeight::get().reads(14)) + .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(p.into()))) + .saturating_add(T::DbWeight::get().writes(9)) + .saturating_add(Weight::from_parts(0, 2786).saturating_mul(p.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::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `Measured`) + /// Storage: `Assets::Asset` (r:4 w:0) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `Measured`) + /// Storage: `Assets::Account` (r:4 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::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)) + /// Storage: `Actors::ActorUnsignaledControlCells` (r:0 w:1) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, mode: `Measured`) + /// Storage: `Actors::ActorRunHead` (r:0 w:1) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `Measured`) + /// Storage: `Actors::ActorRunPayload` (r:0 w:1) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(1491), added: 3966, mode: `Measured`) + /// The range of component `p` is `[0, 3]`. + fn scheduler_inner_suspended_head_opening_complete(p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `2560 + p * (207 ±0)` + // Estimated: `6017 + p * (2694 ±5)` + // Minimum execution time: 103_436_000 picoseconds. + Weight::from_parts(128_652_913, 0) + .saturating_add(Weight::from_parts(0, 6017)) + // Standard Error: 527_744 + .saturating_add(Weight::from_parts(4_574_010, 0).saturating_mul(p.into())) + .saturating_add(T::DbWeight::get().reads(6)) + .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, 2).saturating_mul(r.into())) + .saturating_add(Weight::from_parts(0, 2694).saturating_mul(p.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) + /// Storage: `Actors::ActorContractHead` (r:1 w:0) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, mode: `Measured`) + /// Storage: `Actors::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `Measured`) + /// Storage: `Assets::Asset` (r:4 w:0) /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `Measured`) - /// Storage: `Assets::Account` (r:3 w:0) + /// Storage: `Assets::Account` (r:4 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: `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::ActorContractTailChunk` (r:1 w:0) + /// Proof: `Actors::ActorContractTailChunk` (`max_values`: None, `max_size`: Some(4070), added: 6545, mode: `Measured`) + /// Storage: `Actors::ActorReadyHead` (r:1 w:0) + /// Proof: `Actors::ActorReadyHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::ActorReadyTail` (r:1 w:1) + /// Proof: `Actors::ActorReadyTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::ActorReadyOccupancy` (r:1 w:1) + /// Proof: `Actors::ActorReadyOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `Measured`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, 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`) + /// The range of component `n` is `[0, 22]`. + /// The range of component `p` is `[0, 3]`. + fn scheduler_inner_suspended_head_opening_progress(_n: u32, p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `4093 + p * (298 ±0)` + // Estimated: `7919 + p * (2736 ±12)` + // Minimum execution time: 181_311_000 picoseconds. + Weight::from_parts(236_649_836, 0) + .saturating_add(Weight::from_parts(0, 7919)) + // Standard Error: 775_970 + .saturating_add(Weight::from_parts(11_253_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(5)) + .saturating_add(Weight::from_parts(0, 2736).saturating_mul(p.into())) + } + /// Storage: `Actors::ActorReadyTail` (r:1 w:0) + /// Proof: `Actors::ActorReadyTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::ActorReadyHead` (r:1 w:1) + /// Proof: `Actors::ActorReadyHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::ActorReadyOccupancy` (r:1 w:1) + /// Proof: `Actors::ActorReadyOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `Measured`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:32 w:32) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, mode: `Measured`) + /// Storage: `Actors::ActorControlLocators` (r:999 w:999) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, 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`) + /// Storage: `Actors::ActorUnsignaledControlCells` (r:0 w:999) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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)) + // Measured: `1666 + n * (533 ±0)` + // Estimated: `3993 + n * (3009 ±0)` + // Minimum execution time: 194_790_000 picoseconds. + Weight::from_parts(199_820_000, 0) + .saturating_add(Weight::from_parts(0, 3993)) + // Standard Error: 1_624_613 + .saturating_add(Weight::from_parts(251_803_938, 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().writes(3)) .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`) + .saturating_add(Weight::from_parts(0, 3009).saturating_mul(n.into())) + } + /// Storage: `Actors::ActorReadyTail` (r:1 w:0) + /// Proof: `Actors::ActorReadyTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::ActorReadyHead` (r:1 w:1) + /// Proof: `Actors::ActorReadyHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::ActorReadyOccupancy` (r:1 w:1) + /// Proof: `Actors::ActorReadyOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `Measured`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:32 w:32) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, mode: `Measured`) + /// Storage: `Actors::ActorControlLocators` (r:1000 w:1000) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, 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) + /// Storage: `Actors::ActorStateHolds` (r:1000 w:0) /// Proof: `Actors::ActorStateHolds` (`max_values`: None, `max_size`: Some(152), added: 2627, mode: `Measured`) - /// Storage: `System::Account` (r:1001 w:1001) + /// Storage: `System::Account` (r:502 w:502) /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `Measured`) + /// Storage: `Actors::ActorRunPayload` (r:500 w:0) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(1491), added: 3966, mode: `Measured`) + /// Storage: `Actors::SovereignIndex` (r:1 w:0) + /// Proof: `Actors::SovereignIndex` (`max_values`: None, `max_size`: Some(56), added: 2531, mode: `Measured`) + /// Storage: `Assets::Asset` (r:1 w:0) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, 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) @@ -3636,28 +3793,28 @@ impl pallet_deos_actors::WeightInfo for S /// 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`) + /// Storage: `Actors::ActorUnsignaledControlCells` (r:0 w:1000) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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()))) + // Measured: `1420 + n * (730 ±0)` + // Estimated: `6958 + n * (3206 ±0)` + // Minimum execution time: 550_497_000 picoseconds. + Weight::from_parts(601_203_000, 0) + .saturating_add(Weight::from_parts(0, 6958)) + // Standard Error: 626_733 + .saturating_add(Weight::from_parts(358_077_352, 0).saturating_mul(n.into())) + .saturating_add(T::DbWeight::get().reads(7)) + .saturating_add(T::DbWeight::get().reads((8_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())) + .saturating_add(T::DbWeight::get().writes((4_u64).saturating_mul(n.into()))) + .saturating_add(Weight::from_parts(0, 3206).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::ActorControlLocators` (r:1 w:0) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `Measured`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, 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) @@ -3665,67 +3822,73 @@ impl pallet_deos_actors::WeightInfo for S /// 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`) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(1491), added: 3966, 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`) + /// Storage: `Actors::ActorWaitingOccupancies` (r:1 w:1) + /// Proof: `Actors::ActorWaitingOccupancies` (`max_values`: None, `max_size`: Some(29), added: 2504, mode: `Measured`) + /// Storage: `Actors::ActorWaitingTails` (r:1 w:1) + /// Proof: `Actors::ActorWaitingTails` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `Measured`) + /// Storage: `Actors::ActorWaitingHeads` (r:1 w:1) + /// Proof: `Actors::ActorWaitingHeads` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `Measured`) + /// Storage: `Actors::ActorWaitingFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorWaitingFrameChunks` (`max_values`: None, `max_size`: Some(11708), added: 14183, mode: `Measured`) + /// Storage: `Actors::ActorWaitingCursorIndices` (r:1 w:1) + /// Proof: `Actors::ActorWaitingCursorIndices` (`max_values`: None, `max_size`: Some(29), added: 2504, 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`) 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)) + // Measured: `2991` + // Estimated: `6456` + // Minimum execution time: 225_661_000 picoseconds. + Weight::from_parts(279_510_000, 0) + .saturating_add(Weight::from_parts(0, 6456)) + .saturating_add(T::DbWeight::get().reads(14)) + .saturating_add(T::DbWeight::get().writes(9)) } + /// Storage: `Actors::ActorControlLocators` (r:1 w:0) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `Measured`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, 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`) - 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)) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(1491), added: 3966, mode: `Measured`) + /// Storage: `Actors::ActorWaitingOccupancies` (r:1 w:1) + /// Proof: `Actors::ActorWaitingOccupancies` (`max_values`: None, `max_size`: Some(29), added: 2504, mode: `Measured`) + /// Storage: `Actors::ActorWaitingTails` (r:1 w:1) + /// Proof: `Actors::ActorWaitingTails` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `Measured`) + /// Storage: `Actors::ActorWaitingHeads` (r:1 w:1) + /// Proof: `Actors::ActorWaitingHeads` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `Measured`) + /// Storage: `Actors::ActorWaitingFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorWaitingFrameChunks` (`max_values`: None, `max_size`: Some(11708), added: 14183, mode: `Measured`) + /// Storage: `Actors::ActorWaitingCursorIndices` (r:1 w:1) + /// Proof: `Actors::ActorWaitingCursorIndices` (`max_values`: None, `max_size`: Some(29), added: 2504, 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`) + fn run_suspend() -> Weight { + // Proof Size summary in bytes: + // Measured: `2406` + // Estimated: `5871` + // Minimum execution time: 211_902_000 picoseconds. + Weight::from_parts(286_075_000, 0) + .saturating_add(Weight::from_parts(0, 5871)) + .saturating_add(T::DbWeight::get().reads(13)) + .saturating_add(T::DbWeight::get().writes(9)) } - /// 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::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `Measured`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, 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) @@ -3733,84 +3896,77 @@ impl pallet_deos_actors::WeightInfo for S /// 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`) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(1491), added: 3966, 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::ActorReadyOccupancy` (r:1 w:1) + /// Proof: `Actors::ActorReadyOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `Measured`) + /// Storage: `Actors::ActorUnsignaledControlCells` (r:0 w:1) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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)) + // Measured: `2772` + // Estimated: `6237` + // Minimum execution time: 159_520_000 picoseconds. + Weight::from_parts(201_984_000, 0) + .saturating_add(Weight::from_parts(0, 6237)) .saturating_add(T::DbWeight::get().reads(8)) - .saturating_add(T::DbWeight::get().writes(4)) + .saturating_add(T::DbWeight::get().writes(6)) } - /// Storage: `Actors::ActorIdentities` (r:1 w:1) + /// Storage: `Actors::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, 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(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorWaitingFrameChunks` (r:3 w:3) + /// Proof: `Actors::ActorWaitingFrameChunks` (`max_values`: None, `max_size`: Some(11708), added: 14183, 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::ActorContractTailChunk` (r:3 w:0) + /// Proof: `Actors::ActorContractTailChunk` (`max_values`: None, `max_size`: Some(4070), added: 6545, 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`) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(1491), added: 3966, 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::ActorWaitingOccupancies` (r:1 w:1) + /// Proof: `Actors::ActorWaitingOccupancies` (`max_values`: None, `max_size`: Some(29), added: 2504, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorWaitingHeads` (r:1 w:0) + /// Proof: `Actors::ActorWaitingHeads` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorWaitingTails` (r:1 w:0) + /// Proof: `Actors::ActorWaitingTails` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorWaitingCursorIndices` (r:1 w:0) + /// Proof: `Actors::ActorWaitingCursorIndices` (`max_values`: None, `max_size`: Some(29), added: 2504, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyHead` (r:1 w:0) + /// Proof: `Actors::ActorReadyHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyTail` (r:1 w:1) + /// Proof: `Actors::ActorReadyTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyOccupancy` (r:1 w:1) + /// Proof: `Actors::ActorReadyOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, 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())) + // Measured: `15853` + // Estimated: `43539` + // Minimum execution time: 438_331_000 picoseconds. + Weight::from_parts(486_871_000, 0) + .saturating_add(Weight::from_parts(0, 43539)) + .saturating_add(T::DbWeight::get().reads(22)) + .saturating_add(T::DbWeight::get().writes(10)) } /// 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::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `Measured`) + /// Storage: `Actors::ActorUnsignaledControlCells` (r:1 w:1) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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) @@ -3819,24 +3975,22 @@ impl pallet_deos_actors::WeightInfo for S /// 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`) + /// Storage: `Actors::ActorReadyHead` (r:1 w:0) + /// Proof: `Actors::ActorReadyHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::ActorReadyTail` (r:1 w:1) + /// Proof: `Actors::ActorReadyTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::ActorReadyOccupancy` (r:1 w:1) + /// Proof: `Actors::ActorReadyOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `Measured`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, 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)) + // Measured: `2359` + // Estimated: `8299` + // Minimum execution time: 163_711_000 picoseconds. + Weight::from_parts(191_298_000, 0) + .saturating_add(Weight::from_parts(0, 8299)) + .saturating_add(T::DbWeight::get().reads(13)) .saturating_add(T::DbWeight::get().writes(8)) } /// Storage: `Actors::ObservationSubscriberCount` (r:1 w:0) @@ -3849,10 +4003,10 @@ impl pallet_deos_actors::WeightInfo for S /// 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` + // Measured: `818` // Estimated: `6184` - // Minimum execution time: 32_756_000 picoseconds. - Weight::from_parts(34_852_000, 0) + // Minimum execution time: 36_597_000 picoseconds. + Weight::from_parts(43_232_000, 0) .saturating_add(Weight::from_parts(0, 6184)) .saturating_add(T::DbWeight::get().reads(5)) .saturating_add(T::DbWeight::get().writes(4)) @@ -3863,10 +4017,10 @@ impl pallet_deos_actors::WeightInfo for S /// 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` + // Measured: `426` // Estimated: `1629` - // Minimum execution time: 4_679_000 picoseconds. - Weight::from_parts(6_285_000, 0) + // Minimum execution time: 5_587_000 picoseconds. + Weight::from_parts(6_775_000, 0) .saturating_add(Weight::from_parts(0, 1629)) .saturating_add(T::DbWeight::get().reads(2)) } @@ -3876,10 +4030,10 @@ impl pallet_deos_actors::WeightInfo for S /// 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` + // Measured: `665` // Estimated: `3587` - // Minimum execution time: 11_803_000 picoseconds. - Weight::from_parts(13_340_000, 0) + // Minimum execution time: 13_270_000 picoseconds. + Weight::from_parts(14_247_000, 0) .saturating_add(Weight::from_parts(0, 3587)) .saturating_add(T::DbWeight::get().reads(2)) } @@ -3893,12 +4047,12 @@ impl pallet_deos_actors::WeightInfo for S /// 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::ActorControlLocators` (r:64 w:64) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorUnsignaledControlCells` (r:64 w:64) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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) @@ -3907,25 +4061,23 @@ impl pallet_deos_actors::WeightInfo for S /// 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) - /// 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`) + /// Storage: `Actors::ActorReadyHead` (r:1 w:0) + /// Proof: `Actors::ActorReadyHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyTail` (r:1 w:1) + /// Proof: `Actors::ActorReadyTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyOccupancy` (r:1 w:1) + /// Proof: `Actors::ActorReadyOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:2 w:2) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, mode: `MaxEncodedLen`) fn observation_fanout_page() -> Weight { // Proof Size summary in bytes: - // Measured: `46609` + // Measured: `57022` // Estimated: `304734` - // Minimum execution time: 4_559_661_000 picoseconds. - Weight::from_parts(4_651_713_000, 0) + // Minimum execution time: 7_457_005_000 picoseconds. + Weight::from_parts(7_859_158_000, 0) .saturating_add(Weight::from_parts(0, 304734)) .saturating_add(T::DbWeight::get().reads(461)) - .saturating_add(T::DbWeight::get().writes(201)) + .saturating_add(T::DbWeight::get().writes(265)) } /// Storage: `Actors::DirtyObservationListState` (r:1 w:1) /// Proof: `Actors::DirtyObservationListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) @@ -3937,12 +4089,12 @@ impl pallet_deos_actors::WeightInfo for S /// 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::ActorControlLocators` (r:64 w:64) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorUnsignaledControlCells` (r:64 w:64) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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) @@ -3951,23 +4103,29 @@ impl pallet_deos_actors::WeightInfo for S /// 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::ActorWaitingOccupancies` (r:1 w:1) + /// Proof: `Actors::ActorWaitingOccupancies` (`max_values`: None, `max_size`: Some(29), added: 2504, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorWaitingTails` (r:1 w:1) + /// Proof: `Actors::ActorWaitingTails` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorWaitingHeads` (r:1 w:1) + /// Proof: `Actors::ActorWaitingHeads` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorWaitingFrameChunks` (r:2 w:2) + /// Proof: `Actors::ActorWaitingFrameChunks` (`max_values`: None, `max_size`: Some(11708), added: 14183, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorWaitingCursorIndices` (r:1 w:1) + /// Proof: `Actors::ActorWaitingCursorIndices` (`max_values`: None, `max_size`: Some(29), added: 2504, 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` + // Measured: `57084` // Estimated: `304734` - // Minimum execution time: 4_927_521_000 picoseconds. - Weight::from_parts(5_061_897_000, 0) + // Minimum execution time: 9_706_978_000 picoseconds. + Weight::from_parts(10_472_871_000, 0) .saturating_add(Weight::from_parts(0, 304734)) - .saturating_add(T::DbWeight::get().reads(457)) - .saturating_add(T::DbWeight::get().writes(200)) + .saturating_add(T::DbWeight::get().reads(462)) + .saturating_add(T::DbWeight::get().writes(267)) } /// Storage: `Actors::DirtyObservationListState` (r:1 w:1) /// Proof: `Actors::DirtyObservationListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) @@ -3979,12 +4137,12 @@ impl pallet_deos_actors::WeightInfo for S /// 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::ActorControlLocators` (r:64 w:0) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:2 w:0) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, 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) @@ -3993,12 +4151,12 @@ impl pallet_deos_actors::WeightInfo for S /// 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` + // Measured: `44460` // Estimated: `304734` - // Minimum execution time: 1_907_535_000 picoseconds. - Weight::from_parts(1_961_593_000, 0) + // Minimum execution time: 4_025_647_000 picoseconds. + Weight::from_parts(4_301_175_000, 0) .saturating_add(Weight::from_parts(0, 304734)) - .saturating_add(T::DbWeight::get().reads(389)) + .saturating_add(T::DbWeight::get().reads(327)) .saturating_add(T::DbWeight::get().writes(2)) } /// Storage: `Actors::DirtyObservationListState` (r:1 w:1) @@ -4011,22 +4169,24 @@ impl pallet_deos_actors::WeightInfo for S /// 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::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorWaitingFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorWaitingFrameChunks` (`max_values`: None, `max_size`: Some(11708), added: 14183, 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::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:1) /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunPayload` (r:1 w:1) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(1491), added: 3966, 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) @@ -4053,21 +4213,33 @@ impl pallet_deos_actors::WeightInfo for S /// 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::ActorWaitingOccupancies` (r:1 w:1) + /// Proof: `Actors::ActorWaitingOccupancies` (`max_values`: None, `max_size`: Some(29), added: 2504, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorWaitingHeads` (r:1 w:1) + /// Proof: `Actors::ActorWaitingHeads` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorWaitingTails` (r:1 w:1) + /// Proof: `Actors::ActorWaitingTails` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorWaitingCursorIndices` (r:1 w:1) + /// Proof: `Actors::ActorWaitingCursorIndices` (`max_values`: None, `max_size`: Some(29), added: 2504, 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::WakeupCursorLen` (r:1 w:1) + /// Proof: `Actors::WakeupCursorLen` (`max_values`: None, `max_size`: Some(21), added: 2496, 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::ActorUnsignaledControlCells` (r:1 w:0) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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)) + // Measured: `3604` + // Estimated: `15173` + // Minimum execution time: 349_562_000 picoseconds. + Weight::from_parts(385_810_000, 0) + .saturating_add(Weight::from_parts(0, 15173)) + .saturating_add(T::DbWeight::get().reads(35)) + .saturating_add(T::DbWeight::get().writes(30)) } /// Storage: `Actors::DirtyObservationListState` (r:1 w:1) /// Proof: `Actors::DirtyObservationListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) @@ -4079,12 +4251,12 @@ impl pallet_deos_actors::WeightInfo for S /// 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::ActorControlLocators` (r:64 w:64) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorUnsignaledControlCells` (r:64 w:64) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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) @@ -4093,37 +4265,37 @@ impl pallet_deos_actors::WeightInfo for S /// 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::ActorReadyHead` (r:1 w:0) + /// Proof: `Actors::ActorReadyHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyTail` (r:1 w:0) + /// Proof: `Actors::ActorReadyTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyOccupancy` (r:1 w:0) + /// Proof: `Actors::ActorReadyOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:2 w:0) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorWaitingOccupancies` (r:1 w:1) + /// Proof: `Actors::ActorWaitingOccupancies` (`max_values`: None, `max_size`: Some(29), added: 2504, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorWaitingTails` (r:1 w:1) + /// Proof: `Actors::ActorWaitingTails` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorWaitingHeads` (r:1 w:1) + /// Proof: `Actors::ActorWaitingHeads` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorWaitingFrameChunks` (r:2 w:2) + /// Proof: `Actors::ActorWaitingFrameChunks` (`max_values`: None, `max_size`: Some(11708), added: 14183, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorWaitingCursorIndices` (r:1 w:1) + /// Proof: `Actors::ActorWaitingCursorIndices` (`max_values`: None, `max_size`: Some(29), added: 2504, 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` + // Measured: `57933` // Estimated: `304734` - // Minimum execution time: 115_118_776_000 picoseconds. - Weight::from_parts(116_219_631_000, 0) + // Minimum execution time: 129_421_072_000 picoseconds. + Weight::from_parts(136_174_555_000, 0) .saturating_add(Weight::from_parts(0, 304734)) - .saturating_add(T::DbWeight::get().reads(591)) - .saturating_add(T::DbWeight::get().writes(200)) + .saturating_add(T::DbWeight::get().reads(467)) + .saturating_add(T::DbWeight::get().writes(267)) } /// Storage: `Actors::CrossingWorkerFaultState` (r:1 w:0) /// Proof: `Actors::CrossingWorkerFaultState` (`max_values`: Some(1), `max_size`: Some(44), added: 539, mode: `MaxEncodedLen`) @@ -4131,10 +4303,10 @@ 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: `421` + // Measured: `465` // Estimated: `1543` - // Minimum execution time: 5_797_000 picoseconds. - Weight::from_parts(7_543_000, 0) + // Minimum execution time: 6_076_000 picoseconds. + Weight::from_parts(6_984_000, 0) .saturating_add(Weight::from_parts(0, 1543)) .saturating_add(T::DbWeight::get().reads(2)) } @@ -4152,24 +4324,22 @@ impl pallet_deos_actors::WeightInfo for S /// 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(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::ActorControlLocators` (r:1 w:0) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:1 w:0) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:1 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) fn crossing_work_probe() -> Weight { // Proof Size summary in bytes: - // 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)) + // Measured: `1892` + // Estimated: `15106` + // Minimum execution time: 64_325_000 picoseconds. + Weight::from_parts(80_668_000, 0) + .saturating_add(Weight::from_parts(0, 15106)) + .saturating_add(T::DbWeight::get().reads(11)) } /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:1) /// Proof: `Actors::CrossingPendingFeedListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `Measured`) @@ -4185,22 +4355,30 @@ impl pallet_deos_actors::WeightInfo for S /// 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::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, 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::ActorUnsignaledControlCells` (r:1 w:1) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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::ActorRunPayload` (r:1 w:0) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(1491), added: 3966, 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::ActorReadyHead` (r:1 w:0) + /// Proof: `Actors::ActorReadyHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::ActorReadyTail` (r:1 w:1) + /// Proof: `Actors::ActorReadyTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `Measured`) + /// Storage: `Actors::ActorReadyOccupancy` (r:1 w:1) + /// Proof: `Actors::ActorReadyOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `Measured`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, 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) @@ -4209,23 +4387,15 @@ impl pallet_deos_actors::WeightInfo for S /// 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)) + // Measured: `4798` + // Estimated: `164188` + // Minimum execution time: 655_122_000 picoseconds. + Weight::from_parts(713_371_000, 0) + .saturating_add(Weight::from_parts(0, 164188)) .saturating_add(T::DbWeight::get().reads(90)) .saturating_add(T::DbWeight::get().writes(81)) } @@ -4237,8 +4407,8 @@ impl pallet_deos_actors::WeightInfo for S // Proof Size summary in bytes: // Measured: `2415` // Estimated: `81886` - // Minimum execution time: 123_202_000 picoseconds. - Weight::from_parts(125_926_000, 0) + // Minimum execution time: 129_208_000 picoseconds. + Weight::from_parts(134_168_000, 0) .saturating_add(Weight::from_parts(0, 81886)) .saturating_add(T::DbWeight::get().reads(33)) } @@ -4256,28 +4426,26 @@ impl pallet_deos_actors::WeightInfo for S /// 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(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::ActorControlLocators` (r:1 w:0) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorUnsignaledControlCells` (r:1 w:0) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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::ActorReadyTail` (r:1 w:0) + /// Proof: `Actors::ActorReadyTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) fn crossing_fire_probe() -> Weight { // Proof Size summary in bytes: - // Measured: `2307` + // Measured: `1948` // Estimated: `11729` - // Minimum execution time: 63_906_000 picoseconds. - Weight::from_parts(66_770_000, 0) + // Minimum execution time: 66_699_000 picoseconds. + Weight::from_parts(89_119_000, 0) .saturating_add(Weight::from_parts(0, 11729)) - .saturating_add(T::DbWeight::get().reads(14)) + .saturating_add(T::DbWeight::get().reads(13)) } /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:0) /// Proof: `Actors::CrossingPendingFeedListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) @@ -4293,45 +4461,45 @@ impl pallet_deos_actors::WeightInfo for S /// 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(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::ActorControlLocators` (r:2 w:0) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorUnsignaledControlCells` (r:2 w:0) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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::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:1 w:0) - /// Proof: `Actors::QueuePages` (`max_values`: None, `max_size`: Some(6170), added: 8645, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyTail` (r:1 w:0) + /// Proof: `Actors::ActorReadyTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyHead` (r:1 w:0) + /// Proof: `Actors::ActorReadyHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyOccupancy` (r:1 w:0) + /// Proof: `Actors::ActorReadyOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:1 w:0) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, 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::ActorFunding` (r:2 w:0) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `MaxEncodedLen`) fn crossing_fire_pair_probe() -> Weight { // Proof Size summary in bytes: - // 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)) + // Measured: `13667` + // Estimated: `15106` + // Minimum execution time: 309_471_000 picoseconds. + Weight::from_parts(350_888_000, 0) + .saturating_add(Weight::from_parts(0, 15106)) + .saturating_add(T::DbWeight::get().reads(26)) } /// 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` + // Measured: `958` // Estimated: `11729` - // Minimum execution time: 17_670_000 picoseconds. - Weight::from_parts(20_114_000, 0) + // Minimum execution time: 18_020_000 picoseconds. + Weight::from_parts(20_324_000, 0) .saturating_add(Weight::from_parts(0, 11729)) .saturating_add(T::DbWeight::get().reads(1)) } @@ -4339,150 +4507,141 @@ impl pallet_deos_actors::WeightInfo for S /// 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::ActorControlLocators` (r:128 w:0) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorUnsignaledControlCells` (r:128 w:0) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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`) + /// Storage: `Actors::ActorReadyTail` (r:1 w:0) + /// Proof: `Actors::ActorReadyTail` (`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_696_000 picoseconds. - Weight::from_parts(41_975_000, 0) + // Measured: `1015 + c * (564 ±0)` + // Estimated: `1493 + c * (2861 ±0)` + // Minimum execution time: 42_744_000 picoseconds. + Weight::from_parts(44_350_000, 0) .saturating_add(Weight::from_parts(0, 1493)) - // Standard Error: 24_685 - .saturating_add(Weight::from_parts(23_857_544, 0).saturating_mul(c.into())) + // Standard Error: 81_409 + .saturating_add(Weight::from_parts(26_177_365, 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())) + .saturating_add(T::DbWeight::get().reads((6_u64).saturating_mul(c.into()))) + .saturating_add(Weight::from_parts(0, 2861).saturating_mul(c.into())) } /// 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::ActorControlLocators` (r:128 w:0) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:4 w:0) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, 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`) + /// Storage: `Actors::ActorReadyTail` (r:1 w:0) + /// Proof: `Actors::ActorReadyTail` (`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: 43_302_000 picoseconds. - Weight::from_parts(44_280_000, 0) - .saturating_add(Weight::from_parts(0, 1493)) - // 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()))) + // Measured: `1045 + c * (547 ±0)` + // Estimated: `9858 + c * (2699 ±0)` + // Minimum execution time: 48_052_000 picoseconds. + Weight::from_parts(49_029_000, 0) + .saturating_add(Weight::from_parts(0, 9858)) + // Standard Error: 81_306 + .saturating_add(Weight::from_parts(38_580_241, 0).saturating_mul(c.into())) + .saturating_add(T::DbWeight::get().reads(3)) + .saturating_add(T::DbWeight::get().reads((5_u64).saturating_mul(c.into()))) .saturating_add(Weight::from_parts(0, 2699).saturating_mul(c.into())) } /// 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::ActorControlLocators` (r:128 w:0) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorUnsignaledControlCells` (r:128 w:0) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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`) + /// Storage: `Actors::ActorReadyTail` (r:1 w:0) + /// Proof: `Actors::ActorReadyTail` (`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_394_000 picoseconds. - Weight::from_parts(43_721_000, 0) + // Measured: `1041 + c * (564 ±0)` + // Estimated: `1493 + c * (2861 ±0)` + // Minimum execution time: 42_953_000 picoseconds. + Weight::from_parts(21_381_679, 0) .saturating_add(Weight::from_parts(0, 1493)) - // Standard Error: 24_923 - .saturating_add(Weight::from_parts(23_827_571, 0).saturating_mul(c.into())) + // Standard Error: 121_122 + .saturating_add(Weight::from_parts(25_141_280, 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())) + .saturating_add(T::DbWeight::get().reads((6_u64).saturating_mul(c.into()))) + .saturating_add(Weight::from_parts(0, 2861).saturating_mul(c.into())) } /// 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::ActorControlLocators` (r:128 w:0) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorUnsignaledControlCells` (r:128 w:0) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:128 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, 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: 38_762_000 picoseconds. - Weight::from_parts(40_718_000, 0) + // Measured: `949 + c * (564 ±0)` + // Estimated: `990 + c * (2861 ±0)` + // Minimum execution time: 37_924_000 picoseconds. + Weight::from_parts(31_379_277, 0) .saturating_add(Weight::from_parts(0, 990)) - // 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())) + // Standard Error: 117_855 + .saturating_add(Weight::from_parts(23_988_770, 0).saturating_mul(c.into())) + .saturating_add(T::DbWeight::get().reads((6_u64).saturating_mul(c.into()))) + .saturating_add(Weight::from_parts(0, 2861).saturating_mul(c.into())) } /// 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::ActorControlLocators` (r:128 w:0) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:4 w:0) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:128 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, 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: 40_439_000 picoseconds. - Weight::from_parts(40_928_000, 0) - .saturating_add(Weight::from_parts(0, 990)) - // 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()))) + // Measured: `953 + c * (547 ±0)` + // Estimated: `9858 + c * (2699 ±0)` + // Minimum execution time: 43_093_000 picoseconds. + Weight::from_parts(55_735_000, 0) + .saturating_add(Weight::from_parts(0, 9858)) + // Standard Error: 78_586 + .saturating_add(Weight::from_parts(36_930_066, 0).saturating_mul(c.into())) + .saturating_add(T::DbWeight::get().reads(1)) + .saturating_add(T::DbWeight::get().reads((5_u64).saturating_mul(c.into()))) .saturating_add(Weight::from_parts(0, 2699).saturating_mul(c.into())) } /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:0) @@ -4499,24 +4658,22 @@ impl pallet_deos_actors::WeightInfo for S /// 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(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::ActorControlLocators` (r:2 w:0) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:1 w:0) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:2 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) fn crossing_rearm_pair_probe() -> Weight { // Proof Size summary in bytes: - // 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)) + // Measured: `2512` + // Estimated: `15106` + // Minimum execution time: 90_097_000 picoseconds. + Weight::from_parts(101_690_000, 0) + .saturating_add(Weight::from_parts(0, 15106)) + .saturating_add(T::DbWeight::get().reads(16)) } /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:0) /// Proof: `Actors::CrossingPendingFeedListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) @@ -4532,24 +4689,22 @@ impl pallet_deos_actors::WeightInfo for S /// 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(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::ActorControlLocators` (r:2 w:0) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorUnsignaledControlCells` (r:2 w:0) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorRunHead` (r:2 w:0) + /// Proof: `Actors::ActorRunHead` (`max_values`: None, `max_size`: Some(224), added: 2699, mode: `MaxEncodedLen`) fn crossing_skip_pair_probe() -> Weight { // Proof Size summary in bytes: - // Measured: `2960` + // Measured: `2520` // Estimated: `11729` - // Minimum execution time: 86_116_000 picoseconds. - Weight::from_parts(89_188_000, 0) + // Minimum execution time: 84_021_000 picoseconds. + Weight::from_parts(89_119_000, 0) .saturating_add(Weight::from_parts(0, 11729)) - .saturating_add(T::DbWeight::get().reads(19)) + .saturating_add(T::DbWeight::get().reads(17)) } /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:1) /// Proof: `Actors::CrossingPendingFeedListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) @@ -4563,10 +4718,10 @@ 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: `1307` + // Measured: `1369` // Estimated: `6636` - // Minimum execution time: 31_289_000 picoseconds. - Weight::from_parts(33_105_000, 0) + // Minimum execution time: 32_896_000 picoseconds. + Weight::from_parts(46_306_000, 0) .saturating_add(Weight::from_parts(0, 6636)) .saturating_add(T::DbWeight::get().reads(5)) .saturating_add(T::DbWeight::get().writes(2)) @@ -4587,44 +4742,44 @@ impl pallet_deos_actors::WeightInfo for S /// 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(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::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, 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::ActorUnsignaledControlCells` (r:1 w:1) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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(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(1491), added: 3966, 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::ActorReadyHead` (r:1 w:0) + /// Proof: `Actors::ActorReadyHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyTail` (r:1 w:1) + /// Proof: `Actors::ActorReadyTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyOccupancy` (r:1 w:1) + /// Proof: `Actors::ActorReadyOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, 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: `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) - /// 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::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: `4751` + // Measured: `4696` // Estimated: `162782` - // Minimum execution time: 530_872_000 picoseconds. - Weight::from_parts(553_082_000, 0) + // Minimum execution time: 660_989_000 picoseconds. + Weight::from_parts(741_098_000, 0) .saturating_add(Weight::from_parts(0, 162782)) .saturating_add(T::DbWeight::get().reads(90)) .saturating_add(T::DbWeight::get().writes(81)) @@ -4645,44 +4800,44 @@ impl pallet_deos_actors::WeightInfo for S /// 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(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::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, 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::ActorUnsignaledControlCells` (r:1 w:1) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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(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(1491), added: 3966, 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::ActorReadyHead` (r:1 w:0) + /// Proof: `Actors::ActorReadyHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyTail` (r:1 w:1) + /// Proof: `Actors::ActorReadyTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyOccupancy` (r:1 w:1) + /// Proof: `Actors::ActorReadyOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, 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: `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) - /// 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::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: `4751` + // Measured: `4696` // Estimated: `162782` - // Minimum execution time: 531_710_000 picoseconds. - Weight::from_parts(553_710_000, 0) + // Minimum execution time: 643_668_000 picoseconds. + Weight::from_parts(713_021_000, 0) .saturating_add(Weight::from_parts(0, 162782)) .saturating_add(T::DbWeight::get().reads(90)) .saturating_add(T::DbWeight::get().writes(81)) @@ -4703,14 +4858,20 @@ impl pallet_deos_actors::WeightInfo for S /// 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(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::ActorControlLocators` (r:1 w:0) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, 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::ActorReadyFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, 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(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(1491), added: 3966, 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) @@ -4719,12 +4880,12 @@ 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: `4436` + // Measured: `4352` // Estimated: `162782` - // Minimum execution time: 405_016_000 picoseconds. - Weight::from_parts(420_591_000, 0) + // Minimum execution time: 482_332_000 picoseconds. + Weight::from_parts(537_717_000, 0) .saturating_add(Weight::from_parts(0, 162782)) - .saturating_add(T::DbWeight::get().reads(80)) + .saturating_add(T::DbWeight::get().reads(83)) .saturating_add(T::DbWeight::get().writes(74)) } /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:1) @@ -4741,14 +4902,20 @@ impl pallet_deos_actors::WeightInfo for S /// 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(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::ActorControlLocators` (r:2 w:0) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, 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::ActorReadyFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, 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::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::ActorRunPayload` (r:2 w:0) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(1491), added: 3966, 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) @@ -4759,13 +4926,13 @@ 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: `5317` + // Measured: `5235` // Estimated: `162782` - // Minimum execution time: 456_420_000 picoseconds. - Weight::from_parts(472_623_000, 0) + // Minimum execution time: 596_664_000 picoseconds. + Weight::from_parts(667_764_000, 0) .saturating_add(Weight::from_parts(0, 162782)) - .saturating_add(T::DbWeight::get().reads(86)) - .saturating_add(T::DbWeight::get().writes(76)) + .saturating_add(T::DbWeight::get().reads(91)) + .saturating_add(T::DbWeight::get().writes(75)) } /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:1) /// Proof: `Actors::CrossingPendingFeedListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) @@ -4783,22 +4950,24 @@ impl pallet_deos_actors::WeightInfo for S /// 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(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::ActorControlLocators` (r:1 w:0) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, 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::ActorReadyFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, 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(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(1491), added: 3966, 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::ActorReadyTail` (r:1 w:0) + /// Proof: `Actors::ActorReadyTail` (`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) @@ -4807,12 +4976,12 @@ impl pallet_deos_actors::WeightInfo for S /// 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: `4529` + // Measured: `4406` // Estimated: `162782` - // Minimum execution time: 423_943_000 picoseconds. - Weight::from_parts(439_728_000, 0) + // Minimum execution time: 525_564_000 picoseconds. + Weight::from_parts(575_362_000, 0) .saturating_add(Weight::from_parts(0, 162782)) - .saturating_add(T::DbWeight::get().reads(84)) + .saturating_add(T::DbWeight::get().reads(85)) .saturating_add(T::DbWeight::get().writes(74)) } /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:1) @@ -4829,22 +4998,24 @@ impl pallet_deos_actors::WeightInfo for S /// 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(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::ActorControlLocators` (r:2 w:0) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, 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::ActorReadyFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, 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::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::ActorRunPayload` (r:2 w:0) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(1491), added: 3966, 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::ActorReadyTail` (r:1 w:0) + /// Proof: `Actors::ActorReadyTail` (`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) @@ -4855,13 +5026,13 @@ impl pallet_deos_actors::WeightInfo for S /// 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: `5442` + // Measured: `5289` // Estimated: `162782` - // Minimum execution time: 499_862_000 picoseconds. - Weight::from_parts(521_862_000, 0) + // Minimum execution time: 700_519_000 picoseconds. + Weight::from_parts(748_851_000, 0) .saturating_add(Weight::from_parts(0, 162782)) - .saturating_add(T::DbWeight::get().reads(92)) - .saturating_add(T::DbWeight::get().writes(76)) + .saturating_add(T::DbWeight::get().reads(93)) + .saturating_add(T::DbWeight::get().writes(75)) } /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:1) /// Proof: `Actors::CrossingPendingFeedListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) @@ -4879,44 +5050,44 @@ impl pallet_deos_actors::WeightInfo for S /// 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(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::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, 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::ActorUnsignaledControlCells` (r:1 w:1) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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(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(1491), added: 3966, 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::ActorReadyHead` (r:1 w:0) + /// Proof: `Actors::ActorReadyHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyTail` (r:1 w:1) + /// Proof: `Actors::ActorReadyTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyOccupancy` (r:1 w:1) + /// Proof: `Actors::ActorReadyOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, 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: `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) - /// 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::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: `4751` + // Measured: `4696` // Estimated: `162782` - // Minimum execution time: 534_154_000 picoseconds. - Weight::from_parts(555_946_000, 0) + // Minimum execution time: 664_411_000 picoseconds. + Weight::from_parts(718_679_000, 0) .saturating_add(Weight::from_parts(0, 162782)) .saturating_add(T::DbWeight::get().reads(90)) .saturating_add(T::DbWeight::get().writes(81)) @@ -4935,28 +5106,28 @@ impl pallet_deos_actors::WeightInfo for S /// Proof: `Actors::CrossingMemberships` (`max_values`: None, `max_size`: Some(74), added: 2549, 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::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::ActorControlLocators` (r:2 w:2) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorUnsignaledControlCells` (r:2 w:2) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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: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: `Actors::ActorReadyTail` (r:1 w:1) + /// Proof: `Actors::ActorReadyTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyHead` (r:1 w:0) + /// Proof: `Actors::ActorReadyHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyOccupancy` (r:1 w:1) + /// Proof: `Actors::ActorReadyOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, 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::ActorFunding` (r:2 w:0) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, 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) @@ -4969,13 +5140,13 @@ impl pallet_deos_actors::WeightInfo for S /// 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: `5231` + // Measured: `5745` // Estimated: `162782` - // Minimum execution time: 562_860_000 picoseconds. - Weight::from_parts(586_537_000, 0) + // Minimum execution time: 731_041_000 picoseconds. + Weight::from_parts(794_527_000, 0) .saturating_add(Weight::from_parts(0, 162782)) - .saturating_add(T::DbWeight::get().reads(97)) - .saturating_add(T::DbWeight::get().writes(85)) + .saturating_add(T::DbWeight::get().reads(98)) + .saturating_add(T::DbWeight::get().writes(86)) } /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:1) /// Proof: `Actors::CrossingPendingFeedListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) @@ -4991,28 +5162,28 @@ impl pallet_deos_actors::WeightInfo for S /// 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::ActorControlLocators` (r:128 w:128) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorUnsignaledControlCells` (r:128 w:128) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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: `Actors::ActorReadyTail` (r:1 w:1) + /// Proof: `Actors::ActorReadyTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyHead` (r:1 w:0) + /// Proof: `Actors::ActorReadyHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyOccupancy` (r:1 w:1) + /// Proof: `Actors::ActorReadyOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:4 w:4) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, 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::ActorFunding` (r:128 w:0) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, 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) @@ -5025,13 +5196,13 @@ impl pallet_deos_actors::WeightInfo for S /// 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_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)) + // Measured: `119443` + // Estimated: `608478` + // Minimum execution time: 19_626_837_000 picoseconds. + Weight::from_parts(20_066_635_000, 0) + .saturating_add(Weight::from_parts(0, 608478)) + .saturating_add(T::DbWeight::get().reads(1111)) + .saturating_add(T::DbWeight::get().writes(595)) } /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:1) /// Proof: `Actors::CrossingPendingFeedListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) @@ -5047,28 +5218,28 @@ impl pallet_deos_actors::WeightInfo for S /// 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::ActorControlLocators` (r:64 w:64) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorUnsignaledControlCells` (r:64 w:64) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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: `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: `Actors::ActorReadyTail` (r:1 w:1) + /// Proof: `Actors::ActorReadyTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyHead` (r:1 w:0) + /// Proof: `Actors::ActorReadyHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyOccupancy` (r:1 w:1) + /// Proof: `Actors::ActorReadyOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:2 w:2) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, 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::ActorFunding` (r:64 w:0) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, 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) @@ -5081,13 +5252,13 @@ impl pallet_deos_actors::WeightInfo for S /// 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` + // Measured: `93319` // Estimated: `327262` - // Minimum execution time: 3_982_833_000 picoseconds. - Weight::from_parts(4_094_371_000, 0) + // Minimum execution time: 8_970_560_000 picoseconds. + Weight::from_parts(9_507_020_000, 0) .saturating_add(Weight::from_parts(0, 327262)) - .saturating_add(T::DbWeight::get().reads(563)) - .saturating_add(T::DbWeight::get().writes(367)) + .saturating_add(T::DbWeight::get().reads(627)) + .saturating_add(T::DbWeight::get().writes(431)) } /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:1) /// Proof: `Actors::CrossingPendingFeedListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) @@ -5103,28 +5274,28 @@ impl pallet_deos_actors::WeightInfo for S /// 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::ActorControlLocators` (r:64 w:64) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorUnsignaledControlCells` (r:64 w:64) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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: `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: `Actors::ActorReadyTail` (r:1 w:1) + /// Proof: `Actors::ActorReadyTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyHead` (r:1 w:0) + /// Proof: `Actors::ActorReadyHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyOccupancy` (r:1 w:1) + /// Proof: `Actors::ActorReadyOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:2 w:2) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, 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::ActorFunding` (r:64 w:0) + /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, 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) @@ -5137,13 +5308,13 @@ impl pallet_deos_actors::WeightInfo for S /// 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` + // Measured: `93647` // Estimated: `327262` - // Minimum execution time: 3_979_340_000 picoseconds. - Weight::from_parts(4_067_622_000, 0) + // Minimum execution time: 9_114_506_000 picoseconds. + Weight::from_parts(9_394_853_000, 0) .saturating_add(Weight::from_parts(0, 327262)) - .saturating_add(T::DbWeight::get().reads(563)) - .saturating_add(T::DbWeight::get().writes(369)) + .saturating_add(T::DbWeight::get().reads(627)) + .saturating_add(T::DbWeight::get().writes(433)) } /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:1) /// Proof: `Actors::CrossingPendingFeedListState` (`max_values`: Some(1), `max_size`: Some(58), added: 553, mode: `MaxEncodedLen`) @@ -5161,18 +5332,30 @@ impl pallet_deos_actors::WeightInfo for S /// 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(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorControlLocators` (r:1 w:0) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, 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::ActorUnsignaledControlCells` (r:1 w:0) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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(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(1491), added: 3966, 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: `3116` + // Measured: `4270` // Estimated: `81886` - // Minimum execution time: 164_409_000 picoseconds. - Weight::from_parts(168_390_000, 0) + // Minimum execution time: 213_858_000 picoseconds. + Weight::from_parts(235_090_000, 0) .saturating_add(Weight::from_parts(0, 81886)) - .saturating_add(T::DbWeight::get().reads(41)) + .saturating_add(T::DbWeight::get().reads(47)) .saturating_add(T::DbWeight::get().writes(2)) } /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:1) @@ -5189,18 +5372,30 @@ impl pallet_deos_actors::WeightInfo for S /// 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(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorControlLocators` (r:2 w:0) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, 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::ActorUnsignaledControlCells` (r:2 w:0) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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::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::ActorRunPayload` (r:2 w:0) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(1491), added: 3966, 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: `1618` + // Measured: `3408` // Estimated: `11729` - // Minimum execution time: 73_334_000 picoseconds. - Weight::from_parts(77_665_000, 0) + // Minimum execution time: 158_682_000 picoseconds. + Weight::from_parts(195_559_000, 0) .saturating_add(Weight::from_parts(0, 11729)) - .saturating_add(T::DbWeight::get().reads(12)) + .saturating_add(T::DbWeight::get().reads(24)) .saturating_add(T::DbWeight::get().writes(2)) } /// Storage: `Actors::CrossingPendingFeedListState` (r:1 w:1) @@ -5219,34 +5414,34 @@ impl pallet_deos_actors::WeightInfo for S /// 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(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, 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::ActorUnsignaledControlCells` (r:1 w:1) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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::ActorRunPayload` (r:1 w:1) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(1491), added: 3966, 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::ActorReadyHead` (r:1 w:0) + /// Proof: `Actors::ActorReadyHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyTail` (r:1 w:0) + /// Proof: `Actors::ActorReadyTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyOccupancy` (r:1 w:0) + /// Proof: `Actors::ActorReadyOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:1 w:0) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, 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::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:1 w:0) - /// 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) @@ -5267,81 +5462,73 @@ impl pallet_deos_actors::WeightInfo for S /// 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: `5191` + // Measured: `5235` // Estimated: `162782` - // Minimum execution time: 706_246_000 picoseconds. - Weight::from_parts(730_831_000, 0) + // Minimum execution time: 788_800_000 picoseconds. + Weight::from_parts(877_570_000, 0) .saturating_add(Weight::from_parts(0, 162782)) .saturating_add(T::DbWeight::get().reads(97)) - .saturating_add(T::DbWeight::get().writes(91)) + .saturating_add(T::DbWeight::get().writes(90)) } /// 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: `779` + // Measured: `878` // Estimated: `6052` - // Minimum execution time: 11_804_000 picoseconds. - Weight::from_parts(13_410_000, 0) + // Minimum execution time: 12_921_000 picoseconds. + Weight::from_parts(14_667_000, 0) .saturating_add(Weight::from_parts(0, 6052)) .saturating_add(T::DbWeight::get().reads(2)) } /// Storage: `Actors::SovereignIndex` (r:1 w:0) /// Proof: `Actors::SovereignIndex` (`max_values`: None, `max_size`: Some(56), added: 2531, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorControlLocators` (r:1 w:0) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, 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(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorWaitingFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorWaitingFrameChunks` (`max_values`: None, `max_size`: Some(11708), added: 14183, 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::ActorContractTailChunk` (r:3 w:0) + /// 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(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`) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(1491), added: 3966, 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) - /// 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:1) - /// 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: `4914` - // Estimated: `5998` - // Minimum execution time: 274_899_000 picoseconds. - Weight::from_parts(290_055_000, 0) - .saturating_add(Weight::from_parts(0, 5998)) + // Measured: `4895` + // Estimated: `20625` + // Minimum execution time: 388_464_000 picoseconds. + Weight::from_parts(473_182_000, 0) + .saturating_add(Weight::from_parts(0, 20625)) .saturating_add(T::DbWeight::get().reads(13)) - .saturating_add(T::DbWeight::get().writes(6)) + .saturating_add(T::DbWeight::get().writes(2)) } /// Storage: `Actors::ActorFunding` (r:1 w:1) /// Proof: `Actors::ActorFunding` (`max_values`: None, `max_size`: Some(1066), added: 3541, mode: `MaxEncodedLen`) - /// The range of component `a` is `[1, 10]`. + /// The range of component `a` is `[1, 24]`. fn funding_snapshot_open(a: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `566 + a * (21 ±0)` + // Measured: `631 + a * (26 ±0)` // Estimated: `4531` - // Minimum execution time: 11_593_000 picoseconds. - Weight::from_parts(13_240_086, 0) + // Minimum execution time: 12_781_000 picoseconds. + Weight::from_parts(15_152_818, 0) .saturating_add(Weight::from_parts(0, 4531)) - // Standard Error: 850 - .saturating_add(Weight::from_parts(122_803, 0).saturating_mul(a.into())) + // Standard Error: 15_336 + .saturating_add(Weight::from_parts(358_026, 0).saturating_mul(a.into())) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) } @@ -5395,8 +5582,8 @@ impl pallet_deos_actors::WeightInfo for S // 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) + // Minimum execution time: 195_117_085_000 picoseconds. + Weight::from_parts(204_517_874_000, 0) .saturating_add(Weight::from_parts(0, 3517)) .saturating_add(T::DbWeight::get().reads(13)) .saturating_add(T::DbWeight::get().writes(23)) @@ -5407,10 +5594,10 @@ impl pallet_deos_actors::WeightInfo for S /// 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` + // Measured: `477` // Estimated: `1560` - // Minimum execution time: 6_775_000 picoseconds. - Weight::from_parts(8_590_000, 0) + // Minimum execution time: 8_101_000 picoseconds. + Weight::from_parts(9_010_000, 0) .saturating_add(Weight::from_parts(0, 1560)) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(2)) @@ -5419,10 +5606,10 @@ impl pallet_deos_actors::WeightInfo for S /// 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` + // Measured: `467` // Estimated: `1560` - // Minimum execution time: 8_940_000 picoseconds. - Weight::from_parts(10_616_000, 0) + // Minimum execution time: 10_616_000 picoseconds. + Weight::from_parts(11_035_000, 0) .saturating_add(Weight::from_parts(0, 1560)) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) @@ -5437,8 +5624,8 @@ impl pallet_deos_actors::WeightInfo for S // Proof Size summary in bytes: // Measured: `0` // Estimated: `248490` - // Minimum execution time: 431_905_000 picoseconds. - Weight::from_parts(446_572_000, 0) + // Minimum execution time: 444_198_000 picoseconds. + Weight::from_parts(465_709_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 49edd02e..e783b64c 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-30, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2026-09-07, 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 @@ -54,30 +54,30 @@ impl pallet_deos_router::WeightInfo for S /// Proof: `Oracle::Observations` (`max_values`: None, `max_size`: Some(61), added: 2536, mode: `MaxEncodedLen`) /// Storage: `Actors::SovereignIndex` (r:1 w:0) /// Proof: `Actors::SovereignIndex` (`max_values`: None, `max_size`: Some(56), added: 2531, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, 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(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorUnsignaledControlCells` (r:1 w:1) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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: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(1491), added: 3966, 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) - /// 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`) + /// Storage: `Actors::ActorReadyHead` (r:1 w:0) + /// Proof: `Actors::ActorReadyHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyTail` (r:1 w:1) + /// Proof: `Actors::ActorReadyTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyOccupancy` (r:1 w:1) + /// Proof: `Actors::ActorReadyOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, mode: `MaxEncodedLen`) /// 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) @@ -96,11 +96,11 @@ impl pallet_deos_router::WeightInfo for S /// 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: `3756` - // Estimated: `9635` - // Minimum execution time: 358_641_000 picoseconds. - Weight::from_parts(389_092_000, 0) - .saturating_add(Weight::from_parts(0, 9635)) + // Measured: `3870` + // Estimated: `15106` + // Minimum execution time: 434_629_000 picoseconds. + Weight::from_parts(495_601_000, 0) + .saturating_add(Weight::from_parts(0, 15106)) .saturating_add(T::DbWeight::get().reads(31)) .saturating_add(T::DbWeight::get().writes(18)) } @@ -116,38 +116,42 @@ impl pallet_deos_router::WeightInfo for S /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) /// Storage: `Actors::SovereignIndex` (r:2 w:0) /// Proof: `Actors::SovereignIndex` (`max_values`: None, `max_size`: Some(56), added: 2531, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorControlLocators` (r:2 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, 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:1) - /// Proof: `Actors::ActorHot` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorUnsignaledControlCells` (r:2 w:1) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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(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::ActorRunPayload` (r:2 w:0) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(1491), added: 3966, 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) - /// 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`) + /// Storage: `Actors::ActorReadyHead` (r:1 w:0) + /// Proof: `Actors::ActorReadyHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyTail` (r:1 w:1) + /// Proof: `Actors::ActorReadyTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyOccupancy` (r:1 w:1) + /// Proof: `Actors::ActorReadyOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, 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::ActorContractTailChunk` (r:1 w:0) + /// Proof: `Actors::ActorContractTailChunk` (`max_values`: None, `max_size`: Some(4070), added: 6545, mode: `MaxEncodedLen`) fn direct_mint_exact_input() -> Weight { // Proof Size summary in bytes: - // Measured: `3715` + // Measured: `3864` // Estimated: `21862` - // Minimum execution time: 368_558_000 picoseconds. - Weight::from_parts(390_349_000, 0) + // Minimum execution time: 510_967_000 picoseconds. + Weight::from_parts(561_603_000, 0) .saturating_add(Weight::from_parts(0, 21862)) - .saturating_add(T::DbWeight::get().reads(34)) + .saturating_add(T::DbWeight::get().reads(38)) .saturating_add(T::DbWeight::get().writes(14)) } /// Storage: `DeosRouter::RouterFee` (r:1 w:0) @@ -166,30 +170,30 @@ impl pallet_deos_router::WeightInfo for S /// Proof: `Oracle::Observations` (`max_values`: None, `max_size`: Some(61), added: 2536, mode: `MaxEncodedLen`) /// Storage: `Actors::SovereignIndex` (r:1 w:0) /// Proof: `Actors::SovereignIndex` (`max_values`: None, `max_size`: Some(56), added: 2531, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, 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(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorUnsignaledControlCells` (r:1 w:1) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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: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(1491), added: 3966, 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) - /// 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`) + /// Storage: `Actors::ActorReadyHead` (r:1 w:0) + /// Proof: `Actors::ActorReadyHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyTail` (r:1 w:1) + /// Proof: `Actors::ActorReadyTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyOccupancy` (r:1 w:1) + /// Proof: `Actors::ActorReadyOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, mode: `MaxEncodedLen`) /// Storage: `Actors::ObservationSubscriberCount` (r:2 w:0) /// Proof: `Actors::ObservationSubscriberCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) /// Storage: `Actors::ObservationIngressRevisions` (r:2 w:2) @@ -208,10 +212,10 @@ impl pallet_deos_router::WeightInfo for S /// 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: `4728` + // Measured: `4842` // Estimated: `19253` - // Minimum execution time: 508_942_000 picoseconds. - Weight::from_parts(562_930_000, 0) + // Minimum execution time: 585_838_000 picoseconds. + Weight::from_parts(646_251_000, 0) .saturating_add(Weight::from_parts(0, 19253)) .saturating_add(T::DbWeight::get().reads(45)) .saturating_add(T::DbWeight::get().writes(27)) @@ -230,30 +234,30 @@ impl pallet_deos_router::WeightInfo for S /// Proof: `Oracle::Observations` (`max_values`: None, `max_size`: Some(61), added: 2536, mode: `MaxEncodedLen`) /// Storage: `Actors::SovereignIndex` (r:1 w:0) /// Proof: `Actors::SovereignIndex` (`max_values`: None, `max_size`: Some(56), added: 2531, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, 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(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorUnsignaledControlCells` (r:1 w:1) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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: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(1491), added: 3966, 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) - /// 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`) + /// Storage: `Actors::ActorReadyHead` (r:1 w:0) + /// Proof: `Actors::ActorReadyHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyTail` (r:1 w:1) + /// Proof: `Actors::ActorReadyTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyOccupancy` (r:1 w:1) + /// Proof: `Actors::ActorReadyOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, mode: `MaxEncodedLen`) /// 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) @@ -272,11 +276,11 @@ impl pallet_deos_router::WeightInfo for S /// 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: `3582` - // Estimated: `9635` - // Minimum execution time: 356_545_000 picoseconds. - Weight::from_parts(387_486_000, 0) - .saturating_add(Weight::from_parts(0, 9635)) + // Measured: `3696` + // Estimated: `15106` + // Minimum execution time: 427_017_000 picoseconds. + Weight::from_parts(481_842_000, 0) + .saturating_add(Weight::from_parts(0, 15106)) .saturating_add(T::DbWeight::get().reads(30)) .saturating_add(T::DbWeight::get().writes(18)) } @@ -294,30 +298,30 @@ impl pallet_deos_router::WeightInfo for S /// Proof: `Oracle::Observations` (`max_values`: None, `max_size`: Some(61), added: 2536, mode: `MaxEncodedLen`) /// Storage: `Actors::SovereignIndex` (r:1 w:0) /// Proof: `Actors::SovereignIndex` (`max_values`: None, `max_size`: Some(56), added: 2531, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorControlLocators` (r:1 w:1) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, 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(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorUnsignaledControlCells` (r:1 w:1) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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: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(1491), added: 3966, 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) - /// 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`) + /// Storage: `Actors::ActorReadyHead` (r:1 w:0) + /// Proof: `Actors::ActorReadyHead` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyTail` (r:1 w:1) + /// Proof: `Actors::ActorReadyTail` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyOccupancy` (r:1 w:1) + /// Proof: `Actors::ActorReadyOccupancy` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorReadyFrameChunks` (r:1 w:1) + /// Proof: `Actors::ActorReadyFrameChunks` (`max_values`: None, `max_size`: Some(11641), added: 14116, mode: `MaxEncodedLen`) /// Storage: `Actors::ObservationSubscriberCount` (r:2 w:0) /// Proof: `Actors::ObservationSubscriberCount` (`max_values`: None, `max_size`: Some(37), added: 2512, mode: `MaxEncodedLen`) /// Storage: `Actors::ObservationIngressRevisions` (r:2 w:2) @@ -336,10 +340,10 @@ impl pallet_deos_router::WeightInfo for S /// 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: `4441` + // Measured: `4555` // Estimated: `19253` - // Minimum execution time: 510_268_000 picoseconds. - Weight::from_parts(556_714_000, 0) + // Minimum execution time: 587_864_000 picoseconds. + Weight::from_parts(656_309_000, 0) .saturating_add(Weight::from_parts(0, 19253)) .saturating_add(T::DbWeight::get().reads(44)) .saturating_add(T::DbWeight::get().writes(27)) @@ -370,8 +374,8 @@ impl pallet_deos_router::WeightInfo for S // Proof Size summary in bytes: // Measured: `812` // Estimated: `34255` - // Minimum execution time: 140_313_000 picoseconds. - Weight::from_parts(144_923_000, 0) + // Minimum execution time: 141_570_000 picoseconds. + Weight::from_parts(151_628_000, 0) .saturating_add(Weight::from_parts(0, 34255)) .saturating_add(T::DbWeight::get().reads(13)) .saturating_add(T::DbWeight::get().writes(10)) @@ -382,8 +386,8 @@ impl pallet_deos_router::WeightInfo for S // Proof Size summary in bytes: // Measured: `4` // Estimated: `1489` - // Minimum execution time: 7_054_000 picoseconds. - Weight::from_parts(8_591_000, 0) + // Minimum execution time: 6_985_000 picoseconds. + Weight::from_parts(8_661_000, 0) .saturating_add(Weight::from_parts(0, 1489)) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) diff --git a/template/runtime/src/weights/pallet_tmc.rs b/template/runtime/src/weights/pallet_tmc.rs index b182a2f8..17fd5c66 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-26, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2026-09-07, 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,8 +46,8 @@ impl pallet_tmc::WeightInfo for Substrate // Proof Size summary in bytes: // Measured: `852` // Estimated: `6360` - // Minimum execution time: 24_655_000 picoseconds. - Weight::from_parts(26_680_000, 0) + // Minimum execution time: 24_235_000 picoseconds. + Weight::from_parts(26_610_000, 0) .saturating_add(Weight::from_parts(0, 6360)) .saturating_add(T::DbWeight::get().reads(3)) .saturating_add(T::DbWeight::get().writes(1)) @@ -58,28 +58,36 @@ impl pallet_tmc::WeightInfo for Substrate /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) /// Storage: `Actors::SovereignIndex` (r:1 w:0) /// Proof: `Actors::SovereignIndex` (`max_values`: None, `max_size`: Some(56), added: 2531, 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: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorControlLocators` (r:1 w:0) + /// Proof: `Actors::ActorControlLocators` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorUnsignaledControlCells` (r:1 w:0) + /// Proof: `Actors::ActorUnsignaledControlCells` (`max_values`: None, `max_size`: Some(386), added: 2861, 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(1082), added: 3557, mode: `MaxEncodedLen`) + /// Proof: `Actors::ActorContractHead` (`max_values`: None, `max_size`: Some(2271), added: 4746, 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::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: `Actors::ActorRunPayload` (r:1 w:0) + /// Proof: `Actors::ActorRunPayload` (`max_values`: None, `max_size`: Some(1491), added: 3966, 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::ActorContractTailChunk` (r:1 w:0) + /// Proof: `Actors::ActorContractTailChunk` (`max_values`: None, `max_size`: Some(4070), added: 6545, mode: `MaxEncodedLen`) + /// Storage: `Actors::ActorIdentities` (r:1 w:0) + /// Proof: `Actors::ActorIdentities` (`max_values`: None, `max_size`: Some(110), added: 2585, 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: `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)) + // Measured: `2549` + // Estimated: `7535` + // Minimum execution time: 243_331_000 picoseconds. + Weight::from_parts(254_994_000, 0) + .saturating_add(Weight::from_parts(0, 7535)) + .saturating_add(T::DbWeight::get().reads(15)) .saturating_add(T::DbWeight::get().writes(4)) } } diff --git a/web-client/.papi/descriptors/package.json b/web-client/.papi/descriptors/package.json index 54801a33..4002421d 100644 --- a/web-client/.papi/descriptors/package.json +++ b/web-client/.papi/descriptors/package.json @@ -1,5 +1,5 @@ { - "version": "0.1.0-autogenerated.16302145885248040146", + "version": "0.1.0-autogenerated.17618355743500111702", "name": "@polkadot-api/descriptors", "files": [ "dist" diff --git a/web-client/.papi/metadata/deos.scale b/web-client/.papi/metadata/deos.scale index 2cc742b5..c228504a 100644 Binary files a/web-client/.papi/metadata/deos.scale and b/web-client/.papi/metadata/deos.scale differ diff --git a/web-client/docs/architecture.en.md b/web-client/docs/architecture.en.md index f49c454d..503479a9 100644 --- a/web-client/docs/architecture.en.md +++ b/web-client/docs/architecture.en.md @@ -74,7 +74,9 @@ The browser realization axis is separate: A session-built chart or retained UI panel must not masquerade as archive truth. A future archive/search/dashboard surface must declare its materialized provider boundary explicitly. `ReadModelValue.fetchedAt` is only a browser observation timestamp for cache/session freshness; canonical chain time or finality must come from bounded chain facts such as `asOfBlock` / `asOfHash` when those facts matter. -The automation widget reads known System actors from `Actors.ActorIdentities`, `Actors.ActorHot`, compact `Actors.ActorContractHead`, sparse `Actors.ActorRunHead`, and `Actors.ActorFunding` at one finalized block. It does not reconstruct physical Contract tails or Run payloads. Authoring permits `0..=MaxContractSteps`; deleting the final Step exposes an explicit Opening-only state rather than fabricating an Action. Current cursor and unsuccessful-attempt count at that cursor are canonical-chain truth; no cycle-global attempt ordinal exists. Historical attempt timelines remain materialized. The automation authoring, analysis, and local simulation contracts derive `MaxContractSteps` and `MaxRetryAttempts` from the generated Actors ABI manifest; RetryLater remains Mutable-only and does not fabricate adapter retryability. +The automation widget resolves each known System Actor through one bounded `ActorControlLocators` lookup and exactly one matching Unsignaled, Ready-chunk, or Waiting-page control cell at one finalized block, then reads compact `ActorContractHead`, sparse `ActorRunHead`, and `ActorFunding`. A missing locator plus dormant identity is Dormant, a missing locator and identity is NotRegistered, and split or mismatched authority fails as corruption rather than absence. It does not reconstruct physical Contract tails or Run payloads. + +Authoring permits `0..=MaxContractSteps`; deleting the final Step exposes an explicit Opening-only state rather than fabricating an Action. Current cursor and unsuccessful-attempt count at that cursor are canonical-chain truth; no cycle-global attempt ordinal exists. Historical attempt timelines remain materialized. The automation authoring, analysis, and local simulation contracts derive `MaxContractSteps` and `MaxRetryAttempts` from the generated Actors ABI manifest; RetryLater remains Mutable-only and does not fabricate adapter retryability. Actors exposes no permanent cache epoch or generic revalidation progress surface. Weight-, adapter-, or envelope-affecting upgrades use their concrete migration contract and finite semantic Weight-class proof rather than client-visible per-actor cache repair state. @@ -86,7 +88,7 @@ The widget also consumes the read-only `ActorEligibilityApi::actor_eligibility` `automation/cost.ts` owns the independent browser contract for `ActorCostApi` and `ActionFeeCharged`: Creation, family-specific Trigger, Pipeline Machine/cleanup, maximum next Action, actual Action receipt, and state hold retain named fields and identities without an activation total or remaining machine budget. `adapters/blockchain/actor-cost.ts` pins the typed API call to the caller's finalized block; unknown variants, malformed identities, inconsistent Pipeline/hold totals, and transport failure remain explicit unavailability. -`automation/cost-vectors.ts` validates runtime-generated `actors-cost-vectors.json` against exact metadata and Actors Weight hashes. The retained fixture matrix covers Manual `0/1/4/8/32` geometry, all six one-Step Trigger families, dormant User absence, explicit System exemption, Pipeline/hold totals, current Action locality, and cold-tail state-hold growth. No visible cost panel is shipped yet. +`automation/cost-vectors.ts` validates runtime-generated `actors-cost-vectors.json` against exact metadata and Actors Weight hashes. The retained fixture matrix covers Manual `0/1/4/8/12` geometry, all six one-Step Trigger families, dormant User absence, explicit System exemption, Pipeline/hold totals, current Action locality, and cold-tail state-hold growth. No visible cost panel is shipped yet. The automation domain validates metadata-bound Actor Contract artifacts. It discovers `ActorContract`, `ActorType`, and `Mutability` from exact runtime metadata, requires SCALE decode/re-encode equality, derives deterministic `contractId`, produces lossless JSON-safe projections, and classifies cross-genesis or cross-metadata diffs as incompatible until explicit rebinding. @@ -158,9 +160,9 @@ Matching-Wasm response validation accepts the runtime's bounded `Completed`, `Ab Actors call composition discovers the pallet and outer `RuntimeCall` from the artifact metadata, then exposes exact SCALE bytes, hash, `contractId`, runtime identity, and required origin. User calls remain direct owner-signed actions. Root-required System calls report `UnsupportedActorRootCall`: current strategic `L1RootAction` decodes only the dedicated runtime-upgrade payload, so call-byte composition does not imply governance admission. -The matching-Wasm trust gate hashes supplied runtime code and binds it to artifact metadata, runtime versions, finalized block/state identity, runtime API identity, actor id, mode, and `contractId`. Its metadata-discovered codec requires exact `ActorSimulationApi` version/signature, canonical SCALE round trips, typed success including `Closed(CloseReason)`, bounded ordered step evidence including `Stopped`, cursor-local unsuccessful-attempt projection, and equality between provider summary and runtime bytes. +The matching-Wasm trust gate hashes supplied runtime code and binds it to artifact metadata, runtime versions, finalized block/state identity, `ActorSimulationApi` v2 identity, explicit synthetic Actor Control/Shared Economic budget, actor id, mode, and `contractId`. Its metadata-discovered codec requires the exact six-input signature, canonical SCALE round trips, typed success including `Continued` and `Closed(CloseReason)`, at most one ordered Step record including `Stopped`, cursor-local unsuccessful-attempt projection, and equality between provider summary and runtime bytes. -The DEOS simulation adapter selects the current finalized hash or accepts an explicitly identified finalized fixture block, reads its header state root, V16 metadata, runtime version, genesis identity, and `:code`, and calls the typed simulation API at that same hash without submission. Local Omni Node evidence covers exact-plan rejection, successful fresh execution, and a stored Continuation attempt. The remote node remains a trusted provider: pin equality prevents drift but does not independently verify Wasm execution or state correctness. +The DEOS simulation adapter selects the current finalized hash or accepts an explicitly identified finalized fixture block, reads its header state root, V16 metadata, runtime version, genesis identity, and `:code`, and calls the typed simulation API at that same hash without submission. The caller supplies explicit synthetic Actor Control and Shared Economic Weight components; the request/response pin preserves them and never presents them as production capacity. Local Omni Node evidence covers exact-plan rejection, successful fresh execution, and a stored Continuation attempt. The remote node remains a trusted provider: pin equality prevents drift but does not independently verify Wasm execution or state correctness. ## 4. Domain Ownership @@ -267,7 +269,7 @@ The widget consumes generated metadata: - `_meta/state.json` for explicit status and provenance; - `_meta/locales.json` for locale/page discovery. -The wiki reader should keep page content primary and show related context/provenance as supporting information. +The wiki reader keeps established facts and Wiki-to-Wiki navigation primary. Source-document provenance remains non-rendered metadata for validation, not a source-path panel or article navigation into repository documentation. Concrete non-document resources may remain linked. ## 10. Validation diff --git a/web-client/package-lock.json b/web-client/package-lock.json index e08cb982..28f3a861 100644 --- a/web-client/package-lock.json +++ b/web-client/package-lock.json @@ -1,12 +1,12 @@ { "name": "web-client", - "version": "0.7.24", + "version": "0.7.25", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "web-client", - "version": "0.7.24", + "version": "0.7.25", "dependencies": { "@polkadot-api/descriptors": "file:.papi/descriptors", "@polkadot-api/metadata-builders": "^0.14.3", @@ -39,7 +39,7 @@ }, ".papi/descriptors": { "name": "@polkadot-api/descriptors", - "version": "0.1.0-autogenerated.16302145885248040146", + "version": "0.1.0-autogenerated.17618355743500111702", "peerDependencies": { "polkadot-api": ">=2.0.0" } diff --git a/web-client/package.json b/web-client/package.json index 1ad7a60d..8d1003d9 100644 --- a/web-client/package.json +++ b/web-client/package.json @@ -1,7 +1,7 @@ { "name": "web-client", "private": true, - "version": "0.7.24", + "version": "0.7.25", "type": "module", "packageManager": "npm@11.7.0", "volta": { diff --git a/web-client/scripts/network-composed-path.mjs b/web-client/scripts/network-composed-path.mjs index 5157107b..7ec681e3 100755 --- a/web-client/scripts/network-composed-path.mjs +++ b/web-client/scripts/network-composed-path.mjs @@ -13,6 +13,9 @@ import { cryptoWaitReady } from '@polkadot/util-crypto'; import { Enum as PapiEnum } from 'polkadot-api'; import { createWsClient } from 'polkadot-api/ws'; +import { readActorControlProjection } from '../src/lib/adapters/blockchain/actor-control.ts'; +import { deriveSystemActorSovereignAccount } from '../src/lib/adapters/blockchain/runtime-accounts.ts'; + const endpoint = process.env.DEOS_WS_ENDPOINT ?? 'ws://127.0.0.1:9988'; const foreignLiteral = process.env.DEOS_COMPOSED_FOREIGN_ID ?? '4026531841'; const amountLiteral = @@ -136,8 +139,7 @@ function eventsOf(records, pallet, variant) { async function finalizedState(client, api, feed) { const block = await client.getFinalizedBlock(); const [ - identity, - actorHot, + control, aliceForeign, burnNative, aliceNative, @@ -145,10 +147,7 @@ async function finalizedState(client, api, feed) { observation, issuance, ] = await Promise.all([ - api.query.Actors.ActorIdentities.getValue(burnActorId, { - at: block.hash, - }), - api.query.Actors.ActorHot.getValue(burnActorId, { at: block.hash }), + readActorControlProjection(api, block.hash, burnActorId), api.view.Assets.balance_of(alice.address, foreignId, { at: block.hash }), api.query.System.Account.getValue(burnActorAccount, { at: block.hash }), api.query.System.Account.getValue(alice.address, { at: block.hash }), @@ -158,14 +157,15 @@ async function finalizedState(client, api, feed) { api.query.Oracle.Observations.getValue(feed, { at: block.hash }), api.query.Balances.TotalIssuance.getValue({ at: block.hash }), ]); - if (!identity || !actorHot) + if (control.status !== 'Active') throw new Error(`Burn Actor ${burnActorId} is not active`); if (!reserves.success) throw new Error('foreign/Native pool reserves are unavailable'); return { block, - identity, - actorHot, + identity: control.cell.identity, + actorHot: control.cell.hot, + actorLocation: control.location, aliceForeign: aliceForeign ?? 0n, burnNative: burnNative.data.free, aliceNative: aliceNative.data.free, @@ -192,8 +192,7 @@ async function waitForActorCycle(client, api, feed, initialNonce, firstBlock) { if ( state.identity.cycle_nonce > initialNonce && !state.actorHot.pending_signal && - state.actorHot.queue_ticket == null && - state.actorHot.wakeup_pointer == null + state.actorLocation.type === 'Unsignaled' ) { return { records, state }; } @@ -212,15 +211,20 @@ let burnActorAccount; try { const api = client.getTypedApi(deos); const discoveryBlock = await client.getFinalizedBlock(); - const [identity, feeds, nativeMinimum] = await Promise.all([ - api.query.Actors.ActorIdentities.getValue(burnActorId, { - at: discoveryBlock.hash, - }), + const [control, feeds, nativeMinimum] = await Promise.all([ + readActorControlProjection(api, discoveryBlock.hash, burnActorId), api.query.Oracle.FeedIds.getValue({ at: discoveryBlock.hash }), api.constants.Balances.ExistentialDeposit({ at: discoveryBlock.hash }), ]); - if (!identity) throw new Error(`Burn Actor ${burnActorId} is not registered`); - burnActorAccount = identity.sovereign_account; + if ( + control.status !== 'Active' || + control.cell.identity.actor_class.type !== 'System' + ) { + throw new Error(`Burn Actor ${burnActorId} is not an active System Actor`); + } + burnActorAccount = deriveSystemActorSovereignAccount( + control.cell.identity.actor_class.value.sovereign_id, + ); const feed = feeds.find(matchesFeed); if (!feed) throw new Error('Native-to-foreign pre-execution Oracle feed is absent'); @@ -230,8 +234,12 @@ try { throw new Error(`Alice Native balance ${before.aliceNative} is too low`); if (before.reserves[0] === 0n || before.reserves[1] === 0n) throw new Error('Native/foreign pool reserves must be non-zero'); - if (before.actorHot.pending_signal || before.actorHot.queue_ticket != null) + if ( + before.actorHot.pending_signal || + before.actorLocation.type !== 'Unsignaled' + ) { throw new Error('Burn Actor must be idle before composed-path execution'); + } if (before.burnNative !== nativeMinimum) throw new Error( `Burn Actor must start with exactly one native ED anchor: expected ${nativeMinimum}, found ${before.burnNative}`, diff --git a/web-client/scripts/test-actors-abi-manifest.mjs b/web-client/scripts/test-actors-abi-manifest.mjs index 5972cb5f..d5aec8a8 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 thirty-two-step execution-plan baseline', () => { +test('metadata exposes the current twelve-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, '0x20000000'); + assert.equal(bound.value, '0x0c000000'); }); test('metadata exposes the protocol-fixed retry-attempt bound', () => { diff --git a/web-client/scripts/test-actors-authoring.mjs b/web-client/scripts/test-actors-authoring.mjs index 8d77f530..1c50cffd 100644 --- a/web-client/scripts/test-actors-authoring.mjs +++ b/web-client/scripts/test-actors-authoring.mjs @@ -157,8 +157,8 @@ test('authoring controls cover every current task and predicate variant', () => assert.equal(ACTORS_AUTHORING_CONDITION_TYPES.length, 10); }); -test('one metadata-aligned zero-to-thirty-two-step baseline applies to both actor classes', () => { - const steps = Array.from({ length: 33 }, (_, index) => +test('one metadata-aligned zero-to-twelve-step baseline applies to both actor classes', () => { + const steps = Array.from({ length: 13 }, (_, index) => authoringStep(`step-${index}`), ); for (const actorType of ['User', 'System']) { @@ -168,7 +168,7 @@ test('one metadata-aligned zero-to-thirty-two-step baseline applies to both acto ); assert.equal( validateActorAuthoringContract( - contract(steps.slice(0, 32), { actorType }), + contract(steps.slice(0, 12), { actorType }), ).valid, true, ); @@ -180,7 +180,7 @@ test('one metadata-aligned zero-to-thirty-two-step baseline applies to both acto assert( tooLong.issues.some( (issue) => - issue.path === 'steps' && /0\.\.32 steps/.test(issue.message), + issue.path === 'steps' && /0\.\.12 steps/.test(issue.message), ), ); } diff --git a/web-client/scripts/test-actors-client-storage.mjs b/web-client/scripts/test-actors-client-storage.mjs index a912a0e9..ac192ae8 100644 --- a/web-client/scripts/test-actors-client-storage.mjs +++ b/web-client/scripts/test-actors-client-storage.mjs @@ -12,6 +12,10 @@ const adapter = await readFile( new URL('../src/lib/adapters/blockchain/index.ts', import.meta.url), 'utf8', ); +const controlProjection = await readFile( + new URL('../src/lib/adapters/blockchain/actor-control.ts', import.meta.url), + 'utf8', +); const upgradeEvidence = await readFile( new URL('./upgrade-state-evidence.mjs', import.meta.url), 'utf8', @@ -20,13 +24,29 @@ const upgradeEvidence = await readFile( test('browser Actor summaries consume compact Contract and Run heads only', () => { assert.match(adapter, /Actors\.ActorContractHead\.getValue/); assert.match(adapter, /Actors\.ActorRunHead\.getValue/); + assert.match(adapter, /readActorControlProjection/); + assert.doesNotMatch(adapter, /Actors\.ActorIdentities\.getValue/); + assert.doesNotMatch(adapter, /Actors\.ActorHot\.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', () => { +test('canonical browser control reads classify one bounded owner and corruption', () => { + assert.match(controlProjection, /Actors\.ActorControlLocators\.getValue/); + assert.match( + controlProjection, + /Actors\.ActorUnsignaledControlCells\.getValue/, + ); + assert.match(controlProjection, /Actors\.ActorReadyFrameChunks\.getValue/); + assert.match(controlProjection, /Actors\.ActorWaitingFrameChunks\.getValue/); + assert.match(controlProjection, /both dormant and active control owners/); +}); + +test('upgrade evidence records canonical control and compact Contract head without reconstructing tails', () => { + assert.match(upgradeEvidence, /readActorControlProjection/); + assert.doesNotMatch(upgradeEvidence, /Actors\.ActorHot\.getValue/); assert.match(upgradeEvidence, /Actors\.ActorContractHead\.getValue/); assert.match(upgradeEvidence, /actor_contract_head: actorContractHead/); assert.doesNotMatch(upgradeEvidence, /Actors\.ActorContract\.getValue/); diff --git a/web-client/scripts/test-actors-control-projection.mjs b/web-client/scripts/test-actors-control-projection.mjs new file mode 100644 index 00000000..ce19b9c4 --- /dev/null +++ b/web-client/scripts/test-actors-control-projection.mjs @@ -0,0 +1,139 @@ +/* +Domain: Actors canonical control projection validation +Owns: Bounded locator resolution and absence/dormancy/corruption classification fixtures. +Excludes: Live RPC, scheduler mutation, Contract reconstruction, and presentation. +Zone: Web-client validation entrypoint; exercises the canonical browser control reader. +*/ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { readActorControlProjection } from '../src/lib/adapters/blockchain/actor-control.ts'; + +const at = `0x${'11'.repeat(32)}`; +const actorId = 65n; +const identity = { + actor_class: { type: 'System', value: { sovereign_id: 4n } }, +}; +const hot = { pending_signal: false }; +const cell = { actor_id: actorId, identity, hot }; + +function api({ location, dormantIdentity, unsignaled, ready, waiting }) { + return { + query: { + Actors: { + ActorControlLocators: { + async getValue(id, options) { + assert.equal(id, actorId); + assert.deepEqual(options, { at }); + return location; + }, + }, + ActorIdentities: { + async getValue(id, options) { + assert.equal(id, actorId); + assert.deepEqual(options, { at }); + return dormantIdentity; + }, + }, + ActorUnsignaledControlCells: { + async getValue() { + return unsignaled; + }, + }, + ActorReadyFrameChunks: { + async getValue(page, options) { + assert.equal(page, 2n); + assert.deepEqual(options, { at }); + return ready; + }, + }, + ActorWaitingFrameChunks: { + async getValue(key, options) { + assert.deepEqual(key, [{ type: 'Block', value: 120 }, 3n]); + assert.deepEqual(options, { at }); + return waiting; + }, + }, + }, + }, + }; +} + +test('canonical control projection distinguishes absence and dormant identity', async () => { + assert.deepEqual(await readActorControlProjection(api({}), at, actorId), { + status: 'NotRegistered', + }); + assert.deepEqual( + await readActorControlProjection( + api({ dormantIdentity: identity }), + at, + actorId, + ), + { status: 'Dormant', identity }, + ); +}); + +test('canonical control projection resolves every bounded active owner', async () => { + const unsignaledLocation = { type: 'Unsignaled', value: undefined }; + assert.deepEqual( + await readActorControlProjection( + api({ location: unsignaledLocation, unsignaled: cell }), + at, + actorId, + ), + { status: 'Active', location: unsignaledLocation, cell }, + ); + + const readyLocation = { type: 'Ready', value: { ticket: 65n } }; + const ready = [undefined, cell]; + assert.deepEqual( + await readActorControlProjection( + api({ location: readyLocation, ready }), + at, + actorId, + ), + { status: 'Active', location: readyLocation, cell }, + ); + + const waitingLocation = { + type: 'Waiting', + value: { key: { type: 'Block', value: 120 }, page: 3n, slot: 1 }, + }; + const waiting = { + entries: [undefined, { type: 'Primary', value: cell }], + }; + assert.deepEqual( + await readActorControlProjection( + api({ location: waitingLocation, waiting }), + at, + actorId, + ), + { status: 'Active', location: waitingLocation, cell }, + ); +}); + +test('canonical control projection fails closed on split or mismatched authority', async () => { + await assert.rejects( + readActorControlProjection( + api({ + location: { type: 'Unsignaled', value: undefined }, + dormantIdentity: identity, + unsignaled: cell, + }), + at, + actorId, + ), + /both dormant and active control owners/, + ); + await assert.rejects( + readActorControlProjection( + api({ + location: { type: 'Unsignaled', value: undefined }, + unsignaled: { ...cell, actor_id: actorId + 1n }, + }), + at, + actorId, + ), + /does not resolve to its Actor/, + ); +}); diff --git a/web-client/scripts/test-actors-cost-projection.mjs b/web-client/scripts/test-actors-cost-projection.mjs index e81bd10b..a1aa4165 100644 --- a/web-client/scripts/test-actors-cost-projection.mjs +++ b/web-client/scripts/test-actors-cost-projection.mjs @@ -261,7 +261,7 @@ test('runtime-generated cost vectors bind metadata, Weight, geometry, and Trigge .sort((left, right) => left.contractStepCount - right.contractStepCount); assert.deepEqual( manual.map((vector) => vector.contractStepCount), - [0, 1, 4, 8, 32], + [0, 1, 4, 8, 12], ); assert.equal(manual[0].quote.maximumNextActionFee.maximumEffectFee, 0n); assert.equal(manual[1].quote.stateHold.components.contractBody, 0n); @@ -336,10 +336,10 @@ test('generated cost vector parser fails closed on drift and malformed ownership const missingGeometry = structuredClone(costVectorArtifact); missingGeometry.vectors = missingGeometry.vectors.filter( - (vector) => vector.name !== 'user-manual-32', + (vector) => vector.name !== 'user-manual-12', ); assert.throws( () => parseActorCostVectors(missingGeometry), - /0\/1\/4\/8\/32 geometry/, + /0\/1\/4\/8\/12 geometry/, ); }); diff --git a/web-client/scripts/test-actors-matching-wasm.mjs b/web-client/scripts/test-actors-matching-wasm.mjs index 90400fd3..4d111280 100644 --- a/web-client/scripts/test-actors-matching-wasm.mjs +++ b/web-client/scripts/test-actors-matching-wasm.mjs @@ -30,6 +30,10 @@ const runtime = { specVersion: 1, transactionVersion: 1, }; +const simulationBudget = { + actorControl: { refTime: 1_000_000_000_000n, proofSize: 10_000_000n }, + sharedEconomic: { refTime: 1_000_000_000_000n, proofSize: 10_000_000n }, +}; const step = { precondition: [ [ @@ -93,7 +97,8 @@ const base = { stateSource: 'FinalizedBlock', }, runtimeApi: 'ActorSimulationApi_simulate_current_contract', - runtimeApiVersion: 1, + runtimeApiVersion: 2, + simulationBudget, }; const suspendedRuntimeValue = { @@ -113,10 +118,6 @@ const suspendedRuntimeValue = { failed_steps: 0, }, steps: [ - { - step_index: 0, - outcome: { type: 'Executed', value: undefined }, - }, { step_index: 1, outcome: { type: 'FundingUnavailable', value: undefined }, @@ -140,7 +141,6 @@ const suspendedOutcome = { failedSteps: 0, }, steps: [ - { stepIndex: 0, outcome: { type: 'Executed' } }, { stepIndex: 1, outcome: { type: 'FundingUnavailable' }, @@ -162,6 +162,31 @@ test('runtime API result codec discovers metadata and preserves bounded evidence resultScale, }, ); + const continuedScale = encodeActorRuntimeSimulationResult(metadataBytes, { + success: true, + value: { + ...suspendedRuntimeValue.value, + status: { type: 'Continued', value: undefined }, + run_cursor: 2, + unsuccessful_attempts_at_cursor: undefined, + steps: [ + { + step_index: 1, + outcome: { type: 'Executed', value: undefined }, + }, + ], + }, + }); + const continued = decodeActorRuntimeSimulationResult( + metadataBytes, + continuedScale, + ); + assert.equal(continued.success, true); + if (continued.success) { + assert.equal(continued.outcome.status, 'Continued'); + assert.equal(continued.outcome.runCursor, 2); + assert.equal(continued.outcome.steps.length, 1); + } const stoppedScale = encodeActorRuntimeSimulationResult(metadataBytes, { success: true, value: { @@ -324,6 +349,7 @@ test('finalized transport pins state and invokes the typed runtime API at one bl artifact, actorId: 14n, mode: 'CurrentRun', + simulationBudget, finalizedBlock: { hash: at, number: 42 }, }); @@ -342,7 +368,17 @@ test('finalized transport pins state and invokes the typed runtime API at one bl type: 'CurrentRun', value: undefined, }); - assert.deepEqual(observedArguments[5], { at }); + assert.deepEqual(observedArguments[5], { + actor_control: { + ref_time: simulationBudget.actorControl.refTime, + proof_size: simulationBudget.actorControl.proofSize, + }, + shared_economic: { + ref_time: simulationBudget.sharedEconomic.refTime, + proof_size: simulationBudget.sharedEconomic.proofSize, + }, + }); + assert.deepEqual(observedArguments[6], { at }); }); test('matching-Wasm gate binds runtime code, metadata, state, API, and Actor Contract identity', async () => { @@ -371,28 +407,39 @@ test('matching-Wasm gate binds runtime code, metadata, state, API, and Actor Con assert.equal(observedRequest.actorId, 14n); assert.equal(observedRequest.mode, 'CurrentRun'); assert.equal(observedRequest.contractScale, artifact.contractScale); + assert.deepEqual(observedRequest.simulationBudget, simulationBudget); assert.equal(result.outcome.runCursor, 1); }); -test('provider cannot change any requested runtime or state dependency', async () => { - await assert.rejects( - runActorMatchingWasmSimulation({ - ...base, - provider: { - async simulate(request) { - return { - engine: 'RuntimeWasm', - pin: { - ...request.pin, - runtimeCodeHash: `0x${'44'.repeat(32)}`, - }, - outcome: suspendedOutcome, - }; +test('provider cannot change any requested runtime, state, or synthetic-budget dependency', async () => { + for (const changedPin of [ + { runtimeCodeHash: `0x${'44'.repeat(32)}` }, + { + simulationBudget: { + ...simulationBudget, + actorControl: { + ...simulationBudget.actorControl, + refTime: simulationBudget.actorControl.refTime - 1n, }, }, - }), - /does not match the requested runtime\/state pin/, - ); + }, + ]) { + await assert.rejects( + runActorMatchingWasmSimulation({ + ...base, + provider: { + async simulate(request) { + return { + engine: 'RuntimeWasm', + pin: { ...request.pin, ...changedPin }, + outcome: suspendedOutcome, + }; + }, + }, + }), + /does not match the requested runtime\/state pin/, + ); + } }); test('provider summary must match canonical runtime SCALE bytes', async () => { @@ -453,4 +500,42 @@ test('local projections and malformed Actor run outcomes fail closed', async () }), /require an Actor run cursor/, ); + await assert.rejects( + runActorMatchingWasmSimulation({ + ...base, + provider: { + async simulate(request) { + return { + engine: 'RuntimeWasm', + pin: request.pin, + outcome: { + ...suspendedOutcome, + steps: [ + ...suspendedOutcome.steps, + { stepIndex: 0, outcome: { type: 'Executed' } }, + ], + }, + }; + }, + }, + }), + /at most one Step record/, + ); + await assert.rejects( + runActorMatchingWasmSimulation({ + ...base, + simulationBudget: { + ...simulationBudget, + actorControl: { refTime: -1n, proofSize: 0n }, + }, + provider: { + async simulate() { + throw new Error( + 'invalid synthetic budget must fail before provider execution', + ); + }, + }, + }), + /unsigned 64-bit Weight components/, + ); }); diff --git a/web-client/scripts/test-actors-observation-inspection.mjs b/web-client/scripts/test-actors-observation-inspection.mjs index 7209e191..834d4074 100644 --- a/web-client/scripts/test-actors-observation-inspection.mjs +++ b/web-client/scripts/test-actors-observation-inspection.mjs @@ -535,7 +535,8 @@ test('typed provider reads the bounded registry and selected exact storage keys' assert.match(source, /Oracle\.FeedIds\.getValue/); assert.match(source, /Oracle\.Feeds\.getValue\(key/); assert.match(source, /Oracle\.Observations\.getValue\(key/); - assert.match(source, /Actors\.ActorHot\.getValue/); + assert.match(source, /readActorControlProjection/); + assert.doesNotMatch(source, /Actors\.ActorHot\.getValue/); assert.match(source, /projectObservationActorDeliveryInspection/); assert.match(source, /Actors\.DirtyObservationFeeds\.getValue/); assert.match(source, /Actors\.DirtyObservationListState\.getValue/); diff --git a/web-client/scripts/test-actors-reactive-authoring.mjs b/web-client/scripts/test-actors-reactive-authoring.mjs index 827efa69..04e8a361 100644 --- a/web-client/scripts/test-actors-reactive-authoring.mjs +++ b/web-client/scripts/test-actors-reactive-authoring.mjs @@ -25,6 +25,10 @@ const runtime = { specVersion: 1, transactionVersion: 1, }; +const simulationBudget = { + actorControl: { refTime: 1_000_000_000_000n, proofSize: 10_000_000n }, + sharedEconomic: { refTime: 1_000_000_000_000n, proofSize: 10_000_000n }, +}; const native = { type: 'Native' }; const quoteAsset = { type: 'Local', id: 7 }; const feed = { @@ -330,7 +334,8 @@ test('matching-Wasm contract accepts canonical productive closure for the fixtur stateSource: 'FinalizedBlock', }, runtimeApi: 'ActorSimulationApi_simulate_current_contract', - runtimeApiVersion: 1, + runtimeApiVersion: 2, + simulationBudget, provider: { async simulate(request) { requestedContractId = request.pin.contractId; diff --git a/web-client/scripts/upgrade-state-evidence.mjs b/web-client/scripts/upgrade-state-evidence.mjs index be595b8c..37fb5f27 100644 --- a/web-client/scripts/upgrade-state-evidence.mjs +++ b/web-client/scripts/upgrade-state-evidence.mjs @@ -12,6 +12,9 @@ import { readFile, writeFile } from 'node:fs/promises'; import { Enum as PapiEnum } from 'polkadot-api'; import { createWsClient } from 'polkadot-api/ws'; +import { readActorControlProjection } from '../src/lib/adapters/blockchain/actor-control.ts'; +import { deriveSystemActorSovereignAccount } from '../src/lib/adapters/blockchain/runtime-accounts.ts'; + if (process.argv.includes('--help') || process.argv.includes('-h')) { console.log(`Usage: upgrade-state-evidence.mjs @@ -77,21 +80,26 @@ function feedMatches(feed) { async function chainSnapshot(client, api) { const block = await client.getFinalizedBlock(); - const [version, codeHex, feeds, actorIdentity] = await Promise.all([ + const [version, codeHex, feeds, actorControl] = await Promise.all([ api.apis.Core.version({ at: block.hash }), client._request('state_getStorage', [codeStorageKey, block.hash]), api.query.Oracle.FeedIds.getValue({ at: block.hash }), - api.query.Actors.ActorIdentities.getValue(burnActorId, { - at: block.hash, - }), + readActorControlProjection(api, block.hash, burnActorId), ]); if (typeof codeHex !== 'string') throw new Error('finalized runtime code is unavailable'); - if (!actorIdentity) throw new Error('Burn Actor identity is unavailable'); + if ( + actorControl.status !== 'Active' || + actorControl.cell.identity.actor_class.type !== 'System' + ) { + throw new Error('Burn Actor active System identity is unavailable'); + } const feed = feeds.find(feedMatches); if (!feed) throw new Error('Native-to-foreign pre-execution Oracle feed is absent'); - const burnAccount = actorIdentity.sovereign_account; + const burnAccount = deriveSystemActorSovereignAccount( + actorControl.cell.identity.actor_class.value.sovereign_id, + ); const [ burnNative, burnForeign, @@ -100,7 +108,6 @@ async function chainSnapshot(client, api) { reserves, lpPairs, observation, - actorHot, actorContractHead, actorFunding, ] = await Promise.all([ @@ -113,7 +120,6 @@ async function chainSnapshot(client, api) { api.view.AssetConversion.get_reserves(native, foreign, { at: block.hash }), 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.ActorContractHead.getValue(burnActorId, { at: block.hash, }), @@ -124,7 +130,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 || !actorContractHead || !actorFunding) + if (!actorContractHead || !actorFunding) throw new Error('Burn Actor state is incomplete'); return jsonValue({ @@ -154,8 +160,9 @@ async function chainSnapshot(client, api) { router_lp_pairs: lpPairs, oracle_feed: feed, oracle_observation: observation, - actor_identity: actorIdentity, - actor_hot: actorHot, + actor_identity: actorControl.cell.identity, + actor_control_location: actorControl.location, + actor_control_cell: actorControl.cell, actor_contract_head: actorContractHead, actor_funding: actorFunding, }, diff --git a/web-client/src/lib/adapters/blockchain/actor-control.ts b/web-client/src/lib/adapters/blockchain/actor-control.ts new file mode 100644 index 00000000..4e776168 --- /dev/null +++ b/web-client/src/lib/adapters/blockchain/actor-control.ts @@ -0,0 +1,64 @@ +/* +Domain: DEOS Actors canonical control projection +Owns: Finalized locator-to-control-cell resolution and absent/dormant/corrupt classification. +Excludes: Scheduler mutation, Contract reconstruction, lifecycle inference, and historical materialization. +Zone: Blockchain adapter capability; consumers read one bounded canonical control owner. +*/ +import type { HexString } from 'polkadot-api'; + +import type { DeosTypedApi } from './deos.ts'; + +export async function readActorControlProjection( + typedApi: DeosTypedApi, + at: HexString, + actorId: bigint, +) { + const [location, dormantIdentity] = await Promise.all([ + typedApi.query.Actors.ActorControlLocators.getValue(actorId, { at }), + typedApi.query.Actors.ActorIdentities.getValue(actorId, { at }), + ]); + if (location == null) { + return dormantIdentity == null + ? ({ status: 'NotRegistered' } as const) + : ({ status: 'Dormant', identity: dormantIdentity } as const); + } + if (dormantIdentity != null) { + throw new Error( + 'Actors identity is present in both dormant and active control owners', + ); + } + + let cell; + switch (location.type) { + case 'Unsignaled': + cell = await typedApi.query.Actors.ActorUnsignaledControlCells.getValue( + actorId, + { at }, + ); + break; + case 'Ready': { + const ticket = location.value.ticket; + const chunk = await typedApi.query.Actors.ActorReadyFrameChunks.getValue( + ticket / 32n, + { at }, + ); + cell = chunk?.[Number(ticket % 32n)]; + break; + } + case 'Waiting': { + const page = await typedApi.query.Actors.ActorWaitingFrameChunks.getValue( + [location.value.key, location.value.page], + { at }, + ); + const entry = page?.entries[location.value.slot]; + cell = entry?.type === 'Primary' ? entry.value : undefined; + break; + } + default: + throw new Error('Actors control locator has an unsupported variant'); + } + if (cell == null || cell.actor_id !== actorId) { + throw new Error('Actors control locator does not resolve to its Actor'); + } + return { status: 'Active', location, cell } as const; +} diff --git a/web-client/src/lib/adapters/blockchain/actor-simulation.ts b/web-client/src/lib/adapters/blockchain/actor-simulation.ts index 98954e49..f7889135 100644 --- a/web-client/src/lib/adapters/blockchain/actor-simulation.ts +++ b/web-client/src/lib/adapters/blockchain/actor-simulation.ts @@ -21,6 +21,7 @@ import { import { ACTORS_SIMULATION_RUNTIME_API, ACTORS_SIMULATION_RUNTIME_API_VERSION, + type ActorRuntimeSimulationBudget, decodeActorRuntimeSimulationResult, encodeActorRuntimeSimulationResult, } from '../../automation/runtime-simulation-codec.ts'; @@ -32,11 +33,13 @@ const HEX_PATTERN = /^0x(?:[0-9a-fA-F]{2})+$/; const PLAN_HEX_PATTERN = /^0x(?:[0-9a-f]{2})+$/; export type ActorFinalizedSimulationMode = 'FreshCurrentPlan' | 'CurrentRun'; +export type ActorFinalizedSimulationBudget = ActorRuntimeSimulationBudget; export type ActorFinalizedSimulationInput = { artifact: ActorContractArtifact; actorId: bigint; mode: ActorFinalizedSimulationMode; + simulationBudget: ActorRuntimeSimulationBudget; finalizedBlock?: { hash: ActorContractHex; number: number }; }; @@ -172,6 +175,7 @@ async function executeSimulation( mutability: ActorContractArtifact['mutability']; runtimeProgram: RuntimeSimulationProgram; mode: ActorFinalizedSimulationMode; + simulationBudget: ActorRuntimeSimulationBudget; }, ) { return typedApi.apis.ActorSimulationApi.simulate_current_contract( @@ -180,6 +184,16 @@ async function executeSimulation( { type: request.mutability, value: undefined }, request.runtimeProgram, { type: request.mode, value: undefined }, + { + actor_control: { + ref_time: request.simulationBudget.actorControl.refTime, + proof_size: request.simulationBudget.actorControl.proofSize, + }, + shared_economic: { + ref_time: request.simulationBudget.sharedEconomic.refTime, + proof_size: request.simulationBudget.sharedEconomic.proofSize, + }, + }, { at }, ); } @@ -210,6 +224,7 @@ export async function runDeosActorFinalizedSimulation( }, runtimeApi: ACTORS_SIMULATION_RUNTIME_API, runtimeApiVersion: ACTORS_SIMULATION_RUNTIME_API_VERSION, + simulationBudget: input.simulationBudget, provider: { async simulate(request) { const inspection = inspectActorContractArtifact( @@ -228,6 +243,7 @@ export async function runDeosActorFinalizedSimulation( mutability: request.mutability, runtimeProgram: inspection.runtimeValue as RuntimeSimulationProgram, mode: request.mode, + simulationBudget: request.simulationBudget, }); const resultScale = encodeActorRuntimeSimulationResult( context.metadataBytes, diff --git a/web-client/src/lib/adapters/blockchain/index.ts b/web-client/src/lib/adapters/blockchain/index.ts index 83f19f1c..fbd2a596 100644 --- a/web-client/src/lib/adapters/blockchain/index.ts +++ b/web-client/src/lib/adapters/blockchain/index.ts @@ -32,6 +32,7 @@ import type { import type { SystemConfig, SystemSnapshot } from '$lib/system/types'; import { DEFAULT_DEOS_DAPP_NAME } from '$lib/wallet/signer'; +import { readActorControlProjection } from './actor-control'; import { readActorEligibility } from './actor-eligibility'; import { readActorMaterializationProjection } from './actor-materialization'; import { readActorResourceProjection } from './actor-resource'; @@ -128,12 +129,6 @@ function automationContinuationSnapshot( }; } -function automationQueueTicket(hot: unknown): bigint | null { - const actor = triggerRecord(hot); - const ticket = actor?.queue_ticket; - return typeof ticket === 'bigint' ? ticket : null; -} - function automationFundingAccumulated( funding: unknown, ): ReadonlyArray<[string, bigint]> { @@ -466,41 +461,60 @@ export class BlockchainAdapter implements Adapter { const snapshot = await (await this.ensurePapi()).snapshot(); return await Promise.all( KNOWN_SYSTEM_ACTORS.map(async (actor) => { - const [identity, hot, contractHead, runHead, funding] = - await Promise.all([ - snapshot.typedApi.query.Actors.ActorIdentities.getValue( - BigInt(actor.actorId), - { at: snapshot.at }, - ), - snapshot.typedApi.query.Actors.ActorHot.getValue( - BigInt(actor.actorId), - { - at: snapshot.at, - }, - ), - snapshot.typedApi.query.Actors.ActorContractHead.getValue( - BigInt(actor.actorId), - { at: snapshot.at }, - ), - snapshot.typedApi.query.Actors.ActorRunHead.getValue( - BigInt(actor.actorId), - { at: snapshot.at }, - ), - snapshot.typedApi.query.Actors.ActorFunding.getValue( - BigInt(actor.actorId), - { at: snapshot.at }, - ), - ]); + const runtimeActorId = BigInt(actor.actorId); + const [control, contractHead, runHead, funding] = await Promise.all([ + readActorControlProjection( + snapshot.typedApi, + snapshot.at, + runtimeActorId, + ), + snapshot.typedApi.query.Actors.ActorContractHead.getValue( + runtimeActorId, + { at: snapshot.at }, + ), + snapshot.typedApi.query.Actors.ActorRunHead.getValue( + runtimeActorId, + { at: snapshot.at }, + ), + snapshot.typedApi.query.Actors.ActorFunding.getValue( + runtimeActorId, + { at: snapshot.at }, + ), + ]); + const identity = + control.status === 'Active' + ? control.cell.identity + : control.status === 'Dormant' + ? control.identity + : null; + const hot = control.status === 'Active' ? control.cell.hot : null; const eligibility = await readActorEligibility( snapshot.typedApi, snapshot.at, actor.actorId, ); - const exists = - identity != null && hot != null && contractHead != null; + if ( + control.status === 'Active' && + (contractHead == null || funding == null) + ) { + throw new Error('Active Actor is missing canonical cold state'); + } + if ( + control.status !== 'Active' && + (contractHead != null || runHead != null || funding != null) + ) { + throw new Error('Inactive Actor retains orphan active state'); + } + const exists = control.status === 'Active'; const sovereignAccount = - identity?.sovereign_account ?? - deriveSystemActorSovereignAccount(actor.actorId); + control.status === 'Dormant' + ? control.identity.sovereign_account + : control.status === 'Active' && + control.cell.identity.actor_class.type === 'System' + ? deriveSystemActorSovereignAccount( + control.cell.identity.actor_class.value.sovereign_id, + ) + : deriveSystemActorSovereignAccount(actor.actorId); const account = await snapshot.typedApi.query.System.Account.getValue( sovereignAccount, { at: snapshot.at }, @@ -524,7 +538,10 @@ export class BlockchainAdapter implements Adapter { completionPolicy: contractHead?.header.completion.type ?? null, triggerLabel: automationTriggerLabel(contractHead?.header.trigger), nativeBalance: account?.data?.free ?? 0n, - queueTicket: automationQueueTicket(hot), + queueTicket: + control.status === 'Active' && control.location.type === 'Ready' + ? control.location.value.ticket + : null, fundingAccumulated: automationFundingAccumulated(funding), fundingSourcePolicy: automationFundingSourcePolicy( contractHead?.header.funding, diff --git a/web-client/src/lib/adapters/blockchain/observations.ts b/web-client/src/lib/adapters/blockchain/observations.ts index 9bb5d2eb..94d369b6 100644 --- a/web-client/src/lib/adapters/blockchain/observations.ts +++ b/web-client/src/lib/adapters/blockchain/observations.ts @@ -24,6 +24,7 @@ import type { ObservationInspection, } from '$lib/observation/types'; +import { readActorControlProjection } from './actor-control'; import type { DeosChainSnapshot } from './deos'; import type { RuntimeAssetKind } from './runtime-assets'; @@ -208,32 +209,35 @@ export class BlockchainObservationReader { throw new Error('Selected Actors id must be a non-negative safe integer'); } const runtimeActorId = BigInt(actorId); - const [identity, hot] = await Promise.all([ - snapshot.typedApi.query.Actors.ActorIdentities.getValue(runtimeActorId, { - at: snapshot.at, - }), - snapshot.typedApi.query.Actors.ActorHot.getValue(runtimeActorId, { - at: snapshot.at, - }), - ]); + const control = await readActorControlProjection( + snapshot.typedApi, + snapshot.at, + runtimeActorId, + ); + if (control.status !== 'Active') { + return projectObservationActorDeliveryInspection({ + actorId: runtimeActorId, + hot: null, + }); + } return projectObservationActorDeliveryInspection({ actorId: runtimeActorId, - hot: - hot == null || identity == null - ? null - : { - actorClass: identity.actor_class.type, - pendingSignal: hot.pending_signal, - queueTicket: hot.queue_ticket ?? null, - wakeup: - hot.wakeup_pointer == null - ? null - : { - key: hot.wakeup_pointer.block, - pageId: hot.wakeup_pointer.page_id, - slot: hot.wakeup_pointer.slot, - }, - }, + hot: { + actorClass: control.cell.identity.actor_class.type, + pendingSignal: control.cell.hot.pending_signal, + queueTicket: + control.location.type === 'Ready' + ? control.location.value.ticket + : null, + wakeup: + control.location.type === 'Waiting' + ? { + key: control.location.value.key, + pageId: control.location.value.page, + slot: control.location.value.slot, + } + : null, + }, }); } diff --git a/web-client/src/lib/adapters/blockchain/runtime-accounts.ts b/web-client/src/lib/adapters/blockchain/runtime-accounts.ts index 1a923d68..386f8d88 100644 --- a/web-client/src/lib/adapters/blockchain/runtime-accounts.ts +++ b/web-client/src/lib/adapters/blockchain/runtime-accounts.ts @@ -50,7 +50,7 @@ export const KNOWN_SYSTEM_ACTORS: readonly KnownSystemActor[] = [ { actorId: 9, label: 'Treasury D', role: 'Dormant treasury' }, { actorId: 10, label: 'BLDR Splitter', role: 'BLDR distribution' }, { actorId: 11, label: 'BLDR Liquidity Actor', role: 'NTVE/BLDR LP composer' }, - { actorId: 12, label: 'BLDR Bucket A', role: 'BLDR anchor LP' }, + { actorId: 12, label: 'BLDR Anchor', role: 'Immutable NTVE/BLDR LP custody' }, { actorId: 13, label: 'BLDR Treasury', role: 'BLDR treasury' }, ]; @@ -65,11 +65,13 @@ function concatBytes(...parts: Uint8Array[]): Uint8Array { return result; } -export function deriveSystemActorSovereignAccount(actorId: number): string { +export function deriveSystemActorSovereignAccount( + sovereignId: number | bigint, +): string { const seed = concatBytes( Binary.fromText(ACTORS_PALLET_ID), Binary.fromText(SYSTEM_ACTORS_LABEL), - u64.enc(BigInt(actorId)), + u64.enc(BigInt(sovereignId)), ); return fromBufferToBase58(SS58_FORMAT)(Blake2256(seed)); } diff --git a/web-client/src/lib/automation/actors-abi-manifest.json b/web-client/src/lib/automation/actors-abi-manifest.json index e953a46a..ddda3d73 100644 --- a/web-client/src/lib/automation/actors-abi-manifest.json +++ b/web-client/src/lib/automation/actors-abi-manifest.json @@ -2,7 +2,7 @@ "format": "deos.actor.abi-manifest", "formatVersion": 1, "metadata": { - "sha256": "27984891721c42acbce79d4e458e9b40dd6b9a046228438a072f4f2c1bd0f74e", + "sha256": "75868ea75fd85e7b79db12650a7407e89e249f5564b2157d55ac46cd26cb5d12", "version": 16 }, "pallet": { @@ -159,17 +159,17 @@ }, { "name": "MinWindowLength", "type": 14, "value": "0x64000000" }, { "name": "PalletId", "type": 446, "value": "0x6163746f72733030" }, - { "name": "MaxContractSteps", "type": 14, "value": "0x20000000" }, + { "name": "MaxContractSteps", "type": 14, "value": "0x0c000000" }, { "name": "MaxFundingTrackedAssets", "type": 14, "value": "0x28000000" }, { "name": "MaxOpeningSnapshotEntries", "type": 14, - "value": "0x40000000" + "value": "0x18000000" }, { "name": "MaxOpeningPredicateResults", "type": 14, - "value": "0x80000000" + "value": "0x30000000" }, { "name": "MaxPreconditionClauses", "type": 14, "value": "0x04000000" }, { "name": "MaxPredicatesPerClause", "type": 14, "value": "0x04000000" }, @@ -811,18 +811,6 @@ "name": "NextActorId", "type": { "tag": "plain", "value": 10 } }, - { - "modifier": 0, - "name": "ActorHot", - "type": { - "tag": "map", - "value": { - "hashers": [{ "tag": "Blake2128Concat" }], - "key": 10, - "value": 658 - } - } - }, { "modifier": 0, "name": "ActorContractHead", @@ -831,7 +819,7 @@ "value": { "hashers": [{ "tag": "Blake2128Concat" }], "key": 10, - "value": 667 + "value": 658 } } }, @@ -843,19 +831,7 @@ "value": { "hashers": [{ "tag": "Blake2128Concat" }], "key": 10, - "value": 673 - } - } - }, - { - "modifier": 0, - "name": "ActorAdmissionCertificate", - "type": { - "tag": "map", - "value": { - "hashers": [{ "tag": "Blake2128Concat" }], - "key": 10, - "value": 674 + "value": 664 } } }, @@ -869,8 +845,8 @@ { "tag": "Blake2128Concat" }, { "tag": "Blake2128Concat" } ], - "key": 675, - "value": 676 + "key": 665, + "value": 666 } } }, @@ -882,7 +858,7 @@ "value": { "hashers": [{ "tag": "Blake2128Concat" }], "key": 10, - "value": 681 + "value": 671 } } }, @@ -894,7 +870,7 @@ "value": { "hashers": [{ "tag": "Blake2128Concat" }], "key": 10, - "value": 686 + "value": 676 } } }, @@ -906,7 +882,7 @@ "value": { "hashers": [{ "tag": "Blake2128Concat" }], "key": 10, - "value": 692 + "value": 682 } } }, @@ -918,7 +894,7 @@ "value": { "hashers": [{ "tag": "Blake2128Concat" }], "key": 10, - "value": 702 + "value": 692 } } }, @@ -935,7 +911,7 @@ "value": { "hashers": [{ "tag": "Blake2128Concat" }], "key": 10, - "value": 703 + "value": 693 } } }, @@ -952,7 +928,7 @@ "value": { "hashers": [{ "tag": "Blake2128Concat" }], "key": 10, - "value": 705 + "value": 695 } } }, @@ -961,74 +937,129 @@ "name": "SystemSovereignCount", "type": { "tag": "plain", "value": 14 } }, - { - "modifier": 1, - "name": "NextQueueTicket", - "type": { "tag": "plain", "value": 10 } - }, { "modifier": 0, "name": "PrepassExecutionCutoff", - "type": { "tag": "plain", "value": 706 } + "type": { "tag": "plain", "value": 696 } }, { "modifier": 0, "name": "CurrentBlockResourceState", - "type": { "tag": "plain", "value": 707 } + "type": { "tag": "plain", "value": 697 } }, { "modifier": 0, "name": "FinalizedBlockResourceTelemetry", - "type": { "tag": "plain", "value": 710 } + "type": { "tag": "plain", "value": 700 } + }, + { + "modifier": 0, + "name": "ActorUnsignaledControlCells", + "type": { + "tag": "map", + "value": { + "hashers": [{ "tag": "Blake2128Concat" }], + "key": 10, + "value": 701 + } + } + }, + { + "modifier": 0, + "name": "ActorReadyFrameChunks", + "type": { + "tag": "map", + "value": { + "hashers": [{ "tag": "Blake2128Concat" }], + "key": 10, + "value": 713 + } + } + }, + { + "modifier": 0, + "name": "ActorWaitingFrameChunks", + "type": { + "tag": "map", + "value": { + "hashers": [{ "tag": "Blake2128Concat" }], + "key": 716, + "value": 717 + } + } + }, + { + "modifier": 0, + "name": "ActorControlLocators", + "type": { + "tag": "map", + "value": { + "hashers": [{ "tag": "Blake2128Concat" }], + "key": 10, + "value": 723 + } + } }, { "modifier": 1, - "name": "QueueHead", + "name": "ActorReadyHead", "type": { "tag": "plain", "value": 10 } }, { "modifier": 1, - "name": "QueueTail", + "name": "ActorReadyTail", "type": { "tag": "plain", "value": 10 } }, { "modifier": 1, - "name": "QueueOccupancy", + "name": "ActorReadyOccupancy", "type": { "tag": "plain", "value": 14 } }, { - "modifier": 0, - "name": "QueuePages", + "modifier": 1, + "name": "ActorWaitingHeads", "type": { "tag": "map", "value": { "hashers": [{ "tag": "Blake2128Concat" }], - "key": 10, - "value": 711 + "key": 364, + "value": 10 } } }, { - "modifier": 0, - "name": "WakeupPages", + "modifier": 1, + "name": "ActorWaitingTails", + "type": { + "tag": "map", + "value": { + "hashers": [{ "tag": "Blake2128Concat" }], + "key": 364, + "value": 10 + } + } + }, + { + "modifier": 1, + "name": "ActorWaitingOccupancies", "type": { "tag": "map", "value": { "hashers": [{ "tag": "Blake2128Concat" }], - "key": 715, - "value": 716 + "key": 364, + "value": 14 } } }, { "modifier": 0, - "name": "WakeupBuckets", + "name": "ActorWaitingCursorIndices", "type": { "tag": "map", "value": { "hashers": [{ "tag": "Blake2128Concat" }], "key": 364, - "value": 721 + "value": 14 } } }, @@ -1039,8 +1070,8 @@ "tag": "map", "value": { "hashers": [{ "tag": "Blake2128Concat" }], - "key": 722, - "value": 724 + "key": 724, + "value": 726 } } }, @@ -1051,7 +1082,7 @@ "tag": "map", "value": { "hashers": [{ "tag": "Blake2128Concat" }], - "key": 723, + "key": 725, "value": 14 } } @@ -1059,7 +1090,7 @@ { "modifier": 1, "name": "NextWakeupClock", - "type": { "tag": "plain", "value": 723 } + "type": { "tag": "plain", "value": 725 } }, { "modifier": 0, @@ -1115,7 +1146,7 @@ "value": { "hashers": [{ "tag": "Blake2128Concat" }], "key": 10, - "value": 726 + "value": 728 } } }, @@ -1161,7 +1192,7 @@ "value": { "hashers": [{ "tag": "Blake2128Concat" }], "key": 14, - "value": 727 + "value": 729 } } }, @@ -1175,8 +1206,8 @@ { "tag": "Blake2128Concat" }, { "tag": "Blake2128Concat" } ], - "key": 728, - "value": 729 + "key": 730, + "value": 731 } } }, @@ -1188,7 +1219,7 @@ "value": { "hashers": [{ "tag": "Blake2128Concat" }], "key": 216, - "value": 732 + "value": 734 } } }, @@ -1229,14 +1260,14 @@ "value": { "hashers": [{ "tag": "Blake2128Concat" }], "key": 216, - "value": 733 + "value": 735 } } }, { "modifier": 1, "name": "DirtyObservationListState", - "type": { "tag": "plain", "value": 736 } + "type": { "tag": "plain", "value": 738 } }, { "modifier": 0, @@ -1251,7 +1282,7 @@ "value": { "hashers": [{ "tag": "Blake2128Concat" }], "key": 10, - "value": 737 + "value": 739 } } }, @@ -1265,8 +1296,8 @@ { "tag": "Blake2128Concat" }, { "tag": "Blake2128Concat" } ], - "key": 740, - "value": 741 + "key": 742, + "value": 743 } } }, @@ -1277,8 +1308,8 @@ "tag": "map", "value": { "hashers": [{ "tag": "Blake2128Concat" }], - "key": 738, - "value": 745 + "key": 740, + "value": 747 } } }, @@ -1289,7 +1320,7 @@ "tag": "map", "value": { "hashers": [{ "tag": "Blake2128Concat" }], - "key": 746, + "key": 748, "value": 327 } } @@ -1326,7 +1357,7 @@ "value": { "hashers": [{ "tag": "Blake2128Concat" }], "key": 216, - "value": 747 + "value": 749 } } }, @@ -1338,14 +1369,14 @@ "value": { "hashers": [{ "tag": "Blake2128Concat" }], "key": 216, - "value": 750 + "value": 752 } } }, { "modifier": 1, "name": "CrossingPendingFeedListState", - "type": { "tag": "plain", "value": 751 } + "type": { "tag": "plain", "value": 753 } }, { "modifier": 0, @@ -1355,7 +1386,7 @@ "value": { "hashers": [{ "tag": "Blake2128Concat" }], "key": 216, - "value": 752 + "value": 754 } } }, @@ -1377,7 +1408,7 @@ { "modifier": 1, "name": "IdleStarvationState", - "type": { "tag": "plain", "value": 753 } + "type": { "tag": "plain", "value": 755 } } ] }, @@ -5045,324 +5076,318 @@ "def": { "tag": "composite", "value": [ + { "docs": [], "name": "header", "type": 659, "typeName": "Header" }, { "docs": [], - "name": "lifecycle", - "type": 659, - "typeName": "ActiveLifecycle" + "name": "first_step", + "type": 661, + "typeName": "Option" }, { "docs": [], - "name": "cycle_state", - "type": 660, - "typeName": "CycleState" + "name": "first_step_resources", + "type": 662, + "typeName": "Option" + } + ] + }, + "id": 658, + "params": [ + { "name": "Header", "type": 659 }, + { "name": "Step", "type": 235 } + ], + "path": ["pallet_deos_actors", "types", "contract", "ActorContractHead"] + }, + { + "def": { + "tag": "composite", + "value": [ + { "docs": [], "name": "trigger", "type": 227, "typeName": "Trigger" }, + { + "docs": [], + "name": "cooldown_blocks", + "type": 14, + "typeName": "u32" }, { "docs": [], - "name": "trigger_runtime_state", - "type": 661, - "typeName": "TriggerRuntimeState" + "name": "window", + "type": 256, + "typeName": "Option>" }, { "docs": [], - "name": "unsuccessful_attempt_streak", - "type": 14, - "typeName": "u32" + "name": "funding", + "type": 253, + "typeName": "FundingPolicy" }, { "docs": [], - "name": "pending_signal", - "type": 50, - "typeName": "bool" + "name": "completion", + "type": 258, + "typeName": "CompletionPolicy" }, { "docs": [], - "name": "queue_ticket", + "name": "auto_close_at_cycle_nonce", "type": 189, "typeName": "Option" }, + { "docs": [], "name": "step_count", "type": 14, "typeName": "u32" }, { "docs": [], - "name": "wakeup_pointer", - "type": 663, - "typeName": "Option>" + "name": "semantic_contract_id", + "type": 2, + "typeName": "Hash" }, { "docs": [], - "name": "trigger_wakeup_pointer", - "type": 665, - "typeName": "Option" + "name": "body_commitment", + "type": 2, + "typeName": "Hash" }, { "docs": [], - "name": "terminal_at", - "type": 186, - "typeName": "Option" + "name": "admission_identity", + "type": 2, + "typeName": "Hash" }, { "docs": [], - "name": "schedule_anchor", - "type": 14, - "typeName": "BlockNumber" + "name": "pipeline_machine_envelope", + "type": 660, + "typeName": "PipelineMachineEnvelope" + } + ] + }, + "id": 659, + "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": "last_cycle_block", - "type": 186, - "typeName": "Option" + "name": "cleanup_fee_upper", + "type": 49, + "typeName": "Balance" } ] }, - "id": 658, - "params": [{ "name": "BlockNumber", "type": 14 }], - "path": ["pallet_deos_actors", "types", "lifecycle", "ActorHotState"] + "id": 660, + "params": [{ "name": "Balance", "type": 49 }], + "path": [ + "pallet_deos_actors", + "types", + "contract", + "PipelineMachineEnvelope" + ] }, { "def": { "tag": "variant", "value": [ - { "docs": [], "fields": [], "index": 0, "name": "Active" }, - { "docs": [], "fields": [], "index": 1, "name": "Paused" } + { "docs": [], "fields": [], "index": 0, "name": "None" }, + { + "docs": [], + "fields": [{ "docs": [], "type": 235 }], + "index": 1, + "name": "Some" + } ] }, - "id": 659, - "params": [], - "path": ["pallet_deos_actors", "types", "lifecycle", "ActiveLifecycle"] + "id": 661, + "params": [{ "name": "T", "type": 235 }], + "path": ["Option"] }, { "def": { "tag": "variant", "value": [ - { "docs": [], "fields": [], "index": 0, "name": "Idle" }, - { "docs": [], "fields": [], "index": 1, "name": "Running" }, - { "docs": [], "fields": [], "index": 2, "name": "Suspended" } + { "docs": [], "fields": [], "index": 0, "name": "None" }, + { + "docs": [], + "fields": [{ "docs": [], "type": 663 }], + "index": 1, + "name": "Some" + } ] }, - "id": 660, - "params": [], - "path": ["pallet_deos_actors", "types", "lifecycle", "CycleState"] + "id": 662, + "params": [{ "name": "T", "type": 663 }], + "path": ["Option"] }, { "def": { - "tag": "variant", + "tag": "composite", "value": [ - { "docs": [], "fields": [], "index": 0, "name": "Stateless" }, { "docs": [], - "fields": [ - { - "docs": [], - "name": "phase", - "type": 662, - "typeName": "CrossingPhase" - }, - { - "docs": [], - "name": "installed_at_revision", - "type": 10, - "typeName": "u64" - } - ], - "index": 1, - "name": "ObservationCrossing" - }, - { - "docs": [], - "fields": [ - { - "docs": [], - "name": "anchor_tick", - "type": 189, - "typeName": "Option" - }, - { "docs": [], "name": "consumed", "type": 50, "typeName": "bool" } - ], - "index": 2, - "name": "AtTime" + "name": "control", + "type": 109, + "typeName": "ActorStepControlWeight" }, { "docs": [], - "fields": [ - { - "docs": [], - "name": "anchor_tick", - "type": 189, - "typeName": "Option" - } - ], - "index": 3, - "name": "Cadenced" + "name": "effect", + "type": 109, + "typeName": "ActorTaskEffectWeight" } ] }, - "id": 661, + "id": 663, "params": [], "path": [ "pallet_deos_actors", "types", - "lifecycle", - "TriggerRuntimeState" + "contract", + "ActorStepResourceEnvelope" ] }, { "def": { - "tag": "variant", - "value": [ - { "docs": [], "fields": [], "index": 0, "name": "Armed" }, - { "docs": [], "fields": [], "index": 1, "name": "WaitingForRearm" } - ] - }, - "id": 662, - "params": [], - "path": ["pallet_deos_actors", "types", "contract", "CrossingPhase"] - }, - { - "def": { - "tag": "variant", + "tag": "composite", "value": [ - { "docs": [], "fields": [], "index": 0, "name": "None" }, { "docs": [], - "fields": [{ "docs": [], "type": 664 }], - "index": 1, - "name": "Some" - } - ] - }, - "id": 663, - "params": [{ "name": "T", "type": 664 }], - "path": ["Option"] - }, - { - "def": { - "tag": "composite", - "value": [ + "name": "feed", + "type": 216, + "typeName": "ObservationFeedId" + }, { "docs": [], - "name": "block", - "type": 364, - "typeName": "WakeupKey" + "name": "cooldown_blocks", + "type": 14, + "typeName": "u32" }, { "docs": [], - "name": "page_id", - "type": 10, - "typeName": "WakeupPageId" + "name": "window", + "type": 256, + "typeName": "Option>" }, - { "docs": [], "name": "slot", "type": 14, "typeName": "WakeupSlot" } - ] - }, - "id": 664, - "params": [{ "name": "BlockNumber", "type": 14 }], - "path": ["pallet_deos_actors", "types", "scheduler", "WakeupPointer"] - }, - { - "def": { - "tag": "variant", - "value": [ - { "docs": [], "fields": [], "index": 0, "name": "None" }, { "docs": [], - "fields": [{ "docs": [], "type": 666 }], - "index": 1, - "name": "Some" - } - ] - }, - "id": 665, - "params": [{ "name": "T", "type": 666 }], - "path": ["Option"] - }, - { - "def": { - "tag": "composite", - "value": [ + "name": "auto_close_at_cycle_nonce", + "type": 189, + "typeName": "Option" + }, { "docs": [], - "name": "tick", - "type": 10, - "typeName": "SchedulerTick" + "name": "semantic_contract_id", + "type": 2, + "typeName": "Hash" }, { "docs": [], - "name": "page_id", - "type": 10, - "typeName": "WakeupPageId" + "name": "body_commitment", + "type": 2, + "typeName": "Hash" }, - { "docs": [], "name": "slot", "type": 14, "typeName": "WakeupSlot" } + { + "docs": [], + "name": "admission_identity", + "type": 2, + "typeName": "Hash" + } ] }, - "id": 666, - "params": [], + "id": 664, + "params": [ + { "name": "ObservationFeedId", "type": 216 }, + { "name": "BlockNumber", "type": 14 }, + { "name": "Hash", "type": 2 } + ], "path": [ "pallet_deos_actors", "types", - "scheduler", - "TriggerWakeupPointer" + "contract", + "ActorActivationAuthority" ] }, + { + "def": { "tag": "tuple", "value": [10, 14] }, + "id": 665, + "params": [], + "path": [] + }, { "def": { "tag": "composite", "value": [ - { "docs": [], "name": "header", "type": 668, "typeName": "Header" }, { "docs": [], - "name": "first_step", + "name": "authority", "type": 670, - "typeName": "Option" + "typeName": "ActorBodyAuthority" }, { "docs": [], - "name": "first_step_resources", - "type": 671, - "typeName": "Option" + "name": "first_step_index", + "type": 14, + "typeName": "u32" + }, + { "docs": [], "name": "steps", "type": 667, "typeName": "Steps" }, + { + "docs": [], + "name": "step_resources", + "type": 668, + "typeName": "Resources" } ] }, - "id": 667, + "id": 666, "params": [ - { "name": "Header", "type": 668 }, - { "name": "Step", "type": 235 } + { "name": "ActorId", "type": 10 }, + { "name": "Hash", "type": 2 }, + { "name": "Steps", "type": 667 }, + { "name": "Resources", "type": 668 } ], - "path": ["pallet_deos_actors", "types", "contract", "ActorContractHead"] + "path": ["pallet_deos_actors", "types", "contract", "ActorStepChunk"] + }, + { + "def": { + "tag": "composite", + "value": [{ "docs": [], "type": 252, "typeName": "Vec" }] + }, + "id": 667, + "params": [{ "name": "T", "type": 235 }, { "name": "S" }], + "path": ["bounded_collections", "bounded_vec", "BoundedVec"] + }, + { + "def": { + "tag": "composite", + "value": [{ "docs": [], "type": 669, "typeName": "Vec" }] + }, + "id": 668, + "params": [{ "name": "T", "type": 663 }, { "name": "S" }], + "path": ["bounded_collections", "bounded_vec", "BoundedVec"] + }, + { + "def": { "tag": "sequence", "value": 663 }, + "id": 669, + "params": [], + "path": [] }, { "def": { "tag": "composite", "value": [ - { "docs": [], "name": "trigger", "type": 227, "typeName": "Trigger" }, - { - "docs": [], - "name": "cooldown_blocks", - "type": 14, - "typeName": "u32" - }, - { - "docs": [], - "name": "window", - "type": 256, - "typeName": "Option>" - }, - { - "docs": [], - "name": "funding", - "type": 253, - "typeName": "FundingPolicy" - }, - { - "docs": [], - "name": "completion", - "type": 258, - "typeName": "CompletionPolicy" - }, - { - "docs": [], - "name": "auto_close_at_cycle_nonce", - "type": 189, - "typeName": "Option" - }, - { "docs": [], "name": "step_count", "type": 14, "typeName": "u32" }, + { "docs": [], "name": "actor_id", "type": 10, "typeName": "ActorId" }, { "docs": [], "name": "semantic_contract_id", @@ -5380,24 +5405,15 @@ "name": "admission_identity", "type": 2, "typeName": "Hash" - }, - { - "docs": [], - "name": "pipeline_machine_envelope", - "type": 669, - "typeName": "PipelineMachineEnvelope" } ] }, - "id": 668, + "id": 670, "params": [ - { "name": "Trigger", "type": 227 }, - { "name": "BlockNumber", "type": 14 }, - { "name": "FundingPolicy", "type": 253 }, - { "name": "Balance", "type": 49 }, + { "name": "ActorId", "type": 10 }, { "name": "Hash", "type": 2 } ], - "path": ["pallet_deos_actors", "types", "contract", "ActorContractHeader"] + "path": ["pallet_deos_actors", "types", "contract", "ActorBodyAuthority"] }, { "def": { @@ -5405,148 +5421,61 @@ "value": [ { "docs": [], - "name": "pipeline_machine_fee_upper", - "type": 49, - "typeName": "Balance" + "name": "funding_accumulated", + "type": 672, + "typeName": "FundingAccumulated" }, { "docs": [], - "name": "cleanup_fee_upper", - "type": 49, - "typeName": "Balance" + "name": "funding_tracked_assets", + "type": 674, + "typeName": "FundingTrackedAssets" } ] }, - "id": 669, - "params": [{ "name": "Balance", "type": 49 }], - "path": [ - "pallet_deos_actors", - "types", - "contract", - "PipelineMachineEnvelope" - ] + "id": 671, + "params": [ + { "name": "FundingAccumulated", "type": 672 }, + { "name": "FundingTrackedAssets", "type": 674 } + ], + "path": ["pallet_deos_actors", "types", "contract", "ActorFundingState"] }, { "def": { - "tag": "variant", - "value": [ - { "docs": [], "fields": [], "index": 0, "name": "None" }, - { - "docs": [], - "fields": [{ "docs": [], "type": 235 }], - "index": 1, - "name": "Some" - } - ] + "tag": "composite", + "value": [{ "docs": [], "type": 673, "typeName": "BTreeMap" }] }, - "id": 670, - "params": [{ "name": "T", "type": 235 }], - "path": ["Option"] + "id": 672, + "params": [ + { "name": "K", "type": 197 }, + { "name": "V", "type": 49 }, + { "name": "S" } + ], + "path": ["bounded_collections", "bounded_btree_map", "BoundedBTreeMap"] }, { - "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": [], "type": 320 }] }, + "id": 673, + "params": [ + { "name": "K", "type": 197 }, + { "name": "V", "type": 49 } + ], + "path": ["BTreeMap"] }, { "def": { "tag": "composite", - "value": [ - { - "docs": [], - "name": "control", - "type": 109, - "typeName": "ActorStepControlWeight" - }, - { - "docs": [], - "name": "effect", - "type": 109, - "typeName": "ActorTaskEffectWeight" - } - ] + "value": [{ "docs": [], "type": 675, "typeName": "BTreeSet" }] }, - "id": 672, - "params": [], - "path": [ - "pallet_deos_actors", - "types", - "contract", - "ActorStepResourceEnvelope" - ] + "id": 674, + "params": [{ "name": "T", "type": 197 }, { "name": "S" }], + "path": ["bounded_collections", "bounded_btree_set", "BoundedBTreeSet"] }, { - "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": [], "type": 232 }] }, + "id": 675, + "params": [{ "name": "T", "type": 197 }], + "path": ["BTreeSet"] }, { "def": { @@ -5554,132 +5483,82 @@ "value": [ { "docs": [], - "name": "semantic_contract_id", - "type": 2, - "typeName": "[u8; 32]" + "name": "contract_authority", + "type": 677, + "typeName": "ActorRunAuthority<[u8; 32]>" }, { "docs": [], - "name": "body_commitment", + "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": "runtime_actor_semantics_version", + "name": "opening_predicate_cursor", "type": 14, "typeName": "u32" }, { "docs": [], - "name": "production_weight_identity", - "type": 2, - "typeName": "[u8; 32]" + "name": "opening_predicate_result_count", + "type": 14, + "typeName": "u32" }, { "docs": [], - "name": "body_geometry_version", + "name": "unsuccessful_attempts_at_cursor", "type": 14, "typeName": "u32" }, { "docs": [], - "name": "configured_bounds_commitment", - "type": 2, - "typeName": "[u8; 32]" + "name": "last_attempt_block", + "type": 14, + "typeName": "BlockNumber" }, { "docs": [], - "name": "maximum_lifecycle_weight", - "type": 109, - "typeName": "Weight" + "name": "last_committed_step_block", + "type": 186, + "typeName": "Option" }, { "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": [ + "name": "eligible_at", + "type": 14, + "typeName": "BlockNumber" + }, { "docs": [], - "name": "authority", - "type": 680, - "typeName": "ActorBodyAuthority" + "name": "cumulative_outcomes", + "type": 350, + "typeName": "OutcomeTotals" }, { "docs": [], - "name": "first_step_index", - "type": 14, - "typeName": "u32" + "name": "last_step_outcome", + "type": 678, + "typeName": "Option" }, - { "docs": [], "name": "steps", "type": 677, "typeName": "Steps" }, { "docs": [], - "name": "step_resources", - "type": 678, - "typeName": "Resources" + "name": "suspension", + "type": 681, + "typeName": "Option" } ] }, "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": [] + "params": [{ "name": "BlockNumber", "type": 14 }], + "path": ["pallet_deos_actors", "types", "lifecycle", "ActorRunHead"] }, { "def": { "tag": "composite", "value": [ - { "docs": [], "name": "actor_id", "type": 10, "typeName": "ActorId" }, { "docs": [], "name": "semantic_contract_id", @@ -5700,74 +5579,241 @@ } ] }, - "id": 680, - "params": [ - { "name": "ActorId", "type": 10 }, - { "name": "Hash", "type": 2 } - ], - "path": ["pallet_deos_actors", "types", "contract", "ActorBodyAuthority"] + "id": 677, + "params": [{ "name": "Hash", "type": 2 }], + "path": ["pallet_deos_actors", "types", "lifecycle", "ActorRunAuthority"] }, { "def": { - "tag": "composite", + "tag": "variant", "value": [ + { "docs": [], "fields": [], "index": 0, "name": "None" }, { "docs": [], - "name": "funding_accumulated", - "type": 682, - "typeName": "FundingAccumulated" - }, - { - "docs": [], - "name": "funding_tracked_assets", - "type": 684, - "typeName": "FundingTrackedAssets" + "fields": [{ "docs": [], "type": 679 }], + "index": 1, + "name": "Some" + } + ] + }, + "id": 678, + "params": [{ "name": "T", "type": 679 }], + "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": 680, "typeName": "crate::TaskFailure" } + ], + "index": 4, + "name": "Failed" + } + ] + }, + "id": 679, + "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": 680, + "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": 681, + "params": [{ "name": "T", "type": 351 }], + "path": ["Option"] + }, + { + "def": { + "tag": "composite", + "value": [ + { + "docs": [], + "name": "opening_snapshot", + "type": 683, + "typeName": "BoundedBTreeMap, Balance, MaxSnapshotEntries>" + }, + { + "docs": [], + "name": "opening_predicate_results", + "type": 688, + "typeName": "BoundedVec, MaxOpeningPredicateResults>" + }, + { + "docs": [], + "name": "funding_snapshot", + "type": 672, + "typeName": "BoundedBTreeMap" + } + ] + }, + "id": 682, "params": [ - { "name": "FundingAccumulated", "type": 682 }, - { "name": "FundingTrackedAssets", "type": 684 } + { "name": "AssetId", "type": 197 }, + { "name": "Balance", "type": 49 }, + { "name": "MaxSnapshotEntries" }, + { "name": "MaxFundingTrackedAssets" }, + { "name": "MaxOpeningPredicateResults" } ], - "path": ["pallet_deos_actors", "types", "contract", "ActorFundingState"] + "path": ["pallet_deos_actors", "types", "lifecycle", "ActorRunPayload"] }, { "def": { "tag": "composite", - "value": [{ "docs": [], "type": 683, "typeName": "BTreeMap" }] + "value": [{ "docs": [], "type": 685, "typeName": "BTreeMap" }] }, - "id": 682, + "id": 683, "params": [ - { "name": "K", "type": 197 }, + { "name": "K", "type": 684 }, { "name": "V", "type": 49 }, { "name": "S" } ], "path": ["bounded_collections", "bounded_btree_map", "BoundedBTreeMap"] }, { - "def": { "tag": "composite", "value": [{ "docs": [], "type": 320 }] }, - "id": 683, + "def": { + "tag": "variant", + "value": [ + { + "docs": [], + "fields": [{ "docs": [], "type": 197, "typeName": "AssetId" }], + "index": 0, + "name": "PreservableAsset" + }, + { + "docs": [], + "fields": [{ "docs": [], "type": 197, "typeName": "AssetId" }], + "index": 1, + "name": "TargetAsset" + }, + { + "docs": [], + "fields": [{ "docs": [], "type": 197, "typeName": "AssetId" }], + "index": 2, + "name": "StakingShares" + } + ] + }, + "id": 684, + "params": [{ "name": "AssetId", "type": 197 }], + "path": ["pallet_deos_actors", "types", "contract", "OpeningSurface"] + }, + { + "def": { "tag": "composite", "value": [{ "docs": [], "type": 686 }] }, + "id": 685, "params": [ - { "name": "K", "type": 197 }, + { "name": "K", "type": 684 }, { "name": "V", "type": 49 } ], "path": ["BTreeMap"] }, + { + "def": { "tag": "sequence", "value": 687 }, + "id": 686, + "params": [], + "path": [] + }, + { + "def": { "tag": "tuple", "value": [684, 49] }, + "id": 687, + "params": [], + "path": [] + }, { "def": { "tag": "composite", - "value": [{ "docs": [], "type": 685, "typeName": "BTreeSet" }] + "value": [{ "docs": [], "type": 691, "typeName": "Vec" }] }, - "id": 684, - "params": [{ "name": "T", "type": 197 }, { "name": "S" }], - "path": ["bounded_collections", "bounded_btree_set", "BoundedBTreeSet"] + "id": 688, + "params": [{ "name": "T", "type": 689 }, { "name": "S" }], + "path": ["bounded_collections", "bounded_vec", "BoundedVec"] }, { - "def": { "tag": "composite", "value": [{ "docs": [], "type": 232 }] }, - "id": 685, - "params": [{ "name": "T", "type": 197 }], - "path": ["BTreeSet"] + "def": { + "tag": "variant", + "value": [ + { + "docs": [], + "fields": [{ "docs": [], "type": 50 }], + "index": 0, + "name": "Ok" + }, + { + "docs": [], + "fields": [{ "docs": [], "type": 690 }], + "index": 1, + "name": "Err" + } + ] + }, + "id": 689, + "params": [ + { "name": "T", "type": 50 }, + { "name": "E", "type": 690 } + ], + "path": ["Result"] + }, + { + "def": { + "tag": "variant", + "value": [ + { "docs": [], "fields": [], "index": 0, "name": "InvalidObservation" } + ] + }, + "id": 690, + "params": [], + "path": ["pallet_deos_actors", "types", "contract", "PredicateError"] + }, + { + "def": { "tag": "sequence", "value": 689 }, + "id": 691, + "params": [], + "path": [] }, { "def": { @@ -5775,77 +5821,183 @@ "value": [ { "docs": [], - "name": "contract_authority", - "type": 687, - "typeName": "ActorRunAuthority<[u8; 32]>" + "name": "sovereign_account", + "type": 1, + "typeName": "AccountId" }, + { "docs": [], "name": "owner", "type": 1, "typeName": "AccountId" }, { "docs": [], - "name": "payload_commitment", - "type": 2, - "typeName": "[u8; 32]" + "name": "actor_class", + "type": 346, + "typeName": "ActorClass" }, - { "docs": [], "name": "cycle_nonce", "type": 10, "typeName": "u64" }, - { "docs": [], "name": "cursor", "type": 14, "typeName": "u32" }, { "docs": [], - "name": "opening_predicate_cursor", - "type": 14, - "typeName": "u32" + "name": "mutability", + "type": 224, + "typeName": "Mutability" }, + { "docs": [], "name": "cycle_nonce", "type": 10, "typeName": "u64" }, { "docs": [], - "name": "opening_predicate_result_count", + "name": "last_control_mutation_block", "type": 14, - "typeName": "u32" + "typeName": "BlockNumber" + } + ] + }, + "id": 692, + "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": 694, + "typeName": "ActorStateHoldBreakdown" + } + ] + }, + "id": 693, + "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": "unsuccessful_attempts_at_cursor", - "type": 14, - "typeName": "u32" + "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": 694, + "params": [{ "name": "Balance", "type": 49 }], + "path": [ + "pallet_deos_actors", + "types", + "lifecycle", + "ActorStateHoldBreakdown" + ] + }, + { + "def": { + "tag": "variant", + "value": [ + { "docs": [], "fields": [], "index": 0, "name": "Vacant" }, + { + "docs": [], + "fields": [{ "docs": [], "type": 10, "typeName": "ActorId" }], + "index": 1, + "name": "Occupied" + } + ] + }, + "id": 695, + "params": [], + "path": [ + "pallet_deos_actors", + "types", + "lifecycle", + "SystemSovereignState" + ] + }, + { + "def": { "tag": "tuple", "value": [14, 10] }, + "id": 696, + "params": [], + "path": [] + }, + { + "def": { + "tag": "composite", + "value": [ { "docs": [], - "name": "last_attempt_block", + "name": "block_number", "type": 14, "typeName": "BlockNumber" }, { "docs": [], - "name": "last_committed_step_block", - "type": 186, - "typeName": "Option" + "name": "phase", + "type": 698, + "typeName": "BlockResourcePhase" }, { "docs": [], - "name": "eligible_at", - "type": 14, - "typeName": "BlockNumber" + "name": "usage", + "type": 699, + "typeName": "BlockResourceUsage" }, { "docs": [], - "name": "cumulative_outcomes", - "type": 350, - "typeName": "OutcomeTotals" + "name": "outstanding_reservations", + "type": 14, + "typeName": "u32" }, { "docs": [], - "name": "last_step_outcome", - "type": 688, - "typeName": "Option" + "name": "finalized_fixed_reserved", + "type": 161, + "typeName": "Option" }, { "docs": [], - "name": "suspension", - "type": 691, - "typeName": "Option" + "name": "optional_actor_work_halted", + "type": 50, + "typeName": "bool" } ] }, - "id": 686, + "id": 697, "params": [{ "name": "BlockNumber", "type": 14 }], - "path": ["pallet_deos_actors", "types", "lifecycle", "ActorRunHead"] + "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": 698, + "params": [], + "path": ["pallet_deos_actors", "types", "resource", "BlockResourcePhase"] }, { "def": { @@ -5853,270 +6005,180 @@ "value": [ { "docs": [], - "name": "semantic_contract_id", - "type": 2, - "typeName": "Hash" + "name": "actor_control", + "type": 109, + "typeName": "Weight" }, { "docs": [], - "name": "body_commitment", - "type": 2, - "typeName": "Hash" + "name": "actor_effect", + "type": 109, + "typeName": "Weight" }, { "docs": [], - "name": "admission_identity", - "type": 2, - "typeName": "Hash" + "name": "user_dispatch", + "type": 109, + "typeName": "Weight" } ] }, - "id": 687, - "params": [{ "name": "Hash", "type": 2 }], - "path": ["pallet_deos_actors", "types", "lifecycle", "ActorRunAuthority"] + "id": 699, + "params": [], + "path": ["pallet_deos_actors", "types", "resource", "BlockResourceUsage"] }, { "def": { - "tag": "variant", + "tag": "composite", "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" }, + "name": "block_number", + "type": 14, + "typeName": "BlockNumber" + }, { "docs": [], - "fields": [ - { "docs": [], "type": 353, "typeName": "StepSkippedReason" } - ], - "index": 2, - "name": "Skipped" + "name": "fixed_reserved", + "type": 109, + "typeName": "Weight" }, { "docs": [], - "fields": [], - "index": 3, - "name": "FundingUnavailable" + "name": "usage", + "type": 699, + "typeName": "BlockResourceUsage" }, { "docs": [], - "fields": [ - { "docs": [], "type": 690, "typeName": "crate::TaskFailure" } - ], - "index": 4, - "name": "Failed" + "name": "optional_actor_work_halted", + "type": 50, + "typeName": "bool" } ] }, - "id": 689, - "params": [], - "path": ["pallet_deos_actors", "types", "lifecycle", "StepOutcome"] + "id": 700, + "params": [{ "name": "BlockNumber", "type": 14 }], + "path": [ + "pallet_deos_actors", + "types", + "resource", + "FinalizedBlockResourceSnapshot" + ] }, { "def": { "tag": "composite", "value": [ + { "docs": [], "name": "actor_id", "type": 10, "typeName": "ActorId" }, { "docs": [], - "name": "error", - "type": 290, - "typeName": "DispatchError" + "name": "identity", + "type": 703, + "typeName": "ActorControlIdentity" }, - { "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": [ + "name": "hot", + "type": 704, + "typeName": "ActorControlHotState" + }, + { "docs": [], "name": "cursor", "type": 14, "typeName": "u32" }, { "docs": [], - "name": "opening_snapshot", - "type": 693, - "typeName": "BoundedBTreeMap, Balance, MaxSnapshotEntries>" + "name": "eligible_at", + "type": 186, + "typeName": "Option" }, { "docs": [], - "name": "opening_predicate_results", - "type": 698, - "typeName": "BoundedVec, MaxOpeningPredicateResults>" + "name": "admission", + "type": 702, + "typeName": "Admission" }, { "docs": [], - "name": "funding_snapshot", - "type": 682, - "typeName": "BoundedBTreeMap" + "name": "resources", + "type": 663, + "typeName": "ActorStepResourceEnvelope" } ] }, - "id": 692, + "id": 701, "params": [ - { "name": "AssetId", "type": 197 }, - { "name": "Balance", "type": 49 }, - { "name": "MaxSnapshotEntries" }, - { "name": "MaxFundingTrackedAssets" }, - { "name": "MaxOpeningPredicateResults" } + { "name": "AccountId", "type": 1 }, + { "name": "BlockNumber", "type": 14 }, + { "name": "Admission", "type": 702 } ], - "path": ["pallet_deos_actors", "types", "lifecycle", "ActorRunPayload"] + "path": ["pallet_deos_actors", "pallet", "ActorControlCell"] }, { "def": { "tag": "composite", - "value": [{ "docs": [], "type": 695, "typeName": "BTreeMap" }] - }, - "id": 693, - "params": [ - { "name": "K", "type": 694 }, - { "name": "V", "type": 49 }, - { "name": "S" } - ], - "path": ["bounded_collections", "bounded_btree_map", "BoundedBTreeMap"] - }, - { - "def": { - "tag": "variant", "value": [ { "docs": [], - "fields": [{ "docs": [], "type": 197, "typeName": "AssetId" }], - "index": 0, - "name": "PreservableAsset" + "name": "semantic_contract_id", + "type": 2, + "typeName": "[u8; 32]" }, { "docs": [], - "fields": [{ "docs": [], "type": 197, "typeName": "AssetId" }], - "index": 1, - "name": "TargetAsset" + "name": "body_commitment", + "type": 2, + "typeName": "[u8; 32]" }, { "docs": [], - "fields": [{ "docs": [], "type": 197, "typeName": "AssetId" }], - "index": 2, - "name": "StakingShares" - } - ] - }, - "id": 694, - "params": [{ "name": "AssetId", "type": 197 }], - "path": ["pallet_deos_actors", "types", "contract", "OpeningSurface"] - }, - { - "def": { "tag": "composite", "value": [{ "docs": [], "type": 696 }] }, - "id": 695, - "params": [ - { "name": "K", "type": 694 }, - { "name": "V", "type": 49 } - ], - "path": ["BTreeMap"] - }, - { - "def": { "tag": "sequence", "value": 697 }, - "id": 696, - "params": [], - "path": [] - }, - { - "def": { "tag": "tuple", "value": [694, 49] }, - "id": 697, - "params": [], - "path": [] - }, - { - "def": { - "tag": "composite", - "value": [{ "docs": [], "type": 701, "typeName": "Vec" }] - }, - "id": 698, - "params": [{ "name": "T", "type": 699 }, { "name": "S" }], - "path": ["bounded_collections", "bounded_vec", "BoundedVec"] - }, - { - "def": { - "tag": "variant", - "value": [ + "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": [], - "fields": [{ "docs": [], "type": 50 }], - "index": 0, - "name": "Ok" + "name": "configured_bounds_commitment", + "type": 2, + "typeName": "[u8; 32]" }, { "docs": [], - "fields": [{ "docs": [], "type": 700 }], - "index": 1, - "name": "Err" + "name": "maximum_lifecycle_weight", + "type": 109, + "typeName": "Weight" + }, + { + "docs": [], + "name": "admission_identity", + "type": 2, + "typeName": "[u8; 32]" } ] }, - "id": 699, - "params": [ - { "name": "T", "type": 50 }, - { "name": "E", "type": 700 } - ], - "path": ["Result"] - }, - { - "def": { - "tag": "variant", - "value": [ - { "docs": [], "fields": [], "index": 0, "name": "InvalidObservation" } - ] - }, - "id": 700, - "params": [], - "path": ["pallet_deos_actors", "types", "contract", "PredicateError"] - }, - { - "def": { "tag": "sequence", "value": 699 }, - "id": 701, - "params": [], - "path": [] + "id": 702, + "params": [{ "name": "Resources" }], + "path": [ + "pallet_deos_actors", + "types", + "contract", + "ActorAdmissionCertificate" + ] }, { "def": { "tag": "composite", "value": [ - { - "docs": [], - "name": "sovereign_account", - "type": 1, - "typeName": "AccountId" - }, { "docs": [], "name": "owner", "type": 1, "typeName": "AccountId" }, { "docs": [], @@ -6139,185 +6201,198 @@ } ] }, - "id": 702, + "id": 703, "params": [ { "name": "AccountId", "type": 1 }, { "name": "BlockNumber", "type": 14 } ], - "path": ["pallet_deos_actors", "types", "lifecycle", "ActorIdentity"] + "path": ["pallet_deos_actors", "pallet", "ActorControlIdentity"] }, { "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" }, + "name": "lifecycle", + "type": 705, + "typeName": "ActiveLifecycle" + }, { "docs": [], - "name": "contract_head", - "type": 49, - "typeName": "Balance" + "name": "cycle_state", + "type": 706, + "typeName": "CycleState" }, { "docs": [], - "name": "contract_body", - "type": 49, - "typeName": "Balance" + "name": "trigger_runtime_state", + "type": 707, + "typeName": "TriggerRuntimeState" }, - { "docs": [], "name": "detector", "type": 49, "typeName": "Balance" }, - { "docs": [], "name": "funding", "type": 49, "typeName": "Balance" }, - { "docs": [], "name": "run", "type": 49, "typeName": "Balance" } + { + "docs": [], + "name": "unsuccessful_attempt_streak", + "type": 14, + "typeName": "u32" + }, + { + "docs": [], + "name": "pending_signal", + "type": 50, + "typeName": "bool" + }, + { + "docs": [], + "name": "wakeup_pointer", + "type": 709, + "typeName": "Option>" + }, + { + "docs": [], + "name": "trigger_wakeup_pointer", + "type": 711, + "typeName": "Option" + }, + { + "docs": [], + "name": "terminal_at", + "type": 186, + "typeName": "Option" + }, + { + "docs": [], + "name": "schedule_anchor", + "type": 14, + "typeName": "BlockNumber" + }, + { + "docs": [], + "name": "last_cycle_block", + "type": 186, + "typeName": "Option" + } ] }, "id": 704, - "params": [{ "name": "Balance", "type": 49 }], - "path": [ - "pallet_deos_actors", - "types", - "lifecycle", - "ActorStateHoldBreakdown" - ] + "params": [{ "name": "BlockNumber", "type": 14 }], + "path": ["pallet_deos_actors", "pallet", "ActorControlHotState"] }, { "def": { "tag": "variant", "value": [ - { "docs": [], "fields": [], "index": 0, "name": "Vacant" }, - { - "docs": [], - "fields": [{ "docs": [], "type": 10, "typeName": "ActorId" }], - "index": 1, - "name": "Occupied" - } + { "docs": [], "fields": [], "index": 0, "name": "Active" }, + { "docs": [], "fields": [], "index": 1, "name": "Paused" } ] }, "id": 705, "params": [], - "path": [ - "pallet_deos_actors", - "types", - "lifecycle", - "SystemSovereignState" - ] + "path": ["pallet_deos_actors", "types", "lifecycle", "ActiveLifecycle"] }, { - "def": { "tag": "tuple", "value": [14, 10] }, + "def": { + "tag": "variant", + "value": [ + { "docs": [], "fields": [], "index": 0, "name": "Idle" }, + { "docs": [], "fields": [], "index": 1, "name": "Running" }, + { "docs": [], "fields": [], "index": 2, "name": "Suspended" } + ] + }, "id": 706, "params": [], - "path": [] + "path": ["pallet_deos_actors", "types", "lifecycle", "CycleState"] }, { "def": { - "tag": "composite", + "tag": "variant", "value": [ + { "docs": [], "fields": [], "index": 0, "name": "Stateless" }, { "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" + "fields": [ + { + "docs": [], + "name": "phase", + "type": 708, + "typeName": "CrossingPhase" + }, + { + "docs": [], + "name": "installed_at_revision", + "type": 10, + "typeName": "u64" + } + ], + "index": 1, + "name": "ObservationCrossing" }, { "docs": [], - "name": "finalized_fixed_reserved", - "type": 161, - "typeName": "Option" + "fields": [ + { + "docs": [], + "name": "anchor_tick", + "type": 189, + "typeName": "Option" + }, + { "docs": [], "name": "consumed", "type": 50, "typeName": "bool" } + ], + "index": 2, + "name": "AtTime" }, { "docs": [], - "name": "optional_actor_work_halted", - "type": 50, - "typeName": "bool" + "fields": [ + { + "docs": [], + "name": "anchor_tick", + "type": 189, + "typeName": "Option" + } + ], + "index": 3, + "name": "Cadenced" } ] }, "id": 707, - "params": [{ "name": "BlockNumber", "type": 14 }], - "path": ["pallet_deos_actors", "types", "resource", "BlockResourceState"] + "params": [], + "path": [ + "pallet_deos_actors", + "types", + "lifecycle", + "TriggerRuntimeState" + ] }, { "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" } + { "docs": [], "fields": [], "index": 0, "name": "Armed" }, + { "docs": [], "fields": [], "index": 1, "name": "WaitingForRearm" } ] }, "id": 708, "params": [], - "path": ["pallet_deos_actors", "types", "resource", "BlockResourcePhase"] + "path": ["pallet_deos_actors", "types", "contract", "CrossingPhase"] }, { "def": { - "tag": "composite", + "tag": "variant", "value": [ + { "docs": [], "fields": [], "index": 0, "name": "None" }, { "docs": [], - "name": "actor_control", - "type": 109, - "typeName": "Weight" - }, - { - "docs": [], - "name": "actor_effect", - "type": 109, - "typeName": "Weight" - }, - { - "docs": [], - "name": "user_dispatch", - "type": 109, - "typeName": "Weight" + "fields": [{ "docs": [], "type": 710 }], + "index": 1, + "name": "Some" } ] }, "id": 709, - "params": [], - "path": ["pallet_deos_actors", "types", "resource", "BlockResourceUsage"] + "params": [{ "name": "T", "type": 710 }], + "path": ["Option"] }, { "def": { @@ -6325,118 +6400,103 @@ "value": [ { "docs": [], - "name": "block_number", - "type": 14, - "typeName": "BlockNumber" - }, - { - "docs": [], - "name": "fixed_reserved", - "type": 109, - "typeName": "Weight" + "name": "block", + "type": 364, + "typeName": "WakeupKey" }, { "docs": [], - "name": "usage", - "type": 709, - "typeName": "BlockResourceUsage" + "name": "page_id", + "type": 10, + "typeName": "WakeupPageId" }, - { - "docs": [], - "name": "optional_actor_work_halted", - "type": 50, - "typeName": "bool" - } + { "docs": [], "name": "slot", "type": 14, "typeName": "WakeupSlot" } ] }, "id": 710, "params": [{ "name": "BlockNumber", "type": 14 }], - "path": [ - "pallet_deos_actors", - "types", - "resource", - "FinalizedBlockResourceSnapshot" - ] + "path": ["pallet_deos_actors", "types", "scheduler", "WakeupPointer"] }, { "def": { - "tag": "composite", - "value": [{ "docs": [], "type": 714, "typeName": "Vec" }] + "tag": "variant", + "value": [ + { "docs": [], "fields": [], "index": 0, "name": "None" }, + { + "docs": [], + "fields": [{ "docs": [], "type": 712 }], + "index": 1, + "name": "Some" + } + ] }, "id": 711, - "params": [{ "name": "T", "type": 712 }, { "name": "S" }], - "path": ["bounded_collections", "bounded_vec", "BoundedVec"] + "params": [{ "name": "T", "type": 712 }], + "path": ["Option"] }, { "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", + "name": "tick", "type": 10, - "typeName": "QueueTicket" + "typeName": "SchedulerTick" }, { "docs": [], - "name": "eligible_at", - "type": 14, - "typeName": "BlockNumber" + "name": "page_id", + "type": 10, + "typeName": "WakeupPageId" }, - { - "docs": [], - "name": "contract_commitment", - "type": 713, - "typeName": "ContractCommitment" - } + { "docs": [], "name": "slot", "type": 14, "typeName": "WakeupSlot" } ] }, "id": 712, - "params": [ - { "name": "BlockNumber", "type": 14 }, - { "name": "ContractCommitment", "type": 713 } - ], - "path": ["pallet_deos_actors", "types", "scheduler", "ActorStepTicket"] + "params": [], + "path": [ + "pallet_deos_actors", + "types", + "scheduler", + "TriggerWakeupPointer" + ] }, { "def": { "tag": "composite", + "value": [{ "docs": [], "type": 715, "typeName": "Vec" }] + }, + "id": 713, + "params": [{ "name": "T", "type": 714 }, { "name": "S" }], + "path": ["bounded_collections", "bounded_vec", "BoundedVec"] + }, + { + "def": { + "tag": "variant", "value": [ + { "docs": [], "fields": [], "index": 0, "name": "None" }, { "docs": [], - "name": "semantic_contract_id", - "type": 2, - "typeName": "Hash" - }, - { - "docs": [], - "name": "body_commitment", - "type": 2, - "typeName": "Hash" + "fields": [{ "docs": [], "type": 701 }], + "index": 1, + "name": "Some" } ] }, - "id": 713, - "params": [{ "name": "Hash", "type": 2 }], - "path": [ - "pallet_deos_actors", - "types", - "contract", - "ActorContractCommitment" - ] + "id": 714, + "params": [{ "name": "T", "type": 701 }], + "path": ["Option"] }, { - "def": { "tag": "sequence", "value": 712 }, - "id": 714, + "def": { "tag": "sequence", "value": 714 }, + "id": 715, "params": [], "path": [] }, { "def": { "tag": "tuple", "value": [364, 10] }, - "id": 715, + "id": 716, "params": [], "path": [] }, @@ -6444,7 +6504,7 @@ "def": { "tag": "composite", "value": [ - { "docs": [], "name": "entries", "type": 717, "typeName": "Entries" }, + { "docs": [], "name": "entries", "type": 718, "typeName": "Entries" }, { "docs": [], "name": "live_entries", "type": 14, "typeName": "u32" }, { "docs": [], @@ -6466,17 +6526,17 @@ } ] }, - "id": 716, - "params": [{ "name": "Entries", "type": 717 }], + "id": 717, + "params": [{ "name": "Entries", "type": 718 }], "path": ["pallet_deos_actors", "types", "scheduler", "WakeupPage"] }, { "def": { "tag": "composite", - "value": [{ "docs": [], "type": 720, "typeName": "Vec" }] + "value": [{ "docs": [], "type": 722, "typeName": "Vec" }] }, - "id": 717, - "params": [{ "name": "T", "type": 718 }, { "name": "S" }], + "id": 718, + "params": [{ "name": "T", "type": 719 }, { "name": "S" }], "path": ["bounded_collections", "bounded_vec", "BoundedVec"] }, { @@ -6486,71 +6546,105 @@ { "docs": [], "fields": [], "index": 0, "name": "None" }, { "docs": [], - "fields": [{ "docs": [], "type": 719 }], + "fields": [{ "docs": [], "type": 720 }], "index": 1, "name": "Some" } ] }, - "id": 718, - "params": [{ "name": "T", "type": 719 }], + "id": 719, + "params": [{ "name": "T", "type": 720 }], "path": ["Option"] }, + { + "def": { + "tag": "variant", + "value": [ + { + "docs": [], + "fields": [{ "docs": [], "type": 701, "typeName": "Cell" }], + "index": 0, + "name": "Primary" + }, + { + "docs": [], + "fields": [ + { "docs": [], "type": 721, "typeName": "ActorWakeupReference" } + ], + "index": 1, + "name": "Reference" + } + ] + }, + "id": 720, + "params": [{ "name": "Cell", "type": 701 }], + "path": ["pallet_deos_actors", "pallet", "ActorWaitingEntry"] + }, { "def": { "tag": "composite", "value": [ - { "docs": [], "name": "actor_id", "type": 10, "typeName": "ActorId" } + { "docs": [], "name": "actor_id", "type": 10, "typeName": "ActorId" }, + { + "docs": [], + "name": "admission_identity", + "type": 2, + "typeName": "[u8; 32]" + } ] }, - "id": 719, + "id": 721, "params": [], - "path": ["pallet_deos_actors", "types", "scheduler", "WakeupEntry"] + "path": ["pallet_deos_actors", "pallet", "ActorWakeupReference"] }, { - "def": { "tag": "sequence", "value": 718 }, - "id": 720, + "def": { "tag": "sequence", "value": 719 }, + "id": 722, "params": [], "path": [] }, { "def": { - "tag": "composite", + "tag": "variant", "value": [ + { "docs": [], "fields": [], "index": 0, "name": "Unsignaled" }, { "docs": [], - "name": "head_page", - "type": 10, - "typeName": "WakeupPageId" - }, - { - "docs": [], - "name": "tail_page", - "type": 10, - "typeName": "WakeupPageId" - }, - { - "docs": [], - "name": "next_page_id", - "type": 10, - "typeName": "WakeupPageId" + "fields": [ + { + "docs": [], + "name": "key", + "type": 364, + "typeName": "WakeupKey" + }, + { "docs": [], "name": "page", "type": 10, "typeName": "u64" }, + { "docs": [], "name": "slot", "type": 3, "typeName": "u8" } + ], + "index": 1, + "name": "Waiting" }, - { "docs": [], "name": "live_entries", "type": 14, "typeName": "u32" }, { "docs": [], - "name": "cursor_index", - "type": 186, - "typeName": "Option" + "fields": [ + { + "docs": [], + "name": "ticket", + "type": 10, + "typeName": "QueueTicket" + } + ], + "index": 2, + "name": "Ready" } ] }, - "id": 721, - "params": [], - "path": ["pallet_deos_actors", "types", "scheduler", "WakeupBucketState"] + "id": 723, + "params": [{ "name": "BlockNumber", "type": 14 }], + "path": ["pallet_deos_actors", "pallet", "ActorControlLocation"] }, { - "def": { "tag": "tuple", "value": [723, 10] }, - "id": 722, + "def": { "tag": "tuple", "value": [725, 10] }, + "id": 724, "params": [], "path": [] }, @@ -6562,22 +6656,22 @@ { "docs": [], "fields": [], "index": 1, "name": "Tick" } ] }, - "id": 723, + "id": 725, "params": [], "path": ["pallet_deos_actors", "types", "scheduler", "WakeupClock"] }, { "def": { "tag": "composite", - "value": [{ "docs": [], "type": 725, "typeName": "Vec" }] + "value": [{ "docs": [], "type": 727, "typeName": "Vec" }] }, - "id": 724, + "id": 726, "params": [{ "name": "T", "type": 364 }, { "name": "S" }], "path": ["bounded_collections", "bounded_vec", "BoundedVec"] }, { "def": { "tag": "sequence", "value": 364 }, - "id": 725, + "id": 727, "params": [], "path": [] }, @@ -6586,7 +6680,7 @@ "tag": "composite", "value": [{ "docs": [], "type": 651, "typeName": "Vec" }] }, - "id": 726, + "id": 728, "params": [{ "name": "T", "type": 216 }, { "name": "S" }], "path": ["bounded_collections", "bounded_vec", "BoundedVec"] }, @@ -6595,13 +6689,13 @@ "tag": "composite", "value": [{ "docs": [], "type": 203, "typeName": "Vec" }] }, - "id": 727, + "id": 729, "params": [{ "name": "T", "type": 14 }, { "name": "S" }], "path": ["bounded_collections", "bounded_vec", "BoundedVec"] }, { "def": { "tag": "tuple", "value": [216, 14] }, - "id": 728, + "id": 730, "params": [], "path": [] }, @@ -6624,12 +6718,12 @@ { "docs": [], "name": "entries", - "type": 730, + "type": 732, "typeName": "BoundedVec, MaxEntries>" } ] }, - "id": 729, + "id": 731, "params": [{ "name": "MaxEntries" }], "path": [ "pallet_deos_actors", @@ -6641,15 +6735,15 @@ { "def": { "tag": "composite", - "value": [{ "docs": [], "type": 731, "typeName": "Vec" }] + "value": [{ "docs": [], "type": 733, "typeName": "Vec" }] }, - "id": 730, + "id": 732, "params": [{ "name": "T", "type": 189 }, { "name": "S" }], "path": ["bounded_collections", "bounded_vec", "BoundedVec"] }, { "def": { "tag": "sequence", "value": 189 }, - "id": 731, + "id": 733, "params": [], "path": [] }, @@ -6662,7 +6756,7 @@ { "docs": [], "name": "count", "type": 14, "typeName": "u32" } ] }, - "id": 732, + "id": 734, "params": [], "path": [ "pallet_deos_actors", @@ -6684,7 +6778,7 @@ { "docs": [], "name": "latest_cause_provenance", - "type": 734, + "type": 736, "typeName": "crate::TriggerCauseProvenance" }, { @@ -6702,7 +6796,7 @@ { "docs": [], "name": "fanout_cause_provenance", - "type": 734, + "type": 736, "typeName": "crate::TriggerCauseProvenance" }, { @@ -6744,18 +6838,18 @@ { "docs": [], "name": "previous_dirty_feed", - "type": 735, + "type": 737, "typeName": "Option" }, { "docs": [], "name": "next_dirty_feed", - "type": 735, + "type": 737, "typeName": "Option" } ] }, - "id": 733, + "id": 735, "params": [ { "name": "FeedId", "type": 216 }, { "name": "BlockNumber", "type": 14 } @@ -6775,7 +6869,7 @@ { "docs": [], "fields": [], "index": 1, "name": "Deferred" } ] }, - "id": 734, + "id": 736, "params": [], "path": [ "pallet_deos_actors", @@ -6797,7 +6891,7 @@ } ] }, - "id": 735, + "id": 737, "params": [{ "name": "T", "type": 216 }], "path": ["Option"] }, @@ -6808,25 +6902,25 @@ { "docs": [], "name": "head", - "type": 735, + "type": 737, "typeName": "Option" }, { "docs": [], "name": "tail", - "type": 735, + "type": 737, "typeName": "Option" }, { "docs": [], "name": "cursor", - "type": 735, + "type": 737, "typeName": "Option" }, { "docs": [], "name": "count", "type": 14, "typeName": "u32" } ] }, - "id": 736, + "id": 738, "params": [{ "name": "FeedId", "type": 216 }], "path": [ "pallet_deos_actors", @@ -6842,7 +6936,7 @@ { "docs": [], "name": "key", - "type": 738, + "type": 740, "typeName": "CrossingLeafKey" }, { "docs": [], "name": "page", "type": 14, "typeName": "u32" }, @@ -6850,7 +6944,7 @@ { "docs": [], "name": "generation", "type": 10, "typeName": "u64" } ] }, - "id": 737, + "id": 739, "params": [{ "name": "FeedId", "type": 216 }], "path": [ "pallet_deos_actors", @@ -6867,7 +6961,7 @@ { "docs": [], "name": "traversal", - "type": 739, + "type": 741, "typeName": "CrossingTraversal" }, { @@ -6878,7 +6972,7 @@ } ] }, - "id": 738, + "id": 740, "params": [{ "name": "FeedId", "type": 216 }], "path": ["pallet_deos_actors", "types", "observation", "CrossingLeafKey"] }, @@ -6890,7 +6984,7 @@ { "docs": [], "fields": [], "index": 1, "name": "Downward" } ] }, - "id": 739, + "id": 741, "params": [], "path": [ "pallet_deos_actors", @@ -6900,8 +6994,8 @@ ] }, { - "def": { "tag": "tuple", "value": [738, 14] }, - "id": 740, + "def": { "tag": "tuple", "value": [740, 14] }, + "id": 742, "params": [], "path": [] }, @@ -6912,12 +7006,12 @@ { "docs": [], "name": "entries", - "type": 742, + "type": 744, "typeName": "BoundedVec" } ] }, - "id": 741, + "id": 743, "params": [{ "name": "MaxEntries" }], "path": [ "pallet_deos_actors", @@ -6929,10 +7023,10 @@ { "def": { "tag": "composite", - "value": [{ "docs": [], "type": 744, "typeName": "Vec" }] + "value": [{ "docs": [], "type": 746, "typeName": "Vec" }] }, - "id": 742, - "params": [{ "name": "T", "type": 743 }, { "name": "S" }], + "id": 744, + "params": [{ "name": "T", "type": 745 }, { "name": "S" }], "path": ["bounded_collections", "bounded_vec", "BoundedVec"] }, { @@ -6955,13 +7049,13 @@ } ] }, - "id": 743, + "id": 745, "params": [], "path": ["pallet_deos_actors", "types", "observation", "CrossingMember"] }, { - "def": { "tag": "sequence", "value": 743 }, - "id": 744, + "def": { "tag": "sequence", "value": 745 }, + "id": 746, "params": [], "path": [] }, @@ -6974,7 +7068,7 @@ { "docs": [], "name": "member_count", "type": 14, "typeName": "u32" } ] }, - "id": 745, + "id": 747, "params": [], "path": [ "pallet_deos_actors", @@ -6991,7 +7085,7 @@ { "docs": [], "name": "traversal", - "type": 739, + "type": 741, "typeName": "CrossingTraversal" }, { "docs": [], "name": "depth", "type": 3, "typeName": "u8" }, @@ -7003,7 +7097,7 @@ } ] }, - "id": 746, + "id": 748, "params": [{ "name": "FeedId", "type": 216 }], "path": [ "pallet_deos_actors", @@ -7015,10 +7109,10 @@ { "def": { "tag": "composite", - "value": [{ "docs": [], "type": 749, "typeName": "Vec" }] + "value": [{ "docs": [], "type": 751, "typeName": "Vec" }] }, - "id": 747, - "params": [{ "name": "T", "type": 748 }, { "name": "S" }], + "id": 749, + "params": [{ "name": "T", "type": 750 }, { "name": "S" }], "path": ["bounded_collections", "bounded_vec", "BoundedVec"] }, { @@ -7046,13 +7140,13 @@ { "docs": [], "name": "cause_provenance", - "type": 734, + "type": 736, "typeName": "crate::TriggerCauseProvenance" }, { "docs": [], "name": "cause_block", "type": 10, "typeName": "u64" } ] }, - "id": 748, + "id": 750, "params": [], "path": [ "pallet_deos_actors", @@ -7062,8 +7156,8 @@ ] }, { - "def": { "tag": "sequence", "value": 748 }, - "id": 749, + "def": { "tag": "sequence", "value": 750 }, + "id": 751, "params": [], "path": [] }, @@ -7074,18 +7168,18 @@ { "docs": [], "name": "previous", - "type": 735, + "type": 737, "typeName": "Option" }, { "docs": [], "name": "next", - "type": 735, + "type": 737, "typeName": "Option" } ] }, - "id": 750, + "id": 752, "params": [{ "name": "FeedId", "type": 216 }], "path": [ "pallet_deos_actors", @@ -7101,25 +7195,25 @@ { "docs": [], "name": "head", - "type": 735, + "type": 737, "typeName": "Option" }, { "docs": [], "name": "tail", - "type": 735, + "type": 737, "typeName": "Option" }, { "docs": [], "name": "cursor", - "type": 735, + "type": 737, "typeName": "Option" }, { "docs": [], "name": "count", "type": 14, "typeName": "u32" } ] }, - "id": 751, + "id": 753, "params": [{ "name": "FeedId", "type": 216 }], "path": [ "pallet_deos_actors", @@ -7141,7 +7235,7 @@ { "docs": [], "name": "traversal", - "type": 739, + "type": 741, "typeName": "CrossingTraversal" }, { @@ -7161,7 +7255,7 @@ { "docs": [], "name": "exhausted", "type": 50, "typeName": "bool" } ] }, - "id": 752, + "id": 754, "params": [], "path": [ "pallet_deos_actors", @@ -7203,7 +7297,7 @@ } ] }, - "id": 753, + "id": 755, "params": [], "path": [ "pallet_deos_actors", @@ -7645,7 +7739,7 @@ } ] }, - "id": 754, + "id": 756, "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 index 755b3805..32732805 100644 --- a/web-client/src/lib/automation/actors-cost-vectors.json +++ b/web-client/src/lib/automation/actors-cost-vectors.json @@ -1 +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"} +{"format":"deos.actor.cost-vectors","formatVersion":1,"metadataSha256":"75868ea75fd85e7b79db12650a7407e89e249f5564b2157d55ac46cd26cb5d12","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":"ee228a85fb382b6de55732d2263c7233b0ea7adc1abc419cb2093eceff2887a6"},"prospectivePipelineFee":{"admissionIdentity":"01fcec5cf69c99050b0a0129399bf63de8a5577733bc8e882f2fa867a8c87470","cleanupFee":"72991064","machineFee":"3264872","productionWeightIdentity":"6e4c46e8eb6b731acf5c32962facb6ad511dc097ffcaf7b058ab1f0944617401","strategy":"UpfrontBounded","totalFee":"76255936"},"prospectiveTriggerFee":{"family":"Manual","fee":"10351120","maximumWeight":{"proofSize":"15106","refTime":"1293890000"},"productionWeightIdentity":"c2ddb60ece8ec0e8de99a9d4dc17a8be5f2708d8715872bd33c7dedc3fd56493"},"stateHold":{"basePerComponent":"1000000000","components":{"contractBody":"0","contractHead":"1437000000","detector":"0","funding":"1002000000","identity":"1087000000","run":"2631000000"},"exempt":false,"perEncodedByte":"1000000","total":"6157000000"}},"triggerFamily":"Manual"},{"actorId":"16","contractStepCount":1,"name":"user-manual-1","quote":{"actorType":"User","creationFee":"2000000000","maximumNextActionFee":{"maximumEffectFee":"18347464","maximumEffectWeight":{"proofSize":"29222","refTime":"2293433000"},"productionWeightIdentity":"ee228a85fb382b6de55732d2263c7233b0ea7adc1abc419cb2093eceff2887a6"},"prospectivePipelineFee":{"admissionIdentity":"f947b4c321e98238319fdfb2319ed766e3e3a5d5b0c559a2d48fcabed71450d2","cleanupFee":"72991064","machineFee":"25626423","productionWeightIdentity":"6e4c46e8eb6b731acf5c32962facb6ad511dc097ffcaf7b058ab1f0944617401","strategy":"UpfrontBounded","totalFee":"98617487"},"prospectiveTriggerFee":{"family":"Manual","fee":"10351120","maximumWeight":{"proofSize":"15106","refTime":"1293890000"},"productionWeightIdentity":"c2ddb60ece8ec0e8de99a9d4dc17a8be5f2708d8715872bd33c7dedc3fd56493"},"stateHold":{"basePerComponent":"1000000000","components":{"contractBody":"0","contractHead":"1508000000","detector":"0","funding":"1002000000","identity":"1087000000","run":"2631000000"},"exempt":false,"perEncodedByte":"1000000","total":"6228000000"}},"triggerFamily":"Manual"},{"actorId":"17","contractStepCount":4,"name":"user-manual-4","quote":{"actorType":"User","creationFee":"2000000000","maximumNextActionFee":{"maximumEffectFee":"18347464","maximumEffectWeight":{"proofSize":"29222","refTime":"2293433000"},"productionWeightIdentity":"ee228a85fb382b6de55732d2263c7233b0ea7adc1abc419cb2093eceff2887a6"},"prospectivePipelineFee":{"admissionIdentity":"bb3bf11c8cf96e5965542d770d533e89b00e61e545e55de70621564dfb52c7d3","cleanupFee":"72991064","machineFee":"99532550","productionWeightIdentity":"6e4c46e8eb6b731acf5c32962facb6ad511dc097ffcaf7b058ab1f0944617401","strategy":"UpfrontBounded","totalFee":"172523614"},"prospectiveTriggerFee":{"family":"Manual","fee":"10351120","maximumWeight":{"proofSize":"15106","refTime":"1293890000"},"productionWeightIdentity":"c2ddb60ece8ec0e8de99a9d4dc17a8be5f2708d8715872bd33c7dedc3fd56493"},"stateHold":{"basePerComponent":"1000000000","components":{"contractBody":"1323000000","contractHead":"1508000000","detector":"0","funding":"1002000000","identity":"1087000000","run":"2631000000"},"exempt":false,"perEncodedByte":"1000000","total":"7551000000"}},"triggerFamily":"Manual"},{"actorId":"18","contractStepCount":8,"name":"user-manual-8","quote":{"actorType":"User","creationFee":"2000000000","maximumNextActionFee":{"maximumEffectFee":"18347464","maximumEffectWeight":{"proofSize":"29222","refTime":"2293433000"},"productionWeightIdentity":"ee228a85fb382b6de55732d2263c7233b0ea7adc1abc419cb2093eceff2887a6"},"prospectivePipelineFee":{"admissionIdentity":"f55891912f8d851983616c1575ac6756e78fd0c12504b9e3827fbb7ce4e6eb6b","cleanupFee":"72991064","machineFee":"198024264","productionWeightIdentity":"6e4c46e8eb6b731acf5c32962facb6ad511dc097ffcaf7b058ab1f0944617401","strategy":"UpfrontBounded","totalFee":"271015328"},"prospectiveTriggerFee":{"family":"Manual","fee":"10351120","maximumWeight":{"proofSize":"15106","refTime":"1293890000"},"productionWeightIdentity":"c2ddb60ece8ec0e8de99a9d4dc17a8be5f2708d8715872bd33c7dedc3fd56493"},"stateHold":{"basePerComponent":"1000000000","components":{"contractBody":"1717000000","contractHead":"1508000000","detector":"0","funding":"1002000000","identity":"1087000000","run":"2631000000"},"exempt":false,"perEncodedByte":"1000000","total":"7945000000"}},"triggerFamily":"Manual"},{"actorId":"19","contractStepCount":12,"name":"user-manual-12","quote":{"actorType":"User","creationFee":"2000000000","maximumNextActionFee":{"maximumEffectFee":"18347464","maximumEffectWeight":{"proofSize":"29222","refTime":"2293433000"},"productionWeightIdentity":"ee228a85fb382b6de55732d2263c7233b0ea7adc1abc419cb2093eceff2887a6"},"prospectivePipelineFee":{"admissionIdentity":"768d9c27c92db5de9302bf52d3c56349291f70b87d5217f36a7989930c590e3b","cleanupFee":"72991064","machineFee":"296515978","productionWeightIdentity":"6e4c46e8eb6b731acf5c32962facb6ad511dc097ffcaf7b058ab1f0944617401","strategy":"UpfrontBounded","totalFee":"369507042"},"prospectiveTriggerFee":{"family":"Manual","fee":"10351120","maximumWeight":{"proofSize":"15106","refTime":"1293890000"},"productionWeightIdentity":"c2ddb60ece8ec0e8de99a9d4dc17a8be5f2708d8715872bd33c7dedc3fd56493"},"stateHold":{"basePerComponent":"1000000000","components":{"contractBody":"2111000000","contractHead":"1508000000","detector":"0","funding":"1002000000","identity":"1087000000","run":"2631000000"},"exempt":false,"perEncodedByte":"1000000","total":"8339000000"}},"triggerFamily":"Manual"},{"actorId":"20","contractStepCount":1,"name":"user-address-event-1","quote":{"actorType":"User","creationFee":"2000000000","maximumNextActionFee":{"maximumEffectFee":"18347464","maximumEffectWeight":{"proofSize":"29222","refTime":"2293433000"},"productionWeightIdentity":"ee228a85fb382b6de55732d2263c7233b0ea7adc1abc419cb2093eceff2887a6"},"prospectivePipelineFee":{"admissionIdentity":"1c98dbfbb321986ba8eac9a60023c830ea35f57bd3928c62cdd7c6de5ba9728f","cleanupFee":"72991064","machineFee":"25626423","productionWeightIdentity":"6e4c46e8eb6b731acf5c32962facb6ad511dc097ffcaf7b058ab1f0944617401","strategy":"UpfrontBounded","totalFee":"98617487"},"prospectiveTriggerFee":{"family":"AddressEvent","fee":"10742232","maximumWeight":{"proofSize":"8394","refTime":"1342779000"},"productionWeightIdentity":"c2ddb60ece8ec0e8de99a9d4dc17a8be5f2708d8715872bd33c7dedc3fd56493"},"stateHold":{"basePerComponent":"1000000000","components":{"contractBody":"0","contractHead":"1510000000","detector":"0","funding":"1002000000","identity":"1087000000","run":"2631000000"},"exempt":false,"perEncodedByte":"1000000","total":"6230000000"}},"triggerFamily":"AddressEvent"},{"actorId":"21","contractStepCount":1,"name":"user-observation-change-1","quote":{"actorType":"User","creationFee":"2000000000","maximumNextActionFee":{"maximumEffectFee":"18347464","maximumEffectWeight":{"proofSize":"29222","refTime":"2293433000"},"productionWeightIdentity":"ee228a85fb382b6de55732d2263c7233b0ea7adc1abc419cb2093eceff2887a6"},"prospectivePipelineFee":{"admissionIdentity":"d14b3f3249a66ef3d0b1f08b8660d8214f12eabcc0d776f21e6eb0526a876ffa","cleanupFee":"72991064","machineFee":"25626423","productionWeightIdentity":"6e4c46e8eb6b731acf5c32962facb6ad511dc097ffcaf7b058ab1f0944617401","strategy":"UpfrontBounded","totalFee":"98617487"},"prospectiveTriggerFee":{"family":"ObservationChange","fee":"10530384","maximumWeight":{"proofSize":"8299","refTime":"1316298000"},"productionWeightIdentity":"c2ddb60ece8ec0e8de99a9d4dc17a8be5f2708d8715872bd33c7dedc3fd56493"},"stateHold":{"basePerComponent":"1000000000","components":{"contractBody":"0","contractHead":"1521000000","detector":"1133000000","funding":"1002000000","identity":"1087000000","run":"2631000000"},"exempt":false,"perEncodedByte":"1000000","total":"7374000000"}},"triggerFamily":"ObservationChange"},{"actorId":"22","contractStepCount":1,"name":"user-observation-crossing-1","quote":{"actorType":"User","creationFee":"2000000000","maximumNextActionFee":{"maximumEffectFee":"18347464","maximumEffectWeight":{"proofSize":"29222","refTime":"2293433000"},"productionWeightIdentity":"ee228a85fb382b6de55732d2263c7233b0ea7adc1abc419cb2093eceff2887a6"},"prospectivePipelineFee":{"admissionIdentity":"eb05ed1d3204fadb1a670a7808bc05430d8adfc3c8593f97c2c6cd9ab7e43b96","cleanupFee":"72991064","machineFee":"25626423","productionWeightIdentity":"6e4c46e8eb6b731acf5c32962facb6ad511dc097ffcaf7b058ab1f0944617401","strategy":"UpfrontBounded","totalFee":"98617487"},"prospectiveTriggerFee":{"family":"ObservationCrossing","fee":"88506968","maximumWeight":{"proofSize":"164188","refTime":"11063371000"},"productionWeightIdentity":"c2ddb60ece8ec0e8de99a9d4dc17a8be5f2708d8715872bd33c7dedc3fd56493"},"stateHold":{"basePerComponent":"1000000000","components":{"contractBody":"0","contractHead":"1554000000","detector":"1161000000","funding":"1002000000","identity":"1087000000","run":"2631000000"},"exempt":false,"perEncodedByte":"1000000","total":"7435000000"}},"triggerFamily":"ObservationCrossing"},{"actorId":"23","contractStepCount":1,"name":"user-at-time-1","quote":{"actorType":"User","creationFee":"2000000000","maximumNextActionFee":{"maximumEffectFee":"18347464","maximumEffectWeight":{"proofSize":"29222","refTime":"2293433000"},"productionWeightIdentity":"ee228a85fb382b6de55732d2263c7233b0ea7adc1abc419cb2093eceff2887a6"},"prospectivePipelineFee":{"admissionIdentity":"f9b054a58ee35fbf27a3b7149acfccab38822c06f01f54b2c9d7f0ac72e08ee0","cleanupFee":"72991064","machineFee":"25626423","productionWeightIdentity":"6e4c46e8eb6b731acf5c32962facb6ad511dc097ffcaf7b058ab1f0944617401","strategy":"UpfrontBounded","totalFee":"98617487"},"prospectiveTriggerFee":{"family":"AtTime","fee":"13996488","maximumWeight":{"proofSize":"8451","refTime":"1749561000"},"productionWeightIdentity":"c2ddb60ece8ec0e8de99a9d4dc17a8be5f2708d8715872bd33c7dedc3fd56493"},"stateHold":{"basePerComponent":"1000000000","components":{"contractBody":"0","contractHead":"1516000000","detector":"1020000000","funding":"1002000000","identity":"1087000000","run":"2631000000"},"exempt":false,"perEncodedByte":"1000000","total":"7256000000"}},"triggerFamily":"AtTime"},{"actorId":"24","contractStepCount":1,"name":"user-cadenced-1","quote":{"actorType":"User","creationFee":"2000000000","maximumNextActionFee":{"maximumEffectFee":"18347464","maximumEffectWeight":{"proofSize":"29222","refTime":"2293433000"},"productionWeightIdentity":"ee228a85fb382b6de55732d2263c7233b0ea7adc1abc419cb2093eceff2887a6"},"prospectivePipelineFee":{"admissionIdentity":"d6867f71dedb805b5e931d7067611e07ca65af4c52c7cada69907811cb9a176a","cleanupFee":"72991064","machineFee":"25626423","productionWeightIdentity":"6e4c46e8eb6b731acf5c32962facb6ad511dc097ffcaf7b058ab1f0944617401","strategy":"UpfrontBounded","totalFee":"98617487"},"prospectiveTriggerFee":{"family":"Cadenced","fee":"22488328","maximumWeight":{"proofSize":"8450","refTime":"2811041000"},"productionWeightIdentity":"c2ddb60ece8ec0e8de99a9d4dc17a8be5f2708d8715872bd33c7dedc3fd56493"},"stateHold":{"basePerComponent":"1000000000","components":{"contractBody":"0","contractHead":"1516000000","detector":"1020000000","funding":"1002000000","identity":"1087000000","run":"2631000000"},"exempt":false,"perEncodedByte":"1000000","total":"7256000000"}},"triggerFamily":"Cadenced"},{"actorId":"25","contractStepCount":1,"name":"system-manual-1","quote":{"actorType":"System","creationFee":"0","maximumNextActionFee":{"maximumEffectFee":"0","maximumEffectWeight":{"proofSize":"29222","refTime":"2293433000"},"productionWeightIdentity":"ee228a85fb382b6de55732d2263c7233b0ea7adc1abc419cb2093eceff2887a6"},"prospectivePipelineFee":{"admissionIdentity":"270805341fc265e34b279f52da1b4a7a510d2db312dacb51ecd8017f8489bb29","cleanupFee":"0","machineFee":"0","productionWeightIdentity":"6e4c46e8eb6b731acf5c32962facb6ad511dc097ffcaf7b058ab1f0944617401","strategy":"UpfrontBounded","totalFee":"0"},"prospectiveTriggerFee":{"family":"Manual","fee":"0","maximumWeight":{"proofSize":"15106","refTime":"1293890000"},"productionWeightIdentity":"c2ddb60ece8ec0e8de99a9d4dc17a8be5f2708d8715872bd33c7dedc3fd56493"},"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":"ee228a85fb382b6de55732d2263c7233b0ea7adc1abc419cb2093eceff2887a6"},"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":"9ce37ade391a29d4f60779f1887ad242dfeb4919d25cf0047ffba85d18f33901"} 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 8125aa25..405413d1 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":"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"}]} +{"format":"deos.actor.fee-envelope-vectors","formatVersion":2,"metadataSha256":"75868ea75fd85e7b79db12650a7407e89e249f5564b2157d55ac46cd26cb5d12","weightSha256":"9ce37ade391a29d4f60779f1887ad242dfeb4919d25cf0047ffba85d18f33901","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 e7299945..6f72f555 100644 --- a/web-client/src/lib/automation/actors-protocol-bounds.generated.ts +++ b/web-client/src/lib/automation/actors-protocol-bounds.generated.ts @@ -1,10 +1,10 @@ -/* Generated from Actors runtime metadata 27984891721c42acbce79d4e458e9b40dd6b9a046228438a072f4f2c1bd0f74e; do not edit. */ -export const ACTORS_MAX_CONTRACT_STEPS = 32; +/* Generated from Actors runtime metadata 75868ea75fd85e7b79db12650a7407e89e249f5564b2157d55ac46cd26cb5d12; do not edit. */ +export const ACTORS_MAX_CONTRACT_STEPS = 12; 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 = 64; -export const ACTORS_MAX_OPENING_PREDICATE_RESULTS = 128; +export const ACTORS_MAX_OPENING_SNAPSHOT_ENTRIES = 24; +export const ACTORS_MAX_OPENING_PREDICATE_RESULTS = 48; 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/cost-vectors.ts b/web-client/src/lib/automation/cost-vectors.ts index 6c493ed6..32cd51d6 100644 --- a/web-client/src/lib/automation/cost-vectors.ts +++ b/web-client/src/lib/automation/cost-vectors.ts @@ -302,8 +302,8 @@ export function parseActorCostVectors(value: unknown): ActorCostVectors { .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'); + if (geometry.join(',') !== '0,1,4,8,12') { + throw new Error('Actor cost vectors must cover Manual 0/1/4/8/12 geometry'); } const families = new Set( vectors diff --git a/web-client/src/lib/automation/matching-wasm.ts b/web-client/src/lib/automation/matching-wasm.ts index 802ae47c..18b6e4fe 100644 --- a/web-client/src/lib/automation/matching-wasm.ts +++ b/web-client/src/lib/automation/matching-wasm.ts @@ -14,6 +14,7 @@ import { } from './contract-artifact.ts'; import { type ActorDecodedRuntimeSimulationOutcome, + type ActorRuntimeSimulationBudget, decodeActorRuntimeSimulationResult, } from './runtime-simulation-codec.ts'; @@ -30,6 +31,7 @@ export type ActorMatchingWasmPin = { transactionVersion: number; runtimeApi: string; runtimeApiVersion: number; + simulationBudget: ActorRuntimeSimulationBudget; }; export type ActorRuntimeSimulationOutcome = @@ -51,6 +53,7 @@ export type ActorMatchingWasmProvider = { contractScale: ActorContractHex; actorType: ActorContractArtifact['actorType']; mutability: ActorContractArtifact['mutability']; + simulationBudget: ActorRuntimeSimulationBudget; }): Promise; }; @@ -69,12 +72,56 @@ function validateIndex(value: number, field: string) { } } +function sameWeight( + left: ActorRuntimeSimulationBudget['actorControl'], + right: ActorRuntimeSimulationBudget['actorControl'], +) { + return left.refTime === right.refTime && left.proofSize === right.proofSize; +} + function samePin(left: ActorMatchingWasmPin, right: ActorMatchingWasmPin) { - return (Object.keys(left) as Array).every( - (key) => left[key] === right[key], + return ( + left.contractId === right.contractId && + left.genesisHash === right.genesisHash && + left.blockHash === right.blockHash && + left.blockNumber === right.blockNumber && + left.stateRoot === right.stateRoot && + left.stateSource === right.stateSource && + left.runtimeCodeHash === right.runtimeCodeHash && + left.metadataHash === right.metadataHash && + left.specVersion === right.specVersion && + left.transactionVersion === right.transactionVersion && + left.runtimeApi === right.runtimeApi && + left.runtimeApiVersion === right.runtimeApiVersion && + sameWeight( + left.simulationBudget.actorControl, + right.simulationBudget.actorControl, + ) && + sameWeight( + left.simulationBudget.sharedEconomic, + right.simulationBudget.sharedEconomic, + ) ); } +function validateWeight( + value: ActorRuntimeSimulationBudget['actorControl'], + field: string, +) { + const maximum = (1n << 64n) - 1n; + if ( + value == null || + typeof value.refTime !== 'bigint' || + typeof value.proofSize !== 'bigint' || + value.refTime < 0n || + value.refTime > maximum || + value.proofSize < 0n || + value.proofSize > maximum + ) { + throw new Error(`${field} must contain unsigned 64-bit Weight components`); + } +} + export async function runActorMatchingWasmSimulation(input: { artifact: ActorContractArtifact; actorId: bigint; @@ -90,6 +137,7 @@ export async function runActorMatchingWasmSimulation(input: { }; runtimeApi: string; runtimeApiVersion: number; + simulationBudget: ActorRuntimeSimulationBudget; provider: ActorMatchingWasmProvider; }): Promise { const inspection = inspectActorContractArtifact( @@ -122,6 +170,19 @@ export async function runActorMatchingWasmSimulation(input: { ); } validateIndex(input.runtimeApiVersion, 'runtimeApiVersion'); + if ( + input.simulationBudget == null || + typeof input.simulationBudget !== 'object' + ) { + throw new Error( + 'simulationBudget must identify both synthetic resource lanes', + ); + } + validateWeight(input.simulationBudget.actorControl, 'actorControl budget'); + validateWeight( + input.simulationBudget.sharedEconomic, + 'sharedEconomic budget', + ); if (input.runtimeApi.trim().length === 0) { throw new Error('runtimeApi must identify the executed runtime method'); } @@ -147,6 +208,7 @@ export async function runActorMatchingWasmSimulation(input: { transactionVersion: input.artifact.transactionVersion, runtimeApi: input.runtimeApi.trim(), runtimeApiVersion: input.runtimeApiVersion, + simulationBudget: input.simulationBudget, }; const response = await input.provider.simulate({ pin, @@ -155,6 +217,7 @@ export async function runActorMatchingWasmSimulation(input: { contractScale: input.artifact.contractScale, actorType: input.artifact.actorType, mutability: input.artifact.mutability, + simulationBudget: input.simulationBudget, }); if (response.engine !== 'RuntimeWasm') { throw new Error('Provider did not attest RuntimeWasm execution'); @@ -211,7 +274,9 @@ function validateOutcome( maxSteps: number, ) { if ( - !['Completed', 'Failed', 'Suspended', 'Closed'].includes(outcome.status) + !['Completed', 'Continued', 'Failed', 'Suspended', 'Closed'].includes( + outcome.status, + ) ) { throw new Error('Unsupported runtime simulation status'); } @@ -224,6 +289,9 @@ function validateOutcome( 'outcome.resultScale must be canonical lowercase SCALE hex', ); } + if (outcome.steps.length > 1) { + throw new Error('Runtime simulation may expose at most one Step record'); + } if (outcome.steps.length > maxSteps) { throw new Error('Runtime Step evidence exceeds admitted Contract Steps'); } @@ -241,9 +309,11 @@ function validateOutcome( } previousStep = step.stepIndex; } - if (outcome.status === 'Suspended') { + if (outcome.status === 'Suspended' || outcome.status === 'Continued') { if (outcome.runCursor == null) { - throw new Error('Suspended runtime outcomes require an Actor run cursor'); + throw new Error( + 'Suspended and Continued runtime outcomes require an Actor run cursor', + ); } validateIndex(outcome.runCursor, 'outcome.runCursor'); if (outcome.runCursor >= maxSteps) { @@ -254,7 +324,7 @@ function validateOutcome( } } else if (outcome.runCursor != null) { throw new Error( - 'Only Suspended runtime outcomes may expose an Actor run cursor', + 'Only Suspended and Continued runtime outcomes may expose an Actor run cursor', ); } } diff --git a/web-client/src/lib/automation/runtime-simulation-codec.ts b/web-client/src/lib/automation/runtime-simulation-codec.ts index 2d0e1659..2ac8d345 100644 --- a/web-client/src/lib/automation/runtime-simulation-codec.ts +++ b/web-client/src/lib/automation/runtime-simulation-codec.ts @@ -17,7 +17,17 @@ import type { ActorContractHex } from './contract-artifact.ts'; export const ACTORS_SIMULATION_RUNTIME_API = 'ActorSimulationApi_simulate_current_contract' as const; -export const ACTORS_SIMULATION_RUNTIME_API_VERSION = 1 as const; +export const ACTORS_SIMULATION_RUNTIME_API_VERSION = 2 as const; + +export type ActorRuntimeSimulationWeight = { + refTime: bigint; + proofSize: bigint; +}; + +export type ActorRuntimeSimulationBudget = { + actorControl: ActorRuntimeSimulationWeight; + sharedEconomic: ActorRuntimeSimulationWeight; +}; export type ActorRuntimeStepOutcome = | { type: 'Executed' } @@ -31,7 +41,7 @@ export type ActorRuntimeStepOutcome = }; export type ActorDecodedRuntimeSimulationOutcome = { - status: 'Completed' | 'Failed' | 'Suspended' | 'Closed'; + status: 'Completed' | 'Continued' | 'Failed' | 'Suspended' | 'Closed'; closeReason: string | null; cycleNonce: bigint; startCursor: number; @@ -64,6 +74,7 @@ const INPUT_NAMES = [ 'expected_mutability', 'expected_contract', 'mode', + 'budget', ]; function bytesToHex(bytes: Uint8Array): ActorContractHex { @@ -94,7 +105,7 @@ function metadataMethod(metadataBytes: Uint8Array) { apis[0].version !== ACTORS_SIMULATION_RUNTIME_API_VERSION ) { throw new Error( - 'Metadata must expose ActorSimulationApi version 1 exactly once', + `Metadata must expose ActorSimulationApi version ${ACTORS_SIMULATION_RUNTIME_API_VERSION} exactly once`, ); } const methods = apis[0].methods.filter( @@ -171,7 +182,11 @@ function projectOutcome(value: unknown): ActorDecodedRuntimeSimulationOutcome { const outcome = asRecord(value, 'simulation outcome'); const parsedStatus = asVariant(outcome.status, 'simulation status'); const status = parsedStatus.type; - if (!['Completed', 'Failed', 'Suspended', 'Closed'].includes(status)) { + if ( + !['Completed', 'Continued', 'Failed', 'Suspended', 'Closed'].includes( + status, + ) + ) { throw new Error(`Unsupported runtime simulation status ${status}`); } const closeReason = @@ -185,6 +200,9 @@ function projectOutcome(value: unknown): ActorDecodedRuntimeSimulationOutcome { if (!Array.isArray(outcome.steps)) { throw new Error('steps must be an ordered runtime array'); } + if (outcome.steps.length > 1) { + throw new Error('steps must contain at most one runtime record'); + } return { status: status as ActorDecodedRuntimeSimulationOutcome['status'], closeReason, diff --git a/web-client/src/lib/index.ts b/web-client/src/lib/index.ts index 0fa568c7..3a84c92c 100644 --- a/web-client/src/lib/index.ts +++ b/web-client/src/lib/index.ts @@ -23,6 +23,7 @@ export { } from './adapters/blockchain'; export { runDeosActorFinalizedSimulation, + type ActorFinalizedSimulationBudget, type ActorFinalizedSimulationInput, type ActorFinalizedSimulationMode, } from './adapters/blockchain/actor-simulation'; diff --git a/web-client/src/lib/observation/runtime-evidence.generated.ts b/web-client/src/lib/observation/runtime-evidence.generated.ts index 11ac3f76..2ba5c77a 100644 --- a/web-client/src/lib/observation/runtime-evidence.generated.ts +++ b/web-client/src/lib/observation/runtime-evidence.generated.ts @@ -15,14 +15,14 @@ export const DEOS_OBSERVATION_RUNTIME_EVIDENCE = { transactionVersion: 1, }, runtimeCodeHash: - '0x4107df9ac3820be5a4f3799884fc33259af10871d2df9d22df0885d96b21eb43', + '0x16f95188d3ce591c43ea500bbb14e1a889051421625c39cd5144fb9f8432dcf9', metadataHash: - '0x89918fe545596de2017c4ea300ff442ed883a02c618c67c04a3be34291bb4919', + '0x63a1cce08329da68dafba9cd377e8f3b750641ed96d724355ec3a6f22f1e50fd', metadataSha256: - '27984891721c42acbce79d4e458e9b40dd6b9a046228438a072f4f2c1bd0f74e', - descriptorIdentity: '0.1.0-autogenerated.16302145885248040146', + '75868ea75fd85e7b79db12650a7407e89e249f5564b2157d55ac46cd26cb5d12', + descriptorIdentity: '0.1.0-autogenerated.17618355743500111702', weightIdentity: - 'sha256:ac206ec06b3f2c2789da23540ca1ae87d343c8d2196f77b1c12c43569c0d3b9e', + 'sha256:9ce37ade391a29d4f60779f1887ad242dfeb4919d25cf0047ffba85d18f33901', certifiedPublishers: ['DEOS Oracle::OnObservationChanged'], fanout: { configuredServiceUnitsPerBlock: 64, @@ -31,11 +31,11 @@ export const DEOS_OBSERVATION_RUNTIME_EVIDENCE = { proofSize: 1000000, }, fanoutBaseWeight: { - refTime: 56285000, + refTime: 56775000, proofSize: 1629, }, serviceUnitWeight: { - refTime: 36276713000, + refTime: 45884158000, proofSize: 304734, }, maxServiceUnitsPerBlock: 3, diff --git a/web-client/src/lib/widgets/AutomationWidget.svelte b/web-client/src/lib/widgets/AutomationWidget.svelte index a8052270..5f7b16e6 100644 --- a/web-client/src/lib/widgets/AutomationWidget.svelte +++ b/web-client/src/lib/widgets/AutomationWidget.svelte @@ -71,7 +71,7 @@ Zone: Presentation widget; composes system projections, automation capabilities, const automationProvenance = fromClientBoundedProjection( true, - 'automationWidget <- Actors.ActorIdentities + Actors.ActorHot + Actors.ActorContract + Actors.ActorRunState + System.Account + ActorEligibilityApi', + 'automationWidget <- bounded Actors control locator/cell projection + Actors.ActorContractHead + Actors.ActorRunHead + Actors.ActorFunding + System.Account + ActorEligibilityApi', ).provenance; function syncViewport() { diff --git a/web-client/src/lib/widgets/WikiWidget.svelte b/web-client/src/lib/widgets/WikiWidget.svelte index cbe22cf5..8857eeb6 100644 --- a/web-client/src/lib/widgets/WikiWidget.svelte +++ b/web-client/src/lib/widgets/WikiWidget.svelte @@ -108,10 +108,6 @@ Zone: Presentation widget; consumes repo-local wiki metadata and trusted wiki he ); } - function localizedSources(value: Record) { - return value[currentLocale] ?? value[wikiState.default_locale] ?? []; - } - function repoWikiPath(path: string) { return `wiki/${path}`; } @@ -289,9 +285,6 @@ Zone: Presentation widget; consumes repo-local wiki metadata and trusted wiki he const selectedPageState = $derived( selectedPageId ? (wikiState.pages[selectedPageId] ?? null) : null, ); - const selectedPageSources = $derived( - selectedPageState ? localizedSources(selectedPageState.sources) : [], - ); const relatedItems: RelatedWikiItem[] = $derived.by(() => { if (!selectedPageId) { return []; @@ -368,7 +361,6 @@ Zone: Presentation widget; consumes repo-local wiki metadata and trusted wiki he provenance: 'Происхождение сведений', status: 'Статус', generatedAt: 'Собрано', - sources: 'Источники', aliasMatch: 'Псевдоним', clearSearch: 'Очистить', copyPath: 'Скопировать путь', @@ -401,7 +393,6 @@ Zone: Presentation widget; consumes repo-local wiki metadata and trusted wiki he provenance: 'Compiled provenance', status: 'Status', generatedAt: 'Generated', - sources: 'Sources', aliasMatch: 'Alias', clearSearch: 'Clear', copyPath: 'Copy path', @@ -728,27 +719,6 @@ Zone: Presentation widget; consumes repo-local wiki metadata and trusted wiki he
{wikiState.generated_at}
- {#if selectedPageSources.length > 0} -
-
- {widgetText.sources} -
-
- {#each selectedPageSources as sourcePath} - - {/each} -
-
- {/if} {/if} {#if loadingPage} diff --git a/wiki/_meta/aliases.json b/wiki/_meta/aliases.json index 4ce0bda1..75e9ac5c 100644 --- a/wiki/_meta/aliases.json +++ b/wiki/_meta/aliases.json @@ -112,6 +112,8 @@ "$VETO": "token-surfaces", "BLDR": "builder-economy", "$BLDR": "builder-economy", + "BLDR Anchor": "builder-economy", + "BLDR Treasury": "builder-economy", "$BLDR Builder Economy": "builder-economy", "Builder": "builder-economy", "Builder Economy": "builder-economy", @@ -308,6 +310,8 @@ "$VETO": "token-surfaces", "BLDR": "builder-economy", "$BLDR": "builder-economy", + "BLDR Anchor": "builder-economy", + "BLDR Treasury": "builder-economy", "Экономика $BLDR": "builder-economy", "Builder": "builder-economy", "Билдер": "builder-economy", diff --git a/wiki/_meta/state.json b/wiki/_meta/state.json index 741003fa..91e80749 100644 --- a/wiki/_meta/state.json +++ b/wiki/_meta/state.json @@ -382,7 +382,6 @@ "en": [ "../docs/README.md", "../README.md", - "../BACKLOG.md", "../CHANGELOG.md", "../web-client/README.md", "../template/README.md", @@ -391,7 +390,6 @@ "ru": [ "../docs/README.md", "../README.md", - "../BACKLOG.md", "../CHANGELOG.md", "../web-client/README.md", "../template/README.md", @@ -694,13 +692,15 @@ "../BACKLOG.md", "../CHANGELOG.md", "../web-client/README.md", - "../web-client/docs/architecture.en.md" + "../web-client/docs/architecture.en.md", + "../template/pallets/actors/docs/architecture.en.md" ], "ru": [ "../BACKLOG.md", "../CHANGELOG.md", "../web-client/README.md", - "../web-client/docs/architecture.en.md" + "../web-client/docs/architecture.en.md", + "../template/pallets/actors/docs/architecture.en.md" ] } }, diff --git a/wiki/concepts/builder-economy.en.md b/wiki/concepts/builder-economy.en.md index 4eb66bfe..798ea1d7 100644 --- a/wiki/concepts/builder-economy.en.md +++ b/wiki/concepts/builder-economy.en.md @@ -10,8 +10,10 @@ available_locales: - ru sources: - resource: ../../docs/README.md - - resource: ../../docs/manifesto.en.md + - resource: ../../docs/builder-economy.contract.en.md - resource: ../../docs/framework-instance.contract.en.md + - resource: ../../docs/tmctol.specification.en.md + - resource: ../../docs/manifesto.en.md - resource: ../../template/pallets/governance/docs/specification.en.md - resource: ../../template/pallets/governance/docs/architecture.en.md - resource: ../../template/pallets/actors/docs/architecture.en.md @@ -65,32 +67,37 @@ A tactical treasury invoice declares: - A beneficiary; - A payout asset; - A base amount; -- The governance-declared BLDR Treasury funding source. +- The explicit governance-approved BLDR Treasury sovereign account; +- A bounded canonical CIDv1 for the content-addressed invoice document and evidence. The `$BLDR` primary track evaluates the invoice through four options: -- `Amplify` — pay `2.0x` the base amount; -- `Approve` — pay `1.0x`; -- `Reduce` — pay `0.5x`; +- `Amplify` — target `2.0x` the base amount, with only the premium above `1.0x` capacity-capped; +- `Approve` — require and pay `1.0x`; +- `Reduce` — require and pay `0.5x`; - `Nay` — reject with no payout. -Native staking power forms the separate `Pass / Veto` protection track. It protects the constitutional boundary but does not price the work. Approved execution is transactional: the full bounded payout succeeds or the runtime records an explicit failure without a partial payment. +Native staking power forms the separate `Pass / Veto` protection track. It protects the constitutional boundary but does not price the work. If an above-base target is `2.0x` and enactment-time capacity is `1.5x`, the complete `1.5x` pays atomically. Capacity below `1.0x` fails with no payout. Targets at or below base require their complete amount and never clip further. + +BLDR Treasury is a Mutable System Actor treasury. Its own bounded Actor Contract or an earlier invoice may spend from the same sovereign custody between submission and enactment, and proposals create no balance reservation. Execution order may therefore reduce only an above-base premium or cause a below-floor failure requiring a new vote after replenishment. Governance may debit only the approved Builder treasury account and does not mutate the treasury's Actor Contract or gain authority over BLDR Anchor. ## Economic Wiring -The reference `$BLDR` TMC flow gives labor funding its own capital circuit: +The `$BLDR` domain is a second-order TMCTOL: its TMC mints `$BLDR` against first-order `$NTVE`, while its TOL component has two independent capital owners. BLDR Anchor holds all protocol-created `$NTVE/$BLDR` LP as a sealed dormant Immutable System identity whose consensus freeze admits inbound LP and blocks every debit or LP-class destruction; BLDR Treasury funds invoices. Technically the former is an immutable Anchor-type bucket. Because that TOL topology has no peer lettered family, its human name remains `BLDR Anchor` rather than acquiring an artificial `Bucket A` qualifier. It is never called Builder Bucket: Bucket Builder (`B`) is the separate first-order spendable lane, while BLDR Anchor signals immutable second-order LP custody. ```text buyer pays $NTVE -> TMC mints $BLDR - -> about 1/3 to buyer - -> about 1/3 to $NTVE/$BLDR protocol-owned liquidity + -> about 1/3 to recipient + -> about 1/3 to immutable BLDR Anchor liquidity -> about 1/3 to BLDR Treasury ``` -TMC sends two thirds of minted `$BLDR` to the BLDR Splitter. The splitter divides that protocol allocation equally between the BLDR Liquidity Actor and BLDR Treasury, while routing the incoming `$NTVE` collateral to liquidity provisioning. The resulting LP accumulates in immutable BLDR Bucket A. +TMC sends two thirds of minted `$BLDR` to the BLDR Splitter. The splitter divides that protocol allocation equally between the BLDR Liquidity Actor and BLDR Treasury. All incoming `$NTVE` collateral is directed to the liquidity lane, and every resulting LP token enters BLDR Anchor; unmatched buffer remains liquidity-lane custody rather than counted LP reserves. + +The parent first-order Bucket Builder (`B`) lane supplies a second capital path. Bucket B gradually releases LP to Treasury B, which receives both `$NTVE` and foreign reserves and routes both into `$BLDR`. Half of recipient output burns and half enters BLDR Treasury. An XYK route buys existing `$BLDR` and contracts issuance by the burned half; a TMC route creates full issuance, separately funds anchor and direct-treasury lanes, and burns only half of its recipient allocation, so it may remain net expansionary. -A separate L1 Building lane can unwind its own LP into Treasury B, gradually buy `$BLDR` on the market, and burn it. Builder payouts and buyback/burn therefore remain distinct flows: one funds useful work, while the other applies bounded market demand and supply reduction while its Actor Contract remains live. +The Builder Economy contract defines this dual-reserve 50/50 bridge analytically; the simulator retains only the inherited project-independent TMCTOL mathematics and does not model the Builder composition. The shipped runtime currently realizes the `$BLDR` mint, split, and anchor/treasury path but its optional Treasury B plan still spends only Native and burns all acquired `$BLDR`; runtime convergence and production evidence remain open. ## A Federated Domain, Not an Isolated Economy @@ -98,7 +105,7 @@ The builder domain has its own token, treasury, governance, liquidity lane, and - `$NTVE` is its TMC collateral and liquidity pair; - Native economic locks protect its governance domain; -- L1 capital can support BLDR buyback and burn; +- L1 capital can support `$BLDR` buyback and burn; - `$BLDR` governance cannot directly rewrite TMC launch physics, global Actors controls, staking administration, or asset registration. This is the Fractal Federation pattern: a tactical domain remains autonomous inside its declared competence while sharing capital, protection, and infrastructure with the parent economy. It behaves like a bounded organ of DEOS rather than a sovereign replacement for it. diff --git a/wiki/concepts/builder-economy.ru.md b/wiki/concepts/builder-economy.ru.md index f5ee0b95..6f525ee1 100644 --- a/wiki/concepts/builder-economy.ru.md +++ b/wiki/concepts/builder-economy.ru.md @@ -11,8 +11,10 @@ available_locales: - ru sources: - resource: ../../docs/README.md - - resource: ../../docs/manifesto.en.md + - resource: ../../docs/builder-economy.contract.en.md - resource: ../../docs/framework-instance.contract.en.md + - resource: ../../docs/tmctol.specification.en.md + - resource: ../../docs/manifesto.en.md - resource: ../../template/pallets/governance/docs/specification.en.md - resource: ../../template/pallets/governance/docs/architecture.en.md - resource: ../../template/pallets/actors/docs/architecture.en.md @@ -42,7 +44,7 @@ related: Экономика `$BLDR` — основной тактический домен L2 в эталонной линии DEOS. Она координирует завершённую полезную работу, а не предоставляет постоянную экономическую привилегию за статус основателя или члена команды. -Держатели `$BLDR` оценивают счета-заявки, экономическая сила нативного актива защищает границу домена, а казна BLDR исполняет одобренные выплаты. Финансирование труда остаётся отделённым от экономической физики L0 и стратегических полномочий L1. +Держатели `$BLDR` оценивают счета-заявки, экономическая сила нативного актива защищает границу домена, а `BLDR Treasury` исполняет одобренные выплаты. Финансирование труда остаётся отделённым от экономической физики L0 и стратегических полномочий L1. ## Основатель как первый работник @@ -66,32 +68,37 @@ related: - Получателя; - Актив выплаты; - Базовую сумму; -- Объявленный системой управления источник средств казны BLDR. +- Явный одобренный системой управления суверенный счёт `BLDR Treasury`; +- Ограниченный канонический CIDv1 документа счёта-заявки и доказательств с адресацией по содержимому. Основной контур `$BLDR` предлагает четыре варианта: -- `Amplify` — выплатить `2.0x` базовой суммы; -- `Approve` — выплатить `1.0x`; -- `Reduce` — выплатить `0.5x`; +- `Amplify` — целевая выплата `2.0x`, причём ёмкостью казны ограничивается только надбавка выше `1.0x`; +- `Approve` — потребовать и выплатить `1.0x`; +- `Reduce` — потребовать и выплатить `0.5x`; - `Nay` — отклонить без выплаты. -Сила нативного стейкинга образует отдельный защитный контур `Pass / Veto`. Он охраняет конституционную границу, но не определяет цену работы. Выплата транзакционна: либо вся ограниченная сумма перечисляется, либо среда исполнения фиксирует явную ошибку без частичного платежа. +Сила нативного стейкинга образует отдельный защитный контур `Pass / Veto`. Он охраняет конституционную границу, но не определяет цену работы. Если целевая сумма выше базы и равна `2.0x`, а при исполнении доступно `1.5x`, вся доступная сумма `1.5x` переводится атомарно. Ёмкость ниже `1.0x` приводит к ошибке без выплаты. Для целей не выше базы требуется вся целевая сумма без дальнейшего уменьшения. + +`BLDR Treasury` — изменяемый `System Actor` казны. Его собственный ограниченный контракт или ранее исполненный счёт-заявка могут расходовать тот же суверенный баланс между подачей предложения и исполнением, а предложение не резервирует средства. Поэтому порядок исполнения может уменьшить только надбавку выше базы либо вызвать ошибку ниже минимальной суммы; после пополнения потребуется новое голосование. Система управления вправе списывать средства только с одобренного счёта казны Builder, не изменяет её контракт и не получает полномочий над `BLDR Anchor`. ## Экономический контур -Эталонный поток TMC для `$BLDR` создаёт отдельный оборот капитала для финансирования труда: +Домен `$BLDR` представляет собой TMCTOL второго порядка: его TMC выпускает `$BLDR` под обеспечение первичным `$NTVE`, а компонент TOL имеет двух независимых владельцев капитала. `BLDR Anchor` хранит все созданные протоколом LP `$NTVE/$BLDR` как запечатанная бездействующая неизменяемая системная идентичность; ограничитель уровня консенсуса принимает входящие LP, но запрещает любые списания и уничтожение класса LP. `BLDR Treasury` финансирует счета-заявки. Технически первая роль является неизменяемой корзиной типа Anchor. Поскольку в этой топологии TOL нет соседнего буквенного семейства, человеческое имя остаётся `BLDR Anchor` без искусственного квалификатора `Bucket A`. Имя `Builder Bucket` не используется: `Bucket Builder (B)` — отдельный расходуемый контур первого порядка, а `BLDR Anchor` обозначает неизменяемое хранение LP второго порядка. ```text покупатель платит $NTVE -> TMC выпускает $BLDR - -> около 1/3 получает покупатель - -> около 1/3 поступает в ликвидность $NTVE/$BLDR во владении протокола - -> около 1/3 поступает в казну BLDR + -> около 1/3 получает адресат + -> около 1/3 поступает в неизменяемую ликвидность BLDR Anchor + -> около 1/3 поступает в BLDR Treasury ``` -TMC направляет две трети выпущенных `$BLDR` в `BLDR Splitter`. Разделитель поровну распределяет эту долю между BLDR Liquidity Actor и казной BLDR, а обеспечение в `$NTVE` направляет на создание ликвидности. Полученные LP накапливаются в неизменяемом BLDR `Bucket A`. +TMC направляет две трети выпущенных `$BLDR` в `BLDR Splitter`. Разделитель поровну распределяет эту долю между BLDR Liquidity Actor и `BLDR Treasury`. Весь входящий залог в `$NTVE` направляется в контур ликвидности, а каждый полученный LP поступает в `BLDR Anchor`; неиспользованный остаток остаётся на хранении в контуре ликвидности и не считается развёрнутым резервом LP. + +Родительский `Bucket Builder (B)` первого порядка создаёт второй путь капитала. `Bucket B` постепенно передаёт LP в `Treasury B`, которая получает резервы в `$NTVE` и иностранном активе и направляет оба актива на приобретение `$BLDR`. Половина полученного адресатом объёма сжигается, а половина поступает в `BLDR Treasury`. Маршрут XYK покупает существующий `$BLDR` и сокращает выпуск на сожжённую половину; маршрут TMC создаёт полный новый выпуск, отдельно пополняет опорную ликвидность и прямую долю казны, а сжигает лишь половину доли адресата, поэтому может оставаться расширяющим предложение. -Отдельный контур L1 Building может вывести базовые активы из собственного LP в `Treasury B`, постепенно покупать `$BLDR` на рынке и сжигать его. Выплаты за работу и выкуп со сжиганием остаются разными потоками: первый финансирует полезную работу, второй создаёт ограниченный рыночный спрос и сокращает предложение, пока соответствующий `Actor Contract` остаётся действующим. +Контракт экономики строителей определяет контур двух резервных активов с разделением 50/50 аналитически; симулятор сохраняет только унаследованную проектно-независимую математику TMCTOL и не моделирует композицию Builder. Действующая среда исполнения уже реализует выпуск `$BLDR`, его разделение и независимые позиции опоры и казны, но необязательный план `Treasury B` пока расходует только Native и сжигает весь приобретённый `$BLDR`; сближение среды исполнения с целевым контрактом и производственные доказательства остаются открытой работой. ## Федеративный, а не изолированный домен diff --git a/wiki/concepts/domain-map.en.md b/wiki/concepts/domain-map.en.md index ff734bc5..b5141ae0 100644 --- a/wiki/concepts/domain-map.en.md +++ b/wiki/concepts/domain-map.en.md @@ -83,7 +83,7 @@ Read [Staking](../overview/staking.en.md) and [Three-Layer Validation](../develo ### Client and read model -The client domain explains how the browser product shows the system without pretending to be the source of truth. It distinguishes direct on-chain state, session-derived projections, and future materialized/indexed providers. +The client domain explains how the browser product shows the system without pretending to be the source of truth. It distinguishes direct on-chain state, session-derived projections, and external materialized/indexed data. Read [Reference Client](../overview/reference-client.en.md) and [Read-Model Split](read-model-split.en.md). @@ -93,9 +93,9 @@ The tooling domain explains how contributors and agents keep the system honest: Read [Three-Layer Validation](../development/three-layer-validation.en.md), [Scripts Layer](../usage/scripts-layer.en.md), and [Development Status](../development/status.en.md). -### Future gates +### Current limitations -The future-gates domain explains what is intentionally not part of the current shipped baseline: permissionless collators, relay-beacon randomness, full indexed portfolio discovery, and richer materialized archives. +This domain explains what is not part of the current shipped baseline: permissionless collators, relay-beacon randomness, full indexed portfolio discovery, and richer materialized archives. Read [Randomness Strategy](../overview/randomness-strategy.en.md) and [Development Status](../development/status.en.md). @@ -112,7 +112,7 @@ A useful traversal is: 7. Staking and rewards connect users to security and incentives. 8. Client and read model expose the system without inventing truth. 9. Tooling and validation keep the graph synchronized. -10. Future gates prevent speculative work from masquerading as shipped reality. +10. Current limitations distinguish available capabilities from unsupported ones. ## Related diff --git a/wiki/concepts/domain-map.ru.md b/wiki/concepts/domain-map.ru.md index e97c68db..622b10e9 100644 --- a/wiki/concepts/domain-map.ru.md +++ b/wiki/concepts/domain-map.ru.md @@ -84,7 +84,7 @@ DEOS проще понять как совокупность связанных ### Клиент и данные для чтения -Клиентский домен объясняет, как браузерный продукт показывает систему, не выдавая себя за источник истины. Он различает состояние непосредственно из блокчейна, вычисленные за сеанс проекции и будущие внешние поставщики индексированных или материализованных данных. +Клиентский домен объясняет, как браузерный продукт показывает систему, не выдавая себя за источник истины. Он различает состояние непосредственно из блокчейна, вычисленные за сеанс проекции и внешние индексированные или материализованные данные. Читайте [Эталонный клиент](../overview/reference-client.ru.md) и [Разделение данных для чтения](read-model-split.ru.md). @@ -94,7 +94,7 @@ DEOS проще понять как совокупность связанных Читайте [Трёхуровневую проверку](../development/three-layer-validation.ru.md), [Слой скриптов](../usage/scripts-layer.ru.md) и [Статус разработки](../development/status.ru.md). -### Условия будущего развития +### Текущие ограничения Этот домен объясняет, что намеренно не входит в действующую поставляемую основу: коллаторы без разрешений, случайность от маяка ретрансляционной цепи, полный поиск портфеля по индексированным данным и более подробные материализованные архивы. @@ -113,7 +113,7 @@ DEOS проще понять как совокупность связанных 7. Стейкинг и вознаграждения связывают пользователей с безопасностью и стимулами. 8. Клиент и данные для чтения показывают систему, не изобретая собственную истину. 9. Инструменты и проверка поддерживают согласованность графа. -10. Условия будущего развития не дают выдавать работу над предполагаемыми возможностями за уже поставленную возможность. +10. Текущие ограничения отделяют доступные возможности от неподдерживаемых. ## Связанные страницы diff --git a/wiki/concepts/end-to-end-flows.en.md b/wiki/concepts/end-to-end-flows.en.md index cd5832ef..d7307d9f 100644 --- a/wiki/concepts/end-to-end-flows.en.md +++ b/wiki/concepts/end-to-end-flows.en.md @@ -90,7 +90,7 @@ This is intentionally a domain-level walkthrough. Bucket ratios and formulas bel 1. A user stakes `$NTVE` and receives liquid `stNTVE` receipt shares. 2. Collator security is not inferred from wallet `stNTVE` balances. -3. The security path uses explicit locked `NTVE/stNTVE` LP custody. +3. The security path uses explicit locked `$NTVE/stNTVE` LP custody. 4. Native nomination reward paths stay separate from generic same-asset staking rewards. 5. Governance-conditioned participation can influence reward coefficients, but governance and staking remain separate subsystems. diff --git a/wiki/concepts/end-to-end-flows.ru.md b/wiki/concepts/end-to-end-flows.ru.md index 3ba933dc..4e846d07 100644 --- a/wiki/concepts/end-to-end-flows.ru.md +++ b/wiki/concepts/end-to-end-flows.ru.md @@ -91,7 +91,7 @@ related: 1. Пользователь размещает `$NTVE` в стейкинге и получает ликвидные доли-квитанции `stNTVE`. 2. Обеспечение безопасности коллаторов не выводится из баланса `stNTVE` в кошельке. -3. Контур безопасности использует явно заблокированные LP `NTVE/stNTVE` на хранении. +3. Контур безопасности использует явно заблокированные LP `$NTVE/stNTVE` на хранении. 4. Вознаграждения за нативные номинации отделены от общих наград за стейкинг того же актива. 5. Участие, обусловленное управлением, может влиять на коэффициенты вознаграждения, но управление и стейкинг остаются разными подсистемами. diff --git a/wiki/concepts/tmctol-standard.en.md b/wiki/concepts/tmctol-standard.en.md index 63f0496d..f7e9a171 100644 --- a/wiki/concepts/tmctol-standard.en.md +++ b/wiki/concepts/tmctol-standard.en.md @@ -67,10 +67,10 @@ This matters because the floor-support properties depend on liquidity accumulati The TOL share is split into four buckets: -- `Bucket A` for anchor liquidity and hard floor support -- `Bucket B` for building-budget and buyback-oriented flows -- `Bucket C` for capital reserve and treasury operations -- `Bucket D` for dormant policy reserve +- `Bucket A / Anchor` for immutable floor-support custody +- `Bucket B / Builder` for ecosystem construction and buyback-oriented flows +- `Bucket C / Capital` for capital reserve and treasury operations +- `Bucket D / Dormant` for sleeping demand, distribution, or marketing reserve The bucket model lets governance steer deployment without treating all protocol liquidity as interchangeable. diff --git a/wiki/concepts/tmctol-standard.ru.md b/wiki/concepts/tmctol-standard.ru.md index ca97deca..a39e6289 100644 --- a/wiki/concepts/tmctol-standard.ru.md +++ b/wiki/concepts/tmctol-standard.ru.md @@ -68,10 +68,10 @@ DEOS Router взимает комиссию с обменов и направл Доля TOL распределяется по четырём корзинам: -- `Bucket A` — якорная ликвидность и основная поддержка нижней границы; -- `Bucket B` — бюджет развития и операции, связанные с выкупом; -- `Bucket C` — резерв капитала и казначейские операции; -- `Bucket D` — неактивный резерв, ожидающий отдельного политического решения. +- `Bucket A / Anchor` — неизменяемое хранение ликвидности, поддерживающей нижнюю границу; +- `Bucket B / Builder` — бюджет развития и операции, связанные с выкупом; +- `Bucket C / Capital` — резерв капитала и казначейские операции; +- `Bucket D / Dormant` — спящий резерв для будущего спроса, распространения или маркетинга. Такое распределение позволяет системе управления определять назначение капитала, не считая всю ликвидность протокола взаимозаменяемой. diff --git a/wiki/concepts/token-surfaces.en.md b/wiki/concepts/token-surfaces.en.md index f224c7e8..f19d7247 100644 --- a/wiki/concepts/token-surfaces.en.md +++ b/wiki/concepts/token-surfaces.en.md @@ -1,7 +1,7 @@ --- type: concept title: Token Surfaces -description: A compact map of the main DEOS/TMCTOL token surfaces, including Native, VETO, BLDR, stNTVE, LP tokens, and how each token participates in economics, governance, staking, and read-model boundaries. +description: A compact map of the main DEOS/TMCTOL token surfaces, including Native, $VETO, $BLDR, stNTVE, LP tokens, and how each token participates in economics, governance, staking, and read-model boundaries. locale: en canonical_page_id: token-surfaces translation_status: source @@ -49,13 +49,13 @@ This page is a compact map. It does not replace the exact formulas, governance r ### `$BLDR` -`$BLDR` is the flagship tactical governance token for builder coordination. In the current line it is associated with invoice-style tactical governance, labor funding, protocol-owned liquidity, and BLDR-specific coordination lanes. [$BLDR Builder Economy](builder-economy.en.md) owns the full pattern. Its value story is not just emission; it depends on whether the downstream builder ecosystem gives that coordination surface real demand. +`$BLDR` is the flagship tactical governance token for builder coordination. In the current line it is associated with invoice-style tactical governance, labor funding, protocol-owned liquidity, and `$BLDR`-specific coordination lanes. [$BLDR Builder Economy](builder-economy.en.md) owns the full pattern. Its value story is not just emission; it depends on whether the downstream builder ecosystem gives that coordination surface real demand. ## Receipt and Position Tokens ### `stNTVE` -`stNTVE` is the native liquid staking receipt. It represents share-vault ownership, not direct collator nomination by itself. Collator security uses explicit locked `NTVE/stNTVE` LP custody. +`stNTVE` is the native liquid staking receipt. It represents share-vault ownership, not direct collator nomination by itself. Collator security uses explicit locked `$NTVE/stNTVE` LP custody. ### LP Tokens diff --git a/wiki/concepts/token-surfaces.ru.md b/wiki/concepts/token-surfaces.ru.md index 7c850233..f0120aac 100644 --- a/wiki/concepts/token-surfaces.ru.md +++ b/wiki/concepts/token-surfaces.ru.md @@ -50,13 +50,13 @@ DEOS одновременно использует несколько видов ### `$BLDR` -`$BLDR` — основной токен тактического управления для координации созидателей. В текущей линии он связан с оценкой счетов-заявок, финансированием труда, ликвидностью во владении протокола и отдельными контурами BLDR. Полную схему описывает [Экономика созидателей $BLDR](builder-economy.ru.md). Ценность `$BLDR` зависит не только от эмиссии, но и от того, создаст ли производная экосистема реальный спрос на такую координацию. +`$BLDR` — основной токен тактического управления для координации созидателей. В текущей линии он связан с оценкой счетов-заявок, финансированием труда, ликвидностью во владении протокола и отдельными контурами `$BLDR`. Полную схему описывает [Экономика созидателей $BLDR](builder-economy.ru.md). Ценность `$BLDR` зависит не только от эмиссии, но и от того, создаст ли производная экосистема реальный спрос на такую координацию. ## Квитанции и токены позиций ### `stNTVE` -`stNTVE` — ликвидная квитанция нативного стейкинга. Она удостоверяет долю в пуле, но сама по себе не означает номинацию коллатора. Безопасность коллаторов опирается на явно заблокированные LP пары `NTVE/stNTVE`. +`stNTVE` — ликвидная квитанция нативного стейкинга. Она удостоверяет долю в пуле, но сама по себе не означает номинацию коллатора. Безопасность коллаторов опирается на явно заблокированные LP пары `$NTVE/stNTVE`. ### LP-токены diff --git a/wiki/concepts/tol-bucket-scenarios.en.md b/wiki/concepts/tol-bucket-scenarios.en.md index 3852d9d9..c9d49740 100644 --- a/wiki/concepts/tol-bucket-scenarios.en.md +++ b/wiki/concepts/tol-bucket-scenarios.en.md @@ -1,7 +1,7 @@ --- type: concept title: TOL Bucket Scenarios -description: Concrete scenarios for the TMCTOL treasury-owned-liquidity bucket model, including bucket A, B, C, D behavior, unwind paths, and actor wakeups. +description: Configurable treasury-owned-liquidity scenarios, from first-order A/B/C/D to independent second-order BLDR Anchor and BLDR Treasury owners. locale: en canonical_page_id: tol-bucket-scenarios translation_status: source @@ -12,6 +12,9 @@ sources: - resource: ../../docs/tmctol.specification.en.md - resource: ../../template/pallets/actors/docs/architecture.en.md - resource: ../../template/pallets/actors/docs/specification.en.md + - resource: ../../docs/core.architecture.en.md + - resource: ../../docs/builder-economy.contract.en.md + - resource: ../../simulator/README.md - resource: ../../AGENTS.md status: stable audience: newcomer @@ -27,15 +30,18 @@ related: - Architecture Diagrams - Actors System - Token-Driven Automation + - Builder Economy --- # TOL Bucket Scenarios ## Summary -TMCTOL uses treasury-owned-liquidity buckets to separate economic intents and preserve reserve provenance. The current reference topology distinguishes immutable Bucket A custody from optional B/C/D unwind and treasury lanes. +TOL is the asset-scoped topology through which a treasury owns protocol liquidity and related strategic capital. Anchors and buckets preserve liquidity custody and provenance; treasuries realize or spend strategic capital. Their counts are configuration rather than part of the universal definition. -Activation status matters: Bucket A is an immutable custody-only sink, while Bucket B, C, D and their paired treasury roles start as dormant System identities without programs. Any later unwind or treasury behavior requires explicit activation with a bounded plan after pool and treasury readiness; balance thresholds do not activate these lanes automatically. +The first-order Native/foreign reference topology instantiates Bucket Anchor (`A`), Bucket Builder (`B`), Bucket Capital (`C`), and Bucket Dormant (`D`). Anchor custody is immutable; B/C/D own optional unwind and treasury lanes. The second-order `$BLDR` topology uses first-order `$NTVE` as collateral and has two independently named owners: BLDR Anchor and BLDR Treasury. Without peer buckets, BLDR Anchor does not inherit the first-order A/B/C/D namespace. + +Activation status matters in the first-order runtime topology: Bucket A is a sealed dormant Immutable System identity whose LP balance is frozen against every debit, while Bucket B, C, D and their paired treasury roles start as dormant Mutable System identities without programs. Any later unwind or treasury behavior requires explicit activation with a bounded plan after pool and treasury readiness; balance thresholds do not activate these lanes automatically. ## Bucket A: Immediate Liquidity @@ -67,6 +73,21 @@ explicit policy and readiness decision No current contract defines automatic threshold-driven wakeups for C or D. +Floor reporting follows live custody rather than historical deposits. A bucket contributes only its current proportional reserve claim while it holds positive LP in an anchor or explicitly active-support state; dormant LP does not count until activation. Once that live position leaves the pool, its claim leaves the reported reserve scope. If live Bucket A anchor support disappears, the report must set `governance_state` to `degraded`. + +## TOL Component of Second-Order `$BLDR` TMCTOL + +Both reference orders direct approximately one third of total issuance to immutable anchor liquidity. They do not share a collateral percentage: first-order Bucket A receives half of collateral under the four-bucket allocation, while BLDR Anchor receives all `$NTVE`. The `$BLDR` TMC concentrates its retained protocol issuance into two equal policy lanes: + +```text +about 1/3 $BLDR -> recipient +about 1/3 $BLDR -> BLDR Anchor +about 1/3 $BLDR -> spendable BLDR Treasury +all $NTVE collateral -> anchor-liquidity lane +``` + +The parent Bucket B bridge may gradually release LP into Treasury B. Both released reserve assets route into `$BLDR`; half of recipient output burns and half enters BLDR Treasury. Market routes buy existing supply, while a TMC route also creates its ordinary anchor and direct-treasury allocations. The Builder Economy contract defines these route-dependent effects analytically; the current simulator does not model the Builder bridge. + ## Why Paired Treasuries Matter Each non-immediate bucket has a distinct paired treasury account in the reference topology. Those lanes keep provenance and policy intent visible even while their System identities remain dormant: @@ -86,3 +107,4 @@ A downstream fork may alter policy, but it should preserve bucket provenance as - [Architecture Diagrams](architecture-diagrams.en.md) - [Actors System](../overview/actor-system.en.md) - [Token-Driven Automation](token-driven-automation.en.md) +- [Builder Economy](builder-economy.en.md) diff --git a/wiki/concepts/tol-bucket-scenarios.ru.md b/wiki/concepts/tol-bucket-scenarios.ru.md index 1acc9ab6..210ff7a0 100644 --- a/wiki/concepts/tol-bucket-scenarios.ru.md +++ b/wiki/concepts/tol-bucket-scenarios.ru.md @@ -1,7 +1,7 @@ --- type: concept title: Сценарии корзин TOL -description: "Сценарии корзин ликвидности, принадлежащей казне: хранение в корзине A, необязательные пути вывода LP из корзин B/C/D и условия активации акторов." +description: "Настраиваемые сценарии ликвидности казны: от первичной топологии A/B/C/D до независимых BLDR Anchor и BLDR Treasury второго порядка." locale: ru canonical_page_id: tol-bucket-scenarios translation_of: tol-bucket-scenarios.en.md @@ -13,6 +13,9 @@ sources: - resource: ../../docs/tmctol.specification.en.md - resource: ../../template/pallets/actors/docs/architecture.en.md - resource: ../../template/pallets/actors/docs/specification.en.md + - resource: ../../docs/core.architecture.en.md + - resource: ../../docs/builder-economy.contract.en.md + - resource: ../../simulator/README.md - resource: ../../AGENTS.md status: stable audience: newcomer @@ -28,15 +31,18 @@ related: - Архитектурные схемы - Система Actors - Токен-управляемая автоматизация + - Экономика строителей --- # Сценарии корзин TOL ## Кратко -TMCTOL распределяет ликвидность, принадлежащую казне, по корзинам с разным экономическим назначением и сохраняет происхождение резервов. В эталонной топологии неизменяемое хранение в `Bucket A` отделено от необязательных путей вывода LP и казначейских контуров B, C и D. +TOL — это относящаяся к одному активу топология, через которую казна владеет протокольной ликвидностью и связанным стратегическим капиталом. Опоры и корзины сохраняют ликвидность и происхождение резервов, а казны позволяют реализовать или расходовать стратегический капитал. Их число задаётся конфигурацией, а не общим определением. -Состояние активации существенно. `Bucket A` — неизменяемый адрес только для хранения. `Bucket B`, `Bucket C`, `Bucket D` и соответствующие казначейские роли при запуске остаются бездействующими System Actor без программ. Для вывода LP или казначейских действий требуется явная активация ограниченного плана после подготовки пула и казны; достижение порога баланса не включает эти контуры автоматически. +Первичная эталонная топология Native/внешний актив создаёт `Bucket Anchor (A)`, `Bucket Builder (B)`, `Bucket Capital (C)` и `Bucket Dormant (D)`. Хранение Anchor неизменно; B/C/D владеют необязательными путями вывода LP и казначейскими контурами. Вторичная топология `$BLDR` использует первичный `$NTVE` как обеспечение и имеет двух независимо именуемых владельцев: `BLDR Anchor` и `BLDR Treasury`. Поскольку равноправных корзин нет, `BLDR Anchor` не наследует первичное пространство имён A/B/C/D. + +Состояние активации существенно для первичной топологии среды исполнения. `Bucket A` — запечатанная бездействующая неизменяемая системная идентичность, LP-баланс которой заморожен для любых списаний. `Bucket B`, `Bucket C`, `Bucket D` и соответствующие казначейские роли при запуске остаются бездействующими изменяемыми System Actor без программ. Для вывода LP или казначейских действий требуется явная активация ограниченного плана после подготовки пула и казны; достижение порога баланса не включает эти контуры автоматически. ## `Bucket A`: непосредственная ликвидность @@ -66,6 +72,21 @@ TMCTOL распределяет ликвидность, принадлежащу Текущий контракт не определяет автоматического пробуждения C или D по порогу баланса. +Отчёт о нижней границе опирается на текущее хранение, а не на историю взносов. Корзина учитывается только в размере текущей пропорциональной доли резервов, соответствующей её положительному объёму LP, и лишь в опорном или явно действующем состоянии; бездействующий LP не учитывается до активации. Когда действующая позиция покидает пул, её доля исключается из состава учитываемых резервов. Если действующая опора `Bucket A` исчезает, отчёт обязан присвоить полю `governance_state` значение `degraded`. + +## Компонент TOL в TMCTOL второго порядка для `$BLDR` + +Оба эталонных порядка направляют около одной трети полного выпуска в неизменяемую опорную ликвидность. Общей доли обеспечения у них нет: первичный `Bucket A` получает половину обеспечения согласно распределению между четырьмя корзинами, а `BLDR Anchor` получает весь `$NTVE`. TMC для `$BLDR` концентрирует удерживаемую протоколом долю выпуска в двух равных направлениях: + +```text +около 1/3 $BLDR -> адресату +около 1/3 $BLDR -> BLDR Anchor +около 1/3 $BLDR -> BLDR Treasury +весь залог $NTVE -> контуру опорной ликвидности +``` + +Родительский контур `Bucket B` может постепенно передавать LP в `Treasury B`. Оба высвобождённых резервных актива направляются на приобретение `$BLDR`; половина полученного адресатом объёма сжигается, а половина поступает в `BLDR Treasury`. Рыночный маршрут покупает существующее предложение, тогда как маршрут TMC дополнительно создаёт обычные доли опорной ликвидности и прямого пополнения казны. Контракт экономики строителей определяет эти зависящие от маршрута эффекты аналитически; текущий симулятор не моделирует мост Builder. + ## Зачем нужны отдельные казначейские счета У каждой необязательной корзины есть отдельный связанный счёт казны: @@ -87,3 +108,4 @@ Bucket D -> Treasury D lane - [Архитектурные схемы](architecture-diagrams.ru.md) - [Система Actors](../overview/actor-system.ru.md) - [Токен-управляемая автоматизация](token-driven-automation.ru.md) +- [Экономика строителей](builder-economy.ru.md) diff --git a/wiki/development/status.en.md b/wiki/development/status.en.md index b117aec1..e0224f7a 100644 --- a/wiki/development/status.en.md +++ b/wiki/development/status.en.md @@ -1,7 +1,7 @@ --- type: status title: Development Status -description: Current implementation status, roadmap context, and active backlog items for the DEOS framework, focused on shipped baseline, open boundaries, and future-gated work without treating the wiki as a release-note surface. +description: Current capabilities and present limitations of the DEOS framework and reference client. locale: en canonical_page_id: status translation_status: source @@ -9,16 +9,15 @@ available_locales: - en - ru sources: - - resource: ../../BACKLOG.md - resource: ../../CHANGELOG.md - resource: ../../web-client/README.md - resource: ../../web-client/docs/architecture.en.md + - resource: ../../template/pallets/actors/docs/architecture.en.md status: stable audience: newcomer tags: - development - status - - roadmap related: - Three-Layer Validation - Reference Client @@ -29,44 +28,27 @@ related: ## Summary -DEOS is in framework-stabilization mode. The runtime, reference client, scripts, docs, and wiki are now being shaped into one coherent reference product rather than a sequence of visible refactor milestones. +DEOS combines a FRAME runtime, a browser reference client, development tools, and documentation. This page describes current capabilities and their limits; it does not certify a release or a deployed network. -This page is a current-state map. It is not the release history and not the full backlog. +## Current capabilities -## Stable baseline areas +- **Economic mechanisms**: TMCTOL issuance, routing, treasury-owned liquidity, and Actor-mediated economic flows form the reference composition. +- **DEOS Actors**: deterministic Actor Contract execution uses a bounded scheduler. Mutable Actors can retry Temporary Step failures while preserving committed prefixes without whole-contract rollback. +- **Staking and governance**: multi-asset share accounting and bounded governance tracks provide staking, typed proposals, and protection mechanisms. +- **Reference client**: the SvelteKit client provides wallet/account selection, tracked-asset balances and transfers, swap, staking, governance, automation, wiki browsing, and transaction feedback. +- **Development tools**: project scripts, benchmarks, metadata export, and package/runtime/client checks support development and validation. -The current framework baseline is best understood by domain: +[Domain Map](../concepts/domain-map.en.md) explains how these areas relate. -- **Economic physics**: TMCTOL minting, routing, treasury-owned liquidity, actor-mediated fee burning, and bounded invariants form the core economic loop. -- **Autonomous actors**: Actors provides deterministic Actor Contract execution plus Mutable-only sparse Continuation for Temporary Step failure. Retries preserve committed prefixes on the canonical bounded scheduler without whole-contract rollback. -- **Staking and governance**: staking uses multi-asset share-vault mechanics, while governance uses bounded domain tracks, typed payloads, and protection surfaces. -- **Reference client**: the SvelteKit client exposes on-chain-first wallet, swap, staking, governance, wiki, chart/status, automation, and execution-feedback surfaces. -- **Tooling and validation**: scripts, benchmarks, metadata export, wiki trust checks, client validation, and context gates support local development and release readiness. +## Present limits -Use [Domain Map](../concepts/domain-map.en.md) when you need the conceptual topology instead of the status snapshot. +The client is a reference interface, not the source of protocol truth. Its canonical chain views expose bounded state. Wallet transfers cover tracked assets; this is not an exhaustive asset-discovery claim. -## Active focus +Browser session history and charts are not an archive. Indexed history, search, and analytics are materialized data, distinct from current canonical chain state. -The current backlog concentrates on closing the contracted model with minimal release machinery: +Current ObservationCrossing admission processes four candidates per block, so 9,000 User memberships require at least 2,250 uncontended detector blocks. The implementation therefore does not satisfy the assurance target of 10,000 User crossings within 100 eligible blocks. -- Pass the direct pull-request validation gate on the final release tree; -- Publish the runtime Wasm, metadata, descriptors, and five generated semantic/runtime evidence assets; -- Publish one checksum file for that eight-asset payload; -- Keep network assurance as an independently useful local operation rather than a release claim. - -## Open boundaries - -The important unfinished areas are intentionally gated: - -- Wallet expansion waits for a materialized/indexed asset-discovery surface; -- Archive/search UX waits for a materialized provider contract; -- Permissionless collators and advanced randomness wait for an upstream relay-beacon path; -- Client composition and provider growth wait for concrete ownership pressure; -- Block-reward routing waits for a concrete subsidy source and amount policy. - -## Where to look next - -For active tasks, use the root backlog. For completed delivery history, use the root changelog. For how to validate a change, use [Three-Layer Validation](three-layer-validation.en.md). +The presence of a feature or a validation command does not by itself establish production readiness, network deployment, or validation of the current source tree. ## Related diff --git a/wiki/development/status.ru.md b/wiki/development/status.ru.md index 1959ad8d..677ea018 100644 --- a/wiki/development/status.ru.md +++ b/wiki/development/status.ru.md @@ -1,7 +1,7 @@ --- type: status title: Статус разработки -description: Текущее состояние реализации и открытые границы фреймворка DEOS без превращения вики в журнал релизов. +description: Текущие возможности и ограничения фреймворка DEOS и эталонного клиента. locale: ru canonical_page_id: status translation_of: status.en.md @@ -10,16 +10,15 @@ available_locales: - en - ru sources: - - resource: ../../BACKLOG.md - resource: ../../CHANGELOG.md - resource: ../../web-client/README.md - resource: ../../web-client/docs/architecture.en.md + - resource: ../../template/pallets/actors/docs/architecture.en.md status: stable audience: newcomer tags: - development - status - - roadmap related: - Трёхуровневая проверка - Эталонный клиент @@ -30,44 +29,27 @@ related: ## Кратко -DEOS находится на этапе стабилизации фреймворка. Среда исполнения, эталонный клиент, скрипты, документация и вики сводятся в единый согласованный эталонный продукт, а не в последовательность заметных промежуточных переделок. +DEOS объединяет среду исполнения на FRAME, эталонный браузерный клиент, инструменты разработки и документацию. Эта страница описывает текущие возможности и их ограничения, но не подтверждает готовность релиза или работающей сети. -Эта страница показывает текущее состояние. Она не заменяет ни историю релизов, ни полный перечень открытых работ. +## Текущие возможности -## Устойчивая основа +- **Экономические механизмы**: эмиссия TMCTOL, маршрутизация, ликвидность во владении казны и экономические операции через акторов образуют эталонную конфигурацию. +- **DEOS Actors**: детерминированное исполнение `Actor Contract` использует ограниченный планировщик. Изменяемый актор хранит разреженное состояние Continuation при временной ошибке и может повторить шаг, сохранив уже зафиксированные результаты без отката всего контракта. +- **Стейкинг и управление**: учёт долей по нескольким активам и ограниченные доменные направления управления обеспечивают стейкинг, типизированные предложения и защитные механизмы. +- **Эталонный клиент**: клиент на SvelteKit позволяет выбирать кошелёк и учётную запись, просматривать остатки отслеживаемых активов и переводить их, выполнять обмены, работать со стейкингом, управлением и автоматизацией, читать вики и следить за транзакциями. +- **Инструменты разработки**: скрипты проекта, измерения производительности, экспорт метаданных и проверки пакетов, среды исполнения и клиента поддерживают разработку и проверку изменений. -Текущую основу фреймворка удобнее рассматривать по доменам: +Связи между этими областями объясняет [Карта доменов](../concepts/domain-map.ru.md). -- **Экономическая физика**: эмиссия TMCTOL, маршрутизация, ликвидность, принадлежащая казне, сжигание комиссий через акторов и ограниченные инварианты образуют основной экономический контур; -- **Автономные акторы**: Actors детерминированно исполняет `Actor Contract` и хранит разреженное состояние Continuation только у изменяемого актора при временном сбое шага. Повтор сохраняет зафиксированный префикс в каноническом ограниченном планировщике без отката всего `Actor Contract`; -- **Стейкинг и управление**: стейкинг использует многовалютный пул с учётом долей, а управление — ограниченные доменные направления, типизированные данные предложений и защитные полномочия; -- **Эталонный клиент**: клиент на SvelteKit показывает кошелёк, обмен, стейкинг, управление, вики, графики и состояние, автоматизацию и ход исполнения, отдавая приоритет данным непосредственно из блокчейна; -- **Инструменты и проверка**: скрипты, измерения производительности, экспорт метаданных, проверка доверенной вики, проверка клиента и контекстные рубежи поддерживают локальную разработку и подготовку к релизу. +## Текущие ограничения -Если нужна концептуальная топология, а не снимок состояния, используйте [Карту доменов](../concepts/domain-map.ru.md). +Клиент — эталонный интерфейс, а не источник истины о протоколе. Канонические данные из блокчейна ограничены по объёму. Переводы в кошельке охватывают отслеживаемые активы; это не означает обнаружение всех активов. -## Текущая работа +История браузерного сеанса и графики не являются архивом. Индексированная история, поиск и аналитика относятся к материализованным данным и отличаются от текущего канонического состояния блокчейна. -Открытые задачи сосредоточены на завершении согласованной модели с минимальными средствами релиза: +Текущий обработчик пересечений наблюдений допускает четыре кандидата за блок: 9 000 пользовательских связей требуют не менее 2 250 блоков без конкурирующей нагрузки. Поэтому реализация не выполняет контрольную цель 10 000 пользовательских пересечений за 100 подходящих блоков. -- Пройти прямой рубеж проверки запроса на слияние на итоговом дереве релиза; -- Опубликовать Wasm среды исполнения, метаданные, дескрипторы и пять созданных файлов семантических свидетельств и свидетельств среды исполнения; -- Опубликовать один файл контрольных сумм для этих восьми файлов; -- Сохранить сетевую проверку как независимо полезную локальную операцию без утверждений о релизе. - -## Открытые границы - -Важные незавершённые области намеренно ждут выполнения условий: - -- Расширение кошелька — появления материализованного или индексированного источника обнаружения активов; -- Архив, поиск и длинная история — контракта материализованного поставщика данных; -- Открытый набор коллаторов и более сильная случайность — доступного парачейну маяка ретрансляционной цепи; -- Рост композиции клиента и числа поставщиков данных — реальной нагрузки на границы владения; -- Маршрутизация наград за блок — конкретного источника субсидии и правила расчёта суммы. - -## Куда смотреть дальше - -Текущие задачи находятся в корневом `BACKLOG.md`, завершённые поставки — в `CHANGELOG.md`, а порядок проверки изменений — на странице [Трёхуровневая проверка](three-layer-validation.ru.md). +В эталонной конфигурации не заявлены награды за блок без конкретного источника субсидии и правила расчёта суммы. Само наличие функции или команды проверки не подтверждает готовность к эксплуатации, запуск сети или успешную проверку текущих исходников. ## Связанные страницы diff --git a/wiki/getting-started/reading-paths.en.md b/wiki/getting-started/reading-paths.en.md index f80705ed..4d595e14 100644 --- a/wiki/getting-started/reading-paths.en.md +++ b/wiki/getting-started/reading-paths.en.md @@ -37,7 +37,7 @@ related: ## Summary -DEOS has several linked domains: framework identity, Economic Physics, autonomous actors, routing, governance, staking, read models, client, tooling, and future gates. You do not need to read everything in one pass. +DEOS has several linked domains: framework identity, Economic Physics, autonomous actors, routing, governance, staking, read models, client, tooling, and current limitations. You do not need to read everything in one pass. If you only need the shortest entry, use [Start Here](start-here.en.md) first. Use this page when you need a broader role-based reading route after choosing the right onboarding path. @@ -131,7 +131,7 @@ Client work should preserve UI Kit reuse, Domain DAG ownership, read-model prove 4. [Generated Wiki](../concepts/generated-wiki.en.md) 5. [Core Terms](../glossary/core-terms.en.md) -Status work should separate shipped baseline, open backlog, completed delivery, and future-gated work. The wiki explains that boundary without becoming release notes itself. +The wiki describes the implemented baseline and its current limitations. Open work belongs in `BACKLOG.md` and completed delivery in `CHANGELOG.md`, not in wiki pages. ## If you are operating scripts or local tooling diff --git a/wiki/getting-started/reading-paths.ru.md b/wiki/getting-started/reading-paths.ru.md index 9a813618..a1ef0ed5 100644 --- a/wiki/getting-started/reading-paths.ru.md +++ b/wiki/getting-started/reading-paths.ru.md @@ -38,7 +38,7 @@ related: ## Кратко -DEOS объединяет несколько связанных областей: устройство фреймворка, экономическую физику, автономных Actors, маршрутизацию, управление, стейкинг, проекции данных для чтения, клиент, инструменты и условия будущего развития. Необязательно изучать всё за один раз. +DEOS объединяет несколько связанных областей: устройство фреймворка, экономическую физику, автономных Actors, маршрутизацию, управление, стейкинг, проекции данных для чтения, клиент, инструменты и текущие ограничения. Необязательно изучать всё за один раз. Для самого короткого знакомства сначала откройте [С чего начать](start-here.ru.md). Эта страница поможет подобрать более подробный маршрут по вашей роли или задаче. @@ -132,7 +132,7 @@ DEOS объединяет несколько связанных областей 4. [Собранная вики](../concepts/generated-wiki.ru.md) 5. [Базовые термины](../glossary/core-terms.ru.md) -Отделяйте реализованную основу, открытые задачи, завершённые изменения и работу, зависящую от будущих условий. Вики объясняет эти границы, но не заменяет журнал выпусков. +Вики описывает реализованную основу и её текущие ограничения. Открытые задачи относятся к `BACKLOG.md`, а завершённые изменения — к `CHANGELOG.md`, не к страницам вики. ## Если вы работаете со сценариями или локальными инструментами diff --git a/wiki/glossary/core-terms.en.md b/wiki/glossary/core-terms.en.md index dc9274dd..4aa52a16 100644 --- a/wiki/glossary/core-terms.en.md +++ b/wiki/glossary/core-terms.en.md @@ -64,7 +64,7 @@ This glossary is a lookup surface, not a second explanation layer. Each term sta ### TOL -`Treasury-Owned Liquidity`. Protocol-controlled liquidity segmented into bucket domains. +`Treasury-Owned Liquidity`. The asset-scoped topology through which a treasury owns protocol liquidity and related strategic capital. Anchors, bucket families, and independent treasury owners are configuration. ### Actors @@ -186,6 +186,10 @@ The protection token used for the strategic constitutional surface on the curren The flagship tactical governance and builder-coordination token on the current line. See [$BLDR Builder Economy](../concepts/builder-economy.en.md). +### `BLDR Anchor` + +The sole immutable System Actor owner of protocol-created `$NTVE/$BLDR` LP. The second-order `$BLDR` topology has no A/B/C/D bucket family, so the owner carries no bucket-letter qualifier. + ### `stXXX` Transferable staking receipts representing share-based ownership in staking pools. diff --git a/wiki/glossary/core-terms.ru.md b/wiki/glossary/core-terms.ru.md index 016c7d5a..1f0c50c4 100644 --- a/wiki/glossary/core-terms.ru.md +++ b/wiki/glossary/core-terms.ru.md @@ -65,7 +65,7 @@ related: ### TOL -`Treasury-Owned Liquidity` — ликвидность, принадлежащая казне. Протокол управляет ею и разделяет её между доменами корзин. +`Treasury-Owned Liquidity` — относящаяся к одному активу топология, через которую казна владеет протокольной ликвидностью и связанным стратегическим капиталом. Число опор, семейств корзин и независимых казн задаётся конфигурацией. ### Actors @@ -187,6 +187,10 @@ related: Флагманский токен тактического управления и координации созидателей в действующей линии. См. [Экономику созидателей $BLDR](../concepts/builder-economy.ru.md). +### `BLDR Anchor` + +Единственный неизменяемый System Actor, владеющий созданными протоколом LP `$NTVE/$BLDR`. Во вторичной топологии `$BLDR` нет семейства корзин A/B/C/D, поэтому имя этой опоры не содержит буквенного квалификатора корзины. + ### `stXXX` Передаваемые расписки DEOS Staking, которые удостоверяют долевое владение в пулах стейкинга. diff --git a/wiki/index.en.md b/wiki/index.en.md index a30080cd..7e3981ba 100644 --- a/wiki/index.en.md +++ b/wiki/index.en.md @@ -60,7 +60,7 @@ This wiki is a self-contained knowledge product for understanding that framework - [TMCTOL Standard](concepts/tmctol-standard.en.md) — economic standard and token laws - [TOL Bucket Scenarios](concepts/tol-bucket-scenarios.en.md) — concrete A/B/C/D bucket wakeups and treasury lanes - [$BLDR Builder Economy](concepts/builder-economy.en.md) — useful-work invoices, tactical funding, and the founder-as-first-worker pattern -- [Token Surfaces](concepts/token-surfaces.en.md) — Native, VETO, BLDR, receipts, and LP roles +- [Token Surfaces](concepts/token-surfaces.en.md) — Native, `$VETO`, `$BLDR`, receipts, and LP roles - [Actors System](overview/actor-system.en.md) — autonomous protocol actors - [DEOS Router](overview/router.en.md) — routing, fees, and protocol liquidity decisions - [Typed Observations](overview/typed-observations.en.md) — current scalar truth and reactive delivery diff --git a/wiki/index.ru.md b/wiki/index.ru.md index 632acb14..1892bfe8 100644 --- a/wiki/index.ru.md +++ b/wiki/index.ru.md @@ -61,7 +61,7 @@ DEOS — фреймворк среды исполнения для програ - [Стандарт TMCTOL](concepts/tmctol-standard.ru.md) — экономический стандарт и правила обращения токена; - [Сценарии корзин TOL](concepts/tol-bucket-scenarios.ru.md) — конкретные пробуждения корзин A/B/C/D и направления средств казны; - [Экономика созидателей $BLDR](concepts/builder-economy.ru.md) — оплата полезной работы, целевое финансирование и модель основателя как первого участника труда; -- [Роли токенов](concepts/token-surfaces.ru.md) — назначение Native, VETO, BLDR, расписок и LP; +- [Роли токенов](concepts/token-surfaces.ru.md) — назначение Native, `$VETO`, `$BLDR`, расписок и LP; - [Система Actors](overview/actor-system.ru.md) — автономные исполнители протокола; - [DEOS Router](overview/router.ru.md) — маршрутизация, комиссии и выбор ликвидности; - [Типизированные наблюдения](overview/typed-observations.ru.md) — актуальные скалярные данные и доставка изменений; diff --git a/wiki/math/tmctol-formulas.en.md b/wiki/math/tmctol-formulas.en.md index c910a51b..2fb14e67 100644 --- a/wiki/math/tmctol-formulas.en.md +++ b/wiki/math/tmctol-formulas.en.md @@ -10,6 +10,8 @@ available_locales: - ru sources: - resource: ../../docs/tmctol.specification.en.md + - resource: ../../docs/builder-economy.contract.en.md + - resource: ../../simulator/README.md status: stable audience: developer tags: @@ -67,6 +69,37 @@ When a mint occurs, the minted token output is distributed according to the conf The collateral payment is transferred separately to its resolved protocol destination; this ratio does not split the foreign collateral. +## DEOS Reference: Second-Order `$BLDR` Allocation + +The anchor/treasury TOL component of second-order `$BLDR` TMCTOL divides the protocol mint allocation equally between anchor liquidity and BLDR Treasury. With total issuance `M`, recipient output `U`, protocol output `T = M - U`, anchor allocation `A`, and direct treasury allocation `D`: + +```text +A = T - floor(T/2) +D = floor(T/2) +U + A + D = M +``` + +All `$NTVE` collateral is assigned to the anchor-liquidity lane. The first-order reference also assigns half of its two-thirds protocol issuance to Bucket A, so both orders direct approximately `M/3` issuance to immutable anchor liquidity. Their collateral rules differ: first-order Bucket A receives `C/2`, while BLDR Anchor receives all collateral `C`. + +Parent Bucket B recycling is route-dependent. If market acquisition returns `Q` existing `$BLDR`: + +```text +burn = floor(Q/2) +recycled_treasury = Q - burn +net_issuance_change = -burn +``` + +If the Router selects the `$BLDR` TMC and full new issuance is `M` with recipient output `U`: + +```text +burn = floor(U/2) +recycled_treasury = U - burn +net_issuance_change = M - burn +treasury_change = direct_treasury_allocation + recycled_treasury +``` + +The same acquisition policy can therefore contract supply through XYK or expand collateralized supply through TMC while funding both the immutable anchor and treasury. + ## XYK Constant Product (Floor Protection) In the idealized positive-reserve XYK model, protocol-owned liquidity creates an asymptotic price curve that stays above zero for any finite sale. This statement does not guarantee a market price or pool liveness. @@ -83,6 +116,18 @@ Price = R_foreign' / R_native' Because $R_{foreign}'$ approaches zero asymptotically, the price can deteriorate indefinitely but never actually reaches zero for any finite $\Delta S$. +## Canonical Reported Floor + +A public floor report applies a named sellable-pressure fraction `λ` to a stated supply basis and compares the stressed price with an explicit reference price: + +```text +x_reported = λ_reported · S_support_scope +P_stress(x) = k / (R_native + x)² +reported_floor_ratio = P_stress(x_reported) / P_ceiling_ref +``` + +The report counts only the current proportional reserve claim of positive-LP anchor or explicitly active-support positions. Dormant LP does not count until explicitly activated. Historical contribution fields are cost-basis telemetry and cannot be added to the same live pool reserves. Missing live Bucket A anchor support sets `governance_state` to `degraded`. Named pressure presets derive from configured allocation shares rather than assumed default percentages. + ## Equilibrium and Backing Metrics The theoretical backing price $P_{backing}$ where the curve-implied market cap equals foreign reserves is: diff --git a/wiki/math/tmctol-formulas.ru.md b/wiki/math/tmctol-formulas.ru.md index 6d534fca..48ffa66e 100644 --- a/wiki/math/tmctol-formulas.ru.md +++ b/wiki/math/tmctol-formulas.ru.md @@ -11,6 +11,8 @@ available_locales: - ru sources: - resource: ../../docs/tmctol.specification.en.md + - resource: ../../docs/builder-economy.contract.en.md + - resource: ../../simulator/README.md status: stable audience: developer tags: @@ -68,6 +70,37 @@ F_required = P₀·ΔS + m·(S₀·ΔS + ΔS²/2)/PRECISION Платёж в обеспечивающем активе отдельно переводится на определённый протоколом адрес. Это соотношение не делит внешнее обеспечение. +## Эталон DEOS: распределение `$BLDR` второго порядка + +Компонент TOL с независимыми опорой и казной в TMCTOL второго порядка для `$BLDR` поровну делит протокольную долю выпуска между опорной ликвидностью и `BLDR Treasury`. Пусть `M` — полный выпуск, `U` — доля адресата, `T = M - U` — доля протокола, `A` — опорная доля, а `D` — прямое пополнение казны: + +```text +A = T - floor(T/2) +D = floor(T/2) +U + A + D = M +``` + +Весь залог в `$NTVE` направляется в контур опорной ликвидности. Первичная эталонная топология также направляет половину своей протокольной доли выпуска в `Bucket A`, поэтому оба порядка передают около `M/3` в неизменяемую опорную ликвидность. Правила обеспечения различаются: первичный `Bucket A` получает `C/2`, а `BLDR Anchor` — весь объём `C`. + +Повторное использование капитала родительского `Bucket B` зависит от маршрута. Если рыночное приобретение возвращает `Q` существующих `$BLDR`: + +```text +burn = floor(Q/2) +recycled_treasury = Q - burn +net_issuance_change = -burn +``` + +Если маршрутизатор выбирает TMC для `$BLDR`, полный новый выпуск равен `M`, а доля адресата — `U`: + +```text +burn = floor(U/2) +recycled_treasury = U - burn +net_issuance_change = M - burn +treasury_change = direct_treasury_allocation + recycled_treasury +``` + +Одна политика приобретения может сокращать предложение через XYK либо расширять обеспеченное предложение через TMC, одновременно пополняя неизменяемую опору и казну. + ## Постоянное произведение XYK и нижняя граница В идеализированной модели XYK с положительными резервами ликвидность во владении протокола задаёт асимптотическую ценовую кривую, которая остаётся выше нуля при любой конечной продаже. Это утверждение не гарантирует ни рыночную цену, ни работоспособность пула. @@ -84,6 +117,18 @@ R_foreign' = k / R_native' Поскольку $R_{foreign}'$ асимптотически приближается к нулю, цена может сколь угодно снижаться, но при конечном объёме продажи `ΔS` не достигает нуля. +## Канонический отчёт о нижней границе + +Публичный отчёт применяет явно заданную долю предложения, доступного для продажи, `λ` к указанной базе и сравнивает стрессовую цену с выбранным ориентиром: + +```text +x_reported = λ_reported · S_support_scope +P_stress(x) = k / (R_native + x)² +reported_floor_ratio = P_stress(x_reported) / P_ceiling_ref +``` + +В отчёте учитывают только текущую пропорциональную долю резервов позиций с положительным объёмом LP, отнесённых к опорным или явно действующим. Бездействующий LP не учитывается до явной активации. Исторические суммы взносов отражают учётную стоимость и не прибавляются к резервам того же пула. При отсутствии действующей опоры `Bucket A` поле `governance_state` принимает значение `degraded`. Явно заданные сценарии продаваемого объёма рассчитывают по настроенным долям распределения, а не по заранее заданным процентам. + ## Равновесие и показатели обеспечения Теоретическая цена обеспечения $P_{backing}$, при которой подразумеваемая кривой капитализация совпадает с внешними резервами, равна приблизительно: diff --git a/wiki/overview/actor-system.en.md b/wiki/overview/actor-system.en.md index 56f9aafc..5e6f585c 100644 --- a/wiki/overview/actor-system.en.md +++ b/wiki/overview/actor-system.en.md @@ -53,9 +53,9 @@ The normative system contract requires: - Lifecycle rules for identity-only dormancy, atomic activation/deactivation, pause, failure, auto-close, manual close, and mandatory internal terminal transitions; - Adapter boundaries with runtime-derived worst-case weights so Actors orchestrates mechanics without owning DEX, staking, or asset logic. -Actor balances can function like trigger messages: an asset arriving on an actor account can wake the Actor Contract, and that pending signal must retain a bounded path to eventual eligibility. Manual and matched address events coalesce through the single `ActorHot.pending_signal` latch; admitted execution clears it atomically while deferral, pause, and scheduler movement preserve it. `OnObservationChange { feed }` now declares reconsideration against one exact typed feed without threshold, callback, payload, or per-revision execution semantics; Actors derives a duplicate-free bounded subscriber index from that source, and the Oracle hook now coalesces only the latest changed revision into reusable dirty-feed state without reading subscribers. An independently metered deferred worker traverses bounded subscriber pages, binds that state to the same latch, and leaves execution to the existing queue/wakeup scheduler. Observation, Manual, periodic-only, and mixed source sets cannot author `PercentageOfTrigger`; only applicable AddressEvent-only sources establish its exact cycle-start balance snapshot contract. +Actor balances can function like trigger messages: an asset arriving on an actor account can wake the Actor Contract, and that pending signal must retain a bounded path to eventual eligibility. Manual and matched address events coalesce through the single `pending_signal` latch; admitted execution clears it atomically while deferral, pause, and scheduler movement preserve it. Pause atomically moves Ready authority to Unsignaled, leaving its former FIFO slot as an ordinary tombstone; while the global breaker is active, a live Ready head remains at the same ticket instead of being consumed and republished. `ObservationChange { feed }` now declares reconsideration against one exact typed feed without threshold, callback, payload, or per-revision execution semantics; Actors derives a duplicate-free bounded subscriber index from that source, and the Oracle hook now coalesces only the latest changed revision into reusable dirty-feed state without reading subscribers. An independently metered deferred worker traverses bounded subscriber pages, binds that state to the same latch, and leaves execution to the existing queue/wakeup scheduler. Observation, Manual, periodic-only, and mixed source sets cannot author `PercentageOfTrigger`; only applicable AddressEvent-only sources establish its exact cycle-start balance snapshot contract. -`ObservationCrossing` is the sparse threshold trigger. It requires distinct fire and rearm thresholds, initializes from current canonical observation without retroactive activation, and retains every committed `previous -> current` revision transition until bounded ordered processing completes. One feed admits at most 9,000 User memberships and 10,000 total memberships, reserving 1,000 positions exclusively for System Actors. Active User Trigger state is backed by an exact refundable runtime-policy bond; replacement adjusts it atomically and deactivation or terminal close releases it. Rising and Falling memberships live in separate exact-threshold radix pages. Manual calls, address movement, broad observation changes, sparse crossings, and cadence deadlines therefore specialize detection but converge through one activation sink, one pending latch, one placement contract, and the same FIFO. +`ObservationCrossing` is the sparse threshold trigger. It requires distinct fire and rearm thresholds, initializes from current canonical observation without retroactive activation, and retains every committed `previous -> current` revision transition until bounded ordered processing completes. One feed admits at most 9,000 User memberships and 10,000 total memberships, reserving 1,000 positions exclusively for System Actors. Active User Trigger state is backed by an exact refundable runtime-policy bond; replacement adjusts it atomically and deactivation or terminal close releases it. Rising and Falling memberships live in separate exact-threshold radix pages. Manual calls, address movement, broad observation changes, sparse crossings, and cadence deadlines therefore specialize detection but converge through one activation sink, one pending latch, one placement contract, and the same FIFO. Current production admission processes four Crossing candidates per block, so the 9,000-User population needs at least 2,250 uncontended detector blocks. It does not satisfy the assurance target of 10,000 User crossings within 100 eligible blocks; System schedules and broad observation fanout are not substitutes. The Automation Observe view reads the bounded feed registry and one selected feed at a finalized block. It shows exact directional identity, scale-formatted scalar, producer/provenance, aggregation, lifecycle, update block, revision, authored age, and Fresh/Stale/Unavailable/Uninitialized status. It also states latest-state coalescing and warns that local pre-execution pool reserves provide neither external fair price nor MEV/ordering protection; history remains materialized. @@ -65,6 +65,8 @@ Funding uses ordinary inbound transfers rather than a dedicated value-transfer c Production execution separates two component-wise `Weight` domains. **Actor Control** has a hard ceiling of one third of schedulable block capacity and pays for detection, materialization, current-Step evaluation, FIFO movement, lifecycle bookkeeping, and bounded cleanup. **Shared Economic Execution** owns the remaining two thirds and pays both ordinary external calls and Actor Task effects through the same host mechanisms and Weight owners. +A generic live FIFO entry charges discovery, the loaded-state probe, source consumption, and one transition owner. If admission closes the actor, or a paid Crossing lacks its required current observation, one complete close/refusal owner replaces that entry's provisional prefix rather than being added to it. The refusal preserves the head and performs no Pipeline collection. Work on earlier tombstones or the next queue head remains separate. This accounting boundary changes neither FIFO order nor actor state semantics. + Under continuous demand, Shared Economic grants equal base turns to Actor effects and signed user dispatch. Either side may use capacity the other leaves idle, but it cannot consume the other's guaranteed turn while both are saturated. `RefTime` and `ProofSize` are checked independently: spare capacity in one component cannot compensate for exhaustion in the other. Borrowing changes available capacity only; it never changes FIFO order, Actor class neutrality, or the causal cutoff. This work-conserving composition is the **Economic Zipper**. Each block follows one consensus sequence: required context inherents, Mandatory Actor Prepass, signed external dispatch, Actor Drain, and finalization. Prepass is the sole pre-external Actor owner. It captures a fixed FIFO cutoff and admits at most one committed Step per Actor in the block. A Trigger occurrence observed in block `N` can become executable no earlier than `N + 1`; congestion may delay it further. `on_initialize` performs no Actor work, and neither the base pass nor Drain may bypass a live FIFO head because a later Step is cheaper. @@ -73,6 +75,8 @@ The system scales with the **active frontier**, not by scanning all identities. These rules are capacity and causality contracts, not latency guarantees. Strict FIFO, one Step per Actor per block, bounded materialization pages, and available two-dimensional Weight determine observed completion time. +FIFO progress also requires available observations for Trigger rearm and successful fee collection when payment capacity exists. If a latched Crossing cannot read its required current value, Opening commits no Pipeline fee or cycle and preserves the paid latch and live head; later Actors cannot bypass it despite spare Weight. In DEOS, paused feeds and old retained values remain available to rearm, but deactivated feeds do not. Observation loss grants no new close authority: recovery depends on restored prerequisites or an already-authorized lifecycle transition. + ## Verifiable Straight-Line Composition Each Actor Contract is represented publicly by bounded `ContractSteps`; `MaxContractSteps` limits the ordered `steps` field for both User and System actors. An absent Step `precondition` is unconditional; a present `Precondition` is one bounded DNF whose outer clauses compose with OR and timed predicates inside each clause compose with AND. Every admitted predicate is visited, any predicate error fails the whole expression, and a false precondition only advances to the next fixed step. `Opening` results freeze for the logical cycle and survive Continuation; `Current` reads run immediately before the step and observe successful earlier effects. No precondition introduces jumps, loops, callbacks, arbitrary calls, or authored successors. @@ -115,7 +119,7 @@ Off-chain feedback analysis separates observation-caused recurrence from shared ## Embedding Boundary -External runtimes can reuse `pallet-deos-actors` without inheriting the DEOS/TMCTOL System actor catalog. The host runtime provides bounded adapters for assets, caller-aware DEX quotes, staking shares, liquidity donation, funding authority, atomic fee collection, fallible ingress, and two-dimensional task weights. Actors owns scheduling, lifecycle, policy-aware amount resolution, fee reservation, and task orchestration. After read-only evaluation, each attempted User step calls `FeeCollector` at most once: non-executable outcomes charge evaluation-only, while executable outcomes charge evaluation plus execution together. The collector transfers the full charge into Fee Sink; downstream allocation remains outside Actors. The DEOS reference Fee Sink currently applies its 50/50 staking/liquidity Actor Contract; equal security/staking/liquidity thirds remain gated on permissionless collators and bounded security settlement. +External runtimes can reuse `pallet-deos-actors` without inheriting the DEOS/TMCTOL System actor catalog. The host runtime provides bounded adapters for assets, caller-aware DEX quotes, staking shares, liquidity donation, funding authority, atomic fee collection, fallible ingress, and two-dimensional task weights. Actors owns scheduling, lifecycle, policy-aware amount resolution, fee reservation, and task orchestration. After read-only evaluation, each attempted User step calls `FeeCollector` at most once: non-executable outcomes charge evaluation-only, while executable outcomes charge evaluation plus execution together. The collector transfers the full charge into Fee Sink; downstream allocation remains outside Actors. The DEOS reference Fee Sink currently applies its 50/50 staking/liquidity Actor Contract. The independent `template/pallets/actors/embedding-runtime` external-consumer fixture makes this boundary executable. It starts with zero System Actors, uses local account/asset types and smaller scheduler pages, and proves direct Executive ingress, fresh-genesis integrity, deterministic unsupported adapters, User/System Continuation, User exact-output swaps, System-only minting, try-state, and no-std operation. It is portability evidence, not a second product or prescribed topology. @@ -140,15 +144,15 @@ Task::Stake { asset, amount } Task::Unstake { asset, shares } ``` -Actors does not encode a DEOS-specific native-only staking task, collator selection, `stNTVE` naming, or `NTVE/stNTVE` LP custody. Runtime adapters decide what a generic staking position means, expose its share balance, and optionally map it to a transferable share asset for last-funding resolution. That share-asset identity remains stable for the admitted position key; a runtime upgrade must introduce a new key rather than reinterpret an active plan. Execution fails closed if the mapping disappears. In DEOS, the adapter routes native staking into generic `pallet-staking::stake` with the configured native asset id, while nomination security remains a separate locked-LP staking/governance surface. +Actors does not encode a DEOS-specific native-only staking task, collator selection, `stNTVE` naming, or `$NTVE/stNTVE` LP custody. Runtime adapters decide what a generic staking position means, expose its share balance, and optionally map it to a transferable share asset for last-funding resolution. That share-asset identity remains stable for the admitted position key; a runtime upgrade must introduce a new key rather than reinterpret an active plan. Execution fails closed if the mapping disappears. In DEOS, the adapter routes native staking into generic `pallet-staking::stake` with the configured native asset id, while nomination security remains a separate locked-LP staking/governance surface. This keeps Actors useful outside one tokenomic configuration. ## Current DEOS Role -On the current reference line, Actors is the execution substrate for runtime-side protocol behavior: burning, liquidity provisioning, treasury splitting, bucket handling, BLDR lane flows, and native staking LP provisioning. +On the current reference line, Actors is the execution substrate for runtime-side protocol behavior: burning, liquidity provisioning, treasury splitting, bucket handling, `$BLDR` lane flows, and native staking LP provisioning. -The shipped runtime reserves fifteen deterministic System addresses but enrolls only three active Actor Contracts at genesis: Burn Actor, Fee Sink, and BLDR Splitter. These contracts react to verified inbound value rather than periodic polling. Ten Mutable System identities start dormant with no Actor Contract, funding, fee, queue, wakeup, or cycle state. Activation accepts one typed active-contract input with an explicit schedule, Contract Steps, and funding policy, and validates it before enrollment. The two permanent Bucket A anchors remain custody-only deterministic accounts outside generic actor storage. Native staking LP provisioning can activate only after the receipt asset, staking pool, dormant identity, and non-empty `NTVE/stNTVE` AMM are ready. +The shipped runtime reserves fifteen deterministic System addresses but enrolls only three active Actor Contracts at genesis: Burn Actor, Fee Sink, and BLDR Splitter. These contracts react to verified inbound value rather than periodic polling. Twelve System identities start dormant with no Actor Contract, funding, fee, queue, wakeup, or cycle state. Ten are Mutable activation candidates. First-order Bucket A and BLDR Anchor are sealed Immutable identities: every control path rejects activation or close, while the runtime LP freezer admits incoming LP but exposes zero reducible balance to ordinary, admin-forced, internal transfer, burn, and LP-class destruction paths. Native staking LP provisioning can activate only after the receipt asset, staking pool, dormant identity, and non-empty `$NTVE/stNTVE` AMM are ready. The DEOS runtime also owns a bounded ranked manifest for certified System-to-System activation edges. Active System Contract installation or replacement is rejected before mutation when it declares an edge outside that acyclic topology. The projection is descriptive host metadata rather than Contract identity or an execution source; Oracle publishers, ordinary users, external markets, and uncertified transfers remain outside its guarantee. User Actors may form cycles without graph analysis, but every attempt remains fee-bearing, FIFO-bounded, limited to one execution per Actor per block, and subject to balance or fee-budget apoptosis when funding stops. diff --git a/wiki/overview/actor-system.ru.md b/wiki/overview/actor-system.ru.md index 1a0d2095..1a18b04b 100644 --- a/wiki/overview/actor-system.ru.md +++ b/wiki/overview/actor-system.ru.md @@ -54,9 +54,9 @@ Actors дает среде исполнения единый переиспол - Правил жизненного цикла для идентичности без `Actor Contract`, атомарного включения и отключения, паузы, ошибки, автоматического и ручного закрытия, а также обязательных внутренних конечных переходов; - Ограниченных интерфейсов среды исполнения с весами худшего случая, чтобы Actors координировал механику, но не владел правилами DEX, стейкинга или учета активов. -Баланс актора может служить сообщением запуска: поступление актива пробуждает следующий ограниченный план, а ожидающий сигнал обязан сохранять ограниченный путь к будущему допуску. Ручной запуск и подходящие события адреса объединяются единым флагом `ActorHot.pending_signal`: допущенное выполнение очищает его атомарно, а отсрочка, пауза и перемещение планировщиком сохраняют. `OnObservationChange { feed }` теперь объявляет повторную проверку одного точного типизированного канала без порога, обратного вызова, полезной нагрузки или обещания исполнения каждой редакции; Actors выводит из этого источника ограниченный индекс подписчиков без повторов, а обработчик Oracle, не читая подписчиков, сохраняет только последнюю изменённую редакцию в переиспользуемом состоянии с последним изменением канала. Отдельно измеряемый отложенный обработчик обходит ограниченные страницы подписчиков, связывает это состояние с тем же флагом и оставляет исполнение существующему планировщику очереди и пробуждения. `PercentageOfTrigger` запрещён для наборов источников, состоящих только из наблюдения, ручного или периодического запуска, а также для их смешанных сочетаний: точный снимок баланса в начале запуска устанавливают только подходящие источники AddressEvent. +Баланс актора может служить сообщением запуска: поступление актива пробуждает следующий ограниченный план, а ожидающий сигнал обязан сохранять ограниченный путь к будущему допуску. Ручной запуск и подходящие события адреса объединяются единым флагом `pending_signal`: допущенное выполнение очищает его атомарно, а отсрочка, пауза и перемещение планировщиком сохраняют. Пауза атомарно переносит готовое полномочие в `Unsignaled`, оставляя прежнее место FIFO обычной удалённой записью; пока включён глобальный предохранитель, живой первый элемент `Ready` сохраняет тот же билет, а не расходуется и публикуется заново. `ObservationChange { feed }` теперь объявляет повторную проверку одного точного типизированного канала без порога, обратного вызова, полезной нагрузки или обещания исполнения каждой редакции; Actors выводит из этого источника ограниченный индекс подписчиков без повторов, а обработчик Oracle, не читая подписчиков, сохраняет только последнюю изменённую редакцию в переиспользуемом состоянии с последним изменением канала. Отдельно измеряемый отложенный обработчик обходит ограниченные страницы подписчиков, связывает это состояние с тем же флагом и оставляет исполнение существующему планировщику очереди и пробуждения. `PercentageOfTrigger` запрещён для наборов источников, состоящих только из наблюдения, ручного или периодического запуска, а также для их смешанных сочетаний: точный снимок баланса в начале запуска устанавливают только подходящие источники AddressEvent. -`ObservationCrossing` — разреженный триггер пересечения порога. Для него задают разные пороги срабатывания и возврата в готовность. При включении он опирается на текущее каноническое наблюдение и не срабатывает задним числом; каждый подтверждённый переход `previous -> current` сохраняется до завершения ограниченной упорядоченной обработки. Один канал допускает не более 9 000 пользовательских и 10 000 общих связей, поэтому 1 000 мест доступны только системным акторам. Состояние триггера активного пользовательского актора обеспечено точным возвратным залогом по правилам среды исполнения: замена меняет залог атомарно, а отключение или окончательное закрытие освобождает его. Записи для восходящего и нисходящего направлений хранятся на отдельных разреженных страницах, разбитых по точным порогам. Поэтому ручные вызовы, перемещения средств, общие изменения наблюдений, пересечения порогов и сроки периодического запуска обнаруживаются разными специализированными механизмами, но используют одну операцию запуска, один флаг ожидающего сигнала, единые правила размещения и общую FIFO-очередь. +`ObservationCrossing` — разреженный триггер пересечения порога. Для него задают разные пороги срабатывания и возврата в готовность. При включении он опирается на текущее каноническое наблюдение и не срабатывает задним числом; каждый подтверждённый переход `previous -> current` сохраняется до завершения ограниченной упорядоченной обработки. Один канал допускает не более 9 000 пользовательских и 10 000 общих связей, поэтому 1 000 мест доступны только системным акторам. Состояние триггера активного пользовательского актора обеспечено точным возвратным залогом по правилам среды исполнения: замена меняет залог атомарно, а отключение или окончательное закрытие освобождает его. Записи для восходящего и нисходящего направлений хранятся на отдельных разреженных страницах, разбитых по точным порогам. Поэтому ручные вызовы, перемещения средств, общие изменения наблюдений, пересечения порогов и сроки периодического запуска обнаруживаются разными специализированными механизмами, но используют одну операцию запуска, один флаг ожидающего сигнала, единые правила размещения и общую FIFO-очередь. Текущий производственный допуск обрабатывает четыре кандидата пересечения за блок, поэтому для 9 000 пользовательских связей требуется не менее 2 250 блоков без конкурирующей нагрузки. Реализация не выполняет контрольную цель 10 000 пользовательских пересечений за 100 подходящих блоков; системные расписания и общий обход изменений наблюдений её не заменяют. Раздел Automation → Observe читает ограниченный реестр каналов и один выбранный канал на финализированном блоке. Для него показаны точное направление, скалярное значение с учётом масштаба, производитель и происхождение данных, способ агрегации, состояние, блок обновления, редакция, заданный предельный возраст и статус `Fresh`, `Stale`, `Unavailable` или `Uninitialized`. Интерфейс объясняет, что изменения объединяются по самому новому состоянию, и предупреждает: локальные резервы пула до исполнения не доказывают внешнюю справедливую цену и не защищают порядок транзакций или MEV; история остаётся материализованной. @@ -66,6 +66,8 @@ Actors дает среде исполнения единый переиспол Рабочее исполнение разделяет двухмерный `Weight` на две области. **Управление Actors** получает жёсткий предел в одну треть доступной вместимости блока и оплачивает обнаружение, материализацию, проверку текущего шага, движение FIFO-очереди, учёт жизненного цикла и ограниченную очистку. **Общее экономическое исполнение** получает оставшиеся две трети и оплачивает как обычные внешние вызовы, так и эффекты задач Actors через одинаковые механизмы среды и одинаковых владельцев веса. +Для обычного активного элемента FIFO отдельно учитываются обнаружение, проверка загруженного состояния, потребление источника и один владелец перехода. Если допуск закрывает актора или у оплаченного Crossing отсутствует обязательное текущее наблюдение, один полный владелец закрытия или отказа заменяет предварительный учёт этого элемента, а не складывается с ним. Отказ сохраняет голову и не взимает сбор за Pipeline. Работа над предшествующими удалёнными записями и следующей головой очереди учитывается отдельно. Эта граница учёта не меняет порядок FIFO и смысл состояния актора. + При непрерывном спросе общее экономическое исполнение предоставляет равные базовые доли эффектам Actors и подписанным пользовательским вызовам. Каждая сторона может использовать остаток, который другая сторона не заняла, но при одновременном насыщении не может отнять её гарантированную долю. `RefTime` и `ProofSize` проверяются независимо: свободный запас одного измерения не компенсирует исчерпание другого. Передача свободного остатка меняет только доступную вместимость, но не порядок FIFO, равенство классов акторов и причинный рубеж. Такая не теряющая свободный ресурс композиция называется **экономической сцепкой** (`Economic Zipper`). Каждый блок проходит одну согласованную последовательность: обязательный контекст, обязательный предварительный проход Actors (`Mandatory Actor Prepass`), подписанные внешние вызовы, заключительный проход Actors (`Actor Drain`) и финализация. Предварительный проход — единственный владелец работы Actors до внешних вызовов. Он фиксирует рубеж FIFO и допускает не более одного зафиксированного шага каждого актора за блок. Сигнал, замеченный в блоке `N`, может быть исполнен не раньше `N + 1`; перегрузка способна отложить его ещё дальше. `on_initialize` не выполняет работу Actors, а базовый проход и Drain не обходят первый активный элемент FIFO ради более дешёвого последующего шага. @@ -74,6 +76,8 @@ Actors дает среде исполнения единый переиспол Это правила вместимости и причинности, а не обещание времени обслуживания. Наблюдаемое время завершения определяют строгая FIFO-очередь, один шаг актора за блок, ограниченные страницы материализации и доступный двухмерный `Weight`. +Для продвижения очереди также нужны доступные наблюдения для повторного включения `Trigger` и успешный сбор комиссии при достаточных средствах. Если у Crossing уже зафиксирован оплаченный сигнал готовности, но необходимое текущее значение недоступно, Opening не приводит ни к списанию комиссии Pipeline, ни к началу цикла. Сигнал сохраняется, а текущий головной элемент FIFO-очереди остаётся на месте. Следующие акторы не могут его обойти даже при запасе `Weight`. В DEOS приостановленный канал и старое сохранённое значение остаются доступными для повторного включения `Trigger`, а деактивированный канал — нет. Потеря наблюдения не даёт новых полномочий закрытия: продвижение зависит от восстановления необходимых условий либо уже разрешённого перехода жизненного цикла. + ## Проверяемая линейная композиция Публичное представление `Actor Contract` — ограниченный тип `ContractSteps`; `MaxContractSteps` задаёт общий предел упорядоченного поля `steps` для пользовательских и системных акторов. Отсутствующее поле шага `precondition` означает безусловное выполнение; присутствующий `Precondition` содержит одну ограниченную ДНФ, где внешние группы связаны ИЛИ, а временные предикаты внутри группы — И. Исполнитель посещает каждый допущенный предикат; ошибка предиката отклоняет всё выражение, а ложное предусловие переводит исполнение к следующему фиксированному шагу. Результаты `Opening` фиксируются на логический цикл и сохраняются в Continuation; `Current` читается непосредственно перед шагом и видит успешные эффекты предыдущих шагов. Предусловие не вводит переходы, циклы, обратные или произвольные вызовы. @@ -118,7 +122,7 @@ Actors хранит состояние нехватки обслуживания Внешняя среда исполнения может переиспользовать `pallet-deos-actors`, не наследуя каталог системных акторов DEOS/TMCTOL. Она предоставляет ограниченные интерфейсы активов, котировок DEX с учетом вызывающего аккаунта, долей стейкинга, пожертвования ликвидности, полномочий финансирования, атомарного сборщика комиссий с возвращаемыми ошибками, адаптеров входящего потока с типизированными ошибками и двухмерных весов задач. Actors владеет планированием, жизненным циклом, разрешением сумм по правилам задачи, резервом комиссий и координацией задач. -После проверок без изменения состояния каждый пользовательский шаг вызывает `FeeCollector` не более одного раза. Невыполняемый шаг платит только за проверку, а выполняемый — одной суммой за проверку и исполнение. Сборщик переводит всю комиссию в Fee Sink; дальнейшее распределение не принадлежит Actors. В эталонной среде DEOS Fee Sink сейчас делит поток поровну между входом стейкинга и обеспечением ликвидности; равные трети для безопасности, стейкинга и ликвидности остаются закрыты до появления открытого набора коллаторов и ограниченного расчета их вознаграждений. +После проверок без изменения состояния каждый пользовательский шаг вызывает `FeeCollector` не более одного раза. Невыполняемый шаг платит только за проверку, а выполняемый — одной суммой за проверку и исполнение. Сборщик переводит всю комиссию в Fee Sink; дальнейшее распределение не принадлежит Actors. В эталонной среде DEOS Fee Sink сейчас делит поток поровну между входом стейкинга и обеспечением ликвидности. Независимая проверочная среда внешнего потребителя `template/pallets/actors/embedding-runtime` делает эту границу исполняемой. Она начинает без системных Actors, использует собственные типы аккаунтов и активов, а также меньшие страницы планировщика. Её профили проверяют прямой вход через исполнитель транзакций, целостность состояния сразу после генезиса, детерминированные постоянные ошибки неподдерживаемых адаптеров, продолжение для пользовательских и системных акторов, пользовательский обмен с точным выходом, выпуск только системным актором, `try-state` и сборку без стандартной библиотеки. Это доказательство переносимости, а не второй продукт или предписанная топология. @@ -143,7 +147,7 @@ Task::Stake { asset, amount } Task::Unstake { asset, shares } ``` -Actors не кодирует особую для DEOS задачу стейкинга только нативного актива, выбор коллатора, название `stNTVE` или хранение `NTVE/stNTVE` LP. Среда исполнения определяет смысл позиции стейкинга, предоставляет баланс её долей и при необходимости связывает позицию с переводимым активом долей, чтобы вычислять сумму по правилу `PercentageOfLastFunding`. Идентификатор актива долей остаётся стабильным для допущенного ключа позиции: обновление среды исполнения обязано ввести новый ключ, а не переосмыслить действующий `Actor Contract`. Если связь исчезает, исполнение завершается безопасной ошибкой до изменения состояния. В DEOS этот интерфейс направляет нативный стейкинг в общий вызов `pallet-staking::stake` с настроенным идентификатором нативного актива, а безопасность номинаций обеспечивается отдельным механизмом управления и хранения заблокированных LP. +Actors не кодирует особую для DEOS задачу стейкинга только нативного актива, выбор коллатора, название `stNTVE` или хранение `$NTVE/stNTVE` LP. Среда исполнения определяет смысл позиции стейкинга, предоставляет баланс её долей и при необходимости связывает позицию с переводимым активом долей, чтобы вычислять сумму по правилу `PercentageOfLastFunding`. Идентификатор актива долей остаётся стабильным для допущенного ключа позиции: обновление среды исполнения обязано ввести новый ключ, а не переосмыслить действующий `Actor Contract`. Если связь исчезает, исполнение завершается безопасной ошибкой до изменения состояния. В DEOS этот интерфейс направляет нативный стейкинг в общий вызов `pallet-staking::stake` с настроенным идентификатором нативного актива, а безопасность номинаций обеспечивается отдельным механизмом управления и хранения заблокированных LP. Так Actors остается полезным за пределами одной токеномической конфигурации. @@ -151,7 +155,7 @@ Actors не кодирует особую для DEOS задачу стейки В текущей эталонной линии Actors служит исполнительным слоем протокольного поведения: сжигания, предоставления ликвидности, разделения казначейских потоков, обслуживания корзин, потоков `$BLDR` и подготовки ликвидности для нативного стейкинга. -Поставляемая среда исполнения резервирует пятнадцать детерминированных системных адресов, но при запуске включает только три `Actor Contract`: Burn Actor, Fee Sink и разделитель `$BLDR`. Эти контракты реагируют на подтверждённое поступление средств вместо периодического опроса. Десять изменяемых системных идентичностей начинают без `Actor Contract`, финансирования, комиссионной работы, очереди, пробуждения и циклов. Включение принимает один типизированный `Actor Contract` с явным расписанием, `ContractSteps` и правилами финансирования, а затем проверяет его до постановки в очередь. Два постоянных якоря корзины A остаются только аккаунтами хранения вне общего хранилища акторов. Подготовка LP для нативного стейкинга может включиться лишь после готовности актива-квитанции, пула стейкинга, отключенной идентичности и непустого AMM `NTVE/stNTVE`. +Поставляемая среда исполнения резервирует пятнадцать детерминированных системных адресов, но при запуске включает только три `Actor Contract`: Burn Actor, Fee Sink и разделитель `$BLDR`. Эти контракты реагируют на подтверждённое поступление средств вместо периодического опроса. Двенадцать системных идентичностей начинают без `Actor Contract`, финансирования, комиссионной работы, очереди, пробуждения и циклов; десять из них допускают последующее включение. Первичный `Bucket A` и `BLDR Anchor` являются запечатанными неизменяемыми идентичностями: все управляющие пути отклоняют их включение или закрытие, а ограничитель LP принимает входящие LP, но оставляет нулевой доступный для списания баланс для обычного, принудительного и внутреннего перевода, сжигания и уничтожения класса LP. Подготовка LP для нативного стейкинга может включиться лишь после готовности актива-квитанции, пула стейкинга, отключенной идентичности и непустого AMM `$NTVE/stNTVE`. Среда исполнения DEOS также владеет ограниченным ранжированным реестром подтверждённых связей запуска между системными акторами. Установка или замена активного системного `Actor Contract` отклоняется до изменения состояния, если контракт объявляет связь вне разрешённого ациклического графа. Проекция описывает этот граф, но не входит в идентичность контракта и не служит источником исполнения. Внешние издатели Oracle, обычные пользователи, внешние рынки и неподтверждённые переводы не входят в эту гарантию. Пользовательские акторы могут образовывать циклы без анализа графа, но каждая попытка оплачивается, подчиняется общей очереди, выполняется не более одного раза на актор за блок и прекращается при исчерпании баланса или бюджета комиссий без нового финансирования. diff --git a/wiki/overview/randomness-strategy.en.md b/wiki/overview/randomness-strategy.en.md index 132a2de2..d4f72480 100644 --- a/wiki/overview/randomness-strategy.en.md +++ b/wiki/overview/randomness-strategy.en.md @@ -1,7 +1,7 @@ --- type: overview title: Randomness Strategy -description: DEOS keeps randomness outside the current Actors contract. The local VRF line was retired, Actors timers now use deterministic cadence only, and any future probabilistic trigger requires a real financially secure entropy contract. +description: DEOS has no local VRF pallet or probabilistic Actors triggers. Actors timers use deterministic cadence, and the runtime uses a trusted collator set. locale: en canonical_page_id: randomness-strategy translation_status: source @@ -30,7 +30,7 @@ related: Randomness is not a first-class product story in the current DEOS launch line. The docs treat it as a secondary infrastructure concern and deliberately simplify it. -The old local `pallet-vrf` line was retired. Actors now exposes deterministic timer cadence only and performs no probability sampling or hash fallback. The preferred long-term randomness direction remains a real relay-provided beacon for consumers that can justify it. +The old local `pallet-vrf` line was retired. Actors now exposes deterministic timer cadence only and performs no probability sampling or hash fallback. ## Current Position @@ -48,11 +48,11 @@ This is intentionally framed as an honest simplification rather than a hidden cl The docs explain that the local VRF path carried too much protocol-owned complexity for the current product needs. Same-block fairness is no longer required, and the project no longer wants to maintain a second local entropy economy just to preserve optional cryptographic ambition. -## Preferred Future Direction +## Current Randomness Boundary -The preferred future is not “rebuild a better local randomness market.” It is “adopt a real relay beacon if the relay ecosystem eventually exposes a parachain-consumable per-block protocol beacon with a stable production contract.” +The runtime does not use a relay-provided per-block randomness beacon. Epoch-scale relay randomness does not satisfy its per-block entropy contract. -Until that exists, the project explicitly refuses to pretend that currently visible epoch-scale relay randomness items solve the product problem. +The trusted collator set is an explicit limitation, not evidence of permissionless fairness. ## Why This Matters for Governance and AA-Actor @@ -60,7 +60,7 @@ Randomness simplification narrows the launch contract: - Actors Has No Probability Gate In The Current Contract - Governance Does Not Need To Carry A Second Entropy Economy -- Permissionless Collator Expansion Stays Gated Behind A Stronger Future Randomness Contract +- Permissionless Collators Are Not Part Of The Current Runtime Baseline ## Related diff --git a/wiki/overview/randomness-strategy.ru.md b/wiki/overview/randomness-strategy.ru.md index e0b36030..65406035 100644 --- a/wiki/overview/randomness-strategy.ru.md +++ b/wiki/overview/randomness-strategy.ru.md @@ -1,7 +1,7 @@ --- type: overview title: Стратегия использования случайности -description: Случайность не входит в нынешний контракт Actors. Локальный VRF удалён, таймеры Actors работают только в детерминированном ритме, а будущий вероятностный запуск потребует надёжного и экономически защищённого источника энтропии. +description: В DEOS нет локального паллета VRF и вероятностных запусков Actors. Таймеры Actors детерминированы, а среда исполнения использует доверенный набор коллаторов. locale: ru canonical_page_id: randomness-strategy translation_of: randomness-strategy.en.md @@ -31,7 +31,7 @@ related: В нынешней линии запуска случайность не считается самостоятельной продуктовой функцией. Документация рассматривает её как второстепенную задачу инфраструктуры и намеренно упрощает этот слой. -Прежний локальный `pallet-vrf` удалён. Actors теперь поддерживает только детерминированный ритм таймеров: в нём нет ни вероятностной выборки, ни резервного способа получения случайности. Для иных потребителей, способных обосновать такую необходимость, предпочтительным долгосрочным источником остаётся настоящий маяк ретрансляционной цепи. +Прежний локальный `pallet-vrf` удалён. Actors теперь поддерживает только детерминированный ритм таймеров: в нём нет ни вероятностной выборки, ни резервного способа получения случайности. ## Нынешняя позиция @@ -43,17 +43,17 @@ related: - На этапе запуска действует доверенный набор коллаторов `Invulnerables`; - Таймеры Actors детерминированы и не зависят от энтропии. -Это честное временное ограничение, а не скрытое заявление о сильной криптографической справедливости в открытом наборе участников. +Это явное ограничение текущей реализации, а не заявление о сильной криптографической справедливости в открытом наборе участников. ## Почему отказались от локального VRF Локальная схема VRF создавала слишком много сложности, которой должен был владеть сам протокол, при слишком малой пользе для нынешнего продукта. Справедливость внутри одного блока больше не входит в требования, поэтому DEOS не поддерживает отдельную локальную экономику энтропии лишь ради необязательных криптографических возможностей в будущем. -## Предпочтительное будущее +## Текущие ограничения случайности -Цель — не строить более сложный локальный рынок случайности, а принять настоящий маяк ретрансляционной цепи, если её экосистема предоставит парачейнам стабильный производственный контракт на новый протокольный маяк для каждого блока. +Среда исполнения не использует маяк ретрансляционной цепи для поблочной случайности. Показатели случайности масштаба эпохи не удовлетворяют её контракту на поблочную энтропию. -Пока такого механизма нет, доступные показатели случайности масштаба эпохи не считаются подходящей заменой. +Доверенный набор коллаторов — явное ограничение, а не доказательство справедливости открытого участия. ## Почему это важно для управления и Actors @@ -61,7 +61,7 @@ related: - В нынешнем контракте Actors нет вероятностного запускающего условия; - Управлению не приходится обслуживать вторую экономику вокруг генерации энтропии; -- Переход к открытому набору коллаторов остаётся заблокирован до появления более сильного внешнего контракта на случайность. +- Открытый набор коллаторов не входит в текущую реализацию среды исполнения. ## Связанные страницы diff --git a/wiki/overview/staking.en.md b/wiki/overview/staking.en.md index 8f585b27..bc25a894 100644 --- a/wiki/overview/staking.en.md +++ b/wiki/overview/staking.en.md @@ -31,7 +31,7 @@ related: DEOS staking is a multi-asset share-vault system. Each registered staking asset has one deterministic pool account and share/receipt accounting so backing can rise without writing rewards to every holder. -The native staking contract separates liquid `$NTVE -> stNTVE` share-vault accounting from collator nomination. The current `TrustedSet` mode uses permissioned collators and prevents new LP-backed nomination obligations. `LpBackedSelection` uses locked `NTVE/stNTVE` LP; a plain `stNTVE` balance never serves as the collator-security signal. +The native staking contract separates liquid `$NTVE -> stNTVE` share-vault accounting from collator nomination. The current `TrustedSet` mode uses permissioned collators and prevents new LP-backed nomination obligations. `LpBackedSelection` uses locked `$NTVE/stNTVE` LP; a plain `stNTVE` balance never serves as the collator-security signal. ## Share-Vault Model @@ -75,8 +75,8 @@ This is a vault deposit and receipt mint, not an ordinary AMM swap. It increases ```text $NTVE + stNTVE - -> add liquidity to NTVE/stNTVE - -> receive NTVE/stNTVE LP + -> add liquidity to $NTVE/stNTVE + -> receive $NTVE/stNTVE LP -> lock_native_lp_for_collator(lp_asset_id, amount, operator) ``` diff --git a/wiki/overview/staking.ru.md b/wiki/overview/staking.ru.md index 901742ed..38f96186 100644 --- a/wiki/overview/staking.ru.md +++ b/wiki/overview/staking.ru.md @@ -32,7 +32,7 @@ related: Стейкинг DEOS — система пулов с учётом долей для нескольких активов. У каждого зарегистрированного актива есть один детерминированный счёт пула и учёт долей или квитанций, поэтому обеспечение может расти без записи награды каждому держателю. -Для нативного актива ликвидный учёт `$NTVE -> stNTVE` отделён от номинации коллаторов. `TrustedSet` использует разрешённый набор коллаторов и не допускает новых обязательств, обеспеченных LP. `LpBackedSelection` использует заблокированные LP пары `NTVE/stNTVE`; один лишь баланс `stNTVE` никогда не служит сигналом безопасности коллатора. +Для нативного актива ликвидный учёт `$NTVE -> stNTVE` отделён от номинации коллаторов. `TrustedSet` использует разрешённый набор коллаторов и не допускает новых обязательств, обеспеченных LP. `LpBackedSelection` использует заблокированные LP пары `$NTVE/stNTVE`; один лишь баланс `stNTVE` никогда не служит сигналом безопасности коллатора. ## Пул с учётом долей @@ -76,8 +76,8 @@ $NTVE ```text $NTVE + stNTVE - -> add liquidity to NTVE/stNTVE - -> receive NTVE/stNTVE LP + -> add liquidity to $NTVE/stNTVE + -> receive $NTVE/stNTVE LP -> lock_native_lp_for_collator(lp_asset_id, amount, operator) ``` diff --git a/wiki/usage/scripts-layer.en.md b/wiki/usage/scripts-layer.en.md index 8ee83ed7..a2be32ca 100644 --- a/wiki/usage/scripts-layer.en.md +++ b/wiki/usage/scripts-layer.en.md @@ -64,7 +64,7 @@ Important examples include: - `seed-web-client-state.sh`: Prepare wallet, swap, and native-staking state for live web-client testing - `export-papi-metadata.sh`: Export Rust runtime metadata and regenerate PAPI descriptors for the web client - `bootstrap-native-staking-local.sh check`: Read native staking bootstrap readiness without submitting transactions -- `bootstrap-native-staking-local.sh prepare-calls`: Emit the next plan-only Root/governance staking-admin or signed operator call data needed to register/initialize native staking, create the canonical `NTVE/stNTVE` pool, or seed initial liquidity +- `bootstrap-native-staking-local.sh prepare-calls`: Emit the next plan-only Root/governance staking-admin or signed operator call data needed to register/initialize native staking, create the canonical `$NTVE/stNTVE` pool, or seed initial liquidity - `authorized-upgrade-local.sh check`: Pin finalized runtime identity, compare live and local code, inspect strategic submission authority and `$VETO` issuance, and verify any pending authorized hash without submitting - `authorized-upgrade-local.sh prepare-authorization`: Emit candidate-bound stake, preimage, and strategic proposal call data without signing; protection `Pass` remains unavailable until the lifecycle is ready - `authorized-upgrade-local.sh apply`: Relay already-authorized runtime code bytes only with explicit `--submit` @@ -76,7 +76,7 @@ Important examples include: The native staking bootstrap path is split into two operator-safe tools: 1. `bootstrap-native-staking-local.sh prepare-calls` reads live state and prepares the next call data for the production/operator path -2. `bootstrap-native-staking-local.sh check` verifies that the canonical `NTVE/stNTVE` pool, native staking exchange rate, and dormant Native Staking Liquidity Actor are ready +2. `bootstrap-native-staking-local.sh check` verifies that the canonical `$NTVE/stNTVE` pool, native staking exchange rate, and dormant Native Staking Liquidity Actor are ready Both helpers are plan/read-only by default. The preparation helper never signs or submits transactions; it only emits call data plus the expected authority for each step. diff --git a/wiki/usage/scripts-layer.ru.md b/wiki/usage/scripts-layer.ru.md index 32a04efd..022f6403 100644 --- a/wiki/usage/scripts-layer.ru.md +++ b/wiki/usage/scripts-layer.ru.md @@ -63,7 +63,7 @@ related: - `seed-web-client-state.sh` — готовит кошелёк, обмен и нативный стейкинг для проверки веб-клиента в действующей сети; - `export-papi-metadata.sh` — экспортирует метаданные Rust и пересобирает дескрипторы PAPI для веб-клиента; - `bootstrap-native-staking-local.sh check` — проверяет готовность начальной настройки нативного стейкинга без отправки транзакций; -- `bootstrap-native-staking-local.sh prepare-calls` — выдаёт данные следующего вызова Root, администратора стейкинга в управлении или подписанного оператора для регистрации и инициализации стейкинга, создания канонического пула `NTVE/stNTVE` либо внесения начальной ликвидности; +- `bootstrap-native-staking-local.sh prepare-calls` — выдаёт данные следующего вызова Root, администратора стейкинга в управлении или подписанного оператора для регистрации и инициализации стейкинга, создания канонического пула `$NTVE/stNTVE` либо внесения начальной ликвидности; - `authorized-upgrade-local.sh check` — закрепляет финализированную идентичность среды исполнения, сравнивает сетевой и локальный код, проверяет полномочия на стратегическое предложение, выпуск `$VETO` и ожидающий авторизованный хеш без отправки транзакций; - `authorized-upgrade-local.sh prepare-authorization` — выдаёт привязанные к кандидату данные вызовов для доли, предварительного образа и стратегического предложения без подписи; защитный вариант `Pass` недоступен до готовности жизненного цикла; - `authorized-upgrade-local.sh apply` — передаёт уже авторизованный код только при явном `--submit`; @@ -75,7 +75,7 @@ related: Этот процесс разделён между двумя безопасными для оператора командами: 1. `bootstrap-native-staking-local.sh prepare-calls` читает состояние сети и готовит данные следующего вызова для рабочего операторского пути; -2. `bootstrap-native-staking-local.sh check` проверяет готовность канонического пула `NTVE/stNTVE`, обменного курса нативного стейкинга и неактивного Liquidity Actor нативного стейкинга. +2. `bootstrap-native-staking-local.sh check` проверяет готовность канонического пула `$NTVE/stNTVE`, обменного курса нативного стейкинга и неактивного Liquidity Actor нативного стейкинга. Обе команды по умолчанию только читают состояние или готовят план. Первая никогда не подписывает и не отправляет транзакции: она лишь выводит данные вызова и ожидаемые полномочия для каждого шага.