Stellar Atlas
Protocol
CAP-0088

Millisecond-Resolution Close Times

Final Comment PeriodGitHub

Specification

Preamble

CAP: 0088
Title: Millisecond-Resolution Close Times
Working Group:
    Owner: Garand Tyson <@SirTyson>
    Authors: Garand Tyson <@SirTyson>
    Consulted: Marta Lokhova <@marta-lokhova>, Simon Chow <@chowbao>, Tamir Sen <@tamirms>, Leigh McCulloch <@leighmcculloch>
Status: Final Comment Period
Created: 2026-08-21
Discussion: https://github.com/orgs/stellar/discussions/1998
Protocol version: TBD

Simple Summary

This CAP adds a millisecond-resolution close time to each ledger. Today close times are whole seconds, which both places a hard one second floor on the interval between consecutive ledgers and prevents non whole-second ledger targets (e.g. moving from a 5,000 ms to a 4,500 ms target). With millisecond resolution, consecutive ledgers may close less than one second apart and at arbitrary millisecond intervals. The existing whole-second closeTime field in StellarValue is unchanged, and every protocol feature that consumes time (transaction time bounds, minSeqAge, claimable balance predicates, Soroban ledger timestamps, and upgrade scheduling) continues to operate on whole seconds, rounding the close time down. Downstream consumers of closeTime must update their XDR definitions to decode the new union arms, but require no semantic changes and may ignore the millisecond-resolution close time entirely.

Working Group

As specified in the Preamble.

Motivation

The close time of a ledger must strictly increase from the previous ledger. Because close times are currently expressed in whole seconds, the smallest possible increment between two ledgers is one second, placing a hard protocol floor of one second on ledger close times. Whole-second resolution also prevents non-whole-second ledger targets: a target of 2.5 seconds cannot be expressed, as the close time that anchors the next ledger trigger timer is rounded to a whole second, causing drift and uneven ledger intervals.

The intention of this change is better time resolution for intra-second block times, leading towards sub-second block times. With millisecond resolution, the protocol itself supports ledgers that close within the same whole second, removing close time as a constraint on ledger latency.

Goals Alignment

This CAP is aligned with the following Stellar Network Goal:

  • The Stellar Network should run at scale and at low cost to all participants of the network.

Abstract

This CAP introduces two new StellarValue extension arms, STELLAR_VALUE_SIGNED_MS and STELLAR_VALUE_EMPTY_TX_SET_MS, which extend the existing STELLAR_VALUE_SIGNED and STELLAR_VALUE_EMPTY_TX_SET arms with a closeTimeMs field holding the close time in milliseconds since the Unix epoch, typed as a new TimePointMs typedef (the millisecond analogue of TimePoint). The existing whole-second closeTime field retains its current meaning and must equal closeTimeMs truncated to whole seconds, so consumers that only need whole-second resolution require no changes beyond updated XDR definitions capable of decoding the new arms.

Starting with this protocol version, the effective close time of a ledger is closeTimeMs, and must strictly increase between consecutive ledgers with a minimum increment of one millisecond (previously one second). All existing time-based protocol features continue to be evaluated against the whole-second closeTime field, i.e. the effective close time rounded down to the nearest whole second.

Specification

XDR changes

This patch of XDR changes is based on the XDR files in commit bd7b53f57b6ccd23ca942bbb97c724208e3db90e of stellar-xdr.

diff --git a/Stellar-ledger.x b/Stellar-ledger.x
index 9acf1d7..ad6e937 100644
--- a/Stellar-ledger.x
+++ b/Stellar-ledger.x
@@ -15,6 +15,11 @@ enum StellarValueType
     STELLAR_VALUE_BASIC = 0,
     STELLAR_VALUE_SIGNED = 1,
     STELLAR_VALUE_EMPTY_TX_SET = 2
+#ifdef MS_CLOSE_TIME
+    ,
+    STELLAR_VALUE_SIGNED_MS = 3,
+    STELLAR_VALUE_EMPTY_TX_SET_MS = 4
+#endif // MS_CLOSE_TIME
 };
 
 struct LedgerCloseValueSignature
@@ -52,6 +57,23 @@ struct StellarValue
             uint32 previousLedgerVersion;
             LedgerCloseValueSignature lcValueSignature;
         } proposedValue;
+#ifdef MS_CLOSE_TIME
+    case STELLAR_VALUE_SIGNED_MS:
+        struct
+        {
+            TimePointMs closeTimeMs;
+            LedgerCloseValueSignature lcValueSignature;
+        } signedMsValue;
+    case STELLAR_VALUE_EMPTY_TX_SET_MS:
+        struct
+        {
+            TimePointMs closeTimeMs;
+            Hash txSetHash;
+            Hash previousLedgerHash;
+            uint32 previousLedgerVersion;
+            LedgerCloseValueSignature lcValueSignature;
+        } proposedMsValue;
+#endif // MS_CLOSE_TIME
     }
     ext;
 };
