Skip to content

Kv: split iterator slot pool into independent primary and secondary pools#330

Merged
heifner merged 2 commits into
masterfrom
refactor/kv-iterator-pool-split
May 13, 2026
Merged

Kv: split iterator slot pool into independent primary and secondary pools#330
heifner merged 2 commits into
masterfrom
refactor/kv-iterator-pool-split

Conversation

@heifner
Copy link
Copy Markdown
Contributor

@heifner heifner commented May 11, 2026

Summary

Pre-split, apply_context kept a single 1024-slot pool with each slot a union of fields for both iterator kinds. Hot paths loaded cache lines holding data the operation did not touch, and a contract iterating both kinds concurrently competed for the same 1024-slot budget.

This change splits the pool into independent primary and secondary pools with type-specific slot layouts, hardens the contract-visible iterator handle ABI, and caps the kv_it_create prefix size against an unbounded-allocation vector.

Pool split

  • kv_iterator_slot_common holds the fields every iterator needs (in_use, status, code, table_id, cached_id).
  • kv_primary_slot adds primary-only byte buffers (prefix, current_key).
  • kv_secondary_slot adds secondary-only fields (current_sec_key, current_pri_key).
  • kv_primary_iterator_pool and kv_secondary_iterator_pool each expose 1024 slots independently. A contract may now hold up to 1024 primary AND 1024 secondary iterators simultaneously.
  • Both pools lazily resize on first allocate() -- actions that never touch KV iterators (e.g. sysio.token::transfer) pay zero heap for the pools.

Handle encoding (CONSENSUS-OBSERVABLE)

Handle values are contract-visible. Layout:

[ 0.. 9]  slot index (10 bits, covers max_kv_iterators = 1024)
[10..15]  RESERVED -- must be zero
[16    ]  secondary-pool tag (1 = secondary, 0 = primary)
[17..30]  RESERVED -- must be zero
[31    ]  always zero -- keeps handles positive when read as int32_t

Bit 16 was chosen over a high bit so handle values stay small and readable in logs (secondary handle 0x00010005 vs 0x40000005). Reserved bits give future protocol features room to encode e.g. generation counters without disturbing deployed contract code. Any change to this layout is a protocol change.

kv_handle_check_reserved_zero() is called at every host-intrinsic entry point that consumes a handle. A contract that fabricates a handle by setting reserved bits fails with a clean kv_invalid_iterator exception instead of silently aliasing a real slot.

Prefix-size bound on kv_it_create

Cap prefix_size at config::max_kv_key_size_limit (1024 B). The prefix bytes are memcpy'd into the iterator slot, so an unbounded prefix_size would let a contract allocate arbitrary host-side storage per iterator. Legitimate CDT usage passes 0 or 8 bytes, far below the cap.

Capacity

No contract that worked pre-split gets a new error. Some that exhausted the shared 1024-slot pool mid-mixed use now succeed (primary+secondary sum up to 2048).

Host ABI / chainbase

No change. Host intrinsic signatures, kv_object/kv_index_object layout, and serialization are all untouched.

…ools

Pre-split, apply_context kept a single 1024-slot kv_iterator_pool with
each slot a union of every field used by either iterator kind (four
std::vector<char> buffers + an is_primary flag).  Hot paths
(kv_it_next, kv_idx_next) loaded cache lines holding data the
operation never touched, and a contract that iterated both kinds
concurrently competed for the same 1024-slot budget.

Pool split
----------

  * kv_iterator_slot_common holds the fields every iterator needs
    (in_use, status, code, table_id, cached_id).
  * kv_primary_slot adds primary-only byte buffers (prefix,
    current_key).
  * kv_secondary_slot adds secondary-only fields (current_sec_key,
    current_pri_key).
  * kv_primary_iterator_pool and kv_secondary_iterator_pool each
    expose config::max_kv_iterators (1024) slots independently.  A
    contract may now hold up to 1024 primary and 1024 secondary
    iterators simultaneously.
  * Both pools lazily resize on first allocate().  Actions that never
    touch KV iterators (e.g. sysio.token transfer, which routes
    through kv_get/kv_set only) pay zero heap for the pools.  First
    allocate pays the same ~82 KB this code paid up front before;
    no realloc, no reference invalidation, identical get() path.

