Spark Program | CKB Revocable Timelock (CRT) — Timelock Encryption You Can Cancel

Spark Program | CKB Revocable Timelock (CRT)

Timelock encryption you can cancel.

Requested: ~$1,400 USD, payable in CKB (≈1,200,000 CKB at the rate on 2026-09-20)
Timeline: 3 months
Category: developer tool — on-chain script, node software, TypeScript client

Repo: psawyerberlin/ckb-revocable-timelock

Contact

Who I am

I’m Patrick Sawyer, a solo developer based in Berlin. My day job is automation and telemetry work for rail operators. On CKB I’ve built VeriCell, a proof-of-existence service that anchors file hashes in live cells, along with other chain work at psawyer.de/ChainSolution. I run my own CKB mainnet and testnet nodes.

I’m currently building VoteSecure, an on-chain voting app that uses drand for timelock encryption. That’s where I ran into the problem below.

The problem

drand lets you encrypt to a future date. It won’t let you take it back.

Once the ciphertext exists, it opens at that round and the sender can do nothing about it. That isn’t a missing feature. drand’s beacon has no per-message state, so there’s nothing message-specific to withdraw, and stopping one release would mean stopping the beacon for everybody.

Where that bites

  • VoteSecure stores encrypted tallies and counting results on-chain under an additively homomorphic scheme. At the end of the voting period the cells are consumed, and from that moment the data has to be unreadable forever. drand can’t give me that: the ciphertext still opens at its scheduled round no matter what happened to the cells.
  • A journalist deposits documents set to open in 90 days as insurance. The dispute settles in week three. They open anyway.
  • A dead man’s switch fires on inactivity. The owner comes back and wants the message destroyed. It can’t be.
  • A sealed-bid auction is cancelled before the reveal. Every bid still goes public on schedule.

People want two things and can currently only have one: release on a condition, and the ability to kill the condition so release becomes impossible.

What CRT does

You get a public key today. Nobody can decrypt until the condition holds on CKB. Consume one cell and the data is unrecoverable by anyone, forever.

The switch is a live cell with a type-id T, locked by the user’s own wallet, holding a deadline in its data. The condition is that this cell exists and its deadline has passed. Each node generates a fresh random secp256k1 keypair for that one switch and keeps the private half. The secret is split 3-of-5 with Shamir and the shares are encrypted to those public keys by the application, then stored alongside the ciphertext — nodes never hold a share, and no single node holds enough to decrypt anything. When deadline + grace ≤ median_time at depth, each node publishes its private key independently; once three are out, anyone can unwrap three shares and reassemble the secret. If instead the user consumes the cell, type-id uniqueness means T can never exist again, so the condition can never be satisfied. Nodes delete their keys too, but the guarantee doesn’t rest on them doing so: it’s visible in the live cell set.

That’s why this is on CKB and not somewhere else. Cancellation needs per-message state that is publicly visible, owned by the user, and impossible to recreate once destroyed. type-id gives all three. On an account-model chain the same thing is a contract flag someone can upgrade around.

Some design choices, argued out in full in SPEC.md:

  • Release is disclosure of the private key, not a signature. Signing schemes invite an attack where a node is tricked into emitting the release value early through domain confusion. Disclosure has no such surface, and it keeps everything on secp256k1.
  • Keys are CSPRNG-random per switch, never derived from a node master secret. Derived keys are recomputable, which makes deletion theatre.
  • No DKG. A group share can’t be deleted, and one successful collusion against it would silently open every ciphertext the network ever produced.
  • The client refuses to proceed if a node’s per-switch pubkey doesn’t verify against the on-chain registry. No skip flag.

Trust model

For this grant, all five nodes are mine. The deliverable proves the protocol works end to end on testnet. It proves nothing about decentralisation and I’m not claiming it does.

The design is permissionless — nodes self-register via an on-chain registry cell, clients discover them by scanning the chain, and fees are escrowed per switch in a Job Cell — but registry, stake and slashing are out of scope here and unfunded. The node does ship as a documented Docker image in week 8, so a reviewer can run their own against the testnet deployment rather than taking my word that the software is runnable by anyone.

