Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions doc/missions.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ Stress test a network of simulated Tier1 topology with classic traffic and repor

### Parameters

- `--enable-trigger-timer=<true|false>`: Enable or disable `EXPERIMENTAL_TRIGGER_TIMER` on all nodes. Defaults to enabled for this mission.
- `--force-old-style-trigger-timer=<true|false>`: Set `FORCE_OLD_STYLE_PREPARE_START_TRIGGER_TIMER` on all nodes, forcing the pre-protocol-28 prepare-start trigger timer. Unset by default, in which case core picks the timer by protocol version (the consensus-close-time timer from protocol 28 on).

## MissionSorobanLoadGeneration

Expand Down Expand Up @@ -157,7 +157,7 @@ Stress test a network of simulated Tier1 topology with a mix of classic and soro

### Parameters

- `--enable-trigger-timer=<true|false>`: Enable or disable `EXPERIMENTAL_TRIGGER_TIMER` on all nodes. Defaults to enabled for this mission.
- `--force-old-style-trigger-timer=<true|false>`: Set `FORCE_OLD_STYLE_PREPARE_START_TRIGGER_TIMER` on all nodes, forcing the pre-protocol-28 prepare-start trigger timer. Unset by default, in which case core picks the timer by protocol version (the consensus-close-time timer from protocol 28 on).

## MissionMinBlockTimeClassic

Expand All @@ -169,11 +169,11 @@ Same as `MissionMinBlockTimeClassic`, but drives an explicit `MIXED_PREGEN_*` ov

## MissionTriggerTimerMixConsensus

Tests the `EXPERIMENTAL_TRIGGER_TIMER` feature on a simulated Public Network topology with a configurable mix of nodes that have the flag enabled versus disabled, under configurable clock-drift distributions. It drives the same `MIXED_PREGEN_*` (classic + synthetic Soroban) overlay-only load as `MissionMinBlockTimeMixed`, but instead of binary-searching for a minimum block time it runs a single load pass at a fixed ledger close time and verifies consensus stays healthy (no errors, pairwise-consistent, all nodes in sync). Requires a generated pubnet topology via `--pubnet-data`.
Tests the protocol-28 consensus-close-time trigger timer on a simulated Public Network topology with a configurable mix of nodes running it versus nodes forced back onto the older prepare-start timer via `FORCE_OLD_STYLE_PREPARE_START_TRIGGER_TIMER`, under configurable clock-drift distributions. It drives the same `MIXED_PREGEN_*` (classic + synthetic Soroban) overlay-only load as `MissionMinBlockTimeMixed`, but instead of binary-searching for a minimum block time it runs a single load pass at a fixed ledger close time and verifies consensus stays healthy (no errors, pairwise-consistent, all nodes in sync). Requires a generated pubnet topology via `--pubnet-data`.

### Parameters

- `--trigger-timer-flag-pct`: Percentage (0-100) of nodes with `EXPERIMENTAL_TRIGGER_TIMER` enabled. Default 100. This mission rejects `--enable-trigger-timer`; use this flag instead.
- `--force-old-style-trigger-timer-pct`: Percentage (0-100) of nodes with `FORCE_OLD_STYLE_PREPARE_START_TRIGGER_TIMER` set, i.e. forced onto the old prepare-start timer. The rest use the protocol default. Default 0. This mission rejects `--force-old-style-trigger-timer`; use this flag instead.
- `--drift-pct`: Percentage (0-100) of nodes that receive clock drift. Default 0.
- `--uniform-drift=lower,upper`: Uniform random clock drift, in signed ms, applied to each drifting node (e.g. `--uniform-drift=-2000,+2000`).
- `--bimodal-drift=min1,max1,min2,max2`: Bimodal clock drift, in signed ms — the first half of the drifting nodes draw from `[min1,max1]`, the second half from `[min2,max2]` (e.g. `--bimodal-drift=-5000,-2000,+2000,+5000`).
Expand Down
22 changes: 11 additions & 11 deletions src/App/Program.fs
Original file line number Diff line number Diff line change
Expand Up @@ -140,12 +140,12 @@ type MissionOptions
minBlockTimeMixedMode: string,
minBlockTimeMixedClassicTxRate: int option,
minBlockTimeMixedSorobanTxRate: int option,
triggerTimerFlagPct: int,
forceOldStyleTriggerTimerPct: int,
uniformDrift: seq<int>,
bimodalDrift: seq<int>,
driftPct: int,
ledgerCloseTimeMs: int option,
enableTriggerTimer: bool option
forceOldStyleTriggerTimer: bool option
) =

