feat(smart-sessions): OneTimeUseId session wiring + burn-op helpers (RHI-5798) - #792
feat(smart-sessions): OneTimeUseId session wiring + burn-op helpers (RHI-5798)#792highskore wants to merge 5 commits into
Conversation
There was a problem hiding this comment.
Request changes — 1 blocker in one-time-use session data.
- design-smell — CLAUDE.md says every fix or feature needs a changeset, and public export additions require a minor changeset; this PR adds
oneTimeUsesession config and new@rhinestone/sdk/smart-sessionsexports without a.changeset/*.md, so the released SDK will not be versioned or changeloged for the new API.
Kevin score: 6/10 · impact 6 · economy 0
There was a problem hiding this comment.
Pull request overview
Adds SDK support for one-time-use smart sessions by wiring the OneTimeUseId policy into smart-session resolution and exposing helper utilities under @rhinestone/sdk/smart-sessions.
Changes:
- Introduces
one-time-use.tshelpers (ABI,encodeOneTimeUseIdInitData,oneTimeUseIdErc1271Policy,buildOneTimeUseBurnOp) and exports them from the smart-sessions entrypoint. - Updates
resolveSessionDatato install the once-policy on every action and relocate Permit2 claim policies onto the ERC-1271 policy surface for one-time-use sessions. - Adds unit tests covering the one-time-use wiring and updates public/session config types to include
oneTimeUse+policyAddresses.oneTimeUseId.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/smart-sessions/index.ts | Re-exports the one-time-use helpers/types from the smart-sessions public entrypoint. |
| src/modules/validators/smart-sessions/types.ts | Extends SessionDefinition/SessionPolicyAddresses to model one-time-use session configuration. |
| src/modules/validators/smart-sessions/resolve.ts | Implements the one-time-use wiring (once-policy on every action; Permit2 claim policies moved onto ERC-1271 surface). |
| src/modules/validators/smart-sessions/resolve.one-time-use.test.ts | Adds example-based tests validating one-time-use wiring in resolveSessionData. |
| src/modules/validators/smart-sessions/policies/addresses.ts | Adds optional oneTimeUseId policy address override (no default). |
| src/modules/validators/smart-sessions/one-time-use.ts | New module implementing the OneTimeUseId policy ABI + encoding/burn-op helpers. |
| src/modules/validators/smart-sessions/one-time-use.test.ts | Adds tests for initData encoding, policy entry generation, and burn-op construction. |
| src/config/account.ts | Mirrors the public config surface updates (adds oneTimeUse + oneTimeUseId docs/fields). |
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
SDK support for one-time-use sessions (RHI-5798): a session pins an id via
`oneTimeUse: { id }` (requires `policyAddresses.oneTimeUseId`) and settles at most
once per chain, across both settlement routes.
The policy is installed where each route actually enforces it, matching the
on-chain policy's install requirements and the E2E harness:
- executor route (checkAction): the once-policy is added to EVERY action's
policies — an action without it would fall through to the sudo fallback and
settle unbounded.
- permit2/arbiter route (checkERC1271): the Permit2 claim policy is co-located
with the once-policy on the 1271 list (an AND — the claim policy binds WHAT
settles, the once-policy binds HOW MANY TIMES), so claimPolicies moves onto
that surface.
Each settlement carries the burn op (buildOneTimeUseBurnOp) in preClaimExecutions:
consume(id) on the executor route, consumeFor(id, 0) on the permit2 route (the
orchestrator stamps the real order nonce as the witness before signing).
Exposes buildOneTimeUseBurnOp / oneTimeUseIdErc1271Policy / encodeOneTimeUseIdInitData
and the `oneTimeUse` session option on the public API.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XmP5wzhhzCja1useM2u6oZ
579fe27 to
9eac0c2
Compare
A one-time-use session enforces its Permit2 claim policy via the ERC-1271 policy list, so the on-chain claim (lockTag) surface must stay empty. The previous cut emptied the high-level `Session.claimPolicies` to achieve that, but the permit2 settlement signature builds the claim policy's calldata from that same list (claimPolicyData in session-signing), so it shipped an empty policy-data segment and the erc1271-resident claim policy had nothing to validate against. Keep the raw claim policies on the high-level session (as a normal permit2 session does) so the signature calldata is built, and add `claimPoliciesEnforcedVia1271` so getSessionData omits them from the on-chain claim surface instead. Result: enforced once via erc1271, encoded once on-chain, and the permit2 route settles. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XmP5wzhhzCja1useM2u6oZ
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
src/modules/validators/smart-sessions/one-time-use.ts:69
buildOneTimeUseBurnOpdoes not validate thatidis a valid non-zero uint256 (even thoughencodeOneTimeUseIdInitData/session wiring enforce that invariant). Adding the same validation here makes the helper fail fast and avoids producing burn ops that are guaranteed to revert.
const { policy, id, route } = params
const data =
Reject a zero id in buildOneTimeUseBurnOp (not just in encodeOneTimeUseIdInitData) so a caller can't emit a burn op whose id can never match a validly-pinned session (the policy rejects id 0 as "not configured"). Extracted a shared assertValidOneTimeUseId. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XmP5wzhhzCja1useM2u6oZ
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.
Suppressed comments (2)
Previously missed (2) — in code that hasn't changed since the last review.
src/modules/validators/smart-sessions/digest.ts:43
getSessionData()omitsclaimPoliciesonly whensession.claimPoliciesEnforcedVia1271is truthy. Because this flag is optional and not part of the publicSessiontype insrc/config/account.ts(seeinterface Sessionaround lines 592-603), consumers can easily construct/deserialize a Session without it and accidentally re-encode Permit2 claim policies onto the lockTag surface (breaking the intended one-time-use wiring). Consider inferring enforcement fromerc7739Policies.erc1271Policiesas a fallback (e.g., detectPERMIT2_CLAIM_POLICY_ADDRESSon the 1271 list).
// When the claim policies are enforced via the erc1271 surface they're
// already in erc7739Policies; encoding them here too would settle them on
// the on-chain claim (lockTag) surface as well.
claimPolicies: session.claimPoliciesEnforcedVia1271
? []
src/config/account.ts:559
- The new JSDoc uses
{@link buildOneTimeUseBurnOp}, butbuildOneTimeUseBurnOpisn’t in scope in this module (no import and no local declaration), so reference generation/linking is likely to produce an unresolved/broken link. Prefer plain inline code formatting here (or import the symbol if that’s safe for the declaration closure).
* Pins a one-time-use id on the session (RHI-5798): the session settles at most
* once per chain. Requires `policyAddresses.oneTimeUseId`. Each settlement must
* carry the matching burn op ({@link buildOneTimeUseBurnOp}) in its
* `preClaimExecutions`; a Permit2-route session must also supply a
…inding Executor-only one-time-use now asserts the 1271 list collapses to exactly [once] (the permissive sudo entry is replaced, no claim policy) while the burn still lands on every action. Also pins the documented invariant that the pinned id does not flow into the permissionId (it's authorized via the enable-signed config), so same-owner one-time-use sessions must not be installed concurrently. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XmP5wzhhzCja1useM2u6oZ
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.
Suppressed comments (2)
Previously missed (2) — in code that hasn't changed since the last review.
src/modules/validators/smart-sessions/one-time-use.ts:55
oneTimeUseIdErc1271Policyis used for both ERC-1271 policy entries and action policy entries (seeresolveSessionDataappendingoncetoaction.actionPolicies). TheErc1271-specific name is misleading for consumers and makes it easy to misuse/overlook in the executor route wiring. Consider renaming to a surface-agnostic helper name (e.g.oneTimeUseIdPolicyEntry/oneTimeUseIdPolicyData) and updating re-exports + changeset accordingly.
// The erc1271 policy entry to add to a session's erc7739Policies.erc1271Policies.
export function oneTimeUseIdErc1271Policy(params: {
policy: Address
id: bigint
}): { readonly policy: Address; readonly initData: Hex } {
src/modules/validators/smart-sessions/resolve.one-time-use.test.ts:12
- The comment claims
fast-checkis "declared in package.json but not installed in every working copy", butfast-checkis a devDependency (and is used by several test files). This wording can confuse contributors since a standard dev install will include it. Consider rephrasing to focus on why this file is separated (keeping tests example-based / lightweight) without implying the dependency is generally unavailable.
// Kept out of resolve.test.ts because that file imports fast-check (declared in
// package.json but not installed in every working copy); these are plain
// example-based tests for the OneTimeUseId wiring (RHI-5798).
… the executor route An already-enabled one-time-use session with no explicit permissions dropped to signatureMode 1 (plain ERC-1271), where the emissary never runs checkAction. Replay of a burned id stays blocked (the 1271 advisory read `!$used` reverts the executor's require), but the policy's action-surface guard — a `consume` may only name the session's own id, the one thing the executor route enforces on-chain — was inert. Carry a `oneTimeUse` marker on the resolved Session and OR it into the verifyExecutions derivation so a one-time-use session always validates in mode 5 (checkAction active), matching the contract's design (OneTimeUseIdPolicy checkAction + the install-on-every- action requirement). Confirmed safe for the permit2 route: mode 5 is a superset of mode 1 and verifyExecution is onlyIntentExecutor, so the arbiter path is unaffected. Also corrects the resolve.ts comment that mis-described the runtime enforcement path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XmP5wzhhzCja1useM2u6oZ
What
SDK support for one-time-use sessions (RHI-5798, smart-sessions-v2#56). A session pins an id via
oneTimeUse: { id }(requirespolicyAddresses.oneTimeUseId) and settles at most once per chain, across both settlement routes. Pairs with the orchestrator witness-stamping (orchestrator#1959).Wiring — the policy is installed where each route actually enforces it
Matching the on-chain policy's install requirements and the E2E harness (
OneTimeUseIdMatrixE2E):checkAction), not the 1271 list — so the once-policy is added to every action of the session. (An action without it falls through to the permissive sudo fallback → unbounded settlement.)checkERC1271) — so the Permit2 claim policy (the digest-binding partner) is co-located with the once-policy on that list (an AND: claim policy binds what settles, once-policy binds how many times).claimPoliciesmoves onto that surface.Each settlement carries the burn op (
buildOneTimeUseBurnOp) inpreClaimExecutions:consume(id)(executor) orconsumeFor(id, 0)(permit2 — the orchestrator stamps the real order nonce as the witness before signing).Changes
one-time-use.ts— ABI +encodeOneTimeUseIdInitData+oneTimeUseIdErc1271Policy+buildOneTimeUseBurnOp.resolveSessionData— once-policy on every action + Permit2-claim/once on the 1271 list for one-time-use sessions.oneTimeUse/oneTimeUseIdexposed on the publicSessionDefinition/SessionPolicyAddresses; helpers exported from@rhinestone/sdk/smart-sessions.claimPoliciesis emptied, and that a normal session is untouched.Review
A 3-reviewer local gate caught (and this PR fixes) a CRITICAL — the first cut only appended the policy to the 1271 list, leaving the executor route unbounded — and a HIGH — the permit2 binding partner was on the wrong surface. Verified against the deployed policy + the E2E harness. Encoding (id→bytes32), the
consumeFor(id,0)placeholder's fail-closed behavior, and permissionId-invariance were all confirmed sound.Closes RHI-5798 (SDK portion)
🤖 Generated with Claude Code
https://claude.ai/code/session_01XmP5wzhhzCja1useM2u6oZ