Tranfr — Programmable Recovery for CKB

I looked at the irreversible-expiry boundary more closely and would suggest the following CKB-native construction for the critical owner-freeze path.

Core correction

For an owner operation that is valid only before a stored deadline, I would not use a caller-selected header_dep as proof of current chain time.

Use a two-phase transition instead, so the time anchor is the actual commitment block of an intermediate Cell:

ACTIVE
  -> owner-signed BEGIN_OWNER_OP
       -> PENDING_OWNER_OP
            -> NORMALIZE -> ACTIVE
            -> RECOVER   -> CLAIMED

The first transaction does not apply the heartbeat or recipient update directly. It only commits to the requested operation.

PendingOwnerOp {
    version

    previous_state_hash
    previous_deadline_epoch
    inactivity_period_epochs

    owner_lock_hash
    previous_recovery_lock_hash

    operation_kind              // HEARTBEAT | RECIPIENT_UPDATE
    operation_payload_hash
    proposed_recovery_lock_hash

    state_nonce
}

BEGIN_OWNER_OP must enforce:

owner signature is valid

exactly one successor PENDING_OWNER_OP Cell exists

protected capacity / typed value is conserved exactly

previous policy is copied exactly

previous deadline is copied exactly

requested operation is committed by hash

no protected value is released

no recipient change is applied directly

no deadline extension is applied directly

Commitment-bound time decision

When PENDING_OWNER_OP is later consumed, the script loads the header associated with that exact input Cell:

ckb_load_header(..., input_index, CKB_SOURCE_INPUT)

The corresponding block header must be present in header_deps, but it is not being trusted as an arbitrary historical header selected by the transaction builder. With CKB_SOURCE_INPUT, the script resolves the block in which that input Cell was created.

Use one consensus metric end-to-end. For v1, an absolute epoch-number-with-fraction policy keeps the comparison deterministic:

begin_epoch  = epoch of the block that committed PENDING_OWNER_OP
old_deadline = previous_deadline_epoch

timely = epoch_less(begin_epoch, old_deadline)

The epoch comparison must use CKB epoch-fraction semantics, not a raw integer comparison of the encoded field.

Effective state

The semantic result is derived only from the commitment-bound begin_epoch:

if timely and operation_kind == HEARTBEAT:

    effective_recovery = previous_recovery

    effective_deadline =
        epoch_add(begin_epoch, inactivity_period_epochs)


if timely and operation_kind == RECIPIENT_UPDATE:

    effective_recovery = proposed_recovery

    effective_deadline =
        epoch_add(begin_epoch, inactivity_period_epochs)


if not timely:

    effective_recovery = previous_recovery

    effective_deadline = old_deadline

    owner_operation_effect = NONE

A late owner transaction can therefore create no new recovery authority and no new deadline.

NORMALIZE is only a canonicalization step. It must not accept a different policy, value set, deadline, or operation payload. For a late pending operation, normalization into a modified owner state is rejected; recovery proceeds under the previous policy.

The pending Cell itself must remain recoverable, so failure to normalize cannot trap the protected value.

Recovery path

Recovery uses the derived effective policy:

recovery signer = effective_recovery

input since =
    absolute epoch threshold
    exactly equal to effective_deadline

The lock verifies that the encoded since threshold matches the derived deadline. Consensus then supplies the lower-bound rule:

target_epoch >= effective_deadline

This creates the required asymmetry:

owner-side update:
    must have entered the chain before old_deadline

recovery:
    cannot enter the chain before effective_deadline

Pre-signed heartbeat test

1. owner signs BEGIN_OWNER_OP before deadline D

2. owner keeps the transaction offline

3. chain passes D

4. owner broadcasts the old transaction

5. PENDING_OWNER_OP is committed after D

6. begin_epoch >= old_deadline

7. owner_operation_effect = NONE

8. previous recovery recipient remains authoritative

9. previous deadline remains authoritative

The signature creation time is irrelevant.

Adding an unrelated old block to header_deps cannot change the result, because the decision uses the header associated with the actual pending input Cell.

Minimum blocking tests

TEST 01
BEGIN committed before D
-> operation is accepted
-> new deadline derives from the actual pending-cell commitment epoch


TEST 02
BEGIN signed before D but committed after D
-> owner operation has no effect
-> old recovery policy remains authoritative


TEST 03
late RECIPIENT_UPDATE
-> proposed recipient never becomes effective


TEST 04
arbitrary stale header added to header_deps
-> cannot substitute for the header associated with the pending input Cell


TEST 05
pending transition changes protected value, previous policy, or previous deadline
-> transaction fails


TEST 06
timely pending Cell is never normalized
-> recovery still becomes possible at the derived effective deadline


TEST 07
same operation is replayed against a later Tranfr state
-> previous_state_hash / state_nonce binding fails


TEST 08
reorganization and reinclusion
-> time anchor follows the canonical commitment block of the live pending Cell


TEST 09
epoch-fraction boundary
-> SDK and lock script derive identical ordering and threshold values

The core invariant is:

Owner authority is determined by when the owner state transition entered the canonical chain, not by when it was signed and not by an arbitrary header chosen by the owner.

One remaining scope boundary

If ordinary owner spends are also meant to become impossible after expiry, they need the same commitment-bound gate.

Otherwise the security claim should be narrower and explicitly state that irreversible expiry freezes heartbeat and recipient-update authority, while ordinary spending follows a different rule.

A single-step owner spend cannot obtain an upper-validity bound from since, because since is a lower-bound precondition.

References

CKB RFC 0022 — Transaction Structure / Header Deps

CKB RFC 0017 — Transaction Since Precondition

Nervos discussion — two-step pattern for an operation valid only before T

3 Likes