Skip to main content

Timelock & Expiry

The TimelockExpiry contract is a reference policy that enforces a time window on reveals. A commitment bound to this policy can only be revealed after a specified unlock time and before a specified expiry time. Outside that window, the reveal is rejected.

Contract

contract TimelockExpiry is IRevealPolicy {
function validate(
bytes32, // commitment (unused)
bytes32, // nullifier (unused)
address, // recipient (unused)
uint256, // amount (unused)
address, // token (unused)
bytes calldata policyParams
) external view returns (bool valid) {
(uint256 lockUntil, uint256 expiresAt) = abi.decode(policyParams, (uint256, uint256));
return block.timestamp >= lockUntil && block.timestamp <= expiresAt;
}
}

The contract is entirely stateless -- no constructor, no storage, no admin functions. It reads block.timestamp and compares it against the two decoded parameters. This simplicity is by design: the contract is pure validation logic with no attack surface beyond the EVM's timestamp mechanism.

Parameters

ParameterTypeDescription
lockUntiluint256Earliest Unix timestamp at which the reveal is allowed. Before this time, validate() returns false.
expiresAtuint256Latest Unix timestamp at which the reveal is allowed. After this time, validate() returns false.

The parameters are ABI-encoded as abi.encode(uint256 lockUntil, uint256 expiresAt) and their hash is bound to the commitment:

policyParamsHash = keccak256(abi.encode(lockUntil, expiresAt)) % BN254_SCALAR_FIELD

Because policyParamsHash is an input to the Poseidon commitment hash, the time window is cryptographically fixed at commit time. Neither the committer nor anyone else can alter the unlock or expiry time after the commitment is created.

Validation Logic

The valid reveal window is the closed interval [lockUntil, expiresAt]. Both boundary values are inclusive.

Use Cases

TimelockExpiry applies to any data type in the Specter protocol. The following examples illustrate the breadth of its applicability.

Token Vesting Schedules

A company commits GHOST tokens for employee vesting. Each tranche has a different lockUntil corresponding to the vesting date:

TranchelockUntilexpiresAtEffect
Year 12027-01-012030-01-01Tokens unlock after 1 year, expire after 4 years
Year 22028-01-012030-01-01Tokens unlock after 2 years
Year 32029-01-012030-01-01Tokens unlock after 3 years

The employee can reveal (claim) each tranche only when the current time is within the window. If the employee leaves the company and does not claim before expiry, the tokens remain permanently locked in the commitment -- they can never be revealed.

Time-Delayed Payments

A buyer commits payment for goods with lockUntil set 7 days in the future. This gives the buyer a dispute window: if the goods are not delivered, the payment cannot be claimed. After 7 days, the seller can reveal and collect the payment. An expiresAt of 30 days ensures the seller must claim within a reasonable period.

Escrow with Deadline

A freelancer commits work product (credential or document hash) with a delivery window. The client knows the work can only be delivered within the agreed timeframe. If the freelancer misses the deadline (block.timestamp exceeds expiresAt), the commitment becomes permanently unredeemable.

Credential Validity Windows

A certification authority issues a credential with a 1-year validity period. The lockUntil is the issuance date and expiresAt is 1 year later. The credential holder can prove their certification during this window but not after it expires. Re-certification requires a new commitment with updated parameters.

Time-Bound API Key Access

A service provider commits an API key hash with a time window matching the subscription period. The key can only be revealed (and thus validated) during the active subscription. After expiry, the commitment is dead -- the key cannot be retrieved or proven valid.

Scheduled Data Release

A journalist commits the hash of an investigative report with lockUntil set to a future publication date. The report cannot be revealed before the embargo lifts, ensuring coordinated disclosure. The expiresAt can be set far in the future or omitted (set to type(uint256).max) to allow indefinite access after the unlock.

Composability

TimelockExpiry can be combined with other policies by creating a custom policy contract that calls multiple validators internally. For example, a contract that requires both a time window AND a specific recipient would combine TimelockExpiry logic with DestinationRestriction logic in a single validate() function.

The policy system does not natively support policy chaining (one commitment can only have one policyId), but composite policies that embed multiple validation checks are straightforward to implement.

Edge Cases

ScenarioBehavior
lockUntil == expiresAtValid for exactly one timestamp (single-block window in practice)
lockUntil > expiresAtNo valid window -- the commitment can never be revealed
lockUntil == 0Effectively no lower bound -- reveal is allowed from genesis
expiresAt == type(uint256).maxEffectively no upper bound -- reveal is allowed indefinitely after unlock
Both are 0Reveal is allowed at timestamp 0 only -- effectively never on a live chain