What the protocol guarantees. Breaking confidentiality takes 3 of 5 nodes dishonest from the start and colluding. Keys are random per switch and never derived, so a node that is honest today and compromised tomorrow has nothing to give up. There is no DKG and no group key, so a coalition must re-form against each switch’s specific node set.

What it does not guarantee. Nothing forces a node to delete a key, and no node can prove it did. Early release is objectively provable against consensus chain time and is slashable in the full design; retention produces no artefact and is not detectable by anyone, ever. For data that warrants it, the answer is running your own node in a required position of the access tree, which is specified and out of scope here.

Not the same as Tranfr

Tranfr (approved here on 2 September) moves on-chain assets to a recovery recipient after owner inactivity. CRT never touches assets and transfers nothing. It releases decryption keys for data held off-chain, and it adds revocation, which Tranfr doesn’t need and drand can’t have. The two compose fine: a Tranfr-style inactivity policy is a reasonable condition to point a CRT switch at. Same for InheritVault in the CKBuilder repo, which is a fixed-date vault of CKB capacity.

VoteSecure would be CRT’s first consumer on testnet. It stays on drand until the operator set is genuinely independent of me.

What the 3 months produce

Week Work
1 Freeze the v0.1 scope. Identity derivation and wire formats, with published test vectors.
2 Write the adversarial cases first, as failing tests: reorg at the deadline boundary, early release, cancel/release race.
3–4 Alive Cell type script in Rust. Create, renew, cancel. Full test suite.
5 Deployed to Pudge: code_hash and deployment tx hash published.
6–7 Node: per-switch keygen, key store, chain follower, predicate engine at 30 confirmations.
8 Node: release and delete paths, HTTP endpoint, backup/restore CLI, published Docker image and operator docs. One node passes the full lifecycle, and a third party can run one.
9 Client: 3-of-5 Shamir, descriptor assembly and verification, create/renew/cancel tx builders via CCC.
10 Recovery kit generator. One self-contained HTML file, zero dependencies. Opens from a USB stick with no network and recovers the key.
11 Five nodes on five hosts. Demo app on testnet with a 15-minute deadline mode so expiry is watchable, and a field for adding your own node by URL. Live URL.
12 Adversarial suite green, docs, short screencast, release, completion report.

Three months rather than eight weeks because I’m doing this alongside a full-time job, and I’d rather give the committee a date I’ll actually hit.

Budget

Area Amount
Alive Cell type script (Rust/ckb-std), adversarial tests, testnet deployment $600
Node: keygen, chain follower, predicate engine, publication, deletion, backup CLI $300
Client: Shamir, descriptor verification, CCC tx builders, offline recovery $200
Demo app, recovery kit generator, five-host deployment and hosting, docs, screencast $300

Over $1,000 because this is three pieces that have to interoperate before any of them demos: an on-chain script, a daemon that has to run in several independent instances, and a client plus UI. There’s no partial version. Week 11 isn’t reachable by delivering two of the three well.

How to check I did it

Twenty minutes, browser and a testnet wallet, nothing to install and no code to read.

Release path. Open the demo, connect, fund from the faucet. Write anything, pick the 15-minute deadline, sign one transaction. Paste the tx hash into CKB Explorer: you should see a live cell with a type-id script and the deadline in its data. Download the recovery kit, turn off your network, open the file, and it should render the descriptor and recovery instructions offline. Wait out the deadline. Three of the five nodes are enough to recover; the status page shows all five publishing. Click Recover: your message comes back.

Cancellation path, which is the claim that matters. Create a second switch, then click Cancel and sign. Explorer shows the cell consumed with no successor carrying that type-id. Wait past the deadline: nodes publish nothing, Recover reports the condition can never be satisfied, and the offline kit fails with a stated reason. You can confirm this without trusting my app at all: T is absent from the live cell set, and type-id semantics mean nothing can bring it back.

Tests. cargo test and npm test print a summary naming release_before_deadline_rejected, post_cancel_release_impossible and reorg_at_deadline_boundary.

