fix(eip8130): allow AuthorizeActor while locked when expiry outlives unlock - #4699
Conversation
…unlock Mirror Keystore PR #90: locked accounts may add actors or re-lease expiry on live actors, while RevokeActor and identity/policy rewrites stay frozen.
✅ Heimdall Review Status
|
…rize_op Structure the locked guard as keystore does: empty live slot is a new add; live slot allows expiry-only re-lease above the unlock floor.
Review SummaryThis PR moves lock enforcement from the ArchitectureThe refactoring is well-structured:
Error handling in block-production pathsThe new Test coverageThorough test coverage for:
No findingsThe implementation is correct, mirrors the reference contract, and has adequate test coverage. No issues identified. |
|
Caution This PR may regress performance. 1 benchmark(s) slower by more than 10% beyond the noise band: Benchmark results (advisory)Median time on the PR head versus the base branch, measured on the same host. Wall-clock, so a change is only flagged when it clears ±10% and the confidence intervals do not overlap. Only benchmarks past the ±10% threshold (plus new or dropped ones) are listed. This check never blocks a merge.
47 benchmark(s) within ±10% omitted. |
The helper took 8 arguments, tripping clippy::too_many_arguments. Its single caller already has `locked`, `now`, and `is_unsequenced` in scope, so inline the JIT-expiry skip and locked-account guard directly into the AuthorizeActor match arm. Behavior is unchanged.
|
✅ All benchmarks green — 14 within ±2% (deterministic instruction counts). View run Benchmark details (14)
|
Review SummaryThe PR moves EIP-8130 lock enforcement from batch-level authorization ( No critical or high-severity findings. The implementation is correct and well-tested. Specific observations: Correctness
Test Coverage
Block Production ImpactThis code runs in the EIP-8130 transaction validation/execution path. The new Minor Notes
LGTM — no blocking issues. |
| if is_unsequenced && config.expiry != 0 && config.expiry <= now { | ||
| continue; | ||
| } | ||
| if locked { |
There was a problem hiding this comment.
This adds a moving deadline (expiry > now + delay) that WatchManifest does not track. A sequenced tx can become invalid without any watched state change and be retried on subsequent builds.
There was a problem hiding this comment.
Agreed this is real, and it's a consequence of moving lock enforcement to apply-time — the now-relative predicate no longer flows into the WatchManifest the authorizer builds. A few notes on scope, and a proposed fix:
- Consensus is unaffected. At inclusion the applier re-runs with the block's real
now, so an op that has crossed the deadline revertsExpiryDoesNotOutliveUnlockdeterministically. This is purely a mempool-efficiency issue: the tx can linger and be retried across builds instead of being dropped when it lapses. - Only hard locks move. The deadline exists only while the account is hard-locked, where
unlock_floor = now + delay. A pending unlock uses a fixedunlocks_at, soexpiry <= unlocks_atis time-independent and doesn't drift. - Bounded expiry only.
expiry == 0always passes, so only bounded grants have a deadline: the op is valid whilenow < expiry - delay.
Proposed fix: fold that per-op deadline (min(expiry - delay) across the batch's hard-locked AuthorizeActor ops) into the manifest's effective_expiry, the same way sender/payer actor expiries already seed manifest_expiry / InvalidationKey::expiry_bucket in the validator — so the pool evicts the tx at the boundary rather than on a watched state change. That spans the applier → validator → manifest boundary and deserves its own tests, so I'd prefer to do it as a focused follow-up rather than expand this PR (happy to land it here instead if you'd rather — let me know). I'll open a follow-up issue/PR either way.
| return Err(ApplyError::ExpiryDoesNotOutliveUnlock); | ||
| } | ||
|
|
||
| let current = storage.resolve_live_actor_config(account, actor_id, now)?; |
There was a problem hiding this comment.
This reads persisted storage and misses inline-self updates held in state from earlier ops in the same batch. It should resolve self against the evolving AccountState; otherwise a second identity/scope rewrite bypasses AccountIsLocked.
There was a problem hiding this comment.
Fixed in c56387b. Added resolve_live_actor_config_with_state, which resolves the inline self from the evolving in-batch AccountState (the explicit actor_config and policy slots are written eagerly, so those are still read from storage). The locked guard now uses it, so an add-then-mutate of the inline self within a single locked batch is correctly caught as a live-entry identity change instead of reading as a fresh add. Added locked_self_release_uses_evolving_state_within_batch, which fails against the previous storage-based read.
| // verifying and estimation pipelines pass the real block | ||
| // timestamp so the skip is applied identically. | ||
| // Replayable JIT path: drop an already-lapsed grant without reverting. | ||
| if is_unsequenced && config.expiry != 0 && config.expiry <= now { |
There was a problem hiding this comment.
This differs from Solidity’s _isExpired, which uses now > expiry. I think we should align and use < now for parity.
There was a problem hiding this comment.
Good catch — aligned with _isExpired. Fixed in c56387b: the JIT skip now uses a shared ActorConfig::is_expired (expiry != 0 && now > expiry), so a grant with expiry == now is still live for that second and installs rather than being dropped. resolve_live_actor_config uses the same helper for consistency, and the boundary test (apply_skips_lapsed_jit_grant) was updated to assert the expiry == now grant lands.
- Resolve the locked-authorize live-entry check against the evolving in-batch AccountState instead of persisted storage, so an add-then-mutate of the inline self within one locked batch cannot bypass AccountIsLocked. Adds a resolve_live_actor_config_with_state helper + regression test. - Align the JIT expiry skip with Keystore._isExpired (strict now > expiry) via a new ActorConfig::is_expired; a grant with expiry == now is still live for that second and installs. Updates the boundary test.
…uthorize # Conflicts: # crates/execution/eip8130/src/account_config.rs
Review SummaryNo issues found. The changes are well-structured and correctly implement the locked-account Key observations:
|
Summary
AuthorizeActoris permitted for a new add or expiry-only re-lease when the granted expiry outlives the unlock floor (now + delayhard-locked,unlocksAtpending unlock;expiry == 0always passes).RevokeActorand delegation still revertAccountIsLocked; grants at or below the unlock floor revertExpiryDoesNotOutliveUnlock.Test plan
cargo test -p base-execution-eip8130(202 tests)ExpiryDoesNotOutliveUnlock; above floor / unbounded → landsAccountIsLockedAccountIsLocked; expired actor id treated as new add