BC-458 · Production-readiness audit

Tighten oversized storage bounds + fixes uncovered along the way

Branch feature/polkadot-v1.6.0-vm-bc458-tighten-bounds · Base feature/polkadot-v1.6.0-vm · 13 commits · 2026-05-18

Several pallet storage bounds were effectively unbounded (u32::MAX, 10_000_000, etc.), which made the benchmark CLI compute multi-megabyte — sometimes ~80 GB — MaxEncodedLen values for storage items. The resulting proof_size weights either over-charged silently or pushed individual extrinsics past the ~5 MB block PoV budget, making them effectively uncallable.

This branch lowers six runtime constants to realistic ceilings, hardens pallet-referral cycle detection to fail closed when the ancestor chain exceeds the depth cap, and regenerates weights for all five affected pallets on a common bench host. Two unrelated production bugs (broken unlock extrinsic on dev/testnet/mainnet, broken benchmark helper) surfaced and were fixed in dedicated commits.

1Runtime constants — what changed and why

All changes live in runtime/src/lib.rs. Why values are paraphrased from the commit bodies; the PoV column shows how each bound's worst-case storage size changed.

Parameter Before After PoV impact Why
pallet_referral MaxChildrenPerNode 10_000_000 200_000 ~200 MB ~4 MB Per-entry ActiveReferralNodes max_size dropped so activate_referral stays under block PoV. Was originally tightened to 50 000, then raised to 200 000 to leave operator headroom while still keeping the dispatch callable.
pallet_reward_pool MaxVectorLength u32::MAX 10_000 ~80 GB bounded Bounds UserUnlockedNfts per user. The u32::MAX ceiling produced a meaningless ~80 GB encoded length that the bench machinery used in proof-size accounting. 10 000 collections per user is a realistic upper bound.
pallet_fractional_nft MaxUnusedIds u32::MAX 1_000_000 overflow ~4 MB UnusedCollectionId and UnusedItemId had a u32-overflowing MaxEncodedLen at the previous bound. Reads of these maps now fit comfortably in block PoV.
pallet_handshake MaxCompletedHandshakes 1_000_000 100_000 ~40 MB ~4 MB With AccountId20, the per-account CompletedHandshakesPerAccount BoundedVec shrinks from ~20 MB to ~2 MB, dropping complete_handshake's worst-case proof_size from ~40 MB to ~4 MB with ~1 MB headroom against MAX_POV_SIZE. 100 000 still leaves a 10× margin over realistic counts.
pallet_reputation_voting MaxVotersPerPoll 1_000_000 100_000 ~21 MB ~210 KB Per-poll voter cap now matches the electorate cap, since electorate size upper-bounds turnout. The doc comment on this constant is preserved (kept in lockstep with the field below).
pallet_reputation_voting ReputationVotingElectorateSize 10_000 100_000 Raised so the electorate cap and MaxVotersPerPoll stay equal. Used by support-percentage calculations; does not retroactively update cached tallies for active polls.

2pallet-referral — cycle detection failing closed

Root cause

walk_ancestors returned Option<R>, conflating "no ancestor found" with "depth cap hit". has_ancestor therefore returned false when traversal exhausted MAX_TREE_DEPTH — and add_referral interpreted !has_ancestor as safe, so a back-edge that would close a cycle against a chain longer than the cap could be accepted silently.

Tightened safety cap

Constant Before After Why
MAX_TREE_DEPTH 1000 200 Defense-in-depth cap reduced from 10× to ~2× the benchmarked range (activate_referral_depth: Linear<0, 100>) plus a small margin above MAX_REFERRAL_DEPTH = 100. The previous envelope let ancestor-walking paths (has_ancestor, add_child, split_tree, collect_reward_recipients) consume up to 10× their charged weight under data corruption or future bound drift.

Return-type rework — WalkOutcome<R>

Before
// silently collapsed two cases:
// - reached root
// - hit MAX_TREE_DEPTH
fn walk_ancestors<R>(...)
    -> Option<R>
After
enum WalkOutcome<R> {
    Found(R),
    Completed,      // reached root
    DepthExceeded,  // cap hit
}

New error — ReferralChainTooDeep