What exists now vs what the grant pays for

The repo is spec-first and has no implementation code. It holds the v0.3 concept document, SPEC.md, and contract READMEs for the node and coordinator. The design is done and public, so anyone can read it today and tell me it’s wrong.

The grant pays for every line of code. Nothing in there currently compiles.

What this doesn’t do

  • Decentralisation and deletion: see Trust model above. Short version — the five nodes are mine, and deletion is unverifiable.
  • 3-of-5 is a weak threshold. At real scale the answer is running your own node and marking it mandatory; that feature is out of scope here.
  • secp256k1, no post-quantum claim. Fine for terms under a year, which is the expected case.

Out of scope for this grant and deliberately not promised: fee escrow, the Job Cell, node staking, registry and slashing, reputation scoring, mandatory-node access trees, claim batching, proof-of-possession, mainnet, and any audit.

MIT licensed.

The three months above are mine to deliver, but the point of building it in the open is that a node network shouldn’t be one person’s. Once the primitive works on testnet I’d like other operators and contributors involved, and I’ll take issues and PRs from week one.

I will be pleased to post regular text updates in this thread once started.

3 Likes

Hi @psawyerberlin,

Thank you for your continued interest in the Spark Program and for submitting the CKB Revocable Timelock (CRT) proposal.

After review by the Spark Program committee, your proposal has been assigned a status of Pending. This is not a rejection, but an invitation to revise.

The committee recognizes the genuine pain point you’ve identified — the inability to revoke or cancel a timelock encryption once scheduled is a real limitation in drand-based systems, and your use cases (VoteSecure, sealed-bid auctions, dead man’s switches) are well-articulated. The concept of leveraging CKB’s cell consumption and type-id uniqueness as a “kill switch” is creative and aligns well with CKB’s state model.

However, the committee has several concerns that need to be addressed before this can proceed:

Main Revision Recommendations:

  1. Decentralization & Node Participation:
    Please disclose the decentralization level of the CRT node network. Who can run a node? Is it permissionless — can anyone participate in key-holding and publishing? If nodes are restricted to a set of operators you select or deploy yourself, the security model changes significantly and you need to clarify this explicitly. Please detail the node discovery mechanism, incentive structure (if any), and how you prevent a malicious node from refusing to publish or colluding with other nodes.

  2. The “No Delete” Problem — Accountability Mechanism:
    Your design states that nodes hold a fresh per-switch private key and should delete it if the owner cancels. But nothing on CKB forces a node to actually delete its copy. A node could keep the private key and decrypt the data later regardless of the on-chain condition, or after the cell is consumed. The committee would like you to explicitly address: if a node fails to delete the key as agreed (or maliciously retains it), what recourse does the user have? Is there any on-chain slashing, penalty, or cryptographic guarantee (e.g., threshold schemes where no single node can decrypt alone)? If the guarantee is purely “nodes are honest and will delete,” please state that honestly and discuss the trust assumptions.

Please revise and resubmit based on the feedback above. We will arrange a new round of review as soon as possible.

Best,
xingtian
On behalf of the Spark Program Committee

cc: @zz_tovarishch , @yixiu.ckbfans.bit , @Hanssen

1 Like

Thanks xingtian, and thanks to the committee for reading it carefully. Both points are fair and I’d rather answer them bluntly than well.

I’ve edited the original post accordingly. Re-reading it, one of my answers to point 2 was buried in a bullet at the bottom and another was missing from the mechanism description entirely. That’s my fault, not the committee’s. I’ve also made the terminology consistent: a switch is the object a user creates, renews and cancels, and a condition is the predicate over chain state that releases it. The spec used both words for both things, which didn’t help.

1. Decentralization and node participation

In this grant, the node set is not decentralised at all. All five nodes are mine. I deploy them, I control the hosts, and I could collude with myself trivially. The correct way to read the deliverable is: this proves the protocol works end to end on testnet. It proves nothing about decentralisation, and I’m not asking the committee to believe otherwise.

