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.
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.
|
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.
| 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.
|
WalkOutcome<R>// silently collapsed two cases: // - reached root // - hit MAX_TREE_DEPTH fn walk_ancestors<R>(...) -> Option<R>
enum WalkOutcome<R> {
Found(R),
Completed, // reached root
DepthExceeded, // cap hit
}
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.
pallet_reward_pool::unlock broken in production
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.
T::Runner::call with is_transactional = false// 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, ... )
// 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.
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.
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.
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:
| Setting | Value |
|---|---|
| Host / CPU | vampik · AMD Ryzen 7 7700X (8C/16T) |
| Date | 2026-05-18 |
| Steps · Repeat | 50 · 20 |
| DB cache | 1024 MB |
| Chain | dev |
| Template | /tmp/frame-weight-template.hbs (polkadot-v1.6.0 substrate) |
| Pallets | pallet-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.