diff --git a/Stellar-types.x b/Stellar-types.x
index ac18496..79b127e 100644
--- a/Stellar-types.x
+++ b/Stellar-types.x
@@ -16,6 +16,10 @@ typedef hyper int64;
 
 typedef uint64 TimePoint;
 typedef uint64 Duration;
+#ifdef MS_CLOSE_TIME
+// Milliseconds since the Unix epoch. TimePoint is whole seconds.
+typedef uint64 TimePointMs;
+#endif // MS_CLOSE_TIME
 
 // An ExtensionPoint is always marshaled as a 32-bit 0 value.  At a
 // later point, it can be replaced by a different union so as to

Semantics

TimePointMs

This CAP adds TimePointMs, a uint64 typedef in Stellar-types.x denoting an absolute point in time expressed as milliseconds since the Unix epoch. It is the millisecond analogue of TimePoint, which denotes an absolute point in time in whole seconds.

Millisecond Close Time Representation

The effective close time of a ledger is closeTimeMs, the close time in milliseconds since the Unix epoch. The existing closeTime field continues to hold the close time in whole seconds since the Unix epoch, and the two fields must denote the same instant: a StellarValue is invalid unless closeTime == closeTimeMs / 1000 (integer division, i.e. the effective close time rounded down to the nearest whole second). Consumers that need millisecond resolution read closeTimeMs alone, while downstream systems can still rely on the legacy whole-second closeTime alone.

STELLAR_VALUE_SIGNED_MS is identical to STELLAR_VALUE_SIGNED plus the closeTimeMs field. STELLAR_VALUE_EMPTY_TX_SET_MS is identical to STELLAR_VALUE_EMPTY_TX_SET plus the closeTimeMs field, and its proposedMsValue fields carry the same meaning as the proposedValue fields specified in CAP-0083.

Value Signatures

The signature in lcValueSignature for the new value types additionally covers closeTimeMs so that it cannot be tampered with. For STELLAR_VALUE_SIGNED_MS and STELLAR_VALUE_EMPTY_TX_SET_MS values, the payload is (networkID, ENVELOPE_TYPE_SCPVALUE, txSetHash, closeTime, closeTimeMs). As in CAP-0083, the txSetHash signed for an empty-tx-set value is the originally proposed transaction set hash carried in proposedMsValue.txSetHash.

Close Time Monotonicity

The effective close time of a ledger must be strictly greater than the effective close time of the previous ledger. Under the previous protocol, the minimum increment between consecutive close times was one second; under this CAP it is one millisecond. As a result, consecutive ledgers may share the same whole-second closeTime value, differing only in closeTimeMs. The existing validity window for close times too far in the future is unchanged and is now evaluated at millisecond resolution.

Value Format at the Protocol Boundary