The design is permissionless, and the parts that make it so are specified but deliberately unfunded here:

  • Discovery. A node self-registers by creating a registry cell on CKB holding its identity public key, endpoint and stake. Clients discover nodes by scanning the chain for registry cells. There’s no allowlist and no gatekeeper: the coordinator indexes registry cells but cannot create, block or alter one, and it never signs a transaction.
  • Incentive. Each switch escrows fees in a Job Cell. Nodes claim against it on-chain after serving the switch. Operation is meant to be self-sustaining rather than donated, which is the difference I’m aiming at versus a fixed federation.
  • Refusal to publish. The threshold is 3-of-5, so two nodes can refuse, fail or vanish and release still works. A release value is also public once disclosed, so any party can relay it: a node that publishes cannot un-publish, and the others can’t suppress it. In the full design a node that misses a release loses its fee, its reputation score and, via slashing, part of its stake.
  • Collusion. There is no DKG and no group key. Every node’s keypair is independent and generated fresh per switch, so a coalition has to re-form against each switch’s specific node set, and different switches draw different sets. There’s no single ceremony that, once broken, opens everything the network has ever produced. That was the main reason I rejected the DKG design in §3.1 of the spec.
  • The real answer for high-value data is the mandatory-node path: the user runs their own node and places it in a required branch of the access tree, so no coalition of every other operator is sufficient. Specified, and out of scope for this grant.

I’ve added two things to the scope at the same budget, because “permissionless by design” is worth less than something a reviewer can test. The node now ships as a documented Docker image in week 8, and the week 11 demo takes a user-supplied node URL. So you can run your own node and include it in a switch rather than taking my word that the software is runnable by anyone. That’s about three days of work.

2. Deletion, and what actually protects the user

The committee is right and I want to concede it without qualification: nothing forces a node to delete anything, no node can prove it deleted anything, and no cryptographic construction I know of would change that. I don’t claim otherwise, and the proposal shouldn’t have made that point in passing.

One correction to the premise, though, because it changes the severity.

A single retained key is not enough to decrypt anything. Nodes don’t hold fragments of the secret at all. Each node holds one private key; the secret is Shamir-split 3-of-5 and the shares are encrypted to those public keys by the application and stored with the ciphertext, never by CRT. A node that keeps its key can unwrap one share, and one share reveals nothing. It takes three of the five to have been dishonest from the start and to find each other afterwards. My original post didn’t make this clear in the mechanism section, which I’ve now fixed.

The trust assumptions, stated plainly:

Attack What it takes
Read data before the deadline 3 of 5 nodes dishonest from creation, colluding
Read data after the owner cancelled the same 3 of 5, all having retained keys they said they deleted
Prevent release at the deadline 3 of 5 nodes refusing or offline
Release early and get away with it not possible undetected, see below

What deletion actually buys, since it can’t be enforced: it protects against a node that is honest today and compromised, seized or subpoenaed tomorrow. Keys are CSPRNG-random per switch and never derived from a node master secret, so an honest node that deletes genuinely cannot reproduce the key later, even under compulsion. It does nothing against a node that was dishonest from the start. The threshold covers that case, and the mandatory-node path covers it properly.

On slashing, I want to be precise rather than reassuring. Early release is objectively provable: the release condition is consensus chain time (median_time at depth), so a release value published before the deadline is evidence anyone can verify and submit, and that is slashable in the full design. Retention is not provable and never will be. A node that quietly keeps a key emits no artefact. I could write a slashing rule for it, but it would be unenforceable, and an unenforceable rule in a spec implies a protection that doesn’t exist.

The honest summary, which goes in the README and the UI rather than only in a proposal: CRT’s cancellation guarantee is cryptographically hard against later compromise and economically hard against collusion. It is not hard against a threshold of operators who were malicious from day one. Run your own node in a required position if your data warrants it.

On what happens after testnet

Question 1 gets at something I should say directly, because it shapes how I’d like this project to go.

CRT’s security doesn’t come from my code. It comes from independent people running nodes who have no reason to collude with each other. The code only ensures that collusion is the sole remaining attack — everything else is a property of who the operators are. That’s why a solo three-month build is the right shape for a testnet proof and the wrong shape for anything beyond it: there’s nothing to decentralise until the primitive works, and once it works, one person shouldn’t own it.

