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:
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) |
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:
- Self-sign -- if the node itself is a trusted timekeeper, it signs locally first.
- Fan-out -- sends
AttestationRequest{LeaseHash}to all connected peers concurrently. - Collect -- waits up to 15 seconds (
AttestationGatherMax) for responses. - Check threshold -- returns an error if fewer than
config.Thresholdattestations 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:
- Validates the identifier is a 64-character hex string (lease hash or provider address).
- Checks the rate limit for the requesting peer.
- Signs an attestation with the current time and the node's key pair.
- 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
)