[<Option('k', "kubeconfig", HelpText = "Kubernetes config file", Required = false, Default = "~/.kube/config")>]
Expand Down Expand Up @@ -651,11 +651,11 @@ type MissionOptions
Required = false)>]
member self.MinBlockTimeMixedSorobanTxRate = minBlockTimeMixedSorobanTxRate

[<Option("trigger-timer-flag-pct",
HelpText = "Percentage (0-100) of nodes with EXPERIMENTAL_TRIGGER_TIMER enabled",
[<Option("force-old-style-trigger-timer-pct",
HelpText = "Percentage (0-100) of nodes with FORCE_OLD_STYLE_PREPARE_START_TRIGGER_TIMER set, i.e. forced onto the pre-protocol-28 prepare-start trigger timer. The remaining nodes use the protocol default",
Required = false,
Default = 100)>]
member self.TriggerTimerFlagPct = triggerTimerFlagPct
Default = 0)>]
member self.ForceOldStyleTriggerTimerPct = forceOldStyleTriggerTimerPct

[<Option("uniform-drift",
Separator = ',',
Expand All @@ -680,10 +680,10 @@ type MissionOptions
Required = false)>]
member self.LedgerCloseTimeMs = ledgerCloseTimeMs

[<Option("enable-trigger-timer",
HelpText = "Enable or disable EXPERIMENTAL_TRIGGER_TIMER on all nodes (true/false). Defaults to true in the max TPS missions and to the stellar-core default everywhere else. Not supported by TriggerTimerMixConsensus, which uses --trigger-timer-flag-pct instead",
[<Option("force-old-style-trigger-timer",
HelpText = "Set FORCE_OLD_STYLE_PREPARE_START_TRIGGER_TIMER on all nodes (true/false), forcing the pre-protocol-28 prepare-start trigger timer. Unset by default, in which case core picks the timer by protocol version. Not supported by TriggerTimerMixConsensus, which uses --force-old-style-trigger-timer-pct instead",
Required = false)>]
member self.EnableTriggerTimer = enableTriggerTimer
member self.ForceOldStyleTriggerTimer = forceOldStyleTriggerTimer

let splitLabel (lab: string) : (string * string option) =
match lab.Split ':' |> Array.toList with
Expand Down Expand Up @@ -929,12 +929,12 @@ let main argv =
minBlockTimeMixedClassicTxRate = mission.MinBlockTimeMixedClassicTxRate
minBlockTimeMixedSorobanTxRate = mission.MinBlockTimeMixedSorobanTxRate
runForMinBlockTime = false
triggerTimerFlagPct = mission.TriggerTimerFlagPct
forceOldStyleTriggerTimerPct = mission.ForceOldStyleTriggerTimerPct
uniformDrift = List.ofSeq mission.UniformDrift
bimodalDrift = List.ofSeq mission.BimodalDrift
driftPct = mission.DriftPct
ledgerCloseTimeMs = mission.LedgerCloseTimeMs
enableTriggerTimer = mission.EnableTriggerTimer }
forceOldStyleTriggerTimer = mission.ForceOldStyleTriggerTimer }

allMissions.[m] missionContext

Expand Down
31 changes: 20 additions & 11 deletions src/FSLibrary.Tests/Tests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -147,12 +147,12 @@ let ctx : MissionContext =
minBlockTimeMixedClassicTxRate = None
minBlockTimeMixedSorobanTxRate = None
runForMinBlockTime = false
triggerTimerFlagPct = 100
forceOldStyleTriggerTimerPct = 0
uniformDrift = []
bimodalDrift = []
driftPct = 0
ledgerCloseTimeMs = None
enableTriggerTimer = None }
forceOldStyleTriggerTimer = None }