For mainnet I’d want several independent operators, ideally including CKB users with no connection to me, and more than one developer on the codebase. A quiet bug here means someone’s data opens early or never opens at all, and that deserves more eyes than mine. I’m not asking the committee to solve that, and I’d rather not pretend a grant creates a community. But a dedicated channel and people watching the build is how a second operator and a second reviewer realistically turn up, and that’s the part of Spark I’m most interested in.

Happy to answer follow-ups here. If it’s useful I can write the threat model up as a standalone THREAT-MODEL.md in the repo before the grant starts, rather than as a deliverable inside it.

3 Likes

Hi @psawyerberlin,

After review by the Spark Program Committee, your proposal is currently still rated as Pending.

The committee recognizes that building a revocable time-lock encryption mechanism based on the CKB cell model is a viable delivery concept with real-world use cases (sealed-bid auctions, dead-man switches, and journalist file storage). We encourage you to continue development in this direction.

However, the committee believes that the node-based secret-sharing scheme has significant issues with its security model. Please carefully consider the following points:

  1. Unresolved collusion resistance issue: Even if the design goal is to prevent nodes from disclosing their private keys before the deadline, there is no mechanism to prevent all nodes from colluding and releasing their respective shares early. Currently, the protocol has no mechanism to prevent or detect such behavior. If all nodes agree to release them, the “time lock” loses its meaning.
  2. Data availability cannot be guaranteed: The solution assumes that at least 3 of the 5 nodes will honestly store their private-key shares until the deadline. But what if a node merely claims to have stored its share and then disappears? When the deadline arrives, if fewer than 3 shares can be recovered, the data will be permanently unrecoverable—which is worse than a simple time lock.
  3. The solution increases vulnerability rather than reducing it: Splitting a user-managed key into multiple shares does not improve the trust model; instead, it introduces more points of failure and more uncertainty. When using a single key, the user has complete control, and the risk of exposure is, of course, borne by the user personally. When using multiple nodes, the user must trust that the other nodes will be both honest and available at the exact moment of decryption. Otherwise, if a node operated by the user is compromised and the other nodes also agree to release their shares, the splitting scheme becomes meaningless and even less controllable. This is a stricter yet weaker form of protection.

The committee hope you can think more deeply about the security model and improve it accordingly. Please revise it based on the feedback above and resubmit it. We will arrange another review as soon as possible.

Sincerely,
xingtian
On behalf of the Spark Program Committee

cc : @zz_tovarishch , @Hanssen , @yixiu.ckbfans.bit

2 Likes

Reply to Spark Committee — second Pending

Thank you xingtian, and thanks to the committee for a genuinely careful second read. I want to answer the three points properly, and then make a proposal about what happens to this application.

Before the details, one thing I should have caught myself: two of the three concerns are answered by mechanisms that exist in the design and that I placed out of scope in the proposal. Proof-of-possession answers point 2. Mandatory-node access trees answer point 1. Both are in SPEC.md and both are in my “deliberately not promised” list. So the committee reviewed a version of CRT with its two load-bearing safeguards removed and correctly found it thin. That’s a scoping mistake on my part, not a disagreement.

1. Collusion

Correct, and I conceded it in my last reply: nothing prevents a threshold of operators who were malicious from the start from sharing keys privately. I’d add three things.

Public early release is detectable, contrary to the note. The release condition is consensus chain time. A release value published before deadline + grace is evidence anyone can verify against the chain and submit, and it is slashable in the full design. What is undetectable is private collusion — operators exchanging keys without publishing. I want to keep those two separate rather than claim detection I can’t deliver.

This is true of every threshold timelock, drand included. drand’s federation could collude and produce a future round’s signature early; nothing structurally prevents it. I’m not offering that as an excuse, only as a calibration: this isn’t a defect CRT introduces, it’s the standing cost of not having the user hold the key.

