From f05165116e7817af5e533e47e85a64bc0872de49 Mon Sep 17 00:00:00 2001 From: Daniel Rebelsky <4641927+drebelsky@users.noreply.github.com> Date: Wed, 19 Aug 2026 11:25:20 -0700 Subject: [PATCH] Clean up min block scripts. Make slp_eval and measure_e2e both easier to use wrappers around one common script. --- scripts/base.sh | 397 +++++++++++++++++++++++++++++++++++++++++ scripts/measure_e2e.sh | 243 ++++++++++++++++++++++--- scripts/slp_eval.sh | 369 +++----------------------------------- 3 files changed, 637 insertions(+), 372 deletions(-) create mode 100644 scripts/base.sh diff --git a/scripts/base.sh b/scripts/base.sh new file mode 100644 index 00000000..6abb6051 --- /dev/null +++ b/scripts/base.sh @@ -0,0 +1,397 @@ +#!/bin/sh + +# Shared implementation behind the MinBlockTimeMixed wrapper scripts. +# +# This file is *sourced*, never executed: +# +# . "$(dirname "$0")/base.sh" +# +# It defines only constants and functions, so sourcing it has no side effects. +# A wrapper is expected to: +# +# 1. Override any of the presets below (NETWORK_SIZE_LIMIT, PUBNET_DATA_FILE, +# BLOCK_TIME_MS, ...). +# 2. Define its own usage(), which die() prints on argument errors. +# 3. Parse its own flags, delegating anything it does not recognize to +# base_parse_arg. +# 4. Call base_validate_args, then base_resolve_derived, then +# base_run_selected_loads. +# +# Benchmark setup: +# - One mission run is started for each selected Soroban load flag: +# --sac, --oz, and/or --soroswap. Passing multiple flags runs them +# sequentially, not as one combined Soroban workload. +# - Every run includes CLASSIC_TX_RATE pre-generated classic payment TPS to +# match current network conditions. The flag value supplies only the Soroban +# TPS for that run, so total TPS is CLASSIC_TX_RATE + selected Soroban TPS. +# - The mission uses MinBlockTimeMixed's MIXED_PREGEN_* overlay-only loadgen +# mode, simulated pubnet network delay, with NETWORK_SIZE_LIMIT nodes. +# - Validators are configured with automatic quorum sets +# (--enable-relaxed-auto-qset-config), which needs a stellar-core build that +# supports SKIP_HIGH_CRITICAL_VALIDATOR_CHECKS_FOR_TESTING. +# - The block-time search range is intentionally narrow: the mission searches +# [BLOCK_TIME_MS - BLOCK_TIME_BAND_MS, BLOCK_TIME_MS + BLOCK_TIME_BAND_MS], +# and because the band matches the mission's binary-search threshold that +# leaves exactly one candidate to evaluate: BLOCK_TIME_MS itself. +# - simulate-apply-duration is derived from SIMULATE_APPLY_BUDGET_MS and the +# total TPS so the synthetic apply sleep budget remains roughly constant as +# the requested Soroban rate changes. A wrapper that changes +# SIMULATE_APPLY_BUDGET_MS moves the per-ledger budget the derivation aims +# at; the per-operation value is always computed, never set directly. +# +# Result interpretation: +# - A zero exit and a "Minimum sustainable block time: ..." log line means the +# run passed the mission SLA for the tested target: on every node, +# ledger.age.closed-histogram P75 was within the configured band around T and +# P99 was <= 2*T, and the network stayed synced/consistent. +# - Because the search range is narrow, interpret a successful result as "the +# image passed this workload at the target close time", not as a precise +# minimum block-time measurement. +# - A non-zero exit, "No block time ... satisfied the SLA", loadgen failure, or +# sync/consistency failure means the image/setup did not pass this benchmark +# configuration. +# - With more than one load flag selected, each load is an independent +# benchmark: a failing run does not skip the ones after it. The failed loads +# are listed once every run has finished, and the exit status is non-zero if +# any of them failed. + +IMAGE_REPOSITORY="746476062914.dkr.ecr.us-east-1.amazonaws.com/dev" + +PROJECT="src/App/App.fsproj" +MISSION="MinBlockTimeMixed" +DESTINATION="evaluation" + +NETDELAY_IMAGE="$IMAGE_REPOSITORY/sdf-netdelay:latest" +POSTGRES_IMAGE="$IMAGE_REPOSITORY/postgres:9.5.22" +NGINX_IMAGE="$IMAGE_REPOSITORY/nginx:latest" +PROMETHEUS_EXPORTER_IMAGE="$IMAGE_REPOSITORY/stellar-core-prometheus-exporter:latest" + +INGRESS_INTERNAL_DOMAIN="stellar-supercluster.kube001-ssc-eks.services.stellar-ops.com" +AVOID_NODE_LABELS="purpose:ssc" + +CLASSIC_TX_RATE=200 +NUM_PREGENERATED_TXS=1000000 +GENESIS_TEST_ACCOUNT_COUNT=1000000 +SIMULATE_APPLY_WEIGHT=100 + +# Matches searchThresholdMs in src/FSLibrary/MinBlockTimeTest.fs. Searching +# [T - BLOCK_TIME_BAND_MS, T + BLOCK_TIME_BAND_MS] leaves the mission's binary +# search exactly one candidate to evaluate, T, rather than measuring a true +# minimum. +BLOCK_TIME_BAND_MS=100 + +# Presets a wrapper may override before it parses arguments, plus the values +# base_parse_arg fills in from the shared flags. +DATA_ROOT="$(pwd)" +STELLAR_CORE_IMAGE= +SAC_TX_RATE= +OZ_TX_RATE= +SOROSWAP_TX_RATE= +BLOCK_TIME_MS=5000 +NETWORK_SIZE_LIMIT=277 +PUBNET_DATA_FILE= + +# Total milliseconds of synthetic apply sleep to aim for per ledger. Divided by +# the per-ledger operation count to get the per-operation microseconds the +# mission actually takes. +SIMULATE_APPLY_BUDGET_MS=600 + +# Cluster-external hostname the driver connects to. Empty means "let the +# mission fall back to the route hostname it derives from +# INGRESS_INTERNAL_DOMAIN". +INGRESS_EXTERNAL_HOST= + +# Reports a usage error against the calling wrapper's usage(), and exits. +die() { + printf '%s\n' "$1" >&2 + usage >&2 + exit 1 +} + +# Reports an error that the usage text would not help with, and exits. +fail() { + printf '%s\n' "$1" >&2 + exit 1 +} + +is_nonnegative_integer() { + case "$1" in + "" | *[!0-9]*) + return 1 + ;; + *) + return 0 + ;; + esac +} + +# Parses the shared option at "$1", if it is one. Call as +# `base_parse_arg "$@"`: a function cannot shift its caller's arguments, so +# BASE_PARSE_SHIFT reports how many were consumed instead. A shift count of 0 +# means "$1" is not a shared option, and the caller must handle or reject it. +base_parse_arg() { + BASE_PARSE_SHIFT=2 + + case "$1" in + --stellar-core-image | --image) + if [ "$#" -lt 2 ] || [ -z "$2" ]; then + die "$1 requires an image." + fi + STELLAR_CORE_IMAGE="$2" + ;; + --stellar-core-image=* | --image=*) + STELLAR_CORE_IMAGE="${1#*=}" + BASE_PARSE_SHIFT=1 + ;; + --data-root | --supercluster-root) + if [ "$#" -lt 2 ] || [ -z "$2" ]; then + die "$1 requires a path." + fi + DATA_ROOT="$2" + ;; + --data-root=* | --supercluster-root=*) + DATA_ROOT="${1#*=}" + BASE_PARSE_SHIFT=1 + ;; + --ingress-external-host) + if [ "$#" -lt 2 ] || [ -z "$2" ]; then + die "$1 requires a hostname." + fi + INGRESS_EXTERNAL_HOST="$2" + ;; + --ingress-external-host=*) + INGRESS_EXTERNAL_HOST="${1#*=}" + # An empty value would silently fall back to the derived route + # hostname, so reject it like the two-token form does. + if [ -z "$INGRESS_EXTERNAL_HOST" ]; then + die "--ingress-external-host requires a hostname." + fi + BASE_PARSE_SHIFT=1 + ;; + --sac) + if [ "$#" -lt 2 ] || [ -z "$2" ]; then + die "--sac requires a Soroban tx rate." + fi + SAC_TX_RATE="$2" + ;; + --sac=*) + SAC_TX_RATE="${1#*=}" + BASE_PARSE_SHIFT=1 + ;; + --oz) + if [ "$#" -lt 2 ] || [ -z "$2" ]; then + die "--oz requires a Soroban tx rate." + fi + OZ_TX_RATE="$2" + ;; + --oz=*) + OZ_TX_RATE="${1#*=}" + BASE_PARSE_SHIFT=1 + ;; + --soroswap) + if [ "$#" -lt 2 ] || [ -z "$2" ]; then + die "--soroswap requires a Soroban tx rate." + fi + SOROSWAP_TX_RATE="$2" + ;; + --soroswap=*) + SOROSWAP_TX_RATE="${1#*=}" + BASE_PARSE_SHIFT=1 + ;; + -h | --help) + usage + exit 0 + ;; + *) + BASE_PARSE_SHIFT=0 + ;; + esac +} + +validate_tx_rate() { + flag="${1:?usage: validate_tx_rate FLAG RATE}" + tx_rate="${2:?usage: validate_tx_rate FLAG RATE}" + + if ! is_nonnegative_integer "$tx_rate"; then + fail "$flag rate must be a non-negative integer." + fi +} + +base_validate_args() { + if [ -z "$STELLAR_CORE_IMAGE" ]; then + die "A Stellar Core image is required." + fi + + if [ -z "$SAC_TX_RATE" ] && [ -z "$OZ_TX_RATE" ] && [ -z "$SOROSWAP_TX_RATE" ]; then + die "At least one load flag is required: --sac, --oz, or --soroswap." + fi + + if [ -n "$SAC_TX_RATE" ]; then + validate_tx_rate "--sac" "$SAC_TX_RATE" + fi + + if [ -n "$OZ_TX_RATE" ]; then + validate_tx_rate "--oz" "$OZ_TX_RATE" + fi + + if [ -n "$SOROSWAP_TX_RATE" ]; then + validate_tx_rate "--soroswap" "$SOROSWAP_TX_RATE" + fi + + # The band is subtracted from the block time to get the search floor, so a + # block time at or below it would produce a non-positive lower bound. + if ! is_nonnegative_integer "$BLOCK_TIME_MS" || [ "$BLOCK_TIME_MS" -le "$BLOCK_TIME_BAND_MS" ]; then + fail "Block time must be an integer greater than ${BLOCK_TIME_BAND_MS}ms." + fi + + if ! is_nonnegative_integer "$SIMULATE_APPLY_BUDGET_MS"; then + fail "Simulated apply budget must be a non-negative integer." + fi + + # Non-numeric values make "[" exit 2 rather than 1, so test for the + # accepted range and negate: anything unparseable is rejected too. + if ! [ "$NETWORK_SIZE_LIMIT" -ge 1 ] 2>/dev/null; then + fail "Network size limit must be a positive integer." + fi +} + +# Fills in everything derived from values a wrapper or the command line may +# have changed, so this must run after argument parsing. +base_resolve_derived() { + if [ -z "$PUBNET_DATA_FILE" ]; then + fail "No pubnet data set is configured." + fi + + PUBNET_DATA="$DATA_ROOT/data/$PUBNET_DATA_FILE" + TIER1_KEYS="$DATA_ROOT/data/tier1keys.json" + MIN_BLOCK_TIME_MS=$((BLOCK_TIME_MS - BLOCK_TIME_BAND_MS)) + MAX_BLOCK_TIME_MS=$((BLOCK_TIME_MS + BLOCK_TIME_BAND_MS)) +} + +calculate_simulate_apply_duration() { + classic_tx_rate="${1:?usage: calculate_simulate_apply_duration CLASSIC_TX_RATE SOROBAN_TX_RATE}" + soroban_tx_rate="${2:?usage: calculate_simulate_apply_duration CLASSIC_TX_RATE SOROBAN_TX_RATE}" + + if ! is_nonnegative_integer "$classic_tx_rate" || ! is_nonnegative_integer "$soroban_tx_rate"; then + printf '%s\n' "Tx rates must be non-negative integers." >&2 + return 1 + fi + + total_tx_rate=$((classic_tx_rate + soroban_tx_rate)) + if [ "$total_tx_rate" -eq 0 ]; then + printf '%s\n' "Total tx rate must be greater than zero." >&2 + return 1 + fi + + simulate_apply_duration=$((SIMULATE_APPLY_BUDGET_MS * 1000000 / (total_tx_rate * BLOCK_TIME_MS))) + + # The division truncates, so a budget that is small next to the per-ledger + # operation count would quietly disable the synthetic apply sleep instead + # of shortening it. A zero budget asks for exactly that, so reject only the + # case where a budget that was asked for rounds away. + if [ "$SIMULATE_APPLY_BUDGET_MS" -ne 0 ] && [ "$simulate_apply_duration" -eq 0 ]; then + printf '%s\n' "An apply budget of ${SIMULATE_APPLY_BUDGET_MS}ms rounds down to 0us per operation at $total_tx_rate TPS and a ${BLOCK_TIME_MS}ms close time; raise the budget." >&2 + return 1 + fi + + printf '%s\n' "$simulate_apply_duration" +} + +resolve_min_block_time_mixed_mode() { + case "$1" in + sac | mixed_pregen_sac_payment) + printf '%s\n' "mixed_pregen_sac_payment" + ;; + oz | mixed_pregen_oz_token_transfer) + printf '%s\n' "mixed_pregen_oz_token_transfer" + ;; + soroswap | mixed_pregen_soroswap_swap) + printf '%s\n' "mixed_pregen_soroswap_swap" + ;; + *) + printf '%s\n' "Unsupported mode '$1'. Use one of: sac, oz, soroswap." >&2 + return 1 + ;; + esac +} + +run_min_block_time_mixed() { + mode_alias="${1:?usage: run_min_block_time_mixed MODE SOROBAN_TX_RATE [MISSION_ARG...]}" + soroban_tx_rate="${2:?usage: run_min_block_time_mixed MODE SOROBAN_TX_RATE [MISSION_ARG...]}" + shift 2 + # Both derivations report their own error, so fail the load rather than + # handing dotnet an empty value. + min_block_time_mixed_mode="$(resolve_min_block_time_mixed_mode "$mode_alias")" || return 1 + simulate_apply_duration="$(calculate_simulate_apply_duration "$CLASSIC_TX_RATE" "$soroban_tx_rate")" || return 1 + + if [ -n "$INGRESS_EXTERNAL_HOST" ]; then + set -- --ingress-external-host "$INGRESS_EXTERNAL_HOST" "$@" + fi + + dotnet run \ + --project "$PROJECT" \ + mission "$MISSION" \ + --destination "$DESTINATION" \ + --image="$STELLAR_CORE_IMAGE" \ + --netdelay-image="$NETDELAY_IMAGE" \ + --postgres-image="$POSTGRES_IMAGE" \ + --nginx-image="$NGINX_IMAGE" \ + --prometheus-exporter-image="$PROMETHEUS_EXPORTER_IMAGE" \ + --ingress-internal-domain="$INGRESS_INTERNAL_DOMAIN" \ + --avoid-node-labels="$AVOID_NODE_LABELS" \ + --export-to-prometheus \ + --enable-relaxed-auto-qset-config \ + --classic-tx-rate="$CLASSIC_TX_RATE" \ + --soroban-tx-rate="$soroban_tx_rate" \ + --min-block-time-mixed-mode="$min_block_time_mixed_mode" \ + --min-block-time-ms="$MIN_BLOCK_TIME_MS" \ + --max-block-time-ms="$MAX_BLOCK_TIME_MS" \ + --num-pregenerated-txs="$NUM_PREGENERATED_TXS" \ + --genesis-test-account-count="$GENESIS_TEST_ACCOUNT_COUNT" \ + --simulate-apply-weight "$SIMULATE_APPLY_WEIGHT" \ + --simulate-apply-duration "$simulate_apply_duration" \ + --pubnet-data "$PUBNET_DATA" \ + --tier1-keys "$TIER1_KEYS" \ + --network-size-limit "$NETWORK_SIZE_LIMIT" \ + --require-node-labels=purpose:largetests \ + --tolerate-node-taints=largetests \ + "$@" +} + +# Runs the mission for one load flag, if that flag was selected. Records the +# flag in BASE_FAILED_LOADS when the run fails, so that a failure neither +# aborts the loads that follow nor goes unreported. +base_run_one_load() { + load_alias="$1" + load_tx_rate="$2" + shift 2 + + if [ -z "$load_tx_rate" ]; then + return 0 + fi + + run_min_block_time_mixed "$load_alias" "$load_tx_rate" "$@" + load_status=$? + + if [ "$load_status" -ne 0 ]; then + printf '%s\n' "Load --$load_alias exited with status $load_status." >&2 + BASE_FAILED_LOADS="$BASE_FAILED_LOADS --$load_alias" + fi +} + +# Runs one mission per selected load flag, appending any arguments given here +# to every mission command line. Returns non-zero if any of the runs failed. +base_run_selected_loads() { + BASE_FAILED_LOADS= + + base_run_one_load sac "$SAC_TX_RATE" "$@" + base_run_one_load oz "$OZ_TX_RATE" "$@" + base_run_one_load soroswap "$SOROSWAP_TX_RATE" "$@" + + if [ -n "$BASE_FAILED_LOADS" ]; then + printf '%s\n' "Failed loads:$BASE_FAILED_LOADS" >&2 + return 1 + fi + + return 0 +} diff --git a/scripts/measure_e2e.sh b/scripts/measure_e2e.sh index a419def5..a100aa94 100644 --- a/scripts/measure_e2e.sh +++ b/scripts/measure_e2e.sh @@ -2,52 +2,239 @@ # SLP transaction end-to-end latency measurement wrapper. # -# Usage: sh scripts/measure_e2e.sh [SLP_EVAL_OPTION...] [-- MISSION_ARG...] +# This script runs the MinBlockTimeMixed mission with stellar-core's loadgen +# end-to-end latency metrics enabled, against the 2026-06-03 pubnet topology +# scaled out to 1000 nodes. It answers "how long does a transaction take to go +# from submission to application?" rather than "what is the minimum block time?" # -# A preset over scripts/slp_eval.sh that sets the options needed to measure -# tx end-to-end latency. +# It differs from scripts/slp_eval.sh in that it uses a much larger network, a +# dedicated set of load-generating nodes, and exposes the knobs worth sweeping +# for a latency study: tier1 org count, ledger close time, and the synthetic +# apply budget. +# +# The benchmark setup and how to read the result are documented in +# scripts/base.sh, which holds the shared implementation. -SLP_EVAL="$(dirname "$0")/slp_eval.sh" +. "$(dirname "$0")/base.sh" NETWORK_SIZE_LIMIT=1000 PUBNET_DATA_FILE="public-network-data-2026-06-03-trimmed-located.json" LOADGEN_KEYS_FILE="public-network-data-2026-06-03-loadgenkeys.json" -# Sets DATA_ROOT and SEPARATOR_GIVEN (whether the user supplied "--" in the -# arguments). Shifts its own copy of the arguments, leaving the caller's "$@" -# intact. -scan_args() { - DATA_ROOT="$(pwd)" - SEPARATOR_GIVEN=0 +# Validators per tier1 org. Matches tier1OrgSize in +# src/FSLibrary/StellarNetworkData.fs, which is what --tier-1-orgs-to-add +# multiplies by. +TIER1_ORG_SIZE=3 + +TIER1_ORG_COUNT= + +# usage() expands its heredoc at call time, and die() prints it after argument +# parsing, so the values it reports as defaults have to be captured before +# parse_args can overwrite them. +DEFAULT_BLOCK_TIME_MS="$BLOCK_TIME_MS" +DEFAULT_SIMULATE_APPLY_BUDGET_MS="$SIMULATE_APPLY_BUDGET_MS" + +usage() { + cat <&2 + return 1 + fi + + # Splitting on commas puts one JSON entry per line, so this counts keys + # whether the file is pretty-printed or minified. + key_count="$(tr ',' '\n' <"$TIER1_KEYS" | grep -c 'publicKey')" + + # grep reports 0 both for a file this does not know how to read and for one + # that really is empty. Either way there is no org count to infer, and + # treating it as zero would add the full --tier1s count on top of whatever + # the file actually holds. + if [ "$key_count" -eq 0 ]; then + printf '%s\n' "$TIER1_KEYS holds no publicKey entries; cannot infer an org count for --tier1s." >&2 + return 1 + fi -exec sh "$SLP_EVAL" \ - --network-size-limit "$NETWORK_SIZE_LIMIT" \ - --pubnet-data "$DATA_ROOT/data/$PUBNET_DATA_FILE" \ - "$@" \ + if [ "$((key_count % TIER1_ORG_SIZE))" -ne 0 ]; then + printf '%s\n' "$TIER1_KEYS holds $key_count keys, which is not a multiple of $TIER1_ORG_SIZE; cannot infer an org count for --tier1s." >&2 + return 1 + fi + + existing=$((key_count / TIER1_ORG_SIZE)) + + if [ "$desired" -lt "$existing" ]; then + printf '%s\n' "--tier1s $desired is below the $existing orgs already in $TIER1_KEYS; orgs can only be added." >&2 + return 1 + fi + + printf '%s\n' "$((desired - existing))" +} + +parse_args "$@" +shift "$PARSED_ARG_COUNT" +base_validate_args +validate_args +base_resolve_derived + +# The mission requires --loadgen-keys for --measure-e2e-latency, and +# --pubnet-data for --loadgen-keys; base.sh always supplies the last of those. +set -- "$@" \ --loadgen-keys "$DATA_ROOT/data/$LOADGEN_KEYS_FILE" \ --measure-e2e-latency + +if [ -n "$TIER1_ORG_COUNT" ]; then + tier1_orgs_to_add="$(resolve_tier1_orgs_to_add "$TIER1_ORG_COUNT")" || exit 1 + set -- "$@" --tier-1-orgs-to-add "$tier1_orgs_to_add" +fi + +base_run_selected_loads "$@" diff --git a/scripts/slp_eval.sh b/scripts/slp_eval.sh index 2df8bdab..8948c6c9 100644 --- a/scripts/slp_eval.sh +++ b/scripts/slp_eval.sh @@ -3,82 +3,22 @@ # SLP mixed-load evaluation wrapper. # # This script runs the MinBlockTimeMixed mission against a stellar-core image -# using the 2026-06-03 pubnet topology data (by default; see --pubnet-data) and -# the fixed benchmark parameters below. It is intended to answer: "does this -# image sustain the selected mixed classic/Soroban load at the normal 5s ledger -# close target?" +# using the 2026-06-03 pubnet topology data and the fixed benchmark parameters +# below. It is intended to answer: "does this image sustain the selected mixed +# classic/Soroban load at the normal 5s ledger close target?" # -# Benchmark setup: -# - One mission run is started for each selected Soroban load flag: -# --sac, --oz, and/or --soroswap. Passing multiple flags runs them -# sequentially, not as one combined Soroban workload. -# - Every run includes CLASSIC_TX_RATE pre-generated classic payment TPS to -# match current network conditions. The flag value supplies only the Soroban -# TPS for that run, so total TPS is CLASSIC_TX_RATE + selected Soroban TPS. -# - The mission uses MinBlockTimeMixed's MIXED_PREGEN_* overlay-only loadgen -# mode, simulated pubnet network delay, with a configurable number of nodes -# (--network-size-limit, default 277). -# - The block-time search range is intentionally narrow: -# [MIN_BLOCK_TIME_MS, MAX_BLOCK_TIME_MS] = [4900, 5100]. With the mission's -# 100ms binary-search threshold, this effectively evaluates the 5s target -# to match the current network close time. -# - simulate-apply-duration is derived from SIMULATE_APPLY_BUDGET_MS and the -# total TPS so the synthetic apply sleep budget remains roughly constant as -# the requested Soroban rate changes. -# - Arguments after a trailing "--" are appended verbatim to every mission -# command line, so one-off flags can be added without editing this script. -# scripts/measure_e2e.sh is a preset built on top of that mechanism. -# -# Result interpretation: -# - A zero exit and a "Minimum sustainable block time: ..." log line means the -# run passed the mission SLA for the tested target: on every node, -# ledger.age.closed-histogram P75 was within the configured band around T and -# P99 was <= 2*T, and the network stayed synced/consistent. -# - Because this wrapper uses a narrow range around 5s, interpret a successful -# result as "the image passed this workload at the normal 5s close time", not -# as a precise minimum block-time measurement. -# - A non-zero exit, "No block time ... satisfied the SLA", loadgen failure, or -# sync/consistency failure means the image/setup did not pass this benchmark -# configuration. - -DATA_ROOT="$(pwd)" -STELLAR_CORE_IMAGE= -SAC_TX_RATE= -OZ_TX_RATE= -SOROSWAP_TX_RATE= -PUBNET_DATA= - -IMAGE_REPOSITORY="746476062914.dkr.ecr.us-east-1.amazonaws.com/dev" - -PROJECT="src/App/App.fsproj" -MISSION="MinBlockTimeMixed" -DESTINATION="evaluation" - -NETDELAY_IMAGE="$IMAGE_REPOSITORY/sdf-netdelay:latest" -POSTGRES_IMAGE="$IMAGE_REPOSITORY/postgres:9.5.22" -NGINX_IMAGE="$IMAGE_REPOSITORY/nginx:latest" -PROMETHEUS_EXPORTER_IMAGE="$IMAGE_REPOSITORY/stellar-core-prometheus-exporter:latest" +# The benchmark setup and how to read the result are documented in +# scripts/base.sh, which holds the shared implementation. -INGRESS_INTERNAL_DOMAIN="stellar-supercluster.kube001-ssc-eks.services.stellar-ops.com" -AVOID_NODE_LABELS="purpose:ssc" +. "$(dirname "$0")/base.sh" -CLASSIC_TX_RATE=200 -MIN_BLOCK_TIME_MS=4900 -MAX_BLOCK_TIME_MS=5100 -BLOCK_TIME_MS=$(((MIN_BLOCK_TIME_MS + MAX_BLOCK_TIME_MS) / 2)) -NUM_PREGENERATED_TXS=1000000 -GENESIS_TEST_ACCOUNT_COUNT=1000000 -SIMULATE_APPLY_WEIGHT=100 -SIMULATE_APPLY_BUDGET_MS=600 -DEFAULT_NETWORK_SIZE_LIMIT=277 -NETWORK_SIZE_LIMIT="$DEFAULT_NETWORK_SIZE_LIMIT" -DEFAULT_PUBNET_DATA_FILE="public-network-data-2026-06-03-trimmed-located.json" +NETWORK_SIZE_LIMIT=277 +PUBNET_DATA_FILE="public-network-data-2026-06-03-trimmed-located.json" usage() { cat </data/${DEFAULT_PUBNET_DATA_FILE}. + --ingress-external-host HOST Cluster-external hostname the driver connects to for + the gateway route. Defaults to the route hostname the + mission derives from the internal domain. --sac RATE Run SAC load with the given Soroban tx rate. Can be supplied with other load flags to run benchmarks sequentially. --oz RATE Run OZ load with the given Soroban tx rate. @@ -99,13 +38,12 @@ Options: --soroswap RATE Run Soroswap load with the given Soroban tx rate. Can be supplied with other load flags to run benchmarks sequentially. -h, --help Show this help. - -- MISSION_ARG... Everything after "--" is appended verbatim to each - mission command line. Use it for one-off flags this - wrapper does not expose. Benchmark constants: Classic TPS: ${CLASSIC_TX_RATE} - Target close time: ${BLOCK_TIME_MS}ms, evaluated via [${MIN_BLOCK_TIME_MS}, ${MAX_BLOCK_TIME_MS}] + Target close time: ${BLOCK_TIME_MS}ms, evaluated via [$((BLOCK_TIME_MS - BLOCK_TIME_BAND_MS)), $((BLOCK_TIME_MS + BLOCK_TIME_BAND_MS))] + Network size limit: ${NETWORK_SIZE_LIMIT} + Data set: data/${PUBNET_DATA_FILE} Results: PASS: command exits 0 and logs "Minimum sustainable block time: ...". @@ -116,276 +54,19 @@ Results: EOF } -is_nonnegative_integer() { - case "$1" in - "" | *[!0-9]*) - return 1 - ;; - *) - return 0 - ;; - esac -} - -# Parses the wrapper's own options and stops at a "--" separator. Sets -# PARSED_ARG_COUNT to the number of arguments consumed (including the -# separator) so the caller can shift them off and keep the mission arguments -# that followed in "$@". parse_args() { - total_arg_count="$#" - while [ "$#" -gt 0 ]; do - case "$1" in - --stellar-core-image | --image) - if [ "$#" -lt 2 ] || [ -z "$2" ]; then - printf '%s\n' "$1 requires an image." >&2 - usage >&2 - exit 1 - fi - STELLAR_CORE_IMAGE="$2" - shift 2 - ;; - --stellar-core-image=* | --image=*) - STELLAR_CORE_IMAGE="${1#*=}" - shift - ;; - --data-root | --supercluster-root) - if [ "$#" -lt 2 ] || [ -z "$2" ]; then - printf '%s\n' "$1 requires a path." >&2 - usage >&2 - exit 1 - fi - DATA_ROOT="$2" - shift 2 - ;; - --data-root=* | --supercluster-root=*) - DATA_ROOT="${1#*=}" - shift - ;; - --sac) - if [ "$#" -lt 2 ] || [ -z "$2" ]; then - printf '%s\n' "--sac requires a Soroban tx rate." >&2 - usage >&2 - exit 1 - fi - SAC_TX_RATE="$2" - shift 2 - ;; - --sac=*) - SAC_TX_RATE="${1#*=}" - shift - ;; - --oz) - if [ "$#" -lt 2 ] || [ -z "$2" ]; then - printf '%s\n' "--oz requires a Soroban tx rate." >&2 - usage >&2 - exit 1 - fi - OZ_TX_RATE="$2" - shift 2 - ;; - --oz=*) - OZ_TX_RATE="${1#*=}" - shift - ;; - --soroswap) - if [ "$#" -lt 2 ] || [ -z "$2" ]; then - printf '%s\n' "--soroswap requires a Soroban tx rate." >&2 - usage >&2 - exit 1 - fi - SOROSWAP_TX_RATE="$2" - shift 2 - ;; - --soroswap=*) - SOROSWAP_TX_RATE="${1#*=}" - shift - ;; - --network-size-limit) - if [ "$#" -lt 2 ] || [ -z "$2" ]; then - printf '%s\n' "--network-size-limit requires a node count." >&2 - usage >&2 - exit 1 - fi - NETWORK_SIZE_LIMIT="$2" - shift 2 - ;; - --network-size-limit=*) - NETWORK_SIZE_LIMIT="${1#*=}" - shift - ;; - --pubnet-data) - if [ "$#" -lt 2 ] || [ -z "$2" ]; then - printf '%s\n' "--pubnet-data requires a path." >&2 - usage >&2 - exit 1 - fi - PUBNET_DATA="$2" - shift 2 - ;; - --pubnet-data=*) - PUBNET_DATA="${1#*=}" - # An empty value here would silently fall back to the - # default data set, so reject it like the two-token form. - if [ -z "$PUBNET_DATA" ]; then - printf '%s\n' "--pubnet-data requires a path." >&2 - usage >&2 - exit 1 - fi - shift - ;; - --) - shift - break - ;; - -h | --help) - usage - exit 0 - ;; - *) - printf 'Unknown argument: %s\n' "$1" >&2 - usage >&2 - exit 1 - ;; - esac - done - - PARSED_ARG_COUNT=$((total_arg_count - $#)) -} - -validate_tx_rate() { - flag="${1:?usage: validate_tx_rate FLAG RATE}" - tx_rate="${2:?usage: validate_tx_rate FLAG RATE}" - - if ! is_nonnegative_integer "$tx_rate"; then - printf '%s\n' "$flag rate must be a non-negative integer." >&2 - exit 1 - fi -} - -validate_args() { - if [ -z "$STELLAR_CORE_IMAGE" ]; then - printf '%s\n' "A Stellar Core image is required." >&2 - usage >&2 - exit 1 - fi - - if [ -z "$SAC_TX_RATE" ] && [ -z "$OZ_TX_RATE" ] && [ -z "$SOROSWAP_TX_RATE" ]; then - printf '%s\n' "At least one load flag is required: --sac, --oz, or --soroswap." >&2 - usage >&2 - exit 1 - fi - - if [ -n "$SAC_TX_RATE" ]; then - validate_tx_rate "--sac" "$SAC_TX_RATE" - fi + base_parse_arg "$@" - if [ -n "$OZ_TX_RATE" ]; then - validate_tx_rate "--oz" "$OZ_TX_RATE" - fi + if [ "$BASE_PARSE_SHIFT" -eq 0 ]; then + die "Unknown argument: $1" + fi - if [ -n "$SOROSWAP_TX_RATE" ]; then - validate_tx_rate "--soroswap" "$SOROSWAP_TX_RATE" - fi - - # Non-numeric values make "[" exit 2 rather than 1, so test for the - # accepted range and negate: anything unparseable is rejected too. - if ! [ "$NETWORK_SIZE_LIMIT" -ge 1 ] 2>/dev/null; then - printf '%s\n' "--network-size-limit must be a positive integer." >&2 - exit 1 - fi -} - -calculate_simulate_apply_duration() { - classic_tx_rate="${1:?usage: calculate_simulate_apply_duration CLASSIC_TX_RATE SOROBAN_TX_RATE}" - soroban_tx_rate="${2:?usage: calculate_simulate_apply_duration CLASSIC_TX_RATE SOROBAN_TX_RATE}" - - if ! is_nonnegative_integer "$classic_tx_rate" || ! is_nonnegative_integer "$soroban_tx_rate"; then - printf '%s\n' "Tx rates must be non-negative integers." >&2 - return 1 - fi - - total_tx_rate=$((classic_tx_rate + soroban_tx_rate)) - if [ "$total_tx_rate" -eq 0 ]; then - printf '%s\n' "Total tx rate must be greater than zero." >&2 - return 1 - fi - - printf '%s\n' "$((SIMULATE_APPLY_BUDGET_MS * 1000000 / (total_tx_rate * BLOCK_TIME_MS)))" -} - -resolve_min_block_time_mixed_mode() { - case "$1" in - sac | mixed_pregen_sac_payment) - printf '%s\n' "mixed_pregen_sac_payment" - ;; - oz | mixed_pregen_oz_token_transfer) - printf '%s\n' "mixed_pregen_oz_token_transfer" - ;; - soroswap | mixed_pregen_soroswap_swap) - printf '%s\n' "mixed_pregen_soroswap_swap" - ;; - *) - printf '%s\n' "Unsupported mode '$1'. Use one of: sac, oz, soroswap." >&2 - return 1 - ;; - esac -} - -run_min_block_time_mixed() { - mode_alias="${1:?usage: run_min_block_time_mixed MODE SOROBAN_TX_RATE [MISSION_ARG...]}" - soroban_tx_rate="${2:?usage: run_min_block_time_mixed MODE SOROBAN_TX_RATE [MISSION_ARG...]}" - shift 2 - min_block_time_mixed_mode="$(resolve_min_block_time_mixed_mode "$mode_alias")" - simulate_apply_duration="$(calculate_simulate_apply_duration "$CLASSIC_TX_RATE" "$soroban_tx_rate")" - - dotnet run \ - --project "$PROJECT" \ - mission "$MISSION" \ - --destination "$DESTINATION" \ - --image="$STELLAR_CORE_IMAGE" \ - --netdelay-image="$NETDELAY_IMAGE" \ - --postgres-image="$POSTGRES_IMAGE" \ - --nginx-image="$NGINX_IMAGE" \ - --prometheus-exporter-image="$PROMETHEUS_EXPORTER_IMAGE" \ - --ingress-internal-domain="$INGRESS_INTERNAL_DOMAIN" \ - --avoid-node-labels="$AVOID_NODE_LABELS" \ - --export-to-prometheus \ - --classic-tx-rate="$CLASSIC_TX_RATE" \ - --soroban-tx-rate="$soroban_tx_rate" \ - --min-block-time-mixed-mode="$min_block_time_mixed_mode" \ - --min-block-time-ms="$MIN_BLOCK_TIME_MS" \ - --max-block-time-ms="$MAX_BLOCK_TIME_MS" \ - --num-pregenerated-txs="$NUM_PREGENERATED_TXS" \ - --genesis-test-account-count="$GENESIS_TEST_ACCOUNT_COUNT" \ - --simulate-apply-weight "$SIMULATE_APPLY_WEIGHT" \ - --simulate-apply-duration "$simulate_apply_duration" \ - --pubnet-data "$PUBNET_DATA" \ - --tier1-keys "$TIER1_KEYS" \ - --network-size-limit "$NETWORK_SIZE_LIMIT" \ - --require-node-labels=purpose:largetests \ - --tolerate-node-taints=largetests \ - "$@" + shift "$BASE_PARSE_SHIFT" + done } parse_args "$@" -shift "$PARSED_ARG_COUNT" -validate_args - -if [ -z "$PUBNET_DATA" ]; then - PUBNET_DATA="$DATA_ROOT/data/$DEFAULT_PUBNET_DATA_FILE" -fi - -TIER1_KEYS="$DATA_ROOT/data/tier1keys.json" - -if [ -n "$SAC_TX_RATE" ]; then - run_min_block_time_mixed sac "$SAC_TX_RATE" "$@" -fi - -if [ -n "$OZ_TX_RATE" ]; then - run_min_block_time_mixed oz "$OZ_TX_RATE" "$@" -fi - -if [ -n "$SOROSWAP_TX_RATE" ]; then - run_min_block_time_mixed soroswap "$SOROSWAP_TX_RATE" "$@" -fi +base_validate_args +base_resolve_derived +base_run_selected_loads