Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 96 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ The Red-Black Tree implementation is based on [Solady's RedBlackTreeLib](https:/
- **Complex Value Support**: Stores `ValueLib.Value` structs containing any size of data. In this example, it shows 6 slots.
- **Red-Black Tree Properties**: Maintains balanced tree structure ensuring O(log n) operations
- **Zero Value Protection**: Prevents insertion of zero keys to maintain tree integrity
- **Dynamic Resizing**: Pre-allocate storage capacity to optimize gas costs for future insertions (available in `RedBlackTreeWithResizeLib`)

### Gas Performance

Expand All @@ -38,15 +39,19 @@ The Red-Black Tree implementation shows **90% gas reduction** for hot insertions

```
src/
├── RedBlackTreeKV.sol # Main KV store implementation
├── MappingKV.sol # Traditional mapping implementation for comparison
├── RedBlackTreeKV.sol # Main KV store implementation
├── RedBlackTreeWithResizeKV.sol # Enhanced KV store with resize capabilities
├── MappingKV.sol # Traditional mapping implementation for comparison
└── lib/
├── RedBlackTreeLib.sol # Red-Black Tree data structure library
└── Value.sol # Value struct definition
├── RedBlackTreeLib.sol # Red-Black Tree data structure library
├── RedBlackTreeWithResizeLib.sol # Enhanced Red-Black Tree with resize capabilities
└── Value.sol # Value struct definition

test/
├── RedBlackTreeKV.t.sol # Comprehensive unit tests
└── RedBlackTreeKVGas.t.sol # Gas benchmark tests
├── RedBlackTreeKV.t.sol # Comprehensive unit tests
├── RedBlackTreeWithResizeKV.t.sol # Tests for resize-enabled KV store
├── RedBlackTreeWithResize.sol # Red-Black Tree library tests with resize functionality
└── RedBlackTreeKVGas.t.sol # Gas benchmark tests
```

## Value Structure
Expand Down Expand Up @@ -95,6 +100,54 @@ kv.deleteValue(1);
uint256[] memory keys = kv.values();
```

### Enhanced Operations with Resize

```solidity
RedBlackTreeWithResizeKV kvWithResize = new RedBlackTreeWithResizeKV();

// Pre-allocate storage for 1000 elements to optimize gas costs
kvWithResize.resize(1000);

// Check current storage metrics
uint256 capacity = kvWithResize.getStorageSize(); // Returns 1000
uint256 size = kvWithResize.getSize(); // Returns current number of elements

// Set values with optimized gas costs
kvWithResize.setValue(1, value);
kvWithResize.setValue(2, anotherValue);

// All other operations work the same
ValueLib.Value memory retrieved = kvWithResize.getValue(1);
kvWithResize.deleteValue(1);
uint256[] memory keys = kvWithResize.values();
```

### WithResize Feature

The `RedBlackTreeWithResizeLib` provides enhanced functionality for pre-allocating storage capacity to optimize gas costs:

```solidity
import "./lib/RedBlackTreeWithResizeLib.sol";

RedBlackTreeWithResizeLib.Tree tree;

// Pre-allocate storage for 1000 elements to save gas on future insertions
RedBlackTreeWithResizeLib.resize(tree, 1000);

// Check current storage capacity
uint256 capacity = RedBlackTreeWithResizeLib.storageSize(tree);

// Insert values with optimized gas costs
RedBlackTreeWithResizeLib.insert(tree, 42);
```

#### Resize Benefits

- **Pre-allocation**: Reserve storage slots in advance to avoid costly allocations during insertion
- **Gas Optimization**: Significantly reduces gas costs for insertions when capacity is pre-allocated
- **Flexible Sizing**: Dynamically adjust storage capacity based on expected usage patterns
- **Auto-increment**: Storage capacity automatically increases when inserting beyond current capacity

### Constraints

- **No Zero Keys**: Keys cannot be 0 (will revert with `ValueIsEmpty()`)
Expand All @@ -117,6 +170,11 @@ forge test

# Run unit tests only
forge test --match-contract RedBlackTreeKVTest
forge test --match-contract RedBlackTreeWithResizeKVTest

# Run resize library tests
forge test --match-contract RedBlackTreeLibTest
forge test --match-contract RedBlackTreeWithResizeLibTest

# Run gas benchmarks
forge test --match-contract MappingGasTest -vv
Expand All @@ -126,6 +184,8 @@ forge test --match-contract MappingGasTest -vv

The test suite includes:

**RedBlackTreeKV Tests:**

- ✅ Basic CRUD operations
- ✅ Edge cases (zero keys, max values)
- ✅ Multiple value operations
Expand All @@ -134,14 +194,40 @@ The test suite includes:
- ✅ Fuzz testing with random inputs
- ✅ Gas consumption benchmarks

**RedBlackTreeWithResizeKV Tests:**

- ✅ All basic KV operations with resize functionality
- ✅ Storage capacity management and pre-allocation
- ✅ Resize operations (expand, shrink, same capacity)
- ✅ Data preservation during resize operations
- ✅ Error handling for invalid resize operations
- ✅ Resize with mixed insert/delete operations
- ✅ Fuzz testing for resize capacity variations

**RedBlackTreeWithResizeLib Tests:**

- ✅ Core Red-Black Tree operations with resize support
- ✅ Storage size tracking and capacity management
- ✅ Tree balancing with dynamic storage allocation
- ✅ Error cases for tree size limits and invalid operations

## Use Cases

This implementation is ideal for:

- **Order Books**: Frequent order insertions and cancellations
- **Gaming**: Player inventory systems with item trading
- **DeFi Protocols**: Position management with frequent updates
- **NFT Marketplaces**: Listing and delisting operations
- **Order Books**: Frequent order insertions and cancellations with predictable volume patterns
- **Gaming**: Player inventory systems with item trading and known capacity requirements
- **DeFi Protocols**: Position management with frequent updates and batch operations
- **NFT Marketplaces**: Listing and delisting operations with seasonal volume spikes
- **Data Analytics**: Time-series data storage with known dataset sizes
- **Batch Processing**: Systems that process large batches of data with predictable patterns

**When to use RedBlackTreeWithResizeKV:**

- You know the approximate maximum number of elements in advance
- You need to optimize for gas costs during high-frequency operations
- Your application has predictable usage patterns or batch processing requirements
- You want to avoid gas spikes during storage allocation

## License

Expand Down
3 changes: 2 additions & 1 deletion src/RedBlackTreeKV.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import {ValueLib} from "./lib/Value.sol";

// A simple RBT kv example. It is gas-efficient if frequently add & remove.
contract RedBlackTreeKV {
uint256 private constant _DATA_SLOT_SEED = 0xdeadbeef; // Arbitrary unique seed
// This seed is derived from the bytes("RedBlackTreeKV")
uint256 private constant _DATA_SLOT_SEED = 0xc8a834a090c3fa550a8f4fd4767f0a9c3eb1fa1ce9154cab77c4b35ad48e385c; // Arbitrary unique seed
uint256 private constant _SLOTS_PER_POSITION = ValueLib.SLOTS_PER_POSITION; // Dense slots per value

RedBlackTreeLib.Tree public tree;
Expand Down
143 changes: 143 additions & 0 deletions src/RedBlackTreeWithResizeKV.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import {console} from "forge-std/Test.sol";
import {RedBlackTreeWithResizeLib} from "./lib/RedBlackTreeWithResizeLib.sol";
import {ValueLib} from "./lib/Value.sol";

// A simple RBT kv example. It is gas-efficient if frequently add & remove.
contract RedBlackTreeWithResizeKV {
// This seed is derived from the bytes("RedBlackTreeKV")
uint256 private constant _DATA_SLOT_SEED = 0xc8a834a090c3fa550a8f4fd4767f0a9c3eb1fa1ce9154cab77c4b35ad48e385c; // Arbitrary unique seed
uint256 private constant _SLOTS_PER_POSITION = ValueLib.SLOTS_PER_POSITION; // Dense slots per value

RedBlackTreeWithResizeLib.Tree public tree;

using RedBlackTreeWithResizeLib for RedBlackTreeWithResizeLib.Tree;

function setValue(uint256 key, ValueLib.Value memory value) public {
tree.insert(key);
bytes32 ptr = tree.find(key);
(, uint256 index) = _unpack(ptr); // Extract dense node index
uint256 dataBase = _dataBase();
uint256 valueSlot = dataBase + index * _SLOTS_PER_POSITION;
/// @solidity memory-safe-assembly
assembly {
sstore(valueSlot, mload(value))
sstore(add(valueSlot, 1), mload(add(value, 0x20)))
sstore(add(valueSlot, 2), mload(add(value, 0x40)))
sstore(add(valueSlot, 3), mload(add(value, 0x60)))
sstore(add(valueSlot, 4), mload(add(value, 0x80)))
sstore(add(valueSlot, 5), mload(add(value, 0xa0)))
}
}

function getValue(uint256 key) public view returns (ValueLib.Value memory) {
bytes32 ptr = tree.find(key);
(, uint256 index) = _unpack(ptr); // Extract dense node index
uint256 dataBase = _dataBase();
uint256 valueSlot = dataBase + index * _SLOTS_PER_POSITION;
ValueLib.Value memory value;
/// @solidity memory-safe-assembly
assembly {
mstore(value, sload(valueSlot))
mstore(add(value, 0x20), sload(add(valueSlot, 1)))
mstore(add(value, 0x40), sload(add(valueSlot, 2)))
mstore(add(value, 0x60), sload(add(valueSlot, 3)))
mstore(add(value, 0x80), sload(add(valueSlot, 4)))
mstore(add(value, 0xa0), sload(add(valueSlot, 5)))
}
return value;
}

function deleteValue(uint256 key) public {
bytes32 ptr = tree.find(key);
(, uint256 deletedIndex) = _unpack(ptr); // Extract dense node index
uint256 dataBase = _dataBase();
uint256 deletedSlot = dataBase + deletedIndex * _SLOTS_PER_POSITION;

uint256 treeSize = tree.size();
uint256 lastIndex = treeSize;
bool needsCopy = (deletedIndex != lastIndex);
uint256 lastSlot = dataBase + lastIndex * _SLOTS_PER_POSITION;

tree.remove(key);

if (needsCopy) {
/// @solidity memory-safe-assembly
assembly {
/// @dev 6 = _SLOTS_PER_POSITION
for { let i := 0 } lt(i, 6) { i := add(i, 1) } { sstore(add(deletedSlot, i), sload(add(lastSlot, i))) }
}
}
}

function resize(uint256 newCapacity) public {
uint256 oldCapacity = tree.storageSize();
uint256 size = tree.size();
if (newCapacity < size) {
revert RedBlackTreeWithResizeLib.InvalidStorageSize();
}
tree.resize(newCapacity);
if (newCapacity > oldCapacity) {
uint256 dataBase = _dataBase();
for (uint256 index = oldCapacity + 1; index <= newCapacity; index++) {
uint256 valueSlot = dataBase + index * _SLOTS_PER_POSITION;
bytes32 slot;
assembly {
slot := sload(valueSlot)
sstore(valueSlot, 1)
sstore(add(valueSlot, 1), 1)
sstore(add(valueSlot, 2), 1)
sstore(add(valueSlot, 3), 1)
sstore(add(valueSlot, 4), 1)
sstore(add(valueSlot, 5), 1)
}
}
} else if (newCapacity < oldCapacity) {
uint256 dataBase = _dataBase();
for (uint256 index = newCapacity + 1; index <= oldCapacity; index++) {
uint256 valueSlot = dataBase + index * _SLOTS_PER_POSITION;
assembly {
sstore(valueSlot, 0)
sstore(add(valueSlot, 1), 0)
sstore(add(valueSlot, 2), 0)
sstore(add(valueSlot, 3), 0)
sstore(add(valueSlot, 4), 0)
sstore(add(valueSlot, 5), 0)
}
}
}
}

// Helper: Compute data base slot (keccak-based, like tree nodes but unique seed)
function _dataBase() internal pure returns (uint256 slotBase) {
/// @solidity memory-safe-assembly
assembly {
mstore(0x20, tree.slot)
mstore(0x00, _DATA_SLOT_SEED)
slotBase := keccak256(0x00, 0x40)
}
}

// Helper: Unpack pointer (copied from lib private _unpack for convenience)
function _unpack(bytes32 ptr) internal pure returns (uint256 nodes, uint256 key) {
/// @solidity memory-safe-assembly
assembly {
nodes := shl(32, shr(32, ptr)) // _NODES_SLOT_SHIFT == 32
key := and(0x7fffffff, ptr) // _BITMASK_KEY == (1 << 31) - 1
}
}

function values() public view returns (uint256[] memory) {
return tree.values();
}

function getStorageSize() public view returns (uint256) {
return tree.storageSize();
}

function getSize() public view returns (uint256) {
return tree.size();
}
}
Loading