Skip to content

Enforce configurable maximum ledger transaction size - #7992

Open
Amaury Chamayou (achamayou) with Copilot wants to merge 29 commits into
mainfrom
copilot/enforce-configurable-max-transaction-size
Open

Enforce configurable maximum ledger transaction size#7992
Amaury Chamayou (achamayou) with Copilot wants to merge 29 commits into
mainfrom
copilot/enforce-configurable-max-transaction-size

Conversation

Copilot AI commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Closes #7488.

Ledger entries store their body size in a 6-byte field, with no further bound, so malformed input could trigger an out-of-bounds read or an excessive allocation. This adds a configurable write-time bound and rejects oversized transactions before they mutate the KV store.

This also changes what an excessively large transaction does to a node. Previously it was fatal, which the batched stress test asserted by running the node to destruction; it is now rejected cleanly while the node stays up and keeps serving.

Configuration

New ledger.max_transaction_size, defaulting to 32MB:

"ledger": {
  "chunk_size": "5MB",
  "max_transaction_size": "32MB"
}

The limit covers the complete ledger entry: the fixed 8-byte entry header, ledger encryption header, public-domain size field, public domain and encrypted private domain. Size strings now use checked integer scaling, so values that cannot fit in size_t fail deterministically during configuration parsing.

memory.max_msg_size keeps its existing 64MB default. A maximum-sized transaction must still fit in a ring-buffer ledger-range response alongside its metadata, and a 32MB transaction limit leaves ample headroom for that, so no change to the ring buffer is needed. Startup validation rejects custom configurations unless ledger.max_transaction_size is at least 2048 bytes smaller than memory.max_msg_size. That validation also runs under --config ... --check, so operators verifying a configuration file are told about a mismatch instead of discovering it when the node starts for real.

Reserved transactions are used solely for internal signatures and remain deliberately exempt: they must fill their pre-reserved ledger version, so rejecting one would create a permanent sequence hole. Snapshots are also exempt because they represent accumulated state rather than a single transaction.

Enforcement

Writes. Before apply_changes(), each non-reserved transaction is walked with a SizeWriter that performs no allocation or byte copying. It uses the same per-field size implementation as RawWriter, includes encryption and fixed-entry overhead, and rejects entries above the configured maximum with 413 / TransactionTooLarge while the store is unchanged.

Conflict detection then runs normally. Conflicting transactions discard only the scalar size accounting; they never materialise serialised public/private domains. After a successful apply assigns the transaction version, the write set is serialised and encrypted once with the final version and commit-evidence digest. Debug builds assert that the projected and actual entry sizes agree, while the final serialiser check remains a fatal safeguard for post-apply invariant failures.

get_header_length() is intentionally not cached. After separating sizing from serialisation it is called once by each short-lived wrapper, so caching offers no meaningful hot-path benefit and would introduce state whose validity depends on the encryptor implementation.

TransactionTooLarge is distinct from the HTTP parser's RequestBodyTooLarge, which also uses status 413 but closes the session.

Reads. LedgerEnclave::get_entry and skip_entry share bounds validation for both the fixed header and claimed body size. Malformed new or duplicate AppendEntries are rejected with std::logic_error, which Raft catches and answers with a NACK, rather than reading beyond the supplied buffer or allocating from an untrusted size.

Deserialisation, historical queries and snapshots are not subject to the configured transaction limit. Entries written under a larger or previously unset limit therefore remain readable during recovery and historical access.

Testing

  • Unit coverage for exact public and encrypted-private entry boundaries, RawWriter/SizeWriter agreement, reserved-signature and snapshot exemptions, deserialisation exemption, commit-evidence metadata, checked size-string overflow, and unrepresentable configured values.
  • Ledger parser coverage for truncated headers and claimed bodies larger than the supplied buffer on both read and skip paths.
  • E2e coverage in tests/limits.py asserts TransactionTooLarge, keeps the payload below max_http_body_size, and confirms rejection writes nothing and does not prevent later transactions.
  • The batched JS stress test replaces its previous run-to-destruction workflow: it now verifies that an oversized write is rejected with TransactionTooLarge, all nodes remain alive, and a subsequent transaction commits successfully.
  • run_configuration_file_checks exercises the shipped sample configurations through --check, which now covers the transaction-size/ring-buffer validation as well.