The StellarValue format for a ledger is governed by the protocol version of the previous ledger, i.e. the last closed ledger at the time SCP runs the slot:

  • If the previous ledger's protocol version is below this CAP's version, the value must be STELLAR_VALUE_SIGNED or STELLAR_VALUE_EMPTY_TX_SET (the latter subject to CAP-0083's protocol gate). The _MS types are invalid.
  • If the previous ledger's protocol version is greater than or equal to this CAP's version, the value must be STELLAR_VALUE_SIGNED_MS or STELLAR_VALUE_EMPTY_TX_SET_MS. The whole-second types are invalid.

Consequently, the ledger that applies the version upgrade still carries a whole-second value; the first ledger to carry an _MS value is the ledger immediately following the upgrade.

See the Protocol Upgrade Transition section for more detail.

Existing Time-Based Protocol Features

All existing protocol features that consume time continue to be evaluated against the whole-second closeTime field, which is the effective close time rounded down to the nearest whole second. None of their semantics change:

  • Transaction time bounds (TimeBounds.minTime/maxTime in PRECOND_TIME and PRECOND_V2): compared against the whole-second closeTime, as today. The same rules apply at the transaction queue and consensus level as at apply time. Note that a transaction whose maxTime equals the current whole second remains valid for every ledger that closes within that second.
  • minSeqAge and seqTime (CAP-0021): AccountEntryExtensionV3.seqTime continues to be recorded as the whole-second closeTime of the ledger in which the sequence number was consumed, and the age comparison remains in whole seconds.
  • ledgerBounds and minSeqLedgerGap (CAP-0021): based on ledger sequence numbers, not time; unaffected.
  • Claimable balance claim predicates (CLAIM_PREDICATE_BEFORE_ABSOLUTE_TIME and CLAIM_PREDICATE_BEFORE_RELATIVE_TIME): evaluated against (and, for relative predicates, anchored to) the whole-second closeTime, as today.
  • Soroban ledger timestamp (the ledger_timestamp host function): returns the whole-second closeTime as a Timepoint. Consecutive ledgers may now observe the same timestamp. This is the only time input to the Soroban host; the Timepoint and Duration value types themselves are unit-agnostic wrappers and are unaffected.
  • Network upgrade scheduling: validator-configured upgrade times remain whole-second values and are compared against the whole-second closeTime.

Warning: these rules are intended as a sound, minimal baseline for non whole-second ledger intervals (e.g. 4.5 seconds) that also remains well defined for hypothetical sub-second intervals. They are not intended to be the final word on sub-second ledgers. If the network's target close time ever drops below one second, the whole-second resolution of transaction time bounds, minSeqAge, claim predicates, and ledger_timestamp becomes coarser than the ledger interval itself, and these interfaces should be re-evaluated at that point (e.g. by introducing millisecond-resolution preconditions). Independently of this CAP, any change to the ledger interval may also break downstream assumptions that one ledger corresponds to a fixed duration (e.g. that a minSeqAge or time-bound window of N seconds spans roughly N / 5 ledgers).

Design Rationale

Separate Millisecond Field Instead of Redefining closeTime

Changing the units of closeTime to milliseconds would break every downstream consumer of the ledger header: Horizon, RPC, SDKs, block explorers, and smart contracts all consume closeTime as whole seconds. Keeping closeTime as the whole-second close time and adding a separate millisecond-resolution close time in the new StellarValue arms means existing consumers keep their semantics unchanged (needing only an XDR schema update) and can adopt the millisecond-resolution field only if they need it.

Whole-Second Semantics for User-Facing Time Features

All user-facing time interfaces (transaction time bounds, minSeqAge, claimable balance predicates, Soroban timestamps) intentionally retain whole-second resolution, rounding the close time down. Rounding down is the natural choice: a ledger closing at time T.500 has passed second T but not second T+1. This keeps transaction semantics identical to today and avoids churn across SDKs and downstream systems for a change whose purpose is consensus-level timing resolution, not transaction-level timing resolution. Millisecond resolution can be added to transaction time bounds and other user-facing interfaces in the future (e.g. via a new precondition type) if a use case emerges, but this does not seem necessary at this time.

Protocol Upgrade Transition

STELLAR_VALUE_SIGNED_MS and STELLAR_VALUE_EMPTY_TX_SET_MS values become valid and mandatory starting with the first slot ratified under the new protocol version, i.e. the ledger immediately following the one that applies the upgrade. All _MS value types are used unconditionally from the upgrade boundary onward.

Because the StellarValue format changes exactly at the upgrade boundary, nodes must take care when validating SCP messages for slots other than LCL+1. A node that has fallen behind the upgrade will receive valid _MS values for future slots from peers that have already upgraded, and a node that has upgraded may still receive and relay whole-second values for slots that predate the upgrade. Nodes must not drop such messages based on their local protocol version alone; format validity for slots other than LCL+1 follows the rules in the Value Format at the Protocol Boundary section. This allows an out-of-sync node to replay the upgrade boundary via SCP messages and regain sync without falling back to history catchup.

Downstream Impact

The transitive dependencies of StellarValue are unchanged from CAP-0083:

StellarValue
    │
    ├──► LedgerHeader.scpValue
    │    │
    │    └──► LedgerHeaderHistoryEntry.header
    │         │
    │         ├──► LedgerCloseMetaV0.ledgerHeader
    │         ├──► LedgerCloseMetaV1.ledgerHeader
    │         └──► LedgerCloseMetaV2.ledgerHeader
    │              │
    │              └──► LedgerCloseMeta (union)
    │                   │
    │                   └──► LedgerCloseMetaBatch.ledgerCloseMetas
    │
    └──► StoredDebugTransactionSet.scpValue

Downstream consumers that reason about StellarValue.ext must handle the two new arms: STELLAR_VALUE_SIGNED_MS should be treated exactly like STELLAR_VALUE_SIGNED, and STELLAR_VALUE_EMPTY_TX_SET_MS exactly like STELLAR_VALUE_EMPTY_TX_SET (i.e. an empty ledger, per CAP-0083). Note that every consumer that decodes StellarValue (or anything containing it, such as ledger headers or ledger close meta) needs XDR definitions that include the new arms, as XDR decoding fails on unknown union discriminants. Beyond that schema update, consumers that only read closeTime need no changes and do not need to ingest closeTimeMs.

Beyond the ledger header, the new arms also appear wherever raw SCP envelopes are exposed, such as the scp category of history archives (SCPHistoryEntry) and node-local SCP history tables. Consumers that decode SCP messages must tolerate the new value types there as well.

Backwards Incompatibilities

There are no backwards incompatibilities at the transaction interface: all time bounds, preconditions, predicates, and contract-visible timestamps keep their existing whole-second semantics.

One behavioral assumption weakens: previously, consecutive ledgers always differed in close time by at least one second, so any whole-second time boundary could flip at most once per ledger. With millisecond close times, boundary conditions on features like minSeqAge can now span multiple ledgers (in theory). For example, several ledgers may close within the same whole second, during which a transaction's minSeqAge requirement remains unmet ledger after ledger, a transaction whose maxTime is the current second remains includable in multiple successive ledgers, and Soroban contracts may observe the same ledger_timestamp across multiple ledgers. Applications that used "one ledger later" as a proxy for "at least one second later" should not rely on that equivalence, though we are not aware of any correct use of closeTime that does.

Implementation

Preamble

Status
Final Comment Period
Protocol version
TBD
Authors
Garand Tyson
Created
2026-08-21

Discussion

0 linked threads