add_referral now calls walk_ancestors_until directly and treats both outcomes as failures: Found → the existing CircularReferralDetected; DepthExceeded → the new ReferralChainTooDeep. Test coverage: test_add_referral_rejects_cycle_past_max_tree_depth builds a pending chain right up to the boundary and verifies both extending it one step further and closing a back-edge to the root fail with the new error.

Active-tree walks (walk_active_ancestors) keep the previous discard-and-log behavior — active chains are bounded by MAX_REFERRAL_DEPTH = 100, so the depth-exceeded branch is unreachable there in normal operation.

3Drive-by fix — pallet_reward_pool::unlock broken in production

Pre-existing production bug — EIP-3607 on the reward-pool account

Commit 699474c (2026-05-01) added a 5-byte precompile bytecode stub at REWARD_POOL_ADDRESS in genesis so OZ-style safeTransferFrom would invoke onERC721Received. That made the reward-pool account non-EOA, and the dispatchable pallet_evm::Pallet::call forces is_transactional = true, which engages EIP-3607 (TransactionMustComeFromEOA). Every unlock call has been rejected on dev/testnet/mainnet since the stub landed. Unit tests didn't surface this because the test mock has no precompile stub.

Fix — route through T::Runner::call with is_transactional = false

Before
// dispatchable forces is_transactional=true
// → EIP-3607 rejects source with code
pallet_evm::Pallet::<T>::call(
    origin,
    reward_pool_account.into(),
    nft_info.address,
    data, ...
)
After
// internal pallet call → EOA gate skipped
// (same exemption as eth_call/eth_estimateGas)
<T as pallet_evm::Config>::Runner::call(
    reward_pool_account.into(),
    nft_info.address,
    data, ...
    /* is_transactional */ false,
    /* validate */ true,
    ..., evm_config,
)

The call is still validated and still charges fees through PotentialCurrencyAdapter; only the EOA gate is skipped. All 37 pallet-reward-pool unit tests pass against the runner-based call.

4Drive-by fix — broken reputation-voting benchmark helper

Pre-existing bench bug

PalletBenchmarkHelper::setup_reputation wrote ActivityReputation but left LastActivityTime unset. Since commit 30b61ea (2026-05-12) made pallet_activity_tracker::get_activity_reputation pure and decay-aware, an unset LastActivityTime is treated as "never seen" and the function returns 0 regardless of the stored reputation. Every pallet_reputation_voting benchmark failed with InsufficientReputation during setup.

Fix

Bench helper now also stamps LastActivityTime at the current block (clamped to ≥1, since benches start at block 0). Bench-only change — never touched at runtime.

5Weights regenerated on a common host

All five affected pallets' weights.rs were regenerated under one bench environment, ensuring the proof_size annotations across the runtime are comparable apples-to-apples:

SettingValue
Host / CPUvampik · AMD Ryzen 7 7700X (8C/16T)
Date2026-05-18
Steps · Repeat50 · 20
DB cache1024 MB
Chaindev
Template/tmp/frame-weight-template.hbs (polkadot-v1.6.0 substrate)
Palletspallet-fractional-nft · pallet-handshake · pallet-referral · pallet-reputation-voting · pallet-reward-pool

pallet-fractional-nft/weights.rs also re-includes the manually maintained helpers (on_finalize_base_weight, referrer_iteration_weight, on_finalize_extra_weight) — the bench CLI overwrites the file's trait section, so they were restored verbatim after regen.

6Commits on this branch (oldest → newest)

bd5dfe2 pallet-referral — tighten MAX_TREE_DEPTH safety cap
98369a6 runtime — tighten oversized BoundedVec / BoundedBTreeSet limits
c410d79 pallet-referral tests — align depth-boundary test with new cap
cb6b819 weights — regenerate for affected pallets
e504845 runtime — raise referral / reputation-voting bounds
46b1dac weights — regenerate after bound updates
b6ed67d runtime — lower MaxCompletedHandshakes to 100_000
b9933a5 weights — regenerate pallet-handshake at 100k bound
2fbe2a2 pallet-referral — fail closed when cycle walk exceeds MAX_TREE_DEPTH
1ea91ac test — add_referral rejects cycle past MAX_TREE_DEPTH
8df0874 pallet-reward-pool — route unlock EVM call through Runner to bypass EIP-3607
735624b bench — stamp LastActivityTime in reputation-voting benchmark helper
8a24286 weights — regenerate for all branch-affected pallets on common host