Present from the Past
Imagine a simple scenario: as a developer, you want to build a “limited-time red envelope” application on CKB. The rules are straightforward — recipients must claim the envelope within 24 hours; otherwise, only the sender can reclaim it. You think this is an easy problem until you realize that CKB contracts can only “see” what’s inside the current transaction, reading timestamps via attached block header proofs.
A contract cannot verify when the block header proof in a transaction was generated. This means an attacker could construct a transaction today while referencing an old block header from 2019. From the script’s perspective, the evidence shows the current time is 2019, meaning there are still seven years before the envelope expires, thus allowing the claim. This possibility of a “Time-Travel Attack” puts any contract with “deadline” logic at risk of being bypassed on CKB.
In CKB’s design, it is easy for developers to prove that a certain block or Cell exists, but conversely difficult to prove its non-existence. A simple solution for obtaining global information that is hard to get on-chain is to have an off-chain observer continuously provide a trusted data source—the well-known Oracle solution.
However, Oracle solutions introduce additional issues such as single points of failure or trust concerns, greatly increasing complexity. For the specific need of obtaining the latest on-chain time, we can satisfy it in a more decentralized way, which I call the CTO (CKB Time Oracle) solution. CTO maintains a group of Cells on-chain, enforcing a rotation mechanism and block header proofs via contracts to provide a trusted, concurrently available time source for other contracts.
GitHub Repository: Hanssen0/ckb-cto
System Design
The core idea of the CTO solution is to assign a globally unique ID to on-chain Cells. Off-chain data providers continuously update these Cells with the latest block header time, allowing other contracts to query Cells with this ID to obtain the most recent on-chain time.
Multi-Cell Rotation
If there were only one available Cell on-chain, it would frequently be occupied by the data provider’s update transactions, causing user transactions to fail often because the referenced Cell is constantly being updated to its next state.
Therefore, the CTO solution introduces an Oracle Group consisting of N Cells to record time. In every update, the CTO contract requires that only the oldest Cell in the Oracle Group can be updated, ensuring that updates proceed sequentially. If a user references the newest Cell in the Oracle Group, they can rest assured that the Cell will not be occupied within an interval of N - 1 blocks, leaving redundant time for other operations.
To understand this diagram, we first need to understand the Cell update process on CKB. Cells on CKB are immutable, meaning developers cannot simply modify data within a Cell. Instead, they must destroy an old Cell and create a new one, using the same ID to mark the relationship between the two. This ID is the Type ID solution on CKB. For a more detailed explanation of Type ID, you can refer to the CKB RFC description or my previous article.
Returning to the diagram, it uses an example of a 5-Cell rotation Oracle Group to show how CTO works. The horizontal axis represents the current latest block time, while the vertical axis represents the Cell number (Out Point). In the first update (at time 4-5), the oldest Cell #0 is destroyed, and a new Cell #5 is created to store the latest block time, and so on. Red Cells are always about to be destroyed to create the next green Cell.
Thus, if I reference Cell #5 at time 5, it remains available until time 10. Subtracting the time for block 5, my transaction has a window of four blocks to be successfully packaged.
Security and Concurrency Design
Generally speaking, any Cell in the CTO solution can be unlocked by anyone, meaning anyone can update the data within. To prevent attackers from maliciously locking a Cell to stop time updates, CTO requires that the Lock Script cannot change when a Cell is updated.
Under this design, oracle users do not need to worry about the data provider stopping updates—it is permissionless! Even if the original data provider leaves, application developers can take over the update task to keep the oracle running. Even if the developers leave, users themselves can continuously (or as needed) update the oracle to keep the application functional. Considering I have provided a JS SDK, a CLI tool, and a web dashboard, this shouldn’t be too difficult. This design eliminates the application’s dependence on a single centralized service provider.
So, what happens if multiple data providers try to update the oracle and cause a conflict? As long as all providers follow the rules and use the latest block time, this shouldn’t cause major issues—if anyone succeeds, the oracle works. Problems only arise when a “bad actor” tries to slow down updates by only moving time forward by a tiny step (one block) each time. In such cases, providers wanting to update to the latest time might be blocked by this actor.
However, in my vision, everyone sharing a single, well-maintained Oracle Group should be enough to meet most needs (why would anyone want to create their own Oracle Group? Although nothing stops them). So, a bad actor updating every block would only keep the oracle in its most up-to-date state. Furthermore, if the bad actor misses even one update, the other providers can instantly restore the Oracle Group to normal operation.
Finally, it’s worth noting that while more Cells in an Oracle Group provide users with more redundant time to use the latest Cell, it also means attackers have more freedom to pick older Cells to deceive contracts using the Oracle Group. An infinitely large Oracle Group is essentially no different from allowing any block header to be used (which obviously won’t work; CTO is capped at 255 Cells per group). Considering CKB’s average block time is about 10 seconds, an N = 13 Oracle Group would leave users with about a two-minute signing window, which should be sufficient.
What if a project wants stricter time limits but doesn’t want to maintain a separate Oracle Group with fewer Cells? This can be easily achieved by requiring multiple Cells from the Oracle Group during use. For example, requiring five Cells from an N = 13 group is equivalent to requiring one Cell from an N = 9 group.
The Tools
As I mentioned earlier, to make the CTO solution ready for developers and users, I have provided a set of supporting tools.
For interested Oracle Group data providers, the CLI is the most convenient tool. It provides standard provider logic, automatically monitoring on-chain Oracle Group status and periodically updating Cell time to ensure the oracle is always “on time.” Additionally, developers can use the CLI to deploy new Oracle Groups or query the current status of an Oracle Group directly in the terminal.
The web dashboard provides all the features of the CLI—querying, deploying, and providing (though you can’t expect high stability for providing data via a browser). Beyond that, the dashboard allows for the discovery of all Oracle Groups on the CKB network and integrates CCC, allowing users to connect supported wallets to deploy groups or provide data.
Naturally, I have wrapped the core functionality of the CTO solution into an SDK for developers to integrate. In fact, both the CLI tool and the dashboard mentioned above are built upon this SDK.
Currently, I have deployed the CTO solution to the CKB Testnet. The related tools have been released, and the source code can be found in the GitHub repository. I welcome community members to review the code for any issues, propose suggestions for the CTO solution, or share what you think is an appropriate number of Cells. If this time oracle solution has sparked any unique ideas, I would love to hear them.