Millisecond-Resolution Close Times
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_SIGNEDorSTELLAR_VALUE_EMPTY_TX_SET(the latter subject to CAP-0083's protocol gate). The_MStypes 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_MSorSTELLAR_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/maxTimeinPRECOND_TIMEandPRECOND_V2): compared against the whole-secondcloseTime, as today. The same rules apply at the transaction queue and consensus level as at apply time. Note that a transaction whosemaxTimeequals the current whole second remains valid for every ledger that closes within that second. minSeqAgeandseqTime(CAP-0021):AccountEntryExtensionV3.seqTimecontinues to be recorded as the whole-secondcloseTimeof the ledger in which the sequence number was consumed, and the age comparison remains in whole seconds.ledgerBoundsandminSeqLedgerGap(CAP-0021): based on ledger sequence numbers, not time; unaffected.- Claimable balance claim predicates
(
CLAIM_PREDICATE_BEFORE_ABSOLUTE_TIMEandCLAIM_PREDICATE_BEFORE_RELATIVE_TIME): evaluated against (and, for relative predicates, anchored to) the whole-secondcloseTime, as today. - Soroban ledger timestamp (the
ledger_timestamphost function): returns the whole-secondcloseTimeas aTimepoint. Consecutive ledgers may now observe the same timestamp. This is the only time input to the Soroban host; theTimepointandDurationvalue 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
- stellar-core: https://github.com/stellar/stellar-core/pull/5423
- stellar-xdr: https://github.com/stellar/stellar-xdr/pull/316
Preamble
Discussion
0 linked threads