The mandatory-node path removes it entirely for the operators the user doesn’t control. The access tree composes an XOR branch with the threshold branch. The user places their own node in the XOR branch, and the secret then requires that node’s key plus a threshold of the rest. Every other operator can collude, and they reconstruct nothing. To be precise about what this does and does not give you: it does not make rogue nodes delete anything. It makes their retention worthless, because their shares don’t reconstruct without the branch the user controls. That is the answer for data that warrants it, and it should have been in scope.

2. Data availability

This is the strongest of the three and I don’t want to wave it away with “enough nodes and good statistics.”

The design’s answer is proof of possession. At intervals, a node must sign a chain-derived challenge — blake2b256("CRT/v1/pop" || T || block_hash(H)) — with the per-switch key, and its fee claim is gated on that signature. Because the challenge derives from a block hash, it can’t be precomputed. A node that quietly dropped its key fails its next PoP, and it fails it while the stake is still bonded and the user can still rewrap, rather than at the deadline when it’s too late. Combined with the headroom figures the coordinator publishes, “claimed to store it and vanished” is a condition that surfaces months early, not at release.

Where the committee is right: PoP is in my out-of-scope list, so as proposed there is no availability guarantee at all. That was wrong. PoP is the mechanism that makes a threshold scheme honest, and it belongs in v0.1.

On “worse than a simple time lock” — I’d push back gently. A simple timelock has an availability assumption too; it’s just hidden in a federation you don’t choose and can’t audit. CRT’s version is explicit, measurable, and the user can buy headroom by widening the set. That’s a real difference, but only if PoP ships.

3. Whether splitting helps at all

Here I think there’s a mismatch in what’s being compared, and I’d like to set it out rather than concede it.

The note compares CRT against a single key under the user’s control. That isn’t the alternative — it’s a different product. If a key exists that the user can use to decrypt, then:

  • a dead man’s switch cannot work, because the user is by definition gone at the moment of release;
  • a sealed-bid auction is not sealed, because the auctioneer can read bids before the reveal;
  • VoteSecure’s tallies are readable by the operator mid-vote, which is exactly the property the scheme exists to prevent.

A timelock’s defining property is that nobody, the creator included, can decrypt before the condition holds. The moment one party can, it’s encryption with a promise, not a timelock. So the honest comparison isn’t CRT versus a user-held key, it’s CRT versus drand: a fixed federation the user cannot choose, cannot extend, cannot add their own node to, and cannot cancel.

On the specific scenario raised — the user’s own node compromised while others release their shares — the mandatory branch makes the user’s node necessary, not sufficient. Compromising it yields nothing on its own; an attacker still needs a threshold of the rest. And the standing trade is one I state plainly in the spec: every mandatory node makes the secret harder to steal and easier to lose, one for one. Users choose where on that line they sit. A fixed federation doesn’t offer the choice.

What I’d like to do with this application

I’ve been working on something broader in parallel: a revenue layer for CKB nodes, where nodes opt into running code embedded in cells and collect application fees for it — a generic service runtime rather than one hardcoded use case. I’ve posted it here: https://talk.nervos.org/t/a-revenue-layer-for-ckb-nodes

CRT is the first service I’d deploy on it, but it isn’t the point of it. And it changes CRT’s economics substantially: with that layer in place, the node software, the operator set and the fee mechanism stop being things I have to build and fund, and CRT becomes a cell-resident service I can develop incrementally as a solo developer.

So rather than have the committee spend a third round on this, I’d like to withdraw the CRT application for now. Not because the concerns are unanswerable — I hope the above shows they’re addressable — but because the right version of CRT now looks like a service on a runtime that doesn’t exist yet, and asking for three months of funding to build the wrong layer would waste your time and mine.

I’ll resubmit if and when the runtime work justifies it, with PoP and mandatory-node trees in scope from the start rather than deferred.

Thank you for the time the committee has put into this. The second review was more useful than the first, and the pressure on the security model is what pushed me to look at the layer underneath it. I’d genuinely welcome the same scrutiny on the revenue-layer thread if any of you have the appetite.

Patrick

2 Likes