Co-authored-by: achamayou <4016369+achamayou@users.noreply.github.com>
Copilot AI changed the title [WIP] Add configurable maximum transaction size enforcement Enforce configurable maximum ledger transaction size Jun 27, 2026
…nsaction_size_limit e2e test

- clang-tidy (modernize-use-nodiscard) required get_max_transaction_size()
  to be marked [[nodiscard]], matching the convention already used for
  other const getters in AbstractStore.
- The transaction_size_limit e2e test configured ledger.max_transaction_size
  to 20KB before starting the network, which is smaller than the
  constitution scripts written to the KV store during service creation,
  causing genesis to fail. Raise the configured limit to 512KB (comfortably
  above the genesis transaction size) and the oversized test payload to 1MB
  so the 413 path is still exercised.
@achamayou

Copy link
Copy Markdown
Member

Copilot look at the CI failure during documentation build, and make the necessary fix.

Co-authored-by: achamayou <4016369+achamayou@users.noreply.github.com>

Copilot AI commented Jul 4, 2026

Copy link
Copy Markdown
Contributor Author

Copilot look at the CI failure during documentation build, and make the necessary fix.

Fixed in 80bad36 by documenting the new max_transaction_size parameter that Doxygen reported as missing. I also validated with sphinx-build --fail-on-warning -b html doc /tmp/ccf-doc-html.

@achamayou

Copy link
Copy Markdown
Member

Copilot resolve the merge conflicts in this pull request

…gurable-max-transaction-size

# Conflicts:
#	src/kv/generic_serialise_wrapper.h

Co-authored-by: achamayou <4016369+achamayou@users.noreply.github.com>

Copilot AI commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

Copilot resolve the merge conflicts in this pull request

Resolved the merge conflicts in c068e5f. The PR is no longer reported as dirty; remaining blockers appear to be non-conflict checks/review state.

- Exempt snapshots from the per-transaction limit (serialise and
  deserialise) and add a regression test; snapshots capture whole-store
  state and may legitimately exceed a single-transaction cap.
- Move the CHANGELOG entry from the released 5.0.0 section to 7.0.7
  (Changed), fix 'serialized' -> 'serialised', and reference PR #7992
  instead of tracking issue #7488.
- Rename describe_serialised_entry_size_error to British spelling to
  match the surrounding code.
- Document that historical-query stores deliberately do not apply the
  limit (read-only reconstruction).
- Add a Store::set_max_transaction_size out-of-range validation test.
- Add missing trailing newline to serialised_entry_format.h.
- Exempt all deserialisation from the size cap; only serialisation of new
  transactions is capped. Remove the limit parameter and check from the
  deserialiser init, the abstract deserialiser interface, deserialise_views,
  get_entry, and both recovery loops; drop recovery_store's limit. Buffer-
  bounds safety checks (header size vs buffer, get_entry truncation) remain.
- Apply the cap to the whole serialised ledger entry (fixed 8-byte header
  plus body) instead of just the body.
- Update the error message, CHANGELOG, and host config schema description;
  remove a stale get_entry doc comment.
- Replace the deserialise-rejection unit test with a deserialise-exemption
  test.
…-size

