Kv: split iterator slot pool into independent primary and secondary pools#330
Merged
Conversation
…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.
huangminghuang
approved these changes
May 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Pre-split,
apply_contextkept 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_createprefix size against an unbounded-allocation vector.Pool split
kv_iterator_slot_commonholds the fields every iterator needs (in_use, status, code, table_id, cached_id).kv_primary_slotadds primary-only byte buffers (prefix,current_key).kv_secondary_slotadds secondary-only fields (current_sec_key,current_pri_key).kv_primary_iterator_poolandkv_secondary_iterator_pooleach expose 1024 slots independently. A contract may now hold up to 1024 primary AND 1024 secondary iterators simultaneously.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:
Bit 16 was chosen over a high bit so handle values stay small and readable in logs (secondary handle
0x00010005vs0x40000005). 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 cleankv_invalid_iteratorexception instead of silently aliasing a real slot.Prefix-size bound on kv_it_create
Cap
prefix_sizeatconfig::max_kv_key_size_limit(1024 B). The prefix bytes arememcpy'd into the iterator slot, so an unboundedprefix_sizewould 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_objectlayout, and serialization are all untouched.