Repository navigation
[Language/stdlib] Add typed CKB temporal and Since domains #12
Description
Summary
Replace ambiguous raw-u64 time values at the authoring boundary with explicit CKB temporal domain types.
CellScript currently returns u64 for the HeaderDep epoch number, epoch start block number, epoch length, input since, and absolute/relative epoch-since constructors. The runtime checks are bounded and fail-closed, but the type system cannot prevent mixing values with different units or comparing an encoded since bitfield to a plain epoch number.
Argent's temporal-value work demonstrates the value of preserving temporal identity in artifacts even when the runtime representation remains integer-based. CellScript should define a CKB-specific, wire-compatible version rather than a generic temporal wrapper.
Problem examples
All of the following values are currently represented as u64:
HeaderDep epoch number
HeaderDep epoch start block number
HeaderDep epoch length
encoded input since
absolute epoch since
relative epoch since
application epoch deadline
application epoch duration
This makes invalid combinations type-correct:
let epoch = env::current_timepoint()
let encoded_since = ckb::input_since(source::input(0))
// Numerically valid u64 operations, semantically invalid domains:
require encoded_since >= epoch
let deadline = encoded_since + epoch
The current implementation also names an epoch-number read current_timepoint, which is easy to misread as a Unix timestamp.
Proposed type families
The exact surface requires an RFC, but the semantic domains should include:
| Type | Meaning | Candidate runtime representation |
|---|---|---|
EpochNumber |
whole CKB epoch number | checked u64 |
BlockNumber |
absolute block number | checked u64 |
Timestamp |
CKB timestamp metric | checked u64 |
EpochLength |
blocks in one epoch | nonzero checked u64 |
EpochFraction |
CKB epoch number/index/length tuple | canonical fixed fields or encoded u64 |
Since<Absolute, BlockNumber> |
absolute block-number constraint | canonical encoded u64 |
Since<Absolute, EpochFraction> |
absolute epoch-fraction constraint | canonical encoded u64 |
Since<Absolute, Timestamp> |
absolute timestamp constraint | canonical encoded u64 |
Since<Relative, BlockNumber> |
relative block-number constraint | canonical encoded u64 |
Since<Relative, EpochFraction> |
relative epoch-fraction constraint | canonical encoded u64 |
Since<Relative, Timestamp> |
relative timestamp constraint | canonical encoded u64 |
DecodedSince |
tagged union returned by decoding raw input since |
one of the six validated variants |
EpochDuration |
application duration in whole epochs | checked u64 |
The exact generic spelling is an RFC decision. The invariant is not: both the relative/absolute mode and the block/epoch/timestamp metric must remain in the type identity. A wrapper that distinguishes only AbsoluteSince from RelativeSince is insufficient.
flowchart TB
H["HeaderDep fields"] --> EN["EpochNumber"]
H --> BN["BlockNumber"]
H --> EL["EpochLength"]
EN --> EWF["EpochNumberWithFraction"]
EL --> EWF
EWF --> SAE["Since<Absolute, EpochFraction>"]
EWF --> SRE["Since<Relative, EpochFraction>"]
BN --> SAB["Since<Absolute, BlockNumber>"]
BN --> SRB["Since<Relative, BlockNumber>"]
TS["Timestamp"] --> SAT["Since<Absolute, Timestamp>"]
TS --> SRT["Since<Relative, Timestamp>"]
RAW["Raw u64"] -. "explicit checked conversion only" .-> EN
RAW -. "decode flags + metric + payload" .-> DS["DecodedSince"]
Required type rules
Allowed by default
- compare two values of the same temporal domain;
- add/subtract a matching duration with overflow/underflow checks;
- construct
EpochNumberWithFractionfrom validated number/index/length; - encode a validated epoch fraction into absolute or relative
Since; - decode an input
sinceinto a tagged validated representation; - serialize to the existing fixed-width ABI where wire compatibility allows.
Rejected without explicit conversion
EpochNumberversusBlockNumber;- epoch values versus encoded
Since; - absolute versus relative
Since; - block-number versus epoch-fraction versus timestamp
Since; - temporal values versus arbitrary
u64; - application durations versus absolute timepoints;
- malformed epoch fractions;
- arithmetic that discards metric or relative flags.
Explicit escape hatch
Low-level protocol work may need raw bits. Provide named operations such as:
Since::from_raw_checked(u64) -> DecodedSince
Since<Mode, Metric>::to_raw()
EpochNumber::from_u64_checked(u64)
EpochNumber::to_u64()
The unsafe-looking direction must be explicit in source and typed-semantics metadata. No implicit widening or assignment conversion should cross these domains.
API migration sketch
A candidate additive first step:
let epoch: EpochNumber = env::current_epoch_number()
let start: BlockNumber = ckb::header_epoch_start_block_number(header)
let length: EpochLength = ckb::header_epoch_length(header)
let maturity: Since<Relative, EpochFraction> =
ckb::since_epoch_relative(number, index, length)
let observed: DecodedSince = ckb::input_since(input)
require observed.satisfies(maturity)
The existing env::current_timepoint() -> u64 should remain only under the existing edition contract, emit a targeted migration warning, and migrate mechanically to the precisely named API. Do not silently change its return type within an edition.
Artifact and interface requirements
Temporal identity must survive every compiler layer:
flowchart LR
SRC["Typed source"] --> AST["Temporal AST type"]
AST --> IR["Distinct IR type / checked ops"]
IR --> TS["typed-semantics record"]
IR --> ABI["Fixed wire representation"]
TS --> CHECK["Standalone checker"]
ABI --> VM["CKB-VM execution"]
The public package interface must record:
- semantic temporal type;
- exact serialized representation;
- constructor/decoder version;
Sincemetric and relative/absolute policy;- target/profile identity;
- migration behavior.
A type alias to u64 is insufficient because it would erase the safety property before IR.
Implementation phases
Phase 0 — inventory and RFC
- inventory every stdlib/source/IR/metadata path that currently uses a temporal
u64; - define the type domains and legal operations;
- define ABI and interface compatibility;
- define edition migration and diagnostics;
- align with official CKB
Sinceand epoch-fraction semantics.
Phase 1 — additive typed APIs
- add typed constructors, readers, decoders, comparisons, and checked arithmetic;
- preserve old APIs with warnings;
- add typed-semantics and interface records;
- add formatter, LSP, docs, and Playground support.
Phase 2 — migration
- provide a mechanical migration command or code action;
- migrate bundled examples and package fixtures;
- keep old-edition dependencies interoperable;
- reject implicit cross-domain arithmetic in the new edition.
Phase 3 — stabilization
- mutation-test
Sinceflag/metric validation; - add exact CKB-VM vectors for valid and malformed encodings;
- document compatibility and wire identity;
- stabilize only after real timelock, DAO, vesting, and atomic-swap fixtures migrate.
Acceptance matrix
| Case | Expected |
|---|---|
compare two EpochNumber values |
pass |
add EpochDuration to EpochNumber |
pass with overflow guard |
compare EpochNumber and BlockNumber |
type error |
compare encoded Since and epoch |
type error |
construct fraction with index >= length |
stable error |
| construct fraction with zero/overflowing length | stable error |
decode unknown Since flags |
stable runtime error |
mix absolute and relative Since |
type error unless explicitly normalized by an accepted operation |
round-trip valid CKB Since vectors |
exact match |
compile old-edition current_timepoint source |
preserved behavior plus migration policy |
cross-package interface changes raw u64 to typed domain |
correctly classified as compatible or breaking according to the accepted edition/ABI policy |
Completion criteria
- An accepted RFC defines every temporal domain and operation.
- Official CKB
Sinceand epoch semantics are cited and pinned with vectors. - Temporal identity survives AST, type checking, IR, typed semantics, interfaces, ABI, lowering, and checker.
- The wire representation remains deterministic and versioned.
- New-edition code cannot implicitly mix temporal domains with
u64. - Existing-edition behavior is preserved with a mechanical migration path.
- Timelock, DAO, vesting, NFT expiry, governance, and atomic-swap examples use the typed API.
- Malformed flags, fractions, and arithmetic boundaries have CKB-VM negative tests.
- LSP, formatter, VS Code, Playground, docs, and generated builders agree.
-
dev,ci, andbackendgates pass.
Non-goals
- inventing wall-clock semantics that CKB does not expose;
- treating a HeaderDep epoch number as a Unix timestamp;
- implicit conversion from arbitrary integers;
- changing existing edition semantics in place;
- generic dimensional-analysis or units-of-measure support;
- hiding CKB
Sinceflags behind unchecked arithmetic.
Activity
a19q3 commentedon Aug 27, 2026
Research update: the gap is nominal typing, not an absent runtime implementation
Comment audit
There were no prior comments on this issue at the time of review.
Current CellScript behavior
The audit confirms the issue's premise, but also narrows it:
CellScript/src/stdlib/mod.rs
Lines 154 to 176 in 8ae6dc4
CellScript/src/ir/mod.rs
Lines 5658 to 5684 in 8ae6dc4
CellScript/src/codegen/runtime.rs
Lines 2041 to 2087 in 8ae6dc4
Therefore this is not “Since is currently unchecked.” The missing property is that source types and IR cannot prevent a valid encoded Since from being compared or added to an unrelated u64 domain.
One current API deserves migration priority
The generic DAO helper dao_require_input_since_at_least loads an 8-byte input Since and performs a raw unsigned comparison:
CellScript/src/codegen/runtime.rs
Lines 5844 to 5908 in 8ae6dc4
A raw integer ordering is only meaningful after both operands are known to use the same relative/absolute mode, metric, and valid encoding. CellScript also has a more specific relative-epoch helper that validates flags and compares epoch fractions. The typed migration should prefer such domain-specific operations and deprecate or strictly constrain the raw lower-bound helper.
This is a type-safety and API-footgun finding, not by itself proof that an existing caller is exploitable.
Exact CKB domains
RFC 17 is the normative source:
https://github.com/nervosnetwork/rfcs/blob/master/rfcs/0017-tx-valid-since/0017-tx-valid-since.md
The encoded u64 contains independent dimensions:
The source model should preserve these dimensions. A single nominal AbsoluteSince and RelativeSince type is still too weak if block, epoch, and timestamp metrics can be mixed.
A robust internal family is conceptually:
The surface syntax may use an enum/tagged value instead of generics. The non-negotiable part is that comparison and arithmetic dispatch on validated domains.
Recommended operations
Allow:
Reject:
A low-level to_raw operation can exist, but using its result should visibly leave the typed temporal domain.
ABI and compatibility
The implementation does not need a new CKB wire encoding. It can preserve u64 at the syscall and entry ABI boundary while keeping nominal types in:
Changing an exported source signature from u64 to a temporal nominal type should be treated as a source API change even if the runtime ABI remains eight bytes. The six-dimensional interface report is well suited to say “source breaking, runtime representation unchanged.”
Edition migration
Recommended sequence:
Do not silently change existing return types inside Edition 2026.
Required vectors
Share exact official vectors across type checker, constant folding, runtime lowering, builders, simulator, CKB-VM, and independent checker:
Argent comparison
Argent PR #48 is a good signal that temporal identity should survive artifacts. It does not define CKB's Since flags or epoch fractions, so it should not determine the type family. Official CKB RFC 17 remains authoritative.
Conclusion
The issue is correctly scoped as an edition-aware type-safety improvement. The backend can reuse the existing validated u64 encoders and metric-specific runtime helpers. The critical implementation work is to stop erasing domain identity before IR and to migrate raw comparison APIs that are only conditionally meaningful.
a19q3 commentedon Sep 11, 2026
0.30 update: commit 8353723 adds an exact temporal CKB-VM scenario fixture. One independently validated typed Since/HeaderDep artifact now covers all four positive inventory rows (absolute timelock, relative timelock, vesting, and combined epoch/timestamp/block access) and all five adversarial rows (cross-domain, missing header, early release, epoch overflow, and one-past HeaderDep). The fixture pins artifact, lowering, source-map, verified-bundle, raw-transaction, and serialized-transaction identities. The temporal test passed twice; artifact-checker 36/36 and Clippy also passed. Remaining work is independent review and exact release-candidate evidence.
a19q3 commentedon Sep 12, 2026
0.30 closure update: the typed temporal/Since contract is complete for the accepted additive scope. Exact fixtures bind the four positive inventory rows and five adversarial rows to the checked artifact, lowering/source-map/bundle identities, and distinct raw/serialized CKB transactions. Absolute and relative Since, epoch/timestamp/block domains, HeaderDep access, overflow, missing-header, cross-domain, early-release, and one-past bounds are covered.
The full clean release replay at 3cf60a1 passed, including CKB-VM and pinned CKB 0.207.0 production admission. Independent review was explicitly waived by the maintainer for this bounded 0.30 scope; it was not performed or claimed as passed. Broader transaction-view work remains separately scoped. Closing as complete.