Resolve conflicts in doc/host_config_schema/host_config.json (take main's formatting and re-add max_transaction_size) and src/kv/test/kv_serialisation.cpp (keep both sets of includes).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Enforcing the limit during serialisation meant the transaction had already taken a version and mutated the maps, so it could only be undone by calling Store::rollback. That is unsafe: apply_changes notes that other non-conflicting transactions may commit at later versions concurrently, so rolling back to the pre-apply TxID can discard their changes, and Store::rollback also clears pending_txs and bumps rollback_count.

Project the exact size of the ledger entry before apply_changes instead, by serialising the change sets (which apply_changes does not modify) through a serialiser that only measures them. An oversized transaction is then rejected without the store having been touched, so it neither writes a value nor stops later transactions.

The limit is still checked when serialising, but as a fatal KvSerialiserException, matching how other post-apply serialisation failures are treated.

Use a distinct TransactionTooLarge error code, so the rejection cannot be confused with the HTTP parser's RequestBodyTooLarge, which is also a 413 and closes the session.

Move the CHANGELOG entry to the current version, under Added.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Serialise transaction domains once before applying changes, reuse the retained bytes after assigning the transaction version, and exempt reserved signature transactions from the configurable cap.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a configurable ledger transaction-size limit to prevent oversized allocations and reject excessive writes before store mutation.

Changes:

  • Adds ledger.max_transaction_size, documentation, and TransactionTooLarge.
  • Pre-serialises transactions for size validation while exempting signatures and snapshots.
  • Adds ledger-read bounds checking and unit/e2e coverage.

Custom instructions used

  • .github/copilot-instructions.md
  • .github/instructions/changelog.instructions.md
  • .github/instructions/reviewing.instructions.md

Reviewed changes

Copilot reviewed 22 out of 22 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/limits.py Adds transaction-limit e2e coverage.
tests/infra/network.py Forwards the new test argument.
tests/infra/e2e_args.py Adds the ledger-limit CLI option.
tests/config.jinja Emits the new configuration field.
src/node/rpc/frontend.h Maps oversized transactions to HTTP 413.
src/node/historical_queries.h Documents historical-query exemption.
src/kv/test/kv_snapshot.cpp Tests snapshot exemption.
src/kv/test/kv_serialisation.cpp Tests limits, boundaries, and exemptions.
src/kv/store.h Stores and validates the configured limit.
src/kv/serialised_entry_format.h Defines representable entry limits.
src/kv/raw_serialise.h Adds size estimation and field patching.
src/kv/kv_types.h Adds the exception and store API.
src/kv/generic_serialise_wrapper.h Enforces incremental size accounting.
src/kv/committable_tx.h Pre-serialises before applying writes.
src/enclave/main.cpp Passes configuration into the enclave.
src/enclave/enclave.h Applies the limit to the store.
src/consensus/ledger_enclave.h Validates ledger entry bounds.
src/common/configuration.h Adds JSON serialisation support.
include/ccf/odata_error.h Adds TransactionTooLarge.
include/ccf/node/startup_config.h Defines the 100 MB default.
doc/host_config_schema/host_config.json Documents the configuration schema.
CHANGELOG.md Records the user-facing behavior.
Suppressed comments (1)

src/consensus/ledger_enclave.h:35

  • This validation is bypassed for entries in an already-deserialised prefix: raft.h:1308-1311 calls LedgerEnclave::skip_entry directly and outside the std::logic_error catch around get_entry. skip_entry still trusts the header and throws serialized::InsufficientSpaceException for a truncated body, so malformed duplicate AppendEntries can escape rather than being rejected with a NACK. Apply equivalent validation and exception handling to the skip path.
      // The size in the entry header is not trusted: check it against the
      // buffer we were given before allocating. This is distinct from the
      // configured max_transaction_size, which applies only when serialising
      // new transactions, so that entries written under a larger or unset
      // limit can always be read back.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/consensus/ledger_enclave.h
Comment thread src/enclave/main.cpp
Comment thread src/consensus/ledger_enclave.h Outdated
Cover exact size enforcement with a real encryption header and mixed public/private writes, and satisfy clang-tidy by initializing the serialised version offset in the constructor initializer list.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Reject truncated ledger headers and bodies consistently on read and skip paths, NACK malformed duplicate entries, and use checked integer scaling for size strings.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Measure transaction entries without materialising bytes before conflict detection, serialise only successful writes, align the transaction default to 64MB, and preserve ring-buffer response headroom with a 65MB message default and startup validation.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@achamayou
Amaury Chamayou (achamayou) marked this pull request as ready for review August 13, 2026 17:01
@achamayou
Amaury Chamayou (achamayou) requested a review from a team as a code owner August 13, 2026 17:01
@github-actions

github-actions Bot commented Aug 13, 2026

Copy link
Copy Markdown
Description

Comparing 5 available runs from this branch (#7992) against the trend of the last 30 main runs.

Each chart plots every benchmark as an axis, with values normalized so 100 is the EWMA baseline of recent main runs, using a 7-run half-life. The 5 orange branch lines run from the oldest (faintest) to the latest (darkest and thickest); the darker blue band is the main baseline +/- 1 std dev and the lighter blue band around it is +/- 2 std dev.

Axis labels show the latest branch value and its difference from the main EWMA baseline, where 0% is on the baseline. They are coloured green where the latest run improves on the baseline, red where it regresses, and grey where the difference is within one std dev of the baseline (within noise). Higher is better for throughput and rate, lower for latency and memory.

A benchmark which does not exist on main yet has no baseline of its own, so its earliest available run from this branch is used as its reference and its band is measured across this branch's runs. Its axis is normalized, scaled and coloured like any other, but the comparison is against this branch rather than against main.

Throughput (tx/s)

---
config:
  radar:
    width: 620
    height: 620
    marginTop: 90
    marginRight: 220
    marginBottom: 60
    marginLeft: 220
    axisLabelFactor: 1.12
    curveTension: 0.08
  theme: base
  themeCSS: |
    .radarCurve-0{fill:color-mix(in srgb, #62B5E5 13%, var(--color-canvas-default,var(--bgColor-default,#fff)))!important;fill-opacity:1!important;stroke:none!important;stroke-width:0!important}
    .radarCurve-1{fill:color-mix(in srgb, #62B5E5 40%, var(--color-canvas-default,var(--bgColor-default,#fff)))!important;fill-opacity:1!important;stroke:none!important;stroke-width:0!important}
    .radarCurve-2{fill:color-mix(in srgb, #62B5E5 13%, var(--color-canvas-default,var(--bgColor-default,#fff)))!important;fill-opacity:1!important;stroke:none!important;stroke-width:0!important}
    .radarCurve-3{fill:var(--color-canvas-default,var(--bgColor-default,#fff))!important;fill-opacity:1!important;stroke:none!important;stroke-width:0!important}
    .radarAxisLabel,.radarTitle{fill:var(--color-fg-default,var(--fgColor-default,#111827))!important;color:var(--color-fg-default,var(--fgColor-default,#111827))!important}
    .radarCurve-4{stroke-width:1.5px!important;stroke-opacity:0.40!important}
    .radarCurve-5{stroke-width:1.5px!important;stroke-opacity:0.50!important}
    .radarCurve-6{stroke-width:1.75px!important;stroke-opacity:1.00!important}
    .radarAxisLabel:nth-of-type(1){fill:#808A94!important}
    .radarAxisLabel:nth-of-type(2){fill:#2DA44E!important}
    .radarAxisLabel:nth-of-type(3){fill:#2DA44E!important}
    .radarAxisLabel:nth-of-type(4){fill:#2DA44E!important}
    .radarAxisLabel:nth-of-type(5){fill:#2DA44E!important}
    .radarAxisLabel:nth-of-type(6){fill:#808A94!important}
    .radarAxisLabel:nth-of-type(7){fill:#808A94!important}
    .radarAxisLabel:nth-of-type(8){fill:#808A94!important}
    .radarAxisLabel:nth-of-type(9){fill:#808A94!important}
    .radarAxisLabel:nth-of-type(10){fill:#E5484D!important}
  themeVariables:
    cScale0: "#62B5E5"
    cScale1: "#62B5E5"
    cScale2: "#62B5E5"
    cScale3: "#62B5E5"
    cScale4: "#F97316"
    cScale5: "#F97316"
    cScale6: "#F97316"
    radar:
      axisColor: "#9CA3AF"
      graticuleColor: "#E5E7EB"
      graticuleOpacity: 0
      axisStrokeWidth: 1
      curveOpacity: 0
---
radar-beta
  axis b0["Basic: 71,967 tx/s ▬ +3%"]
  axis b1["Basic Blocking: 1,049 tx/s ▲ 3%"]
  axis b2["Basic Blocking Locust 100ms: 3,059 tx/s ▲ 1%"]
  axis b3["Basic Blocking Locust 20ms: 15,191 tx/s ▲ 1%"]
  axis b4["Basic Blocking Locust 2ms: 32,655 tx/s ▲ 7%"]
  axis b5["Basic JS: 4,714 tx/s ▬ -1%"]
  axis b6["Basic Multi-Threaded: 87,964 tx/s ▬ +2%"]
  axis b7["Historical Queries: 209,377 tx/s ▬ +2%"]
  axis b8["Logging: 66,692 tx/s ▬ +3%"]
  axis b9["Logging JWT: 9,599 tx/s ▼ 5%"]
  curve stddev2_high["main EWMA + 2 std dev"]{108.07, 103.18, 100.78, 100.77, 110.61, 107.24, 106.88, 109.72, 108.74, 106.49}
  curve stddev1_high["main EWMA + 1 std dev"]{104.04, 101.59, 100.39, 100.39, 105.31, 103.62, 103.44, 104.86, 104.37, 103.25}
  curve stddev1_low["main EWMA - 1 std dev"]{95.96, 98.41, 99.61, 99.61, 94.69, 96.38, 96.56, 95.14, 95.63, 96.75}
  curve stddev2_low["main EWMA - 2 std dev"]{91.93, 96.82, 99.22, 99.23, 89.39, 92.76, 93.12, 90.28, 91.26, 93.51}
  curve branch_2["#7992 (2 runs earlier)"]{100.26, 99.65, 100.24, 100.25, 105.40, 100.18, 100.44, 100.35, 100.17, 101.56}
  curve branch_3["#7992 (1 run earlier)"]{102.18, 98.70, 100.43, 100.47, 106.22, 101.42, 99.41, 102.40, 97.35, 99.60}
  curve branch_4["#7992"]{102.94, 102.67, 100.52, 100.60, 106.87, 99.34, 102.03, 101.55, 102.72, 94.59}
  graticule polygon
  max 119
  min 81
  ticks 0
  showLegend false
Loading

Latency (ms)

---
config:
  radar:
    width: 620
    height: 620
    marginTop: 90
    marginRight: 220
    marginBottom: 60
    marginLeft: 220
    axisLabelFactor: 1.12
    curveTension: 0.08
  theme: base
  themeCSS: |
    .radarCurve-0{fill:color-mix(in srgb, #62B5E5 13%, var(--color-canvas-default,var(--bgColor-default,#fff)))!important;fill-opacity:1!important;stroke:none!important;stroke-width:0!important}
    .radarCurve-1{fill:color-mix(in srgb, #62B5E5 40%, var(--color-canvas-default,var(--bgColor-default,#fff)))!important;fill-opacity:1!important;stroke:none!important;stroke-width:0!important}
    .radarCurve-2{fill:color-mix(in srgb, #62B5E5 13%, var(--color-canvas-default,var(--bgColor-default,#fff)))!important;fill-opacity:1!important;stroke:none!important;stroke-width:0!important}
    .radarCurve-3{fill:var(--color-canvas-default,var(--bgColor-default,#fff))!important;fill-opacity:1!important;stroke:none!important;stroke-width:0!important}
    .radarAxisLabel,.radarTitle{fill:var(--color-fg-default,var(--fgColor-default,#111827))!important;color:var(--color-fg-default,var(--fgColor-default,#111827))!important}
    .radarCurve-4{stroke-width:1.5px!important;stroke-opacity:0.40!important}
    .radarCurve-5{stroke-width:1.5px!important;stroke-opacity:0.50!important}
    .radarCurve-6{stroke-width:1.75px!important;stroke-opacity:1.00!important}
    .radarAxisLabel:nth-of-type(1){fill:#808A94!important}
    .radarAxisLabel:nth-of-type(2){fill:#808A94!important}
    .radarAxisLabel:nth-of-type(3){fill:#2DA44E!important}
    .radarAxisLabel:nth-of-type(4){fill:#808A94!important}
    .radarAxisLabel:nth-of-type(5){fill:#808A94!important}
    .radarAxisLabel:nth-of-type(6){fill:#E5484D!important}
  themeVariables:
    cScale0: "#62B5E5"
    cScale1: "#62B5E5"
    cScale2: "#62B5E5"
    cScale3: "#62B5E5"
    cScale4: "#F97316"
    cScale5: "#F97316"
    cScale6: "#F97316"
    radar:
      axisColor: "#9CA3AF"
      graticuleColor: "#E5E7EB"
      graticuleOpacity: 0
      axisStrokeWidth: 1
      curveOpacity: 0
---
radar-beta
  axis b0["Basic Blocking Locust 100ms: 99 ms ▬ 0%"]
  axis b1["Basic Blocking Locust 20ms: 19 ms ▬ 0%"]
  axis b2["Basic Blocking Locust 2ms: 9 ms ▼ 6%"]
  axis b3["Commit Latency 16ms: 4.28 ms ▬ -9%"]
  axis b4["Commit Latency 1ms: 1.97 ms ▬ +1%"]
  axis b5["Commit Latency 256ms: 213 ms ▲ 3%"]
  curve stddev2_high["main EWMA + 2 std dev"]{100.54, 100.00, 111.09, 128.66, 104.90, 102.98}
  curve stddev1_high["main EWMA + 1 std dev"]{100.27, 100.00, 105.55, 114.33, 102.45, 101.49}
  curve stddev1_low["main EWMA - 1 std dev"]{99.73, 100.00, 94.45, 85.67, 97.55, 98.51}
  curve stddev2_low["main EWMA - 2 std dev"]{99.46, 100.00, 88.91, 71.34, 95.10, 97.02}
  curve branch_2["#7992 (2 runs earlier)"]{99.97, 100.00, 93.66, 110.99, 98.99, 98.85}
  curve branch_3["#7992 (1 run earlier)"]{99.97, 100.00, 93.66, 97.72, 102.97, 98.34}
  curve branch_4["#7992"]{99.97, 100.00, 93.66, 91.33, 101.34, 103.33}
  graticule polygon
  max 149
  min 51
  ticks 0
  showLegend false
Loading

Memory (bytes)

---
config:
  radar:
    width: 620
    height: 620
    marginTop: 90
    marginRight: 220
    marginBottom: 60
    marginLeft: 220
    axisLabelFactor: 1.12
    curveTension: 0.08
  theme: base
  themeCSS: |
    .radarCurve-0{fill:color-mix(in srgb, #62B5E5 13%, var(--color-canvas-default,var(--bgColor-default,#fff)))!important;fill-opacity:1!important;stroke:none!important;stroke-width:0!important}
    .radarCurve-1{fill:color-mix(in srgb, #62B5E5 40%, var(--color-canvas-default,var(--bgColor-default,#fff)))!important;fill-opacity:1!important;stroke:none!important;stroke-width:0!important}
    .radarCurve-2{fill:color-mix(in srgb, #62B5E5 13%, var(--color-canvas-default,var(--bgColor-default,#fff)))!important;fill-opacity:1!important;stroke:none!important;stroke-width:0!important}
    .radarCurve-3{fill:var(--color-canvas-default,var(--bgColor-default,#fff))!important;fill-opacity:1!important;stroke:none!important;stroke-width:0!important}
    .radarAxisLabel,.radarTitle{fill:var(--color-fg-default,var(--fgColor-default,#111827))!important;color:var(--color-fg-default,var(--fgColor-default,#111827))!important}
    .radarCurve-4{stroke-width:1.5px!important;stroke-opacity:0.40!important}
    .radarCurve-5{stroke-width:1.5px!important;stroke-opacity:0.50!important}
    .radarCurve-6{stroke-width:1.75px!important;stroke-opacity:1.00!important}
    .radarAxisLabel:nth-of-type(1){fill:#808A94!important}
    .radarAxisLabel:nth-of-type(2){fill:#808A94!important}
    .radarAxisLabel:nth-of-type(3){fill:#808A94!important}
    .radarAxisLabel:nth-of-type(4){fill:#808A94!important}
    .radarAxisLabel:nth-of-type(5){fill:#E5484D!important}
    .radarAxisLabel:nth-of-type(6){fill:#808A94!important}
    .radarAxisLabel:nth-of-type(7){fill:#2DA44E!important}
    .radarAxisLabel:nth-of-type(8){fill:#2DA44E!important}
    .radarAxisLabel:nth-of-type(9){fill:#808A94!important}
  themeVariables:
    cScale0: "#62B5E5"
    cScale1: "#62B5E5"
    cScale2: "#62B5E5"
    cScale3: "#62B5E5"
    cScale4: "#F97316"
    cScale5: "#F97316"
    cScale6: "#F97316"
    radar:
      axisColor: "#9CA3AF"
      graticuleColor: "#E5E7EB"
      graticuleOpacity: 0
      axisStrokeWidth: 1
      curveOpacity: 0
---
radar-beta
  axis b0["Basic: 84.9 MiB ▬ -2%"]
  axis b1["Basic Blocking: 71.6 MiB ▬ 0%"]
  axis b2["Basic Blocking Locust 100ms: 90.7 MiB ▬ 0%"]
  axis b3["Basic Blocking Locust 20ms: 91.1 MiB ▬ +1%"]
  axis b4["Basic Blocking Locust 2ms: 95.1 MiB ▲ 2%"]
  axis b5["Basic JS: 72 MiB ▬ +1%"]
  axis b6["Basic Multi-Threaded: 85.9 MiB ▼ 3%"]
  axis b7["Logging: 75.3 MiB ▼ 1%"]
  axis b8["Logging JWT: 68.5 MiB ▬ 0%"]
  curve stddev2_high["main EWMA + 2 std dev"]{106.58, 100.73, 102.13, 102.38, 101.94, 103.86, 102.86, 101.44, 101.34}
  curve stddev1_high["main EWMA + 1 std dev"]{103.29, 100.36, 101.07, 101.19, 100.97, 101.93, 101.43, 100.72, 100.67}
  curve stddev1_low["main EWMA - 1 std dev"]{96.71, 99.64, 98.93, 98.81, 99.03, 98.07, 98.57, 99.28, 99.33}
  curve stddev2_low["main EWMA - 2 std dev"]{93.42, 99.27, 97.87, 97.62, 98.06, 96.14, 97.14, 98.56, 98.66}
  curve branch_2["#7992 (2 runs earlier)"]{102.29, 99.60, 99.22, 100.29, 100.59, 100.49, 101.23, 100.03, 99.65}
  curve branch_3["#7992 (1 run earlier)"]{103.02, 100.26, 98.43, 103.03, 100.17, 99.08, 101.20, 101.13, 99.06}
  curve branch_4["#7992"]{97.73, 99.83, 99.88, 100.60, 102.32, 101.03, 96.54, 98.80, 99.74}
  graticule polygon
  max 112
  min 88
  ticks 0
  showLegend false
Loading

Rate (ops/s)

---
config:
  radar:
    width: 620
    height: 620
    marginTop: 90
    marginRight: 220
    marginBottom: 60
    marginLeft: 220
    axisLabelFactor: 1.12
    curveTension: 0.08
  theme: base
  themeCSS: |
    .radarCurve-0{fill:color-mix(in srgb, #62B5E5 13%, var(--color-canvas-default,var(--bgColor-default,#fff)))!important;fill-opacity:1!important;stroke:none!important;stroke-width:0!important}
    .radarCurve-1{fill:color-mix(in srgb, #62B5E5 40%, var(--color-canvas-default,var(--bgColor-default,#fff)))!important;fill-opacity:1!important;stroke:none!important;stroke-width:0!important}
    .radarCurve-2{fill:color-mix(in srgb, #62B5E5 13%, var(--color-canvas-default,var(--bgColor-default,#fff)))!important;fill-opacity:1!important;stroke:none!important;stroke-width:0!important}
    .radarCurve-3{fill:var(--color-canvas-default,var(--bgColor-default,#fff))!important;fill-opacity:1!important;stroke:none!important;stroke-width:0!important}
    .radarAxisLabel,.radarTitle{fill:var(--color-fg-default,var(--fgColor-default,#111827))!important;color:var(--color-fg-default,var(--fgColor-default,#111827))!important}
    .radarCurve-4{stroke-width:1.5px!important;stroke-opacity:0.20!important}
    .radarCurve-5{stroke-width:1.5px!important;stroke-opacity:0.30!important}
    .radarCurve-6{stroke-width:1.5px!important;stroke-opacity:0.40!important}
    .radarCurve-7{stroke-width:1.5px!important;stroke-opacity:0.50!important}
    .radarCurve-8{stroke-width:1.75px!important;stroke-opacity:1.00!important}
    .radarAxisLabel:nth-of-type(1){fill:#808A94!important}
    .radarAxisLabel:nth-of-type(2){fill:#808A94!important}
    .radarAxisLabel:nth-of-type(3){fill:#808A94!important}
    .radarAxisLabel:nth-of-type(4){fill:#E5484D!important}
    .radarAxisLabel:nth-of-type(5){fill:#808A94!important}
    .radarAxisLabel:nth-of-type(6){fill:#808A94!important}
  themeVariables:
    cScale0: "#62B5E5"
    cScale1: "#62B5E5"
    cScale2: "#62B5E5"
    cScale3: "#62B5E5"
    cScale4: "#F97316"
    cScale5: "#F97316"
    cScale6: "#F97316"
    cScale7: "#F97316"
    cScale8: "#F97316"
    radar:
      axisColor: "#9CA3AF"
      graticuleColor: "#E5E7EB"
      graticuleOpacity: 0
      axisStrokeWidth: 1
      curveOpacity: 0
---
radar-beta
  axis b0["CHAMP get: 38,122,894 ops/s ▬ +1%"]
  axis b1["CHAMP put: 5,505,687 ops/s ▬ +1%"]
  axis b2["KV deserialisation: 1,634,254 ops/s ▬ +1%"]
  axis b3["KV serialisation: 1,386,770 ops/s ▼ 4%"]
  axis b4["KV s…t deserialisation: 4,219 ops/s ▬ +2%"]
  axis b5["KV snapshot serialisation: 4,614 ops/s ▬ +1%"]
  curve stddev2_high["main EWMA + 2 std dev"]{106.97, 105.91, 106.81, 105.56, 106.24, 109.10}
  curve stddev1_high["main EWMA + 1 std dev"]{103.48, 102.96, 103.40, 102.78, 103.12, 104.55}
  curve stddev1_low["main EWMA - 1 std dev"]{96.52, 97.04, 96.60, 97.22, 96.88, 95.45}
  curve stddev2_low["main EWMA - 2 std dev"]{93.03, 94.09, 93.19, 94.44, 93.76, 90.90}
  curve branch_0["#7992 (4 runs earlier)"]{100.21, 100.85, 102.91, 96.19, 101.96, 94.96}
  curve branch_1["#7992 (3 runs earlier)"]{100.64, 100.24, 101.39, 94.88, 101.96, 92.55}
  curve branch_2["#7992 (2 runs earlier)"]{100.55, 100.30, 103.60, 97.27, 100.07, 97.93}
  curve branch_3["#7992 (1 run earlier)"]{102.33, 100.82, 102.57, 95.53, 100.69, 97.86}
  curve branch_4["#7992"]{100.75, 100.95, 100.89, 96.19, 101.60, 100.58}
  graticule polygon
  max 116
  min 84
  ticks 0
  showLegend false
Loading

Replace the obsolete expectation that an oversized transaction terminates a node with assertions for TransactionTooLarge, continued node health, and a successful subsequent commit.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The 64MB transaction limit required raising memory.max_msg_size to 65MB, so that a maximum-sized entry still fit in a ring-buffer range response alongside its metadata. Halving the transaction limit to 32MB leaves ample headroom, so max_msg_size returns to its previous 64MB default.

Also corrects the config schema, which documented max_transaction_size as 65MB while the code default was 64MB. That documented value would itself have been rejected by the new startup validation, and it now documents the constraint against memory.max_msg_size.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
validate_ledger_transaction_size() ran after the --check early return, so verifying a configuration file reported success even when max_transaction_size left insufficient room in max_msg_size, and the mismatch only surfaced when the node was started for real. Run it before the early return.

validate_and_adjust_recovery_threshold() deliberately stays where it is, since it mutates the configuration and is only meaningful for an actual start.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The entry described memory.max_msg_size as unchanged, which only made sense relative to an intermediate state of this branch where it had been raised. State the constraint instead, name the user-visible error code, and note that oversized transactions previously terminated the node.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Enforce a configurable maximum transaction size

3 participants