let netdata = __SOURCE_DIRECTORY__ + "/../../../data/public-network-data-2024-08-01.json"
let pubkeys = __SOURCE_DIRECTORY__ + "/../../../data/tier1keys.json"
Expand Down Expand Up @@ -195,14 +195,14 @@ type Tests(output: ITestOutputHelper) =
Assert.Contains("HTTP_PORT = " + CfgVal.httpPort.ToString(), toml)
// Trigger timer and clock offset settings must be omitted unless
// explicitly configured on the CoreSet or the mission context.
Assert.DoesNotContain("EXPERIMENTAL_TRIGGER_TIMER", toml)
Assert.DoesNotContain("FORCE_OLD_STYLE_PREPARE_START_TRIGGER_TIMER", toml)
Assert.DoesNotContain("ARTIFICIALLY_SET_SYSTEM_CLOCK_OFFSET_FOR_TESTING", toml)

[<Fact>]
member __.``TOML Config emits trigger timer and per-node clock offsets``() =
let opts =
{ coreSetOptions with
experimentalTriggerTimer = Some true
forceOldStyleTriggerTimer = Some true
clockOffsets = Some [ 0; -800; 1500 ] }

let cs = MakeLiveCoreSet "test" opts
Expand All @@ -211,7 +211,7 @@ type Tests(output: ITestOutputHelper) =
let tomlOfNode i = cfg.StellarCoreCfg(cs, i, MainCoreContainer).ToString()

for i in 0 .. 2 do
Assert.Contains("EXPERIMENTAL_TRIGGER_TIMER = true", tomlOfNode i)
Assert.Contains("FORCE_OLD_STYLE_PREPARE_START_TRIGGER_TIMER = true", tomlOfNode i)

Assert.Contains("ARTIFICIALLY_SET_SYSTEM_CLOCK_OFFSET_FOR_TESTING = 0", tomlOfNode 0)
Assert.Contains("ARTIFICIALLY_SET_SYSTEM_CLOCK_OFFSET_FOR_TESTING = -800", tomlOfNode 1)
Expand All @@ -225,18 +225,27 @@ type Tests(output: ITestOutputHelper) =

// The CoreSet leaves the option unset, so the mission-level flag
// decides whether (and with which value) the key is emitted.
Assert.Contains("EXPERIMENTAL_TRIGGER_TIMER = true", tomlWith { ctx with enableTriggerTimer = Some true })
Assert.Contains("EXPERIMENTAL_TRIGGER_TIMER = false", tomlWith { ctx with enableTriggerTimer = Some false })
Assert.DoesNotContain("EXPERIMENTAL_TRIGGER_TIMER", tomlWith ctx)
Assert.Contains(
"FORCE_OLD_STYLE_PREPARE_START_TRIGGER_TIMER = true",
tomlWith { ctx with forceOldStyleTriggerTimer = Some true }
)

Assert.Contains(
"FORCE_OLD_STYLE_PREPARE_START_TRIGGER_TIMER = false",
tomlWith { ctx with forceOldStyleTriggerTimer = Some false }
)

Assert.DoesNotContain("FORCE_OLD_STYLE_PREPARE_START_TRIGGER_TIMER", tomlWith ctx)

// A CoreSet-level setting wins over the mission-level flag.
let csOn =
MakeLiveCoreSet "test" { coreSetOptions with experimentalTriggerTimer = Some true }
MakeLiveCoreSet "test" { coreSetOptions with forceOldStyleTriggerTimer = Some true }

let cfgOn = MakeNetworkCfg { ctx with enableTriggerTimer = Some false } [ csOn ] passOpt
let cfgOn =
MakeNetworkCfg { ctx with forceOldStyleTriggerTimer = Some false } [ csOn ] passOpt

Assert.Contains(
"EXPERIMENTAL_TRIGGER_TIMER = true",
"FORCE_OLD_STYLE_PREPARE_START_TRIGGER_TIMER = true",
cfgOn.StellarCoreCfg(csOn, 0, MainCoreContainer).ToString()
)

