Skip to content

Timekeeper Attestations

Lease acceptance and 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.

Mandatory enforcement

Attestations are a hard requirement. Both lease_accept and lease_settle blocks are rejected without valid attestations. There is no fallback to self-reported timestamps. Nodes running in provider mode refuse to start if sys.timekeepers is 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.

Both lease_accept and lease_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.

The MaxAttestationSkew (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 (currently 2-of-3) 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          │  │  │
     │  (excluded from 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. The current testnet uses the 3 bootstrap nodes as timekeepers with a threshold of 2.

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_bytes is the 32-byte decoded hex of the original lease block's hash.
  • timestamp_big_endian_8_bytes is 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) Validates quorum and returns the median timestamp

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 (10 minutes)
Array cap At most MaxAttestationsPerBlock (20) attestations per block

Skew rejection

Attestations with timestamps more than 10 minutes from 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.

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.

Median selection

Given 4 valid timestamps [100, 200, 300, 400], the index is (4-1)/2 = 1, so the median is 200 (not the average of 200 and 300).

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 is 1001. The compromised value has no effect.

Attestation gathering

The node gathers attestations by sending attest_timestamp direct messages to all connected peers in parallel:

  1. Self-sign -- if the node itself is a trusted timekeeper, it signs locally first.
  2. Fan-out -- sends AttestationRequest{LeaseHash} to all connected peers concurrently.
  3. Collect -- waits up to 15 seconds (AttestationGatherMax) for responses.
  4. Check threshold -- returns an error if fewer than config.Threshold attestations were gathered.

Rate limiting

Timekeeper nodes rate-limit attestation requests: one attestation per peer per lease per 30 seconds (AttestationRateLimit). This prevents flooding.

Timekeeper endpoint

Any node can act as a timekeeper. When a node receives an attest_timestamp message, it:

  1. Validates the identifier is a 64-character hex string (lease hash or provider address).
  2. Checks the rate limit for the requesting peer.
  3. Signs an attestation with the current time and the node's key pair.
  4. Returns the signed attestation.

Not restricted to lease hashes

The attestation endpoint accepts any 64-char hex identifier. This allows attestations for both lease operations (using the lease block hash) and performance certificates (using the provider's account address).

Excluded from block hash

Attestations are attached to blocks after signing. They are excluded from the block's SHA-256 hash computation, allowing attestations to be collected independently without invalidating the block signature.

Constants

const (
    MaxAttestationSkew     = 10 * time.Minute  // max allowed timestamp drift
    MaxAttestationsPerBlock = 20                // hard cap per block
    AttestationTimeout      = 10 * time.Second  // per-peer request timeout
    AttestationGatherMax    = 15 * time.Second  // total gather deadline
    AttestationRateLimit    = 30 * time.Second  // per-peer-per-lease cooldown
)