Handle encoding (CONSENSUS-OBSERVABLE)
--------------------------------------

Handle values are contract-visible -- they are the return value of
kv_it_create / kv_idx_find_secondary / kv_idx_lower_bound, and
contracts may read, store, and branch on them.  The encoding is
part of the consensus surface.

    [ 0.. 9]  slot index (covers max_kv_iterators = 1024)
    [10..15]  RESERVED -- must be zero
    [16    ]  secondary-pool tag (1 = secondary, 0 = primary)
    [17..30]  RESERVED -- must be zero
    [31    ]  always zero -- keeps handles positive when read as
              int32_t (negative is reserved for "not found")

Bit 16 was chosen over a high bit so handle values stay small and
readable in logs/error messages (secondary handle 0x00010005 vs
0x40000005).  Reserved bits give future protocol features room to
encode e.g. iterator generation counters or additional pool
discriminators without disturbing deployed contract code.  Any
change to this layout is a protocol change.

Reserved-bit guard
------------------

kv_handle_check_reserved_zero() is called at every host-intrinsic
entry point that consumes a handle.  A contract that fabricates a
handle by setting reserved bits fails with a clean
kv_invalid_iterator exception instead of silently aliasing a real
slot through the truncated slot-index mask.

Prefix-size bound on kv_it_create
---------------------------------

Cap prefix_size at config::max_kv_key_size_limit (constexpr absolute
ceiling, 1024 B).  The prefix bytes are memcpy'd into the iterator
slot's std::vector<char>, so an unbounded prefix_size would let a
contract allocate arbitrary host-side storage per iterator (up to
max_kv_iterators slots per action).  The absolute ceiling is used
instead of the dynamic max_kv_key_size because the cap exists to
limit host-side slot memory, not to match the current on-chain
stored-key limit, and the constexpr compile-time compare has zero
runtime cost.  Legitimate CDT usage passes 0 or kv_scope_size
(8 bytes), far below the cap.

Capacity change
---------------

No contract that worked pre-split gets a new error.  Some that
exhausted the shared 1024-slot pool mid-mixed use now succeed
(primary+secondary sum up to 2048).

Host ABI / chainbase
--------------------

No change.  Host intrinsic signatures, kv_object/kv_index_object
layout, and serialization are all untouched.
…re tests

- kv_context.hpp: static_assert that config::max_kv_iterators fits in
  kv_handle_slot_index_mask + 1 (catches mask-narrowing footgun if the
  iterator budget grows).

- kv_context.hpp: validate_primary_handle / validate_secondary_handle
  and kv_check_prefix_size moved out of apply_context.cpp into the
  header as inline helpers. Lets the destroy paths share the same
  validation as the other entry points and makes the checks unit-
  testable without spinning up an apply_context.

- apply_context.cpp: kv_it_destroy / kv_idx_destroy now go through
  validate_primary_handle / validate_secondary_handle, matching the
  pattern of every other kv_it_* / kv_idx_* entry. kv_it_create's
  prefix-size cap moves to kv_check_prefix_size.

- kv_tests.cpp:
    * kv_validate_handle_dispatch: end-to-end coverage of the
      validators (well-formed handle, wrong-pool tag, each
      reserved-bit class).
    * kv_check_prefix_size_bounds: at-limit OK, over-limit throws.
    * kv_iterator_pool_basic: drop the implicit assumption that
      primary handles equal slot indices -- compare against
      kv_handle_slot_index(h1) so future encoding changes do not
      break the test.
@heifner heifner requested a review from huangminghuang May 13, 2026 15:07
@heifner heifner merged commit 238c91c into master May 13, 2026
28 checks passed
@heifner heifner deleted the refactor/kv-iterator-pool-split branch May 13, 2026 15:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants