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
- Discord: patb4967
- Email:
[email protected] - GitHub: psawyerberlin
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.