Timekeeper Attestations
How timekeepers attest to lease state
Lease acceptance, settlement and force-settlement require timekeeper attestations — signed timestamps from trusted nodes that prove when an event occurred. This prevents providers from manipulating start or settle times to shorten lease durations or claim early rewards, and stops consumers force-settling before the provider's window has closed.
[!DANGER] Mandatory enforcement Attestations are a hard requirement.
lease_accept,lease_settleandlease_force_settleblocks are rejected without valid attestations. There is no fallback to self-reported timestamps. Nodes running in provider mode refuse to start ifsys.timekeepersis not configured in the state chain.
Overview
Timekeepers are trusted nodes that can each independently provide a signed timestamp. Any node in the network can serve as a timekeeper — the trust is established by listing its public key in the sys.timekeepers state chain entry.
lease_accept, lease_settle and lease_force_settle blocks carry an array of attestations. The ledger validates that a threshold of distinct trusted timekeepers have attested a timestamp within an acceptable skew window, then uses the median timestamp as the canonical time.
Why require multiple attestations?
A single trusted timekeeper is sufficient to prove when an event occurred. The threshold exists as a defense against a compromised timekeeper.
Timestamps directly control XE emission: the attested start time on lease_accept and the attested end time on lease_settle determine how long a lease ran, which determines how much XE is minted. A compromised timekeeper could sign a far-future settle timestamp, making a lease appear to have run longer than it did and minting more XE than earned.
MaxAttestationSkew (production default 10 minutes) limits how far any single attestation can deviate from wall clock time, but within that window there is still room for manipulation — especially on short leases. Requiring a majority (2-of-3 on the current testnet) and taking the median ensures that a single compromised timekeeper cannot bias the canonical timestamp at all. To manipulate the median, an attacker would need to compromise a majority of timekeepers simultaneously.
This is a conservative design choice. The nodes are operator-controlled trusted infrastructure, so compromise of a single node is already a serious incident. The threshold adds defense-in-depth, not trustlessness.
Provider Node Timekeepers
│ │ │ │
│ attest_timestamp request │ │ │
│──────────────────────────►│ │ │
│──────────────────────────────►│ │
│─────────────────────────────────►│
│ │ │ │
│ signed attestation │ │ │
│◄──────────────────────────│ │ │
│◄────────────────────────────│ │
│◄───────────────────────────────│
│ │ │ │
│ attach to block, then │ │ │
│ sign (bound into hash) │ │ │TimekeeperAttestation struct
type TimekeeperAttestation struct {
PublicKey string // hex-encoded ed25519 public key of the timekeeper
Timestamp int64 // unix nanoseconds attested
Signature string // hex-encoded ed25519 signature over the attestation payload
}TimekeeperConfig struct
type TimekeeperConfig struct {
Keys []string // hex-encoded ed25519 public keys of trusted timekeepers
Threshold int // number of valid attestations required (quorum)
}The timekeeper configuration is stored in the state chain under the sys.timekeepers key. It must be present in the genesis block for any network that supports compute leases.
[!WARNING] Testnet-specific The current testnet uses the 3 bootstrap nodes as timekeepers with a threshold of 2. The timekeeper set and threshold are per-network configuration, not protocol values.
Attestation payload
The signed payload is a SHA-256 hash of the lease hash concatenated with the timestamp:
payload = sha256(lease_hash_bytes || timestamp_big_endian_8_bytes)Where:
lease_hash_bytesis the 32-byte decoded hex of the original lease block's hash.timestamp_big_endian_8_bytesis the 8-byte big-endian encoding of the unix nanosecond timestamp.
func AttestationPayload(leaseHash string, timestamp int64) ([]byte, error) {
hashBytes, _ := hex.DecodeString(leaseHash) // 32 bytes
var ts [8]byte
binary.BigEndian.PutUint64(ts[:], uint64(timestamp))
h := sha256.New()
h.Write(hashBytes)
h.Write(ts[:])
return h.Sum(nil), nil
}Core functions
Function
Description
AttestationPayload(leaseHash, timestamp)
Computes the SHA-256 payload to be signed
SignAttestation(leaseHash, timestamp, keyPair)
Creates a signed attestation using an ed25519 key pair
VerifyAttestation(attestation, leaseHash)
Verifies a single attestation's signature
ValidateAttestations(attestations, leaseHash, config, skipSkew)
Validates quorum and returns the median timestamp; skipSkew bypasses the skew bound on the sync path
Validation rules
ValidateAttestations enforces the following:
Rule
Detail
Quorum
At least config.Threshold valid attestations from distinct trusted keys
Trusted keys only
Attestation's PublicKey must be in config.Keys
No duplicates
Each trusted key counted at most once
Signature valid
ed25519.Verify must pass for the attestation payload
Skew limit
abs(attestation.Timestamp - now) must be <= MaxAttestationSkew (production default 10 minutes)
Array cap
At most MaxAttestationsPerBlock (20) attestations per block
The skew check is bypassed on the sync path (skipSkew), so historical blocks whose attestations are long past can still be replayed — mirroring the block-timestamp escape in addBlock.
[!WARNING] Skew rejection Attestations with timestamps more than
MaxAttestationSkewfrom the validating node's current time are silently dropped. This prevents a compromised timekeeper from signing far-future timestamps that would allow instant lease settlement.
[!IMPORTANT] The skew is a network parameter
MaxAttestationSkewis pinned by the genesis block alongside the lease timings (#662). The production default is 10 minutes; a network may pin a smaller window, which in turn lets it shrinkLeaseForceSettleGap(which must stay above 2 × the skew). Read the live value fromGET /node(lease_timing.max_attestation_skew_ns).
Median timestamp
After filtering to valid attestations, the median is used as the canonical timestamp rather than the mean or any single value:
sort.Slice(validTimestamps, func(i, j int) bool {
return validTimestamps[i] < validTimestamps[j]
})
median := validTimestamps[(len(validTimestamps)-1)/2]For even counts, the lower-middle value is used. This is deliberately conservative — it underestimates time rather than overestimates. Since timestamps control XE emission (later settle time = more XE minted), underestimating is the safe direction. A single compromised timekeeper contributing a high timestamp cannot shift the lower-middle median upward.
[!EXAMPLE] Median selection Given 4 valid timestamps
[100, 200, 300, 400], the index is(4-1)/2 = 1, so the median is200(not the average of 200 and 300).
[!EXAMPLE] Compromised timekeeper With 3 timekeepers and threshold 2, honest nodes report timestamps
[1000, 1001]and a compromised node reports[1600](within the 10-minute skew). The sorted valid set is[1000, 1001, 1600], median index(3-1)/2 = 1, canonical timestamp is1001. The compromised value has no effect.
Attestation gathering
The node gathers attestations by sending attest_timestamp direct messages to all connected peers in parallel:
- Self-sign -- if the node itself is a trusted timekeeper, it signs locally first.
- Fan-out -- sends
AttestationRequest{LeaseHash}to all connected peers concurrently, with a 10 second per-peer timeout (AttestationTimeout). - Collect -- waits up to 15 seconds (
AttestationGatherMax) for responses. - Filter and cap -- drops untrusted, duplicate and invalid attestations, then caps the set at
MaxAttestationsPerBlock. Without this a provider with more than 20 connected peers would build a block that failed its own validation. - Check threshold -- returns an error if fewer than
config.Thresholdattestations survive.
Rate limiting
Timekeeper nodes rate-limit attestation requests: one attestation per peer per identifier per 30 seconds (AttestationRateLimit). This prevents flooding. The cooldown slot is recorded only once a request has passed validation, so a rejected request does not burn the window for the retry that would have succeeded.
Timekeeper endpoint
Any node can act as a timekeeper. When a node receives an attest_timestamp message, it:
- Checks the rate limit for the requesting peer and identifier.
- Validates the identifier is a 64-character hex string.
- Validates that the identifier is one it is allowed to attest (see below).
- Signs an attestation with the current time and the node's key pair.
- Returns the signed attestation.
[!DANGER] Not a free-form signing oracle A timekeeper will only sign for an identifier it can account for: a lease it holds that still has attestation-gated transitions left (
createdoraccepted), or a performance certificate benchmark for a directory-registered provider — that provider's address, or the same address with its middle 16 hex characters zeroed (the end-of-benchmark identifier). Anything else is refused. Signing arbitrary 64-char hex would let an attacker harvest attestations and replay them inside craftedlease_accept/lease_force_settleblocks.
[!NOTE] Bound into the block hash Attestations are attached to the block before it is signed and are bound into the block's SHA-256 hash, together with
certificate_hash, via an auxiliary canonical encoding. Attestations are sorted by public key so the encoding is order-independent. This means they cannot be tampered with in transit while leaving the hash and signature valid.
Constants
// core/attestation.go
const DefaultMaxAttestationSkew = int64(10 * time.Minute) // genesis-pinnable default
const MaxAttestationsPerBlock = 20 // hard cap per block
var MaxAttestationSkew = DefaultMaxAttestationSkew // set from genesis at load
// node/attestation.go
const (
AttestationTimeout = 10 * time.Second // per-peer request timeout
AttestationGatherMax = 15 * time.Second // total gather deadline
AttestationRateLimit = 30 * time.Second // per-peer-per-identifier cooldown
)