mirror of
				https://github.com/Xahau/xahaud.git
				synced 2025-11-04 02:35:48 +00:00 
			
		
		
		
	feat: snapshot wip
This commit is contained in:
		
							
								
								
									
										1420
									
								
								branch-docs/extended-hook-state-spec.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1420
									
								
								branch-docs/extended-hook-state-spec.md
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										367
									
								
								branch-docs/high-water-mark-implementation.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										367
									
								
								branch-docs/high-water-mark-implementation.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,367 @@
 | 
			
		||||
# High Water Mark Capacity Implementation
 | 
			
		||||
 | 
			
		||||
## Summary
 | 
			
		||||
 | 
			
		||||
**Completely replaced account-wide scale design with per-entry high water mark capacity.**
 | 
			
		||||
 | 
			
		||||
### What Changed
 | 
			
		||||
 | 
			
		||||
**Before (Account-Wide Scale)**:
 | 
			
		||||
- `sfHookStateScale` in AccountRoot (1-16 multiplier)
 | 
			
		||||
- All entries cost `scale × 1` reserves
 | 
			
		||||
- Can't decrease scale if `stateCount > 0`
 | 
			
		||||
- Must set scale via AccountSet before creating large entries
 | 
			
		||||
- All entries limited to same max size
 | 
			
		||||
 | 
			
		||||
**After (High Water Mark Capacity)**:
 | 
			
		||||
- `sfHookStateCapacity` in ltHOOK_STATE (per-entry, uint16)
 | 
			
		||||
- Each entry costs `capacity` reserves where capacity = `ceil(dataSize / 256)`
 | 
			
		||||
- Capacity = max size ever seen for that entry
 | 
			
		||||
- Capacity only grows, never shrinks
 | 
			
		||||
- No upfront configuration needed
 | 
			
		||||
- Mixed sizes naturally supported
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## Implementation Details
 | 
			
		||||
 | 
			
		||||
### Protocol Changes
 | 
			
		||||
 | 
			
		||||
**New Field**:
 | 
			
		||||
```cpp
 | 
			
		||||
// src/ripple/protocol/SField.h & SField.cpp
 | 
			
		||||
SF_UINT16 sfHookStateCapacity; // Field #22
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
**Ledger Format**:
 | 
			
		||||
```cpp
 | 
			
		||||
// src/ripple/protocol/impl/LedgerFormats.cpp
 | 
			
		||||
add(jss::HookState, ltHOOK_STATE, {
 | 
			
		||||
    {sfOwnerNode, soeREQUIRED},
 | 
			
		||||
    {sfHookStateKey, soeREQUIRED},
 | 
			
		||||
    {sfHookStateData, soeREQUIRED},
 | 
			
		||||
    {sfHookStateCapacity, soeOPTIONAL},  // NEW
 | 
			
		||||
}, commonFields);
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
**Removed**:
 | 
			
		||||
- `sfHookStateScale` from AccountRoot
 | 
			
		||||
- `sfHookStateScale` from AccountSet transaction
 | 
			
		||||
- All scale validation/enforcement logic in SetAccount.cpp
 | 
			
		||||
 | 
			
		||||
### Cache Structure
 | 
			
		||||
 | 
			
		||||
**Before**:
 | 
			
		||||
```cpp
 | 
			
		||||
std::map<AccountID, std::tuple<
 | 
			
		||||
    int64_t,   // available reserves
 | 
			
		||||
    int64_t,   // namespace count
 | 
			
		||||
    uint16_t,  // hook state scale (account-wide)
 | 
			
		||||
    std::map<uint256, std::map<uint256,
 | 
			
		||||
        std::pair<bool, Blob>>>>>  // modified, data
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
**After**:
 | 
			
		||||
```cpp
 | 
			
		||||
std::map<AccountID, std::tuple<
 | 
			
		||||
    int64_t,   // available reserves
 | 
			
		||||
    int64_t,   // namespace count
 | 
			
		||||
    std::map<uint256, std::map<uint256,
 | 
			
		||||
        std::tuple<bool, Blob, uint16_t>>>>>  // modified, data, capacity
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Core Logic
 | 
			
		||||
 | 
			
		||||
**set_state_cache() - Phase 1 Virtual Accounting**:
 | 
			
		||||
```cpp
 | 
			
		||||
// Calculate capacity from data size
 | 
			
		||||
uint16_t newCapacity = data.empty() ? 0 : (data.size() + 255) / 256;
 | 
			
		||||
 | 
			
		||||
// Get old capacity from ledger/cache
 | 
			
		||||
uint16_t oldCapacity = 0;
 | 
			
		||||
if (existing entry) {
 | 
			
		||||
    oldCapacity = entry.capacity ?: ceil(entry.dataSize / 256);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// High water mark
 | 
			
		||||
uint16_t finalCapacity = max(newCapacity, oldCapacity);
 | 
			
		||||
uint16_t capacityDelta = finalCapacity - oldCapacity;
 | 
			
		||||
 | 
			
		||||
// Only deduct reserves if capacity INCREASES
 | 
			
		||||
if (modified && capacityDelta > 0) {
 | 
			
		||||
    if (availableForReserves < capacityDelta)
 | 
			
		||||
        return RESERVE_INSUFFICIENT;
 | 
			
		||||
    availableForReserves -= capacityDelta;
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
**setHookState() - Phase 2 Actual Ledger Updates**:
 | 
			
		||||
```cpp
 | 
			
		||||
// For creates
 | 
			
		||||
ownerCount += capacity;
 | 
			
		||||
adjustOwnerCount(view, sleAccount, capacity, j);
 | 
			
		||||
hookState->setFieldU16(sfHookStateCapacity, capacity);
 | 
			
		||||
 | 
			
		||||
// For modifications
 | 
			
		||||
uint16_t oldCapacity = hookState->getFieldU16(sfHookStateCapacity);
 | 
			
		||||