Expand Down
11 changes: 0 additions & 11 deletions src/FSLibrary/MaxTPSTest.fs
Original file line number Diff line number Diff line change
Expand Up @@ -133,17 +133,6 @@ let maxTPSTest (context: MissionContext) (baseLoadGen: LoadGen) (setupCfg: LoadG
context.image
(if context.flatQuorum.IsSome then context.flatQuorum.Value else false)

// The max TPS missions enable EXPERIMENTAL_TRIGGER_TIMER unless
// --enable-trigger-timer=false is passed explicitly.
let triggerTimer = context.enableTriggerTimer |> Option.defaultValue true

let allNodes =
allNodes
|> List.map
(fun (cs: CoreSet) ->
{ cs with
options = { cs.options with experimentalTriggerTimer = Some triggerTimer } })

// PayPregenerated requires node restart between failed iterations to ensure validity of the pregenerated transactions
// However, large-scale simulation restarts can be slow, so for now only use the new mode on small networks
let baseLoadGen =
Expand Down
53 changes: 30 additions & 23 deletions src/FSLibrary/MissionTriggerTimerMixConsensus.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
// under the Apache License, Version 2.0. See the COPYING file at the root
// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0

// This mission tests the EXPERIMENTAL_TRIGGER_TIMER feature with a mix of
// nodes that have it enabled vs disabled, under configurable clock drift
// distributions. It uses generated pubnet topologies (--pubnet-data) and
// overlays trigger timer and clock offset settings onto the CoreSets.
// This mission tests the protocol-28 consensus-close-time trigger timer with a
// mix of nodes running it vs nodes forced back onto the older prepare-start
// timer via FORCE_OLD_STYLE_PREPARE_START_TRIGGER_TIMER, under configurable
// clock drift distributions. It uses generated pubnet topologies
// (--pubnet-data) and overlays trigger timer and clock offset settings onto the
// CoreSets.
//
// Load is always generated in the same MIXED_PREGEN_* (classic + synthetic
// Soroban) mode as MissionMinBlockTimeMixed, with the same node resources,
Expand All @@ -14,7 +16,7 @@
// load pass at a fixed block time and check consensus stays healthy.
//
// CLI parameters:
// --trigger-timer-flag-pct N percentage of nodes with the flag (0-100, default 100)
// --force-old-style-trigger-timer-pct N percentage of nodes forced onto the old timer (0-100, default 0)
// --drift-pct N percentage of nodes that drift (0-100, default 0)
// --uniform-drift=lower,upper uniform random drift in [lower,upper] signed ms (e.g. -2000,+2000)
// --bimodal-drift=m1,M1,m2,M2 first half in [m1,M1], second half in [m2,M2] signed ms
Expand Down Expand Up @@ -59,15 +61,15 @@ let private driftSuffix (ms: int) =
// than 63 chars (it appends an 11-char nonce of its own; see StellarCoreSet.fs).
// With a 16-char run nonce plus the "-sts-"/"-N" scaffolding, the CoreSet name
// itself only has ~29 chars of headroom. Real pubnet home-domain names (e.g.
// "blockdaemon-non-tier1") plus our "-expr"/drift annotations overflow that, so
// "blockdaemon-non-tier1") plus our "-old"/drift annotations overflow that, so
// we cap the name length here.
let private maxCoreSetNameLen = 26

// Build an annotated CoreSet name: "<home-domain>-<idx><flag><drift>". The
// globally-unique idx guarantees the name stays distinct even after the
// descriptive home-domain prefix is truncated to fit the pod-name budget.
let private annotateName (baseName: string) (idx: int) (flagEnabled: bool) (offsetMs: int) =
let flagPart = if flagEnabled then "-expr" else ""
let private annotateName (baseName: string) (idx: int) (forceOldTimer: bool) (offsetMs: int) =
let flagPart = if forceOldTimer then "-old" else ""
let suffix = sprintf "-%d%s%s" idx flagPart (driftSuffix offsetMs)
let budget = maxCoreSetNameLen - suffix.Length

Expand Down Expand Up @@ -113,16 +115,16 @@ let private parseDrift (context: MissionContext) : ClockDriftDistribution =
let triggerTimerMixConsensus (baseContext: MissionContext) =
// This mission assigns the trigger timer per node; a blanket setting for
// all nodes would defeat its purpose.
if baseContext.enableTriggerTimer.IsSome then
if baseContext.forceOldStyleTriggerTimer.IsSome then
failwith
"--enable-trigger-timer is not supported by TriggerTimerMixConsensus; use --trigger-timer-flag-pct instead"
"--force-old-style-trigger-timer is not supported by TriggerTimerMixConsensus; use --force-old-style-trigger-timer-pct instead"

let drift = parseDrift baseContext
let flagPct = baseContext.triggerTimerFlagPct
let forceOldPct = baseContext.forceOldStyleTriggerTimerPct
let driftPct = baseContext.driftPct

if flagPct < 0 || flagPct > 100 then
failwith (sprintf "trigger-timer-flag-pct must be 0-100, got %d" flagPct)
if forceOldPct < 0 || forceOldPct > 100 then
failwith (sprintf "force-old-style-trigger-timer-pct must be 0-100, got %d" forceOldPct)

if driftPct < 0 || driftPct > 100 then
failwith (sprintf "drift-pct must be 0-100, got %d" driftPct)
Expand Down Expand Up @@ -204,21 +206,22 @@ let triggerTimerMixConsensus (baseContext: MissionContext) =
let totalNodes = List.sumBy (fun (cs: CoreSet) -> cs.options.nodeCount) baseCoreSets

LogInfo
"TriggerTimerMixConsensus: %d total nodes, flag-pct=%d%%, drift-pct=%d%%, mode=%s, classic-tps=%d, soroban-tps=%d, block-time=%dms"
"TriggerTimerMixConsensus: %d total nodes, force-old-timer-pct=%d%%, drift-pct=%d%%, mode=%s, classic-tps=%d, soroban-tps=%d, block-time=%dms"
totalNodes
flagPct
forceOldPct
driftPct
(mode.ToString())
classicTxRate
sorobanTxRate
targetMs

// Each node independently has a flagPct% chance of having the trigger
// timer flag enabled, and a driftPct% chance of drifting. When drifting,
// bimodal nodes have a 50/50 chance of being in the first or second group.
// Each node independently has a forceOldPct% chance of being forced onto
// the old prepare-start trigger timer, and a driftPct% chance of drifting.
// When drifting, bimodal nodes have a 50/50 chance of being in the first or
// second group.
let rng = System.Random(context.randomSeed)

let sampleFlag () = rng.Next(100) < flagPct
let sampleForceOldTimer () = rng.Next(100) < forceOldPct

let sampleOffset () =
match drift with
Expand All @@ -240,14 +243,18 @@ let triggerTimerMixConsensus (baseContext: MissionContext) =
let nc = cs.options.nodeCount

[ for j in 0 .. nc - 1 do
let flagEnabled = sampleFlag ()
let forceOldTimer = sampleForceOldTimer ()
let offset = sampleOffset ()
let idx = nodeIdx
nodeIdx <- nodeIdx + 1

let annotatedName = annotateName cs.name.StringName idx flagEnabled offset
let annotatedName = annotateName cs.name.StringName idx forceOldTimer offset

LogInfo " Node %s: trigger_timer=%b, offset=%d" annotatedName.StringName flagEnabled offset
LogInfo
" Node %s: force_old_trigger_timer=%b, offset=%d"
annotatedName.StringName
forceOldTimer
offset

{ cs with
name = annotatedName
Expand All @@ -256,7 +263,7 @@ let triggerTimerMixConsensus (baseContext: MissionContext) =
{ cs.options with
nodeCount = 1
nodeLocs = cs.options.nodeLocs |> Option.map (fun locs -> [ locs.[j] ])
experimentalTriggerTimer = if flagEnabled then Some true else None
forceOldStyleTriggerTimer = if forceOldTimer then Some true else None
clockOffsets = if offset <> 0 then Some [ offset ] else None } } ])

let tier1 = List.filter (fun (cs: CoreSet) -> cs.options.tier1 = Some true) modifiedCoreSets
Expand Down
Loading
Loading