Skip to content

feat(node): enforce TIP-1011 restrictions#3386

Merged
legion2002 merged 4 commits intotanishk/tip-1011-precompiles-exactfrom
tanishk/tip-1011-rest-exact
Apr 1, 2026
Merged

feat(node): enforce TIP-1011 restrictions#3386
legion2002 merged 4 commits intotanishk/tip-1011-precompiles-exactfrom
tanishk/tip-1011-rest-exact

Conversation

@legion2002
Copy link
Copy Markdown
Contributor

@legion2002 legion2002 commented Mar 30, 2026

Stack Context

This is the top PR in the TIP-1011 stack.

  • Parent: #3385 feat(precompiles): implement TIP-1011 restrictions
  • Child: none

What This PR Does

This PR enforces TIP-1011 at runtime:

  • handler-side validation and intrinsic gas accounting for key_authorization
  • execution-time enforcement of periodic limits and call scopes
  • rejection of invalid access-key contract-creation or scoped calls before execution
  • txpool validation so invalid transactions are filtered before propagation

This is the integration layer where the protocol shape from PR1 and the precompile semantics from PR2 are actually enforced by the node.

How To Review

  1. Start with crates/revm/src/handler.rs. This is the core of the PR: hardfork gating, intrinsic gas, same-transaction authorization behavior, and per-call enforcement.
  2. Then review crates/transaction-pool/src/validator.rs and confirm txpool admission matches the runtime rules closely enough to reject bad transactions early without diverging from execution.
  3. Review the remaining runtime tests and regressions after that.
  4. Think about the stack as a whole while reviewing this PR: how the signed payload from #3384 feeds the precompile state from #3385, and how this handler and validator layer consumes both.

Note

If you want the full end-to-end diff in one place, see #3323. It is useful for seeing how the ideas interact across the stack, and Cyclops also runs there.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0b1f5476ff

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@legion2002 legion2002 force-pushed the tanishk/tip-1011-precompiles-exact branch from 476b865 to 9f1ed91 Compare March 31, 2026 10:03
@legion2002 legion2002 force-pushed the tanishk/tip-1011-precompiles-exact branch from 9f1ed91 to 5434753 Compare March 31, 2026 10:10
@legion2002 legion2002 force-pushed the tanishk/tip-1011-rest-exact branch 3 times, most recently from b609885 to e5c4a35 Compare March 31, 2026 10:13
Comment on lines 1104 to 1114
let authorize_call = authorizeKeyCall {
keyId: access_key_addr,
signatureType: signature_type,
expiry,
enforceLimits: enforce_limits,
limits: precompile_limits,
config: KeyRestrictions {
expiry,
enforceLimits: enforce_limits,
limits: precompile_limits,
enforceAllowedCalls: enforce_allowed_calls,
allowedCalls: precompile_allowed_calls,
},
};
Copy link
Copy Markdown
Contributor

@0xrusowsky 0xrusowsky Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it probably doesn't matter much, but for perf reasons, i think fn authorize_key (and any other precompile fn used by the handler) shouldn't generally require the SolCall struct, as it is deconstructed in the precompile impl

imo it would be better to flatten the fn sig of fn authorize_key to avoid useless conversions

it may be optimized by the compiler, but feels cleaner (cc: @klkvr in case u are opinionated or think this proposal is not useful)

Copy link
Copy Markdown
Member

@fgimenez fgimenez left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm, might be good to extend a bit test coverage, the tip spec in the branch has updated test cases that are not used and could be interesting

tempo/tips/tip-1011.md

Lines 583 to 613 in e5c4a35

## Test Cases
1. Periodic reset after elapsed period.
2. No rollover of unused periodic allowance.
3. Address + multi-selector scope allow.
4. Address-only allow (`selector_rules=None`).
5. Deny when no scope matches.
6. `allowed_calls=None` allows all non-create calls.
7. `allowed_calls=Some([])` denies all calls.
8. Mixed one-time and periodic token limits.
9. Existing keys continue to function after the fork.
10. Batch validation rejects the transaction before execution when any call is invalid.
11. Shared key IDs across accounts cannot overwrite each other’s scopes.
12. Reject `allowed_calls` entries with `S > MAX_CALL_SCOPES`.
13. Reject any target scope with selector-rule count `> MAX_SELECTOR_RULES_PER_SCOPE`.
14. Reject calls that do not provide at least 4 selector bytes when explicit selector matching is required.
15. `setAllowedCalls(..., scope.selectorRules = [])` allows any selector on that target.
16. `setAllowedCalls` create-or-replace semantics are enforced.
17. `removeAllowedCalls(keyId, target)` removes that target scope and leaves the key in deny-all mode if no target scopes remain.
19. Address-only scopes allow selectorless/fallback-style calls to the scoped target.
20. Access-key transactions with CREATE as first call are rejected.
21. Access-key transactions with any CREATE in a batch are rejected.
22. For constrained TIP-20 selectors (`transfer`, `approve`, `transferWithMemo`), calls succeed iff calldata argument `0` is in the configured recipient set.
23. Single-recipient and multi-recipient selector rules both enforce the same membership rule.
24. Reject the transaction before execution when a selector rule with a recipient allowlist is matched and calldata is shorter than `4 + 32` bytes.
25. Reject the transaction before execution when a selector rule with a recipient allowlist is matched and ABI argument `0` is not canonically encoded as an address.
27. Reject selector rules with recipient allowlists for selectors outside the fixed constrained-selector set.
28. Reject duplicate selector rules for the same `(target, selector)`.
29. Reject duplicate recipients within a selector rule.
30. Reject key authorization when selector rules with recipient allowlists are used on a non-TIP-20 target.
like 10, 21, 27, 28