if (capacity > oldCapacity) {
 | 
			
		||||
    uint16_t capacityDelta = capacity - oldCapacity;
 | 
			
		||||
    ownerCount += capacityDelta;
 | 
			
		||||
    adjustOwnerCount(view, sleAccount, capacityDelta, j);
 | 
			
		||||
    hookState->setFieldU16(sfHookStateCapacity, capacity);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// For deletes
 | 
			
		||||
adjustOwnerCount(view, sleAccount, -capacity, j);
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
**Namespace Deletion**:
 | 
			
		||||
```cpp
 | 
			
		||||
// Track individual capacities during directory walk
 | 
			
		||||
std::vector<uint16_t> capacities;
 | 
			
		||||
for (each entry) {
 | 
			
		||||
    uint16_t cap = entry.capacity ?: ceil(entry.dataSize / 256);
 | 
			
		||||
    capacities.push_back(cap);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Sum total capacity being deleted
 | 
			
		||||
uint32_t totalCapacity = sum(capacities);
 | 
			
		||||
adjustOwnerCount(view, sleAccount, -totalCapacity, j);
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Size Limits
 | 
			
		||||
 | 
			
		||||
**Before**:
 | 
			
		||||
```cpp
 | 
			
		||||
// Account sets scale=8
 | 
			
		||||
maxSize = 256 * scale = 2048 bytes
 | 
			
		||||
 | 
			
		||||
// In state_foreign_set
 | 
			
		||||
maxSize = maxHookStateDataSize(hookStateScale);
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
**After**:
 | 
			
		||||
```cpp
 | 
			
		||||
// No account parameter needed
 | 
			
		||||
// Absolute maximum: 16 * 256 = 4096 bytes
 | 
			
		||||
constexpr uint32_t maxAbsoluteSize = 16 * 256;
 | 
			
		||||
 | 
			
		||||
// Per-entry check happens automatically in set_state_cache
 | 
			
		||||
// based on calculated capacity
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Backward Compatibility
 | 
			
		||||
 | 
			
		||||
**Legacy entries without capacity**:
 | 
			
		||||
```cpp
 | 
			
		||||
uint16_t oldCapacity;
 | 
			
		||||
if (hookState->isFieldPresent(sfHookStateCapacity)) {
 | 
			
		||||
    oldCapacity = hookState->getFieldU16(sfHookStateCapacity);
 | 
			
		||||
} else {
 | 
			
		||||
    // Legacy: calculate from current data size
 | 
			
		||||
    auto const& data = hookState->getFieldVL(sfHookStateData);
 | 
			
		||||
    oldCapacity = data.empty() ? 1 : (data.size() + 255) / 256;
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
**Old scale field**:
 | 
			
		||||
- `sfHookStateScale` still defined in protocol (field #21)
 | 
			
		||||
- No longer used anywhere in implementation
 | 
			
		||||
- Reading it returns 0 (field absent)
 | 
			
		||||
- Can coexist with new capacity field
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## Test Changes
 | 
			
		||||
 | 
			
		||||
### AccountSet_test.cpp
 | 
			
		||||
 | 
			
		||||
**Removed**: `testHookStateScale()` - entire account-wide scale test suite
 | 
			
		||||
 | 
			
		||||
**Added**: `testHookStateCapacity()` - placeholder confirming no account-wide config needed
 | 
			
		||||
 | 
			
		||||
### SetHook_test.cpp
 | 
			
		||||
 | 
			
		||||
**Removed**: Scale-based test checking:
 | 
			
		||||
- Setting scale via AccountSet
 | 
			
		||||
- OwnerCount = `scale × stateCount`
 | 
			
		||||
- Scale validation
 | 
			
		||||
- Scale decrease restrictions
 | 
			
		||||
 | 
			
		||||
**Added**: High water mark test checking:
 | 
			
		||||
- Capacity grows automatically (300 → 800 → 1100 bytes)
 | 
			
		||||
- Capacity never shrinks (800 → 100 bytes, capacity stays at 4)
 | 
			
		||||
- Reserves adjust only on capacity growth
 | 
			
		||||
- Maximum 4096 bytes enforced
 | 
			
		||||
- OwnerCount = sum of individual capacities
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## Migration Impact
 | 
			
		||||
 | 
			
		||||
### For Existing Deployments
 | 
			
		||||
 | 
			
		||||
**If merged before ExtendedHookState amendment activates**:
 | 
			
		||||
- No migration needed
 | 
			
		||||
- Feature ships with high water mark from day 1
 | 
			
		||||
- No legacy scale-based entries exist
 | 
			
		||||
 | 
			
		||||
**If ExtendedHookState already active**:
 | 
			
		||||
- Existing entries created under old design have no capacity field
 | 
			
		||||
- Backward compatibility logic calculates capacity from data size
 | 
			
		||||
- First modification adds capacity field
 | 
			
		||||
- Gradual migration as entries are modified
 | 
			
		||||
 | 
			
		||||
### For Developers
 | 
			
		||||
 | 
			
		||||
**Before**:
 | 
			
		||||
```javascript
 | 
			
		||||
// Set scale before creating state
 | 
			
		||||
env(accountSet(alice, {HookStateScale: 8}));
 | 
			
		||||
// All entries limited to 8 * 256 = 2048 bytes
 | 
			
		||||
// Can't decrease later
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
**After**:
 | 
			
		||||
```javascript
 | 
			
		||||
// No setup needed!
 | 
			
		||||
// Just create state, capacity grows automatically
 | 
			
		||||
// Each entry independent
 | 
			
		||||
// Shrinking data keeps capacity (high water mark)
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## Benefits
 | 
			
		||||
 | 
			
		||||
### ✅ Eliminates Account-Wide Lock-In
 | 
			
		||||
```
 | 
			
		||||
Before: Set scale=8 → stuck at 8× reserves forever
 | 
			
		||||
After:  No account parameter → no lock-in
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### ✅ Optimal Reserve Usage
 | 
			
		||||
```
 | 
			
		||||
Before: 1000 small entries at scale=8 = 8000 reserves
 | 
			
		||||
After:  1000 small entries with capacity=1 each = 1000 reserves
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### ✅ No Upfront Guessing
 | 
			
		||||
```
 | 
			
		||||
Before: Must predict max size before creating entries
 | 
			
		||||
After:  Capacity grows organically with usage
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### ✅ Predictable Modifications
 | 
			
		||||
```
 | 
			
		||||
Before: Modifications within scale never fail on reserves
 | 
			
		||||
After:  Modifications within capacity never fail (same)
 | 
			
		||||
        Modifications growing capacity: reserve check (clear, expected)
 | 
			
		||||
        Modifications shrinking size: never fail (high water mark)
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### ✅ Mixed Workloads
 | 
			
		||||
```
 | 
			
		||||
Before: All entries same max size
 | 
			
		||||
After:  Each entry independent - 256 bytes, 2KB, 4KB naturally coexist
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## Trade-offs
 | 
			
		||||
 | 
			
		||||
### Limitation: Can't Reclaim Reserves Without Delete
 | 
			
		||||
 | 
			
		||||
**Scenario**:
 | 
			
		||||
```
 | 
			
		||||
Entry grows to 2KB (capacity=8)
 | 
			
		||||
Data shrinks to 100 bytes permanently
 | 
			
		||||
Still paying 8 reserves
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
**Workaround**: Delete + recreate entry to reset capacity to 1
 | 
			
		||||
 | 
			
		||||
**Rationale**:
 | 
			
		||||
- Satisfies Richard's concern: reserves based on capacity, not current size
 | 
			
		||||
- One-way growth = simple, predictable
 | 
			
		||||
- Deletion still allows cleanup
 | 
			
		||||
- Penalty for "accidental growth" is bounded
 | 
			
		||||
 | 
			
		||||
### Storage Overhead
 | 
			
		||||
 | 
			
		||||
**Before**: 0 bytes (account-wide scale in AccountRoot)
 | 
			
		||||
 | 
			
		||||
**After**: 2 bytes per entry (uint16_t capacity)
 | 
			
		||||
 | 
			
		||||
**Impact**: Minimal - each entry already has 32-byte key + variable data
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## Files Modified
 | 
			
		||||
 | 
			
		||||
### Protocol (13 files)
 | 
			
		||||
- src/ripple/protocol/SField.h
 | 
			
		||||
- src/ripple/protocol/impl/SField.cpp
 | 
			
		||||
- src/ripple/protocol/impl/LedgerFormats.cpp
 | 
			
		||||
- src/ripple/protocol/impl/TxFormats.cpp (removed scale from AccountSet)
 | 
			
		||||
- hook/sfcodes.h
 | 
			
		||||
 | 
			
		||||
### Core Logic (3 files)
 | 
			
		||||
- src/ripple/app/hook/applyHook.h
 | 
			
		||||
- src/ripple/app/hook/impl/applyHook.cpp
 | 
			
		||||
- src/ripple/app/hook/Enum.h
 | 
			
		||||
 | 
			
		||||
### Transaction Processing (2 files)
 | 
			
		||||
- src/ripple/app/tx/impl/SetAccount.cpp (removed all scale logic)
 | 
			
		||||
- src/ripple/app/tx/impl/SetHook.cpp (namespace deletion)
 | 
			
		||||
 | 
			
		||||
### Tests (2 files)
 | 
			
		||||
- src/test/rpc/AccountSet_test.cpp
 | 
			
		||||
- src/test/app/SetHook_test.cpp
 | 
			
		||||
 | 
			
		||||
### Total: 20 files modified
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## Lines Changed
 | 
			
		||||
 | 
			
		||||
**Additions**: ~250 lines
 | 
			
		||||
- High water mark logic in set_state_cache()
 | 
			
		||||
- Capacity tracking in setHookState()
 | 
			
		||||
- Capacity summation in namespace deletion
 | 
			
		||||
- New test cases
 | 
			
		||||
 | 
			
		||||
**Deletions**: ~150 lines
 | 
			
		||||
- All account-wide scale logic
 | 
			
		||||
- Scale validation in SetAccount
 | 
			
		||||
- Scale-based tests
 | 
			
		||||
- AccountSet scale transaction handling
 | 
			
		||||
 | 
			
		||||
**Net**: ~100 lines added
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## Verification Steps
 | 
			
		||||
 | 
			
		||||
1. **Compile**: ✅ All compilation errors fixed
 | 
			
		||||
2. **Tests**: Run SetHook_test.cpp capacity test
 | 
			
		||||
3. **Integration**: Verify OwnerCount calculations
 | 
			
		||||
4. **Backward compat**: Test legacy entries without capacity field
 | 
			
		||||
5. **Limits**: Verify 4096 byte maximum enforced
 | 
			
		||||
6. **Reserves**: Verify capacity growth triggers reserve checks
 | 
			
		||||
7. **Deletion**: Verify capacity refunds on delete
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## Design Rationale
 | 
			
		||||
 | 
			
		||||
This implementation follows the "High Water Mark Capacity" design from the extended-hook-state-spec.md, which was identified as the optimal approach during the PR review discussion.
 | 
			
		||||
 | 
			
		||||
**Key Design Principles**:
 | 
			
		||||
1. **Capacity-based reserves**: Satisfies Richard's concern about size-based reserves
 | 
			
		||||
2. **One-way growth**: Simple, predictable behavior
 | 
			
		||||
3. **No upfront guessing**: Organic growth with usage
 | 
			
		||||
4. **Per-entry independence**: No account-wide lock-in
 | 
			
		||||
5. **Backward compatible**: Graceful handling of legacy entries
 | 
			
		||||
 | 
			
		||||
This design provides the flexibility of per-entry capacity with the simplicity of one-way growth, eliminating the "Scale Commitment Trap" while maintaining predictable reserve mechanics.
 | 
			
		||||
							
								
								
									
										331
									
								
								branch-docs/pr-406-discussion.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										331
									
								
								branch-docs/pr-406-discussion.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,331 @@
 | 
			
		||||
# PR #406 Discussion Context - Extended Hook State
 | 
			
		||||
 | 
			
		||||
This document captures all discussion from GitHub PR #406 (ExtendedHookState feature).
 | 
			
		||||
 | 
			
		||||
## Timeline
 | 
			
		||||
 | 
			
		||||
- **Initial PR**: Feature to extend hook state beyond 256 bytes using account-wide scale parameter
 | 
			
		||||
- **Early Reviews**: Feb 2025 - tequdev's TODO notes
 | 
			
		||||
- **Richard's Feedback**: June 2025 - Design philosophy discussion
 | 
			
		||||
- **Recent Reviews**: Oct 2025 - Code review comments from RichardAH, tequdev, sublimator
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## Design Philosophy Discussion (June 2025)
 | 
			
		||||
 | 
			
		||||
### Richard's Initial Position (2025-06-18)
 | 
			
		||||
 | 
			
		||||
> I think we should not add the complexity of incremental reserve increase, rather just increase the allowable size for the same existing reserve cost. I think 1 reserve for 2048 bytes is probably ok.
 | 
			
		||||
>
 | 
			
		||||
> An alternative simpler fee model would be to nominate your account for "large hook data" whereupon all your hook states cost more to store but you have access to the larger size. **I really dislike the idea of a variable length field claiming different reserves depending on its current size.**
 | 
			
		||||
 | 
			
		||||
**Key insight**: Richard's core objection is to reserves varying based on current data size.
 | 
			
		||||
 | 
			
		||||
### Evolution to Scale-Based Design (2025-06-20)
 | 
			
		||||
 | 
			
		||||
Richard's "large hook data" proposal:
 | 
			
		||||
 | 
			
		||||
> Sure, so you do an account set flag which is like the equivalent of specifying large pages in your OS. All your hook states cost more but they're allowed to be bigger. It makes the computation easier. So let's say in large mode you get 2048 bytes per hook state but in computing reserve requirements for your hook state we take your HookStateCount and multiply it by 8. Once the flag is enabled it can only be disabled if HookStateCount is 0.
 | 
			
		||||
 | 
			
		||||
### Tequdev's Refinement (2025-06-22)
 | 
			
		||||
 | 
			
		||||
> That's interesting. Instead of using the flag, how about using a numeric field to represent the maximum size the account allows?
 | 
			
		||||
>
 | 
			
		||||
> If we use the flag, whenever we need to increase the protocol's maximum allowed size, we end up having to add a new flag.
 | 
			
		||||
 | 
			
		||||
### Richard Agrees (2025-06-25)
 | 
			
		||||
 | 
			
		||||
> I think that's reasonable. We could say HookStateScale or something 0,1 ...
 | 
			
		||||
 | 
			
		||||
**Result**: Current design with `sfHookStateScale` numeric field (1-16) was born from this discussion.
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## Code Review Comments (October 2025)
 | 
			
		||||
 | 
			
		||||
### Critical Issues
 | 
			
		||||
 | 
			
		||||
#### 1. Scale Change Restriction (RichardAH, 2025-10-22)
 | 
			
		||||
 | 
			
		||||
**Location**: `src/ripple/app/tx/impl/SetAccount.cpp:273`
 | 
			
		||||
 | 
			
		||||
> shouldn't be able to set/change it at all unless hook state count is zero
 | 
			
		||||
 | 
			
		||||
**Current code**:
 | 
			
		||||
```cpp
 | 
			
		||||
if (stateCount > 0 && newScale < currentScale)
 | 
			
		||||
    return tecHAS_HOOK_STATE;
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
**Richard's concern**: Should block ALL changes when `stateCount > 0`, not just decreases.
 | 
			
		||||
 | 
			
		||||
**sublimator's response** (2025-10-22):
 | 
			
		||||
> This does make the feature hard to test out without trapping yourself though, right? Buyer beware?
 | 
			
		||||
 | 
			
		||||
**Analysis**:
 | 
			
		||||
- Current: Can increase scale freely, can only decrease if `stateCount == 0`
 | 
			
		||||
- Richard wants: Can only change (up or down) if `stateCount == 0`
 | 
			
		||||
- Trade-off: More restrictive = safer but less flexible
 | 
			
		||||
 | 
			
		||||
**sublimator's test suggestion** (2025-10-23):
 | 
			
		||||
```cpp
 | 
			
		||||
// Test the escape hatch - decrease WITHOUT state
 | 
			
		||||
applyCount(5, 0, 50);  // ← stateCount=0 (all state deleted)
 | 
			
		||||
jt[sfHookStateScale.fieldName] = 2;
 | 
			
		||||
env(jt);  // ← Should succeed! This is how you escape high scale
 | 
			
		||||
BEAST_EXPECT(env.le(alice)->getFieldU16(sfHookStateScale) == 2);
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
#### 2. Overflow Sanity Check (RichardAH, 2025-10-22)
 | 
			
		||||
 | 
			
		||||
**Location**: `src/ripple/app/tx/impl/SetAccount.cpp:680`
 | 
			
		||||
 | 
			
		||||
> sanity check newOwnerCount > oldOwnerCount
 | 
			
		||||
 | 
			
		||||
**Current code**:
 | 
			
		||||
```cpp
 | 
			
		||||
uint32_t const newOwnerCount = oldOwnerCount -
 | 
			
		||||
    (oldScale * stateCount) + (newScale * stateCount);
 | 
			
		||||
 | 
			
		||||
if (newOwnerCount < oldOwnerCount)
 | 
			
		||||
    return tecINTERNAL;  // ← This IS the sanity check
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
**Status**: Actually already implemented correctly! The check detects underflow.
 | 
			
		||||
 | 
			
		||||
**Mathematical impossibility**: Would need 268M entries × 16 scale ≈ 43B XRP in reserves - economically impossible.
 | 
			
		||||
 | 
			
		||||
#### 3. TER Code Comment Clarity (RichardAH, 2025-10-22)
 | 
			
		||||
 | 
			
		||||
**Location**: `src/ripple/protocol/impl/TER.cpp:97`
 | 
			
		||||
 | 
			
		||||
**First suggestion** (2025-06-30):
 | 
			
		||||
> maybe expand the explanation to "The account has hook state. Delete all existing state before changing the scale."
 | 
			
		||||
 | 
			
		||||
**Updated suggestion** (2025-10-22):
 | 
			
		||||
> Change comment to something like: Delete all hook state before reducing scale.
 | 
			
		||||
 | 
			
		||||
**tequdev's response** (2025-10-22):
 | 
			
		||||
> Since the number of TER codes is limited, we used a generic error message. However, we will change it as you pointed out, as it can be modified later when this code is used for other purposes.
 | 
			
		||||
 | 
			
		||||
**Status**: Will be updated to be more specific about scale reduction requirement.
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
### Code Quality Issues
 | 
			
		||||
 | 
			
		||||
#### 4. Magic Number Constants (sublimator, 2025-10-22)
 | 
			
		||||
 | 
			
		||||
**Location 1**: `src/ripple/app/tx/impl/SetAccount.cpp:195`
 | 
			
		||||
```cpp
 | 
			
		||||
if (scale == 0 || scale > 16)  // ← extract constant
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
> extract a constant for this ?
 | 
			
		||||
 | 
			
		||||
**Location 2**: `src/ripple/app/hook/Enum.h:56`
 | 
			
		||||
```cpp
 | 
			
		||||
return 256U * hookStateScale;  // ← add defensive check
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
> we could add an extra defensive check here with a constant, I guess
 | 
			
		||||
 | 
			
		||||
**Suggested improvement**:
 | 
			
		||||
```cpp
 | 
			
		||||
constexpr uint16_t MAX_HOOK_STATE_SCALE = 16;
 | 
			
		||||
constexpr uint32_t HOOK_STATE_BASE_SIZE = 256;
 | 
			
		||||
 | 
			
		||||
// In validation:
 | 
			
		||||
if (scale == 0 || scale > MAX_HOOK_STATE_SCALE)
 | 
			
		||||
    return temMALFORMED;
 | 
			
		||||
 | 
			
		||||
// In size calculation:
 | 
			
		||||
inline uint32_t maxHookStateDataSize(uint16_t hookStateScale)
 | 
			
		||||
{
 | 
			
		||||
    if (hookStateScale == 0 || hookStateScale > MAX_HOOK_STATE_SCALE)
 | 
			
		||||
        return HOOK_STATE_BASE_SIZE;
 | 
			
		||||
    return HOOK_STATE_BASE_SIZE * hookStateScale;
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
#### 5. Type Consistency (RichardAH, 2025-06-29)
 | 
			
		||||
 | 
			
		||||
**Location**: `src/ripple/app/hook/applyHook.h:35`
 | 
			
		||||
 | 
			
		||||
> I don't mind if you use int16_t or uint16_t or uint32_t but keep it consistent with the function calls? like `maxHookStateDataSize(uint32_t hookStateScale)` same for the sf type
 | 
			
		||||
 | 
			
		||||
**Issue**: `sfHookStateScale` is `uint16_t` but function parameter types vary.
 | 
			
		||||
 | 
			
		||||
**Status**: Needs consistency pass to ensure all uses of scale parameter use `uint16_t`.
 | 
			
		||||
 | 
			
		||||
#### 6. Pointless Change? (RichardAH, 2025-06-29)
 | 
			
		||||
 | 
			
		||||
**Location**: `src/ripple/app/tx/impl/SetHook.cpp:930`
 | 
			
		||||
 | 
			
		||||
> was there a reason for changing this? I assume it doesn't change the outcome
 | 
			
		||||
 | 
			
		||||
**tequdev's response** (2025-10-22):
 | 
			
		||||
> https://github.com/Xahau/xahaud/commit/a75daaea0249c1fabd773c4ef1ee08929cd9d3a3
 | 
			
		||||
 | 
			
		||||
**Status**: Addressed in commit a75daae.
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
### Defensive Programming Issues
 | 
			
		||||
 | 
			
		||||
#### 7. Overflow Checks in Hook API (RichardAH, 2025-06-29)
 | 
			
		||||
 | 
			
		||||
**Locations**:
 | 
			
		||||
- `src/ripple/app/hook/impl/applyHook.cpp:1540`
 | 
			
		||||
- `src/ripple/app/hook/impl/applyHook.cpp:1577`
 | 
			
		||||
- `src/ripple/app/hook/impl/applyHook.cpp:1599`
 | 
			
		||||
- `src/ripple/app/tx/impl/SetHook.cpp:946`
 | 
			
		||||
 | 
			
		||||
> we need defensive sanity checks on math like this
 | 
			
		||||
> overflow sanity check needed
 | 
			
		||||
> here too
 | 
			
		||||
> we should do a sanity check on this value before passing it to make sure it is reasonable and hasn't overflowed
 | 
			
		||||
 | 
			
		||||
**tequdev's response** (2025-10-18):
 | 
			
		||||
All addressed in commit `91f683a9994e02c29f85cd3aede807bb6ebee93f`
 | 
			
		||||
 | 
			
		||||
**Status**: ✅ Fixed with sanity checks added.
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## Early Development TODOs (Feb-March 2025)
 | 
			
		||||
 | 
			
		||||
### 1. Reserve Check Optimization (tequdev, 2025-02-12)
 | 
			
		||||
 | 
			
		||||
**Location**: `src/ripple/app/hook/impl/applyHook.cpp:1170`
 | 
			
		||||
 | 
			
		||||
> TODO: Don't return `tecINSUFFICIENT_RESERVE` if the OwnerCount decreases.
 | 
			
		||||
 | 
			
		||||
**Follow-up** (2025-02-14):
 | 
			
		||||
> This check is not made because set_state_cache, which performs the reserve check, does not access the StateData before the change.
 | 
			
		||||
 | 
			
		||||
**Analysis**: When modifying existing state to be smaller, current code may incorrectly require reserves. However, the virtual reserve check in `set_state_cache()` can't see the old data size to know if it's decreasing.
 | 
			
		||||
 | 
			
		||||
### 2. Owner Count Decrease Testing (tequdev, 2025-02-14)
 | 
			
		||||
 | 
			
		||||
**Location**: `src/ripple/app/tx/impl/SetHook.cpp:929`
 | 
			
		||||
 | 
			
		||||
> TODO: needs tests that OwnerCount decreases depending on HookState size when NS deleted.
 | 
			
		||||
 | 
			
		||||
**Status**: Still needed. When namespace (NS) is deleted, OwnerCount should decrease by `scale × stateCount`.
 | 
			
		||||
 | 
			
		||||
### 3. Insufficient Reserves Testing (tequdev, 2025-03-29)
 | 
			
		||||
 | 
			
		||||
**Review submission**:
 | 
			
		||||
> TODO: Tests for Insufficient Reserves
 | 
			
		||||
 | 
			
		||||
**sublimator's test case** (2025-10-23):
 | 
			
		||||
```cpp
 | 
			
		||||
// Insufficient reserves on increase
 | 
			
		||||
applyCount(1, 100, 200);  // 100 entries at scale=1
 | 
			
		||||
// Manually set balance to just cover current reserves
 | 
			
		||||
jt[sfHookStateScale.fieldName] = 16;  // Needs 1600 reserves (100×16)
 | 
			
		||||
env(jt, ter(tecINSUFFICIENT_RESERVE));  // Should fail - can't afford it
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## Summary of Outstanding Issues
 | 
			
		||||
 | 
			
		||||
### Must Fix Before Merge
 | 
			
		||||
 | 
			
		||||
1. **TER comment clarity** - Update error message per Richard's suggestion
 | 
			
		||||
2. **Extract magic constants** - Define `MAX_HOOK_STATE_SCALE` and `HOOK_STATE_BASE_SIZE`
 | 
			
		||||
3. **Type consistency** - Ensure all scale parameters use `uint16_t`
 | 
			
		||||
 | 
			
		||||
### Should Fix Before Merge
 | 
			
		||||
 | 
			
		||||
4. **Scale change restriction** - Decide: block all changes when `stateCount > 0`, or just decreases?
 | 
			
		||||
5. **Test coverage gaps**:
 | 
			
		||||
   - Escape hatch test (decrease scale after deleting all state)
 | 
			
		||||
   - Insufficient reserves on scale increase
 | 
			
		||||
   - OwnerCount decrease when namespace deleted
 | 
			
		||||
   - Reserve check optimization when modifying to smaller size
 | 
			
		||||
 | 
			
		||||
### Already Fixed ✅
 | 
			
		||||
 | 
			
		||||
- Overflow sanity checks (commit 91f683a)
 | 
			
		||||
- Pointless change in SetHook.cpp (commit a75daae)
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## Design Trade-offs Discussed
 | 
			
		||||
 | 
			
		||||
### Current Design: Account-Wide Scale
 | 
			
		||||
 | 
			
		||||
**Pros**:
 | 
			
		||||
- Simple reserve calculation: `stateCount × scale`
 | 
			
		||||
- Deterministic reserve requirements
 | 
			
		||||
- No per-entry metadata overhead
 | 
			
		||||
- Satisfies Richard's objection to size-based reserves
 | 
			
		||||
 | 
			
		||||
**Cons**:
 | 
			
		||||
- "Scale Commitment Trap" - can't decrease without deleting all state
 | 
			
		||||
- All entries pay for max capacity even if using less
 | 
			
		||||
- Hard to test without trapping yourself (sublimator's concern)
 | 
			
		||||
 | 
			
		||||
### Alternative: High Water Mark Capacity (from spec)
 | 
			
		||||
 | 
			
		||||
**Concept**: Each entry stores `HookStateCapacity` = max size ever seen
 | 
			
		||||
 | 
			
		||||
**Pros**:
 | 
			
		||||
- Granular reserves per entry
 | 
			
		||||
- One-way growth (can't shrink capacity)
 | 
			
		||||
- Still deterministic (capacity-based, not size-based)
 | 
			
		||||
- Satisfies Richard's objection (reserves based on capacity, not current size)
 | 
			
		||||
- More economical for mixed workloads
 | 
			
		||||
 | 
			
		||||
**Cons**:
 | 
			
		||||
- Per-entry metadata overhead (2 bytes per entry)
 | 
			
		||||
- More complex reserve calculations
 | 
			
		||||
- Ledger format changes required
 | 
			
		||||
 | 
			
		||||
### Richard's Original Objection
 | 
			
		||||
 | 
			
		||||
> I really dislike the idea of a variable length field claiming different reserves depending on its current size.
 | 
			
		||||
 | 
			
		||||
Both current design (scale-based) and high water mark satisfy this because reserves are based on:
 | 
			
		||||
- Current: declared scale capacity
 | 
			
		||||
- Alternative: declared entry capacity
 | 
			
		||||
 | 
			
		||||
Neither varies with current data size.
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## Key Insights from Discussion
 | 
			
		||||
 | 
			
		||||
1. **Scale as "Large Pages"**: Richard's OS analogy - scale is like requesting large page size for all your hook states
 | 
			
		||||
 | 
			
		||||
2. **Numeric Field > Flags**: Tequdev's insight that numeric field is more extensible than adding flags for each size tier
 | 
			
		||||
 | 
			
		||||
3. **Testing Trap**: sublimator identified that you can't easily test increasing scale without potentially trapping yourself
 | 
			
		||||
 | 
			
		||||
4. **Escape Hatch**: The ability to decrease scale when `stateCount == 0` is critical - it's the only way out of high scale
 | 
			
		||||
 | 
			
		||||
5. **Economic Impossibility**: Overflow concerns are theoretical - would require ~43B XRP in reserves
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## Chronological Review Activity
 | 
			
		||||
 | 
			
		||||
- **Feb 12, 2025**: tequdev - Reserve check optimization TODO
 | 
			
		||||
- **Feb 14, 2025**: tequdev - Clarification on reserve check limitation
 | 
			
		||||
- **Feb 14, 2025**: tequdev - OwnerCount decrease testing TODO
 | 
			
		||||
- **Mar 29, 2025**: tequdev - Insufficient reserves testing TODO
 | 
			
		||||
- **Jun 18, 2025**: RichardAH - Design philosophy: dislike size-based reserves
 | 
			
		||||
- **Jun 19, 2025**: tequdev - Request clarification on "large hook data"
 | 
			
		||||
- **Jun 20, 2025**: RichardAH - Explain flag-based approach
 | 
			
		||||
- **Jun 22, 2025**: tequdev - Suggest numeric field instead of flags
 | 
			
		||||
- **Jun 25, 2025**: RichardAH - Agree to HookStateScale field
 | 
			
		||||
- **Jun 29-30, 2025**: RichardAH - Code review: consistency, sanity checks, comments
 | 
			
		||||
- **Oct 18, 2025**: tequdev - Address overflow checks (commit 91f683a)
 | 
			
		||||
- **Oct 22, 2025**: tequdev - Address SetHook.cpp changes (commit a75daae)
 | 
			
		||||
- **Oct 22, 2025**: RichardAH - Scale change restriction, overflow sanity, TER comment
 | 
			
		||||
- **Oct 22, 2025**: tequdev - TER comment rationale
 | 
			
		||||
- **Oct 22, 2025**: sublimator - Magic constants, testing trap concern
 | 
			
		||||
- **Oct 23, 2025**: sublimator - Test case suggestions (escape hatch, insufficient reserves)
 | 
			
		||||
							
								
								
									
										506
									
								
								branch-docs/tests-rewrite-plan.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										506
									
								
								branch-docs/tests-rewrite-plan.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,506 @@
 | 
			
		||||
# High Water Mark Capacity Tests - Implementation Plan
 | 
			
		||||
 | 
			
		||||
## Current Situation
 | 
			
		||||
 | 
			
		||||
**Problem:** Tried to add high water mark capacity tests to existing `test_state_set()` function, but:
 | 
			
		||||
- Earlier tests in the suite install hooks on shared accounts (bob, alice, etc.)
 | 
			
		||||
- These hooks remain installed and interfere with later tests
 | 
			
		||||
- Test execution order and account state are entangled
 | 
			
		||||
- Hard to isolate failures
 | 
			
		||||
 | 
			
		||||
**Solution:** Create a new dedicated test function with clean accounts.
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## Test Strategy
 | 
			
		||||
 | 
			
		||||
### New Test Function: `test_high_water_mark_capacity()`
 | 
			
		||||
 | 
			
		||||
Create a completely isolated test function that:
 | 
			
		||||
- Uses fresh accounts not touched by other tests
 | 
			
		||||
- Tests high water mark capacity behavior systematically
 | 
			
		||||
- Is independent and can run in any order
 | 
			
		||||
- Guards execution with `featureExtendedHookState` check
 | 
			
		||||
 | 
			
		||||
### Location
 | 
			
		||||
 | 
			
		||||
Add to `SetHook_test.cpp` as a new test case:
 | 
			
		||||
 | 
			
		||||
```cpp
 | 
			
		||||
void
 | 
			
		||||
test_high_water_mark_capacity(FeatureBitset features)
 | 
			
		||||
{
 | 
			
		||||
    testcase("Test high water mark capacity");
 | 
			
		||||
    using namespace jtx;
 | 
			
		||||
 | 
			
		||||
    // Only run if ExtendedHookState feature is enabled
 | 
			
		||||
    if (!features[featureExtendedHookState])
 | 
			
		||||
        return;
 | 
			
		||||
 | 
			
		||||
    Env env{*this, features};
 | 
			
		||||
 | 
			
		||||
    // Use completely fresh accounts
 | 
			
		||||
    auto const alice = Account{"alice_hwm"};
 | 
			
		||||
    auto const bob = Account{"bob_hwm"};
 | 
			
		||||
 | 
			
		||||
    env.fund(XRP(10000), alice);
 | 
			
		||||
    env.fund(XRP(10000), bob);
 | 
			
		||||
 | 
			
		||||
    // ... tests here ...
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Register in test runner:
 | 
			
		||||
 | 
			
		||||
```cpp
 | 
			
		||||
void run() override
 | 
			
		||||
{
 | 
			
		||||
    // ... existing tests ...
 | 
			
		||||
 | 
			
		||||
    testHookStateScale(env);  // Remove this (old account-wide scale tests)
 | 
			
		||||
    test_high_water_mark_capacity(env);  // Add this
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## Test Cases
 | 
			
		||||
 | 
			
		||||
### Test 1: Capacity Grows Automatically (300 → 800 bytes)
 | 
			
		||||
 | 
			
		||||
**Purpose:** Verify capacity increases when data size exceeds current capacity.
 | 
			
		||||
 | 
			
		||||
**Setup:**
 | 
			
		||||
- Install hook that writes 300 bytes to key "test1"
 | 
			
		||||
- Invoke hook → creates entry with capacity=2 (ceil(300/256))
 | 
			
		||||
 | 
			
		||||
**Action:**
 | 
			
		||||
- Update hook to write 800 bytes to same key
 | 
			
		||||
- Invoke hook
 | 
			
		||||
 | 
			
		||||
**Assertions:**
 | 
			
		||||
- OwnerCount increases from 3 to 5 (1 hook + 2→4 capacity)
 | 
			
		||||
- HookStateCount stays at 1 (modifying, not creating)
 | 
			
		||||
- ledger entry `sfHookStateCapacity` = 4
 | 
			
		||||
- ledger entry `sfHookStateData.size()` = 800
 | 
			
		||||
 | 
			
		||||
**Hook WASM:**
 | 
			
		||||
```cpp
 | 
			
		||||
static uint8_t data[800] = { /* pattern */ };
 | 
			
		||||
int64_t hook(uint32_t reserved)
 | 
			
		||||
{
 | 
			
		||||
    _g(1,1);
 | 
			
		||||
    uint8_t key[32] = {0};
 | 
			
		||||
    ASSERT(state_set(SBUF(data), SBUF(key)) == 800);
 | 
			
		||||
    accept(0,0,0);
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
### Test 2: Capacity Never Shrinks (800 → 100 bytes)
 | 
			
		||||
 | 
			
		||||
**Purpose:** Verify high water mark - capacity doesn't decrease when data shrinks.
 | 
			
		||||
 | 
			
		||||
**Setup:**
 | 
			
		||||
- Entry already exists with capacity=4 from Test 1
 | 
			
		||||
 | 
			
		||||
**Action:**
 | 
			
		||||
- Update hook to write 100 bytes to same key
 | 
			
		||||
- Invoke hook
 | 
			
		||||
 | 
			
		||||
**Assertions:**
 | 
			
		||||
- OwnerCount stays at 5 (capacity unchanged)
 | 
			
		||||
- `sfHookStateCapacity` = 4 (unchanged - high water mark!)
 | 
			
		||||
- `sfHookStateData.size()` = 100 (data did shrink)
 | 
			
		||||
 | 
			
		||||
**Key Insight:** Paying for capacity 4, but only using 100 bytes.
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
### Test 3: Re-growth Within Capacity (100 → 700 bytes)
 | 
			
		||||
 | 
			
		||||
**Purpose:** Verify no reserve check when growing within existing capacity.
 | 
			
		||||
 | 
			
		||||
**Setup:**
 | 
			
		||||
- Entry has capacity=4, current data=100 bytes
 | 
			
		||||
 | 
			
		||||
**Action:**
 | 
			
		||||
- Update hook to write 700 bytes (needs capacity=3, have capacity=4)
 | 
			
		||||
- Invoke hook
 | 
			
		||||
 | 
			
		||||
**Assertions:**
 | 
			
		||||
- OwnerCount stays at 5 (no reserve change)
 | 
			
		||||
- `sfHookStateCapacity` = 4 (unchanged)
 | 
			
		||||
- `sfHookStateData.size()` = 700
 | 
			
		||||
 | 
			
		||||
**Key Insight:** Can grow back to 1024 bytes (4*256) without paying more reserves.
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
### Test 4: Growth Beyond High Water Mark (700 → 1100 bytes)
 | 
			
		||||
 | 
			
		||||
**Purpose:** Verify capacity increases and reserves charged when exceeding high water mark.
 | 
			
		||||
 | 
			
		||||
**Setup:**
 | 
			
		||||
- Entry has capacity=4, current data=700 bytes
 | 
			
		||||
 | 
			
		||||
**Action:**
 | 
			
		||||
- Update hook to write 1100 bytes (needs capacity=5)
 | 
			
		||||
- Invoke hook
 | 
			
		||||
 | 
			
		||||
**Assertions:**
 | 
			
		||||
- OwnerCount increases from 5 to 6 (capacity delta +1)
 | 
			
		||||
- `sfHookStateCapacity` = 5
 | 
			
		||||
- `sfHookStateData.size()` = 1100
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
### Test 5: Multiple Entries with Mixed Capacities
 | 
			
		||||
 | 
			
		||||
**Purpose:** Verify each entry tracks its own capacity independently.
 | 
			
		||||
 | 
			
		||||
**Setup:**
 | 
			
		||||
- Create entry at key1 with 256 bytes (capacity=1)
 | 
			
		||||
- Create entry at key2 with 2000 bytes (capacity=8)
 | 
			
		||||
- Create entry at key3 with 500 bytes (capacity=2)
 | 
			
		||||
 | 
			
		||||
**Assertions:**
 | 
			
		||||
- OwnerCount = 12 (1 hook + 1 + 8 + 2 capacities)
 | 
			
		||||
- HookStateCount = 3
 | 
			
		||||
- Each entry has correct `sfHookStateCapacity`
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
### Test 6: Maximum Size Enforcement (4096 bytes)
 | 
			
		||||
 | 
			
		||||
**Purpose:** Verify absolute maximum size limit.
 | 
			
		||||
 | 
			
		||||
**Action:**
 | 
			
		||||
- Hook attempts to write 4096 bytes → should succeed
 | 
			
		||||
- Hook attempts to write 4097 bytes → should fail with TOO_BIG
 | 
			
		||||
 | 
			
		||||
**Assertions:**
 | 
			
		||||
- 4096 byte entry: capacity=16, succeeds
 | 
			
		||||
- 4097 byte entry: fails in hook with TOO_BIG error code
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
### Test 7: Deletion Refunds Capacity
 | 
			
		||||
 | 
			
		||||
**Purpose:** Verify deleting entry refunds reserves based on capacity, not current data size.
 | 
			
		||||
 | 
			
		||||
**Setup:**
 | 
			
		||||
- Entry has capacity=8, current data=100 bytes
 | 
			
		||||
- OwnerCount = 9 (1 hook + 8 capacity)
 | 
			
		||||
 | 
			
		||||
**Action:**
 | 
			
		||||
- Delete entry (set empty data)
 | 
			
		||||
 | 
			
		||||
**Assertions:**
 | 
			
		||||
- OwnerCount = 1 (refunded 8, not 1)
 | 
			
		||||
- HookStateCount = 0
 | 
			
		||||
- Entry removed from ledger
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
### Test 8: Namespace Deletion with Mixed Capacities
 | 
			
		||||
 | 
			
		||||
**Purpose:** Verify namespace deletion sums individual entry capacities correctly.
 | 
			
		||||
 | 
			
		||||
**Setup:**
 | 
			
		||||
- Create entries in namespace "test":
 | 
			
		||||
  - key1: capacity=2
 | 
			
		||||
  - key2: capacity=5
 | 
			
		||||
  - key3: capacity=1
 | 
			
		||||
- OwnerCount = 9 (1 hook + 2 + 5 + 1)
 | 
			
		||||
 | 
			
		||||
**Action:**
 | 
			
		||||
- Delete namespace via SetHook with hsfNSDELETE flag
 | 
			
		||||
 | 
			
		||||
**Assertions:**
 | 
			
		||||
- OwnerCount = 1 (refunded 2+5+1 = 8)
 | 
			
		||||
- HookStateCount = 0
 | 
			
		||||
- All entries removed
 | 
			
		||||
- Namespace removed from HookNamespaces
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
### Test 9: Legacy Entry Migration (no capacity field)
 | 
			
		||||
 | 
			
		||||
**Purpose:** Verify backward compatibility - entries without capacity field work correctly.
 | 
			
		||||
 | 
			
		||||
**Setup:**
 | 
			
		||||
- Manually create ledger entry without `sfHookStateCapacity` field
 | 
			
		||||
- Entry has 512 bytes of data
 | 
			
		||||
 | 
			
		||||
**Action:**
 | 
			
		||||
- Hook modifies entry to 300 bytes
 | 
			
		||||
 | 
			
		||||
**Expected Behavior:**
 | 
			
		||||
- Old capacity calculated as ceil(512/256) = 2
 | 
			
		||||
- New capacity max(ceil(300/256), 2) = max(2, 2) = 2
 | 
			
		||||
- No reserve change
 | 
			
		||||
- Entry now has `sfHookStateCapacity` = 2 (field added)
 | 
			
		||||
 | 
			
		||||
**Alternative Action:**
 | 
			
		||||
- Hook modifies entry to 800 bytes
 | 
			
		||||
 | 
			
		||||
**Expected Behavior:**
 | 
			
		||||
- Old capacity = 2 (from data size)
 | 
			
		||||
- New capacity = max(4, 2) = 4
 | 
			
		||||
- Reserves increase by 2
 | 
			
		||||
- Entry now has `sfHookStateCapacity` = 4
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
### Test 10: Reserve Exhaustion
 | 
			
		||||
 | 
			
		||||
**Purpose:** Verify reserve checks prevent capacity growth when insufficient balance.
 | 
			
		||||
 | 
			
		||||
**Setup:**
 | 
			
		||||
- Account with minimal balance (e.g., 2600 XAH)
 | 
			
		||||
- Already has 1 hook + some state
 | 
			
		||||
 | 
			
		||||
**Action:**
 | 
			
		||||
- Try to create entry with large capacity that would exceed available reserves
 | 
			
		||||
 | 
			
		||||
**Assertions:**
 | 
			
		||||
- Hook rejects with RESERVE_INSUFFICIENT error code
 | 
			
		||||
- No state created
 | 
			
		||||
- OwnerCount unchanged
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## Hook Implementation Patterns
 | 
			
		||||
 | 
			
		||||
### Pattern 1: Simple Hook (Fixed Size)
 | 
			
		||||
 | 
			
		||||
Use static/global arrays to avoid stack limits:
 | 
			
		||||
 | 
			
		||||
```cpp
 | 
			
		||||
static uint8_t data[SIZE] = {
 | 
			
		||||
    0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0A,
 | 
			
		||||
    // ... partial initialization, rest zero-filled
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
int64_t hook(uint32_t reserved)
 | 
			
		||||
{
 | 
			
		||||
    _g(1,1);
 | 
			
		||||
    uint8_t key[32] = {0};
 | 
			
		||||
    ASSERT(state_set(SBUF(data), SBUF(key)) == SIZE);
 | 
			
		||||
    accept(0,0,0);
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Pattern 2: Multi-Entry Hook
 | 
			
		||||
 | 
			
		||||
```cpp
 | 
			
		||||
static uint8_t data1[256] = { /* ... */ };
 | 
			
		||||
static uint8_t data2[2000] = { /* ... */ };
 | 
			
		||||
static uint8_t data3[500] = { /* ... */ };
 | 
			
		||||
 | 
			
		||||
int64_t hook(uint32_t reserved)
 | 
			
		||||
{
 | 
			
		||||
    _g(1,1);
 | 
			
		||||
    uint8_t key1[32] = {1};
 | 
			
		||||
    uint8_t key2[32] = {2};
 | 
			
		||||
    uint8_t key3[32] = {3};
 | 
			
		||||
 | 
			
		||||
    ASSERT(state_set(SBUF(data1), SBUF(key1)) == 256);
 | 
			
		||||
    ASSERT(state_set(SBUF(data2), SBUF(key2)) == 2000);
 | 
			
		||||
    ASSERT(state_set(SBUF(data3), SBUF(key3)) == 500);
 | 
			
		||||
 | 
			
		||||
    accept(0,0,0);
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Pattern 3: Delete Hook
 | 
			
		||||
 | 
			
		||||
```cpp
 | 
			
		||||
int64_t hook(uint32_t reserved)
 | 
			
		||||
{
 | 
			
		||||
    _g(1,1);
 | 
			
		||||
    uint8_t key[32] = {0};
 | 
			
		||||
    ASSERT(state_set(0, 0, SBUF(key)) == 0);  // Empty data = delete
 | 
			
		||||
    accept(0,0,0);
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## Test Organization
 | 
			
		||||
 | 
			
		||||
```cpp
 | 
			
		||||
void
 | 
			
		||||
test_high_water_mark_capacity(FeatureBitset features)
 | 
			
		||||
{
 | 
			
		||||
    testcase("Test high water mark capacity");
 | 
			
		||||
 | 
			
		||||
    if (!features[featureExtendedHookState])
 | 
			
		||||
        return;
 | 
			
		||||
 | 
			
		||||
    using namespace jtx;
 | 
			
		||||
    Env env{*this, features};
 | 
			
		||||
 | 
			
		||||
    // Fresh accounts
 | 
			
		||||
    auto const alice = Account{"alice_hwm"};
 | 
			
		||||
    auto const bob = Account{"bob_hwm"};
 | 
			
		||||
    env.fund(XRP(10000), alice);
 | 
			
		||||
    env.fund(XRP(10000), bob);
 | 
			
		||||
 | 
			
		||||
    // Test 1: Capacity grows automatically
 | 
			
		||||
    {
 | 
			
		||||
        TestHook hook300 = wasm[R"[test.hook](
 | 
			
		||||
            // ... 300 byte hook ...
 | 
			
		||||
        )[test.hook]"];
 | 
			
		||||
 | 
			
		||||
        env(ripple::test::jtx::hook(alice, {{hso(hook300, overrideFlag)}}, 0),
 | 
			
		||||
            M("install 300 byte hook"), HSFEE);
 | 
			
		||||
        env.close();
 | 
			
		||||
 | 
			
		||||
        env(pay(bob, alice, XRP(1)), M("create 300 byte entry"), fee(XRP(1)));
 | 
			
		||||
        env.close();
 | 
			
		||||
 | 
			
		||||
        BEAST_EXPECT((*env.le(alice))[sfOwnerCount] == 3);
 | 
			
		||||
        // ... more assertions ...
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Test 2: Capacity never shrinks
 | 
			
		||||
    {
 | 
			
		||||
        TestHook hook800 = wasm[R"[test.hook](
 | 
			
		||||
            // ... 800 byte hook ...
 | 
			
		||||
        )[test.hook]"];
 | 
			
		||||
 | 
			
		||||
        env(ripple::test::jtx::hook(alice, {{hso(hook800, overrideFlag)}}, 0),
 | 
			
		||||
            M("update to 800 byte hook"), HSFEE);
 | 
			
		||||
        env.close();
 | 
			
		||||
 | 
			
		||||
        env(pay(bob, alice, XRP(1)), M("grow to 800 bytes"), fee(XRP(1)));
 | 
			
		||||
        env.close();
 | 
			
		||||
 | 
			
		||||
        BEAST_EXPECT((*env.le(alice))[sfOwnerCount] == 5);
 | 
			
		||||
        // ... more assertions ...
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Test 3: Shrink data size
 | 
			
		||||
    {
 | 
			
		||||
        TestHook hook100 = wasm[R"[test.hook](
 | 
			
		||||
            // ... 100 byte hook ...
 | 
			
		||||
        )[test.hook]"];
 | 
			
		||||
 | 
			
		||||
        env(ripple::test::jtx::hook(alice, {{hso(hook100, overrideFlag)}}, 0),
 | 
			
		||||
            M("update to 100 byte hook"), HSFEE);
 | 
			
		||||
        env.close();
 | 
			
		||||
 | 
			
		||||
        env(pay(bob, alice, XRP(1)), M("shrink to 100 bytes"), fee(XRP(1)));
 | 
			
		||||
        env.close();
 | 
			
		||||
 | 
			
		||||
        // High water mark - capacity stays at 4
 | 
			
		||||
        BEAST_EXPECT((*env.le(alice))[sfOwnerCount] == 5);
 | 
			
		||||
 | 
			
		||||
        auto const state = env.le(ripple::keylet::hookState(
 | 
			
		||||
            alice.id(), beast::zero, beast::zero));
 | 
			
		||||
        BEAST_REQUIRE(!!state);
 | 
			
		||||
        BEAST_EXPECT(state->getFieldU16(sfHookStateCapacity) == 4);
 | 
			
		||||
        BEAST_EXPECT(state->getFieldVL(sfHookStateData).size() == 100);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // ... more tests ...
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## Cleanup Actions
 | 
			
		||||
 | 
			
		||||
### 1. Revert Current Changes
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
git checkout origin/dev -- src/test/app/SetHook_test.cpp
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### 2. Remove Old Scale Tests
 | 
			
		||||
 | 
			
		||||
The old `testHookStateScale()` function in `src/test/rpc/AccountSet_test.cpp` should be removed since we're replacing account-wide scale with per-entry capacity.
 | 
			
		||||
 | 
			
		||||
### 3. Update Test Runner
 | 
			
		||||
 | 
			
		||||
In the main test runner, replace:
 | 
			
		||||
```cpp
 | 
			
		||||
testHookStateScale(env);
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
With:
 | 
			
		||||
```cpp
 | 
			
		||||
test_high_water_mark_capacity(env);
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## Success Criteria
 | 
			
		||||
 | 
			
		||||
All tests should pass with these characteristics:
 | 
			
		||||
 | 
			
		||||
- ✅ Tests run in isolation (fresh accounts)
 | 
			
		||||
- ✅ Tests are deterministic (no state from previous tests)
 | 
			
		||||
- ✅ All 10 test cases pass
 | 
			
		||||
- ✅ Capacity field correctly stored/retrieved
 | 
			
		||||
- ✅ OwnerCount calculations correct
 | 
			
		||||
- ✅ Reserve checks work correctly
 | 
			
		||||
- ✅ High water mark behavior verified
 | 
			
		||||
- ✅ Legacy compatibility verified
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## Next Steps
 | 
			
		||||
 | 
			
		||||
1. **Review this plan** - Make sure approach is sound
 | 
			
		||||
2. **Revert test file** - Clean slate
 | 
			
		||||
3. **Implement test function** - One test case at a time
 | 
			
		||||
4. **Verify each test** - Run and debug incrementally
 | 
			
		||||
5. **Add to test suite** - Register in runner
 | 
			
		||||
6. **Document** - Update high-water-mark-implementation.md with test results
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## Notes
 | 
			
		||||
 | 
			
		||||
### Why New Test Function?
 | 
			
		||||
 | 
			
		||||
- **Isolation:** No interference from other tests
 | 
			
		||||
- **Clarity:** Tests are focused and easy to understand
 | 
			
		||||
- **Maintainability:** Easy to add/modify tests
 | 
			
		||||
- **Debugging:** Failures are easy to track down
 | 
			
		||||
 | 
			
		||||
### Why Fresh Accounts?
 | 
			
		||||
 | 
			
		||||
- Accounts like `alice`, `bob`, `cho` are used throughout the test suite
 | 
			
		||||
- They accumulate hooks, state, and other ledger objects
 | 
			
		||||
- Trying to predict their state at any point is fragile
 | 
			
		||||
- Fresh accounts (`alice_hwm`, `bob_hwm`) are clean
 | 
			
		||||
 | 
			
		||||
### Why Guard with Feature Check?
 | 
			
		||||
 | 
			
		||||
- Tests should only run when `featureExtendedHookState` is enabled
 | 
			
		||||
- Allows tests to compile but skip when feature is disabled
 | 
			
		||||
- Clean way to handle feature-gated functionality
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## Alternative Approach: Separate File
 | 
			
		||||
 | 
			
		||||
Instead of adding to `SetHook_test.cpp`, could create `SetHook_ExtendedState_test.cpp`:
 | 
			
		||||
 | 
			
		||||
**Pros:**
 | 
			
		||||
- Complete isolation
 | 
			
		||||
- Easier to maintain
 | 
			
		||||
- Can have its own test fixture
 | 
			
		||||
- Clear separation of concerns
 | 
			
		||||
 | 
			
		||||
**Cons:**
 | 
			
		||||
- Additional build target
 | 
			
		||||
- Test discovery configuration
 | 
			
		||||
- More files to maintain
 | 
			
		||||
 | 
			
		||||
**Recommendation:** Start with adding to existing file. If tests grow large, consider splitting later.
 | 
			
		||||
		Reference in New Issue
	
	Block a user