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_settle and lease_force_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.

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_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, 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 MaxAttestationSkew 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.

[!IMPORTANT] The skew is a network parameter MaxAttestationSkew is 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 shrink LeaseForceSettleGap (which must stay above 2 × the skew). Read the live value from GET /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 is 200 (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 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, with a 10 second per-peer timeout (AttestationTimeout).
  3. Collect -- waits up to 15 seconds (AttestationGatherMax) for responses.
  4. 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.
  5. Check threshold -- returns an error if fewer than config.Threshold attestations 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:

  1. Checks the rate limit for the requesting peer and identifier.
  2. Validates the identifier is a 64-character hex string.
  3. Validates that the identifier is one it is allowed to attest (see below).
  4. Signs an attestation with the current time and the node's key pair.
  5. 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 (created or accepted), 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 crafted lease_accept / lease_force_settle blocks.

[!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
)