@legion2002 legion2002 force-pushed the tanishk/tip-1011-precompiles-exact branch from 5434753 to 42d8683 Compare March 31, 2026 19:04
@legion2002 legion2002 force-pushed the tanishk/tip-1011-rest-exact branch from f5c06cf to e92a22e Compare March 31, 2026 21:26
recipients.contains(&Address::from_slice(&recipient_word[12..]))
}

fn validate_inline_t3_call_scopes(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can we add doc comments to this function?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressed in 3a6c0bb

Ok(())
}

fn validate_t3_key_authorization_shape(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can we add doc comments on this function?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressed in 3a6c0bb

Comment on lines +172 to +203
"duplicate token limits are not allowed",
));
}

if limit.limit > U256::from(u128::MAX) {
return Err(TempoPoolTransactionError::Keychain(
"spending limit exceeds u128::MAX",
));
}
}
}

let Some(scopes) = auth.allowed_calls.as_ref() else {
return Ok(());
};

if scopes.len() > MAX_CALL_SCOPES as usize {
return Err(TempoPoolTransactionError::Keychain(
"too many call scopes in key authorization",
));
}

let mut seen_targets = HashSet::with_capacity(scopes.len());
for scope in scopes {
if !seen_targets.insert(scope.target) {
return Err(TempoPoolTransactionError::Keychain(
"duplicate call scope targets are not allowed",
));
}

let Some(selector_rules) = scope.selector_rules.as_ref() else {
continue;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we also add some lighweight comments here detailing what each group of checks are doing?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressed in 3a6c0bb

Comment on lines +139 to +143
fn call_scope_storage_slots(auth: &tempo_primitives::transaction::KeyAuthorization) -> u64 {
match auth.allowed_calls.as_ref() {
None => 0,
Some(scopes) if scopes.is_empty() => 1,
Some(scopes) => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like the comments for calculate_key_authorization_gas were not moved with the function. Can we move the comments to the correct function and add new doc comments for call_scope_storage_slots.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressed in 3a6c0bb

Comment on lines +1099 to +1128
let precompile_allowed_calls = key_auth
.allowed_calls
.as_ref()
.map(|scopes| {
scopes
.iter()
.filter_map(|scope| match scope.selector_rules.as_ref() {
Some(rules) if rules.is_empty() => None,
_ => Some(tempo_precompiles::account_keychain::CallScope {
target: scope.target,
selectorRules: scope
.selector_rules
.as_ref()
.map(|rules| {
rules
.iter()
.map(|rule| {
tempo_precompiles::account_keychain::SelectorRule {
selector: rule.selector.into(),
recipients: rule
.recipients
.clone()
.unwrap_or_default(),
}
})
.collect()
})
.unwrap_or_default(),
}),
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: consider making this a bit more readable.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressed in 3a6c0bb

@0xKitsune 0xKitsune added the cyclops Trigger Cyclops PR audit label Apr 1, 2026
@legion2002 legion2002 force-pushed the tanishk/tip-1011-rest-exact branch from e92a22e to 3a6c0bb Compare April 1, 2026 10:32
@legion2002 legion2002 force-pushed the tanishk/tip-1011-precompiles-exact branch from a29e4af to e51b9cf Compare April 1, 2026 13:00
@legion2002 legion2002 force-pushed the tanishk/tip-1011-rest-exact branch from 3a6c0bb to f2e4a1f Compare April 1, 2026 13:07
@legion2002 legion2002 merged commit 73d8adb into tanishk/tip-1011-precompiles-exact Apr 1, 2026
20 of 31 checks passed
@legion2002 legion2002 deleted the tanishk/tip-1011-rest-exact branch April 1, 2026 13:11
legion2002 added a commit that referenced this pull request Apr 1, 2026
## Stack Context

This is the middle PR in the TIP-1011 stack.
- Parent: [#3384](#3384)
`feat(primitives): add TIP-1011 wire format and ABI`
- Child: [#3386](#3386)
`feat(node): enforce TIP-1011 restrictions`

## What This PR Does

This PR implements TIP-1011 inside the `AccountKeychain` precompile:
- periodic spending-limit state and reset behavior
- allowed-call target / selector / recipient scoping
- precompile mutators and readers for the new restriction state
- hardfork-gated dispatch and direct readers that depend on the new
layout

This is the PR where the spec becomes concrete precompile and storage
behavior. It intentionally stops short of handler and txpool runtime
enforcement, which lands in PR3.

## How To Review

Review this like a smart-contract PR.

1. Start in `crates/precompiles/src/account_keychain/mod.rs` and verify
the storage model and invariants: uniqueness, rollover behavior,
normalization, selector and recipient validation, and any assumptions
about reads and writes.
2. Look for efficiency opportunities as you go: slot count, tight
packing, unnecessary writes, and whether the chosen layout is the right
tradeoff for expected access patterns.
3. Check access gating carefully for the important mutators, especially
`authorizeKey`, `setAllowedCalls`, `removeAllowedCalls`, and
`updateSpendingLimit`.
4. Review `crates/precompiles/src/account_keychain/dispatch.rs` for ABI
routing and fork gating.
5. As a final pass, think ahead to how the handler will interact with
these precompile functions and where they will be used in
[#3386](#3386).
6. Related context: [#3370](#3370)
is a draft PR that adds an enumerable map utility and makes the
`targets` / `target_scopes` path more efficient. It is worth keeping in
mind when evaluating this layout.

## Note

If you want the full end-to-end diff in one place, see
[#3323](#3323). It is useful for
seeing how the ideas interact across the stack, and Cyclops also runs
there.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cyclops Trigger Cyclops PR audit

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants