oor: Bind OOR checkpoint outputs to the session operator key - #699
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a mechanism to normalize checkpoint owner leaves to the session operator key rather than the spent input VTXO's historical operator key, preventing submission failures after operator key rotation. This logic is integrated into the StartTransferEvent processing, and comprehensive unit tests have been added. The feedback suggests a minor refactoring in the loop of normalizeCheckpointOwnerLeaves to use a pointer to the slice elements for better readability and idiomatic Go code.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
|
@claude review this |
|
Claude finished @ellemouton's task in 2m 19s —— View job Review: oor — Bind OOR checkpoint outputs to the session operator key
Overall this is a well-targeted, well-documented fix. The core insight — that the checkpoint output owner leaf is governed by the session policy while the input spend stays on the historical key — is correct, and the placement of the normalization (before I traced the fix through the downstream Correctness — the fix composes cleanly ✅The one thing worth confirming is that the later
Minor: unreachable
|
aa20c30 to
582d18c
Compare
|
Thanks for the review! Addressed in the amended commit:
Re: resume of pre-fix sessions (Q2) — no in-flight migration concern. A session created before this change was necessarily created when the input and current operator keys were equal (no rotation relevant to it had occurred yet), and its point-of-no-return co-sign happened under that key. Deterministic resume reconstructs byte-identically under that same key, so it completes consistently. This fix only changes how new sessions (created after the upgrade) bind the checkpoint-output owner leaf — it never re-derives an already-bound session's leaf. The end-to-end rotation itests on the server side ( |
582d18c to
fb55fd2
Compare
9cf0912 to
96f6cb4
Compare
fb55fd2 to
6ed2fde
Compare
96f6cb4 to
33e433c
Compare
6ed2fde to
9dc752f
Compare
33e433c to
5bdf260
Compare
4ae736b to
1a3fa02
Compare
The checkpoint OUTPUT owner collaborative leaf was built from the spent
input VTXO's operator key. But the checkpoint output -- and the Ark tx
that spends it cooperatively -- is governed by the session checkpoint
policy, whose operator key is the operator's current key at session
creation. A VTXO created under an older operator key (after the operator
rotated) therefore produced a checkpoint output committed to a stale
key: the server's submit-rebuild rejects it ("owner leaf policy does not
contain operator key") and the operator's Ark co-signature, made with
the session key, fails the leaf. Before any rotation the input and
session keys are equal, so this was invisible.
Rebind each standard input's checkpoint output owner leaf to the session
operator key in the StartTransfer transition, before the deterministic
build and before the inputs flow into signing and persistence. The input
SPEND path is untouched -- spending the VTXO still uses its own
tapscript, committed to the historical key, which the server resolves
and co-signs per input. Custom spends (e.g. vHTLC) carry their own owner
leaf and are skipped.
This is the client half of the operator-key-rotation OOR work; the
server already co-signs each checkpoint input with its historical key
and binds and persists the session checkpoint policy.
1a3fa02 to
144ea95
Compare
What
When building an OOR transfer, the client derived the checkpoint output's owner collaborative leaf from the spent input VTXO's operator key. But the checkpoint output — and the Ark tx that spends it cooperatively — is governed by the session checkpoint policy, whose operator key is the operator's current key at session-creation time.
After the operator rotates its key, a VTXO created under an older key (X) would produce a checkpoint output committed to X, while the server (correctly) rebuilds and co-signs that output under the current/session key (Y). The post-rotation submit was rejected at
AwaitingSubmitValidationStatewith"owner leaf policy does not contain operator key", before the server's per-input co-sign path even ran — so any OOR spend of a pre-rotation VTXO failed. Before a rotation X == Y, so this was invisible.Fix
Rebind each standard input's checkpoint-output owner leaf to the session operator key (
StartTransferEvent.Policy.OperatorKey) in theStartTransfertransition, before the deterministic build and before the inputs flow into signing/persistence (normalizeCheckpointOwnerLeaves).The input spend path is untouched — spending the VTXO still uses its own tapscript, committed to the historical key, which the server resolves and co-signs per input. Custom spends (e.g. vHTLC) carry their own owner leaf and are skipped.
This is the client half of the operator-key-rotation OOR work. The server half (per-input historical co-sign + session-bound checkpoint policy persistence + indexer surfacing) is in darepo (stacked on #525 work).
Testing
oor/checkpoint_owner_leaf_test.go): owner leaf rebinds to the session key (and not the input's historical key); custom spends untouched; nil policy key rejected.TestOORSendAfterOperatorKeyRotation(single rotated-key OOR send) andTestOORSendMixedHistoricalOperatorKeys(one transfer spending inputs under two different historical keys) both pass.Part of the operator-key-rotation epic (#525).
🤖 Generated with Claude Code